From 953c892aab021d6e36b0d6e27c8e92eaf11c2f02 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Mon, 3 Oct 2016 21:43:31 -0400 Subject: [PATCH 01/31] Add unit test coverage for operator blocks. Resolves GH-103 --- test/unit/blocks_operators.js | 175 ++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 test/unit/blocks_operators.js diff --git a/test/unit/blocks_operators.js b/test/unit/blocks_operators.js new file mode 100644 index 000000000..e2477f03f --- /dev/null +++ b/test/unit/blocks_operators.js @@ -0,0 +1,175 @@ +var test = require('tap').test; +var Operators = require('../../src/blocks/scratch3_operators'); + +var blocks = new Operators(null); + +test('getPrimitives', function (t) { + t.type(blocks.getPrimitives(), 'object'); + t.end(); +}); + +test('add', function (t) { + t.strictEqual(blocks.add({NUM1:'1', NUM2:'1'}), 2); + t.strictEqual(blocks.add({NUM1:'foo', NUM2:'bar'}), 0); + t.end(); +}); + +test('subtract', function (t) { + t.strictEqual(blocks.subtract({NUM1:'1', NUM2:'1'}), 0); + t.strictEqual(blocks.subtract({NUM1:'foo', NUM2:'bar'}), 0); + t.end(); +}); + +test('multiply', function (t) { + t.strictEqual(blocks.multiply({NUM1:'2', NUM2:'2'}), 4); + t.strictEqual(blocks.multiply({NUM1:'foo', NUM2:'bar'}), 0); + t.end(); +}); + +test('divide', function (t) { + t.strictEqual(blocks.divide({NUM1:'2', NUM2:'2'}), 1); + t.strictEqual(blocks.divide({NUM1:'1', NUM2:'0'}), Infinity); // @todo + t.ok(isNaN(blocks.divide({NUM1:'foo', NUM2:'bar'}))); // @todo + t.end(); +}); + +test('lt', function (t) { + t.strictEqual(blocks.lt({OPERAND1:'1', OPERAND2:'2'}), true); + t.strictEqual(blocks.lt({OPERAND1:'2', OPERAND2:'1'}), false); + t.strictEqual(blocks.lt({OPERAND1:'1', OPERAND2:'1'}), false); + t.end(); +}); + +test('equals', function (t) { + t.strictEqual(blocks.equals({OPERAND1:'1', OPERAND2:'2'}), false); + t.strictEqual(blocks.equals({OPERAND1:'2', OPERAND2:'1'}), false); + t.strictEqual(blocks.equals({OPERAND1:'1', OPERAND2:'1'}), true); + t.end(); +}); + +test('gt', function (t) { + t.strictEqual(blocks.gt({OPERAND1:'1', OPERAND2:'2'}), false); + t.strictEqual(blocks.gt({OPERAND1:'2', OPERAND2:'1'}), true); + t.strictEqual(blocks.gt({OPERAND1:'1', OPERAND2:'1'}), false); + t.end(); +}); + +test('and', function (t) { + t.strictEqual(blocks.and({OPERAND1:true, OPERAND2:true}), true); + t.strictEqual(blocks.and({OPERAND1:true, OPERAND2:false}), false); + t.strictEqual(blocks.and({OPERAND1:false, OPERAND2:false}), false); + t.end(); +}); + +test('or', function (t) { + t.strictEqual(blocks.or({OPERAND1:true, OPERAND2:true}), true); + t.strictEqual(blocks.or({OPERAND1:true, OPERAND2:false}), true); + t.strictEqual(blocks.or({OPERAND1:false, OPERAND2:false}), false); + t.end(); +}); + +test('not', function (t) { + t.strictEqual(blocks.not({OPERAND:true}), false); + t.strictEqual(blocks.not({OPERAND:false}), true); + t.end(); +}); + +test('random', function (t) { + var min = 0; + var max = 100; + var result = blocks.random({FROM:0, TO:1}); + t.ok(result >= min); + t.ok(result <= max); + t.end(); +}); + +test('random - equal', function (t) { + var min = 1; + var max = 1; + t.strictEqual(blocks.random({FROM:min, TO:max}), min); + t.end(); +}); + +test('random - decimal', function (t) { + var min = 0.1; + var max = 10; + var result = blocks.random({FROM:min, TO:max}); + t.ok(result >= min); + t.ok(result <= max); + t.end(); +}); + +test('random - int', function (t) { + var min = 0; + var max = 10; + var result = blocks.random({FROM:min, TO:max}); + t.ok(result >= min); + t.ok(result <= max); + t.end(); +}); + +test('random - reverse', function (t) { + var min = 0; + var max = 10; + var result = blocks.random({FROM:max, TO:min}); + t.ok(result >= min); + t.ok(result <= max); + t.end(); +}); + +test('join', function (t) { + t.strictEqual(blocks.join({STRING1:'foo', STRING2:'bar'}), 'foobar'); + t.strictEqual(blocks.join({STRING1:'1', STRING2:'2'}), '12'); + t.end(); +}); + +test('letterOf', function (t) { + t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:0}), ''); + t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:1}), 'f'); + t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:2}), 'o'); + t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:3}), 'o'); + t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:4}), ''); + t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:'bar'}), ''); + t.end(); +}); + +test('length', function (t) { + t.strictEqual(blocks.length({STRING:''}), 0); + t.strictEqual(blocks.length({STRING:'foo'}), 3); + t.strictEqual(blocks.length({STRING:'1'}), 1); + t.strictEqual(blocks.length({STRING:'100'}), 3); + t.end(); +}); + +test('mod', function (t) { + t.strictEqual(blocks.mod({NUM1:1, NUM2:1}), 0); + t.strictEqual(blocks.mod({NUM1:3, NUM2:6}), 3); + t.strictEqual(blocks.mod({NUM1:-3, NUM2:6}), 3); + t.end(); +}); + +test('round', function (t) { + t.strictEqual(blocks.round({NUM:1}), 1); + t.strictEqual(blocks.round({NUM:1.1}), 1); + t.strictEqual(blocks.round({NUM:1.5}), 2); + t.end(); +}); + +test('mathop', function (t) { + t.strictEqual(blocks.mathop({OPERATOR:'abs', NUM:-1}), 1); + t.strictEqual(blocks.mathop({OPERATOR:'floor', NUM:1.5}), 1); + t.strictEqual(blocks.mathop({OPERATOR:'ceiling', NUM:0.1}), 1); + t.strictEqual(blocks.mathop({OPERATOR:'sqrt', NUM:1}), 1); + t.strictEqual(blocks.mathop({OPERATOR:'sin', NUM:1}), 0.01745240643728351); + t.strictEqual(blocks.mathop({OPERATOR:'cos', NUM:1}), 0.9998476951563913); + t.strictEqual(blocks.mathop({OPERATOR:'tan', NUM:1}), 0.017455064928217585); + t.strictEqual(blocks.mathop({OPERATOR:'asin', NUM:1}), 90); + t.strictEqual(blocks.mathop({OPERATOR:'acos', NUM:1}), 0); + t.strictEqual(blocks.mathop({OPERATOR:'atan', NUM:1}), 45); + t.strictEqual(blocks.mathop({OPERATOR:'ln', NUM:1}), 0); + t.strictEqual(blocks.mathop({OPERATOR:'log', NUM:1}), 0); + t.strictEqual(blocks.mathop({OPERATOR:'e ^', NUM:1}), 2.718281828459045); + t.strictEqual(blocks.mathop({OPERATOR:'10 ^', NUM:1}), 10); + t.strictEqual(blocks.mathop({OPERATOR:'undefined', NUM:1}), 0); + t.end(); +}); From edb109a296cb13e81667ba527156f1f5ad52c90c Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Mon, 3 Oct 2016 22:05:45 -0400 Subject: [PATCH 02/31] Add code coverage badge to README. Re GH-103 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b71d51604..b701ca103 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ #### Scratch VM is a library for representing, running, and maintaining the state of computer programs written using [Scratch Blocks](https://github.com/LLK/scratch-blocks). [![Build Status](https://travis-ci.org/LLK/scratch-vm.svg?branch=develop)](https://travis-ci.org/LLK/scratch-vm) +[![Coverage Status](https://coveralls.io/repos/github/LLK/scratch-vm/badge.svg?branch=develop)](https://coveralls.io/github/LLK/scratch-vm?branch=develop) [![Dependency Status](https://david-dm.org/LLK/scratch-vm.svg)](https://david-dm.org/LLK/scratch-vm) [![devDependency Status](https://david-dm.org/LLK/scratch-vm/dev-status.svg)](https://david-dm.org/LLK/scratch-vm#info=devDependencies) From 157b6152f52fe67f3d1ca8f0cdfa1fd754b6197c Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Tue, 4 Oct 2016 14:37:39 -0400 Subject: [PATCH 03/31] Address feedback from PR review. GH-103 --- test/unit/blocks_operators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/blocks_operators.js b/test/unit/blocks_operators.js index e2477f03f..f7befa063 100644 --- a/test/unit/blocks_operators.js +++ b/test/unit/blocks_operators.js @@ -77,7 +77,7 @@ test('not', function (t) { test('random', function (t) { var min = 0; var max = 100; - var result = blocks.random({FROM:0, TO:1}); + var result = blocks.random({FROM:min, TO:max}); t.ok(result >= min); t.ok(result <= max); t.end(); From 886bcbe3c1be9069e2ca5f395928c3cbc4a42eb5 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 4 Oct 2016 16:20:53 -0300 Subject: [PATCH 04/31] Implement the point-towards block (#235) * Implement the point-towards block * Pointing towards a nonexistent sprite should do nothing, not throw an error --- src/blocks/scratch3_motion.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 8b8e7356e..18c7bc7e9 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -21,6 +21,7 @@ Scratch3MotionBlocks.prototype.getPrimitives = function() { 'motion_turnright': this.turnRight, 'motion_turnleft': this.turnLeft, 'motion_pointindirection': this.pointInDirection, + 'motion_pointtowards': this.pointTowards, 'motion_glidesecstoxy': this.glide, 'motion_setrotationstyle': this.setRotationStyle, 'motion_changexby': this.changeX, @@ -62,6 +63,25 @@ Scratch3MotionBlocks.prototype.pointInDirection = function (args, util) { util.target.setDirection(direction); }; +Scratch3MotionBlocks.prototype.pointTowards = function (args, util) { + var targetX = 0; + var targetY = 0; + if (args.TOWARDS === '_mouse_') { + targetX = util.ioQuery('mouse', 'getX'); + targetY = util.ioQuery('mouse', 'getY'); + } else { + var pointTarget = this.runtime.getSpriteTargetByName(args.TOWARDS); + if (!pointTarget) return; + targetX = pointTarget.x; + targetY = pointTarget.y; + } + + var dx = targetX - util.target.x; + var dy = targetY - util.target.y; + var direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx)); + util.target.setDirection(direction); +}; + Scratch3MotionBlocks.prototype.glide = function (args, util) { if (!util.stackFrame.timer) { // First time: save data for future use. From 6cd9697bed93782f94c5a3b3023561b39283fac3 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Tue, 4 Oct 2016 15:21:55 -0400 Subject: [PATCH 05/31] Add secure environment var for coveralls. Re GH-103 --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 091fe53fe..70433651b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,15 @@ language: node_js node_js: -- "4" -- "stable" +- '4' +- stable sudo: false cache: directories: - node_modules +env: + global: + secure: pgTtizuHLxuSOtDZ/DBoxcBeigRPiBTAuWo5Kn5wVtnXhqpWfzcT6DGe9vVwbaFLvb+zdY3pYmopAZnZTUCmGRGgPgNdE7BFwwCpMSbHHjtlHkcwCOwRUaNcW6iB9WjfwOM/I6UTy2N0OiUQdZcysW2ybdIxzYdYHElQE47kJHIecOoxJXRw/c0nv7MUkmVYnMt7gOqH2sK9kWyUDgDbtG4sCLb5LhN5/WFzUopACpN6VvAYaCIU8BTAAnb0XxIAD5mmBrOScbANTodnN4ubFjtNfMBd+j7h02bFBHbbJ73Z5gnRRonDos5kkRuFI08IV/0QauMwMSv2TKCVvsbaf53zWbfPkWr7zbj6gtisscfK0zEW9lTz7lvhyUeiPonzAqeV39gd+oPowWM5ZuyIZoawPbIvTJtGb4RlkEPI8OQST6O6g71hMksw0VDWDiLDneLCcHYFw7Q25qAkBg9P4ahH6McP4BB4gTJfQFKCDKvGYFsGrxM2IvYkXcZ0CdcatnNZGLsvHFk24sIQleLZPiCXOZKfp6zbNLwoJWhkoLz8aZ5hJoiRpoztyyrAdh6IL9MwZIgQ9KI10GfKpHmX5jbnh55KRc3G8a65fksHP5TFt2JjKtiN6A3oJAGzltdatW2TwlhyweEnjfpmtQcN8I4OKfgnsOcohcS+RaBNPCQ= before_install: -# Install the most up to date scratch-* dependencies - rm -rf node_modules/scratch-* after_script: - | From 5871672551e70b539d8da6772d2c54b33c4d61dd Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 4 Oct 2016 19:19:52 -0300 Subject: [PATCH 06/31] Implement "go to" block (#238) * Implement "go to" block * Add a missing semicolon My text editor doesn't automatically insert them and I'm not used to using semicolons so much. :( * Implement go-to-random * Clean up the go-to-random code a bit * Add rounding to _random_ picks --- src/blocks/scratch3_motion.js | 21 +++++++++++++++++++++ src/engine/runtime.js | 12 ++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 18c7bc7e9..3e19e6ef1 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -18,6 +18,7 @@ Scratch3MotionBlocks.prototype.getPrimitives = function() { return { 'motion_movesteps': this.moveSteps, 'motion_gotoxy': this.goToXY, + 'motion_goto': this.goTo, 'motion_turnright': this.turnRight, 'motion_turnleft': this.turnLeft, 'motion_pointindirection': this.pointInDirection, @@ -48,6 +49,26 @@ Scratch3MotionBlocks.prototype.goToXY = function (args, util) { util.target.setXY(x, y); }; +Scratch3MotionBlocks.prototype.goTo = function (args, util) { + var targetX = 0; + var targetY = 0; + if (args.TO === '_mouse_') { + targetX = util.ioQuery('mouse', 'getX'); + targetY = util.ioQuery('mouse', 'getY'); + } else if (args.TO === '_random_') { + var stageWidth = this.runtime.constructor.STAGE_WIDTH; + var stageHeight = this.runtime.constructor.STAGE_HEIGHT; + targetX = Math.round(stageWidth * (Math.random() - 0.5)); + targetY = Math.round(stageHeight * (Math.random() - 0.5)); + } else { + var goToTarget = this.runtime.getSpriteTargetByName(args.TO); + if (!goToTarget) return; + targetX = goToTarget.x; + targetY = goToTarget.y; + } + util.target.setXY(targetX, targetY); +}; + Scratch3MotionBlocks.prototype.turnRight = function (args, util) { var degrees = Cast.toNumber(args.DEGREES); util.target.setDirection(util.target.direction + degrees); diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 18cffd8ec..10c025199 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -69,6 +69,18 @@ function Runtime () { this._cloneCounter = 0; } +/** + * Width of the stage, in pixels. + * @const {number} + */ +Runtime.STAGE_WIDTH = 480; + +/** + * Height of the stage, in pixels. + * @const {number} + */ +Runtime.STAGE_HEIGHT = 360; + /** * Event name for glowing a script. * @const {string} From cb4a75928e9036a94a99c59f7c27d0425c2dd218 Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Tue, 4 Oct 2016 20:25:06 -0400 Subject: [PATCH 07/31] Add NPM Script 'build' --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e042b6de6..0b9743912 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "main": "./dist.js", "scripts": { "prepublish": "./node_modules/.bin/webpack --bail", - "start": "make serve", + "start": "webpack-dev-server", + "start": "webpack --colors --progress", "test": "make test", "version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"" }, From 9b07889b3f042ac1880f0a7ae44a154bf8706f63 Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Tue, 4 Oct 2016 20:38:11 -0400 Subject: [PATCH 08/31] Fix Stage PNG (#241) * Fix Stage PNG * Add New Stage PNG * Remove Old Stage PNG * Delete backdrop1.png * Rename New Stage PNG * Delete stage.png * Fix Stage Resolution --- assets/stage.png | Bin 1346 -> 3552 bytes src/index.js | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/stage.png b/assets/stage.png index c60861c2bd315d6eaffdb9dfb632abb49e1102d4..b13e9d1f5f77b8f623b272d4520eecc34c0aa797 100644 GIT binary patch literal 3552 zcmeAS@N?(olHy`uVBq!ia0y~yU_QXWz;uCw2`F+wwC^#HVk{1FcVbv~PUa<$!;&U>cv7h@-A}f&3S>O>_%)r2R7=#&*=dVZsnpEoP;uum9_x740BZGke^TJ=|bEAIT zc4W2{pNWlFLR1*ehf3boFyt=akR{0Pf7lyZ`_I literal 1346 zcmeAS@N?(olHy`uVBq!ia0y~yV0-|?85~SN5r)wHmdKI;Vst00K&1Q~&?~ diff --git a/src/index.js b/src/index.js index 321a0f889..fd987f4de 100644 --- a/src/index.js +++ b/src/index.js @@ -137,9 +137,9 @@ VirtualMachine.prototype.createEmptyProject = function () { stage.costumes.push({ skin: '/assets/stage.png', name: 'backdrop1', - bitmapResolution: 1, - rotationCenterX: 240, - rotationCenterY: 180 + bitmapResolution: 2, + rotationCenterX: 480, + rotationCenterY: 360 }); var target2 = stage.createClone(); this.runtime.targets.push(target2); From 30535d8e6ea4f2cbb6c7c1fe51db433d0fca6b3d Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 4 Oct 2016 22:26:59 -0300 Subject: [PATCH 09/31] Implement "distance to" block (#239) * Implement "distance-to" block * distance-to in stage should always be 10000 --- src/blocks/scratch3_sensing.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 5e392ef86..15466e3c0 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -16,6 +16,7 @@ Scratch3SensingBlocks.prototype.getPrimitives = function() { return { 'sensing_touchingcolor': this.touchingColor, 'sensing_coloristouchingcolor': this.colorTouchingColor, + 'sensing_distanceto': this.distanceTo, 'sensing_timer': this.getTimer, 'sensing_resettimer': this.resetTimer, 'sensing_mousex': this.getMouseX, @@ -37,6 +38,28 @@ Scratch3SensingBlocks.prototype.colorTouchingColor = function (args, util) { return util.target.colorIsTouchingColor(targetColor, maskColor); }; +Scratch3SensingBlocks.prototype.distanceTo = function (args, util) { + if (util.target.isStage) return 10000; + + var targetX = 0; + var targetY = 0; + if (args.DISTANCETOMENU === '_mouse_') { + targetX = util.ioQuery('mouse', 'getX'); + targetY = util.ioQuery('mouse', 'getY'); + } else { + var distTarget = this.runtime.getSpriteTargetByName( + args.DISTANCETOMENU + ); + if (!distTarget) return 10000; + targetX = distTarget.x; + targetY = distTarget.y; + } + + var dx = util.target.x - targetX; + var dy = util.target.y - targetY; + return Math.sqrt((dx * dx) + (dy * dy)); +}; + Scratch3SensingBlocks.prototype.getTimer = function (args, util) { return util.ioQuery('clock', 'projectTimer'); }; From 2b54366d1c291e81d636235606722c1b2deac03a Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 5 Oct 2016 10:00:28 -0400 Subject: [PATCH 10/31] Add issue and PR templates for contributors --- .github/ISSUE_TEMPLATE.md | 15 +++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 14 ++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..9ea236ba5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ +### Expected behavior + +_Please describe what should happen_ + +### Actual behavior + +_Describe what actually happens_ + +### Steps to reproduce + +_Explain what someone needs to do in order to see what's described in *Actual behavior* above_ + +### Operating system and browser + +_e.g. Mac OS 10.11.6 Safari 10.0_ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..1030318c8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +### Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor +- [ ] "Chore" (changes to build script, updates to README, etc.) + +### Proposed changes + +_Describe what this Pull Request does_ + +### Reason for changes + +_Explain why these changes should be made. Please include an issue # if applicable._ From b28c67633abf68066bda4b94ed8659195ab23d1e Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 5 Oct 2016 10:19:25 -0400 Subject: [PATCH 11/31] Remove Type section --- .github/PULL_REQUEST_TEMPLATE.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1030318c8..696bd38d7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,3 @@ -### Type of change - -- [ ] Bug fix -- [ ] New feature -- [ ] Refactor -- [ ] "Chore" (changes to build script, updates to README, etc.) - ### Proposed changes _Describe what this Pull Request does_ From da5b70ffd252c82c2ef70c9e48ee20ef708e4112 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Wed, 5 Oct 2016 16:37:55 -0400 Subject: [PATCH 12/31] Add CONTRIBUTING.md for scratch-vm (#244) * Add CONTRIBUTING.md for scratch-vm * Thought through language * Forum post -> developers link * paper --- CONTRIBUTING.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..ddd2e36c3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +## Contributing +The development of scratch-vm is an ongoing process, +and we love to have people in the Scratch and open source communities help us along the way. + +If you're interested in contributing, please take a look at the +[issues](https://github.com/LLK/scratch-vm/issues) on this repository. +Two great ways of helping are by identifying bugs and documenting them as issues, +or fixing issues and creating pull requests. When submitting pull requests please be patient +-- it can take a while to find time to review them. +The organization and class structures can't be radically changed without significant coordination +and collaboration from the Scratch Team, so these types of changes should be avoided. + +It's been said that the Scratch Team spends about one hour of design discussion for every pixel in Scratch, +but some think that estimate is a little low. While we welcome suggestions for new features in our +[suggestions forum](https://scratch.mit.edu/discuss/1/) (especially ones that come with mockups), we are unlikely to accept PRs with +new features that haven't been thought through and discussed as a group. Why? Because we have a strong belief +in the value of keeping things simple for new users. To learn more about our design philosophy, +see [the Scratch Developers page](https://scratch.mit.edu/developers), or +[this paper](http://web.media.mit.edu/~mres/papers/Scratch-CACM-final.pdf). + +Beyond this repo, there are also some other resources that you might want to take a look at: +* [Community Guidelines](https://github.com/LLK/scratch-www/wiki/Community-Guidelines) (we find it important to maintain a constructive and welcoming community, just like on Scratch) +* [Open Source forum](https://scratch.mit.edu/discuss/49/) on Scratch +* [Suggestions forum](https://scratch.mit.edu/discuss/1/) on Scratch +* [Bugs & Glitches forum](https://scratch.mit.edu/discuss/3/) on Scratch From 5c6d80ab8e2cb31e7ef27b10c2a73a2aa107ab5e Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Fri, 7 Oct 2016 15:51:03 -0400 Subject: [PATCH 13/31] Toolbox update Oct 7 (#246) * Toolbox XML update October 7 * Blocks font CSS --- playground/index.html | 44 ++++++++++++++++++--------------------- playground/playground.css | 1 + 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/playground/index.html b/playground/index.html index e8b980141..41c5ac4f0 100644 --- a/playground/index.html +++ b/playground/index.html @@ -52,7 +52,7 @@
diff --git a/playground/playground.css b/playground/playground.css index 41586749f..3eccd2625 100644 --- a/playground/playground.css +++ b/playground/playground.css @@ -13,6 +13,7 @@ h2 { right: 0; top: 0; bottom: 0; + font-family: "Helvetica Neue", Helvetica, sans-serif; } #vm-devtools { color: rgb(217,217,217); From a7522c7734b587e699d3ed332d7b3e21a456461d Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Tue, 11 Oct 2016 17:12:19 -0400 Subject: [PATCH 14/31] Fix data_listcontents block name (#254) --- src/blocks/scratch3_data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/scratch3_data.js b/src/blocks/scratch3_data.js index 576eb975f..a40172427 100644 --- a/src/blocks/scratch3_data.js +++ b/src/blocks/scratch3_data.js @@ -17,7 +17,7 @@ Scratch3DataBlocks.prototype.getPrimitives = function () { 'data_variable': this.getVariable, 'data_setvariableto': this.setVariableTo, 'data_changevariableby': this.changeVariableBy, - 'data_list': this.getListContents, + 'data_listcontents': this.getListContents, 'data_addtolist': this.addToList, 'data_deleteoflist': this.deleteOfList, 'data_insertatlist': this.insertAtList, From 49d6bd3b67922d4dfef43f06ae9e3f72059e1d2c Mon Sep 17 00:00:00 2001 From: dekrain Date: Wed, 12 Oct 2016 19:56:31 +0200 Subject: [PATCH 15/31] Fix new project creation (#256) * Fix new project creation * Fix new project creation * const -> var --- playground/playground.js | 10 ++++++++-- src/index.js | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/playground/playground.js b/playground/playground.js index 053f6e925..2aee59d58 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -1,6 +1,12 @@ +var NEW_PROJECT_HASH = 'createEmptyProject'; + var loadProject = function () { var id = location.hash.substring(1); - if (id.length < 1) { + if (id === NEW_PROJECT_HASH) { + window.vm.createEmptyProject(); + return; + } + if (id.length < 1 || !isFinite(id)) { id = '119615668'; } var url = 'https://projects.scratch.mit.edu/internalapi/project/' + @@ -32,7 +38,7 @@ window.onload = function() { }; document.getElementById('createEmptyProject').addEventListener('click', function() { - document.location = '#' + 'createEmptyProject'; + document.location = '#' + NEW_PROJECT_HASH; location.reload(); }); loadProject(); diff --git a/src/index.js b/src/index.js index fd987f4de..f608932c8 100644 --- a/src/index.js +++ b/src/index.js @@ -132,7 +132,7 @@ VirtualMachine.prototype.loadProject = function (json) { VirtualMachine.prototype.createEmptyProject = function () { // Stage. var blocks2 = new Blocks(); - var stage = new Sprite(blocks2); + var stage = new Sprite(blocks2, this.runtime); stage.name = 'Stage'; stage.costumes.push({ skin: '/assets/stage.png', @@ -151,7 +151,7 @@ VirtualMachine.prototype.createEmptyProject = function () { target2.isStage = true; // Sprite1 (cat). var blocks1 = new Blocks(); - var sprite = new Sprite(blocks1); + var sprite = new Sprite(blocks1, this.runtime); sprite.name = 'Sprite1'; sprite.costumes.push({ skin: '/assets/scratch_cat.svg', From 1d99efbcd8a52234f78288c1f8d7688ebcfd6d5e Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Wed, 12 Oct 2016 20:58:03 -0400 Subject: [PATCH 16/31] Copy Assets To Playground With Webpack --- webpack.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webpack.config.js b/webpack.config.js index 6d08c73ac..68d377b62 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -52,6 +52,7 @@ module.exports = [ entry: { 'dist': './src/index.js' }, + output: { library: 'VirtualMachine', libraryTarget: 'commonjs2', @@ -108,6 +109,8 @@ module.exports = [ to: 'media' }, { from: 'node_modules/highlightjs/styles/zenburn.css' + }, { + from: 'assets' }]) ]) }) From 476221fc1be50e4ff282fcd5f6be05422ddde022 Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Wed, 12 Oct 2016 21:00:10 -0400 Subject: [PATCH 17/31] change second start to build --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b9743912..15f2dd459 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "prepublish": "./node_modules/.bin/webpack --bail", "start": "webpack-dev-server", - "start": "webpack --colors --progress", + "build": "webpack --colors --progress", "test": "make test", "version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"" }, From 91037a261edafd71ad853048376a699a9afafd8b Mon Sep 17 00:00:00 2001 From: dekrain Date: Thu, 13 Oct 2016 16:19:42 +0200 Subject: [PATCH 18/31] Fix relative localization for assets --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index f608932c8..fa392ff69 100644 --- a/src/index.js +++ b/src/index.js @@ -135,7 +135,7 @@ VirtualMachine.prototype.createEmptyProject = function () { var stage = new Sprite(blocks2, this.runtime); stage.name = 'Stage'; stage.costumes.push({ - skin: '/assets/stage.png', + skin: './assets/stage.png', name: 'backdrop1', bitmapResolution: 2, rotationCenterX: 480, @@ -154,7 +154,7 @@ VirtualMachine.prototype.createEmptyProject = function () { var sprite = new Sprite(blocks1, this.runtime); sprite.name = 'Sprite1'; sprite.costumes.push({ - skin: '/assets/scratch_cat.svg', + skin: './assets/scratch_cat.svg', name: 'costume1', bitmapResolution: 1, rotationCenterX: 47, From 8c654bbe607c88b2d74aeef95fd08324927598be Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Thu, 13 Oct 2016 13:11:26 -0400 Subject: [PATCH 19/31] Procedure blocks (#264) --- src/blocks/scratch3_procedures.js | 19 +++++++- src/engine/blocks.js | 23 +++++++++- src/engine/execute.js | 9 ++++ src/engine/sequencer.js | 5 +++ src/engine/thread.js | 16 +++++++ src/import/sb2import.js | 74 ++++++++++++++++++++++++++++++- src/import/sb2specmap.js | 14 ++---- 7 files changed, 144 insertions(+), 16 deletions(-) diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js index 8d43c4c92..eb33e322b 100644 --- a/src/blocks/scratch3_procedures.js +++ b/src/blocks/scratch3_procedures.js @@ -13,7 +13,8 @@ function Scratch3ProcedureBlocks(runtime) { Scratch3ProcedureBlocks.prototype.getPrimitives = function() { return { 'procedures_defnoreturn': this.defNoReturn, - 'procedures_callnoreturn': this.callNoReturn + 'procedures_callnoreturn': this.callNoReturn, + 'procedures_param': this.param }; }; @@ -23,10 +24,24 @@ Scratch3ProcedureBlocks.prototype.defNoReturn = function () { Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) { if (!util.stackFrame.executed) { - var procedureName = args.mutation.name; + var procedureName = args.mutation.proccode; + var paramNames = util.getProcedureParamNames(procedureName); + for (var i = 0; i < paramNames.length; i++) { + if (args.hasOwnProperty('input' + i)) { + util.pushParam(paramNames[i], args['input' + i]); + } + } util.stackFrame.executed = true; util.startProcedure(procedureName); } }; +Scratch3ProcedureBlocks.prototype.param = function (args, util) { + var value = util.getParam(args.mutation.paramname); + if (!value) { + return 0; + } + return value; +}; + module.exports = Scratch3ProcedureBlocks; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index f96c916ad..e3384d46c 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -151,13 +151,30 @@ Blocks.prototype.getProcedureDefinition = function (name) { var block = this._blocks[id]; if ((block.opcode == 'procedures_defnoreturn' || block.opcode == 'procedures_defreturn') && - block.fields['NAME'].value == name) { + block.mutation.proccode == name) { return id; } } return null; }; +/** + * Get the procedure definition for a given name. + * @param {?string} name Name of procedure to query. + * @return {?string} ID of procedure definition. + */ +Blocks.prototype.getProcedureParamNames = function (name) { + for (var id in this._blocks) { + var block = this._blocks[id]; + if ((block.opcode == 'procedures_defnoreturn' || + block.opcode == 'procedures_defreturn') && + block.mutation.proccode == name) { + return JSON.parse(block.mutation.argumentnames); + } + } + return null; +}; + // --------------------------------------------------------------------- /** @@ -434,7 +451,9 @@ Blocks.prototype.mutationToXML = function (mutation) { var mutationString = '<' + mutation.tagName; for (var prop in mutation) { if (prop == 'children' || prop == 'tagName') continue; - mutationString += ' ' + prop + '="' + mutation[prop] + '"'; + var mutationValue = (typeof mutation[prop] === 'string') ? + xmlEscape(mutation[prop]) : mutation[prop]; + mutationString += ' ' + prop + '="' + mutationValue + '"'; } mutationString += '>'; for (var i = 0; i < mutation.children.length; i++) { diff --git a/src/engine/execute.js b/src/engine/execute.js index 59b4a3cbf..241f238a3 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -164,6 +164,15 @@ var execute = function (sequencer, thread) { startProcedure: function (procedureName) { sequencer.stepToProcedure(thread, procedureName); }, + getProcedureParamNames: function (procedureName) { + return thread.target.blocks.getProcedureParamNames(procedureName); + }, + pushParam: function (paramName, paramValue) { + thread.pushParam(paramName, paramValue); + }, + getParam: function (paramName) { + return thread.getParam(paramName); + }, startHats: function(requestedHat, opt_matchFields, opt_target) { return ( runtime.startHats(requestedHat, opt_matchFields, opt_target) diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 85575c804..0e54ca4ab 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -132,6 +132,11 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) { Sequencer.prototype.stepToProcedure = function (thread, procedureName) { var definition = thread.target.blocks.getProcedureDefinition(procedureName); thread.pushStack(definition); + // Check if the call is recursive. If so, yield. + // @todo: Have behavior match Scratch 2.0. + if (thread.stack.indexOf(definition) > -1) { + thread.setStatus(Thread.STATUS_YIELD_FRAME); + } }; /** diff --git a/src/engine/thread.js b/src/engine/thread.js index 3bb53596b..6a08361fd 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -83,6 +83,7 @@ Thread.prototype.pushStack = function (blockId) { this.stackFrames.push({ reported: {}, // Collects reported input values. waitingReporter: null, // Name of waiting reporter. + params: {}, // Procedure parameters. executionContext: {} // A context passed to block implementations. }); } @@ -135,6 +136,21 @@ Thread.prototype.pushReportedValue = function (value) { } }; +Thread.prototype.pushParam = function (paramName, value) { + var stackFrame = this.peekStackFrame(); + stackFrame.params[paramName] = value; +}; + +Thread.prototype.getParam = function (paramName) { + for (var i = this.stackFrames.length - 1; i >= 0; i--) { + var frame = this.stackFrames[i]; + if (frame.params.hasOwnProperty(paramName)) { + return frame.params[paramName]; + } + } + return null; +}; + /** * Whether the current execution of a thread is at the top of the stack. * @return {Boolean} True if execution is at top of the stack. diff --git a/src/import/sb2import.js b/src/import/sb2import.js index f633de9c0..752e39298 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -203,6 +203,40 @@ function flatten (blocks) { return finalBlocks; } +/** + * Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n") + * into an argument map. This allows us to provide the expected inputs + * to a mutated procedure call. + * @param {string} procCode Scratch 2.0 procedure string. + * @return {Object} Argument map compatible with those in sb2specmap. + */ +function parseProcedureArgMap (procCode) { + var argMap = [ + {} // First item in list is op string. + ]; + var INPUT_PREFIX = 'input'; + var inputCount = 0; + // Split by %n, %b, %s. + var parts = procCode.split(/(?=[^\\]\%[nbs])/); + for (var i = 0; i < parts.length; i++) { + var part = parts[i].trim(); + if (part.substring(0, 1) == '%') { + var argType = part.substring(1, 2); + var arg = { + type: 'input', + inputName: INPUT_PREFIX + (inputCount++) + }; + if (argType == 'n') { + arg.inputOp = 'math_number'; + } else if (argType == 's') { + arg.inputOp = 'text'; + } + argMap.push(arg); + } + } + return argMap; +} + /** * Parse a single SB2 JSON-formatted block and its children. * @param {!Object} sb2block SB2 JSON-formatted block. @@ -227,6 +261,10 @@ function parseBlock (sb2block) { shadow: false, // No shadow blocks in an SB2 by default. children: [] // Store any generated children, flattened in `flatten`. }; + // For a procedure call, generate argument map from proc string. + if (oldOpcode == 'call') { + blockMetadata.argMap = parseProcedureArgMap(sb2block[1]); + } // Look at the expected arguments in `blockMetadata.argMap.` // The basic problem here is to turn positional SB2 arguments into // non-positional named Scratch VM arguments. @@ -328,11 +366,43 @@ function parseBlock (sb2block) { } } // Special cases to generate mutations. - if (oldOpcode == 'call') { + if (oldOpcode == 'stopScripts') { + // Mutation for stop block: if the argument is 'other scripts', + // the block needs a next connection. + if (sb2block[1] == 'other scripts in sprite') { + activeBlock.mutation = { + tagName: 'mutation', + hasnext: 'true', + children: [] + }; + } + } else if (oldOpcode == 'procDef') { + // Mutation for procedure definition: + // store all 2.0 proc data. + var 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: [] + }; + } else if (oldOpcode == 'call') { + // Mutation for procedure call: + // string for proc code (e.g., "abc %n %b %s"). activeBlock.mutation = { tagName: 'mutation', children: [], - name: sb2block[1] + proccode: 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'. }; } return activeBlock; diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js index 76e9683e1..1cf0620fc 100644 --- a/src/import/sb2specmap.js +++ b/src/import/sb2specmap.js @@ -752,9 +752,8 @@ var specMap = { 'opcode':'control_stop', 'argMap':[ { - 'type':'input', - 'inputOp':'control_stop_menu', - 'inputName':'STOP_OPTION' + 'type':'field', + 'fieldName':'STOP_OPTION' } ] }, @@ -1375,15 +1374,10 @@ var specMap = { }, 'procDef':{ 'opcode':'procedures_defnoreturn', - 'argMap':[ - { - 'type':'field', - 'fieldName':'NAME' - } - ] + 'argMap':[] }, 'getParam':{ - 'opcode':'proc_param', + 'opcode':'procedures_param', 'argMap':[] }, 'call':{ From dcda82a0094fc6c6dcd7e0c6fdd4f5190e3a3e8a Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Thu, 13 Oct 2016 17:05:53 -0400 Subject: [PATCH 20/31] Use default toolbox XML (#265) --- playground/index.html | 708 --------------------------------------- playground/playground.js | 2 - 2 files changed, 710 deletions(-) diff --git a/playground/index.html b/playground/index.html index 41c5ac4f0..645980893 100644 --- a/playground/index.html +++ b/playground/index.html @@ -51,714 +51,6 @@
- diff --git a/playground/playground.js b/playground/playground.js index 2aee59d58..76db62ed3 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -50,9 +50,7 @@ window.onload = function() { vm.attachRenderer(renderer); // Instantiate scratch-blocks and attach it to the DOM. - var toolbox = document.getElementById('toolbox'); var workspace = window.Blockly.inject('blocks', { - toolbox: toolbox, media: './media/', zoom: { controls: true, From 1e86d48a31986cb96b12c29a9f6ee13ad79d9b18 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Thu, 13 Oct 2016 17:15:49 -0400 Subject: [PATCH 21/31] Clicking blocks in the toolbox (#267) --- playground/playground.js | 5 ++--- src/engine/blocks.js | 23 +++++++++++++---------- src/engine/execute.js | 31 +++++++++++++++++++++++-------- src/engine/runtime.js | 7 +++++++ src/index.js | 15 ++++++++++----- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/playground/playground.js b/playground/playground.js index 76db62ed3..949a02c5a 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -71,8 +71,9 @@ window.onload = function() { window.workspace = workspace; // Attach scratch-blocks events to VM. - // @todo: Re-enable flyout listening after fixing GH-69. workspace.addChangeListener(vm.blockListener); + var flyoutWorkspace = workspace.getFlyout().getWorkspace(); + flyoutWorkspace.addChangeListener(vm.flyoutBlockListener); // Create FPS counter. var stats = new window.Stats(); @@ -117,11 +118,9 @@ window.onload = function() { // Receipt of new block XML for the selected target. vm.on('workspaceUpdate', function (data) { - window.Blockly.Events.disable(); workspace.clear(); var dom = window.Blockly.Xml.textToDom(data.xml); window.Blockly.Xml.domToWorkspace(dom, workspace); - window.Blockly.Events.enable(); }); // Receipt of new list of targets, selected target update. diff --git a/src/engine/blocks.js b/src/engine/blocks.js index e3384d46c..0e89025e9 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -181,11 +181,10 @@ Blocks.prototype.getProcedureParamNames = function (name) { * Create event listener for blocks. Handles validation and serves as a generic * adapter between the blocks and the runtime interface. * @param {Object} e Blockly "block" event - * @param {boolean} isFlyout If true, create a listener for flyout events. * @param {?Runtime} opt_runtime Optional runtime to forward click events to. */ -Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) { +Blocks.prototype.blocklyListen = function (e, opt_runtime) { // Validate event if (typeof e !== 'object') return; if (typeof e.blockId !== 'string') return; @@ -204,7 +203,7 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) { var newBlocks = adapter(e); // A create event can create many blocks. Add them all. for (var i = 0; i < newBlocks.length; i++) { - this.createBlock(newBlocks[i], isFlyout); + this.createBlock(newBlocks[i]); } break; case 'change': @@ -226,8 +225,10 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) { }); break; case 'delete': - // Don't accept delete events for shadow blocks being obscured. - if (this._blocks[e.blockId].shadow) { + // Don't accept delete events for missing blocks, + // or shadow blocks being obscured. + if (!this._blocks.hasOwnProperty(e.blockId) || + this._blocks[e.blockId].shadow) { return; } // Inform any runtime to forget about glows on this script. @@ -246,9 +247,8 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) { /** * Block management: create blocks and scripts from a `create` event * @param {!Object} block Blockly create event to be processed - * @param {boolean} opt_isFlyoutBlock Whether the block is in the flyout. */ -Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) { +Blocks.prototype.createBlock = function (block) { // Does the block already exist? // Could happen, e.g., for an unobscured shadow. if (this._blocks.hasOwnProperty(block.id)) { @@ -258,9 +258,8 @@ Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) { this._blocks[block.id] = block; // Push block id to scripts array. // Blocks are added as a top-level stack if they are marked as a top-block - // (if they were top-level XML in the event) and if they are not - // flyout blocks. - if (!opt_isFlyoutBlock && block.topLevel) { + // (if they were top-level XML in the event). + if (block.topLevel) { this._addScript(block.id); } }; @@ -288,6 +287,10 @@ Blocks.prototype.changeBlock = function (args) { * @param {!Object} e Blockly move event to be processed */ Blocks.prototype.moveBlock = function (e) { + if (!this._blocks.hasOwnProperty(e.id)) { + return; + } + // Move coordinate changes. if (e.newCoordinate) { this._blocks[e.id].x = e.newCoordinate.x; diff --git a/src/engine/execute.js b/src/engine/execute.js index 241f238a3..f51c0acaa 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -22,19 +22,34 @@ var execute = function (sequencer, thread) { var currentBlockId = thread.peekStack(); var currentStackFrame = thread.peekStackFrame(); - // Verify that the block still exists. - if (!target || - typeof target.blocks.getBlock(currentBlockId) === 'undefined') { + // Check where the block lives: target blocks or flyout blocks. + var targetHasBlock = ( + typeof target.blocks.getBlock(currentBlockId) !== 'undefined' + ); + var flyoutHasBlock = ( + typeof runtime.flyoutBlocks.getBlock(currentBlockId) !== 'undefined' + ); + + // Stop if block or target no longer exists. + if (!target || (!targetHasBlock && !flyoutHasBlock)) { // No block found: stop the thread; script no longer exists. sequencer.retireThread(thread); return; } + // Query info about the block. - var opcode = target.blocks.getOpcode(currentBlockId); + var blockContainer = null; + if (targetHasBlock) { + blockContainer = target.blocks; + } else { + blockContainer = runtime.flyoutBlocks; + } + var opcode = blockContainer.getOpcode(currentBlockId); + var fields = blockContainer.getFields(currentBlockId); + var inputs = blockContainer.getInputs(currentBlockId); var blockFunction = runtime.getOpcodeFunction(opcode); var isHat = runtime.getIsHat(opcode); - var fields = target.blocks.getFields(currentBlockId); - var inputs = target.blocks.getInputs(currentBlockId); + if (!opcode) { console.warn('Could not get opcode for block: ' + currentBlockId); @@ -133,7 +148,7 @@ var execute = function (sequencer, thread) { } // Add any mutation to args (e.g., for procedures). - var mutation = target.blocks.getMutation(currentBlockId); + var mutation = blockContainer.getMutation(currentBlockId); if (mutation) { argValues.mutation = mutation; } @@ -165,7 +180,7 @@ var execute = function (sequencer, thread) { sequencer.stepToProcedure(thread, procedureName); }, getProcedureParamNames: function (procedureName) { - return thread.target.blocks.getProcedureParamNames(procedureName); + return blockContainer.getProcedureParamNames(procedureName); }, pushParam: function (paramName, paramValue) { thread.pushParam(paramName, paramValue); diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 10c025199..952dd213e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1,5 +1,6 @@ var EventEmitter = require('events'); var Sequencer = require('./sequencer'); +var Blocks = require('./blocks'); var Thread = require('./thread'); var util = require('util'); @@ -44,6 +45,8 @@ function Runtime () { /** @type {!Sequencer} */ this.sequencer = new Sequencer(this); + this.flyoutBlocks = new Blocks(); + /** * Map to look up a block primitive's implementation function by its opcode. * This is a two-step lookup: package name first, then primitive name. @@ -463,6 +466,10 @@ Runtime.prototype._updateScriptGlows = function () { if (thread.requestScriptGlowInFrame && target == this._editingTarget) { var blockForThread = thread.peekStack() || thread.topBlock; var script = target.blocks.getTopLevelScript(blockForThread); + if (!script) { + // Attempt to find in flyout blocks. + script = this.flyoutBlocks.getTopLevelScript(blockForThread); + } if (script) { requestedGlowsThisFrame.push(script); } diff --git a/src/index.js b/src/index.js index fa392ff69..469e3fec0 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,7 @@ function VirtualMachine () { }); this.blockListener = this.blockListener.bind(this); + this.flyoutBlockListener = this.flyoutBlockListener.bind(this); } /** @@ -186,14 +187,18 @@ VirtualMachine.prototype.attachRenderer = function (renderer) { */ VirtualMachine.prototype.blockListener = function (e) { if (this.editingTarget) { - this.editingTarget.blocks.blocklyListen( - e, - false, - this.runtime - ); + this.editingTarget.blocks.blocklyListen(e, this.runtime); } }; +/** + * Handle a Blockly event for the flyout. + * @param {!Blockly.Event} e Any Blockly event. + */ +VirtualMachine.prototype.flyoutBlockListener = function (e) { + this.runtime.flyoutBlocks.blocklyListen(e, this.runtime); +}; + /** * Set an editing target. An editor UI can use this function to switch * between editing different targets, sprites, etc. From ebe8f6323255afb4ecf2343dafd057a8251944e2 Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Thu, 13 Oct 2016 20:01:17 -0400 Subject: [PATCH 22/31] Fix --- webpack.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 68d377b62..d46f55bf6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -110,7 +110,8 @@ module.exports = [ }, { from: 'node_modules/highlightjs/styles/zenburn.css' }, { - from: 'assets' + from: 'assets', + to: 'assets' }]) ]) }) From c45b4201157a6a761713281775610612d245b032 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Thu, 13 Oct 2016 22:14:19 -0400 Subject: [PATCH 23/31] Fix check for existing hat threads (#269) --- src/engine/runtime.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 952dd213e..f78424109 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -344,7 +344,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, // any existing threads starting with the top block. for (var i = 0; i < instance.threads.length; i++) { if (instance.threads[i].topBlock === topBlockId && - (!opt_target || instance.threads[i].target == opt_target)) { + instance.threads[i].target == target) { instance._removeThread(instance.threads[i]); } } @@ -353,7 +353,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, // give up if any threads with the top block are running. for (var j = 0; j < instance.threads.length; j++) { if (instance.threads[j].topBlock === topBlockId && - (!opt_target || instance.threads[j].target == opt_target)) { + instance.threads[j].target == target) { // Some thread is already running. return; } From 3bfd755e6005fcf528a544548e517dce6122c8b0 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Thu, 13 Oct 2016 23:00:46 -0400 Subject: [PATCH 24/31] Complete implementation of stop block (#271) * "Other scripts in stage" sb2 * Complete implementation of "stop" block --- src/blocks/scratch3_control.js | 13 ++++++++++--- src/engine/execute.js | 9 +++++++++ src/engine/runtime.js | 9 ++++++++- src/import/sb2import.js | 3 ++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index eab6c1a0f..efa0c2fb6 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -133,9 +133,16 @@ Scratch3ControlBlocks.prototype.ifElse = function(args, util) { } }; -Scratch3ControlBlocks.prototype.stop = function() { - // @todo - don't use this.runtime - this.runtime.stopAll(); +Scratch3ControlBlocks.prototype.stop = function(args, util) { + var option = args.STOP_OPTION; + if (option == 'all') { + util.stopAll(); + } else if (option == 'other scripts in sprite' || + option == 'other scripts in stage') { + util.stopOtherTargetThreads(); + } else if (option == 'this script') { + util.stopThread(); + } }; // @todo (GH-146): remove. diff --git a/src/engine/execute.js b/src/engine/execute.js index f51c0acaa..c857b73d7 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -176,6 +176,15 @@ var execute = function (sequencer, thread) { startBranch: function (branchNum) { sequencer.stepToBranch(thread, branchNum); }, + stopAll: function () { + runtime.stopAll(); + }, + stopOtherTargetThreads: function() { + runtime.stopForTarget(target, thread); + }, + stopThread: function() { + sequencer.retireThread(thread); + }, startProcedure: function (procedureName) { sequencer.stepToProcedure(thread, procedureName); }, diff --git a/src/engine/runtime.js b/src/engine/runtime.js index f78424109..4a101147d 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -246,6 +246,9 @@ Runtime.prototype._pushThread = function (id, target) { * @param {?Thread} thread Thread object to remove from actives */ Runtime.prototype._removeThread = function (thread) { + // Inform sequencer to stop executing that thread. + this.sequencer.retireThread(thread); + // Remove from the list. var i = this.threads.indexOf(thread); if (i > -1) { this.threads.splice(i, 1); @@ -382,10 +385,14 @@ Runtime.prototype.disposeTarget = function (target) { /** * Stop any threads acting on the target. * @param {!Target} target Target to stop threads for. + * @param {Thread=} opt_threadException Optional thread to skip. */ -Runtime.prototype.stopForTarget = function (target) { +Runtime.prototype.stopForTarget = function (target, opt_threadException) { // Stop any threads on the target. for (var i = 0; i < this.threads.length; i++) { + if (this.threads[i] === opt_threadException) { + continue; + } if (this.threads[i].target == target) { this._removeThread(this.threads[i]); } diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 752e39298..b052a4ee9 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -369,7 +369,8 @@ function parseBlock (sb2block) { if (oldOpcode == 'stopScripts') { // Mutation for stop block: if the argument is 'other scripts', // the block needs a next connection. - if (sb2block[1] == 'other scripts in sprite') { + if (sb2block[1] == 'other scripts in sprite' || + sb2block[1] == 'other scripts in stage') { activeBlock.mutation = { tagName: 'mutation', hasnext: 'true', From 95dc6d38b3dd4c55865d71acf7e0a71567adc668 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 17 Oct 2016 09:38:41 -0400 Subject: [PATCH 25/31] Update packages after installing This is the less hacky way to keep scratch-* packages up to date (and any others in the future). This is only necessary because we cache the node_modules directory. --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 091fe53fe..16b90a8ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,9 @@ sudo: false cache: directories: - node_modules -before_install: -# Install the most up to date scratch-* dependencies -- rm -rf node_modules/scratch-* +install: +- npm install +- npm update after_script: - | # RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel From ace9a96fc2cdd5962deab0f48f9064cfa8212788 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Mon, 17 Oct 2016 11:52:02 -0400 Subject: [PATCH 26/31] Fix degToRad function definition. Resolves GH-229 --- src/blocks/scratch3_motion.js | 2 +- src/util/math-util.js | 2 +- test/unit/util_math.js | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 3e19e6ef1..a8687e4f9 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -37,7 +37,7 @@ Scratch3MotionBlocks.prototype.getPrimitives = function() { Scratch3MotionBlocks.prototype.moveSteps = function (args, util) { var steps = Cast.toNumber(args.STEPS); - var radians = MathUtil.degToRad(util.target.direction); + var radians = MathUtil.degToRad(90 - util.target.direction); var dx = steps * Math.cos(radians); var dy = steps * Math.sin(radians); util.target.setXY(util.target.x + dx, util.target.y + dy); diff --git a/src/util/math-util.js b/src/util/math-util.js index b53698ef1..14ebb450e 100644 --- a/src/util/math-util.js +++ b/src/util/math-util.js @@ -6,7 +6,7 @@ function MathUtil () {} * @return {!number} Equivalent value in radians. */ MathUtil.degToRad = function (deg) { - return (Math.PI * (90 - deg)) / 180; + return deg * Math.PI / 180; }; /** diff --git a/test/unit/util_math.js b/test/unit/util_math.js index b282e90cc..b198ef1d3 100644 --- a/test/unit/util_math.js +++ b/test/unit/util_math.js @@ -2,12 +2,11 @@ var test = require('tap').test; var math = require('../../src/util/math-util'); test('degToRad', function (t) { - // @todo This is incorrect - t.strictEqual(math.degToRad(0), 1.5707963267948966); - t.strictEqual(math.degToRad(1), 1.5533430342749535); - t.strictEqual(math.degToRad(180), -1.5707963267948966); - t.strictEqual(math.degToRad(360), -4.71238898038469); - t.strictEqual(math.degToRad(720), -10.995574287564276); + t.strictEqual(math.degToRad(0), 0); + t.strictEqual(math.degToRad(1), 0.017453292519943295); + t.strictEqual(math.degToRad(180), Math.PI); + t.strictEqual(math.degToRad(360), 2 * Math.PI); + t.strictEqual(math.degToRad(720), 4 * Math.PI); t.end(); }); From 9cd4d961011ff1f5050490fe6395a5c859c446cd Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Mon, 17 Oct 2016 11:55:21 -0400 Subject: [PATCH 27/31] Add test coverage for SB2 importer (#278) --- test/fixtures/default.json | 71 ++++++++ test/fixtures/demo.json | 359 +++++++++++++++++++++++++++++++++++++ test/unit/import_sb2.js | 88 +++++++++ 3 files changed, 518 insertions(+) create mode 100644 test/fixtures/default.json create mode 100644 test/fixtures/demo.json create mode 100644 test/unit/import_sb2.js diff --git a/test/fixtures/default.json b/test/fixtures/default.json new file mode 100644 index 000000000..af478a4e2 --- /dev/null +++ b/test/fixtures/default.json @@ -0,0 +1,71 @@ +{ + "objName": "Stage", + "sounds": [{ + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "backdrop1", + "baseLayerID": -1, + "baseLayerMD5": "739b5e2a2435f6e1ec2993791b423146.png", + "bitmapResolution": 1, + "rotationCenterX": 240, + "rotationCenterY": 180 + }], + "currentCostumeIndex": 0, + "penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png", + "penLayerID": -1, + "tempoBPM": 60, + "videoAlpha": 0.5, + "children": [{ + "objName": "Sprite1", + "sounds": [{ + "soundName": "meow", + "soundID": -1, + "md5": "83c36d806dc92327b9e7049a565c6bff.wav", + "sampleCount": 18688, + "rate": 22050, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": -1, + "baseLayerMD5": "09dc888b0b7df19f70d81588ae73420e.svg", + "bitmapResolution": 1, + "rotationCenterX": 47, + "rotationCenterY": 55 + }, + { + "costumeName": "costume2", + "baseLayerID": -1, + "baseLayerMD5": "3696356a03a8d938318876a593572843.svg", + "bitmapResolution": 1, + "rotationCenterX": 47, + "rotationCenterY": 55 + }], + "currentCostumeIndex": 0, + "scratchX": 0, + "scratchY": 0, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 1, + "visible": true, + "spriteInfo": { + } + }], + "info": { + "videoOn": false, + "userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/50.0.2661.102 Safari\/537.36", + "swfVersion": "v446", + "scriptCount": 0, + "spriteCount": 1, + "hasCloudData": false, + "flashVersion": "MAC 21,0,0,242" + } +} diff --git a/test/fixtures/demo.json b/test/fixtures/demo.json new file mode 100644 index 000000000..b10376ec7 --- /dev/null +++ b/test/fixtures/demo.json @@ -0,0 +1,359 @@ +{ + "objName": "Stage", + "variables": [{ + "name": "x", + "value": "1", + "isPersistent": false + }, + { + "name": "y", + "value": "1", + "isPersistent": false + }, + { + "name": "z", + "value": "1", + "isPersistent": false + }, + { + "name": "d", + "value": "1", + "isPersistent": false + }, + { + "name": "a", + "value": 4, + "isPersistent": false + }], + "lists": [{ + "listName": "D# Minor Pentatonic", + "contents": ["78", + "75", + "73", + "75", + "70", + "78", + "73", + "75", + "75", + "78", + "75", + "73", + "75", + "70", + "75", + "78", + "73", + "75", + "78", + "75", + "73", + "75", + "70", + "73", + "68", + "70", + "66", + "68", + "63"], + "isPersistent": false, + "x": 5, + "y": 32, + "width": 125, + "height": 206, + "visible": true + }], + "scripts": [[52, + 8, + [["whenIReceive", "start"], + ["setVar:to:", "a", "1"], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "a"], "D# Minor Pentatonic"], 0.5], ["changeVar:by:", "a", 1]]]]], + [53, + 186, + [["whenIReceive", "start"], + ["setVar:to:", "x", "1"], + ["rest:elapsed:from:", 7.25], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "x"], "D# Minor Pentatonic"], 0.25], ["changeVar:by:", "x", 1]]]]], + [48, + 557, + [["whenIReceive", "start"], + ["setVar:to:", "z", "1"], + ["rest:elapsed:from:", 13], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "z"], "D# Minor Pentatonic"], 0.0625], ["changeVar:by:", "z", 1]]]]], + [49, + 368, + [["whenIReceive", "start"], + ["setVar:to:", "y", "1"], + ["rest:elapsed:from:", 11], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "y"], "D# Minor Pentatonic"], 0.125], ["changeVar:by:", "y", 1]]]]], + [52, + 745, + [["whenIReceive", "start"], + ["setVar:to:", "d", "1"], + ["rest:elapsed:from:", 13.5], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "d"], "D# Minor Pentatonic"], 0.03125], ["changeVar:by:", "d", 1]]]]]], + "sounds": [{ + "soundName": "pop", + "soundID": 0, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "backdrop1", + "baseLayerID": 4, + "baseLayerMD5": "b61b1077b0ea1931abee9dbbfa7903ff.png", + "bitmapResolution": 2, + "rotationCenterX": 480, + "rotationCenterY": 360 + }], + "currentCostumeIndex": 0, + "penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png", + "penLayerID": 0, + "tempoBPM": 60, + "videoAlpha": 0.5, + "children": [{ + "objName": "Indicator", + "scripts": [[247.85, + 32.8, + [["procDef", "foo %n", ["bar"], [1], false], + ["hide"], + ["clearPenTrails"], + ["penColor:", 5968094], + ["say:", ["getParam", "bar", "r"]], + ["stopScripts", "this script"]]], + [41, 36, [["whenGreenFlag"], ["call", "foo %n", 1]]]], + "sounds": [{ + "soundName": "pop", + "soundID": 0, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": 1, + "baseLayerMD5": "d36f6603ec293d2c2198d3ea05109fe0.png", + "bitmapResolution": 2, + "rotationCenterX": 0, + "rotationCenterY": 0 + }], + "currentCostumeIndex": 0, + "scratchX": 22, + "scratchY": -26, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 3, + "visible": false, + "spriteInfo": { + } + }, + { + "target": "Stage", + "cmd": "timer", + "param": null, + "color": 2926050, + "label": "timer", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 5, + "y": 5, + "visible": false + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "x", + "color": 15629590, + "label": "x", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 5, + "y": 268, + "visible": true + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "y", + "color": 15629590, + "label": "y", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 5, + "y": 295, + "visible": true + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "z", + "color": 15629590, + "label": "z", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 78, + "y": 268, + "visible": true + }, + { + "objName": "Play", + "scripts": [[32, 33, [["whenClicked"], ["broadcast:", "start"]]]], + "sounds": [{ + "soundName": "pop", + "soundID": 0, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": 2, + "baseLayerMD5": "30f811366ae3a53e6447932cc7f0212d.png", + "bitmapResolution": 2, + "rotationCenterX": 68, + "rotationCenterY": 115 + }], + "currentCostumeIndex": 0, + "scratchX": 2, + "scratchY": -48, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 1, + "visible": true, + "spriteInfo": { + } + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "d", + "color": 15629590, + "label": "d", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 5, + "y": 241, + "visible": true + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "a", + "color": 15629590, + "label": "a", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 78, + "y": 241, + "visible": true + }, + { + "objName": "Stop", + "scripts": [[45, 104, [["whenClicked"], ["stopScripts", "all"]]]], + "sounds": [{ + "soundName": "pop", + "soundID": 0, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": 3, + "baseLayerMD5": "3de406f265b8d664406adf7c70762514.png", + "bitmapResolution": 2, + "rotationCenterX": 68, + "rotationCenterY": 70 + }], + "currentCostumeIndex": 0, + "scratchX": 121, + "scratchY": -33, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 2, + "visible": true, + "spriteInfo": { + } + }, + { + "listName": "D# Minor Pentatonic", + "contents": ["78", + "75", + "73", + "75", + "70", + "78", + "73", + "75", + "75", + "78", + "75", + "73", + "75", + "70", + "75", + "78", + "73", + "75", + "78", + "75", + "73", + "75", + "70", + "73", + "68", + "70", + "66", + "68", + "63"], + "isPersistent": false, + "x": 5, + "y": 32, + "width": 125, + "height": 206, + "visible": true + }], + "info": { + "spriteCount": 3, + "projectID": "118381369", + "videoOn": false, + "hasCloudData": false, + "userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/53.0.2785.143 Safari\/537.36", + "scriptCount": 9, + "flashVersion": "MAC 23,0,0,185", + "swfVersion": "v450.1" + } +} diff --git a/test/unit/import_sb2.js b/test/unit/import_sb2.js new file mode 100644 index 000000000..be4d62864 --- /dev/null +++ b/test/unit/import_sb2.js @@ -0,0 +1,88 @@ +var fs = require('fs'); +var path = require('path'); +var test = require('tap').test; + +var clone = require('../../src/sprites/clone'); +var runtime = require('../../src/engine/runtime'); +var sb2 = require('../../src/import/sb2import'); + +test('spec', function (t) { + t.type(sb2, 'function'); + t.end(); +}); + +test('default', function (t) { + // Get SB2 JSON (string) + var uri = path.resolve(__dirname, '../fixtures/default.json'); + var file = fs.readFileSync(uri, 'utf8'); + + // Create runtime instance & load SB2 into it + var rt = new runtime(); + sb2(file, rt); + + // Test + t.type(file, 'string'); + t.type(rt, 'object'); + t.type(rt.targets, 'object'); + + t.ok(rt.targets[0] instanceof clone); + t.type(rt.targets[0].id, 'string'); + t.type(rt.targets[0].blocks, 'object'); + t.type(rt.targets[0].variables, 'object'); + t.type(rt.targets[0].lists, 'object'); + + t.equal(rt.targets[0].isOriginal, true); + t.equal(rt.targets[0].currentCostume, 0); + t.equal(rt.targets[0].isOriginal, true); + t.equal(rt.targets[0].isStage, true); + + t.ok(rt.targets[1] instanceof clone); + t.type(rt.targets[1].id, 'string'); + t.type(rt.targets[1].blocks, 'object'); + t.type(rt.targets[1].variables, 'object'); + t.type(rt.targets[1].lists, 'object'); + + t.equal(rt.targets[1].isOriginal, true); + t.equal(rt.targets[1].currentCostume, 0); + t.equal(rt.targets[1].isOriginal, true); + t.equal(rt.targets[1].isStage, false); + t.end(); +}); + +test('demo', function (t) { + // Get SB2 JSON (string) + var uri = path.resolve(__dirname, '../fixtures/demo.json'); + var file = fs.readFileSync(uri, 'utf8'); + + // Create runtime instance & load SB2 into it + var rt = new runtime(); + sb2(file, rt); + + // Test + t.type(file, 'string'); + t.type(rt, 'object'); + t.type(rt.targets, 'object'); + + t.ok(rt.targets[0] instanceof clone); + t.type(rt.targets[0].id, 'string'); + t.type(rt.targets[0].blocks, 'object'); + t.type(rt.targets[0].variables, 'object'); + t.type(rt.targets[0].lists, 'object'); + + t.equal(rt.targets[0].isOriginal, true); + t.equal(rt.targets[0].currentCostume, 0); + t.equal(rt.targets[0].isOriginal, true); + t.equal(rt.targets[0].isStage, true); + + t.ok(rt.targets[1] instanceof clone); + t.type(rt.targets[1].id, 'string'); + t.type(rt.targets[1].blocks, 'object'); + t.type(rt.targets[1].variables, 'object'); + t.type(rt.targets[1].lists, 'object'); + + t.equal(rt.targets[1].isOriginal, true); + t.equal(rt.targets[1].currentCostume, 0); + t.equal(rt.targets[1].isOriginal, true); + t.equal(rt.targets[1].isStage, false); + t.end(); +}); From bd95c1461df9e46d25bf6cac16167c7d789294f0 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 17 Oct 2016 13:43:38 -0400 Subject: [PATCH 28/31] Add VM.clear method Use it before loading projects so targets don't accumulate when multiple projects are loaded on the same instance. Move check to see if the clone is the original clone onto the block implementation so all clones can be removed. Fixes #274 --- src/blocks/scratch3_control.js | 1 + src/engine/runtime.js | 24 ++++++++++++++++-------- src/index.js | 12 +++++++++++- src/sprites/clone.js | 3 --- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index efa0c2fb6..a2bab50e1 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -167,6 +167,7 @@ Scratch3ControlBlocks.prototype.createClone = function (args, util) { }; Scratch3ControlBlocks.prototype.deleteClone = function (args, util) { + if (util.target.isOriginal) return; this.runtime.disposeTarget(util.target); this.runtime.stopForTarget(util.target); }; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 4a101147d..f96dfd514 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -368,18 +368,26 @@ Runtime.prototype.startHats = function (requestedHatOpcode, return newThreads; }; +/** + * Dispose all targets. Return to clean state. + */ +Runtime.prototype.dispose = function () { + this.stopAll(); + this.targets.map(this.disposeTarget, this); +}; + /** * Dispose of a target. * @param {!Target} target Target to dispose of. */ -Runtime.prototype.disposeTarget = function (target) { - // Allow target to do dispose actions. - target.dispose(); - // Remove from list of targets. - var index = this.targets.indexOf(target); - if (index > -1) { - this.targets.splice(index, 1); - } +Runtime.prototype.disposeTarget = function (disposingTarget) { + this.targets = this.targets.filter(function (target) { + if (disposingTarget !== target) return true; + // Allow target to do dispose actions. + target.dispose(); + // Remove from list of targets. + return false; + }); }; /** diff --git a/src/index.js b/src/index.js index 469e3fec0..818ea4f1a 100644 --- a/src/index.js +++ b/src/index.js @@ -73,6 +73,15 @@ VirtualMachine.prototype.stopAll = function () { this.runtime.stopAll(); }; +/** + * Clear out current running project data. + */ +VirtualMachine.prototype.clear = function () { + this.runtime.dispose(); + this.editingTarget = null; + this.emitTargetsUpdate(); +}; + /** * Get data for playground. Data comes back in an emitted event. */ @@ -116,6 +125,7 @@ VirtualMachine.prototype.postIOData = function (device, data) { * @param {?string} json JSON string representing the project. */ VirtualMachine.prototype.loadProject = function (json) { + this.clear(); // @todo: Handle other formats, e.g., Scratch 1.4, Scratch 3.0. sb2import(json, this.runtime); // Select the first target for editing, e.g., the stage. @@ -237,7 +247,7 @@ VirtualMachine.prototype.emitTargetsUpdate = function () { return [target.id, target.getName()]; }), // Currently editing target id. - editingTarget: this.editingTarget.id + editingTarget: this.editingTarget ? this.editingTarget.id : null }); }; diff --git a/src/sprites/clone.js b/src/sprites/clone.js index 4d7c3672c..f63bbfe09 100644 --- a/src/sprites/clone.js +++ b/src/sprites/clone.js @@ -418,9 +418,6 @@ Clone.prototype.onGreenFlag = function () { * Dispose of this clone, destroying any run-time properties. */ Clone.prototype.dispose = function () { - if (this.isOriginal) { // Don't allow a non-clone to delete itself. - return; - } this.runtime.changeCloneCounter(-1); if (this.renderer && this.drawableID !== null) { this.renderer.destroyDrawable(this.drawableID); From 5615583fb298c028b7adc45296bdc7cfe98478f4 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 17 Oct 2016 13:53:49 -0400 Subject: [PATCH 29/31] Lint --- .gitignore | 1 + src/engine/runtime.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 753e3f651..aad54ff5e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ npm-* /dist.js /vm.js /vm.min.js +/playground/assets /playground/media /playground/vendor.js /playground/vm.js diff --git a/src/engine/runtime.js b/src/engine/runtime.js index f96dfd514..ea47a3ba9 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -378,7 +378,7 @@ Runtime.prototype.dispose = function () { /** * Dispose of a target. - * @param {!Target} target Target to dispose of. + * @param {!Target} disposingTarget Target to dispose of. */ Runtime.prototype.disposeTarget = function (disposingTarget) { this.targets = this.targets.filter(function (target) { From 454eb904a35a1119713ffe2ce324a07c48299289 Mon Sep 17 00:00:00 2001 From: Cosmic Web Services Date: Mon, 17 Oct 2016 13:57:25 -0400 Subject: [PATCH 30/31] Add days since 2000 block (#272) * Add days since 2000 block * Empty line addition * Add semicolin * Empty line addition * Tabed * Remove extra line * Make it exactly like scratch 2 was * Fixed spacing --- src/blocks/scratch3_sensing.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 15466e3c0..c4e780dd4 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -23,7 +23,8 @@ Scratch3SensingBlocks.prototype.getPrimitives = function() { 'sensing_mousey': this.getMouseY, 'sensing_mousedown': this.getMouseDown, 'sensing_keypressed': this.getKeyPressed, - 'sensing_current': this.current + 'sensing_current': this.current, + 'sensing_dayssince2000': this.daysSince2000 }; }; @@ -99,4 +100,15 @@ Scratch3SensingBlocks.prototype.getKeyPressed = function (args, util) { return util.ioQuery('keyboard', 'getKeyIsDown', args.KEY_OPTION); }; +Scratch3SensingBlocks.prototype.daysSince2000 = function() +{ + var msPerDay = 24 * 60 * 60 * 1000; + var start = new Date(2000, 1-1, 1); + var today = new Date(); + var dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset(); + var mSecsSinceStart = today.valueOf() - start.valueOf(); + mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000); + return mSecsSinceStart / msPerDay; +}; + module.exports = Scratch3SensingBlocks; From 352c6516d3d18818fbc744c13497f1e3400e4050 Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Mon, 17 Oct 2016 16:44:33 -0400 Subject: [PATCH 31/31] Fix parent setting in sb2 importer (#283) --- src/import/sb2import.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/import/sb2import.js b/src/import/sb2import.js index b052a4ee9..b97144d20 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -292,8 +292,14 @@ function parseBlock (sb2block) { // Single block occupies the input. innerBlocks = [parseBlock(providedArg)]; } + var previousBlock = null; for (var j = 0; j < innerBlocks.length; j++) { - innerBlocks[j].parent = activeBlock.id; + if (j == 0) { + innerBlocks[j].parent = activeBlock.id; + } else { + innerBlocks[j].parent = previousBlock; + } + previousBlock = innerBlocks[j].id; } // Obscures any shadow. shadowObscured = true;