This commit is contained in:
liam4 2016-10-18 08:05:29 -03:00
commit 052cae0176
No known key found for this signature in database
GPG key ID: 3A7605D17C36EF05
74 changed files with 9177 additions and 14908 deletions

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 4
trim_trailing_whitespace = true
[*.{js,html}]
indent_style = space

View file

@ -8,11 +8,13 @@
"max-len": [2, 80, 4], "max-len": [2, 80, 4],
"semi": [2, "always"], "semi": [2, "always"],
"strict": [2, "never"], "strict": [2, "never"],
"no-console": [2, {"allow": ["log", "warn", "error"]}] "no-console": [2, {"allow": ["log", "warn", "error", "groupCollapsed", "groupEnd", "time", "timeEnd"]}],
"valid-jsdoc": ["error", {"requireReturn": false}]
}, },
"env": { "env": {
"node": true, "node": true,
"browser": true "browser": true,
"worker": true
}, },
"extends": "eslint:recommended" "extends": "eslint:recommended"
} }

15
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -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_

7
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,7 @@
### Proposed changes
_Describe what this Pull Request does_
### Reason for changes
_Explain why these changes should be made. Please include an issue # if applicable._

8
.gitignore vendored
View file

@ -8,3 +8,11 @@ npm-*
# Testing # Testing
/.nyc_output /.nyc_output
/coverage /coverage
/dist.js
/vm.js
/vm.min.js
/playground/assets
/playground/media
/playground/vendor.js
/playground/vm.js
/playground/zenburn.css

2
.npmignore Normal file
View file

@ -0,0 +1,2 @@
/.nyc_output
/coverage

View file

@ -1,8 +1,35 @@
language: node_js language: node_js
node_js: node_js:
- "4" - '4'
- "stable" - stable
sudo: false sudo: false
cache: cache:
directories: directories:
- node_modules - node_modules
install:
- npm install
- npm update
after_script:
- |
# RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel
declare exitCode
$(npm bin)/travis-after-all
exitCode=$?
if [[
# Execute after all jobs finish successfully
$exitCode = 0 &&
# Only release on release branches
$RELEASE_BRANCHES =~ $TRAVIS_BRANCH &&
# Don't release on PR builds
$TRAVIS_PULL_REQUEST = "false"
]]; then
# Authenticate NPM
echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
# Set version to timestamp
npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
npm publish
# Publish to gh-pages as most recent committer
git config --global user.email $(git log --pretty=format:"%ce" -n1)
git config --global user.name $(git log --pretty=format:"%cn" -n1)
./node_modules/.bin/gh-pages -x -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -d playground -m "Build for $(git log --pretty=format:%H)"
fi

25
CONTRIBUTING.md Normal file
View file

@ -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

View file

@ -2,6 +2,7 @@ ESLINT=./node_modules/.bin/eslint
NODE=node NODE=node
TAP=./node_modules/.bin/tap TAP=./node_modules/.bin/tap
WEBPACK=./node_modules/.bin/webpack --progress --colors WEBPACK=./node_modules/.bin/webpack --progress --colors
WEBPACK_DEV_SERVER=./node_modules/.bin/webpack-dev-server
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -11,6 +12,9 @@ build:
watch: watch:
$(WEBPACK) --watch $(WEBPACK) --watch
serve:
$(WEBPACK_DEV_SERVER)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
lint: lint:
@ -27,4 +31,4 @@ coverage:
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
.PHONY: build lint test coverage benchmark .PHONY: build lint test coverage benchmark serve

113
README.md
View file

@ -1,33 +1,45 @@
## scratch-vm ## scratch-vm
#### 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). #### 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.com/LLK/scratch-vm.svg?token=xzzHj4ct3SyBTpeqxnx1&branch=develop)](https://travis-ci.com/LLK/scratch-vm) [![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)
## Installation ## Installation
This requires you to have Git and Node.js installed.
In your own node environment/application:
```bash ```bash
npm install scratch-vm npm install https://github.com/LLK/scratch-vm.git
```
If you want to edit/play yourself:
```bash
git clone https://github.com/LLK/scratch-vm.git
cd scratch-vm
npm install
``` ```
## Setup ## Development Server
```js This requires Node.js to be installed.
var VirtualMachine = require('scratch-vm');
var vm = new VirtualMachine();
// Block events For convenience, we've included a development server with the VM. This is sometimes useful when running in an environment that's loading remote resources (e.g., SVGs from the Scratch server).
workspace.addChangeListener(function(e) {
// Handle "tapping" a block
if (e instanceof Blockly.Events.Ui && e.element === 'click') {
var stackBlock = workspace.getBlockById(e.blockId).getRootBlock().id;
vm.runtime.toggleStack(stackBlock);
// Otherwise, pass along to the block listener
} else {
vm.blockListener(e);
}
});
// Run threads ## Running the Development Server
vm.runtime.start(); Open a Command Prompt or Terminal in the repository and run:
```bash
npm start
``` ```
Or on Windows:
```bash
StartServerWindows.bat
```
## Playground
To run the Playground, make sure the dev server's running and go to [http://localhost:8080/](http://localhost:8080/) - you will be directed to the playground, which demonstrates various tools and internal state.
![VM Playground Screenshot](https://i.imgur.com/nOCNqEc.gif)
## Standalone Build ## Standalone Build
```bash ```bash
@ -42,42 +54,51 @@ make build
</script> </script>
``` ```
## How to include in a Node.js App
For an extended setup example, check out the /playground directory, which includes a fully running VM instance.
```js
var VirtualMachine = require('scratch-vm');
var vm = new VirtualMachine();
// Block events
workspace.addChangeListener(vm.blockListener);
// Run threads
vm.start();
```
## Abstract Syntax Tree ## Abstract Syntax Tree
#### Overview #### Overview
The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST) by listening to events emitted by the [scratch-blocks](https://github.com/LLK/scratch-blocks) workspace via the `blockListener`. At any time, the current state of the AST can be viewed by inspecting the `vm.runtime.blocks` object. The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST) by listening to events emitted by the [scratch-blocks](https://github.com/LLK/scratch-blocks) workspace via the `blockListener`. Each target (code-running object, for example, a sprite) keeps an AST for its blocks. At any time, the current state of an AST can be viewed by inspecting the `vm.runtime.targets[...].blocks` object.
#### Anatomy of a Block #### Anatomy of a Block
The VM's block representation contains all the important information for execution and storage. Here's an example representing the "when key pressed" script on a workspace:
```json ```json
{ {
"id": "^1r~63Gdl7;Dh?I*OP3_", "_blocks": {
"opcode": "wedo_motorclockwise", "Q]PK~yJ@BTV8Y~FfISeo": {
"next": null, "id": "Q]PK~yJ@BTV8Y~FfISeo",
"opcode": "event_whenkeypressed",
"inputs": {
},
"fields": { "fields": {
"DURATION": { "KEY_OPTION": {
"name": "DURATION", "name": "KEY_OPTION",
"value": null, "value": "space"
"blocks": {
"1?P=eV(OiDY3vMk!24Ip": {
"id": "1?P=eV(OiDY3vMk!24Ip",
"opcode": "math_number",
"next": null,
"fields": {
"NUM": {
"name": "NUM",
"value": "10",
"blocks": null
}
}
}
} }
}, },
"SUBSTACK": { "next": null,
"name": "SUBSTACK", "topLevel": true,
"value": "@1ln(HsUO4!]*2*%BrE|", "parent": null,
"blocks": null "shadow": false,
} "x": -69.333333333333,
"y": 174
} }
},
"_scripts": [
"Q]PK~yJ@BTV8Y~FfISeo"
]
} }
``` ```
@ -90,5 +111,5 @@ make test
make coverage make coverage
``` ```
## Donation ## Donate
We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, community, and resource development efforts. Donations of any size are appreciated. Thank you! We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, design, community, and resource development efforts. Donations of any size are appreciated. Thank you!

2
StartServerWindows.bat Normal file
View file

@ -0,0 +1,2 @@
@echo off
node_modules\.bin\webpack-dev-server --host 0.0.0.0 --content-base .\playground

42
assets/scratch_cat.svg Normal file
View file

@ -0,0 +1,42 @@
<svg version="1.1" id="cat" x="0px" y="0px" width="95px" height="111px" viewBox="0 0 95 111" enable-background="new 0 0 95 111" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<g id="Layer_3">
<path fill="#FAA51D" stroke="#000000" d="M22.462,79.039c-2.415-0.451-5.304-1.309-7.742-3.503&#xD;&#xA;&#x9;&#x9;C9.268,70.629,7.526,62.535,3.672,64.622c-3.856,2.088-3.782,15.165,8.353,19.194c4.182,1.391,7.998,1.396,11.091,1.312&#xD;&#xA;&#x9;&#x9;c0.811-0.025,7.717-0.654,10.079-4.074c2.361-3.42,0.719-4.272-0.09-4.744C32.295,75.838,25.878,79.677,22.462,79.039z"/>
<path fill="#FFFFFF" d="M4.236,64.877c-1.989,0.613-3.075,4.998-2.076,8.484c0.998,3.49,2.634,5.022,3.863,6.398&#xD;&#xA;&#x9;&#x9;c1.528,1.038-0.72-2.402,1.361-4.15c2.075-1.744,5.733-0.914,5.733-0.914s-2.909-3.987-4.57-6.396&#xD;&#xA;&#x9;&#x9;C6.975,65.988,6.359,64.375,4.236,64.877z"/>
</g>
<g>
<path fill="#FAA51D" d="M38.217,86.756c0,0-8.832,6.2-17.071,8.412l0.086,0.215c1.247,1.824,5.87,7.497-0.334,9.496&#xD;&#xA;&#x9;&#x9;c-5.333,1.717-15.12-13.104-10.821-15.902c2.626-1.713,4.892-0.252,4.892-0.252s3.474-1.07,6.001-2.345&#xD;&#xA;&#x9;&#x9;c4.303-2.161,5.784-3.453,5.784-3.453s4.184-4.306,6.856-4.137C36.281,78.96,41.669,83.504,38.217,86.756z"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M21.232,95.383c1.247,1.824,5.87,7.497-0.334,9.496&#xD;&#xA;&#x9;&#x9;c-5.333,1.717-15.329-13.344-11.03-16.145c2.626-1.713,5.101-0.01,5.101-0.01s3.474-1.072,6.001-2.348&#xD;&#xA;&#x9;&#x9;c4.303-2.161,5.784-3.453,5.784-3.453"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M38.217,86.756c0,0-10.123,7.107-18.804,8.819"/>
</g>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.169,74.885c0,0,1.235,0.165,4.744,3.676&#xD;&#xA;&#x9;c3.509,3.508,6.026,2.16,8.911,0.724c2.877-1.443,10.537-6.126,6.49-9.817c-4.049-3.688-6.207,1.146-9.715,2.405&#xD;&#xA;&#x9;c-3.512,1.26-5.061-2.487-6.858-4.287c-0.589-0.593-1.188-1.099-1.729-1.505c0,0-0.971-0.76-1.906,2.79&#xD;&#xA;&#x9;C51.172,72.412,50.162,73.415,52.169,74.885z"/>
<g id="Layer_2_1_">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M46.753,82.012c1.188-0.912,2.397-2.402,3.951-4.713&#xD;&#xA;&#x9;&#x9;c1.296-1.927,2.7-5.578,2.7-5.578c0.875-2.521,1.934-6.576-1.902-7.296c-1.553-0.291-4.079-0.098-7.67-0.776&#xD;&#xA;&#x9;&#x9;c-3.593-0.681-6.798-2.522-9.517,2.233c-2.718,4.757-9.59,8.271-1.056,16.563c0,0,4.901,3.842,10.764,9.639&#xD;&#xA;&#x9;&#x9;c4.831,4.775,12.045,10.602,12.045,10.602s18.972,2.188,19.535-0.693c1.922-9.79-14.777-6.911-14.777-6.911&#xD;&#xA;&#x9;&#x9;s-4.605-3.933-6.725-5.794c-3.478-3.059-11.125-10.771-11.125-10.771"/>
<path fill="#FFFFFF" d="M51.253,75.434c0,0,2.47-2.66-2.469-5.317c-4.939-2.657-7.213-0.017-8.739,1.521&#xD;&#xA;&#x9;&#x9;c-2.644,2.655,3.443,6.611,3.443,6.611l3.176,3.204c0,0,1.738-1.647,2.499-2.979C50.036,77.26,51.253,75.434,51.253,75.434"/>
</g>
<g id="Layer_8"/>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M29.926,73.218c0.749-0.571,2.889-2.202,4.854-3.657&#xD;&#xA;&#x9;c2.428-1.799,6.117-5.849,1.077-7.646c-5.04-1.801-7.507,1.604-11.519,4.946c-2.159,1.801-5.308,2.699-4.319,6.209&#xD;&#xA;&#x9;c0.993,3.511,4.862,13.408,11.789,10.17c6.929-3.239-1.799-9.18-3.06-11.157"/>
<g id="Layer_2">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.709,14.156c-1.54-0.143-4.75-0.316-6.518-0.231&#xD;&#xA;&#x9;&#x9;c-4.728,0.225-9.224,1.928-9.224,1.928L23.949,7.357l2.235,18.906c0.646-0.782-10.555,12.804-3.479,24.224&#xD;&#xA;&#x9;&#x9;c7.08,11.426,22.233,16.518,40.988,12.792c18.755-3.729,23.229-14.531,21.986-20.246c-1.242-5.714-8.322-7.823-8.322-7.823&#xD;&#xA;&#x9;&#x9;s-0.09-4.48-3.328-9.97c-1.926-3.268-8.348-8.041-8.348-8.041L62.822,5.647l-7.452,7.204L52.709,14.156z"/>
<path fill="#FFFFFF" d="M76.42,35.066l-2.482-2.064l-9.115,2.661c0,0,0,3.419-4.367,4.367c-4.37,0.951-11.211-2.277-11.211-2.277&#xD;&#xA;&#x9;&#x9;L41.46,41.17c0,0-8.437,0.928-8.739,6.081C32.048,58.704,46.1,63.479,51.425,63.783c2.905,0.167,8.235-0.338,12.277-1.141&#xD;&#xA;&#x9;&#x9;c17.752-3.234,22.551-13.919,21.31-19.635c-1.242-5.714-7.978-7.196-7.978-7.196L76.42,35.066z"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M10.673,46.155c0,0,4.107,0.374,5.974,0.268&#xD;&#xA;&#x9;&#x9;c1.865-0.107,5.492-0.587,5.492-0.587"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M81.656,40.671c0,0,4.549-0.743,6.859-1.549&#xD;&#xA;&#x9;&#x9;c2.715-0.942,4.543-2.545,4.543-2.545"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M22.337,41.909c0,0-2.384-1.777-6.117-3.43&#xD;&#xA;&#x9;&#x9;c-4.134-1.831-6.405-2.303-6.405-2.303"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M82.117,46.622c0,0,2.726,1.104,5.533,1.385&#xD;&#xA;&#x9;&#x9;c2.77,0.276,4.646,0.11,4.646,0.11"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M52.35,14.212&#xD;&#xA;&#x9;&#x9;c2.84,0.7,3.887,1.469,3.887,1.469"/>
<line fill="none" stroke="#000000" x1="33.898" y1="13.684" x2="39.956" y2="18.042"/>
</g>
<g id="Layer_5">
<path fill="#FFFFFF" stroke="#231F20" d="M71.84,25.366c2.924,4.479,3.033,9.591,0.242,11.415&#xD;&#xA;&#x9;&#x9;c-2.793,1.825-7.426-0.332-10.354-4.813c-2.933-4.48-3.037-9.589-0.244-11.415C64.275,18.73,68.913,20.884,71.84,25.366z"/>
<path fill="#231F20" d="M71.089,32.522c0,1.08-0.802,1.956-1.8,1.956c-0.993,0-1.803-0.877-1.803-1.956&#xD;&#xA;&#x9;&#x9;c0-1.08,0.81-1.958,1.803-1.958C70.287,30.564,71.089,31.442,71.089,32.522"/>
</g>
<g id="Layer_7">
<path fill="#FFFFFF" stroke="#231F20" d="M47.867,28.619c2.926,4.48,2.619,9.862-0.681,12.015&#xD;&#xA;&#x9;&#x9;c-3.302,2.159-8.351,0.272-11.276-4.208c-2.928-4.48-2.624-9.86,0.678-12.017C39.891,22.253,44.938,24.137,47.867,28.619z"/>
<path fill="#231F20" d="M46.079,34.507c0,1.081-0.803,1.957-1.801,1.957c-0.992,0-1.803-0.878-1.803-1.957&#xD;&#xA;&#x9;&#x9;c0-1.08,0.811-1.957,1.803-1.957C45.274,32.55,46.079,33.427,46.079,34.507"/>
</g>
<path fill="#5E4A42" stroke="#000000" d="M59.766,37.926c1.854,0,4.555-0.284,4.697,0.569c0.143,0.855-1.709,4.203-2.988,4.345&#xD;&#xA;&#x9;c-1.283,0.142-6.125-2.353-6.195-3.919C55.206,37.355,58.055,37.926,59.766,37.926z"/>
<g id="Layer_4">
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M46.774,45.235c0,0,10.347,3.054,14.217,3.897&#xD;&#xA;&#x9;&#x9;c3.868,0.842,10.851,1.684,10.851,1.684s-7.99,10.245-17.328,7.644C45.176,55.863,45.345,49.975,46.774,45.235z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
assets/stage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -9,18 +9,32 @@
"type": "git", "type": "git",
"url": "git+ssh://git@github.com/LLK/scratch-vm.git" "url": "git+ssh://git@github.com/LLK/scratch-vm.git"
}, },
"main": "./src/index.js", "main": "./dist.js",
"scripts": { "scripts": {
"test": "make test" "prepublish": "./node_modules/.bin/webpack --bail",
}, "start": "webpack-dev-server",
"dependencies": { "build": "webpack --colors --progress",
"htmlparser2": "3.9.0", "test": "make test",
"memoizee": "0.3.10" "version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "3.0.1",
"eslint": "2.7.0", "eslint": "2.7.0",
"expose-loader": "0.7.1",
"gh-pages": "0.11.0",
"highlightjs": "8.7.0",
"htmlparser2": "3.9.0",
"json": "9.0.4",
"json-loader": "0.5.4", "json-loader": "0.5.4",
"lodash.defaultsdeep": "4.6.0",
"promise": "7.1.1",
"scratch-blocks": "^0.1.0-prepublish",
"scratch-render": "^0.1.0-prepublish",
"script-loader": "0.7.0",
"stats.js": "0.16.0",
"tap": "5.7.1", "tap": "5.7.1",
"webpack": "1.13.0" "travis-after-all": "1.4.4",
"webpack": "1.13.0",
"webpack-dev-server": "1.14.1"
} }
} }

89
playground/index.html Normal file
View file

@ -0,0 +1,89 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Scratch VM Playground</title>
<link rel="stylesheet" href="playground.css">
<link rel="stylesheet" href="zenburn.css">
</head>
<body>
<div id="vm-devtools">
<h2>Scratch VM Playground</h2>
<select id="selectedTarget" multiple></select>
<div id="projectButtons">
<button id="greenflag">Green flag</button>
<button id="stopall">Stop</button>
</div>
<div>
Turbo: <input id='turbomode' type='checkbox' />
</div>
<div>
Pause: <input id='pausemode' type='checkbox' />
</div>
<div>
Compatibility (30 TPS): <input id='compatmode' type='checkbox' />
</div>
<div>
Single stepping: <input id='singlestepmode' type='checkbox' />
<input id='singlestepspeed' type='range' min='1' max='20' value='10' />
</div>
<br />
<ul id="playgroundLinks">
<li><a id="renderexplorer-link" href="#">Renderer</a></li>
<li><a id="threadexplorer-link" href="#">Threads</a></li>
<li><a id="blockexplorer-link" href="#">Block Representation</a></li>
<li><a id="importexport-link" href="#">Import/Export</a></li>
</ul><br />
<div id="tab-renderexplorer">
Renderer<br />
<canvas id="scratch-stage" style="width: 480px; height: 360px;"></canvas><br />
</div>
<div id="tab-threadexplorer">
Thread explorer
<pre id="threadexplorer"></pre>
</div>
<div id="tab-blockexplorer">
Block explorer
<pre id="blockexplorer"></pre>
</div>
<div id="tab-importexport">
Import/Export<br />
Project ID: <input id="projectId" value="119615668" />
<button id="projectLoadButton">Load</button>
<button id="createEmptyProject">New Project</button><br />
<p>
<input type="button" value="Export to XML" onclick="toXml()">
&nbsp;
<input type="button" value="Import from XML" onclick="fromXml()" id="import">
<br /><br />
<textarea id="importExport"></textarea>
</p>
</div>
</div>
<div id="blocks"></div>
<!-- FPS counter, Syntax highlighter, Blocks, Renderer -->
<script src="./vendor.js"></script>
<!-- VM Worker -->
<script src="./vm.js"></script>
<!-- Playground -->
<script src="./playground.js"></script>
<script>
function toXml() {
var output = document.getElementById('importExport');
var xml = Blockly.Xml.workspaceToDom(workspace);
output.value = Blockly.Xml.domToPrettyText(xml);
output.focus();
output.select();
}
function fromXml() {
var input = document.getElementById('importExport');
var xml = Blockly.Xml.textToDom(input.value);
Blockly.Xml.domToWorkspace(workspace, xml);
}
</script>
</body>
</html>

73
playground/playground.css Normal file
View file

@ -0,0 +1,73 @@
body {
background: rgb(36,36,36);
}
a {
color: rgb(217,217,217);
}
h2 {
font-size: 1em;
}
#blocks {
position: absolute;
left: 40%;
right: 0;
top: 0;
bottom: 0;
font-family: "Helvetica Neue", Helvetica, sans-serif;
}
#vm-devtools {
color: rgb(217,217,217);
position: absolute;
left: 1%;
right: 60%;
top: 1%;
bottom: 0;
width: 35%;
}
#blockexplorer, #threadexplorer, #importexport {
position: absolute;
height: 75%;
overflow: scroll;
border: 1px solid #fff;
background: rgb(36,36,36);
color: rgb(217,217,217);
font-family: monospace;
font-size: 10pt;
width: 480px;
height: 360px;
}
#tab-blockexplorer, #tab-threadexplorer, #tab-importexport {
display: none;
}
#importExport {
width: 480px;
height: 360px;
background: rgb(36,36,36);
color: rgb(217,217,217);
}
#projectId {
background: rgb(36,36,36);
color: rgb(217,217,217);
font-family: monospace;
font-size: 10pt;
}
ul#playgroundLinks {
display: block;
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
}
#playgroundLinks li {
float: left;
}
#playgroundLinks li a {
display: block;
color: white;
text-align: center;
padding: 5px 10px;
}
#playgroundLinks li a:hover {
background-color: #111;
}

312
playground/playground.js Normal file
View file

