mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-26 07:52:50 -05:00
Merge branch 'develop' of https://github.com/llk/scratch-vm
This commit is contained in:
commit
052cae0176
74 changed files with 9177 additions and 14908 deletions
11
.editorconfig
Normal file
11
.editorconfig
Normal 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
|
|
@ -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
15
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
8
.gitignore
vendored
|
@ -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
2
.npmignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/.nyc_output
|
||||||
|
/coverage
|
31
.travis.yml
31
.travis.yml
|
@ -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
25
CONTRIBUTING.md
Normal 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
|
6
Makefile
6
Makefile
|
@ -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
113
README.md
|
@ -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
2
StartServerWindows.bat
Normal 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
42
assets/scratch_cat.svg
Normal 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
		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
		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
		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
		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
		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
		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
		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
		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
	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
	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
	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
		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
		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
		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
		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
		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
	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
	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
		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
		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
		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
		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
		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
		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
		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
		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
		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
		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
		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
		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
		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
		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
	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
		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
BIN
assets/stage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
28
package.json
28
package.json
|
@ -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
89
playground/index.html
Normal 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()">
|
||||||
|
|
||||||
|
<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
73
playground/playground.css
Normal 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
312
playground/playground.js
Normal 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';
|
||||||
|
});
|
||||||
|
};
|
|
@ -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;
|
|
139
src/blocks/scratch3_control.js
Normal file
139
src/blocks/scratch3_control.js
Normal 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
136
src/blocks/scratch3_data.js
Normal 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;
|
89
src/blocks/scratch3_event.js
Normal file
89
src/blocks/scratch3_event.js
Normal 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;
|
221
src/blocks/scratch3_looks.js
Normal file
221
src/blocks/scratch3_looks.js
Normal 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;
|
234
src/blocks/scratch3_motion.js
Normal file
234
src/blocks/scratch3_motion.js
Normal 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;
|
143
src/blocks/scratch3_operators.js
Normal file
143
src/blocks/scratch3_operators.js
Normal 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;
|
44
src/blocks/scratch3_procedures.js
Normal file
44
src/blocks/scratch3_procedures.js
Normal 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;
|
128
src/blocks/scratch3_sensing.js
Normal file
128
src/blocks/scratch3_sensing.js
Normal 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;
|
|
@ -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;
|
|
|
@ -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
494
src/engine/blocks.js
Normal 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
248
src/engine/execute.js
Normal 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
16
src/engine/list.js
Normal 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;
|
39
src/engine/mutation-adapter.js
Normal file
39
src/engine/mutation-adapter.js
Normal 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
|
@ -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
116
src/engine/target.js
Normal 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;
|
|
@ -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
18
src/engine/variable.js
Normal 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
418
src/import/sb2import.js
Normal 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
1388
src/import/sb2specmap.js
Normal file
File diff suppressed because it is too large
Load diff
349
src/index.js
349
src/index.js
|
@ -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
37
src/io/clock.js
Normal 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
85
src/io/keyboard.js
Normal 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
57
src/io/mouse.js
Normal 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
588
src/sprites/clone.js
Normal 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
57
src/sprites/sprite.js
Normal 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
163
src/util/cast.js
Normal 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
76
src/util/color.js
Normal 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
48
src/util/math-util.js
Normal 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;
|
|
@ -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
29
src/util/uid.js
Normal 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
21
src/util/xml-escape.js
Normal 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 '<';
|
||||||
|
case '>': return '>';
|
||||||
|
case '&': return '&';
|
||||||
|
case '\'': return ''';
|
||||||
|
case '"': return '"';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = xmlEscape;
|
|
@ -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
71
test/fixtures/default.json
vendored
Normal 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
359
test/fixtures/demo.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
63
test/fixtures/events.json
vendored
63
test/fixtures/events.json
vendored
|
@ -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>"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
175
test/unit/blocks_operators.js
Normal file
175
test/unit/blocks_operators.js
Normal 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
187
test/unit/engine_adapter.js
Normal 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
544
test/unit/engine_blocks.js
Normal 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();
|
||||||
|
});
|
12
test/unit/engine_runtime.js
Normal file
12
test/unit/engine_runtime.js
Normal 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
88
test/unit/import_sb2.js
Normal 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();
|
||||||
|
});
|
|
@ -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();
|
|
||||||
});
|
|
13
test/unit/sprites_clone.js
Normal file
13
test/unit/sprites_clone.js
Normal 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
179
test/unit/util_cast.js
Normal 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
62
test/unit/util_color.js
Normal 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
36
test/unit/util_math.js
Normal 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
9
test/unit/util_xml.js
Normal 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 = '<foo bar="he & llo '"></foo>';
|
||||||
|
t.strictEqual(xml(input), output);
|
||||||
|
t.end();
|
||||||
|
});
|
11
vm.min.js
vendored
11
vm.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -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'
|
||||||
|
}])
|
||||||
|
])
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
Loading…
Reference in a new issue