Merge branch 'develop' of https://github.com/LLK/scratch-vm into scroll-detection
This commit is contained in:
commit
b201ce948d
224 files changed
+20567
-7839
No files matched your search
@@ -0,0 +1,5 @@
|
||||
coverage/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
playground/*
|
||||
benchmark/*
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"curly": [2, "multi-line"],
|
||||
"eol-last": [2],
|
||||
"indent": [2, 4],
|
||||
"quotes": [2, "single"],
|
||||
"linebreak-style": [2, "unix"],
|
||||
"max-len": [2, 80, 4],
|
||||
"semi": [2, "always"],
|
||||
"strict": [2, "never"],
|
||||
"no-console": [2, {"allow": ["log", "warn", "error", "groupCollapsed", "groupEnd", "time", "timeEnd"]}],
|
||||
"valid-jsdoc": ["error", {"requireReturn": false}]
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"worker": true
|
||||
},
|
||||
"extends": "eslint:recommended"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['scratch', 'scratch/node', 'scratch/es6']
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Explicitly specify line endings for as many files as possible.
|
||||
# People who (for example) rsync between Windows and Linux need this.
|
||||
|
||||
# File types which we know are binary
|
||||
*.sb2 binary
|
||||
|
||||
# Prefer LF for most file types
|
||||
*.css text eol=lf
|
||||
*.frag text eol=lf
|
||||
*.htm text eol=lf
|
||||
*.html text eol=lf
|
||||
*.iml text eol=lf
|
||||
*.js text eol=lf
|
||||
*.js.map text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.vert text eol=lf
|
||||
*.xml text eol=lf
|
||||
*.yml text eol=lf
|
||||
|
||||
# Prefer LF for these files
|
||||
.editorconfig text eol=lf
|
||||
.eslintignore text eol=lf
|
||||
.eslintrc text eol=lf
|
||||
.gitattributes text eol=lf
|
||||
.gitignore text eol=lf
|
||||
.gitmodules text eol=lf
|
||||
.npmignore text eol=lf
|
||||
LICENSE text eol=lf
|
||||
Makefile text eol=lf
|
||||
README text eol=lf
|
||||
TRADEMARK text eol=lf
|
||||
|
||||
# Use CRLF for Windows-specific file types
|
||||
@@ -0,0 +1,13 @@
|
||||
## Contributing
|
||||
The development of Scratch 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 looking for bugs to fix, please look for the ["Help Wanted" label](https://github.com/LLK/scratch-vm/issues?q=label%3A%22help+wanted%22). Bugs with this label have been specifically set aside for Open Source contributors. Issues without the label can also be worked on but we ask that you comment on the issue prior to starting work. 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
|
||||
@@ -1,15 +1,15 @@
|
||||
### Expected behavior
|
||||
### Expected Behavior
|
||||
|
||||
_Please describe what should happen_
|
||||
|
||||
### Actual behavior
|
||||
### Actual Behavior
|
||||
|
||||
_Describe what actually happens_
|
||||
|
||||
### Steps to reproduce
|
||||
### Steps to Reproduce
|
||||
|
||||
_Explain what someone needs to do in order to see what's described in *Actual behavior* above_
|
||||
|
||||
### Operating system and browser
|
||||
### Operating System and Browser
|
||||
|
||||
_e.g. Mac OS 10.11.6 Safari 10.0_
|
||||
@@ -1,7 +1,15 @@
|
||||
### Proposed changes
|
||||
### Resolves
|
||||
|
||||
_What Github issue does this resolve (please include link)?_
|
||||
|
||||
### Proposed Changes
|
||||
|
||||
_Describe what this Pull Request does_
|
||||
|
||||
### Reason for changes
|
||||
### Reason for Changes
|
||||
|
||||
_Explain why these changes should be made. Please include an issue # if applicable._
|
||||
_Explain why these changes should be made_
|
||||
|
||||
### Test Coverage
|
||||
|
||||
_Please show how you have added tests to cover your changes_
|
||||
+12
-8
@@ -2,17 +2,21 @@
|
||||
.DS_Store
|
||||
|
||||
# NPM
|
||||
package-lock.json
|
||||
/node_modules
|
||||
npm-*
|
||||
|
||||
# Testing
|
||||
/.nyc_output
|
||||
/coverage
|
||||
/dist.js
|
||||
/vm.js
|
||||
/vm.min.js
|
||||
/playground/assets
|
||||
/playground/media
|
||||
/playground/vendor.js
|
||||
/playground/vm.js
|
||||
/playground/zenburn.css
|
||||
|
||||
# IDEA
|
||||
/.idea
|
||||
|
||||
# Build
|
||||
/dist
|
||||
/playground
|
||||
/benchmark
|
||||
|
||||
# Localization
|
||||
/translations
|
||||
+34
-28
@@ -1,35 +1,41 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '4'
|
||||
- stable
|
||||
- 6
|
||||
- node
|
||||
env:
|
||||
- NPM_SCRIPT="tap:unit -- --jobs=4"
|
||||
- NPM_SCRIPT="tap:integration -- --jobs=4"
|
||||
- NODE_ENV=production
|
||||
sudo: false
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
install:
|
||||
- npm install
|
||||
- npm update
|
||||
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
|
||||
- npm --production=false install
|
||||
- npm --production=false update
|
||||
script: npm run $NPM_SCRIPT
|
||||
jobs:
|
||||
include:
|
||||
- env: NPM_SCRIPT=lint
|
||||
node_js: 6
|
||||
- stage: release
|
||||
node_js: 6
|
||||
env: NPM_SCRIPT=build
|
||||
before_deploy:
|
||||
- npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
|
||||
- git config --global user.email $(git log --pretty=format:"%ae" -n1)
|
||||
- git config --global user.name $(git log --pretty=format:"%an" -n1)
|
||||
deploy:
|
||||
- provider: script
|
||||
"on":
|
||||
all_branches: true
|
||||
condition: $RELEASE_BRANCHES =~ $TRAVIS_BRANCH
|
||||
skip_cleanup: true
|
||||
script: npm run --silent deploy -- -x -r $GH_PAGES_REPO
|
||||
- provider: npm
|
||||
"on":
|
||||
all_branches: true
|
||||
condition: $RELEASE_BRANCHES =~ $TRAVIS_BRANCH
|
||||
skip_cleanup: true
|
||||
email: $NPM_EMAIL
|
||||
api_key: $NPM_TOKEN
|
||||
@@ -0,0 +1,8 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[experimental-scratch.pen]
|
||||
file_filter = translations/pen/<lang>.json
|
||||
source_file = translations/pen/en.json
|
||||
source_lang = en
|
||||
type = CHROME
|
||||
@@ -1,25 +0,0 @@
|
||||
## 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
|
||||
@@ -1,34 +0,0 @@
|
||||
ESLINT=./node_modules/.bin/eslint
|
||||
NODE=node
|
||||
TAP=./node_modules/.bin/tap
|
||||
WEBPACK=./node_modules/.bin/webpack --progress --colors
|
||||
WEBPACK_DEV_SERVER=./node_modules/.bin/webpack-dev-server
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
build:
|
||||
$(WEBPACK)
|
||||
|
||||
watch:
|
||||
$(WEBPACK) --watch
|
||||
|
||||
serve:
|
||||
$(WEBPACK_DEV_SERVER)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
lint:
|
||||
$(ESLINT) ./src/*.js
|
||||
$(ESLINT) ./src/**/*.js
|
||||
$(ESLINT) ./test/**/*.js
|
||||
|
||||
test:
|
||||
@make lint
|
||||
$(TAP) ./test/{unit,integration}/*.js
|
||||
|
||||
coverage:
|
||||
$(TAP) ./test/{unit,integration}/*.js --coverage --coverage-report=lcov
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
.PHONY: build lint test coverage benchmark serve
|
||||
@@ -3,17 +3,16 @@
|
||||
|
||||
[](https://travis-ci.org/LLK/scratch-vm)
|
||||
[](https://coveralls.io/github/LLK/scratch-vm?branch=develop)
|
||||
[](https://david-dm.org/LLK/scratch-vm)
|
||||
[](https://david-dm.org/LLK/scratch-vm#info=devDependencies)
|
||||
[](https://greenkeeper.io/)
|
||||
|
||||
## Installation
|
||||
This requires you to have Git and Node.js installed.
|
||||
|
||||
In your own node environment/application:
|
||||
To install as a dependency for your own application:
|
||||
```bash
|
||||
npm install https://github.com/LLK/scratch-vm.git
|
||||
npm install scratch-vm
|
||||
```
|
||||
If you want to edit/play yourself:
|
||||
To set up a development environment to edit scratch-vm yourself:
|
||||
```bash
|
||||
git clone https://github.com/LLK/scratch-vm.git
|
||||
cd scratch-vm
|
||||
@@ -23,31 +22,27 @@ npm install
|
||||
## Development Server
|
||||
This requires Node.js to be installed.
|
||||
|
||||
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).
|
||||
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). If you would like to use your modified VM with the full Scratch 3.0 GUI, [follow the instructions to link the VM to the GUI](https://github.com/LLK/scratch-gui/wiki/Getting-Started).
|
||||
|
||||
## Running the Development Server
|
||||
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.
|
||||
To view the Playground, make sure the dev server's running and go to [http://localhost:8073/playground/](http://localhost:8073/playground/) - you will be directed to the playground, which demonstrates various tools and internal state.
|
||||
|
||||

|
||||
|
||||
|
||||
## Standalone Build
|
||||
```bash
|
||||
make build
|
||||
npm run build
|
||||
```
|
||||
|
||||
```html
|
||||
<script src="/path/to/vm.js"></script>
|
||||
<script src="/path/to/dist/web/scratch-vm.js"></script>
|
||||
<script>
|
||||
var vm = new window.VirtualMachine();
|
||||
// do things
|
||||
@@ -55,13 +50,13 @@ make build
|
||||
```
|
||||
|
||||
## 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.
|
||||
For an extended setup example, check out the /src/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);
|
||||
Scratch.workspace.addChangeListener(vm.blockListener);
|
||||
|
||||
// Run threads
|
||||
vm.start();
|
||||
@@ -104,11 +99,23 @@ The VM's block representation contains all the important information for executi
|
||||
|
||||
## Testing
|
||||
```bash
|
||||
make test
|
||||
npm test
|
||||
```
|
||||
|
||||
```bash
|
||||
make coverage
|
||||
npm run coverage
|
||||
```
|
||||
|
||||
## Publishing to GitHub Pages
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
This will push the currently built playground to the gh-pages branch of the
|
||||
currently tracked remote. If you would like to change where to push to, add
|
||||
a repo url argument:
|
||||
```bash
|
||||
npm run deploy -- -r <your repo url>
|
||||
```
|
||||
|
||||
## Donate
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
node_modules\.bin\webpack-dev-server --host 0.0.0.0 --content-base .\playground
|
||||
@@ -1,42 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
+50
-22
@@ -9,32 +9,60 @@
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/LLK/scratch-vm.git"
|
||||
},
|
||||
"main": "./dist.js",
|
||||
"main": "./dist/node/scratch-vm.js",
|
||||
"browser": "./dist/web/scratch-vm.js",
|
||||
"scripts": {
|
||||
"prepublish": "./node_modules/.bin/webpack --bail",
|
||||
"build": "webpack --progress --colors --bail",
|
||||
"coverage": "tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
|
||||
"deploy": "touch playground/.nojekyll && gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"",
|
||||
"extract:pen": "mkdirp translations/pen && format-message extract --out-file translations/pen/en.json src/extensions/scratch3_pen/index.js",
|
||||
"i18n:src": "npm run extract:pen",
|
||||
"lint": "eslint . && format-message lint src/**/*.js",
|
||||
"prepublish": "in-publish && npm run build || not-in-publish",
|
||||
"start": "webpack-dev-server",
|
||||
"build": "webpack --colors --progress",
|
||||
"test": "make test",
|
||||
"version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
||||
"tap": "tap ./test/{unit,integration}/*.js",
|
||||
"tap:unit": "tap ./test/unit/*.js",
|
||||
"tap:integration": "tap ./test/integration/*.js",
|
||||
"test": "npm run lint && npm run tap",
|
||||
"watch": "webpack --progress --colors --watch",
|
||||
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "3.0.1",
|
||||
"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",
|
||||
"adm-zip": "0.4.7",
|
||||
"babel-core": "^6.24.1",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^7.0.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"copy-webpack-plugin": "4.2.1",
|
||||
"decode-html": "2.0.0",
|
||||
"escape-html": "1.0.3",
|
||||
"eslint": "^4.5.0",
|
||||
"eslint-config-scratch": "^5.0.0",
|
||||
"expose-loader": "0.7.4",
|
||||
"format-message": "5.2.1",
|
||||
"format-message-cli": "5.2.1",
|
||||
"gh-pages": "^1.1.0",
|
||||
"highlightjs": "^9.8.0",
|
||||
"htmlparser2": "3.9.2",
|
||||
"immutable": "3.8.1",
|
||||
"in-publish": "^2.0.0",
|
||||
"json": "^9.0.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",
|
||||
"travis-after-all": "1.4.4",
|
||||
"webpack": "1.13.0",
|
||||
"webpack-dev-server": "1.14.1"
|
||||
"minilog": "3.1.0",
|
||||
"nets": "3.2.0",
|
||||
"promise": "8.0.1",
|
||||
"scratch-audio": "latest",
|
||||
"scratch-blocks": "latest",
|
||||
"scratch-render": "latest",
|
||||
"scratch-storage": "^0.3.0",
|
||||
"script-loader": "0.7.2",
|
||||
"socket.io-client": "2.0.4",
|
||||
"stats.js": "^0.17.0",
|
||||
"tap": "^11.0.1",
|
||||
"text-encoding": "0.6.4",
|
||||
"tiny-worker": "^2.1.1",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-dev-server": "^2.4.1",
|
||||
"worker-loader": "1.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,73 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
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';
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['scratch', 'scratch/es6'],
|
||||
env: {
|
||||
browser: true
|
||||
}
|
||||
};
|
||||
+112
-116
@@ -1,139 +1,135 @@
|
||||
var Cast = require('../util/cast');
|
||||
var Timer = require('../util/timer');
|
||||
const Cast = require('../util/cast');
|
||||
|
||||
class Scratch3ControlBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
function Scratch3ControlBlocks(runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
getPrimitives () {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
};
|
||||
getHats () {
|
||||
return {
|
||||
control_start_as_clone: {
|
||||
restartExistingThreads: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Scratch3ControlBlocks.prototype.getHats = function () {
|
||||
return {
|
||||
'control_start_as_clone': {
|
||||
restartExistingThreads: false
|
||||
repeat (args, util) {
|
||||
const times = Math.floor(Cast.toNumber(args.TIMES));
|
||||
// Initialize loop
|
||||
if (typeof 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.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);
|
||||
|
||||
repeatUntil (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
// If the condition is true, start the branch.
|
||||
if (!condition) {
|
||||
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) {
|
||||
waitUntil (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
if (!condition) {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.if = function(args, util) {
|
||||
var condition = Cast.toBoolean(args.CONDITION);
|
||||
if (condition) {
|
||||
util.startBranch(1, false);
|
||||
forever (args, util) {
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.ifElse = function(args, util) {
|
||||
var condition = Cast.toBoolean(args.CONDITION);
|
||||
if (condition) {
|
||||
util.startBranch(1, false);
|
||||
} else {
|
||||
util.startBranch(2, false);
|
||||
wait (args) {
|
||||
const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, duration);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
if (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
if (condition) {
|
||||
util.startBranch(1, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.createClone = function (args, util) {
|
||||
var cloneTarget;
|
||||
if (args.CLONE_OPTION == '_myself_') {
|
||||
cloneTarget = util.target;
|
||||
} else {
|
||||
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
|
||||
ifElse (args, util) {
|
||||
const condition = Cast.toBoolean(args.CONDITION);
|
||||
if (condition) {
|
||||
util.startBranch(1, false);
|
||||
} else {
|
||||
util.startBranch(2, false);
|
||||
}
|
||||
}
|
||||
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);
|
||||
};
|
||||
stop (args, util) {
|
||||
const 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.stopThisScript();
|
||||
}
|
||||
}
|
||||
|
||||
createClone (args, util) {
|
||||
let cloneTarget;
|
||||
if (args.CLONE_OPTION === '_myself_') {
|
||||
cloneTarget = util.target;
|
||||
} else {
|
||||
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
|
||||
}
|
||||
if (!cloneTarget) {
|
||||
return;
|
||||
}
|
||||
const newClone = cloneTarget.makeClone();
|
||||
if (newClone) {
|
||||
this.runtime.targets.push(newClone);
|
||||
}
|
||||
}
|
||||
|
||||
deleteClone (args, util) {
|
||||
if (util.target.isOriginal) return;
|
||||
this.runtime.disposeTarget(util.target);
|
||||
this.runtime.stopForTarget(util.target);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3ControlBlocks;
|
||||
+126
-113
@@ -1,136 +1,149 @@
|
||||
var Cast = require('../util/cast');
|
||||
const Cast = require('../util/cast');
|
||||
|
||||
class Scratch3DataBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
function Scratch3DataBlocks(runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
getPrimitives () {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
};
|
||||
getVariable (args, util) {
|
||||
const variable = util.target.lookupOrCreateVariable(
|
||||
args.VARIABLE.id, args.VARIABLE.name);
|
||||
return variable.value;
|
||||
}
|
||||
|
||||
Scratch3DataBlocks.prototype.getVariable = function (args, util) {
|
||||
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
|
||||
return variable.value;
|
||||
};
|
||||
setVariableTo (args, util) {
|
||||
const variable = util.target.lookupOrCreateVariable(
|
||||
args.VARIABLE.id, args.VARIABLE.name);
|
||||
variable.value = args.VALUE;
|
||||
}
|
||||
|
||||
Scratch3DataBlocks.prototype.setVariableTo = function (args, util) {
|
||||
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
|
||||
variable.value = args.VALUE;
|
||||
};
|
||||
changeVariableBy (args, util) {
|
||||
const variable = util.target.lookupOrCreateVariable(
|
||||
args.VARIABLE.id, args.VARIABLE.name);
|
||||
const castedValue = Cast.toNumber(variable.value);
|
||||
const dValue = Cast.toNumber(args.VALUE);
|
||||
variable.value = castedValue + dValue;
|
||||
}
|
||||
|
||||
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;
|
||||
getListContents (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
// 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.
|
||||
let allSingleLetters = true;
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
const listItem = list.value[i];
|
||||
if (!((typeof listItem === 'string') &&
|
||||
(listItem.length === 1))) {
|
||||
allSingleLetters = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allSingleLetters) {
|
||||
return list.contents.join('');
|
||||
} else {
|
||||
return list.contents.join(' ');
|
||||
}
|
||||
};
|
||||
if (allSingleLetters) {
|
||||
return list.value.join('');
|
||||
}
|
||||
return list.value.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;
|
||||
addToList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
list.value.push(args.ITEM);
|
||||
}
|
||||
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;
|
||||
deleteOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
} else if (index === Cast.LIST_ALL) {
|
||||
list.value = [];
|
||||
return;
|
||||
}
|
||||
list.value.splice(index - 1, 1);
|
||||
}
|
||||
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 '';
|
||||
insertAtList (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length + 1);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
}
|
||||
list.value.splice(index - 1, 0, item);
|
||||
}
|
||||
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;
|
||||
replaceItemOfList (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
}
|
||||
list.value.splice(index - 1, 1, item);
|
||||
}
|
||||
// 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) {
|
||||
|
||||
getItemOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return '';
|
||||
}
|
||||
return list.value[index - 1];
|
||||
}
|
||||
|
||||
lengthOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
return list.value.length;
|
||||
}
|
||||
|
||||
listContainsItem (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(
|
||||
args.LIST.id, args.LIST.name);
|
||||
if (list.value.indexOf(item) >= 0) {
|
||||
return true;
|
||||
}
|
||||
// Try using Scratch comparison operator on each item.
|
||||
// (Scratch considers the string '123' equal to the number 123).
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
if (Cast.compare(list.value[i], item) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Scratch3DataBlocks;
|
||||
@@ -1,89 +1,97 @@
|
||||
var Cast = require('../util/cast');
|
||||
const Cast = require('../util/cast');
|
||||
|
||||
class Scratch3EventBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
function Scratch3EventBlocks(runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
getPrimitives () {
|
||||
return {
|
||||
event_broadcast: this.broadcast,
|
||||
event_broadcastandwait: this.broadcastAndWait,
|
||||
event_whengreaterthan: this.hatGreaterThanPredicate
|
||||
};
|
||||
}
|
||||
|
||||
getHats () {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
hatGreaterThanPredicate (args, util) {
|
||||
const option = Cast.toString(args.WHENGREATERTHANMENU).toLowerCase();
|
||||
const value = Cast.toNumber(args.VALUE);
|
||||
// @todo: Other cases :)
|
||||
if (option === 'timer') {
|
||||
return util.ioQuery('clock', 'projectTimer') > value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
broadcast (args, util) {
|
||||
const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg(
|
||||
args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name);
|
||||
if (broadcastVar) {
|
||||
const broadcastOption = broadcastVar.name;
|
||||
util.startHats('event_whenbroadcastreceived', {
|
||||
BROADCAST_OPTION: broadcastOption
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
broadcastAndWait (args, util) {
|
||||
const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg(
|
||||
args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name);
|
||||
if (broadcastVar) {
|
||||
const broadcastOption = broadcastVar.name;
|
||||
// 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.
|
||||
const instance = this;
|
||||
const waiting = util.stackFrame.startedThreads.some(thread => instance.runtime.isActiveThread(thread));
|
||||
if (waiting) {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
+431
-200
@@ -1,221 +1,452 @@
|
||||
var Cast = require('../util/cast');
|
||||
const Cast = require('../util/cast');
|
||||
const Clone = require('../util/clone');
|
||||
const RenderedTarget = require('../sprites/rendered-target');
|
||||
|
||||
/**
|
||||
* @typedef {object} BubbleState - the bubble state associated with a particular target.
|
||||
* @property {Boolean} onSpriteRight - tracks whether the bubble is right or left of the sprite.
|
||||
* @property {?int} drawableId - the ID of the associated bubble Drawable, null if none.
|
||||
* @property {Boolean} drawableVisible - false if drawable has been hidden by blank text.
|
||||
* See _renderBubble for explanation of this optimization.
|
||||
* @property {string} text - the text of the bubble.
|
||||
* @property {string} type - the type of the bubble, "say" or "think"
|
||||
*/
|
||||
|
||||
class Scratch3LooksBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
this._onTargetMoved = this._onTargetMoved.bind(this);
|
||||
this._onResetBubbles = this._onResetBubbles.bind(this);
|
||||
this._onTargetWillExit = this._onTargetWillExit.bind(this);
|
||||
this._updateBubble = this._updateBubble.bind(this);
|
||||
|
||||
// Reset all bubbles on start/stop
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._onResetBubbles);
|
||||
this.runtime.on('targetWasRemoved', this._onTargetWillExit);
|
||||
|
||||
// Enable other blocks to use bubbles like ask/answer
|
||||
this.runtime.on('SAY', this._updateBubble);
|
||||
}
|
||||
|
||||
function Scratch3LooksBlocks(runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
* The default bubble state, to be used when a target has no existing bubble state.
|
||||
* @type {BubbleState}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
static get DEFAULT_BUBBLE_STATE () {
|
||||
return {
|
||||
drawableId: null,
|
||||
drawableVisible: true,
|
||||
onSpriteRight: true,
|
||||
skinId: null,
|
||||
text: '',
|
||||
type: 'say'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
};
|
||||
/**
|
||||
* The key to load & store a target's bubble-related state.
|
||||
* @type {string}
|
||||
*/
|
||||
static get STATE_KEY () {
|
||||
return 'Scratch.looks';
|
||||
}
|
||||
|
||||
Scratch3LooksBlocks.prototype.say = function (args, util) {
|
||||
util.target.setSay('say', args.MESSAGE);
|
||||
};
|
||||
/**
|
||||
* @param {Target} target - collect bubble state for this target. Probably, but not necessarily, a RenderedTarget.
|
||||
* @returns {BubbleState} the mutable bubble state associated with that target. This will be created if necessary.
|
||||
* @private
|
||||
*/
|
||||
_getBubbleState (target) {
|
||||
let bubbleState = target.getCustomState(Scratch3LooksBlocks.STATE_KEY);
|
||||
if (!bubbleState) {
|
||||
bubbleState = Clone.simple(Scratch3LooksBlocks.DEFAULT_BUBBLE_STATE);
|
||||
target.setCustomState(Scratch3LooksBlocks.STATE_KEY, bubbleState);
|
||||
}
|
||||
return bubbleState;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Handle a target which has moved.
|
||||
* @param {RenderedTarget} target - the target which has moved.
|
||||
* @private
|
||||
*/
|
||||
_onTargetMoved (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
if (bubbleState.drawableId) {
|
||||
this._positionBubble(target);
|
||||
}
|
||||
}
|
||||
|
||||
Scratch3LooksBlocks.prototype.think = function (args, util) {
|
||||
util.target.setSay('think', args.MESSAGE);
|
||||
};
|
||||
/**
|
||||
* Handle a target which is exiting.
|
||||
* @param {RenderedTarget} target - the target.
|
||||
* @private
|
||||
*/
|
||||
_onTargetWillExit (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
if (bubbleState.drawableId && bubbleState.skinId) {
|
||||
this.runtime.renderer.destroyDrawable(bubbleState.drawableId);
|
||||
this.runtime.renderer.destroySkin(bubbleState.skinId);
|
||||
bubbleState.drawableId = null;
|
||||
bubbleState.skinId = null;
|
||||
bubbleState.drawableVisible = true; // Reset back to default value
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
target.removeListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Handle project start/stop by clearing all visible bubbles.
|
||||
* @private
|
||||
*/
|
||||
_onResetBubbles () {
|
||||
for (let n = 0; n < this.runtime.targets.length; n++) {
|
||||
this._onTargetWillExit(this.runtime.targets[n]);
|
||||
}
|
||||
clearTimeout(this._bubbleTimeout);
|
||||
}
|
||||
|
||||
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);
|
||||
/**
|
||||
* Position the bubble of a target. If it doesn't fit on the specified side, flip and rerender.
|
||||
* @param {!Target} target Target whose bubble needs positioning.
|
||||
* @private
|
||||
*/
|
||||
_positionBubble (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
const [bubbleWidth, bubbleHeight] = this.runtime.renderer.getSkinSize(bubbleState.drawableId);
|
||||
const targetBounds = target.getBounds();
|
||||
const stageBounds = this.runtime.getTargetForStage().getBounds();
|
||||
if (bubbleState.onSpriteRight && bubbleWidth + targetBounds.right > stageBounds.right &&
|
||||
(targetBounds.left - bubbleWidth > stageBounds.left)) { // Only flip if it would fit
|
||||
bubbleState.onSpriteRight = false;
|
||||
this._renderBubble(target);
|
||||
} else if (!bubbleState.onSpriteRight && targetBounds.left - bubbleWidth < stageBounds.left &&
|
||||
(bubbleWidth + targetBounds.right < stageBounds.right)) { // Only flip if it would fit
|
||||
bubbleState.onSpriteRight = true;
|
||||
this._renderBubble(target);
|
||||
} else {
|
||||
var forcedNumber = Cast.toNumber(requestedCostume);
|
||||
if (!isNaN(forcedNumber)) {
|
||||
target.setCostume(opt_zeroIndex ?
|
||||
forcedNumber : forcedNumber - 1);
|
||||
this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, {
|
||||
position: [
|
||||
bubbleState.onSpriteRight ? (
|
||||
Math.min(stageBounds.right - bubbleWidth, targetBounds.right)
|
||||
) : (
|
||||
Math.max(stageBounds.left, targetBounds.left - bubbleWidth)
|
||||
),
|
||||
Math.min(stageBounds.top, targetBounds.top + bubbleHeight)
|
||||
]
|
||||
});
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a visible bubble for a target. If a bubble exists for the target,
|
||||
* just set it to visible and update the type/text. Otherwise create a new
|
||||
* bubble and update the relevant custom state.
|
||||
* @param {!Target} target Target who needs a bubble.
|
||||
* @return {undefined} Early return if text is empty string.
|
||||
* @private
|
||||
*/
|
||||
_renderBubble (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
const {drawableVisible, type, text, onSpriteRight} = bubbleState;
|
||||
|
||||
// Remove the bubble if target is not visible, or text is being set to blank
|
||||
// without being initialized. See comment below about blank text optimization.
|
||||
if (!target.visible || (text === '' && !bubbleState.skinId)) {
|
||||
return this._onTargetWillExit(target);
|
||||
}
|
||||
|
||||
if (bubbleState.skinId) {
|
||||
// Optimization: if text is set to blank, hide the drawable instead of
|
||||
// getting rid of it. This prevents flickering in "typewriter" projects
|
||||
if ((text === '' && drawableVisible) || (text !== '' && !drawableVisible)) {
|
||||
bubbleState.drawableVisible = text !== '';
|
||||
this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, {
|
||||
visible: bubbleState.drawableVisible
|
||||
});
|
||||
}
|
||||
if (bubbleState.drawableVisible) {
|
||||
this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight, [0, 0]);
|
||||
}
|
||||
} else {
|
||||
target.addListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
|
||||
|
||||
// TODO is there a way to figure out before rendering whether to default left or right?
|
||||
const targetBounds = target.getBounds();
|
||||
const stageBounds = this.runtime.getTargetForStage().getBounds();
|
||||
if (targetBounds.right + 170 > stageBounds.right) {
|
||||
bubbleState.onSpriteRight = false;
|
||||
}
|
||||
|
||||
bubbleState.drawableId = this.runtime.renderer.createDrawable();
|
||||
bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, bubbleState.onSpriteRight, [0, 0]);
|
||||
|
||||
this.runtime.renderer.setDrawableOrder(bubbleState.drawableId, Infinity);
|
||||
this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, {
|
||||
skinId: bubbleState.skinId
|
||||
});
|
||||
}
|
||||
|
||||
this._positionBubble(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point for say/think blocks. Clears existing bubble if the text is empty.
|
||||
* Set the bubble custom state and then call _renderBubble.
|
||||
* @param {!Target} target Target that say/think blocks are being called on.
|
||||
* @param {!string} type Either "say" or "think"
|
||||
* @param {!string} text The text for the bubble, empty string clears the bubble.
|
||||
* @private
|
||||
*/
|
||||
_updateBubble (target, type, text) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
bubbleState.type = type;
|
||||
bubbleState.text = text;
|
||||
this._renderBubble(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
looks_say: this.say,
|
||||
looks_sayforsecs: this.sayforsecs,
|
||||
looks_think: this.think,
|
||||
looks_thinkforsecs: this.thinkforsecs,
|
||||
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_gotofrontback: this.goToFrontBack,
|
||||
looks_goforwardbackwardlayers: this.goForwardBackwardLayers,
|
||||
looks_size: this.getSize,
|
||||
looks_costumenumbername: this.getCostumeNumberName,
|
||||
looks_backdropnumbername: this.getBackdropNumberName
|
||||
};
|
||||
}
|
||||
|
||||
getMonitored () {
|
||||
return {
|
||||
looks_size: {isSpriteSpecific: true},
|
||||
looks_costumenumbername: {isSpriteSpecific: true},
|
||||
looks_backdropnumbername: {}
|
||||
};
|
||||
}
|
||||
|
||||
say (args, util) {
|
||||
// @TODO in 2.0 calling say/think resets the right/left bias of the bubble
|
||||
this._updateBubble(util.target, 'say', String(args.MESSAGE));
|
||||
}
|
||||
|
||||
sayforsecs (args, util) {
|
||||
this.say(args, util);
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
this._bubbleTimeout = setTimeout(() => {
|
||||
this._bubbleTimeout = null;
|
||||
// Clear say bubble and proceed.
|
||||
this._updateBubble(_target, 'say', '');
|
||||
resolve();
|
||||
}, 1000 * args.SECS);
|
||||
});
|
||||
}
|
||||
|
||||
think (args, util) {
|
||||
this._updateBubble(util.target, 'think', String(args.MESSAGE));
|
||||
}
|
||||
|
||||
thinkforsecs (args, util) {
|
||||
this.think(args, util);
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
this._bubbleTimeout = setTimeout(() => {
|
||||
this._bubbleTimeout = null;
|
||||
// Clear say bubble and proceed.
|
||||
this._updateBubble(_target, 'think', '');
|
||||
resolve();
|
||||
}, 1000 * args.SECS);
|
||||
});
|
||||
}
|
||||
|
||||
show (args, util) {
|
||||
util.target.setVisible(true);
|
||||
this._renderBubble(util.target);
|
||||
}
|
||||
|
||||
hide (args, util) {
|
||||
util.target.setVisible(false);
|
||||
this._renderBubble(util.target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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=} optZeroIndex Set to zero-index the requestedCostume.
|
||||
* @return {Array.<!Thread>} Any threads started by this switch.
|
||||
*/
|
||||
_setCostumeOrBackdrop (target,
|
||||
requestedCostume, optZeroIndex) {
|
||||
if (typeof requestedCostume === 'number') {
|
||||
target.setCostume(optZeroIndex ?
|
||||
requestedCostume : requestedCostume - 1);
|
||||
} else {
|
||||
const costumeIndex = target.getCostumeIndexByName(requestedCostume);
|
||||
if (costumeIndex > -1) {
|
||||
target.setCostume(costumeIndex);
|
||||
} else if (requestedCostume === 'previous costume' ||
|
||||
requestedCostume === 'previous backdrop') {
|
||||
target.setCostume(target.currentCostume - 1);
|
||||
} else if (requestedCostume === 'next costume' ||
|
||||
requestedCostume === 'next backdrop') {
|
||||
target.setCostume(target.currentCostume + 1);
|
||||
} else {
|
||||
const forcedNumber = Number(requestedCostume);
|
||||
if (!isNaN(forcedNumber)) {
|
||||
target.setCostume(optZeroIndex ?
|
||||
forcedNumber : forcedNumber - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target === this.runtime.getTargetForStage()) {
|
||||
// Target is the stage - start hats.
|
||||
const newName = target.sprite.costumes[target.currentCostume].name;
|
||||
return this.runtime.startHats('event_whenbackdropswitchesto', {
|
||||
BACKDROP: newName
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
switchCostume (args, util) {
|
||||
this._setCostumeOrBackdrop(util.target, args.COSTUME);
|
||||
}
|
||||
|
||||
nextCostume (args, util) {
|
||||
this._setCostumeOrBackdrop(
|
||||
util.target, util.target.currentCostume + 1, true
|
||||
);
|
||||
}
|
||||
|
||||
switchBackdrop (args) {
|
||||
this._setCostumeOrBackdrop(this.runtime.getTargetForStage(), args.BACKDROP);
|
||||
}
|
||||
|
||||
switchBackdropAndWait (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.
|
||||
const instance = this;
|
||||
const waiting = util.stackFrame.startedThreads.some(thread => instance.runtime.isActiveThread(thread));
|
||||
if (waiting) {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
|
||||
nextBackdrop () {
|
||||
const stage = this.runtime.getTargetForStage();
|
||||
this._setCostumeOrBackdrop(
|
||||
stage, stage.currentCostume + 1, true
|
||||
);
|
||||
}
|
||||
|
||||
changeEffect (args, util) {
|
||||
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
||||
const change = Cast.toNumber(args.CHANGE);
|
||||
if (!util.target.effects.hasOwnProperty(effect)) return;
|
||||
const newValue = change + util.target.effects[effect];
|
||||
util.target.setEffect(effect, newValue);
|
||||
}
|
||||
|
||||
setEffect (args, util) {
|
||||
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
||||
const value = Cast.toNumber(args.VALUE);
|
||||
util.target.setEffect(effect, value);
|
||||
}
|
||||
|
||||
clearEffects (args, util) {
|
||||
util.target.clearEffects();
|
||||
}
|
||||
|
||||
changeSize (args, util) {
|
||||
const change = Cast.toNumber(args.CHANGE);
|
||||
util.target.setSize(util.target.size + change);
|
||||
}
|
||||
|
||||
setSize (args, util) {
|
||||
const size = Cast.toNumber(args.SIZE);
|
||||
util.target.setSize(size);
|
||||
}
|
||||
|
||||
goToFrontBack (args, util) {
|
||||
if (!util.target.isStage) {
|
||||
if (args.FRONT_BACK === 'front') {
|
||||
util.target.goToFront();
|
||||
} else {
|
||||
util.target.goToBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
goForwardBackwardLayers (args, util) {
|
||||
if (!util.target.isStage) {
|
||||
if (args.FORWARD_BACKWARD === 'forward') {
|
||||
util.target.goForwardLayers(Cast.toNumber(args.NUM));
|
||||
} else {
|
||||
util.target.goBackwardLayers(Cast.toNumber(args.NUM));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
|
||||
getSize (args, util) {
|
||||
return Math.round(util.target.size);
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3LooksBlocks.prototype.nextBackdrop = function () {
|
||||
var stage = this.runtime.getTargetForStage();
|
||||
this._setCostumeOrBackdrop(
|
||||
stage, stage.currentCostume + 1, true
|
||||
);
|
||||
};
|
||||
getBackdropNumberName (args) {
|
||||
const stage = this.runtime.getTargetForStage();
|
||||
if (args.NUMBER_NAME === 'number') {
|
||||
return stage.currentCostume + 1;
|
||||
}
|
||||
// Else return name
|
||||
return stage.sprite.costumes[stage.currentCostume].name;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
getCostumeNumberName (args, util) {
|
||||
if (args.NUMBER_NAME === 'number') {
|
||||
return util.target.currentCostume + 1;
|
||||
}
|
||||
// Else return name
|
||||
return util.target.sprite.costumes[util.target.currentCostume].name;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3LooksBlocks;
|
||||
+238
-213
@@ -1,234 +1,259 @@
|
||||
var Cast = require('../util/cast');
|
||||
var MathUtil = require('../util/math-util');
|
||||
var Timer = require('../util/timer');
|
||||
const Cast = require('../util/cast');
|
||||
const MathUtil = require('../util/math-util');
|
||||
const Timer = require('../util/timer');
|
||||
|
||||
class Scratch3MotionBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
function Scratch3MotionBlocks(runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
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;
|
||||
getPrimitives () {
|
||||
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_glideto: this.glideTo,
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
getMonitored () {
|
||||
return {
|
||||
motion_xposition: {isSpriteSpecific: true},
|
||||
motion_yposition: {isSpriteSpecific: true},
|
||||
motion_direction: {isSpriteSpecific: true}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
moveSteps (args, util) {
|
||||
const steps = Cast.toNumber(args.STEPS);
|
||||
const radians = MathUtil.degToRad(90 - util.target.direction);
|
||||
const dx = steps * Math.cos(radians);
|
||||
const dy = steps * Math.sin(radians);
|
||||
util.target.setXY(util.target.x + dx, util.target.y + dy);
|
||||
}
|
||||
|
||||
goToXY (args, util) {
|
||||
const x = Cast.toNumber(args.X);
|
||||
const y = Cast.toNumber(args.Y);
|
||||
util.target.setXY(x, y);
|
||||
}
|
||||
|
||||
getTargetXY (targetName, util) {
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
if (targetName === '_mouse_') {
|
||||
targetX = util.ioQuery('mouse', 'getScratchX');
|
||||
targetY = util.ioQuery('mouse', 'getScratchY');
|
||||
} else if (targetName === '_random_') {
|
||||
const stageWidth = this.runtime.constructor.STAGE_WIDTH;
|
||||
const stageHeight = this.runtime.constructor.STAGE_HEIGHT;
|
||||
targetX = Math.round(stageWidth * (Math.random() - 0.5));
|
||||
targetY = Math.round(stageHeight * (Math.random() - 0.5));
|
||||
} else {
|
||||
const goToTarget = this.runtime.getSpriteTargetByName(targetName);
|
||||
if (!goToTarget) return;
|
||||
targetX = goToTarget.x;
|
||||
targetY = goToTarget.y;
|
||||
}
|
||||
return [targetX, targetY];
|
||||
}
|
||||
|
||||
goTo (args, util) {
|
||||
const targetXY = this.getTargetXY(args.TO, util);
|
||||
if (targetXY) {
|
||||
util.target.setXY(targetXY[0], targetXY[1]);
|
||||
}
|
||||
}
|
||||
|
||||
turnRight (args, util) {
|
||||
const degrees = Cast.toNumber(args.DEGREES);
|
||||
util.target.setDirection(util.target.direction + degrees);
|
||||
}
|
||||
|
||||
turnLeft (args, util) {
|
||||
const degrees = Cast.toNumber(args.DEGREES);
|
||||
util.target.setDirection(util.target.direction - degrees);
|
||||
}
|
||||
|
||||
pointInDirection (args, util) {
|
||||
const direction = Cast.toNumber(args.DIRECTION);
|
||||
util.target.setDirection(direction);
|
||||
}
|
||||
|
||||
pointTowards (args, util) {
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
if (args.TOWARDS === '_mouse_') {
|
||||
targetX = util.ioQuery('mouse', 'getScratchX');
|
||||
targetY = util.ioQuery('mouse', 'getScratchY');
|
||||
} else {
|
||||
const pointTarget = this.runtime.getSpriteTargetByName(args.TOWARDS);
|
||||
if (!pointTarget) return;
|
||||
targetX = pointTarget.x;
|
||||
targetY = pointTarget.y;
|
||||
}
|
||||
|
||||
const dx = targetX - util.target.x;
|
||||
const dy = targetY - util.target.y;
|
||||
const direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx));
|
||||
util.target.setDirection(direction);
|
||||
}
|
||||
|
||||
glide (args, util) {
|
||||
if (util.stackFrame.timer) {
|
||||
const timeElapsed = util.stackFrame.timer.timeElapsed();
|
||||
if (timeElapsed < util.stackFrame.duration * 1000) {
|
||||
// In progress: move to intermediate position.
|
||||
const frac = timeElapsed / (util.stackFrame.duration * 1000);
|
||||
const dx = frac * (util.stackFrame.endX - util.stackFrame.startX);
|
||||
const 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);
|
||||
}
|
||||
} else {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
glideTo (args, util) {
|
||||
const targetXY = this.getTargetXY(args.TO, util);
|
||||
if (targetXY) {
|
||||
this.glide({SECS: args.SECS, X: targetXY[0], Y: targetXY[1]}, util);
|
||||
}
|
||||
}
|
||||
|
||||
ifOnEdgeBounce (args, util) {
|
||||
const bounds = util.target.getBounds();
|
||||
if (!bounds) {
|
||||
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);
|
||||
// Measure distance to edges.
|
||||
// Values are positive when the sprite is far away,
|
||||
// and clamped to zero when the sprite is beyond.
|
||||
const stageWidth = this.runtime.constructor.STAGE_WIDTH;
|
||||
const stageHeight = this.runtime.constructor.STAGE_HEIGHT;
|
||||
const distLeft = Math.max(0, (stageWidth / 2) + bounds.left);
|
||||
const distTop = Math.max(0, (stageHeight / 2) - bounds.top);
|
||||
const distRight = Math.max(0, (stageWidth / 2) - bounds.right);
|
||||
const distBottom = Math.max(0, (stageHeight / 2) + bounds.bottom);
|
||||
// Find the nearest edge.
|
||||
let nearestEdge = '';
|
||||
let 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.
|
||||
const radians = MathUtil.degToRad(90 - util.target.direction);
|
||||
let dx = Math.cos(radians);
|
||||
let 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));
|
||||
}
|
||||
const newDirection = MathUtil.radToDeg(Math.atan2(dy, dx)) + 90;
|
||||
util.target.setDirection(newDirection);
|
||||
// Keep within the stage.
|
||||
const fencedPosition = util.target.keepInFence(util.target.x, util.target.y);
|
||||
util.target.setXY(fencedPosition[0], fencedPosition[1]);
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3MotionBlocks.prototype.ifOnEdgeBounce = function (args, util) {
|
||||
var bounds = util.target.getBounds();
|
||||
if (!bounds) {
|
||||
return;
|
||||
setRotationStyle (args, util) {
|
||||
util.target.setRotationStyle(args.STYLE);
|
||||
}
|
||||
// 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';
|
||||
|
||||
changeX (args, util) {
|
||||
const dx = Cast.toNumber(args.DX);
|
||||
util.target.setXY(util.target.x + dx, util.target.y);
|
||||
}
|
||||
if (distTop < minDist) {
|
||||
minDist = distTop;
|
||||
nearestEdge = 'top';
|
||||
|
||||
setX (args, util) {
|
||||
const x = Cast.toNumber(args.X);
|
||||
util.target.setXY(x, util.target.y);
|
||||
}
|
||||
if (distRight < minDist) {
|
||||
minDist = distRight;
|
||||
nearestEdge = 'right';
|
||||
|
||||
changeY (args, util) {
|
||||
const dy = Cast.toNumber(args.DY);
|
||||
util.target.setXY(util.target.x, util.target.y + dy);
|
||||
}
|
||||
if (distBottom < minDist) {
|
||||
minDist = distBottom;
|
||||
nearestEdge = 'bottom';
|
||||
|
||||
setY (args, util) {
|
||||
const y = Cast.toNumber(args.Y);
|
||||
util.target.setXY(util.target.x, y);
|
||||
}
|
||||
if (minDist > 0) {
|
||||
return; // Not touching any edge.
|
||||
|
||||
getX (args, util) {
|
||||
return util.target.x;
|
||||
}
|
||||
// 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));
|
||||
|
||||
getY (args, util) {
|
||||
return util.target.y;
|
||||
}
|
||||
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;
|
||||
};
|
||||
getDirection (args, util) {
|
||||
return util.target.direction;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3MotionBlocks;
|
||||
+148
-137
@@ -1,143 +1,154 @@
|
||||
var Cast = require('../util/cast.js');
|
||||
const Cast = require('../util/cast.js');
|
||||
const MathUtil = require('../util/math-util.js');
|
||||
|
||||
class Scratch3OperatorsBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
function Scratch3OperatorsBlocks(runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
getPrimitives () {
|
||||
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_contains: this.contains,
|
||||
operator_mod: this.mod,
|
||||
operator_round: this.round,
|
||||
operator_mathop: this.mathop
|
||||
};
|
||||
}
|
||||
|
||||
add (args) {
|
||||
return Cast.toNumber(args.NUM1) + Cast.toNumber(args.NUM2);
|
||||
}
|
||||
|
||||
subtract (args) {
|
||||
return Cast.toNumber(args.NUM1) - Cast.toNumber(args.NUM2);
|
||||
}
|
||||
|
||||
multiply (args) {
|
||||
return Cast.toNumber(args.NUM1) * Cast.toNumber(args.NUM2);
|
||||
}
|
||||
|
||||
divide (args) {
|
||||
return Cast.toNumber(args.NUM1) / Cast.toNumber(args.NUM2);
|
||||
}
|
||||
|
||||
lt (args) {
|
||||
return Cast.compare(args.OPERAND1, args.OPERAND2) < 0;
|
||||
}
|
||||
|
||||
equals (args) {
|
||||
return Cast.compare(args.OPERAND1, args.OPERAND2) === 0;
|
||||
}
|
||||
|
||||
gt (args) {
|
||||
return Cast.compare(args.OPERAND1, args.OPERAND2) > 0;
|
||||
}
|
||||
|
||||
and (args) {
|
||||
return Cast.toBoolean(args.OPERAND1) && Cast.toBoolean(args.OPERAND2);
|
||||
}
|
||||
|
||||
or (args) {
|
||||
return Cast.toBoolean(args.OPERAND1) || Cast.toBoolean(args.OPERAND2);
|
||||
}
|
||||
|
||||
not (args) {
|
||||
return !Cast.toBoolean(args.OPERAND);
|
||||
}
|
||||
|
||||
random (args) {
|
||||
const nFrom = Cast.toNumber(args.FROM);
|
||||
const nTo = Cast.toNumber(args.TO);
|
||||
const low = nFrom <= nTo ? nFrom : nTo;
|
||||
const 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 + Math.floor(Math.random() * ((high + 1) - low));
|
||||
}
|
||||
return (Math.random() * (high - low)) + low;
|
||||
}
|
||||
|
||||
join (args) {
|
||||
return Cast.toString(args.STRING1) + Cast.toString(args.STRING2);
|
||||
}
|
||||
|
||||
letterOf (args) {
|
||||
const index = Cast.toNumber(args.LETTER) - 1;
|
||||
const str = Cast.toString(args.STRING);
|
||||
// Out of bounds?
|
||||
if (index < 0 || index >= str.length) {
|
||||
return '';
|
||||
}
|
||||
return str.charAt(index);
|
||||
}
|
||||
|
||||
length (args) {
|
||||
return Cast.toString(args.STRING).length;
|
||||
}
|
||||
|
||||
contains (args) {
|
||||
const format = function (string) {
|
||||
return Cast.toString(string).toLowerCase();
|
||||
};
|
||||
return format(args.STRING1).includes(format(args.STRING2));
|
||||
}
|
||||
|
||||
mod (args) {
|
||||
const n = Cast.toNumber(args.NUM1);
|
||||
const modulus = Cast.toNumber(args.NUM2);
|
||||
let result = n % modulus;
|
||||
// Scratch mod is kept positive.
|
||||
if (result / modulus < 0) result += modulus;
|
||||
return result;
|
||||
}
|
||||
|
||||
round (args) {
|
||||
return Math.round(Cast.toNumber(args.NUM));
|
||||
}
|
||||
|
||||
mathop (args) {
|
||||
const operator = Cast.toString(args.OPERATOR).toLowerCase();
|
||||
const 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 parseFloat(Math.sin((Math.PI * n) / 180).toFixed(10));
|
||||
case 'cos': return parseFloat(Math.cos((Math.PI * n) / 180).toFixed(10));
|
||||
case 'tan': return MathUtil.tan(n);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -1,44 +1,69 @@
|
||||
function Scratch3ProcedureBlocks(runtime) {
|
||||
class Scratch3ProcedureBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
getPrimitives () {
|
||||
return {
|
||||
procedures_definition: this.definition,
|
||||
procedures_call: this.call,
|
||||
argument_reporter_string_number: this.argumentReporterStringNumber,
|
||||
argument_reporter_boolean: this.argumentReporterBoolean
|
||||
};
|
||||
}
|
||||
|
||||
definition () {
|
||||
// No-op: execute the blocks.
|
||||
}
|
||||
|
||||
call (args, util) {
|
||||
if (!util.stackFrame.executed) {
|
||||
const procedureCode = args.mutation.proccode;
|
||||
const paramNamesAndIds = util.getProcedureParamNamesAndIds(procedureCode);
|
||||
|
||||
// If null, procedure could not be found, which can happen if custom
|
||||
// block is dragged between sprites without the definition.
|
||||
// Match Scratch 2.0 behavior and noop.
|
||||
if (paramNamesAndIds === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [paramNames, paramIds] = paramNamesAndIds;
|
||||
|
||||
for (let i = 0; i < paramIds.length; i++) {
|
||||
if (args.hasOwnProperty(paramIds[i])) {
|
||||
util.pushParam(paramNames[i], args[paramIds[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
util.stackFrame.executed = true;
|
||||
util.startProcedure(procedureCode);
|
||||
}
|
||||
}
|
||||
|
||||
argumentReporterStringNumber (args, util) {
|
||||
const value = util.getParam(args.VALUE);
|
||||
if (value === null) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
argumentReporterBoolean (args, util) {
|
||||
const value = util.getParam(args.VALUE);
|
||||
if (value === null) {
|
||||
return false;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
+263
-122
@@ -1,128 +1,269 @@
|
||||
var Cast = require('../util/cast');
|
||||
const Cast = require('../util/cast');
|
||||
|
||||
class Scratch3SensingBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
/**
|
||||
* The "answer" block value.
|
||||
* @type {string}
|
||||
*/
|
||||
this._answer = '';
|
||||
|
||||
/**
|
||||
* The list of queued questions and respective `resolve` callbacks.
|
||||
* @type {!Array}
|
||||
*/
|
||||
this._questionList = [];
|
||||
|
||||
this.runtime.on('ANSWER', this._onAnswer.bind(this));
|
||||
this.runtime.on('PROJECT_START', this._resetAnswer.bind(this));
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._clearAllQuestions.bind(this));
|
||||
}
|
||||
|
||||
function Scratch3SensingBlocks(runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
getPrimitives () {
|
||||
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_of: this.getAttributeOf,
|
||||
sensing_mousex: this.getMouseX,
|
||||
sensing_mousey: this.getMouseY,
|
||||
sensing_setdragmode: this.setDragMode,
|
||||
sensing_mousedown: this.getMouseDown,
|
||||
sensing_keypressed: this.getKeyPressed,
|
||||
sensing_current: this.current,
|
||||
sensing_dayssince2000: this.daysSince2000,
|
||||
sensing_loudness: this.getLoudness,
|
||||
sensing_askandwait: this.askAndWait,
|
||||
sensing_answer: this.getAnswer
|
||||
};
|
||||
}
|
||||
|
||||
getMonitored () {
|
||||
return {
|
||||
sensing_answer: {},
|
||||
sensing_loudness: {},
|
||||
sensing_timer: {},
|
||||
sensing_current: {}
|
||||
};
|
||||
}
|
||||
|
||||
_onAnswer (answer) {
|
||||
this._answer = answer;
|
||||
const questionObj = this._questionList.shift();
|
||||
if (questionObj) {
|
||||
const [_question, resolve, target, wasVisible, wasStage] = questionObj;
|
||||
// If the target was visible when asked, hide the say bubble unless the target was the stage.
|
||||
if (wasVisible && !wasStage) {
|
||||
this.runtime.emit('SAY', target, 'say', '');
|
||||
}
|
||||
resolve();
|
||||
this._askNextQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
_resetAnswer () {
|
||||
this._answer = '';
|
||||
}
|
||||
|
||||
_enqueueAsk (question, resolve, target, wasVisible, wasStage) {
|
||||
this._questionList.push([question, resolve, target, wasVisible, wasStage]);
|
||||
}
|
||||
|
||||
_askNextQuestion () {
|
||||
if (this._questionList.length > 0) {
|
||||
const [question, _resolve, target, wasVisible, wasStage] = this._questionList[0];
|
||||
// If the target is visible, emit a blank question and use the
|
||||
// say event to trigger a bubble unless the target was the stage.
|
||||
if (wasVisible && !wasStage) {
|
||||
this.runtime.emit('SAY', target, 'say', question);
|
||||
this.runtime.emit('QUESTION', '');
|
||||
} else {
|
||||
this.runtime.emit('QUESTION', question);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllQuestions () {
|
||||
this._questionList = [];
|
||||
this.runtime.emit('QUESTION', null);
|
||||
}
|
||||
|
||||
askAndWait (args, util) {
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
const isQuestionAsked = this._questionList.length > 0;
|
||||
this._enqueueAsk(String(args.QUESTION), resolve, _target, _target.visible, _target.isStage);
|
||||
if (!isQuestionAsked) {
|
||||
this._askNextQuestion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAnswer () {
|
||||
return this._answer;
|
||||
}
|
||||
|
||||
touchingObject (args, util) {
|
||||
const requestedObject = args.TOUCHINGOBJECTMENU;
|
||||
if (requestedObject === '_mouse_') {
|
||||
const mouseX = util.ioQuery('mouse', 'getClientX');
|
||||
const mouseY = util.ioQuery('mouse', 'getClientY');
|
||||
return util.target.isTouchingPoint(mouseX, mouseY);
|
||||
} else if (requestedObject === '_edge_') {
|
||||
return util.target.isTouchingEdge();
|
||||
}
|
||||
return util.target.isTouchingSprite(requestedObject);
|
||||
|
||||
}
|
||||
|
||||
touchingColor (args, util) {
|
||||
const color = Cast.toRgbColorList(args.COLOR);
|
||||
return util.target.isTouchingColor(color);
|
||||
}
|
||||
|
||||
colorTouchingColor (args, util) {
|
||||
const maskColor = Cast.toRgbColorList(args.COLOR);
|
||||
const targetColor = Cast.toRgbColorList(args.COLOR2);
|
||||
return util.target.colorIsTouchingColor(targetColor, maskColor);
|
||||
}
|
||||
|
||||
distanceTo (args, util) {
|
||||
if (util.target.isStage) return 10000;
|
||||
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
if (args.DISTANCETOMENU === '_mouse_') {
|
||||
targetX = util.ioQuery('mouse', 'getScratchX');
|
||||
targetY = util.ioQuery('mouse', 'getScratchY');
|
||||
} else {
|
||||
const distTarget = this.runtime.getSpriteTargetByName(
|
||||
args.DISTANCETOMENU
|
||||
);
|
||||
if (!distTarget) return 10000;
|
||||
targetX = distTarget.x;
|
||||
targetY = distTarget.y;
|
||||
}
|
||||
|
||||
const dx = util.target.x - targetX;
|
||||
const dy = util.target.y - targetY;
|
||||
return Math.sqrt((dx * dx) + (dy * dy));
|
||||
}
|
||||
|
||||
setDragMode (args, util) {
|
||||
util.target.setDraggable(args.DRAG_MODE === 'draggable');
|
||||
}
|
||||
|
||||
getTimer (args, util) {
|
||||
return util.ioQuery('clock', 'projectTimer');
|
||||
}
|
||||
|
||||
resetTimer (args, util) {
|
||||
util.ioQuery('clock', 'resetProjectTimer');
|
||||
}
|
||||
|
||||
getMouseX (args, util) {
|
||||
return util.ioQuery('mouse', 'getScratchX');
|
||||
}
|
||||
|
||||
getMouseY (args, util) {
|
||||
return util.ioQuery('mouse', 'getScratchY');
|
||||
}
|
||||
|
||||
getMouseDown (args, util) {
|
||||
return util.ioQuery('mouse', 'getIsDown');
|
||||
}
|
||||
|
||||
current (args) {
|
||||
const menuOption = Cast.toString(args.CURRENTMENU).toLowerCase();
|
||||
const 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;
|
||||
}
|
||||
|
||||
getKeyPressed (args, util) {
|
||||
return util.ioQuery('keyboard', 'getKeyIsDown', [args.KEY_OPTION]);
|
||||
}
|
||||
|
||||
daysSince2000 () {
|
||||
const msPerDay = 24 * 60 * 60 * 1000;
|
||||
const start = new Date(2000, 0, 1); // Months are 0-indexed.
|
||||
const today = new Date();
|
||||
const dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset();
|
||||
let mSecsSinceStart = today.valueOf() - start.valueOf();
|
||||
mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000);
|
||||
return mSecsSinceStart / msPerDay;
|
||||
}
|
||||
|
||||
getLoudness () {
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return -1;
|
||||
return this.runtime.audioEngine.getLoudness();
|
||||
}
|
||||
|
||||
getAttributeOf (args) {
|
||||
let attrTarget;
|
||||
|
||||
if (args.OBJECT === '_stage_') {
|
||||
attrTarget = this.runtime.getTargetForStage();
|
||||
} else {
|
||||
attrTarget = this.runtime.getSpriteTargetByName(args.OBJECT);
|
||||
}
|
||||
|
||||
// Generic attributes
|
||||
if (attrTarget.isStage) {
|
||||
switch (args.PROPERTY) {
|
||||
// Scratch 1.4 support
|
||||
case 'background #': return attrTarget.currentCostume + 1;
|
||||
|
||||
case 'backdrop #': return attrTarget.currentCostume + 1;
|
||||
case 'backdrop name':
|
||||
return attrTarget.sprite.costumes[attrTarget.currentCostume].name;
|
||||
case 'volume': return; // @todo: Keep this in mind for sound blocks!
|
||||
}
|
||||
} else {
|
||||
switch (args.PROPERTY) {
|
||||
case 'x position': return attrTarget.x;
|
||||
case 'y position': return attrTarget.y;
|
||||
case 'direction': return attrTarget.direction;
|
||||
case 'costume #': return attrTarget.currentCostume + 1;
|
||||
case 'costume name':
|
||||
return attrTarget.sprite.costumes[attrTarget.currentCostume].name;
|
||||
case 'size': return attrTarget.size;
|
||||
case 'volume': return; // @todo: above, keep in mind for sound blocks..
|
||||
}
|
||||
}
|
||||
|
||||
// Variables
|
||||
const varName = args.PROPERTY;
|
||||
for (const id in attrTarget.variables) {
|
||||
if (attrTarget.variables[id].name === varName) {
|
||||
return attrTarget.variables[id].value;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, 0
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,301 @@
|
||||
const MathUtil = require('../util/math-util');
|
||||
const Cast = require('../util/cast');
|
||||
const Clone = require('../util/clone');
|
||||
|
||||
class Scratch3SoundBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
// Clear sound effects on green flag and stop button events.
|
||||
this._clearEffectsForAllTargets = this._clearEffectsForAllTargets.bind(this);
|
||||
if (this.runtime) {
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._clearEffectsForAllTargets);
|
||||
this.runtime.on('PROJECT_START', this._clearEffectsForAllTargets);
|
||||
}
|
||||
|
||||
this._onTargetCreated = this._onTargetCreated.bind(this);
|
||||
if (this.runtime) {
|
||||
runtime.on('targetWasCreated', this._onTargetCreated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The key to load & store a target's sound-related state.
|
||||
* @type {string}
|
||||
*/
|
||||
static get STATE_KEY () {
|
||||
return 'Scratch.sound';
|
||||
}
|
||||
|
||||
/**
|
||||
* The default sound-related state, to be used when a target has no existing sound state.
|
||||
* @type {SoundState}
|
||||
*/
|
||||
static get DEFAULT_SOUND_STATE () {
|
||||
return {
|
||||
volume: 100,
|
||||
effects: {
|
||||
pitch: 0,
|
||||
pan: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum and maximum MIDI note numbers, for clamping the input to play note.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get MIDI_NOTE_RANGE () {
|
||||
return {min: 36, max: 96}; // C2 to C7
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum and maximum beat values, for clamping the duration of play note, play drum and rest.
|
||||
* 100 beats at the default tempo of 60bpm is 100 seconds.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get BEAT_RANGE () {
|
||||
return {min: 0, max: 100};
|
||||
}
|
||||
|
||||
/** The minimum and maximum tempo values, in bpm.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get TEMPO_RANGE () {
|
||||
return {min: 20, max: 500};
|
||||
}
|
||||
|
||||
/** The minimum and maximum values for each sound effect.
|
||||
* @type {{effect:{min: number, max: number}}}
|
||||
*/
|
||||
static get EFFECT_RANGE () {
|
||||
return {
|
||||
pitch: {min: -600, max: 600}, // -5 to 5 octaves
|
||||
pan: {min: -100, max: 100} // 100% left to 100% right
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Target} target - collect sound state for this target.
|
||||
* @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary.
|
||||
* @private
|
||||
*/
|
||||
_getSoundState (target) {
|
||||
let soundState = target.getCustomState(Scratch3SoundBlocks.STATE_KEY);
|
||||
if (!soundState) {
|
||||
soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE);
|
||||
target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState);
|
||||
}
|
||||
return soundState;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a Target is cloned, clone the sound state.
|
||||
* @param {Target} newTarget - the newly created target.
|
||||
* @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
|
||||
* @listens Runtime#event:targetWasCreated
|
||||
* @private
|
||||
*/
|
||||
_onTargetCreated (newTarget, sourceTarget) {
|
||||
if (sourceTarget) {
|
||||
const soundState = sourceTarget.getCustomState(Scratch3SoundBlocks.STATE_KEY);
|
||||
if (soundState && newTarget) {
|
||||
newTarget.setCustomState(Scratch3SoundBlocks.STATE_KEY, Clone.simple(soundState));
|
||||
this._syncEffectsForTarget(newTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
sound_play: this.playSound,
|
||||
sound_playuntildone: this.playSoundAndWait,
|
||||
sound_stopallsounds: this.stopAllSounds,
|
||||
sound_seteffectto: this.setEffect,
|
||||
sound_changeeffectby: this.changeEffect,
|
||||
sound_cleareffects: this.clearEffects,
|
||||
sound_sounds_menu: this.soundsMenu,
|
||||
sound_beats_menu: this.beatsMenu,
|
||||
sound_effects_menu: this.effectsMenu,
|
||||
sound_setvolumeto: this.setVolume,
|
||||
sound_changevolumeby: this.changeVolume,
|
||||
sound_volume: this.getVolume
|
||||
};
|
||||
}
|
||||
|
||||
getMonitored () {
|
||||
return {
|
||||
sound_volume: {}
|
||||
};
|
||||
}
|
||||
|
||||
playSound (args, util) {
|
||||
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
||||
if (index >= 0) {
|
||||
const soundId = util.target.sprite.sounds[index].soundId;
|
||||
if (util.target.audioPlayer === null) return;
|
||||
util.target.audioPlayer.playSound(soundId);
|
||||
}
|
||||
}
|
||||
|
||||
playSoundAndWait (args, util) {
|
||||
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
||||
if (index >= 0) {
|
||||
const soundId = util.target.sprite.sounds[index].soundId;
|
||||
if (util.target.audioPlayer === null) return;
|
||||
return util.target.audioPlayer.playSound(soundId);
|
||||
}
|
||||
}
|
||||
|
||||
_getSoundIndex (soundName, util) {
|
||||
// if the sprite has no sounds, return -1
|
||||
const len = util.target.sprite.sounds.length;
|
||||
if (len === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// look up by name first
|
||||
const index = this.getSoundIndexByName(soundName, util);
|
||||
if (index !== -1) {
|
||||
return index;
|
||||
}
|
||||
|
||||
// then try using the sound name as a 1-indexed index
|
||||
const oneIndexedIndex = parseInt(soundName, 10);
|
||||
if (!isNaN(oneIndexedIndex)) {
|
||||
return MathUtil.wrapClamp(oneIndexedIndex - 1, 0, len - 1);
|
||||
}
|
||||
|
||||
// could not be found as a name or converted to index, return -1
|
||||
return -1;
|
||||
}
|
||||
|
||||
getSoundIndexByName (soundName, util) {
|
||||
const sounds = util.target.sprite.sounds;
|
||||
for (let i = 0; i < sounds.length; i++) {
|
||||
if (sounds[i].name === soundName) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// if there is no sound by that name, return -1
|
||||
return -1;
|
||||
}
|
||||
|
||||
stopAllSounds () {
|
||||
if (this.runtime.targets === null) return;
|
||||
const allTargets = this.runtime.targets;
|
||||
for (let i = 0; i < allTargets.length; i++) {
|
||||
this._stopAllSoundsForTarget(allTargets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_stopAllSoundsForTarget (target) {
|
||||
if (target.audioPlayer === null) return;
|
||||
target.audioPlayer.stopAllSounds();
|
||||
}
|
||||
|
||||
setEffect (args, util) {
|
||||
this._updateEffect(args, util, false);
|
||||
}
|
||||
|
||||
changeEffect (args, util) {
|
||||
this._updateEffect(args, util, true);
|
||||
}
|
||||
|
||||
_updateEffect (args, util, change) {
|
||||
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
||||
const value = Cast.toNumber(args.VALUE);
|
||||
|
||||
const soundState = this._getSoundState(util.target);
|
||||
if (!soundState.effects.hasOwnProperty(effect)) return;
|
||||
|
||||
if (change) {
|
||||
soundState.effects[effect] += value;
|
||||
} else {
|
||||
soundState.effects[effect] = value;
|
||||
}
|
||||
|
||||
const effectRange = Scratch3SoundBlocks.EFFECT_RANGE[effect];
|
||||
soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], effectRange.min, effectRange.max);
|
||||
|
||||
if (util.target.audioPlayer === null) return;
|
||||
util.target.audioPlayer.setEffect(effect, soundState.effects[effect]);
|
||||
}
|
||||
|
||||
_syncEffectsForTarget (target) {
|
||||
if (!target || !target.audioPlayer) return;
|
||||
const soundState = this._getSoundState(target);
|
||||
for (const effect in soundState.effects) {
|
||||
if (!soundState.effects.hasOwnProperty(effect)) continue;
|
||||
target.audioPlayer.setEffect(effect, soundState.effects[effect]);
|
||||
}
|
||||
}
|
||||
|
||||
clearEffects (args, util) {
|
||||
this._clearEffectsForTarget(util.target);
|
||||
}
|
||||
|
||||
_clearEffectsForTarget (target) {
|
||||
const soundState = this._getSoundState(target);
|
||||
for (const effect in soundState.effects) {
|
||||
if (!soundState.effects.hasOwnProperty(effect)) continue;
|
||||
soundState.effects[effect] = 0;
|
||||
}
|
||||
if (target.audioPlayer === null) return;
|
||||
target.audioPlayer.clearEffects();
|
||||
}
|
||||
|
||||
_clearEffectsForAllTargets () {
|
||||
if (this.runtime.targets === null) return;
|
||||
const allTargets = this.runtime.targets;
|
||||
for (let i = 0; i < allTargets.length; i++) {
|
||||
this._clearEffectsForTarget(allTargets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
setVolume (args, util) {
|
||||
const volume = Cast.toNumber(args.VOLUME);
|
||||
this._updateVolume(volume, util);
|
||||
}
|
||||
|
||||
changeVolume (args, util) {
|
||||
const soundState = this._getSoundState(util.target);
|
||||
const volume = Cast.toNumber(args.VOLUME) + soundState.volume;
|
||||
this._updateVolume(volume, util);
|
||||
}
|
||||
|
||||
_updateVolume (volume, util) {
|
||||
const soundState = this._getSoundState(util.target);
|
||||
volume = MathUtil.clamp(volume, 0, 100);
|
||||
soundState.volume = volume;
|
||||
if (util.target.audioPlayer === null) return;
|
||||
util.target.audioPlayer.setVolume(soundState.volume);
|
||||
}
|
||||
|
||||
getVolume (args, util) {
|
||||
const soundState = this._getSoundState(util.target);
|
||||
return soundState.volume;
|
||||
}
|
||||
|
||||
soundsMenu (args) {
|
||||
return args.SOUND_MENU;
|
||||
}
|
||||
|
||||
beatsMenu (args) {
|
||||
return args.BEATS;
|
||||
}
|
||||
|
||||
effectsMenu (args) {
|
||||
return args.EFFECT;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3SoundBlocks;
|
||||
@@ -0,0 +1,111 @@
|
||||
const SharedDispatch = require('./shared-dispatch');
|
||||
|
||||
const log = require('../util/log');
|
||||
|
||||
/**
|
||||
* This class serves as the central broker for message dispatch. It expects to operate on the main thread / Window and
|
||||
* it must be informed of any Worker threads which will participate in the messaging system. From any context in the
|
||||
* messaging system, the dispatcher's "call" method can call any method on any "service" provided in any participating
|
||||
* context. The dispatch system will forward function arguments and return values across worker boundaries as needed.
|
||||
* @see {WorkerDispatch}
|
||||
*/
|
||||
class CentralDispatch extends SharedDispatch {
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
/**
|
||||
* Map of channel name to worker or local service provider.
|
||||
* If the entry is a Worker, the service is provided by an object on that worker.
|
||||
* Otherwise, the service is provided locally and methods on the service will be called directly.
|
||||
* @see {setService}
|
||||
* @type {object.<Worker|object>}
|
||||
*/
|
||||
this.services = {};
|
||||
|
||||
/**
|
||||
* The constructor we will use to recognize workers.
|
||||
* @type {Function}
|
||||
*/
|
||||
this.workerClass = (typeof Worker === 'undefined' ? null : Worker);
|
||||
|
||||
/**
|
||||
* List of workers attached to this dispatcher.
|
||||
* @type {Array}
|
||||
*/
|
||||
this.workers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a local object as the global provider of the specified service.
|
||||
* WARNING: Any method on the provider can be called from any worker within the dispatch system.
|
||||
* @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'.
|
||||
* @param {object} provider - a local object which provides this service.
|
||||
* @returns {Promise} - a promise which will resolve once the service is registered.
|
||||
*/
|
||||
setService (service, provider) {
|
||||
/** Return a promise for consistency with {@link WorkerDispatch#setService} */
|
||||
try {
|
||||
if (this.services.hasOwnProperty(service)) {
|
||||
log.warn(`Central dispatch replacing existing service provider for ${service}`);
|
||||
}
|
||||
this.services[service] = provider;
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a worker to the message dispatch system. The worker must implement a compatible message dispatch framework.
|
||||
* The dispatcher will immediately attempt to "handshake" with the worker.
|
||||
* @param {Worker} worker - the worker to add into the dispatch system.
|
||||
*/
|
||||
addWorker (worker) {
|
||||
if (this.workers.indexOf(worker) === -1) {
|
||||
this.workers.push(worker);
|
||||
worker.onmessage = this._onMessage.bind(this, worker);
|
||||
this._remoteCall(worker, 'dispatch', 'handshake').catch(e => {
|
||||
log.error(`Could not handshake with worker: ${JSON.stringify(e)}`);
|
||||
});
|
||||
} else {
|
||||
log.warn('Central dispatch ignoring attempt to add duplicate worker');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the service provider object for a particular service name.
|
||||
* @override
|
||||
* @param {string} service - the name of the service to look up
|
||||
* @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found
|
||||
* @protected
|
||||
*/
|
||||
_getServiceProvider (service) {
|
||||
const provider = this.services[service];
|
||||
return provider && {
|
||||
provider,
|
||||
isRemote: provider instanceof this.workerClass
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a call message sent to the dispatch service itself
|
||||
* @override
|
||||
* @param {Worker} worker - the worker which sent the message.
|
||||
* @param {DispatchCallMessage} message - the message to be handled.
|
||||
* @returns {Promise|undefined} - a promise for the results of this operation, if appropriate
|
||||
* @protected
|
||||
*/
|
||||
_onDispatchMessage (worker, message) {
|
||||
let promise;
|
||||
switch (message.method) {
|
||||
case 'setService':
|
||||
promise = this.setService(message.args[0], worker);
|
||||
break;
|
||||
default:
|
||||
log.error(`Central dispatch received message for unknown method: ${message.method}`);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CentralDispatch();
|
||||
@@ -0,0 +1,233 @@
|
||||
const log = require('../util/log');
|
||||
|
||||
/**
|
||||
* @typedef {object} DispatchCallMessage - a message to the dispatch system representing a service method call
|
||||
* @property {*} responseId - send a response message with this response ID. See {@link DispatchResponseMessage}
|
||||
* @property {string} service - the name of the service to be called
|
||||
* @property {string} method - the name of the method to be called
|
||||
* @property {Array|undefined} args - the arguments to be passed to the method
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} DispatchResponseMessage - a message to the dispatch system representing the results of a call
|
||||
* @property {*} responseId - a copy of the response ID from the call which generated this response
|
||||
* @property {*|undefined} error - if this is truthy, then it contains results from a failed call (such as an exception)
|
||||
* @property {*|undefined} result - if error is not truthy, then this contains the return value of the call (if any)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {DispatchCallMessage|DispatchResponseMessage} DispatchMessage
|
||||
* Any message to the dispatch system.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The SharedDispatch class is responsible for dispatch features shared by
|
||||
* {@link CentralDispatch} and {@link WorkerDispatch}.
|
||||
*/
|
||||
class SharedDispatch {
|
||||
constructor () {
|
||||
/**
|
||||
* List of callback registrations for promises waiting for a response from a call to a service on another
|
||||
* worker. A callback registration is an array of [resolve,reject] Promise functions.
|
||||
* Calls to local services don't enter this list.
|
||||
* @type {Array.<[Function,Function]>}
|
||||
*/
|
||||
this.callbacks = [];
|
||||
|
||||
/**
|
||||
* The next response ID to be used.
|
||||
* @type {int}
|
||||
*/
|
||||
this.nextResponseId = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a particular method on a particular service, regardless of whether that service is provided locally or on
|
||||
* a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone
|
||||
* algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be
|
||||
* transferred to the worker, and they should not be used after this call.
|
||||
* @example
|
||||
* dispatcher.call('vm', 'setData', 'cat', 42);
|
||||
* // this finds the worker for the 'vm' service, then on that worker calls:
|
||||
* vm.setData('cat', 42);
|
||||
* @param {string} service - the name of the service.
|
||||
* @param {string} method - the name of the method.
|
||||
* @param {*} [args] - the arguments to be copied to the method, if any.
|
||||
* @returns {Promise} - a promise for the return value of the service method.
|
||||
*/
|
||||
call (service, method, ...args) {
|
||||
return this.transferCall(service, method, null, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a particular method on a particular service, regardless of whether that service is provided locally or on
|
||||
* a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone
|
||||
* algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be
|
||||
* transferred to the worker, and they should not be used after this call.
|
||||
* @example
|
||||
* dispatcher.transferCall('vm', 'setData', [myArrayBuffer], 'cat', myArrayBuffer);
|
||||
* // this finds the worker for the 'vm' service, transfers `myArrayBuffer` to it, then on that worker calls:
|
||||
* vm.setData('cat', myArrayBuffer);
|
||||
* @param {string} service - the name of the service.
|
||||
* @param {string} method - the name of the method.
|
||||
* @param {Array} [transfer] - objects to be transferred instead of copied. Must be present in `args` to be useful.
|
||||
* @param {*} [args] - the arguments to be copied to the method, if any.
|
||||
* @returns {Promise} - a promise for the return value of the service method.
|
||||
*/
|
||||
transferCall (service, method, transfer, ...args) {
|
||||
try {
|
||||
const {provider, isRemote} = this._getServiceProvider(service);
|
||||
if (provider) {
|
||||
if (isRemote) {
|
||||
return this._remoteTransferCall(provider, service, method, transfer, ...args);
|
||||
}
|
||||
|
||||
const result = provider[method].apply(provider, args);
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
return Promise.reject(new Error(`Service not found: ${service}`));
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a particular service lives on another worker.
|
||||
* @param {string} service - the service to check.
|
||||
* @returns {boolean} - true if the service is remote (calls must cross a Worker boundary), false otherwise.
|
||||
* @private
|
||||
*/
|
||||
_isRemoteService (service) {
|
||||
return this._getServiceProvider(service).isRemote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link call}, but force the call to be posted through a particular communication channel.
|
||||
* @param {object} provider - send the call through this object's `postMessage` function.
|
||||
* @param {string} service - the name of the service.
|
||||
* @param {string} method - the name of the method.
|
||||
* @param {*} [args] - the arguments to be copied to the method, if any.
|
||||
* @returns {Promise} - a promise for the return value of the service method.
|
||||
*/
|
||||
_remoteCall (provider, service, method, ...args) {
|
||||
return this._remoteTransferCall(provider, service, method, null, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link transferCall}, but force the call to be posted through a particular communication channel.
|
||||
* @param {object} provider - send the call through this object's `postMessage` function.
|
||||
* @param {string} service - the name of the service.
|
||||
* @param {string} method - the name of the method.
|
||||
* @param {Array} [transfer] - objects to be transferred instead of copied. Must be present in `args` to be useful.
|
||||
* @param {*} [args] - the arguments to be copied to the method, if any.
|
||||
* @returns {Promise} - a promise for the return value of the service method.
|
||||
*/
|
||||
_remoteTransferCall (provider, service, method, transfer, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const responseId = this._storeCallbacks(resolve, reject);
|
||||
|
||||
/** @TODO: remove this hack! this is just here so we don't try to send `util` to a worker */
|
||||
if ((args.length > 0) && (typeof args[args.length - 1].yield === 'function')) {
|
||||
args.pop();
|
||||
}
|
||||
|
||||
if (transfer) {
|
||||
provider.postMessage({service, method, responseId, args}, transfer);
|
||||
} else {
|
||||
provider.postMessage({service, method, responseId, args});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store callback functions pending a response message.
|
||||
* @param {Function} resolve - function to call if the service method returns.
|
||||
* @param {Function} reject - function to call if the service method throws.
|
||||
* @returns {*} - a unique response ID for this set of callbacks. See {@link _deliverResponse}.
|
||||
* @protected
|
||||
*/
|
||||
_storeCallbacks (resolve, reject) {
|
||||
const responseId = this.nextResponseId++;
|
||||
this.callbacks[responseId] = [resolve, reject];
|
||||
return responseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver call response from a worker. This should only be called as the result of a message from a worker.
|
||||
* @param {int} responseId - the response ID of the callback set to call.
|
||||
* @param {DispatchResponseMessage} message - the message containing the response value(s).
|
||||
* @protected
|
||||
*/
|
||||
_deliverResponse (responseId, message) {
|
||||
try {
|
||||
const [resolve, reject] = this.callbacks[responseId];
|
||||
delete this.callbacks[responseId];
|
||||
if (message.error) {
|
||||
reject(message.error);
|
||||
} else {
|
||||
resolve(message.result);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(`Dispatch callback failed: ${JSON.stringify(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a message event received from a connected worker.
|
||||
* @param {Worker} worker - the worker which sent the message, or the global object if running in a worker.
|
||||
* @param {MessageEvent} event - the message event to be handled.
|
||||
* @protected
|
||||
*/
|
||||
_onMessage (worker, event) {
|
||||
/** @type {DispatchMessage} */
|
||||
const message = event.data;
|
||||
message.args = message.args || [];
|
||||
let promise;
|
||||
if (message.service) {
|
||||
if (message.service === 'dispatch') {
|
||||
promise = this._onDispatchMessage(worker, message);
|
||||
} else {
|
||||
promise = this.call(message.service, message.method, ...message.args);
|
||||
}
|
||||
} else if (typeof message.responseId === 'undefined') {
|
||||
log.error(`Dispatch caught malformed message from a worker: ${JSON.stringify(event)}`);
|
||||
} else {
|
||||
this._deliverResponse(message.responseId, message);
|
||||
}
|
||||
if (promise) {
|
||||
if (typeof message.responseId === 'undefined') {
|
||||
log.error(`Dispatch message missing required response ID: ${JSON.stringify(event)}`);
|
||||
} else {
|
||||
promise.then(
|
||||
result => worker.postMessage({responseId: message.responseId, result}),
|
||||
error => worker.postMessage({responseId: message.responseId, error})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the service provider object for a particular service name.
|
||||
* @abstract
|
||||
* @param {string} service - the name of the service to look up
|
||||
* @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found
|
||||
* @protected
|
||||
*/
|
||||
_getServiceProvider (service) {
|
||||
throw new Error(`Could not get provider for ${service}: _getServiceProvider not implemented`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a call message sent to the dispatch service itself
|
||||
* @abstract
|
||||
* @param {Worker} worker - the worker which sent the message.
|
||||
* @param {DispatchCallMessage} message - the message to be handled.
|
||||
* @returns {Promise|undefined} - a promise for the results of this operation, if appropriate
|
||||
* @private
|
||||
*/
|
||||
_onDispatchMessage (worker, message) {
|
||||
throw new Error(`Unimplemented dispatch message handler cannot handle ${message.method} method`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SharedDispatch;
|
||||
@@ -0,0 +1,110 @@
|
||||
const SharedDispatch = require('./shared-dispatch');
|
||||
|
||||
const log = require('../util/log');
|
||||
|
||||
/**
|
||||
* This class provides a Worker with the means to participate in the message dispatch system managed by CentralDispatch.
|
||||
* From any context in the messaging system, the dispatcher's "call" method can call any method on any "service"
|
||||
* provided in any participating context. The dispatch system will forward function arguments and return values across
|
||||
* worker boundaries as needed.
|
||||
* @see {CentralDispatch}
|
||||
*/
|
||||
class WorkerDispatch extends SharedDispatch {
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
/**
|
||||
* This promise will be resolved when we have successfully connected to central dispatch.
|
||||
* @type {Promise}
|
||||
* @see {waitForConnection}
|
||||
* @private
|
||||
*/
|
||||
this._connectionPromise = new Promise(resolve => {
|
||||
this._onConnect = resolve;
|
||||
});
|
||||
|
||||
/**
|
||||
* Map of service name to local service provider.
|
||||
* If a service is not listed here, it is assumed to be provided by another context (another Worker or the main
|
||||
* thread).
|
||||
* @see {setService}
|
||||
* @type {object}
|
||||
*/
|
||||
this.services = {};
|
||||
|
||||
this._onMessage = this._onMessage.bind(this, self);
|
||||
if (typeof self !== 'undefined') {
|
||||
self.onmessage = this._onMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise} a promise which will resolve upon connection to central dispatch. If you need to make a call
|
||||
* immediately on "startup" you can attach a 'then' to this promise.
|
||||
* @example
|
||||
* dispatch.waitForConnection.then(() => {
|
||||
* dispatch.call('myService', 'hello');
|
||||
* })
|
||||
*/
|
||||
get waitForConnection () {
|
||||
return this._connectionPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a local object as the global provider of the specified service.
|
||||
* WARNING: Any method on the provider can be called from any worker within the dispatch system.
|
||||
* @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'.
|
||||
* @param {object} provider - a local object which provides this service.
|
||||
* @returns {Promise} - a promise which will resolve once the service is registered.
|
||||
*/
|
||||
setService (service, provider) {
|
||||
if (this.services.hasOwnProperty(service)) {
|
||||
log.warn(`Worker dispatch replacing existing service provider for ${service}`);
|
||||
}
|
||||
this.services[service] = provider;
|
||||
return this.waitForConnection.then(() => this._remoteCall(self, 'dispatch', 'setService', service));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the service provider object for a particular service name.
|
||||
* @override
|
||||
* @param {string} service - the name of the service to look up
|
||||
* @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found
|
||||
* @protected
|
||||
*/
|
||||
_getServiceProvider (service) {
|
||||
// if we don't have a local service by this name, contact central dispatch by calling `postMessage` on self
|
||||
const provider = this.services[service];
|
||||
return {
|
||||
provider: provider || self,
|
||||
isRemote: !provider
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a call message sent to the dispatch service itself
|
||||
* @override
|
||||
* @param {Worker} worker - the worker which sent the message.
|
||||
* @param {DispatchCallMessage} message - the message to be handled.
|
||||
* @returns {Promise|undefined} - a promise for the results of this operation, if appropriate
|
||||
* @protected
|
||||
*/
|
||||
_onDispatchMessage (worker, message) {
|
||||
let promise;
|
||||
switch (message.method) {
|
||||
case 'handshake':
|
||||
promise = this._onConnect();
|
||||
break;
|
||||
case 'terminate':
|
||||
// Don't close until next tick, after sending confirmation back
|
||||
setTimeout(() => self.close(), 0);
|
||||
promise = Promise.resolve();
|
||||
break;
|
||||
default:
|
||||
log.error(`Worker dispatch received message for unknown method: ${message.method}`);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WorkerDispatch();
|
||||
+82
-63
@@ -1,59 +1,18 @@
|
||||
var mutationAdapter = require('./mutation-adapter');
|
||||
var html = require('htmlparser2');
|
||||
|
||||
/**
|
||||
* Adapter between block creation events and block representation which can be
|
||||
* used by the Scratch runtime.
|
||||
* @param {Object} e `Blockly.events.create`
|
||||
* @return {Array.<Object>} List of blocks from this CREATE event.
|
||||
*/
|
||||
module.exports = function (e) {
|
||||
// Validate input
|
||||
if (typeof e !== 'object') return;
|
||||
if (typeof e.xml !== 'object') return;
|
||||
|
||||
return domToBlocks(html.parseDOM(e.xml.outerHTML));
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert outer blocks DOM from a Blockly CREATE event
|
||||
* to a usable form for the Scratch runtime.
|
||||
* This structure is based on Blockly xml.js:`domToWorkspace` and `domToBlock`.
|
||||
* @param {Element} blocksDOM DOM tree for this event.
|
||||
* @return {Array.<Object>} Usable list of blocks from this CREATE event.
|
||||
*/
|
||||
function domToBlocks (blocksDOM) {
|
||||
// At this level, there could be multiple blocks adjacent in the DOM tree.
|
||||
var blocks = {};
|
||||
for (var i = 0; i < blocksDOM.length; i++) {
|
||||
var block = blocksDOM[i];
|
||||
if (!block.name || !block.attribs) {
|
||||
continue;
|
||||
}
|
||||
var tagName = block.name.toLowerCase();
|
||||
if (tagName == 'block' || tagName == 'shadow') {
|
||||
domToBlock(block, blocks, true, null);
|
||||
}
|
||||
}
|
||||
// Flatten blocks object into a list.
|
||||
var blocksList = [];
|
||||
for (var b in blocks) {
|
||||
blocksList.push(blocks[b]);
|
||||
}
|
||||
return blocksList;
|
||||
}
|
||||
const mutationAdapter = require('./mutation-adapter');
|
||||
const html = require('htmlparser2');
|
||||
|
||||
/**
|
||||
* Convert and an individual block DOM to the representation tree.
|
||||
* Based on Blockly's `domToBlockHeadless_`.
|
||||
* @param {Element} blockDOM DOM tree for an individual block.
|
||||
* @param {Object} blocks Collection of blocks to add to.
|
||||
* @param {Boolean} isTopBlock Whether blocks at this level are "top blocks."
|
||||
* @param {object} blocks Collection of blocks to add to.
|
||||
* @param {boolean} isTopBlock Whether blocks at this level are "top blocks."
|
||||
* @param {?string} parent Parent block ID.
|
||||
* @return {undefined}
|
||||
*/
|
||||
function domToBlock (blockDOM, blocks, isTopBlock, parent) {
|
||||
const domToBlock = function (blockDOM, blocks, isTopBlock, parent) {
|
||||
// Block skeleton.
|
||||
var block = {
|
||||
const block = {
|
||||
id: blockDOM.attribs.id, // Block ID
|
||||
opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag".
|
||||
inputs: {}, // Inputs to this block and the blocks they point to.
|
||||
@@ -61,7 +20,7 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
|
||||
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.
|
||||
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.
|
||||
};
|
||||
@@ -70,21 +29,21 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
|
||||
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];
|
||||
for (let i = 0; i < blockDOM.children.length; i++) {
|
||||
const 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];
|
||||
let childBlockNode = null;
|
||||
let childShadowNode = null;
|
||||
for (let j = 0; j < xmlChild.children.length; j++) {
|
||||
const grandChildNode = xmlChild.children[j];
|
||||
if (!grandChildNode.name) {
|
||||
// Non-XML tag node.
|
||||
continue;
|
||||
}
|
||||
var grandChildNodeName = grandChildNode.name.toLowerCase();
|
||||
if (grandChildNodeName == 'block') {
|
||||
const grandChildNodeName = grandChildNode.name.toLowerCase();
|
||||
if (grandChildNodeName === 'block') {
|
||||
childBlockNode = grandChildNode;
|
||||
} else if (grandChildNodeName == 'shadow') {
|
||||
} else if (grandChildNodeName === 'shadow') {
|
||||
childShadowNode = grandChildNode;
|
||||
}
|
||||
}
|
||||
@@ -98,9 +57,12 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
|
||||
// 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 = '';
|
||||
const fieldName = xmlChild.attribs.name;
|
||||
// Add id in case it is a variable field
|
||||
const fieldId = xmlChild.attribs.id;
|
||||
let fieldData = '';
|
||||
if (xmlChild.children.length > 0 && xmlChild.children[0].data) {
|
||||
fieldData = xmlChild.children[0].data;
|
||||
} else {
|
||||
@@ -110,26 +72,35 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
|
||||
}
|
||||
block.fields[fieldName] = {
|
||||
name: fieldName,
|
||||
id: fieldId,
|
||||
value: fieldData
|
||||
};
|
||||
const fieldVarType = xmlChild.attribs.variabletype;
|
||||
if (typeof fieldVarType === 'string') {
|
||||
block.fields[fieldName].variableType = fieldVarType;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'value':
|
||||
case 'statement':
|
||||
{
|
||||
// Recursively generate block structure for input block.
|
||||
domToBlock(childBlockNode, blocks, false, block.id);
|
||||
if (childShadowNode && childBlockNode != childShadowNode) {
|
||||
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;
|
||||
const 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;
|
||||
@@ -139,9 +110,57 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
|
||||
// Link next block to this block.
|
||||
block.next = childBlockNode.attribs.id;
|
||||
break;
|
||||
}
|
||||
case 'mutation':
|
||||
{
|
||||
block.mutation = mutationAdapter(xmlChild);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert outer blocks DOM from a Blockly CREATE event
|
||||
* to a usable form for the Scratch runtime.
|
||||
* This structure is based on Blockly xml.js:`domToWorkspace` and `domToBlock`.
|
||||
* @param {Element} blocksDOM DOM tree for this event.
|
||||
* @return {Array.<object>} Usable list of blocks from this CREATE event.
|
||||
*/
|
||||
const domToBlocks = function (blocksDOM) {
|
||||
// At this level, there could be multiple blocks adjacent in the DOM tree.
|
||||
const blocks = {};
|
||||
for (let i = 0; i < blocksDOM.length; i++) {
|
||||
const block = blocksDOM[i];
|
||||
if (!block.name || !block.attribs) {
|
||||
continue;
|
||||
}
|
||||
const tagName = block.name.toLowerCase();
|
||||
if (tagName === 'block' || tagName === 'shadow') {
|
||||
domToBlock(block, blocks, true, null);
|
||||
}
|
||||
}
|
||||
// Flatten blocks object into a list.
|
||||
const blocksList = [];
|
||||
for (const b in blocks) {
|
||||
if (!blocks.hasOwnProperty(b)) continue;
|
||||
blocksList.push(blocks[b]);
|
||||
}
|
||||
return blocksList;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adapter between block creation events and block representation which can be
|
||||
* used by the Scratch runtime.
|
||||
* @param {object} e `Blockly.events.create`
|
||||
* @return {Array.<object>} List of blocks from this CREATE event.
|
||||
*/
|
||||
const adapter = function (e) {
|
||||
// Validate input
|
||||
if (typeof e !== 'object') return;
|
||||
if (typeof e.xml !== 'object') return;
|
||||
|
||||
return domToBlocks(html.parseDOM(e.xml.outerHTML, {decodeEntities: true}));
|
||||
};
|
||||
|
||||
module.exports = adapter;
|
||||
@@ -0,0 +1,153 @@
|
||||
const Thread = require('./thread');
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
* Interface provided to block primitive functions for interacting with the
|
||||
* runtime, thread, target, and convenient methods.
|
||||
*/
|
||||
|
||||
class BlockUtility {
|
||||
constructor (sequencer = null, thread = null) {
|
||||
/**
|
||||
* A sequencer block primitives use to branch or start procedures with
|
||||
* @type {?Sequencer}
|
||||
*/
|
||||
this.sequencer = sequencer;
|
||||
|
||||
/**
|
||||
* The block primitives thread with the block's target, stackFrame and
|
||||
* modifiable status.
|
||||
* @type {?Thread}
|
||||
*/
|
||||
this.thread = thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* The target the primitive is working on.
|
||||
* @type {Target}
|
||||
*/
|
||||
get target () {
|
||||
return this.thread.target;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime the block primitive is running in.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
get runtime () {
|
||||
return this.sequencer.runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* The stack frame used by loop and other blocks to track internal state.
|
||||
* @type {object}
|
||||
*/
|
||||
get stackFrame () {
|
||||
return this.thread.peekStackFrame().executionContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the thread to yield.
|
||||
*/
|
||||
yield () {
|
||||
this.thread.status = Thread.STATUS_YIELD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a branch in the current block.
|
||||
* @param {number} branchNum Which branch to step to (i.e., 1, 2).
|
||||
* @param {boolean} isLoop Whether this block is a loop.
|
||||
*/
|
||||
startBranch (branchNum, isLoop) {
|
||||
this.sequencer.stepToBranch(this.thread, branchNum, isLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all threads.
|
||||
*/
|
||||
stopAll () {
|
||||
this.sequencer.runtime.stopAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop threads other on this target other than the thread holding the
|
||||
* executed block.
|
||||
*/
|
||||
stopOtherTargetThreads () {
|
||||
this.sequencer.runtime.stopForTarget(this.thread.target, this.thread);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop this thread.
|
||||
*/
|
||||
stopThisScript () {
|
||||
this.thread.stopThisScript();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a specified procedure on this thread.
|
||||
* @param {string} procedureCode Procedure code for procedure to start.
|
||||
*/
|
||||
startProcedure (procedureCode) {
|
||||
this.sequencer.stepToProcedure(this.thread, procedureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get names for parameters for the given procedure.
|
||||
* @param {string} procedureCode Procedure code for procedure to query.
|
||||
* @return {Array.<string>} List of param names for a procedure.
|
||||
*/
|
||||
getProcedureParamNamesAndIds (procedureCode) {
|
||||
return this.thread.target.blocks.getProcedureParamNamesAndIds(procedureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a procedure parameter value by its name.
|
||||
* @param {string} paramName The procedure's parameter name.
|
||||
* @param {*} paramValue The procedure's parameter value.
|
||||
*/
|
||||
pushParam (paramName, paramValue) {
|
||||
this.thread.pushParam(paramName, paramValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the stored parameter value for a given parameter name.
|
||||
* @param {string} paramName The procedure's parameter name.
|
||||
* @return {*} The parameter's current stored value.
|
||||
*/
|
||||
getParam (paramName) {
|
||||
return this.thread.getParam(paramName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all relevant hats.
|
||||
* @param {!string} requestedHat Opcode of hats to start.
|
||||
* @param {object=} optMatchFields Optionally, fields to match on the hat.
|
||||
* @param {Target=} optTarget Optionally, a target to restrict to.
|
||||
* @return {Array.<Thread>} List of threads started by this function.
|
||||
*/
|
||||
startHats (requestedHat, optMatchFields, optTarget) {
|
||||
return (
|
||||
this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query a named IO device.
|
||||
* @param {string} device The name of like the device, like keyboard.
|
||||
* @param {string} func The name of the device's function to query.
|
||||
* @param {Array.<*>} args Arguments to pass to the device's function.
|
||||
* @return {*} The expected output for the device's function.
|
||||
*/
|
||||
ioQuery (device, func, args) {
|
||||
// Find the I/O device and execute the query/function call.
|
||||
if (
|
||||
this.sequencer.runtime.ioDevices[device] &&
|
||||
this.sequencer.runtime.ioDevices[device][func]) {
|
||||
const devObject = this.sequencer.runtime.ioDevices[device];
|
||||
return devObject[func].apply(devObject, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlockUtility;
|
||||
+659
-453
File diff suppressed because it is too large.
Load diff
+229
-152
@@ -1,12 +1,101 @@
|
||||
var Thread = require('./thread');
|
||||
const BlockUtility = require('./block-utility');
|
||||
const log = require('../util/log');
|
||||
const Thread = require('./thread');
|
||||
const {Map} = require('immutable');
|
||||
const cast = require('../util/cast');
|
||||
|
||||
/**
|
||||
* Single BlockUtility instance reused by execute for every pritimive ran.
|
||||
* @const
|
||||
*/
|
||||
const blockUtility = new BlockUtility();
|
||||
|
||||
/**
|
||||
* Profiler frame name for block functions.
|
||||
* @const {string}
|
||||
*/
|
||||
const blockFunctionProfilerFrame = 'blockFunction';
|
||||
|
||||
/**
|
||||
* Profiler frame ID for 'blockFunction'.
|
||||
* @type {number}
|
||||
*/
|
||||
let blockFunctionProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Utility function to determine if a value is a Promise.
|
||||
* @param {*} value Value to check for a Promise.
|
||||
* @return {Boolean} True if the value appears to be a Promise.
|
||||
* @return {boolean} True if the value appears to be a Promise.
|
||||
*/
|
||||
var isPromise = function (value) {
|
||||
return value && value.then && typeof value.then === 'function';
|
||||
const isPromise = function (value) {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.then === 'function'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle any reported value from the primitive, either directly returned
|
||||
* or after a promise resolves.
|
||||
* @param {*} resolvedValue Value eventually returned from the primitive.
|
||||
* @param {!Sequencer} sequencer Sequencer stepping the thread for the ran
|
||||
* primitive.
|
||||
* @param {!Thread} thread Thread containing the primitive.
|
||||
* @param {!string} currentBlockId Id of the block in its thread for value from
|
||||
* the primitive.
|
||||
* @param {!string} opcode opcode used to identify a block function primitive.
|
||||
* @param {!boolean} isHat Is the current block a hat?
|
||||
*/
|
||||
// @todo move this to callback attached to the thread when we have performance
|
||||
// metrics (dd)
|
||||
const handleReport = function (
|
||||
resolvedValue, sequencer, thread, currentBlockId, opcode, isHat) {
|
||||
thread.pushReportedValue(resolvedValue);
|
||||
if (isHat) {
|
||||
// Hat predicate was evaluated.
|
||||
if (sequencer.runtime.getIsEdgeActivatedHat(opcode)) {
|
||||
// If this is an edge-activated hat, only proceed if the value is
|
||||
// true and used to be false, or the stack was activated explicitly
|
||||
// via stack click
|
||||
if (!thread.stackClick) {
|
||||
const oldEdgeValue = sequencer.runtime.updateEdgeActivatedValue(
|
||||
currentBlockId,
|
||||
resolvedValue
|
||||
);
|
||||
const edgeWasActivated = !oldEdgeValue && resolvedValue;
|
||||
if (!edgeWasActivated) {
|
||||
sequencer.retireThread(thread);
|
||||
}
|
||||
}
|
||||
} else if (!resolvedValue) {
|
||||
// Not an edge-activated hat: retire the thread
|
||||
// if predicate was false.
|
||||
sequencer.retireThread(thread);
|
||||
}
|
||||
} else {
|
||||
// In a non-hat, report the value visually if necessary if
|
||||
// at the top of the thread stack.
|
||||
if (typeof resolvedValue !== 'undefined' && thread.atStackTop()) {
|
||||
if (thread.stackClick) {
|
||||
sequencer.runtime.visualReport(currentBlockId, resolvedValue);
|
||||
}
|
||||
if (thread.updateMonitor) {
|
||||
const targetId = sequencer.runtime.monitorBlocks.getBlock(currentBlockId).targetId;
|
||||
if (targetId && !sequencer.runtime.getTargetById(targetId)) {
|
||||
// Target no longer exists
|
||||
return;
|
||||
}
|
||||
sequencer.runtime.requestUpdateMonitor(Map({
|
||||
id: currentBlockId,
|
||||
spriteName: targetId ? sequencer.runtime.getTargetById(targetId).getName() : null,
|
||||
value: String(resolvedValue)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Finished any yields.
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -14,127 +103,99 @@ var isPromise = function (value) {
|
||||
* @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'
|
||||
);
|
||||
const execute = function (sequencer, thread) {
|
||||
const runtime = sequencer.runtime;
|
||||
const target = thread.target;
|
||||
|
||||
// Stop if block or target no longer exists.
|
||||
if (!target || (!targetHasBlock && !flyoutHasBlock)) {
|
||||
if (target === null) {
|
||||
// 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;
|
||||
// Current block to execute is the one on the top of the stack.
|
||||
const currentBlockId = thread.peekStack();
|
||||
const currentStackFrame = thread.peekStackFrame();
|
||||
|
||||
let blockContainer;
|
||||
if (thread.updateMonitor) {
|
||||
blockContainer = runtime.monitorBlocks;
|
||||
} else {
|
||||
blockContainer = runtime.flyoutBlocks;
|
||||
blockContainer = target.blocks;
|
||||
}
|
||||
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);
|
||||
let block = blockContainer.getBlock(currentBlockId);
|
||||
if (typeof block === 'undefined') {
|
||||
blockContainer = runtime.flyoutBlocks;
|
||||
block = blockContainer.getBlock(currentBlockId);
|
||||
// Stop if block or target no longer exists.
|
||||
if (typeof block === 'undefined') {
|
||||
// No block found: stop the thread; script no longer exists.
|
||||
sequencer.retireThread(thread);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const opcode = blockContainer.getOpcode(block);
|
||||
const fields = blockContainer.getFields(block);
|
||||
const inputs = blockContainer.getInputs(block);
|
||||
const blockFunction = runtime.getOpcodeFunction(opcode);
|
||||
const isHat = runtime.getIsHat(opcode);
|
||||
|
||||
|
||||
if (!opcode) {
|
||||
console.warn('Could not get opcode for block: ' + currentBlockId);
|
||||
log.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 (typeof blockFunction === 'undefined') {
|
||||
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;
|
||||
}
|
||||
const keys = Object.keys(fields);
|
||||
if (keys.length === 1 && Object.keys(inputs).length === 0) {
|
||||
// One field and no inputs - treat as arg.
|
||||
handleReport(fields[keys[0]].value, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
} else {
|
||||
log.warn(`Could not get implementation for opcode: ${opcode}`);
|
||||
}
|
||||
thread.requestScriptGlowInFrame = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate values for arguments (inputs).
|
||||
var argValues = {};
|
||||
const argValues = {};
|
||||
|
||||
// Add all fields on this block to the argValues.
|
||||
for (var fieldName in fields) {
|
||||
argValues[fieldName] = fields[fieldName].value;
|
||||
for (const fieldName in fields) {
|
||||
if (!fields.hasOwnProperty(fieldName)) continue;
|
||||
if (fieldName === 'VARIABLE' || fieldName === 'LIST' ||
|
||||
fieldName === 'BROADCAST_OPTION') {
|
||||
argValues[fieldName] = {
|
||||
id: fields[fieldName].id,
|
||||
name: fields[fieldName].value
|
||||
};
|
||||
} else {
|
||||
argValues[fieldName] = fields[fieldName].value;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively evaluate input blocks.
|
||||
for (var inputName in inputs) {
|
||||
var input = inputs[inputName];
|
||||
var inputBlockId = input.block;
|
||||
for (const inputName in inputs) {
|
||||
if (!inputs.hasOwnProperty(inputName)) continue;
|
||||
// Do not evaluate the internal custom command block within definition
|
||||
if (inputName === 'custom_block') continue;
|
||||
const input = inputs[inputName];
|
||||
const inputBlockId = input.block;
|
||||
// Is there no value for this input waiting in the stack frame?
|
||||
if (typeof currentStackFrame.reported[inputName] === 'undefined'
|
||||
&& inputBlockId) {
|
||||
if (inputBlockId !== null && typeof currentStackFrame.reported[inputName] === 'undefined') {
|
||||
// If there's not, we need to evaluate the block.
|
||||
// Push to the stack to evaluate the reporter block.
|
||||
thread.pushStack(inputBlockId);
|
||||
@@ -144,19 +205,42 @@ var execute = function (sequencer, thread) {
|
||||
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();
|
||||
}
|
||||
|
||||
// Execution returned immediately,
|
||||
// and presumably a value was reported, so pop the stack.
|
||||
currentStackFrame.waitingReporter = null;
|
||||
thread.popStack();
|
||||
}
|
||||
const inputValue = currentStackFrame.reported[inputName];
|
||||
if (inputName === 'BROADCAST_INPUT') {
|
||||
const broadcastInput = inputs[inputName];
|
||||
// Check if something is plugged into the broadcast block, or
|
||||
// if the shadow dropdown menu is being used.
|
||||
if (broadcastInput.block === broadcastInput.shadow) {
|
||||
// Shadow dropdown menu is being used.
|
||||
// Get the appropriate information out of it.
|
||||
const shadow = blockContainer.getBlock(broadcastInput.shadow);
|
||||
const broadcastField = shadow.fields.BROADCAST_OPTION;
|
||||
argValues.BROADCAST_OPTION = {
|
||||
id: broadcastField.id,
|
||||
name: broadcastField.value
|
||||
};
|
||||
} else {
|
||||
// Something is plugged into the broadcast input.
|
||||
// Cast it to a string. We don't need an id here.
|
||||
argValues.BROADCAST_OPTION = {
|
||||
name: cast.toString(inputValue)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
argValues[inputName] = inputValue;
|
||||
}
|
||||
argValues[inputName] = currentStackFrame.reported[inputName];
|
||||
}
|
||||
|
||||
// Add any mutation to args (e.g., for procedures).
|
||||
var mutation = blockContainer.getMutation(currentBlockId);
|
||||
if (mutation) {
|
||||
const mutation = blockContainer.getMutation(block);
|
||||
if (mutation !== null) {
|
||||
argValues.mutation = mutation;
|
||||
}
|
||||
|
||||
@@ -166,50 +250,26 @@ var execute = function (sequencer, thread) {
|
||||
// (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);
|
||||
}
|
||||
let primitiveReportedValue = null;
|
||||
blockUtility.sequencer = sequencer;
|
||||
blockUtility.thread = thread;
|
||||
if (runtime.profiler !== null) {
|
||||
if (blockFunctionProfilerId === -1) {
|
||||
blockFunctionProfilerId = runtime.profiler.idByName(blockFunctionProfilerFrame);
|
||||
}
|
||||
});
|
||||
// The method commented below has its code inlined underneath to reduce
|
||||
// the bias recorded for the profiler's calls in this time sensitive
|
||||
// execute function.
|
||||
//
|
||||
// runtime.profiler.start(blockFunctionProfilerId, opcode);
|
||||
runtime.profiler.records.push(
|
||||
runtime.profiler.START, blockFunctionProfilerId, opcode, performance.now());
|
||||
}
|
||||
primitiveReportedValue = blockFunction(argValues, blockUtility);
|
||||
if (runtime.profiler !== null) {
|
||||
// runtime.profiler.stop(blockFunctionProfilerId);
|
||||
runtime.profiler.records.push(runtime.profiler.STOP, performance.now());
|
||||
}
|
||||
|
||||
if (typeof primitiveReportedValue === 'undefined') {
|
||||
// No value reported - potentially a command block.
|
||||
@@ -224,24 +284,41 @@ var execute = function (sequencer, 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);
|
||||
primitiveReportedValue.then(resolvedValue => {
|
||||
handleReport(resolvedValue, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
if (typeof resolvedValue === 'undefined') {
|
||||
let stackFrame;
|
||||
let nextBlockId;
|
||||
do {
|
||||
// In the case that the promise is the last block in the current thread stack
|
||||
// We need to pop out repeatedly until we find the next block.
|
||||
const popped = thread.popStack();
|
||||
if (popped === null) {
|
||||
return;
|
||||
}
|
||||
nextBlockId = thread.target.blocks.getNextBlock(popped);
|
||||
if (nextBlockId !== null) {
|
||||
// A next block exists so break out this loop
|
||||
break;
|
||||
}
|
||||
// Investigate the next block and if not in a loop,
|
||||
// then repeat and pop the next item off the stack frame
|
||||
stackFrame = thread.peekStackFrame();
|
||||
} while (stackFrame !== null && !stackFrame.isLoop);
|
||||
|
||||
thread.pushStack(nextBlockId);
|
||||
} else {
|
||||
thread.popStack();
|
||||
}
|
||||
}, function(rejectionReason) {
|
||||
}, rejectionReason => {
|
||||
// Promise rejected: the primitive had some error.
|
||||
// Log it and proceed.
|
||||
console.warn('Primitive rejected promise: ', rejectionReason);
|
||||
log.warn('Primitive rejected promise: ', rejectionReason);
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
thread.popStack();
|
||||
});
|
||||
} else if (thread.status === Thread.STATUS_RUNNING) {
|
||||
handleReport(primitiveReportedValue);
|
||||
handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* Object representing a Scratch list.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {!string} name Name of the list.
|
||||
* @param {Array} contents Contents of the list, as an array.
|
||||
* @constructor
|
||||
*/
|
||||
function List (name, contents) {
|
||||
this.name = name;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
module.exports = List;
|
||||
@@ -0,0 +1,14 @@
|
||||
const {Record} = require('immutable');
|
||||
|
||||
const MonitorRecord = Record({
|
||||
id: null,
|
||||
/** Present only if the monitor is sprite-specific, such as x position */
|
||||
spriteName: null,
|
||||
/** Present only if the monitor is sprite-specific, such as x position */
|
||||
targetId: null,
|
||||
opcode: null,
|
||||
value: null,
|
||||
params: null
|
||||
});
|
||||
|
||||
module.exports = MonitorRecord;
|
||||
@@ -1,13 +1,35 @@
|
||||
var html = require('htmlparser2');
|
||||
const html = require('htmlparser2');
|
||||
const decodeHtml = require('decode-html');
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const mutatorTagToObject = function (dom) {
|
||||
const obj = Object.create(null);
|
||||
obj.tagName = dom.name;
|
||||
obj.children = [];
|
||||
for (const prop in dom.attribs) {
|
||||
if (prop === 'xmlns') continue;
|
||||
obj[prop] = decodeHtml(dom.attribs[prop]);
|
||||
}
|
||||
for (let i = 0; i < dom.children.length; i++) {
|
||||
obj.children.push(
|
||||
mutatorTagToObject(dom.children[i])
|
||||
);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {(object|string)} mutation Mutation XML string or DOM.
|
||||
* @return {object} Object representing the mutation.
|
||||
*/
|
||||
module.exports = function (mutation) {
|
||||
var mutationParsed;
|
||||
const mutationAdpater = function (mutation) {
|
||||
let mutationParsed;
|
||||
// Check if the mutation is already parsed; if not, parse it.
|
||||
if (typeof mutation === 'object') {
|
||||
mutationParsed = mutation;
|
||||
@@ -17,23 +39,4 @@ module.exports = function (mutation) {
|
||||
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;
|
||||
}
|
||||
module.exports = mutationAdpater;
|
||||
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* A way to profile Scratch internal performance. Like what blocks run during a
|
||||
* step? How much time do they take? How much time is spent inbetween blocks?
|
||||
*
|
||||
* Profiler aims for to spend as little time inside its functions while
|
||||
* recording. For this it has a simple internal record structure that records a
|
||||
* series of values for each START and STOP event in a single array. This lets
|
||||
* all the values be pushed in one call for the array. This simplicity allows
|
||||
* the contents of the start() and stop() calls to be inlined in areas that are
|
||||
* called frequently enough to want even greater performance from Profiler so
|
||||
* what is recorded better reflects on the profiled code and not Profiler
|
||||
* itself.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The next id returned for a new profile'd function.
|
||||
* @type {number}
|
||||
*/
|
||||
let nextId = 0;
|
||||
|
||||
/**
|
||||
* The mapping of names to ids.
|
||||
* @const {Object.<string, number>}
|
||||
*/
|
||||
const profilerNames = {};
|
||||
|
||||
/**
|
||||
* The START event identifier in Profiler records.
|
||||
* @const {number}
|
||||
*/
|
||||
const START = 0;
|
||||
|
||||
/**
|
||||
* The STOP event identifier in Profiler records.
|
||||
* @const {number}
|
||||
*/
|
||||
const STOP = 1;
|
||||
|
||||
/**
|
||||
* The number of cells used in the records array by a START event.
|
||||
* @const {number}
|
||||
*/
|
||||
const START_SIZE = 4;
|
||||
|
||||
/**
|
||||
* The number of cells used in the records array by a STOP event.
|
||||
* @const {number}
|
||||
*/
|
||||
const STOP_SIZE = 2;
|
||||
|
||||
/**
|
||||
* Stored reference to Performance instance provided by the Browser.
|
||||
* @const {Performance}
|
||||
*/
|
||||
const performance = typeof window === 'object' && window.performance;
|
||||
|
||||
|
||||
/**
|
||||
* Callback handle called by Profiler for each frame it decodes from its
|
||||
* records.
|
||||
* @callback FrameCallback
|
||||
* @param {ProfilerFrame} frame
|
||||
*/
|
||||
|
||||
/**
|
||||
* A set of information about a frame of execution that was recorded.
|
||||
*/
|
||||
class ProfilerFrame {
|
||||
/**
|
||||
* @param {number} depth Depth of the frame in the recorded stack.
|
||||
*/
|
||||
constructor (depth) {
|
||||
/**
|
||||
* The numeric id of a record symbol like Runtime._step or
|
||||
* blockFunction.
|
||||
* @type {number}
|
||||
*/
|
||||
this.id = -1;
|
||||
|
||||
/**
|
||||
* The amount of time spent inside the recorded frame and any deeper
|
||||
* frames.
|
||||
* @type {number}
|
||||
*/
|
||||
this.totalTime = 0;
|
||||
|
||||
/**
|
||||
* The amount of time spent only inside this record frame. Not
|
||||
* including time in any deeper frames.
|
||||
* @type {number}
|
||||
*/
|
||||
this.selfTime = 0;
|
||||
|
||||
/**
|
||||
* An arbitrary argument for the recorded frame. For example a block
|
||||
* function might record its opcode as an argument.
|
||||
* @type {*}
|
||||
*/
|
||||
this.arg = null;
|
||||
|
||||
/**
|
||||
* The depth of the recorded frame. This can help compare recursive
|
||||
* funtions that are recorded. Each level of recursion with have a
|
||||
* different depth value.
|
||||
* @type {number}
|
||||
*/
|
||||
this.depth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
class Profiler {
|
||||
/**
|
||||
* @param {FrameCallback} onFrame a handle called for each recorded frame.
|
||||
* The passed frame value may not be stored as it'll be updated with later
|
||||
* frame information. Any information that is further stored by the handler
|
||||
* should make copies or reduce the information.
|
||||
*/
|
||||
constructor (onFrame = function () {}) {
|
||||
/**
|
||||
* A series of START and STOP values followed by arguments. After
|
||||
* recording is complete the full set of records is reported back by
|
||||
* stepping through the series to connect the relative START and STOP
|
||||
* information.
|
||||
* @type {Array.<*>}
|
||||
*/
|
||||
this.records = [];
|
||||
|
||||
/**
|
||||
* A cache of ProfilerFrames to reuse when reporting the recorded
|
||||
* frames in records.
|
||||
* @type {Array.<ProfilerFrame>}
|
||||
*/
|
||||
this._stack = [new ProfilerFrame(0)];
|
||||
|
||||
/**
|
||||
* A callback handle called with each decoded frame when reporting back
|
||||
* all the recorded times.
|
||||
* @type {FrameCallback}
|
||||
*/
|
||||
this.onFrame = onFrame;
|
||||
|
||||
/**
|
||||
* A reference to the START record id constant.
|
||||
* @const {number}
|
||||
*/
|
||||
this.START = START;
|
||||
|
||||
/**
|
||||
* A reference to the STOP record id constant.
|
||||
* @const {number}
|
||||
*/
|
||||
this.STOP = STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start recording a frame of time for an id and optional argument.
|
||||
* @param {number} id The id returned by idByName for a name symbol like
|
||||
* Runtime._step.
|
||||
* @param {?*} arg An arbitrary argument value to store with the frame.
|
||||
*/
|
||||
start (id, arg) {
|
||||
this.records.push(START, id, arg, performance.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current frame.
|
||||
*/
|
||||
stop () {
|
||||
this.records.push(STOP, performance.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode records and report all frames to `this.onFrame`.
|
||||
*/
|
||||
reportFrames () {
|
||||
const stack = this._stack;
|
||||
let depth = 1;
|
||||
|
||||
// Step through the records and initialize Frame instances from the
|
||||
// START and STOP events. START and STOP events are separated by events
|
||||
// for deeper frames run by higher frames. Frames are stored on a stack
|
||||
// and reinitialized for each START event. When a stop event is reach
|
||||
// the Frame for the current depth has its final values stored and its
|
||||
// passed to the current onFrame callback. This way Frames are "pushed"
|
||||
// for each START event and "popped" for each STOP and handed to an
|
||||
// outside handle to any desired reduction of the collected data.
|
||||
for (let i = 0; i < this.records.length;) {
|
||||
if (this.records[i] === START) {
|
||||
if (depth >= stack.length) {
|
||||
stack.push(new ProfilerFrame(depth));
|
||||
}
|
||||
|
||||
// Store id, arg, totalTime, and initialize selfTime.
|
||||
const frame = stack[depth++];
|
||||
frame.id = this.records[i + 1];
|
||||
frame.arg = this.records[i + 2];
|
||||
// totalTime is first set as the time recorded by this START
|
||||
// event. Once the STOP event is reached the stored start time
|
||||
// is subtracted from the recorded stop time. The resulting
|
||||
// difference is the actual totalTime, and replaces the start
|
||||
// time in frame.totalTime.
|
||||
//
|
||||
// totalTime is used this way as a convenient member to store a
|
||||
// value between the two events without needing additional
|
||||
// members on the Frame or in a shadow map.
|
||||
frame.totalTime = this.records[i + 3];
|
||||
// selfTime is decremented until we reach the STOP event for
|
||||
// this frame. totalTime will be added to it then to get the
|
||||
// time difference.
|
||||
frame.selfTime = 0;
|
||||
|
||||
i += START_SIZE;
|
||||
} else if (this.records[i] === STOP) {
|
||||
const now = this.records[i + 1];
|
||||
|
||||
const frame = stack[--depth];
|
||||
// totalTime is the difference between the start event time
|
||||
// stored in totalTime and the stop event time pulled from this
|
||||
// record.
|
||||
frame.totalTime = now - frame.totalTime;
|
||||
// selfTime is the difference of this frame's totalTime and the
|
||||
// sum of totalTime of deeper frames.
|
||||
frame.selfTime += frame.totalTime;
|
||||
|
||||
// Remove this frames totalTime from the parent's selfTime.
|
||||
stack[depth - 1].selfTime -= frame.totalTime;
|
||||
|
||||
this.onFrame(frame);
|
||||
|
||||
i += STOP_SIZE;
|
||||
} else {
|
||||
this.records.length = 0;
|
||||
throw new Error('Unable to decode Profiler records.');
|
||||
}
|
||||
}
|
||||
|
||||
this.records.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup or create an id for a frame name.
|
||||
* @param {string} name The name to return an id for.
|
||||
* @return {number} The id for the passed name.
|
||||
*/
|
||||
idByName (name) {
|
||||
return Profiler.idByName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse lookup the name from a given frame id.
|
||||
* @param {number} id The id to search for.
|
||||
* @return {string} The name for the given id.
|
||||
*/
|
||||
nameById (id) {
|
||||
return Profiler.nameById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup or create an id for a frame name.
|
||||
* @static
|
||||
* @param {string} name The name to return an id for.
|
||||
* @return {number} The id for the passed name.
|
||||
*/
|
||||
static idByName (name) {
|
||||
if (typeof profilerNames[name] !== 'number') {
|
||||
profilerNames[name] = nextId++;
|
||||
}
|
||||
return profilerNames[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse lookup the name from a given frame id.
|
||||
* @static
|
||||
* @param {number} id The id to search for.
|
||||
* @return {string} The name for the given id.
|
||||
*/
|
||||
static nameById (id) {
|
||||
for (const name in profilerNames) {
|
||||
if (profilerNames[name] === id) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profiler is only available on platforms with the Performance API.
|
||||
* @return {boolean} Can the Profiler run in this browser?
|
||||
*/
|
||||
static available () {
|
||||
return (
|
||||
typeof window === 'object' &&
|
||||
typeof window.performance !== 'undefined');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A reference to the START record id constant.
|
||||
* @const {number}
|
||||
*/
|
||||
Profiler.START = START;
|
||||
|
||||
/**
|
||||
* A reference to the STOP record id constant.
|
||||
* @const {number}
|
||||
*/
|
||||
Profiler.STOP = STOP;
|
||||
|
||||
module.exports = Profiler;
|
||||
+1524
-780
File diff suppressed because it is too large.
Load diff
+315
-220
@@ -1,254 +1,349 @@
|
||||
var Timer = require('../util/timer');
|
||||
var Thread = require('./thread');
|
||||
var execute = require('./execute.js');
|
||||
|
||||
function Sequencer (runtime) {
|
||||
/**
|
||||
* A utility timer for timing thread sequencing.
|
||||
* @type {!Timer}
|
||||
*/
|
||||
this.timer = new Timer();
|
||||
|
||||
/**
|
||||
* Reference to the runtime owning this sequencer.
|
||||
* @type {!Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
const Timer = require('../util/timer');
|
||||
const Thread = require('./thread');
|
||||
const execute = require('./execute.js');
|
||||
|
||||
/**
|
||||
* Time to run a warp-mode thread, in ms.
|
||||
* Profiler frame name for stepping a single thread.
|
||||
* @const {string}
|
||||
*/
|
||||
const stepThreadProfilerFrame = 'Sequencer.stepThread';
|
||||
|
||||
/**
|
||||
* Profiler frame name for the inner loop of stepThreads.
|
||||
* @const {string}
|
||||
*/
|
||||
const stepThreadsInnerProfilerFrame = 'Sequencer.stepThreads#inner';
|
||||
|
||||
/**
|
||||
* Profiler frame name for execute.
|
||||
* @const {string}
|
||||
*/
|
||||
const executeProfilerFrame = 'execute';
|
||||
|
||||
/**
|
||||
* Profiler frame ID for stepThreadProfilerFrame.
|
||||
* @type {number}
|
||||
*/
|
||||
Sequencer.WARP_TIME = 500;
|
||||
let stepThreadProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Step through all threads in `this.runtime.threads`, running them in order.
|
||||
* @return {Array.<!Thread>} List of inactive threads after stepping.
|
||||
* Profiler frame ID for stepThreadsInnerProfilerFrame.
|
||||
* @type {number}
|
||||
*/
|
||||
Sequencer.prototype.stepThreads = function () {
|
||||
// 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();
|
||||
// Count of active threads.
|
||||
var numActiveThreads = Infinity;
|
||||
// Whether `stepThreads` has run through a full single tick.
|
||||
var ranFirstTick = false;
|
||||
var inactiveThreads = [];
|
||||
// Conditions for continuing to stepping threads:
|
||||
// 1. We must have threads in the list, and some must be active.
|
||||
// 2. Time elapsed must be less than WORK_TIME.
|
||||
// 3. Either turbo mode, or no redraw has been requested by a primitive.
|
||||
while (this.runtime.threads.length > 0 &&
|
||||
numActiveThreads > 0 &&
|
||||
this.timer.timeElapsed() < WORK_TIME &&
|
||||
(this.runtime.turboMode || !this.runtime.redrawRequested)) {
|
||||
let stepThreadsInnerProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Profiler frame ID for executeProfilerFrame.
|
||||
* @type {number}
|
||||
*/
|
||||
let executeProfilerId = -1;
|
||||
|
||||
class Sequencer {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* A utility timer for timing thread sequencing.
|
||||
* @type {!Timer}
|
||||
*/
|
||||
this.timer = new Timer();
|
||||
|
||||
/**
|
||||
* Reference to the runtime owning this sequencer.
|
||||
* @type {!Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time to run a warp-mode thread, in ms.
|
||||
* @type {number}
|
||||
*/
|
||||
static get WARP_TIME () {
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step through all threads in `this.runtime.threads`, running them in order.
|
||||
* @return {Array.<!Thread>} List of inactive threads after stepping.
|
||||
*/
|
||||
stepThreads () {
|
||||
// Work time is 75% of the thread stepping interval.
|
||||
const WORK_TIME = 0.75 * this.runtime.currentStepTime;
|
||||
// Start counting toward WORK_TIME.
|
||||
this.timer.start();
|
||||
// Count of active threads.
|
||||
let numActiveThreads = Infinity;
|
||||
// Whether `stepThreads` has run through a full single tick.
|
||||
let ranFirstTick = false;
|
||||
const doneThreads = this.runtime.threads.map(() => null);
|
||||
// Conditions for continuing to stepping threads:
|
||||
// 1. We must have threads in the list, and some must be active.
|
||||
// 2. Time elapsed must be less than WORK_TIME.
|
||||
// 3. Either turbo mode, or no redraw has been requested by a primitive.
|
||||
while (this.runtime.threads.length > 0 &&
|
||||
numActiveThreads > 0 &&
|
||||
this.timer.timeElapsed() < WORK_TIME &&
|
||||
(this.runtime.turboMode || !this.runtime.redrawRequested)) {
|
||||
if (this.runtime.profiler !== null) {
|
||||
if (stepThreadsInnerProfilerId === -1) {
|
||||
stepThreadsInnerProfilerId = this.runtime.profiler.idByName(stepThreadsInnerProfilerFrame);
|
||||
}
|
||||
this.runtime.profiler.start(stepThreadsInnerProfilerId);
|
||||
}
|
||||
|
||||
numActiveThreads = 0;
|
||||
// Attempt to run each thread one time.
|
||||
for (let i = 0; i < this.runtime.threads.length; i++) {
|
||||
const activeThread = this.runtime.threads[i];
|
||||
if (activeThread.stack.length === 0 ||
|
||||
activeThread.status === Thread.STATUS_DONE) {
|
||||
// Finished with this thread.
|
||||
doneThreads[i] = activeThread;
|
||||
continue;
|
||||
}
|
||||
// A thread was removed, added or this thread was restarted.
|
||||
if (doneThreads[i] !== null) {
|
||||
doneThreads[i] = null;
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_YIELD_TICK &&
|
||||
!ranFirstTick) {
|
||||
// Clear single-tick yield from the last call of `stepThreads`.
|
||||
activeThread.status = Thread.STATUS_RUNNING;
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_RUNNING ||
|
||||
activeThread.status === Thread.STATUS_YIELD) {
|
||||
// Normal-mode thread: step.
|
||||
if (this.runtime.profiler !== null) {
|
||||
if (stepThreadProfilerId === -1) {
|
||||
stepThreadProfilerId = this.runtime.profiler.idByName(stepThreadProfilerFrame);
|
||||
}
|
||||
this.runtime.profiler.start(stepThreadProfilerId);
|
||||
}
|
||||
this.stepThread(activeThread);
|
||||
if (this.runtime.profiler !== null) {
|
||||
this.runtime.profiler.stop();
|
||||
}
|
||||
activeThread.warpTimer = null;
|
||||
if (activeThread.isKilled) {
|
||||
i--; // if the thread is removed from the list (killed), do not increase index
|
||||
}
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_RUNNING) {
|
||||
numActiveThreads++;
|
||||
}
|
||||
}
|
||||
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
|
||||
// threads on the next tick.
|
||||
ranFirstTick = true;
|
||||
|
||||
if (this.runtime.profiler !== null) {
|
||||
this.runtime.profiler.stop();
|
||||
}
|
||||
}
|
||||
// Filter inactive threads from `this.runtime.threads`.
|
||||
numActiveThreads = 0;
|
||||
// Inline copy of the threads, updated on each step.
|
||||
var threadsCopy = this.runtime.threads.slice();
|
||||
// Attempt to run each thread one time.
|
||||
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.
|
||||
this.stepThread(activeThread);
|
||||
activeThread.warpTimer = null;
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_RUNNING) {
|
||||
// After stepping, status is still running.
|
||||
// If we're in single-stepping mode, mark the thread as
|
||||
// a single-tick yield so it doesn't re-execute
|
||||
// until the next frame.
|
||||
if (this.runtime.singleStepping) {
|
||||
activeThread.status = Thread.STATUS_YIELD_TICK;
|
||||
}
|
||||
for (let i = 0; i < this.runtime.threads.length; i++) {
|
||||
const thread = this.runtime.threads[i];
|
||||
if (doneThreads[i] === null) {
|
||||
this.runtime.threads[numActiveThreads] = thread;
|
||||
numActiveThreads++;
|
||||
}
|
||||
}
|
||||
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
|
||||
// threads on the next tick.
|
||||
ranFirstTick = true;
|
||||
}
|
||||
// Filter inactive threads from `this.runtime.threads`.
|
||||
this.runtime.threads = this.runtime.threads.filter(function(thread) {
|
||||
if (inactiveThreads.indexOf(thread) > -1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return inactiveThreads;
|
||||
};
|
||||
this.runtime.threads.length = numActiveThreads;
|
||||
|
||||
/**
|
||||
* Step the requested thread for as long as necessary.
|
||||
* @param {!Thread} thread Thread object to step.
|
||||
*/
|
||||
Sequencer.prototype.stepThread = function (thread) {
|
||||
var currentBlockId = thread.peekStack();
|
||||
if (!currentBlockId) {
|
||||
// A "null block" - empty branch.
|
||||
thread.popStack();
|
||||
}
|
||||
while (thread.peekStack()) {
|
||||
var isWarpMode = thread.peekStackFrame().warpMode;
|
||||
if (isWarpMode && !thread.warpTimer) {
|
||||
// Initialize warp-mode timer if it hasn't been already.
|
||||
// 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;
|
||||
// Filter undefined and null values from `doneThreads`.
|
||||
let numDoneThreads = 0;
|
||||
for (let i = 0; i < doneThreads.length; i++) {
|
||||
const maybeThread = doneThreads[i];
|
||||
if (maybeThread !== null) {
|
||||
doneThreads[numDoneThreads] = maybeThread;
|
||||
numDoneThreads++;
|
||||
}
|
||||
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()) {
|
||||
doneThreads.length = numDoneThreads;
|
||||
|
||||
return doneThreads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the requested thread for as long as necessary.
|
||||
* @param {!Thread} thread Thread object to step.
|
||||
*/
|
||||
stepThread (thread) {
|
||||
let currentBlockId = thread.peekStack();
|
||||
if (!currentBlockId) {
|
||||
// A "null block" - empty branch.
|
||||
thread.popStack();
|
||||
if (thread.stack.length === 0) {
|
||||
// No more stack to run!
|
||||
thread.status = Thread.STATUS_DONE;
|
||||
}
|
||||
while (thread.peekStack()) {
|
||||
let isWarpMode = thread.peekStackFrame().warpMode;
|
||||
if (isWarpMode && !thread.warpTimer) {
|
||||
// Initialize warp-mode timer if it hasn't been already.
|
||||
// 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();
|
||||
if (this.runtime.profiler !== null) {
|
||||
if (executeProfilerId === -1) {
|
||||
executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame);
|
||||
}
|
||||
// The method commented below has its code inlined underneath to
|
||||
// reduce the bias recorded for the profiler's calls in this
|
||||
// time sensitive stepThread method.
|
||||
//
|
||||
// this.runtime.profiler.start(executeProfilerId, null);
|
||||
this.runtime.profiler.records.push(
|
||||
this.runtime.profiler.START, executeProfilerId, null, performance.now());
|
||||
}
|
||||
execute(this, thread);
|
||||
if (this.runtime.profiler !== null) {
|
||||
// this.runtime.profiler.stop();
|
||||
this.runtime.profiler.records.push(this.runtime.profiler.STOP, performance.now());
|
||||
}
|
||||
thread.blockGlowInFrame = currentBlockId;
|
||||
// If the thread has yielded or is waiting, yield to other threads.
|
||||
if (thread.status === Thread.STATUS_YIELD) {
|
||||
// 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 (thread.peekStackFrame().isLoop) {
|
||||
// The current level of the stack is marked as a loop.
|
||||
// Return to yield for the frame/tick in general.
|
||||
// Unless we're in warp mode - then only return if the
|
||||
// warp timer is up.
|
||||
if (!isWarpMode ||
|
||||
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
|
||||
// Don't do anything to the stack, since loops need
|
||||
// to be re-executed.
|
||||
// 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;
|
||||
return;
|
||||
} else {
|
||||
}
|
||||
|
||||
const stackFrame = thread.peekStackFrame();
|
||||
isWarpMode = stackFrame.warpMode;
|
||||
|
||||
if (stackFrame.isLoop) {
|
||||
// The current level of the stack is marked as a loop.
|
||||
// Return to yield for the frame/tick in general.
|
||||
// Unless we're in warp mode - then only return if the
|
||||
// warp timer is up.
|
||||
if (!isWarpMode ||
|
||||
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
|
||||
// Don't do anything to the stack, since loops need
|
||||
// to be re-executed.
|
||||
return;
|
||||
}
|
||||
// Don't go to the next block for this level of the stack,
|
||||
// since loops need to be re-executed.
|
||||
continue;
|
||||
|
||||
} else if (stackFrame.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;
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
// 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) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Step a thread into a block's branch.
|
||||
* @param {!Thread} thread Thread object to step to branch.
|
||||
* @param {number} branchNum Which branch to step to (i.e., 1, 2).
|
||||
* @param {boolean} isLoop Whether this block is a loop.
|
||||
*/
|
||||
stepToBranch (thread, branchNum, isLoop) {
|
||||
if (!branchNum) {
|
||||
branchNum = 1;
|
||||
}
|
||||
const currentBlockId = thread.peekStack();
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step a procedure.
|
||||
* @param {!Thread} thread Thread object to step to procedure.
|
||||
* @param {!string} procedureCode Procedure code of procedure to step to.
|
||||
*/
|
||||
stepToProcedure (thread, procedureCode) {
|
||||
const definition = thread.target.blocks.getProcedureDefinition(procedureCode);
|
||||
if (!definition) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Step a thread into a block's branch.
|
||||
* @param {!Thread} thread Thread object to step to branch.
|
||||
* @param {Number} branchNum Which branch to step to (i.e., 1, 2).
|
||||
* @param {Boolean} isLoop Whether this block is a loop.
|
||||
*/
|
||||
Sequencer.prototype.stepToBranch = function (thread, branchNum, isLoop) {
|
||||
if (!branchNum) {
|
||||
branchNum = 1;
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Step a procedure.
|
||||
* @param {!Thread} thread Thread object to step to procedure.
|
||||
* @param {!string} procedureCode Procedure code of procedure to step to.
|
||||
*/
|
||||
Sequencer.prototype.stepToProcedure = function (thread, procedureCode) {
|
||||
var definition = thread.target.blocks.getProcedureDefinition(procedureCode);
|
||||
if (!definition) {
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
// Check if the call is recursive.
|
||||
// If so, set the thread to yield after pushing.
|
||||
const 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 {
|
||||
// In normal-mode threads, yield any time we have a recursive call.
|
||||
if (isRecursive) {
|
||||
// Look for warp-mode flag on definition, and set the thread
|
||||
// to warp-mode if needed.
|
||||
const definitionBlock = thread.target.blocks.getBlock(definition);
|
||||
const innerBlock = thread.target.blocks.getBlock(
|
||||
definitionBlock.inputs.custom_block.block);
|
||||
let doWarp = false;
|
||||
if (innerBlock && innerBlock.mutation) {
|
||||
const warp = innerBlock.mutation.warp;
|
||||
if (typeof warp === 'boolean') {
|
||||
doWarp = warp;
|
||||
} else if (typeof warp === 'string') {
|
||||
doWarp = JSON.parse(warp);
|
||||
}
|
||||
}
|
||||
if (doWarp) {
|
||||
thread.peekStackFrame().warpMode = true;
|
||||
} else if (isRecursive) {
|
||||
// In normal-mode threads, yield any time we have a recursive call.
|
||||
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;
|
||||
};
|
||||
/**
|
||||
* Retire a thread in the middle, without considering further blocks.
|
||||
* @param {!Thread} thread Thread object to retire.
|
||||
*/
|
||||
retireThread (thread) {
|
||||
thread.stack = [];
|
||||
thread.stackFrame = [];
|
||||
thread.requestScriptGlowInFrame = false;
|
||||
thread.status = Thread.STATUS_DONE;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Sequencer;
|
||||
+251
-97
@@ -1,7 +1,10 @@
|
||||
var Blocks = require('./blocks');
|
||||
var Variable = require('../engine/variable');
|
||||
var List = require('../engine/list');
|
||||
var uid = require('../util/uid');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const Blocks = require('./blocks');
|
||||
const Variable = require('../engine/variable');
|
||||
const uid = require('../util/uid');
|
||||
const {Map} = require('immutable');
|
||||
const log = require('../util/log');
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
@@ -9,108 +12,259 @@ var uid = require('../util/uid');
|
||||
* 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);
|
||||
class Target extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @param {Runtime} runtime Reference to the runtime.
|
||||
* @param {?Blocks} blocks Blocks instance for the blocks owned by this target.
|
||||
* @constructor
|
||||
*/
|
||||
constructor (runtime, blocks) {
|
||||
super();
|
||||
|
||||
if (!blocks) {
|
||||
blocks = new Blocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to the runtime.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
/**
|
||||
* 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 = {};
|
||||
/**
|
||||
* Dictionary of custom state for this target.
|
||||
* This can be used to store target-specific custom state for blocks which need it.
|
||||
* TODO: do we want to persist this in SB3 files?
|
||||
* @type {Object.<string,*>}
|
||||
*/
|
||||
this._customState = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* A unique ID for this target.
|
||||
* @type {string}
|
||||
* Called when the project receives a "green flag."
|
||||
* @abstract
|
||||
*/
|
||||
this.id = uid();
|
||||
onGreenFlag () {}
|
||||
|
||||
/**
|
||||
* Blocks run as code for this target.
|
||||
* @type {!Blocks}
|
||||
* Return a human-readable name for this target.
|
||||
* Target implementations should override this.
|
||||
* @abstract
|
||||
* @returns {string} Human-readable name for the target.
|
||||
*/
|
||||
this.blocks = blocks;
|
||||
getName () {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictionary of variables and their values for this target.
|
||||
* Key is the variable name.
|
||||
* @type {Object.<string,*>}
|
||||
* Look up a variable object, and create it if one doesn't exist.
|
||||
* @param {string} id Id of the variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @return {!Variable} Variable object.
|
||||
*/
|
||||
this.variables = {};
|
||||
lookupOrCreateVariable (id, name) {
|
||||
const variable = this.lookupVariableById(id);
|
||||
if (variable) return variable;
|
||||
// No variable with this name exists - create it locally.
|
||||
const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false);
|
||||
this.variables[id] = newVariable;
|
||||
return newVariable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictionary of lists and their contents for this target.
|
||||
* Key is the list name.
|
||||
* @type {Object.<string,*>}
|
||||
* Look up a broadcast message object with the given id and return it
|
||||
* if it exists.
|
||||
* @param {string} id Id of the variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @return {?Variable} Variable object.
|
||||
*/
|
||||
this.lists = {};
|
||||
lookupBroadcastMsg (id, name) {
|
||||
let broadcastMsg;
|
||||
if (id) {
|
||||
broadcastMsg = this.lookupVariableById(id);
|
||||
} else if (name) {
|
||||
broadcastMsg = this.lookupBroadcastByInputValue(name);
|
||||
} else {
|
||||
log.error('Cannot find broadcast message if neither id nor name are provided.');
|
||||
}
|
||||
if (broadcastMsg) {
|
||||
if (name && (broadcastMsg.name.toLowerCase() !== name.toLowerCase())) {
|
||||
log.error(`Found broadcast message with id: ${id}, but` +
|
||||
`its name, ${broadcastMsg.name} did not match expected name ${name}.`);
|
||||
}
|
||||
if (broadcastMsg.type !== Variable.BROADCAST_MESSAGE_TYPE) {
|
||||
log.error(`Found variable with id: ${id}, but its type ${broadcastMsg.type}` +
|
||||
`did not match expected type ${Variable.BROADCAST_MESSAGE_TYPE}`);
|
||||
}
|
||||
return broadcastMsg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a broadcast message with the given name and return the variable
|
||||
* if it exists. Does not create a new broadcast message variable if
|
||||
* it doesn't exist.
|
||||
* @param {string} name Name of the variable.
|
||||
* @return {?Variable} Variable object.
|
||||
*/
|
||||
lookupBroadcastByInputValue (name) {
|
||||
const vars = this.variables;
|
||||
for (const propName in vars) {
|
||||
if ((vars[propName].type === Variable.BROADCAST_MESSAGE_TYPE) &&
|
||||
(vars[propName].name.toLowerCase() === name.toLowerCase())) {
|
||||
return vars[propName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a variable object.
|
||||
* Search begins for local variables; then look for globals.
|
||||
* @param {string} id Id of the variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @return {!Variable} Variable object.
|
||||
*/
|
||||
lookupVariableById (id) {
|
||||
// If we have a local copy, return it.
|
||||
if (this.variables.hasOwnProperty(id)) {
|
||||
return this.variables[id];
|
||||
}
|
||||
// If the stage has a global copy, return it.
|
||||
if (this.runtime && !this.isStage) {
|
||||
const stage = this.runtime.getTargetForStage();
|
||||
if (stage.variables.hasOwnProperty(id)) {
|
||||
return stage.variables[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a list object for this target, and create it if one doesn't exist.
|
||||
* Search begins for local lists; then look for globals.
|
||||
* @param {!string} id Id of the list.
|
||||
* @param {!string} name Name of the list.
|
||||
* @return {!Varible} Variable object representing the found/created list.
|
||||
*/
|
||||
lookupOrCreateList (id, name) {
|
||||
const list = this.lookupVariableById(id);
|
||||
if (list) return list;
|
||||
// No variable with this name exists - create it locally.
|
||||
const newList = new Variable(id, name, Variable.LIST_TYPE, false);
|
||||
this.variables[id] = newList;
|
||||
return newList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a variable with the given id and name and adds it to the
|
||||
* dictionary of variables.
|
||||
* @param {string} id Id of variable
|
||||
* @param {string} name Name of variable.
|
||||
* @param {string} type Type of variable, '', 'broadcast_msg', or 'list'
|
||||
*/
|
||||
createVariable (id, name, type) {
|
||||
if (!this.variables.hasOwnProperty(id)) {
|
||||
const newVariable = new Variable(id, name, type, false);
|
||||
this.variables[id] = newVariable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the variable with the given id to newName.
|
||||
* @param {string} id Id of renamed variable.
|
||||
* @param {string} newName New name for the variable.
|
||||
*/
|
||||
renameVariable (id, newName) {
|
||||
if (this.variables.hasOwnProperty(id)) {
|
||||
const variable = this.variables[id];
|
||||
if (variable.id === id) {
|
||||
variable.name = newName;
|
||||
|
||||
if (this.runtime) {
|
||||
const blocks = this.runtime.monitorBlocks;
|
||||
blocks.changeBlock({
|
||||
id: id,
|
||||
element: 'field',
|
||||
name: 'VARIABLE',
|
||||
value: id
|
||||
}, this.runtime);
|
||||
this.runtime.requestUpdateMonitor(Map({
|
||||
id: id,
|
||||
params: blocks._getBlockParams(blocks.getBlock(variable.id))
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the variable with the given id from the dictionary of variables.
|
||||
* @param {string} id Id of renamed variable.
|
||||
*/
|
||||
deleteVariable (id) {
|
||||
if (this.variables.hasOwnProperty(id)) {
|
||||
delete this.variables[id];
|
||||
if (this.runtime) {
|
||||
this.runtime.monitorBlocks.deleteBlock(id);
|
||||
this.runtime.requestRemoveMonitor(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post/edit sprite info.
|
||||
* @param {object} data An object with sprite info data to set.
|
||||
* @abstract
|
||||
*/
|
||||
postSpriteInfo () {}
|
||||
|
||||
/**
|
||||
* Retrieve custom state associated with this target and the provided state ID.
|
||||
* @param {string} stateId - specify which piece of state to retrieve.
|
||||
* @returns {*} the associated state, if any was found.
|
||||
*/
|
||||
getCustomState (stateId) {
|
||||
return this._customState[stateId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Store custom state associated with this target and the provided state ID.
|
||||
* @param {string} stateId - specify which piece of state to store on this target.
|
||||
* @param {*} newValue - the state value to store.
|
||||
*/
|
||||
setCustomState (stateId, newValue) {
|
||||
this._customState[stateId] = newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to destroy a target.
|
||||
* @abstract
|
||||
*/
|
||||
dispose () {
|
||||
this._customState = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
+255
-208
@@ -3,235 +3,282 @@
|
||||
* @param {?string} firstBlock First block to execute in the thread.
|
||||
* @constructor
|
||||
*/
|
||||
function Thread (firstBlock) {
|
||||
/**
|
||||
* ID of top block of the thread
|
||||
* @type {!string}
|
||||
*/
|
||||
this.topBlock = firstBlock;
|
||||
class Thread {
|
||||
constructor (firstBlock) {
|
||||
/**
|
||||
* ID of top block of the thread
|
||||
* @type {!string}
|
||||
*/
|
||||
this.topBlock = firstBlock;
|
||||
|
||||
/**
|
||||
* Stack for the thread. When the sequencer enters a control structure,
|
||||
* the block is pushed onto the stack so we know where to exit.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
this.stack = [];
|
||||
|
||||
/**
|
||||
* Stack frames for the thread. Store metadata for the executing blocks.
|
||||
* @type {Array.<Object>}
|
||||
*/
|
||||
this.stackFrames = [];
|
||||
|
||||
/**
|
||||
* Status of the thread, one of three states (below)
|
||||
* @type {number}
|
||||
*/
|
||||
this.status = 0; /* Thread.STATUS_RUNNING */
|
||||
|
||||
/**
|
||||
* Whether the thread is killed in the middle of execution.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isKilled = false;
|
||||
|
||||
/**
|
||||
* Target of this thread.
|
||||
* @type {?Target}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack for the thread. When the sequencer enters a control structure,
|
||||
* the block is pushed onto the stack so we know where to exit.
|
||||
* @type {Array.<string>}
|
||||
* Thread status for initialized or running thread.
|
||||
* This is the default state for a thread - execution should run normally,
|
||||
* stepping from block to block.
|
||||
* @const
|
||||
*/
|
||||
this.stack = [];
|
||||
static get STATUS_RUNNING () {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack frames for the thread. Store metadata for the executing blocks.
|
||||
* @type {Array.<Object>}
|
||||
* Threads are in this state when a primitive is waiting on a promise;
|
||||
* execution is paused until the promise changes thread status.
|
||||
* @const
|
||||
*/
|
||||
this.stackFrames = [];
|
||||
static get STATUS_PROMISE_WAIT () {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of the thread, one of three states (below)
|
||||
* @type {number}
|
||||
* Thread status for yield.
|
||||
* @const
|
||||
*/
|
||||
this.status = 0; /* Thread.STATUS_RUNNING */
|
||||
static get STATUS_YIELD () {
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Target of this thread.
|
||||
* @type {?Target}
|
||||
* Thread status for a single-tick yield. This will be cleared when the
|
||||
* thread is resumed.
|
||||
* @const
|
||||
*/
|
||||
this.target = null;
|
||||
static get STATUS_YIELD_TICK () {
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the thread requests its script to glow during this frame.
|
||||
* @type {boolean}
|
||||
* Thread status for a finished/done thread.
|
||||
* Thread is in this state when there are no more blocks to execute.
|
||||
* @const
|
||||
*/
|
||||
this.requestScriptGlowInFrame = false;
|
||||
static get STATUS_DONE () {
|
||||
return 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which block ID should glow during this frame, if any.
|
||||
* @type {?string}
|
||||
* Push stack and update stack frames appropriately.
|
||||
* @param {string} blockId Block ID to push to stack.
|
||||
*/
|
||||
this.blockGlowInFrame = null;
|
||||
pushStack (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.
|
||||
let warpMode = false;
|
||||
if (this.stackFrames.length > 0 && 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.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A timer for when the thread enters warp mode.
|
||||
* Substitutes the sequencer's count toward WORK_TIME on a per-thread basis.
|
||||
* @type {?Timer}
|
||||
* Reset the stack frame for use by the next block.
|
||||
* (avoids popping and re-pushing a new stack frame - keeps the warpmode the same
|
||||
* @param {string} blockId Block ID to push to stack.
|
||||
*/
|
||||
this.warpTimer = null;
|
||||
reuseStackForNextBlock (blockId) {
|
||||
this.stack[this.stack.length - 1] = blockId;
|
||||
const frame = this.stackFrames[this.stackFrames.length - 1];
|
||||
frame.isLoop = false;
|
||||
// frame.warpMode = warpMode; // warp mode stays the same when reusing the stack frame.
|
||||
frame.reported = {};
|
||||
frame.waitingReporter = null;
|
||||
frame.params = {};
|
||||
frame.executionContext = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop last block on the stack and its stack frame.
|
||||
* @return {string} Block ID popped from the stack.
|
||||
*/
|
||||
popStack () {
|
||||
this.stackFrames.pop();
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop back down the stack frame until we hit a procedure call or the stack frame is emptied
|
||||
*/
|
||||
stopThisScript () {
|
||||
let blockID = this.peekStack();
|
||||
while (blockID !== null) {
|
||||
const block = this.target.blocks.getBlock(blockID);
|
||||
if (typeof block !== 'undefined' && block.opcode === 'procedures_call') {
|
||||
break;
|
||||
}
|
||||
this.popStack();
|
||||
blockID = this.peekStack();
|
||||
}
|
||||
|
||||
if (this.stack.length === 0) {
|
||||
// Clean up!
|
||||
this.requestScriptGlowInFrame = false;
|
||||
this.status = Thread.STATUS_DONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top stack item.
|
||||
* @return {?string} Block ID on top of stack.
|
||||
*/
|
||||
peekStack () {
|
||||
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get top stack frame.
|
||||
* @return {?object} Last stack frame stored on this thread.
|
||||
*/
|
||||
peekStackFrame () {
|
||||
return this.stackFrames.length > 0 ? this.stackFrames[this.stackFrames.length - 1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stack frame above the current top.
|
||||
* @return {?object} Second to last stack frame stored on this thread.
|
||||
*/
|
||||
peekParentStackFrame () {
|
||||
return this.stackFrames.length > 1 ? this.stackFrames[this.stackFrames.length - 2] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a reported value to the parent of the current stack frame.
|
||||
* @param {*} value Reported value to push.
|
||||
*/
|
||||
pushReportedValue (value) {
|
||||
const parentStackFrame = this.peekParentStackFrame();
|
||||
if (parentStackFrame) {
|
||||
const 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.
|
||||
*/
|
||||
pushParam (paramName, value) {
|
||||
const 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.
|
||||
*/
|
||||
getParam (paramName) {
|
||||
for (let i = this.stackFrames.length - 1; i >= 0; i--) {
|
||||
const 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.
|
||||
*/
|
||||
atStackTop () {
|
||||
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.
|
||||
*/
|
||||
goToNextBlock () {
|
||||
const nextBlockId = this.target.blocks.getNextBlock(this.peekStack());
|
||||
this.reuseStackForNextBlock(nextBlockId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
isRecursiveCall (procedureCode) {
|
||||
let callCount = 5; // Max number of enclosing procedure calls to examine.
|
||||
const sp = this.stack.length - 1;
|
||||
for (let i = sp - 1; i >= 0; i--) {
|
||||
const block = this.target.blocks.getBlock(this.stack[i]);
|
||||
if (block.opcode === 'procedures_call' &&
|
||||
block.mutation.proccode === procedureCode) {
|
||||
return true;
|
||||
}
|
||||
if (--callCount < 0) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread status for initialized or running thread.
|
||||
* This is the default state for a thread - execution should run normally,
|
||||
* stepping from block to block.
|
||||
* @const
|
||||
*/
|
||||
Thread.STATUS_RUNNING = 0;
|
||||
|
||||
/**
|
||||
* Threads are in this state when a primitive is waiting on a promise;
|
||||
* execution is paused until the promise changes thread status.
|
||||
* @const
|
||||
*/
|
||||
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 is in this state when there are no more blocks to execute.
|
||||
* @const
|
||||
*/
|
||||
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;
|
||||
+59
-10
@@ -3,16 +3,65 @@
|
||||
* 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;
|
||||
const uid = require('../util/uid');
|
||||
|
||||
class Variable {
|
||||
/**
|
||||
* @param {string} id Id of the variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @param {string} type Type of the variable, one of '' or 'list'
|
||||
* @param {boolean} isCloud Whether the variable is stored in the cloud.
|
||||
* @constructor
|
||||
*/
|
||||
constructor (id, name, type, isCloud) {
|
||||
this.id = id || uid();
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.isCloud = isCloud;
|
||||
switch (this.type) {
|
||||
case Variable.SCALAR_TYPE:
|
||||
this.value = 0;
|
||||
break;
|
||||
case Variable.LIST_TYPE:
|
||||
this.value = [];
|
||||
break;
|
||||
case Variable.BROADCAST_MESSAGE_TYPE:
|
||||
this.value = this.name;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid variable type: ${this.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
toXML () {
|
||||
return `<variable type="${this.type}" id="${this.id}">${this.name}</variable>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representation for scalar variables.
|
||||
* This is currently represented as ''
|
||||
* for compatibility with blockly.
|
||||
* @const {string}
|
||||
*/
|
||||
static get SCALAR_TYPE () {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representation for list variables.
|
||||
* @const {string}
|
||||
*/
|
||||
static get LIST_TYPE () {
|
||||
return 'list';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representation for list variables.
|
||||
* @const {string}
|
||||
*/
|
||||
static get BROADCAST_MESSAGE_TYPE () {
|
||||
return 'broadcast_msg';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Variable;
|
||||
@@ -0,0 +1,9 @@
|
||||
const ArgumentType = {
|
||||
ANGLE: 'angle',
|
||||
BOOLEAN: 'Boolean',
|
||||
COLOR: 'color',
|
||||
NUMBER: 'number',
|
||||
STRING: 'string'
|
||||
};
|
||||
|
||||
module.exports = ArgumentType;
|
||||
@@ -0,0 +1,9 @@
|
||||
const BlockType = {
|
||||
BOOLEAN: 'Boolean',
|
||||
COMMAND: 'command',
|
||||
CONDITIONAL: 'conditional',
|
||||
HAT: 'hat',
|
||||
REPORTER: 'reporter'
|
||||
};
|
||||
|
||||
module.exports = BlockType;
|
||||
@@ -0,0 +1,280 @@
|
||||
const dispatch = require('../dispatch/central-dispatch');
|
||||
const log = require('../util/log');
|
||||
|
||||
const BlockType = require('./block-type');
|
||||
|
||||
// These extensions are currently built into the VM repository but should not be loaded at startup.
|
||||
// TODO: move these out into a separate repository?
|
||||
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods
|
||||
const Scratch3PenBlocks = require('../extensions/scratch3_pen');
|
||||
const Scratch3WeDo2Blocks = require('../extensions/scratch3_wedo2');
|
||||
const Scratch3MusicBlocks = require('../extensions/scratch3_music');
|
||||
const builtinExtensions = {
|
||||
pen: Scratch3PenBlocks,
|
||||
wedo2: Scratch3WeDo2Blocks,
|
||||
music: Scratch3MusicBlocks
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} ArgumentInfo - Information about an extension block argument
|
||||
* @property {ArgumentType} type - the type of value this argument can take
|
||||
* @property {*|undefined} default - the default value of this argument (default: blank)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} BlockInfo - Information about an extension block
|
||||
* @property {string} opcode - the block opcode
|
||||
* @property {string|object} text - the human-readable text on this block
|
||||
* @property {BlockType|undefined} blockType - the type of block (default: BlockType.COMMAND)
|
||||
* @property {int|undefined} branchCount - the number of branches this block controls, if conditional (default: 0)
|
||||
* @property {Boolean|undefined} isTerminal - true if this block ends a stack (default: false)
|
||||
* @property {Boolean|undefined} blockAllThreads - true if all threads must wait for this block to run (default: false)
|
||||
* @property {object.<string,ArgumentInfo>|undefined} arguments - information about this block's arguments, if any
|
||||
* @property {string|Function|undefined} func - the method for this block on the extension service (default: opcode)
|
||||
* @property {Array.<string>|undefined} filter - the list of targets for which this block should appear (default: all)
|
||||
* @property {Boolean|undefined} hideFromPalette - true if should not be appear in the palette. (default false)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} CategoryInfo - Information about a block category
|
||||
* @property {string} id - the unique ID of this category
|
||||
* @property {string} color1 - the primary color for this category, in '#rrggbb' format
|
||||
* @property {string} color2 - the secondary color for this category, in '#rrggbb' format
|
||||
* @property {string} color3 - the tertiary color for this category, in '#rrggbb' format
|
||||
* @property {Array.<BlockInfo>} block - the blocks in this category
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} PendingExtensionWorker - Information about an extension worker still initializing
|
||||
* @property {string} extensionURL - the URL of the extension to be loaded by this worker
|
||||
* @property {Function} resolve - function to call on successful worker startup
|
||||
* @property {Function} reject - function to call on failed worker startup
|
||||
*/
|
||||
|
||||
class ExtensionManager {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The ID number to provide to the next extension worker.
|
||||
* @type {int}
|
||||
*/
|
||||
this.nextExtensionWorker = 0;
|
||||
|
||||
/**
|
||||
* FIFO queue of extensions which have been requested but not yet loaded in a worker,
|
||||
* along with promise resolution functions to call once the worker is ready or failed.
|
||||
*
|
||||
* @type {Array.<PendingExtensionWorker>}
|
||||
*/
|
||||
this.pendingExtensions = [];
|
||||
|
||||
/**
|
||||
* Map of worker ID to workers which have been allocated but have not yet finished initialization.
|
||||
* @type {Array.<PendingExtensionWorker>}
|
||||
*/
|
||||
this.pendingWorkers = [];
|
||||
|
||||
/**
|
||||
* Set of loaded extension URLs/IDs (equivalent for built-in extensions).
|
||||
* @type {Set.<string>}
|
||||
* @private
|
||||
*/
|
||||
this._loadedExtensions = new Map();
|
||||
|
||||
/**
|
||||
* Keep a reference to the runtime so we can construct internal extension objects.
|
||||
* TODO: remove this in favor of extensions accessing the runtime as a service.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
dispatch.setService('extensions', this).catch(e => {
|
||||
log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an extension is registered or is in the process of loading. This is intended to control loading or
|
||||
* adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by
|
||||
* `loadExtensionURL` if you need to wait until the extension is truly ready.
|
||||
* @param {string} extensionID - the ID of the extension.
|
||||
* @returns {boolean} - true if loaded, false otherwise.
|
||||
*/
|
||||
isExtensionLoaded (extensionID) {
|
||||
return this._loadedExtensions.has(extensionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an extension by URL or internal extension ID
|
||||
* @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
|
||||
* @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
|
||||
*/
|
||||
loadExtensionURL (extensionURL) {
|
||||
if (builtinExtensions.hasOwnProperty(extensionURL)) {
|
||||
/** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
|
||||
if (this.isExtensionLoaded(extensionURL)) {
|
||||
const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
|
||||
log.warn(message);
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
|
||||
const extension = builtinExtensions[extensionURL];
|
||||
const extensionInstance = new extension(this.runtime);
|
||||
return this._registerInternalExtension(extensionInstance).then(serviceName => {
|
||||
this._loadedExtensions.set(extensionURL, serviceName);
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// If we `require` this at the global level it breaks non-webpack targets, including tests
|
||||
const ExtensionWorker = require('worker-loader?name=extension-worker.js!./extension-worker');
|
||||
|
||||
this.pendingExtensions.push({extensionURL, resolve, reject});
|
||||
dispatch.addWorker(new ExtensionWorker());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* regenerate blockinfo for any loaded extensions
|
||||
*/
|
||||
refreshBlocks () {
|
||||
this._loadedExtensions.forEach(serviceName => {
|
||||
dispatch.call(serviceName, 'getInfo')
|
||||
.then(info => {
|
||||
dispatch.call('runtime', '_refreshExtensionPrimitives', info);
|
||||
})
|
||||
.catch(e => {
|
||||
log.error(`Failed to refresh buildtin extension primitives: ${JSON.stringify(e)}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
allocateWorker () {
|
||||
const id = this.nextExtensionWorker++;
|
||||
const workerInfo = this.pendingExtensions.shift();
|
||||
this.pendingWorkers[id] = workerInfo;
|
||||
return [id, workerInfo.extensionURL];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect extension metadata from the specified service and begin the extension registration process.
|
||||
* @param {string} serviceName - the name of the service hosting the extension.
|
||||
*/
|
||||
registerExtensionService (serviceName) {
|
||||
dispatch.call(serviceName, 'getInfo').then(info => {
|
||||
this._registerExtensionInfo(serviceName, info);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by an extension worker to indicate that the worker has finished initialization.
|
||||
* @param {int} id - the worker ID.
|
||||
* @param {*?} e - the error encountered during initialization, if any.
|
||||
*/
|
||||
onWorkerInit (id, e) {
|
||||
const workerInfo = this.pendingWorkers[id];
|
||||
delete this.pendingWorkers[id];
|
||||
if (e) {
|
||||
workerInfo.reject(e);
|
||||
} else {
|
||||
workerInfo.resolve(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an internal (non-Worker) extension object
|
||||
* @param {object} extensionObject - the extension object to register
|
||||
* @returns {Promise} resolved once the extension is fully registered or rejected on failure
|
||||
*/
|
||||
_registerInternalExtension (extensionObject) {
|
||||
const extensionInfo = extensionObject.getInfo();
|
||||
const fakeWorkerId = this.nextExtensionWorker++;
|
||||
const serviceName = `extension.${fakeWorkerId}.${extensionInfo.id}`;
|
||||
return dispatch.setService(serviceName, extensionObject)
|
||||
.then(() => {
|
||||
dispatch.call('extensions', 'registerExtensionService', serviceName);
|
||||
return serviceName;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize extension info then register its primitives with the VM.
|
||||
* @param {string} serviceName - the name of the service hosting the extension
|
||||
* @param {ExtensionInfo} extensionInfo - the extension's metadata
|
||||
* @private
|
||||
*/
|
||||
_registerExtensionInfo (serviceName, extensionInfo) {
|
||||
extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo);
|
||||
dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => {
|
||||
log.error(`Failed to register primitives for extension on service ${serviceName}: ${JSON.stringify(e)}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML.
|
||||
* @param {string} text - the text to be sanitized
|
||||
* @returns {string} - the sanitized text
|
||||
* @private
|
||||
*/
|
||||
_sanitizeID (text) {
|
||||
return text.toString().replace(/[<"&]/, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply minor cleanup and defaults for optional extension fields.
|
||||
* TODO: make the ID unique in cases where two copies of the same extension are loaded.
|
||||
* @param {string} serviceName - the name of the service hosting this extension block
|
||||
* @param {ExtensionInfo} extensionInfo - the extension info to be sanitized
|
||||
* @returns {ExtensionInfo} - a new extension info object with cleaned-up values
|
||||
* @private
|
||||
*/
|
||||
_prepareExtensionInfo (serviceName, extensionInfo) {
|
||||
extensionInfo = Object.assign({}, extensionInfo);
|
||||
extensionInfo.id = this._sanitizeID(extensionInfo.id);
|
||||
extensionInfo.name = extensionInfo.name || extensionInfo.id;
|
||||
extensionInfo.blocks = extensionInfo.blocks || [];
|
||||
extensionInfo.targetTypes = extensionInfo.targetTypes || [];
|
||||
extensionInfo.blocks = extensionInfo.blocks.reduce((result, blockInfo) => {
|
||||
try {
|
||||
result.push(this._prepareBlockInfo(serviceName, blockInfo));
|
||||
} catch (e) {
|
||||
// TODO: more meaningful error reporting
|
||||
log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
return extensionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply defaults for optional block fields.
|
||||
* @param {string} serviceName - the name of the service hosting this extension block
|
||||
* @param {BlockInfo} blockInfo - the block info from the extension
|
||||
* @returns {BlockInfo} - a new block info object which has values for all relevant optional fields.
|
||||
* @private
|
||||
*/
|
||||
_prepareBlockInfo (serviceName, blockInfo) {
|
||||
blockInfo = Object.assign({}, {
|
||||
blockType: BlockType.COMMAND,
|
||||
terminal: false,
|
||||
blockAllThreads: false,
|
||||
arguments: {}
|
||||
}, blockInfo);
|
||||
blockInfo.opcode = this._sanitizeID(blockInfo.opcode);
|
||||
blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
|
||||
blockInfo.text = blockInfo.text || blockInfo.opcode;
|
||||
|
||||
/**
|
||||
* This is only here because the VM performs poorly when blocks return promises.
|
||||
* @TODO make it possible for the VM to resolve a promise and continue during the same frame.
|
||||
*/
|
||||
if (dispatch._isRemoteService(serviceName)) {
|
||||
blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
|
||||
} else {
|
||||
const serviceObject = dispatch.services[serviceName];
|
||||
blockInfo.func = serviceObject[blockInfo.func].bind(serviceObject);
|
||||
}
|
||||
return blockInfo;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExtensionManager;
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-env worker */
|
||||
|
||||
const ArgumentType = require('../extension-support/argument-type');
|
||||
const BlockType = require('../extension-support/block-type');
|
||||
const dispatch = require('../dispatch/worker-dispatch');
|
||||
|
||||
class ExtensionWorker {
|
||||
constructor () {
|
||||
this.nextExtensionId = 0;
|
||||
|
||||
this.initialRegistrations = [];
|
||||
|
||||
dispatch.waitForConnection.then(() => {
|
||||
dispatch.call('extensions', 'allocateWorker').then(x => {
|
||||
const [id, extension] = x;
|
||||
this.workerId = id;
|
||||
|
||||
try {
|
||||
importScripts(extension);
|
||||
|
||||
const initialRegistrations = this.initialRegistrations;
|
||||
this.initialRegistrations = null;
|
||||
|
||||
Promise.all(initialRegistrations).then(() => dispatch.call('extensions', 'onWorkerInit', id));
|
||||
} catch (e) {
|
||||
dispatch.call('extensions', 'onWorkerInit', id, e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.extensions = [];
|
||||
}
|
||||
|
||||
register (extensionObject) {
|
||||
const extensionId = this.nextExtensionId++;
|
||||
this.extensions.push(extensionObject);
|
||||
const serviceName = `extension.${this.workerId}.${extensionId}`;
|
||||
const promise = dispatch.setService(serviceName, extensionObject)
|
||||
.then(() => dispatch.call('extensions', 'registerExtensionService', serviceName));
|
||||
if (this.initialRegistrations) {
|
||||
this.initialRegistrations.push(promise);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
global.Scratch = global.Scratch || {};
|
||||
global.Scratch.ArgumentType = ArgumentType;
|
||||
global.Scratch.BlockType = BlockType;
|
||||
|
||||
/**
|
||||
* Expose only specific parts of the worker to extensions.
|
||||
*/
|
||||
const extensionWorker = new ExtensionWorker();
|
||||
global.Scratch.extensions = {
|
||||
register: extensionWorker.register.bind(extensionWorker)
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user