@ -0,0 +1,312 @@
var NEW_PROJECT_HASH = 'createEmptyProject';
var loadProject = function () {
var id = location.hash.substring(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/' +
id + '/get/';
var r = new XMLHttpRequest();
r.onreadystatechange = function() {
if (this.readyState === 4) {
if (r.status === 200) {
window.vm.loadProject(this.responseText);
} else {
window.vm.createEmptyProject();
}
}
};
r.open('GET', url);
r.send();
};
window.onload = function() {
// Lots of global variables to make debugging easier
// Instantiate the VM.
var vm = new window.VirtualMachine();
window.vm = vm;
// Loading projects from the server.
document.getElementById('projectLoadButton').onclick = function () {
document.location = '#' + document.getElementById('projectId').value;
location.reload();
};
document.getElementById('createEmptyProject').addEventListener('click',
function() {
document.location = '#' + NEW_PROJECT_HASH;
location.reload();
});
loadProject();
// Instantiate the renderer and connect it to the VM.
var canvas = document.getElementById('scratch-stage');
var renderer = new window.RenderWebGL(canvas);
window.renderer = renderer;
vm.attachRenderer(renderer);
// Instantiate scratch-blocks and attach it to the DOM.
var workspace = window.Blockly.inject('blocks', {
media: './media/',
zoom: {
controls: true,
wheel: true,
startScale: 0.75
},
colours: {
workspace: '#334771',
flyout: '#283856',
scrollbar: '#24324D',
scrollbarHover: '#0C111A',
insertionMarker: '#FFFFFF',
insertionMarkerOpacity: 0.3,
fieldShadow: 'rgba(255, 255, 255, 0.3)',
dragShadowOpacity: 0.6
}
});
window.workspace = workspace;
// Attach scratch-blocks events to VM.
workspace.addChangeListener(vm.blockListener);
var flyoutWorkspace = workspace.getFlyout().getWorkspace();
flyoutWorkspace.addChangeListener(vm.flyoutBlockListener);
// Create FPS counter.
var stats = new window.Stats();
document.getElementById('tab-renderexplorer').appendChild(stats.dom);
stats.dom.style.position = 'relative';
stats.begin();
// Playground data tabs.
// Block representation tab.
var blockexplorer = document.getElementById('blockexplorer');
var updateBlockExplorer = function(blocks) {
blockexplorer.innerHTML = JSON.stringify(blocks, null, 2);
window.hljs.highlightBlock(blockexplorer);
};
// Thread representation tab.
var threadexplorer = document.getElementById('threadexplorer');
var cachedThreadJSON = '';
var updateThreadExplorer = function (newJSON) {
if (newJSON != cachedThreadJSON) {
cachedThreadJSON = newJSON;
threadexplorer.innerHTML = cachedThreadJSON;
window.hljs.highlightBlock(threadexplorer);
}
};
// Only request data from the VM thread if the appropriate tab is open.
window.exploreTabOpen = false;
var getPlaygroundData = function () {
vm.getPlaygroundData();
if (window.exploreTabOpen) {
window.requestAnimationFrame(getPlaygroundData);
}
};
// VM handlers.
// Receipt of new playground data (thread, block representations).
vm.on('playgroundData', function(data) {
updateThreadExplorer(data.threads);
updateBlockExplorer(data.blocks);
});
// Receipt of new block XML for the selected target.
vm.on('workspaceUpdate', function (data) {
workspace.clear();
var dom = window.Blockly.Xml.textToDom(data.xml);
window.Blockly.Xml.domToWorkspace(dom, workspace);
});
// Receipt of new list of targets, selected target update.
var selectedTarget = document.getElementById('selectedTarget');
vm.on('targetsUpdate', function (data) {
// Clear select box.
while (selectedTarget.firstChild) {
selectedTarget.removeChild(selectedTarget.firstChild);
}
// Generate new select box.
for (var i = 0; i < data.targetList.length; i++) {
var targetOption = document.createElement('option');
targetOption.setAttribute('value', data.targetList[i][0]);
// If target id matches editingTarget id, select it.
if (data.targetList[i][0] == data.editingTarget) {
targetOption.setAttribute('selected', 'selected');
}
targetOption.appendChild(
document.createTextNode(data.targetList[i][1])
);
selectedTarget.appendChild(targetOption);
}
});
selectedTarget.onchange = function () {
vm.setEditingTarget(this.value);
};
// Feedback for stacks and blocks running.
vm.on('STACK_GLOW_ON', function(data) {
workspace.glowStack(data.id, true);
});
vm.on('STACK_GLOW_OFF', function(data) {
workspace.glowStack(data.id, false);
});
vm.on('BLOCK_GLOW_ON', function(data) {
workspace.glowBlock(data.id, true);
});
vm.on('BLOCK_GLOW_OFF', function(data) {
workspace.glowBlock(data.id, false);
});
vm.on('VISUAL_REPORT', function(data) {
workspace.reportValue(data.id, data.value);
});
// Feed mouse events as VM I/O events.
document.addEventListener('mousemove', function (e) {
var rect = canvas.getBoundingClientRect();
var coordinates = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
canvasWidth: rect.width,
canvasHeight: rect.height
};
window.vm.postIOData('mouse', coordinates);
});
canvas.addEventListener('mousedown', function (e) {
var rect = canvas.getBoundingClientRect();
var data = {
isDown: true,
x: e.clientX - rect.left,
y: e.clientY - rect.top,
canvasWidth: rect.width,
canvasHeight: rect.height
};
window.vm.postIOData('mouse', data);
e.preventDefault();
});
canvas.addEventListener('mouseup', function (e) {
var rect = canvas.getBoundingClientRect();
var data = {
isDown: false,
x: e.clientX - rect.left,
y: e.clientY - rect.top,
canvasWidth: rect.width,
canvasHeight: rect.height
};
window.vm.postIOData('mouse', data);
e.preventDefault();
});
// Feed keyboard events as VM I/O events.
document.addEventListener('keydown', function (e) {
// Don't capture keys intended for Blockly inputs.
if (e.target != document && e.target != document.body) {
return;
}
window.vm.postIOData('keyboard', {
keyCode: e.keyCode,
isDown: true
});
e.preventDefault();
});
document.addEventListener('keyup', function(e) {
// Always capture up events,
// even those that have switched to other targets.
window.vm.postIOData('keyboard', {
keyCode: e.keyCode,
isDown: false
});
// E.g., prevent scroll.
if (e.target != document && e.target != document.body) {
e.preventDefault();
}
});
// Run threads
vm.start();
// Inform VM of animation frames.
var animate = function() {
stats.end();
stats.begin();
window.vm.animationFrame();
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
// Handlers for green flag and stop all.
document.getElementById('greenflag').addEventListener('click', function() {
vm.greenFlag();
});
document.getElementById('stopall').addEventListener('click', function() {
vm.stopAll();
});
document.getElementById('turbomode').addEventListener('change', function() {
var turboOn = document.getElementById('turbomode').checked;
vm.setTurboMode(turboOn);
});
document.getElementById('pausemode').addEventListener('change', function() {
var pauseOn = document.getElementById('pausemode').checked;
vm.setPauseMode(pauseOn);
});
document.getElementById('compatmode').addEventListener('change',
function() {
var compatibilityMode = document.getElementById('compatmode').checked;
vm.setCompatibilityMode(compatibilityMode);
});
document.getElementById('singlestepmode').addEventListener('change',
function() {
var singleStep = document.getElementById('singlestepmode').checked;
vm.setSingleSteppingMode(singleStep);
});
document.getElementById('singlestepspeed').addEventListener('input',
function() {
var speed = document.getElementById('singlestepspeed').value;
vm.setSingleSteppingSpeed(speed);
});
var tabBlockExplorer = document.getElementById('tab-blockexplorer');
var tabThreadExplorer = document.getElementById('tab-threadexplorer');
var tabRenderExplorer = document.getElementById('tab-renderexplorer');
var tabImportExport = document.getElementById('tab-importexport');
// Handlers to show different explorers.
document.getElementById('threadexplorer-link').addEventListener('click',
function () {
window.exploreTabOpen = true;
getPlaygroundData();
tabBlockExplorer.style.display = 'none';
tabRenderExplorer.style.display = 'none';
tabThreadExplorer.style.display = 'block';
tabImportExport.style.display = 'none';
});
document.getElementById('blockexplorer-link').addEventListener('click',
function () {
window.exploreTabOpen = true;
getPlaygroundData();
tabBlockExplorer.style.display = 'block';
tabRenderExplorer.style.display = 'none';
tabThreadExplorer.style.display = 'none';
tabImportExport.style.display = 'none';
});
document.getElementById('renderexplorer-link').addEventListener('click',
function () {
window.exploreTabOpen = false;
tabBlockExplorer.style.display = 'none';
tabRenderExplorer.style.display = 'block';
tabThreadExplorer.style.display = 'none';
tabImportExport.style.display = 'none';
});
document.getElementById('importexport-link').addEventListener('click',
function () {
window.exploreTabOpen = false;
tabBlockExplorer.style.display = 'none';
tabRenderExplorer.style.display = 'none';
tabThreadExplorer.style.display = 'none';
tabImportExport.style.display = 'block';
});
};

View file

@ -1,82 +0,0 @@
function Scratch3Blocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3Blocks.prototype.getPrimitives = function() {
return {
'control_repeat': this.repeat,
'control_forever': this.forever,
'control_wait': this.wait,
'control_stop': this.stop,
'event_whenflagclicked': this.whenFlagClicked,
'event_whenbroadcastreceived': this.whenBroadcastReceived,
'event_broadcast': this.broadcast
};
};
Scratch3Blocks.prototype.repeat = function(argValues, util) {
console.log('Running: control_repeat');
// Initialize loop
if (util.stackFrame.loopCounter === undefined) {
util.stackFrame.loopCounter = parseInt(argValues[0]); // @todo arg
}
// Decrease counter
util.stackFrame.loopCounter--;
// If we still have some left, start the substack
if (util.stackFrame.loopCounter >= 0) {
util.startSubstack();
}
};
Scratch3Blocks.prototype.forever = function(argValues, util) {
console.log('Running: control_forever');
util.startSubstack();
};
Scratch3Blocks.prototype.wait = function(argValues, util) {
console.log('Running: control_wait');
util.yield();
util.timeout(function() {
util.done();
}, 1000 * parseFloat(argValues[0]));
};
Scratch3Blocks.prototype.stop = function() {
console.log('Running: control_stop');
// @todo - don't use this.runtime
this.runtime.stopAll();
};
Scratch3Blocks.prototype.whenFlagClicked = function() {
console.log('Running: event_whenflagclicked');
// No-op
};
Scratch3Blocks.prototype.whenBroadcastReceived = function() {
console.log('Running: event_whenbroadcastreceived');
// No-op
};
Scratch3Blocks.prototype.broadcast = function(argValues, util) {
console.log('Running: event_broadcast');
util.startHats(function(hat) {
if (hat.opcode === 'event_whenbroadcastreceived') {
var shadows = hat.fields.CHOICE.blocks;
for (var sb in shadows) {
var shadowblock = shadows[sb];
return shadowblock.fields.CHOICE.value === argValues[0];
}
}
return false;
});
};
module.exports = Scratch3Blocks;

View file

@ -0,0 +1,139 @@
var Cast = require('../util/cast');
var Timer = require('../util/timer');
function Scratch3ControlBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3ControlBlocks.prototype.getPrimitives = function() {
return {
'control_repeat': this.repeat,
'control_repeat_until': this.repeatUntil,
'control_forever': this.forever,
'control_wait': this.wait,
'control_wait_until': this.waitUntil,
'control_if': this.if,
'control_if_else': this.ifElse,
'control_stop': this.stop,
'control_create_clone_of': this.createClone,
'control_delete_this_clone': this.deleteClone
};
};
Scratch3ControlBlocks.prototype.getHats = function () {
return {
'control_start_as_clone': {
restartExistingThreads: false
}
};
};
Scratch3ControlBlocks.prototype.repeat = function(args, util) {
var times = Math.floor(Cast.toNumber(args.TIMES));
// Initialize loop
if (util.stackFrame.loopCounter === undefined) {
util.stackFrame.loopCounter = times;
}
// Only execute once per frame.
// When the branch finishes, `repeat` will be executed again and
// the second branch will be taken, yielding for the rest of the frame.
// Decrease counter
util.stackFrame.loopCounter--;
// If we still have some left, start the branch.
if (util.stackFrame.loopCounter >= 0) {
util.startBranch(1, true);
}
};
Scratch3ControlBlocks.prototype.repeatUntil = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// If the condition is true, start the branch.
if (!condition) {
util.startBranch(1, true);
}
};
Scratch3ControlBlocks.prototype.waitUntil = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
if (!condition) {
util.yield();
}
};
Scratch3ControlBlocks.prototype.forever = function(args, util) {
util.startBranch(1, true);
};
Scratch3ControlBlocks.prototype.wait = function(args, util) {
if (!util.stackFrame.timer) {
util.stackFrame.timer = new Timer();
util.stackFrame.timer.start();
util.yield();
this.runtime.requestRedraw();
} else {
var duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
if (util.stackFrame.timer.timeElapsed() < duration) {
util.yield();
}
}
};
Scratch3ControlBlocks.prototype.if = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
if (condition) {
util.startBranch(1, false);
}
};
Scratch3ControlBlocks.prototype.ifElse = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
if (condition) {
util.startBranch(1, false);
} else {
util.startBranch(2, false);
}
};
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();
}
};
Scratch3ControlBlocks.prototype.createClone = function (args, util) {
var cloneTarget;
if (args.CLONE_OPTION == '_myself_') {
cloneTarget = util.target;
} else {
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
}
if (!cloneTarget) {
return;
}
var newClone = cloneTarget.makeClone();
if (newClone) {
this.runtime.targets.push(newClone);
}
};
Scratch3ControlBlocks.prototype.deleteClone = function (args, util) {
if (util.target.isOriginal) return;
this.runtime.disposeTarget(util.target);
this.runtime.stopForTarget(util.target);
};
module.exports = Scratch3ControlBlocks;

136
src/blocks/scratch3_data.js Normal file
View file

@ -0,0 +1,136 @@
var Cast = require('../util/cast');
function Scratch3DataBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3DataBlocks.prototype.getPrimitives = function () {
return {
'data_variable': this.getVariable,
'data_setvariableto': this.setVariableTo,
'data_changevariableby': this.changeVariableBy,
'data_listcontents': this.getListContents,
'data_addtolist': this.addToList,
'data_deleteoflist': this.deleteOfList,
'data_insertatlist': this.insertAtList,
'data_replaceitemoflist': this.replaceItemOfList,
'data_itemoflist': this.getItemOfList,
'data_lengthoflist': this.lengthOfList,
'data_listcontainsitem': this.listContainsItem
};
};
Scratch3DataBlocks.prototype.getVariable = function (args, util) {
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
return variable.value;
};
Scratch3DataBlocks.prototype.setVariableTo = function (args, util) {
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
variable.value = args.VALUE;
};
Scratch3DataBlocks.prototype.changeVariableBy = function (args, util) {
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
var castedValue = Cast.toNumber(variable.value);
var dValue = Cast.toNumber(args.VALUE);
variable.value = castedValue + dValue;
};
Scratch3DataBlocks.prototype.getListContents = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
// Determine if the list is all single letters.
// If it is, report contents joined together with no separator.
// If it's not, report contents joined together with a space.
var allSingleLetters = true;
for (var i = 0; i < list.contents.length; i++) {
var listItem = list.contents[i];
if (!((typeof listItem === 'string') &&
(listItem.length == 1))) {
allSingleLetters = false;
break;
}
}
if (allSingleLetters) {
return list.contents.join('');
} else {
return list.contents.join(' ');
}
};
Scratch3DataBlocks.prototype.addToList = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
list.contents.push(args.ITEM);
};
Scratch3DataBlocks.prototype.deleteOfList = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
var index = Cast.toListIndex(args.INDEX, list.contents.length);
if (index === Cast.LIST_INVALID) {
return;
} else if (index === Cast.LIST_ALL) {
list.contents = [];
return;
}
list.contents.splice(index - 1, 1);
};
Scratch3DataBlocks.prototype.insertAtList = function (args, util) {
var item = args.ITEM;
var list = util.target.lookupOrCreateList(args.LIST);
var index = Cast.toListIndex(args.INDEX, list.contents.length + 1);
if (index === Cast.LIST_INVALID) {
return;
}
list.contents.splice(index - 1, 0, item);
};
Scratch3DataBlocks.prototype.replaceItemOfList = function (args, util) {
var item = args.ITEM;
var list = util.target.lookupOrCreateList(args.LIST);
var index = Cast.toListIndex(args.INDEX, list.contents.length);
if (index === Cast.LIST_INVALID) {
return;
}
list.contents.splice(index - 1, 1, item);
};
Scratch3DataBlocks.prototype.getItemOfList = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
var index = Cast.toListIndex(args.INDEX, list.contents.length);
if (index === Cast.LIST_INVALID) {
return '';
}
return list.contents[index - 1];
};
Scratch3DataBlocks.prototype.lengthOfList = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
return list.contents.length;
};
Scratch3DataBlocks.prototype.listContainsItem = function (args, util) {
var item = args.ITEM;
var list = util.target.lookupOrCreateList(args.LIST);
if (list.contents.indexOf(item) >= 0) {
return true;
}
// Try using Scratch comparison operator on each item.
// (Scratch considers the string '123' equal to the number 123).
for (var i = 0; i < list.contents.length; i++) {
if (Cast.compare(list.contents[i], item) == 0) {
return true;
}
}
return false;
};
module.exports = Scratch3DataBlocks;

View file

@ -0,0 +1,89 @@
var Cast = require('../util/cast');
function Scratch3EventBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3EventBlocks.prototype.getPrimitives = function() {
return {
'event_broadcast': this.broadcast,
'event_broadcastandwait': this.broadcastAndWait,
'event_whengreaterthan': this.hatGreaterThanPredicate
};
};
Scratch3EventBlocks.prototype.getHats = function () {
return {
'event_whenflagclicked': {
restartExistingThreads: true
},
'event_whenkeypressed': {
restartExistingThreads: false
},
'event_whenthisspriteclicked': {
restartExistingThreads: true
},
'event_whenbackdropswitchesto': {
restartExistingThreads: true
},
'event_whengreaterthan': {
restartExistingThreads: false,
edgeActivated: true
},
'event_whenbroadcastreceived': {
restartExistingThreads: true
}
};
};
Scratch3EventBlocks.prototype.hatGreaterThanPredicate = function (args, util) {
var option = Cast.toString(args.WHENGREATERTHANMENU).toLowerCase();
var value = Cast.toNumber(args.VALUE);
// @todo: Other cases :)
if (option == 'timer') {
return util.ioQuery('clock', 'projectTimer') > value;
}
return false;
};
Scratch3EventBlocks.prototype.broadcast = function(args, util) {
var broadcastOption = Cast.toString(args.BROADCAST_OPTION);
util.startHats('event_whenbroadcastreceived', {
'BROADCAST_OPTION': broadcastOption
});
};
Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) {
var broadcastOption = Cast.toString(args.BROADCAST_OPTION);
// Have we run before, starting threads?
if (!util.stackFrame.startedThreads) {
// No - start hats for this broadcast.
util.stackFrame.startedThreads = util.startHats(
'event_whenbroadcastreceived', {
'BROADCAST_OPTION': broadcastOption
}
);
if (util.stackFrame.startedThreads.length == 0) {
// Nothing was started.
return;
}
}
// We've run before; check if the wait is still going on.
var instance = this;
var waiting = util.stackFrame.startedThreads.some(function(thread) {
return instance.runtime.isActiveThread(thread);
});
if (waiting) {
util.yield();
}
};
module.exports = Scratch3EventBlocks;

View file

@ -0,0 +1,221 @@
var Cast = require('../util/cast');
function Scratch3LooksBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3LooksBlocks.prototype.getPrimitives = function() {
return {
'looks_say': this.say,
'looks_sayforsecs': this.sayforsecs,
'looks_think': this.think,
'looks_thinkforsecs': this.sayforsecs,
'looks_show': this.show,
'looks_hide': this.hide,
'looks_switchcostumeto': this.switchCostume,
'looks_switchbackdropto': this.switchBackdrop,
'looks_switchbackdroptoandwait': this.switchBackdropAndWait,
'looks_nextcostume': this.nextCostume,
'looks_nextbackdrop': this.nextBackdrop,
'looks_changeeffectby': this.changeEffect,
'looks_seteffectto': this.setEffect,
'looks_cleargraphiceffects': this.clearEffects,
'looks_changesizeby': this.changeSize,
'looks_setsizeto': this.setSize,
'looks_gotofront': this.goToFront,
'looks_gobacklayers': this.goBackLayers,
'looks_size': this.getSize,
'looks_costumeorder': this.getCostumeIndex,
'looks_backdroporder': this.getBackdropIndex,
'looks_backdropname': this.getBackdropName
};
};
Scratch3LooksBlocks.prototype.say = function (args, util) {
util.target.setSay('say', args.MESSAGE);
};
Scratch3LooksBlocks.prototype.sayforsecs = function (args, util) {
util.target.setSay('say', args.MESSAGE);
return new Promise(function(resolve) {
setTimeout(function() {
// Clear say bubble and proceed.
util.target.setSay();
resolve();
}, 1000 * args.SECS);
});
};
Scratch3LooksBlocks.prototype.think = function (args, util) {
util.target.setSay('think', args.MESSAGE);
};
Scratch3LooksBlocks.prototype.thinkforsecs = function (args, util) {
util.target.setSay('think', args.MESSAGE);
return new Promise(function(resolve) {
setTimeout(function() {
// Clear say bubble and proceed.
util.target.setSay();
resolve();
}, 1000 * args.SECS);
});
};
Scratch3LooksBlocks.prototype.show = function (args, util) {
util.target.setVisible(true);
};
Scratch3LooksBlocks.prototype.hide = function (args, util) {
util.target.setVisible(false);
};
/**
* Utility function to set the costume or backdrop of a target.
* Matches the behavior of Scratch 2.0 for different types of arguments.
* @param {!Target} target Target to set costume/backdrop to.
* @param {Any} requestedCostume Costume requested, e.g., 0, 'name', etc.
* @param {boolean=} opt_zeroIndex Set to zero-index the requestedCostume.
* @return {Array.<!Thread>} Any threads started by this switch.
*/
Scratch3LooksBlocks.prototype._setCostumeOrBackdrop = function (target,
requestedCostume, opt_zeroIndex) {
if (typeof requestedCostume === 'number') {
target.setCostume(opt_zeroIndex ?
requestedCostume : requestedCostume - 1);
} else {
var costumeIndex = target.getCostumeIndexByName(requestedCostume);
if (costumeIndex > -1) {
target.setCostume(costumeIndex);
} else if (costumeIndex == 'previous costume' ||
costumeIndex == 'previous backdrop') {
target.setCostume(target.currentCostume - 1);
} else if (costumeIndex == 'next costume' ||
costumeIndex == 'next backdrop') {
target.setCostume(target.currentCostume + 1);
} else {
var forcedNumber = Cast.toNumber(requestedCostume);
if (!isNaN(forcedNumber)) {
target.setCostume(opt_zeroIndex ?
forcedNumber : forcedNumber - 1);
}
}
}
if (target == this.runtime.getTargetForStage()) {
// Target is the stage - start hats.
var newName = target.sprite.costumes[target.currentCostume].name;
return this.runtime.startHats('event_whenbackdropswitchesto', {
'BACKDROP': newName
});
}
return [];
};
Scratch3LooksBlocks.prototype.switchCostume = function (args, util) {
this._setCostumeOrBackdrop(util.target, args.COSTUME);
};
Scratch3LooksBlocks.prototype.nextCostume = function (args, util) {
this._setCostumeOrBackdrop(
util.target, util.target.currentCostume + 1, true
);
};
Scratch3LooksBlocks.prototype.switchBackdrop = function (args) {
this._setCostumeOrBackdrop(this.runtime.getTargetForStage(), args.BACKDROP);
};
Scratch3LooksBlocks.prototype.switchBackdropAndWait = function (args, util) {
// Have we run before, starting threads?
if (!util.stackFrame.startedThreads) {
// No - switch the backdrop.
util.stackFrame.startedThreads = (
this._setCostumeOrBackdrop(
this.runtime.getTargetForStage(),
args.BACKDROP
)
);
if (util.stackFrame.startedThreads.length == 0) {
// Nothing was started.
return;
}
}
// We've run before; check if the wait is still going on.
var instance = this;
var waiting = util.stackFrame.startedThreads.some(function(thread) {
return instance.runtime.isActiveThread(thread);
});
if (waiting) {
util.yield();
}
};
Scratch3LooksBlocks.prototype.nextBackdrop = function () {
var stage = this.runtime.getTargetForStage();
this._setCostumeOrBackdrop(
stage, stage.currentCostume + 1, true
);
};
Scratch3LooksBlocks.prototype.changeEffect = function (args, util) {
var effect = Cast.toString(args.EFFECT).toLowerCase();
var change = Cast.toNumber(args.CHANGE);
if (!util.target.effects.hasOwnProperty(effect)) return;
var newValue = change + util.target.effects[effect];
util.target.setEffect(effect, newValue);
};
Scratch3LooksBlocks.prototype.setEffect = function (args, util) {
var effect = Cast.toString(args.EFFECT).toLowerCase();
var value = Cast.toNumber(args.VALUE);
util.target.setEffect(effect, value);
};
Scratch3LooksBlocks.prototype.clearEffects = function (args, util) {
util.target.clearEffects();
};
Scratch3LooksBlocks.prototype.changeSize = function (args, util) {
var change = Cast.toNumber(args.CHANGE);
util.target.setSize(util.target.size + change);
};
Scratch3LooksBlocks.prototype.setSize = function (args, util) {
var size = Cast.toNumber(args.SIZE);
util.target.setSize(size);
};
Scratch3LooksBlocks.prototype.goToFront = function (args, util) {
util.target.goToFront();
};
Scratch3LooksBlocks.prototype.goBackLayers = function (args, util) {
util.target.goBackLayers(args.NUM);
};
Scratch3LooksBlocks.prototype.getSize = function (args, util) {
return util.target.size;
};
Scratch3LooksBlocks.prototype.getBackdropIndex = function () {
var stage = this.runtime.getTargetForStage();
return stage.currentCostume + 1;
};
Scratch3LooksBlocks.prototype.getBackdropName = function () {
var stage = this.runtime.getTargetForStage();
return stage.sprite.costumes[stage.currentCostume].name;
};
Scratch3LooksBlocks.prototype.getCostumeIndex = function (args, util) {
return util.target.currentCostume + 1;
};
module.exports = Scratch3LooksBlocks;

View file

