From 85eee4f18e85af2f69e9d079c665d073004a2a42 Mon Sep 17 00:00:00 2001 From: Magmaboat Date: Thu, 9 Feb 2017 21:12:47 -0500 Subject: [PATCH 01/31] Perform hasOwnProperty validation --- src/blocks/scratch3_sound.js | 1 + src/engine/adapter.js | 1 + src/engine/blocks.js | 4 ++++ src/engine/execute.js | 3 +++ src/engine/runtime.js | 3 +++ src/sprites/rendered-target.js | 6 ++++-- 6 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 20d137580..ecce15b35 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -192,6 +192,7 @@ Scratch3SoundBlocks.prototype.changeEffect = function (args, util) { Scratch3SoundBlocks.prototype.clearEffects = function (args, util) { var soundState = this._getSoundState(util.target); for (var effect in soundState.effects) { + if (!soundState.effects.hasOwnProperty(effect)) continue; soundState.effects[effect] = 0; } if (util.target.audioPlayer === null) return; diff --git a/src/engine/adapter.js b/src/engine/adapter.js index 56275e1cb..88c8a18c5 100644 --- a/src/engine/adapter.js +++ b/src/engine/adapter.js @@ -24,6 +24,7 @@ var domToBlocks = function (blocksDOM) { // Flatten blocks object into a list. var blocksList = []; for (var b in blocks) { + if (!blocks.hasOwnProperty(b)) continue; blocksList.push(blocks[b]); } return blocksList; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index ed03be324..d9172228c 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -149,6 +149,7 @@ Blocks.prototype.getTopLevelScript = function (id) { */ Blocks.prototype.getProcedureDefinition = function (name) { for (var id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; var block = this._blocks[id]; if ((block.opcode === 'procedures_defnoreturn' || block.opcode === 'procedures_defreturn') && @@ -166,6 +167,7 @@ Blocks.prototype.getProcedureDefinition = function (name) { */ Blocks.prototype.getProcedureParamNames = function (name) { for (var id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; var block = this._blocks[id]; if ((block.opcode === 'procedures_defnoreturn' || block.opcode === 'procedures_defreturn') && @@ -415,6 +417,7 @@ Blocks.prototype.blockToXML = function (blockId) { } // Add any inputs on this block. for (var input in block.inputs) { + if (!block.inputs.hasOwnProperty(input)) continue; var blockInput = block.inputs[input]; // Only encode a value tag if the value input is occupied. if (blockInput.block || blockInput.shadow) { @@ -431,6 +434,7 @@ Blocks.prototype.blockToXML = function (blockId) { } // Add any fields on this block. for (var field in block.fields) { + if (!block.fields.hasOwnProperty(field)) continue; var blockField = block.fields[field]; var value = blockField.value; if (typeof value === 'string') { diff --git a/src/engine/execute.js b/src/engine/execute.js index 226a866a7..598cd5783 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -110,6 +110,7 @@ var execute = function (sequencer, thread) { Object.keys(inputs).length === 0) { // One field and no inputs - treat as arg. for (var fieldKey in fields) { // One iteration. + if (!fields.hasOwnProperty(fieldKey)) continue; handleReport(fields[fieldKey].value); } } else { @@ -126,11 +127,13 @@ var execute = function (sequencer, thread) { // Add all fields on this block to the argValues. for (var fieldName in fields) { + if (!fields.hasOwnProperty(fieldName)) continue; argValues[fieldName] = fields[fieldName].value; } // Recursively evaluate input blocks. for (var inputName in inputs) { + if (!inputs.hasOwnProperty(inputName)) continue; var input = inputs[inputName]; var inputBlockId = input.block; // Is there no value for this input waiting in the stack frame? diff --git a/src/engine/runtime.js b/src/engine/runtime.js index fc3b17a06..5efb44fdd 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -441,6 +441,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, var newThreads = []; for (var opts in optMatchFields) { + if (!optMatchFields.hasOwnProperty(opts)) continue; optMatchFields[opts] = optMatchFields[opts].toUpperCase(); } @@ -463,6 +464,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, if (Object.keys(hatFields).length === 0) { var hatInputs = target.blocks.getInputs(topBlockId); for (var input in hatInputs) { + if (!hatInputs.hasOwnProperty(input)) continue; var id = hatInputs[input].block; var fields = target.blocks.getFields(id); hatFields = Object.assign(fields, hatFields); @@ -592,6 +594,7 @@ Runtime.prototype.stopAll = function () { Runtime.prototype._step = function () { // Find all edge-activated hats, and add them to threads to be evaluated. for (var hatType in this._hats) { + if (!this._hats.hasOwnProperty(hatType)) continue; var hat = this._hats[hatType]; if (hat.edgeActivated) { this.startHats(hatType); diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9bbc20b23..3f6ddf0da 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -315,6 +315,7 @@ RenderedTarget.prototype.setEffect = function (effectName, value) { */ RenderedTarget.prototype.clearEffects = function () { for (var effectName in this.effects) { + if (!this.effects.hasOwnProperty(effectName)) continue; this.effects[effectName] = 0; } if (this.renderer) { @@ -434,8 +435,9 @@ RenderedTarget.prototype.updateAllDrawableProperties = function () { costume.rotationCenterY / costume.bitmapResolution ] }; - for (var effectID in this.effects) { - props[effectID] = this.effects[effectID]; + for (var effectName in this.effects) { + if (!this.effects.hasOwnProperty(effectName)) continue; + props[effectName] = this.effects[effectName]; } this.renderer.updateDrawableProperties(this.drawableID, props); if (this.visible) { From a69950add4d0e2f19bad940b0875b7ec2fcf08f2 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 14 Feb 2017 18:57:35 +0000 Subject: [PATCH 02/31] chore(package): update expose-loader to version 0.7.3 Closes #454 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c04928eec..37bd5086d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "copy-webpack-plugin": "4.0.1", "eslint": "3.15.0", "eslint-config-scratch": "^3.1.0", - "expose-loader": "0.7.1", + "expose-loader": "0.7.3", "gh-pages": "0.12.0", "highlightjs": "9.8.0", "htmlparser2": "3.9.2", From 7bc141e76e1f56714c2076a9205e9165fbe259a8 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Wed, 15 Feb 2017 15:59:54 +0000 Subject: [PATCH 03/31] Feature Fencing This is a re-comit after the previous pull was reverted. --- src/sprites/rendered-target.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9bbc20b23..a60c35084 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -167,15 +167,20 @@ RenderedTarget.prototype.setXY = function (x, y) { } var oldX = this.x; var oldY = this.y; - this.x = x; - this.y = y; if (this.renderer) { + var position = this.renderer.getFencedPositionOfDrawable(this.drawableID, [x, y]); + this.x = position[0]; + this.y = position[1]; + this.renderer.updateDrawableProperties(this.drawableID, { - position: [this.x, this.y] + position: position }); if (this.visible) { this.runtime.requestRedraw(); } + } else { + this.x = x; + this.y = y; } this.emit(RenderedTarget.EVENT_TARGET_MOVED, this, oldX, oldY); this.runtime.spriteInfoReport(this); From d7bff34a6602782698f0b9e83a7631ebfc31d22c Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 19 Feb 2017 01:19:16 +0000 Subject: [PATCH 04/31] chore(package): update tap to version 10.2.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 459a971a0..062243db6 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "scratch-render": "latest", "script-loader": "0.7.0", "stats.js": "0.17.0", - "tap": "10.1.2", + "tap": "10.2.0", "travis-after-all": "1.4.4", "webpack": "2.2.1", "webpack-dev-server": "2.3.0" From 0118a19cd52710b5c4a081e64c49295d67fa27d6 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 19 Feb 2017 22:15:28 +0000 Subject: [PATCH 05/31] chore(package): update webpack-dev-server to version 2.4.1 Closes #470 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 459a971a0..5f14710c9 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,6 @@ "tap": "10.1.2", "travis-after-all": "1.4.4", "webpack": "2.2.1", - "webpack-dev-server": "2.3.0" + "webpack-dev-server": "2.4.1" } } From 362a2312796e4fda1a3201241c5de671a2c86e5a Mon Sep 17 00:00:00 2001 From: griffpatch Date: Mon, 20 Feb 2017 08:20:21 +0000 Subject: [PATCH 06/31] bug - escape from conditional branches Execution bug: can't escape from conditional branches that end with a Promise-resolution-terminating command block (see #464) --- src/engine/execute.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 226a866a7..5d2734dde 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -233,8 +233,15 @@ var execute = function (sequencer, thread) { primitiveReportedValue.then(function (resolvedValue) { handleReport(resolvedValue); if (typeof resolvedValue === 'undefined') { - var popped = thread.popStack(); - var nextBlockId = thread.target.blocks.getNextBlock(popped); + do { + // In the case that the promise is the last block in the current thread stack + // We need to pop out repeatedly until we find the next block. + var popped = thread.popStack(); + if (popped === null) { + return; + } + var nextBlockId = thread.target.blocks.getNextBlock(popped); + } while (nextBlockId === null); thread.pushStack(nextBlockId); } else { thread.popStack(); From c5eb8ece1628cf0d142b27d76238a8551f6f40de Mon Sep 17 00:00:00 2001 From: griffpatch Date: Mon, 20 Feb 2017 09:40:32 +0000 Subject: [PATCH 07/31] Handle actual looping cases And it starts to get a little less elegant :/ Wondering if this should not be handled better in another part of the codebase? We don't want to be duplicating existing code stepping functionality locally at the end of the promise script really... What do you think? --- src/engine/execute.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 5d2734dde..7ff0203ec 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -241,7 +241,15 @@ var execute = function (sequencer, thread) { return; } var nextBlockId = thread.target.blocks.getNextBlock(popped); - } while (nextBlockId === null); + if (nextBlockId !== null) { + // A next block exists so break out this loop + break; + } + // Investigate the next block and if not in a loop, + // then repeat and pop the next item off the stack frame + var stackFrame = thread.peekStackFrame(); + } while (stackFrame !== null && !stackFrame.isLoop); + thread.pushStack(nextBlockId); } else { thread.popStack(); From 867d3c79f50144c082d1cad0fc71e6db4a1cf6bd Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 20 Feb 2017 16:37:01 +0000 Subject: [PATCH 08/31] chore(package): update eslint to version 3.16.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 459a971a0..fd47e7fe3 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "adm-zip": "0.4.7", "babel-eslint": "7.1.1", "copy-webpack-plugin": "4.0.1", - "eslint": "3.15.0", + "eslint": "3.16.0", "eslint-config-scratch": "^3.1.0", "expose-loader": "0.7.1", "gh-pages": "0.12.0", From ed4b0080b6e379c3e19d9230cb683b9a9e774cf6 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 20 Feb 2017 17:13:40 -0500 Subject: [PATCH 09/31] Use caret ranges for dev dependencies These packages don't affect the output of the built package, so don't require a specific version. This should quiet Greenkeeper down a bit about our dependencies. --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index f1f497ad7..3ba71fb0d 100644 --- a/package.json +++ b/package.json @@ -25,15 +25,15 @@ }, "devDependencies": { "adm-zip": "0.4.7", - "babel-eslint": "7.1.1", + "babel-eslint": "^7.1.1", "copy-webpack-plugin": "4.0.1", - "eslint": "3.16.0", + "eslint": "^3.16.0", "eslint-config-scratch": "^3.1.0", "expose-loader": "0.7.3", - "gh-pages": "0.12.0", - "highlightjs": "9.8.0", + "gh-pages": "^0.12.0", + "highlightjs": "^9.8.0", "htmlparser2": "3.9.2", - "json": "9.0.4", + "json": "^9.0.4", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", "promise": "7.1.1", @@ -41,10 +41,10 @@ "scratch-blocks": "latest", "scratch-render": "latest", "script-loader": "0.7.0", - "stats.js": "0.17.0", - "tap": "10.2.0", - "travis-after-all": "1.4.4", + "stats.js": "^0.17.0", + "tap": "^10.2.0", + "travis-after-all": "^1.4.4", "webpack": "2.2.1", - "webpack-dev-server": "2.4.1" + "webpack-dev-server": "^2.4.1" } } From 99e1b242852102b04b1f4a5ee79402d0cc3e0304 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Tue, 14 Feb 2017 17:06:16 -0500 Subject: [PATCH 10/31] Add rudimentary toolbox filtering method --- src/virtual-machine.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 003c80e9f..52616e7c2 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -336,4 +336,23 @@ VirtualMachine.prototype.postSpriteInfo = function (data) { this.editingTarget.postSpriteInfo(data); }; +VirtualMachine.prototype.filterToolbox = function (toolboxDOM) { + var filteredToolbox = toolboxDOM.cloneNode(); + var category = toolboxDOM.firstElementChild; + while (category) { + var filteredCategory = category.cloneNode(); + var block = category.firstElementChild; + while (block) { + var opcode = block.getAttribute('type'); + if (opcode in this.runtime._primitives || opcode in this.runtime._hats) { + filteredCategory.appendChild(block.cloneNode(true)); + } + block = block.nextElementSibling; + } + if (filteredCategory.hasChildNodes()) filteredToolbox.appendChild(filteredCategory); + category = category.nextElementSibling; + } + return filteredToolbox; +}; + module.exports = VirtualMachine; From a9b495fced0d883ea62795d584b1cb5e11f9cfad Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 15 Feb 2017 15:24:48 -0500 Subject: [PATCH 11/31] Filter the playground toolbox --- src/playground/playground.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/playground/playground.js b/src/playground/playground.js index 9ee924122..f32e17ef9 100644 --- a/src/playground/playground.js +++ b/src/playground/playground.js @@ -59,6 +59,11 @@ window.onload = function () { }); window.workspace = workspace; + // Filter available blocks + var toolbox = vm.filterToolbox(workspace.options.languageTree); + // var toolbox = workspace.options.languageTree; + workspace.updateToolbox(toolbox); + // Attach scratch-blocks events to VM. workspace.addChangeListener(vm.blockListener); var flyoutWorkspace = workspace.getFlyout().getWorkspace(); From 390b2373e9efae553d2337ad1d356e7ca626b7fb Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 15 Feb 2017 19:16:37 -0500 Subject: [PATCH 12/31] Refactor filter methods into util --- src/util/filter-toolbox.js | 49 ++++++++++++++++++++++++++++++++++++++ src/virtual-machine.js | 29 ++++++++++------------ 2 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 src/util/filter-toolbox.js diff --git a/src/util/filter-toolbox.js b/src/util/filter-toolbox.js new file mode 100644 index 000000000..c4b18b41c --- /dev/null +++ b/src/util/filter-toolbox.js @@ -0,0 +1,49 @@ +/** + * Filter Blockly toolbox XML node containing blocks to only those with + * valid opcodes. Return a copy of the node with valid blocks. + * @param {HTMLElement} node Blockly toolbox XML node + * @param {Array.} opcodes Valid opcodes. Blocks producing other opcodes + * will be filtered. + * @returns {HTMLElement} filtered toolbox XML node + */ +var filterToolboxNode = function (node, opcodes) { + var filteredCategory = node.cloneNode(); + for (var block = node.firstElementChild; block; block = block.nextElementSibling) { + if (block.nodeName.toLowerCase() !== 'block') continue; + var opcode = block.getAttribute('type').toLowerCase(); + if (opcodes.indexOf(opcode) !== -1) { + filteredCategory.appendChild(block.cloneNode(true)); + } + } + return filteredCategory; +}; + +/** + * Filter Blockly toolbox XML and return a copy which only contains blocks with + * existent opcodes. Categories with no valid children will be removed. + * @param {HTMLElement} toolbox Blockly toolbox XML node + * @param {Array.} opcodes Valid opcodes. Blocks producing other opcodes + * will be filtered. + * @returns {HTMLElement} filtered toolbox XML node + */ +var filterToolbox = function (toolbox, opcodes) { + if (!toolbox.hasChildNodes()) return toolbox; + var filteredToolbox; + if (toolbox.firstElementChild.nodeName.toLowerCase() === 'category') { + filteredToolbox = toolbox.cloneNode(); + for ( + var category = toolbox.firstElementChild; + category; + category = category.nextElementSibling + ) { + if (category.nodeName.toLowerCase() !== 'category') continue; + var filteredCategory = filterToolboxNode(category, opcodes); + if (filteredCategory.hasChildNodes()) filteredToolbox.appendChild(filteredCategory); + } + } else { + filteredToolbox = filterToolboxNode(toolbox, opcodes); + } + return filteredToolbox; +}; + +module.exports = filterToolbox; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 52616e7c2..1f3bdfa1f 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1,6 +1,7 @@ var EventEmitter = require('events'); var util = require('util'); +var filterToolbox = require('./util/filter-toolbox'); var Runtime = require('./engine/runtime'); var sb2import = require('./import/sb2import'); @@ -336,23 +337,17 @@ VirtualMachine.prototype.postSpriteInfo = function (data) { this.editingTarget.postSpriteInfo(data); }; -VirtualMachine.prototype.filterToolbox = function (toolboxDOM) { - var filteredToolbox = toolboxDOM.cloneNode(); - var category = toolboxDOM.firstElementChild; - while (category) { - var filteredCategory = category.cloneNode(); - var block = category.firstElementChild; - while (block) { - var opcode = block.getAttribute('type'); - if (opcode in this.runtime._primitives || opcode in this.runtime._hats) { - filteredCategory.appendChild(block.cloneNode(true)); - } - block = block.nextElementSibling; - } - if (filteredCategory.hasChildNodes()) filteredToolbox.appendChild(filteredCategory); - category = category.nextElementSibling; - } - return filteredToolbox; + +/** + * Filter Blockly toolbox XML and return a copy which only contains blocks with + * existent opcodes. Categories with no valid children will be removed. + * @param {HTMLElement} toolbox Blockly toolbox XML node + * @returns {HTMLElement} filtered toolbox XML node + */ +VirtualMachine.prototype.filterToolbox = function (toolbox) { + var opcodes = Object.keys(this.runtime._primitives) + .concat(Object.keys(this.runtime._hats)); + return filterToolbox(toolbox, opcodes); }; module.exports = VirtualMachine; From 6ea9b54539178229594bff4c7a93b2a420d97b93 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 27 Feb 2017 12:57:03 -0500 Subject: [PATCH 13/31] Add filterToolbox unit tests --- package.json | 1 + test/fixtures/.eslintrc.js | 5 + test/fixtures/toolboxes.js | 845 +++++++++++++++++++++++++++++++ test/unit/util_filter-toolbox.js | 22 + 4 files changed, 873 insertions(+) create mode 100644 test/fixtures/.eslintrc.js create mode 100644 test/fixtures/toolboxes.js create mode 100644 test/unit/util_filter-toolbox.js diff --git a/package.json b/package.json index 3ba71fb0d..4418452f5 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "gh-pages": "^0.12.0", "highlightjs": "^9.8.0", "htmlparser2": "3.9.2", + "jsdom": "^9.11.0", "json": "^9.0.4", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", diff --git a/test/fixtures/.eslintrc.js b/test/fixtures/.eslintrc.js new file mode 100644 index 000000000..8df47e667 --- /dev/null +++ b/test/fixtures/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'max-len': [0] + } +}; diff --git a/test/fixtures/toolboxes.js b/test/fixtures/toolboxes.js new file mode 100644 index 000000000..e8efe6f96 --- /dev/null +++ b/test/fixtures/toolboxes.js @@ -0,0 +1,845 @@ +var jsdom = require('jsdom').jsdom; +var categories = ''; +var simple = ''; +var empty = ''; +module.exports = { + categories: jsdom(categories).body.firstElementChild, + simple: jsdom(simple).body.firstElementChild, + empty: jsdom(empty).body.firstElementChild +}; diff --git a/test/unit/util_filter-toolbox.js b/test/unit/util_filter-toolbox.js new file mode 100644 index 000000000..4334b819d --- /dev/null +++ b/test/unit/util_filter-toolbox.js @@ -0,0 +1,22 @@ +var toolboxes = require('../fixtures/toolboxes'); +var test = require('tap').test; +var filterToolbox = require('../../src/util/filter-toolbox'); + +test('categories', function (t) { + var filteredToolbox = filterToolbox(toolboxes.categories, ['operator_random']); + t.strictEqual(filteredToolbox.children.length, 1); + t.strictEqual(filteredToolbox.firstElementChild.children.length, 1); + t.end(); +}); + +test('simple', function (t) { + var filteredToolbox = filterToolbox(toolboxes.simple, ['operator_random']); + t.strictEqual(filteredToolbox.children.length, 1); + t.end(); +}); + +test('empty', function (t) { + var filteredToolbox = filterToolbox(toolboxes.empty, ['operator_random']); + t.strictEqual(filteredToolbox.children.length, 0); + t.end(); +}); From 739c5deb63ab749982c5648a056f0ba7b42c7537 Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Wed, 1 Mar 2017 09:25:11 -0500 Subject: [PATCH 14/31] Show Categories that use custom code to load (variables, procedures) (#483) --- src/util/filter-toolbox.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/util/filter-toolbox.js b/src/util/filter-toolbox.js index c4b18b41c..54d59ced8 100644 --- a/src/util/filter-toolbox.js +++ b/src/util/filter-toolbox.js @@ -38,7 +38,11 @@ var filterToolbox = function (toolbox, opcodes) { ) { if (category.nodeName.toLowerCase() !== 'category') continue; var filteredCategory = filterToolboxNode(category, opcodes); - if (filteredCategory.hasChildNodes()) filteredToolbox.appendChild(filteredCategory); + if (filteredCategory.hasChildNodes() || + filteredCategory.hasAttribute('custom') + ) { + filteredToolbox.appendChild(filteredCategory); + } } } else { filteredToolbox = filterToolboxNode(toolbox, opcodes); From 0b2d05317760ec4ab98a85c3d61aa90f898c2d19 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 1 Mar 2017 12:57:46 -0500 Subject: [PATCH 15/31] Update tests to account for custom categories --- test/fixtures/toolboxes.js | 223 +++++++++++++++---------------- test/unit/util_filter-toolbox.js | 8 +- 2 files changed, 113 insertions(+), 118 deletions(-) diff --git a/test/fixtures/toolboxes.js b/test/fixtures/toolboxes.js index e8efe6f96..5f74c89df 100644 --- a/test/fixtures/toolboxes.js +++ b/test/fixtures/toolboxes.js @@ -724,120 +724,115 @@ var categories = ''; -var simple = ''; -var empty = ''; +var simple = ''; +var empty = ''; module.exports = { categories: jsdom(categories).body.firstElementChild, simple: jsdom(simple).body.firstElementChild, diff --git a/test/unit/util_filter-toolbox.js b/test/unit/util_filter-toolbox.js index 4334b819d..9c3ea073c 100644 --- a/test/unit/util_filter-toolbox.js +++ b/test/unit/util_filter-toolbox.js @@ -3,20 +3,20 @@ var test = require('tap').test; var filterToolbox = require('../../src/util/filter-toolbox'); test('categories', function (t) { - var filteredToolbox = filterToolbox(toolboxes.categories, ['operator_random']); - t.strictEqual(filteredToolbox.children.length, 1); + var filteredToolbox = filterToolbox(toolboxes.categories, ['motion_movesteps']); + t.strictEqual(filteredToolbox.children.length, 3); t.strictEqual(filteredToolbox.firstElementChild.children.length, 1); t.end(); }); test('simple', function (t) { - var filteredToolbox = filterToolbox(toolboxes.simple, ['operator_random']); + var filteredToolbox = filterToolbox(toolboxes.simple, ['motion_movesteps']); t.strictEqual(filteredToolbox.children.length, 1); t.end(); }); test('empty', function (t) { - var filteredToolbox = filterToolbox(toolboxes.empty, ['operator_random']); + var filteredToolbox = filterToolbox(toolboxes.empty, ['motion_movesteps']); t.strictEqual(filteredToolbox.children.length, 0); t.end(); }); From 8dadc42857ed390281eaa83779e6fff15abc771c Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 1 Mar 2017 18:49:17 -0500 Subject: [PATCH 16/31] Add draggable property to sprites This property should be managed by the VM so that it can be displayed in the sprite info panel in the GUI. --- src/import/sb2import.js | 3 +++ src/sprites/rendered-target.js | 22 ++++++++++++++++++++++ test/integration/complex.js | 1 + 3 files changed, 26 insertions(+) diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 20669466e..39a1b1f15 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -104,6 +104,9 @@ var parseScratchObject = function (object, runtime, topLevel) { if (object.hasOwnProperty('direction')) { target.direction = object.direction; } + if (object.hasOwnProperty('isDraggable')) { + target.draggable = object.isDraggable; + } if (object.hasOwnProperty('scale')) { // SB2 stores as 1.0 = 100%; we use % in the VM. target.size = object.scale * 100; diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9c64b9aa0..049fbfd02 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -106,6 +106,12 @@ RenderedTarget.prototype.y = 0; */ RenderedTarget.prototype.direction = 90; +/** + * Whether the rendered target is draggable on the stage + * @type {boolean} + */ +RenderedTarget.prototype.draggable = false; + /** * Whether the rendered target is currently visible. * @type {boolean} @@ -229,6 +235,16 @@ RenderedTarget.prototype.setDirection = function (direction) { this.runtime.spriteInfoReport(this); }; +/** + * Set draggability; i.e., whether it's able to be dragged in the player + * @param {!boolean} draggable True if should be draggable. + */ +RenderedTarget.prototype.setDraggable = function (draggable) { + if (this.isStage) return; + this.draggable = !!draggable; + this.runtime.spriteInfoReport(this); +}; + /** * Set a say bubble. * @param {?string} type Type of say bubble: "say", "think", or null. @@ -431,6 +447,7 @@ RenderedTarget.prototype.updateAllDrawableProperties = function () { var props = { position: [this.x, this.y], direction: renderedDirectionScale.direction, + draggable: this.draggable, scale: renderedDirectionScale.scale, visible: this.visible, skin: costume.skin, @@ -656,6 +673,7 @@ RenderedTarget.prototype.makeClone = function () { newClone.x = this.x; newClone.y = this.y; newClone.direction = this.direction; + newClone.draggable = this.draggable; newClone.visible = this.visible; newClone.size = this.size; newClone.currentCostume = this.currentCostume; @@ -703,6 +721,9 @@ RenderedTarget.prototype.postSpriteInfo = function (data) { if (data.hasOwnProperty('direction')) { this.setDirection(data.direction); } + if (data.hasOwnProperty('draggable')) { + this.setDraggable(data.draggable); + } if (data.hasOwnProperty('rotationStyle')) { this.setRotationStyle(data.rotationStyle); } @@ -723,6 +744,7 @@ RenderedTarget.prototype.toJSON = function () { x: this.x, y: this.y, direction: this.direction, + draggable: this.draggable, costume: this.getCurrentCostume(), costumeCount: this.getCostumes().length, visible: this.visible, diff --git a/test/integration/complex.js b/test/integration/complex.js index 1ff1d384f..dea8d40bc 100644 --- a/test/integration/complex.js +++ b/test/integration/complex.js @@ -34,6 +34,7 @@ test('complex', function (t) { x: 0, y: 10, direction: 90, + draggable: true, rotationStyle: 'all around', visible: true }); From a4578ba0862f674686df2d534426f5c80ba421c4 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Thu, 2 Mar 2017 11:21:01 -0500 Subject: [PATCH 17/31] Default bitmapResolution to 1 if not provided in costume metadata (#485) * Default bitmapResolution to 1 if not provided in costume metadata. Resolves GH-484 --- src/sprites/rendered-target.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9c64b9aa0..d934f72eb 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -428,16 +428,17 @@ RenderedTarget.prototype.updateAllDrawableProperties = function () { if (this.renderer) { var renderedDirectionScale = this._getRenderedDirectionAndScale(); var costume = this.sprite.costumes[this.currentCostume]; + var bitmapResolution = costume.bitmapResolution || 1; var props = { position: [this.x, this.y], direction: renderedDirectionScale.direction, scale: renderedDirectionScale.scale, visible: this.visible, skin: costume.skin, - costumeResolution: costume.bitmapResolution, + costumeResolution: bitmapResolution, rotationCenter: [ - costume.rotationCenterX / costume.bitmapResolution, - costume.rotationCenterY / costume.bitmapResolution + costume.rotationCenterX / bitmapResolution, + costume.rotationCenterY / bitmapResolution ] }; for (var effectName in this.effects) { From 11e28111c54d1b92eab8ec46b114f462f52a07b2 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 20 Feb 2017 16:22:13 -0500 Subject: [PATCH 18/31] Activate click hats on mouse up to match 2.0 --- src/io/mouse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/mouse.js b/src/io/mouse.js index bcd19087e..4a7361fa4 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -46,7 +46,7 @@ Mouse.prototype.postData = function (data) { } if (typeof data.isDown !== 'undefined') { this._isDown = data.isDown; - if (this._isDown) { + if (!this._isDown) { this._activateClickHats(data.x, data.y); } } From d0cd9cd8972ab93bc5d0241f2cd3bffaed7b8c59 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Fri, 3 Mar 2017 09:35:57 -0500 Subject: [PATCH 19/31] Add methods for dragging and dropping sprites * Add `getTargetIdForDrawableId` to translate between renderer picks and VM targets * Add `startDrag` to stop sprite motion while dragging * Add `stopDrag` to return sprite motion after dragging --- src/engine/runtime.js | 12 ++++++++++++ src/sprites/rendered-target.js | 34 +++++++++++++++++++++++++++------ src/virtual-machine.js | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index a060d63a7..130c4ae95 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -793,6 +793,18 @@ Runtime.prototype.getSpriteTargetByName = function (spriteName) { } }; +/** + * Get a target by its drawable id. + * @param {number} drawableID drawable id of target to find + * @return {?Target} The target, if found + */ +Runtime.prototype.getTargetByDrawableId = function (drawableID) { + for (var i = 0; i < this.targets.length; i++) { + var target = this.targets[i]; + if (target.drawableID === drawableID) return target; + } +}; + /** * Update the clone counter to track how many clones are created. * @param {number} changeAmount How many clones have been created/destroyed. diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index d934f72eb..1e60cad48 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -33,6 +33,13 @@ var RenderedTarget = function (sprite, runtime) { */ this.drawableID = null; + /** + * Drag state of this rendered target. If true, x/y position can't be + * changed by blocks. + * @type {boolean} + */ + this.dragging = false; + /** * Map of current graphic effect values. * @type {!Object.} @@ -160,11 +167,11 @@ RenderedTarget.prototype.rotationStyle = ( * Set the X and Y coordinates. * @param {!number} x New X coordinate, in Scratch coordinates. * @param {!number} y New Y coordinate, in Scratch coordinates. + * @param {?boolean} force Force setting X/Y, in case of dragging */ -RenderedTarget.prototype.setXY = function (x, y) { - if (this.isStage) { - return; - } +RenderedTarget.prototype.setXY = function (x, y, force) { + if (this.isStage) return; + if (this.dragging && !force) return; var oldX = this.x; var oldY = this.y; if (this.renderer) { @@ -695,11 +702,12 @@ RenderedTarget.prototype.onStopAll = function () { * @param {object} data An object with sprite info data to set. */ RenderedTarget.prototype.postSpriteInfo = function (data) { + var force = data.hasOwnProperty('force') ? data.force : null; if (data.hasOwnProperty('x')) { - this.setXY(data.x, this.y); + this.setXY(data.x, this.y, force); } if (data.hasOwnProperty('y')) { - this.setXY(this.x, data.y); + this.setXY(this.x, data.y, force); } if (data.hasOwnProperty('direction')) { this.setDirection(data.direction); @@ -712,6 +720,20 @@ RenderedTarget.prototype.postSpriteInfo = function (data) { } }; +/** + * Put the sprite into the drag state. While in effect, setXY must be forced + */ +RenderedTarget.prototype.startDrag = function () { + this.dragging = true; +}; + +/** + * Remove the sprite from the drag state. + */ +RenderedTarget.prototype.stopDrag = function () { + this.dragging = false; +}; + /** * Serialize sprite info, used when emitting events about the sprite * @returns {object} sprite data as a simple object diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 1f3bdfa1f..9d8477c45 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -329,6 +329,41 @@ VirtualMachine.prototype.emitWorkspaceUpdate = function () { }); }; +/** + * Get a target id for a drawable id. Useful for interacting with the renderer + * @param {int} drawableId The drawable id to request the target id for + * @returns {?string} The target id, if found. Will also be null if the target found is the stage. + */ +VirtualMachine.prototype.getTargetIdForDrawableId = function (drawableId) { + var target = this.runtime.getTargetByDrawableId(drawableId); + if (target.hasOwnProperty('id') && target.hasOwnProperty('isStage') && !target.isStage) { + return target.id; + } + return null; +}; + +/** + * Put a target into a "drag" state, during which its X/Y positions will be unaffected + * by blocks. + * @param {string} targetId The id for the target to put into a drag state + */ +VirtualMachine.prototype.startDrag = function (targetId) { + var target = this.runtime.getTargetById(targetId); + if (target) { + target.startDrag(); + this.setEditingTarget(target.id); + } +}; + +/** + * Remove a target from a drag state, so blocks may begin affecting X/Y position again + * @param {string} targetId The id for the target to remove from the drag state + */ +VirtualMachine.prototype.stopDrag = function (targetId) { + var target = this.runtime.getTargetById(targetId); + if (target) target.stopDrag(); +}; + /** * Post/edit sprite info for the current editing target. * @param {object} data An object with sprite info data to set. From 5b820966709014b1b91fd844e4c96a16f31c4b50 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 6 Mar 2017 15:54:52 -0500 Subject: [PATCH 20/31] Clear gfx on stop all --- src/sprites/rendered-target.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index adf945739..e062df53f 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -706,9 +706,10 @@ RenderedTarget.prototype.onGreenFlag = function () { /** * Called when the project receives a "stop all" - * Stop all sounds + * Stop all sounds and clear graphic effects. */ RenderedTarget.prototype.onStopAll = function () { + this.clearEffects(); if (this.audioPlayer) { this.audioPlayer.stopAllSounds(); this.audioPlayer.clearEffects(); From fe3799bb4816094dfe5d5facede2ff00b8adaf05 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 6 Mar 2017 16:15:04 -0500 Subject: [PATCH 21/31] Add test for gfx reset on stop --- test/unit/sprites_rendered-target.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/unit/sprites_rendered-target.js b/test/unit/sprites_rendered-target.js index 58c61bdf7..3ff40e2cd 100644 --- a/test/unit/sprites_rendered-target.js +++ b/test/unit/sprites_rendered-target.js @@ -11,3 +11,13 @@ test('clone effects', function (t) { t.ok(a.effects !== b.effects); t.end(); }); + +test('#stopAll clears graphics effects', function (t) { + var spr = new Sprite(); + var a = new RenderedTarget(spr, null); + var effectName = 'brightness'; + a.setEffect(effectName, 100); + a.onStopAll(); + t.equals(a.effects[effectName], 0); + t.end(); +}); From 8aaadf2fab0ae1a87e77b8658ef4b380c50c31a1 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 8 Mar 2017 11:40:02 -0500 Subject: [PATCH 22/31] Add check to make sure target exists --- src/virtual-machine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 9d8477c45..b7b56af2c 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -336,7 +336,7 @@ VirtualMachine.prototype.emitWorkspaceUpdate = function () { */ VirtualMachine.prototype.getTargetIdForDrawableId = function (drawableId) { var target = this.runtime.getTargetByDrawableId(drawableId); - if (target.hasOwnProperty('id') && target.hasOwnProperty('isStage') && !target.isStage) { + if (target && target.hasOwnProperty('id') && target.hasOwnProperty('isStage') && !target.isStage) { return target.id; } return null; From b03768cad6696691313a149ea6d790fdff5b6f0f Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Thu, 9 Mar 2017 16:41:06 -0500 Subject: [PATCH 23/31] Remove filter toolbox utility (#496) * Revert "Merge pull request #486 from rschamp/fix-filter-tests" This reverts commit ba00db897fe328f34e58ce39975c218059a68a94, reversing changes made to 739c5deb63ab749982c5648a056f0ba7b42c7537. * Revert "Show Categories that use custom code to load (variables, procedures) (#483)" This reverts commit 739c5deb63ab749982c5648a056f0ba7b42c7537. * Revert "Merge pull request #461 from rschamp/filter-toolbox" This reverts commit 343b5bfe8ecc6e497688dc33dd489ab6f8d230f6, reversing changes made to 370f2c6a47733dc3f54fac5a950be291e7042690. --- package.json | 1 - src/playground/playground.js | 5 - src/util/filter-toolbox.js | 53 -- src/virtual-machine.js | 14 - test/fixtures/.eslintrc.js | 5 - test/fixtures/toolboxes.js | 840 ------------------------------- test/unit/util_filter-toolbox.js | 22 - 7 files changed, 940 deletions(-) delete mode 100644 src/util/filter-toolbox.js delete mode 100644 test/fixtures/.eslintrc.js delete mode 100644 test/fixtures/toolboxes.js delete mode 100644 test/unit/util_filter-toolbox.js diff --git a/package.json b/package.json index 4418452f5..3ba71fb0d 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "gh-pages": "^0.12.0", "highlightjs": "^9.8.0", "htmlparser2": "3.9.2", - "jsdom": "^9.11.0", "json": "^9.0.4", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", diff --git a/src/playground/playground.js b/src/playground/playground.js index f32e17ef9..9ee924122 100644 --- a/src/playground/playground.js +++ b/src/playground/playground.js @@ -59,11 +59,6 @@ window.onload = function () { }); window.workspace = workspace; - // Filter available blocks - var toolbox = vm.filterToolbox(workspace.options.languageTree); - // var toolbox = workspace.options.languageTree; - workspace.updateToolbox(toolbox); - // Attach scratch-blocks events to VM. workspace.addChangeListener(vm.blockListener); var flyoutWorkspace = workspace.getFlyout().getWorkspace(); diff --git a/src/util/filter-toolbox.js b/src/util/filter-toolbox.js deleted file mode 100644 index 54d59ced8..000000000 --- a/src/util/filter-toolbox.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Filter Blockly toolbox XML node containing blocks to only those with - * valid opcodes. Return a copy of the node with valid blocks. - * @param {HTMLElement} node Blockly toolbox XML node - * @param {Array.} opcodes Valid opcodes. Blocks producing other opcodes - * will be filtered. - * @returns {HTMLElement} filtered toolbox XML node - */ -var filterToolboxNode = function (node, opcodes) { - var filteredCategory = node.cloneNode(); - for (var block = node.firstElementChild; block; block = block.nextElementSibling) { - if (block.nodeName.toLowerCase() !== 'block') continue; - var opcode = block.getAttribute('type').toLowerCase(); - if (opcodes.indexOf(opcode) !== -1) { - filteredCategory.appendChild(block.cloneNode(true)); - } - } - return filteredCategory; -}; - -/** - * Filter Blockly toolbox XML and return a copy which only contains blocks with - * existent opcodes. Categories with no valid children will be removed. - * @param {HTMLElement} toolbox Blockly toolbox XML node - * @param {Array.} opcodes Valid opcodes. Blocks producing other opcodes - * will be filtered. - * @returns {HTMLElement} filtered toolbox XML node - */ -var filterToolbox = function (toolbox, opcodes) { - if (!toolbox.hasChildNodes()) return toolbox; - var filteredToolbox; - if (toolbox.firstElementChild.nodeName.toLowerCase() === 'category') { - filteredToolbox = toolbox.cloneNode(); - for ( - var category = toolbox.firstElementChild; - category; - category = category.nextElementSibling - ) { - if (category.nodeName.toLowerCase() !== 'category') continue; - var filteredCategory = filterToolboxNode(category, opcodes); - if (filteredCategory.hasChildNodes() || - filteredCategory.hasAttribute('custom') - ) { - filteredToolbox.appendChild(filteredCategory); - } - } - } else { - filteredToolbox = filterToolboxNode(toolbox, opcodes); - } - return filteredToolbox; -}; - -module.exports = filterToolbox; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index b7b56af2c..1431640fb 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1,7 +1,6 @@ var EventEmitter = require('events'); var util = require('util'); -var filterToolbox = require('./util/filter-toolbox'); var Runtime = require('./engine/runtime'); var sb2import = require('./import/sb2import'); @@ -372,17 +371,4 @@ VirtualMachine.prototype.postSpriteInfo = function (data) { this.editingTarget.postSpriteInfo(data); }; - -/** - * Filter Blockly toolbox XML and return a copy which only contains blocks with - * existent opcodes. Categories with no valid children will be removed. - * @param {HTMLElement} toolbox Blockly toolbox XML node - * @returns {HTMLElement} filtered toolbox XML node - */ -VirtualMachine.prototype.filterToolbox = function (toolbox) { - var opcodes = Object.keys(this.runtime._primitives) - .concat(Object.keys(this.runtime._hats)); - return filterToolbox(toolbox, opcodes); -}; - module.exports = VirtualMachine; diff --git a/test/fixtures/.eslintrc.js b/test/fixtures/.eslintrc.js deleted file mode 100644 index 8df47e667..000000000 --- a/test/fixtures/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - rules: { - 'max-len': [0] - } -}; diff --git a/test/fixtures/toolboxes.js b/test/fixtures/toolboxes.js deleted file mode 100644 index 5f74c89df..000000000 --- a/test/fixtures/toolboxes.js +++ /dev/null @@ -1,840 +0,0 @@ -var jsdom = require('jsdom').jsdom; -var categories = ''; -var simple = ''; -var empty = ''; -module.exports = { - categories: jsdom(categories).body.firstElementChild, - simple: jsdom(simple).body.firstElementChild, - empty: jsdom(empty).body.firstElementChild -}; diff --git a/test/unit/util_filter-toolbox.js b/test/unit/util_filter-toolbox.js deleted file mode 100644 index 9c3ea073c..000000000 --- a/test/unit/util_filter-toolbox.js +++ /dev/null @@ -1,22 +0,0 @@ -var toolboxes = require('../fixtures/toolboxes'); -var test = require('tap').test; -var filterToolbox = require('../../src/util/filter-toolbox'); - -test('categories', function (t) { - var filteredToolbox = filterToolbox(toolboxes.categories, ['motion_movesteps']); - t.strictEqual(filteredToolbox.children.length, 3); - t.strictEqual(filteredToolbox.firstElementChild.children.length, 1); - t.end(); -}); - -test('simple', function (t) { - var filteredToolbox = filterToolbox(toolboxes.simple, ['motion_movesteps']); - t.strictEqual(filteredToolbox.children.length, 1); - t.end(); -}); - -test('empty', function (t) { - var filteredToolbox = filterToolbox(toolboxes.empty, ['motion_movesteps']); - t.strictEqual(filteredToolbox.children.length, 0); - t.end(); -}); From 1ba7ad0218474547aded1668f06b9a29559a0364 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Fri, 17 Mar 2017 17:23:53 -0400 Subject: [PATCH 24/31] =?UTF-8?q?Clamp=20range=20for=20=E2=80=9Cplay=20not?= =?UTF-8?q?e=E2=80=9D=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clamp to 36-96, which is C2-C7. This is a temporary fix to prevent errors, until we have a new instrument player implementation, which may have a different range. --- src/blocks/scratch3_sound.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 76c03f6fd..0a5476310 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -33,6 +33,12 @@ Scratch3SoundBlocks.DEFAULT_SOUND_STATE = { } }; +/** + * The minimum and maximum MIDI note numbers, for clamping the input to play note. + * @type {{min: number, max: number}} + */ +Scratch3SoundBlocks.MIDI_NOTE_RANGE = {min: 36, max: 96}; // C2 to C7 + /** * @param {Target} target - collect sound state for this target. * @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary. @@ -132,6 +138,7 @@ Scratch3SoundBlocks.prototype.stopAllSounds = function (args, util) { Scratch3SoundBlocks.prototype.playNoteForBeats = function (args, util) { var note = Cast.toNumber(args.NOTE); + note = MathUtil.clamp(note, Scratch3SoundBlocks.MIDI_NOTE_RANGE.min, Scratch3SoundBlocks.MIDI_NOTE_RANGE.max); var beats = Cast.toNumber(args.BEATS); var soundState = this._getSoundState(util.target); var inst = soundState.currentInstrument; From f2ac36585931e0f9cc6621bed946af3aef1237f5 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Fri, 17 Mar 2017 17:29:23 -0400 Subject: [PATCH 25/31] Clamp beat durations for play note, play drum and rest --- src/blocks/scratch3_sound.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 0a5476310..6bbc9e6a8 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -39,6 +39,13 @@ Scratch3SoundBlocks.DEFAULT_SOUND_STATE = { */ Scratch3SoundBlocks.MIDI_NOTE_RANGE = {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}} + */ +Scratch3SoundBlocks.BEAT_RANGE = {min: 0, max: 100}; + /** * @param {Target} target - collect sound state for this target. * @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary. @@ -140,6 +147,7 @@ Scratch3SoundBlocks.prototype.playNoteForBeats = function (args, util) { var note = Cast.toNumber(args.NOTE); note = MathUtil.clamp(note, Scratch3SoundBlocks.MIDI_NOTE_RANGE.min, Scratch3SoundBlocks.MIDI_NOTE_RANGE.max); var beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); var soundState = this._getSoundState(util.target); var inst = soundState.currentInstrument; var vol = soundState.volume; @@ -153,16 +161,22 @@ Scratch3SoundBlocks.prototype.playDrumForBeats = function (args, util) { if (typeof this.runtime.audioEngine === 'undefined') return; drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums); var beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); if (util.target.audioPlayer === null) return; return util.target.audioPlayer.playDrumForBeats(drum, beats); }; Scratch3SoundBlocks.prototype.restForBeats = function (args) { var beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); if (typeof this.runtime.audioEngine === 'undefined') return; return this.runtime.audioEngine.waitForBeats(beats); }; +Scratch3SoundBlocks.prototype._clampBeats = function (beats) { + return MathUtil.clamp(beats, Scratch3SoundBlocks.BEAT_RANGE.min, Scratch3SoundBlocks.BEAT_RANGE.max); +}; + Scratch3SoundBlocks.prototype.setInstrument = function (args, util) { var soundState = this._getSoundState(util.target); var instNum = Cast.toNumber(args.INSTRUMENT); From c9338f37ede057d935f11c7021c22c5011682d3f Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Fri, 17 Mar 2017 18:13:33 -0400 Subject: [PATCH 26/31] Clamp tempo range The function in AudioEngine for change tempo can now be removed --- src/blocks/scratch3_sound.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 6bbc9e6a8..696ece4a8 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -46,6 +46,11 @@ Scratch3SoundBlocks.MIDI_NOTE_RANGE = {min: 36, max: 96}; // C2 to C7 */ Scratch3SoundBlocks.BEAT_RANGE = {min: 0, max: 100}; + /** The minimum and maximum tempo values, in bpm. + * @type {{min: number, max: number}} + */ +Scratch3SoundBlocks.TEMPO_RANGE = {min: 20, max: 500}; + /** * @param {Target} target - collect sound state for this target. * @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary. @@ -246,15 +251,21 @@ Scratch3SoundBlocks.prototype.getVolume = function (args, util) { }; Scratch3SoundBlocks.prototype.setTempo = function (args) { - var value = Cast.toNumber(args.TEMPO); - if (typeof this.runtime.audioEngine === 'undefined') return; - this.runtime.audioEngine.setTempo(value); + var tempo = Cast.toNumber(args.TEMPO); + this._updateTempo(tempo); }; Scratch3SoundBlocks.prototype.changeTempo = function (args) { - var value = Cast.toNumber(args.TEMPO); + var change = Cast.toNumber(args.TEMPO); if (typeof this.runtime.audioEngine === 'undefined') return; - this.runtime.audioEngine.changeTempo(value); + var tempo = change + this.runtime.audioEngine.currentTempo; + this._updateTempo(tempo); +}; + +Scratch3SoundBlocks.prototype._updateTempo = function (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); }; Scratch3SoundBlocks.prototype.getTempo = function () { From 627fc1d9c346cb95c71d6cc8aea379779f1c380f Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 20 Mar 2017 11:13:24 -0400 Subject: [PATCH 27/31] Add tests for existing vm.renameSprite functionality --- test/unit/virtual-machine.js | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 test/unit/virtual-machine.js diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js new file mode 100644 index 000000000..75c857965 --- /dev/null +++ b/test/unit/virtual-machine.js @@ -0,0 +1,51 @@ +var test = require('tap').test; +var VirtualMachine = require('../../src/virtual-machine.js'); + +test('renameSprite throws when there is no sprite with that id', function (t) { + var vm = new VirtualMachine(); + vm.runtime.getTargetById = () => null; + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('No target with the provided id.') + ); + t.end(); +}); + +test('renameSprite throws when used on a non-sprite target', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + isSprite: () => false + }; + vm.runtime.getTargetById = () => (fakeTarget); + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('Cannot rename non-sprite targets.') + ); + t.end(); +}); + +test('renameSprite throws when there is no sprite for given target', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + sprite: null, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('No sprite associated with this target.') + ); + t.end(); +}); + +test('renameSprite sets the sprite name', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', 'not-original'); + t.equal(fakeTarget.sprite.name, 'not-original'); + t.end(); +}); From 5d2352e47187b182bb11588e977c1800564f8cc2 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 20 Mar 2017 11:14:25 -0400 Subject: [PATCH 28/31] Add test for skipping rename on blank input --- src/virtual-machine.js | 4 +++- test/unit/virtual-machine.js | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 1431640fb..aae77937d 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -204,7 +204,9 @@ VirtualMachine.prototype.renameSprite = function (targetId, newName) { if (!sprite) { throw new Error('No sprite associated with this target.'); } - sprite.name = newName; + if (newName) { + sprite.name = newName; + } this.emitTargetsUpdate(); } else { throw new Error('No target with the provided id.'); diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js index 75c857965..28bcdd764 100644 --- a/test/unit/virtual-machine.js +++ b/test/unit/virtual-machine.js @@ -49,3 +49,15 @@ test('renameSprite sets the sprite name', function (t) { t.equal(fakeTarget.sprite.name, 'not-original'); t.end(); }); + +test('renameSprite does not set sprite names to an empty string ', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', ''); + t.equal(fakeTarget.sprite.name, 'original'); + t.end(); +}); From 052ecef91c356552c98023c4f666a8753b5e0fed Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 20 Mar 2017 12:12:38 -0400 Subject: [PATCH 29/31] Add string utils with tests --- src/util/string-util.js | 17 ++++++++++++ test/unit/util_string.js | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/util/string-util.js create mode 100644 test/unit/util_string.js diff --git a/src/util/string-util.js b/src/util/string-util.js new file mode 100644 index 000000000..6270cd885 --- /dev/null +++ b/src/util/string-util.js @@ -0,0 +1,17 @@ +var StringUtil = function () {}; + +StringUtil.withoutTrailingDigits = function (s) { + var i = s.length - 1; + while ((i >= 0) && ('0123456789'.indexOf(s.charAt(i)) > -1)) i--; + return s.slice(0, i + 1); +}; + +StringUtil.unusedName = function (name, existingNames) { + if (existingNames.indexOf(name) < 0) return name; + name = StringUtil.withoutTrailingDigits(name); + var i = 2; + while (existingNames.indexOf(name + i) >= 0) i++; + return name + i; +}; + +module.exports = StringUtil; diff --git a/test/unit/util_string.js b/test/unit/util_string.js new file mode 100644 index 000000000..fdcd439d3 --- /dev/null +++ b/test/unit/util_string.js @@ -0,0 +1,57 @@ +var test = require('tap').test; +var StringUtil = require('../../src/util/string-util'); + +test('withoutTrailingDigits', function (t) { + t.strictEqual(StringUtil.withoutTrailingDigits('boeing747'), 'boeing'); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing747 '), 'boeing747 '); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing𝟨'), 'boeing𝟨'); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing 747'), 'boeing '); + t.strictEqual(StringUtil.withoutTrailingDigits('747'), ''); + t.end(); +}); + +test('unusedName', function (t) { + t.strictEqual( + StringUtil.unusedName( + 'name', + ['not the same name'] + ), + 'name' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name'] + ), + 'name2' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name30'] + ), + 'name' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name', 'name2'] + ), + 'name3' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name', 'name3'] + ), + 'name2' + ); + t.strictEqual( + StringUtil.unusedName( + 'boeing747', + ['boeing747'] + ), + 'boeing2' // Yup, this matches scratch-flash... + ); + t.end(); +}); From 8bbb395b35d2532b0b20e40d94fa4798f5b10fba Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 20 Mar 2017 12:52:57 -0400 Subject: [PATCH 30/31] Add sprite name incrementing and reserved naming --- src/virtual-machine.js | 13 +++++++++++-- test/integration/complex.js | 2 +- test/unit/virtual-machine.js | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index aae77937d..576e3a78a 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -3,6 +3,9 @@ var util = require('util'); var Runtime = require('./engine/runtime'); var sb2import = require('./import/sb2import'); +var StringUtil = require('./util/string-util'); + +var RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; /** * Handles connections between blocks, stage, and extensions. @@ -204,8 +207,14 @@ VirtualMachine.prototype.renameSprite = function (targetId, newName) { if (!sprite) { throw new Error('No sprite associated with this target.'); } - if (newName) { - sprite.name = newName; + if (newName && RESERVED_NAMES.indexOf(newName) === -1) { + var names = this.runtime.targets.filter(function (runtimeTarget) { + return runtimeTarget.isSprite(); + }).map(function (runtimeTarget) { + return runtimeTarget.sprite.name; + }); + + sprite.name = StringUtil.unusedName(newName, names); } this.emitTargetsUpdate(); } else { diff --git a/test/integration/complex.js b/test/integration/complex.js index dea8d40bc..f8dbd2cc2 100644 --- a/test/integration/complex.js +++ b/test/integration/complex.js @@ -26,7 +26,7 @@ test('complex', function (t) { var targets = data.targetList; for (var i in targets) { if (targets[i].isStage === true) continue; - if (targets[i].name === 'test') continue; + if (targets[i].name.match(/test/)) continue; vm.setEditingTarget(targets[i].id); vm.renameSprite(targets[i].id, 'test'); diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js index 28bcdd764..eed537dd3 100644 --- a/test/unit/virtual-machine.js +++ b/test/unit/virtual-machine.js @@ -50,7 +50,7 @@ test('renameSprite sets the sprite name', function (t) { t.end(); }); -test('renameSprite does not set sprite names to an empty string ', function (t) { +test('renameSprite does not set sprite names to an empty string', function (t) { var vm = new VirtualMachine(); var fakeTarget = { sprite: {name: 'original'}, @@ -61,3 +61,36 @@ test('renameSprite does not set sprite names to an empty string ', function (t) t.equal(fakeTarget.sprite.name, 'original'); t.end(); }); + +test('renameSprite does not set sprite names to reserved names', function (t) { + var vm = new VirtualMachine(); + var fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', '_mouse_'); + t.equal(fakeTarget.sprite.name, 'original'); + t.end(); +}); + +test('renameSprite increments from existing sprite names', function (t) { + var vm = new VirtualMachine(); + vm.emitTargetsUpdate = () => {}; + vm.runtime.targets = [{ + id: 'id1', + isSprite: () => true, + sprite: { + name: 'this name' + } + }, { + id: 'id2', + isSprite: () => true, + sprite: { + name: 'that name' + } + }]; + vm.renameSprite('id1', 'that name'); + t.equal(vm.runtime.targets[0].sprite.name, 'that name2'); + t.end(); +}); From 106db6a0244e788b6821a2e36bf38d8b09d5bae2 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 20 Mar 2017 14:53:04 -0400 Subject: [PATCH 31/31] Clamp range for each audio effect --- src/blocks/scratch3_sound.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 696ece4a8..c60e41fb0 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -51,6 +51,18 @@ Scratch3SoundBlocks.BEAT_RANGE = {min: 0, max: 100}; */ Scratch3SoundBlocks.TEMPO_RANGE = {min: 20, max: 500}; + /** The minimum and maximum values for each sound effect. + * @type {{effect:{min: number, max: number}}} + */ +Scratch3SoundBlocks.EFFECT_RANGE = { + pitch: {min: -600, max: 600}, // -5 to 5 octaves + pan: {min: -100, max: 100}, // 100% left to 100% right + echo: {min: 0, max: 100}, // 0 to max (75%) feedback + reverb: {min: 0, max: 100}, // wet/dry: 0 to 100% wet + fuzz: {min: 0, max: 100}, // wed/dry: 0 to 100% wet + robot: {min: 0, max: 600} // 0 to 5 octaves +}; + /** * @param {Target} target - collect sound state for this target. * @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary. @@ -193,25 +205,29 @@ Scratch3SoundBlocks.prototype.setInstrument = function (args, util) { }; Scratch3SoundBlocks.prototype.setEffect = function (args, util) { - var effect = Cast.toString(args.EFFECT).toLowerCase(); - var value = Cast.toNumber(args.VALUE); - - var soundState = this._getSoundState(util.target); - if (!soundState.effects.hasOwnProperty(effect)) return; - - soundState.effects[effect] = value; - if (util.target.audioPlayer === null) return; - util.target.audioPlayer.setEffect(effect, soundState.effects[effect]); + this._updateEffect(args, util, false); }; Scratch3SoundBlocks.prototype.changeEffect = function (args, util) { + this._updateEffect(args, util, true); +}; + +Scratch3SoundBlocks.prototype._updateEffect = function (args, util, change) { var effect = Cast.toString(args.EFFECT).toLowerCase(); var value = Cast.toNumber(args.VALUE); var soundState = this._getSoundState(util.target); if (!soundState.effects.hasOwnProperty(effect)) return; - soundState.effects[effect] += value; + if (change) { + soundState.effects[effect] += value; + } else { + soundState.effects[effect] = value; + } + + var effectRange = Scratch3SoundBlocks.EFFECT_RANGE[effect]; + soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], effectRange.min, effectRange.max); + if (util.target.audioPlayer === null) return; util.target.audioPlayer.setEffect(effect, soundState.effects[effect]); };