@ -0,0 +1,234 @@
var Cast = require('../util/cast');
var MathUtil = require('../util/math-util');
var Timer = require('../util/timer');
function Scratch3MotionBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
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,
'motion_pointtowards': this.pointTowards,
'motion_glidesecstoxy': this.glide,
'motion_ifonedgebounce': this.ifOnEdgeBounce,
'motion_setrotationstyle': this.setRotationStyle,
'motion_changexby': this.changeX,
'motion_setx': this.setX,
'motion_changeyby': this.changeY,
'motion_sety': this.setY,
'motion_xposition': this.getX,
'motion_yposition': this.getY,
'motion_direction': this.getDirection
};
};
Scratch3MotionBlocks.prototype.moveSteps = function (args, util) {
var steps = Cast.toNumber(args.STEPS);
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);
};
Scratch3MotionBlocks.prototype.goToXY = function (args, util) {
var x = Cast.toNumber(args.X);
var y = Cast.toNumber(args.Y);
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);
};
Scratch3MotionBlocks.prototype.turnLeft = function (args, util) {
var degrees = Cast.toNumber(args.DEGREES);
util.target.setDirection(util.target.direction - degrees);
};
Scratch3MotionBlocks.prototype.pointInDirection = function (args, util) {
var direction = Cast.toNumber(args.DIRECTION);
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.
util.stackFrame.timer = new Timer();
util.stackFrame.timer.start();
util.stackFrame.duration = Cast.toNumber(args.SECS);
util.stackFrame.startX = util.target.x;
util.stackFrame.startY = util.target.y;
util.stackFrame.endX = Cast.toNumber(args.X);
util.stackFrame.endY = Cast.toNumber(args.Y);
if (util.stackFrame.duration <= 0) {
// Duration too short to glide.
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
return;
}
util.yield();
} else {
var timeElapsed = util.stackFrame.timer.timeElapsed();
if (timeElapsed < util.stackFrame.duration * 1000) {
// In progress: move to intermediate position.
var frac = timeElapsed / (util.stackFrame.duration * 1000);
var dx = frac * (util.stackFrame.endX - util.stackFrame.startX);
var dy = frac * (util.stackFrame.endY - util.stackFrame.startY);
util.target.setXY(
util.stackFrame.startX + dx,
util.stackFrame.startY + dy
);
util.yield();
} else {
// Finished: move to final position.
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
}
}
};
Scratch3MotionBlocks.prototype.ifOnEdgeBounce = function (args, util) {
var bounds = util.target.getBounds();
if (!bounds) {
return;
}
// Measure distance to edges.
// Values are positive when the sprite is far away,
// and clamped to zero when the sprite is beyond.
var stageWidth = this.runtime.constructor.STAGE_WIDTH;
var stageHeight = this.runtime.constructor.STAGE_HEIGHT;
var distLeft = Math.max(0, stageWidth / 2 + bounds.left);
var distTop = Math.max(0, stageHeight / 2 - bounds.top);
var distRight = Math.max(0, stageWidth / 2 - bounds.right);
var distBottom = Math.max(0, stageHeight / 2 + bounds.bottom);
// Find the nearest edge.
var nearestEdge = '';
var minDist = Infinity;
if (distLeft < minDist) {
minDist = distLeft;
nearestEdge = 'left';
}
if (distTop < minDist) {
minDist = distTop;
nearestEdge = 'top';
}
if (distRight < minDist) {
minDist = distRight;
nearestEdge = 'right';
}
if (distBottom < minDist) {
minDist = distBottom;
nearestEdge = 'bottom';
}
if (minDist > 0) {
return; // Not touching any edge.
}
// Point away from the nearest edge.
var radians = MathUtil.degToRad(90 - util.target.direction);
var dx = Math.cos(radians);
var dy = -Math.sin(radians);
if (nearestEdge == 'left') {
dx = Math.max(0.2, Math.abs(dx));
} else if (nearestEdge == 'top') {
dy = Math.max(0.2, Math.abs(dy));
} else if (nearestEdge == 'right') {
dx = 0 - Math.max(0.2, Math.abs(dx));
} else if (nearestEdge == 'bottom') {
dy = 0 - Math.max(0.2, Math.abs(dy));
}
var newDirection = MathUtil.radToDeg(Math.atan2(dy, dx)) + 90;
util.target.setDirection(newDirection);
// Keep within the stage.
var fencedPosition = util.target.keepInFence(util.target.x, util.target.y);
util.target.setXY(fencedPosition[0], fencedPosition[1]);
};
Scratch3MotionBlocks.prototype.setRotationStyle = function (args, util) {
util.target.setRotationStyle(args.STYLE);
};
Scratch3MotionBlocks.prototype.changeX = function (args, util) {
var dx = Cast.toNumber(args.DX);
util.target.setXY(util.target.x + dx, util.target.y);
};
Scratch3MotionBlocks.prototype.setX = function (args, util) {
var x = Cast.toNumber(args.X);
util.target.setXY(x, util.target.y);
};
Scratch3MotionBlocks.prototype.changeY = function (args, util) {
var dy = Cast.toNumber(args.DY);
util.target.setXY(util.target.x, util.target.y + dy);
};
Scratch3MotionBlocks.prototype.setY = function (args, util) {
var y = Cast.toNumber(args.Y);
util.target.setXY(util.target.x, y);
};
Scratch3MotionBlocks.prototype.getX = function (args, util) {
return util.target.x;
};
Scratch3MotionBlocks.prototype.getY = function (args, util) {
return util.target.y;
};
Scratch3MotionBlocks.prototype.getDirection = function (args, util) {
return util.target.direction;
};
module.exports = Scratch3MotionBlocks;

View file

@ -0,0 +1,143 @@
var Cast = require('../util/cast.js');
function Scratch3OperatorsBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3OperatorsBlocks.prototype.getPrimitives = function() {
return {
'operator_add': this.add,
'operator_subtract': this.subtract,
'operator_multiply': this.multiply,
'operator_divide': this.divide,
'operator_lt': this.lt,
'operator_equals': this.equals,
'operator_gt': this.gt,
'operator_and': this.and,
'operator_or': this.or,
'operator_not': this.not,
'operator_random': this.random,
'operator_join': this.join,
'operator_letter_of': this.letterOf,
'operator_length': this.length,
'operator_mod': this.mod,
'operator_round': this.round,
'operator_mathop': this.mathop
};
};
Scratch3OperatorsBlocks.prototype.add = function (args) {
return Cast.toNumber(args.NUM1) + Cast.toNumber(args.NUM2);
};
Scratch3OperatorsBlocks.prototype.subtract = function (args) {
return Cast.toNumber(args.NUM1) - Cast.toNumber(args.NUM2);
};
Scratch3OperatorsBlocks.prototype.multiply = function (args) {
return Cast.toNumber(args.NUM1) * Cast.toNumber(args.NUM2);
};
Scratch3OperatorsBlocks.prototype.divide = function (args) {
return Cast.toNumber(args.NUM1) / Cast.toNumber(args.NUM2);
};
Scratch3OperatorsBlocks.prototype.lt = function (args) {
return Cast.compare(args.OPERAND1, args.OPERAND2) < 0;
};
Scratch3OperatorsBlocks.prototype.equals = function (args) {
return Cast.compare(args.OPERAND1, args.OPERAND2) == 0;
};
Scratch3OperatorsBlocks.prototype.gt = function (args) {
return Cast.compare(args.OPERAND1, args.OPERAND2) > 0;
};
Scratch3OperatorsBlocks.prototype.and = function (args) {
return Cast.toBoolean(args.OPERAND1) && Cast.toBoolean(args.OPERAND2);
};
Scratch3OperatorsBlocks.prototype.or = function (args) {
return Cast.toBoolean(args.OPERAND1) || Cast.toBoolean(args.OPERAND2);
};
Scratch3OperatorsBlocks.prototype.not = function (args) {
return !Cast.toBoolean(args.OPERAND);
};
Scratch3OperatorsBlocks.prototype.random = function (args) {
var nFrom = Cast.toNumber(args.FROM);
var nTo = Cast.toNumber(args.TO);
var low = nFrom <= nTo ? nFrom : nTo;
var high = nFrom <= nTo ? nTo : nFrom;
if (low == high) return low;
// If both arguments are ints, truncate the result to an int.
if (Cast.isInt(args.FROM) && Cast.isInt(args.TO)) {
return low + parseInt(Math.random() * ((high + 1) - low));
}
return (Math.random() * (high - low)) + low;
};
Scratch3OperatorsBlocks.prototype.join = function (args) {
return Cast.toString(args.STRING1) + Cast.toString(args.STRING2);
};
Scratch3OperatorsBlocks.prototype.letterOf = function (args) {
var index = Cast.toNumber(args.LETTER) - 1;
var str = Cast.toString(args.STRING);
// Out of bounds?
if (index < 0 || index >= str.length) {
return '';
}
return str.charAt(index);
};
Scratch3OperatorsBlocks.prototype.length = function (args) {
return Cast.toString(args.STRING).length;
};
Scratch3OperatorsBlocks.prototype.mod = function (args) {
var n = Cast.toNumber(args.NUM1);
var modulus = Cast.toNumber(args.NUM2);
var result = n % modulus;
// Scratch mod is kept positive.
if (result / modulus < 0) result += modulus;
return result;
};
Scratch3OperatorsBlocks.prototype.round = function (args) {
return Math.round(Cast.toNumber(args.NUM));
};
Scratch3OperatorsBlocks.prototype.mathop = function (args) {
var operator = Cast.toString(args.OPERATOR).toLowerCase();
var n = Cast.toNumber(args.NUM);
switch (operator) {
case 'abs': return Math.abs(n);
case 'floor': return Math.floor(n);
case 'ceiling': return Math.ceil(n);
case 'sqrt': return Math.sqrt(n);
case 'sin': return Math.sin((Math.PI * n) / 180);
case 'cos': return Math.cos((Math.PI * n) / 180);
case 'tan': return Math.tan((Math.PI * n) / 180);
case 'asin': return (Math.asin(n) * 180) / Math.PI;
case 'acos': return (Math.acos(n) * 180) / Math.PI;
case 'atan': return (Math.atan(n) * 180) / Math.PI;
case 'ln': return Math.log(n);
case 'log': return Math.log(n) / Math.LN10;
case 'e ^': return Math.exp(n);
case '10 ^': return Math.pow(10, n);
}
return 0;
};
module.exports = Scratch3OperatorsBlocks;

View file

@ -0,0 +1,44 @@
function Scratch3ProcedureBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3ProcedureBlocks.prototype.getPrimitives = function() {
return {
'procedures_defnoreturn': this.defNoReturn,
'procedures_callnoreturn': this.callNoReturn,
'procedures_param': this.param
};
};
Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
// No-op: execute the blocks.
};
Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
if (!util.stackFrame.executed) {
var procedureCode = args.mutation.proccode;
var paramNames = util.getProcedureParamNames(procedureCode);
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(procedureCode);
}
};
Scratch3ProcedureBlocks.prototype.param = function (args, util) {
var value = util.getParam(args.mutation.paramname);
return value;
};
module.exports = Scratch3ProcedureBlocks;

View file

@ -0,0 +1,128 @@
var Cast = require('../util/cast');
function Scratch3SensingBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3SensingBlocks.prototype.getPrimitives = function() {
return {
'sensing_touchingobject': this.touchingObject,
'sensing_touchingcolor': this.touchingColor,
'sensing_coloristouchingcolor': this.colorTouchingColor,
'sensing_distanceto': this.distanceTo,
'sensing_timer': this.getTimer,
'sensing_resettimer': this.resetTimer,
'sensing_mousex': this.getMouseX,
'sensing_mousey': this.getMouseY,
'sensing_mousedown': this.getMouseDown,
'sensing_keypressed': this.getKeyPressed,
'sensing_current': this.current,
'sensing_dayssince2000': this.daysSince2000
};
};
Scratch3SensingBlocks.prototype.touchingObject = function (args, util) {
var requestedObject = args.TOUCHINGOBJECTMENU;
if (requestedObject == '_mouse_') {
var mouseX = util.ioQuery('mouse', 'getX');
var mouseY = util.ioQuery('mouse', 'getY');
return util.target.isTouchingPoint(mouseX, mouseY);
} else if (requestedObject == '_edge_') {
return util.target.isTouchingEdge();
} else {
return util.target.isTouchingSprite(requestedObject);
}
};
Scratch3SensingBlocks.prototype.touchingColor = function (args, util) {
var color = Cast.toRgbColorList(args.COLOR);
return util.target.isTouchingColor(color);
};
Scratch3SensingBlocks.prototype.colorTouchingColor = function (args, util) {
var maskColor = Cast.toRgbColorList(args.COLOR);
var targetColor = Cast.toRgbColorList(args.COLOR2);
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');
};
Scratch3SensingBlocks.prototype.resetTimer = function (args, util) {
util.ioQuery('clock', 'resetProjectTimer');
};
Scratch3SensingBlocks.prototype.getMouseX = function (args, util) {
return util.ioQuery('mouse', 'getX');
};
Scratch3SensingBlocks.prototype.getMouseY = function (args, util) {
return util.ioQuery('mouse', 'getY');
};
Scratch3SensingBlocks.prototype.getMouseDown = function (args, util) {
return util.ioQuery('mouse', 'getIsDown');
};
Scratch3SensingBlocks.prototype.current = function (args) {
var menuOption = Cast.toString(args.CURRENTMENU).toLowerCase();
var date = new Date();
switch (menuOption) {
case 'year': return date.getFullYear();
case 'month': return date.getMonth() + 1; // getMonth is zero-based
case 'date': return date.getDate();
case 'dayofweek': return date.getDay() + 1; // getDay is zero-based, Sun=0
case 'hour': return date.getHours();
case 'minute': return date.getMinutes();
case 'second': return date.getSeconds();
}
return 0;
};
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;

View file

@ -1,154 +0,0 @@
var YieldTimers = require('../util/yieldtimers.js');
function WeDo2Blocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
/**
* Current motor speed, as a percentage (100 = full speed).
* @type {number}
* @private
*/
this._motorSpeed = 100;
/**
* The timeout ID for a pending motor action.
* @type {?int}
* @private
*/
this._motorTimeout = null;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
WeDo2Blocks.prototype.getPrimitives = function() {
return {
'wedo_motorclockwise': this.motorClockwise,
'wedo_motorcounterclockwise': this.motorCounterClockwise,
'wedo_motorspeed': this.motorSpeed,
'wedo_setcolor': this.setColor,
'wedo_whendistanceclose': this.whenDistanceClose,
'wedo_whentilt': this.whenTilt
};
};
/**
* Clamp a value between a minimum and maximum value.
* @todo move this to a common utility class.
* @param val The value to clamp.
* @param min The minimum return value.
* @param max The maximum return value.
* @returns {number} The clamped value.
* @private
*/
WeDo2Blocks.prototype._clamp = function(val, min, max) {
return Math.max(min, Math.min(val, max));
};
/**
* Common implementation for motor blocks.
* @param direction The direction to turn ('left' or 'right').
* @param durationSeconds The number of seconds to run.
* @param util The util instance to use for yielding and finishing.
* @private
*/
WeDo2Blocks.prototype._motorOnFor = function(direction, durationSeconds, util) {
if (this._motorTimeout > 0) {
// @todo maybe this should go through util
YieldTimers.resolve(this._motorTimeout);
this._motorTimeout = null;
}
if (window.native) {
window.native.motorRun(direction, this._motorSpeed);
}
var instance = this;
var myTimeout = this._motorTimeout = util.timeout(function() {
if (instance._motorTimeout == myTimeout) {
instance._motorTimeout = null;
}
if (window.native) {
window.native.motorStop();
}
util.done();
}, 1000 * durationSeconds);
util.yield();
};
WeDo2Blocks.prototype.motorClockwise = function(argValues, util) {
this._motorOnFor('right', parseFloat(argValues[0]), util);
};
WeDo2Blocks.prototype.motorCounterClockwise = function(argValues, util) {
this._motorOnFor('left', parseFloat(argValues[0]), util);
};
WeDo2Blocks.prototype.motorSpeed = function(argValues) {
var speed = argValues[0];
switch (speed) {
case 'slow':
this._motorSpeed = 20;
break;
case 'medium':
this._motorSpeed = 50;
break;
case 'fast':
this._motorSpeed = 100;
break;
}
};
/**
* Convert a color name to a WeDo color index.
* Supports 'mystery' for a random hue.
* @param colorName The color to retrieve.
* @returns {number} The WeDo color index.
* @private
*/
WeDo2Blocks.prototype._getColor = function(colorName) {
var colors = {
'yellow': 7,
'orange': 8,
'coral': 9,
'magenta': 1,
'purple': 2,
'blue': 3,
'green': 6,
'white': 10
};
if (colorName == 'mystery') {
return Math.floor((Math.random() * 10) + 1);
}
return colors[colorName];
};
WeDo2Blocks.prototype.setColor = function(argValues, util) {
if (window.native) {
var colorIndex = this._getColor(argValues[0]);
window.native.setLedColor(colorIndex);
}
// Pause for quarter second
util.yield();
util.timeout(function() {
util.done();
}, 250);
};
WeDo2Blocks.prototype.whenDistanceClose = function() {
console.log('Running: wedo_whendistanceclose');
};
WeDo2Blocks.prototype.whenTilt = function() {
console.log('Running: wedo_whentilt');
};
module.exports = WeDo2Blocks;

View file

@ -1,87 +1,147 @@
var mutationAdapter = require('./mutation-adapter');
var html = require('htmlparser2'); var html = require('htmlparser2');
var memoize = require('memoizee');
var parseDOM = memoize(html.parseDOM, {
length: 1,
resolvers: [String],
max: 200
});
/** /**
* Adapter between block creation events and block representation which can be * Adapter between block creation events and block representation which can be
* used by the Scratch runtime. * used by the Scratch runtime.
* * @param {Object} e `Blockly.events.create`
* @param {Object} `Blockly.events.create` * @return {Array.<Object>} List of blocks from this CREATE event.
*
* @return {Object}
*/ */
module.exports = function (e) { module.exports = function (e) {
// Validate input // Validate input
if (typeof e !== 'object') return; if (typeof e !== 'object') return;
if (typeof e.blockId !== 'string') return;
if (typeof e.xml !== 'object') return; if (typeof e.xml !== 'object') return;
// Storage object return domToBlocks(html.parseDOM(e.xml.outerHTML));
var obj = {
id: e.blockId,
opcode: null,
next: null,
fields: {}
};
// Set opcode
if (typeof e.xml.attributes === 'object') {
obj.opcode = e.xml.attributes.type.value;
}
// Extract fields from event's `innerHTML`
if (typeof e.xml.innerHTML !== 'string') return obj;
if (e.xml.innerHTML === '') return obj;
obj.fields = extract(parseDOM(e.xml.innerHTML));
return obj;
}; };
/** /**
* Extracts fields from a block's innerHTML. * Convert outer blocks DOM from a Blockly CREATE event
* @todo Extend this to support vertical grammar / nested blocks. * to a usable form for the Scratch runtime.
* * This structure is based on Blockly xml.js:`domToWorkspace` and `domToBlock`.
* @param {Object} DOM representation of block's innerHTML * @param {Element} blocksDOM DOM tree for this event.
* * @return {Array.<Object>} Usable list of blocks from this CREATE event.
* @return {Object}
*/ */
function extract (dom) { function domToBlocks (blocksDOM) {
// Storage object // At this level, there could be multiple blocks adjacent in the DOM tree.
var fields = {}; var blocks = {};
for (var i = 0; i < blocksDOM.length; i++) {
// Field var block = blocksDOM[i];
var field = dom[0]; if (!block.name || !block.attribs) {
var fieldName = field.attribs.name; continue;
fields[fieldName] = { }
name: fieldName, var tagName = block.name.toLowerCase();
value: null, if (tagName == 'block' || tagName == 'shadow') {
blocks: {} domToBlock(block, blocks, true, null);
}; }
}
// Shadow block // Flatten blocks object into a list.
var shadow = field.children[0]; var blocksList = [];
var shadowId = shadow.attribs.id; for (var b in blocks) {
var shadowOpcode = shadow.attribs.type; blocksList.push(blocks[b]);
fields[fieldName].blocks[shadowId] = { }
id: shadowId, return blocksList;
opcode: shadowOpcode, }
next: null,
fields: {} /**
}; * Convert and an individual block DOM to the representation tree.
* Based on Blockly's `domToBlockHeadless_`.
// Primitive * @param {Element} blockDOM DOM tree for an individual block.
var primitive = shadow.children[0]; * @param {Object} blocks Collection of blocks to add to.
var primitiveName = primitive.attribs.name; * @param {Boolean} isTopBlock Whether blocks at this level are "top blocks."
var primitiveValue = primitive.children[0].data; * @param {?string} parent Parent block ID.
fields[fieldName].blocks[shadowId].fields[primitiveName] = { */
name: primitiveName, function domToBlock (blockDOM, blocks, isTopBlock, parent) {
value: primitiveValue, // Block skeleton.
blocks: null var block = {
}; id: blockDOM.attribs.id, // Block ID
opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag".
return fields; inputs: {}, // Inputs to this block and the blocks they point to.
fields: {}, // Fields on this block and their values.
next: null, // Next block in the stack, if one exists.
topLevel: isTopBlock, // If this block starts a stack.
parent: parent, // Parent block ID, if available.
shadow: blockDOM.name == 'shadow', // If this represents a shadow/slot.
x: blockDOM.attribs.x, // X position of script, if top-level.
y: blockDOM.attribs.y // Y position of script, if top-level.
};
// Add the block to the representation tree.
blocks[block.id] = block;
// Process XML children and find enclosed blocks, fields, etc.
for (var i = 0; i < blockDOM.children.length; i++) {
var xmlChild = blockDOM.children[i];
// Enclosed blocks and shadows
var childBlockNode = null;
var childShadowNode = null;
for (var j = 0; j < xmlChild.children.length; j++) {
var grandChildNode = xmlChild.children[j];
if (!grandChildNode.name) {
// Non-XML tag node.
continue;
}
var grandChildNodeName = grandChildNode.name.toLowerCase();
if (grandChildNodeName == 'block') {
childBlockNode = grandChildNode;
} else if (grandChildNodeName == 'shadow') {
childShadowNode = grandChildNode;
}
}
// Use shadow block only if there's no real block node.
if (!childBlockNode && childShadowNode) {
childBlockNode = childShadowNode;
}
// Not all Blockly-type blocks are handled here,
// as we won't be using all of them for Scratch.
switch (xmlChild.name.toLowerCase()) {
case 'field':
// Add the field to this block.
var fieldName = xmlChild.attribs.name;
var fieldData = '';
if (xmlChild.children.length > 0 && xmlChild.children[0].data) {
fieldData = xmlChild.children[0].data;
} else {
// If the child of the field with a data property
// doesn't exist, set the data to an empty string.
fieldData = '';
}
block.fields[fieldName] = {
name: fieldName,
value: fieldData
};
break;
case 'value':
case 'statement':
// Recursively generate block structure for input block.
domToBlock(childBlockNode, blocks, false, block.id);
if (childShadowNode && childBlockNode != childShadowNode) {
// Also generate the shadow block.
domToBlock(childShadowNode, blocks, false, block.id);
}
// Link this block's input to the child block.
var inputName = xmlChild.attribs.name;
block.inputs[inputName] = {
name: inputName,
block: childBlockNode.attribs.id,
shadow: childShadowNode ? childShadowNode.attribs.id : null
};
break;
case 'next':
if (!childBlockNode || !childBlockNode.attribs) {
// Invalid child block.
continue;
}
// Recursively generate block structure for next block.
domToBlock(childBlockNode, blocks, false, block.id);
// Link next block to this block.
block.next = childBlockNode.attribs.id;
break;
case 'mutation':
block.mutation = mutationAdapter(xmlChild);
break;
}
}
} }

494
src/engine/blocks.js Normal file
View file

@ -0,0 +1,494 @@
var adapter = require('./adapter');
var mutationAdapter = require('./mutation-adapter');
var xmlEscape = require('../util/xml-escape');
/**
* @fileoverview
* Store and mutate the VM block representation,
* and handle updates from Scratch Blocks events.
*/
function Blocks () {
/**
* All blocks in the workspace.
* Keys are block IDs, values are metadata about the block.
* @type {Object.<string, Object>}
*/
this._blocks = {};
/**
* All top-level scripts in the workspace.
* A list of block IDs that represent scripts (i.e., first block in script).
* @type {Array.<String>}
*/
this._scripts = [];
}
/**
* Blockly inputs that represent statements/branch.
* are prefixed with this string.
* @const{string}
*/
Blocks.BRANCH_INPUT_PREFIX = 'SUBSTACK';
/**
* Provide an object with metadata for the requested block ID.
* @param {!string} blockId ID of block we have stored.
* @return {?Object} Metadata about the block, if it exists.
*/
Blocks.prototype.getBlock = function (blockId) {
return this._blocks[blockId];
};
/**
* Get all known top-level blocks that start scripts.
* @return {Array.<string>} List of block IDs.
*/
Blocks.prototype.getScripts = function () {
return this._scripts;
};
/**
* Get the next block for a particular block
* @param {?string} id ID of block to get the next block for
* @return {?string} ID of next block in the sequence
*/
Blocks.prototype.getNextBlock = function (id) {
if (typeof this._blocks[id] === 'undefined') return null;
return this._blocks[id].next;
};
/**
* Get the branch for a particular C-shaped block.
* @param {?string} id ID for block to get the branch for.
* @param {?number} branchNum Which branch to select (e.g. for if-else).
* @return {?string} ID of block in the branch.
*/
Blocks.prototype.getBranch = function (id, branchNum) {
var block = this._blocks[id];
if (typeof block === 'undefined') return null;
if (!branchNum) branchNum = 1;
var inputName = Blocks.BRANCH_INPUT_PREFIX;
if (branchNum > 1) {
inputName += branchNum;
}
// Empty C-block?
if (!(inputName in block.inputs)) return null;
return block.inputs[inputName].block;
};
/**
* Get the opcode for a particular block
* @param {?string} id ID of block to query
* @return {?string} the opcode corresponding to that block
*/
Blocks.prototype.getOpcode = function (id) {
if (typeof this._blocks[id] === 'undefined') return null;
return this._blocks[id].opcode;
};
/**
* Get all fields and their values for a block.
* @param {?string} id ID of block to query.
* @return {!Object} All fields and their values.
*/
Blocks.prototype.getFields = function (id) {
if (typeof this._blocks[id] === 'undefined') return null;
return this._blocks[id].fields;
};
/**
* Get all non-branch inputs for a block.
* @param {?string} id ID of block to query.
* @return {!Object} All non-branch inputs and their associated blocks.
*/
Blocks.prototype.getInputs = function (id) {
if (typeof this._blocks[id] === 'undefined') return null;
var inputs = {};
for (var input in this._blocks[id].inputs) {
// Ignore blocks prefixed with branch prefix.
if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length)
!= Blocks.BRANCH_INPUT_PREFIX) {
inputs[input] = this._blocks[id].inputs[input];
}
}
return inputs;
};
/**
* Get mutation data for a block.
* @param {?string} id ID of block to query.
* @return {!Object} Mutation for the block.
*/
Blocks.prototype.getMutation = function (id) {
if (typeof this._blocks[id] === 'undefined') return null;
return this._blocks[id].mutation;
};
/**
* Get the top-level script for a given block.
* @param {?string} id ID of block to query.
* @return {?string} ID of top-level script block.
*/
Blocks.prototype.getTopLevelScript = function (id) {
if (typeof this._blocks[id] === 'undefined') return null;
var block = this._blocks[id];
while (block.parent !== null) {
block = this._blocks[block.parent];
}
return block.id;
};
/**
* Get the procedure definition for a given name.
* @param {?string} name Name of procedure to query.
* @return {?string} ID of procedure definition.
*/
Blocks.prototype.getProcedureDefinition = 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 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;
};
// ---------------------------------------------------------------------
/**
* 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 {?Runtime} opt_runtime Optional runtime to forward click events to.
*/
Blocks.prototype.blocklyListen = function (e, opt_runtime) {
// Validate event
if (typeof e !== 'object') return;
if (typeof e.blockId !== 'string') return;
// UI event: clicked scripts toggle in the runtime.
if (e.element === 'stackclick') {
if (opt_runtime) {
opt_runtime.toggleScript(e.blockId);
}
return;
}
// Block create/update/destroy
switch (e.type) {
case 'create':
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]);
}
break;
case 'change':
this.changeBlock({
id: e.blockId,
element: e.element,
name: e.name,
value: e.newValue
});
break;
case 'move':
this.moveBlock({
id: e.blockId,
oldParent: e.oldParentId,
oldInput: e.oldInputName,
newParent: e.newParentId,
newInput: e.newInputName,
newCoordinate: e.newCoordinate
});
break;
case 'delete':
// 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.
if (opt_runtime && this._blocks[e.blockId].topLevel) {
opt_runtime.quietGlow(e.blockId);
}
this.deleteBlock({
id: e.blockId
});
break;
}
};
// ---------------------------------------------------------------------
/**
* Block management: create blocks and scripts from a `create` event
* @param {!Object} block Blockly create event to be processed
*/
Blocks.prototype.createBlock = function (block) {
// Does the block already exist?
// Could happen, e.g., for an unobscured shadow.
if (this._blocks.hasOwnProperty(block.id)) {
return;
}
// Create new block.
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).
if (block.topLevel) {
this._addScript(block.id);
}
};
/**
* Block management: change block field values
* @param {!Object} args Blockly change event to be processed
*/
Blocks.prototype.changeBlock = function (args) {
// Validate
if (args.element !== 'field' && args.element !== 'mutation') return;
if (typeof this._blocks[args.id] === 'undefined') return;
if (args.element == 'field') {
// Update block value
if (!this._blocks[args.id].fields[args.name]) return;
this._blocks[args.id].fields[args.name].value = args.value;
} else if (args.element == 'mutation') {
this._blocks[args.id].mutation = mutationAdapter(args.value);
}
};
/**
* Block management: move blocks from parent to parent
* @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;
this._blocks[e.id].y = e.newCoordinate.y;
}
// Remove from any old parent.
if (e.oldParent !== undefined) {
var oldParent = this._blocks[e.oldParent];
if (e.oldInput !== undefined &&
oldParent.inputs[e.oldInput].block === e.id) {
// This block was connected to the old parent's input.
oldParent.inputs[e.oldInput].block = null;
} else if (oldParent.next === e.id) {
// This block was connected to the old parent's next connection.
oldParent.next = null;
}
this._blocks[e.id].parent = null;
}
// Has the block become a top-level block?
if (e.newParent === undefined) {
this._addScript(e.id);
} else {
// Remove script, if one exists.
this._deleteScript(e.id);
// Otherwise, try to connect it in its new place.
if (e.newInput !== undefined) {
// Moved to the new parent's input.
// Don't obscure the shadow block.
var oldShadow = null;
if (this._blocks[e.newParent].inputs.hasOwnProperty(e.newInput)) {
oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow;
}
this._blocks[e.newParent].inputs[e.newInput] = {
name: e.newInput,
block: e.id,
shadow: oldShadow
};
} else {
// Moved to the new parent's next connection.
this._blocks[e.newParent].next = e.id;
}
this._blocks[e.id].parent = e.newParent;
}
};
/**
* Block management: delete blocks and their associated scripts.
* @param {!Object} e Blockly delete event to be processed.
*/
Blocks.prototype.deleteBlock = function (e) {
// @todo In runtime, stop threads running on this script.
// Get block
var block = this._blocks[e.id];
// Delete children
if (block.next !== null) {
this.deleteBlock({id: block.next});
}
// Delete inputs (including branches)
for (var input in block.inputs) {
// If it's null, the block in this input moved away.
if (block.inputs[input].block !== null) {
this.deleteBlock({id: block.inputs[input].block});
}
// Delete obscured shadow blocks.
if (block.inputs[input].shadow !== null &&
block.inputs[input].shadow !== block.inputs[input].block) {
this.deleteBlock({id: block.inputs[input].shadow});
}
}
// Delete any script starting with this block.
this._deleteScript(e.id);
// Delete block itself.
delete this._blocks[e.id];
};
// ---------------------------------------------------------------------
/**
* Encode all of `this._blocks` as an XML string usable
* by a Blockly/scratch-blocks workspace.
* @return {string} String of XML representing this object's blocks.
*/
Blocks.prototype.toXML = function () {
var xmlString = '<xml xmlns="http://www.w3.org/1999/xhtml">';
for (var i = 0; i < this._scripts.length; i++) {
xmlString += this.blockToXML(this._scripts[i]);
}
return xmlString + '</xml>';
};
/**
* Recursively encode an individual block and its children
* into a Blockly/scratch-blocks XML string.
* @param {!string} blockId ID of block to encode.
* @return {string} String of XML representing this block and any children.
*/
Blocks.prototype.blockToXML = function (blockId) {
var block = this._blocks[blockId];
// Encode properties of this block.
var tagName = (block.shadow) ? 'shadow' : 'block';
var xy = (block.topLevel) ?
' x="' + block.x +'"' + ' y="' + block.y +'"' :
'';
var xmlString = '';
xmlString += '<' + tagName +
' id="' + block.id + '"' +
' type="' + block.opcode + '"' +
xy +
'>';
// Add any mutation. Must come before inputs.
if (block.mutation) {
xmlString += this.mutationToXML(block.mutation);
}
// Add any inputs on this block.
for (var input in block.inputs) {
var blockInput = block.inputs[input];
// Only encode a value tag if the value input is occupied.
if (blockInput.block || blockInput.shadow) {
xmlString += '<value name="' + blockInput.name + '">';
if (blockInput.block) {
xmlString += this.blockToXML(blockInput.block);
}
if (blockInput.shadow && blockInput.shadow != blockInput.block) {
// Obscured shadow.
xmlString += this.blockToXML(blockInput.shadow);
}
xmlString += '</value>';
}
}
// Add any fields on this block.
for (var field in block.fields) {
var blockField = block.fields[field];
var value = blockField.value;
if (typeof value === 'string') {
value = xmlEscape(blockField.value);
}
xmlString += '<field name="' + blockField.name + '">' +
value + '</field>';
}
// Add blocks connected to the next connection.
if (block.next) {
xmlString += '<next>' + this.blockToXML(block.next) + '</next>';
}
xmlString += '</' + tagName + '>';
return xmlString;
};
/**
* Recursively encode a mutation object to XML.
* @param {!Object} mutation Object representing a mutation.
* @return {string} XML string representing a mutation.
*/
Blocks.prototype.mutationToXML = function (mutation) {
var mutationString = '<' + mutation.tagName;
for (var prop in mutation) {
if (prop == 'children' || prop == 'tagName') continue;
var mutationValue = (typeof mutation[prop] === 'string') ?
xmlEscape(mutation[prop]) : mutation[prop];
mutationString += ' ' + prop + '="' + mutationValue + '"';
}
mutationString += '>';
for (var i = 0; i < mutation.children.length; i++) {
mutationString += this.mutationToXML(mutation.children[i]);
}
mutationString += '</' + mutation.tagName + '>';
return mutationString;
};
// ---------------------------------------------------------------------
/**
* Helper to add a stack to `this._scripts`.
* @param {?string} topBlockId ID of block that starts the script.
*/
Blocks.prototype._addScript = function (topBlockId) {
var i = this._scripts.indexOf(topBlockId);
if (i > -1) return; // Already in scripts.
this._scripts.push(topBlockId);
// Update `topLevel` property on the top block.
this._blocks[topBlockId].topLevel = true;
};
/**
* Helper to remove a script from `this._scripts`.
* @param {?string} topBlockId ID of block that starts the script.
*/
Blocks.prototype._deleteScript = function (topBlockId) {
var i = this._scripts.indexOf(topBlockId);
if (i > -1) this._scripts.splice(i, 1);
// Update `topLevel` property on the top block.
if (this._blocks[topBlockId]) this._blocks[topBlockId].topLevel = false;
};
module.exports = Blocks;

248
src/engine/execute.js Normal file
View file

@ -0,0 +1,248 @@
var Thread = require('./thread');
/**
* Utility function to determine if a value is a Promise.
* @param {*} value Value to check for a Promise.
* @return {Boolean} True if the value appears to be a Promise.
*/
var isPromise = function (value) {
return value && value.then && typeof value.then === 'function';
};
/**
* Execute a block.
* @param {!Sequencer} sequencer Which sequencer is executing.
* @param {!Thread} thread Thread which to read and execute.
*/
var execute = function (sequencer, thread) {
var runtime = sequencer.runtime;
var target = thread.target;
// Current block to execute is the one on the top of the stack.
var currentBlockId = thread.peekStack();
var currentStackFrame = thread.peekStackFrame();
// 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 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);
if (!opcode) {
console.warn('Could not get opcode for block: ' + currentBlockId);
return;
}
/**
* Handle any reported value from the primitive, either directly returned
* or after a promise resolves.
* @param {*} resolvedValue Value eventually returned from the primitive.
*/
var handleReport = function (resolvedValue) {
thread.pushReportedValue(resolvedValue);
if (isHat) {
// Hat predicate was evaluated.
if (runtime.getIsEdgeActivatedHat(opcode)) {
// If this is an edge-activated hat, only proceed if
// the value is true and used to be false.
var oldEdgeValue = runtime.updateEdgeActivatedValue(
currentBlockId,
resolvedValue
);
var edgeWasActivated = !oldEdgeValue && resolvedValue;
if (!edgeWasActivated) {
sequencer.retireThread(thread);
}
} else {
// Not an edge-activated hat: retire the thread
// if predicate was false.
if (!resolvedValue) {
sequencer.retireThread(thread);
}
}
} else {
// In a non-hat, report the value visually if necessary if
// at the top of the thread stack.
if (typeof resolvedValue !== 'undefined' && thread.atStackTop()) {
runtime.visualReport(currentBlockId, resolvedValue);
}
// Finished any yields.
thread.status = Thread.STATUS_RUNNING;
}
};
// Hats and single-field shadows are implemented slightly differently
// from regular blocks.
// For hats: if they have an associated block function,
// it's treated as a predicate; if not, execution will proceed as a no-op.
// For single-field shadows: If the block has a single field, and no inputs,
// immediately return the value of the field.
if (!blockFunction) {
if (isHat) {
// Skip through the block (hat with no predicate).
return;
} else {
if (Object.keys(fields).length == 1 &&
Object.keys(inputs).length == 0) {
// One field and no inputs - treat as arg.
for (var fieldKey in fields) { // One iteration.
handleReport(fields[fieldKey].value);
}
} else {
console.warn('Could not get implementation for opcode: ' +
opcode);
}
thread.requestScriptGlowInFrame = true;
return;
}
}
// Generate values for arguments (inputs).
var argValues = {};
// Add all fields on this block to the argValues.
for (var fieldName in fields) {
argValues[fieldName] = fields[fieldName].value;
}
// Recursively evaluate input blocks.
for (var inputName in inputs) {
var input = inputs[inputName];
var inputBlockId = input.block;
// Is there no value for this input waiting in the stack frame?
if (typeof currentStackFrame.reported[inputName] === 'undefined'
&& inputBlockId) {
// If there's not, we need to evaluate the block.
// Push to the stack to evaluate the reporter block.
thread.pushStack(inputBlockId);
// Save name of input for `Thread.pushReportedValue`.
currentStackFrame.waitingReporter = inputName;
// Actually execute the block.
execute(sequencer, thread);
if (thread.status === Thread.STATUS_PROMISE_WAIT) {
return;
} else {
// Execution returned immediately,
// and presumably a value was reported, so pop the stack.
currentStackFrame.waitingReporter = null;
thread.popStack();
}
}
argValues[inputName] = currentStackFrame.reported[inputName];
}
// Add any mutation to args (e.g., for procedures).
var mutation = blockContainer.getMutation(currentBlockId);
if (mutation) {
argValues.mutation = mutation;
}
// If we've gotten this far, all of the input blocks are evaluated,
// and `argValues` is fully populated. So, execute the block primitive.
// First, clear `currentStackFrame.reported`, so any subsequent execution
// (e.g., on return from a branch) gets fresh inputs.
currentStackFrame.reported = {};
var primitiveReportedValue = null;
primitiveReportedValue = blockFunction(argValues, {
stackFrame: currentStackFrame.executionContext,
target: target,
yield: function() {
thread.status = Thread.STATUS_YIELD;
},
startBranch: function (branchNum, isLoop) {
sequencer.stepToBranch(thread, branchNum, isLoop);
},
stopAll: function () {
runtime.stopAll();
},
stopOtherTargetThreads: function() {
runtime.stopForTarget(target, thread);
},
stopThread: function() {
sequencer.retireThread(thread);
},
startProcedure: function (procedureCode) {
sequencer.stepToProcedure(thread, procedureCode);
},
getProcedureParamNames: function (procedureCode) {
return blockContainer.getProcedureParamNames(procedureCode);
},
pushParam: function (paramName, paramValue) {
thread.pushParam(paramName, paramValue);
},
getParam: function (paramName) {
return thread.getParam(paramName);
},
startHats: function(requestedHat, opt_matchFields, opt_target) {
return (
runtime.startHats(requestedHat, opt_matchFields, opt_target)
);
},
ioQuery: function (device, func, args) {
// Find the I/O device and execute the query/function call.
if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
var devObject = runtime.ioDevices[device];
return devObject[func].call(devObject, args);
}
}
});
if (typeof primitiveReportedValue === 'undefined') {
// No value reported - potentially a command block.
// Edge-activated hats don't request a glow; all commands do.
thread.requestScriptGlowInFrame = true;
}
// If it's a promise, wait until promise resolves.
if (isPromise(primitiveReportedValue)) {
if (thread.status === Thread.STATUS_RUNNING) {
// Primitive returned a promise; automatically yield thread.
thread.status = Thread.STATUS_PROMISE_WAIT;
}
// Promise handlers
primitiveReportedValue.then(function(resolvedValue) {
handleReport(resolvedValue);
if (typeof resolvedValue !== 'undefined') {
thread.popStack();
} else {
var popped = thread.popStack();
var nextBlockId = thread.target.blocks.getNextBlock(popped);
thread.pushStack(nextBlockId);
}
}, function(rejectionReason) {
// Promise rejected: the primitive had some error.
// Log it and proceed.
console.warn('Primitive rejected promise: ', rejectionReason);
thread.status = Thread.STATUS_RUNNING;
thread.popStack();
});
} else if (thread.status === Thread.STATUS_RUNNING) {
handleReport(primitiveReportedValue);
}
};
module.exports = execute;

16
src/engine/list.js Normal file
View file

@ -0,0 +1,16 @@
/**
* @fileoverview
* Object representing a Scratch list.
*/
/**
* @param {!string} name Name of the list.
* @param {Array} contents Contents of the list, as an array.
* @constructor
*/
function List (name, contents) {
this.name = name;
this.contents = contents;
}
module.exports = List;

View file

@ -0,0 +1,39 @@
var html = require('htmlparser2');
/**
* Adapter between mutator XML or DOM and block representation which can be
* used by the Scratch runtime.
* @param {(Object|string)} mutation Mutation XML string or DOM.
* @return {Object} Object representing the mutation.
*/
module.exports = function (mutation) {
var mutationParsed;
// Check if the mutation is already parsed; if not, parse it.
if (typeof mutation === 'object') {
mutationParsed = mutation;
} else {
mutationParsed = html.parseDOM(mutation)[0];
}
return mutatorTagToObject(mutationParsed);
};
/**
* Convert a part of a mutation DOM to a mutation VM object, recursively.
* @param {Object} dom DOM object for mutation tag.
* @return {Object} Object representing useful parts of this mutation.
*/
function mutatorTagToObject (dom) {
var obj = Object.create(null);
obj.tagName = dom.name;
obj.children = [];
for (var prop in dom.attribs) {
if (prop == 'xmlns') continue;
obj[prop] = dom.attribs[prop];
}
for (var i = 0; i < dom.children.length; i++) {
obj.children.push(
mutatorTagToObject(dom.children[i])
);
}
return obj;
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
var Timer = require('../util/timer'); var Timer = require('../util/timer');
var Thread = require('./thread'); var Thread = require('./thread');
var YieldTimers = require('../util/yieldtimers.js'); var execute = require('./execute.js');
function Sequencer (runtime) { function Sequencer (runtime) {
/** /**
@ -17,225 +17,238 @@ function Sequencer (runtime) {
} }
/** /**
* The sequencer does as much work as it can within WORK_TIME milliseconds, * Time to run a warp-mode thread, in ms.
* then yields. This is essentially a rate-limiter for blocks. * @type {number}
* In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
* @const {!number}
*/ */
Sequencer.WORK_TIME = 10; Sequencer.WARP_TIME = 500;
/** /**
* Step through all threads in `this.threads`, running them in order. * Step through all threads in `this.runtime.threads`, running them in order.
* @return {Array.<Thread>} All threads which have finished in this iteration. * @return {Array.<!Thread>} List of inactive threads after stepping.
*/ */
Sequencer.prototype.stepThreads = function (threads) { Sequencer.prototype.stepThreads = function () {
// Start counting toward WORK_TIME // Work time is 75% of the thread stepping interval.
var WORK_TIME = 0.75 * this.runtime.currentStepTime;
// Start counting toward WORK_TIME.
this.timer.start(); this.timer.start();
// List of threads which have been killed by this step. // Count of active threads.
var numActiveThreads = Infinity;
// Whether `stepThreads` has run through a full single tick.
var ranFirstTick = false;
var inactiveThreads = []; var inactiveThreads = [];
// If all of the threads are yielding, we should yield. // Conditions for continuing to stepping threads:
var numYieldingThreads = 0; // 1. We must have threads in the list, and some must be active.
// While there are still threads to run and we are within WORK_TIME, // 2. Time elapsed must be less than WORK_TIME.
// continue executing threads. // 3. Either turbo mode, or no redraw has been requested by a primitive.
while (threads.length > 0 && while (this.runtime.threads.length > 0 &&
threads.length > numYieldingThreads && numActiveThreads > 0 &&
this.timer.timeElapsed() < Sequencer.WORK_TIME) { this.timer.timeElapsed() < WORK_TIME &&
// New threads at the end of the iteration. (this.runtime.turboMode || !this.runtime.redrawRequested)) {
var newThreads = []; numActiveThreads = 0;
// Attempt to run each thread one time // Inline copy of the threads, updated on each step.
for (var i = 0; i < threads.length; i++) { var threadsCopy = this.runtime.threads.slice();
var activeThread = threads[i]; // Attempt to run each thread one time.
if (activeThread.status === Thread.STATUS_RUNNING) { for (var i = 0; i < threadsCopy.length; i++) {
var activeThread = threadsCopy[i];
if (activeThread.stack.length === 0 ||
activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread.
if (inactiveThreads.indexOf(activeThread) < 0) {
inactiveThreads.push(activeThread);
}
continue;
}
if (activeThread.status === Thread.STATUS_YIELD_TICK &&
!ranFirstTick) {
// Clear single-tick yield from the last call of `stepThreads`.
activeThread.status = Thread.STATUS_RUNNING;
}
if (activeThread.status === Thread.STATUS_RUNNING ||
activeThread.status === Thread.STATUS_YIELD) {
// Normal-mode thread: step. // Normal-mode thread: step.
this.stepThread(activeThread); this.stepThread(activeThread);
} else if (activeThread.status === Thread.STATUS_YIELD) { activeThread.warpTimer = null;
// Yield-mode thread: check if the time has passed.
YieldTimers.resolve(activeThread.yieldTimerId);
numYieldingThreads++;
} else if (activeThread.status === Thread.STATUS_DONE) {
// Moved to a done state - finish up
activeThread.status = Thread.STATUS_RUNNING;
// @todo Deal with the return value
} }
// First attempt to pop from the stack if (activeThread.status === Thread.STATUS_RUNNING) {
if (activeThread.stack.length > 0 && // After stepping, status is still running.
activeThread.nextBlock === null && // If we're in single-stepping mode, mark the thread as
activeThread.status === Thread.STATUS_DONE) { // a single-tick yield so it doesn't re-execute
activeThread.nextBlock = activeThread.stack.pop(); // until the next frame.
// Don't pop stack frame - we need the data. if (this.runtime.singleStepping) {
// A new one won't be created when we execute. activeThread.status = Thread.STATUS_YIELD_TICK;
if (activeThread.nextBlock !== null) { }
activeThread.status === Thread.STATUS_RUNNING; numActiveThreads++;
} }
} }
if (activeThread.nextBlock === null && // We successfully ticked once. Prevents running STATUS_YIELD_TICK
activeThread.status === Thread.STATUS_DONE) { // threads on the next tick.
// Finished with this thread - tell runtime to clean it up. ranFirstTick = true;
inactiveThreads.push(activeThread);
} else {
// Keep this thead in the loop.
newThreads.push(activeThread);
} }
// Filter inactive threads from `this.runtime.threads`.
this.runtime.threads = this.runtime.threads.filter(function(thread) {
if (inactiveThreads.indexOf(thread) > -1) {
return false;
} }
// Effectively filters out threads that have stopped. return true;
threads = newThreads; });
}
return inactiveThreads; return inactiveThreads;
}; };
/** /**
* Step the requested thread * Step the requested thread for as long as necessary.
* @param {!Thread} thread Thread object to step * @param {!Thread} thread Thread object to step.
*/ */
Sequencer.prototype.stepThread = function (thread) { Sequencer.prototype.stepThread = function (thread) {
// Save the yield timer ID, in case a primitive makes a new one var currentBlockId = thread.peekStack();
// @todo hack - perhaps patch this to allow more than one timer per if (!currentBlockId) {
// primitive, for example... // A "null block" - empty branch.
var oldYieldTimerId = YieldTimers.timerId; thread.popStack();
}
// Save the current block and set the nextBlock. while (thread.peekStack()) {
// If the primitive would like to do control flow, var isWarpMode = thread.peekStackFrame().warpMode;
// it can overwrite nextBlock. if (isWarpMode && !thread.warpTimer) {
var currentBlock = thread.nextBlock; // Initialize warp-mode timer if it hasn't been already.
if (!currentBlock || !this.runtime.blocks[currentBlock]) { // This will start counting the thread toward `Sequencer.WARP_TIME`.
thread.warpTimer = new Timer();
thread.warpTimer.start();
}
// Execute the current block.
// Save the current block ID to notice if we did control flow.
currentBlockId = thread.peekStack();
execute(this, thread);
thread.blockGlowInFrame = currentBlockId;
// If the thread has yielded or is waiting, yield to other threads.
if (thread.status === Thread.STATUS_YIELD) {
// Mark as running for next iteration.
thread.status = Thread.STATUS_RUNNING;
// In warp mode, yielded blocks are re-executed immediately.
if (isWarpMode &&
thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) {
continue;
}
return;
} else if (thread.status === Thread.STATUS_PROMISE_WAIT) {
// A promise was returned by the primitive. Yield the thread
// until the promise resolves. Promise resolution should reset
// thread.status to Thread.STATUS_RUNNING.
return;
}
// If no control flow has happened, switch to next block.
if (thread.peekStack() === currentBlockId) {
thread.goToNextBlock();
}
// If no next block has been found at this point, look on the stack.
while (!thread.peekStack()) {
thread.popStack();
if (thread.stack.length === 0) {
// No more stack to run!
thread.status = Thread.STATUS_DONE; thread.status = Thread.STATUS_DONE;
return; return;
} }
thread.nextBlock = this.runtime._getNextBlock(currentBlock); if (thread.peekStackFrame().isLoop) {
// The current level of the stack is marked as a loop.
var opcode = this.runtime._getOpcode(currentBlock); // Return to yield for the frame/tick in general.
// Unless we're in warp mode - then only return if the
// Push the current block to the stack // warp timer is up.
thread.stack.push(currentBlock); if (!isWarpMode ||
// Push an empty stack frame, if we need one. thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
// Might not, if we just popped the stack. // Don't do anything to the stack, since loops need
if (thread.stack.length > thread.stackFrames.length) { // to be re-executed.
thread.stackFrames.push({}); return;
}
var currentStackFrame = thread.stackFrames[thread.stackFrames.length - 1];
/**
* A callback for the primitive to indicate its thread should yield.
* @type {Function}
*/
var threadYieldCallback = function () {
thread.status = Thread.STATUS_YIELD;
};
/**
* A callback for the primitive to indicate its thread is finished
* @type {Function}
*/
var instance = this;
var threadDoneCallback = function () {
thread.status = Thread.STATUS_DONE;
// Refresh nextBlock in case it has changed during a yield.
thread.nextBlock = instance.runtime._getNextBlock(currentBlock);
// Pop the stack and stack frame
thread.stack.pop();
thread.stackFrames.pop();
};
/**
* A callback for the primitive to start hats.
* @todo very hacked...
*/
var startHats = function(callback) {
for (var i = 0; i < instance.runtime.stacks.length; i++) {
var stack = instance.runtime.stacks[i];
var stackBlock = instance.runtime.blocks[stack];
var result = callback(stackBlock);
if (result) {
// Check if the stack is already running
var stackRunning = false;
for (var j = 0; j < instance.runtime.threads.length; j++) {
if (instance.runtime.threads[j].topBlock == stack) {
stackRunning = true;
break;
}
}
if (!stackRunning) {
instance.runtime._pushThread(stack);
}
}
}
};
/**
* Record whether we have switched stack,
* to avoid proceeding the thread automatically.
* @type {boolean}
*/
var switchedStack = false;
/**
* A callback for a primitive to start a substack.
* @type {Function}
*/
var threadStartSubstack = function () {
// Set nextBlock to the start of the substack
var substack = instance.runtime._getSubstack(currentBlock);
if (substack && substack.value) {
thread.nextBlock = substack.value;
} else { } else {
thread.nextBlock = null; // Don't go to the next block for this level of the stack,
// since loops need to be re-executed.
continue;
} }
switchedStack = true; } else if (thread.peekStackFrame().waitingReporter) {
}; // This level of the stack was waiting for a value.
// This means a reporter has just returned - so don't go
// to the next block for this level of the stack.
return;
}
// Get next block of existing block on the stack.
thread.goToNextBlock();
}
// In single-stepping mode, force `stepThread` to only run one block
// at a time.
if (this.runtime.singleStepping) {
return;
}
}
};
// @todo extreme hack to get the single argument value for prototype /**
var argValues = []; * Step a thread into a block's branch.
var blockInputs = this.runtime.blocks[currentBlock].fields; * @param {!Thread} thread Thread object to step to branch.
for (var bi in blockInputs) { * @param {Number} branchNum Which branch to step to (i.e., 1, 2).
var outer = blockInputs[bi]; * @param {Boolean} isLoop Whether this block is a loop.
for (var b in outer.blocks) { */
var block = outer.blocks[b]; Sequencer.prototype.stepToBranch = function (thread, branchNum, isLoop) {
var fields = block.fields; if (!branchNum) {
for (var f in fields) { branchNum = 1;
var field = fields[f];
argValues.push(field.value);
}
} }
var currentBlockId = thread.peekStack();
var branchId = thread.target.blocks.getBranch(
currentBlockId,
branchNum
);
thread.peekStackFrame().isLoop = isLoop;
if (branchId) {
// Push branch ID to the thread's stack.
thread.pushStack(branchId);
} else {
thread.pushStack(null);
} }
};
if (!opcode) { /**
console.warn('Could not get opcode for block: ' + currentBlock); * Step a procedure.
} * @param {!Thread} thread Thread object to step to procedure.
else { * @param {!string} procedureCode Procedure code of procedure to step to.
var blockFunction = this.runtime.getOpcodeFunction(opcode); */
if (!blockFunction) { Sequencer.prototype.stepToProcedure = function (thread, procedureCode) {
console.warn('Could not get implementation for opcode: ' + opcode); var definition = thread.target.blocks.getProcedureDefinition(procedureCode);
} if (!definition) {
else { return;
try {
// @todo deal with the return value
blockFunction(argValues, {
yield: threadYieldCallback,
done: threadDoneCallback,
timeout: YieldTimers.timeout,
stackFrame: currentStackFrame,
startSubstack: threadStartSubstack,
startHats: startHats
});
}
catch(e) {
console.error(
'Exception calling block function for opcode: ' +
opcode + '\n' + e);
} finally {
// Update if the thread has set a yield timer ID
// @todo hack
if (YieldTimers.timerId > oldYieldTimerId) {
thread.yieldTimerId = YieldTimers.timerId;
}
if (thread.status === Thread.STATUS_RUNNING && !switchedStack) {
// Thread executed without yielding - move to done
threadDoneCallback();
} }
// Check if the call is recursive.
// If so, set the thread to yield after pushing.
var isRecursive = thread.isRecursiveCall(procedureCode);
// To step to a procedure, we put its definition on the stack.
// Execution for the thread will proceed through the definition hat
// and on to the main definition of the procedure.
// When that set of blocks finishes executing, it will be popped
// from the stack by the sequencer, returning control to the caller.
thread.pushStack(definition);
// In known warp-mode threads, only yield when time is up.
if (thread.peekStackFrame().warpMode &&
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
thread.status = Thread.STATUS_YIELD;
} else {
// Look for warp-mode flag on definition, and set the thread
// to warp-mode if needed.
var definitionBlock = thread.target.blocks.getBlock(definition);
var doWarp = definitionBlock.mutation.warp;
if (doWarp) {
thread.peekStackFrame().warpMode = true;
} else {
// In normal-mode threads, yield any time we have a recursive call.
if (isRecursive) {
thread.status = Thread.STATUS_YIELD;
} }
} }
} }
};
/**
* Retire a thread in the middle, without considering further blocks.
* @param {!Thread} thread Thread object to retire.
*/
Sequencer.prototype.retireThread = function (thread) {
thread.stack = [];
thread.stackFrame = [];
thread.requestScriptGlowInFrame = false;
thread.status = Thread.STATUS_DONE;
}; };
module.exports = Sequencer; module.exports = Sequencer;

116
src/engine/target.js Normal file
View file

@ -0,0 +1,116 @@
var Blocks = require('./blocks');
var Variable = require('../engine/variable');
var List = require('../engine/list');
var uid = require('../util/uid');
/**
* @fileoverview
* A Target is an abstract "code-running" object for the Scratch VM.
* Examples include sprites/clones or potentially physical-world devices.
*/
/**
* @param {?Blocks} blocks Blocks instance for the blocks owned by this target.
* @constructor
*/
function Target (blocks) {
if (!blocks) {
blocks = new Blocks(this);
}
/**
* A unique ID for this target.
* @type {string}
*/
this.id = uid();
/**
* Blocks run as code for this target.
* @type {!Blocks}
*/
this.blocks = blocks;
/**
* Dictionary of variables and their values for this target.
* Key is the variable name.
* @type {Object.<string,*>}
*/
this.variables = {};
/**
* Dictionary of lists and their contents for this target.
* Key is the list name.
* @type {Object.<string,*>}
*/
this.lists = {};
}
/**
* Called when the project receives a "green flag."
* @abstract
*/
Target.prototype.onGreenFlag = function () {};
/**
* Return a human-readable name for this target.
* Target implementations should override this.
* @abstract
* @returns {string} Human-readable name for the target.
*/
Target.prototype.getName = function () {
return this.id;
};
/**
* Look up a variable object, and create it if one doesn't exist.
* Search begins for local variables; then look for globals.
* @param {!string} name Name of the variable.
* @return {!Variable} Variable object.
*/
Target.prototype.lookupOrCreateVariable = function (name) {
// If we have a local copy, return it.
if (this.variables.hasOwnProperty(name)) {
return this.variables[name];
}
// If the stage has a global copy, return it.
if (this.runtime && !this.isStage) {
var stage = this.runtime.getTargetForStage();
if (stage.variables.hasOwnProperty(name)) {
return stage.variables[name];
}
}
// No variable with this name exists - create it locally.
var newVariable = new Variable(name, 0, false);
this.variables[name] = newVariable;
return newVariable;
};
/**
* Look up a list object for this target, and create it if one doesn't exist.
* Search begins for local lists; then look for globals.
* @param {!string} name Name of the list.
* @return {!List} List object.
*/
Target.prototype.lookupOrCreateList = function (name) {
// If we have a local copy, return it.
if (this.lists.hasOwnProperty(name)) {
return this.lists[name];
}
// If the stage has a global copy, return it.
if (this.runtime && !this.isStage) {
var stage = this.runtime.getTargetForStage();
if (stage.lists.hasOwnProperty(name)) {
return stage.lists[name];
}
}
// No list with this name exists - create it locally.
var newList = new List(name, []);
this.lists[name] = newList;
return newList;
};
/**
* Call to destroy a target.
* @abstract
*/
Target.prototype.dispose = function () {
};
module.exports = Target;

View file

@ -9,11 +9,7 @@ function Thread (firstBlock) {
* @type {!string} * @type {!string}
*/ */
this.topBlock = firstBlock; this.topBlock = firstBlock;
/**
* ID of next block that the thread will execute, or null if none.
* @type {?string}
*/
this.nextBlock = firstBlock;
/** /**
* Stack for the thread. When the sequencer enters a control structure, * Stack for the thread. When the sequencer enters a control structure,
* the block is pushed onto the stack so we know where to exit. * the block is pushed onto the stack so we know where to exit.
@ -34,32 +30,208 @@ function Thread (firstBlock) {
this.status = 0; /* Thread.STATUS_RUNNING */ this.status = 0; /* Thread.STATUS_RUNNING */
/** /**
* Yield timer ID (for checking when the thread should unyield). * Target of this thread.
* @type {number} * @type {?Target}
*/ */
this.yieldTimerId = -1; this.target = null;
/**
* Whether the thread requests its script to glow during this frame.
* @type {boolean}
*/
this.requestScriptGlowInFrame = false;
/**
* Which block ID should glow during this frame, if any.
* @type {?string}
*/
this.blockGlowInFrame = null;
/**
* A timer for when the thread enters warp mode.
* Substitutes the sequencer's count toward WORK_TIME on a per-thread basis.
* @type {?Timer}
*/
this.warpTimer = null;
} }
/** /**
* Thread status for initialized or running thread. * Thread status for initialized or running thread.
* Threads are in this state when the primitive is called for the first time. * This is the default state for a thread - execution should run normally,
* stepping from block to block.
* @const * @const
*/ */
Thread.STATUS_RUNNING = 0; Thread.STATUS_RUNNING = 0;
/** /**
* Thread status for a yielded thread. * Threads are in this state when a primitive is waiting on a promise;
* Threads are in this state when a primitive has yielded. * execution is paused until the promise changes thread status.
* @const * @const
*/ */
Thread.STATUS_YIELD = 1; Thread.STATUS_PROMISE_WAIT = 1;
/**
* Thread status for yield.
* @const
*/
Thread.STATUS_YIELD = 2;
/**
* Thread status for a single-tick yield. This will be cleared when the
* thread is resumed.
* @const
*/
Thread.STATUS_YIELD_TICK = 3;
/** /**
* Thread status for a finished/done thread. * Thread status for a finished/done thread.
* Thread is moved to this state when the interpreter * Thread is in this state when there are no more blocks to execute.
* can proceed with execution.
* @const * @const
*/ */
Thread.STATUS_DONE = 2; Thread.STATUS_DONE = 4;
/**
* Push stack and update stack frames appropriately.
* @param {string} blockId Block ID to push to stack.
*/
Thread.prototype.pushStack = function (blockId) {
this.stack.push(blockId);
// Push an empty stack frame, if we need one.
// Might not, if we just popped the stack.
if (this.stack.length > this.stackFrames.length) {
// Copy warp mode from any higher level.
var warpMode = false;
if (this.stackFrames[this.stackFrames.length - 1]) {
warpMode = this.stackFrames[this.stackFrames.length - 1].warpMode;
}
this.stackFrames.push({
isLoop: false, // Whether this level of the stack is a loop.
warpMode: warpMode, // Whether this level is in warp mode.
reported: {}, // Collects reported input values.
waitingReporter: null, // Name of waiting reporter.
params: {}, // Procedure parameters.
executionContext: {} // A context passed to block implementations.
});
}
};
/**
* Pop last block on the stack and its stack frame.
* @return {string} Block ID popped from the stack.
*/
Thread.prototype.popStack = function () {
this.stackFrames.pop();
return this.stack.pop();
};
/**
* Get top stack item.
* @return {?string} Block ID on top of stack.
*/
Thread.prototype.peekStack = function () {
return this.stack[this.stack.length - 1];
};
/**
* Get top stack frame.
* @return {?Object} Last stack frame stored on this thread.
*/
Thread.prototype.peekStackFrame = function () {
return this.stackFrames[this.stackFrames.length - 1];
};
/**
* Get stack frame above the current top.
* @return {?Object} Second to last stack frame stored on this thread.
*/
Thread.prototype.peekParentStackFrame = function () {
return this.stackFrames[this.stackFrames.length - 2];
};
/**
* Push a reported value to the parent of the current stack frame.
* @param {*} value Reported value to push.
*/
Thread.prototype.pushReportedValue = function (value) {
var parentStackFrame = this.peekParentStackFrame();
if (parentStackFrame) {
var waitingReporter = parentStackFrame.waitingReporter;
parentStackFrame.reported[waitingReporter] = value;
}
};
/**
* Add a parameter to the stack frame.
* Use when calling a procedure with parameter values.
* @param {!string} paramName Name of parameter.
* @param {*} value Value to set for parameter.
*/
Thread.prototype.pushParam = function (paramName, value) {
var stackFrame = this.peekStackFrame();
stackFrame.params[paramName] = value;
};
/**
* Get a parameter at the lowest possible level of the stack.
* @param {!string} paramName Name of parameter.
* @return {*} value Value for parameter.
*/
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.
*/
Thread.prototype.atStackTop = function () {
return this.peekStack() === this.topBlock;
};
/**
* Switch the thread to the next block at the current level of the stack.
* For example, this is used in a standard sequence of blocks,
* where execution proceeds from one block to the next.
*/
Thread.prototype.goToNextBlock = function () {
var nextBlockId = this.target.blocks.getNextBlock(this.peekStack());
// Copy warp mode to next block.
var warpMode = this.peekStackFrame().warpMode;
// The current block is on the stack - pop it and push the next.
// Note that this could push `null` - that is handled by the sequencer.
this.popStack();
this.pushStack(nextBlockId);
if (this.peekStackFrame()) {
this.peekStackFrame().warpMode = warpMode;
}
};
/**
* Attempt to determine whether a procedure call is recursive,
* by examining the stack.
* @param {!string} procedureCode Procedure code of procedure being called.
* @return {boolean} True if the call appears recursive.
*/
Thread.prototype.isRecursiveCall = function (procedureCode) {
var callCount = 5; // Max number of enclosing procedure calls to examine.
var sp = this.stack.length - 1;
for (var i = sp - 1; i >= 0; i--) {
var block = this.target.blocks.getBlock(this.stack[i]);
if (block.opcode == 'procedures_callnoreturn' &&
block.mutation.proccode == procedureCode) {
return true;
}
if (--callCount < 0) return false;
}
return false;
};
module.exports = Thread; module.exports = Thread;

18
src/engine/variable.js Normal file
View file

@ -0,0 +1,18 @@
/**
* @fileoverview
* Object representing a Scratch variable.
*/
/**
* @param {!string} name Name of the variable.
* @param {(string|Number)} value Value of the variable.
* @param {boolean} isCloud Whether the variable is stored in the cloud.
* @constructor
*/
function Variable (name, value, isCloud) {
this.name = name;
this.value = value;
this.isCloud = isCloud;
}
module.exports = Variable;

418
src/import/sb2import.js Normal file
View file

@ -0,0 +1,418 @@
/**
* @fileoverview
* Partial implementation of an SB2 JSON importer.
* Parses provided JSON and then generates all needed
* scratch-vm runtime structures.
*/
var Blocks = require('../engine/blocks');
var Clone = require('../sprites/clone');
var Sprite = require('../sprites/sprite');
var Color = require('../util/color.js');
var uid = require('../util/uid');
var specMap = require('./sb2specmap');
var Variable = require('../engine/variable');
var List = require('../engine/list');
/**
* Top-level handler. Parse provided JSON,
* and process the top-level object (the stage object).
* @param {!string} json SB2-format JSON to load.
* @param {!Runtime} runtime Runtime object to load all structures into.
*/
function sb2import (json, runtime) {
parseScratchObject(
JSON.parse(json),
runtime,
true
);
}
/**
* Parse a single "Scratch object" and create all its in-memory VM objects.
* @param {!Object} object From-JSON "Scratch object:" sprite, stage, watcher.
* @param {!Runtime} runtime Runtime object to load all structures into.
* @param {boolean} topLevel Whether this is the top-level object (stage).
*/
function parseScratchObject (object, runtime, topLevel) {
if (!object.hasOwnProperty('objName')) {
// Watcher/monitor - skip this object until those are implemented in VM.
// @todo
return;
}
// Blocks container for this object.
var blocks = new Blocks();
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
var sprite = new Sprite(blocks, runtime);
// Sprite/stage name from JSON.
if (object.hasOwnProperty('objName')) {
sprite.name = object.objName;
}
// Costumes from JSON.
if (object.hasOwnProperty('costumes')) {
for (var i = 0; i < object.costumes.length; i++) {
var costume = object.costumes[i];
// @todo: Make sure all the relevant metadata is being pulled out.
sprite.costumes.push({
skin: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/'
+ costume.baseLayerMD5 + '/get/',
name: costume.costumeName,
bitmapResolution: costume.bitmapResolution,
rotationCenterX: costume.rotationCenterX,
rotationCenterY: costume.rotationCenterY
});
}
}
// If included, parse any and all scripts/blocks on the object.
if (object.hasOwnProperty('scripts')) {
parseScripts(object.scripts, blocks);
}
// Create the first clone, and load its run-state from JSON.
var target = sprite.createClone();
// Add it to the runtime's list of targets.
runtime.targets.push(target);
// Load target properties from JSON.
if (object.hasOwnProperty('variables')) {
for (var j = 0; j < object.variables.length; j++) {
var variable = object.variables[j];
target.variables[variable.name] = new Variable(
variable.name,
variable.value,
variable.isPersistent
);
}
}
if (object.hasOwnProperty('lists')) {
for (var k = 0; k < object.lists.length; k++) {
var list = object.lists[k];
// @todo: monitor properties.
target.lists[list.listName] = new List(
list.listName,
list.contents
);
}
}
if (object.hasOwnProperty('scratchX')) {
target.x = object.scratchX;
}
if (object.hasOwnProperty('scratchY')) {
target.y = object.scratchY;
}
if (object.hasOwnProperty('direction')) {
target.direction = object.direction;
}
if (object.hasOwnProperty('scale')) {
// SB2 stores as 1.0 = 100%; we use % in the VM.
target.size = object.scale * 100;
}
if (object.hasOwnProperty('visible')) {
target.visible = object.visible;
}
if (object.hasOwnProperty('currentCostumeIndex')) {
target.currentCostume = Math.round(object.currentCostumeIndex);
}
if (object.hasOwnProperty('rotationStyle')) {
if (object.rotationStyle == 'none') {
target.rotationStyle = Clone.ROTATION_STYLE_NONE;
} else if (object.rotationStyle == 'leftRight') {
target.rotationStyle = Clone.ROTATION_STYLE_LEFT_RIGHT;
} else if (object.rotationStyle == 'normal') {
target.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
}
}
target.isStage = topLevel;
target.updateAllDrawableProperties();
// The stage will have child objects; recursively process them.
if (object.children) {
for (var m = 0; m < object.children.length; m++) {
parseScratchObject(object.children[m], runtime, false);
}
}
}
/**
* Parse a Scratch object's scripts into VM blocks.
* This should only handle top-level scripts that include X, Y coordinates.
* @param {!Object} scripts Scripts object from SB2 JSON.
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
*/
function parseScripts (scripts, blocks) {
for (var i = 0; i < scripts.length; i++) {
var script = scripts[i];
var scriptX = script[0];
var scriptY = script[1];
var blockList = script[2];
var parsedBlockList = parseBlockList(blockList);
if (parsedBlockList[0]) {
// Adjust script coordinates to account for
// larger block size in scratch-blocks.
// @todo: Determine more precisely the right formulas here.
parsedBlockList[0].x = scriptX * 1.1;
parsedBlockList[0].y = scriptY * 1.1;
parsedBlockList[0].topLevel = true;
parsedBlockList[0].parent = null;
}
// Flatten children and create add the blocks.
var convertedBlocks = flatten(parsedBlockList);
for (var j = 0; j < convertedBlocks.length; j++) {
blocks.createBlock(convertedBlocks[j]);
}
}
}
/**
* Parse any list of blocks from SB2 JSON into a list of VM-format blocks.
* Could be used to parse a top-level script,
* a list of blocks in a branch (e.g., in forever),
* or a list of blocks in an argument (e.g., move [pick random...]).
* @param {Array.<Object>} blockList SB2 JSON-format block list.
* @return {Array.<Object>} Scratch VM-format block list.
*/
function parseBlockList (blockList) {
var resultingList = [];
var previousBlock = null; // For setting next.
for (var i = 0; i < blockList.length; i++) {
var block = blockList[i];
var parsedBlock = parseBlock(block);
if (previousBlock) {
parsedBlock.parent = previousBlock.id;
previousBlock.next = parsedBlock.id;
}
previousBlock = parsedBlock;
resultingList.push(parsedBlock);
}
return resultingList;
}
/**
* Flatten a block tree into a block list.
* Children are temporarily stored on the `block.children` property.
* @param {Array.<Object>} blocks list generated by `parseBlockList`.
* @return {Array.<Object>} Flattened list to be passed to `blocks.createBlock`.
*/
function flatten (blocks) {
var finalBlocks = [];
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
finalBlocks.push(block);
if (block.children) {
finalBlocks = finalBlocks.concat(flatten(block.children));
}
delete block.children;
}
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.
* @return {Object} Scratch VM format block.
*/
function parseBlock (sb2block) {
// First item in block object is the old opcode (e.g., 'forward:').
var oldOpcode = sb2block[0];
// Convert the block using the specMap. See sb2specmap.js.
if (!oldOpcode || !specMap[oldOpcode]) {
console.warn('Couldn\'t find SB2 block: ', oldOpcode);
return;
}
var blockMetadata = specMap[oldOpcode];
// Block skeleton.
var activeBlock = {
id: uid(), // Generate a new block unique ID.
opcode: blockMetadata.opcode, // Converted, e.g. "motion_movesteps".
inputs: {}, // Inputs to this block and the blocks they point to.
fields: {}, // Fields on this block and their values.
next: null, // Next block.
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.
for (var i = 0; i < blockMetadata.argMap.length; i++) {
var expectedArg = blockMetadata.argMap[i];
var providedArg = sb2block[i + 1]; // (i = 0 is opcode)
// Whether the input is obscuring a shadow.
var shadowObscured = false;
// Positional argument is an input.
if (expectedArg.type == 'input') {
// Create a new block and input metadata.
var inputUid = uid();
activeBlock.inputs[expectedArg.inputName] = {
name: expectedArg.inputName,
block: null,
shadow: null
};
if (typeof providedArg == 'object' && providedArg) {
// Block or block list occupies the input.
var innerBlocks;
if (typeof providedArg[0] == 'object' && providedArg[0]) {
// Block list occupies the input.
innerBlocks = parseBlockList(providedArg);
} else {
// Single block occupies the input.
innerBlocks = [parseBlock(providedArg)];
}
var previousBlock = null;
for (var j = 0; j < innerBlocks.length; j++) {
if (j == 0) {
innerBlocks[j].parent = activeBlock.id;
} else {
innerBlocks[j].parent = previousBlock;
}
previousBlock = innerBlocks[j].id;
}
// Obscures any shadow.
shadowObscured = true;
activeBlock.inputs[expectedArg.inputName].block = (
innerBlocks[0].id
);
activeBlock.children = (
activeBlock.children.concat(innerBlocks)
);
}
// Generate a shadow block to occupy the input.
if (!expectedArg.inputOp) {
// No editable shadow input; e.g., for a boolean.
continue;
}
// Each shadow has a field generated for it automatically.
// Value to be filled in the field.
var fieldValue = providedArg;
// Shadows' field names match the input name, except for these:
var fieldName = expectedArg.inputName;
if (expectedArg.inputOp == 'math_number' ||
expectedArg.inputOp == 'math_whole_number' ||
expectedArg.inputOp == 'math_positive_number' ||
expectedArg.inputOp == 'math_integer' ||
expectedArg.inputOp == 'math_angle') {
fieldName = 'NUM';
// Fields are given Scratch 2.0 default values if obscured.
if (shadowObscured) {
fieldValue = 10;
}
} else if (expectedArg.inputOp == 'text') {
fieldName = 'TEXT';
if (shadowObscured) {
fieldValue = '';
}
} else if (expectedArg.inputOp == 'colour_picker') {
// Convert SB2 color to hex.
fieldValue = Color.decimalToHex(providedArg);
fieldName = 'COLOUR';
if (shadowObscured) {
fieldValue = '#990000';
}
}
var fields = {};
fields[fieldName] = {
name: fieldName,
value: fieldValue
};
activeBlock.children.push({
id: inputUid,
opcode: expectedArg.inputOp,
inputs: {},
fields: fields,
next: null,
topLevel: false,
parent: activeBlock.id,
shadow: true
});
activeBlock.inputs[expectedArg.inputName].shadow = inputUid;
// If no block occupying the input, alias to the shadow.
if (!activeBlock.inputs[expectedArg.inputName].block) {
activeBlock.inputs[expectedArg.inputName].block = inputUid;
}
} else if (expectedArg.type == 'field') {
// Add as a field on this block.
activeBlock.fields[expectedArg.fieldName] = {
name: expectedArg.fieldName,
value: providedArg
};
}
}
// Special cases to generate mutations.
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' ||
sb2block[1] == 'other scripts in stage') {
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: [],
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;
}
module.exports = sb2import;

1388
src/import/sb2specmap.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,9 @@ var EventEmitter = require('events');
var util = require('util'); var util = require('util');
var Runtime = require('./engine/runtime'); var Runtime = require('./engine/runtime');
var adapter = require('./engine/adapter'); var sb2import = require('./import/sb2import');
var Sprite = require('./sprites/sprite');
var Blocks = require('./engine/blocks');
/** /**
* Handles connections between blocks, stage, and extensions. * Handles connections between blocks, stage, and extensions.
@ -11,73 +13,38 @@ var adapter = require('./engine/adapter');
*/ */
function VirtualMachine () { function VirtualMachine () {
var instance = this; var instance = this;
// Bind event emitter and runtime to VM instance // Bind event emitter and runtime to VM instance
// @todo Post message (Web Worker) polyfill
EventEmitter.call(instance); EventEmitter.call(instance);
instance.runtime = new Runtime();
/** /**
* Event listener for blocks. Handles validation and serves as a generic * VM runtime, to store blocks, I/O devices, sprites/targets, etc.
* adapter between the blocks and the runtime interface. * @type {!Runtime}
*
* @param {Object} Blockly "block" event
*/ */
instance.blockListener = function (e) { instance.runtime = new Runtime();
// Validate event /**
if (typeof e !== 'object') return; * The "currently editing"/selected target ID for the VM.
if (typeof e.blockId !== 'string') return; * Block events from any Blockly workspace are routed to this target.
* @type {!string}
*/
instance.editingTarget = null;
// Runtime emits are passed along as VM emits.
instance.runtime.on(Runtime.SCRIPT_GLOW_ON, function (id) {
instance.emit(Runtime.SCRIPT_GLOW_ON, {id: id});
});
instance.runtime.on(Runtime.SCRIPT_GLOW_OFF, function (id) {
instance.emit(Runtime.SCRIPT_GLOW_OFF, {id: id});
});
instance.runtime.on(Runtime.BLOCK_GLOW_ON, function (id) {
instance.emit(Runtime.BLOCK_GLOW_ON, {id: id});
});
instance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (id) {
instance.emit(Runtime.BLOCK_GLOW_OFF, {id: id});
});
instance.runtime.on(Runtime.VISUAL_REPORT, function (id, value) {
instance.emit(Runtime.VISUAL_REPORT, {id: id, value: value});
});
// Blocks this.blockListener = this.blockListener.bind(this);
switch (e.type) { this.flyoutBlockListener = this.flyoutBlockListener.bind(this);
case 'create':
instance.runtime.createBlock(adapter(e), false);
break;
case 'change':
instance.runtime.changeBlock({
id: e.blockId,
element: e.element,
name: e.name,
value: e.newValue
});
break;
case 'move':
instance.runtime.moveBlock({
id: e.blockId,
oldParent: e.oldParentId,
oldField: e.oldInputName,
newParent: e.newParentId,
newField: e.newInputName
});
break;
case 'delete':
instance.runtime.deleteBlock({
id: e.blockId
});
break;
}
};
instance.flyoutBlockListener = function (e) {
switch (e.type) {
case 'create':
instance.runtime.createBlock(adapter(e), true);
break;
case 'change':
instance.runtime.changeBlock({
id: e.blockId,
element: e.element,
name: e.name,
value: e.newValue
});
break;
case 'delete':
instance.runtime.deleteBlock({
id: e.blockId
});
break;
}
};
} }
/** /**
@ -86,7 +53,259 @@ function VirtualMachine () {
util.inherits(VirtualMachine, EventEmitter); util.inherits(VirtualMachine, EventEmitter);
/** /**
* Export and bind to `window` * Start running the VM - do this before anything else.
*/ */
VirtualMachine.prototype.start = function () {
this.runtime.start();
};
/**
* "Green flag" handler - start all threads starting with a green flag.
*/
VirtualMachine.prototype.greenFlag = function () {
this.runtime.greenFlag();
};
/**
* Set whether the VM is in "turbo mode."
* When true, loops don't yield to redraw.
* @param {Boolean} turboModeOn Whether turbo mode should be set.
*/
VirtualMachine.prototype.setTurboMode = function (turboModeOn) {
this.runtime.turboMode = !!turboModeOn;
};
/**
* Set whether the VM is in "pause mode."
* When true, nothing is stepped.
* @param {Boolean} pauseModeOn Whether pause mode should be set.
*/
VirtualMachine.prototype.setPauseMode = function (pauseModeOn) {
this.runtime.setPauseMode(!!pauseModeOn);
};
/**
* Set whether the VM is in 2.0 "compatibility mode."
* When true, ticks go at 2.0 speed (30 TPS).
* @param {Boolean} compatibilityModeOn Whether compatibility mode is set.
*/
VirtualMachine.prototype.setCompatibilityMode = function (compatibilityModeOn) {
this.runtime.setCompatibilityMode(!!compatibilityModeOn);
};
/**
* Set whether the VM is in "single stepping mode."
* When true, blocks execute slowly and are highlighted visually.
* @param {Boolean} singleSteppingOn Whether single-stepping mode is set.
*/
VirtualMachine.prototype.setSingleSteppingMode = function (singleSteppingOn) {
this.runtime.setSingleSteppingMode(!!singleSteppingOn);
};
/**
* Set single-stepping mode speed.
* When in single-stepping mode, adjusts the speed of execution.
* @param {Number} speed Interval length in ms.
*/
VirtualMachine.prototype.setSingleSteppingSpeed = function (speed) {
this.runtime.setSingleSteppingSpeed(speed);
};
/**
* Stop all threads and running activities.
*/
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.
*/
VirtualMachine.prototype.getPlaygroundData = function () {
var instance = this;
// Only send back thread data for the current editingTarget.
var threadData = this.runtime.threads.filter(function(thread) {
return thread.target == instance.editingTarget;
});
// Remove the target key, since it's a circular reference.
var filteredThreadData = JSON.stringify(threadData, function(key, value) {
if (key == 'target') return undefined;
return value;
}, 2);
this.emit('playgroundData', {
blocks: this.editingTarget.blocks,
threads: filteredThreadData
});
};
/**
* Handle an animation frame.
*/
VirtualMachine.prototype.animationFrame = function () {
this.runtime.animationFrame();
};
/**
* Post I/O data to the virtual devices.
* @param {?string} device Name of virtual I/O device.
* @param {Object} data Any data object to post to the I/O device.
*/
VirtualMachine.prototype.postIOData = function (device, data) {
if (this.runtime.ioDevices[device]) {
this.runtime.ioDevices[device].postData(data);
}
};
/**
* Load a project from a Scratch 2.0 JSON representation.
* @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.
this.editingTarget = this.runtime.targets[0];
// Update the VM user's knowledge of targets and blocks on the workspace.
this.emitTargetsUpdate();
this.emitWorkspaceUpdate();
this.runtime.setEditingTarget(this.editingTarget);
};
/**
* Temporary way to make an empty project, in case the desired project
* cannot be loaded from the online server.
*/
VirtualMachine.prototype.createEmptyProject = function () {
// Stage.
var blocks2 = new Blocks();
var stage = new Sprite(blocks2, this.runtime);
stage.name = 'Stage';
stage.costumes.push({
skin: './assets/stage.png',
name: 'backdrop1',
bitmapResolution: 2,
rotationCenterX: 480,
rotationCenterY: 360
});
var target2 = stage.createClone();
this.runtime.targets.push(target2);
target2.x = 0;
target2.y = 0;
target2.direction = 90;
target2.size = 200;
target2.visible = true;
target2.isStage = true;
// Sprite1 (cat).
var blocks1 = new Blocks();
var sprite = new Sprite(blocks1, this.runtime);
sprite.name = 'Sprite1';
sprite.costumes.push({
skin: './assets/scratch_cat.svg',
name: 'costume1',
bitmapResolution: 1,
rotationCenterX: 47,
rotationCenterY: 55
});
var target1 = sprite.createClone();
this.runtime.targets.push(target1);
target1.x = 0;
target1.y = 0;
target1.direction = 90;
target1.size = 100;
target1.visible = true;
this.editingTarget = this.runtime.targets[0];
this.emitTargetsUpdate();
this.emitWorkspaceUpdate();
};
/**
* Set the renderer for the VM/runtime
* @param {!RenderWebGL} renderer The renderer to attach
*/
VirtualMachine.prototype.attachRenderer = function (renderer) {
this.runtime.attachRenderer(renderer);
};
/**
* Handle a Blockly event for the current editing target.
* @param {!Blockly.Event} e Any Blockly event.
*/
VirtualMachine.prototype.blockListener = function (e) {
if (this.editingTarget) {
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.
* After switching the editing target, the VM may emit updates
* to the list of targets and any attached workspace blocks
* (see `emitTargetsUpdate` and `emitWorkspaceUpdate`).
* @param {string} targetId Id of target to set as editing.
*/
VirtualMachine.prototype.setEditingTarget = function (targetId) {
// Has the target id changed? If not, exit.
if (targetId == this.editingTarget.id) {
return;
}
var target = this.runtime.getTargetById(targetId);
if (target) {
this.editingTarget = target;
// Emit appropriate UI updates.
this.emitTargetsUpdate();
this.emitWorkspaceUpdate();
this.runtime.setEditingTarget(target);
}
};
/**
* Emit metadata about available targets.
* An editor UI could use this to display a list of targets and show
* the currently editing one.
*/
VirtualMachine.prototype.emitTargetsUpdate = function () {
this.emit('targetsUpdate', {
// [[target id, human readable target name], ...].
targetList: this.runtime.targets.filter(function (target) {
// Don't report clones.
return !target.hasOwnProperty('isOriginal') || target.isOriginal;
}).map(function(target) {
return [target.id, target.getName()];
}),
// Currently editing target id.
editingTarget: this.editingTarget ? this.editingTarget.id : null
});
};
/**
* Emit an Blockly/scratch-blocks compatible XML representation
* of the current editing target's blocks.
*/
VirtualMachine.prototype.emitWorkspaceUpdate = function () {
this.emit('workspaceUpdate', {
'xml': this.editingTarget.blocks.toXML()
});
};
module.exports = VirtualMachine; module.exports = VirtualMachine;
if (typeof window !== 'undefined') window.VirtualMachine = module.exports;

37
src/io/clock.js Normal file
View file

@ -0,0 +1,37 @@
var Timer = require('../util/timer');
function Clock (runtime) {
this._projectTimer = new Timer();
this._projectTimer.start();
this._pausedTime = null;
this._paused = false;
/**
* Reference to the owning Runtime.
* @type{!Runtime}
*/
this.runtime = runtime;
}
Clock.prototype.projectTimer = function () {
if (this._paused) {
return this._pausedTime / 1000;
}
return this._projectTimer.timeElapsed() / 1000;
};
Clock.prototype.pause = function () {
this._paused = true;
this._pausedTime = this._projectTimer.timeElapsed();
};
Clock.prototype.resume = function () {
this._paused = false;
var dt = this._projectTimer.timeElapsed() - this._pausedTime;
this._projectTimer.startTime += dt;
};
Clock.prototype.resetProjectTimer = function () {
this._projectTimer.start();
};
module.exports = Clock;

85
src/io/keyboard.js Normal file
View file

@ -0,0 +1,85 @@
var Cast = require('../util/cast');
function Keyboard (runtime) {
/**
* List of currently pressed keys.
* @type{Array.<number>}
*/
this._keysPressed = [];
/**
* Reference to the owning Runtime.
* Can be used, for example, to activate hats.
* @type{!Runtime}
*/
this.runtime = runtime;
}
/**
* Convert a Scratch key name to a DOM keyCode.
* @param {Any} keyName Scratch key argument.
* @return {number} Key code corresponding to a DOM event.
*/
Keyboard.prototype._scratchKeyToKeyCode = function (keyName) {
if (typeof keyName == 'number') {
// Key codes placed in with number blocks.
return keyName;
}
var keyString = Cast.toString(keyName);
switch (keyString) {
case 'space': return 32;
case 'left arrow': return 37;
case 'up arrow': return 38;
case 'right arrow': return 39;
case 'down arrow': return 40;
// @todo: Consider adding other special keys here.
}
// Keys reported by DOM keyCode are upper case.
return keyString.toUpperCase().charCodeAt(0);
};
Keyboard.prototype._keyCodeToScratchKey = function (keyCode) {
if (keyCode >= 48 && keyCode <= 90) {
// Standard letter.
return String.fromCharCode(keyCode).toLowerCase();
}
switch (keyCode) {
case 32: return 'space';
case 37: return 'left arrow';
case 38: return 'up arrow';
case 39: return 'right arrow';
case 40: return 'down arrow';
}
return null;
};
Keyboard.prototype.postData = function (data) {
if (data.keyCode) {
var index = this._keysPressed.indexOf(data.keyCode);
if (data.isDown) {
// If not already present, add to the list.
if (index < 0) {
this._keysPressed.push(data.keyCode);
}
// Always trigger hats, even if it was already pressed.
this.runtime.startHats('event_whenkeypressed', {
'KEY_OPTION': this._keyCodeToScratchKey(data.keyCode)
});
this.runtime.startHats('event_whenkeypressed', {
'KEY_OPTION': 'any'
});
} else if (index > -1) {
// If already present, remove from the list.
this._keysPressed.splice(index, 1);
}
}
};
Keyboard.prototype.getKeyIsDown = function (key) {
if (key == 'any') {
return this._keysPressed.length > 0;
}
var keyCode = this._scratchKeyToKeyCode(key);
return this._keysPressed.indexOf(keyCode) > -1;
};
module.exports = Keyboard;

57
src/io/mouse.js Normal file
View file

@ -0,0 +1,57 @@
var MathUtil = require('../util/math-util');
function Mouse (runtime) {
this._x = 0;
this._y = 0;
this._isDown = false;
/**
* Reference to the owning Runtime.
* Can be used, for example, to activate hats.
* @type{!Runtime}
*/
this.runtime = runtime;
}
Mouse.prototype.postData = function(data) {
if (data.x) {
this._x = data.x - data.canvasWidth / 2;
}
if (data.y) {
this._y = data.y - data.canvasHeight / 2;
}
if (typeof data.isDown !== 'undefined') {
this._isDown = data.isDown;
if (this._isDown) {
this._activateClickHats(data.x, data.y);
}
}
};
Mouse.prototype._activateClickHats = function (x, y) {
if (this.runtime.renderer) {
var drawableID = this.runtime.renderer.pick(x, y);
for (var i = 0; i < this.runtime.targets.length; i++) {
var target = this.runtime.targets[i];
if (target.hasOwnProperty('drawableID') &&
target.drawableID == drawableID) {
this.runtime.startHats('event_whenthisspriteclicked',
null, target);
return;
}
}
}
};
Mouse.prototype.getX = function () {
return MathUtil.clamp(this._x, -240, 240);
};
Mouse.prototype.getY = function () {
return MathUtil.clamp(-this._y, -180, 180);
};
Mouse.prototype.getIsDown = function () {
return this._isDown;
};
module.exports = Mouse;

588
src/sprites/clone.js Normal file
View file

@ -0,0 +1,588 @@
var util = require('util');
var MathUtil = require('../util/math-util');
var Target = require('../engine/target');
/**
* Clone (instance) of a sprite.
* @param {!Sprite} sprite Reference to the sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
function Clone(sprite, runtime) {
Target.call(this, sprite.blocks);
this.runtime = runtime;
/**
* Reference to the sprite that this is a clone of.
* @type {!Sprite}
*/
this.sprite = sprite;
/**
* Reference to the global renderer for this VM, if one exists.
* @type {?RenderWebGLWorker}
*/
this.renderer = null;
if (this.runtime) {
this.renderer = this.runtime.renderer;
}
/**
* ID of the drawable for this clone returned by the renderer, if rendered.
* @type {?Number}
*/
this.drawableID = null;
/**
* Map of current graphic effect values.
* @type {!Object.<string, number>}
*/
this.effects = {
'color': 0,
'fisheye': 0,
'whirl': 0,
'pixelate': 0,
'mosaic': 0,
'brightness': 0,
'ghost': 0
};
}
util.inherits(Clone, Target);
/**
* Create a clone's drawable with the this.renderer.
*/
Clone.prototype.initDrawable = function () {
if (this.renderer) {
this.drawableID = this.renderer.createDrawable();
}
// If we're a clone, start the hats.
if (!this.isOriginal) {
this.runtime.startHats(
'control_start_as_clone', null, this
);
}
};
// Clone-level properties.
/**
* Whether this represents an "original" clone, i.e., created by the editor
* and not clone blocks. In interface terms, this true for a "sprite."
* @type {boolean}
*/
Clone.prototype.isOriginal = true;
/**
* Whether this clone represents the Scratch stage.
* @type {boolean}
*/
Clone.prototype.isStage = false;
/**
* Scratch X coordinate. Currently should range from -240 to 240.
* @type {Number}
*/
Clone.prototype.x = 0;
/**
* Scratch Y coordinate. Currently should range from -180 to 180.
* @type {number}
*/
Clone.prototype.y = 0;
/**
* Scratch direction. Currently should range from -179 to 180.
* @type {number}
*/
Clone.prototype.direction = 90;
/**
* Whether the clone is currently visible.
* @type {boolean}
*/
Clone.prototype.visible = true;
/**
* Size of clone as a percent of costume size. Ranges from 5% to 535%.
* @type {number}
*/
Clone.prototype.size = 100;
/**
* Currently selected costume index.
* @type {number}
*/
Clone.prototype.currentCostume = 0;
/**
* Rotation style for "all around"/spinning.
* @enum
*/
Clone.ROTATION_STYLE_ALL_AROUND = 'all around';
/**
* Rotation style for "left-right"/flipping.
* @enum
*/
Clone.ROTATION_STYLE_LEFT_RIGHT = 'left-right';
/**
* Rotation style for "no rotation."
* @enum
*/
Clone.ROTATION_STYLE_NONE = 'don\'t rotate';
/**
* Current rotation style.
* @type {!string}
*/
Clone.prototype.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
// End clone-level properties.
/**
* Set the X and Y coordinates of a clone.
* @param {!number} x New X coordinate of clone, in Scratch coordinates.
* @param {!number} y New Y coordinate of clone, in Scratch coordinates.
*/
Clone.prototype.setXY = function (x, y) {
if (this.isStage) {
return;
}
this.x = x;
this.y = y;
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, {
position: [this.x, this.y]
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Get the rendered direction and scale, after applying rotation style.
* @return {Object<string, number>} Direction and scale to render.
*/
Clone.prototype._getRenderedDirectionAndScale = function () {
// Default: no changes to `this.direction` or `this.scale`.
var finalDirection = this.direction;
var finalScale = [this.size, this.size];
if (this.rotationStyle == Clone.ROTATION_STYLE_NONE) {
// Force rendered direction to be 90.
finalDirection = 90;
} else if (this.rotationStyle === Clone.ROTATION_STYLE_LEFT_RIGHT) {
// Force rendered direction to be 90, and flip drawable if needed.
finalDirection = 90;
var scaleFlip = (this.direction < 0) ? -1 : 1;
finalScale = [scaleFlip * this.size, this.size];
}
return {direction: finalDirection, scale: finalScale};
};
/**
* Set the direction of a clone.
* @param {!number} direction New direction of clone.
*/
Clone.prototype.setDirection = function (direction) {
if (this.isStage) {
return;
}
// Keep direction between -179 and +180.
this.direction = MathUtil.wrapClamp(direction, -179, 180);
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Set a say bubble on this clone.
* @param {?string} type Type of say bubble: "say", "think", or null.
* @param {?string} message Message to put in say bubble.
*/
Clone.prototype.setSay = function (type, message) {
if (this.isStage) {
return;
}
// @todo: Render to stage.
if (!type || !message) {
console.log('Clearing say bubble');
return;
}
console.log('Setting say bubble:', type, message);
};
/**
* Set visibility of the clone; i.e., whether it's shown or hidden.
* @param {!boolean} visible True if the sprite should be shown.
*/
Clone.prototype.setVisible = function (visible) {
if (this.isStage) {
return;
}
this.visible = visible;
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, {
visible: this.visible
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Set size of the clone, as a percentage of the costume size.
* @param {!number} size Size of clone, from 5 to 535.
*/
Clone.prototype.setSize = function (size) {
if (this.isStage) {
return;
}
// Keep size between 5% and 535%.
this.size = MathUtil.clamp(size, 5, 535);
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Set a particular graphic effect on this clone.
* @param {!string} effectName Name of effect (see `Clone.prototype.effects`).
* @param {!number} value Numerical magnitude of effect.
*/
Clone.prototype.setEffect = function (effectName, value) {
if (!this.effects.hasOwnProperty(effectName)) return;
this.effects[effectName] = value;
if (this.renderer) {
var props = {};
props[effectName] = this.effects[effectName];
this.renderer.updateDrawableProperties(this.drawableID, props);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Clear all graphic effects on this clone.
*/
Clone.prototype.clearEffects = function () {
for (var effectName in this.effects) {
this.effects[effectName] = 0;
}
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, this.effects);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Set the current costume of this clone.
* @param {number} index New index of costume.
*/
Clone.prototype.setCostume = function (index) {
// Keep the costume index within possible values.
index = Math.round(index);
this.currentCostume = MathUtil.wrapClamp(
index, 0, this.sprite.costumes.length - 1
);
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, {
skin: this.sprite.costumes[this.currentCostume].skin
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Update the rotation style for this clone.
* @param {!string} rotationStyle New rotation style.
*/
Clone.prototype.setRotationStyle = function (rotationStyle) {
if (rotationStyle == Clone.ROTATION_STYLE_NONE) {
this.rotationStyle = Clone.ROTATION_STYLE_NONE;
} else if (rotationStyle == Clone.ROTATION_STYLE_ALL_AROUND) {
this.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
} else if (rotationStyle == Clone.ROTATION_STYLE_LEFT_RIGHT) {
this.rotationStyle = Clone.ROTATION_STYLE_LEFT_RIGHT;
}
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Get a costume index of this clone, by name of the costume.
* @param {?string} costumeName Name of a costume.
* @return {number} Index of the named costume, or -1 if not present.
*/
Clone.prototype.getCostumeIndexByName = function (costumeName) {
for (var i = 0; i < this.sprite.costumes.length; i++) {
if (this.sprite.costumes[i].name == costumeName) {
return i;
}
}
return -1;
};
/**
* Update all drawable properties for this clone.
* Use when a batch has changed, e.g., when the drawable is first created.
*/
Clone.prototype.updateAllDrawableProperties = function () {
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
position: [this.x, this.y],
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale,
visible: this.visible,
skin: this.sprite.costumes[this.currentCostume].skin
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
/**
* Return the human-readable name for this clone, i.e., the sprite's name.
* @override
* @returns {string} Human-readable name for the clone.
*/
Clone.prototype.getName = function () {
return this.sprite.name;
};
/**
* Return the clone's tight bounding box.
* Includes top, left, bottom, right attributes in Scratch coordinates.
* @return {?Object} Tight bounding box of clone, or null.
*/
Clone.prototype.getBounds = function () {
if (this.renderer) {
return this.runtime.renderer.getBounds(this.drawableID);
}
return null;
};
/**
* Return whether the clone is touching a point.
* @param {number} x X coordinate of test point.
* @param {number} y Y coordinate of test point.
* @return {Boolean} True iff the clone is touching the point.
*/
Clone.prototype.isTouchingPoint = function (x, y) {
if (this.renderer) {
// @todo: Update once pick is in Scratch coordinates.
// Limits test to this Drawable, so this will return true
// even if the clone is obscured by another Drawable.
var pickResult = this.runtime.renderer.pick(
x + this.runtime.constructor.STAGE_WIDTH / 2,
-y + this.runtime.constructor.STAGE_HEIGHT / 2,
null, null,
[this.drawableID]
);
return pickResult === this.drawableID;
}
return false;
};
/**
* Return whether the clone is touching a stage edge.
* @return {Boolean} True iff the clone is touching the stage edge.
*/
Clone.prototype.isTouchingEdge = function () {
if (this.renderer) {
var stageWidth = this.runtime.constructor.STAGE_WIDTH;
var stageHeight = this.runtime.constructor.STAGE_HEIGHT;
var bounds = this.getBounds();
if (bounds.left < -stageWidth / 2 ||
bounds.right > stageWidth / 2 ||
bounds.top > stageHeight / 2 ||
bounds.bottom < -stageHeight / 2) {
return true;
}
}
return false;
};
/**
* Return whether the clone is touching a named sprite.
* @param {string} spriteName Name fo the sprite.
* @return {Boolean} True iff the clone is touching a clone of the sprite.
*/
Clone.prototype.isTouchingSprite = function (spriteName) {
var firstClone = this.runtime.getSpriteTargetByName(spriteName);
if (!firstClone || !this.renderer) {
return false;
}
var drawableCandidates = firstClone.sprite.clones.map(function(clone) {
return clone.drawableID;
});
return this.renderer.isTouchingDrawables(
this.drawableID, drawableCandidates);
};
/**
* Return whether the clone is touching a color.
* @param {Array.<number>} rgb [r,g,b], values between 0-255.
* @return {Promise.<Boolean>} True iff the clone is touching the color.
*/
Clone.prototype.isTouchingColor = function (rgb) {
if (this.renderer) {
return this.renderer.isTouchingColor(this.drawableID, rgb);
}
return false;
};
/**
* Return whether the clone's color is touching a color.
* @param {Object} targetRgb {Array.<number>} [r,g,b], values between 0-255.
* @param {Object} maskRgb {Array.<number>} [r,g,b], values between 0-255.
* @return {Promise.<Boolean>} True iff the clone's color is touching the color.
*/
Clone.prototype.colorIsTouchingColor = function (targetRgb, maskRgb) {
if (this.renderer) {
return this.renderer.isTouchingColor(
this.drawableID,
targetRgb,
maskRgb
);
}
return false;
};
/**
* Move clone to the front layer.
*/
Clone.prototype.goToFront = function () {
if (this.renderer) {
this.renderer.setDrawableOrder(this.drawableID, Infinity);
}
};
/**
* Move clone back a number of layers.
* @param {number} nLayers How many layers to go back.
*/
Clone.prototype.goBackLayers = function (nLayers) {
if (this.renderer) {
this.renderer.setDrawableOrder(this.drawableID, -nLayers, true, 1);
}
};
/**
* Keep a desired position within a fence.
* @param {number} newX New desired X position.
* @param {number} newY New desired Y position.
* @param {Object=} opt_fence Optional fence with left, right, top bottom.
* @return {Array.<number>} Fenced X and Y coordinates.
*/
Clone.prototype.keepInFence = function (newX, newY, opt_fence) {
var fence = opt_fence;
if (!fence) {
fence = {
left: -this.runtime.constructor.STAGE_WIDTH / 2,
right: this.runtime.constructor.STAGE_WIDTH / 2,
top: this.runtime.constructor.STAGE_HEIGHT / 2,
bottom: -this.runtime.constructor.STAGE_HEIGHT / 2
};
}
var bounds = this.getBounds();
if (!bounds) return;
// Adjust the known bounds to the target position.
bounds.left += (newX - this.x);
bounds.right += (newX - this.x);
bounds.top += (newY - this.y);
bounds.bottom += (newY - this.y);
// Find how far we need to move the target position.
var dx = 0;
var dy = 0;
if (bounds.left < fence.left) {
dx += fence.left - bounds.left;
}
if (bounds.right > fence.right) {
dx += fence.right - bounds.right;
}
if (bounds.top > fence.top) {
dy += fence.top - bounds.top;
}
if (bounds.bottom < fence.bottom) {
dy += fence.bottom - bounds.bottom;
}
return [newX + dx, newY + dy];
};
/**
* Make a clone of this clone, copying any run-time properties.
* If we've hit the global clone limit, returns null.
* @return {!Clone} New clone object.
*/
Clone.prototype.makeClone = function () {
if (!this.runtime.clonesAvailable()) {
return; // Hit max clone limit.
}
this.runtime.changeCloneCounter(1);
var newClone = this.sprite.createClone();
newClone.x = this.x;
newClone.y = this.y;
newClone.direction = this.direction;
newClone.visible = this.visible;
newClone.size = this.size;
newClone.currentCostume = this.currentCostume;
newClone.rotationStyle = this.rotationStyle;
newClone.effects = JSON.parse(JSON.stringify(this.effects));
newClone.variables = JSON.parse(JSON.stringify(this.variables));
newClone.lists = JSON.parse(JSON.stringify(this.lists));
newClone.initDrawable();
newClone.updateAllDrawableProperties();
return newClone;
};
/**
* Called when the project receives a "green flag."
* For a clone, this clears graphic effects.
*/
Clone.prototype.onGreenFlag = function () {
this.clearEffects();
};
/**
* Dispose of this clone, destroying any run-time properties.
*/
Clone.prototype.dispose = function () {
this.runtime.changeCloneCounter(-1);
if (this.renderer && this.drawableID !== null) {
this.renderer.destroyDrawable(this.drawableID);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
module.exports = Clone;

57
src/sprites/sprite.js Normal file
View file

@ -0,0 +1,57 @@
var Clone = require('./clone');
var Blocks = require('../engine/blocks');
/**
* Sprite to be used on the Scratch stage.
* All clones of a sprite have shared blocks, shared costumes, shared variables.
* @param {?Blocks} blocks Shared blocks object for all clones of sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
function Sprite (blocks, runtime) {
this.runtime = runtime;
if (!blocks) {
// Shared set of blocks for all clones.
blocks = new Blocks();
}
this.blocks = blocks;
/**
* Human-readable name for this sprite (and all clones).
* @type {string}
*/
this.name = '';
/**
* List of costumes for this sprite.
* Each entry is an object, e.g.,
* {
* skin: "costume.svg",
* name: "Costume Name",
* bitmapResolution: 2,
* rotationCenterX: 0,
* rotationCenterY: 0
* }
* @type {Array.<!Object>}
*/
this.costumes = [];
/**
* List of clones for this sprite, including the original.
* @type {Array.<!Clone>}
*/
this.clones = [];
}
/**
* Create a clone of this sprite.
* @returns {!Clone} Newly created clone.
*/
Sprite.prototype.createClone = function () {
var newClone = new Clone(this, this.runtime);
newClone.isOriginal = this.clones.length == 0;
this.clones.push(newClone);
if (newClone.isOriginal) {
newClone.initDrawable();
}
return newClone;
};
module.exports = Sprite;

163
src/util/cast.js Normal file
View file

@ -0,0 +1,163 @@
var Color = require('../util/color');
function Cast () {}
/**
* @fileoverview
* Utilities for casting and comparing Scratch data-types.
* Scratch behaves slightly differently from JavaScript in many respects,
* and these differences should be encapsulated below.
* For example, in Scratch, add(1, join("hello", world")) -> 1.
* This is because "hello world" is cast to 0.
* In JavaScript, 1 + Number("hello" + "world") would give you NaN.
* Use when coercing a value before computation.
*/
/**
* Scratch cast to number.
* Treats NaN as 0.
* In Scratch 2.0, this is captured by `interp.numArg.`
* @param {*} value Value to cast to number.
* @return {number} The Scratch-casted number value.
*/
Cast.toNumber = function (value) {
var n = Number(value);
if (isNaN(n)) {
// Scratch treats NaN as 0, when needed as a number.
// E.g., 0 + NaN -> 0.
return 0;
}
return n;
};
/**
* Scratch cast to boolean.
* In Scratch 2.0, this is captured by `interp.boolArg.`
* Treats some string values differently from JavaScript.
* @param {*} value Value to cast to boolean.
* @return {boolean} The Scratch-casted boolean value.
*/
Cast.toBoolean = function (value) {
// Already a boolean?
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
// These specific strings are treated as false in Scratch.
if ((value == '') ||
(value == '0') ||
(value.toLowerCase() == 'false')) {
return false;
}
// All other strings treated as true.
return true;
}
// Coerce other values and numbers.
return Boolean(value);
};
/**
* Scratch cast to string.
* @param {*} value Value to cast to string.
* @return {string} The Scratch-casted string value.
*/
Cast.toString = function (value) {
return String(value);
};
/**
* Cast any Scratch argument to an RGB color object to be used for the renderer.
* @param {*} value Value to convert to RGB color object.
* @return {Array.<number>} [r,g,b], values between 0-255.
*/
Cast.toRgbColorList = function (value) {
var color;
if (typeof value == 'string' && value.substring(0, 1) == '#') {
color = Color.hexToRgb(value);
} else {
color = Color.decimalToRgb(Cast.toNumber(value));
}
return [color.r, color.g, color.b];
};
/**
* Compare two values, using Scratch cast, case-insensitive string compare, etc.
* In Scratch 2.0, this is captured by `interp.compare.`
* @param {*} v1 First value to compare.
* @param {*} v2 Second value to compare.
* @returns {Number} Negative number if v1 < v2; 0 if equal; positive otherwise.
*/
Cast.compare = function (v1, v2) {
var n1 = Number(v1);
var n2 = Number(v2);
if (isNaN(n1) || isNaN(n2)) {
// At least one argument can't be converted to a number.
// Scratch compares strings as case insensitive.
var s1 = String(v1).toLowerCase();
var s2 = String(v2).toLowerCase();
return s1.localeCompare(s2);
} else {
// Compare as numbers.
return n1 - n2;
}
};
/**
* Determine if a Scratch argument number represents a round integer.
* @param {*} val Value to check.
* @return {boolean} True if number looks like an integer.
*/
Cast.isInt = function (val) {
// Values that are already numbers.
if (typeof val === 'number') {
if (isNaN(val)) { // NaN is considered an integer.
return true;
}
// True if it's "round" (e.g., 2.0 and 2).
return val == parseInt(val);
} else if (typeof val === 'boolean') {
// `True` and `false` always represent integer after Scratch cast.
return true;
} else if (typeof val === 'string') {
// If it contains a decimal point, don't consider it an int.
return val.indexOf('.') < 0;
}
return false;
};
Cast.LIST_INVALID = 'INVALID';
Cast.LIST_ALL = 'ALL';
/**
* Compute a 1-based index into a list, based on a Scratch argument.
* Two special cases may be returned:
* LIST_ALL: if the block is referring to all of the items in the list.
* LIST_INVALID: if the index was invalid in any way.
* @param {*} index Scratch arg, including 1-based numbers or special cases.
* @param {number} length Length of the list.
* @return {(number|string)} 1-based index for list, LIST_ALL, or LIST_INVALID.
*/
Cast.toListIndex = function (index, length) {
if (typeof index !== 'number') {
if (index == 'all') {
return Cast.LIST_ALL;
}
if (index == 'last') {
if (length > 0) {
return length;
}
return Cast.LIST_INVALID;
} else if (index == 'random' || index == 'any') {
if (length > 0) {
return 1 + Math.floor(Math.random() * length);
}
return Cast.LIST_INVALID;
}
}
index = Math.floor(Cast.toNumber(index));
if (index < 1 || index > length) {
return Cast.LIST_INVALID;
}
return index;
};
module.exports = Cast;

76
src/util/color.js Normal file
View file

@ -0,0 +1,76 @@
function Color () {}
/**
* Convert a Scratch decimal color to a hex string, #RRGGBB.
* @param {number} decimal RGB color as a decimal.
* @return {string} RGB color as #RRGGBB hex string.
*/
Color.decimalToHex = function (decimal) {
if (decimal < 0) {
decimal += 0xFFFFFF + 1;
}
var hex = Number(decimal).toString(16);
hex = '#' + '000000'.substr(0, 6 - hex.length) + hex;
return hex;
};
/**
* Convert a Scratch decimal color to an RGB color object.
* @param {number} decimal RGB color as decimal.
* @returns {Object} {r: R, g: G, b: B}, values between 0-255
*/
Color.decimalToRgb = function (decimal) {
var r = (decimal >> 16) & 0xFF;
var g = (decimal >> 8) & 0xFF;
var b = decimal & 0xFF;
return {r: r, g: g, b: b};
};
/**
* Convert a hex color (e.g., F00, #03F, #0033FF) to an RGB color object.
* CC-BY-SA Tim Down:
* https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
* @param {!string} hex Hex representation of the color.
* @return {Object} {r: R, g: G, b: B}, 0-255, or null.
*/
Color.hexToRgb = function (hex) {
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
/**
* Convert an RGB color object to a hex color.
* @param {Object} rgb {r: R, g: G, b: B}, values between 0-255.
* @return {!string} Hex representation of the color.
*/
Color.rgbToHex = function (rgb) {
return Color.decimalToHex(Color.rgbToDecimal(rgb));
};
/**
* Convert an RGB color object to a Scratch decimal color.
* @param {Object} rgb {r: R, g: G, b: B}, values between 0-255.
* @return {!number} Number representing the color.
*/
Color.rgbToDecimal = function (rgb) {
return (rgb.r << 16) + (rgb.g << 8) + rgb.b;
};
/**
* Convert a hex color (e.g., F00, #03F, #0033FF) to a decimal color number.
* @param {!string} hex Hex representation of the color.
* @return {!number} Number representing the color.
*/
Color.hexToDecimal = function (hex) {
return Color.rgbToDecimal(Color.hexToRgb(hex));
};
module.exports = Color;

48
src/util/math-util.js Normal file
View file

@ -0,0 +1,48 @@
function MathUtil () {}
/**
* Convert a value from degrees to radians.
* @param {!number} deg Value in degrees.
* @return {!number} Equivalent value in radians.
*/
MathUtil.degToRad = function (deg) {
return deg * Math.PI / 180;
};
/**
* Convert a value from radians to degrees.
* @param {!number} rad Value in radians.
* @return {!number} Equivalent value in degrees.
*/
MathUtil.radToDeg = function (rad) {
return rad * 180 / Math.PI;
};
/**
* Clamp a number between two limits.
* If n < min, return min. If n > max, return max. Else, return n.
* @param {!number} n Number to clamp.
* @param {!number} min Minimum limit.
* @param {!number} max Maximum limit.
* @return {!number} Value of n clamped to min and max.
*/
MathUtil.clamp = function (n, min, max) {
return Math.min(Math.max(n, min), max);
};
/**
* Keep a number between two limits, wrapping "extra" into the range.
* e.g., wrapClamp(7, 1, 5) == 2
* wrapClamp(0, 1, 5) == 5
* wrapClamp(-11, -10, 6) == 6, etc.
* @param {!number} n Number to wrap.
* @param {!number} min Minimum limit.
* @param {!number} max Maximum limit.
* @return {!number} Value of n wrapped between min and max.
*/
MathUtil.wrapClamp = function (n, min, max) {
var range = (max - min) + 1;
return n - Math.floor((n - min) / range) * range;
};
module.exports = MathUtil;

View file

@ -1,20 +1,70 @@
/** /**
* Constructor * @fileoverview
* A utility for accurately measuring time.
* To use:
* ---
* var timer = new Timer();
* timer.start();
* ... pass some time ...
* var timeDifference = timer.timeElapsed();
* ---
* Or, you can use the `time` and `relativeTime`
* to do some measurement yourself.
*/ */
function Timer () {
this.startTime = 0;
}
/**
* @constructor
*/
function Timer () {}
/**
* Used to store the start time of a timer action.
* Updated when calling `timer.start`.
*/
Timer.prototype.startTime = 0;
/**
* Return the currently known absolute time, in ms precision.
* @returns {number} ms elapsed since 1 January 1970 00:00:00 UTC.
*/
Timer.prototype.time = function () { Timer.prototype.time = function () {
if (Date.now) {
return Date.now(); return Date.now();
} else {
return new Date().getTime();
}
}; };
/**
* Returns a time accurate relative to other times produced by this function.
* If possible, will use sub-millisecond precision.
* If not, will use millisecond precision.
* Not guaranteed to produce the same absolute values per-system.
* @returns {number} ms-scale accurate time relative to other relative times.
*/
Timer.prototype.relativeTime = function () {
if (typeof self !== 'undefined' &&
self.performance && 'now' in self.performance) {
return self.performance.now();
} else {
return this.time();
}
};
/**
* Start a timer for measuring elapsed time,
* at the most accurate precision possible.
*/
Timer.prototype.start = function () { Timer.prototype.start = function () {
this.startTime = this.time(); this.startTime = this.relativeTime();
}; };
/**
* Check time elapsed since `timer.start` was called.
* @returns {number} Time elapsed, in ms (possibly sub-ms precision).
*/
Timer.prototype.timeElapsed = function () { Timer.prototype.timeElapsed = function () {
return this.time() - this.startTime; return this.relativeTime() - this.startTime;
}; };
module.exports = Timer; module.exports = Timer;

29
src/util/uid.js Normal file
View file

@ -0,0 +1,29 @@
/**
* @fileoverview UID generator, from Blockly.
*/
/**
* Legal characters for the unique ID.
* Should be all on a US keyboard. No XML special characters or control codes.
* Removed $ due to issue 251.
* @private
*/
var soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
/**
* Generate a unique ID, from Blockly. This should be globally unique.
* 87 characters ^ 20 length > 128 bits (better than a UUID).
* @return {string} A globally unique ID string.
*/
var uid = function () {
var length = 20;
var soupLength = soup_.length;
var id = [];
for (var i = 0; i < length; i++) {
id[i] = soup_.charAt(Math.random() * soupLength);
}
return id.join('');
};
module.exports = uid;

21
src/util/xml-escape.js Normal file
View file

@ -0,0 +1,21 @@
/**
* Escape a string to be safe to use in XML content.
* CC-BY-SA: hgoebl
* https://stackoverflow.com/questions/7918868/
* how-to-escape-xml-entities-in-javascript
* @param {!string} unsafe Unsafe string.
* @return {string} XML-escaped string, for use within an XML tag.
*/
var xmlEscape = function (unsafe) {
return unsafe.replace(/[<>&'"]/g, function (c) {
switch (c) {
case '<': return '&lt;';
case '>': return '&gt;';
case '&': return '&amp;';
case '\'': return '&apos;';
case '"': return '&quot;';
}
});
};
module.exports = xmlEscape;

View file

@ -1,90 +0,0 @@
/**
* @fileoverview Timers that are synchronized with the Scratch sequencer.
*/
var Timer = require('./timer');
function YieldTimers () {}
/**
* Shared collection of timers.
* Each timer is a [Function, number] with the callback
* and absolute time for it to run.
* @type {Object.<number,Array>}
*/
YieldTimers.timers = {};
/**
* Monotonically increasing timer ID.
* @type {number}
*/
YieldTimers.timerId = 0;
/**
* Utility for measuring time.
* @type {!Timer}
*/
YieldTimers.globalTimer = new Timer();
/**
* The timeout function is passed to primitives and is intended
* as a convenient replacement for window.setTimeout.
* The sequencer will attempt to resolve the timer every time
* the yielded thread would have been stepped.
* @param {!Function} callback To be called when the timer is done.
* @param {number} timeDelta Time to wait, in ms.
* @return {number} Timer ID to be used with other methods.
*/
YieldTimers.timeout = function (callback, timeDelta) {
var id = ++YieldTimers.timerId;
YieldTimers.timers[id] = [
callback,
YieldTimers.globalTimer.time() + timeDelta
];
return id;
};
/**
* Attempt to resolve a timeout.
* If the time has passed, call the callback.
* Otherwise, do nothing.
* @param {number} id Timer ID to resolve.
* @return {boolean} True if the timer has resolved.
*/
YieldTimers.resolve = function (id) {
var timer = YieldTimers.timers[id];
if (!timer) {
// No such timer.
return false;
}
var callback = timer[0];
var time = timer[1];
if (YieldTimers.globalTimer.time() < time) {
// Not done yet.
return false;
}
// Execute the callback and remove the timer.
callback();
delete YieldTimers.timers[id];
return true;
};
/**
* Reject a timer so the callback never executes.
* @param {number} id Timer ID to reject.
*/
YieldTimers.reject = function (id) {
if (YieldTimers.timers[id]) {
delete YieldTimers.timers[id];
}
};
/**
* Reject all timers currently stored.
* Especially useful for a Scratch "stop."
*/
YieldTimers.rejectAll = function () {
YieldTimers.timers = {};
YieldTimers.timerId = 0;
};
module.exports = YieldTimers;

71
test/fixtures/default.json vendored Normal file
View file

@ -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"
}
}

359
test/fixtures/demo.json vendored Normal file
View file

@ -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"
}
}

View file

@ -1,20 +1,69 @@
{ {
"create": { "create": {
"blockId": "z!+#Nqr,_(V=xz0y7a@d",
"workspaceId": "7Luws3lyb*Z98~Kk+IG|", "workspaceId": "7Luws3lyb*Z98~Kk+IG|",
"group": ";OswyM#@%`%,xOrhOXC=", "group": ";OswyM#@%`%,xOrhOXC=",
"recordUndo": true, "recordUndo": true,
"name": "block",
"xml": { "xml": {
"attributes": { "outerHTML": "<block type=\"wedo_motorclockwise\" id=\"z!+#Nqr,_(V=xz0y7a@d\"><value name=\"DURATION\"><shadow type=\"math_number\" id=\"!6Ahqg4f}Ljl}X5Hws?Z\"><field name=\"NUM\">10</field></shadow></value></block>"
"type": {
"value": "wedo_motorclockwise"
}
},
"innerHTML": "<value name=\"DURATION\"><shadow type=\"math_number\" id=\"!6Ahqg4f}Ljl}X5Hws?Z\"><field name=\"NUM\">10</field></shadow></value>"
}, },
"ids": [ "ids": [
"z!+#Nqr,_(V=xz0y7a@d", "z!+#Nqr,_(V=xz0y7a@d",
"!6Ahqg4f}Ljl}X5Hws?Z" "!6Ahqg4f}Ljl}X5Hws?Z"
] ]
},
"createbranch": {
"name": "block",
"xml": {
"outerHTML": "<block type=\"control_forever\" id=\"r9`RpL74T6*SXPKv7}Dq\" x=\"61\" y=\"90\"><statement name=\"SUBSTACK\"><block type=\"control_wait\" id=\"{Rwt[LFtD1-JPAi-qf:.\"><value name=\"DURATION\"><shadow type=\"math_number\" id=\"VMDxt_9SYe5{*eNRe5dZ\"><field name=\"NUM\">1</field></shadow></value></block></statement></block>"
}
},
"createtwobranches": {
"name": "block",
"xml": {
"outerHTML": "<block type=\"control_if_else\" id=\"8W?lmIY!Tgnh)~0!G#9-\" x=\"87\" y=\"159\"><statement name=\"SUBSTACK\"><block type=\"event_broadcast\" id=\"lgU2GGtwlREuasCB02Vr\"></block></statement><statement name=\"SUBSTACK2\"><block type=\"event_broadcast\" id=\"Gb]N,2P;|J%F?pxSwz(2\"></block></statement></block>"
}
},
"createtoplevelshadow": {
"name": "shadow",
"xml": {
"outerHTML": "<shadow type=\"math_number\" id=\"z9d57=IUI5se;DBbyug)\"><field name=\"NUM\">4</field></shadow>"
}
},
"createwithnext": {
"name": "block",
"xml": {
"outerHTML": "<block type=\"wedo_setcolor\" id=\"*CT)7+UKjQIEtUw.OGT6\" x=\"89\" y=\"48\"><next><block type=\"wedo_motorspeed\" id=\"Er*:^o7yYL#dX+5)R^xq\"></block></next></block>"
}
},
"createinvalid": {
"name": "whatever",
"xml": {
"outerHTML": "<xml></xml>"
}
},
"createinvalidgrandchild": {
"name": "block",
"xml": {
"outerHTML": "<block type=\"control_forever\" id=\"r9`RpL74T6*SXPKv7}Dq\" x=\"61\" y=\"90\"><next><invalidgrandchild>xxx</invalidgrandchild></next></block>"
}
},
"createbadxml": {
"name": "whatever",
"xml": {
"outerHTML": "></xml>"
}
},
"createemptyfield": {
"name": "block",
"xml": {
"outerHTML": "<block type='operator_equals' id='l^H_{8[DDyDW?m)HIt@b' x='100' y='362'><value name='OPERAND1'><shadow type='text' id='Ud@4y]bc./]uv~te?brb'><field name='TEXT'></field></shadow></value><value name='OPERAND2'><shadow type='text' id='p8[y..,[K;~G,k7]N;08'><field name='TEXT'></field></shadow></value></block>"
}
},
"createobscuredshadow": {
"name": "block",
"xml": {
"outerHTML": "<block type='operator_add' id='D;MqidqmaN}Dft)y#Bf`' x='80' y='98'><value name='NUM1'><shadow type='math_number' id='F[IFAdLbq8!q25+Nio@i'><field name='NUM'></field></shadow><block type='sensing_answer' id='D~ZQ|BYb1)xw4)8ziI%.'></block</value><value name='NUM2'><shadow type='math_number' id='|Sjv4!*X6;wj?QaCE{-9'><field name='NUM'></field></shadow></value></block>"
}
} }
} }

View file

@ -6,7 +6,6 @@ test('spec', function (t) {
t.type(VirtualMachine, 'function'); t.type(VirtualMachine, 'function');
t.type(vm, 'object'); t.type(vm, 'object');
t.type(vm.blockListener, 'function');
t.end(); t.end();
}); });

View file

@ -1,20 +0,0 @@
var test = require('tap').test;
var adapter = require('../../src/engine/adapter');
var events = require('../fixtures/events.json');
test('spec', function (t) {
t.type(adapter, 'function');
t.end();
});
test('create event', function (t) {
var result = adapter(events.create);
t.type(result, 'object');
t.type(result.id, 'string');
t.type(result.opcode, 'string');
t.type(result.fields, 'object');
t.type(result.fields['DURATION'], 'object');
t.end();
});

View file

@ -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:min, TO:max});
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();
});

187
test/unit/engine_adapter.js Normal file
View file

@ -0,0 +1,187 @@
var test = require('tap').test;
var adapter = require('../../src/engine/adapter');
var events = require('../fixtures/events.json');
test('spec', function (t) {
t.type(adapter, 'function');
t.end();
});
test('invalid inputs', function(t) {
var nothing = adapter('not an object');
t.type(nothing, 'undefined');
nothing = adapter({noxmlproperty:true});
t.type(nothing, 'undefined');
t.end();
});
test('create event', function (t) {
var result = adapter(events.create);
t.ok(Array.isArray(result));
t.equal(result.length, 2);
// Outer block
t.type(result[0].id, 'string');
t.type(result[0].opcode, 'string');
t.type(result[0].fields, 'object');
t.type(result[0].inputs, 'object');
t.type(result[0].inputs['DURATION'], 'object');
t.type(result[0].topLevel, 'boolean');
t.equal(result[0].topLevel, true);
// Enclosed shadow block
t.type(result[1].id, 'string');
t.type(result[1].opcode, 'string');
t.type(result[1].fields, 'object');
t.type(result[1].inputs, 'object');
t.type(result[1].fields['NUM'], 'object');
t.type(result[1].fields['NUM'].value, '10');
t.type(result[1].topLevel, 'boolean');
t.equal(result[1].topLevel, false);
t.end();
});
test('create with branch', function (t) {
var result = adapter(events.createbranch);
// Outer block
t.type(result[0].id, 'string');
t.type(result[0].opcode, 'string');
t.type(result[0].fields, 'object');
t.type(result[0].inputs, 'object');
t.type(result[0].inputs['SUBSTACK'], 'object');
t.type(result[0].topLevel, 'boolean');
t.equal(result[0].topLevel, true);
// In branch
var branchBlockId = result[0].inputs['SUBSTACK']['block'];
var branchShadowId = result[0].inputs['SUBSTACK']['shadow'];
t.type(branchBlockId, 'string');
t.equal(branchShadowId, null);
// Find actual branch block
var branchBlock = null;
for (var i = 0; i < result.length; i++) {
if (result[i].id == branchBlockId) {
branchBlock = result[i];
}
}
t.type(branchBlock, 'object');
t.end();
});
test('create with two branches', function (t) {
var result = adapter(events.createtwobranches);
// Outer block
t.type(result[0].id, 'string');
t.type(result[0].opcode, 'string');
t.type(result[0].fields, 'object');
t.type(result[0].inputs, 'object');
t.type(result[0].inputs['SUBSTACK'], 'object');
t.type(result[0].inputs['SUBSTACK2'], 'object');
t.type(result[0].topLevel, 'boolean');
t.equal(result[0].topLevel, true);
// In branchs
var firstBranchBlockId = result[0].inputs['SUBSTACK']['block'];
var secondBranchBlockId = result[0].inputs['SUBSTACK2']['block'];
t.type(firstBranchBlockId, 'string');
t.type(secondBranchBlockId, 'string');
var firstBranchShadowBlockId = result[0].inputs['SUBSTACK']['shadow'];
var secondBranchShadowBlockId = result[0].inputs['SUBSTACK2']['shadow'];
t.equal(firstBranchShadowBlockId, null);
t.equal(secondBranchShadowBlockId, null);
// Find actual branch blocks
var firstBranchBlock = null;
var secondBranchBlock = null;
for (var i = 0; i < result.length; i++) {
if (result[i].id == firstBranchBlockId) {
firstBranchBlock = result[i];
}
if (result[i].id == secondBranchBlockId) {
secondBranchBlock = result[i];
}
}
t.type(firstBranchBlock, 'object');
t.type(secondBranchBlock, 'object');
t.end();
});
test('create with top-level shadow', function (t) {
var result = adapter(events.createtoplevelshadow);
t.ok(Array.isArray(result));
t.equal(result.length, 1);
// Outer block
t.type(result[0].id, 'string');
t.type(result[0].opcode, 'string');
t.type(result[0].fields, 'object');
t.type(result[0].inputs, 'object');
t.type(result[0].topLevel, 'boolean');
t.equal(result[0].topLevel, true);
t.end();
});
test('create with next connection', function (t) {
var result = adapter(events.createwithnext);
t.ok(Array.isArray(result));
t.equal(result.length, 2);
// First block
t.type(result[0].id, 'string');
t.type(result[0].opcode, 'string');
t.type(result[0].fields, 'object');
t.type(result[0].inputs, 'object');
t.type(result[0].topLevel, 'boolean');
t.equal(result[0].topLevel, true);
t.type(result[0].next, 'string');
t.equal(result[0].next, result[1].id);
// Second block
t.type(result[1].id, 'string');
t.type(result[1].opcode, 'string');
t.type(result[1].fields, 'object');
t.type(result[1].inputs, 'object');
t.type(result[1].topLevel, 'boolean');
t.equal(result[1].topLevel, false);
t.equal(result[1].next, null);
t.end();
});
test('create with obscured shadow', function (t) {
var result = adapter(events.createobscuredshadow);
t.ok(Array.isArray(result));
t.equal(result.length, 4);
t.end();
});
test('create with invalid block xml', function (t) {
// Entirely invalid block XML
var result = adapter(events.createinvalid);
t.ok(Array.isArray(result));
t.equal(result.length, 0);
// Invalid grandchild tag
var result2 = adapter(events.createinvalidgrandchild);
t.ok(Array.isArray(result2));
t.equal(result2.length, 1);
t.type(result2[0].id, 'string');
t.equal(Object.keys(result2[0].inputs).length, 0);
t.equal(Object.keys(result2[0].fields).length, 0);
t.end();
});
test('create with invalid xml', function (t) {
var result = adapter(events.createbadxml);
t.ok(Array.isArray(result));
t.equal(result.length, 0);
t.end();
});
test('create with empty field', function (t) {
var result = adapter(events.createemptyfield);
t.ok(Array.isArray(result));
t.equal(result.length, 3);
t.end();
});

544
test/unit/engine_blocks.js Normal file
View file

@ -0,0 +1,544 @@
var test = require('tap').test;
var Blocks = require('../../src/engine/blocks');
test('spec', function (t) {
var b = new Blocks();
t.type(Blocks, 'function');
t.type(b, 'object');
t.ok(b instanceof Blocks);
t.type(b._blocks, 'object');
t.type(b._scripts, 'object');
t.ok(Array.isArray(b._scripts));
t.type(b.createBlock, 'function');
t.type(b.moveBlock, 'function');
t.type(b.changeBlock, 'function');
t.type(b.deleteBlock, 'function');
t.type(b.getBlock, 'function');
t.type(b.getScripts, 'function');
t.type(b.getNextBlock, 'function');
t.type(b.getBranch, 'function');
t.type(b.getOpcode, 'function');
t.end();
});
// Getter tests
test('getBlock', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
var block = b.getBlock('foo');
t.type(block, 'object');
var notBlock = b.getBlock('?');
t.type(notBlock, 'undefined');
t.end();
});
test('getScripts', function (t) {
var b = new Blocks();
var scripts = b.getScripts();
t.type(scripts, 'object');
t.equals(scripts.length, 0);
// Create two top-level blocks and one not.
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
b.createBlock({
id: 'foo2',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
b.createBlock({
id: 'foo3',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: false
});
scripts = b.getScripts();
t.type(scripts, 'object');
t.equals(scripts.length, 2);
t.ok(scripts.indexOf('foo') > -1);
t.ok(scripts.indexOf('foo2') > -1);
t.equals(scripts.indexOf('foo3'), -1);
t.end();
});
test('getNextBlock', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
var next = b.getNextBlock('foo');
t.equals(next, null);
// Add a block with "foo" as its next.
b.createBlock({
id: 'foo2',
opcode: 'TEST_BLOCK',
next: 'foo',
fields: {},
inputs: {},
topLevel: true
});
next = b.getNextBlock('foo2');
t.equals(next, 'foo');
// Block that doesn't exist.
var noBlock = b.getNextBlock('?');
t.equals(noBlock, null);
t.end();
});
test('getBranch', function (t) {
var b = new Blocks();
// Single branch
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {
SUBSTACK: {
name: 'SUBSTACK',
block: 'foo2',
shadow: null
}
},
topLevel: true
});
b.createBlock({
id: 'foo2',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: false
});
var branch = b.getBranch('foo');
t.equals(branch, 'foo2');
var notBranch = b.getBranch('?');
t.equals(notBranch, null);
t.end();
});
test('getBranch2', function (t) {
var b = new Blocks();
// Second branch
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {
SUBSTACK: {
name: 'SUBSTACK',
block: 'foo2',
shadow: null
},
SUBSTACK2: {
name: 'SUBSTACK2',
block: 'foo3',
shadow: null
}
},
topLevel: true
});
b.createBlock({
id: 'foo2',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: false
});
b.createBlock({
id: 'foo3',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: false
});
var branch1 = b.getBranch('foo', 1);
var branch2 = b.getBranch('foo', 2);
t.equals(branch1, 'foo2');
t.equals(branch2, 'foo3');
t.end();
});
test('getBranch with none', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
var noBranch = b.getBranch('foo');
t.equals(noBranch, null);
t.end();
});
test('getOpcode', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
var opcode = b.getOpcode('foo');
t.equals(opcode, 'TEST_BLOCK');
var notOpcode = b.getOpcode('?');
t.equals(notOpcode, null);
t.end();
});
// Block events tests
test('create', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
t.type(b._blocks['foo'], 'object');
t.equal(b._blocks['foo'].opcode, 'TEST_BLOCK');
t.notEqual(b._scripts.indexOf('foo'), -1);
t.end();
});
test('move', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
b.createBlock({
id: 'bar',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
// Attach 'bar' to the end of 'foo'
b.moveBlock({
id: 'bar',
newParent: 'foo'
});
t.equal(b._scripts.length, 1);
t.equal(Object.keys(b._blocks).length, 2);
t.equal(b._blocks['foo'].next, 'bar');
// Detach 'bar' from 'foo'
b.moveBlock({
id: 'bar',
oldParent: 'foo'
});
t.equal(b._scripts.length, 2);
t.equal(Object.keys(b._blocks).length, 2);
t.equal(b._blocks['foo'].next, null);
t.end();
});
test('move into empty', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
b.createBlock({
id: 'bar',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
b.moveBlock({
id: 'bar',
newInput: 'fooInput',
newParent: 'foo'
});
t.equal(b._blocks['foo'].inputs['fooInput'].block, 'bar');
t.end();
});
test('move no obscure shadow', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {
'fooInput': {
name: 'fooInput',
block: 'x',
shadow: 'y'
}
},
topLevel: true
});
b.createBlock({
id: 'bar',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
b.moveBlock({
id: 'bar',
newInput: 'fooInput',
newParent: 'foo'
});
t.equal(b._blocks['foo'].inputs['fooInput'].block, 'bar');
t.equal(b._blocks['foo'].inputs['fooInput'].shadow, 'y');
t.end();
});
test('change', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {
someField: {
name: 'someField',
value: 'initial-value'
}
},
inputs: {},
topLevel: true
});
// Test that the field is updated
t.equal(b._blocks['foo'].fields.someField.value, 'initial-value');
b.changeBlock({
element: 'field',
id: 'foo',
name: 'someField',
value: 'final-value'
});
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
// Invalid cases
// No `element`
b.changeBlock({
id: 'foo',
name: 'someField',
value: 'invalid-value'
});
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
// No block ID
b.changeBlock({
element: 'field',
name: 'someField',
value: 'invalid-value'
});
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
// No such field
b.changeBlock({
element: 'field',
id: 'foo',
name: 'someWrongField',
value: 'final-value'
});
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
t.end();
});
test('delete', function (t) {
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: true
});
b.deleteBlock({
id: 'foo'
});
t.type(b._blocks['foo'], 'undefined');
t.equal(b._scripts.indexOf('foo'), -1);
t.end();
});
test('delete chain', function (t) {
// Create a chain of connected blocks and delete the top one.
// All of them should be deleted.
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: 'foo2',
fields: {},
inputs: {},
topLevel: true
});
b.createBlock({
id: 'foo2',
opcode: 'TEST_BLOCK',
next: 'foo3',
fields: {},
inputs: {},
topLevel: false
});
b.createBlock({
id: 'foo3',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: false
});
b.deleteBlock({
id: 'foo'
});
t.type(b._blocks['foo'], 'undefined');
t.type(b._blocks['foo2'], 'undefined');
t.type(b._blocks['foo3'], 'undefined');
t.equal(b._scripts.indexOf('foo'), -1);
t.equal(Object.keys(b._blocks).length, 0);
t.equal(b._scripts.length, 0);
t.end();
});
test('delete inputs', function (t) {
// Create a block with two inputs, one of which has its own input.
// Delete the block - all of them should be deleted.
var b = new Blocks();
b.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {
input1: {
name: 'input1',
block: 'foo2',
shadow: 'foo2'
},
SUBSTACK: {
name: 'SUBSTACK',
block: 'foo3',
shadow: null
}
},
topLevel: true
});
b.createBlock({
id: 'foo2',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: false
});
b.createBlock({
id: 'foo5',
opcode: 'TEST_OBSCURED_SHADOW',
next: null,
fields: {},
inputs: {},
topLevel: false
});
b.createBlock({
id: 'foo3',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {
subinput: {
name: 'subinput',
block: 'foo4',
shadow: 'foo5'
}
},
topLevel: false
});
b.createBlock({
id: 'foo4',
opcode: 'TEST_BLOCK',
next: null,
fields: {},
inputs: {},
topLevel: false
});
b.deleteBlock({
id: 'foo'
});
t.type(b._blocks['foo'], 'undefined');
t.type(b._blocks['foo2'], 'undefined');
t.type(b._blocks['foo3'], 'undefined');
t.type(b._blocks['foo4'], 'undefined');
t.type(b._blocks['foo5'], 'undefined');
t.equal(b._scripts.indexOf('foo'), -1);
t.equal(Object.keys(b._blocks).length, 0);
t.equal(b._scripts.length, 0);
t.end();
});

View file

@ -0,0 +1,12 @@
var test = require('tap').test;
var Runtime = require('../../src/engine/runtime');
test('spec', function (t) {
var r = new Runtime();
t.type(Runtime, 'function');
t.type(r, 'object');
t.ok(r instanceof Runtime);
t.end();
});

88
test/unit/import_sb2.js Normal file
View file

@ -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();
});

View file

@ -1,89 +0,0 @@
var test = require('tap').test;
var Runtime = require('../../src/engine/runtime');
test('spec', function (t) {
var r = new Runtime();
t.type(Runtime, 'function');
t.type(r, 'object');
t.ok(r instanceof Runtime);
t.type(r.blocks, 'object');
t.type(r.stacks, 'object');
t.ok(Array.isArray(r.stacks));
t.type(r.createBlock, 'function');
t.type(r.moveBlock, 'function');
t.type(r.changeBlock, 'function');
t.type(r.deleteBlock, 'function');
t.end();
});
test('create', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {}
});
t.type(r.blocks['foo'], 'object');
t.equal(r.blocks['foo'].opcode, 'TEST_BLOCK');
t.notEqual(r.stacks.indexOf('foo'), -1);
t.end();
});
test('move', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {}
});
r.createBlock({
id: 'bar',
opcode: 'TEST_BLOCK',
next: null,
fields: {}
});
// Attach 'bar' to the end of 'foo'
r.moveBlock({
id: 'bar',
newParent: 'foo'
});
t.equal(r.stacks.length, 1);
t.equal(Object.keys(r.blocks).length, 2);
t.equal(r.blocks['foo'].next, 'bar');
// Detach 'bar' from 'foo'
r.moveBlock({
id: 'bar',
oldParent: 'foo'
});
t.equal(r.stacks.length, 2);
t.equal(Object.keys(r.blocks).length, 2);
t.equal(r.blocks['foo'].next, null);
t.end();
});
test('delete', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK',
next: null,
fields: {}
});
r.deleteBlock({
id: 'foo'
});
t.type(r.blocks['foo'], 'undefined');
t.equal(r.stacks.indexOf('foo'), -1);
t.end();
});

View file

@ -0,0 +1,13 @@
var test = require('tap').test;
var Clone = require('../../src/sprites/clone');
var Sprite = require('../../src/sprites/sprite');
test('clone effects', function (t) {
// Create two clones and ensure they have different graphic effect objects.
// Regression test for Github issue #224
var spr = new Sprite();
var a = new Clone(spr, null);
var b = new Clone(spr, null);
t.ok(a.effects !== b.effects);
t.end();
});

179
test/unit/util_cast.js Normal file
View file

@ -0,0 +1,179 @@
var test = require('tap').test;
var cast = require('../../src/util/cast');
test('toNumber', function (t) {
// Numeric
t.strictEqual(cast.toNumber(0), 0);
t.strictEqual(cast.toNumber(1), 1);
t.strictEqual(cast.toNumber(3.14), 3.14);
// String
t.strictEqual(cast.toNumber('0'), 0);
t.strictEqual(cast.toNumber('1'), 1);
t.strictEqual(cast.toNumber('3.14'), 3.14);
t.strictEqual(cast.toNumber('0.1e10'), 1000000000);
t.strictEqual(cast.toNumber('foobar'), 0);
// Boolean
t.strictEqual(cast.toNumber(true), 1);
t.strictEqual(cast.toNumber(false), 0);
t.strictEqual(cast.toNumber('true'), 0);
t.strictEqual(cast.toNumber('false'), 0);
// Undefined & object
t.strictEqual(cast.toNumber(undefined), 0);
t.strictEqual(cast.toNumber({}), 0);
t.strictEqual(cast.toNumber(NaN), 0);
t.end();
});
test('toBoolean', function (t) {
// Numeric
t.strictEqual(cast.toBoolean(0), false);
t.strictEqual(cast.toBoolean(1), true);
t.strictEqual(cast.toBoolean(3.14), true);
// String
t.strictEqual(cast.toBoolean('0'), false);
t.strictEqual(cast.toBoolean('1'), true);
t.strictEqual(cast.toBoolean('3.14'), true);
t.strictEqual(cast.toBoolean('0.1e10'), true);
t.strictEqual(cast.toBoolean('foobar'), true);
// Boolean
t.strictEqual(cast.toBoolean(true), true);
t.strictEqual(cast.toBoolean(false), false);
// Undefined & object
t.strictEqual(cast.toBoolean(undefined), false);
t.strictEqual(cast.toBoolean({}), true);
t.end();
});
test('toString', function (t) {
// Numeric
t.strictEqual(cast.toString(0), '0');
t.strictEqual(cast.toString(1), '1');
t.strictEqual(cast.toString(3.14), '3.14');
// String
t.strictEqual(cast.toString('0'), '0');
t.strictEqual(cast.toString('1'), '1');
t.strictEqual(cast.toString('3.14'), '3.14');
t.strictEqual(cast.toString('0.1e10'), '0.1e10');
t.strictEqual(cast.toString('foobar'), 'foobar');
// Boolean
t.strictEqual(cast.toString(true), 'true');
t.strictEqual(cast.toString(false), 'false');
// Undefined & object
t.strictEqual(cast.toString(undefined), 'undefined');
t.strictEqual(cast.toString({}), '[object Object]');
t.end();
});
test('toRbgColorList', function (t) {
// Hex (minimal, see "color" util tests)
t.deepEqual(cast.toRgbColorList('#000'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('#000000'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('#fff'), [255,255,255]);
t.deepEqual(cast.toRgbColorList('#ffffff'), [255,255,255]);
// Decimal (minimal, see "color" util tests)
t.deepEqual(cast.toRgbColorList(0), [0,0,0]);
t.deepEqual(cast.toRgbColorList(1), [0,0,1]);
t.deepEqual(cast.toRgbColorList(16777215), [255,255,255]);
// Malformed
t.deepEqual(cast.toRgbColorList('ffffff'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('foobar'), [0,0,0]);
t.end();
});
test('compare', function (t) {
// Numeric
t.strictEqual(cast.compare(0, 0), 0);
t.strictEqual(cast.compare(1, 0), 1);
t.strictEqual(cast.compare(0, 1), -1);
t.strictEqual(cast.compare(1, 1), 0);
// String
t.strictEqual(cast.compare('0', '0'), 0);
t.strictEqual(cast.compare('0.1e10', '1000000000'), 0);
t.strictEqual(cast.compare('foobar', 'FOOBAR'), 0);
t.ok(cast.compare('dog', 'cat') > 0);
// Boolean
t.strictEqual(cast.compare(true, true), 0);
t.strictEqual(cast.compare(true, false), 1);
t.strictEqual(cast.compare(false, true), -1);
t.strictEqual(cast.compare(true, true), 0);
// Undefined & object
t.strictEqual(cast.compare(undefined, undefined), 0);
t.strictEqual(cast.compare(undefined, 'undefined'), 0);
t.strictEqual(cast.compare({}, {}), 0);
t.strictEqual(cast.compare({}, '[object Object]'), 0);
t.end();
});
test('isInt', function (t) {
// Numeric
t.strictEqual(cast.isInt(0), true);
t.strictEqual(cast.isInt(1), true);
t.strictEqual(cast.isInt(0.0), true);
t.strictEqual(cast.isInt(3.14), false);
t.strictEqual(cast.isInt(NaN), true);
// String
t.strictEqual(cast.isInt('0'), true);
t.strictEqual(cast.isInt('1'), true);
t.strictEqual(cast.isInt('0.0'), false);
t.strictEqual(cast.isInt('0.1e10'), false);
t.strictEqual(cast.isInt('3.14'), false);
// Boolean
t.strictEqual(cast.isInt(true), true);
t.strictEqual(cast.isInt(false), true);
// Undefined & object
t.strictEqual(cast.isInt(undefined), false);
t.strictEqual(cast.isInt({}), false);
t.end();
});
test('toListIndex', function (t) {
var list = [0,1,2,3,4,5];
var empty = [];
// Valid
t.strictEqual(cast.toListIndex(1, list.length), 1);
t.strictEqual(cast.toListIndex(6, list.length), 6);
// Invalid
t.strictEqual(cast.toListIndex(-1, list.length), cast.LIST_INVALID);
t.strictEqual(cast.toListIndex(0.1, list.length), cast.LIST_INVALID);
t.strictEqual(cast.toListIndex(0, list.length), cast.LIST_INVALID);
t.strictEqual(cast.toListIndex(7, list.length), cast.LIST_INVALID);
// "all"
t.strictEqual(cast.toListIndex('all', list.length), cast.LIST_ALL);
// "last"
t.strictEqual(cast.toListIndex('last', list.length), list.length);
t.strictEqual(cast.toListIndex('last', empty.length), cast.LIST_INVALID);
// "random"
var random = cast.toListIndex('random', list.length);
t.ok(random <= list.length);
t.ok(random > 0);
t.strictEqual(cast.toListIndex('random', empty.length), cast.LIST_INVALID);
// "any" (alias for "random")
var any = cast.toListIndex('any', list.length);
t.ok(any <= list.length);
t.ok(any > 0);
t.strictEqual(cast.toListIndex('any', empty.length), cast.LIST_INVALID);
t.end();
});

62
test/unit/util_color.js Normal file
View file

@ -0,0 +1,62 @@
var test = require('tap').test;
var color = require('../../src/util/color');
test('decimalToHex', function (t) {
t.strictEqual(color.decimalToHex(0), '#000000');
t.strictEqual(color.decimalToHex(1), '#000001');
t.strictEqual(color.decimalToHex(16777215), '#ffffff');
t.strictEqual(color.decimalToHex(-16777215), '#000001');
t.strictEqual(color.decimalToHex(99999999), '#5f5e0ff');
t.end();
});
test('decimalToRgb', function (t) {
t.deepEqual(color.decimalToRgb(0), {r:0,g:0,b:0});
t.deepEqual(color.decimalToRgb(1), {r:0,g:0,b:1});
t.deepEqual(color.decimalToRgb(16777215), {r:255,g:255,b:255});
t.deepEqual(color.decimalToRgb(-16777215), {r:0,g:0,b:1});
t.deepEqual(color.decimalToRgb(99999999), {r:245,g:224,b:255});
t.end();
});
test('hexToRgb', function (t) {
t.deepEqual(color.hexToRgb('#000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('#000000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('#fff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('#ffffff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('#0fa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('#00ffaa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('fff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('00ffaa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('0'), null);
t.deepEqual(color.hexToRgb('hello world'), null);
t.end();
});
test('rgbToHex', function (t) {
t.strictEqual(color.rgbToHex({r:0,g:0,b:0}), '#000000');
t.strictEqual(color.rgbToHex({r:255,g:255,b:255}), '#ffffff');
t.strictEqual(color.rgbToHex({r:0,g:255,b:170}), '#00ffaa');
t.end();
});
test('rgbToDecimal', function (t) {
t.strictEqual(color.rgbToDecimal({r:0,g:0,b:0}), 0);
t.strictEqual(color.rgbToDecimal({r:255,g:255,b:255}), 16777215);
t.strictEqual(color.rgbToDecimal({r:0,g:255,b:170}), 65450);
t.end();
});
test('hexToDecimal', function (t) {
t.strictEqual(color.hexToDecimal('#000'), 0);
t.strictEqual(color.hexToDecimal('#000000'), 0);
t.strictEqual(color.hexToDecimal('#fff'), 16777215);
t.strictEqual(color.hexToDecimal('#ffffff'), 16777215);
t.strictEqual(color.hexToDecimal('#0fa'), 65450);
t.strictEqual(color.hexToDecimal('#00ffaa'), 65450);
t.end();
});

36
test/unit/util_math.js Normal file
View file

@ -0,0 +1,36 @@
var test = require('tap').test;
var math = require('../../src/util/math-util');
test('degToRad', function (t) {
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();
});
test('radToDeg', function (t) {
t.strictEqual(math.radToDeg(0), 0);
t.strictEqual(math.radToDeg(1), 57.29577951308232);
t.strictEqual(math.radToDeg(180), 10313.240312354817);
t.strictEqual(math.radToDeg(360), 20626.480624709635);
t.strictEqual(math.radToDeg(720), 41252.96124941927);
t.end();
});
test('clamp', function (t) {
t.strictEqual(math.clamp(0, 0, 10), 0);
t.strictEqual(math.clamp(1, 0, 10), 1);
t.strictEqual(math.clamp(-10, 0, 10), 0);
t.strictEqual(math.clamp(100, 0, 10), 10);
t.end();
});
test('wrapClamp', function (t) {
t.strictEqual(math.wrapClamp(0, 0, 10), 0);
t.strictEqual(math.wrapClamp(1, 0, 10), 1);
t.strictEqual(math.wrapClamp(-10, 0, 10), 1);
t.strictEqual(math.wrapClamp(100, 0, 10), 1);
t.end();
});

9
test/unit/util_xml.js Normal file
View file

@ -0,0 +1,9 @@
var test = require('tap').test;
var xml = require('../../src/util/xml-escape');
test('escape', function (t) {
var input = '<foo bar="he & llo \'"></foo>';
var output = '&lt;foo bar=&quot;he &amp; llo &apos;&quot;&gt;&lt;/foo&gt;';
t.strictEqual(xml(input), output);
t.end();
});

13805
vm.js

File diff suppressed because it is too large Load diff

11
vm.min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,12 @@
var CopyWebpackPlugin = require('copy-webpack-plugin');
var defaultsDeep = require('lodash.defaultsdeep');
var path = require('path');
var webpack = require('webpack'); var webpack = require('webpack');
module.exports = { var base = {
entry: { devServer: {
'vm': './src/index.js', contentBase: path.resolve(__dirname, 'playground'),
'vm.min': './src/index.js' host: '0.0.0.0'
},
output: {
path: __dirname,
filename: '[name].js'
}, },
module: { module: {
loaders: [ loaders: [
@ -27,3 +26,93 @@ module.exports = {
}) })
] ]
}; };
module.exports = [
// Web-compatible, playground
defaultsDeep({}, base, {
entry: {
'vm': './src/index.js',
'vm.min': './src/index.js'
},
output: {
path: __dirname,
filename: '[name].js'
},
module: {
loaders: base.module.loaders.concat([
{
test: require.resolve('./src/index.js'),
loader: 'expose?VirtualMachine'
}
])
}
}),
// Webpack-compatible
defaultsDeep({}, base, {
entry: {
'dist': './src/index.js'
},
output: {
library: 'VirtualMachine',
libraryTarget: 'commonjs2',
path: __dirname,
filename: '[name].js'
}
}),
// Playground
defaultsDeep({}, base, {
entry: {
'vm': './src/index.js',
'vendor': [
// FPS counter
'stats.js/build/stats.min.js',
// Syntax highlighter
'highlightjs/highlight.pack.min.js',
// Scratch Blocks
'scratch-blocks/dist/vertical.js',
// Renderer
'scratch-render'
]
},
output: {
path: path.resolve(__dirname, 'playground'),
filename: '[name].js'
},
module: {
loaders: base.module.loaders.concat([
{
test: require.resolve('./src/index.js'),
loader: 'expose?VirtualMachine'
},
{
test: require.resolve('stats.js/build/stats.min.js'),
loader: 'script'
},
{
test: require.resolve('highlightjs/highlight.pack.min.js'),
loader: 'script'
},
{
test: require.resolve('scratch-blocks/dist/vertical.js'),
loader: 'expose?Blockly'
},
{
test: require.resolve('scratch-render'),
loader: 'expose?RenderWebGL'
}
])
},
plugins: base.plugins.concat([
new CopyWebpackPlugin([{
from: 'node_modules/scratch-blocks/media',
to: 'media'
}, {
from: 'node_modules/highlightjs/styles/zenburn.css'
}, {
from: 'assets',
to: 'assets'
}])
])
})
];