diff --git a/.gitattributes b/.gitattributes index f8c53a2d8a609e3f217f62e4cbfedd654d083c00..43a8476c895e425a3455b2271a2e23935f7e9b7a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -148,3 +148,12 @@ static/images/favicon[[:space:]](2).png filter=lfs diff=lfs merge=lfs -text static/images/512.png filter=lfs diff=lfs merge=lfs -text favicon[[:space:]](1).ico filter=lfs diff=lfs merge=lfs -text static/favicon.ico filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/src/extensions/jg_3dVr/headset.pdn filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/test/fixtures/complex.sb2 filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/test/fixtures/event.sb2 filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/test/fixtures/execute/sprite-number-name.sb2 filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-dolphin-3d.sb2 filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-dolphin-3d.sb3 filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/test/fixtures/looks.sb2 filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/test/fixtures/ordering.sb2 filter=lfs diff=lfs merge=lfs -text +local-scratch-vm/test/fixtures/pen.sb2 filter=lfs diff=lfs merge=lfs -text diff --git a/local-scratch-vm/.browserslistrc b/local-scratch-vm/.browserslistrc new file mode 100644 index 0000000000000000000000000000000000000000..c9d45f3f598a90ad59c75f869cdecf3a37a871a6 --- /dev/null +++ b/local-scratch-vm/.browserslistrc @@ -0,0 +1,6 @@ +chrome >= 70 +chromeandroid >= 70 +ios >= 12 +safari >= 12 +edge >= 17 +firefox >= 68 \ No newline at end of file diff --git a/local-scratch-vm/.editorconfig b/local-scratch-vm/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..e84613dd6c76b1f75668c71b96f56ed141f05b2d --- /dev/null +++ b/local-scratch-vm/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 4 +trim_trailing_whitespace = true + +[*.{js,html}] +indent_style = space diff --git a/local-scratch-vm/.eslintignore b/local-scratch-vm/.eslintignore new file mode 100644 index 0000000000000000000000000000000000000000..f571fb2a36b8a90efa2f4ed153617290552ac289 --- /dev/null +++ b/local-scratch-vm/.eslintignore @@ -0,0 +1,5 @@ +coverage/* +dist/* +node_modules/* +playground/* +benchmark/* diff --git a/local-scratch-vm/.eslintrc.js b/local-scratch-vm/.eslintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..3010f60d3f158c2528b5fa1e329c9efa7a2c3376 --- /dev/null +++ b/local-scratch-vm/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['scratch', 'scratch/node', 'scratch/es6'] +}; diff --git a/local-scratch-vm/.gitattributes b/local-scratch-vm/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..71521c9c5b5f7c7bcf95a8715b7bbf750aed57fb --- /dev/null +++ b/local-scratch-vm/.gitattributes @@ -0,0 +1,38 @@ +# 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 +*.json5 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 diff --git a/local-scratch-vm/.github/ISSUE_TEMPLATE.md b/local-scratch-vm/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000000000000000000000000000000000..38756937c53bdddf1540acf4b81e6faf090a2240 --- /dev/null +++ b/local-scratch-vm/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ +### Expected Behavior + +_Please describe what should happen_ + +### Actual Behavior + +_Describe what actually happens_ + +### Steps to Reproduce + +_Explain what someone needs to do in order to see what's described in *Actual behavior* above_ + +### Operating System and Browser + +_e.g. Mac OS 10.11.6 Safari 10.0_ diff --git a/local-scratch-vm/.github/PULL_REQUEST_TEMPLATE.md b/local-scratch-vm/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000000000000000000000000000000..33ff6dfdeed7d056d44937f3dacc680facd663c2 --- /dev/null +++ b/local-scratch-vm/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +### Resolves + +_What Github issue does this resolve (please include link)?_ + +### Proposed Changes + +_Describe what this Pull Request does_ + +### Reason for Changes + +_Explain why these changes should be made_ + +### Test Coverage + +_Please show how you have added tests to cover your changes_ diff --git a/local-scratch-vm/.github/workflows/nodejs.yml b/local-scratch-vm/.github/workflows/nodejs.yml new file mode 100644 index 0000000000000000000000000000000000000000..89ef78e1a92d5077ef700723b5fe720723d26a9d --- /dev/null +++ b/local-scratch-vm/.github/workflows/nodejs.yml @@ -0,0 +1,20 @@ +name: Remote Dispatch Action Initiator +#test +on: [push] + +jobs: + ping-pong: + runs-on: ubuntu-latest + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v2.0.1 + with: + token: ${{ secrets.t }} + event-type: update + repository: PenguinMod/penguinmod.github.io + - name: Repository Dispatch2 + uses: peter-evans/repository-dispatch@v2.0.1 + with: + token: ${{ secrets.t }} + event-type: update + repository: PenguinMod/PenguinMod-Packager diff --git a/local-scratch-vm/.gitignore b/local-scratch-vm/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..36d1d4093b38321911cea00d910e61b2f3e31d63 --- /dev/null +++ b/local-scratch-vm/.gitignore @@ -0,0 +1,22 @@ +# Mac OS +.DS_Store + +# NPM +/node_modules +npm-* + +# Testing +/.nyc_output +/coverage + +# Editor +/.idea +/.vscode + +# Build +/dist +/playground +/benchmark + +# Localization +/translations diff --git a/local-scratch-vm/.gitpod.yml b/local-scratch-vm/.gitpod.yml new file mode 100644 index 0000000000000000000000000000000000000000..e43643f73a07b8c4822ac5387b6ac698aa9ac3b7 --- /dev/null +++ b/local-scratch-vm/.gitpod.yml @@ -0,0 +1,11 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) +# and commit this file to your remote git repository to share the goodness with others. + +# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart + +tasks: + - init: npm install && npm run build + command: npm run start + + diff --git a/local-scratch-vm/.idx/dev.nix b/local-scratch-vm/.idx/dev.nix new file mode 100644 index 0000000000000000000000000000000000000000..b19353f6d6532600d9bdf3646926441006e4f69c --- /dev/null +++ b/local-scratch-vm/.idx/dev.nix @@ -0,0 +1,50 @@ +# To learn more about how to use Nix to configure your environment +# see: https://developers.google.com/idx/guides/customize-idx-env +{ pkgs, ... }: { + # Which nixpkgs channel to use. + channel = "stable-24.05"; # or "unstable" + + # Use https://search.nixos.org/packages to find packages + packages = [ + pkgs.nodejs_20 + ]; + + # Sets environment variables in the workspace + env = {}; + idx = { + # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" + extensions = [ + # "vscodevim.vim" + ]; + + # Enable previews + previews = { + enable = true; + previews = { + # web = { + # # Example: run "npm run dev" with PORT set to IDX's defined port for previews, + # # and show it in IDX's web preview panel + # command = ["npm" "run" "dev"]; + # manager = "web"; + # env = { + # # Environment variables to set for your server + # PORT = "$PORT"; + # }; + # }; + }; + }; + + # Workspace lifecycle hooks + workspace = { + # Runs when a workspace is first created + onCreate = { + npm-install = "npm install"; + }; + # Runs when the workspace is (re)started + onStart = { + # Example: start a background task to watch and re-build backend code + # watch-backend = "npm run watch-backend"; + }; + }; + }; +} diff --git a/local-scratch-vm/.jsdoc.json b/local-scratch-vm/.jsdoc.json new file mode 100644 index 0000000000000000000000000000000000000000..f4d8b9fcbaad79a6f9a6571d3fb736332e68c89f --- /dev/null +++ b/local-scratch-vm/.jsdoc.json @@ -0,0 +1,20 @@ +{ + "plugins": ["plugins/markdown"], + "templates": { + "default": { + "includeDate": false, + "outputSourceFiles": false + } + }, + "source": { + "include": ["src"] + }, + "opts": { + "destination": "playground/docs", + "pedantic": true, + "private": true, + "readme": "README.md", + "recurse": true, + "template": "node_modules/docdash" + } +} diff --git a/local-scratch-vm/.npmignore b/local-scratch-vm/.npmignore new file mode 100644 index 0000000000000000000000000000000000000000..7bb4d9895331d7677d8506b75000a024a6ea2218 --- /dev/null +++ b/local-scratch-vm/.npmignore @@ -0,0 +1,19 @@ +# Development files +.eslintrc.js +/.editorconfig +/.eslintignore +/.gitattributes +/.github +/.travis.yml +/.tx +/test + +# Build created files +/playground + +# Coverage created files +/.nyc_output +/coverage + +# Exclude already built packages from testing with npm pack +/scratch-vm-*.{tar,tgz} diff --git a/local-scratch-vm/.travis.yml b/local-scratch-vm/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..658ac864571b952a04876322b3b445d22f4dfbc5 --- /dev/null +++ b/local-scratch-vm/.travis.yml @@ -0,0 +1,74 @@ +language: node_js +node_js: +- lts/* +env: + global: + - NODE_ENV=production + - NPM_TAG=latest + - RELEASE_TIMESTAMP="$(date +'%Y%m%d%H%M%S')" + matrix: + - NPM_SCRIPT="tap:unit -- --jobs=4" + - NPM_SCRIPT="tap:integration -- --jobs=4" +cache: + directories: + - "$HOME/.npm" +install: +- npm ci --production=false +script: npm run $NPM_SCRIPT +jobs: + include: + - env: NPM_SCRIPT=lint + - env: NPM_SCRIPT=build + if: not (type != pull_request AND (branch =~ /^(develop|master|hotfix\/)/)) + - stage: release + env: NPM_SCRIPT=build + before_deploy: + - > + if [ -z "$BEFORE_DEPLOY_RAN" ]; then + VPKG=$($(npm bin)/json -f package.json version) + export RELEASE_VERSION=${VPKG}-prerelease.${RELEASE_TIMESTAMP} + npm --no-git-tag-version version $RELEASE_VERSION + if [[ "$TRAVIS_BRANCH" == hotfix/* ]]; then # double brackets are important for matching the wildcard + export NPM_TAG=hotfix + fi + git config --global user.email "$(git log --pretty=format:"%ae" -n1)" + git config --global user.name "$(git log --pretty=format:"%an" -n1)" + export BEFORE_DEPLOY_RAN=true + fi + deploy: + - provider: npm + on: + branch: + - master + - develop + - hotfix/* + condition: $TRAVIS_EVENT_TYPE != cron + skip_cleanup: true + email: $NPM_EMAIL + api_key: $NPM_TOKEN + tag: $NPM_TAG + - provider: script + on: + branch: + - master + - develop + - hotfix/* + condition: $TRAVIS_EVENT_TYPE != cron + skip_cleanup: true + script: if npm info | grep -q $RELEASE_VERSION; then git tag $RELEASE_VERSION && git push https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git $RELEASE_VERSION; fi + - provider: script + on: + all_branches: true + condition: $TRAVIS_EVENT_TYPE != cron + skip_cleanup: true + script: npm run --silent deploy -- -x -r $GH_PAGES_REPO + - provider: script + on: + branch: develop + condition: $TRAVIS_EVENT_TYPE == cron + skip_cleanup: true + script: npm run i18n:src && npm run i18n:push +stages: +- test +- name: release + if: type != pull_request AND (branch =~ /^(develop|master|hotfix\/)/) diff --git a/local-scratch-vm/.tx/config b/local-scratch-vm/.tx/config new file mode 100644 index 0000000000000000000000000000000000000000..eaa58921f56c32e16eefb236ca940a69454564ee --- /dev/null +++ b/local-scratch-vm/.tx/config @@ -0,0 +1,8 @@ +[main] +host = https://www.transifex.com + +[scratch-editor.extensions] +file_filter = translations/core/.json +source_file = translations/core/en.json +source_lang = en +type = CHROME diff --git a/local-scratch-vm/LICENSE b/local-scratch-vm/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..c03278a9b738319d95a6927dec3c88b71d1bc1ce --- /dev/null +++ b/local-scratch-vm/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2016, Massachusetts Institute of Technology +Copyright (c) 2020-2022, Thomas Weber +Copyright (c) 2023-2024 Penguinmod +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/local-scratch-vm/README.md b/local-scratch-vm/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8073d183a305e7d038dfd622c00dbb15180f24e0 --- /dev/null +++ b/local-scratch-vm/README.md @@ -0,0 +1,152 @@ +## PenguinMod/PenguinMod-Vm + +Modified Scratch VM with a JIT compiler and more features. + +This is a drop-in replacement for LLK/scratch-vm. + +## Setup + +See https://github.com/TurboWarp/scratch-gui/wiki/Getting-Started to setup the complete TurboWarp environment. + +If you just want to play with the VM then it's the same process as upstream scratch-vm. + +## Extension authors + +If you only use the standard reporter, boolean, and command block types, everything should just work without any changes. + +## Compiler Overview + +For a high-level overview of how the compiler works, see https://docs.turbowarp.org/how + +For more technical information, read the code in src/compiler. + +## Public API + +This section was too out of date to be useful. We hope to re-add it as some point. + + +e diff --git a/local-scratch-vm/TRADEMARK b/local-scratch-vm/TRADEMARK new file mode 100644 index 0000000000000000000000000000000000000000..17b5d4c919c9336be6f546ff5d958e38685380af --- /dev/null +++ b/local-scratch-vm/TRADEMARK @@ -0,0 +1 @@ +The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT). Marks may not be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/local-scratch-vm/docs/extensions.md b/local-scratch-vm/docs/extensions.md new file mode 100644 index 0000000000000000000000000000000000000000..602398c0d80380e3a43e345d5c049842c00f98ad --- /dev/null +++ b/local-scratch-vm/docs/extensions.md @@ -0,0 +1,527 @@ +# Scratch 3.0 Extensions + +This document describes technical topics related to Scratch 3.0 extension development, including the Scratch 3.0 +extension specification. + +## Types of Extensions + +There are four types of extensions that can define everything from the Scratch's core library (such as the "Looks" and +"Operators" categories) to unofficial extensions that can be loaded from a remote URL. + +**Scratch 3.0 does not yet support unofficial extensions.** + +| | Core | Team | Official | Unofficial | +| ------------------------------ | ---- | ---- | -------- | ---------- | +| Developed by Scratch Team | √ | √ | O | X | +| Maintained by Scratch Team | √ | √ | O | X | +| Shown in Library | X | √ | √ | X | +| Sandboxed | X | X | √ | √ | +| Can save projects to community | √ | √ | √ | X | + +## JavaScript Environment + +Most Scratch 3.0 is written using JavaScript features not yet commonly supported by browsers. For compatibility we +transpile the code to ES5 before publishing or deploying. Any extension included in the `scratch-vm` repository may +use ES6+ features and may use `require` to reference other code within the `scratch-vm` repository. + +Unofficial extensions must be self-contained. Authors of unofficial extensions are responsible for ensuring browser +compatibility for those extensions, including transpiling if necessary. + +## Translation + +Scratch extensions use the [ICU message format](http://userguide.icu-project.org/formatparse/messages) to handle +translation across languages. For **core, team, and official** extensions, the function `formatMessage` is used to +wrap any ICU messages that need to be exported to the [Scratch Transifex group](https://www.transifex.com/llk/public/) +for translation. + +**All extensions** may additionally define a `translation_map` object within the `getInfo` function which can provide +translations within an extension itself. The "Annotated Example" below provides a more complete illustration of how +translation within an extension can be managed. **WARNING:** the `translation_map` feature is currently in the +proposal phase and may change before implementation. + +## Backwards Compatibility + +Scratch is designed to be fully backwards compatible. Because of this, block definitions and opcodes should *never* +change in a way that could cause previously saved projects to fail to load or to act in unexpected / inconsistent +ways. + +## Defining an Extension + +Scratch extensions are defined as a single Javascript class which accepts either a reference to the Scratch +[VM](https://github.com/llk/scratch-vm) runtime or a "runtime proxy" which handles communication with the Scratch VM +across a well defined worker boundary (i.e. the sandbox). + +```js +class SomeBlocks { + constructor (runtime) { + /** + * Store this for later communication with the Scratch VM runtime. + * If this extension is running in a sandbox then `runtime` is an async proxy object. + * @type {Runtime} + */ + this.runtime = runtime; + } + + // ... +} +``` + +All extensions must define a function called `getInfo` which returns an object that contains the information needed to +render both the blocks and the extension itself. + +```js +// Core, Team, and Official extensions can `require` VM code: +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); + +class SomeBlocks { + // ... + getInfo () { + return { + id: 'someBlocks', + name: 'Some Blocks', + blocks: [ + { + opcode: 'myReporter', + blockType: BlockType.REPORTER, + text: 'letter [LETTER_NUM] of [TEXT]', + arguments: { + LETTER_NUM: { + type: ArgumentType.STRING, + defaultValue: '1' + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: 'text' + } + } + } + ] + }; + } + // ... +} +``` + +Finally the extension must define a function for any "opcode" defined in the blocks. For example: + +```js +class SomeBlocks { + // ... + myReporter (args) { + return args.TEXT.charAt(args.LETTER_NUM); + }; + // ... +} +``` +### Block Arguments +In addition to displaying text, blocks can have arguments in the form of slots to take other blocks getting plugged in, or dropdown menus to select an argument value from a list of possible values. + +The possible types of block arguments are as follows: + +- String - a string input, this is a type-able field which also accepts other reporter blocks to be plugged in +- Number - an input similar to the string input, but the type-able values are constrained to numbers. +- Angle - an input similar to the number input, but it has an additional UI to be able to pick an angle from a +circular dial +- Boolean - an input for a boolean (hexagonal shaped) reporter block. This field is not type-able. +- Color - an input which displays a color swatch. This field has additional UI to pick a color by choosing values for the color's hue, saturation and brightness. Optionally, the defaultValue for the color picker can also be chosen if the extension developer wishes to display the same color every time the extension is added. If the defaultValue is left out, the default behavior of picking a random color when the extension is loaded will be used. +- Matrix - an input which displays a 5 x 5 matrix of cells, where each cell can be filled in or clear. +- Note - a numeric input which can select a musical note. This field has additional UI to select a note from a +visual keyboard. +- Image - an inline image displayed on a block. This is a special argument type in that it does not represent a value and does not accept other blocks to be plugged-in in place of this block field. See the section below about "Adding an Inline Image". + +#### Adding an Inline Image +In addition to specifying block arguments (an example of string arguments shown in the code snippet above), +you can also specify an inline image for the block. You must include a dataURI for the image. If left unspecified, blank space will be allocated for the image and a warning will be logged in the console. +You can optionally also specify `flipRTL`, a property indicating whether the image should be flipped horizontally when the editor has a right to left language selected as its locale. By default, the image is not flipped. + +```js +return { + // ... + blocks: [ + { + //... + arguments: { + MY_IMAGE: { + type: ArgumentType.IMAGE, + dataURI: 'myImageData', + alt: 'This is an image', + flipRTL: true + } + } + } + ] +} +``` + + + + +#### Defining a Menu + +To display a drop-down menu for a block argument, specify the `menu` property of that argument and a matching item in +the `menus` section of your extension's definition: + +```js +return { + // ... + blocks: [ + { + // ... + arguments: { + FOO: { + type: ArgumentType.NUMBER, + menu: 'fooMenu' + } + } + } + ], + menus: { + fooMenu: { + items: ['a', 'b', 'c'] + } + } +} +``` + +The items in a menu may be specified with an array or with the name of a function which returns an array. The two +simplest forms for menu definitions are: + +```js +getInfo () { + return { + menus: { + staticMenu: ['static 1', 'static 2', 'static 3'], + dynamicMenu: 'getDynamicMenuItems' + } + }; +} +// this member function will be called each time the menu opens +getDynamicMenuItems () { + return ['dynamic 1', 'dynamic 2', 'dynamic 3']; +} +``` + +The examples above are shorthand for these equivalent definitions: + +```js +getInfo () { + return { + menus: { + staticMenu: { + items: ['static 1', 'static 2', 'static 3'] + }, + dynamicMenu: { + items: 'getDynamicMenuItems' + } + } + }; +} +// this member function will be called each time the menu opens +getDynamicMenuItems () { + return ['dynamic 1', 'dynamic 2', 'dynamic 3']; +} +``` + +If a menu item needs a label that doesn't match its value -- for example, if the label needs to be displayed in the +user's language but the value needs to stay constant -- the menu item may be an object instead of a string. This works +for both static and dynamic menu items: + +```js +menus: { + staticMenu: [ + { + text: formatMessage(/* ... */), + value: 42 + } + ] +} +``` + +##### Accepting reporters ("droppable" menus) + +By default it is not possible to specify the value of a dropdown menu by inserting a reporter block. While we +encourage extension authors to make their menus accept reporters when possible, doing so requires careful +consideration to avoid confusion and frustration on the part of those using the extension. + +A few of these considerations include: + +* The valid values for the menu should not change when the user changes the Scratch language setting. + * In particular, changing languages should never break a working project. +* The average Scratch user should be able to figure out the valid values for this input without referring to extension + documentation. + * One way to ensure this is to make an item's text match or include the item's value. For example, the official Music + extension contains menu items with names like "(1) Piano" with value 1, "(8) Cello" with value 8, and so on. +* The block should accept any value as input, even "invalid" values. + * Scratch has no concept of a runtime error! + * For a command block, sometimes the best option is to do nothing. + * For a reporter, returning zero or the empty string might make sense. +* The block should be forgiving in its interpretation of inputs. + * For example, if the block expects a string and receives a number it may make sense to interpret the number as a + string instead of treating it as invalid input. + +The `acceptReporters` flag indicates that the user can drop a reporter onto the menu input: + +```js +menus: { + staticMenu: { + acceptReporters: true, + items: [/*...*/] + }, + dynamicMenu: { + acceptReporters: true, + items: 'getDynamicMenuItems' + } +} +``` + +## Annotated Example + +```js +// Core, Team, and Official extensions can `require` VM code: +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const TargetType = require('../../extension-support/target-type'); + +// ...or VM dependencies: +const formatMessage = require('format-message'); + +// Core, Team, and Official extension classes should be registered statically with the Extension Manager. +// See: scratch-vm/src/extension-support/extension-manager.js +class SomeBlocks { + constructor (runtime) { + /** + * Store this for later communication with the Scratch VM runtime. + * If this extension is running in a sandbox then `runtime` is an async proxy object. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @return {object} This extension's metadata. + */ + getInfo () { + return { + // Required: the machine-readable name of this extension. + // Will be used as the extension's namespace. + // Allowed characters are those matching the regular expression [\w-]: A-Z, a-z, 0-9, and hyphen ("-"). + id: 'someBlocks', + + // Core extensions only: override the default extension block colors. + color1: '#FF8C1A', + color2: '#DB6E00', + + // Optional: the human-readable name of this extension as string. + // This and any other string to be displayed in the Scratch UI may either be + // a string or a call to `formatMessage`; a plain string will not be + // translated whereas a call to `formatMessage` will connect the string + // to the translation map (see below). The `formatMessage` call is + // similar to `formatMessage` from `react-intl` in form, but will actually + // call some extension support code to do its magic. For example, we will + // internally namespace the messages such that two extensions could have + // messages with the same ID without colliding. + // See also: https://github.com/yahoo/react-intl/wiki/API#formatmessage + name: formatMessage({ + id: 'extensionName', + defaultMessage: 'Some Blocks', + description: 'The name of the "Some Blocks" extension' + }), + + // Optional: URI for a block icon, to display at the edge of each block for this + // extension. Data URI OK. + // TODO: what file types are OK? All web images? Just PNG? + blockIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', + + // Optional: URI for an icon to be displayed in the blocks category menu. + // If not present, the menu will display the block icon, if one is present. + // Otherwise, the category menu shows its default filled circle. + // Data URI OK. + // TODO: what file types are OK? All web images? Just PNG? + menuIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', + + // Optional: Link to documentation content for this extension. + // If not present, offer no link. + docsURI: 'https://....', + + // Required: the list of blocks implemented by this extension, + // in the order intended for display. + blocks: [ + { + // Required: the machine-readable name of this operation. + // This will appear in project JSON. + opcode: 'myReporter', // becomes 'someBlocks.myReporter' + + // Required: the kind of block we're defining, from a predefined list. + // Fully supported block types: + // BlockType.BOOLEAN - same as REPORTER but returns a Boolean value + // BlockType.COMMAND - a normal command block, like "move {} steps" + // BlockType.HAT - starts a stack if its value changes from falsy to truthy ("edge triggered") + // BlockType.REPORTER - returns a value, like "direction" + // Block types in development or for internal use only: + // BlockType.BUTTON - place a button in the block palette + // BlockType.CONDITIONAL - control flow, like "if {}" or "if {} else {}" + // A CONDITIONAL block may return the one-based index of a branch to + // run, or it may return zero/falsy to run no branch. + // BlockType.EVENT - starts a stack in response to an event (full spec TBD) + // BlockType.LOOP - control flow, like "repeat {} {}" or "forever {}" + // A LOOP block is like a CONDITIONAL block with two differences: + // - the block is assumed to have exactly one child branch, and + // - each time a child branch finishes, the loop block is called again. + blockType: BlockType.REPORTER, + + // Required for CONDITIONAL blocks, ignored for others: the number of + // child branches this block controls. An "if" or "repeat" block would + // specify a branch count of 1; an "if-else" block would specify a + // branch count of 2. + // TODO: should we support dynamic branch count for "switch"-likes? + branchCount: 0, + + // Optional, default false: whether or not this block ends a stack. + // The "forever" and "stop all" blocks would specify true here. + terminal: true, + + // Optional, default false: whether or not to block all threads while + // this block is busy. This is for things like the "touching color" + // block in compatibility mode, and is only needed if the VM runs in a + // worker. We might even consider omitting it from extension docs... + blockAllThreads: false, + + // Required: the human-readable text on this block, including argument + // placeholders. Argument placeholders should be in [MACRO_CASE] and + // must be [ENCLOSED_WITHIN_SQUARE_BRACKETS]. + text: formatMessage({ + id: 'myReporter', + defaultMessage: 'letter [LETTER_NUM] of [TEXT]', + description: 'Label on the "myReporter" block' + }), + + // Required: describe each argument. + // Argument order may change during translation, so arguments are + // identified by their placeholder name. In those situations where + // arguments must be ordered or assigned an ordinal, such as interaction + // with Scratch Blocks, arguments are ordered as they are in the default + // translation (probably English). + arguments: { + // Required: the ID of the argument, which will be the name in the + // args object passed to the implementation function. + LETTER_NUM: { + // Required: type of the argument / shape of the block input + type: ArgumentType.NUMBER, + + // Optional: the default value of the argument + default: 1 + }, + + // Required: the ID of the argument, which will be the name in the + // args object passed to the implementation function. + TEXT: { + // Required: type of the argument / shape of the block input + type: ArgumentType.STRING, + + // Optional: the default value of the argument + default: formatMessage({ + id: 'myReporter.TEXT_default', + defaultMessage: 'text', + description: 'Default for "TEXT" argument of "someBlocks.myReporter"' + }) + } + }, + + // Optional: the function implementing this block. + // If absent, assume `func` is the same as `opcode`. + func: 'myReporter', + + // Optional: list of target types for which this block should appear. + // If absent, assume it applies to all builtin targets -- that is: + // [TargetType.SPRITE, TargetType.STAGE] + filter: [TargetType.SPRITE] + }, + { + // Another block... + } + ], + + // Optional: define extension-specific menus here. + menus: { + // Required: an identifier for this menu, unique within this extension. + menuA: [ + // Static menu: list items which should appear in the menu. + { + // Required: the value of the menu item when it is chosen. + value: 'itemId1', + + // Optional: the human-readable label for this item. + // Use `value` as the text if this is absent. + text: formatMessage({ + id: 'menuA_item1', + defaultMessage: 'Item One', + description: 'Label for item 1 of menu A in "Some Blocks" extension' + }) + }, + + // The simplest form of a list item is a string which will be used as + // both value and text. + 'itemId2' + ], + + // Dynamic menu: returns an array as above. + // Called each time the menu is opened. + menuB: 'getItemsForMenuB', + + // The examples above are shorthand for setting only the `items` property in this full form: + menuC: { + // This flag makes a "droppable" menu: the menu will allow dropping a reporter in for the input. + acceptReporters: true, + + // The `item` property may be an array or function name as in previous menu examples. + items: [/*...*/] || 'getItemsForMenuC' + } + }, + + // Optional: translations (UNSTABLE - NOT YET SUPPORTED) + translation_map: { + de: { + 'extensionName': 'Einige Blöcke', + 'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]', + 'myReporter.TEXT_default': 'Text', + 'menuA_item1': 'Artikel eins', + + // Dynamic menus can be translated too + 'menuB_example': 'Beispiel', + + // This message contains ICU placeholders (see `myReporter()` below) + 'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.' + }, + it: { + // ... + } + } + }; + }; + + /** + * Implement myReporter. + * @param {object} args - the block's arguments. + * @property {string} MY_ARG - the string value of the argument. + * @returns {string} a string which includes the block argument value. + */ + myReporter (args) { + // This message contains ICU placeholders, not Scratch placeholders + const message = formatMessage({ + id: 'myReporter.result', + defaultMessage: 'Letter {LETTER_NUM} of {TEXT} is {LETTER}.', + description: 'The text template for the "myReporter" block result' + }); + + // Note: this implementation is not Unicode-clean; it's just here as an example. + const result = args.TEXT.charAt(args.LETTER_NUM); + + return message.format({ + LETTER_NUM: args.LETTER_NUM, + TEXT: args.TEXT, + LETTER: result + }); + }; +} +``` diff --git a/local-scratch-vm/package-lock.json b/local-scratch-vm/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..21178b24e12cab9c98454ed50c63740b89d11674 --- /dev/null +++ b/local-scratch-vm/package-lock.json @@ -0,0 +1,30391 @@ +{ + "name": "scratch-vm", + "version": "0.2.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "scratch-vm", + "version": "0.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "@turbowarp/json": "^0.1.2", + "@vernier/godirect": "1.5.0", + "arraybuffer-loader": "^1.0.6", + "atob": "2.1.2", + "btoa": "1.2.1", + "cannon-es": "0.20.0", + "canvas-toBlob": "1.0.0", + "decode-html": "2.0.0", + "diff-match-patch": "1.0.4", + "dompurify": "^3.0.9", + "format-message": "6.2.1", + "htmlparser2": "^3.10.0", + "immutable": "3.8.2", + "jszip": "^3.1.5", + "lz-string": "^1.5.0", + "mathjs": "11.11.1", + "matter-js": "^0.20.0", + "mersenne-twister": "^1.1.0", + "minilog": "3.1.0", + "pathfinding": "^0.4.18", + "schema-utils": "^2.7.1", + "scratch-parser": "git+https://github.com/PenguinMod/PenguinMod-Parser.git#master", + "scratch-sb1-converter": "0.2.7", + "scratch-translate-extension-languages": "0.0.20191118205314", + "simplex-noise": "^4.0.1", + "text-encoding": "0.7.0", + "three": "0.153.0", + "three-mesh-bvh": "0.6.0", + "tone": "^14.7.77", + "worker-loader": "^1.1.1" + }, + "devDependencies": { + "@babel/core": "7.13.10", + "@babel/preset-env": "7.14.8", + "adm-zip": "0.4.11", + "babel-eslint": "10.1.0", + "babel-loader": "8.2.2", + "callsite": "1.0.0", + "copy-webpack-plugin": "4.5.4", + "docdash": "1.2.0", + "eslint": "5.3.0", + "eslint-config-scratch": "5.1.0", + "expose-loader": "0.7.5", + "file-loader": "2.0.0", + "format-message-cli": "6.2.0", + "gh-pages": "1.2.0", + "in-publish": "2.0.1", + "js-md5": "0.7.3", + "jsdoc": "3.6.6", + "json": "^9.0.4", + "lodash.defaultsdeep": "4.6.1", + "pngjs": "3.3.3", + "scratch-audio": "0.1.0-prerelease.20200528195344", + "scratch-blocks": "git+https://github.com/PenguinMod/PenguinMod-Blocks.git#develop-builds", + "scratch-l10n": "3.14.20220526031602", + "scratch-render": "0.1.0-prerelease.20211028200436", + "scratch-render-fonts": "github:PenguinMod/penguinmod-render-fonts#master", + "scratch-storage": "git+https://github.com/PenguinMod/PenguinMod-Storage.git#develop", + "scratch-svg-renderer": "0.2.0-prerelease.20210727023023", + "script-loader": "0.7.2", + "stats.js": "0.17.0", + "tap": "12.0.1", + "tiny-worker": "2.3.0", + "uglifyjs-webpack-plugin": "1.2.7", + "webpack": "4.46.0", + "webpack-cli": "3.1.0", + "webpack-dev-server": "3.11.2" + }, + "peerDependencies": { + "scratch-svg-renderer": "^0.2.0-prerelease" + } + }, + "node_modules/@babel/cli": { + "version": "7.23.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "commander": "^4.0.1", + "convert-source-map": "^2.0.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "bin": { + "babel": "bin/babel.js", + "babel-external-helpers": "bin/babel-external-helpers.js" + }, + "engines": { + "node": ">=6.9.0" + }, + "optionalDependencies": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/cli/node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@babel/cli/node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/cli/node_modules/make-dir": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/cli/node_modules/pify": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/cli/node_modules/semver": { + "version": "5.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@babel/cli/node_modules/slash": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.13.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-compilation-targets": "^7.13.10", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helpers": "^7.13.10", + "@babel/parser": "^7.13.10", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.23.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.6", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.23.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.23.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.23.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.23.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.14.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.14.7", + "@babel/helper-compilation-targets": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-async-generator-functions": "^7.14.7", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-proposal-class-static-block": "^7.14.5", + "@babel/plugin-proposal-dynamic-import": "^7.14.5", + "@babel/plugin-proposal-export-namespace-from": "^7.14.5", + "@babel/plugin-proposal-json-strings": "^7.14.5", + "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", + "@babel/plugin-proposal-numeric-separator": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.14.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.14.5", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-private-methods": "^7.14.5", + "@babel/plugin-proposal-private-property-in-object": "^7.14.5", + "@babel/plugin-proposal-unicode-property-regex": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.14.5", + "@babel/plugin-transform-async-to-generator": "^7.14.5", + "@babel/plugin-transform-block-scoped-functions": "^7.14.5", + "@babel/plugin-transform-block-scoping": "^7.14.5", + "@babel/plugin-transform-classes": "^7.14.5", + "@babel/plugin-transform-computed-properties": "^7.14.5", + "@babel/plugin-transform-destructuring": "^7.14.7", + "@babel/plugin-transform-dotall-regex": "^7.14.5", + "@babel/plugin-transform-duplicate-keys": "^7.14.5", + "@babel/plugin-transform-exponentiation-operator": "^7.14.5", + "@babel/plugin-transform-for-of": "^7.14.5", + "@babel/plugin-transform-function-name": "^7.14.5", + "@babel/plugin-transform-literals": "^7.14.5", + "@babel/plugin-transform-member-expression-literals": "^7.14.5", + "@babel/plugin-transform-modules-amd": "^7.14.5", + "@babel/plugin-transform-modules-commonjs": "^7.14.5", + "@babel/plugin-transform-modules-systemjs": "^7.14.5", + "@babel/plugin-transform-modules-umd": "^7.14.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.7", + "@babel/plugin-transform-new-target": "^7.14.5", + "@babel/plugin-transform-object-super": "^7.14.5", + "@babel/plugin-transform-parameters": "^7.14.5", + "@babel/plugin-transform-property-literals": "^7.14.5", + "@babel/plugin-transform-regenerator": "^7.14.5", + "@babel/plugin-transform-reserved-words": "^7.14.5", + "@babel/plugin-transform-shorthand-properties": "^7.14.5", + "@babel/plugin-transform-spread": "^7.14.6", + "@babel/plugin-transform-sticky-regex": "^7.14.5", + "@babel/plugin-transform-template-literals": "^7.14.5", + "@babel/plugin-transform-typeof-symbol": "^7.14.5", + "@babel/plugin-transform-unicode-escapes": "^7.14.5", + "@babel/plugin-transform-unicode-regex": "^7.14.5", + "@babel/preset-modules": "^0.1.4", + "@babel/types": "^7.14.8", + "babel-plugin-polyfill-corejs2": "^0.2.2", + "babel-plugin-polyfill-corejs3": "^0.2.2", + "babel-plugin-polyfill-regenerator": "^0.2.2", + "core-js-compat": "^3.15.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/runtime": { + "version": "7.23.6", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@turbowarp/json": { + "version": "0.1.2", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.10.5", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@vernier/godirect": { + "version": "1.5.0", + "license": "BSD-3-Clause" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "node_modules/@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "license": "ISC" + }, + "node_modules/@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.9.0", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wast-parser": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.9.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "6.4.2", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adm-zip": { + "version": "0.4.11", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "license": "MIT", + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-html": { + "version": "0.0.7", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "license": "ISC", + "optional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.7", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/arraybuffer-loader": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "loader-utils": "^1.1.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.0", + "license": "MIT" + }, + "node_modules/assert": { + "version": "1.5.1", + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.4", + "util": "^0.10.4" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.3", + "license": "ISC" + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.4", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "2.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/async-each": { + "version": "1.0.6", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/atob": { + "version": "2.1.2", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/audio-context": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "global": "^4.3.1" + } + }, + "node_modules/automation-events": { + "version": "6.0.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-styles": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/js-tokens": { + "version": "3.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-code-frame/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/supports-color": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-eslint": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "eslint": ">= 4.12.1" + } + }, + "node_modules/babel-loader": { + "version": "8.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-plugin-extract-format-message": { + "version": "6.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "format-message-estree-util": "^6.2.4", + "format-message-generate-id": "^6.2.4", + "format-message-parse": "^6.2.4", + "format-message-print": "^6.2.4" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.4", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.2.2", + "core-js-compat": "^3.16.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.2.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-react-intl": { + "version": "3.5.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.4.5", + "@babel/helper-plugin-utils": "^7.0.0", + "@types/babel__core": "^7.1.2", + "fs-extra": "^8.0.1", + "intl-messageformat-parser": "^1.8.1" + } + }, + "node_modules/babel-plugin-react-intl/node_modules/fs-extra": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/babel-plugin-transform-format-message": { + "version": "6.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/parser": "^7.0.0", + "format-message": "^6.2.4", + "format-message-estree-util": "^6.2.4", + "format-message-formats": "^6.2.4", + "format-message-generate-id": "^6.2.4", + "format-message-parse": "^6.2.4", + "lookup-closest-locale": "^6.2.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-plugin-transform-format-message/node_modules/format-message": { + "version": "6.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "format-message-formats": "^6.2.4", + "format-message-interpret": "^6.2.4", + "format-message-parse": "^6.2.4", + "lookup-closest-locale": "^6.2.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/base": { + "version": "0.11.2", + "license": "MIT", + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "0.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/base64-loader": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/batch": { + "version": "0.6.1", + "dev": true, + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bind-obj-methods": { + "version": "2.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/bl": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bl/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/bl/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.1", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.11.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bonjour": { + "version": "3.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brfs": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "quote-stream": "^1.0.1", + "resolve": "^1.1.5", + "static-module": "^2.2.0", + "through2": "^2.0.0" + }, + "bin": { + "brfs": "bin/cmd.js" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/browser-stdout": { + "version": "1.3.0", + "dev": true, + "license": "ISC" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.2", + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.4", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.22.2", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/btoa": { + "version": "1.2.1", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/buffer-indexof": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/buffer/node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "10.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.1", + "mississippi": "^2.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^5.2.4", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "4.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "2.1.2", + "dev": true, + "license": "ISC" + }, + "node_modules/cache-base": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-path": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/callsite": { + "version": "1.0.0", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001572", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cannon-es": { + "version": "0.20.0", + "license": "MIT" + }, + "node_modules/canvas-toBlob": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/caseless": { + "version": "0.12.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/catharsis": { + "version": "0.8.11", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "license": "ISC" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/circular-json": { + "version": "0.3.3", + "dev": true, + "license": "MIT" + }, + "node_modules/class-utils": { + "version": "0.3.6", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clean-yaml-object": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-width": { + "version": "2.2.1", + "dev": true, + "license": "ISC" + }, + "node_modules/cliui": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colors": { + "version": "0.6.2", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/complex.js": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0" + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-concurrently": { + "version": "1.0.5", + "license": "ISC", + "dependencies": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "globby": "^7.1.1", + "is-glob": "^4.0.0", + "loader-utils": "^1.1.0", + "minimatch": "^3.0.4", + "p-limit": "^1.0.0", + "serialize-javascript": "^1.4.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/find-cache-dir": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/find-up": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/locate-path": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/make-dir": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/p-locate": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/pkg-dir": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/core-js": { + "version": "2.3.0", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.35.0", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.22.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/coveralls": { + "version": "3.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "js-yaml": "^3.13.1", + "lcov-parse": "^1.0.0", + "log-driver": "^1.2.7", + "minimist": "^1.2.5", + "request": "^2.88.2" + }, + "bin": { + "coveralls": "bin/coveralls.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/crc32": { + "version": "0.2.2", + "dev": true, + "bin": { + "crc32": "bin/runner.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.0", + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.0", + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cyclist": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "license": "MIT" + }, + "node_modules/decode-html": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-equal": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/default-gateway": { + "version": "4.2.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/del/node_modules/globby": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/pify": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "1.4.0", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.4", + "license": "Apache-2.0" + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.0", + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/dns-txt": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-indexof": "^1.0.0" + } + }, + "node_modules/docdash": { + "version": "1.2.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-serializer": { + "version": "0.2.2", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.3.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "dev": true + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "2.4.2", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/dompurify": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", + "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" + }, + "node_modules/domutils": { + "version": "1.7.0", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/duplexify": { + "version": "3.7.1", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.616", + "dev": true, + "license": "ISC" + }, + "node_modules/elliptic": { + "version": "6.5.4", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.0", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "4.5.0", + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/enhanced-resolve/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/enhanced-resolve/node_modules/memory-fs": { + "version": "0.5.0", + "license": "MIT", + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/enhanced-resolve/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/enhanced-resolve/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/enhanced-resolve/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/entities": { + "version": "1.1.2", + "license": "BSD-2-Clause" + }, + "node_modules/errno": { + "version": "0.1.8", + "license": "MIT", + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.62", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-map": { + "version": "0.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "node_modules/es6-promise": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/es6-set": { + "version": "0.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "es6-iterator": "~2.0.3", + "es6-symbol": "^3.1.3", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-set/node_modules/type": { + "version": "2.7.2", + "dev": true, + "license": "ISC" + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-latex": { + "version": "1.2.0", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "1.9.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/esprima": { + "version": "3.1.3", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escope": { + "version": "3.6.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/escope/node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.5.0", + "babel-code-frame": "^6.26.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^4.0.0", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.2", + "imurmurhash": "^0.1.4", + "inquirer": "^5.2.0", + "is-resolvable": "^1.1.0", + "js-yaml": "^3.11.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^2.0.0", + "require-uncached": "^1.0.3", + "semver": "^5.5.0", + "string.prototype.matchall": "^2.0.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^4.0.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^6.14.0 || ^8.10.0 || >=9.10.0" + } + }, + "node_modules/eslint-config-scratch": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "optionalDependencies": { + "eslint-plugin-react": ">=7.14.2" + }, + "peerDependencies": { + "babel-eslint": ">=8.0.1", + "eslint": ">=4.0.0" + } + }, + "node_modules/eslint-plugin-format-message": { + "version": "6.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "format-message": "^6.2.4", + "format-message-estree-util": "^6.2.4", + "format-message-generate-id": "^6.2.4", + "format-message-parse": "^6.2.4", + "lookup-closest-locale": "^6.2.0" + }, + "peerDependencies": { + "eslint": ">=2.0.0" + } + }, + "node_modules/eslint-plugin-format-message/node_modules/format-message": { + "version": "6.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "format-message-formats": "^6.2.4", + "format-message-interpret": "^6.2.4", + "format-message-parse": "^6.2.4", + "lookup-closest-locale": "^6.2.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.33.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/string.prototype.matchall": { + "version": "4.0.10", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "4.0.3", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-utils": { + "version": "1.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint/node_modules/semver": { + "version": "5.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "4.1.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^6.0.2", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-to-array": { + "version": "1.1.2", + "dev": true, + "license": "ISC" + }, + "node_modules/eventsource": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/exit-hook": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.7", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/expose-loader": { + "version": "0.7.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.3 < 5.0.0 || >= 5.10" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/express": { + "version": "4.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "dev": true, + "license": "ISC" + }, + "node_modules/extend": { + "version": "3.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/falafel": { + "version": "2.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "isarray": "^2.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/falafel/node_modules/acorn": { + "version": "7.4.1", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/figgy-pudding": { + "version": "3.5.2", + "license": "ISC" + }, + "node_modules/figures": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/file-entry-cache": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-loader": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^1.0.2", + "schema-utils": "^1.0.0" + }, + "engines": { + "node": ">= 6.9.0 < 7.0.0 || >= 8.9.0" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/filename-reserved-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filenamify": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filenamify-url": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup": { + "version": "0.1.5", + "dev": true, + "dependencies": { + "colors": "~0.6.0-1", + "commander": "~2.1.0" + }, + "bin": { + "findup": "bin/findup.js" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/findup/node_modules/commander": { + "version": "2.1.0", + "dev": true, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/flat-cache": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "2.6.3", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/flush-write-stream/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/flush-write-stream/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/flush-write-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/flush-write-stream/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/foreground-child": { + "version": "1.5.6", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "node_modules/foreground-child/node_modules/lru-cache": { + "version": "4.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/foreground-child/node_modules/yallist": { + "version": "2.1.2", + "dev": true, + "license": "ISC" + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/format-message": { + "version": "6.2.1", + "license": "MIT", + "dependencies": { + "format-message-formats": "^6.2.0", + "format-message-interpret": "^6.2.0", + "format-message-parse": "^6.2.0", + "lookup-closest-locale": "^6.2.0" + } + }, + "node_modules/format-message-cli": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.0.0", + "babel-plugin-extract-format-message": "^6.2.0", + "babel-plugin-transform-format-message": "^6.2.0", + "commander": "^2.11.0", + "eslint": "^3.19.0", + "eslint-plugin-format-message": "^6.2.0", + "glob": "^5.0.15", + "js-yaml": "^3.10.0", + "mkdirp": "^0.5.1", + "safe-buffer": "^5.1.1", + "source-map": "^0.5.7" + }, + "bin": { + "format-message": "format-message" + } + }, + "node_modules/format-message-cli/node_modules/acorn": { + "version": "5.7.4", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/format-message-cli/node_modules/acorn-jsx": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^3.0.4" + } + }, + "node_modules/format-message-cli/node_modules/acorn-jsx/node_modules/acorn": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/format-message-cli/node_modules/ajv": { + "version": "4.11.8", + "dev": true, + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "node_modules/format-message-cli/node_modules/ajv-keywords": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": ">=4.10.0" + } + }, + "node_modules/format-message-cli/node_modules/ansi-escapes": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/ansi-styles": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/chalk": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/cli-cursor": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/format-message-cli/node_modules/eslint": { + "version": "3.19.0", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-code-frame": "^6.16.0", + "chalk": "^1.1.3", + "concat-stream": "^1.5.2", + "debug": "^2.1.1", + "doctrine": "^2.0.0", + "escope": "^3.6.0", + "espree": "^3.4.0", + "esquery": "^1.0.0", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "glob": "^7.0.3", + "globals": "^9.14.0", + "ignore": "^3.2.0", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.7.5", + "strip-bom": "^3.0.0", + "strip-json-comments": "~2.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/format-message-cli/node_modules/eslint/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/format-message-cli/node_modules/espree": { + "version": "3.5.4", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/format-message-cli/node_modules/figures": { + "version": "1.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/glob": { + "version": "5.0.15", + "dev": true, + "license": "ISC", + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/format-message-cli/node_modules/globals": { + "version": "9.18.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/ignore": { + "version": "3.3.10", + "dev": true, + "license": "MIT" + }, + "node_modules/format-message-cli/node_modules/inquirer": { + "version": "0.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "node_modules/format-message-cli/node_modules/inquirer/node_modules/string-width": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/format-message-cli/node_modules/onetime": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/pluralize": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/format-message-cli/node_modules/progress": { + "version": "1.1.8", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/format-message-cli/node_modules/restore-cursor": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/run-async": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.3.0" + } + }, + "node_modules/format-message-cli/node_modules/slice-ansi": { + "version": "0.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/format-message-cli/node_modules/supports-color": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/format-message-cli/node_modules/table": { + "version": "3.8.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + } + }, + "node_modules/format-message-estree-util": { + "version": "6.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/format-message-formats": { + "version": "6.2.4", + "license": "MIT" + }, + "node_modules/format-message-generate-id": { + "version": "6.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "crc32": "^0.2.2", + "format-message-parse": "^6.2.4", + "format-message-print": "^6.2.4" + } + }, + "node_modules/format-message-interpret": { + "version": "6.2.4", + "license": "MIT", + "dependencies": { + "format-message-formats": "^6.2.4", + "lookup-closest-locale": "^6.2.0" + } + }, + "node_modules/format-message-parse": { + "version": "6.2.4", + "license": "MIT" + }, + "node_modules/format-message-print": { + "version": "6.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.4", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "license": "MIT", + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2-array": { + "version": "0.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.0.3" + } + }, + "node_modules/from2/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fs-exists-cached": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-write-stream-atomic": { + "version": "1.0.10", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "node_modules/fs-write-stream-atomic/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/fs-write-stream-atomic/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/fs-write-stream-atomic/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/fs-write-stream-atomic/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-loop": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generate-object-property": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "1.0.3", + "dev": true, + "license": "ISC" + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-stream/node_modules/pump": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gh-pages": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "2.6.1", + "commander": "2.15.1", + "filenamify-url": "^1.0.0", + "fs-extra": "^5.0.0", + "globby": "^6.1.0", + "graceful-fs": "4.1.11", + "rimraf": "^2.6.2" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "2.15.1", + "dev": true, + "license": "MIT" + }, + "node_modules/gh-pages/node_modules/globby": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages/node_modules/graceful-fs": { + "version": "4.1.11", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/gh-pages/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/global-modules-path": { + "version": "2.3.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/globals": { + "version": "11.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "3.3.10", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/grapheme-breaker": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "brfs": "^1.2.0", + "unicode-trie": "^0.3.1" + } + }, + "node_modules/growl": { + "version": "1.10.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.x" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-base": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/heap": { + "version": "0.2.5" + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "license": "MIT", + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "0.19.1", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/hull.js": { + "version": "0.2.10", + "dev": true, + "license": "BSD" + }, + "node_modules/humanize-url": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/iferr": { + "version": "0.1.5", + "license": "MIT" + }, + "node_modules/ify-loader": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "findup": "^0.1.5", + "from2-array": "0.0.4", + "map-limit": "0.0.1", + "multipipe": "^0.3.0", + "read-package-json": "^2.0.2", + "resolve": "^1.1.6" + } + }, + "node_modules/ignore": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "license": "MIT" + }, + "node_modules/immutable": { + "version": "3.8.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^2.0.0", + "resolve-cwd": "^2.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local/node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/in-publish": { + "version": "2.0.1", + "dev": true, + "license": "ISC", + "bin": { + "in-install": "in-install.js", + "in-publish": "in-publish.js", + "not-in-install": "not-in-install.js", + "not-in-publish": "not-in-publish.js" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/internal-ip": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/intl-messageformat-parser": { + "version": "1.8.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/invert-kv": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ip": { + "version": "1.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-absolute-url": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "license": "MIT", + "optional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-extendable": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-my-ip-valid": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-my-json-valid": { + "version": "2.20.6", + "dev": true, + "license": "MIT", + "dependencies": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^5.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-in-cwd": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-path-inside": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "path-is-inside": "^1.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-resolvable": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/is-set": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "license": "MIT" + }, + "node_modules/js-md5": { + "version": "0.7.3", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdoc": { + "version": "3.6.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.9.4", + "bluebird": "^3.7.2", + "catharsis": "^0.8.11", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.1", + "klaw": "^3.0.0", + "markdown-it": "^10.0.0", + "markdown-it-anchor": "^5.2.7", + "marked": "^0.8.2", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "taffydb": "2.6.2", + "underscore": "~1.10.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=8.15.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsdoc/node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json": { + "version": "9.0.6", + "dev": true, + "bin": { + "json": "lib/json.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "license": "MIT" + }, + "node_modules/json-stable-stringify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/killable": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/lcid": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "invert-kv": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lcov-parse": { + "version": "1.0.0", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "lcov-parse": "bin/cli.js" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linebreak": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "brfs": "^1.3.0", + "unicode-trie": "^0.3.0" + } + }, + "node_modules/linkify-it": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/loader-runner": { + "version": "2.4.0", + "license": "MIT", + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/loader-utils": { + "version": "1.4.2", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaultsdeep": { + "version": "4.6.1", + "dev": true, + "license": "MIT" + }, + "node_modules/log-driver": { + "version": "1.2.7", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=0.8.6" + } + }, + "node_modules/loglevel": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "vlq": "^0.2.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-limit": { + "version": "0.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/map-limit/node_modules/once": { + "version": "1.3.3", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-it": { + "version": "10.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "entities": "~2.0.0", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "5.3.0", + "dev": true, + "license": "Unlicense", + "peerDependencies": { + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/marked": { + "version": "0.8.2", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">= 8.16.2" + } + }, + "node_modules/mathjs": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.11.1.tgz", + "integrity": "sha512-uWrwMrhU31TCqHKmm1yFz0C352njGUVr/I1UnpMOxI/VBTTbCktx/mREUXx5Vyg11xrFdg/F3wnMM7Ql/csVsQ==", + "dependencies": { + "@babel/runtime": "^7.22.15", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "4.3.4", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/matter-js": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.20.0.tgz", + "integrity": "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA==", + "license": "MIT" + }, + "node_modules/md5.js": { + "version": "1.3.5", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mem": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mem/node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/memory-fs": { + "version": "0.4.1", + "license": "MIT", + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "node_modules/memory-fs/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/memory-fs/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/memory-fs/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/memory-fs/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-source-map": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/mersenne-twister": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/microee": { + "version": "0.0.6", + "license": "BSD" + }, + "node_modules/micromatch": { + "version": "3.1.10", + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces": { + "version": "2.3.2", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-extendable": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-number": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/to-regex-range": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.0", + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "dev": true, + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/minilog": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "microee": "0.0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "2.9.0", + "dev": true, + "license": "ISC", + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/mississippi": { + "version": "2.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^2.0.1", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkpath": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/mocha/node_modules/commander": { + "version": "2.11.0", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/debug": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "3.3.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mocha/node_modules/minimist": { + "version": "0.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/mkdirp": { + "version": "0.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/move-concurrently": { + "version": "1.0.1", + "license": "ISC", + "dependencies": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/multipipe": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer2": "^0.1.2" + } + }, + "node_modules/mute-stream": { + "version": "0.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "license": "MIT" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "0.10.0", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "license": "MIT", + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/punycode": { + "version": "1.4.1", + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc": { + "version": "11.9.0", + "bundleDependencies": [ + "archy", + "arrify", + "caching-transform", + "convert-source-map", + "debug-log", + "default-require-extensions", + "find-cache-dir", + "find-up", + "foreground-child", + "glob", + "istanbul-lib-coverage", + "istanbul-lib-hook", + "istanbul-lib-instrument", + "istanbul-lib-report", + "istanbul-lib-source-maps", + "istanbul-reports", + "md5-hex", + "merge-source-map", + "micromatch", + "mkdirp", + "resolve-from", + "rimraf", + "signal-exit", + "spawn-wrap", + "test-exclude", + "yargs", + "yargs-parser", + "align-text", + "amdefine", + "ansi-regex", + "ansi-styles", + "append-transform", + "arr-diff", + "arr-flatten", + "arr-union", + "array-unique", + "assign-symbols", + "async", + "atob", + "babel-code-frame", + "babel-generator", + "babel-messages", + "babel-runtime", + "babel-template", + "babel-traverse", + "babel-types", + "babylon", + "balanced-match", + "base", + "brace-expansion", + "braces", + "builtin-modules", + "cache-base", + "camelcase", + "center-align", + "chalk", + "class-utils", + "cliui", + "code-point-at", + "collection-visit", + "commondir", + "component-emitter", + "concat-map", + "copy-descriptor", + "core-js", + "cross-spawn", + "debug", + "decamelize", + "decode-uri-component", + "define-property", + "detect-indent", + "error-ex", + "escape-string-regexp", + "esutils", + "execa", + "expand-brackets", + "extend-shallow", + "extglob", + "fill-range", + "for-in", + "fragment-cache", + "fs.realpath", + "get-caller-file", + "get-stream", + "get-value", + "globals", + "graceful-fs", + "handlebars", + "has-ansi", + "has-flag", + "has-value", + "has-values", + "hosted-git-info", + "imurmurhash", + "inflight", + "inherits", + "invariant", + "invert-kv", + "is-accessor-descriptor", + "is-arrayish", + "is-buffer", + "is-builtin-module", + "is-data-descriptor", + "is-descriptor", + "is-extendable", + "is-finite", + "is-fullwidth-code-point", + "is-number", + "is-odd", + "is-plain-object", + "is-stream", + "is-utf8", + "is-windows", + "isarray", + "isexe", + "isobject", + "js-tokens", + "jsesc", + "kind-of", + "lazy-cache", + "lcid", + "load-json-file", + "locate-path", + "lodash", + "longest", + "loose-envify", + "lru-cache", + "map-cache", + "map-visit", + "md5-o-matic", + "mem", + "mimic-fn", + "minimatch", + "minimist", + "mixin-deep", + "ms", + "nanomatch", + "normalize-package-data", + "npm-run-path", + "number-is-nan", + "object-assign", + "object-copy", + "object-visit", + "object.pick", + "once", + "optimist", + "os-homedir", + "os-locale", + "p-finally", + "p-limit", + "p-locate", + "p-try", + "parse-json", + "pascalcase", + "path-exists", + "path-is-absolute", + "path-key", + "path-parse", + "path-type", + "pify", + "pinkie", + "pinkie-promise", + "pkg-dir", + "posix-character-classes", + "pseudomap", + "read-pkg", + "read-pkg-up", + "regenerator-runtime", + "regex-not", + "repeat-element", + "repeat-string", + "repeating", + "require-directory", + "require-main-filename", + "resolve-url", + "ret", + "right-align", + "safe-regex", + "semver", + "set-blocking", + "set-value", + "shebang-command", + "shebang-regex", + "slide", + "snapdragon", + "snapdragon-node", + "snapdragon-util", + "source-map", + "source-map-resolve", + "source-map-url", + "spdx-correct", + "spdx-exceptions", + "spdx-expression-parse", + "spdx-license-ids", + "split-string", + "static-extend", + "string-width", + "strip-ansi", + "strip-bom", + "strip-eof", + "supports-color", + "to-fast-properties", + "to-object-path", + "to-regex", + "to-regex-range", + "trim-right", + "uglify-js", + "uglify-to-browserify", + "union-value", + "unset-value", + "urix", + "use", + "validate-npm-package-license", + "which", + "which-module", + "window-size", + "wordwrap", + "wrap-ansi", + "wrappy", + "write-file-atomic", + "y18n", + "yallist" + ], + "dev": true, + "license": "ISC", + "dependencies": { + "archy": "^1.0.0", + "arrify": "^1.0.1", + "caching-transform": "^1.0.0", + "convert-source-map": "^1.5.1", + "debug-log": "^1.0.1", + "default-require-extensions": "^1.0.0", + "find-cache-dir": "^0.1.1", + "find-up": "^2.1.0", + "foreground-child": "^1.5.3", + "glob": "^7.0.6", + "istanbul-lib-coverage": "^1.1.2", + "istanbul-lib-hook": "^1.1.0", + "istanbul-lib-instrument": "^1.10.0", + "istanbul-lib-report": "^1.1.3", + "istanbul-lib-source-maps": "^1.2.3", + "istanbul-reports": "^1.4.0", + "md5-hex": "^1.2.0", + "merge-source-map": "^1.1.0", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.0", + "resolve-from": "^2.0.0", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.1", + "spawn-wrap": "^1.4.2", + "test-exclude": "^4.2.0", + "yargs": "11.1.0", + "yargs-parser": "^8.0.0" + }, + "bin": { + "nyc": "bin/nyc.js" + } + }, + "node_modules/nyc/node_modules/align-text": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/amdefine": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause OR MIT", + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/ansi-styles": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/append-transform": { + "version": "0.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "default-require-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/arr-diff": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/arr-flatten": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/arr-union": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/array-unique": { + "version": "0.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/arrify": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/assign-symbols": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/async": { + "version": "1.5.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/atob": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/nyc/node_modules/babel-code-frame": { + "version": "6.26.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/nyc/node_modules/babel-generator": { + "version": "6.26.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "node_modules/nyc/node_modules/babel-messages": { + "version": "6.23.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/nyc/node_modules/babel-runtime": { + "version": "6.26.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/nyc/node_modules/babel-template": { + "version": "6.26.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "node_modules/nyc/node_modules/babel-traverse": { + "version": "6.26.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "node_modules/nyc/node_modules/babel-types": { + "version": "6.26.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "node_modules/nyc/node_modules/babylon": { + "version": "6.18.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "babylon": "bin/babylon.js" + } + }, + "node_modules/nyc/node_modules/balanced-match": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/base": { + "version": "0.11.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/base/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/base/node_modules/is-data-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/base/node_modules/is-descriptor": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/base/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/base/node_modules/kind-of": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nyc/node_modules/braces": { + "version": "2.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/builtin-modules": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/cache-base": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/cache-base/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/caching-transform": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "md5-hex": "^1.2.0", + "mkdirp": "^0.5.1", + "write-file-atomic": "^1.1.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/camelcase": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/center-align": { + "version": "0.1.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/chalk": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/class-utils": { + "version": "0.3.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/class-utils/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "node_modules/nyc/node_modules/cliui/node_modules/wordwrap": { + "version": "0.0.2", + "dev": true, + "inBundle": true, + "license": "MIT/X11", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/nyc/node_modules/code-point-at": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/collection-visit": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/commondir": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/component-emitter": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/convert-source-map": { + "version": "1.5.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/copy-descriptor": { + "version": "0.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/core-js": { + "version": "2.5.6", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/cross-spawn": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "node_modules/nyc/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/nyc/node_modules/debug-log": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/decamelize": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/decode-uri-component": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/nyc/node_modules/default-require-extensions": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/define-property": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/define-property/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/define-property/node_modules/is-data-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/define-property/node_modules/is-descriptor": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/define-property/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/define-property/node_modules/kind-of": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/detect-indent": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/error-ex": { + "version": "1.3.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/nyc/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nyc/node_modules/esutils": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/execa": { + "version": "0.7.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/execa/node_modules/cross-spawn": { + "version": "5.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/nyc/node_modules/expand-brackets": { + "version": "2.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extend-shallow": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extend-shallow/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extglob": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extglob/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extglob/node_modules/is-data-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extglob/node_modules/is-descriptor": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/extglob/node_modules/kind-of": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/fill-range": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/find-cache-dir": { + "version": "0.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/find-up": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/for-in": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/foreground-child": { + "version": "1.5.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + } + }, + "node_modules/nyc/node_modules/fragment-cache": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/get-caller-file": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/get-stream": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/get-value": { + "version": "2.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nyc/node_modules/globals": { + "version": "9.18.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/graceful-fs": { + "version": "4.1.11", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/nyc/node_modules/handlebars": { + "version": "4.0.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^2.6" + } + }, + "node_modules/nyc/node_modules/handlebars/node_modules/source-map": { + "version": "0.4.4", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nyc/node_modules/has-ansi": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/has-flag": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/has-value": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/has-value/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/has-values": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/hosted-git-info": { + "version": "2.6.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/nyc/node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/nyc/node_modules/inherits": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/invariant": { + "version": "2.2.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/nyc/node_modules/invert-kv": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/is-buffer": { + "version": "1.1.6", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/is-builtin-module": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-descriptor": { + "version": "0.1.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-extendable": { + "version": "0.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-finite": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/is-number": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-odd": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-number": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-odd/node_modules/is-number": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-plain-object/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/is-utf8": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/is-windows": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-coverage": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/nyc/node_modules/istanbul-lib-hook": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "append-transform": "^0.4.0" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-instrument": { + "version": "1.10.1", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-report": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "3.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-source-maps": { + "version": "1.2.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/nyc/node_modules/istanbul-reports": { + "version": "1.4.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "handlebars": "^4.0.3" + } + }, + "node_modules/nyc/node_modules/js-tokens": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/jsesc": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/nyc/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/lazy-cache": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/lcid": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/load-json-file": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/locate-path/node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/lodash": { + "version": "4.17.10", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/longest": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/loose-envify": { + "version": "1.3.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nyc/node_modules/lru-cache": { + "version": "4.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/nyc/node_modules/map-cache": { + "version": "0.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/map-visit": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/md5-hex": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "md5-o-matic": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/md5-o-matic": { + "version": "0.1.1", + "dev": true, + "inBundle": true + }, + "node_modules/nyc/node_modules/mem": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/merge-source-map": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/nyc/node_modules/merge-source-map/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/micromatch": { + "version": "3.1.10", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/mimic-fn": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/minimatch": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nyc/node_modules/minimist": { + "version": "0.0.8", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/mixin-deep": { + "version": "1.3.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/mkdirp": { + "version": "0.5.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/nyc/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/nanomatch": { + "version": "1.2.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/nanomatch/node_modules/arr-diff": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/nanomatch/node_modules/array-unique": { + "version": "0.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/nanomatch/node_modules/kind-of": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/normalize-package-data": { + "version": "2.4.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/nyc/node_modules/npm-run-path": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/number-is-nan": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/object-copy": { + "version": "0.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/object-visit": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/object-visit/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/object.pick": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/object.pick/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/once": { + "version": "1.4.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/nyc/node_modules/optimist": { + "version": "0.6.1", + "dev": true, + "inBundle": true, + "license": "MIT/X11", + "dependencies": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "node_modules/nyc/node_modules/os-homedir": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/os-locale": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/p-finally": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/p-try": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/parse-json": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/pascalcase": { + "version": "0.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/path-exists": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/path-parse": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/path-type": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/pinkie": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/pinkie-promise": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/pkg-dir": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "find-up": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/pkg-dir/node_modules/find-up": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/posix-character-classes": { + "version": "0.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/pseudomap": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/read-pkg": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/read-pkg-up": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/regenerator-runtime": { + "version": "0.11.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/regex-not": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/repeat-element": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/repeat-string": { + "version": "1.6.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/nyc/node_modules/repeating": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/require-main-filename": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/resolve-url": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/ret": { + "version": "0.1.15", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/nyc/node_modules/right-align": { + "version": "0.1.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/rimraf": { + "version": "2.6.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/nyc/node_modules/safe-regex": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/nyc/node_modules/semver": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nyc/node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/set-value": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/signal-exit": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/slide": { + "version": "1.1.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/nyc/node_modules/snapdragon": { + "version": "0.8.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon-node": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon-node/node_modules/is-data-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon-node/node_modules/is-descriptor": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon-node/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon-node/node_modules/kind-of": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon-util": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/source-map": { + "version": "0.5.7", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/source-map-resolve": { + "version": "0.5.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "atob": "^2.0.0", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/nyc/node_modules/source-map-url": { + "version": "0.4.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/spawn-wrap": { + "version": "1.4.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "node_modules/nyc/node_modules/spdx-correct": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/nyc/node_modules/spdx-exceptions": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/nyc/node_modules/spdx-expression-parse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/nyc/node_modules/spdx-license-ids": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/nyc/node_modules/split-string": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/static-extend": { + "version": "0.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/string-width": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/string-width/node_modules/ansi-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/strip-bom": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/strip-eof": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/supports-color": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nyc/node_modules/test-exclude": { + "version": "4.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "arrify": "^1.0.1", + "micromatch": "^3.1.8", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/arr-diff": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/array-unique": { + "version": "0.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/braces": { + "version": "2.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets": { + "version": "2.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/expand-brackets/node_modules/kind-of": { + "version": "5.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/extglob": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/fill-range": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/is-data-descriptor": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/is-descriptor": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/is-number": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/kind-of": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/test-exclude/node_modules/micromatch": { + "version": "3.1.10", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/to-fast-properties": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/to-object-path": { + "version": "0.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/to-regex": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/to-regex-range": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/to-regex-range/node_modules/is-number": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/trim-right": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/uglify-js": { + "version": "2.8.29", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "source-map": "~0.5.1", + "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + } + }, + "node_modules/nyc/node_modules/uglify-js/node_modules/yargs": { + "version": "3.10.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + }, + "node_modules/nyc/node_modules/uglify-to-browserify": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/nyc/node_modules/union-value": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/union-value/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/union-value/node_modules/set-value": { + "version": "0.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/unset-value": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/unset-value/node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/urix": { + "version": "0.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/use": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/use/node_modules/kind-of": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/validate-npm-package-license": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/nyc/node_modules/which": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/nyc/node_modules/which-module": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/window-size": { + "version": "0.1.0", + "dev": true, + "inBundle": true, + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/nyc/node_modules/wordwrap": { + "version": "0.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/wrap-ansi/node_modules/string-width": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/write-file-atomic": { + "version": "1.3.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "3.2.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/yallist": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/yargs": { + "version": "11.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "camelcase": "^4.1.0" + } + }, + "node_modules/nyc/node_modules/yargs-parser/node_modules/camelcase": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/yargs/node_modules/ansi-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/yargs/node_modules/camelcase": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/yargs/node_modules/cliui": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/nyc/node_modules/yargs/node_modules/strip-ansi": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nyc/node_modules/yargs/node_modules/yargs-parser": { + "version": "9.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "camelcase": "^4.1.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.7", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.7", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/opn": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "license": "MIT" + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-locale": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-or": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/own-or-env": { + "version": "1.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "own-or": "^1.0.0" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-map": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-retry": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "^0.12.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "license": "(MIT AND Zlib)" + }, + "node_modules/parallel-transform": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "node_modules/parallel-transform/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/parallel-transform/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/parallel-transform/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/parallel-transform/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.6", + "license": "ISC", + "dependencies": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-browserify": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pathfinding": { + "version": "0.4.18", + "dependencies": { + "heap": "0.2.5" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.2", + "license": "MIT", + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pngjs": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/async": { + "version": "2.6.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/psl": { + "version": "1.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.0", + "license": "MIT" + }, + "node_modules/pump": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "license": "MIT", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/query-string": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/quote-stream": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "minimist": "^1.1.3", + "through2": "^2.0.0" + }, + "bin": { + "quote-stream": "bin/cmd.js" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-loader": { + "version": "0.5.1", + "dev": true + }, + "node_modules/react-is": { + "version": "16.13.1", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "license": "MIT", + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readline2": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "node_modules/readline2/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readline2/node_modules/mute-stream": { + "version": "0.0.5", + "dev": true, + "license": "ISC" + }, + "node_modules/rechoir": { + "version": "0.6.2", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "devOptional": true, + "license": "ISC" + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/request": { + "version": "2.88.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/require-uncached": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/requizzle": { + "version": "0.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-from": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-queue": { + "version": "1.0.3", + "license": "ISC", + "dependencies": { + "aproba": "^1.1.1" + } + }, + "node_modules/rx-lite": { + "version": "3.1.2", + "dev": true + }, + "node_modules/rxjs": { + "version": "5.5.12", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "symbol-observable": "1.0.1" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/scratch-audio": { + "version": "0.1.0-prerelease.20200528195344", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "audio-context": "1.0.1", + "minilog": "^3.0.1", + "startaudiocontext": "1.2.1" + } + }, + "node_modules/scratch-blocks": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/PenguinMod/PenguinMod-Blocks.git#db277700d4d9a9d00c5596015fb8f8810a4cca19", + "dev": true, + "license": "GPL-3.0" + }, + "node_modules/scratch-l10n": { + "version": "3.14.20220526031602", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/cli": "^7.1.2", + "@babel/core": "^7.1.2", + "babel-plugin-react-intl": "^3.0.1", + "transifex": "1.6.6" + }, + "bin": { + "build-i18n-src": "scripts/build-i18n-src.js", + "tx-push-src": "scripts/tx-push-src.js" + } + }, + "node_modules/scratch-parser": { + "version": "0.0.0-development", + "resolved": "git+ssh://git@github.com/PenguinMod/PenguinMod-Parser.git#c56c7aad93f71aa5d1a126bae1d5e663c161e8eb", + "license": "BSD-3-Clause", + "dependencies": { + "@turbowarp/json": "^0.1.1", + "ajv": "6.3.0", + "jszip": "3.1.5", + "pify": "4.0.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/scratch-parser/node_modules/ajv": { + "version": "6.3.0", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "node_modules/scratch-parser/node_modules/fast-deep-equal": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/scratch-parser/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/scratch-parser/node_modules/json-schema-traverse": { + "version": "0.3.1", + "license": "MIT" + }, + "node_modules/scratch-parser/node_modules/jszip": { + "version": "3.1.5", + "license": "(MIT OR GPL-3.0)", + "dependencies": { + "core-js": "~2.3.0", + "es6-promise": "~3.0.2", + "lie": "~3.1.0", + "pako": "~1.0.2", + "readable-stream": "~2.0.6" + } + }, + "node_modules/scratch-parser/node_modules/lie": { + "version": "3.1.1", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/scratch-parser/node_modules/pify": { + "version": "4.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/scratch-parser/node_modules/process-nextick-args": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/scratch-parser/node_modules/readable-stream": { + "version": "2.0.6", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/scratch-parser/node_modules/string_decoder": { + "version": "0.10.31", + "license": "MIT" + }, + "node_modules/scratch-render": { + "version": "0.1.0-prerelease.20211028200436", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "grapheme-breaker": "0.3.2", + "hull.js": "0.2.10", + "ify-loader": "1.0.4", + "linebreak": "0.3.0", + "minilog": "3.1.0", + "raw-loader": "^0.5.1", + "scratch-storage": "^1.0.0", + "scratch-svg-renderer": "0.2.0-prerelease.20210727023023", + "twgl.js": "4.4.0" + }, + "peerDependencies": { + "scratch-render-fonts": "^1.0.0-prerelease" + } + }, + "node_modules/scratch-render-fonts": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/PenguinMod/penguinmod-render-fonts.git#fbca3cc01bd32e73d1c3e42cd5c2ee5f60a9a7c5", + "dev": true, + "dependencies": { + "base64-loader": "1.0.0" + } + }, + "node_modules/scratch-render/node_modules/base64-js": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/scratch-render/node_modules/schema-utils": { + "version": "0.4.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/scratch-render/node_modules/scratch-storage": { + "version": "1.3.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "arraybuffer-loader": "^1.0.3", + "base64-js": "1.3.0", + "fastestsmallesttextencoderdecoder": "^1.0.7", + "js-md5": "0.7.3", + "minilog": "3.1.0", + "worker-loader": "^2.0.0" + } + }, + "node_modules/scratch-render/node_modules/worker-loader": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" + }, + "engines": { + "node": ">= 6.9.0 || >= 8.9.0" + }, + "peerDependencies": { + "webpack": "^3.0.0 || ^4.0.0-alpha.0 || ^4.0.0" + } + }, + "node_modules/scratch-sb1-converter": { + "version": "0.2.7", + "license": "BSD-3-Clause", + "dependencies": { + "js-md5": "0.7.3", + "minilog": "3.1.0", + "text-encoding": "^0.7.0" + } + }, + "node_modules/scratch-storage": { + "version": "0.0.0-development", + "resolved": "git+ssh://git@github.com/PenguinMod/PenguinMod-Storage.git#96f45f701dc11648bc88fcc5307193d591afea84", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "arraybuffer-loader": "^1.0.3", + "base64-js": "1.3.0", + "fastestsmallesttextencoderdecoder": "^1.0.7", + "js-md5": "0.7.3", + "minilog": "3.1.0", + "worker-loader": "^2.0.0" + } + }, + "node_modules/scratch-storage/node_modules/base64-js": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/scratch-storage/node_modules/schema-utils": { + "version": "0.4.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/scratch-storage/node_modules/worker-loader": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" + }, + "engines": { + "node": ">= 6.9.0 || >= 8.9.0" + }, + "peerDependencies": { + "webpack": "^3.0.0 || ^4.0.0-alpha.0 || ^4.0.0" + } + }, + "node_modules/scratch-svg-renderer": { + "version": "0.2.0-prerelease.20210727023023", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "base64-js": "1.2.1", + "base64-loader": "1.0.0", + "dompurify": "2.2.7", + "minilog": "3.1.0", + "transformation-matrix": "1.15.0" + }, + "peerDependencies": { + "scratch-render-fonts": "^1.0.0-prerelease" + } + }, + "node_modules/scratch-svg-renderer/node_modules/base64-js": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/scratch-svg-renderer/node_modules/dompurify": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.7.tgz", + "integrity": "sha512-jdtDffdGNY+C76jvodNTu9jt5yYj59vuTUyx+wXdzcSwAGTYZDAQkQ7Iwx9zcGrA4ixC1syU4H3RZROqRxokxg==", + "dev": true + }, + "node_modules/scratch-translate-extension-languages": { + "version": "0.0.20191118205314", + "license": "BSD-3-Clause" + }, + "node_modules/script-loader": { + "version": "0.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "raw-loader": "~0.5.1" + } + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "license": "MIT" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "1.10.14", + "dev": true, + "license": "MIT", + "dependencies": { + "node-forge": "^0.10.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "1.9.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/serve-index": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-value": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shallow-copy": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shelljs": { + "version": "0.7.8", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "iojs": "*", + "node": ">=0.11.0" + } + }, + "node_modules/should": { + "version": "13.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/simplex-noise": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/slash": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/slice-ansi": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "license": "MIT", + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.7", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/sockjs": { + "version": "0.3.24", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs-client": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "node_modules/sockjs-client/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.5.7", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "license": "MIT" + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sshpk": { + "version": "1.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "5.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/stack-utils": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standardized-audio-context": { + "version": "25.3.61", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.6", + "automation-events": "^6.0.13", + "tslib": "^2.6.2" + } + }, + "node_modules/startaudiocontext": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/static-eval": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escodegen": "^1.11.1" + } + }, + "node_modules/static-eval/node_modules/escodegen": { + "version": "1.14.3", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/static-eval/node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/static-eval/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.7", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/static-module": { + "version": "2.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "concat-stream": "~1.6.0", + "convert-source-map": "^1.5.1", + "duplexer2": "~0.1.4", + "escodegen": "~1.9.0", + "falafel": "^2.1.0", + "has": "^1.0.1", + "magic-string": "^0.22.4", + "merge-source-map": "1.0.4", + "object-inspect": "~1.4.0", + "quote-stream": "~1.0.2", + "readable-stream": "~2.3.3", + "shallow-copy": "~0.0.1", + "static-eval": "^2.0.0", + "through2": "~2.0.3" + } + }, + "node_modules/static-module/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/static-module/node_modules/object-inspect": { + "version": "1.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/static-module/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/static-module/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/static-module/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/stats.js": { + "version": "0.17.0", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-browserify": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-browserify/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-browserify/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/stream-browserify/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/stream-each": { + "version": "1.2.3", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/stream-http": { + "version": "2.8.3", + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-http/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-http/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/stream-http/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.2", + "es-abstract": "^1.10.0", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "regexp.prototype.flags": "^1.2.0" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-url-auth": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/table": { + "version": "4.0.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^6.0.1", + "ajv-keywords": "^3.0.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/taffydb": { + "version": "2.6.2", + "dev": true + }, + "node_modules/tap": { + "version": "12.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "bind-obj-methods": "^2.0.0", + "bluebird": "^3.5.1", + "clean-yaml-object": "^0.1.0", + "color-support": "^1.1.0", + "coveralls": "^3.0.1", + "foreground-child": "^1.3.3", + "fs-exists-cached": "^1.0.0", + "function-loop": "^1.0.1", + "glob": "^7.0.0", + "isexe": "^2.0.0", + "js-yaml": "^3.11.0", + "minipass": "^2.3.0", + "mkdirp": "^0.5.1", + "nyc": "^11.8.0", + "opener": "^1.4.1", + "os-homedir": "^1.0.2", + "own-or": "^1.0.0", + "own-or-env": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.0", + "source-map-support": "^0.5.6", + "stack-utils": "^1.0.0", + "tap-mocha-reporter": "^3.0.7", + "tap-parser": "^7.0.0", + "tmatch": "^4.0.0", + "trivial-deferred": "^1.0.1", + "tsame": "^2.0.0", + "write-file-atomic": "^2.3.0", + "yapool": "^1.0.0" + }, + "bin": { + "tap": "bin/run.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tap-mocha-reporter": { + "version": "3.0.9", + "dev": true, + "license": "ISC", + "dependencies": { + "color-support": "^1.1.0", + "debug": "^2.1.3", + "diff": "^1.3.2", + "escape-string-regexp": "^1.0.3", + "glob": "^7.0.5", + "js-yaml": "^3.3.1", + "tap-parser": "^5.1.0", + "unicode-length": "^1.0.0" + }, + "bin": { + "tap-mocha-reporter": "index.js" + }, + "optionalDependencies": { + "readable-stream": "^2.1.5" + } + }, + "node_modules/tap-mocha-reporter/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/tap-mocha-reporter/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/tap-mocha-reporter/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tap-mocha-reporter/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/tap-mocha-reporter/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/tap-mocha-reporter/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tap-mocha-reporter/node_modules/tap-parser": { + "version": "5.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "events-to-array": "^1.0.1", + "js-yaml": "^3.2.7" + }, + "bin": { + "tap-parser": "bin/cmd.js" + }, + "optionalDependencies": { + "readable-stream": "^2" + } + }, + "node_modules/tap-parser": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "events-to-array": "^1.0.1", + "js-yaml": "^3.2.7", + "minipass": "^2.2.0" + }, + "bin": { + "tap-parser": "bin/cmd.js" + } + }, + "node_modules/tapable": { + "version": "1.1.3", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "4.8.1", + "license": "BSD-2-Clause", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "1.4.5", + "license": "MIT", + "dependencies": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/cacache": { + "version": "12.0.4", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/find-cache-dir": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/find-up": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/locate-path": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/make-dir": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/mississippi": { + "version": "3.0.0", + "license": "BSD-2-Clause", + "dependencies": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/p-limit": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser-webpack-plugin/node_modules/p-locate": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/p-try": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/path-exists": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/terser-webpack-plugin/node_modules/pify": { + "version": "4.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/pkg-dir": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/pump": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/terser-webpack-plugin/node_modules/semver": { + "version": "5.7.2", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "4.0.0", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ssri": { + "version": "6.0.2", + "license": "ISC", + "dependencies": { + "figgy-pudding": "^3.5.1" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/text-encoding": { + "version": "0.7.0", + "license": "(Unlicense OR Apache-2.0)" + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/three": { + "version": "0.153.0", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.6.0", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, + "node_modules/through": { + "version": "2.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-worker": { + "version": "2.3.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "esm": "^3.2.25" + } + }, + "node_modules/tmatch": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/tmp": { + "version": "0.0.33", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tone": { + "version": "14.7.77", + "license": "MIT", + "dependencies": { + "standardized-audio-context": "^25.1.8", + "tslib": "^2.0.1" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/transformation-matrix": { + "version": "1.15.0", + "dev": true, + "license": "MIT" + }, + "node_modules/transifex": { + "version": "1.6.6", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.9.0", + "lodash": "^4.17.1", + "mkpath": "^1.0.0", + "mocha": "^4.0.0", + "request": "^2.34.0", + "should": "^13.0.0" + }, + "bin": { + "transifex": "bin/index.js" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trivial-deferred": { + "version": "1.1.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 8" + } + }, + "node_modules/tsame": { + "version": "2.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/tslib": { + "version": "2.6.2", + "license": "0BSD" + }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "license": "MIT" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "dev": true, + "license": "Unlicense" + }, + "node_modules/twgl.js": { + "version": "4.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/type": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-function": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "license": "MIT" + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-es": { + "version": "3.3.9", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "commander": "~2.13.0", + "source-map": "~0.6.1" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uglify-es/node_modules/commander": { + "version": "2.13.0", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-es/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglifyjs-webpack-plugin": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "schema-utils": "^0.4.5", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "uglify-es": "^3.3.4", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + }, + "engines": { + "node": ">= 4.8 < 5.0.0 || >= 5.10" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/find-cache-dir": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/find-up": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/locate-path": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/make-dir": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/p-locate": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/pkg-dir": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/schema-utils": { + "version": "0.4.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/uglifyjs-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/underscore": { + "version": "1.10.2", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-length": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^1.3.2", + "strip-ansi": "^3.0.1" + } + }, + "node_modules/unicode-length/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unicode-length/node_modules/punycode": { + "version": "1.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-length/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-trie": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "dev": true, + "license": "MIT" + }, + "node_modules/union-value": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "license": "ISC", + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/upath": { + "version": "1.2.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "license": "MIT" + }, + "node_modules/url": { + "version": "0.11.3", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "license": "MIT" + }, + "node_modules/url/node_modules/qs": { + "version": "6.11.2", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/use": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/user-home": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util": { + "version": "0.11.1", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "license": "ISC" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vlq": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "1.7.5", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + }, + "optionalDependencies": { + "chokidar": "^3.4.1", + "watchpack-chokidar2": "^2.0.1" + } + }, + "node_modules/watchpack-chokidar2": { + "version": "2.0.1", + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^2.1.8" + } + }, + "node_modules/watchpack-chokidar2/node_modules/anymatch": { + "version": "2.0.0", + "license": "ISC", + "optional": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/watchpack-chokidar2/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "license": "MIT", + "optional": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/binary-extensions": { + "version": "1.13.1", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/braces": { + "version": "2.3.2", + "license": "MIT", + "optional": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/chokidar": { + "version": "2.1.8", + "license": "MIT", + "optional": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/watchpack-chokidar2/node_modules/extend-shallow": { + "version": "2.0.1", + "license": "MIT", + "optional": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/fill-range": { + "version": "4.0.0", + "license": "MIT", + "optional": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/glob-parent": { + "version": "3.1.0", + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/is-binary-path": { + "version": "1.0.1", + "license": "MIT", + "optional": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/is-extendable": { + "version": "0.1.1", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/is-number": { + "version": "3.0.0", + "license": "MIT", + "optional": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT", + "optional": true + }, + "node_modules/watchpack-chokidar2/node_modules/kind-of": { + "version": "3.2.2", + "license": "MIT", + "optional": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/watchpack-chokidar2/node_modules/readdirp": { + "version": "2.2.1", + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/watchpack-chokidar2/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT", + "optional": true + }, + "node_modules/watchpack-chokidar2/node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/to-regex-range": { + "version": "2.1.1", + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "4.46.0", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.5.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + }, + "webpack-command": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.0.0", + "global-modules-path": "^2.1.0", + "import-local": "^1.0.0", + "inquirer": "^6.0.0", + "interpret": "^1.1.0", + "loader-utils": "^1.1.0", + "supports-color": "^5.4.0", + "v8-compile-cache": "^2.0.0", + "yargs": "^12.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=6.11.5" + }, + "peerDependencies": { + "webpack": "^4.x.x" + } + }, + "node_modules/webpack-cli/node_modules/ansi-regex": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-cli/node_modules/chardet": { + "version": "0.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-cli/node_modules/external-editor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-cli/node_modules/inquirer": { + "version": "6.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/webpack-cli/node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/webpack-cli/node_modules/strip-ansi": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-cli/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/webpack-dev-middleware": { + "version": "3.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime": { + "version": "2.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "3.11.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.8", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 6.11.5" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/anymatch": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/webpack-dev-server/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/binary-extensions": { + "version": "1.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/braces": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "2.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/webpack-dev-server/node_modules/cliui": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/webpack-dev-server/node_modules/cliui/node_modules/ansi-regex": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/fill-range": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/find-up": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "3.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/import-local": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/is-binary-path": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/is-extendable": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/is-number": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/locate-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/p-locate": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/pkg-dir": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/webpack-dev-server/node_modules/require-main-filename": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/webpack-dev-server/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/webpack-dev-server/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/webpack-dev-server/node_modules/string-width": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/string-width/node_modules/ansi-regex": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/string-width/node_modules/strip-ansi": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/supports-color": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/to-regex-range": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/wrap-ansi": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/yargs": { + "version": "13.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/webpack-dev-server/node_modules/yargs-parser": { + "version": "13.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/webpack-log": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-sources": { + "version": "1.4.3", + "license": "MIT", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/webpack-sources/node_modules/source-map": { + "version": "0.6.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/worker-farm": { + "version": "1.7.0", + "license": "MIT", + "dependencies": { + "errno": "~0.1.7" + } + }, + "node_modules/worker-loader": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" + }, + "engines": { + "node": ">= 4.8 < 5.0.0 || >= 5.10" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/worker-loader/node_modules/schema-utils": { + "version": "0.4.7", + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/wrap-ansi": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/write": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/write-file-atomic": { + "version": "2.4.3", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/ws": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/xtend": { + "version": "4.0.2", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "license": "ISC" + }, + "node_modules/yapool": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "12.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "node_modules/yargs-parser": { + "version": "11.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + } + }, + "dependencies": { + "@babel/cli": { + "version": "7.23.4", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0", + "commander": "^4.0.1", + "convert-source-map": "^2.0.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "dev": true + }, + "make-dir": { + "version": "2.1.0", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "dev": true + }, + "semver": { + "version": "5.7.2", + "dev": true + }, + "slash": { + "version": "2.0.0", + "dev": true + } + } + }, + "@babel/code-frame": { + "version": "7.23.5", + "dev": true, + "requires": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + } + }, + "@babel/compat-data": { + "version": "7.23.5", + "dev": true + }, + "@babel/core": { + "version": "7.13.10", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-compilation-targets": "^7.13.10", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helpers": "^7.13.10", + "@babel/parser": "^7.13.10", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^6.3.0", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.23.6", + "dev": true, + "requires": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.23.6", + "dev": true, + "requires": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.23.6", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.2.4", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "dev": true, + "requires": { + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + } + }, + "@babel/helper-replace-supers": { + "version": "7.22.20", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.23.4", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.23.5", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.22.20", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + } + }, + "@babel/helpers": { + "version": "7.23.6", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6" + } + }, + "@babel/highlight": { + "version": "7.23.4", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.23.6", + "dev": true + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.23.3" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.23.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.23.5", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.23.6", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.23.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.23.3", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/preset-env": { + "version": "7.14.8", + "dev": true, + "requires": { + "@babel/compat-data": "^7.14.7", + "@babel/helper-compilation-targets": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-async-generator-functions": "^7.14.7", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-proposal-class-static-block": "^7.14.5", + "@babel/plugin-proposal-dynamic-import": "^7.14.5", + "@babel/plugin-proposal-export-namespace-from": "^7.14.5", + "@babel/plugin-proposal-json-strings": "^7.14.5", + "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", + "@babel/plugin-proposal-numeric-separator": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.14.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.14.5", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-private-methods": "^7.14.5", + "@babel/plugin-proposal-private-property-in-object": "^7.14.5", + "@babel/plugin-proposal-unicode-property-regex": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.14.5", + "@babel/plugin-transform-async-to-generator": "^7.14.5", + "@babel/plugin-transform-block-scoped-functions": "^7.14.5", + "@babel/plugin-transform-block-scoping": "^7.14.5", + "@babel/plugin-transform-classes": "^7.14.5", + "@babel/plugin-transform-computed-properties": "^7.14.5", + "@babel/plugin-transform-destructuring": "^7.14.7", + "@babel/plugin-transform-dotall-regex": "^7.14.5", + "@babel/plugin-transform-duplicate-keys": "^7.14.5", + "@babel/plugin-transform-exponentiation-operator": "^7.14.5", + "@babel/plugin-transform-for-of": "^7.14.5", + "@babel/plugin-transform-function-name": "^7.14.5", + "@babel/plugin-transform-literals": "^7.14.5", + "@babel/plugin-transform-member-expression-literals": "^7.14.5", + "@babel/plugin-transform-modules-amd": "^7.14.5", + "@babel/plugin-transform-modules-commonjs": "^7.14.5", + "@babel/plugin-transform-modules-systemjs": "^7.14.5", + "@babel/plugin-transform-modules-umd": "^7.14.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.7", + "@babel/plugin-transform-new-target": "^7.14.5", + "@babel/plugin-transform-object-super": "^7.14.5", + "@babel/plugin-transform-parameters": "^7.14.5", + "@babel/plugin-transform-property-literals": "^7.14.5", + "@babel/plugin-transform-regenerator": "^7.14.5", + "@babel/plugin-transform-reserved-words": "^7.14.5", + "@babel/plugin-transform-shorthand-properties": "^7.14.5", + "@babel/plugin-transform-spread": "^7.14.6", + "@babel/plugin-transform-sticky-regex": "^7.14.5", + "@babel/plugin-transform-template-literals": "^7.14.5", + "@babel/plugin-transform-typeof-symbol": "^7.14.5", + "@babel/plugin-transform-unicode-escapes": "^7.14.5", + "@babel/plugin-transform-unicode-regex": "^7.14.5", + "@babel/preset-modules": "^0.1.4", + "@babel/types": "^7.14.8", + "babel-plugin-polyfill-corejs2": "^0.2.2", + "babel-plugin-polyfill-corejs3": "^0.2.2", + "babel-plugin-polyfill-regenerator": "^0.2.2", + "core-js-compat": "^3.15.0", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.6", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/regjsgen": { + "version": "0.8.0", + "dev": true + }, + "@babel/runtime": { + "version": "7.23.6", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.22.15", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.6", + "dev": true, + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.23.6", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "dev": true, + "optional": true + }, + "@turbowarp/json": { + "version": "0.1.2" + }, + "@types/babel__core": { + "version": "7.20.5", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.4", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/glob": { + "version": "7.2.0", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/json-schema": { + "version": "7.0.15" + }, + "@types/minimatch": { + "version": "5.1.2", + "dev": true + }, + "@types/node": { + "version": "20.10.5", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@vernier/godirect": { + "version": "1.5.0" + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0" + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0" + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0" + }, + "@xtuc/long": { + "version": "4.2.2" + }, + "accepts": { + "version": "1.3.8", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "6.4.2" + }, + "acorn-jsx": { + "version": "5.3.2", + "dev": true, + "requires": {} + }, + "adm-zip": { + "version": "0.4.11", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "requires": {} + }, + "ajv-keywords": { + "version": "3.5.2", + "requires": {} + }, + "ansi-colors": { + "version": "3.2.4", + "dev": true + }, + "ansi-escapes": { + "version": "3.2.0", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "dev": true + }, + "ansi-regex": { + "version": "3.0.1", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.3", + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0" + }, + "argparse": { + "version": "1.0.10", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0" + }, + "arr-flatten": { + "version": "1.1.0" + }, + "arr-union": { + "version": "3.1.0" + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-flatten": { + "version": "2.1.2", + "dev": true + }, + "array-includes": { + "version": "3.1.7", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "1.0.2", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "dev": true + }, + "array-unique": { + "version": "0.3.2" + }, + "array.prototype.flat": { + "version": "1.3.2", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.2", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.tosorted": { + "version": "1.1.2", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "arraybuffer-loader": { + "version": "1.0.8", + "requires": { + "loader-utils": "^1.1.0" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, + "asn1": { + "version": "0.2.6", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "5.4.1", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0" + } + } + }, + "assert": { + "version": "1.5.1", + "requires": { + "object.assign": "^4.1.4", + "util": "^0.10.4" + }, + "dependencies": { + "inherits": { + "version": "2.0.3" + }, + "util": { + "version": "0.10.4", + "requires": { + "inherits": "2.0.3" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0" + }, + "async": { + "version": "2.6.1", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "async-each": { + "version": "1.0.6", + "devOptional": true + }, + "async-limiter": { + "version": "1.0.1", + "dev": true + }, + "asynciterator.prototype": { + "version": "1.0.0", + "dev": true, + "optional": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "asynckit": { + "version": "0.4.0", + "dev": true + }, + "atob": { + "version": "2.1.2" + }, + "audio-context": { + "version": "1.0.1", + "dev": true, + "requires": { + "global": "^4.3.1" + } + }, + "automation-events": { + "version": "6.0.13", + "requires": { + "@babel/runtime": "^7.23.5", + "tslib": "^2.6.2" + } + }, + "available-typed-arrays": { + "version": "1.0.5", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "dev": true + }, + "aws4": { + "version": "1.12.0", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "dev": true + } + } + }, + "babel-eslint": { + "version": "10.1.0", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, + "babel-loader": { + "version": "8.2.2", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + } + }, + "babel-plugin-extract-format-message": { + "version": "6.2.4", + "dev": true, + "requires": { + "format-message-estree-util": "^6.2.4", + "format-message-generate-id": "^6.2.4", + "format-message-parse": "^6.2.4", + "format-message-print": "^6.2.4" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.2.3", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.4", + "semver": "^6.1.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.2.5", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.2", + "core-js-compat": "^3.16.2" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.2.3", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.4" + } + }, + "babel-plugin-react-intl": { + "version": "3.5.1", + "dev": true, + "requires": { + "@babel/core": "^7.4.5", + "@babel/helper-plugin-utils": "^7.0.0", + "@types/babel__core": "^7.1.2", + "fs-extra": "^8.0.1", + "intl-messageformat-parser": "^1.8.1" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "babel-plugin-transform-format-message": { + "version": "6.2.4", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/parser": "^7.0.0", + "format-message": "^6.2.4", + "format-message-estree-util": "^6.2.4", + "format-message-formats": "^6.2.4", + "format-message-generate-id": "^6.2.4", + "format-message-parse": "^6.2.4", + "lookup-closest-locale": "^6.2.0", + "source-map": "^0.5.7" + }, + "dependencies": { + "format-message": { + "version": "6.2.4", + "dev": true, + "requires": { + "format-message-formats": "^6.2.4", + "format-message-interpret": "^6.2.4", + "format-message-parse": "^6.2.4", + "lookup-closest-locale": "^6.2.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.2" + }, + "base": { + "version": "0.11.2", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "base64-js": { + "version": "0.0.8", + "dev": true + }, + "base64-loader": { + "version": "1.0.0", + "dev": true + }, + "batch": { + "version": "0.6.1", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "5.2.2" + }, + "binary-extensions": { + "version": "2.2.0", + "optional": true + }, + "bind-obj-methods": { + "version": "2.0.2", + "dev": true + }, + "bl": { + "version": "1.2.3", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "dev": true + }, + "readable-stream": { + "version": "2.3.8", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "bluebird": { + "version": "3.7.2" + }, + "bn.js": { + "version": "5.2.1" + }, + "body-parser": { + "version": "1.20.1", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "dev": true + }, + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "qs": { + "version": "6.11.0", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, + "bonjour": { + "version": "3.5.0", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "brfs": { + "version": "1.6.1", + "dev": true, + "requires": { + "quote-stream": "^1.0.1", + "resolve": "^1.1.5", + "static-module": "^2.2.0", + "through2": "^2.0.0" + } + }, + "brorand": { + "version": "1.1.0" + }, + "browser-stdout": { + "version": "1.3.0", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.2", + "requires": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.4", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.22.2", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "btoa": { + "version": "1.2.1" + }, + "buffer": { + "version": "4.9.2", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "base64-js": { + "version": "1.5.1" + }, + "isarray": { + "version": "1.0.0" + } + } + }, + "buffer-equal": { + "version": "0.0.1", + "dev": true + }, + "buffer-from": { + "version": "1.1.2" + }, + "buffer-indexof": { + "version": "1.1.1", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3" + }, + "builtin-status-codes": { + "version": "3.0.0" + }, + "bytes": { + "version": "3.0.0", + "dev": true + }, + "cacache": { + "version": "10.0.4", + "dev": true, + "requires": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.1", + "mississippi": "^2.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^5.2.4", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.5", + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "caller-path": { + "version": "0.1.0", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsite": { + "version": "1.0.0", + "dev": true + }, + "callsites": { + "version": "0.2.0", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001572", + "dev": true + }, + "cannon-es": { + "version": "0.20.0" + }, + "canvas-toBlob": { + "version": "1.0.0" + }, + "caseless": { + "version": "0.12.0", + "dev": true + }, + "catharsis": { + "version": "0.8.11", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.4.2", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "optional": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "1.1.4" + }, + "chrome-trace-event": { + "version": "1.0.3" + }, + "cipher-base": { + "version": "1.0.4", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-json": { + "version": "0.3.3", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + } + } + }, + "clean-yaml-object": { + "version": "0.1.0", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.1", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "co": { + "version": "4.6.0", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "color-support": { + "version": "1.1.3", + "dev": true + }, + "colors": { + "version": "0.6.2", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3" + }, + "commondir": { + "version": "1.0.1" + }, + "complex.js": { + "version": "2.1.1" + }, + "component-emitter": { + "version": "1.3.1" + }, + "compressible": { + "version": "2.0.18", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1" + }, + "concat-stream": { + "version": "1.6.2", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "dev": true + }, + "console-browserify": { + "version": "1.2.0" + }, + "constants-browserify": { + "version": "1.0.0" + }, + "content-disposition": { + "version": "0.5.4", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "dev": true + }, + "cookie": { + "version": "0.5.0", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1" + }, + "copy-webpack-plugin": { + "version": "4.5.4", + "dev": true, + "requires": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "globby": "^7.1.1", + "is-glob": "^4.0.0", + "loader-utils": "^1.1.0", + "minimatch": "^3.0.4", + "p-limit": "^1.0.0", + "serialize-javascript": "^1.4.0" + }, + "dependencies": { + "find-cache-dir": { + "version": "1.0.0", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "1.3.0", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "path-exists": { + "version": "3.0.0", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "core-js": { + "version": "2.3.0" + }, + "core-js-compat": { + "version": "3.35.0", + "dev": true, + "requires": { + "browserslist": "^4.22.2" + } + }, + "core-util-is": { + "version": "1.0.2" + }, + "coveralls": { + "version": "3.1.1", + "dev": true, + "requires": { + "js-yaml": "^3.13.1", + "lcov-parse": "^1.0.0", + "log-driver": "^1.2.7", + "minimist": "^1.2.5", + "request": "^2.88.2" + } + }, + "crc32": { + "version": "0.2.2", + "dev": true + }, + "create-ecdh": { + "version": "4.0.4", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0" + } + } + }, + "create-hash": { + "version": "1.2.0", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "dev": true + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "cyclist": { + "version": "1.0.2" + }, + "d": { + "version": "1.0.1", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.3.4", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "dev": true + }, + "decimal.js": { + "version": "10.4.3" + }, + "decode-html": { + "version": "2.0.0" + }, + "decode-uri-component": { + "version": "0.2.2" + }, + "deep-equal": { + "version": "1.1.2", + "dev": true, + "requires": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + } + }, + "deep-is": { + "version": "0.1.4", + "dev": true + }, + "default-gateway": { + "version": "4.2.0", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "define-data-property": { + "version": "1.1.1", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "define-properties": { + "version": "1.2.1", + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "define-property": { + "version": "2.0.2", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "del": { + "version": "4.1.1", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "globby": { + "version": "6.1.0", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "dev": true + } + } + }, + "pify": { + "version": "4.0.1", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "dev": true + }, + "depd": { + "version": "2.0.0", + "dev": true + }, + "des.js": { + "version": "1.1.0", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.2.0", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "dev": true + }, + "diff": { + "version": "1.4.0", + "dev": true + }, + "diff-match-patch": { + "version": "1.0.4" + }, + "diffie-hellman": { + "version": "5.0.3", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0" + } + } + }, + "dir-glob": { + "version": "2.2.2", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "dev": true + }, + "dns-packet": { + "version": "1.3.4", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "docdash": { + "version": "1.2.0", + "dev": true + }, + "doctrine": { + "version": "2.1.0", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-serializer": { + "version": "0.2.2", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.3.0" + }, + "entities": { + "version": "2.2.0" + } + } + }, + "dom-walk": { + "version": "0.1.2", + "dev": true + }, + "domain-browser": { + "version": "1.2.0" + }, + "domelementtype": { + "version": "1.3.1" + }, + "domhandler": { + "version": "2.4.2", + "requires": { + "domelementtype": "1" + } + }, + "dompurify": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", + "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" + }, + "domutils": { + "version": "1.7.0", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "duplexer2": { + "version": "0.1.4", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "dev": true + }, + "readable-stream": { + "version": "2.3.8", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "duplexify": { + "version": "3.7.1", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.616", + "dev": true + }, + "elliptic": { + "version": "6.5.4", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0" + } + } + }, + "emoji-regex": { + "version": "7.0.3", + "dev": true + }, + "emojis-list": { + "version": "3.0.0" + }, + "encodeurl": { + "version": "1.0.2", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.5.0", + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "memory-fs": { + "version": "0.5.0", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "entities": { + "version": "1.1.2" + }, + "errno": { + "version": "0.1.8", + "requires": { + "prr": "~1.0.1" + } + }, + "es-abstract": { + "version": "1.22.3", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + } + }, + "es-iterator-helpers": { + "version": "1.0.15", + "dev": true, + "optional": true, + "requires": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "es-set-tostringtag": { + "version": "2.0.2", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "dev": true, + "optional": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es5-ext": { + "version": "0.10.62", + "dev": true, + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-promise": { + "version": "3.0.2" + }, + "es6-set": { + "version": "0.1.6", + "dev": true, + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "es6-iterator": "~2.0.3", + "es6-symbol": "^3.1.3", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "dependencies": { + "type": { + "version": "2.7.2", + "dev": true + } + } + }, + "es6-symbol": { + "version": "3.1.3", + "dev": true, + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "escalade": { + "version": "3.1.1", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "dev": true + }, + "escape-latex": { + "version": "1.2.0" + }, + "escape-string-regexp": { + "version": "1.0.5", + "dev": true + }, + "escodegen": { + "version": "1.9.1", + "dev": true, + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "dev": true + }, + "estraverse": { + "version": "4.3.0", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "dev": true, + "optional": true + } + } + }, + "escope": { + "version": "3.6.0", + "dev": true, + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "dev": true + } + } + }, + "eslint": { + "version": "5.3.0", + "dev": true, + "requires": { + "ajv": "^6.5.0", + "babel-code-frame": "^6.26.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^4.0.0", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.2", + "imurmurhash": "^0.1.4", + "inquirer": "^5.2.0", + "is-resolvable": "^1.1.0", + "js-yaml": "^3.11.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^2.0.0", + "require-uncached": "^1.0.3", + "semver": "^5.5.0", + "string.prototype.matchall": "^2.0.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^4.0.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "semver": { + "version": "5.7.2", + "dev": true + } + } + }, + "eslint-config-scratch": { + "version": "5.1.0", + "dev": true, + "requires": { + "eslint-plugin-react": ">=7.14.2" + } + }, + "eslint-plugin-format-message": { + "version": "6.2.4", + "dev": true, + "requires": { + "format-message": "^6.2.4", + "format-message-estree-util": "^6.2.4", + "format-message-generate-id": "^6.2.4", + "format-message-parse": "^6.2.4", + "lookup-closest-locale": "^6.2.0" + }, + "dependencies": { + "format-message": { + "version": "6.2.4", + "dev": true, + "requires": { + "format-message-formats": "^6.2.4", + "format-message-interpret": "^6.2.4", + "format-message-parse": "^6.2.4", + "lookup-closest-locale": "^6.2.0" + } + } + } + }, + "eslint-plugin-react": { + "version": "7.33.2", + "dev": true, + "optional": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "dependencies": { + "resolve": { + "version": "2.0.0-next.5", + "dev": true, + "optional": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "string.prototype.matchall": { + "version": "4.0.10", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + } + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0" + } + } + }, + "eslint-utils": { + "version": "1.4.3", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "dev": true + }, + "esm": { + "version": "3.2.25", + "dev": true + }, + "espree": { + "version": "4.1.0", + "dev": true, + "requires": { + "acorn": "^6.0.2", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "dev": true + }, + "esquery": { + "version": "1.5.0", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0" + }, + "esutils": { + "version": "2.0.3", + "dev": true + }, + "etag": { + "version": "1.8.1", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "eventemitter3": { + "version": "4.0.7", + "dev": true + }, + "events": { + "version": "3.3.0" + }, + "events-to-array": { + "version": "1.1.2", + "dev": true + }, + "eventsource": { + "version": "2.0.2", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit-hook": { + "version": "1.1.1", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + }, + "is-extendable": { + "version": "0.1.1" + }, + "ms": { + "version": "2.0.0" + } + } + }, + "expose-loader": { + "version": "0.7.5", + "dev": true, + "requires": {} + }, + "express": { + "version": "4.18.2", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "dev": true + }, + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "qs": { + "version": "6.11.0", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, + "ext": { + "version": "1.7.0", + "dev": true, + "requires": { + "type": "^2.7.2" + }, + "dependencies": { + "type": { + "version": "2.7.2", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "external-editor": { + "version": "2.2.0", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1" + } + } + }, + "extsprintf": { + "version": "1.3.0", + "dev": true + }, + "falafel": { + "version": "2.2.5", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "isarray": "^2.0.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "dev": true + } + } + }, + "fast-deep-equal": { + "version": "3.1.3" + }, + "fast-json-stable-stringify": { + "version": "2.1.0" + }, + "fast-levenshtein": { + "version": "2.0.6", + "dev": true + }, + "fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "dev": true + }, + "faye-websocket": { + "version": "0.11.4", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figgy-pudding": { + "version": "3.5.2" + }, + "figures": { + "version": "2.0.0", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "file-loader": { + "version": "2.0.0", + "dev": true, + "requires": { + "loader-utils": "^1.0.2", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "filename-reserved-regex": { + "version": "1.0.0", + "dev": true + }, + "filenamify": { + "version": "1.2.1", + "dev": true, + "requires": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "filenamify-url": { + "version": "1.0.0", + "dev": true, + "requires": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "findup": { + "version": "0.1.5", + "dev": true, + "requires": { + "colors": "~0.6.0-1", + "commander": "~2.1.0" + }, + "dependencies": { + "commander": { + "version": "2.1.0", + "dev": true + } + } + }, + "flat-cache": { + "version": "1.3.4", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flush-write-stream": { + "version": "1.1.1", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "follow-redirects": { + "version": "1.15.3", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "for-in": { + "version": "1.0.2" + }, + "foreground-child": { + "version": "1.5.6", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "lru-cache": { + "version": "4.1.5", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "dev": true + } + } + }, + "forever-agent": { + "version": "0.6.1", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "format-message": { + "version": "6.2.1", + "requires": { + "format-message-formats": "^6.2.0", + "format-message-interpret": "^6.2.0", + "format-message-parse": "^6.2.0", + "lookup-closest-locale": "^6.2.0" + } + }, + "format-message-cli": { + "version": "6.2.0", + "dev": true, + "requires": { + "@babel/core": "^7.0.0", + "babel-plugin-extract-format-message": "^6.2.0", + "babel-plugin-transform-format-message": "^6.2.0", + "commander": "^2.11.0", + "eslint": "^3.19.0", + "eslint-plugin-format-message": "^6.2.0", + "glob": "^5.0.15", + "js-yaml": "^3.10.0", + "mkdirp": "^0.5.1", + "safe-buffer": "^5.1.1", + "source-map": "^0.5.7" + }, + "dependencies": { + "acorn": { + "version": "5.7.4", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "dev": true + } + } + }, + "ajv": { + "version": "4.11.8", + "dev": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "dev": true, + "requires": {} + }, + "ansi-escapes": { + "version": "1.4.0", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-cursor": { + "version": "1.0.2", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "eslint": { + "version": "3.19.0", + "dev": true, + "requires": { + "babel-code-frame": "^6.16.0", + "chalk": "^1.1.3", + "concat-stream": "^1.5.2", + "debug": "^2.1.1", + "doctrine": "^2.0.0", + "escope": "^3.6.0", + "espree": "^3.4.0", + "esquery": "^1.0.0", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "glob": "^7.0.3", + "globals": "^9.14.0", + "ignore": "^3.2.0", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.7.5", + "strip-bom": "^3.0.0", + "strip-json-comments": "~2.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "espree": { + "version": "3.5.4", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "estraverse": { + "version": "4.3.0", + "dev": true + }, + "figures": { + "version": "1.7.0", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "glob": { + "version": "5.0.15", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "9.18.0", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "onetime": { + "version": "1.1.0", + "dev": true + }, + "pluralize": { + "version": "1.2.1", + "dev": true + }, + "progress": { + "version": "1.1.8", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "run-async": { + "version": "0.1.0", + "dev": true, + "requires": { + "once": "^1.3.0" + } + }, + "slice-ansi": { + "version": "0.0.4", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "dev": true + }, + "table": { + "version": "3.8.3", + "dev": true, + "requires": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + } + } + } + }, + "format-message-estree-util": { + "version": "6.2.4", + "dev": true + }, + "format-message-formats": { + "version": "6.2.4" + }, + "format-message-generate-id": { + "version": "6.2.4", + "dev": true, + "requires": { + "crc32": "^0.2.2", + "format-message-parse": "^6.2.4", + "format-message-print": "^6.2.4" + } + }, + "format-message-interpret": { + "version": "6.2.4", + "requires": { + "format-message-formats": "^6.2.4", + "lookup-closest-locale": "^6.2.0" + } + }, + "format-message-parse": { + "version": "6.2.4" + }, + "format-message-print": { + "version": "6.2.4", + "dev": true + }, + "forwarded": { + "version": "0.2.0", + "dev": true + }, + "fraction.js": { + "version": "4.3.4" + }, + "fragment-cache": { + "version": "0.2.1", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "dev": true + }, + "from2": { + "version": "2.3.0", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "from2-array": { + "version": "0.0.4", + "dev": true, + "requires": { + "from2": "^2.0.3" + } + }, + "fs-exists-cached": { + "version": "1.0.0", + "dev": true + }, + "fs-extra": { + "version": "5.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "dev": true + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0" + }, + "function-bind": { + "version": "1.1.2" + }, + "function-loop": { + "version": "1.0.2", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.6", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "dev": true + }, + "generate-function": { + "version": "2.3.1", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.2", + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "dev": true, + "requires": { + "pump": "^3.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "get-symbol-description": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "get-value": { + "version": "2.0.6" + }, + "getpass": { + "version": "0.1.7", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "gh-pages": { + "version": "1.2.0", + "dev": true, + "requires": { + "async": "2.6.1", + "commander": "2.15.1", + "filenamify-url": "^1.0.0", + "fs-extra": "^5.0.0", + "globby": "^6.1.0", + "graceful-fs": "4.1.11", + "rimraf": "^2.6.2" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "dev": true + }, + "globby": { + "version": "6.1.0", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "dev": true + }, + "pify": { + "version": "2.3.0", + "dev": true + } + } + }, + "glob": { + "version": "7.2.3", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global": { + "version": "4.4.0", + "dev": true, + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "global-modules-path": { + "version": "2.3.1", + "dev": true + }, + "globals": { + "version": "11.12.0", + "dev": true + }, + "globalthis": { + "version": "1.0.3", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "7.1.1", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "dependencies": { + "ignore": { + "version": "3.3.10", + "dev": true + } + } + }, + "gopd": { + "version": "1.0.1", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11" + }, + "grapheme-breaker": { + "version": "0.3.2", + "dev": true, + "requires": { + "brfs": "^1.2.0", + "unicode-trie": "^0.3.1" + } + }, + "growl": { + "version": "1.10.3", + "dev": true + }, + "handle-thing": { + "version": "2.0.1", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.4", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "dev": true + } + } + }, + "has-bigints": { + "version": "1.0.2", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.1", + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1" + }, + "has-symbols": { + "version": "1.0.3" + }, + "has-tostringtag": { + "version": "1.0.0", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "has-value": { + "version": "1.0.0", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasown": { + "version": "2.0.0", + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.1.1", + "dev": true + }, + "heap": { + "version": "0.2.5" + }, + "hmac-drbg": { + "version": "1.0.1", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "dev": true + }, + "readable-stream": { + "version": "2.3.8", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-entities": { + "version": "1.4.0", + "dev": true + }, + "htmlparser2": { + "version": "3.10.1", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "http-deceiver": { + "version": "1.2.7", + "dev": true + }, + "http-errors": { + "version": "2.0.0", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.8", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + } + }, + "http-signature": { + "version": "1.2.0", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0" + }, + "hull.js": { + "version": "0.2.10", + "dev": true + }, + "humanize-url": { + "version": "1.0.1", + "dev": true, + "requires": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1" + }, + "iferr": { + "version": "0.1.5" + }, + "ify-loader": { + "version": "1.0.4", + "dev": true, + "requires": { + "bl": "^1.0.0", + "findup": "^0.1.5", + "from2-array": "0.0.4", + "map-limit": "0.0.1", + "multipipe": "^0.3.0", + "read-package-json": "^2.0.2", + "resolve": "^1.1.6" + } + }, + "ignore": { + "version": "4.0.6", + "dev": true + }, + "immediate": { + "version": "3.0.6" + }, + "immutable": { + "version": "3.8.2" + }, + "import-local": { + "version": "1.0.0", + "dev": true, + "requires": { + "pkg-dir": "^2.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "path-exists": { + "version": "3.0.0", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4" + }, + "in-publish": { + "version": "2.0.1", + "dev": true + }, + "infer-owner": { + "version": "1.0.4" + }, + "inflight": { + "version": "1.0.6", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4" + }, + "inquirer": { + "version": "5.2.0", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "internal-ip": { + "version": "4.3.0", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "internal-slot": { + "version": "1.0.6", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "interpret": { + "version": "1.4.0", + "dev": true + }, + "intl-messageformat-parser": { + "version": "1.8.1", + "dev": true + }, + "invert-kv": { + "version": "2.0.0", + "dev": true + }, + "ip": { + "version": "1.1.8", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "dev": true + }, + "is-absolute-url": { + "version": "3.0.3", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.1", + "requires": { + "hasown": "^2.0.0" + } + }, + "is-arguments": { + "version": "1.1.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-async-function": { + "version": "2.0.0", + "dev": true, + "optional": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6" + }, + "is-callable": { + "version": "1.2.7", + "dev": true + }, + "is-core-module": { + "version": "2.13.1", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.1", + "requires": { + "hasown": "^2.0.0" + } + }, + "is-date-object": { + "version": "1.0.5", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-descriptor": { + "version": "1.0.3", + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-extglob": { + "version": "2.1.1", + "devOptional": true + }, + "is-finalizationregistry": { + "version": "1.0.2", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "dev": true, + "optional": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "devOptional": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.2", + "dev": true, + "optional": true + }, + "is-my-ip-valid": { + "version": "1.0.1", + "dev": true + }, + "is-my-json-valid": { + "version": "2.20.6", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^5.0.0", + "xtend": "^4.0.0" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "optional": true + }, + "is-number-object": { + "version": "1.0.7", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-cwd": { + "version": "2.2.0", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-property": { + "version": "1.0.2", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-resolvable": { + "version": "1.1.0", + "dev": true + }, + "is-set": { + "version": "2.0.2", + "dev": true, + "optional": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "1.1.0", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-typedarray": { + "version": "1.0.0", + "dev": true + }, + "is-weakmap": { + "version": "2.0.1", + "dev": true, + "optional": true + }, + "is-weakref": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.2", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "is-windows": { + "version": "1.0.2" + }, + "is-wsl": { + "version": "1.1.0" + }, + "isarray": { + "version": "2.0.5", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "isobject": { + "version": "3.0.1" + }, + "isstream": { + "version": "0.1.2", + "dev": true + }, + "iterator.prototype": { + "version": "1.1.2", + "dev": true, + "optional": true, + "requires": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "javascript-natural-sort": { + "version": "0.7.1" + }, + "js-md5": { + "version": "0.7.3" + }, + "js-tokens": { + "version": "4.0.0", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "js2xmlparser": { + "version": "4.0.2", + "dev": true, + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsbn": { + "version": "0.1.1", + "dev": true + }, + "jsdoc": { + "version": "3.6.6", + "dev": true, + "requires": { + "@babel/parser": "^7.9.4", + "bluebird": "^3.7.2", + "catharsis": "^0.8.11", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.1", + "klaw": "^3.0.0", + "markdown-it": "^10.0.0", + "markdown-it-anchor": "^5.2.7", + "marked": "^0.8.2", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "taffydb": "2.6.2", + "underscore": "~1.10.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "dev": true + } + } + }, + "jsesc": { + "version": "2.5.2", + "dev": true + }, + "json": { + "version": "9.0.6", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2" + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true + }, + "json-schema": { + "version": "0.4.0", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1" + }, + "json-stable-stringify": { + "version": "1.1.0", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "dev": true + }, + "json5": { + "version": "2.2.3", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.1", + "dev": true + }, + "jsonpointer": { + "version": "5.0.1", + "dev": true + }, + "jsprim": { + "version": "1.4.2", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "jsx-ast-utils": { + "version": "3.3.5", + "dev": true, + "optional": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, + "jszip": { + "version": "3.10.1", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "killable": { + "version": "1.0.1", + "dev": true + }, + "kind-of": { + "version": "6.0.3" + }, + "klaw": { + "version": "3.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "lcid": { + "version": "2.0.0", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "lcov-parse": { + "version": "1.0.0", + "dev": true + }, + "levn": { + "version": "0.3.0", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lie": { + "version": "3.3.0", + "requires": { + "immediate": "~3.0.5" + } + }, + "linebreak": { + "version": "0.3.0", + "dev": true, + "requires": { + "base64-js": "0.0.8", + "brfs": "^1.3.0", + "unicode-trie": "^0.3.0" + } + }, + "linkify-it": { + "version": "2.2.0", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "loader-runner": { + "version": "2.4.0" + }, + "loader-utils": { + "version": "1.4.2", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "locate-path": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "dev": true + }, + "lodash.defaultsdeep": { + "version": "4.6.1", + "dev": true + }, + "log-driver": { + "version": "1.2.7", + "dev": true + }, + "loglevel": { + "version": "1.8.1", + "dev": true + }, + "lookup-closest-locale": { + "version": "6.2.0" + }, + "loose-envify": { + "version": "1.4.0", + "dev": true, + "optional": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "requires": { + "yallist": "^3.0.2" + } + }, + "lz-string": { + "version": "1.5.0" + }, + "magic-string": { + "version": "0.22.5", + "dev": true, + "requires": { + "vlq": "^0.2.2" + } + }, + "make-dir": { + "version": "3.1.0", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2" + }, + "map-limit": { + "version": "0.0.1", + "dev": true, + "requires": { + "once": "~1.3.0" + }, + "dependencies": { + "once": { + "version": "1.3.3", + "dev": true, + "requires": { + "wrappy": "1" + } + } + } + }, + "map-visit": { + "version": "1.0.0", + "requires": { + "object-visit": "^1.0.0" + } + }, + "markdown-it": { + "version": "10.0.0", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "entities": "~2.0.0", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "entities": { + "version": "2.0.3", + "dev": true + } + } + }, + "markdown-it-anchor": { + "version": "5.3.0", + "dev": true, + "requires": {} + }, + "marked": { + "version": "0.8.2", + "dev": true + }, + "mathjs": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.11.1.tgz", + "integrity": "sha512-uWrwMrhU31TCqHKmm1yFz0C352njGUVr/I1UnpMOxI/VBTTbCktx/mREUXx5Vyg11xrFdg/F3wnMM7Ql/csVsQ==", + "requires": { + "@babel/runtime": "^7.22.15", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "4.3.4", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.1" + } + }, + "matter-js": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.20.0.tgz", + "integrity": "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA==" + }, + "md5.js": { + "version": "1.3.5", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdurl": { + "version": "1.0.1", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "dev": true + }, + "mem": { + "version": "4.3.0", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "dev": true + } + } + }, + "memory-fs": { + "version": "0.4.1", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "dev": true + }, + "merge-source-map": { + "version": "1.0.4", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "mersenne-twister": { + "version": "1.1.0" + }, + "methods": { + "version": "1.1.2", + "dev": true + }, + "microee": { + "version": "0.0.6" + }, + "micromatch": { + "version": "3.1.10", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-extendable": { + "version": "0.1.1" + }, + "is-number": { + "version": "3.0.0", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "miller-rabin": { + "version": "4.0.1", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0" + } + } + }, + "mime": { + "version": "1.6.0", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "dev": true + }, + "min-document": { + "version": "2.19.0", + "dev": true, + "requires": { + "dom-walk": "^0.1.0" + } + }, + "minilog": { + "version": "3.1.0", + "requires": { + "microee": "0.0.6" + } + }, + "minimalistic-assert": { + "version": "1.0.1" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1" + }, + "minimatch": { + "version": "3.1.2", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8" + }, + "minipass": { + "version": "2.9.0", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "mississippi": { + "version": "2.0.0", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^2.0.1", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + } + }, + "mkdirp": { + "version": "0.5.6", + "requires": { + "minimist": "^1.2.6" + } + }, + "mkpath": { + "version": "1.0.0", + "dev": true + }, + "mocha": { + "version": "4.1.0", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "commander": { + "version": "2.11.0", + "dev": true + }, + "debug": { + "version": "3.1.0", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.3.1", + "dev": true + }, + "glob": { + "version": "7.1.2", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "2.0.0", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "supports-color": { + "version": "4.4.0", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + } + } + }, + "move-concurrently": { + "version": "1.0.1", + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.1.2", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "dev": true + }, + "multipipe": { + "version": "0.3.1", + "dev": true, + "requires": { + "duplexer2": "^0.1.2" + } + }, + "mute-stream": { + "version": "0.0.7", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "dev": true + }, + "neo-async": { + "version": "2.6.2" + }, + "next-tick": { + "version": "1.1.0", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "dev": true + }, + "node-forge": { + "version": "0.10.0", + "dev": true + }, + "node-libs-browser": { + "version": "2.2.1", + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "punycode": { + "version": "1.4.1" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "node-releases": { + "version": "2.0.14", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "devOptional": true + }, + "normalize-url": { + "version": "1.9.1", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "dev": true + }, + "nyc": { + "version": "11.9.0", + "dev": true, + "requires": { + "archy": "^1.0.0", + "arrify": "^1.0.1", + "caching-transform": "^1.0.0", + "convert-source-map": "^1.5.1", + "debug-log": "^1.0.1", + "default-require-extensions": "^1.0.0", + "find-cache-dir": "^0.1.1", + "find-up": "^2.1.0", + "foreground-child": "^1.5.3", + "glob": "^7.0.6", + "istanbul-lib-coverage": "^1.1.2", + "istanbul-lib-hook": "^1.1.0", + "istanbul-lib-instrument": "^1.10.0", + "istanbul-lib-report": "^1.1.3", + "istanbul-lib-source-maps": "^1.2.3", + "istanbul-reports": "^1.4.0", + "md5-hex": "^1.2.0", + "merge-source-map": "^1.1.0", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.0", + "resolve-from": "^2.0.0", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.1", + "spawn-wrap": "^1.4.2", + "test-exclude": "^4.2.0", + "yargs": "11.1.0", + "yargs-parser": "^8.0.0" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true, + "dev": true + }, + "append-transform": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "requires": { + "default-require-extensions": "^1.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true, + "dev": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "async": { + "version": "1.5.2", + "bundled": true, + "dev": true + }, + "atob": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-generator": { + "version": "6.26.1", + "bundled": true, + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "bundled": true, + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "base": { + "version": "0.11.2", + "bundled": true, + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "caching-transform": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "md5-hex": "^1.2.0", + "mkdirp": "^0.5.1", + "write-file-atomic": "^1.1.4" + } + }, + "camelcase": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "class-utils": { + "version": "0.3.6", + "bundled": true, + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "cliui": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "commondir": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "bundled": true, + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "core-js": { + "version": "2.5.6", + "bundled": true, + "dev": true + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-log": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "bundled": true, + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "strip-bom": "^2.0.0" + } + }, + "define-property": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "detect-indent": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "error-ex": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true, + "dev": true + }, + "execa": { + "version": "0.7.0", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "fill-range": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-cache-dir": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "requires": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + } + }, + "fragment-cache": { + "version": "0.2.1", + "bundled": true, + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "get-caller-file": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "get-value": { + "version": "2.0.6", + "bundled": true, + "dev": true + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "9.18.0", + "bundled": true, + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "bundled": true, + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "bundled": true, + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "has-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hosted-git-info": { + "version": "2.6.0", + "bundled": true, + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "invariant": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "bundled": true, + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-odd": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "bundled": true, + "dev": true + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "is-stream": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "append-transform": "^0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "bundled": true, + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.3", + "bundled": true, + "dev": true, + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "handlebars": "^4.0.3" + } + }, + "js-tokens": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "jsesc": { + "version": "1.3.0", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "bundled": true, + "dev": true + } + } + }, + "lodash": { + "version": "4.17.10", + "bundled": true, + "dev": true + }, + "longest": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "loose-envify": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "js-tokens": "^3.0.0" + } + }, + "lru-cache": { + "version": "4.1.3", + "bundled": true, + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "bundled": true, + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5-hex": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "md5-o-matic": "^0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "mem": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "merge-source-map": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true + } + } + }, + "micromatch": { + "version": "3.1.10", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "mimic-fn": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "nanomatch": { + "version": "1.2.9", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "object.pick": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "path-type": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "bundled": true, + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true, + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + } + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "bundled": true, + "dev": true + }, + "regex-not": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "repeat-element": { + "version": "1.1.2", + "bundled": true, + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "bundled": true, + "dev": true + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "ret": { + "version": "0.1.15", + "bundled": true, + "dev": true + }, + "right-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-regex": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "set-value": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "slide": { + "version": "1.1.6", + "bundled": true, + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "source-map": { + "version": "0.5.7", + "bundled": true, + "dev": true + }, + "source-map-resolve": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "atob": "^2.0.0", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "bundled": true, + "dev": true + }, + "spawn-wrap": { + "version": "1.4.2", + "bundled": true, + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "spdx-correct": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "bundled": true, + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "split-string": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "static-extend": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "test-exclude": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "requires": { + "arrify": "^1.0.1", + "micromatch": "^3.1.8", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true, + "dev": true + }, + "braces": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "to-fast-properties": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "trim-right": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "union-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "bundled": true, + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "bundled": true, + "dev": true + }, + "use": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "window-size": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true, + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "y18n": { + "version": "3.2.1", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "yargs": { + "version": "11.1.0", + "bundled": true, + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "bundled": true, + "dev": true + }, + "cliui": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "yargs-parser": { + "version": "9.0.2", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "8.1.0", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "bundled": true, + "dev": true + } + } + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + }, + "kind-of": { + "version": "3.2.2", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.13.1" + }, + "object-is": { + "version": "1.1.5", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1" + }, + "object-visit": { + "version": "1.0.1", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.5", + "requires": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.7", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.fromentries": { + "version": "2.0.7", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.hasown": { + "version": "1.1.3", + "dev": true, + "optional": true, + "requires": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.pick": { + "version": "1.3.0", + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.7", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "obuf": { + "version": "1.1.2", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "dev": true + }, + "once": { + "version": "1.4.0", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "opener": { + "version": "1.5.2", + "dev": true + }, + "opn": { + "version": "5.5.0", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-browserify": { + "version": "0.3.0" + }, + "os-homedir": { + "version": "1.0.2", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "dev": true + }, + "own-or": { + "version": "1.0.0", + "dev": true + }, + "own-or-env": { + "version": "1.0.2", + "dev": true, + "requires": { + "own-or": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "dev": true + } + } + }, + "p-map": { + "version": "2.1.0", + "dev": true + }, + "p-retry": { + "version": "3.0.1", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "1.0.0", + "dev": true + }, + "pako": { + "version": "1.0.11" + }, + "parallel-transform": { + "version": "1.2.0", + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "parse-asn1": { + "version": "5.1.6", + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "dev": true + }, + "pascalcase": { + "version": "0.1.1" + }, + "path-browserify": { + "version": "0.0.1" + }, + "path-dirname": { + "version": "1.0.2", + "devOptional": true + }, + "path-exists": { + "version": "4.0.0", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1" + }, + "path-is-inside": { + "version": "1.0.2", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pathfinding": { + "version": "0.4.18", + "requires": { + "heap": "0.2.5" + } + }, + "pbkdf2": { + "version": "3.1.2", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "optional": true + }, + "pify": { + "version": "3.0.0", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "pluralize": { + "version": "7.0.0", + "dev": true + }, + "pngjs": { + "version": "3.3.3", + "dev": true + }, + "portfinder": { + "version": "1.0.32", + "dev": true, + "requires": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "dependencies": { + "async": { + "version": "2.6.4", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1" + }, + "prelude-ls": { + "version": "1.1.2", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "dev": true + }, + "process": { + "version": "0.11.10" + }, + "process-nextick-args": { + "version": "2.0.1" + }, + "progress": { + "version": "2.0.3", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1" + }, + "prop-types": { + "version": "15.8.1", + "dev": true, + "optional": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "proxy-addr": { + "version": "2.0.7", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "prr": { + "version": "1.0.1" + }, + "pseudomap": { + "version": "1.0.2", + "dev": true + }, + "psl": { + "version": "1.9.0", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0" + } + } + }, + "pump": { + "version": "2.0.1", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "punycode": { + "version": "2.3.1" + }, + "qs": { + "version": "6.5.3", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring-es3": { + "version": "0.2.1" + }, + "querystringify": { + "version": "2.2.0", + "dev": true + }, + "quote-stream": { + "version": "1.0.2", + "dev": true, + "requires": { + "buffer-equal": "0.0.1", + "minimist": "^1.1.3", + "through2": "^2.0.0" + } + }, + "randombytes": { + "version": "2.1.0", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "dev": true + }, + "raw-body": { + "version": "2.5.1", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "dev": true + } + } + }, + "raw-loader": { + "version": "0.5.1", + "dev": true + }, + "react-is": { + "version": "16.13.1", + "dev": true, + "optional": true + }, + "read-package-json": { + "version": "2.1.2", + "dev": true, + "requires": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "readline2": { + "version": "1.0.1", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "mute-stream": { + "version": "0.0.5", + "dev": true + } + } + }, + "rechoir": { + "version": "0.6.2", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "reflect.getprototypeof": { + "version": "1.0.4", + "dev": true, + "optional": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + } + }, + "regenerate": { + "version": "1.4.2", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.1", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.14.1" + }, + "regenerator-transform": { + "version": "0.15.2", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-not": { + "version": "1.0.2", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.5.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + } + }, + "regexpp": { + "version": "2.0.1", + "dev": true + }, + "regexpu-core": { + "version": "5.3.2", + "dev": true, + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsparser": { + "version": "0.9.1", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "devOptional": true + }, + "repeat-element": { + "version": "1.1.4" + }, + "repeat-string": { + "version": "1.6.1" + }, + "request": { + "version": "2.88.2", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-directory": { + "version": "2.1.1", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "requires-port": { + "version": "1.0.0", + "dev": true + }, + "requizzle": { + "version": "0.2.4", + "dev": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "resolve": { + "version": "1.22.8", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "dev": true + } + } + }, + "resolve-from": { + "version": "1.0.1", + "dev": true + }, + "resolve-url": { + "version": "0.2.1" + }, + "restore-cursor": { + "version": "2.0.0", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15" + }, + "retry": { + "version": "0.12.0", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "2.4.1", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "requires": { + "aproba": "^1.1.1" + } + }, + "rx-lite": { + "version": "3.1.2", + "dev": true + }, + "rxjs": { + "version": "5.5.12", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + } + }, + "safe-array-concat": { + "version": "1.0.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, + "safe-buffer": { + "version": "5.2.1" + }, + "safe-regex": { + "version": "1.1.0", + "requires": { + "ret": "~0.1.10" + } + }, + "safe-regex-test": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2" + }, + "schema-utils": { + "version": "2.7.1", + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "scratch-audio": { + "version": "0.1.0-prerelease.20200528195344", + "dev": true, + "requires": { + "audio-context": "1.0.1", + "minilog": "^3.0.1", + "startaudiocontext": "1.2.1" + } + }, + "scratch-blocks": { + "version": "git+ssh://git@github.com/PenguinMod/PenguinMod-Blocks.git#db277700d4d9a9d00c5596015fb8f8810a4cca19", + "dev": true, + "from": "scratch-blocks@git+https://github.com/PenguinMod/PenguinMod-Blocks.git#develop-builds" + }, + "scratch-l10n": { + "version": "3.14.20220526031602", + "dev": true, + "requires": { + "@babel/cli": "^7.1.2", + "@babel/core": "^7.1.2", + "babel-plugin-react-intl": "^3.0.1", + "transifex": "1.6.6" + } + }, + "scratch-parser": { + "version": "git+ssh://git@github.com/PenguinMod/PenguinMod-Parser.git#c56c7aad93f71aa5d1a126bae1d5e663c161e8eb", + "from": "scratch-parser@git+https://github.com/PenguinMod/PenguinMod-Parser.git#master", + "requires": { + "@turbowarp/json": "^0.1.1", + "ajv": "6.3.0", + "jszip": "3.1.5", + "pify": "4.0.1" + }, + "dependencies": { + "ajv": { + "version": "6.3.0", + "requires": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0" + }, + "isarray": { + "version": "1.0.0" + }, + "json-schema-traverse": { + "version": "0.3.1" + }, + "jszip": { + "version": "3.1.5", + "requires": { + "core-js": "~2.3.0", + "es6-promise": "~3.0.2", + "lie": "~3.1.0", + "pako": "~1.0.2", + "readable-stream": "~2.0.6" + } + }, + "lie": { + "version": "3.1.1", + "requires": { + "immediate": "~3.0.5" + } + }, + "pify": { + "version": "4.0.1" + }, + "process-nextick-args": { + "version": "1.0.7" + }, + "readable-stream": { + "version": "2.0.6", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31" + } + } + }, + "scratch-render": { + "version": "0.1.0-prerelease.20211028200436", + "dev": true, + "requires": { + "grapheme-breaker": "0.3.2", + "hull.js": "0.2.10", + "ify-loader": "1.0.4", + "linebreak": "0.3.0", + "minilog": "3.1.0", + "raw-loader": "^0.5.1", + "scratch-storage": "^1.0.0", + "scratch-svg-renderer": "0.2.0-prerelease.20210727023023", + "twgl.js": "4.4.0" + }, + "dependencies": { + "base64-js": { + "version": "1.3.0", + "dev": true + }, + "schema-utils": { + "version": "0.4.7", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + }, + "scratch-storage": { + "version": "1.3.6", + "dev": true, + "requires": { + "arraybuffer-loader": "^1.0.3", + "base64-js": "1.3.0", + "fastestsmallesttextencoderdecoder": "^1.0.7", + "js-md5": "0.7.3", + "minilog": "3.1.0", + "worker-loader": "^2.0.0" + } + }, + "worker-loader": { + "version": "2.0.0", + "dev": true, + "requires": { + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" + } + } + } + }, + "scratch-render-fonts": { + "version": "git+ssh://git@github.com/PenguinMod/penguinmod-render-fonts.git#fbca3cc01bd32e73d1c3e42cd5c2ee5f60a9a7c5", + "dev": true, + "from": "scratch-render-fonts@github:PenguinMod/penguinmod-render-fonts#master", + "requires": { + "base64-loader": "1.0.0" + } + }, + "scratch-sb1-converter": { + "version": "0.2.7", + "requires": { + "js-md5": "0.7.3", + "minilog": "3.1.0", + "text-encoding": "^0.7.0" + } + }, + "scratch-storage": { + "version": "git+ssh://git@github.com/PenguinMod/PenguinMod-Storage.git#96f45f701dc11648bc88fcc5307193d591afea84", + "dev": true, + "from": "scratch-storage@git+https://github.com/PenguinMod/PenguinMod-Storage.git#develop", + "requires": { + "arraybuffer-loader": "^1.0.3", + "base64-js": "1.3.0", + "fastestsmallesttextencoderdecoder": "^1.0.7", + "js-md5": "0.7.3", + "minilog": "3.1.0", + "worker-loader": "^2.0.0" + }, + "dependencies": { + "base64-js": { + "version": "1.3.0", + "dev": true + }, + "schema-utils": { + "version": "0.4.7", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + }, + "worker-loader": { + "version": "2.0.0", + "dev": true, + "requires": { + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" + } + } + } + }, + "scratch-svg-renderer": { + "version": "0.2.0-prerelease.20210727023023", + "dev": true, + "requires": { + "base64-js": "1.2.1", + "base64-loader": "1.0.0", + "dompurify": "2.2.7", + "minilog": "3.1.0", + "transformation-matrix": "1.15.0" + }, + "dependencies": { + "base64-js": { + "version": "1.2.1", + "dev": true + }, + "dompurify": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.7.tgz", + "integrity": "sha512-jdtDffdGNY+C76jvodNTu9jt5yYj59vuTUyx+wXdzcSwAGTYZDAQkQ7Iwx9zcGrA4ixC1syU4H3RZROqRxokxg==", + "dev": true + } + } + }, + "scratch-translate-extension-languages": { + "version": "0.0.20191118205314" + }, + "script-loader": { + "version": "0.7.2", + "dev": true, + "requires": { + "raw-loader": "~0.5.1" + } + }, + "seedrandom": { + "version": "3.0.5" + }, + "select-hose": { + "version": "2.0.0", + "dev": true + }, + "selfsigned": { + "version": "1.10.14", + "dev": true, + "requires": { + "node-forge": "^0.10.0" + } + }, + "semver": { + "version": "6.3.1", + "dev": true + }, + "send": { + "version": "0.18.0", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "dev": true + } + } + }, + "ms": { + "version": "2.1.3", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "1.9.1", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "dev": true + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "dev": true + } + } + }, + "serve-static": { + "version": "1.15.0", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "dev": true + }, + "set-function-length": { + "version": "1.1.1", + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "set-function-name": { + "version": "2.0.1", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, + "set-value": { + "version": "2.0.1", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1" + } + } + }, + "setimmediate": { + "version": "1.0.5" + }, + "setprototypeof": { + "version": "1.2.0", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-copy": { + "version": "0.0.1", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "dev": true + }, + "shelljs": { + "version": "0.7.8", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "should": { + "version": "13.2.3", + "dev": true, + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "dev": true, + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.1", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "dev": true + }, + "simplex-noise": { + "version": "4.0.1" + }, + "slash": { + "version": "1.0.0", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + }, + "is-extendable": { + "version": "0.1.1" + }, + "ms": { + "version": "2.0.0" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.24", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "dev": true + } + } + }, + "sockjs-client": { + "version": "1.6.1", + "dev": true, + "requires": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1" + }, + "source-map": { + "version": "0.5.7" + }, + "source-map-resolve": { + "version": "0.5.3", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.21", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1" + } + } + }, + "source-map-url": { + "version": "0.4.1" + }, + "spdx-correct": { + "version": "3.2.0", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.16", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "split-string": { + "version": "3.1.0", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "dev": true + }, + "sshpk": { + "version": "1.18.0", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "5.3.0", + "dev": true, + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "stack-utils": { + "version": "1.0.5", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "dev": true + } + } + }, + "standardized-audio-context": { + "version": "25.3.61", + "requires": { + "@babel/runtime": "^7.23.6", + "automation-events": "^6.0.13", + "tslib": "^2.6.2" + } + }, + "startaudiocontext": { + "version": "1.2.1", + "dev": true + }, + "static-eval": { + "version": "2.1.0", + "dev": true, + "requires": { + "escodegen": "^1.11.1" + }, + "dependencies": { + "escodegen": { + "version": "1.14.3", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "estraverse": { + "version": "4.3.0", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "dev": true, + "optional": true + } + } + }, + "static-extend": { + "version": "0.1.2", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + } + } + }, + "static-module": { + "version": "2.2.5", + "dev": true, + "requires": { + "concat-stream": "~1.6.0", + "convert-source-map": "^1.5.1", + "duplexer2": "~0.1.4", + "escodegen": "~1.9.0", + "falafel": "^2.1.0", + "has": "^1.0.1", + "magic-string": "^0.22.4", + "merge-source-map": "1.0.4", + "object-inspect": "~1.4.0", + "quote-stream": "~1.0.2", + "readable-stream": "~2.3.3", + "shallow-copy": "~0.0.1", + "static-eval": "^2.0.0", + "through2": "~2.0.3" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "dev": true + }, + "object-inspect": { + "version": "1.4.1", + "dev": true + }, + "readable-stream": { + "version": "2.3.8", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stats.js": { + "version": "0.17.0", + "dev": true + }, + "statuses": { + "version": "2.0.1", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-each": { + "version": "1.2.3", + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.1" + }, + "strict-uri-encode": { + "version": "1.1.0", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "2.1.1", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string.prototype.matchall": { + "version": "2.0.0", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.10.0", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "regexp.prototype.flags": "^1.2.0" + } + }, + "string.prototype.trim": { + "version": "1.2.8", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.7", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.7", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "dev": true + }, + "strip-outer": { + "version": "1.0.1", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "strip-url-auth": { + "version": "1.0.1", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true + }, + "symbol-observable": { + "version": "1.0.1", + "dev": true + }, + "table": { + "version": "4.0.3", + "dev": true, + "requires": { + "ajv": "^6.0.1", + "ajv-keywords": "^3.0.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "taffydb": { + "version": "2.6.2", + "dev": true + }, + "tap": { + "version": "12.0.1", + "dev": true, + "requires": { + "bind-obj-methods": "^2.0.0", + "bluebird": "^3.5.1", + "clean-yaml-object": "^0.1.0", + "color-support": "^1.1.0", + "coveralls": "^3.0.1", + "foreground-child": "^1.3.3", + "fs-exists-cached": "^1.0.0", + "function-loop": "^1.0.1", + "glob": "^7.0.0", + "isexe": "^2.0.0", + "js-yaml": "^3.11.0", + "minipass": "^2.3.0", + "mkdirp": "^0.5.1", + "nyc": "^11.8.0", + "opener": "^1.4.1", + "os-homedir": "^1.0.2", + "own-or": "^1.0.0", + "own-or-env": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.0", + "source-map-support": "^0.5.6", + "stack-utils": "^1.0.0", + "tap-mocha-reporter": "^3.0.7", + "tap-parser": "^7.0.0", + "tmatch": "^4.0.0", + "trivial-deferred": "^1.0.1", + "tsame": "^2.0.0", + "write-file-atomic": "^2.3.0", + "yapool": "^1.0.0" + } + }, + "tap-mocha-reporter": { + "version": "3.0.9", + "dev": true, + "requires": { + "color-support": "^1.1.0", + "debug": "^2.1.3", + "diff": "^1.3.2", + "escape-string-regexp": "^1.0.3", + "glob": "^7.0.5", + "js-yaml": "^3.3.1", + "readable-stream": "^2.1.5", + "tap-parser": "^5.1.0", + "unicode-length": "^1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "dev": true, + "optional": true + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "readable-stream": { + "version": "2.3.8", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "dev": true, + "optional": true + }, + "string_decoder": { + "version": "1.1.1", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "tap-parser": { + "version": "5.4.0", + "dev": true, + "requires": { + "events-to-array": "^1.0.1", + "js-yaml": "^3.2.7", + "readable-stream": "^2" + } + } + } + }, + "tap-parser": { + "version": "7.0.0", + "dev": true, + "requires": { + "events-to-array": "^1.0.1", + "js-yaml": "^3.2.7", + "minipass": "^2.2.0" + } + }, + "tapable": { + "version": "1.1.3" + }, + "terser": { + "version": "4.8.1", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1" + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.5", + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "cacache": { + "version": "12.0.4", + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "find-cache-dir": { + "version": "2.1.0", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "mississippi": { + "version": "3.0.0", + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0" + }, + "path-exists": { + "version": "3.0.0" + }, + "pify": { + "version": "4.0.1" + }, + "pkg-dir": { + "version": "3.0.0", + "requires": { + "find-up": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "5.7.2" + }, + "serialize-javascript": { + "version": "4.0.0", + "requires": { + "randombytes": "^2.1.0" + } + }, + "source-map": { + "version": "0.6.1" + }, + "ssri": { + "version": "6.0.2", + "requires": { + "figgy-pudding": "^3.5.1" + } + } + } + }, + "text-encoding": { + "version": "0.7.0" + }, + "text-table": { + "version": "0.2.0", + "dev": true + }, + "three": { + "version": "0.153.0" + }, + "three-mesh-bvh": { + "version": "0.6.0", + "requires": {} + }, + "through": { + "version": "2.3.8", + "dev": true + }, + "through2": { + "version": "2.0.5", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "thunky": { + "version": "1.1.0", + "dev": true + }, + "timers-browserify": { + "version": "2.0.12", + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tiny-emitter": { + "version": "2.1.0" + }, + "tiny-inflate": { + "version": "1.0.3", + "dev": true + }, + "tiny-worker": { + "version": "2.3.0", + "dev": true, + "requires": { + "esm": "^3.2.25" + } + }, + "tmatch": { + "version": "4.0.0", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-arraybuffer": { + "version": "1.0.1" + }, + "to-fast-properties": { + "version": "2.0.0", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "dev": true + }, + "tone": { + "version": "14.7.77", + "requires": { + "standardized-audio-context": "^25.1.8", + "tslib": "^2.0.1" + } + }, + "tough-cookie": { + "version": "2.5.0", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "transformation-matrix": { + "version": "1.15.0", + "dev": true + }, + "transifex": { + "version": "1.6.6", + "dev": true, + "requires": { + "commander": "^2.9.0", + "lodash": "^4.17.1", + "mkpath": "^1.0.0", + "mocha": "^4.0.0", + "request": "^2.34.0", + "should": "^13.0.0" + } + }, + "trim-repeated": { + "version": "1.0.0", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "trivial-deferred": { + "version": "1.1.2", + "dev": true + }, + "tsame": { + "version": "2.0.1", + "dev": true + }, + "tslib": { + "version": "2.6.2" + }, + "tty-browserify": { + "version": "0.0.0" + }, + "tunnel-agent": { + "version": "0.6.0", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "dev": true + }, + "twgl.js": { + "version": "4.4.0", + "dev": true + }, + "type": { + "version": "1.2.0", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typed-array-buffer": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-length": { + "version": "1.0.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "typed-function": { + "version": "4.1.1" + }, + "typedarray": { + "version": "0.0.6" + }, + "uc.micro": { + "version": "1.0.6", + "dev": true + }, + "uglify-es": { + "version": "3.3.9", + "dev": true, + "requires": { + "commander": "~2.13.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "dev": true + } + } + }, + "uglifyjs-webpack-plugin": { + "version": "1.2.7", + "dev": true, + "requires": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "schema-utils": "^0.4.5", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "uglify-es": "^3.3.4", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + }, + "dependencies": { + "find-cache-dir": { + "version": "1.0.0", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "1.3.0", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "path-exists": { + "version": "3.0.0", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "schema-utils": { + "version": "0.4.7", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "dev": true + } + } + }, + "unbox-primitive": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "underscore": { + "version": "1.10.2", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "dev": true + }, + "unicode-length": { + "version": "1.0.3", + "dev": true, + "requires": { + "punycode": "^1.3.2", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "dev": true + }, + "unicode-trie": { + "version": "0.3.1", + "dev": true, + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + }, + "dependencies": { + "pako": { + "version": "0.2.9", + "dev": true + } + } + }, + "union-value": { + "version": "1.0.1", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "0.1.1" + } + } + }, + "unique-filename": { + "version": "1.1.1", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4" + }, + "isarray": { + "version": "1.0.0" + } + } + }, + "upath": { + "version": "1.2.0", + "devOptional": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0" + }, + "url": { + "version": "0.11.3", + "requires": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1" + }, + "qs": { + "version": "6.11.2", + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, + "url-parse": { + "version": "1.5.10", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1" + }, + "user-home": { + "version": "2.0.0", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "util": { + "version": "0.11.1", + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3" + } + } + }, + "util-deprecate": { + "version": "1.0.2" + }, + "utils-merge": { + "version": "1.0.1", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "dev": true + }, + "v8-compile-cache": { + "version": "2.4.0", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "dev": true + }, + "verror": { + "version": "1.10.0", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vlq": { + "version": "0.2.3", + "dev": true + }, + "vm-browserify": { + "version": "1.1.2" + }, + "watchpack": { + "version": "1.7.5", + "requires": { + "chokidar": "^3.4.1", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.1" + } + }, + "watchpack-chokidar2": { + "version": "2.0.1", + "optional": true, + "requires": { + "chokidar": "^2.1.8" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "optional": true + }, + "braces": { + "version": "2.3.2", + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "optional": true + }, + "is-number": { + "version": "3.0.0", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "optional": true + }, + "kind-of": { + "version": "3.2.2", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "readable-stream": { + "version": "2.3.8", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "optional": true + }, + "string_decoder": { + "version": "1.1.1", + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "wbuf": { + "version": "1.7.3", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webpack": { + "version": "4.46.0", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.5.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "webpack-cli": { + "version": "3.1.0", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.0.0", + "global-modules-path": "^2.1.0", + "import-local": "^1.0.0", + "inquirer": "^6.0.0", + "interpret": "^1.1.0", + "loader-utils": "^1.1.0", + "supports-color": "^5.4.0", + "v8-compile-cache": "^2.0.0", + "yargs": "^12.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "dev": true + }, + "chardet": { + "version": "0.7.0", + "dev": true + }, + "external-editor": { + "version": "3.1.0", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "inquirer": { + "version": "6.5.2", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "rxjs": { + "version": "6.6.7", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "tslib": { + "version": "1.14.1", + "dev": true + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.3", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "mime": { + "version": "2.6.0", + "dev": true + } + } + }, + "webpack-dev-server": { + "version": "3.11.2", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.8", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "dev": true + }, + "braces": { + "version": "2.3.2", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "cliui": { + "version": "5.0.0", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "extend-shallow": { + "version": "2.0.1", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "dev": true + }, + "glob-parent": { + "version": "3.1.0", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "import-local": { + "version": "2.0.0", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "is-binary-path": { + "version": "1.0.1", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "locate-path": { + "version": "3.0.0", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "readable-stream": { + "version": "2.3.8", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "require-main-filename": { + "version": "2.0.0", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "3.1.0", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "6.1.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs": { + "version": "13.3.2", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-sources": { + "version": "1.4.3", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1" + } + } + }, + "websocket-driver": { + "version": "0.7.4", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "dev": true + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-builtin-type": { + "version": "1.1.3", + "dev": true, + "optional": true, + "requires": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + } + }, + "which-collection": { + "version": "1.0.1", + "dev": true, + "optional": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-module": { + "version": "2.0.1", + "dev": true + }, + "which-typed-array": { + "version": "1.1.13", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "requires": { + "errno": "~0.1.7" + } + }, + "worker-loader": { + "version": "1.1.1", + "requires": { + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" + }, + "dependencies": { + "schema-utils": { + "version": "0.4.7", + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "wrap-ansi": { + "version": "2.1.0", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2" + }, + "write": { + "version": "0.2.1", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "2.4.3", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "6.2.2", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xmlcreate": { + "version": "2.0.4", + "dev": true + }, + "xtend": { + "version": "4.0.2" + }, + "y18n": { + "version": "4.0.3" + }, + "yallist": { + "version": "3.1.1" + }, + "yapool": { + "version": "1.0.0", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "dev": true + } + } + }, + "yargs-parser": { + "version": "11.1.1", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/local-scratch-vm/package.json b/local-scratch-vm/package.json new file mode 100644 index 0000000000000000000000000000000000000000..946d8bcef03407349b9a48d15a95f3eb1cd1b46e --- /dev/null +++ b/local-scratch-vm/package.json @@ -0,0 +1,104 @@ +{ + "name": "scratch-vm", + "version": "0.2.0", + "description": "Virtual Machine for Scratch 3.0", + "author": "Massachusetts Institute of Technology", + "license": "BSD-3-Clause", + "homepage": "https://github.com/LLK/scratch-vm#readme", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/LLK/scratch-vm.git" + }, + "main": "./src/index.js", + "browser": "./src/index.js", + "scripts": { + "build": "npm run docs && 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)\"", + "docs": "exit 0 || jsdoc -c .jsdoc.json", + "i18n:src": "mkdirp translations/core && format-message extract --out-file translations/core/en.json src/extensions/**/index.js", + "i18n:push": "tx-push-src scratch-editor extensions translations/core/en.json", + "lint": "exit 0 || eslint . && format-message lint src/**/*.js", + "prepublishOnly": "in-publish && npm run build || not-in-publish", + "start": "webpack-dev-server", + "tap": "tap ./test/{unit,integration}/*.js", + "tap:unit": "tap ./test/unit/*.js", + "tap:integration": "tap ./test/integration/*.js", + "test": "npm run lint && npm run docs && npm run tap", + "watch": "webpack --progress --colors --watch", + "version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"" + }, + "dependencies": { + "@turbowarp/json": "^0.1.2", + "@vernier/godirect": "1.5.0", + "arraybuffer-loader": "^1.0.6", + "atob": "2.1.2", + "btoa": "1.2.1", + "cannon-es": "0.20.0", + "canvas-toBlob": "1.0.0", + "decode-html": "2.0.0", + "diff-match-patch": "1.0.4", + "dompurify": "^3.0.9", + "format-message": "6.2.1", + "htmlparser2": "^3.10.0", + "immutable": "3.8.2", + "jszip": "^3.1.5", + "lz-string": "^1.5.0", + "mathjs": "11.11.1", + "matter-js": "^0.20.0", + "mersenne-twister": "^1.1.0", + "minilog": "3.1.0", + "pathfinding": "^0.4.18", + "schema-utils": "^2.7.1", + "scratch-parser": "git+https://github.com/PenguinMod/PenguinMod-Parser.git#master", + "scratch-sb1-converter": "0.2.7", + "scratch-translate-extension-languages": "0.0.20191118205314", + "simplex-noise": "^4.0.1", + "text-encoding": "0.7.0", + "three": "0.153.0", + "three-mesh-bvh": "0.6.0", + "tone": "^14.7.77", + "worker-loader": "^1.1.1" + }, + "peerDependencies": { + "scratch-svg-renderer": "^0.2.0-prerelease" + }, + "devDependencies": { + "@babel/core": "7.13.10", + "@babel/preset-env": "7.14.8", + "adm-zip": "0.4.11", + "babel-eslint": "10.1.0", + "babel-loader": "8.2.2", + "callsite": "1.0.0", + "copy-webpack-plugin": "4.5.4", + "docdash": "1.2.0", + "eslint": "5.3.0", + "eslint-config-scratch": "5.1.0", + "expose-loader": "0.7.5", + "file-loader": "2.0.0", + "format-message-cli": "6.2.0", + "gh-pages": "1.2.0", + "in-publish": "2.0.1", + "js-md5": "0.7.3", + "jsdoc": "3.6.6", + "json": "^9.0.4", + "lodash.defaultsdeep": "4.6.1", + "pngjs": "3.3.3", + "scratch-audio": "0.1.0-prerelease.20200528195344", + "scratch-blocks": "git+https://github.com/PenguinMod/PenguinMod-Blocks.git#develop-builds", + "scratch-l10n": "3.14.20220526031602", + "scratch-render": "0.1.0-prerelease.20211028200436", + "scratch-render-fonts": "github:PenguinMod/penguinmod-render-fonts#master", + "scratch-storage": "git+https://github.com/PenguinMod/PenguinMod-Storage.git#develop", + "scratch-svg-renderer": "0.2.0-prerelease.20210727023023", + "script-loader": "0.7.2", + "stats.js": "0.17.0", + "tap": "12.0.1", + "tiny-worker": "2.3.0", + "uglifyjs-webpack-plugin": "1.2.7", + "webpack": "4.46.0", + "webpack-cli": "3.1.0", + "webpack-dev-server": "3.11.2" + }, + "private": true +} diff --git a/local-scratch-vm/renovate.json5 b/local-scratch-vm/renovate.json5 new file mode 100644 index 0000000000000000000000000000000000000000..5615b310dbab76f23ee84ce5281503ac818cfc77 --- /dev/null +++ b/local-scratch-vm/renovate.json5 @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + + "extends": [ + "github>LLK/scratch-renovate-config:conservative" + ] +} diff --git a/local-scratch-vm/src/.eslintrc.js b/local-scratch-vm/src/.eslintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..278cf88bfcf97924b25e3fc5306ef76dc2ba34d1 --- /dev/null +++ b/local-scratch-vm/src/.eslintrc.js @@ -0,0 +1,32 @@ +module.exports = { + root: true, + extends: ['scratch', 'scratch/es6'], + env: { + browser: true + }, + rules: { + 'no-case-declarations': 'off', + 'no-console': 'off', + 'no-shadow': 'off', + 'quotes': 'off', + 'prefer-template': 'warn', + 'linebreak-style': 'off', + 'no-alert': 'off', + 'quote-props': 'off', + 'no-trailing-spaces': 'off', + 'object-curly-spacing': 'off', + 'curly': 'off', + 'operator-linebreak': 'off', + 'one-var': 'off', + 'brace-style': 'off', + 'camelcase': 'off', + 'comma-spacing': 'off', + 'no-negated-condition': 'off', + // @todo please jg, stop having your formater REMOVE THE SPACES + 'space-before-function-paren': 'off', + 'no-throw-literal': 'off' + }, + "globals": { + "vm": true + }, +}; diff --git a/local-scratch-vm/src/blocks/pm_live tests.js b/local-scratch-vm/src/blocks/pm_live tests.js new file mode 100644 index 0000000000000000000000000000000000000000..3fad778a5bfc4091b7f36d1074f5a7bb301cfb2f --- /dev/null +++ b/local-scratch-vm/src/blocks/pm_live tests.js @@ -0,0 +1,38 @@ +/* +compiled blocks: + sensing_set_of: jsgen.js@1195, irgen.js@1420 +*/ +const Cast = require('../util/cast'); + +class pmLiveTests { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + looks_setVertTransform: this.setVerticalTransform, + looks_setHorizTransform: this.setHorizontalTransform + }; + } + + setVerticalTransform (args, {target}) { + const percent = Cast.toNumber(args.PERCENT) / 100; + target.setTransform([percent, target.transform[1]]); + } + + setHorizontalTransform (args, {target}) { + const percent = Cast.toNumber(args.PERCENT) / 100; + target.setTransform([target.transform[0], percent]); + } +} + +module.exports = pmLiveTests; diff --git a/local-scratch-vm/src/blocks/scratch3_control.js b/local-scratch-vm/src/blocks/scratch3_control.js new file mode 100644 index 0000000000000000000000000000000000000000..7d0889306cf7cc571eff3ba6f1e915f0b8f47a58 --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_control.js @@ -0,0 +1,345 @@ +const Cast = require('../util/cast'); +const SandboxRunner = require('../util/sandboxed-javascript-runner.js'); + +class Scratch3ControlBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * The "counter" block value. For compatibility with 2.0. + * @type {number} + */ + this._counter = 0; // used by compiler + + /** + * The "error" block value. + * @type {string} + */ + this._error = ''; // used by compiler + + this.runtime.on('RUNTIME_DISPOSED', this.clearCounter.bind(this)); + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + control_repeat: this.repeat, + control_repeat_until: this.repeatUntil, + control_while: this.repeatWhile, + control_for_each: this.forEach, + control_forever: this.forever, + control_wait: this.wait, + control_repeatForSeconds: this.repeatForSeconds, + control_waittick: this.waitTick, + control_waitsecondsoruntil: this.waitOrUntil, + control_wait_until: this.waitUntil, + control_if: this.if, + control_if_else: this.ifElse, + control_stop: this.stop, + control_stop_sprite: this.stopSprite, + control_create_clone_of: this.createClone, + control_delete_this_clone: this.deleteClone, + control_delete_clones_of: this.deleteClonesOf, + control_get_counter: this.getCounter, + control_incr_counter: this.incrCounter, + control_decr_counter: this.decrCounter, + control_set_counter: this.setCounter, + control_clear_counter: this.clearCounter, + control_all_at_once: this.allAtOnce, + control_backToGreenFlag: this.backToGreenFlag, + control_if_return_else_return: this.if_return_else_return, + control_javascript_command: this.runJavascript + }; + } + + getMonitored () { + return { + control_get_counter: { + getId: () => 'get_counter' + } + }; + } + + backToGreenFlag(_, util) { + const thisThread = util.thread.topBlock; + this.runtime.emit("PROJECT_START_BEFORE_RESET"); + this.runtime.threads + .filter(thread => thread.topBlock !== thisThread) + .forEach(thread => thread.stopThisScript()); + // green flag behaviour + this.runtime.emit("PROJECT_START"); + this.runtime.updateCurrentMSecs(); + this.runtime.ioDevices.clock.resetProjectTimer(); + this.runtime.targets.forEach(target => target.clearEdgeActivatedValues()); + for (let i = this.runtime.targets.length - 1; i >= 0; i--) { + const thisTarget = this.runtime.targets[i]; + thisTarget.onGreenFlag(); + if (!thisTarget.isOriginal) { + this.runtime.disposeTarget(thisTarget); + this.runtime.stopForTarget(thisTarget); + } + } + this.runtime.startHats("event_whenflagclicked"); + } + + if_return_else_return (args) { + return Cast.toBoolean(args.boolean) ? args.TEXT1 : args.TEXT2; + } + + getHats () { + return { + control_start_as_clone: { + restartExistingThreads: false + } + }; + } + + runJavascript(args) { + return new Promise((resolve) => { + const js = Cast.toString(args.JS); + SandboxRunner.execute(js).then(result => { + resolve(result.value); + }); + }); + } + + repeat (args, util) { + const times = Math.round(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); + } + } + + repeatUntil (args, util) { + const condition = Cast.toBoolean(args.CONDITION); + // If the condition is false (repeat UNTIL), start the branch. + if (!condition) { + util.startBranch(1, true); + } + } + + repeatWhile (args, util) { + const condition = Cast.toBoolean(args.CONDITION); + // If the condition is true (repeat WHILE), start the branch. + if (condition) { + util.startBranch(1, true); + } + } + + forEach (args, util) { + const variable = util.target.lookupOrCreateVariable( + args.VARIABLE.id, args.VARIABLE.name); + + if (typeof util.stackFrame.index === 'undefined') { + util.stackFrame.index = 0; + } + + if (util.stackFrame.index < Number(args.VALUE)) { + util.stackFrame.index++; + variable.value = util.stackFrame.index; + util.startBranch(1, true); + } + } + + waitUntil (args, util) { + const condition = Cast.toBoolean(args.CONDITION); + if (!condition) { + util.yield(); + } + } + + forever (args, util) { + util.startBranch(1, true); + } + + wait (args, util) { + if (util.stackTimerNeedsInit()) { + const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION)); + + util.startStackTimer(duration); + this.runtime.requestRedraw(); + util.yield(); + } else if (!util.stackTimerFinished()) { + util.yield(); + } + } + + repeatForSeconds (args, util) { + if (util.stackTimerNeedsInit()) { + const duration = Math.max(0, 1000 * Cast.toNumber(args.TIMES)); + + util.startStackTimer(duration); + this.runtime.requestRedraw(); + util.startBranch(1, true); + util.yield(); + } else if (!util.stackTimerFinished()) { + util.startBranch(1, true); + util.yield(); + } + } + + waitTick (_, util) { + util.yieldTick(); + } + + waitOrUntil (args, util) { + const condition = Cast.toBoolean(args.CONDITION); + if (!condition) { + if (util.stackTimerNeedsInit()) { + const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION)); + + util.startStackTimer(duration); + this.runtime.requestRedraw(); + util.yield(); + return; + } + if (!util.stackTimerFinished()) { + util.yield(); + } + } + } + + if (args, util) { + const condition = Cast.toBoolean(args.CONDITION); + if (condition) { + util.startBranch(1, false); + } + } + + ifElse (args, util) { + const condition = Cast.toBoolean(args.CONDITION); + if (condition) { + util.startBranch(1, false); + } else { + util.startBranch(2, false); + } + } + + 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(); + } + } + + stopSprite (args, util) { + const option = args.STOP_OPTION; + // Set target + let target; + if (option === '_myself_') { + target = util.target; + } else if (option === '_stage_') { + target = this.runtime.getTargetForStage(); + } else { + target = this.runtime.getSpriteTargetByName(option); + } + if (!target) return; + this.runtime.stopForTarget(target); + } + + createClone (args, util) { + this._createClone(Cast.toString(args.CLONE_OPTION), util.target); + } + _createClone (cloneOption, target) { // used by compiler + // Set clone target + let cloneTarget; + if (cloneOption === '_myself_') { + cloneTarget = target; + } else { + cloneTarget = this.runtime.getSpriteTargetByName(cloneOption); + } + + // If clone target is not found, return + if (!cloneTarget) return; + + // Create clone + const newClone = cloneTarget.makeClone(); + if (newClone) { + this.runtime.addTarget(newClone); + + // Place behind the original target. + newClone.goBehindOther(cloneTarget); + } + } + + deleteClone (args, util) { + if (util.target.isOriginal) return; + this.runtime.disposeTarget(util.target); + this.runtime.stopForTarget(util.target); + } + + deleteClonesOf (args, util) { + const cloneOption = Cast.toString(args.CLONE_OPTION); + // Set clone target + let cloneTarget; + if (cloneOption === '_myself_') { + cloneTarget = util.target; + } else { + cloneTarget = this.runtime.getSpriteTargetByName(cloneOption); + } + + // If clone target is not found, return + if (!cloneTarget) return; + const sprite = cloneTarget.sprite; + if (!sprite) return; + if (!sprite.clones) return; + const cloneList = [].concat(sprite.clones); + cloneList.forEach(clone => { + if (clone.isOriginal) return; + if (clone.isStage) return; + this.runtime.disposeTarget(clone); + this.runtime.stopForTarget(clone); + }) + } + + getCounter () { + return this._counter; + } + + setCounter (args) { + const num = Cast.toNumber(args.VALUE); + this._counter = num; + } + + clearCounter () { + this._counter = 0; + } + + incrCounter () { + this._counter++; + } + + decrCounter () { + this._counter--; + } + + allAtOnce (util) { + util.thread.peekStackFrame().warpMode = false; + util.startBranch(1, false); + util.thread.peekStackFrame().warpMode = true; + } +} + +module.exports = Scratch3ControlBlocks; diff --git a/local-scratch-vm/src/blocks/scratch3_core_example.js b/local-scratch-vm/src/blocks/scratch3_core_example.js new file mode 100644 index 0000000000000000000000000000000000000000..b0fb03993d59b34f7c80b96874b86050557583e0 --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_core_example.js @@ -0,0 +1,86 @@ +const BlockType = require('../extension-support/block-type'); +const ArgumentType = require('../extension-support/argument-type'); + +/* eslint-disable-next-line max-len */ +const blockIconURI = 'data:image/svg+xml,%3Csvg id="rotate-counter-clockwise" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%233d79cc;%7D.cls-2%7Bfill:%23fff;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3Erotate-counter-clockwise%3C/title%3E%3Cpath class="cls-1" d="M22.68,12.2a1.6,1.6,0,0,1-1.27.63H13.72a1.59,1.59,0,0,1-1.16-2.58l1.12-1.41a4.82,4.82,0,0,0-3.14-.77,4.31,4.31,0,0,0-2,.8,4.25,4.25,0,0,0-1.34,1.73,5.06,5.06,0,0,0,.54,4.62A5.58,5.58,0,0,0,12,17.74h0a2.26,2.26,0,0,1-.16,4.52A10.25,10.25,0,0,1,3.74,18,10.14,10.14,0,0,1,2.25,8.78,9.7,9.7,0,0,1,5.08,4.64,9.92,9.92,0,0,1,9.66,2.5a10.66,10.66,0,0,1,7.72,1.68l1.08-1.35a1.57,1.57,0,0,1,1.24-.6,1.6,1.6,0,0,1,1.54,1.21l1.7,7.37A1.57,1.57,0,0,1,22.68,12.2Z"/%3E%3Cpath class="cls-2" d="M21.38,11.83H13.77a.59.59,0,0,1-.43-1l1.75-2.19a5.9,5.9,0,0,0-4.7-1.58,5.07,5.07,0,0,0-4.11,3.17A6,6,0,0,0,7,15.77a6.51,6.51,0,0,0,5,2.92,1.31,1.31,0,0,1-.08,2.62,9.3,9.3,0,0,1-7.35-3.82A9.16,9.16,0,0,1,3.17,9.12,8.51,8.51,0,0,1,5.71,5.4,8.76,8.76,0,0,1,9.82,3.48a9.71,9.71,0,0,1,7.75,2.07l1.67-2.1a.59.59,0,0,1,1,.21L22,11.08A.59.59,0,0,1,21.38,11.83Z"/%3E%3C/svg%3E'; + +/** + * An example core block implemented using the extension spec. + * This is not loaded as part of the core blocks in the VM but it is provided + * and used as part of tests. + */ +class Scratch3CoreExample { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'coreExample', + name: 'CoreEx', // This string does not need to be translated as this extension is only used as an example. + blocks: [ + { + opcode: 'exampleOpcode', + blockType: BlockType.REPORTER, + text: 'example block' + }, + { + opcode: 'exampleWithInlineImage', + blockType: BlockType.COMMAND, + text: 'block with image [CLOCKWISE] inline', + arguments: { + CLOCKWISE: { + type: ArgumentType.IMAGE, + dataURI: blockIconURI + } + } + }, + { + opcode: 'exampleNodeInputs', + blockType: BlockType.COMMAND, + text: 'block with some node inputs [CLOCKWISE]', + arguments: { + CLOCKWISE: { + type: ArgumentType.POLYGON, + nodes: 3 + } + } + } + ] + }; + } + + /** + * Example opcode just returns the name of the stage target. + * @returns {string} The name of the first target in the project. + */ + exampleOpcode () { + const stage = this.runtime.getTargetForStage(); + return stage ? stage.getName() : 'no stage yet'; + } + + exampleWithInlineImage () { + return; + } + + exampleNodeInputs (args, util) { + const nodes = args.CLOCKWISE; + this.runtime.ext_pen._penUp(util.target); + this.runtime.ext_pen.clear(); + util.target.setXY(nodes[0].x, nodes[0].y); + this.runtime.ext_pen._penDown(util.target); + nodes.forEach(node => { + util.target.setXY(node.x, node.y); + }); + util.target.setXY(nodes[0].x, nodes[0].y); + } +} + +module.exports = Scratch3CoreExample; diff --git a/local-scratch-vm/src/blocks/scratch3_data.js b/local-scratch-vm/src/blocks/scratch3_data.js new file mode 100644 index 0000000000000000000000000000000000000000..7f9fea075fce89d2286a63984e0cb45a62221e89 --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_data.js @@ -0,0 +1,327 @@ +const Cast = require('../util/cast'); +const { validateArray } = require('../util/json-block-utilities'); + +class Scratch3DataBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + data_variable: this.getVariable, + data_setvariableto: this.setVariableTo, + data_changevariableby: this.changeVariableBy, + data_hidevariable: this.hideVariable, + data_showvariable: this.showVariable, + data_listcontents: this.getListContents, + data_addtolist: this.addToList, + data_deleteoflist: this.deleteOfList, + data_deletealloflist: this.deleteAllOfList, + data_insertatlist: this.insertAtList, + data_replaceitemoflist: this.replaceItemOfList, + data_itemoflist: this.getItemOfList, + data_itemnumoflist: this.getItemNumOfList, + data_lengthoflist: this.lengthOfList, + data_listcontainsitem: this.listContainsItem, + data_hidelist: this.hideList, + data_showlist: this.showList, + data_reverselist: this.data_reverselist, + data_itemexistslist: this.data_itemexistslist, + data_listisempty: this.data_listisempty, + data_listarray: this.data_listarray, + data_arraylist: this.data_arraylist, + data_listforeachnum: this.data_listforeachnum, + data_listforeachitem: this.data_listforeachitem + }; + } + + data_reverselist (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + list.value.reverse(); + list._monitorUpToDate = false; + } + data_itemexistslist (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + const index = Cast.toListIndex(args.INDEX, list.value.length, false); + if (index === Cast.LIST_INVALID) { + return false; + } + return true; + } + data_listisempty (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + return list.value.length < 1; + } + data_listarray (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + return JSON.stringify(list.value); + } + data_arraylist (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + const array = validateArray(args.VALUE).array + .map(v => { + if (typeof v === 'object') return JSON.stringify(v); + return String(v); + }); + list.value = array; + } + data_listforeachnum (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + if (typeof util.stackFrame.loopCounter === 'undefined') { + util.stackFrame.loopCounter = list.value.length; + } + // 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) { + this.setVariableTo({ + VARIABLE: args.INDEX, + VALUE: util.stackFrame.loopCounter + }, util); + util.startBranch(1, true); + } + } + data_listforeachitem (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + if (typeof util.stackFrame.loopCounter === 'undefined') { + util.stackFrame.loopCounter = list.value.length; + } + // 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) { + this.setVariableTo({ + VARIABLE: args.INDEX, + VALUE: list.value[util.stackFrame.loopCounter] + }, util); + util.startBranch(1, true); + } + } + + getVariable (args, util) { + const variable = util.target.lookupOrCreateVariable( + args.VARIABLE.id, args.VARIABLE.name); + return variable.value; + } + + setVariableTo (args, util) { + const variable = util.target.lookupOrCreateVariable( + args.VARIABLE.id, args.VARIABLE.name); + variable.value = args.VALUE; + + if (variable.isCloud) { + util.ioQuery('cloud', 'requestUpdateVariable', [variable.name, 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); + const newValue = castedValue + dValue; + variable.value = newValue; + + if (variable.isCloud) { + util.ioQuery('cloud', 'requestUpdateVariable', [variable.name, newValue]); + } + } + + changeMonitorVisibility (id, visible) { + // Send the monitor blocks an event like the flyout checkbox event. + // This both updates the monitor state and changes the isMonitored block flag. + this.runtime.monitorBlocks.changeBlock({ + id: id, // Monitor blocks for variables are the variable ID. + element: 'checkbox', // Mimic checkbox event from flyout. + value: visible + }, this.runtime); + } + + showVariable (args) { + this.changeMonitorVisibility(args.VARIABLE.id, true); + } + + hideVariable (args) { + this.changeMonitorVisibility(args.VARIABLE.id, false); + } + + showList (args) { + this.changeMonitorVisibility(args.LIST.id, true); + } + + hideList (args) { + this.changeMonitorVisibility(args.LIST.id, false); + } + + getListContents (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + + // If block is running for monitors, return copy of list as an array if changed. + if (util.thread.updateMonitor) { + // Return original list value if up-to-date, which doesn't trigger monitor update. + if (list._monitorUpToDate) return list.value; + // If value changed, reset the flag and return a copy to trigger monitor update. + // Because monitors use Immutable data structures, only new objects trigger updates. + list._monitorUpToDate = true; + return list.value.slice(); + } + + // 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.value.join(''); + } + return list.value.join(' '); + + } + + addToList (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + list.value.push(args.ITEM); + list._monitorUpToDate = false; + } + + deleteOfList (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + const index = Cast.toListIndex(args.INDEX, list.value.length, true); + if (index === Cast.LIST_INVALID) { + return; + } else if (index === Cast.LIST_ALL) { + list.value = []; + return; + } + list.value.splice(index - 1, 1); + list._monitorUpToDate = false; + } + + deleteAllOfList (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + list.value = []; + 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, false); + if (index === Cast.LIST_INVALID) { + return; + } + list.value.splice(index - 1, 0, item); + list._monitorUpToDate = false; + } + + 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, false); + if (index === Cast.LIST_INVALID) { + return; + } + list.value[index - 1] = item; + list._monitorUpToDate = false; + } + + getItemOfList (args, util) { + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + const index = Cast.toListIndex(args.INDEX, list.value.length, false); + if (index === Cast.LIST_INVALID) { + return ''; + } + return list.value[index - 1]; + } + + getItemNumOfList (args, util) { + const item = args.ITEM; + const list = util.target.lookupOrCreateList( + args.LIST.id, args.LIST.name); + + // Go through the list items one-by-one using Cast.compare. This is for + // cases like checking if 123 is contained in a list [4, 7, '123'] -- + // Scratch considers 123 and '123' to be equal. + for (let i = 0; i < list.value.length; i++) { + if (Cast.compare(list.value[i], item) === 0) { + return i + 1; + } + } + + // We don't bother using .indexOf() at all, because it would end up with + // edge cases such as the index of '123' in [4, 7, 123, '123', 9]. + // If we use indexOf(), this block would return 4 instead of 3, because + // indexOf() sees the first occurence of the string 123 as the fourth + // item in the list. With Scratch, this would be confusing -- after all, + // '123' and 123 look the same, so one would expect the block to say + // that the first occurrence of '123' (or 123) to be the third item. + + // Default to 0 if there's no match. Since Scratch lists are 1-indexed, + // we don't have to worry about this conflicting with the "this item is + // the first value" number (in JS that is 0, but in Scratch it's 1). + return 0; + } + + 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; + } + + _listFilterItem = "" + _listFilterIndex = 0 +} + +module.exports = Scratch3DataBlocks; diff --git a/local-scratch-vm/src/blocks/scratch3_event.js b/local-scratch-vm/src/blocks/scratch3_event.js new file mode 100644 index 0000000000000000000000000000000000000000..f6b3380e037492c150f4b5296ea387bd43206fd3 --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_event.js @@ -0,0 +1,219 @@ +const Cast = require('../util/cast'); +const SandboxRunner = require('../util/sandboxed-javascript-runner.js'); + +class Scratch3EventBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.runtime.on('KEY_PRESSED', key => { + this.runtime.startHats('event_whenkeypressed', { + KEY_OPTION: key + }); + this.runtime.startHats('event_whenkeypressed', { + KEY_OPTION: 'any' + }); + }); + + this.runtime.on('KEY_HIT', key => { + this.runtime.startHats('event_whenkeyhit', { + KEY_OPTION: key + }); + this.runtime.startHats('event_whenkeyhit', { + KEY_OPTION: 'any' + }); + }); + + this.isStarting = false; + this.runtime.on('PROJECT_START_BEFORE_RESET', () => { + // we need to remember that the project is starting + // otherwise the stop block will run when flag is clicked + this.isStarting = true; + }); + this.runtime.on('PROJECT_STOP_ALL', () => { + // if green flag is clicked, dont bother starting the hat + if (this.isStarting) { + this.isStarting = false; + return; + } + // we need to wait for runtime to step once + // otherwise the hat will be stopped as soon as it starts + this.runtime.once('RUNTIME_STEP_START', () => { + this.runtime.startHats('event_whenstopclicked'); + }) + this.isStarting = false; + }); + this.runtime.on('RUNTIME_STEP_START', () => { + this.runtime.startHats('event_always'); + }); + + this.runtime.on("AFTER_EXECUTE", () => { + // Use a timeout as regular Block Threads and Events Blocks dont run at the Same Speed + setTimeout(() => { + const stage = this.runtime.getTargetForStage(); + if (!stage) return; // happens when project is loading + const stageVars = stage.variables; + for (const key in stageVars) { + if (stageVars[key].isSent !== undefined) stageVars[key].isSent = false; + } + }, 10); + }); + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + event_whenanything: this.whenanything, + event_whenjavascript: this.whenjavascript, + event_whentouchingobject: this.touchingObject, + event_broadcast: this.broadcast, + event_broadcastandwait: this.broadcastAndWait, + event_whengreaterthan: this.hatGreaterThanPredicate + }; + } + + whenanything (args) { + return Cast.toBoolean(args.ANYTHING); + } + + whenjavascript (args) { + return new Promise((resolve) => { + const js = Cast.toString(args.JS); + SandboxRunner.execute(js).then(result => { + resolve(result.value === true); + }) + }) + } + + getHats () { + return { + event_whenflagclicked: { + restartExistingThreads: true + }, + event_whenstopclicked: { + restartExistingThreads: true + }, + event_always: { + restartExistingThreads: false + }, + event_whenkeypressed: { + restartExistingThreads: false + }, + event_whenkeyhit: { + restartExistingThreads: false + }, + event_whenmousescrolled: { + restartExistingThreads: false + }, + event_whenanything: { + restartExistingThreads: false, + edgeActivated: true + }, + event_whenjavascript: { + restartExistingThreads: false, + edgeActivated: true + }, + event_whenthisspriteclicked: { + restartExistingThreads: true + }, + event_whentouchingobject: { + restartExistingThreads: false, + edgeActivated: true + }, + event_whenstageclicked: { + restartExistingThreads: true + }, + event_whenbackdropswitchesto: { + restartExistingThreads: true + }, + event_whengreaterthan: { + restartExistingThreads: false, + edgeActivated: true + }, + event_whenbroadcastreceived: { + restartExistingThreads: true + } + }; + } + + touchingObject (args, util) { + return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU); + } + + hatGreaterThanPredicate (args, util) { + const option = Cast.toString(args.WHENGREATERTHANMENU).toLowerCase(); + const value = Cast.toNumber(args.VALUE); + switch (option) { + case 'timer': + return util.ioQuery('clock', 'projectTimer') > value; + case 'loudness': + return this.runtime.audioEngine && this.runtime.audioEngine.getLoudness() > 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; + broadcastVar.isSent = true; + util.startHats('event_whenbroadcastreceived', { + BROADCAST_OPTION: broadcastOption + }); + } + } + + broadcastAndWait (args, util) { + if (!util.stackFrame.broadcastVar) { + util.stackFrame.broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg( + args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); + } + if (util.stackFrame.broadcastVar) { + const broadcastOption = util.stackFrame.broadcastVar.name; + // Have we run before, starting threads? + if (!util.stackFrame.startedThreads) { + broadcastVar.isSent = true; + // 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; + // Scratch 2 considers threads to be waiting if they are still in + // runtime.threads. Threads that have run all their blocks, or are + // marked done but still in runtime.threads are still considered to + // be waiting. + const waiting = util.stackFrame.startedThreads + .some(thread => instance.runtime.threads.indexOf(thread) !== -1); + if (waiting) { + // If all threads are waiting for the next tick or later yield + // for a tick as well. Otherwise yield until the next loop of + // the threads. + if ( + util.stackFrame.startedThreads + .every(thread => instance.runtime.isWaitingThread(thread)) + ) { + util.yieldTick(); + } else { + util.yield(); + } + } + } + } +} + +module.exports = Scratch3EventBlocks; diff --git a/local-scratch-vm/src/blocks/scratch3_looks.js b/local-scratch-vm/src/blocks/scratch3_looks.js new file mode 100644 index 0000000000000000000000000000000000000000..2d1ef89137aca9df9a0623382452906f568f6173 --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_looks.js @@ -0,0 +1,996 @@ +const Cast = require('../util/cast'); +const Color = require('../util/color'); +const Clone = require('../util/clone'); +const uid = require('../util/uid'); +const StageLayering = require('../engine/stage-layering'); +const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id'); +const MathUtil = require('../util/math-util'); + +/** + * @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 {string} text - the text of the bubble. + * @property {string} type - the type of the bubble, "say" or "think" + * @property {?string} usageId - ID indicating the most recent usage of the say/think bubble. + * Used for comparison when determining whether to clear a say/think bubble. + */ + +class Scratch3LooksBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this._onTargetChanged = this._onTargetChanged.bind(this); + this._onResetBubbles = this._onResetBubbles.bind(this); + this._onTargetWillExit = this._onTargetWillExit.bind(this); + this._updateBubble = this._updateBubble.bind(this); + + this.SAY_BUBBLE_LIMITdefault = 330; + this.SAY_BUBBLE_LIMIT = this.SAY_BUBBLE_LIMITdefault; + this.defaultBubble = { + MAX_LINE_WIDTH: 170, // Maximum width, in Scratch pixels, of a single line of text + + MIN_WIDTH: 50, // Minimum width, in Scratch pixels, of a text bubble + STROKE_WIDTH: 4, // Thickness of the stroke around the bubble. + // Only half's visible because it's drawn under the fill + PADDING: 10, // Padding around the text area + CORNER_RADIUS: 16, // Radius of the rounded corners + TAIL_HEIGHT: 12, // Height of the speech bubble's "tail". Probably should be a constant. + + FONT: 'Helvetica', // Font to render the text with + FONT_SIZE: 14, // Font size, in Scratch pixels + FONT_HEIGHT_RATIO: 0.9, // Height, in Scratch pixels, of the text, as a proportion of the font's size + LINE_HEIGHT: 16, // Spacing between each line of text + + COLORS: { + BUBBLE_FILL: 'white', + BUBBLE_STROKE: 'rgba(0, 0, 0, 0.15)', + TEXT_FILL: '#575E75' + } + }; + + // 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(Scratch3LooksBlocks.SAY_OR_THINK, this._updateBubble); + } + + /** + * The default bubble state, to be used when a target has no existing bubble state. + * @type {BubbleState} + */ + static get DEFAULT_BUBBLE_STATE () { + return { + drawableId: null, + onSpriteRight: true, + skinId: null, + text: '', + type: 'say', + usageId: null, + // @todo make this read from renderer + props: this.defaultBubble + }; + } + + /** + * The key to load & store a target's bubble-related state. + * @type {string} + */ + static get STATE_KEY () { + return 'Scratch.looks'; + } + + /** + * Event name for a text bubble being created or updated. + * @const {string} + */ + static get SAY_OR_THINK () { + // There are currently many places in the codebase which explicitly refer to this event by the string 'SAY', + // so keep this as the string 'SAY' for now rather than changing it to 'SAY_OR_THINK' and breaking things. + return 'SAY'; + } + + /** + * Limit for ghost effect + * @const {object} + */ + static get EFFECT_GHOST_LIMIT (){ + return {min: 0, max: 100}; + } + + /** + * Limit for brightness effect + * @const {object} + */ + static get EFFECT_BRIGHTNESS_LIMIT (){ + return {min: -100, max: 100}; + } + + /** + * @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; + } + + /** + * resets the text bubble of a sprite + * @param {Target} target the target to reset + */ + _resetBubbles (target) { + const state = this._getBubbleState(target); + this.SAY_BUBBLE_LIMIT = this.SAY_BUBBLE_LIMITdefault; + state.props = this.defaultBubble; + } + + /** + * set any property of the text bubble of any given target + * @param {Target} target the target to modify + * @param {array} props the property names to change + * @param {array} value the values the set the properties to + */ + _setBubbleProperty (target, props, value) { + const object = this._getBubbleState(target); + if (!object.props) object.props = this.defaultBubble; + props.forEach((prop, index) => { + if (prop.startsWith('COLORS')) { + object.props.COLORS[prop.split('.')[1]] = value[index]; + } else { + object.props[prop] = value[index]; + } + }); + + target.setCustomState(Scratch3LooksBlocks.STATE_KEY, object); + } + + /** + * Handle a target which has moved. + * @param {RenderedTarget} target - the target which has moved. + * @private + */ + _onTargetChanged (target) { + const bubbleState = this._getBubbleState(target); + if (bubbleState.drawableId) { + this._positionBubble(target); + } + } + + /** + * 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, StageLayering.SPRITE_LAYER); + this.runtime.renderer.destroySkin(bubbleState.skinId); + bubbleState.drawableId = null; + bubbleState.skinId = null; + this.runtime.requestRedraw(); + } + target.onTargetVisualChange = null; + } + + /** + * Handle project start/stop by clearing all visible bubbles. + * @private + */ + _onResetBubbles () { + for (let n = 0; n < this.runtime.targets.length; n++) { + const bubbleState = this._getBubbleState(this.runtime.targets[n]); + bubbleState.text = ''; + this._onTargetWillExit(this.runtime.targets[n]); + } + clearTimeout(this._bubbleTimeout); + } + + /** + * 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) { + if (!target.visible) return; + const bubbleState = this._getBubbleState(target); + const [bubbleWidth, bubbleHeight] = this.runtime.renderer.getCurrentSkinSize(bubbleState.drawableId); + let targetBounds; + try { + targetBounds = target.getBoundsForBubble(); + } catch (error_) { + // Bounds calculation could fail (e.g. on empty costumes), in that case + // use the x/y position of the target. + targetBounds = { + left: target.x, + right: target.x, + top: target.y, + bottom: target.y + }; + } + const stageSize = this.runtime.renderer.getNativeSize(); + const stageBounds = { + left: -stageSize[0] / 2, + right: stageSize[0] / 2, + top: stageSize[1] / 2, + bottom: -stageSize[1] / 2 + }; + 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 { + this.runtime.renderer.updateDrawablePosition(bubbleState.drawableId, [ + bubbleState.onSpriteRight ? ( + Math.max( + stageBounds.left, // Bubble should not extend past left edge of stage + Math.min(stageBounds.right - bubbleWidth, targetBounds.right) + ) + ) : ( + Math.min( + stageBounds.right - bubbleWidth, // Bubble should not extend past right edge of stage + Math.max(stageBounds.left, targetBounds.left - bubbleWidth) + ) + ), + // Bubble should not extend past the top of the stage + Math.min(stageBounds.top, targetBounds.bottom + 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) { // used by compiler + if (!this.runtime.renderer) return; + + const bubbleState = this._getBubbleState(target); + const {type, text, onSpriteRight} = bubbleState; + + // Remove the bubble if target is not visible, or text is being set to blank. + if (!target.visible || text === '') { + this._onTargetWillExit(target); + return; + } + + if (bubbleState.skinId) { + this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight, bubbleState.props); + } else { + target.onTargetVisualChange = this._onTargetChanged; + bubbleState.drawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER); + bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, + bubbleState.onSpriteRight, bubbleState.props); + this.runtime.renderer.updateDrawableSkinId(bubbleState.drawableId, bubbleState.skinId); + } + + this._positionBubble(target); + } + + /** + * Properly format text for a text bubble. + * @param {string} text The text to be formatted + * @return {string} The formatted text + * @private + */ + _formatBubbleText (text) { + if (text === '') return text; + + // Non-integers should be rounded to 2 decimal places (no more, no less), unless they're small enough that + // rounding would display them as 0.00. This matches 2.0's behavior: + // https://github.com/LLK/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/scratch/ScratchSprite.as#L579-L585 + if (typeof text === 'number' && + Math.abs(text) >= 0.01 && text % 1 !== 0) { + text = text.toFixed(2); + } + + // Limit the length of the string. + text = String(text).slice(0, this.SAY_BUBBLE_LIMIT); + + return text; + } + + /** + * 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 = this._formatBubbleText(text); + bubbleState.usageId = uid(); + this._renderBubble(target); + } + _percentToRatio (percent) { + return percent / 100; + } + _doesFontSuport (size, font) { + const check = size + 'px ' + font; + return document.fonts.check(check); + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + looks_say: this.say, + looks_sayforsecs: this.sayforsecs, + looks_think: this.think, + looks_thinkforsecs: this.thinkforsecs, + looks_setFont: this.setFont, + looks_setColor: this.setColor, + looks_setShape: this.setShape, + looks_show: this.show, + looks_hide: this.hide, + looks_getSpriteVisible: this.getSpriteVisible, + looks_getOtherSpriteVisible: this.getOtherSpriteVisible, + looks_hideallsprites: () => {}, // legacy no-op block + looks_switchcostumeto: this.switchCostume, + looks_switchbackdropto: this.switchBackdrop, + looks_switchbackdroptoandwait: this.switchBackdropAndWait, + looks_nextcostume: this.nextCostume, + looks_nextbackdrop: this.nextBackdrop, + looks_previouscostume: this.previousCostume, + looks_previousbackdrop: this.previousBackdrop, + looks_changeeffectby: this.changeEffect, + looks_seteffectto: this.setEffect, + looks_cleargraphiceffects: this.clearEffects, + looks_getEffectValue: this.getEffectValue, + looks_changesizeby: this.changeSize, + looks_setsizeto: this.setSize, + looks_changestretchby: () => {}, + looks_setstretchto: this.stretchSet, + looks_gotofrontback: this.goToFrontBack, + looks_goforwardbackwardlayers: this.goForwardBackwardLayers, + looks_goTargetLayer: this.goTargetLayer, + looks_layersSetLayer: this.setSpriteLayer, + looks_layersGetLayer: this.getSpriteLayer, + looks_size: this.getSize, + looks_costumenumbername: this.getCostumeNumberName, + looks_backdropnumbername: this.getBackdropNumberName, + looks_setStretch: this.stretchSet, + looks_changeStretch: this.changeStretch, + looks_stretchGetX: this.getStretchX, + looks_stretchGetY: this.getStretchY, + looks_sayWidth: this.getBubbleWidth, + looks_sayHeight: this.getBubbleHeight, + looks_changeVisibilityOfSprite: this.showOrHideSprite, + looks_changeVisibilityOfSpriteShow: this.showSprite, + looks_changeVisibilityOfSpriteHide: this.hideSprite, + looks_stoptalking: this.stopTalking, + looks_getinputofcostume: this.getCostumeValue, + looks_tintColor: this.getTintColor, + looks_setTintColor: this.setTintColor + }; + } + + getSpriteLayer (_, util) { + const target = util.target; + return target.getLayerOrder(); + } + + setSpriteLayer (args, util) { + const target = util.target; + const targetLayer = Cast.toNumber(args.NUM); + const currentLayer = target.getLayerOrder(); + // i dont know how to set layer lol + target.goForwardLayers(targetLayer - currentLayer); + } + + _getBubbleSize (target) { + const bubbleState = this._getBubbleState(target); + return this.runtime.renderer.getSkinSize(bubbleState.skinId); + } + + getBubbleWidth (_, util) { + const target = util.target; + let val = 0; + try { + val = this._getBubbleSize(target)[0]; + } catch { + val = 0; + } + return val; + } + + getBubbleHeight (_, util) { + const target = util.target; + let val = 0; + try { + val = this._getBubbleSize(target)[1]; + } catch { + val = 0; + } + return val; + } + + getStretchY (args, util) { + return util.target._getRenderedDirectionAndScale().stretch[1]; + } + getStretchX (args, util) { + return util.target._getRenderedDirectionAndScale().stretch[0]; + } + + stretchSet (args, util) { + util.target.setStretch(args.X, args.Y); + } + + changeStretch(args, util) { + let [x, y] = util.target._getRenderedDirectionAndScale().stretch; + let new_x = x + Cast.toNumber(args.X) + let new_y = y + Cast.toNumber(args.Y) + util.target.setStretch(new_x, new_y) + } + + setFont (args, util) { + this._setBubbleProperty( + util.target, + ['FONT', 'FONT_SIZE'], + [args.font, args.size] + ); + } + setColor (args, util) { + const numColor = Number(args.color); + if (!isNaN(numColor)) { + args.color = Color.decimalToHex(numColor); + } + this._setBubbleProperty( + util.target, + ['COLORS.' + args.prop], + [args.color] + ); + } + setShape (args, util) { + if (args.prop === 'texlim') { + this.SAY_BUBBLE_LIMIT = Math.max(args.color, 1); + return; + } + this._setBubbleProperty( + util.target, + [args.prop], + [args.color] + ); + } + + getMonitored () { + return { + looks_size: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_size` + }, + looks_stretchGetX: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_stretchX` + }, + looks_stretchGetY: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_stretchY` + }, + looks_sayWidth: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_sayWidth` + }, + looks_sayHeight: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_sayHeight` + }, + looks_getEffectValue: { + isSpriteSpecific: true, + getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_getEffectValue`, fields) + }, + looks_tintColor: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_tintColor` + }, + looks_getSpriteVisible: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_getSpriteVisible` + }, + looks_layersGetLayer: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_layersGetLayer` + }, + looks_costumenumbername: { + isSpriteSpecific: true, + getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_costumenumbername`, fields) + }, + looks_backdropnumbername: { + getId: (_, fields) => getMonitorIdForBlockWithArgs('backdropnumbername', fields) + } + }; + } + + say (args, util) { + // @TODO in 2.0 calling say/think resets the right/left bias of the bubble + const message = args.MESSAGE; + this._say(message, util.target); + } + _say (message, target) { // used by compiler + this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, target, 'say', message); + } + + stopTalking (_, util) { + this.say({ MESSAGE: '' }, util); + } + + sayforsecs (args, util) { + this.say(args, util); + const target = util.target; + const usageId = this._getBubbleState(target).usageId; + return new Promise(resolve => { + this._bubbleTimeout = setTimeout(() => { + this._bubbleTimeout = null; + // Clear say bubble if it hasn't been changed and proceed. + if (this._getBubbleState(target).usageId === usageId) { + this._updateBubble(target, 'say', ''); + } + resolve(); + }, 1000 * args.SECS); + }); + } + + think (args, util) { + this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'think', args.MESSAGE); + } + + thinkforsecs (args, util) { + this.think(args, util); + const target = util.target; + const usageId = this._getBubbleState(target).usageId; + return new Promise(resolve => { + this._bubbleTimeout = setTimeout(() => { + this._bubbleTimeout = null; + // Clear think bubble if it hasn't been changed and proceed. + if (this._getBubbleState(target).usageId === usageId) { + 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); + } + + showOrHideSprite (args, util) { + const option = args.VISIBLE_OPTION; + const visibleOption = Cast.toString(args.VISIBLE_TYPE).toLowerCase(); + // Set target + let target; + if (option === '_myself_') { + target = util.target; + } else if (option === '_stage_') { + target = this.runtime.getTargetForStage(); + } else { + target = this.runtime.getSpriteTargetByName(option); + } + if (!target) return; + target.setVisible(visibleOption === 'show'); + this._renderBubble(target); + } + + showSprite (args, util) { + this.showOrHideSprite({ VISIBLE_OPTION: args.VISIBLE_OPTION, VISIBLE_TYPE: "show" }, util); + } + hideSprite (args, util) { + this.showOrHideSprite({ VISIBLE_OPTION: args.VISIBLE_OPTION, VISIBLE_TYPE: "hide" }, util); + } + + getSpriteVisible (args, util) { + return util.target.visible; + } + + getOtherSpriteVisible (args, util) { + const option = args.VISIBLE_OPTION; + // Set target + let target; + if (option === '_myself_') { + target = util.target; + } else if (option === '_stage_') { + target = this.runtime.getTargetForStage(); + } else { + target = this.runtime.getSpriteTargetByName(option); + } + if (!target) return; + return target.visible; + } + + getEffectValue (args, util) { + const effect = Cast.toString(args.EFFECT).toLowerCase(); + const effects = util.target.effects; + if (!effects.hasOwnProperty(effect)) return 0; + const value = Cast.toNumber(effects[effect]); + return value; + } + + getTintColor (_, util) { + const effects = util.target.effects; + if (typeof effects.tintColor !== 'number') return '#ffffff'; + return Color.decimalToHex(effects.tintColor - 1); + } + setTintColor (args, util) { // used by compiler + const rgb = Cast.toRgbColorObject(args.color); + const decimal = Color.rgbToDecimal(rgb); + util.target.setEffect("tintColor", decimal + 1); + } + + /** + * Utility function to set the costume of a target. + * Matches the behavior of Scratch 2.0 for different types of arguments. + * @param {!Target} target Target to set costume to. + * @param {Any} requestedCostume Costume requested, e.g., 0, 'name', etc. + * @param {boolean=} optZeroIndex Set to zero-index the requestedCostume. + * @return {Array.} Any threads started by this switch. + */ + _setCostume (target, requestedCostume, optZeroIndex) { // used by compiler + if (typeof requestedCostume === 'number') { + // Numbers should be treated as costume indices, always + target.setCostume(optZeroIndex ? requestedCostume : requestedCostume - 1); + } else { + // Strings should be treated as costume names, where possible + const costumeIndex = target.getCostumeIndexByName(requestedCostume.toString()); + + if (costumeIndex !== -1) { + target.setCostume(costumeIndex); + } else if (requestedCostume === 'next costume') { + target.setCostume(target.currentCostume + 1); + } else if (requestedCostume === 'previous costume') { + target.setCostume(target.currentCostume - 1); + // Try to cast the string to a number (and treat it as a costume index) + // Pure whitespace should not be treated as a number + // Note: isNaN will cast the string to a number before checking if it's NaN + } else if (requestedCostume === 'random costume') { + let randomIndex = MathUtil.inclusiveRandIntWithout( + 0, + target.sprite.costumes_.length - 1, + target.currentCostume + ) + if (randomIndex >= target.sprite.costumes_.length) { + randomIndex = 0; + // This really only accounts for if there's only 1 + // costume. + } + target.setCostume(randomIndex); + } else if (!(isNaN(requestedCostume) || Cast.isWhiteSpace(requestedCostume))) { + target.setCostume(optZeroIndex ? Number(requestedCostume) : Number(requestedCostume) - 1); + } + } + + // Per 2.0, 'switch costume' can't start threads even in the Stage. + return []; + } + + costumeValueToDefaultNone (value) { + switch (value) { + case 'width': + case 'height': + case 'rotation center x': + case 'rotation center y': + return 0; + default: + return ''; + } + } + getCostumeValue (args, util) { + let costumeIndex = 0; + const target = util.target + const requestedCostume = args.COSTUME; + const requestedValue = Cast.toString(args.INPUT); + if (typeof requestedCostume === 'number') { + // Numbers should be treated as costume indices, always + costumeIndex = (requestedCostume === 0) ? 0 : requestedCostume - 1; + } else { + let noun = target.isStage ? "backdrop" : "costume"; + switch (Cast.toString(requestedCostume)) { + case "next " + noun: + costumeIndex = target.currentCostume + 1; + if (costumeIndex >= target.sprite.costumes_.length) { + costumeIndex = 0 + // loop around to front + } + break; + case "previous " + noun: + costumeIndex = target.currentCostume - 1; + if (costumeIndex < 0) { + costumeIndex = target.sprite.costumes_.length - 1; + // Loop around to back + } + break; + case "random " + noun: + costumeIndex = MathUtil.inclusiveRandIntWithout( + 0, + target.sprite.costumes_.length - 1, + target.currentCostume + ) + if (costumeIndex >= target.sprite.costumes_.length) { + costumeIndex = 0; + // This really only accounts for if there's only 1 + // costume. + } + break; + default: + costumeIndex = target.getCostumeIndexByName(Cast.toString(requestedCostume)); + } + } + if (costumeIndex < 0) return this.costumeValueToDefaultNone(requestedValue); + if (!target.sprite) return this.costumeValueToDefaultNone(requestedValue); + if (!target.sprite.costumes_) return this.costumeValueToDefaultNone(requestedValue); + const costume = target.sprite.costumes_[costumeIndex]; + if (!costume) return this.costumeValueToDefaultNone(requestedValue); + switch (requestedValue) { + case 'width': + return costume.size[0]; + case 'height': + return costume.size[1]; + case 'rotation center x': + return costume.rotationCenterX; + case 'rotation center y': + return costume.rotationCenterY; + case 'drawing mode': + return ((costume.dataFormat === "svg") ? "Vector" : "Bitmap"); + default: + return ''; + } + } + + /** + * Utility function to set the backdrop of a target. + * Matches the behavior of Scratch 2.0 for different types of arguments. + * @param {!Target} stage Target to set backdrop to. + * @param {Any} requestedBackdrop Backdrop requested, e.g., 0, 'name', etc. + * @param {boolean=} optZeroIndex Set to zero-index the requestedBackdrop. + * @return {Array.} Any threads started by this switch. + */ + _setBackdrop (stage, requestedBackdrop, optZeroIndex) { // used by compiler + if (typeof requestedBackdrop === 'number') { + // Numbers should be treated as backdrop indices, always + stage.setCostume(optZeroIndex ? requestedBackdrop : requestedBackdrop - 1); + } else { + // Strings should be treated as backdrop names where possible + const costumeIndex = stage.getCostumeIndexByName(requestedBackdrop.toString()); + + if (costumeIndex !== -1) { + stage.setCostume(costumeIndex); + } else if (requestedBackdrop === 'next backdrop') { + stage.setCostume(stage.currentCostume + 1); + } else if (requestedBackdrop === 'previous backdrop') { + stage.setCostume(stage.currentCostume - 1); + } else if (requestedBackdrop === 'random backdrop') { + const numCostumes = stage.getCostumes().length; + if (numCostumes > 1) { + // Don't pick the current backdrop, so that the block + // will always have an observable effect. + const lowerBound = 0; + const upperBound = numCostumes - 1; + const costumeToExclude = stage.currentCostume; + + const nextCostume = MathUtil.inclusiveRandIntWithout(lowerBound, upperBound, costumeToExclude); + + stage.setCostume(nextCostume); + } + // Try to cast the string to a number (and treat it as a costume index) + // Pure whitespace should not be treated as a number + // Note: isNaN will cast the string to a number before checking if it's NaN + } else if (!(isNaN(requestedBackdrop) || Cast.isWhiteSpace(requestedBackdrop))) { + stage.setCostume(optZeroIndex ? Number(requestedBackdrop) : Number(requestedBackdrop) - 1); + } + } + + const newName = stage.getCostumes()[stage.currentCostume].name; + return this.runtime.startHats('event_whenbackdropswitchesto', { + BACKDROP: newName + }); + } + + switchCostume (args, util) { + this._setCostume(util.target, args.COSTUME); // used by compiler + } + + nextCostume (args, util) { + this._setCostume( + util.target, util.target.currentCostume + 1, true + ); + } + + previousCostume (args, util) { + this._setCostume( + util.target, util.target.currentCostume - 1, true + ); + } + + switchBackdrop (args) { + this._setBackdrop(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._setBackdrop( + 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; + // Scratch 2 considers threads to be waiting if they are still in + // runtime.threads. Threads that have run all their blocks, or are + // marked done but still in runtime.threads are still considered to + // be waiting. + const waiting = util.stackFrame.startedThreads + .some(thread => instance.runtime.threads.indexOf(thread) !== -1); + if (waiting) { + // If all threads are waiting for the next tick or later yield + // for a tick as well. Otherwise yield until the next loop of + // the threads. + if ( + util.stackFrame.startedThreads + .every(thread => instance.runtime.isWaitingThread(thread)) + ) { + util.yieldTick(); + } else { + util.yield(); + } + } + } + + nextBackdrop () { + const stage = this.runtime.getTargetForStage(); + this._setBackdrop( + stage, stage.currentCostume + 1, true + ); + } + + previousBackdrop() { + const stage = this.runtime.getTargetForStage(); + this._setBackdrop( + stage, stage.currentCostume - 1, true + ); + } + + clampEffect (effect, value) { // used by compiler + let clampedValue = value; + switch (effect) { + case 'ghost': + clampedValue = MathUtil.clamp(value, + Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.min, + Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.max); + break; + case 'brightness': + clampedValue = MathUtil.clamp(value, + Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.min, + Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.max); + break; + } + return clampedValue; + } + + changeEffect (args, util) { + const effect = Cast.toString(args.EFFECT).toLowerCase(); + const change = Cast.toNumber(args.CHANGE); + if (!util.target.effects.hasOwnProperty(effect)) return; + let newValue = change + util.target.effects[effect]; + newValue = this.clampEffect(effect, newValue); + util.target.setEffect(effect, newValue); + } + + setEffect (args, util) { + const effect = Cast.toString(args.EFFECT).toLowerCase(); + let value = Cast.toNumber(args.VALUE); + value = this.clampEffect(effect, value); + util.target.setEffect(effect, value); + } + + clearEffects (args, util) { + util.target.clearEffects(); + this._resetBubbles(util.target); + } + + 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(); + } + } + } + + 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)); + } + } + } + + goTargetLayer (args, util) { + let target; + const option = args.VISIBLE_OPTION; + if (option === '_stage_') target = this.runtime.getTargetForStage(); + else target = this.runtime.getSpriteTargetByName(option); + if (!util.target.isStage && target) { + if (args.FORWARD_BACKWARD === 'infront') { + util.target.goBehindOther(target); + util.target.goForwardLayers(1); + } else { + util.target.goBehindOther(target); + } + } + } + + getSize (args, util) { + return Math.round(util.target.size); + } + + getBackdropNumberName (args) { + const stage = this.runtime.getTargetForStage(); + if (args.NUMBER_NAME === 'number') { + return stage.currentCostume + 1; + } + // Else return name + return stage.getCostumes()[stage.currentCostume].name; + } + + getCostumeNumberName (args, util) { + if (args.NUMBER_NAME === 'number') { + return util.target.currentCostume + 1; + } + // Else return name + return util.target.getCostumes()[util.target.currentCostume].name; + } +} + +module.exports = Scratch3LooksBlocks; diff --git a/local-scratch-vm/src/blocks/scratch3_motion.js b/local-scratch-vm/src/blocks/scratch3_motion.js new file mode 100644 index 0000000000000000000000000000000000000000..60e03c7ff91642ed4eee3ae419cdbea6971ba252 --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_motion.js @@ -0,0 +1,532 @@ +const Cast = require('../util/cast'); +const MathUtil = require('../util/math-util'); +const Timer = require('../util/timer'); + +const STAGE_ALIGNMENT = { + TOP_LEFT: 'top-left', + TOP_RIGHT: 'top-right', + BOTTOM_LEFT: 'bottom-left', + BOTTOM_RIGHT: 'bottom-right', + TOP: 'top', + LEFT: 'left', + RIGHT: 'right', + MIDDLE: 'middle', + BOTTOM: 'bottom' +}; + +class Scratch3MotionBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + motion_movesteps: this.moveSteps, + motion_movebacksteps: this.moveStepsBack, + motion_moveupdownsteps: this.moveStepsUpDown, + motion_gotoxy: this.goToXY, + motion_goto: this.goTo, + motion_turnright: this.turnRight, + motion_turnleft: this.turnLeft, + motion_turnrightaroundxy: this.turnRightAround, + motion_turnleftaroundxy: this.turnLeftAround, + motion_turnaround: this.turnAround, + motion_pointinrandomdirection: this.pointInDirectionRandom, + motion_pointtowardsxy: this.pointTowardsXY, + motion_pointindirection: this.pointInDirection, + motion_pointtowards: this.pointTowards, + motion_glidesecstoxy: this.glide, + motion_glideto: this.glideTo, + motion_ifonedgebounce: this.ifOnEdgeBounce, + motion_ifonxybounce: this.ifOnXYBounce, + motion_ifonspritebounce: this.ifOnSpriteBounce, + motion_setrotationstyle: this.setRotationStyle, + motion_changexby: this.changeX, + motion_setx: this.setX, + motion_changeyby: this.changeY, + motion_sety: this.setY, + motion_changebyxy: this.changeXY, + motion_xposition: this.getX, + motion_yposition: this.getY, + motion_direction: this.getDirection, + motion_move_sprite_to_scene_side: this.moveToStageSide, + // Legacy no-op blocks: + motion_scroll_right: () => {}, + motion_scroll_up: () => {}, + motion_align_scene: () => {}, + motion_xscroll: () => {}, + motion_yscroll: () => {} + }; + } + + moveToStageSide(args, util) { + if (!this.runtime.renderer) return; + const side = Cast.toString(args.ALIGNMENT); + const stageWidth = this.runtime.stageWidth / 2; + const stageHeight = this.runtime.stageHeight / 2; + const snap = []; + switch (side) { + case STAGE_ALIGNMENT.TOP: + util.target.setXY(0, stageHeight); + snap.push('top'); + break; + case STAGE_ALIGNMENT.LEFT: + util.target.setXY(0 - stageWidth, 0); + snap.push('left'); + break; + case STAGE_ALIGNMENT.MIDDLE: + util.target.setXY(0, 0); + break; + case STAGE_ALIGNMENT.RIGHT: + util.target.setXY(stageWidth, 0); + snap.push('right'); + break; + case STAGE_ALIGNMENT.BOTTOM: + util.target.setXY(0, 0 - stageHeight); + snap.push('bottom'); + break; + case STAGE_ALIGNMENT.TOP_LEFT: + util.target.setXY(0 - stageWidth, stageHeight); + snap.push('top'); + snap.push('left'); + break; + case STAGE_ALIGNMENT.TOP_RIGHT: + util.target.setXY(stageWidth, stageHeight); + snap.push('top'); + snap.push('right'); + break; + case STAGE_ALIGNMENT.BOTTOM_LEFT: + util.target.setXY(0 - stageWidth, 0 - stageHeight); + snap.push('bottom'); + snap.push('left'); + break; + case STAGE_ALIGNMENT.BOTTOM_RIGHT: + util.target.setXY(stageWidth, 0 - stageHeight); + snap.push('bottom'); + snap.push('right'); + break; + } + const drawableID = util.target.drawableID; + const drawable = this.runtime.renderer._allDrawables[drawableID]; + const boundingBox = drawable._skin.getFenceBounds(drawable); + snap.forEach(side => { + switch (side) { + case 'top': + util.target.setXY(util.target.x, boundingBox.bottom); + break; + case 'bottom': + util.target.setXY(util.target.x, boundingBox.top); + break; + case 'left': + util.target.setXY(boundingBox.right, util.target.y); + break; + case 'right': + util.target.setXY(boundingBox.left, util.target.y); + break; + } + }); + } + + getMonitored () { + return { + motion_xposition: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_xposition` + }, + motion_yposition: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_yposition` + }, + motion_direction: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_direction` + } + }; + } + + moveSteps (args, util) { + const steps = Cast.toNumber(args.STEPS); + this._moveSteps(steps, util.target); + } + moveStepsBack (args, util) { + const steps = Cast.toNumber(args.STEPS); + this._moveSteps(0 - steps, util.target); + } + moveStepsUpDown (args, util) { + const direction = Cast.toString(args.DIRECTION); + const steps = Cast.toNumber(args.STEPS); + this.turnLeft({ DEGREES: 90 }, util); + if (direction === 'up') { + this._moveSteps(steps, util.target); + } else if (direction === 'down') { + this._moveSteps(0 - steps, util.target); + } + this.turnRight({ DEGREES: 90 }, util); + } + _moveSteps (steps, target) { // used by compiler + const radians = MathUtil.degToRad(90 - target.direction); + const dx = steps * Math.cos(radians); + const dy = steps * Math.sin(radians); + target.setXY(target.x + dx, 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.stageWidth; + const stageHeight = this.runtime.stageHeight; + targetX = Math.round(stageWidth * (Math.random() - 0.5)); + targetY = Math.round(stageHeight * (Math.random() - 0.5)); + } else { + targetName = Cast.toString(targetName); + 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); + } + + turnRightAround (args, util) { + this.turnLeftAround({ + DEGREES: -Cast.toNumber(args.DEGREES), + X: Cast.toNumber(args.X), + Y: Cast.toNumber(args.Y) + }, util); + } + + turnLeftAround (args, util) { + const degrees = Cast.toNumber(args.DEGREES); + + const center = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y) + }; + const radians = (Math.PI * degrees) / 180; + const cos = Math.cos(radians); + const sin = Math.sin(radians); + const dx = util.target.x - center.x; + const dy = util.target.y - center.y; + const newPosition = { + x: (cos * dx) - (sin * dy) + center.x, + y: (cos * dy) + (sin * dx) + center.y + }; + util.target.setXY(newPosition.x, newPosition.y); + } + + pointInDirection (args, util) { + const direction = Cast.toNumber(args.DIRECTION); + util.target.setDirection(direction); + } + + turnAround (_, util) { + this.turnRight({ DEGREES: 180 }, util); + } + + pointInDirectionRandom (_, util) { + this.pointTowards({ TOWARDS: '_random_' }, util); + } + + pointTowardsXY (args, util) { + const targetX = Cast.toNumber(args.X); + const targetY = Cast.toNumber(args.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); + } + + pointTowards (args, util) { + let targetX = 0; + let targetY = 0; + if (args.TOWARDS === '_mouse_') { + targetX = util.ioQuery('mouse', 'getScratchX'); + targetY = util.ioQuery('mouse', 'getScratchY'); + } else if (args.TOWARDS === '_random_') { + util.target.setDirection(Math.round(Math.random() * 360) - 180); + return; + } else { + args.TOWARDS = Cast.toString(args.TOWARDS); + 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) { + this._ifOnEdgeBounce(util.target); + } + _ifOnEdgeBounce (target) { // used by compiler + const bounds = target.getBounds(); + if (!bounds) { + return; + } + // Measure distance to edges. + // Values are positive when the sprite is far away, + // and clamped to zero when the sprite is beyond. + const stageWidth = this.runtime.stageWidth; + const stageHeight = this.runtime.stageHeight; + 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 - 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; + target.setDirection(newDirection); + // Keep within the stage. + const fencedPosition = target.keepInFence(target.x, target.y); + target.setXY(fencedPosition[0], fencedPosition[1]); + } + ifOnXYBounce(args, util, _, __, ___, touchingCondition) { + const x = Cast.toNumber(args.X); + const y = Cast.toNumber(args.Y); + const target = util.target; + const bounds = target.getBounds(); + if (!bounds) { + return; + } + // Check to see if the point is inside the bounding box. + const xInBounds = (x >= bounds.left) && (x <= bounds.right); + const yInBounds = (y >= bounds.bottom) && (y <= bounds.top); + if (touchingCondition !== true) { + if (!(xInBounds && yInBounds)) { + return; // Not inside the bounding box. + } + } + // Find the distance to the point for all sides. + // We use this to figure out which side to bounce on. + let nearestEdge = ''; + let minDist = Infinity; + for (let i = 0; i < 4; i++) { + const sides = ['left', 'top', 'right', 'bottom']; + let distx; + let disty; + switch (sides[i]) { + case 'left': + case 'right': + distx = x - bounds[sides[i]]; + disty = y - target.y; + break; + case 'top': + case 'bottom': + distx = x - target.x; + disty = y - bounds[sides[i]]; + break; + } + const distance = Math.sqrt((distx * distx) + (disty * disty)); + if (distance < minDist) { + minDist = distance; + nearestEdge = sides[i]; + } + } + // Point away from the nearest edge. + const radians = MathUtil.degToRad(90 - 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; + target.setDirection(newDirection); + // Keep within the stage. + const fencedPosition = target.keepInFence(target.x, target.y); + target.setXY(fencedPosition[0], fencedPosition[1]); + } + ifOnSpriteBounce (args, util) { + if (args.SPRITE === '_mouse_') { + const x = util.ioQuery('mouse', 'getScratchX'); + const y = util.ioQuery('mouse', 'getScratchY'); + return this.ifOnXYBounce({ X: x, Y: y }, util); + } else if (args.SPRITE === '_random_') { + const stageWidth = this.runtime.stageWidth; + const stageHeight = this.runtime.stageHeight; + const x = Math.round(stageWidth * (Math.random() - 0.5)); + const y = Math.round(stageHeight * (Math.random() - 0.5)); + return this.ifOnXYBounce({ X: x, Y: y }, util); + } + const spriteName = Cast.toString(args.SPRITE); + const bounceTarget = this.runtime.getSpriteTargetByName(spriteName); + if (!bounceTarget) return; + const point = util.target.spriteTouchingPoint(spriteName); + if (!point) return; + return this.ifOnXYBounce({ X: point[0], Y: point[1] }, util); + + } + + setRotationStyle (args, util) { + util.target.setRotationStyle(args.STYLE); + } + + changeX (args, util) { + const dx = Cast.toNumber(args.DX); + util.target.setXY(util.target.x + dx, util.target.y); + } + + setX (args, util) { + const x = Cast.toNumber(args.X); + util.target.setXY(x, util.target.y); + } + + changeY (args, util) { + const dy = Cast.toNumber(args.DY); + util.target.setXY(util.target.x, util.target.y + dy); + } + + setY (args, util) { + const y = Cast.toNumber(args.Y); + util.target.setXY(util.target.x, y); + } + + changeXY (args, util) { + const dx = Cast.toNumber(args.DX); + const dy = Cast.toNumber(args.DY); + util.target.setXY(util.target.x + dx, util.target.y + dy); + } + + getX (args, util) { + return this.limitPrecision(util.target.x); + } + + getY (args, util) { + return this.limitPrecision(util.target.y); + } + + getDirection (args, util) { + return util.target.direction; + } + + // This corresponds to snapToInteger in Scratch 2 + limitPrecision (coordinate) { + const rounded = Math.round(coordinate); + const delta = coordinate - rounded; + const limitedCoord = (Math.abs(delta) < 1e-9) ? rounded : coordinate; + + return limitedCoord; + } +} + +module.exports = Scratch3MotionBlocks; diff --git a/local-scratch-vm/src/blocks/scratch3_operators.js b/local-scratch-vm/src/blocks/scratch3_operators.js new file mode 100644 index 0000000000000000000000000000000000000000..2ca2691304e2f8e4f1ba80dc3c844061bfe4854e --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_operators.js @@ -0,0 +1,412 @@ +const Cast = require('../util/cast.js'); +const MathUtil = require('../util/math-util.js'); +const SandboxRunner = require('../util/sandboxed-javascript-runner.js'); +const { validateRegex } = require('../util/json-block-utilities'); + +class Scratch3OperatorsBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + // + getPrimitives () { + return { + operator_add: this.add, + operator_subtract: this.subtract, + operator_multiply: this.multiply, + operator_divide: this.divide, + operator_power: this.power, + operator_lt: this.lt, + operator_equals: this.equals, + operator_notequal: this.notequals, + operator_gt: this.gt, + operator_ltorequal: this.ltorequal, + operator_gtorequal: this.gtorequal, + operator_and: this.and, + operator_nand: this.nand, + operator_nor: this.nor, + operator_xor: this.xor, + operator_xnor: this.xnor, + operator_or: this.or, + operator_not: this.not, + operator_random: this.random, + operator_join: this.join, + operator_join3: this.join3, + 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, + operator_advlog: this.advlog, + operator_regexmatch: this.regexmatch, + operator_replaceAll: this.replaceAll, + operator_replaceFirst: this.replaceFirst, + operator_getLettersFromIndexToIndexInText: this.getLettersFromIndexToIndexInText, + operator_getLettersFromIndexToIndexInTextFixed: this.getLettersFromIndexToIndexInTextFixed, + operator_readLineInMultilineText: this.readLineInMultilineText, + operator_newLine: this.newLine, + operator_tabCharacter: this.tabCharacter, + operator_stringify: this.stringify, + operator_boolify: this.boolify, + operator_lerpFunc: this.lerpFunc, + operator_advMath: this.advMath, + operator_advMathExpanded: this.advMathExpanded, + operator_constrainnumber: this.constrainnumber, + operator_trueBoolean: this.true, + operator_falseBoolean: this.false, + operator_randomBoolean: this.randomBoolean, + operator_indexOfTextInText: this.indexOfTextInText, + operator_lastIndexOfTextInText: this.lastIndexOfTextInText, + operator_toUpperLowerCase: this.toCase, + operator_character_to_code: this.charToCode, + operator_code_to_character: this.codeToChar, + operator_textStartsOrEndsWith: this.textStartsOrEndsWith, + operator_countAppearTimes: this.countAppearTimes, + operator_textIncludesLetterFrom: this.textIncludesLetterFrom, + operator_javascript_output: this.javascriptOutput, + operator_javascript_boolean: this.javascriptBoolean + }; + } + + + javascriptOutput (args) { + return new Promise((resolve, reject) => { + const js = Cast.toString(args.JS); + SandboxRunner.execute(js).then(result => { + resolve(result.value) + }) + }) + } + javascriptBoolean(args) { + return new Promise((resolve, reject) => { + const js = Cast.toString(args.JS); + SandboxRunner.execute(js).then(result => { + resolve(result.value === true) + }) + }) + } + + charToCode (args) { + const char = Cast.toString(args.ONE); + if (!char) return NaN; + return char.charCodeAt(0); + } + codeToChar (args) { + const code = Cast.toNumber(args.ONE); + return String.fromCharCode(code); + } + + toCase (args) { + const text = Cast.toString(args.TEXT); + switch (args.OPTION) { + case 'upper': + return text.toUpperCase(); + case 'lower': + return text.toLowerCase(); + } + } + + indexOfTextInText (args) { + const lookfor = Cast.toString(args.TEXT1); + const searchin = Cast.toString(args.TEXT2); + let index = 0; + if (searchin.includes(lookfor)) { + index = searchin.indexOf(lookfor) + 1; + } + return index; + } + + lastIndexOfTextInText (args) { + const lookfor = Cast.toString(args.TEXT1); + const searchin = Cast.toString(args.TEXT2); + let index = 0; + if (searchin.includes(lookfor)) { + index = searchin.lastIndexOf(lookfor) + 1; + } + return index; + } + + textStartsOrEndsWith (args) { + const text = Cast.toString(args.TEXT1); + const startsOrEnds = Cast.toString(args.OPTION); + const withh = Cast.toString(args.TEXT2); + return (startsOrEnds === "starts") ? (text.startsWith(withh)) : (text.endsWith(withh)); + } + + countAppearTimes (args) { + const text = Cast.toString(args.TEXT2); + const otherText = Cast.toString(args.TEXT1); + + const aray = text.split(otherText); + if (aray.length <= 1) { + return 0; + } + + return aray.length - 1; + } + + textIncludesLetterFrom (args) { + const text = Cast.toString(args.TEXT1); + const from = Cast.toString(args.TEXT2); + + let includes = false; + + const aray = from.split(""); + aray.forEach(i => { + if (text.includes(i)) includes = true; + }) + + return includes; + } + + true () { return true; } + false () { return false; } + randomBoolean () { return Boolean(Math.round(Math.random())); } + + constrainnumber (args) { + return Math.min(Math.max(args.min, args.inp), args.max); + } + + lerpFunc (args) { + const one = Cast.toNumber(args.ONE); + const two = Cast.toNumber(args.TWO); + const amount = Cast.toNumber(args.AMOUNT); + return ((two - one) * amount) + one; + } + advMath (args) { + const one = isNaN(Cast.toNumber(args.ONE)) ? 0 : Cast.toNumber(args.ONE); + const two = isNaN(Cast.toNumber(args.TWO)) ? 0 : Cast.toNumber(args.TWO); + const operator = Cast.toString(args.OPTION); + switch (operator) { + case "^": return one ** two; + case "root": return one ** 1 / two; + case "log": return Math.log(two) / Math.log(one); + default: return 0; + } + } + advMathExpanded (args) { + const one = Cast.toNumber(args.ONE); + const two = Cast.toNumber(args.TWO); + const three = Cast.toNumber(args.THREE); + const operator = Cast.toString(args.OPTION); + switch (operator) { + case "root": return one * Math.pow(three, 1 / two); + case "log": return one * Math.log(three) / Math.log(two); + default: return 0; + } + } + + stringify (args) { return Cast.toString(args.ONE); } + + boolify (args) { return Cast.toBoolean(args.ONE); } + + newLine () { return "\n"; } + + tabCharacter () { return "\t"; } + + readLineInMultilineText (args) { + const line = (Cast.toNumber(args.LINE) ? Cast.toNumber(args.LINE) : 1) - 1; + const text = Cast.toString(args.TEXT); + const readline = text.split("\n")[line] || ""; + return readline; + } + + getLettersFromIndexToIndexInTextFixed (args) { + const index1 = (Cast.toNumber(args.INDEX1) ? Cast.toNumber(args.INDEX1) : 1) - 1; + const index2 = (Cast.toNumber(args.INDEX2) ? Cast.toNumber(args.INDEX2) : 1); + const string = Cast.toString(args.TEXT); + const substring = string.substring(index1, index2); + return substring; + } + getLettersFromIndexToIndexInText (args) { + const index1 = (Cast.toNumber(args.INDEX1) ? Cast.toNumber(args.INDEX1) : 1) - 1; + const index2 = (Cast.toNumber(args.INDEX2) ? Cast.toNumber(args.INDEX2) : 1) - 1; + const string = Cast.toString(args.TEXT); + const substring = string.substring(index1, index2); + return substring; + } + + replaceAll (args) { + return Cast.toString(args.text).replaceAll(args.term, args.res); + } + replaceFirst (args) { + return Cast.toString(args.text).replace(args.term, args.res); + } + + regexmatch (args) { + if (!validateRegex(args.reg, args.regrule)) return "[]"; + const regex = new RegExp(args.reg, args.regrule); + const matches = args.text.match(regex); + return JSON.stringify(matches ? matches : []); + } + + 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); + } + + power (args) { + return Math.pow(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; + } + + notequals (args) { + return !this.equals(args); + } + + gt (args) { + return Cast.compare(args.OPERAND1, args.OPERAND2) > 0; + } + + gtorequal (args) { + return !this.lt(args); + } + + ltorequal (args) { + return !this.gt(args); + } + + and (args) { + return Cast.toBoolean(args.OPERAND1) && Cast.toBoolean(args.OPERAND2); + } + + nand (args) { + return !(Cast.toBoolean(args.OPERAND1) && Cast.toBoolean(args.OPERAND2)); + } + + nor (args) { + return !(Cast.toBoolean(args.OPERAND1) || Cast.toBoolean(args.OPERAND2)); + } + + xor (args) { + const op1 = Cast.toBoolean(args.OPERAND1); + const op2 = Cast.toBoolean(args.OPERAND2); + return (op1 ? !op2 : op2); + } + + xnor (args) { + return !this.xor(args); + } + + or (args) { + return Cast.toBoolean(args.OPERAND1) || Cast.toBoolean(args.OPERAND2); + } + + not (args) { + return !Cast.toBoolean(args.OPERAND); + } + + random (args) { + return this._random(args.FROM, args.TO); + } + _random (from, to) { // used by compiler + const nFrom = Cast.toNumber(from); + const nTo = Cast.toNumber(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(from) && Cast.isInt(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); + } + + join3 (args) { + return Cast.toString(args.STRING1) + Cast.toString(args.STRING2) + Cast.toString(args.STRING3); + } + + 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 uses floored division instead of truncated division. + 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 Math.round(Math.sin((Math.PI * n) / 180) * 1e10) / 1e10; + case 'cos': return Math.round(Math.cos((Math.PI * n) / 180) * 1e10) / 1e10; + 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 'log2': return Math.log2(n); + case 'e ^': return Math.exp(n); + case '10 ^': return Math.pow(10, n); + } + return 0; + } + + advlog (args) { + return (Math.log(Cast.toNumber(args.NUM2)) / Math.log(Cast.toNumber(args.NUM1))); + } +} + +module.exports = Scratch3OperatorsBlocks; diff --git a/local-scratch-vm/src/blocks/scratch3_procedures.js b/local-scratch-vm/src/blocks/scratch3_procedures.js new file mode 100644 index 0000000000000000000000000000000000000000..da368606eeff3987e615fb51cbe50e6f4b1eb47c --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_procedures.js @@ -0,0 +1,124 @@ +const Cast = require('../util/cast'); +class Scratch3ProcedureBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives() { + return { + procedures_definition: this.definition, + procedures_call: this.call, + procedures_set: this.set, + argument_reporter_string_number: this.argumentReporterStringNumber, + argument_reporter_boolean: this.argumentReporterBoolean, + argument_reporter_command: this.argumentReporterCommand + }; + } + + definition() { + // No-op: execute the blocks. + } + + call (args, util) { + if (!util.stackFrame.executed) { + const procedureCode = args.mutation.proccode; + const paramNamesIdsAndDefaults = util.getProcedureParamNamesIdsAndDefaults(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 (paramNamesIdsAndDefaults === null) { + return; + } + + const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults; + + // Initialize params for the current stackFrame to {}, even if the procedure does + // not take any arguments. This is so that `getParam` down the line does not look + // at earlier stack frames for the values of a given parameter (#1729) + util.initParams(); + for (let i = 0; i < paramIds.length; i++) { + if (args.hasOwnProperty(paramIds[i])) { + util.pushParam(paramNames[i], args[paramIds[i]]); + } else { + util.pushParam(paramNames[i], paramDefaults[i]); + } + } + + const addonBlock = util.runtime.getAddonBlock(procedureCode); + if (addonBlock) { + const result = addonBlock.callback(util.thread.getAllparams(), util); + if (util.thread.status === 1 /* STATUS_PROMISE_WAIT */) { + // If the addon block is using STATUS_PROMISE_WAIT to force us to sleep, + // make sure to not re-run this block when we resume. + util.stackFrame.executed = true; + } + return result; + } + + util.stackFrame.executed = true; + + util.startProcedure(procedureCode); + } + } + + set(args, util) { + const contain = util.thread.blockContainer; + const block = contain.getBlock(util.thread.isCompiled ? util.thread.peekStack() : util.thread.peekStackFrame().op.id); + if (!block) return; + const thread = util.thread; + const param = contain.getBlock(block.inputs.PARAM?.block); + if (param) { + try { + const curParams = thread.stackFrames[0].params; + if (curParams !== null) thread.stackFrames[0].params[param.fields.VALUE.value] = args.VALUE; + else thread.stackFrames[0].params = { [param.fields.VALUE.value]: args.VALUE } + } catch { /* shouldn't happen */ } + } + } + + argumentReporterStringNumber(args, util) { + const value = util.getParam(args.VALUE); + if (value === null) { + // When the parameter is not found in the most recent procedure + // call, the default is always 0. + return 0; + } + return value; + } + + argumentReporterBoolean(args, util) { + const value = util.getParam(args.VALUE); + if (value === null) { + // When the parameter is not found in the most recent procedure + // call, the default is always 0. + return 0; + } + return value; + } + + argumentReporterCommand(args, util) { + const branchInfo = util.getParam(args.VALUE) || {}; + if (branchInfo.entry === null) return; + const [branchId, target] = util.getBranchAndTarget( + branchInfo.callerId, + branchInfo.entry + ) || []; + if (branchId) { + // Push branch ID to the thread's stack. + util.thread.pushStack(branchId, target); + } else { + util.thread.pushStack(null); + } + } +} + +module.exports = Scratch3ProcedureBlocks; diff --git a/local-scratch-vm/src/blocks/scratch3_sensing.js b/local-scratch-vm/src/blocks/scratch3_sensing.js new file mode 100644 index 0000000000000000000000000000000000000000..5f25d2e4f77a383500a38f3fa8801049e75af43d --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_sensing.js @@ -0,0 +1,659 @@ +const Cast = require('../util/cast'); +const Timer = require('../util/timer'); +const MathUtil = require('../util/math-util'); +const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id'); +const { validateRegex } = require('../util/json-block-utilities'); + +class Scratch3SensingBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * The "answer" block value. + * @type {string} + */ + this._answer = ''; // used by compiler + + /** + * The timer utility. + * @type {Timer} + */ + this._timer = new Timer(); + + /** + * The stored microphone loudness measurement. + * @type {number} + */ + this._cachedLoudness = -1; + + /** + * The time of the most recent microphone loudness measurement. + * @type {number} + */ + this._cachedLoudnessTimestamp = 0; + + /** + * The list of loudness values to determine the average. + * @type {!Array} + */ + this._loudnessList = []; + + /** + * 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)); + this.runtime.on('STOP_FOR_TARGET', this._clearTargetQuestions.bind(this)); + this.runtime.on('RUNTIME_DISPOSED', this._resetAnswer.bind(this)); + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + sensing_objecttouchingobject: this.objectTouchingObject, + sensing_objecttouchingclonesprite: this.objectTouchingCloneOfSprite, + 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_loud: this.isLoud, + sensing_askandwait: this.askAndWait, + sensing_answer: this.getAnswer, + sensing_username: this.getUsername, + sensing_loggedin: this.getLoggedIn, + sensing_userid: () => {}, // legacy no-op block + sensing_regextest: this.regextest, + sensing_thing_is_number: this.thing_is_number, + sensing_thing_has_number: this.thing_has_number, + sensing_mobile: this.mobile, + sensing_thing_is_text: this.thing_is_text, + sensing_getspritewithattrib: this.getspritewithattrib, + sensing_directionTo: this.getDirectionToFrom, + sensing_distanceTo: this.getDistanceToFrom, + sensing_isUpperCase: this.isCharecterUppercase, + sensing_mouseclicked: this.mouseClicked, + sensing_keyhit: this.keyHit, + sensing_mousescrolling: this.mouseScrolling, + sensing_fingerdown: this.fingerDown, + sensing_fingertapped: this.fingerTapped, + sensing_fingerx: this.getFingerX, + sensing_fingery: this.getFingerY, + sensing_setclipboard: this.setClipboard, + sensing_getclipboard: this.getClipboard, + sensing_getdragmode: this.getDragMode, + sensing_getoperatingsystem: this.getOS, + sensing_getbrowser: this.getBrowser, + sensing_geturl: this.getUrl, + sensing_getxyoftouchingsprite: this.getXYOfTouchingSprite + }; + } + + getOS () { + if (!('userAgent' in navigator)) return 'Unknown'; + const agent = navigator.userAgent; + if (agent.includes('Windows')) { + return 'Windows'; + } + if (agent.includes('Android')) { + return 'Android'; + } + if (agent.includes('iPad') || agent.includes('iPod') || agent.includes('iPhone')) { + return 'iOS'; + } + if (agent.includes('Linux')) { + return 'Linux'; + } + if (agent.includes('CrOS')) { + return 'ChromeOS'; + } + if (agent.includes('Mac OS')) { + return 'MacOS'; + } + return 'Unknown'; + } + getBrowser () { + if (!('userAgent' in navigator)) return 'Unknown'; + const agent = navigator.userAgent; + if ('userAgentData' in navigator) { + const agentData = JSON.stringify(navigator.userAgentData.brands); + if (agentData.includes('Google Chrome')) { + return 'Chrome'; + } + if (agentData.includes('Opera')) { + return 'Opera'; + } + if (agentData.includes('Microsoft Edge')) { + return 'Edge'; + } + } + if (agent.includes('Chrome')) { + return 'Chrome'; + } + if (agent.includes('Firefox')) { + return 'Firefox'; + } + // PenguinMod cannot be loaded in IE 11 (the last supported version) + // if (agent.includes('MSIE') || agent.includes('rv:')) { + // return 'Internet Explorer'; + // } + if (agent.includes('Safari')) { + return 'Safari'; + } + return 'Unknown'; + } + getUrl () { + if (!('href' in location)) return ''; + return location.href; + } + + setClipboard (args) { + const text = Cast.toString(args.ITEM); + if (!navigator) return; + if (('clipboard' in navigator) && ('writeText' in navigator.clipboard)) { + navigator.clipboard.writeText(text); + } + } + getClipboard () { + if (!navigator) return ''; + if (('clipboard' in navigator) && ('readText' in navigator.clipboard)) { + return navigator.clipboard.readText(); + } else { + return ''; + } + } + + getDragMode (_, util) { + return util.target.draggable; + } + + mouseClicked (_, util) { + return util.ioQuery('mouse', 'getIsClicked'); + } + keyHit (args, util) { + return util.ioQuery('keyboard', 'getKeyIsHit', [args.KEY_OPTION]); + } + mouseScrolling (args, util) { + const delta = util.ioQuery('mouseWheel', 'getScrollDelta'); + const option = args.SCROLL_OPTION; + switch (option) { + case "up": + return delta < 0; + case "down": + return delta > 0; + default: + return false; + } + } + + isCharecterUppercase (args) { + return (/[A-Z]/g).test(args.text); + } + + getDirectionToFrom (args) { + const dx = args.x2 - args.x1; + const dy = args.y2 - args.y1; + const direction = MathUtil.wrapClamp(90 - MathUtil.radToDeg(Math.atan2(dy, dx)), -179, 180); + return direction; + } + + getDistanceToFrom (args) { + const dx = args.x2 - args.x1; + const dy = args.y2 - args.y1; + return Math.sqrt((dx * dx) + (dy * dy)); + } + + getspritewithattrib (args, util) { + // strip out usless data + const sprites = util.runtime.targets.map(x => ({ + id: x.id, + name: x.sprite ? x.sprite.name : "Unknown", + variables: Object.values(x.variables).reduce((obj, value) => { + if (!value.name) return obj; + obj[value.name] = String(value.value); + return obj; + }, {}) + })); + // get the target with variable x set to y + let res = "No sprites found"; + for ( + // define the index and the sprite + let idx = 1, sprite = sprites[0]; + // standard for loop thing + idx < sprites.length; + // set sprite to a new item + sprite = sprites[idx++] + ) { + if (sprite.variables[args.var] === args.val) { + res = `{"id": "${sprite.id}", "name": "${sprite.name}"}`; + break; + } + } + + return res; + } + thing_is_number (args) { + // i hate js + // i also hate regex + // so im gonna do this the lazy way + // no. String(Number(value)) === value does infact do the job X) + // also what was originaly here was inificiant as hell + + // jg: why dont you literally just do what "is text" did but the opposite + // except also account for numbers that end with . (that aint a number) + if (Cast.toString(args.TEXT1).trim().endsWith(".")) { + return false; + } + return !this.thing_is_text(args); + } + thing_is_text (args) { + // WHY IS NAN NOT EQUAL TO ITSELF + // HOW IS NAN A NUMBER + // because nan is how numbers say the value put into me is not a number + return isNaN(Number(args.TEXT1)); + } + + thing_has_number(args) { + return /\d/.test(Cast.toString(args.TEXT1)); + } + + mobile () { + return typeof window !== 'undefined' && 'ontouchstart' in window; + } + + regextest (args) { + if (!validateRegex(args.reg, args.regrule)) return false; + const regex = new RegExp(args.reg, args.regrule); + return regex.test(args.text); + } + + getMonitored () { + return { + sensing_answer: { + getId: () => 'answer' + }, + sensing_mousedown: { + getId: () => 'mousedown' + }, + sensing_mouseclicked: { + getId: () => 'mouseclicked' + }, + sensing_mousex: { + getId: () => 'mousex' + }, + sensing_mousey: { + getId: () => 'mousey' + }, + sensing_getclipboard: { + getId: () => 'getclipboard' + }, + sensing_getdragmode: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_getdragmode` + }, + sensing_loudness: { + getId: () => 'loudness' + }, + sensing_loud: { + getId: () => 'loud' + }, + sensing_timer: { + getId: () => 'timer' + }, + sensing_dayssince2000: { + getId: () => 'dayssince2000' + }, + sensing_current: { + // This is different from the default toolbox xml id in order to support + // importing multiple monitors from the same opcode from sb2 files, + // something that is not currently supported in scratch 3. + getId: (_, fields) => getMonitorIdForBlockWithArgs('current', fields) // _${param}` + }, + sensing_loggedin: { + getId: () => 'loggedin' + }, + }; + } + + _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); + } + + _clearTargetQuestions (stopTarget) { + const currentlyAsking = this._questionList.length > 0 && this._questionList[0][2] === stopTarget; + this._questionList = this._questionList.filter(question => ( + question[2] !== stopTarget + )); + + if (currentlyAsking) { + this.runtime.emit('SAY', stopTarget, 'say', ''); + if (this._questionList.length > 0) { + this._askNextQuestion(); + } else { + 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; + } + + objectTouchingObject (args, util) { + const object1 = (args.FULLTOUCHINGOBJECTMENU) === "_myself_" ? util.target.getName() : args.FULLTOUCHINGOBJECTMENU; + const object2 = args.SPRITETOUCHINGOBJECTMENU; + if (object2 === "_myself_") { + return util.target.isTouchingObject(object1); + } + const target = this.runtime.getSpriteTargetByName(object2); + if (!target) return false; + return target.isTouchingObject(object1); + } + objectTouchingCloneOfSprite (args, util) { + const object1 = args.FULLTOUCHINGOBJECTMENU; + let object2 = args.SPRITETOUCHINGOBJECTMENU; + if (object2 === "_myself_") { + object2 = util.target.getName(); + } + if (object1 === "_myself_") { + return util.target.isTouchingObject(object2, true); + } + + const target = this.runtime.getSpriteTargetByName(object2); + if (!target) return false; + if (object1 === "_mouse_") { + if (!this.runtime.ioDevices.mouse) return false; + const mouseX = this.runtime.ioDevices.mouse.getClientX(); + const mouseY = this.runtime.ioDevices.mouse.getClientY(); + const clones = target.sprite.clones.filter(clone => !clone.isOriginal && clone.isTouchingPoint(mouseX, mouseY)); + return clones.length > 0; + } else if (object1 === '_edge_') { + const clones = target.sprite.clones.filter(clone => !clone.isOriginal && clone.isTouchingEdge()); + return clones.length > 0; + } + + const originalSprite = this.runtime.getSpriteTargetByName(object1); + if (!originalSprite) return false; + return originalSprite.isTouchingObject(object2, true); + } + + touchingObject (args, util) { + return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU); + } + + getXYOfTouchingSprite (args, util) { + const object = args.SPRITE; + if (object === '_mouse_') { + // we can just return mouse pos + // if mouse is touching us, the mouse size is practically 1x1 anyways + const x = util.ioQuery('mouse', 'getScratchX'); + const y = util.ioQuery('mouse', 'getScratchY'); + if (args.XY === 'y') return y; + return x; + } + const point = util.target.spriteTouchingPoint(object); + if (!point) return ''; + if (args.XY === 'y') return point[1]; + return point[0]; + } + + 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 { + args.DISTANCETOMENU = Cast.toString(args.DISTANCETOMENU); + 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'); + } + + getFingerX (args, util) { + return util.ioQuery('touch', 'getScratchX', [Cast.toNumber(args.FINGER_OPTION) - 1]); + } + + getFingerY (args, util) { + return util.ioQuery('touch', 'getScratchY', [Cast.toNumber(args.FINGER_OPTION) - 1]); + } + + fingerDown (args, util) { + return util.ioQuery('touch', 'getIsDown', [Cast.toNumber(args.FINGER_OPTION) - 1]); + } + + fingerTapped (args, util) { + return util.ioQuery('touch', 'getIsTapped', [Cast.toNumber(args.FINGER_OPTION) - 1]); + } + + 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; + if (this.runtime.currentStepTime === null) return -1; + + // Only measure loudness once per step + const timeSinceLoudness = this._timer.time() - this._cachedLoudnessTimestamp; + if (timeSinceLoudness < this.runtime.currentStepTime) { + return this._cachedLoudness; + } + + this._cachedLoudnessTimestamp = this._timer.time(); + this._cachedLoudness = this.runtime.audioEngine.getLoudness(); + this.pushLoudness(this._cachedLoudness); + return this._cachedLoudness; + } + + isLoud () { + this.pushLoudness(); + let sum = this._loudnessList.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + sum /= this._loudnessList.length; + return this.getLoudness() > sum + 15; + } + pushLoudness (value) { + if (this._loudnessList.length >= 30) this._loudnessList.shift(); // remove first item + this._loudnessList.push(value ?? this.getLoudness()); + } + + getAttributeOf (args) { + let attrTarget; + + if (args.OBJECT === '_stage_') { + attrTarget = this.runtime.getTargetForStage(); + } else { + args.OBJECT = Cast.toString(args.OBJECT); + attrTarget = this.runtime.getSpriteTargetByName(args.OBJECT); + } + + // attrTarget can be undefined if the target does not exist + // (e.g. single sprite uploaded from larger project referencing + // another sprite that wasn't uploaded) + if (!attrTarget) return 0; + + // 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.getCostumes()[attrTarget.currentCostume].name; + case 'volume': return attrTarget.volume; + } + } 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.getCostumes()[attrTarget.currentCostume].name; + case 'layer': return attrTarget.getLayerOrder(); + case 'size': return attrTarget.size; + case 'volume': return attrTarget.volume; + } + } + + // Target variables. + const varName = args.PROPERTY; + const variable = attrTarget.lookupVariableByNameAndType(varName, '', true); + if (variable) { + return variable.value; + } + + // Otherwise, 0 + return 0; + } + + getUsername (args, util) { + return util.ioQuery('userData', 'getUsername'); + } + + getLoggedIn(args, util) { + return util.ioQuery('userData', 'getLoggedIn'); + } +} + +module.exports = Scratch3SensingBlocks; diff --git a/local-scratch-vm/src/blocks/scratch3_sound.js b/local-scratch-vm/src/blocks/scratch3_sound.js new file mode 100644 index 0000000000000000000000000000000000000000..edf593b61f327115b49494937e74dd330924016d --- /dev/null +++ b/local-scratch-vm/src/blocks/scratch3_sound.js @@ -0,0 +1,537 @@ +const MathUtil = require('../util/math-util'); +const Cast = require('../util/cast'); +const Clone = require('../util/clone'); +const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id'); + +/** + * Occluded boolean value to make its use more understandable. + * @const {boolean} + */ +const STORE_WAITING = true; + +class Scratch3SoundBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.waitingSounds = {}; + + // Clear sound effects on green flag and stop button events. + this.stopAllSounds = this.stopAllSounds.bind(this); + this._stopWaitingSoundsForTarget = this._stopWaitingSoundsForTarget.bind(this); + this._clearEffectsForAllTargets = this._clearEffectsForAllTargets.bind(this); + if (this.runtime) { + this.runtime.on('PROJECT_STOP_ALL', this.stopAllSounds); + this.runtime.on('PROJECT_STOP_ALL', this._clearEffectsForAllTargets); + this.runtime.on('STOP_FOR_TARGET', this._stopWaitingSoundsForTarget); + 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 { + 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: -360, max: 360}, // -3 to 3 octaves + pan: {min: -100, max: 100} // 100% left to 100% right + }; + } + + /** The minimum and maximum values for sound effects when miscellaneous limits are removed. */ + static get LARGER_EFFECT_RANGE () { + return { + // scratch-audio throws if pitch is too big because some math results in Infinity + pitch: {min: -1000, max: 1000}, + + // No reason for these to go beyond 100 + pan: {min: -100, max: 100} + }; + } + + /** + * @param {Target} target - collect sound state for this target. + * @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary. + * @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); + target.soundEffects = soundState.effects; + } + 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.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + sound_play: this.playSound, + sound_playallsounds: this.playSoundAllLolOpAOIUHFoiubea87fge87iufwhef87wye87fn, + sound_playuntildone: this.playSoundAndWait, + sound_stop: this.stopSpecificSound, + 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, + sound_isSoundPlaying: this.isSoundPlaying, + sound_getEffectValue: this.getEffectValue, + sound_getLength: this.getLength, + sound_set_stop_fadeout_to: this.setStopFadeout, + sound_play_at_seconds: this.playAtSeconds, + sound_play_at_seconds_until_done: this.playAtSecondsAndWait, + sound_getSoundVolume: this.currentSoundVolume + }; + } + + getMonitored () { + return { + sound_volume: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_volume` + }, + sound_getEffectValue: { + isSpriteSpecific: true, + getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_getEffectValue`, fields) + }, + }; + } + + currentSoundVolume (args, util) { + + } + + playAtSeconds (args, util) { + const seconds = Cast.toNumber(args.VALUE); + if (seconds < 0) { + return; + } + + this._playSoundAtTimePosition({ + sound: Cast.toString(args.SOUND_MENU), + seconds: seconds + }, util, STORE_WAITING); + } + playAtSecondsAndWait (args, util) { + // return promise + const seconds = Cast.toNumber(args.VALUE); + if (seconds < 0) { + return; + } + + return this._playSoundAtTimePosition({ + sound: Cast.toString(args.SOUND_MENU), + seconds: seconds + }, util, STORE_WAITING); + } + + _playSoundAtTimePosition ({ sound, seconds }, util, storeWaiting) { + const index = this._getSoundIndex(sound, util); + if (index >= 0) { + const {target} = util; + const {sprite} = target; + const {soundId} = sprite.sounds[index]; + if (sprite.soundBank) { + if (storeWaiting === STORE_WAITING) { + this._addWaitingSound(target.id, soundId); + } else { + this._removeWaitingSound(target.id, soundId); + } + return sprite.soundBank.playSound(target, soundId, seconds); + } + } + } + + setStopFadeout (args, util) { + const id = Cast.toString(args.SOUND_MENU); + const index = this._getSoundIndex(id, util); + if (index < 0) return; + + const target = util.target; + const sprite = target.sprite; + if (!sprite) return; + if (!sprite.sounds) return; + + const { soundId } = sprite.sounds[index]; + + const soundBank = sprite.soundBank + if (!soundBank) return; + + const decayTime = Cast.toNumber(args.VALUE); + if (decayTime <= 0) { + soundBank.soundPlayers[soundId].stopFadeDecay = 0; + return; + } + + soundBank.soundPlayers[soundId].stopFadeDecay = decayTime; + } + + getEffectValue (args, util) { + const target = util.target; + + const effects = target.soundEffects; + if (!effects) return 0; + + const effect = Cast.toString(args.EFFECT).toLowerCase(); + if (!effects.hasOwnProperty(effect)) return 0; + const value = Cast.toNumber(effects[effect]); + + return value; + } + + isSoundPlaying (args, util) { + const index = this._getSoundIndex(args.SOUND_MENU, util); + if (index < 0) return false; + + const target = util.target; + const sprite = target.sprite; + if (!sprite) return false; + + const { soundId } = sprite.sounds[index]; + + const soundBank = sprite.soundBank + if (!soundBank) return false; + const players = soundBank.soundPlayers; + if (!players) return false; + if (!players.hasOwnProperty(soundId)) return false; + + return players[soundId].isPlaying == true; + } + + getLength (args, util) { + const index = this._getSoundIndex(args.SOUND_MENU, util); + if (index < 0) return 0; + + const target = util.target; + const sprite = target.sprite; + if (!sprite) return 0; + + const { soundId } = sprite.sounds[index]; + + const soundBank = sprite.soundBank + if (!soundBank) return 0; + const players = soundBank.soundPlayers; + if (!players) return 0; + if (!players.hasOwnProperty(soundId)) return 0; + const buffer = players[soundId].buffer; + if (!buffer) return 0; + + return Cast.toNumber(buffer.duration); + } + + stopSpecificSound (args, util) { + const index = this._getSoundIndex(args.SOUND_MENU, util); + if (index < 0) return; + + const target = util.target; + const sprite = target.sprite; + if (!sprite) return; + + const { soundId } = sprite.sounds[index]; + + const soundBank = sprite.soundBank + if (!soundBank) return; + + soundBank.stop(target, soundId); + } + + playSound (args, util) { + // Don't return the promise, it's the only difference for AndWait + this._playSound(args, util); + } + + playSoundAndWait (args, util) { + return this._playSound(args, util, STORE_WAITING); + } + + _playSound (args, util, storeWaiting) { + const index = this._getSoundIndex(args.SOUND_MENU, util); + if (index >= 0) { + const {target} = util; + const {sprite} = target; + const {soundId} = sprite.sounds[index]; + if (sprite.soundBank) { + if (storeWaiting === STORE_WAITING) { + this._addWaitingSound(target.id, soundId); + } else { + this._removeWaitingSound(target.id, soundId); + } + return sprite.soundBank.playSound(target, soundId); + } + } + } + + _addWaitingSound (targetId, soundId) { + if (!this.waitingSounds[targetId]) { + this.waitingSounds[targetId] = new Set(); + } + this.waitingSounds[targetId].add(soundId); + } + + _removeWaitingSound (targetId, soundId) { + if (!this.waitingSounds[targetId]) { + return; + } + this.waitingSounds[targetId].delete(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]); + } + } + + playSoundAllLolOpAOIUHFoiubea87fge87iufwhef87wye87fn (_, util) { + const target = util.target; + const sprite = target.sprite; + if (!sprite) return; + for (let i = 0; i < sprite.sounds.length; i++) { + const { soundId } = sprite.sounds[i]; + if (sprite.soundBank) { + sprite.soundBank.playSound(target, soundId); + } + } + } + + _stopAllSoundsForTarget (target) { + if (target.sprite.soundBank) { + target.sprite.soundBank.stopAllSounds(target); + if (this.waitingSounds[target.id]) { + this.waitingSounds[target.id].clear(); + } + } + } + + _stopWaitingSoundsForTarget (target) { + if (target.sprite.soundBank) { + if (this.waitingSounds[target.id]) { + for (const soundId of this.waitingSounds[target.id].values()) { + target.sprite.soundBank.stop(target, soundId); + } + this.waitingSounds[target.id].clear(); + } + } + } + + setEffect (args, util) { + return this._updateEffect(args, util, false); + } + + changeEffect (args, util) { + return 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 miscLimits = this.runtime.runtimeOptions.miscLimits; + const {min, max} = miscLimits ? + Scratch3SoundBlocks.EFFECT_RANGE[effect] : + Scratch3SoundBlocks.LARGER_EFFECT_RANGE[effect]; + soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], min, max); + + this._syncEffectsForTarget(util.target); + if (miscLimits) { + // Yield until the next tick. + return Promise.resolve(); + } + + // Requesting a redraw makes sure that "forever: change pitch by 1" still work but without + // yielding unnecessarily in other cases + this.runtime.requestRedraw(); + } + + _syncEffectsForTarget (target) { + if (!target || !target.sprite.soundBank) return; + target.soundEffects = this._getSoundState(target).effects; + + target.sprite.soundBank.setEffects(target); + } + + 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; + } + this._syncEffectsForTarget(target); + } + + _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); + return this._updateVolume(volume, util.target); + } + + changeVolume (args, util) { + const volume = Cast.toNumber(args.VOLUME) + util.target.volume; + return this._updateVolume(volume, util.target); + } + + _updateVolume (volume, target) { + volume = MathUtil.clamp(volume, 0, 100); + target.volume = volume; + this._syncEffectsForTarget(target); + + if (this.runtime.runtimeOptions.miscLimits) { + // Yield until the next tick. + return Promise.resolve(); + } + this.runtime.requestRedraw(); + } + + getVolume (args, util) { + return util.target.volume; + } + + soundsMenu (args) { + return args.SOUND_MENU; + } + + beatsMenu (args) { + return args.BEATS; + } + + effectsMenu (args) { + return args.EFFECT; + } +} + +module.exports = Scratch3SoundBlocks; diff --git a/local-scratch-vm/src/cli/index.js b/local-scratch-vm/src/cli/index.js new file mode 100644 index 0000000000000000000000000000000000000000..925411620992b4806d7b74f492e3ee715752c887 --- /dev/null +++ b/local-scratch-vm/src/cli/index.js @@ -0,0 +1,41 @@ +const fs = require('fs'); +const VirtualMachine = require('../index'); + +/* eslint-env node */ +/* eslint-disable no-console */ + +const file = process.argv[2]; +if (!file) { + throw new Error('Invalid file'); +} + +const runProject = async buffer => { + const vm = new VirtualMachine(); + vm.runtime.on('SAY', (target, type, text) => { + console.log(text); + }); + vm.setCompatibilityMode(true); + vm.clear(); + await vm.loadProject(buffer); + vm.start(); + vm.greenFlag(); + await new Promise(resolve => { + const interval = setInterval(() => { + let active = 0; + const threads = vm.runtime.threads; + for (let i = 0; i < threads.length; i++) { + if (!threads[i].updateMonitor) { + active += 1; + } + } + if (active === 0) { + clearInterval(interval); + resolve(); + } + }, 50); + }); + vm.stopAll(); + vm.stop(); +}; + +runProject(fs.readFileSync(file)); diff --git a/local-scratch-vm/src/compiler/compat-block-utility.js b/local-scratch-vm/src/compiler/compat-block-utility.js new file mode 100644 index 0000000000000000000000000000000000000000..5cf95a855b43439f83339b212c14550a7f09d7f1 --- /dev/null +++ b/local-scratch-vm/src/compiler/compat-block-utility.js @@ -0,0 +1,62 @@ +const BlockUtility = require('../engine/block-utility'); + +class CompatibilityLayerBlockUtility extends BlockUtility { + constructor () { + super(); + this._startedBranch = null; + } + + get stackFrame () { + return this.thread.compatibilityStackFrame; + } + + startBranch (branchNumber, isLoop, onEnd) { + if (this._branchInfo && onEnd) this._branchInfo.onEnd.push(onEnd); + this._startedBranch = [branchNumber, isLoop]; + } + + /** + * runs any given procedure + * @param {String} proccode the procedure to start + * @param {Object} args + * @returns the return value of the procedure, returns undefined if statement + */ + startProcedure (proccode, args) { + if (!args) + return this.thread.procedures[proccode](); + if (!(typeof args === 'object')) + throw new Error(`procedure arguments can only be of type undefined|object. instead got "${typeof args}"`); + let evaluate = `this.thread.procedures[proccode](`; + const inputs = []; + for (const arg in args) { + inputs.push(String(args[arg])); + } + evaluate += `${inputs.join(',')})`; + return new Function(`Procedure ${proccode}`, evaluate)(); + } + + /* + // Parameters are not used by compiled scripts. + initParams () { + throw new Error('initParams is not supported by this BlockUtility'); + } + pushParam () { + throw new Error('pushParam is not supported by this BlockUtility'); + } + getParam () { + throw new Error('getParam is not supported by this BlockUtility'); + } + */ + + init (thread, fakeBlockId, stackFrame, branchInfo) { + this.thread = thread; + this.sequencer = thread.target.runtime.sequencer; + this._startedBranch = null; + this._branchInfo = branchInfo; + thread.stack[0] = fakeBlockId; + thread.compatibilityStackFrame = stackFrame; + } +} + +// Export a single instance to be reused. +module.exports = new CompatibilityLayerBlockUtility(); diff --git a/local-scratch-vm/src/compiler/compat-blocks.js b/local-scratch-vm/src/compiler/compat-blocks.js new file mode 100644 index 0000000000000000000000000000000000000000..f78413fd28d0ad98191235394fa930525973ed22 --- /dev/null +++ b/local-scratch-vm/src/compiler/compat-blocks.js @@ -0,0 +1,170 @@ +/** + * @fileoverview List of blocks to be supported in the compiler compatibility layer. + * This is only for native blocks. Extensions should not be listed here. + */ + +// Please keep these lists alphabetical. +// no >:( +// haha cry about it - jerem + +const statementBlocks = [ + 'looks_hideallsprites', + 'looks_say', + 'looks_sayforsecs', + 'looks_setstretchto', + 'looks_switchbackdroptoandwait', + 'looks_think', + 'looks_thinkforsecs', + 'motion_align_scene', + 'motion_glidesecstoxy', + 'motion_glideto', + 'motion_goto', + 'motion_pointtowards', + 'motion_scroll_right', + 'motion_scroll_up', + 'sensing_askandwait', + 'sensing_setdragmode', + 'sound_changeeffectby', + 'sound_changevolumeby', + 'sound_cleareffects', + 'sound_play', + 'sound_playuntildone', + 'sound_stop', + 'sound_seteffectto', + 'sound_setvolumeto', + 'sound_stopallsounds', + "looks_setStretch", + "looks_changeStretch", + "data_reverselist", + "data_arraylist", + "control_switch", + "control_switch_default", + "control_case", + "control_exitCase", + "control_case_next", + "control_backToGreenFlag", + 'looks_setHorizTransform', + 'looks_setVertTransform', + 'looks_layersSetLayer', + 'control_waitsecondsoruntil', + 'control_delete_clones_of', + 'control_stop_sprite', + 'looks_changeVisibilityOfSprite', + 'looks_previouscostume', + 'looks_previousbackdrop', + 'motion_pointinrandomdirection', + 'motion_move_sprite_to_scene_side', + 'sound_playallsounds', + 'looks_stoptalking', + 'sensing_setclipboard', + 'motion_movebacksteps', + 'motion_moveupdownsteps', + 'motion_turnrightaroundxy', + 'motion_turnleftaroundxy', + 'motion_turnaround', + 'motion_pointinrandomdirection', + 'motion_pointtowardsxy', + 'motion_glidedirectionstepsinseconds', + 'motion_changebyxy', + 'motion_ifonspritebounce', + 'motion_ifonxybounce', + 'motion_move_sprite_to_scene_side', + 'control_javascript_command', + 'looks_changeVisibilityOfSpriteShow', + 'looks_changeVisibilityOfSpriteHide', + 'sound_pause', + 'sound_set_stop_fadeout_to', + 'sound_play_at_seconds', + 'sound_play_at_seconds_until_done', + 'sound_pauseallsounds', + 'argument_reporter_command' +]; + +const outputBlocks = [ + 'motion_xscroll', + 'motion_yscroll', + 'sensing_loud', + 'sensing_loudness', + 'sensing_userid', + 'sound_volume', + "control_if_return_else_return", + "looks_stretchGetX", + "looks_stretchGetY", + "sensing_getspritewithattrib", + "sensing_thing_is_text", + "sensing_mobile", + "sensing_thing_is_number", + "sensing_regextest", + "operator_indexOfTextInText", + "operator_constrainnumber", + "operator_advMath", + "operator_advMathExpanded", + "operator_lerpFunc", + "operator_stringify", + "operator_newLine", + "operator_readLineInMultilineText", + "operator_getLettersFromIndexToIndexInText", + "operator_getLettersFromIndexToIndexInTextFixed", + "operator_replaceAll", + "operator_regexmatch", + "data_itemexistslist", + "data_listisempty", + "data_listarray", + "looks_sayHeight", + "looks_sayWidth", + "sensing_isUpperCase", + "operator_toUpperLowerCase", + "looks_getSpriteVisible", + "looks_getEffectValue", + 'looks_layersGetLayer', + 'sound_isSoundPlaying', + 'sound_getEffectValue', + 'sound_getLength', + "sensing_directionTo", + "sensing_distanceTo", + "operator_boolify", + "operator_tabCharacter", + "operator_character_to_code", + "operator_code_to_character", + "sensing_keyhit", + "sensing_mousescrolling", + "sensing_mouseclicked", + "sensing_thing_has_text", + "sensing_thing_has_number", + "sensing_objecttouchingobject", + "sensing_objecttouchingclonesprite", + 'looks_getOtherSpriteVisible', + 'operator_gtorequal', + 'operator_ltorequal', + 'operator_notequal', + 'operator_join3', + 'operator_replaceFirst', + 'operator_lastIndexOfTextInText', + 'operator_countAppearTimes', + 'operator_textIncludesLetterFrom', + 'operator_textStartsOrEndsWith', + 'sensing_fingerdown', + 'sensing_fingertapped', + 'sensing_fingerx', + 'sensing_fingery', + 'sensing_getclipboard', + 'sensing_getdragmode', + 'sensing_getoperatingsystem', + 'sensing_getbrowser', + 'sensing_geturl', + 'operator_javascript_output', + 'operator_javascript_boolean', + 'sensing_getxyoftouchingsprite', + 'operator_nand', + 'operator_nor', + 'operator_xor', + 'operator_xnor', + 'looks_getinputofcostume', + 'sound_getTimePosition', + 'sound_getSoundVolume' +]; + +module.exports = { + statementBlocks, + outputBlocks +}; diff --git a/local-scratch-vm/src/compiler/compile.js b/local-scratch-vm/src/compiler/compile.js new file mode 100644 index 0000000000000000000000000000000000000000..fb43164504205662ce48900c2dcd426d37adbfb5 --- /dev/null +++ b/local-scratch-vm/src/compiler/compile.js @@ -0,0 +1,37 @@ +const IRGenerator = require('./irgen'); +const JSGenerator = require('./jsgen'); + +const compile = thread => { + const irGenerator = new IRGenerator(thread); + const ir = irGenerator.generate(); + + const procedures = {}; + const target = thread.target; + + const compileScript = script => { + if (script.cachedCompileResult) { + return script.cachedCompileResult; + } + + const compiler = new JSGenerator(script, ir, target); + const result = compiler.compile(); + script.cachedCompileResult = result; + return result; + }; + + const entry = compileScript(ir.entry); + + for (const procedureVariant of Object.keys(ir.procedures)) { + const procedureData = ir.procedures[procedureVariant]; + const procedureTree = compileScript(procedureData); + procedures[procedureVariant] = procedureTree; + } + + return { + startingFunction: entry, + procedures, + executableHat: ir.entry.executableHat + }; +}; + +module.exports = compile; diff --git a/local-scratch-vm/src/compiler/environment.js b/local-scratch-vm/src/compiler/environment.js new file mode 100644 index 0000000000000000000000000000000000000000..75b300a64feed39a917a54196d297ec176d3bbff --- /dev/null +++ b/local-scratch-vm/src/compiler/environment.js @@ -0,0 +1,20 @@ +/* eslint-disable no-eval */ + +/** + * @returns {boolean} true if the nullish coalescing operator (x ?? y) is supported. + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator + */ +const supportsNullishCoalescing = () => { + try { + // eslint-disable-next-line no-unused-vars + const fn = new Function('undefined ?? 3'); + // if function construction succeeds, the browser understood the syntax. + return true; + } catch (e) { + return false; + } +}; + +module.exports = { + supportsNullishCoalescing: supportsNullishCoalescing() +}; diff --git a/local-scratch-vm/src/compiler/intermediate.js b/local-scratch-vm/src/compiler/intermediate.js new file mode 100644 index 0000000000000000000000000000000000000000..7dd8777a1a53126b5588fcfd828d67c7e8fae7d6 --- /dev/null +++ b/local-scratch-vm/src/compiler/intermediate.js @@ -0,0 +1,115 @@ +/** + * @fileoverview Common intermediates shared amongst parts of the compiler. + */ + +/** + * An IntermediateScript describes a single script. + * Scripts do not necessarily have hats. + */ +class IntermediateScript { + constructor () { + /** + * The ID of the top block of this script. + * @type {string} + */ + this.topBlockId = null; + + /** + * List of nodes that make up this script. + * @type {Array|null} + */ + this.stack = null; + + /** + * Whether this script is a procedure. + * @type {boolean} + */ + this.isProcedure = false; + + /** + * This procedure's code, if any. + * @type {string} + */ + this.procedureCode = ''; + + /** + * List of names of arguments accepted by this function, if it is a procedure. + * @type {string[]} + */ + this.arguments = []; + + /** + * Whether this script should be run in warp mode. + * @type {boolean} + */ + this.isWarp = false; + + /** + * pm: Whether this script should use dangerous optimizations. + * @type {boolean} + */ + this.isOptimized = false; + + /** + * pm: An object containing stuff for optimization. + * @type {object} + */ + this.optimizationUtil = {}; + + /** + * Whether this script can `yield` + * If false, this script will be compiled as a regular JavaScript function (function) + * If true, this script will be compiled as a generator function (function*) + * @type {boolean} + */ + this.yields = true; + + /** + * Whether this script should use the "warp timer" + * @type {boolean} + */ + this.warpTimer = false; + + /** + * List of procedure IDs that this script needs. + * @readonly + */ + this.dependedProcedures = []; + + /** + * Cached result of compiling this script. + * @type {Function|null} + */ + this.cachedCompileResult = null; + + /** + * Whether the top block of this script is an executable hat. + * @type {boolean} + */ + this.executableHat = false; + } +} + +/** + * An IntermediateRepresentation contains scripts. + */ +class IntermediateRepresentation { + constructor () { + /** + * The entry point of this IR. + * @type {IntermediateScript} + */ + this.entry = null; + + /** + * Maps procedure variants to their intermediate script. + * @type {Object.} + */ + this.procedures = {}; + } +} + +module.exports = { + IntermediateScript, + IntermediateRepresentation +}; diff --git a/local-scratch-vm/src/compiler/irgen.js b/local-scratch-vm/src/compiler/irgen.js new file mode 100644 index 0000000000000000000000000000000000000000..69d9105749dbff1e68f9a816f2b83616ce9b5dc7 --- /dev/null +++ b/local-scratch-vm/src/compiler/irgen.js @@ -0,0 +1,2475 @@ +const Cast = require('../util/cast'); +const StringUtil = require('../util/string-util'); +const BlockType = require('../extension-support/block-type'); +const Sequencer = require('../engine/sequencer'); +const BlockUtility = require('../engine/block-utility'); +const Variable = require('../engine/variable'); +const Color = require('../util/color'); +const log = require('../util/log'); +const Clone = require('../util/clone'); +const {IntermediateScript, IntermediateRepresentation} = require('./intermediate'); +const compatBlocks = require('./compat-blocks'); + +/** + * @fileoverview Generate intermediate representations from Scratch blocks. + */ + +const SCALAR_TYPE = ''; +const LIST_TYPE = 'list'; + +/** + * @typedef {Object.} Node + * @property {string} kind + */ + +/** + * Create a variable codegen object. + * @param {'target'|'stage'} scope The scope of this variable -- which object owns it. + * @param {import('../engine/variable.js')} varObj The Scratch Variable + * @returns {*} A variable codegen object. + */ +const createVariableData = (scope, varObj) => ({ + scope, + id: varObj.id, + name: varObj.name, + isCloud: varObj.isCloud +}); + +/** + * @param {string} code + * @param {boolean} warp + * @returns {string} + */ +const generateProcedureVariant = (code, warp) => { + if (warp) { + return `W${code}`; + } + return `Z${code}`; +}; + +/** + * @param {string} variant Variant generated by generateProcedureVariant() + * @returns {string} original procedure code + */ +const parseProcedureCode = variant => variant.substring(1); + +/** + * @param {string} variant Variant generated by generateProcedureVariant() + * @returns {boolean} true if warp enabled + */ +const parseIsWarp = variant => variant.charAt(0) === 'W'; + +class ScriptTreeGenerator { + constructor (thread) { + /** @private */ + this.thread = thread; + /** @private */ + this.target = thread.target; + /** @private */ + this.blocks = thread.blockContainer; + /** @private */ + this.runtime = this.target.runtime; + /** @private */ + this.stage = this.runtime.getTargetForStage(); + /** @private */ + this.util = new BlockUtility(this.runtime.sequencer, this.thread); + + /** + * This script's intermediate representation. + */ + this.script = new IntermediateScript(); + this.script.warpTimer = this.target.runtime.compilerOptions.warpTimer; + this.script.isOptimized = this.target.runtime.runtimeOptions.dangerousOptimizations; + this.script.optimizationUtil = this.target.runtime.optimizationUtil; + + /** + * Cache of variable ID to variable data object. + * @type {Object.} + * @private + */ + this.variableCache = {}; + + this.usesTimer = false; + } + + setProcedureVariant (procedureVariant) { + const procedureCode = parseProcedureCode(procedureVariant); + + this.script.procedureCode = procedureCode; + this.script.isProcedure = true; + this.script.yields = false; + + const paramNamesIdsAndDefaults = this.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode); + if (paramNamesIdsAndDefaults === null) { + throw new Error(`IR: cannot find procedure: ${procedureVariant}`); + } + + const [paramNames, _paramIds, _paramDefaults] = paramNamesIdsAndDefaults; + this.script.arguments = paramNames; + } + + enableWarp () { + this.script.isWarp = true; + } + + getBlockById (blockId) { + // Flyout blocks are stored in a special container. + return this.blocks.getBlock(blockId) || this.blocks.runtime.flyoutBlocks.getBlock(blockId); + } + + getBlockInfo (fullOpcode) { + const [category, opcode] = StringUtil.splitFirst(fullOpcode, '_'); + if (!category || !opcode) { + return null; + } + const categoryInfo = this.runtime._blockInfo.find(ci => ci.id === category); + if (!categoryInfo) { + return null; + } + const blockInfo = categoryInfo.blocks.find(b => b.info.opcode === opcode); + if (!blockInfo) { + return null; + } + return blockInfo; + } + + /** + * Descend into a child input of a block. (eg. the input STRING of "length of ( )") + * @param {*} parentBlock The parent Scratch block that contains the input. + * @param {string} inputName The name of the input to descend into. + * @private + * @returns {Node} Compiled input node for this input. + */ + descendInputOfBlock (parentBlock, inputName) { + const input = parentBlock.inputs[inputName]; + if (!input) { + log.warn(`IR: ${parentBlock.opcode}: missing input ${inputName}`, parentBlock); + return { + kind: 'constant', + value: 0 + }; + } + const inputId = input.block; + const block = this.getBlockById(inputId); + if (!block) { + log.warn(`IR: ${parentBlock.opcode}: could not find input ${inputName} with ID ${inputId}`); + return { + kind: 'constant', + value: 0 + }; + } + + return this.descendInput(block); + } + + /** + * Descend into an input. (eg. "length of ( )") + * @param {*} block The parent Scratch block input. + * @private + * @returns {Node} Compiled input node for this input. + */ + descendInput (block) { + // check if we have extension ir for this opcode + const extensionId = String(block.opcode).split('_')[0]; + const blockId = String(block.opcode).replace(extensionId + '_', ''); + if (IRGenerator.hasExtensionIr(extensionId) && IRGenerator.getExtensionIr(extensionId)[blockId]) { + // this is an extension block that wants to be compiled + const irFunc = IRGenerator.getExtensionIr(extensionId)[blockId]; + let irData = null; + // make sure irFunc isnt broken + try { + irData = irFunc(this, block); + } catch (err) { + log.warn(extensionId + '_' + blockId, 'failed to create IR data;', err); + } + if (irData) { + // check if it is this type, we dont want to descend a stack as an input + if (irData.kind === 'input') { + // set proper kind + irData.kind = extensionId + '.' + blockId; + return irData; + } + } + } + + switch (block.opcode) { + case 'colour_picker': + return { + kind: 'constant', + value: block.fields.COLOUR.value + }; + case 'math_angle': + case 'math_integer': + case 'math_number': + case 'math_positive_number': + case 'math_whole_number': + return { + kind: 'constant', + value: block.fields.NUM.value + }; + case 'text': + return { + kind: 'constant', + value: block.fields.TEXT.value + }; + case 'polygon': + const points = []; + for (let point = 1; point <= block.mutation.points; point++) { + const xn = `x${point}`; + const yn = `y${point}`; + points.push({ + x: this.descendInputOfBlock(block, xn), + y: this.descendInputOfBlock(block, yn) + }); + } + return { + kind: 'math.polygon', + points + }; + + case 'argument_reporter_string_number': { + const name = block.fields.VALUE.value; + // lastIndexOf because multiple parameters with the same name will use the value of the last definition + const index = this.script.arguments.lastIndexOf(name); + if (index === -1) { + // Legacy support + if (name.toLowerCase() === 'last key pressed') { + return { + kind: 'tw.lastKeyPressed' + }; + } + } + if (index === -1) { + return { + kind: 'constant', + value: 0 + }; + } + return { + kind: 'args.stringNumber', + index: index + }; + } + case 'argument_reporter_boolean': { + // see argument_reporter_string_number above + const name = block.fields.VALUE.value; + const index = this.script.arguments.lastIndexOf(name); + if (index === -1) { + if (name.toLowerCase() === 'is compiled?' || + name.toLowerCase() === 'is turbowarp?' || + name.toLowerCase() === 'is penguinmod or turbowarp?') { + return { + kind: 'constant', + value: true + }; + } + return { + kind: 'constant', + value: 0 + }; + } + return { + kind: 'args.boolean', + index: index + }; + } + + case 'control_get_counter': + return { + kind: 'counter.get' + }; + case 'control_error': + return { + kind: 'control.error' + }; + case 'control_is_clone': + return { + kind: 'control.isclone' + }; + + case 'data_variable': + return { + kind: 'var.get', + variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE) + }; + case 'data_itemoflist': + return { + kind: 'list.get', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + index: this.descendInputOfBlock(block, 'INDEX') + }; + case 'data_lengthoflist': + return { + kind: 'list.length', + list: this.descendVariable(block, 'LIST', LIST_TYPE) + }; + case 'data_listcontainsitem': + return { + kind: 'list.contains', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + item: this.descendInputOfBlock(block, 'ITEM') + }; + case 'data_itemnumoflist': + return { + kind: 'list.indexOf', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + item: this.descendInputOfBlock(block, 'ITEM') + }; + case 'data_amountinlist': + return { + kind: 'list.amountOf', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + value: this.descendInputOfBlock(block, 'VALUE') + }; + case 'data_listcontents': + return { + kind: 'list.contents', + list: this.descendVariable(block, 'LIST', LIST_TYPE) + }; + case 'data_filterlistitem': + return { + kind: 'list.filteritem' + }; + case 'data_filterlistindex': + return { + kind: 'list.filterindex' + }; + + case 'event_broadcast_menu': { + const broadcastOption = block.fields.BROADCAST_OPTION; + const broadcastVariable = this.target.lookupBroadcastMsg(broadcastOption.id, broadcastOption.value); + // TODO: empty string probably isn't the correct fallback + const broadcastName = broadcastVariable ? broadcastVariable.name : ''; + return { + kind: 'constant', + value: broadcastName + }; + } + + case 'pmEventsExpansion_broadcastFunction': + this.script.yields = true; + return { + kind: 'pmEventsExpansion.broadcastFunction', + broadcast: this.descendInputOfBlock(block, 'BROADCAST') + }; + case 'pmEventsExpansion_broadcastFunctionArgs': + this.script.yields = true; + return { + kind: 'pmEventsExpansion.broadcastFunctionArgs', + broadcast: this.descendInputOfBlock(block, 'BROADCAST'), + args: this.descendInputOfBlock(block, 'ARGS') + }; + + case 'control_inline_stack_output': + return { + kind: 'control.inlineStackOutput', + code: this.descendSubstack(block, 'SUBSTACK') + }; + + case 'looks_backdropnumbername': + if (block.fields.NUMBER_NAME.value === 'number') { + return { + kind: 'looks.backdropNumber' + }; + } + return { + kind: 'looks.backdropName' + }; + case 'looks_costumenumbername': + if (block.fields.NUMBER_NAME.value === 'number') { + return { + kind: 'looks.costumeNumber' + }; + } + return { + kind: 'looks.costumeName' + }; + case 'looks_size': + return { + kind: 'looks.size' + }; + case 'looks_tintColor': + return { + kind: 'looks.tintColor' + }; + case 'motion_direction': + return { + kind: 'motion.direction' + }; + case 'motion_xposition': + return { + kind: 'motion.x' + }; + case 'motion_yposition': + return { + kind: 'motion.y' + }; + + case 'operator_add': + return { + kind: 'op.add', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; + case 'operator_and': + return { + kind: 'op.and', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; + case 'operator_contains': + return { + kind: 'op.contains', + string: this.descendInputOfBlock(block, 'STRING1'), + contains: this.descendInputOfBlock(block, 'STRING2') + }; + case 'operator_divide': + return { + kind: 'op.divide', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; + case 'operator_power': + return { + kind: 'op.power', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; + case 'operator_equals': + return { + kind: 'op.equals', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; + case 'operator_gt': + return { + kind: 'op.greater', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; + case 'operator_join': + return { + kind: 'op.join', + left: this.descendInputOfBlock(block, 'STRING1'), + right: this.descendInputOfBlock(block, 'STRING2') + }; + case 'operator_length': + return { + kind: 'op.length', + string: this.descendInputOfBlock(block, 'STRING') + }; + case 'operator_letter_of': + return { + kind: 'op.letterOf', + letter: this.descendInputOfBlock(block, 'LETTER'), + string: this.descendInputOfBlock(block, 'STRING') + }; + case 'operator_lt': + return { + kind: 'op.less', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; + case 'operator_mathop': { + const value = this.descendInputOfBlock(block, 'NUM'); + const operator = block.fields.OPERATOR.value.toLowerCase(); + switch (operator) { + case 'abs': return { + kind: 'op.abs', + value + }; + case 'floor': return { + kind: 'op.floor', + value + }; + case 'ceiling': return { + kind: 'op.ceiling', + value + }; + case 'sign': return { + kind: 'op.sign', + value + }; + case 'sqrt': return { + kind: 'op.sqrt', + value + }; + case 'sin': return { + kind: 'op.sin', + value + }; + case 'cos': return { + kind: 'op.cos', + value + }; + case 'tan': return { + kind: 'op.tan', + value + }; + case 'asin': return { + kind: 'op.asin', + value + }; + case 'acos': return { + kind: 'op.acos', + value + }; + case 'atan': return { + kind: 'op.atan', + value + }; + case 'ln': return { + kind: 'op.ln', + value + }; + case 'log': return { + kind: 'op.log', + value + }; + case 'log2': return { + kind: 'op.log2', + value + }; + case 'e ^': return { + kind: 'op.e^', + value + }; + case '10 ^': return { + kind: 'op.10^', + value + }; + default: return { + kind: 'constant', + value: 0 + }; + } + } + case 'operator_advlog': + return { + kind: 'op.advlog', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; + case 'operator_mod': + return { + kind: 'op.mod', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; + case 'operator_multiply': + return { + kind: 'op.multiply', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; + case 'operator_not': + return { + kind: 'op.not', + operand: this.descendInputOfBlock(block, 'OPERAND') + }; + case 'operator_or': + return { + kind: 'op.or', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; + case 'operator_random': { + const from = this.descendInputOfBlock(block, 'FROM'); + const to = this.descendInputOfBlock(block, 'TO'); + // If both values are known at compile time, we can do some optimizations. + // TODO: move optimizations to jsgen? + if (from.kind === 'constant' && to.kind === 'constant') { + const sFrom = from.value; + const sTo = to.value; + const nFrom = Cast.toNumber(sFrom); + const nTo = Cast.toNumber(sTo); + // If both numbers are the same, random is unnecessary. + // todo: this probably never happens so consider removing + if (nFrom === nTo) { + return { + kind: 'constant', + value: nFrom + }; + } + // If both are ints, hint this to the compiler + if (Cast.isInt(sFrom) && Cast.isInt(sTo)) { + return { + kind: 'op.random', + low: nFrom <= nTo ? from : to, + high: nFrom <= nTo ? to : from, + useInts: true, + useFloats: false + }; + } + // Otherwise hint that these are floats + return { + kind: 'op.random', + low: nFrom <= nTo ? from : to, + high: nFrom <= nTo ? to : from, + useInts: false, + useFloats: true + }; + } else if (from.kind === 'constant') { + // If only one value is known at compile-time, we can still attempt some optimizations. + if (!Cast.isInt(Cast.toNumber(from.value))) { + return { + kind: 'op.random', + low: from, + high: to, + useInts: false, + useFloats: true + }; + } + } else if (to.kind === 'constant') { + if (!Cast.isInt(Cast.toNumber(to.value))) { + return { + kind: 'op.random', + low: from, + high: to, + useInts: false, + useFloats: true + }; + } + } + // No optimizations possible + return { + kind: 'op.random', + low: from, + high: to, + useInts: false, + useFloats: false + }; + } + case 'operator_round': + return { + kind: 'op.round', + value: this.descendInputOfBlock(block, 'NUM') + }; + case 'operator_subtract': + return { + kind: 'op.subtract', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; + + case 'sensing_answer': + return { + kind: 'sensing.answer' + }; + case 'sensing_coloristouchingcolor': + return { + kind: 'sensing.colorTouchingColor', + target: this.descendInputOfBlock(block, 'COLOR2'), + mask: this.descendInputOfBlock(block, 'COLOR') + }; + case 'sensing_current': + switch (block.fields.CURRENTMENU.value.toLowerCase()) { + case 'year': + return { + kind: 'sensing.year' + }; + case 'month': + return { + kind: 'sensing.month' + }; + case 'date': + return { + kind: 'sensing.date' + }; + case 'dayofweek': + return { + kind: 'sensing.dayofweek' + }; + case 'hour': + return { + kind: 'sensing.hour' + }; + case 'minute': + return { + kind: 'sensing.minute' + }; + case 'second': + return { + kind: 'sensing.second' + }; + case 'timestamp': + return { + kind: 'sensing.timestamp' + }; + } + + return { + kind: 'constant', + value: 0 + }; + case 'sensing_dayssince2000': + return { + kind: 'sensing.daysSince2000' + }; + case 'sensing_distanceto': + return { + kind: 'sensing.distance', + target: this.descendInputOfBlock(block, 'DISTANCETOMENU') + }; + case 'sensing_keypressed': + return { + kind: 'keyboard.pressed', + key: this.descendInputOfBlock(block, 'KEY_OPTION') + }; + case 'sensing_mousedown': + return { + kind: 'mouse.down' + }; + case 'sensing_mousex': + return { + kind: 'mouse.x' + }; + case 'sensing_mousey': + return { + kind: 'mouse.y' + }; + case 'sensing_of': + return { + kind: 'sensing.of', + property: block.fields.PROPERTY.value, + object: this.descendInputOfBlock(block, 'OBJECT') + }; + case 'sensing_timer': + this.usesTimer = true; + return { + kind: 'timer.get' + }; + case 'sensing_touchingcolor': + return { + kind: 'sensing.touchingColor', + color: this.descendInputOfBlock(block, 'COLOR') + }; + case 'sensing_touchingobject': + return { + kind: 'sensing.touching', + object: this.descendInputOfBlock(block, 'TOUCHINGOBJECTMENU') + }; + case 'sensing_username': + return { + kind: 'sensing.username' + }; + case 'sensing_loggedin': + return { + kind: 'sensing.loggedin' + }; + case 'operator_trueBoolean': + return { + kind: 'op.true' + }; + case 'operator_falseBoolean': + return { + kind: 'op.false' + }; + case 'operator_randomBoolean': + return { + kind: 'op.randbool' + }; + + case 'sound_sounds_menu': + return { + kind: 'constant', + value: block.fields.SOUND_MENU.value + }; + + case 'lmsTempVars2_getRuntimeVariable': + return { + kind: 'tempVars.get', + var: this.descendInputOfBlock(block, 'VAR'), + runtime: true + }; + case 'lmsTempVars2_getThreadVariable': + return { + kind: 'tempVars.get', + var: this.descendInputOfBlock(block, 'VAR'), + thread: true + }; + case 'tempVars_getVariable': + return { + kind: 'tempVars.get', + var: this.descendInputOfBlock(block, 'name') + }; + + case 'lmsTempVars2_runtimeVariableExists': + return { + kind: 'tempVars.exists', + var: this.descendInputOfBlock(block, 'VAR'), + runtime: true + }; + case 'lmsTempVars2_threadVariableExists': + return { + kind: 'tempVars.exists', + var: this.descendInputOfBlock(block, 'VAR'), + thread: true + }; + case 'tempVars_variableExists': + // This menu is special compared to other menus -- it actually has an opcode function. + return { + kind: 'tempVars.exists', + var: this.descendInputOfBlock(block, 'name') + }; + + case 'lmsTempVars2_listRuntimeVariables': + return { + kind: 'tempVars.all', + runtime: true + }; + case 'lmsTempVars2_listThreadVariables': + return { + kind: 'tempVars.all', + thread: true + }; + case 'tempVars_allVariables': + return { + kind: 'tempVars.all' + }; + + // used by the stacked version of this block to run as an input block + // despite there being a stacked version + case 'procedures_call_return': + case 'procedures_call': { + // setting of yields will be handled later in the analysis phase + + const procedureCode = block.mutation.proccode; + if (procedureCode === 'tw:debugger;') { + return { + kind: 'tw.debugger' + }; + } + const paramNamesIdsAndDefaults = this.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode); + if (paramNamesIdsAndDefaults === null) { + return { + kind: 'noop' + }; + } + + const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults; + + const addonBlock = this.runtime.getAddonBlock(procedureCode); + if (addonBlock) { + this.script.yields = true; + const args = {}; + for (let i = 0; i < paramIds.length; i++) { + let value; + if (block.inputs[paramIds[i]] && block.inputs[paramIds[i]].block) { + value = this.descendInputOfBlock(block, paramIds[i]); + } else { + value = { + kind: 'constant', + value: paramDefaults[i] + }; + } + args[paramNames[i]] = value; + } + return { + kind: 'addons.call', + code: procedureCode, + arguments: args, + blockId: block.id + }; + } + + const definitionId = this.blocks.getProcedureDefinition(procedureCode); + const definitionBlock = this.blocks.getBlock(definitionId); + if (!definitionBlock) { + return { + kind: 'noop' + }; + } + const innerDefinition = this.blocks.getBlock(definitionBlock.inputs.custom_block.block); + + let isWarp = this.script.isWarp; + if (!isWarp) { + if (innerDefinition && innerDefinition.mutation) { + const warp = innerDefinition.mutation.warp; + if (typeof warp === 'boolean') { + isWarp = warp; + } else if (typeof warp === 'string') { + isWarp = JSON.parse(warp); + } + } + } + + const variant = generateProcedureVariant(procedureCode, isWarp); + + if (!this.script.dependedProcedures.includes(variant)) { + this.script.dependedProcedures.push(variant); + } + + // Non-warp direct recursion yields. + if (!this.script.isWarp) { + if (procedureCode === this.script.procedureCode) { + this.script.yields = true; + } + } + + const args = []; + for (let i = 0; i < paramIds.length; i++) { + let value; + if (block.inputs[paramIds[i]] && block.inputs[paramIds[i]].block) { + if (paramIds[i].startsWith("SUBSTACK")) { + value = this.descendSubstack(block, paramIds[i]) + } else { + value = this.descendInputOfBlock(block, paramIds[i]); + } + } else { + value = { + kind: 'constant', + value: paramDefaults[i] + }; + } + args.push(value); + } + + return { + kind: 'procedures.call', + code: procedureCode, + variant, + returns: true, + arguments: args, + type: JSON.parse(block.mutation.opType || '"string"') + }; + } + + case 'tw_getLastKeyPressed': + return { + kind: 'tw.lastKeyPressed' + }; + + case 'control_dualblock': + return { + kind: 'control.dualBlock' + }; + + default: { + const opcodeFunction = this.runtime.getOpcodeFunction(block.opcode); + if (opcodeFunction) { + // It might be a non-compiled primitive from a standard category + if (compatBlocks.outputBlocks.includes(block.opcode)) { + return this.descendCompatLayer(block); + } + // It might be an extension block. + const blockInfo = this.getBlockInfo(block.opcode); + if (blockInfo) { + const type = blockInfo.info.blockType; + const args = this.descendCompatLayer(block); + args.block = block; + if (block.mutation) args.mutation = block.mutation; + if (type === BlockType.REPORTER || type === BlockType.BOOLEAN) { + return args; + } + } + } + + // It might be a menu. + const inputs = Object.keys(block.inputs); + const fields = Object.keys(block.fields); + if (inputs.length === 0 && fields.length === 1) { + return { + kind: 'constant', + value: block.fields[fields[0]].value + }; + } + + log.warn(`IR: Unknown input: ${block.opcode}`, block); + throw new Error(`IR: Unknown input: ${block.opcode}`); + } + } + } + + /** + * Descend into a stacked block. (eg. "move ( ) steps") + * @param {*} block The Scratch block to parse. + * @private + * @returns {Node} Compiled node for this block. + */ + descendStackedBlock (block) { + // check if we have extension ir for this opcode + const extensionId = String(block.opcode).split('_')[0]; + const blockId = String(block.opcode).replace(extensionId + '_', ''); + if (IRGenerator.hasExtensionIr(extensionId) && IRGenerator.getExtensionIr(extensionId)[blockId]) { + // this is an extension block that wants to be compiled + const irFunc = IRGenerator.getExtensionIr(extensionId)[blockId]; + let irData = null; + // make sure irFunc isnt broken + try { + irData = irFunc(this, block); + } catch (err) { + log.warn(extensionId + '_' + blockId, 'failed to create IR data;', err); + } + if (irData) { + // check if it is this type, we dont want to descend an input as a stack + if (irData.kind === 'stack') { + // set proper kind + irData.kind = extensionId + '.' + blockId; + return irData; + } + } + } + + switch (block.opcode) { + case 'your_mom': + return { + kind: 'your mom' + }; + case 'control_switch': + return { + kind: 'control.switch', + test: this.descendInputOfBlock(block, 'CONDITION'), + conditions: this.descendSubstack(block, 'SUBSTACK'), + default: [] + }; + case 'control_switch_default': + return { + kind: 'control.switch', + test: this.descendInputOfBlock(block, 'CONDITION'), + conditions: this.descendSubstack(block, 'SUBSTACK1'), + default: this.descendSubstack(block, 'SUBSTACK2') + }; + case 'control_case_next': + return { + kind: 'control.case', + condition: this.descendInputOfBlock(block, 'CONDITION'), + code: this.descendSubstack(block, 'SUBSTACK'), + runsNext: true + }; + case 'control_case': + return { + kind: 'control.case', + condition: this.descendInputOfBlock(block, 'CONDITION'), + code: this.descendSubstack(block, 'SUBSTACK'), + runsNext: false + }; + case 'control_exitCase': + return { + kind: 'control.exitCase' + }; + case 'control_exitLoop': + return { + kind: 'control.exitLoop' + }; + case 'control_continueLoop': + return { + kind: 'control.continueLoop' + }; + case 'control_all_at_once': + // In Scratch 3, this block behaves like "if 1 = 1" + // WE ARE IN PM NOW IT BEHAVES PROPERLY LESS GO + return { + kind: 'control.allAtOnce', + condition: { + kind: 'constant', + value: true + }, + code: this.descendSubstack(block, 'SUBSTACK') + }; + case 'control_clear_counter': + return { + kind: 'counter.clear' + }; + case 'control_create_clone_of': + return { + kind: 'control.createClone', + target: this.descendInputOfBlock(block, 'CLONE_OPTION') + }; + case 'control_delete_this_clone': + this.script.yields = true; + return { + kind: 'control.deleteClone' + }; + case 'control_forever': + this.analyzeLoop(); + return { + kind: 'control.while', + condition: { + kind: 'constant', + value: true + }, + do: this.descendSubstack(block, 'SUBSTACK') + }; + case 'control_for_each': + this.analyzeLoop(); + return { + kind: 'control.for', + variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE), + count: this.descendInputOfBlock(block, 'VALUE'), + do: this.descendSubstack(block, 'SUBSTACK') + }; + case 'control_if': + return { + kind: 'control.if', + condition: this.descendInputOfBlock(block, 'CONDITION'), + whenTrue: this.descendSubstack(block, 'SUBSTACK'), + whenFalse: [] + }; + case 'control_if_else': + return { + kind: 'control.if', + condition: this.descendInputOfBlock(block, 'CONDITION'), + whenTrue: this.descendSubstack(block, 'SUBSTACK'), + whenFalse: this.descendSubstack(block, 'SUBSTACK2') + }; + case 'control_try_catch': + return { + kind: 'control.trycatch', + try: this.descendSubstack(block, 'SUBSTACK'), + catch: this.descendSubstack(block, 'SUBSTACK2') + }; + case 'control_throw_error': + return { + kind: 'control.throwError', + error: this.descendInputOfBlock(block, 'ERROR'), + }; + case 'control_incr_counter': + return { + kind: 'counter.increment' + }; + case 'control_decr_counter': + return { + kind: 'counter.decrement' + }; + case 'control_set_counter': + return { + kind: 'counter.set', + value: this.descendInputOfBlock(block, 'VALUE') + }; + case 'control_repeat': + this.analyzeLoop(); + return { + kind: 'control.repeat', + times: this.descendInputOfBlock(block, 'TIMES'), + do: this.descendSubstack(block, 'SUBSTACK') + }; + case 'control_repeatForSeconds': + this.analyzeLoop(); + return { + kind: 'control.repeatForSeconds', + times: this.descendInputOfBlock(block, 'TIMES'), + do: this.descendSubstack(block, 'SUBSTACK') + }; + case 'control_repeat_until': { + this.analyzeLoop(); + // Dirty hack: automatically enable warp timer for this block if it uses timer + // This fixes project that do things like "repeat until timer > 0.5" + this.usesTimer = false; + const condition = this.descendInputOfBlock(block, 'CONDITION'); + const needsWarpTimer = this.usesTimer; + if (needsWarpTimer) { + this.script.yields = true; + } + return { + kind: 'control.while', + condition: { + kind: 'op.not', + operand: condition + }, + do: this.descendSubstack(block, 'SUBSTACK'), + warpTimer: needsWarpTimer + }; + } + case 'control_stop': { + const level = block.fields.STOP_OPTION.value; + if (level === 'all') { + this.script.yields = true; + return { + kind: 'control.stopAll' + }; + } else if (level === 'other scripts in sprite' || level === 'other scripts in stage') { + return { + kind: 'control.stopOthers' + }; + } else if (level === 'this script') { + return { + kind: 'control.stopScript' + }; + } + return { + kind: 'noop' + }; + } + case 'control_wait': + this.script.yields = true; + return { + kind: 'control.wait', + seconds: this.descendInputOfBlock(block, 'DURATION') + }; + case 'control_waittick': + this.script.yields = true; + return { + kind: 'control.waitTick' + }; + case 'control_wait_until': + this.script.yields = true; + return { + kind: 'control.waitUntil', + condition: this.descendInputOfBlock(block, 'CONDITION') + }; + case 'control_waitsecondsoruntil': + this.script.yields = true; + return { + kind: 'control.waitOrUntil', + seconds: this.descendInputOfBlock(block, 'DURATION'), + condition: this.descendInputOfBlock(block, 'CONDITION') + }; + case 'control_while': + this.analyzeLoop(); + return { + kind: 'control.while', + condition: this.descendInputOfBlock(block, 'CONDITION'), + do: this.descendSubstack(block, 'SUBSTACK'), + // We should consider analyzing this like we do for control_repeat_until + warpTimer: false + }; + case 'control_run_as_sprite': + return { + kind: 'control.runAsSprite', + sprite: this.descendInputOfBlock(block, 'RUN_AS_OPTION'), + substack: this.descendSubstack(block, 'SUBSTACK') + }; + case 'control_new_script': + return { + kind: 'control.newScript', + substack: this.descendSubstack(block, 'SUBSTACK') + }; + case 'data_addtolist': + return { + kind: 'list.add', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + item: this.descendInputOfBlock(block, 'ITEM') + }; + case 'data_changevariableby': { + const variable = this.descendVariable(block, 'VARIABLE', SCALAR_TYPE); + return { + kind: 'var.set', + variable, + value: { + kind: 'op.add', + left: { + kind: 'var.get', + variable + }, + right: this.descendInputOfBlock(block, 'VALUE') + } + }; + } + case 'data_deletealloflist': + return { + kind: 'list.deleteAll', + list: this.descendVariable(block, 'LIST', LIST_TYPE) + }; + case 'data_listforeachnum': + this.analyzeLoop(); + return { + kind: 'list.forEach', + num: true, + list: this.descendVariable(block, 'LIST', LIST_TYPE), + variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE), + do: this.descendSubstack(block, 'SUBSTACK') + }; + case 'data_listforeachitem': + this.analyzeLoop(); + return { + kind: 'list.forEach', + num: false, + list: this.descendVariable(block, 'LIST', LIST_TYPE), + variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE), + do: this.descendSubstack(block, 'SUBSTACK') + }; + case 'data_deleteoflist': { + const index = this.descendInputOfBlock(block, 'INDEX'); + if (index.kind === 'constant' && index.value === 'all') { + return { + kind: 'list.deleteAll', + list: this.descendVariable(block, 'LIST', LIST_TYPE) + }; + } + return { + kind: 'list.delete', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + index: index + }; + } + case 'data_shiftlist': { + return { + kind: 'list.shift', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + index: this.descendInputOfBlock(block, 'INDEX') + }; + } + case 'data_hidelist': + return { + kind: 'list.hide', + list: this.descendVariable(block, 'LIST', LIST_TYPE) + }; + case 'data_hidevariable': + return { + kind: 'var.hide', + variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE) + }; + case 'data_insertatlist': + return { + kind: 'list.insert', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + index: this.descendInputOfBlock(block, 'INDEX'), + item: this.descendInputOfBlock(block, 'ITEM') + }; + case 'data_replaceitemoflist': + return { + kind: 'list.replace', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + index: this.descendInputOfBlock(block, 'INDEX'), + item: this.descendInputOfBlock(block, 'ITEM') + }; + case 'data_setvariableto': + return { + kind: 'var.set', + variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE), + value: this.descendInputOfBlock(block, 'VALUE') + }; + case 'data_showlist': + return { + kind: 'list.show', + list: this.descendVariable(block, 'LIST', LIST_TYPE) + }; + case 'data_showvariable': + return { + kind: 'var.show', + variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE) + }; + case 'data_filterlist': + return { + kind: 'list.filter', + list: this.descendVariable(block, 'LIST', LIST_TYPE), + bool: this.descendInputOfBlock(block, 'BOOL') + }; + + case 'event_broadcast': + return { + kind: 'event.broadcast', + broadcast: this.descendInputOfBlock(block, 'BROADCAST_INPUT') + }; + case 'event_broadcastandwait': + this.script.yields = true; + return { + kind: 'event.broadcastAndWait', + broadcast: this.descendInputOfBlock(block, 'BROADCAST_INPUT') + }; + + case 'looks_changeeffectby': + return { + kind: 'looks.changeEffect', + effect: block.fields.EFFECT.value.toLowerCase(), + value: this.descendInputOfBlock(block, 'CHANGE') + }; + case 'looks_changesizeby': + return { + kind: 'looks.changeSize', + size: this.descendInputOfBlock(block, 'CHANGE') + }; + case 'looks_cleargraphiceffects': + return { + kind: 'looks.clearEffects' + }; + case 'looks_goforwardbackwardlayers': + if (block.fields.FORWARD_BACKWARD.value === 'forward') { + return { + kind: 'looks.forwardLayers', + layers: this.descendInputOfBlock(block, 'NUM') + }; + } + return { + kind: 'looks.backwardLayers', + layers: this.descendInputOfBlock(block, 'NUM') + }; + case 'looks_goTargetLayer': + if (block.fields.FORWARD_BACKWARD.value === 'infront') { + return { + kind: 'looks.targetFront', + layers: this.descendInputOfBlock(block, 'VISIBLE_OPTION') + }; + } + return { + kind: 'looks.targetBack', + layers: this.descendInputOfBlock(block, 'VISIBLE_OPTION') + }; + case 'looks_gotofrontback': + if (block.fields.FRONT_BACK.value === 'front') { + return { + kind: 'looks.goToFront' + }; + } + return { + kind: 'looks.goToBack' + }; + case 'looks_hide': + return { + kind: 'looks.hide' + }; + case 'looks_nextbackdrop': + return { + kind: 'looks.nextBackdrop' + }; + case 'looks_nextcostume': + return { + kind: 'looks.nextCostume' + }; + case 'looks_seteffectto': + return { + kind: 'looks.setEffect', + effect: block.fields.EFFECT.value.toLowerCase(), + value: this.descendInputOfBlock(block, 'VALUE') + }; + case 'looks_setsizeto': + return { + kind: 'looks.setSize', + size: this.descendInputOfBlock(block, 'SIZE') + }; + case "looks_setFont": + return { + kind: 'looks.setFont', + font: this.descendInputOfBlock(block, 'font'), + size: this.descendInputOfBlock(block, 'size') + }; + case "looks_setColor": + return { + kind: 'looks.setColor', + prop: block.fields.prop.value, + color: this.descendInputOfBlock(block, 'color') + }; + case "looks_setTintColor": + return { + kind: 'looks.setTintColor', + color: this.descendInputOfBlock(block, 'color') + }; + case "looks_setShape": + return { + kind: 'looks.setShape', + prop: block.fields.prop.value, + value: this.descendInputOfBlock(block, 'color') + }; + case 'looks_show': + return { + kind: 'looks.show' + }; + case 'looks_switchbackdropto': + return { + kind: 'looks.switchBackdrop', + backdrop: this.descendInputOfBlock(block, 'BACKDROP') + }; + case 'looks_switchcostumeto': + return { + kind: 'looks.switchCostume', + costume: this.descendInputOfBlock(block, 'COSTUME') + }; + + case 'motion_changexby': + return { + kind: 'motion.changeX', + dx: this.descendInputOfBlock(block, 'DX') + }; + case 'motion_changeyby': + return { + kind: 'motion.changeY', + dy: this.descendInputOfBlock(block, 'DY') + }; + case 'motion_gotoxy': + return { + kind: 'motion.setXY', + x: this.descendInputOfBlock(block, 'X'), + y: this.descendInputOfBlock(block, 'Y') + }; + case 'motion_ifonedgebounce': + return { + kind: 'motion.ifOnEdgeBounce' + }; + case 'motion_movesteps': + return { + kind: 'motion.step', + steps: this.descendInputOfBlock(block, 'STEPS') + }; + case 'motion_pointindirection': + return { + kind: 'motion.setDirection', + direction: this.descendInputOfBlock(block, 'DIRECTION') + }; + case 'motion_setrotationstyle': + return { + kind: 'motion.setRotationStyle', + style: block.fields.STYLE.value + }; + case 'motion_setx': + return { + kind: 'motion.setX', + x: this.descendInputOfBlock(block, 'X') + }; + case 'motion_sety': + return { + kind: 'motion.setY', + y: this.descendInputOfBlock(block, 'Y') + }; + case 'motion_turnleft': + return { + kind: 'motion.setDirection', + direction: { + kind: 'op.subtract', + left: { + kind: 'motion.direction' + }, + right: this.descendInputOfBlock(block, 'DEGREES') + } + }; + case 'motion_turnright': + return { + kind: 'motion.setDirection', + direction: { + kind: 'op.add', + left: { + kind: 'motion.direction' + }, + right: this.descendInputOfBlock(block, 'DEGREES') + } + }; + + case 'pen_clear': + return { + kind: 'pen.clear' + }; + case 'pen_changePenColorParamBy': + return { + kind: 'pen.changeParam', + param: this.descendInputOfBlock(block, 'COLOR_PARAM'), + value: this.descendInputOfBlock(block, 'VALUE') + }; + case 'pen_changePenHueBy': + return { + kind: 'pen.legacyChangeHue', + hue: this.descendInputOfBlock(block, 'HUE') + }; + case 'pen_changePenShadeBy': + return { + kind: 'pen.legacyChangeShade', + shade: this.descendInputOfBlock(block, 'SHADE') + }; + case 'pen_penDown': + return { + kind: 'pen.down' + }; + case 'pen_penUp': + return { + kind: 'pen.up' + }; + case 'pen_setPenColorParamTo': + return { + kind: 'pen.setParam', + param: this.descendInputOfBlock(block, 'COLOR_PARAM'), + value: this.descendInputOfBlock(block, 'VALUE') + }; + case 'pen_setPenColorToColor': + return { + kind: 'pen.setColor', + color: this.descendInputOfBlock(block, 'COLOR') + }; + case 'pen_setPenHueToNumber': + return { + kind: 'pen.legacySetHue', + hue: this.descendInputOfBlock(block, 'HUE') + }; + case 'pen_setPenShadeToNumber': + return { + kind: 'pen.legacySetShade', + shade: this.descendInputOfBlock(block, 'SHADE') + }; + case 'pen_setPenSizeTo': + return { + kind: 'pen.setSize', + size: this.descendInputOfBlock(block, 'SIZE') + }; + case 'pen_changePenSizeBy': + return { + kind: 'pen.changeSize', + size: this.descendInputOfBlock(block, 'SIZE') + }; + case 'pen_stamp': + return { + kind: 'pen.stamp' + }; + case 'procedures_return': + return { + kind: 'procedures.return', + return: this.descendInputOfBlock(block, 'return') + }; + case 'procedures_set': + return { + kind: 'procedures.set', + param: this.descendInputOfBlock(block, "PARAM"), + val: this.descendInputOfBlock(block, "VALUE") + }; + case 'procedures_call': { + // setting of yields will be handled later in the analysis phase + // patches output previewing + if (block.mutation.returns === 'true') { + const Block = Clone.simple(block); + Block.opcode = 'procedures_call_return'; + return this.descendStackedBlock(Block); + } + + const procedureCode = block.mutation.proccode; + if (procedureCode === 'tw:debugger;') { + return { + kind: 'tw.debugger' + }; + } + const paramNamesIdsAndDefaults = this.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode); + if (paramNamesIdsAndDefaults === null) { + return { + kind: 'noop' + }; + } + + const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults; + + const addonBlock = this.runtime.getAddonBlock(procedureCode); + if (addonBlock) { + this.script.yields = true; + const args = {}; + for (let i = 0; i < paramIds.length; i++) { + let value; + if (block.inputs[paramIds[i]] && block.inputs[paramIds[i]].block) { + value = this.descendInputOfBlock(block, paramIds[i]); + } else { + value = { + kind: 'constant', + value: paramDefaults[i] + }; + } + args[paramNames[i]] = value; + } + return { + kind: 'addons.call', + code: procedureCode, + arguments: args, + blockId: block.id + }; + } + + const definitionId = this.blocks.getProcedureDefinition(procedureCode); + const definitionBlock = this.blocks.getBlock(definitionId); + if (!definitionBlock) { + return { + kind: 'noop' + }; + } + const innerDefinition = this.blocks.getBlock(definitionBlock.inputs.custom_block.block); + + let isWarp = this.script.isWarp; + if (!isWarp) { + if (innerDefinition && innerDefinition.mutation) { + const warp = innerDefinition.mutation.warp; + if (typeof warp === 'boolean') { + isWarp = warp; + } else if (typeof warp === 'string') { + isWarp = JSON.parse(warp); + } + } + } + + const variant = generateProcedureVariant(procedureCode, isWarp); + + if (!this.script.dependedProcedures.includes(variant)) { + this.script.dependedProcedures.push(variant); + } + + // Non-warp direct recursion yields. + if (!this.script.isWarp) { + if (procedureCode === this.script.procedureCode) { + this.script.yields = true; + } + } + + const args = []; + for (let i = 0; i < paramIds.length; i++) { + let value; + if (block.inputs[paramIds[i]] && block.inputs[paramIds[i]].block) { + if (paramIds[i].startsWith("SUBSTACK")) { + value = this.descendSubstack(block, paramIds[i]) + } else { + value = this.descendInputOfBlock(block, paramIds[i]); + } + } else { + value = { + kind: 'constant', + value: paramDefaults[i] + }; + } + args.push(value); + } + + return { + kind: 'procedures.call', + code: procedureCode, + variant, + returns: false, + arguments: args, + type: JSON.parse(block.mutation.optype || '"statement"') + }; + } + + case 'sensing_set_of': + return { + kind: 'sensing.set.of', + property: block.fields.PROPERTY.value, + object: this.descendInputOfBlock(block, 'OBJECT'), + value: this.descendInputOfBlock(block, 'VALUE') + }; + case 'sensing_resettimer': + return { + kind: 'timer.reset' + }; + + /* + can someone set up the jsgen for these, i dont want to rn + case "sensing_regextest": + return { + kind: "sensing.regextest", + regex: this.descendInputOfBlock(block, 'reg'), + text: this.descendInputOfBlock(block, 'text') + } + case "sensing_thing_is_number": + return { + kind: "sensing.thing.is.number", + text: this.descendInputOfBlock(block, 'TEXT1') + } + case "sensing_mobile": + return { + kind: "sensing.mobile", + } + case "sensing_thing_is_text": + return { + kind: "sensing.thing.is.text", + text: this.descendInputOfBlock(block, 'TEXT1') + } + case "sensing_getspritewithattrib": + return { + kind: "sensing.getspritewithattrib", + variable: this.descendInputOfBlock(block, 'var'), + value: this.descendInputOfBlock(block, 'val') + } + + case "operator_regexmatch": + return { + kind: "operator.regexmatch", + regex: this.descendInputOfBlock(block, 'reg'), + text: this.descendInputOfBlock(block, 'text') + } + case "operator_replaceAll": + return { + kind: "operator.replaceAll", + text: this.descendInputOfBlock(block, 'term'), + with: this.descendInputOfBlock(block, 'res'), + in: this.descendInputOfBlock(block, 'text') + } + case "operator_getLettersFromIndexToIndexInTextFixed": + case "operator_getLettersFromIndexToIndexInText": + return { + kind: "operator.getLettersFromIndexToIndexInText", + from: this.descendInputOfBlock(block, 'INDEX1'), + to: this.descendInputOfBlock(block, 'INDEX2'), + ammount: this.descendInputOfBlock(block, 'TEXT') + } + case "operator_readLineInMultilineText": + return { + kind: "operator.readLineInMultilineText", + line: this.descendInputOfBlock(block, 'LINE'), + text: this.descendInputOfBlock(block, 'TEXT') + } + case "operator_newLine": + return { + kind: "operator.newLine", + } + case "operator_stringify": + return { + kind: "operator.stringify", + pass: this.descendInputOfBlock(block, 'ONE') + } + case "operator_lerpFunc": + return { + kind: "operator.lerpFunc", + from: this.descendInputOfBlock(block, 'ONE'), + to: this.descendInputOfBlock(block, 'TWO'), + ammount: this.descendInputOfBlock(block, 'AMOUNT') + } + case "operator_advMath": + return { + kind: "operator.advMath", + num1: this.descendInputOfBlock(block, 'ONE'), + num2: this.descendInputOfBlock(block, 'TWO'), + op: block.fields.OPTION.value + } + case "operator_constrainnumber": + return { + kind: "operator.constrainnumber", + number: this.descendInputOfBlock(block, 'inp'), + min: this.descendInputOfBlock(block, 'min'), + max: this.descendInputOfBlock(block, 'max') + } + case "operator_trueBoolean": + return { + kind: "operator.trueBoolean", + } + case "operator_falseBoolean": + return { + kind: "operator.falseBoolean", + } + case "operator_randomBoolean": + return { + kind: "operator.randomBoolean", + } + case "operator_indexOfTextInText": + return { + kind: "operator.indexOfTextInText", + check: this.descendInputOfBlock(block, 'TEXT1'), + text: this.descendInputOfBlock(block, 'TEXT2') + } + + case "event_whenanything": + return { + kind: "event.whenanything", + } + case "event_always": + return { + kind: "event.always", + event: this.descendInputOfBlock(block, 'ANYTHING') + } + + case "control_backToGreenFlag": + return { + kind: "control.backToGreenFlag", + } + case "control_if_return_else_return": + return { + kind: "control.if.return.else.return", + if: this.descendInputOfBlock(block, 'boolean'), + true: this.descendInputOfBlock(block, 'TEXT1'), + false: this.descendInputOfBlock(block, 'TEXT2'), + } + all the names so you dont have to get them + sensing.regextest + sensing.thing.is.number + sensing.mobile + sensing.thing.is.text + sensing.getspritewithattrib + + operator.regexmatch + operator.replaceAll + operator.getLettersFromIndexToIndexInText + operator.readLineInMultilineText + operator.newLine + operator.stringify + operator.lerpFunc + operator.advMath + operator.constrainnumber + operator.trueBoolean + operator.falseBoolean + operator.randomBoolean + operator.indexOfTextInText + + event.whenanything + event.always + + control.backToGreenFlag + control.if.return.else.return + */ + + case 'lmsTempVars2_setRuntimeVariable': + return { + kind: 'tempVars.set', + var: this.descendInputOfBlock(block, 'VAR'), + val: this.descendInputOfBlock(block, 'STRING'), + runtime: true + }; + case 'lmsTempVars2_setThreadVariable': + return { + kind: 'tempVars.set', + var: this.descendInputOfBlock(block, 'VAR'), + val: this.descendInputOfBlock(block, 'STRING'), + thread: true + }; + case 'tempVars_setVariable': + return { + kind: 'tempVars.set', + var: this.descendInputOfBlock(block, 'name'), + val: this.descendInputOfBlock(block, 'value') + }; + + case 'lmsTempVars2_changeRuntimeVariable': + const name = this.descendInputOfBlock(block, 'VAR'); + return { + kind: 'tempVars.set', + var: name, + val: { + kind: 'op.add', + left: { + kind: 'tempVars.get', + var: name, + runtime: true + }, + right: this.descendInputOfBlock(block, 'NUM') + }, + runtime: true + }; + case 'lmsTempVars2_changeThreadVariable': { + const name = this.descendInputOfBlock(block, 'VAR'); + return { + kind: 'tempVars.set', + var: name, + val: { + kind: 'op.add', + left: { + kind: 'tempVars.get', + var: name, + thread: true + }, + right: this.descendInputOfBlock(block, 'NUM') + }, + thread: true + }; + } + case 'tempVars_changeVariable': { + const name = this.descendInputOfBlock(block, 'name'); + return { + kind: 'tempVars.set', + var: name, + val: { + kind: 'op.add', + left: { + kind: 'tempVars.get', + var: name + }, + right: this.descendInputOfBlock(block, 'value') + } + }; + } + + case 'lmsTempVars2_deleteRuntimeVariable': + return { + kind: 'tempVars.delete', + var: this.descendInputOfBlock(block, 'VAR'), + runtime: true + }; + case 'tempVars_deleteVariable': + return { + kind: 'tempVars.delete', + var: this.descendInputOfBlock(block, 'name') + }; + + case 'lmsTempVars2_deleteAllRuntimeVariables': + return { + kind: 'tempVars.deleteAll', + runtime: true + }; + case 'tempVars_deleteAllVariables': + return { + kind: 'tempVars.deleteAll' + }; + + case 'lmsTempVars2_forEachThreadVariable': + return { + kind: 'tempVars.forEach', + var: this.descendInputOfBlock(block, 'VAR'), + loops: this.descendInputOfBlock(block, 'NUM'), + do: this.descendSubstack(block, 'SUBSTACK'), + thread: true + }; + case 'tempVars_forEachTempVar': + this.analyzeLoop(); + return { + kind: 'tempVars.forEach', + var: this.descendInputOfBlock(block, 'NAME'), + loops: this.descendInputOfBlock(block, 'REPEAT'), + do: this.descendSubstack(block, 'SUBSTACK') + }; + case 'control_dualblock': + return { + kind: 'control.dualBlock' + }; + + default: { + const opcodeFunction = this.runtime.getOpcodeFunction(block.opcode); + if (opcodeFunction) { + // It might be a non-compiled primitive from a standard category + if (compatBlocks.statementBlocks.includes(block.opcode)) { + return this.descendCompatLayer(block); + } + // It might be an extension block. + const blockInfo = this.getBlockInfo(block.opcode); + if (blockInfo) { + const type = blockInfo.info.blockType; + const args = this.descendCompatLayer(block, blockInfo.info); + args.block = block; + if (block.mutation) args.mutation = block.mutation; + if (type === BlockType.COMMAND || type === BlockType.CONDITIONAL || type === BlockType.LOOP) { + return args; + } + } + } + + // When this thread was triggered by a stack click, attempt to compile as an input. + // TODO: perhaps this should be moved to generate()? + if (this.thread.stackClick) { + try { + const inputNode = this.descendInput(block); + return { + kind: 'visualReport', + input: inputNode + }; + } catch (e) { + // Ignore + } + } + + log.warn(`IR: Unknown stacked block: ${block.opcode}`, block); + throw new Error(`IR: Unknown stacked block: ${block.opcode}`); + } + } + } + + /** + * Descend into a stack of blocks (eg. the blocks contained within an "if" block) + * @param {*} parentBlock The parent Scratch block that contains the stack to parse. + * @param {*} substackName The name of the stack to descend into. + * @private + * @returns {Node[]} List of stacked block nodes. + */ + descendSubstack (parentBlock, substackName) { + const input = parentBlock.inputs[substackName]; + if (!input) { + return []; + } + const stackId = input.block; + return this.walkStack(stackId); + } + + /** + * Descend into and walk the siblings of a stack. + * @param {string} startingBlockId The ID of the first block of a stack. + * @private + * @returns {Node[]} List of stacked block nodes. + */ + walkStack (startingBlockId) { + const result = []; + let blockId = startingBlockId; + + while (blockId !== null) { + const block = this.getBlockById(blockId); + if (!block) { + break; + } + + const node = this.descendStackedBlock(block); + result.push(node); + + blockId = block.next; + } + + return result; + } + + /** + * Descend into a variable. + * @param {*} block The block that has the variable. + * @param {string} fieldName The name of the field that the variable is stored in. + * @param {''|'list'} type Variable type, '' for scalar and 'list' for list. + * @private + * @returns {*} A parsed variable object. + */ + descendVariable (block, fieldName, type) { + const variable = block.fields[fieldName]; + const id = variable.id; + + if (this.variableCache.hasOwnProperty(id)) { + return this.variableCache[id]; + } + + const data = this._descendVariable(id, variable.value, type); + this.variableCache[id] = data; + return data; + } + + /** + * @param {string} id The ID of the variable. + * @param {string} name The name of the variable. + * @param {''|'list'} type The variable type. + * @private + * @returns {*} A parsed variable object. + */ + _descendVariable (id, name, type) { + const target = this.target; + const stage = this.stage; + + // Look for by ID in target... + if (target.variables.hasOwnProperty(id)) { + return createVariableData('target', target.variables[id]); + } + + // Look for by ID in stage... + if (!target.isStage) { + if (stage && stage.variables.hasOwnProperty(id)) { + return createVariableData('stage', stage.variables[id]); + } + } + + // Look for by name and type in target... + for (const varId in target.variables) { + if (target.variables.hasOwnProperty(varId)) { + const currVar = target.variables[varId]; + if (currVar.name === name && currVar.type === type) { + return createVariableData('target', currVar); + } + } + } + + // Look for by name and type in stage... + if (!target.isStage && stage) { + for (const varId in stage.variables) { + if (stage.variables.hasOwnProperty(varId)) { + const currVar = stage.variables[varId]; + if (currVar.name === name && currVar.type === type) { + return createVariableData('stage', currVar); + } + } + } + } + + // Create it locally... + const newVariable = this.runtime.newVariableInstance(type, id, name, false); + target.variables[id] = newVariable; + + if (target.sprite) { + // Create the variable in all instances of this sprite. + // This is necessary because the script cache is shared between clones. + // sprite.clones has all instances of this sprite including the original and all clones + for (const clone of target.sprite.clones) { + if (!clone.variables.hasOwnProperty(id)) { + clone.variables[id] = this.runtime.newVariableInstance(type, id, name, false); + } + } + } + + return createVariableData('target', newVariable); + } + + /** + * Descend into a block that uses the compatibility layer. + * @param {*} block The block to use the compatibility layer for. + * @private + * @returns {Node} The parsed node. + */ + descendCompatLayer (block, blockInfo) { + this.script.yields = true; + if (!blockInfo) { + blockInfo = this.getBlockInfo(block.opcode); + blockInfo = blockInfo ? blockInfo.info : null; + } + + const inputs = {}; + for (const name of Object.keys(block.inputs)) { + if (!name.startsWith('SUBSTACK')) { + inputs[name] = this.descendInputOfBlock(block, name); + } + } + + const fields = {}; + const substacks = []; + const blockType = (blockInfo && blockInfo.blockType) || BlockType.COMMAND; + if (blockType === BlockType.CONDITIONAL || blockType === BlockType.LOOP) { + for (let i in (blockInfo.branches || [])) { + const inputName = i === "0" ? 'SUBSTACK' : `SUBSTACK${Number(i) + 1}`; + substacks.push(this.descendSubstack(block, inputName)); + } + } + for (const name of Object.keys(block.fields)) { + const type = block.fields[name].variableType; + if (typeof type !== 'undefined') { + const data = this.descendVariable(block, name, type); + fields[name] = data; + continue; + } + fields[name] = block.fields[name].value; + } + return { + kind: 'compat', + id: block.id, + opcode: block.opcode, + blockType, + inputs, + fields, + substacks + }; + } + + analyzeLoop () { + if (!this.script.isWarp || this.script.warpTimer) { + this.script.yields = true; + } + } + + readTopBlockComment (commentId) { + const comment = this.target.comments[commentId]; + if (!comment) { + // can't find the comment + // this is safe to ignore + return; + } + + const text = comment.text; + + for (const line of text.split('\n')) { + if (!/^tw\b/.test(line)) { + continue; + } + + const flags = line.split(' '); + for (const flag of flags) { + switch (flag) { + case 'nocompile': + throw new Error('Script explicitly disables compilation'); + case 'stuck': + this.script.warpTimer = true; + break; + } + } + + // Only the first 'tw' line is parsed. + break; + } + } + + /** + * @param {Block} hatBlock + */ + walkHat(hatBlock) { + const nextBlock = hatBlock.next; + const opcode = hatBlock.opcode; + const hatInfo = this.runtime._hats[opcode]; + + if (this.thread.stackClick) { + // We still need to treat the hat as a normal block (so executableHat should be false) for + // interpreter parity, but the reuslt is ignored. + const opcodeFunction = this.runtime.getOpcodeFunction(opcode); + if (opcodeFunction) { + return [ + this.descendCompatLayer(hatBlock), + ...this.walkStack(nextBlock) + ]; + } + return this.walkStack(nextBlock); + } + + if (hatInfo.edgeActivated) { + // Edge-activated HAT + this.script.yields = true; + this.script.executableHat = true; + return [ + { + kind: 'hat.edge', + id: hatBlock.id, + condition: this.descendCompatLayer(hatBlock) + }, + ...this.walkStack(nextBlock) + ]; + } + + const opcodeFunction = this.runtime.getOpcodeFunction(opcode); + if (opcodeFunction) { + // Predicate-based HAT + this.script.yields = true; + this.script.executableHat = true; + return [ + { + kind: 'hat.predicate', + condition: this.descendCompatLayer(hatBlock) + }, + ...this.walkStack(nextBlock) + ]; + } + + return this.walkStack(nextBlock); + } + + /** + * @param {string} topBlockId The ID of the top block of the script. + * @returns {IntermediateScript} + */ + generate (topBlockId) { + this.blocks.populateProcedureCache(); + + this.script.topBlockId = topBlockId; + + const topBlock = this.getBlockById(topBlockId); + if (!topBlock) { + if (this.script.isProcedure) { + // Empty procedure + return this.script; + } + throw new Error('Cannot find top block'); + } + + if (topBlock.comment) { + this.readTopBlockComment(topBlock.comment); + } + + // We do need to evaluate empty hats + const hatInfo = this.runtime._hats[topBlock.opcode]; + const isHat = !!hatInfo; + if (isHat) { + this.script.stack = this.walkHat(topBlock); + } else { + // We don't evaluate the procedures_definition top block as it never does anything + // We also don't want it to be treated like a hat block + let entryBlock; + if ( + topBlock.opcode === 'procedures_definition' + || topBlock.opcode === 'procedures_definition_return' + ) { + entryBlock = topBlock.next; + } else { + entryBlock = topBlockId; + } + + if (entryBlock) { + this.script.stack = this.walkStack(entryBlock); + } + } + + return this.script; + } +} + +class IRGenerator { + constructor (thread) { + this.thread = thread; + this.blocks = thread.blockContainer; + + this.proceduresToCompile = new Map(); + this.compilingProcedures = new Map(); + /** @type {Object.} */ + this.procedures = {}; + + this.analyzedProcedures = []; + } + + static _extensionIRInfo = {}; + static setExtensionIr(id, data) { + IRGenerator._extensionIRInfo[id] = data; + } + static hasExtensionIr(id) { + return Boolean(IRGenerator._extensionIRInfo[id]); + } + static getExtensionIr(id) { + return IRGenerator._extensionIRInfo[id]; + } + + addProcedureDependencies (dependencies) { + for (const procedureVariant of dependencies) { + if (this.procedures.hasOwnProperty(procedureVariant)) { + continue; + } + if (this.compilingProcedures.has(procedureVariant)) { + continue; + } + if (this.proceduresToCompile.has(procedureVariant)) { + continue; + } + const procedureCode = parseProcedureCode(procedureVariant); + const definition = this.blocks.getProcedureDefinition(procedureCode); + this.proceduresToCompile.set(procedureVariant, definition); + } + } + + /** + * @param {ScriptTreeGenerator} generator The generator to run. + * @param {string} topBlockId The ID of the top block in the stack. + * @returns {IntermediateScript} Intermediate script. + */ + generateScriptTree (generator, topBlockId) { + const result = generator.generate(topBlockId); + this.addProcedureDependencies(result.dependedProcedures); + return result; + } + + /** + * Recursively analyze a script and its dependencies. + * @param {IntermediateScript} script Intermediate script. + */ + analyzeScript (script) { + let madeChanges = false; + for (const procedureCode of script.dependedProcedures) { + const procedureData = this.procedures[procedureCode]; + + // Analyze newly found procedures. + if (!this.analyzedProcedures.includes(procedureCode)) { + this.analyzedProcedures.push(procedureCode); + if (this.analyzeScript(procedureData)) { + madeChanges = true; + } + this.analyzedProcedures.pop(); + } + + // If a procedure used by a script may yield, the script itself may yield. + if (procedureData.yields && !script.yields) { + script.yields = true; + madeChanges = true; + } + } + return madeChanges; + } + + /** + * @returns {IntermediateRepresentation} Intermediate representation. + */ + generate () { + const entry = this.generateScriptTree(new ScriptTreeGenerator(this.thread), this.thread.topBlock); + + // Compile any required procedures. + // As procedures can depend on other procedures, this process may take several iterations. + const procedureTreeCache = this.blocks._cache.compiledProcedures; + while (this.proceduresToCompile.size > 0) { + this.compilingProcedures = this.proceduresToCompile; + this.proceduresToCompile = new Map(); + + for (const [procedureVariant, definitionId] of this.compilingProcedures.entries()) { + if (procedureTreeCache[procedureVariant]) { + const result = procedureTreeCache[procedureVariant]; + this.procedures[procedureVariant] = result; + this.addProcedureDependencies(result.dependedProcedures); + } else { + const isWarp = parseIsWarp(procedureVariant); + const generator = new ScriptTreeGenerator(this.thread); + generator.setProcedureVariant(procedureVariant); + if (isWarp) generator.enableWarp(); + const compiledProcedure = this.generateScriptTree(generator, definitionId); + this.procedures[procedureVariant] = compiledProcedure; + procedureTreeCache[procedureVariant] = compiledProcedure; + } + } + } + + // Analyze scripts until no changes are made. + while (this.analyzeScript(entry)); + + const ir = new IntermediateRepresentation(); + ir.entry = entry; + ir.procedures = this.procedures; + return ir; + } + + static exports = { + ScriptTreeGenerator + } +} + +module.exports = IRGenerator; diff --git a/local-scratch-vm/src/compiler/jsexecute.js b/local-scratch-vm/src/compiler/jsexecute.js new file mode 100644 index 0000000000000000000000000000000000000000..77a383804fb7cd693cd0865e40e07284f3a0924c --- /dev/null +++ b/local-scratch-vm/src/compiler/jsexecute.js @@ -0,0 +1,715 @@ +/** + * @fileoverview Runtime for scripts generated by jsgen + */ + +/* eslint-disable no-unused-vars */ +/* eslint-disable prefer-template */ +/* eslint-disable valid-jsdoc */ +/* eslint-disable max-len */ + +const globalState = { + Timer: require('../util/timer'), + Cast: require('../util/cast'), + log: require('../util/log'), + blockUtility: require('./compat-block-utility'), + thread: null +}; + +let baseRuntime = ''; +const runtimeFunctions = {}; + +/** + * Determine whether the current tick is likely stuck. + * This implements similar functionality to the warp timer found in Scratch. + * @returns {boolean} true if the current tick is likely stuck. + */ +baseRuntime += `let stuckCounter = 0; +const isStuck = () => { + // The real time is not checked on every call for performance. + stuckCounter++; + if (stuckCounter === 100) { + stuckCounter = 0; + return globalState.thread.target.runtime.sequencer.timer.timeElapsed() > 500; + } + return false; +};`; + +/** + * Alternative for nullish Coalescing + * @param {string} name The variable to get + * @returns {any} The value of the temp var or an empty string if its nullish + */ +runtimeFunctions.nullish = `const nullish = (check, alt) => { + if (!check) { + if (val === undefined) return alt + if (val === null) return alt + return check + } else { + return check + } +}`; + +/** + * Start hats by opcode. + * @param {string} requestedHat The opcode of the hat to start. + * @param {*} optMatchFields Fields to match. + * @returns {Array} A list of threads that were started. + */ +runtimeFunctions.startHats = `const startHats = (requestedHat, optMatchFields) => { + const thread = globalState.thread; + const threads = thread.target.runtime.startHats(requestedHat, optMatchFields); + return threads; +}`; + +/** + * Implements "thread waiting", where scripts are halted until all the scripts have finished executing. + * @param {Array} threads The list of threads. + */ +runtimeFunctions.waitThreads = `const waitThreads = function*(threads) { + const thread = globalState.thread; + const runtime = thread.target.runtime; + + while (true) { + // determine whether any threads are running + let anyRunning = false; + for (let i = 0; i < threads.length; i++) { + if (runtime.threads.indexOf(threads[i]) !== -1) { + anyRunning = true; + break; + } + } + if (!anyRunning) { + // all threads are finished, can resume + return; + } + + let allWaiting = true; + for (let i = 0; i < threads.length; i++) { + if (!runtime.isWaitingThread(threads[i])) { + allWaiting = false; + break; + } + } + if (allWaiting) { + thread.status = 3; // STATUS_YIELD_TICK + } + + yield; + } +}`; + +/** + * waitPromise: Wait until a Promise resolves or rejects before continuing. + * @param {Promise} promise The promise to wait for. + * @returns {*} the value that the promise resolves to, otherwise undefined if the promise rejects + */ +runtimeFunctions.waitPromise = ` +const waitPromise = function*(promise) { + const thread = globalState.thread; + let returnValue; + let errorReturn; + + promise + .then(value => { + returnValue = value; + thread.status = 0; // STATUS_RUNNING + }) + .catch(error => { + errorReturn = error; + // i realized, i dont actually know what would happen if we never do this but throw and exit anyways + thread.status = 0; // STATUS_RUNNING + }); + + // enter STATUS_PROMISE_WAIT and yield + // this will stop script execution until the promise handlers reset the thread status + thread.status = 1; // STATUS_PROMISE_WAIT + yield; + + // throw the promise error if ee got one + if (errorReturn) throw errorReturn + return returnValue; +}`; + +/** + * isPromise: Determine if a value is Promise-like + * @param {unknown} promise The value to check + * @returns {promise is PromiseLike} True if the value is Promise-like (has a .then()) + */ + +/** + * executeInCompatibilityLayer: Execute a scratch-vm primitive. + * @param {*} inputs The inputs to pass to the block. + * @param {function} blockFunction The primitive's function. + * @param {boolean} useFlags Whether to set flags (hasResumedFromPromise) + * @param {string} blockId Block ID to set on the emulated block utility. + * @param {*|null} branchInfo Extra information object for CONDITIONAL and LOOP blocks. See createBranchInfo(). + * @returns {*} the value returned by the block, if any. + */ +runtimeFunctions.executeInCompatibilityLayer = `let hasResumedFromPromise = false; + +const isPromise = value => ( + // see engine/execute.js + value !== null && + typeof value === 'object' && + typeof value.then === 'function' +); +const executeInCompatibilityLayer = function*(inputs, blockFunction, isWarp, useFlags, blockId, branchInfo, visualReport) { + const thread = globalState.thread; + const blockUtility = globalState.blockUtility; + const stackFrame = branchInfo ? branchInfo.stackFrame : {}; + const finish = (returnValue) => { + if (branchInfo) { + if (typeof returnValue === 'undefined' && blockUtility._startedBranch) { + branchInfo.isLoop = blockUtility._startedBranch[1]; + return blockUtility._startedBranch[0]; + } + branchInfo.isLoop = branchInfo.defaultIsLoop; + return returnValue; + } + return returnValue; + }; + + // reset the stackframe + // we only ever use one stackframe at a time, so this shouldn't cause issues + thread.stackFrames[thread.stackFrames.length - 1].reuse(isWarp); + + const executeBlock = () => { + blockUtility.init(thread, blockId, stackFrame, branchInfo); + return blockFunction(inputs, blockUtility, visualReport); + }; + + let returnValue = executeBlock(); + if (isPromise(returnValue)) { + returnValue = finish(yield* waitPromise(returnValue)); + if (useFlags) hasResumedFromPromise = true; + return returnValue; + } + + if (thread.status === 1 /* STATUS_PROMISE_WAIT */ || thread.status === 4 /* STATUS_DONE */) { + // Something external is forcing us to stop + yield; + // Make up a return value because whatever is forcing us to stop can't specify one + return ''; + } + + while (thread.status === 2 /* STATUS_YIELD */ || thread.status === 3 /* STATUS_YIELD_TICK */) { + // Yielded threads will run next iteration. + if (thread.status === 2 /* STATUS_YIELD */) { + thread.status = 0; // STATUS_RUNNING + // Yield back to the event loop when stuck or not in warp mode. + if (!isWarp || isStuck()) { + yield; + } + } else { + // status is STATUS_YIELD_TICK, always yield to the event loop + yield; + } + + returnValue = executeBlock(); + if (isPromise(returnValue)) { + returnValue = finish(yield* waitPromise(returnValue)); + if (useFlags) hasResumedFromPromise = true; + return returnValue; + } + + if (thread.status === 1 /* STATUS_PROMISE_WAIT */ || thread.status === 4 /* STATUS_DONE */) { + yield; + return finish(''); + } + } + return finish(returnValue); +}`; + +/** + * @param {boolean} isLoop True if the block is a LOOP by default (can be overridden by startBranch() call) + * @returns {unknown} Branch info object for compatibility layer. + */ +runtimeFunctions.createBranchInfo = `const createBranchInfo = (isLoop) => ({ + defaultIsLoop: isLoop, + isLoop: false, + branch: 0, + stackFrame: {}, + onEnd: [], +});`; + +/** + * End the current script. + */ +runtimeFunctions.retire = `const retire = () => { + const thread = globalState.thread; + thread.target.runtime.sequencer.retireThread(thread); +}`; + +/** + * Scratch cast to boolean. + * Similar to Cast.toBoolean() + * @param {*} value The value to cast + * @returns {boolean} The value cast to a boolean + */ +runtimeFunctions.toBoolean = `const toBoolean = value => { + if (typeof value === 'boolean') { + return value; + } + if (typeof value === 'string') { + if (value === '' || value === '0' || value.toLowerCase() === 'false') { + return false; + } + return true; + } + return !!value; +}`; + +/** + * If a number is very close to a whole number, round to that whole number. + * @param {number} value Value to round + * @returns {number} Rounded number or original number + */ +runtimeFunctions.limitPrecision = `const limitPrecision = value => { + const rounded = Math.round(value); + const delta = value - rounded; + return (Math.abs(delta) < 1e-9) ? rounded : value; +}`; + +/** + * Used internally by the compare family of function. + * See similar method in cast.js. + * @param {*} val A value that evaluates to 0 in JS string-to-number conversation such as empty string, 0, or tab. + * @returns {boolean} True if the value should not be treated as the number zero. + */ +baseRuntime += `const isNotActuallyZero = val => { + if (typeof val !== 'string') return false; + for (let i = 0; i < val.length; i++) { + const code = val.charCodeAt(i); + if (code === 48 || code === 9) { + return false; + } + } + return true; +};`; + +/** + * Determine if two values are equal. + * @param {*} v1 First value + * @param {*} v2 Second value + * @returns {boolean} true if v1 is equal to v2 + */ +baseRuntime += `const compareEqualSlow = (v1, v2) => { + const n1 = +v1; + if (isNaN(n1) || (n1 === 0 && isNotActuallyZero(v1))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase(); + const n2 = +v2; + if (isNaN(n2) || (n2 === 0 && isNotActuallyZero(v2))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase(); + return n1 === n2; +}; +const compareEqual = (v1, v2) => (typeof v1 === 'number' && typeof v2 === 'number' && !isNaN(v1) && !isNaN(v2) || v1 === v2) ? v1 === v2 : compareEqualSlow(v1, v2);`; + +/** + * Determine if one value is greater than another. + * @param {*} v1 First value + * @param {*} v2 Second value + * @returns {boolean} true if v1 is greater than v2 + */ +runtimeFunctions.compareGreaterThan = `const compareGreaterThanSlow = (v1, v2) => { + let n1 = +v1; + let n2 = +v2; + if (n1 === 0 && isNotActuallyZero(v1)) { + n1 = NaN; + } else if (n2 === 0 && isNotActuallyZero(v2)) { + n2 = NaN; + } + if (isNaN(n1) || isNaN(n2)) { + const s1 = ('' + v1).toLowerCase(); + const s2 = ('' + v2).toLowerCase(); + return s1 > s2; + } + return n1 > n2; +}; +const compareGreaterThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && !isNaN(v1) ? v1 > v2 : compareGreaterThanSlow(v1, v2)`; + +/** + * Determine if one value is less than another. + * @param {*} v1 First value + * @param {*} v2 Second value + * @returns {boolean} true if v1 is less than v2 + */ +runtimeFunctions.compareLessThan = `const compareLessThanSlow = (v1, v2) => { + let n1 = +v1; + let n2 = +v2; + if (n1 === 0 && isNotActuallyZero(v1)) { + n1 = NaN; + } else if (n2 === 0 && isNotActuallyZero(v2)) { + n2 = NaN; + } + if (isNaN(n1) || isNaN(n2)) { + const s1 = ('' + v1).toLowerCase(); + const s2 = ('' + v2).toLowerCase(); + return s1 < s2; + } + return n1 < n2; +}; +const compareLessThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && !isNaN(v2) ? v1 < v2 : compareLessThanSlow(v1, v2)`; + +/** + * Generate a random integer. + * @param {number} low Lower bound + * @param {number} high Upper bound + * @returns {number} A random integer between low and high, inclusive. + */ +runtimeFunctions.randomInt = `const randomInt = (low, high) => low + Math.floor(Math.random() * ((high + 1) - low))`; + +/** + * Generate a random float. + * @param {number} low Lower bound + * @param {number} high Upper bound + * @returns {number} A random floating point number between low and high. + */ +runtimeFunctions.randomFloat = `const randomFloat = (low, high) => (Math.random() * (high - low)) + low`; + +/** + * Create and start a timer. + * @returns {Timer} A started timer + */ +runtimeFunctions.timer = `const timer = () => { + const t = new globalState.Timer({ + now: () => globalState.thread.target.runtime.currentMSecs + }); + t.start(); + return t; +}`; + +/** + * Returns the amount of days since January 1st, 2000. + * @returns {number} Days since 2000. + */ +// Date.UTC(2000, 0, 1) === 946684800000 +// Hardcoding it is marginally faster +runtimeFunctions.daysSince2000 = `const daysSince2000 = () => (Date.now() - 946684800000) / (24 * 60 * 60 * 1000)`; + +/** + * Determine distance to a sprite or point. + * @param {string} menu The name of the sprite or location to find. + * @returns {number} Distance to the point, or 10000 if it cannot be calculated. + */ +runtimeFunctions.distance = `const distance = menu => { + const thread = globalState.thread; + if (thread.target.isStage) return 10000; + + let targetX = 0; + let targetY = 0; + if (menu === '_mouse_') { + targetX = thread.target.runtime.ioDevices.mouse.getScratchX(); + targetY = thread.target.runtime.ioDevices.mouse.getScratchY(); + } else { + const distTarget = thread.target.runtime.getSpriteTargetByName(menu); + if (!distTarget) return 10000; + targetX = distTarget.x; + targetY = distTarget.y; + } + + const dx = thread.target.x - targetX; + const dy = thread.target.y - targetY; + return Math.sqrt((dx * dx) + (dy * dy)); +}`; + +/** + * Convert a Scratch list index to a JavaScript list index. + * "all" is not considered as a list index. + * Similar to Cast.toListIndex() + * @param {number} index Scratch list index. + * @param {number} length Length of the list. + * @returns {number} 0 based list index, or -1 if invalid. + */ +baseRuntime += `const listIndexSlow = (index, length) => { + if (index === 'last') { + return length - 1; + } else if (index === 'random' || index === 'any') { + if (length > 0) { + return (Math.random() * length) | 0; + } + return -1; + } + index = (+index || 0) | 0; + if (index < 1 || index > length) { + return -1; + } + return index - 1; +}; +const listIndex = (index, length) => { + if (typeof index !== 'number') { + return listIndexSlow(index, length); + } + index = index | 0; + return index < 1 || index > length ? -1 : index - 1; +};`; + +/** + * Get a value from a list. + * @param {Array} list The list + * @param {*} idx The 1-indexed index in the list. + * @returns {*} The list item, otherwise empty string if it does not exist. + */ +runtimeFunctions.listGet = `const listGet = (list, idx) => { + const index = listIndex(idx, list.length); + if (index === -1) { + return ''; + } + return list[index]; +}`; + +/** + * Replace a value in a list. + * @param {import('../engine/variable')} list The list + * @param {*} idx List index, Scratch style. + * @param {*} value The new value. + */ +runtimeFunctions.listReplace = `const listReplace = (list, idx, value) => { + const index = listIndex(idx, list.value.length); + if (index === -1) { + return; + } + list.value[index] = value; + list._monitorUpToDate = false; +}`; + +/** + * Insert a value in a list. + * @param {import('../engine/variable')} list The list. + * @param {*} idx The Scratch index in the list. + * @param {*} value The value to insert. + */ +runtimeFunctions.listInsert = `const listInsert = (list, idx, value) => { + const index = listIndex(idx, list.value.length + 1); + if (index === -1) { + return; + } + list.value.splice(index, 0, value); + list._monitorUpToDate = false; +}`; + +/** + * Delete a value from a list. + * @param {import('../engine/variable')} list The list. + * @param {*} idx The Scratch index in the list. + */ +runtimeFunctions.listDelete = `const listDelete = (list, idx) => { + if (idx === 'all') { + list.value = []; + return; + } + const index = listIndex(idx, list.value.length); + if (index === -1) { + return; + } + list.value.splice(index, 1); + list._monitorUpToDate = false; +}`; + +/** + * Return whether a list contains a value. + * @param {import('../engine/variable')} list The list. + * @param {*} item The value to search for. + * @returns {boolean} True if the list contains the item + */ +runtimeFunctions.listContains = `const listContains = (list, item) => { + // TODO: evaluate whether indexOf is worthwhile here + if (list.value.indexOf(item) !== -1) { + return true; + } + for (let i = 0; i < list.value.length; i++) { + if (compareEqual(list.value[i], item)) { + return true; + } + } + return false; +}`; + +/** + * pm: Returns whether a list contains a value, using Array.some + * @param {import('../engine/variable')} list The list. + * @param {*} item The value to search for. + * @returns {boolean} True if the list contains the item + */ +runtimeFunctions.listContainsFastest = `const listContainsFastest = (list, item) => { + return list.value.some(litem => compareEqual(litem, item)); +}`; + +/** + * Find the 1-indexed index of an item in a list. + * @param {import('../engine/variable')} list The list. + * @param {*} item The item to search for + * @returns {number} The 1-indexed index of the item in the list, otherwise 0 + */ +runtimeFunctions.listIndexOf = `const listIndexOf = (list, item) => { + for (let i = 0; i < list.value.length; i++) { + if (compareEqual(list.value[i], item)) { + return i + 1; + } + } + return 0; +}`; + +/** + * Get the stringified form of a list. + * @param {import('../engine/variable')} list The list. + * @returns {string} Stringified form of the list. + */ +runtimeFunctions.listContents = `const listContents = list => { + for (let i = 0; i < list.value.length; i++) { + const listItem = list.value[i]; + // this is an intentional break from what scratch 3 does to address our automatic string -> number conversions + // it fixes more than it breaks + if ((listItem + '').length !== 1) { + return list.value.join(' '); + } + } + return list.value.join(''); +}`; + +/** + * Convert a color to an RGB list + * @param {*} color The color value to convert + * @return {Array.} [r,g,b], values between 0-255. + */ +runtimeFunctions.colorToList = `const colorToList = color => globalState.Cast.toRgbColorList(color)`; + +/** + * Implements Scratch modulo (floored division instead of truncated division) + * @param {number} n Number + * @param {number} modulus Base + * @returns {number} n % modulus (floored division) + */ +runtimeFunctions.mod = `const mod = (n, modulus) => { + let result = n % modulus; + if (result / modulus < 0) result += modulus; + return result; +}`; + +/** + * Implements Scratch tangent. + * @param {number} angle Angle in degrees. + * @returns {number} value of tangent or Infinity or -Infinity + */ +runtimeFunctions.tan = `const tan = (angle) => { + switch (angle % 360) { + case -270: case 90: return Infinity; + case -90: case 270: return -Infinity; + } + return Math.round(Math.tan((Math.PI * angle) / 180) * 1e10) / 1e10; +}`; + +runtimeFunctions.resolveImageURL = `const resolveImageURL = imgURL => + typeof imgURL === 'object' && imgURL.type === 'canvas' + ? Promise.resolve(imgURL.canvas) + : new Promise(resolve => { + const image = new Image(); + image.crossOrigin = "anonymous"; + image.onload = resolve(image); + image.onerror = resolve; // ignore loading errors lol! + image.src = ''+imgURL; + })`; + +runtimeFunctions.parseJSONSafe = `const parseJSONSafe = json => { + try return JSON.parse(json) + catch return {} +}`; + +runtimeFunctions._resolveKeyPath = `const _resolveKeyPath = (obj, keyPath) => { + const path = keyPath.matchAll(/(\\.|^)(?[^.[]+)|\\[(?(\\\\\\]|\\\\|[^]])+)\\]/g); + let top = obj; + let pre; + let tok; + let key; + while (!(tok = path.next()).done) { + key = tok.value.groups.key ?? tok.value.groups.litKey.replaceAll('\\\\\\\\', '\\\\').replaceAll('\\\\]', ']'); + pre = top; + top = top?.get?.(key) ?? top?.[key]; + if (top === undefined) return [obj, keyPath]; + } + return [pre, key]; +}`; + +runtimeFunctions.get = `const get = (obj, keyPath) => { + const [root, key] = _resolveKeyPath(obj, keyPath); + return typeof root === 'undefined' + ? '' + : root.get?.(key) ?? root[key]; +}`; + +runtimeFunctions.set = `const set = (obj, keyPath, val) => { + const [root, key] = _resolveKeyPath(obj, keyPath); + return typeof root === 'undefined' + ? '' + : root.set?.(key) ?? (root[key] = val); +}`; + +runtimeFunctions.remove = `const remove = (obj, keyPath) => { + const [root, key] = _resolveKeyPath(obj, keyPath); + return typeof root === 'undefined' + ? '' + : root.delete?.(key) ?? root.remove?.(key) ?? (delete root[key]); +}`; + +runtimeFunctions.includes = `const includes = (obj, keyPath) => { + const [root, key] = _resolveKeyPath(obj, keyPath); + return typeof root === 'undefined' + ? '' + : root.has?.(key) ?? (key in root); +}`; + +/** + * Step a compiled thread. + * @param {Thread} thread The thread to step. + */ +const execute = thread => { + globalState.thread = thread; + thread.generator.next(); +}; + +const threadStack = []; +const saveGlobalState = () => { + threadStack.push(globalState.thread); +}; +const restoreGlobalState = () => { + globalState.thread = threadStack.pop(); +}; + +const insertRuntime = source => { + let result = baseRuntime; + for (const functionName of Object.keys(runtimeFunctions)) { + if (source.includes(functionName)) { + result += `${runtimeFunctions[functionName]};`; + } + } + if (result.includes('executeInCompatibilityLayer') && !result.includes('const waitPromise')) { + result = result.replace('let hasResumedFromPromise = false;', `let hasResumedFromPromise = false;\n${runtimeFunctions.waitPromise}`); + } + if (result.includes('_resolveKeyPath') && !result.includes('const _resolveKeyPath')) { + result = runtimeFunctions._resolveKeyPath + ';' + result; + } + result += `return ${source}`; + return result; +}; + +/** + * Evaluate arbitrary JS in the context of the runtime. + * @param {string} source The string to evaluate. + * @returns {*} The result of evaluating the string. + */ +const scopedEval = source => { + const withRuntime = insertRuntime(source); + try { + return new Function('globalState', withRuntime)(globalState); + } catch (e) { + globalState.log.error('was unable to compile script', withRuntime); + console.log(e); + throw e; + } +}; + +execute.scopedEval = scopedEval; +execute.runtimeFunctions = runtimeFunctions; +execute.saveGlobalState = saveGlobalState; +execute.restoreGlobalState = restoreGlobalState; +// not actually used, this is an export for extensions +execute.globalState = globalState; + +module.exports = execute; diff --git a/local-scratch-vm/src/compiler/jsgen.js b/local-scratch-vm/src/compiler/jsgen.js new file mode 100644 index 0000000000000000000000000000000000000000..0a5773a46be9768d86cd96f9081e3c8f0fe5ef5c --- /dev/null +++ b/local-scratch-vm/src/compiler/jsgen.js @@ -0,0 +1,2114 @@ +const log = require('../util/log'); +const Cast = require('../util/cast'); +const BlockType = require('../extension-support/block-type'); +const VariablePool = require('./variable-pool'); +const jsexecute = require('./jsexecute'); +const environment = require('./environment'); + +// Imported for JSDoc types, not to actually use +// eslint-disable-next-line no-unused-vars +const {IntermediateScript, IntermediateRepresentation} = require('./intermediate'); + +/** + * @fileoverview Convert intermediate representations to JavaScript functions. + */ + +/* eslint-disable max-len */ +/* eslint-disable prefer-template */ + +const sanitize = string => { + if (typeof string !== 'string') { + log.warn(`sanitize got unexpected type: ${typeof string}`); + string = '' + string; + } + return JSON.stringify(string).slice(1, -1); +}; + +const TYPE_NUMBER = 1; +const TYPE_STRING = 2; +const TYPE_BOOLEAN = 3; +const TYPE_UNKNOWN = 4; +const TYPE_NUMBER_NAN = 5; + + +// Pen-related constants +const PEN_EXT = 'runtime.ext_pen'; +const PEN_STATE = `${PEN_EXT}._getPenState(target)`; + +/** + * Variable pool used for factory function names. + */ +const factoryNameVariablePool = new VariablePool('factory'); + +/** + * Variable pool used for generated functions (non-generator) + */ +const functionNameVariablePool = new VariablePool('fun'); + +/** + * Variable pool used for generated generator functions. + */ +const generatorNameVariablePool = new VariablePool('gen'); + +/** + * @typedef Input + * @property {() => string} asNumber + * @property {() => string} asNumberOrNaN + * @property {() => string} asString + * @property {() => string} asBoolean + * @property {() => string} asColor + * @property {() => string} asUnknown + * @property {() => string} asSafe + * @property {() => boolean} isAlwaysNumber + * @property {() => boolean} isAlwaysNumberOrNaN + * @property {() => boolean} isNeverNumber + */ + +/** + * @implements {Input} + */ +class TypedInput { + constructor (source, type) { + this.source = source; + this.type = type; + } + + asNumber () { + if (this.type === TYPE_NUMBER) return this.source; + if (this.type === TYPE_NUMBER_NAN) return `(${this.source} || 0)`; + return `(+${this.source} || 0)`; + } + + asNumberOrNaN () { + if (this.type === TYPE_NUMBER || this.type === TYPE_NUMBER_NAN) return this.source; + return `(+${this.source})`; + } + + asString () { + if (this.type === TYPE_STRING) return this.source; + return `("" + ${this.source})`; + } + + asBoolean () { + if (this.type === TYPE_UNKNOWN) return `toBoolean(${this.source})`; + if (this.type === TYPE_STRING) return `${this.source} === 'false' || ${this.source} === '0' ? false : true`; + if (this.type === TYPE_NUMBER) return `${this.source} !== 0`; + if (this.type === TYPE_NUMBER_NAN) return `(${this.source} || 0) !== 0`; + + return this.source; + } + + asColor () { + return this.asUnknown(); + } + + asUnknown () { + return this.source; + } + + asSafe () { + return this.asUnknown(); + } + + isAlwaysNumber () { + return this.type === TYPE_NUMBER; + } + + isAlwaysNumberOrNaN () { + return this.type === TYPE_NUMBER || this.type === TYPE_NUMBER_NAN; + } + + isNeverNumber () { + return false; + } +} + +/** + * @implements {Input} + */ +class ConstantInput { + constructor (constantValue, safe) { + this.constantValue = constantValue; + this.safe = safe; + } + + asNumber () { + // Compute at compilation time + const numberValue = +this.constantValue; + if (numberValue) { + // It's important that we use the number's stringified value and not the constant value + // Using the constant value allows numbers such as "010" to be interpreted as 8 (or SyntaxError in strict mode) instead of 10. + return numberValue.toString(); + } + // numberValue is one of 0, -0, or NaN + if (Object.is(numberValue, -0)) { + return '-0'; + } + return '0'; + } + + asNumberOrNaN () { + return this.asNumber(); + } + + asString () { + return `"${sanitize('' + this.constantValue)}"`; + } + + asBoolean () { + // Compute at compilation time + return Cast.toBoolean(this.constantValue).toString(); + } + + asColor () { + // Attempt to parse hex code at compilation time + if (/^#[0-9a-f]{6,8}$/i.test(this.constantValue)) { + const hex = this.constantValue.slice(1); + return Number.parseInt(hex, 16).toString(); + } + return this.asUnknown(); + } + + asUnknown () { + // Attempt to convert strings to numbers if it is unlikely to break things + if (typeof this.constantValue === 'number') { + // todo: handle NaN? + return this.constantValue; + } + const numberValue = +this.constantValue; + if (numberValue.toString() === this.constantValue) { + return this.constantValue; + } + return this.asString(); + } + + asSafe () { + if (this.safe) { + return this.asUnknown(); + } + return this.asString(); + } + + isAlwaysNumber () { + const value = +this.constantValue; + if (Number.isNaN(value)) { + return false; + } + // Empty strings evaluate to 0 but should not be considered a number. + if (value === 0) { + return this.constantValue.toString().trim() !== ''; + } + return true; + } + + isAlwaysNumberOrNaN () { + return this.isAlwaysNumber(); + } + + isNeverNumber () { + return Number.isNaN(+this.constantValue); + } +} + +/** + * @implements {Input} + */ +class VariableInput { + constructor (source) { + this.source = source; + this.type = TYPE_UNKNOWN; + /** + * The value this variable was most recently set to, if any. + * @type {Input} + * @private + */ + this._value = null; + } + + /** + * @param {Input} input The input this variable was most recently set to. + */ + setInput (input) { + if (input instanceof VariableInput) { + // When being set to another variable, extract the value it was set to. + // Otherwise, you may end up with infinite recursion in analysis methods when a variable is set to itself. + if (input._value) { + input = input._value; + } else { + this.type = TYPE_UNKNOWN; + this._value = null; + return; + } + } + this._value = input; + if (input instanceof TypedInput) { + this.type = input.type; + } else { + this.type = TYPE_UNKNOWN; + } + } + + asNumber () { + if (this.type === TYPE_NUMBER) return this.source; + if (this.type === TYPE_NUMBER_NAN) return `(${this.source} || 0)`; + return `(+${this.source} || 0)`; + } + + asNumberOrNaN () { + if (this.type === TYPE_NUMBER || this.type === TYPE_NUMBER_NAN) return this.source; + return `(+${this.source})`; + } + + asString () { + if (this.type === TYPE_STRING) return this.source; + return `("" + ${this.source})`; + } + + asBoolean () { + if (this.type === TYPE_BOOLEAN) return this.source; + return `toBoolean(${this.source})`; + } + + asColor () { + return this.asUnknown(); + } + + asUnknown () { + return this.source; + } + + asSafe () { + return this.asUnknown(); + } + + isAlwaysNumber () { + if (this._value) { + return this._value.isAlwaysNumber(); + } + return false; + } + + isAlwaysNumberOrNaN () { + if (this._value) { + return this._value.isAlwaysNumberOrNaN(); + } + return false; + } + + isNeverNumber () { + if (this._value) { + return this._value.isNeverNumber(); + } + return false; + } +} + +const getNamesOfCostumesAndSounds = runtime => { + const result = new Set(); + for (const target of runtime.targets) { + if (target.isOriginal) { + const sprite = target.sprite; + for (const costume of sprite.costumes) { + result.add(costume.name); + } + for (const sound of sprite.sounds) { + result.add(sound.name); + } + } + } + return result; +}; + +const isSafeConstantForEqualsOptimization = input => { + const numberValue = +input.constantValue; + // Do not optimize 0 + if (!numberValue) { + return false; + } + // Do not optimize numbers when the original form does not match + return numberValue.toString() === input.constantValue.toString(); +}; + +/** + * A frame contains some information about the current substack being compiled. + */ +class Frame { + constructor (isLoop, parentKind) { + /** + * Whether the current stack runs in a loop (while, for) + * @type {boolean} + * @readonly + */ + this.isLoop = isLoop; + + /** + * Whether the current block is the last block in the stack. + * @type {boolean} + */ + this.isLastBlock = false; + + /** + * General important data that needs to be carried down from other threads. + * @type {boolean} + */ + this.importantData = { + parents: [parentKind] + }; + if (isLoop) + this.importantData.containedByLoop = isLoop; + + /** + * the block who created this frame + * @type {string} + * @readonly + */ + this.parent = parentKind; + } + + assignData(obj) { + if (obj instanceof Frame) { + obj = obj.importantData; + obj.parents = obj.parents.concat(this.importantData.parents); + } + Object.assign(this.importantData, obj); + } +} + +class JSGenerator { + /** + * @param {IntermediateScript} script + * @param {IntermediateRepresentation} ir + * @param {Target} target + */ + constructor (script, ir, target) { + this.script = script; + this.ir = ir; + this.target = target; + this.source = ''; + + /** + * @type {Object.} + */ + this.variableInputs = {}; + + this.isWarp = script.isWarp; + this.isOptimized = script.isOptimized; + this.optimizationUtil = script.optimizationUtil; + this.isProcedure = script.isProcedure; + this.warpTimer = script.warpTimer; + + /** + * Stack of frames, most recent is last item. + * @type {Frame[]} + */ + this.frames = []; + + /** + * The current Frame. + * @type {Frame} + */ + this.currentFrame = null; + + this.namesOfCostumesAndSounds = getNamesOfCostumesAndSounds(target.runtime); + + this.localVariables = new VariablePool('a'); + this._setupVariablesPool = new VariablePool('b'); + this._setupVariables = {}; + + this.descendedIntoModulo = false; + this.isInHat = false; + + this.debug = this.target.runtime.debug; + } + + static exports = { + TypedInput, + ConstantInput, + VariableInput, + Frame, + VariablePool, + TYPE_NUMBER, + TYPE_STRING, + TYPE_BOOLEAN, + TYPE_UNKNOWN, + TYPE_NUMBER_NAN, + PEN_EXT, + PEN_STATE, + factoryNameVariablePool, + functionNameVariablePool, + generatorNameVariablePool + } + + static _extensionJSInfo = {}; + static setExtensionJs(id, data) { + JSGenerator._extensionJSInfo[id] = data; + } + static hasExtensionJs(id) { + return Boolean(JSGenerator._extensionJSInfo[id]); + } + static getExtensionJs(id) { + return JSGenerator._extensionJSInfo[id]; + } + + static getExtensionImports() { + // used so extensions have things like the Frame class + return { + Frame: Frame, + TypedInput: TypedInput, + VariableInput: VariableInput, + ConstantInput: ConstantInput, + VariablePool: VariablePool, + + TYPE_NUMBER: TYPE_NUMBER, + TYPE_STRING: TYPE_STRING, + TYPE_BOOLEAN: TYPE_BOOLEAN, + TYPE_UNKNOWN: TYPE_UNKNOWN, + TYPE_NUMBER_NAN: TYPE_NUMBER_NAN + }; + } + + /** + * Enter a new frame + * @param {Frame} frame New frame. + */ + pushFrame (frame) { + this.frames.push(frame); + this.currentFrame = frame; + } + + /** + * Exit the current frame + */ + popFrame () { + this.frames.pop(); + this.currentFrame = this.frames[this.frames.length - 1]; + } + + /** + * @returns {boolean} true if the current block is the last command of a loop + */ + isLastBlockInLoop () { + for (let i = this.frames.length - 1; i >= 0; i--) { + const frame = this.frames[i]; + if (!frame.isLastBlock) { + return false; + } + if (frame.isLoop) { + return true; + } + } + return false; + } + + /** + * @param {object} node Input node to compile. + * @param {boolean} visualReport if this is being called to get visual reporter content + * @returns {Input} Compiled input. + */ + descendInput (node, visualReport = false) { + // check if we have extension js for this kind + const extensionId = String(node.kind).split('.')[0]; + const blockId = String(node.kind).replace(extensionId + '.', ''); + if (JSGenerator.hasExtensionJs(extensionId) && JSGenerator.getExtensionJs(extensionId)[blockId]) { + // this is an extension block that wants to be compiled + const imports = JSGenerator.getExtensionImports(); + const jsFunc = JSGenerator.getExtensionJs(extensionId)[blockId]; + // return the input + let input = null; + try { + input = jsFunc(node, this, imports); + } catch (err) { + log.warn(extensionId + '_' + blockId, 'failed to compile JavaScript;', err); + } + // log.log(input); + return input; + } + + switch (node.kind) { + case 'args.boolean': + return new TypedInput(`toBoolean(p${node.index})`, TYPE_BOOLEAN); + case 'args.stringNumber': + return new TypedInput(`p${node.index}`, TYPE_UNKNOWN); + + case 'compat': + // Compatibility layer inputs never use flags. + // log.log('compat') + return new TypedInput(`(${this.generateCompatibilityLayerCall(node, false, null, visualReport)})`, TYPE_UNKNOWN); + + case 'constant': + return this.safeConstantInput(node.value); + case 'counter.get': + return new TypedInput('runtime.ext_scratch3_control._counter', TYPE_NUMBER); + case 'control.error': + return new TypedInput('runtime.ext_scratch3_control._error', TYPE_STRING); + case 'control.isclone': + return new TypedInput('(!target.isOriginal)', TYPE_BOOLEAN); + case 'math.polygon': + let points = JSON.stringify(node.points.map((point, num) => ({x: `x${num}`, y: `y${num}`}))); + for (let num = 0; num < node.points.length; num++) { + const point = node.points[num]; + const xn = `"x${num}"`; + const yn = `"y${num}"`; + points = points + .replace(xn, this.descendInput(point.x).asNumber()) + .replace(yn, this.descendInput(point.y).asNumber()); + } + return new TypedInput(points, TYPE_UNKNOWN); + + case 'control.inlineStackOutput': { + // reset this.source but save it + const originalSource = this.source; + this.source = '(yield* (function*() {'; + // descend now since descendStack modifies source + this.descendStack(node.code, new Frame(false, 'control.inlineStackOutput')); + this.source += '})())'; + // save edited + const stackSource = this.source; + this.source = originalSource; + return new TypedInput(stackSource, TYPE_UNKNOWN); + } + + case 'keyboard.pressed': + return new TypedInput(`runtime.ioDevices.keyboard.getKeyIsDown(${this.descendInput(node.key).asSafe()})`, TYPE_BOOLEAN); + + case 'list.contains': + if (this.isOptimized) { + // pm: we can use a better function here + return new TypedInput(`listContainsFastest(${this.referenceVariable(node.list)}, ${this.descendInput(node.item).asUnknown()})`, TYPE_BOOLEAN); + } + return new TypedInput(`listContains(${this.referenceVariable(node.list)}, ${this.descendInput(node.item).asUnknown()})`, TYPE_BOOLEAN); + case 'list.contents': + if (this.isOptimized) { + // pm: its more consistent to just return the list with spaces inbetween + return new TypedInput(`(${this.referenceVariable(node.list)}.value.join(' '))`, TYPE_STRING); + } + return new TypedInput(`listContents(${this.referenceVariable(node.list)})`, TYPE_STRING); + case 'list.get': { + const index = this.descendInput(node.index); + if (environment.supportsNullishCoalescing) { + if (index.isAlwaysNumberOrNaN()) { + return new TypedInput(`(${this.referenceVariable(node.list)}.value[(${index.asNumber()} | 0) - 1] ?? "")`, TYPE_UNKNOWN); + } + if (index instanceof ConstantInput && index.constantValue === 'last') { + return new TypedInput(`(${this.referenceVariable(node.list)}.value[${this.referenceVariable(node.list)}.value.length - 1] ?? "")`, TYPE_UNKNOWN); + } + } + if (this.isOptimized) { + // pm: we can just use this as an index ignoring the string input, the nullish coalescing operator will just make sure we dont return undefined + return new TypedInput(`(${this.referenceVariable(node.list)}.value[${index.asUnknown()} - 1] ?? "")`, TYPE_UNKNOWN); + } + return new TypedInput(`listGet(${this.referenceVariable(node.list)}.value, ${index.asUnknown()})`, TYPE_UNKNOWN); + } + case 'list.indexOf': + return new TypedInput(`listIndexOf(${this.referenceVariable(node.list)}, ${this.descendInput(node.item).asUnknown()})`, TYPE_NUMBER); + case 'list.amountOf': + return new TypedInput(`${this.referenceVariable(node.list)}.value.filter((x) => x == ${this.descendInput(node.value).asUnknown()}).length`, TYPE_NUMBER); + case 'list.length': + return new TypedInput(`${this.referenceVariable(node.list)}.value.length`, TYPE_NUMBER); + + case 'list.filteritem': + return new TypedInput('runtime.ext_scratch3_data._listFilterItem', TYPE_UNKNOWN); + case 'list.filterindex': + return new TypedInput('runtime.ext_scratch3_data._listFilterIndex', TYPE_UNKNOWN); + + case 'looks.size': + return new TypedInput('target.size', TYPE_NUMBER); + case 'looks.tintColor': + return new TypedInput('runtime.ext_scratch3_looks.getTintColor(null, { target: target })', TYPE_NUMBER); + case 'looks.backdropName': + return new TypedInput('stage.getCostumes()[stage.currentCostume].name', TYPE_STRING); + case 'looks.backdropNumber': + return new TypedInput('(stage.currentCostume + 1)', TYPE_NUMBER); + case 'looks.costumeName': + return new TypedInput('target.getCostumes()[target.currentCostume].name', TYPE_STRING); + case 'looks.costumeNumber': + return new TypedInput('(target.currentCostume + 1)', TYPE_NUMBER); + + case 'motion.direction': + return new TypedInput('target.direction', TYPE_NUMBER); + + case 'motion.x': + if (this.isOptimized) { + return new TypedInput('(target.x)', TYPE_NUMBER); + } + return new TypedInput('limitPrecision(target.x)', TYPE_NUMBER); + case 'motion.y': + if (this.isOptimized) { + return new TypedInput('(target.y)', TYPE_NUMBER); + } + return new TypedInput('limitPrecision(target.y)', TYPE_NUMBER); + + case 'mouse.down': + return new TypedInput('runtime.ioDevices.mouse.getIsDown()', TYPE_BOOLEAN); + case 'mouse.x': + return new TypedInput('runtime.ioDevices.mouse.getScratchX()', TYPE_NUMBER); + case 'mouse.y': + return new TypedInput('runtime.ioDevices.mouse.getScratchY()', TYPE_NUMBER); + + case 'op.true': + return new TypedInput('(true)', TYPE_BOOLEAN); + case 'op.false': + return new TypedInput('(false)', TYPE_BOOLEAN); + case 'op.randbool': + return new TypedInput('(Boolean(Math.round(Math.random())))', TYPE_BOOLEAN); + + case 'pmEventsExpansion.broadcastFunction': + // we need to do function otherwise this block would be stupidly long + let source = '(yield* (function*() {'; + source += `var broadcastVar = runtime.getTargetForStage().lookupBroadcastMsg("", ${this.descendInput(node.broadcast).asString()} );`; + source += `if (broadcastVar) broadcastVar.isSent = true;`; + const threads = this.localVariables.next(); + source += `var ${threads} = startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: ${this.descendInput(node.broadcast).asString()} });`; + const threadVar = this.localVariables.next(); + source += `for (const ${threadVar} of ${threads}) { ${threadVar}.__evex_recievedDataa = '' };`; + source += `yield* waitThreads(${threads});`; + // wait an extra frame so the thread has the new value + if (this.isWarp) { + source += 'if (isStuck()) yield;\n'; + } else { + source += 'yield;\n'; + } + // Control may have been yielded to another script -- all bets are off. + this.resetVariableInputs(); + // get value + const value = this.localVariables.next(); + const thread = this.localVariables.next(); + source += `var ${value} = undefined;`; + source += `for (var ${thread} of ${threads}) {`; + // if not undefined, return value + source += `if (typeof ${thread}.__evex_returnDataa !== 'undefined') {`; + source += `return ${thread}.__evex_returnDataa;`; + source += `}`; + source += `}`; + // no value, return empty value + source += `return '';`; + source += '})())'; + return new TypedInput(source, TYPE_STRING); + case 'pmEventsExpansion.broadcastFunctionArgs': { + // we need to do function otherwise this block would be stupidly long + let source = '(yield* (function*() {'; + const threads = this.localVariables.next(); + source += `var broadcastVar = runtime.getTargetForStage().lookupBroadcastMsg("", ${this.descendInput(node.broadcast).asString()} );`; + source += `if (broadcastVar) broadcastVar.isSent = true;`; + source += `var ${threads} = startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: ${this.descendInput(node.broadcast).asString()} });`; + const threadVar = this.localVariables.next(); + source += `for (const ${threadVar} of ${threads}) { ${threadVar}.__evex_recievedDataa = ${this.descendInput(node.args).asString()} };`; + source += `yield* waitThreads(${threads});`; + // wait an extra frame so the thread has the new value + if (this.isWarp) { + source += 'if (isStuck()) yield;\n'; + } else { + source += 'yield;\n'; + } + // Control may have been yielded to another script -- all bets are off. + this.resetVariableInputs(); + // get value + const value = this.localVariables.next(); + const thread = this.localVariables.next(); + source += `var ${value} = undefined;`; + source += `for (var ${thread} of ${threads}) {`; + // if not undefined, return value + source += `if (typeof ${thread}.__evex_returnDataa !== 'undefined') {`; + source += `return ${thread}.__evex_returnDataa;`; + source += `}`; + source += `}`; + // no value, return empty value + source += `return '';`; + source += '})())'; + return new TypedInput(source, TYPE_STRING); + } + case 'op.abs': + return new TypedInput(`Math.abs(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); + case 'op.acos': + // Needs to be marked as NaN because Math.acos(1.0001) === NaN + return new TypedInput(`((Math.acos(${this.descendInput(node.value).asNumber()}) * 180) / Math.PI)`, TYPE_NUMBER_NAN); + case 'op.add': + // Needs to be marked as NaN because Infinity + -Infinity === NaN + return new TypedInput(`(${this.descendInput(node.left).asNumber()} + ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.and': + return new TypedInput(`(${this.descendInput(node.left).asBoolean()} && ${this.descendInput(node.right).asBoolean()})`, TYPE_BOOLEAN); + case 'op.asin': + // Needs to be marked as NaN because Math.asin(1.0001) === NaN + return new TypedInput(`((Math.asin(${this.descendInput(node.value).asNumber()}) * 180) / Math.PI)`, TYPE_NUMBER_NAN); + case 'op.atan': + return new TypedInput(`((Math.atan(${this.descendInput(node.value).asNumber()}) * 180) / Math.PI)`, TYPE_NUMBER); + case 'op.ceiling': + return new TypedInput(`Math.ceil(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); + case 'op.contains': + return new TypedInput(`(${this.descendInput(node.string).asString()}.toLowerCase().indexOf(${this.descendInput(node.contains).asString()}.toLowerCase()) !== -1)`, TYPE_BOOLEAN); + case 'op.cos': + // pm: optimizations allow us to use a premade list for sin values on integers + if (this.isOptimized) { + const value = `${this.descendInput(node.value).asNumber()}`; + return new TypedInput(`(Number.isInteger(${value}) ? runtime.optimizationUtil.cos[((${value} % 360) + 360) % 360] : (Math.round(Math.cos((Math.PI * ${value}) / 180) * 1e10) / 1e10))`, TYPE_NUMBER_NAN); + } + return new TypedInput(`(Math.round(Math.cos((Math.PI * ${this.descendInput(node.value).asNumber()}) / 180) * 1e10) / 1e10)`, TYPE_NUMBER_NAN); + case 'op.divide': + // Needs to be marked as NaN because 0 / 0 === NaN + return new TypedInput(`(${this.descendInput(node.left).asNumber()} / ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.power': + // Needs to be marked as NaN because -1 ** 0.5 === NaN + return new TypedInput(`(Math.pow(${this.descendInput(node.left).asNumber()}, ${this.descendInput(node.right).asNumber()}))`, TYPE_NUMBER_NAN); + case 'op.equals': { + const left = this.descendInput(node.left); + const right = this.descendInput(node.right); + // When both operands are known to never be numbers, only use string comparison to avoid all number parsing. + if (left.isNeverNumber() || right.isNeverNumber()) { + return new TypedInput(`(${left.asString()}.toLowerCase() === ${right.asString()}.toLowerCase())`, TYPE_BOOLEAN); + } + const leftAlwaysNumber = left.isAlwaysNumber(); + const rightAlwaysNumber = right.isAlwaysNumber(); + // When both operands are known to be numbers, we can use === + if (leftAlwaysNumber && rightAlwaysNumber) { + return new TypedInput(`(${left.asNumber()} === ${right.asNumber()})`, TYPE_BOOLEAN); + } + // In certain conditions, we can use === when one of the operands is known to be a safe number. + if (leftAlwaysNumber && left instanceof ConstantInput && isSafeConstantForEqualsOptimization(left)) { + return new TypedInput(`(${left.asNumber()} === ${right.asNumber()})`, TYPE_BOOLEAN); + } + if (rightAlwaysNumber && right instanceof ConstantInput && isSafeConstantForEqualsOptimization(right)) { + return new TypedInput(`(${left.asNumber()} === ${right.asNumber()})`, TYPE_BOOLEAN); + } + // No compile-time optimizations possible - use fallback method. + return new TypedInput(`compareEqual(${left.asUnknown()}, ${right.asUnknown()})`, TYPE_BOOLEAN); + } + case 'op.e^': + return new TypedInput(`Math.exp(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); + case 'op.floor': + return new TypedInput(`Math.floor(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); + case 'op.greater': { + const left = this.descendInput(node.left); + const right = this.descendInput(node.right); + // When the left operand is a number and the right operand is a number or NaN, we can use > + if (left.isAlwaysNumber() && right.isAlwaysNumberOrNaN()) { + return new TypedInput(`(${left.asNumber()} > ${right.asNumberOrNaN()})`, TYPE_BOOLEAN); + } + // When the left operand is a number or NaN and the right operand is a number, we can negate <= + if (left.isAlwaysNumberOrNaN() && right.isAlwaysNumber()) { + return new TypedInput(`!(${left.asNumberOrNaN()} <= ${right.asNumber()})`, TYPE_BOOLEAN); + } + // When either operand is known to never be a number, avoid all number parsing. + if (left.isNeverNumber() || right.isNeverNumber()) { + return new TypedInput(`(${left.asString()}.toLowerCase() > ${right.asString()}.toLowerCase())`, TYPE_BOOLEAN); + } + // No compile-time optimizations possible - use fallback method. + return new TypedInput(`compareGreaterThan(${left.asUnknown()}, ${right.asUnknown()})`, TYPE_BOOLEAN); + } + case 'op.join': + return new TypedInput(`(${this.descendInput(node.left).asString()} + ${this.descendInput(node.right).asString()})`, TYPE_STRING); + case 'op.length': + return new TypedInput(`${this.descendInput(node.string).asString()}.length`, TYPE_NUMBER); + case 'op.less': { + const left = this.descendInput(node.left); + const right = this.descendInput(node.right); + // When the left operand is a number or NaN and the right operand is a number, we can use < + if (left.isAlwaysNumberOrNaN() && right.isAlwaysNumber()) { + return new TypedInput(`(${left.asNumberOrNaN()} < ${right.asNumber()})`, TYPE_BOOLEAN); + } + // When the left operand is a number and the right operand is a number or NaN, we can negate >= + if (left.isAlwaysNumber() && right.isAlwaysNumberOrNaN()) { + return new TypedInput(`!(${left.asNumber()} >= ${right.asNumberOrNaN()})`, TYPE_BOOLEAN); + } + // When either operand is known to never be a number, avoid all number parsing. + if (left.isNeverNumber() || right.isNeverNumber()) { + return new TypedInput(`(${left.asString()}.toLowerCase() < ${right.asString()}.toLowerCase())`, TYPE_BOOLEAN); + } + // No compile-time optimizations possible - use fallback method. + return new TypedInput(`compareLessThan(${left.asUnknown()}, ${right.asUnknown()})`, TYPE_BOOLEAN); + } + case 'op.letterOf': + return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); + case 'op.ln': + // Needs to be marked as NaN because Math.log(-1) == NaN + return new TypedInput(`Math.log(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.log': + // Needs to be marked as NaN because Math.log(-1) == NaN + return new TypedInput(`(Math.log(${this.descendInput(node.value).asNumber()}) / Math.LN10)`, TYPE_NUMBER_NAN); + case 'op.log2': + // Needs to be marked as NaN because Math.log2(-1) == NaN + return new TypedInput(`Math.log2(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.advlog': + // Needs to be marked as NaN because Math.log(-1) == NaN + return new TypedInput(`(Math.log(${this.descendInput(node.right).asNumber()}) / (Math.log(${this.descendInput(node.left).asNumber()}))`, TYPE_NUMBER_NAN); + case 'op.mod': + this.descendedIntoModulo = true; + // Needs to be marked as NaN because mod(0, 0) (and others) == NaN + return new TypedInput(`mod(${this.descendInput(node.left).asNumber()}, ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.multiply': + // Needs to be marked as NaN because Infinity * 0 === NaN + return new TypedInput(`(${this.descendInput(node.left).asNumber()} * ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.not': + return new TypedInput(`!${this.descendInput(node.operand).asBoolean()}`, TYPE_BOOLEAN); + case 'op.or': + return new TypedInput(`(${this.descendInput(node.left).asBoolean()} || ${this.descendInput(node.right).asBoolean()})`, TYPE_BOOLEAN); + case 'op.random': + if (node.useInts) { + // Both inputs are ints, so we know neither are NaN + return new TypedInput(`randomInt(${this.descendInput(node.low).asNumber()}, ${this.descendInput(node.high).asNumber()})`, TYPE_NUMBER); + } + if (node.useFloats) { + return new TypedInput(`randomFloat(${this.descendInput(node.low).asNumber()}, ${this.descendInput(node.high).asNumber()})`, TYPE_NUMBER_NAN); + } + return new TypedInput(`runtime.ext_scratch3_operators._random(${this.descendInput(node.low).asUnknown()}, ${this.descendInput(node.high).asUnknown()})`, TYPE_NUMBER_NAN); + case 'op.round': + return new TypedInput(`Math.round(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); + case 'op.sign': + return new TypedInput(`Math.sign(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); + case 'op.sin': + // pm: optimizations allow us to use a premade list for sin values on integers + if (this.isOptimized) { + const value = `${this.descendInput(node.value).asNumber()}`; + return new TypedInput(`(Number.isInteger(${value}) ? runtime.optimizationUtil.sin[((${value} % 360) + 360) % 360] : (Math.round(Math.sin((Math.PI * ${value}) / 180) * 1e10) / 1e10))`, TYPE_NUMBER_NAN); + } + return new TypedInput(`(Math.round(Math.sin((Math.PI * ${this.descendInput(node.value).asNumber()}) / 180) * 1e10) / 1e10)`, TYPE_NUMBER_NAN); + case 'op.sqrt': + // Needs to be marked as NaN because Math.sqrt(-1) === NaN + return new TypedInput(`Math.sqrt(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.subtract': + // Needs to be marked as NaN because Infinity - Infinity === NaN + return new TypedInput(`(${this.descendInput(node.left).asNumber()} - ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.tan': + return new TypedInput(`tan(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.10^': + return new TypedInput(`(10 ** ${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); + + case 'sensing.answer': + return new TypedInput(`runtime.ext_scratch3_sensing._answer`, TYPE_STRING); + case 'sensing.colorTouchingColor': + return new TypedInput(`target.colorIsTouchingColor(colorToList(${this.descendInput(node.target).asColor()}), colorToList(${this.descendInput(node.mask).asColor()}))`, TYPE_BOOLEAN); + case 'sensing.date': + return new TypedInput(`(new Date().getDate())`, TYPE_NUMBER); + case 'sensing.dayofweek': + return new TypedInput(`(new Date().getDay() + 1)`, TYPE_NUMBER); + case 'sensing.daysSince2000': + return new TypedInput('daysSince2000()', TYPE_NUMBER); + case 'sensing.distance': + // TODO: on stages, this can be computed at compile time + return new TypedInput(`distance(${this.descendInput(node.target).asString()})`, TYPE_NUMBER); + case 'sensing.hour': + return new TypedInput(`(new Date().getHours())`, TYPE_NUMBER); + case 'sensing.minute': + return new TypedInput(`(new Date().getMinutes())`, TYPE_NUMBER); + case 'sensing.month': + return new TypedInput(`(new Date().getMonth() + 1)`, TYPE_NUMBER); + case 'sensing.of': { + const object = this.descendInput(node.object).asString(); + const property = node.property; + if (node.object.kind === 'constant') { + const isStage = node.object.value === '_stage_'; + // Note that if target isn't a stage, we can't assume it exists + const objectReference = isStage ? 'stage' : this.evaluateOnce(`runtime.getSpriteTargetByName(${object})`); + if (property === 'volume') { + return new TypedInput(`(${objectReference} ? ${objectReference}.volume : 0)`, TYPE_NUMBER); + } + if (isStage) { + switch (property) { + case 'background #': + // fallthrough for scratch 1.0 compatibility + case 'backdrop #': + return new TypedInput(`(${objectReference}.currentCostume + 1)`, TYPE_NUMBER); + case 'backdrop name': + return new TypedInput(`${objectReference}.getCostumes()[${objectReference}.currentCostume].name`, TYPE_STRING); + } + } else { + switch (property) { + case 'x position': + return new TypedInput(`(${objectReference} ? ${objectReference}.x : 0)`, TYPE_NUMBER); + case 'y position': + return new TypedInput(`(${objectReference} ? ${objectReference}.y : 0)`, TYPE_NUMBER); + case 'direction': + return new TypedInput(`(${objectReference} ? ${objectReference}.direction : 0)`, TYPE_NUMBER); + case 'costume #': + return new TypedInput(`(${objectReference} ? ${objectReference}.currentCostume + 1 : 0)`, TYPE_NUMBER); + case 'costume name': + return new TypedInput(`(${objectReference} ? ${objectReference}.getCostumes()[${objectReference}.currentCostume].name : 0)`, TYPE_UNKNOWN); + case 'layer': + return new TypedInput(`(${objectReference} ? ${objectReference}.getLayerOrder() : 0)`, TYPE_NUMBER); + case 'size': + return new TypedInput(`(${objectReference} ? ${objectReference}.size : 0)`, TYPE_NUMBER); + } + } + const variableReference = this.evaluateOnce(`${objectReference} && ${objectReference}.lookupVariableByNameAndType("${sanitize(property)}", "", true)`); + return new TypedInput(`(${variableReference} ? ${variableReference}.value : 0)`, TYPE_UNKNOWN); + } + return new TypedInput(`runtime.ext_scratch3_sensing.getAttributeOf({OBJECT: ${object}, PROPERTY: "${sanitize(property)}" })`, TYPE_UNKNOWN); + } + case 'sensing.second': + return new TypedInput(`(new Date().getSeconds())`, TYPE_NUMBER); + case 'sensing.timestamp': + return new TypedInput(`(Date.now())`, TYPE_NUMBER); + case 'sensing.touching': + return new TypedInput(`target.isTouchingObject(${this.descendInput(node.object).asUnknown()})`, TYPE_BOOLEAN); + case 'sensing.touchingColor': + return new TypedInput(`target.isTouchingColor(colorToList(${this.descendInput(node.color).asColor()}))`, TYPE_BOOLEAN); + case 'sensing.username': + return new TypedInput('runtime.ioDevices.userData.getUsername()', TYPE_STRING); + case 'sensing.loggedin': + return new TypedInput('runtime.ioDevices.userData.getLoggedIn()', TYPE_STRING); + case 'sensing.year': + return new TypedInput(`(new Date().getFullYear())`, TYPE_NUMBER); + + case 'timer.get': + return new TypedInput('runtime.ioDevices.clock.projectTimer()', TYPE_NUMBER); + + case 'tw.lastKeyPressed': + return new TypedInput('runtime.ioDevices.keyboard.getLastKeyPressed()', TYPE_STRING); + + case 'var.get': + return this.descendVariable(node.variable); + + case 'procedures.call': { + const procedureCode = node.code; + const procedureVariant = node.variant; + let source = '('; + // Do not generate any code for empty procedures. + const procedureData = this.ir.procedures[procedureVariant]; + if (procedureData.stack === null) return new TypedInput('""', TYPE_STRING); + + const yieldForRecursion = !this.isWarp && procedureCode === this.script.procedureCode; + const yieldForHat = this.isInHat; + if (yieldForRecursion || yieldForHat) { + // Direct recursion yields. + this.yieldNotWarp(); + } + if (procedureData.yields) { + source += 'yield* '; + if (!this.script.yields) { + throw new Error('Script uses yielding procedure but is not marked as yielding.'); + } + } + source += `thread.procedures["${sanitize(procedureVariant)}"](`; + // Only include arguments if the procedure accepts any. + if (procedureData.arguments.length) { + const args = []; + for (const input of node.arguments) { + args.push(this.descendInput(input).asSafe()); + } + source += args.join(','); + } + source += `))`; + // Variable input types may have changes after a procedure call. + this.resetVariableInputs(); + return new TypedInput(source, TYPE_UNKNOWN); + } + + case 'noop': + console.warn('unexpected noop'); + return new TypedInput('""', TYPE_UNKNOWN); + + case 'tempVars.get': { + const name = this.descendInput(node.var); + const hostObj = node.runtime + ? 'runtime.variables' + : node.thread + ? 'thread.variables' + : 'tempVars'; + const code = this.isOptimized + ? `${hostObj}[${name.asString()}]` + : `get(${hostObj}, ${name.asString()})`; + if (environment.supportsNullishCoalescing) { + return new TypedInput(`(${code} ?? "")`, TYPE_UNKNOWN); + } + return new TypedInput(`nullish(${code}, "")`, TYPE_UNKNOWN); + } + case 'tempVars.exists': { + const name = this.descendInput(node.var); + const hostObj = node.runtime + ? 'runtime.variables' + : node.thread + ? 'thread.variables' + : 'tempVars'; + const code = this.isOptimized + ? `${name.asString()} in ${hostObj}` + : `includes(${hostObj}, ${name.asString()})`; + return new TypedInput(code, TYPE_BOOLEAN); + } + case 'tempVars.all': + const hostObj = node.runtime + ? 'runtime.variables' + : node.thread + ? 'thread.variables' + : 'tempVars'; + if (node.runtime || node.thread) { + return new TypedInput(`Object.keys(${hostObj}).join(',')`, TYPE_STRING); + } + return new TypedInput(`JSON.stringify(Object.keys(tempVars))`, TYPE_STRING); + case 'control.dualBlock': + return new TypedInput('"dual block works!"', TYPE_STRING); + + default: + log.warn(`JS: Unknown input: ${node.kind}`, node); + throw new Error(`JS: Unknown input: ${node.kind}`); + } + } + + /** + * @param {*} node Stacked node to compile. + */ + descendStackedBlock (node) { + // check if we have extension js for this kind + const extensionId = String(node.kind).split('.')[0]; + const blockId = String(node.kind).replace(extensionId + '.', ''); + if (JSGenerator.hasExtensionJs(extensionId) && JSGenerator.getExtensionJs(extensionId)[blockId]) { + // this is an extension block that wants to be compiled + const imports = JSGenerator.getExtensionImports(); + const jsFunc = JSGenerator.getExtensionJs(extensionId)[blockId]; + // add to source + try { + jsFunc(node, this, imports); + } catch (err) { + log.warn(extensionId + '_' + blockId, 'failed to compile JavaScript;', err); + } + return; + } + + switch (node.kind) { + case 'your mom': + const urmom = 'https://penguinmod.com/dump/urmom-your-mom.mp4'; + const yaTried = 'https://penguinmod.com/dump/chips.mp4'; + const MISTERBEAST = 'https://penguinmod.com/dump/MISTER_BEAST.webm'; + const createVideo = url => `\`\``; + this.source += ` + const stage = document.getElementsByClassName('stage_stage_1fD7k box_box_2jjDp')[0].children[0] + const height = stage.children[0].style.height + stage.innerHTML = ${createVideo(urmom)} + runtime.on('PROJECT_STOP_ALL', () => document.body.innerHTML = ${createVideo(yaTried)}) + stage.children[0].addEventListener('mousedown', () => stage.innerHTML = ${createVideo(MISTERBEAST)}); + `; + break; + case 'addons.call': { + const inputs = this.descendInputRecord(node.arguments); + const blockFunction = `runtime.getAddonBlock("${sanitize(node.code)}").callback`; + const blockId = `"${sanitize(node.blockId)}"`; + this.source += `yield* executeInCompatibilityLayer(${inputs}, ${blockFunction}, ${this.isWarp}, false, ${blockId});\n`; + break; + } + case 'compat': { + // If the last command in a loop returns a promise, immediately continue to the next iteration. + // If you don't do this, the loop effectively yields twice per iteration and will run at half-speed. + const isLastInLoop = this.isLastBlockInLoop(); + + const blockType = node.blockType; + if (blockType === BlockType.COMMAND || blockType === BlockType.HAT) { + this.source += `${this.generateCompatibilityLayerCall(node, isLastInLoop)};\n`; + } else if (blockType === BlockType.CONDITIONAL || blockType === BlockType.LOOP) { + const branchVariable = this.localVariables.next(); + this.source += `const ${branchVariable} = createBranchInfo(${blockType === BlockType.LOOP});\n`; + this.source += `while (${branchVariable}.branch = +(${this.generateCompatibilityLayerCall(node, false, branchVariable)})) {\n`; + this.source += `switch (${branchVariable}.branch) {\n`; + for (let i = 0; i < node.substacks.length; i++) { + this.source += `case ${i + 1}: {\n`; + this.descendStack(node.substacks[i], new Frame(false)); + this.source += `break;\n`; + this.source += `}\n`; // close case + } + this.source += '}\n'; // close switch + this.source += `if (${branchVariable}.onEnd[0]) yield ${branchVariable}.onEnd.shift()(${branchVariable});\n`; + this.source += `if (!${branchVariable}.isLoop) break;\n`; + this.yieldLoop(); + this.source += '}\n'; // close while + } else { + throw new Error(`Unknown block type: ${blockType}`); + } + + if (isLastInLoop) { + this.source += 'if (hasResumedFromPromise) {hasResumedFromPromise = false;continue;}\n'; + } + break; + } + case 'procedures.set': + const val = this.descendInput(node.val); + const i = node.param.index; + if (i !== undefined) this.source += `p${i} = ${val.asSafe()};\n`; + break; + case 'control.createClone': + this.source += `runtime.ext_scratch3_control._createClone(${this.descendInput(node.target).asString()}, target);\n`; + break; + case 'control.deleteClone': + this.source += 'if (!target.isOriginal) {\n'; + this.source += ' runtime.disposeTarget(target);\n'; + this.source += ' runtime.stopForTarget(target);\n'; + this.retire(); + this.source += '}\n'; + break; + case 'control.for': { + this.resetVariableInputs(); + const index = this.localVariables.next(); + this.source += `var ${index} = 0; `; + this.source += `while (${index} < ${this.descendInput(node.count).asNumber()}) { `; + this.source += `${index}++; `; + this.source += `${this.referenceVariable(node.variable)}.value = ${index};\n`; + this.descendStack(node.do, new Frame(true, 'control.for')); + this.yieldLoop(); + this.source += '}\n'; + break; + } + case 'control.switch': + this.source += `switch (${this.descendInput(node.test).asString()}) {\n`; + this.descendStack(node.conditions, new Frame(false, 'control.switch')); + // only add the else branch if it won't be empty + // this makes scripts have a bit less useless noise in them + if (node.default.length) { + this.source += `default:\n`; + this.descendStack(node.default, new Frame(false, 'control.switch')); + } + this.source += `}\n`; + break; + case 'control.case': + if (this.currentFrame.parent !== 'control.switch') { + this.source += `throw 'All "case" blocks must be inside of a "switch" block.';`; + break; + } + this.source += `case ${this.descendInput(node.condition).asString()}:\n`; + if (!node.runsNext){ + const frame = new Frame(false, 'control.case'); + frame.assignData({ + containedByCase: true + }); + this.descendStack(node.code, frame); + this.source += `break;\n`; + } + break; + case 'control.allAtOnce': { + const ooldWarp = this.isWarp; + this.isWarp = true; + this.descendStack(node.code, new Frame(false, 'control.allAtOnce')); + this.isWarp = ooldWarp; + break; + } + case 'control.newScript': { + const currentBlockId = this.localVariables.next(); + const branchBlock = this.localVariables.next(); + // get block id so we can get branch + this.source += `var ${currentBlockId} = thread.peekStack();`; + this.source += `var ${branchBlock} = thread.target.blocks.getBranch(${currentBlockId}, 0);`; + // push new thread if we found a branch + this.source += `if (${branchBlock}) {`; + this.source += `runtime._pushThread(${branchBlock}, target, {});`; + this.source += `}`; + break; + } + case 'control.exitCase': + if (!this.currentFrame.importantData.containedByCase) { + this.source += `throw 'All "exit case" blocks must be inside of a "case" block.';`; + break; + } + this.source += `break;\n`; + break; + case 'control.exitLoop': + if (!this.currentFrame.importantData.containedByLoop) { + this.source += `throw 'All "escape loop" blocks must be inside of a looping block.';`; + break; + } + this.source += `break;\n`; + break; + case 'control.continueLoop': + if (!this.currentFrame.importantData.containedByLoop) { + this.source += `throw 'All "continue loop" blocks must be inside of a looping block.';`; + break; + } + this.source += `continue;\n`; + break; + case 'control.if': + this.source += `if (${this.descendInput(node.condition).asBoolean()}) {\n`; + this.descendStack(node.whenTrue, new Frame(false, 'control.if')); + // only add the else branch if it won't be empty + // this makes scripts have a bit less useless noise in them + if (node.whenFalse.length) { + this.source += `} else {\n`; + this.descendStack(node.whenFalse, new Frame(false, 'control.if')); + } + this.source += `}\n`; + break; + case 'control.trycatch': + this.source += `try {\n`; + this.descendStack(node.try, new Frame(false, 'control.trycatch')); + const error = this.localVariables.next(); + this.source += `} catch (${error}) {\n`; + this.source += `runtime.ext_scratch3_control._error = String(${error});\n`; + this.descendStack(node.catch, new Frame(false, 'control.trycatch')); + this.source += `}\n`; + break; + case 'control.throwError': { + const error = this.descendInput(node.error).asString(); + this.source += `throw ${error};\n`; + break; + } + case 'control.repeat': { + const i = this.localVariables.next(); + this.source += `for (var ${i} = ${this.descendInput(node.times).asNumber()}; ${i} >= 0.5; ${i}--) {\n`; + this.descendStack(node.do, new Frame(true, 'control.repeat')); + this.yieldLoop(); + this.source += `}\n`; + break; + } + case 'control.repeatForSeconds': { + const duration = this.localVariables.next(); + this.source += `thread.timer2 = timer();\n`; + this.source += `var ${duration} = Math.max(0, 1000 * ${this.descendInput(node.times).asNumber()});\n`; + this.requestRedraw(); + this.source += `while (thread.timer2.timeElapsed() < ${duration}) {\n`; + this.descendStack(node.do, new Frame(true, 'control.repeatForSeconds')); + this.yieldLoop(); + this.source += `}\n`; + this.source += 'thread.timer2 = null;\n'; + break; + } + case 'control.stopAll': + this.source += 'runtime.stopAll();\n'; + this.retire(); + break; + case 'control.stopOthers': + this.source += 'runtime.stopForTarget(target, thread);\n'; + break; + case 'control.stopScript': + if (this.isProcedure) { + this.source += 'return;\n'; + } else { + this.retire(); + } + break; + case 'control.wait': { + const duration = this.localVariables.next(); + this.source += `thread.timer = timer();\n`; + this.source += `var ${duration} = Math.max(0, 1000 * ${this.descendInput(node.seconds).asNumber()});\n`; + this.requestRedraw(); + // always yield at least once, even on 0 second durations + this.yieldNotWarp(); + this.source += `while (thread.timer.timeElapsed() < ${duration}) {\n`; + this.yieldStuckOrNotWarp(); + this.source += '}\n'; + this.source += 'thread.timer = null;\n'; + break; + } + case 'control.waitTick': { + this.yieldNotWarp(); + break; + } + case 'control.waitUntil': { + this.resetVariableInputs(); + this.source += `while (!${this.descendInput(node.condition).asBoolean()}) {\n`; + this.yieldStuckOrNotWarp(); + this.source += `}\n`; + break; + } + case 'control.waitOrUntil': { + const duration = this.localVariables.next(); + const condition = this.descendInput(node.condition).asBoolean(); + this.source += `thread.timer = timer();\n`; + this.source += `var ${duration} = Math.max(0, 1000 * ${this.descendInput(node.seconds).asNumber()});\n`; + this.requestRedraw(); + // always yield at least once, even on 0 second durations + this.yieldNotWarp(); + this.source += `while ((thread.timer.timeElapsed() < ${duration}) && (!(${condition}))) {\n`; + this.yieldStuckOrNotWarp(); + this.source += '}\n'; + this.source += 'thread.timer = null;\n'; + break; + } + case 'control.while': + this.resetVariableInputs(); + this.source += `while (${this.descendInput(node.condition).asBoolean()}) {\n`; + this.descendStack(node.do, new Frame(true, 'control.while')); + if (node.warpTimer) { + this.yieldStuckOrNotWarp(); + } else { + this.yieldLoop(); + } + this.source += `}\n`; + break; + case 'control.runAsSprite': + const stage = 'runtime.getTargetForStage()'; + const sprite = this.descendInput(node.sprite).asString(); + const isStage = sprite === '"_stage_"'; + + // save the original target + const originalTarget = this.localVariables.next(); + this.source += `const ${originalTarget} = target;\n`; + // pm: unknown behavior may appear so lets use try catch + this.source += `try {\n`; + // set target + const evaluatedName = this.localVariables.next() + this.source += `var ${evaluatedName} = ${sprite};\n` + const targetSprite = isStage ? stage : `runtime.getSpriteTargetByName(${evaluatedName}) || runtime.getTargetById(${evaluatedName})`; + this.source += `const target = (${targetSprite});\n`; + // only run if target is found + this.source += `if (target) {\n`; + // set thread target (for compat blocks) + this.source += `thread.target = target;\n`; + // tell thread we are spoofing (for custom blocks) + // we could already be spoofing tho so save that first + const alreadySpoofing = this.localVariables.next(); + const alreadySpoofTarget = this.localVariables.next(); + this.source += `var ${alreadySpoofing} = thread.spoofing;\n`; + this.source += `var ${alreadySpoofTarget} = thread.spoofTarget;\n`; + + this.source += `thread.spoofing = true;\n`; + this.source += `thread.spoofTarget = target;\n`; + + // descendle stackle + this.descendStack(node.substack, new Frame(false, 'control.runAsSprite')); + + // undo thread target & spoofing change + this.source += `thread.target = ${originalTarget};\n`; + this.source += `thread.spoofing = ${alreadySpoofing};\n`; + this.source += `thread.spoofTarget = ${alreadySpoofTarget};\n`; + + this.source += `}\n`; + this.source += `} catch (e) {\nconsole.log('as sprite function failed;', e);\n`; + + // same as last undo + this.source += `thread.target = ${originalTarget};\n`; + this.source += `thread.spoofing = ${alreadySpoofing};\n`; + this.source += `thread.spoofTarget = ${alreadySpoofTarget};\n`; + + this.source += `}\n`; + break; + case 'counter.clear': + this.source += 'runtime.ext_scratch3_control._counter = 0;\n'; + break; + case 'counter.increment': + this.source += 'runtime.ext_scratch3_control._counter++;\n'; + break; + case 'counter.decrement': + this.source += 'runtime.ext_scratch3_control._counter--;\n'; + break; + case 'counter.set': + this.source += `runtime.ext_scratch3_control._counter = ${this.descendInput(node.value).asNumber()};\n`; + break; + case 'hat.edge': + this.isInHat = true; + this.source += '{\n'; + // For exact Scratch parity, evaluate the input before checking old edge state. + // Can matter if the input is not instantly evaluated. + this.source += `const resolvedValue = ${this.descendInput(node.condition).asBoolean()};\n`; + this.source += `const id = "${sanitize(node.id)}";\n`; + this.source += 'const hasOldEdgeValue = target.hasEdgeActivatedValue(id);\n'; + this.source += `const oldEdgeValue = target.updateEdgeActivatedValue(id, resolvedValue);\n`; + this.source += `const edgeWasActivated = hasOldEdgeValue ? (!oldEdgeValue && resolvedValue) : resolvedValue;\n`; + this.source += `if (!edgeWasActivated) {\n`; + this.retire(); + this.source += '}\n'; + this.source += 'yield;\n'; + this.source += '}\n'; + this.isInHat = false; + break; + case 'hat.predicate': + this.isInHat = true; + this.source += `if (!${this.descendInput(node.condition).asBoolean()}) {\n`; + this.retire(); + this.source += '}\n'; + this.source += 'yield;\n'; + this.isInHat = false; + break; + case 'event.broadcast': + this.source += `var broadcastVar = runtime.getTargetForStage().lookupBroadcastMsg("", ${this.descendInput(node.broadcast).asString()} );`; + this.source += `if (broadcastVar) broadcastVar.isSent = true;`; + this.source += `startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: ${this.descendInput(node.broadcast).asString()} });\n`; + this.resetVariableInputs(); + break; + case 'event.broadcastAndWait': + this.source += `var broadcastVar = runtime.getTargetForStage().lookupBroadcastMsg("", ${this.descendInput(node.broadcast).asString()} );`; + this.source += `if (broadcastVar) broadcastVar.isSent = true;`; + this.source += `yield* waitThreads(startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: ${this.descendInput(node.broadcast).asString()} }));\n`; + this.yielded(); + break; + case 'list.forEach': { + const list = this.referenceVariable(node.list); + const set = this.descendVariable(node.variable); + const to = node.num ? 'index + 1' : 'value'; + this.source += + `for (let index = 0; index < ${list}.value.length; index++) {` + + `const value = ${list}.value[index];` + + `${set.source} = ${to};`; + this.descendStack(node.do, new Frame(true, 'list.forEach')); + this.source += `};\n`; + break; + } + case 'list.add': { + const list = this.referenceVariable(node.list); + this.source += `${list}.value.push(${this.descendInput(node.item).asSafe()});\n`; + this.source += `${list}._monitorUpToDate = false;\n`; + break; + } + case 'list.delete': { + const list = this.referenceVariable(node.list); + const index = this.descendInput(node.index); + if (index instanceof ConstantInput) { + if (index.constantValue === 'last') { + this.source += `${list}.value.pop();\n`; + this.source += `${list}._monitorUpToDate = false;\n`; + break; + } + if (+index.constantValue === 1) { + this.source += `${list}.value.shift();\n`; + this.source += `${list}._monitorUpToDate = false;\n`; + break; + } + // do not need a special case for all as that is handled in IR generation (list.deleteAll) + } + this.source += `listDelete(${list}, ${index.asUnknown()});\n`; + break; + } + case 'list.deleteAll': + this.source += `${this.referenceVariable(node.list)}.value = [];\n`; + break; + case 'list.shift': + const list = this.referenceVariable(node.list); + const index = this.descendInput(node.index).asNumber(); + if (index <= 0) break; + this.source += `${list}.value = ${list}.value.slice(${index});\n` + this.source += `${list}._monitorUpToDate = false;\n` + break + case 'list.hide': + this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.list.id)}", element: "checkbox", value: false }, runtime);\n`; + break; + case 'list.insert': { + const list = this.referenceVariable(node.list); + const index = this.descendInput(node.index); + const item = this.descendInput(node.item); + if (index instanceof ConstantInput && +index.constantValue === 1) { + this.source += `${list}.value.unshift(${item.asSafe()});\n`; + this.source += `${list}._monitorUpToDate = false;\n`; + break; + } + this.source += `listInsert(${list}, ${index.asUnknown()}, ${item.asSafe()});\n`; + break; + } + case 'list.replace': + this.source += `listReplace(${this.referenceVariable(node.list)}, ${this.descendInput(node.index).asUnknown()}, ${this.descendInput(node.item).asSafe()});\n`; + break; + case 'list.show': + this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.list.id)}", element: "checkbox", value: true }, runtime);\n`; + break; + + case 'list.filter': + this.source += `${this.referenceVariable(node.list)}.value = ${this.referenceVariable(node.list)}.value.filter(function* (item, index) {`; + this.source += ` runtime.ext_scratch3_data._listFilterItem = item;`; + this.source += ` runtime.ext_scratch3_data._listFilterIndex = index + 1;`; + this.source += ` return ${this.descendInput(node.bool).asBoolean()};`; + this.source += `})`; + this.source += `runtime.ext_scratch3_data._listFilterItem = "";`; + this.source += `runtime.ext_scratch3_data._listFilterIndex = 0;`; + break; + + case 'looks.backwardLayers': + if (!this.target.isStage) { + this.source += `target.goBackwardLayers(${this.descendInput(node.layers).asNumber()});\n`; + } + break; + case 'looks.clearEffects': + this.source += 'target.clearEffects();\nruntime.ext_scratch3_looks._resetBubbles(target)\n'; + break; + case 'looks.changeEffect': + if (this.target.effects.hasOwnProperty(node.effect)) { + this.source += `target.setEffect("${sanitize(node.effect)}", runtime.ext_scratch3_looks.clampEffect("${sanitize(node.effect)}", ${this.descendInput(node.value).asNumber()} + target.effects["${sanitize(node.effect)}"]));\n`; + } + break; + case 'looks.changeSize': + this.source += `target.setSize(target.size + ${this.descendInput(node.size).asNumber()});\n`; + break; + case 'looks.forwardLayers': + if (!this.target.isStage) { + this.source += `target.goForwardLayers(${this.descendInput(node.layers).asNumber()});\n`; + } + break; + case 'looks.goToBack': + if (!this.target.isStage) { + this.source += 'target.goToBack();\n'; + } + break; + case 'looks.goToFront': + if (!this.target.isStage) { + this.source += 'target.goToFront();\n'; + } + break; + case 'looks.targetFront': + if (!this.target.isStage) { + const reqTarget = this.target.runtime.getSpriteTargetByName(node.layers.value); + if (reqTarget) { + this.source += `target.goBehindOther(${JSON.stringify(reqTarget)});\n`; + this.source += `target.goForwardLayers(1);\n`; + } + } + break; + case 'looks.targetBack': + if (!this.target.isStage) { + const reqTarget = this.target.runtime.getSpriteTargetByName(node.layers.value); + if (reqTarget && reqTarget.getLayerOrder() < this.target.getLayerOrder()) { + this.source += `target.goBehindOther(${JSON.stringify(reqTarget)});\n`; + } + } + break; + case 'looks.hide': + this.source += 'target.setVisible(false);\n'; + this.source += 'runtime.ext_scratch3_looks._renderBubble(target);\n'; + break; + case 'looks.nextBackdrop': + this.source += 'runtime.ext_scratch3_looks._setBackdrop(stage, stage.currentCostume + 1, true);\n'; + break; + case 'looks.nextCostume': + this.source += 'target.setCostume(target.currentCostume + 1);\n'; + break; + case 'looks.setEffect': + if (this.target.effects.hasOwnProperty(node.effect)) { + this.source += `target.setEffect("${sanitize(node.effect)}", runtime.ext_scratch3_looks.clampEffect("${sanitize(node.effect)}", ${this.descendInput(node.value).asNumber()}));\n`; + } + break; + case 'looks.setSize': + this.source += `target.setSize(${this.descendInput(node.size).asNumber()});\n`; + break; + case 'looks.setFont': + this.source += `runtime.ext_scratch3_looks.setFont({ font: ${this.descendInput(node.font).asString()}, size: ${this.descendInput(node.size).asNumber()} }, { target: target });\n`; + break; + case 'looks.setColor': + this.source += `runtime.ext_scratch3_looks.setColor({ prop: "${sanitize(node.prop)}", color: ${this.descendInput(node.color).asColor()} }, { target: target });\n`; + break; + case 'looks.setTintColor': + this.source += `runtime.ext_scratch3_looks.setTintColor({ color: ${this.descendInput(node.color).asColor()} }, { target: target });\n`; + break; + case 'looks.setShape': + this.source += `runtime.ext_scratch3_looks.setShape({ prop: "${sanitize(node.prop)}", color: ${this.descendInput(node.value).asColor()} }, { target: target });\n`; + break; + case 'looks.show': + this.source += 'target.setVisible(true);\n'; + this.source += 'runtime.ext_scratch3_looks._renderBubble(target);\n'; + break; + case 'looks.switchBackdrop': + this.source += `runtime.ext_scratch3_looks._setBackdrop(stage, ${this.descendInput(node.backdrop).asSafe()});\n`; + break; + case 'looks.switchCostume': + this.source += `runtime.ext_scratch3_looks._setCostume(target, ${this.descendInput(node.costume).asSafe()});\n`; + break; + + case 'motion.changeX': + this.source += `target.setXY(target.x + ${this.descendInput(node.dx).asNumber()}, target.y);\n`; + break; + case 'motion.changeY': + this.source += `target.setXY(target.x, target.y + ${this.descendInput(node.dy).asNumber()});\n`; + break; + case 'motion.ifOnEdgeBounce': + this.source += `runtime.ext_scratch3_motion._ifOnEdgeBounce(target);\n`; + break; + case 'motion.setDirection': + this.source += `target.setDirection(${this.descendInput(node.direction).asNumber()});\n`; + break; + case 'motion.setRotationStyle': + this.source += `target.setRotationStyle("${sanitize(node.style)}");\n`; + break; + case 'motion.setX': // fallthrough + case 'motion.setY': // fallthrough + case 'motion.setXY': { + this.descendedIntoModulo = false; + const x = 'x' in node ? this.descendInput(node.x).asNumber() : 'target.x'; + const y = 'y' in node ? this.descendInput(node.y).asNumber() : 'target.y'; + this.source += `target.setXY(${x}, ${y});\n`; + if (this.descendedIntoModulo) { + this.source += `if (target.interpolationData) target.interpolationData = null;\n`; + } + break; + } + case 'motion.step': + this.source += `runtime.ext_scratch3_motion._moveSteps(${this.descendInput(node.steps).asNumber()}, target);\n`; + break; + + case 'noop': + console.warn('unexpected noop'); + break; + + case 'pen.clear': + this.source += `${PEN_EXT}.clear();\n`; + break; + case 'pen.down': + this.source += `${PEN_EXT}._penDown(target);\n`; + break; + case 'pen.changeParam': + this.source += `${PEN_EXT}._setOrChangeColorParam(${this.descendInput(node.param).asString()}, ${this.descendInput(node.value).asNumber()}, ${PEN_STATE}, true);\n`; + break; + case 'pen.changeSize': + this.source += `${PEN_EXT}._changePenSizeBy(${this.descendInput(node.size).asNumber()}, target);\n`; + break; + case 'pen.legacyChangeHue': + this.source += `${PEN_EXT}._changePenHueBy(${this.descendInput(node.hue).asNumber()}, target);\n`; + break; + case 'pen.legacyChangeShade': + this.source += `${PEN_EXT}._changePenShadeBy(${this.descendInput(node.shade).asNumber()}, target);\n`; + break; + case 'pen.legacySetHue': + this.source += `${PEN_EXT}._setPenHueToNumber(${this.descendInput(node.hue).asNumber()}, target);\n`; + break; + case 'pen.legacySetShade': + this.source += `${PEN_EXT}._setPenShadeToNumber(${this.descendInput(node.shade).asNumber()}, target);\n`; + break; + case 'pen.setColor': + this.source += `${PEN_EXT}._setPenColorToColor(${this.descendInput(node.color).asColor()}, target);\n`; + break; + case 'pen.setParam': + this.source += `${PEN_EXT}._setOrChangeColorParam(${this.descendInput(node.param).asString()}, ${this.descendInput(node.value).asNumber()}, ${PEN_STATE}, false);\n`; + break; + case 'pen.setSize': + this.source += `${PEN_EXT}._setPenSizeTo(${this.descendInput(node.size).asNumber()}, target);\n`; + break; + case 'pen.stamp': + this.source += `${PEN_EXT}._stamp(target);\n`; + break; + case 'pen.up': + this.source += `${PEN_EXT}._penUp(target);\n`; + break; + + case 'procedures.return': + this.source += `return ${this.descendInput(node.return).asUnknown()};`; + break; + case 'procedures.call': { + const procedureCode = node.code; + const procedureVariant = node.variant; + // Do not generate any code for empty procedures. + const procedureData = this.ir.procedures[procedureVariant]; + if (procedureData.stack === null) { + break; + } + if (!this.isWarp && procedureCode === this.script.procedureCode) { + // Direct recursion yields. + this.yieldNotWarp(); + } + if (procedureData.yields) { + this.source += 'yield* '; + if (!this.script.yields) { + throw new Error('Script uses yielding procedure but is not marked as yielding.'); + } + } + this.source += `thread.procedures["${sanitize(procedureVariant)}"](`; + // Only include arguments if the procedure accepts any. + if (procedureData.arguments.length) { + const args = []; + for (const input of node.arguments) { + args.push(this.descendInput(input).asSafe()); + } + this.source += args.join(','); + } + this.source += `);\n`; + if (node.type === 'hat') { + throw new Error('Custom hat blocks are not supported'); + } + // Variable input types may have changes after a procedure call. + this.resetVariableInputs(); + break; + } + + case 'timer.reset': + this.source += 'runtime.ioDevices.clock.resetProjectTimer();\n'; + break; + + case 'tw.debugger': + this.source += 'debugger;\n'; + break; + + case 'var.hide': + this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.variable.id)}", element: "checkbox", value: false }, runtime);\n`; + break; + case 'var.set': { + const variable = this.descendVariable(node.variable); + const value = this.descendInput(node.value); + variable.setInput(value); + this.source += `${variable.source} = ${value.asSafe()};\n`; + if (node.variable.isCloud) { + this.source += `runtime.ioDevices.cloud.requestUpdateVariable("${sanitize(node.variable.name)}", ${variable.source});\n`; + } + break; + } + case 'var.show': + this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.variable.id)}", element: "checkbox", value: true }, runtime);\n`; + break; + + case 'visualReport': { + const value = this.localVariables.next(); + this.source += `const ${value} = ${this.descendInput(node.input, true).asUnknown()};`; + // blocks like legacy no-ops can return a literal `undefined` + this.source += `if (${value} !== undefined) runtime.visualReport("${sanitize(this.script.topBlockId)}", ${value});\n`; + break; + } + case 'sensing.set.of': { + const object = this.descendInput(node.object).asString(); + const value = this.descendInput(node.value); + const property = node.property; + const isStage = node.object.value === '_stage_'; + const objectReference = isStage ? 'stage' : this.evaluateOnce(`runtime.getSpriteTargetByName(${object})`); + + this.source += `if (${objectReference})`; + + switch (property) { + case 'volume': + this.source += `runtime.ext_scratch3_sound._updateVolume(${value.asNumber()}, ${objectReference});`; + break; + case 'x position': + // comment + this.source += `${objectReference}.setXY(${value.asNumber()}, ${objectReference}.y);`; + break; + case 'y position': + this.source += `${objectReference}.setXY(${objectReference}.x, ${value.asNumber()});`; + break; + case 'direction': + this.source += `${objectReference}.setDirection(${value.asNumber()});`; + break; + case 'costume': + const costume = value.type === TYPE_NUMBER + ? value.asNumber() + : value.asString(); + this.source += `runtime.ext_scratch3_looks._setCostume(${objectReference}, ${costume});`; + break; + case 'backdrop': + const backdrop = value.type === TYPE_NUMBER + ? value.asNumber() + : value.asString(); + this.source += `runtime.ext_scratch3_looks._setBackdrop(${objectReference}, ${backdrop});`; + break; + case 'size': + this.source += `${objectReference}.setSize(${value.asNumber()});`; + break; + default: + const variableReference = this.evaluateOnce(`${objectReference} && ${objectReference}.lookupVariableByNameAndType("${sanitize(property)}", "", true)`); + this.source += `if (${variableReference}) `; + this.source += `${variableReference}.value = ${value.asString()};`; + break; + } + break; + } + + case 'tempVars.set': { + const name = this.descendInput(node.var); + const val = this.descendInput(node.val); + const hostObj = node.runtime + ? 'runtime.variables' + : node.thread + ? 'thread.variables' + : 'tempVars'; + this.source += this.isOptimized + ? `${hostObj}[${name.asString()}] = ${val.asUnknown()};` + : `set(${hostObj}, ${name.asString()}, ${val.asUnknown()});`; + break; + } + case 'tempVars.delete': { + const name = this.descendInput(node.var); + const hostObj = node.runtime + ? 'runtime.variables' + : node.thread + ? 'thread.variables' + : 'tempVars'; + this.source += this.isOptimized + ? `delete ${hostObj}[${name.asString()}];` + : `remove(${hostObj}, ${name.asString()});`; + break; + } + case 'tempVars.deleteAll': { + const hostObj = node.runtime + ? 'runtime.variables' + : node.thread + ? 'thread.variables' + : 'tempVars'; + this.source += `${hostObj} = Object.create(null);`; + break; + } + case 'tempVars.forEach': { + const name = this.descendInput(node.var); + const loops = this.descendInput(node.loops); + const hostObj = node.runtime + ? 'runtime.variables' + : node.thread + ? 'thread.variables' + : 'tempVars'; + const rootVar = this.localVariables.next(); + const keyVar = this.localVariables.next(); + const index = this.isOptimized + ? `${hostObj}[${name.asString()}]` + : `${rootVar}[${keyVar}]`; + if (!this.isOptimized) + this.source += `const [${rootVar},${keyVar}] = _resolveKeyPath(${hostObj}, ${name.asString()}); `; + this.source += `${index} = 0; `; + this.source += `while (${index} < ${loops.asNumber()}) { `; + this.source += `${index}++;\n`; + this.descendStack(node.do, new Frame(true, 'tempVars.forEach')); + this.yieldLoop(); + this.source += '}\n'; + break; + } + case 'control.dualBlock': + this.source += `console.log("dual block works");` + break + + default: + log.warn(`JS: Unknown stacked block: ${node.kind}`, node); + throw new Error(`JS: Unknown stacked block: ${node.kind}`); + } + } + + /** + * Compile a Record of input objects into a safe JS string. + * @param {Record} inputs + * @returns {string} + */ + descendInputRecord (inputs) { + let result = '{'; + for (const name of Object.keys(inputs)) { + const node = inputs[name]; + result += `"${sanitize(name)}":${this.descendInput(node).asSafe()},`; + } + result += '}'; + return result; + } + + resetVariableInputs () { + this.variableInputs = {}; + } + + descendStack (nodes, frame) { + // Entering a stack -- all bets are off. + // TODO: allow if/else to inherit values + this.resetVariableInputs(); + frame.assignData(this.currentFrame); + this.pushFrame(frame); + + for (let i = 0; i < nodes.length; i++) { + frame.isLastBlock = i === nodes.length - 1; + this.descendStackedBlock(nodes[i]); + } + + // Leaving a stack -- any assumptions made in the current stack do not apply outside of it + // TODO: in if/else this might create an extra unused object + this.resetVariableInputs(); + this.popFrame(); + } + + descendVariable (variable) { + if (this.variableInputs.hasOwnProperty(variable.id)) { + return this.variableInputs[variable.id]; + } + const input = new VariableInput(`${this.referenceVariable(variable)}.value`); + this.variableInputs[variable.id] = input; + return input; + } + + referenceVariable (variable) { + if (variable.scope === 'target') { + return this.evaluateOnce(`target.variables["${sanitize(variable.id)}"]`); + } + return this.evaluateOnce(`stage.variables["${sanitize(variable.id)}"]`); + } + + evaluateOnce (source) { + if (this._setupVariables.hasOwnProperty(source)) { + return this._setupVariables[source]; + } + const variable = this._setupVariablesPool.next(); + this._setupVariables[source] = variable; + return variable; + } + + retire () { + // After running retire() (sets thread status and cleans up some unused data), we need to return to the event loop. + // When in a procedure, return will only send us back to the previous procedure, so instead we yield back to the sequencer. + // Outside of a procedure, return will correctly bring us back to the sequencer. + if (this.isProcedure) { + this.source += 'retire(); yield;\n'; + } else { + this.source += 'retire(); return;\n'; + } + } + + yieldLoop () { + if (this.warpTimer) { + this.yieldStuckOrNotWarp(); + } else { + this.yieldNotWarp(); + } + } + + /** + * Write JS to yield the current thread if warp mode is disabled. + */ + yieldNotWarp () { + if (!this.isWarp) { + this.source += 'yield;\n'; + this.yielded(); + } + } + + /** + * Write JS to yield the current thread if warp mode is disabled or if the script seems to be stuck. + */ + yieldStuckOrNotWarp () { + if (this.isWarp) { + this.source += 'if (isStuck()) yield;\n'; + } else { + this.source += 'yield;\n'; + } + this.yielded(); + } + + yielded () { + if (!this.script.yields) { + throw new Error('Script yielded but is not marked as yielding.'); + } + // Control may have been yielded to another script -- all bets are off. + this.resetVariableInputs(); + } + + /** + * Write JS to request a redraw. + */ + requestRedraw () { + this.source += 'runtime.requestRedraw();\n'; + } + + safeConstantInput (value) { + const unsafe = typeof value === 'string' && this.namesOfCostumesAndSounds.has(value); + return new ConstantInput(value, !unsafe); + } + + /** + * Generate a call into the compatibility layer. + * @param {*} node The "compat" kind node to generate from. + * @param {boolean} setFlags Whether flags should be set describing how this function was processed. + * @param {string|null} [frameName] Name of the stack frame variable, if any + * @param {boolean} visualReport if this is being called to get visual reporter content + * @returns {string} The JS of the call. + */ + generateCompatibilityLayerCall (node, setFlags, frameName = null, visualReport) { + const opcode = node.opcode; + + let result = 'yield* executeInCompatibilityLayer({'; + + for (const inputName of Object.keys(node.inputs)) { + const input = node.inputs[inputName]; + if (inputName.startsWith('substack')) { + result += `"${sanitize(inputName.toLowerCase())}":(function* () {\n`; + this.descendStack(input, new Frame(true, opcode)); + result += '}),'; + continue; + } + const compiledInput = this.descendInput(input).asSafe(); + result += `"${sanitize(inputName)}":${compiledInput},`; + } + for (const fieldName of Object.keys(node.fields)) { + const field = node.fields[fieldName]; + if (typeof field !== 'string') { + result += `"${sanitize(fieldName)}":${JSON.stringify(field)},`; + continue; + } + result += `"${sanitize(fieldName)}":"${sanitize(field)}",`; + } + result += `"mutation":${JSON.stringify(node.mutation)},`; + const opcodeFunction = this.evaluateOnce(`runtime.getOpcodeFunction("${sanitize(opcode)}")`); + result += `}, ${opcodeFunction}, ${this.isWarp}, ${setFlags}, "${sanitize(node.id)}", ${frameName}, ${visualReport})`; + + return result; + } + + getScriptFactoryName () { + return factoryNameVariablePool.next(); + } + + getScriptName (yields) { + let name = yields ? generatorNameVariablePool.next() : functionNameVariablePool.next(); + if (this.isProcedure) { + const simplifiedProcedureCode = this.script.procedureCode + .replace(/%[\w]/g, '') // remove arguments + .replace(/[^a-zA-Z0-9]/g, '_') // remove unsafe + .substring(0, 20); // keep length reasonable + name += `_${simplifiedProcedureCode}`; + } + return name; + } + + /** + * Generate the JS to pass into eval() based on the current state of the compiler. + * @returns {string} JS to pass into eval() + */ + createScriptFactory () { + let script = ''; + + // Setup the factory + script += `(function ${this.getScriptFactoryName()}(thread) { `; + script += 'let __target = thread.target; '; + script += 'let target = __target; '; + script += 'const runtime = __target.runtime; '; + script += 'const stage = runtime.getTargetForStage();\n'; + for (const varValue of Object.keys(this._setupVariables)) { + const varName = this._setupVariables[varValue]; + script += `const ${varName} = ${varValue};\n`; + } + + // Generated script + script += 'return '; + if (this.script.yields) { + script += `function* `; + } else { + script += `function `; + } + script += this.getScriptName(this.script.yields); + script += ' ('; + if (this.script.arguments.length) { + const args = []; + for (let i = 0; i < this.script.arguments.length; i++) { + args.push(`p${i}`); + } + script += args.join(','); + } + script += ') {\n'; + script += 'let tempVars = Object.create(null);'; + + // pm: check if we are spoofing the target + // ex: as (Sprite) {} block needs to replace the target + // with a different one + + // create new var with target so we can define target as the current one + script += `let target = __target;\n`; + script += `if (thread.spoofing) {\n`; + script += `target = thread.spoofTarget;\n`; + script += `};\n`; + script += 'try {\n'; + + script += this.source; + + script += '} catch (err) {'; + script += `console.log("${sanitize(script)}");`; + script += 'console.error(err);'; + script += `runtime.emit("BLOCK_STACK_ERROR", {`; + script += `id:"${sanitize(this.script.topBlockId)}",`; + script += `value:String(err)`; + script += `});`; + script += '}\n'; + if (!this.isProcedure) { + script += 'retire();\n'; + } + script += '}; })'; + return script; + } + + /** + * Compile this script. + * @returns {Function} The factory function for the script. + */ + compile () { + if (this.script.stack) { + this.descendStack(this.script.stack, new Frame(false)); + } + + const factory = this.createScriptFactory(); + const fn = jsexecute.scopedEval(factory); + + if (this.debug) { + log.info(`JS: ${this.target.getName()}: compiled ${this.script.procedureCode || 'script'}`, factory); + } + + if (JSGenerator.testingApparatus) { + JSGenerator.testingApparatus.report(this, factory); + } + + return fn; + } +} + +// Test hook used by automated snapshot testing. +JSGenerator.testingApparatus = null; + +module.exports = JSGenerator; diff --git a/local-scratch-vm/src/compiler/variable-pool.js b/local-scratch-vm/src/compiler/variable-pool.js new file mode 100644 index 0000000000000000000000000000000000000000..11f50594021e3e6f45c98aec733cdc0edf511c07 --- /dev/null +++ b/local-scratch-vm/src/compiler/variable-pool.js @@ -0,0 +1,21 @@ +class VariablePool { + /** + * @param {string} prefix The prefix at the start of the variable name. + */ + constructor (prefix) { + if (prefix.trim().length === 0) { + throw new Error('prefix cannot be empty'); + } + this.prefix = prefix; + /** + * @private + */ + this.count = 0; + } + + next () { + return `${this.prefix}${this.count++}`; + } +} + +module.exports = VariablePool; diff --git a/local-scratch-vm/src/dispatch/central-dispatch.js b/local-scratch-vm/src/dispatch/central-dispatch.js new file mode 100644 index 0000000000000000000000000000000000000000..81a3e765f63f6810f8645aa03d14b53fee36b046 --- /dev/null +++ b/local-scratch-vm/src/dispatch/central-dispatch.js @@ -0,0 +1,141 @@ +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.} + */ + 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 = []; + } + + /** + * Synchronously call a particular method on a particular service provided locally. + * Calling this function on a remote service will fail. + * @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 {*} - the return value of the service method. + */ + callSync (service, method, ...args) { + const {provider, isRemote} = this._getServiceProvider(service); + if (provider) { + if (isRemote) { + throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`); + } + + return provider[method].apply(provider, args); + } + throw new Error(`Provider not found for service: ${service}`); + } + + /** + * Synchronously 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. + */ + setServiceSync (service, provider) { + if (this.services.hasOwnProperty(service)) { + log.warn(`Central dispatch replacing existing service provider for ${service}`); + } + this.services[service] = provider; + } + + /** + * 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 { + this.setServiceSync(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: ${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: Boolean((this.workerClass && provider instanceof this.workerClass) || provider.isRemote) + }; + } + + /** + * 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(); diff --git a/local-scratch-vm/src/dispatch/shared-dispatch.js b/local-scratch-vm/src/dispatch/shared-dispatch.js new file mode 100644 index 0000000000000000000000000000000000000000..d400e8846a0230c22bf01ebd4b0d6d29745c7bcd --- /dev/null +++ b/local-scratch-vm/src/dispatch/shared-dispatch.js @@ -0,0 +1,237 @@ +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.} + */ + 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 */ + // tw: upstream's logic is broken + // Args is actually a 3 length list of [args, util, real block info] + // We only want to send args. The others will throw errors when they try to be cloned + if ((args.length > 0) && (typeof args[args.length - 1].func === 'function')) { + args.pop(); + 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: ${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: `${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; diff --git a/local-scratch-vm/src/dispatch/worker-dispatch.js b/local-scratch-vm/src/dispatch/worker-dispatch.js new file mode 100644 index 0000000000000000000000000000000000000000..f413e6f34e738bbcb427b815252311eaacffec99 --- /dev/null +++ b/local-scratch-vm/src/dispatch/worker-dispatch.js @@ -0,0 +1,113 @@ +const SharedDispatch = require('./shared-dispatch'); + +const log = require('../util/log'); +const {centralDispatchService} = require('../extension-support/tw-extension-worker-context'); + +/** + * 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, centralDispatchService); + 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(centralDispatchService, '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 || centralDispatchService, + 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(); diff --git a/local-scratch-vm/src/engine/adapter.js b/local-scratch-vm/src/engine/adapter.js new file mode 100644 index 0000000000000000000000000000000000000000..0696ac2e49eb23884ddbdac9f5b10fa2ebad2c0b --- /dev/null +++ b/local-scratch-vm/src/engine/adapter.js @@ -0,0 +1,185 @@ +const mutationAdapter = require('./mutation-adapter'); +const uid = require('../util/uid'); + +/** + * 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 {?string} parent Parent block ID. + * @return {undefined} + */ +const domToBlock = function (blockDOM, blocks, isTopBlock, parent) { + if (!blockDOM.attributes.id) { + blockDOM.attributes.id = {}; + blockDOM.attributes.id.value = uid(); + } + + // make sure errors arnt thrown when there is no postion + blockDOM.attributes.x ??= {}; + blockDOM.attributes.y ??= {}; + + // Block skeleton. + const block = { + id: blockDOM.attributes.id.value, // Block ID + opcode: blockDOM.attributes.type.value, // For execution, "event_whengreenflag". + inputs: {}, // Inputs to this block and the blocks they point to. + fields: {}, // Fields on this block and their values. + next: null, // Next block in the stack, if one exists. + topLevel: isTopBlock, // If this block starts a stack. + parent: parent, // Parent block ID, if available. + shadow: blockDOM.tagName === 'shadow', // If this represents a shadow/slot. + x: blockDOM.attributes.x.value, // X position of script, if top-level. + y: blockDOM.attributes.y.value // Y position of script, if top-level. + }; + + // Add the block to the representation tree. + blocks[block.id] = block; + + // Process XML children and find enclosed blocks, fields, etc. + for (let i = 0; i < blockDOM.children.length; i++) { + const xmlChild = blockDOM.children[i]; + // Enclosed blocks and shadows + let childBlockNode = null; + let childShadowNode = null; + for (let j = 0; j < xmlChild.children.length; j++) { + const grandChildNode = xmlChild.children[j]; + if (!grandChildNode.tagName) { + // Non-XML tag node. + continue; + } + const grandChildNodeName = grandChildNode.tagName; + if (grandChildNodeName === 'block') { + childBlockNode = grandChildNode; + } else if (grandChildNodeName === 'shadow') { + childShadowNode = grandChildNode; + } + } + + // Use shadow block only if there's no real block node. + if (!childBlockNode && childShadowNode) { + childBlockNode = childShadowNode; + } + + // Not all Blockly-type blocks are handled here, + // as we won't be using all of them for Scratch. + switch (xmlChild.tagName) { + case 'field': + { + // Add the field to this block. + const fieldName = xmlChild.attributes.name.value; + // make sure the id exists and is valid nomatter what + xmlChild.attributes.id ??= { value: uid() }; + // Add id in case it is a variable field + const fieldId = xmlChild.attributes.id.value; + let fieldData = ''; + if (xmlChild.innerHTML) { + fieldData = xmlChild.textContent; + } else { + // If the child of the field with a data property + // doesn't exist, set the data to an empty string. + fieldData = ''; + } + block.fields[fieldName] = { + name: fieldName, + id: fieldId, + value: fieldData + }; + xmlChild.attributes.variabletype ??= {}; + const fieldVarType = xmlChild.attributes.variabletype.value; + if (typeof fieldVarType === 'string') { + block.fields[fieldName].variableType = fieldVarType; + } + break; + } + case 'comment': + { + block.comment = xmlChild.attributes.id.value; + break; + } + case 'value': + case 'statement': + { + // Recursively generate block structure for input block. + domToBlock(childBlockNode, blocks, false, block.id); + if (childShadowNode && childBlockNode !== childShadowNode) { + // Also generate the shadow block. + domToBlock(childShadowNode, blocks, false, block.id); + } + // Link this block's input to the child block. + const inputName = xmlChild.attributes.name.value; + block.inputs[inputName] = { + name: inputName, + block: childBlockNode.attributes.id.value, + shadow: childShadowNode ? childShadowNode.attributes.id.value : null + }; + break; + } + case 'next': + { + if (!childBlockNode || !childBlockNode.attributes) { + // Invalid child block. + continue; + } + // Recursively generate block structure for next block. + domToBlock(childBlockNode, blocks, false, block.id); + // Link next block to this block. + block.next = childBlockNode.attributes.id.value; + 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.} 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.tagName || !block.attributes) { + continue; + } + const tagName = block.tagName; + 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` or `Blockly.events.endDrag` + * @return {Array.} List of blocks from this CREATE event. + */ +const adapter = function (e) { + // Validate input + if (typeof e !== 'object') return; + if (typeof e.xml !== 'object') return; + const parser = new DOMParser(); + const doc = parser.parseFromString(e.xml.outerHTML, "application/xml"); + return domToBlocks(doc.childNodes); +}; + +module.exports = adapter; diff --git a/local-scratch-vm/src/engine/block-utility.js b/local-scratch-vm/src/engine/block-utility.js new file mode 100644 index 0000000000000000000000000000000000000000..b5bf4e12abfbf670eace1154abf729877921856f --- /dev/null +++ b/local-scratch-vm/src/engine/block-utility.js @@ -0,0 +1,261 @@ +const Thread = require('./thread'); +const Timer = require('../util/timer'); + +/** + * @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; + + this._nowObj = { + now: () => this.sequencer.runtime.currentMSecs + }; + } + + /** + * 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; + } + + /** + * Use the runtime's currentMSecs value as a timestamp value for now + * This is useful in some cases where we need compatibility with Scratch 2 + * @type {function} + */ + get nowObj () { + if (this.runtime) { + return this._nowObj; + } + return null; + } + + /** + * The stack frame used by loop and other blocks to track internal state. + * @type {object} + */ + get stackFrame () { + const frame = this.thread.peekStackFrame(); + if (frame.executionContext === null) { + frame.executionContext = {}; + } + return frame.executionContext; + } + + /** + * Check the stack timer and return a boolean based on whether it has finished or not. + * @return {boolean} - true if the stack timer has finished. + */ + stackTimerFinished () { + const timeElapsed = this.stackFrame.timer.timeElapsed(); + if (timeElapsed < this.stackFrame.duration) { + return false; + } + return true; + } + + /** + * Check if the stack timer needs initialization. + * @return {boolean} - true if the stack timer needs to be initialized. + */ + stackTimerNeedsInit () { + return !this.stackFrame.timer; + } + + /** + * Create and start a stack timer + * @param {number} duration - a duration in milliseconds to set the timer for. + */ + startStackTimer (duration) { + if (this.nowObj) { + this.stackFrame.timer = new Timer(this.nowObj); + } else { + this.stackFrame.timer = new Timer(); + } + this.stackFrame.timer.start(); + this.stackFrame.duration = duration; + } + + /** + * Set the thread to yield. + */ + yield () { + this.thread.status = Thread.STATUS_YIELD; + } + + /** + * pm: Set the thread to the running state. + */ + defaultStatus () { + this.thread.status = Thread.STATUS_RUNNING; + } + + /** + * Set the thread to yield until the next tick of the runtime. + */ + yieldTick () { + this.thread.status = Thread.STATUS_YIELD_TICK; + } + + /** + * 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); + } + + /** + * Get the branch for a particular C-shaped block, and it's target. + * @param {string} id ID for block to get the branch for. + * @param {string} branchId Which branch to select (e.g. for if-else). + * @return {string} ID of block in the branch. + */ + getBranchAndTarget (id, branchId) { + const result = this.thread.blockContainer.getBranch(id, branchId); + if (result) { + return [result, this.thread.target]; + } + return this.sequencer.runtime.getBranchAndTarget(id, branchId); + } + + /** + * 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 and ids of parameters for the given procedure. + * @param {string} procedureCode Procedure code for procedure to query. + * @return {Array.} List of param names for a procedure. + */ + getProcedureParamNamesAndIds (procedureCode) { + return this.thread.target.blocks.getProcedureParamNamesAndIds(procedureCode); + } + + /** + * Get names, ids, and defaults of parameters for the given procedure. + * @param {string} procedureCode Procedure code for procedure to query. + * @return {Array.} List of param names for a procedure. + */ + getProcedureParamNamesIdsAndDefaults (procedureCode) { + return this.thread.target.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode); + } + + /** + * Initialize procedure parameters in the thread before pushing parameters. + */ + initParams () { + this.thread.initParams(); + } + + /** + * 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.} List of threads started by this function. + */ + startHats (requestedHat, optMatchFields, optTarget) { + // Store thread and sequencer to ensure we can return to the calling block's context. + // startHats may execute further blocks and dirty the BlockUtility's execution context + // and confuse the calling block when we return to it. + const callerThread = this.thread; + const callerSequencer = this.sequencer; + const result = this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget); + + // Restore thread and sequencer to prior values before we return to the calling block. + this.thread = callerThread; + this.sequencer = callerSequencer; + + return result; + } + + /** + * 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; diff --git a/local-scratch-vm/src/engine/blocks-execute-cache.js b/local-scratch-vm/src/engine/blocks-execute-cache.js new file mode 100644 index 0000000000000000000000000000000000000000..ffa8708b2aada1945b74e7e3bc696cb92b7cb827 --- /dev/null +++ b/local-scratch-vm/src/engine/blocks-execute-cache.js @@ -0,0 +1,19 @@ +/** + * @fileoverview + * Access point for private method shared between blocks.js and execute.js for + * caching execute information. + */ + +/** + * A private method shared with execute to build an object containing the block + * information execute needs and that is reset when other cached Blocks info is + * reset. + * @param {Blocks} blocks Blocks containing the expected blockId + * @param {string} blockId blockId for the desired execute cache + */ +exports.getCached = function () { + throw new Error('blocks.js has not initialized BlocksExecuteCache'); +}; + +// Call after the default throwing getCached is assigned for Blocks to replace. +require('./blocks'); diff --git a/local-scratch-vm/src/engine/blocks-runtime-cache.js b/local-scratch-vm/src/engine/blocks-runtime-cache.js new file mode 100644 index 0000000000000000000000000000000000000000..cc30e83457684db44d7d2dce9c57136588ed3c77 --- /dev/null +++ b/local-scratch-vm/src/engine/blocks-runtime-cache.js @@ -0,0 +1,78 @@ +/** + * @fileoverview + * The BlocksRuntimeCache caches data about the top block of scripts so that + * Runtime can iterate a targeted opcode and iterate the returned set faster. + * Many top blocks need to match fields as well as opcode, since that matching + * compares strings in uppercase we can go ahead and uppercase the cached value + * so we don't need to in the future. + */ + +/** + * A set of cached data about the top block of a script. + * @param {Blocks} container - Container holding the block and related data + * @param {string} blockId - Id for whose block data is cached in this instance + */ +class RuntimeScriptCache { + constructor (container, blockId) { + /** + * Container with block data for blockId. + * @type {Blocks} + */ + this.container = container; + + /** + * ID for block this instance caches. + * @type {string} + */ + this.blockId = blockId; + + const block = container.getBlock(blockId); + const fields = container.getFields(block); + + /** + * Formatted fields or fields of input blocks ready for comparison in + * runtime. + * + * This is a clone of parts of the targeted blocks. Changes to these + * clones are limited to copies under RuntimeScriptCache and will not + * appear in the original blocks in their container. This copy is + * modified changing the case of strings to uppercase. These uppercase + * values will be compared later by the VM. + * @type {object} + */ + this.fieldsOfInputs = Object.assign({}, fields); + if (Object.keys(fields).length === 0) { + const inputs = container.getInputs(block); + for (const input in inputs) { + if (!inputs.hasOwnProperty(input)) continue; + const id = inputs[input].block; + const inputBlock = container.getBlock(id); + const inputFields = container.getFields(inputBlock); + Object.assign(this.fieldsOfInputs, inputFields); + } + } + for (const key in this.fieldsOfInputs) { + const field = this.fieldsOfInputs[key] = Object.assign({}, this.fieldsOfInputs[key]); + if (field.value.toUpperCase) { + field.value = field.value.toUpperCase(); + } + } + } +} + +/** + * Get an array of scripts from a block container prefiltered to match opcode. + * @param {Blocks} container - Container of blocks + * @param {string} opcode - Opcode to filter top blocks by + */ +exports.getScripts = function () { + throw new Error('blocks.js has not initialized BlocksRuntimeCache'); +}; + +/** + * Exposed RuntimeScriptCache class used by integration in blocks.js. + * @private + */ +exports._RuntimeScriptCache = RuntimeScriptCache; + +require('./blocks'); diff --git a/local-scratch-vm/src/engine/blocks.js b/local-scratch-vm/src/engine/blocks.js new file mode 100644 index 0000000000000000000000000000000000000000..15dce19c1e8eef08a2c73c55ca852c8d04a0a9d1 --- /dev/null +++ b/local-scratch-vm/src/engine/blocks.js @@ -0,0 +1,1427 @@ +const adapter = require('./adapter'); +const mutationAdapter = require('./mutation-adapter'); +const xmlEscape = require('../util/xml-escape'); +const MonitorRecord = require('./monitor-record'); +const Clone = require('../util/clone'); +const {Map} = require('immutable'); +const BlocksExecuteCache = require('./blocks-execute-cache'); +const BlocksRuntimeCache = require('./blocks-runtime-cache'); +const log = require('../util/log'); +const Variable = require('./variable'); +const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id'); + +/** + * @fileoverview + * Store and mutate the VM block representation, + * and handle updates from Scratch Blocks events. + */ + +/** + * Create a block container. + * @param {Runtime} runtime The runtime this block container operates within + * @param {boolean} optNoGlow Optional flag to indicate that blocks in this container + * should not request glows. This does not affect glows when clicking on a block to execute it. + */ +class Blocks { + constructor (runtime, optNoGlow) { + this.runtime = runtime; + + /** + * All blocks in the workspace. + * Keys are block IDs, values are metadata about the block. + * @type {Object.} + */ + this._blocks = {}; + + /** + * All top-level scripts in the workspace. + * A list of block IDs that represent scripts (i.e., first block in script). + * @type {Array.} + */ + this._scripts = []; + + /** + * Runtime Cache + * @type {{inputs: {}, procedureParamNames: {}, procedureDefinitions: {}}} + * @private + */ + Object.defineProperty(this, '_cache', {writable: true, enumerable: false}); + this._cache = { + /** + * Cache block inputs by block id + * @type {object.>} + */ + inputs: {}, + /** + * Cache procedure Param Names by block id + * @type {object.>} + */ + procedureParamNames: {}, + /** + * Cache procedure definitions by block id + * @type {object.} + */ + procedureDefinitions: {}, + + /** + * A cache for execute to use and store on. Only available to + * execute. + * @type {object.} + */ + _executeCached: {}, + + /** + * A cache of block IDs and targets to start threads on as they are + * actively monitored. + * @type {Array<{blockId: string, target: Target}>} + */ + _monitored: null, + + /** + * A cache of hat opcodes to collection of theads to execute. + * @type {object.} + */ + scripts: {}, + + /** + * tw: A cache of top block (usually hat, but not always) opcodes to compiled scripts. + * @type {object.} + */ + compiledScripts: {}, + + /** + * tw: A cache of procedure code opcodes to a parsed intermediate representation + * @type {object.} + */ + compiledProcedures: {}, + + /** + * tw: Whether populateProcedureCache has been run + */ + proceduresPopulated: false + }; + + /** + * Flag which indicates that blocks in this container should not glow. + * Blocks will still glow when clicked on, but this flag is used to control + * whether the blocks in this container can request a glow as part of + * a running stack. E.g. the flyout block container and the monitor block container + * should not be able to request a glow, but blocks containers belonging to + * sprites should. + * @type {boolean} + */ + this.forceNoGlow = optNoGlow || false; + } + + /** + * Get the cached compilation result of a block. + * @param {string} blockId ID of the top block. + * @returns {{success: boolean; value: any}|null} Cached success or error, or null if there is no cached value. + */ + getCachedCompileResult (blockId) { + if (this._cache.compiledScripts.hasOwnProperty(blockId)) { + return this._cache.compiledScripts[blockId]; + } + return null; + } + + /** + * Set the cached compilation result of a script. + * @param {string} blockId ID of the top block. + * @param {*} value The compilation result to store. + */ + cacheCompileResult (blockId, value) { + this._cache.compiledScripts[blockId] = { + success: true, + value: value + }; + } + + /** + * Set the cached error of a script. + * @param {string} blockId ID of the top block. + * @param {*} error The error to store. + */ + cacheCompileError (blockId, error) { + this._cache.compiledScripts[blockId] = { + success: false, + value: error + }; + } + + /** + * Blockly inputs that represent statements/branch. + * are prefixed with this string. + * @const{string} + */ + static get BRANCH_INPUT_PREFIX () { + return 'SUBSTACK'; + } + + /** + * Provide an object with metadata for the requested block ID. + * @param {!string} blockId ID of block we have stored. + * @return {?object} Metadata about the block, if it exists. + */ + getBlock (blockId) { + return this._blocks[blockId]; + } + + /** + * Get all known top-level blocks that start scripts. + * @return {Array.} List of block IDs. + */ + getScripts () { + return this._scripts; + } + + /** + * Get the next block for a particular block + * @param {?string} id ID of block to get the next block for + * @return {?string} ID of next block in the sequence + */ + getNextBlock (id) { + const block = this._blocks[id]; + return (typeof block === 'undefined') ? null : block.next; + } + + /** + * Get the branch for a particular C-shaped block. + * @param {?string} id ID for block to get the branch for. + * @param {?number} branchNum Which branch to select (e.g. for if-else). + * @return {?string} ID of block in the branch. + */ + getBranch (id, branchNum) { + const block = this._blocks[id]; + if (typeof block === 'undefined') return null; + if (!branchNum) branchNum = 1; + + let inputName = Blocks.BRANCH_INPUT_PREFIX; + if (branchNum > 1) { + inputName += branchNum; + } + + // Empty C-block? + const input = block.inputs[inputName]; + return (typeof input === 'undefined') ? null : input.block; + } + + /** + * Get the opcode for a particular block + * @param {?object} block The block to query + * @return {?string} the opcode corresponding to that block + */ + getOpcode (block) { + return (typeof block === 'undefined') ? null : block.opcode; + } + + /** + * Get all fields and their values for a block. + * @param {?object} block The block to query. + * @return {?object} All fields and their values. + */ + getFields (block) { + return (typeof block === 'undefined') ? null : block.fields; + } + + /** + * Get all non-branch inputs for a block. + * @param {?object} block the block to query. + * @return {?Array.} All non-branch inputs and their associated blocks. + */ + getInputs (block) { + if (typeof block === 'undefined') return null; + let inputs = this._cache.inputs[block.id]; + if (typeof inputs !== 'undefined') { + return inputs; + } + + inputs = {}; + for (const input in block.inputs) { + // Ignore blocks prefixed with branch prefix. + if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !== + Blocks.BRANCH_INPUT_PREFIX) { + inputs[input] = block.inputs[input]; + } + } + + this._cache.inputs[block.id] = inputs; + return inputs; + } + + /** + * Get mutation data for a block. + * @param {?object} block The block to query. + * @return {?object} Mutation for the block. + */ + getMutation (block) { + return (typeof block === 'undefined') ? null : block.mutation; + } + + /** + * Get the top-level script for a given block. + * @param {?string} id ID of block to query. + * @return {?string} ID of top-level script block. + */ + getTopLevelScript (id) { + let block = this._blocks[id]; + if (typeof block === 'undefined') return null; + while (block.parent !== null) { + block = this._blocks[block.parent]; + } + return block.id; + } + + /** + * Get the procedure definition for a given name. + * @param {?string} name Name of procedure to query. + * @return {?string} ID of procedure definition. + */ + getProcedureDefinition (name) { + const blockID = this._cache.procedureDefinitions[name]; + if (typeof blockID !== 'undefined') { + return blockID; + } + + for (const id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; + const block = this._blocks[id]; + if (block.opcode === 'procedures_definition' || block.opcode === 'procedures_definition_return') { + // tw: make sure that populateProcedureCache is kept up to date with this method + const internal = this._getCustomBlockInternal(block); + if (internal && internal.mutation.proccode === name) { + this._cache.procedureDefinitions[name] = id; // The outer define block id + return id; + } + } + } + + this._cache.procedureDefinitions[name] = null; + return null; + } + + /** + * Get names and ids of parameters for the given procedure. + * @param {?string} name Name of procedure to query. + * @return {?Array.} List of param names for a procedure. + */ + getProcedureParamNamesAndIds (name) { + return this.getProcedureParamNamesIdsAndDefaults(name).slice(0, 2); + } + + /** + * Get names, ids, and defaults of parameters for the given procedure. + * @param {?string} name Name of procedure to query. + * @return {?Array.} List of param names for a procedure. + */ + getProcedureParamNamesIdsAndDefaults (name) { + const cachedNames = this._cache.procedureParamNames[name]; + if (typeof cachedNames !== 'undefined') { + return cachedNames; + } + + for (const id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; + const block = this._blocks[id]; + if (block.opcode === 'procedures_prototype' && + block.mutation.proccode === name) { + // tw: make sure that populateProcedureCache is kept up to date with this method + const names = JSON.parse(block.mutation.argumentnames); + const ids = JSON.parse(block.mutation.argumentids); + const defaults = JSON.parse(block.mutation.argumentdefaults); + + this._cache.procedureParamNames[name] = [names, ids, defaults]; + return this._cache.procedureParamNames[name]; + } + } + + const addonBlock = this.runtime.getAddonBlock(name); + if (addonBlock) { + this._cache.procedureParamNames[name] = addonBlock.namesIdsDefaults; + return addonBlock.namesIdsDefaults; + } + + this._cache.procedureParamNames[name] = null; + return null; + } + + /** + * tw: Setup the procedureParamNames and procedureDefinitions caches all at once. + * This makes subsequent calls to these methods faster. + */ + populateProcedureCache () { + if (this._cache.proceduresPopulated) { + return; + } + for (const id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; + const block = this._blocks[id]; + + if (block.opcode === 'procedures_prototype') { + const name = block.mutation.proccode; + if (!this._cache.procedureParamNames[name]) { + const names = JSON.parse(block.mutation.argumentnames); + const ids = JSON.parse(block.mutation.argumentids); + const defaults = JSON.parse(block.mutation.argumentdefaults); + this._cache.procedureParamNames[name] = [names, ids, defaults]; + } + continue; + } + + if (block.opcode === 'procedures_definition' || block.opcode === 'procedures_definition_return') { + const internal = this._getCustomBlockInternal(block); + if (internal) { + const name = internal.mutation.proccode; + if (!this._cache.procedureDefinitions[name]) { + this._cache.procedureDefinitions[name] = id; + } + continue; + } + } + } + this._cache.proceduresPopulated = true; + } + + duplicate () { + const newBlocks = new Blocks(this.runtime, this.forceNoGlow); + newBlocks._blocks = Clone.simple(this._blocks); + newBlocks._scripts = Clone.simple(this._scripts); + return newBlocks; + } + // --------------------------------------------------------------------- + + /** + * Create event listener for blocks, variables, and comments. Handles validation and + * serves as a generic adapter between the blocks, variables, and the + * runtime interface. + * @param {object} e Blockly "block" or "variable" event + */ + blocklyListen (e) { + // Validate event + if (typeof e !== 'object') return; + if (typeof e.blockId !== 'string' && typeof e.varId !== 'string' && + typeof e.commentId !== 'string') { + return; + } + const stage = this.runtime.getTargetForStage(); + const editingTarget = this.runtime.getEditingTarget(); + + // UI event: clicked scripts toggle in the runtime. + if (e.element === 'stackclick') { + this.runtime.toggleScript(e.blockId, {stackClick: true}); + return; + } + + // Block create/update/destroy + switch (e.type) { + case 'create': { + const newBlocks = adapter(e); + // A create event can create many blocks. Add them all. + for (let i = 0; i < newBlocks.length; i++) { + this.createBlock(newBlocks[i]); + } + break; + } + case 'change': + this.changeBlock({ + id: e.blockId, + element: e.element, + name: e.name, + value: e.newValue + }); + break; + case 'move': + this.moveBlock({ + id: e.blockId, + oldParent: e.oldParentId, + oldInput: e.oldInputName, + newParent: e.newParentId, + newInput: e.newInputName, + newCoordinate: e.newCoordinate + }); + break; + case 'dragOutside': + this.runtime.emitBlockDragUpdate(e.isOutside); + break; + case 'endDrag': + this.runtime.emitBlockDragUpdate(false /* areBlocksOverGui */); + + // Drag blocks onto another sprite + if (e.isOutside) { + const newBlocks = adapter(e); + this.runtime.emitBlockEndDrag(newBlocks, e.blockId); + } + break; + case 'delete': + // Don't accept delete events for missing blocks, + // or shadow blocks being obscured. + if (!this._blocks.hasOwnProperty(e.blockId) || + this._blocks[e.blockId].shadow) { + return; + } + // Inform any runtime to forget about glows on this script. + if (this._blocks[e.blockId].topLevel) { + this.runtime.quietGlow(e.blockId); + } + this.deleteBlock(e.blockId); + break; + case 'var_create': + this.resetCache(); // tw: more aggressive cache resetting + // Check if the variable being created is global or local + // If local, create a local var on the current editing target, as long + // as there are no conflicts, and the current target is actually a sprite + // If global or if the editing target is not present or we somehow got + // into a state where a local var was requested for the stage, + // create a stage (global) var after checking for name conflicts + // on all the sprites. + if (e.isLocal && editingTarget && !editingTarget.isStage && !e.isCloud) { + if (!editingTarget.lookupVariableById(e.varId)) { + editingTarget.createVariable(e.varId, e.varName, e.varType); + this.emitProjectChanged(); + } + } else { + if (stage.lookupVariableById(e.varId)) { + // Do not re-create a variable if it already exists + return; + } + // Check for name conflicts in all of the targets + const allTargets = this.runtime.targets.filter(t => t.isOriginal); + for (const target of allTargets) { + if (target.lookupVariableByNameAndType(e.varName, e.varType, true)) { + return; + } + } + stage.createVariable(e.varId, e.varName, e.varType, e.isCloud); + this.runtime.emit('variableCreate', e.varType, e.varId, e.varName, e.isCloud); + this.emitProjectChanged(); + } + break; + case 'var_rename': + if (editingTarget && editingTarget.variables.hasOwnProperty(e.varId)) { + // This is a local variable, rename on the current target + editingTarget.renameVariable(e.varId, e.newName); + // Update all the blocks on the current target that use + // this variable + editingTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName); + } else { + // This is a global variable + stage.renameVariable(e.varId, e.newName); + // Update all blocks on all targets that use the renamed variable + const targets = this.runtime.targets; + for (let i = 0; i < targets.length; i++) { + const currTarget = targets[i]; + currTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName); + } + } + this.runtime.emit('variableChange', e.varType, e.varId, e.varName); + this.emitProjectChanged(); + break; + case 'var_delete': { + this.resetCache(); // tw: more aggressive cache resetting + const target = (editingTarget && editingTarget.variables.hasOwnProperty(e.varId)) ? + editingTarget : stage; + this.runtime.emit('variableDelete', e.varType, e.varId); + target.deleteVariable(e.varId); + this.emitProjectChanged(); + break; + } + case 'comment_create': + this.resetCache(); // tw: comments can affect compilation + if (this.runtime.getEditingTarget()) { + const currTarget = this.runtime.getEditingTarget(); + currTarget.createComment(e.commentId, e.blockId, e.text, + e.xy.x, e.xy.y, e.width, e.height, e.minimized); + + if (currTarget.comments[e.commentId].x === null && + currTarget.comments[e.commentId].y === null) { + // Block comments imported from 2.0 projects are imported with their + // x and y coordinates set to null so that scratch-blocks can + // auto-position them. If we are receiving a create event for these + // comments, then the auto positioning should have taken place. + // Update the x and y position of these comments to match the + // one from the event. + currTarget.comments[e.commentId].x = e.xy.x; + currTarget.comments[e.commentId].y = e.xy.y; + } + } + this.emitProjectChanged(); + break; + case 'comment_change': + this.resetCache(); // tw: comments can affect compilation + if (this.runtime.getEditingTarget()) { + const currTarget = this.runtime.getEditingTarget(); + if (!currTarget.comments.hasOwnProperty(e.commentId)) { + log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`); + return; + } + const comment = currTarget.comments[e.commentId]; + const change = e.newContents_; + if (change.hasOwnProperty('minimized')) { + comment.minimized = change.minimized; + } + if (change.hasOwnProperty('width') && change.hasOwnProperty('height')){ + comment.width = change.width; + comment.height = change.height; + } + if (change.hasOwnProperty('text')) { + comment.text = change.text; + } + this.emitProjectChanged(); + } + break; + case 'comment_move': + if (this.runtime.getEditingTarget()) { + const currTarget = this.runtime.getEditingTarget(); + if (currTarget && !currTarget.comments.hasOwnProperty(e.commentId)) { + log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`); + return; + } + const comment = currTarget.comments[e.commentId]; + const newCoord = e.newCoordinate_; + comment.x = newCoord.x; + comment.y = newCoord.y; + + this.emitProjectChanged(); + } + break; + case 'comment_delete': + this.resetCache(); // tw: comments can affect compilation + if (this.runtime.getEditingTarget()) { + const currTarget = this.runtime.getEditingTarget(); + if (!currTarget.comments.hasOwnProperty(e.commentId)) { + // If we're in this state, we have probably received + // a delete event from a workspace that we switched from + // (e.g. a delete event for a comment on sprite a's workspace + // when switching from sprite a to sprite b) + return; + } + delete currTarget.comments[e.commentId]; + if (e.blockId) { + const block = currTarget.blocks.getBlock(e.blockId); + if (!block) { + log.warn(`Could not find block referenced by comment with id: ${e.commentId}`); + return; + } + delete block.comment; + } + + this.emitProjectChanged(); + } + break; + } + } + + // --------------------------------------------------------------------- + + /** + * Reset all runtime caches. + */ + resetCache () { + this._cache.inputs = {}; + this._cache.procedureParamNames = {}; + this._cache.procedureDefinitions = {}; + this._cache._executeCached = {}; + this._cache._monitored = null; + this._cache.scripts = {}; + this._cache.compiledScripts = {}; + this._cache.compiledProcedures = {}; + this._cache.proceduresPopulated = false; + } + + /** + * Emit a project changed event if this is a block container + * that can affect the project state. + */ + emitProjectChanged () { + if (!this.forceNoGlow) { + this.runtime.emitProjectChanged(); + } + } + + /** + * Block management: create blocks and scripts from a `create` event + * @param {!object} block Blockly create event to be processed + */ + createBlock (block) { + // Does the block already exist? + // Could happen, e.g., for an unobscured shadow. + if (this._blocks.hasOwnProperty(block.id)) { + return; + } + // Create new block. + this._blocks[block.id] = block; + // Push block id to scripts array. + // Blocks are added as a top-level stack if they are marked as a top-block + // (if they were top-level XML in the event). + if (block.topLevel) { + this._addScript(block.id); + } + + this.resetCache(); + + // A new block was actually added to the block container, + // emit a project changed event + this.emitProjectChanged(); + } + + /** + * Block management: change block field values + * @param {!object} args Blockly change event to be processed + */ + changeBlock (args) { + // Validate + if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return; + let block = this._blocks[args.id]; + if (typeof block === 'undefined') return; + switch (args.element) { + case 'field': + // TODO when the field of a monitored block changes, + // update the checkbox in the flyout based on whether + // a monitor for that current combination of selected parameters exists + // e.g. + // 1. check (current [v year]) + // 2. switch dropdown in flyout block to (current [v minute]) + // 3. the checkbox should become unchecked if we're not already + // monitoring current minute + + + // Update block value + if (!block.fields[args.name]) return; + const field = block.fields[args.name]; + if (typeof field.variableType === 'string') { + // Get variable name using the id in args.value. + const variable = this.runtime.getEditingTarget().lookupVariableById(args.value); + if (variable) { + block.fields[args.name].value = variable.name; + block.fields[args.name].id = args.value; + } + } else { + // Changing the value in a dropdown + block.fields[args.name].value = args.value; + + // The selected item in the sensing of block menu needs to change based on the + // selected target. Set it to the first item in the menu list. + // TODO: (#1787) + if (block.opcode === 'sensing_of_object_menu') { + if (block.fields.OBJECT.value === '_stage_') { + this._blocks[block.parent].fields.PROPERTY.value = 'backdrop #'; + } else { + this._blocks[block.parent].fields.PROPERTY.value = 'x position'; + } + this.runtime.requestBlocksUpdate(); + } + + const flyoutBlock = block.shadow && block.parent ? this._blocks[block.parent] : block; + if (flyoutBlock.isMonitored) { + this.runtime.requestUpdateMonitor(Map({ + id: flyoutBlock.id, + params: this._getBlockParams(flyoutBlock) + })); + } + } + break; + case 'mutation': + block.mutation = mutationAdapter(args.value); + break; + case 'checkbox': { + // A checkbox usually has a one to one correspondence with the monitor + // block but in the case of monitored reporters that have arguments, + // map the old id to a new id, creating a new monitor block if necessary + if (block.fields && Object.keys(block.fields).length > 0 && + block.opcode !== 'data_variable' && block.opcode !== 'data_listcontents') { + + // This block has an argument which needs to get separated out into + // multiple monitor blocks with ids based on the selected argument + const newId = getMonitorIdForBlockWithArgs(block.id, block.fields); + // Note: we're not just constantly creating a longer and longer id everytime we check + // the checkbox because we're using the id of the block in the flyout as the base + + // check if a block with the new id already exists, otherwise create + let newBlock = this.runtime.monitorBlocks.getBlock(newId); + if (!newBlock) { + newBlock = JSON.parse(JSON.stringify(block)); + newBlock.id = newId; + this.runtime.monitorBlocks.createBlock(newBlock); + } + + block = newBlock; // Carry on through the rest of this code with newBlock + } + + const wasMonitored = block.isMonitored; + block.isMonitored = args.value; + + // Variable blocks may be sprite specific depending on the owner of the variable + let isSpriteLocalVariable = false; + if (block.opcode === 'data_variable') { + isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.VARIABLE.id]); + } else if (block.opcode === 'data_listcontents') { + isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.LIST.id]); + } + + const isSpriteSpecific = isSpriteLocalVariable || + (this.runtime.monitorBlockInfo.hasOwnProperty(block.opcode) && + this.runtime.monitorBlockInfo[block.opcode].isSpriteSpecific); + if (isSpriteSpecific) { + // If creating a new sprite specific monitor, the only possible target is + // the current editing one b/c you cannot dynamically create monitors. + // Also, do not change the targetId if it has already been assigned + block.targetId = block.targetId || this.runtime.getEditingTarget().id; + } else { + block.targetId = null; + } + + if (wasMonitored && !block.isMonitored) { + this.runtime.requestHideMonitor(block.id); + } else if (!wasMonitored && block.isMonitored) { + // Tries to show the monitor for specified block. If it doesn't exist, add the monitor. + if (!this.runtime.requestShowMonitor(block.id)) { + this.runtime.requestAddMonitor(MonitorRecord({ + id: block.id, + targetId: block.targetId, + spriteName: block.targetId ? this.runtime.getTargetById(block.targetId).getName() : null, + opcode: block.opcode, + params: this._getBlockParams(block), + // @todo(vm#565) for numerical values with decimals, some countries use comma + value: '', + mode: block.opcode === 'data_listcontents' ? 'list' : 'default' + })); + } + } + break; + } + } + + this.emitProjectChanged(); + + this.resetCache(); + } + + /** + * Block management: move blocks from parent to parent + * @param {!object} e Blockly move event to be processed + */ + moveBlock (e) { + if (!this._blocks.hasOwnProperty(e.id)) { + return; + } + + const block = this._blocks[e.id]; + // Track whether a change actually occurred + // ignoring changes like routine re-positioning + // of a block when loading a workspace + let didChange = false; + + // Move coordinate changes. + if (e.newCoordinate) { + + didChange = (block.x !== e.newCoordinate.x) || (block.y !== e.newCoordinate.y); + + block.x = e.newCoordinate.x; + block.y = e.newCoordinate.y; + } + + // Remove from any old parent. + if (typeof e.oldParent !== 'undefined') { + const oldParent = this._blocks[e.oldParent]; + if (typeof e.oldInput !== 'undefined' && + oldParent.inputs[e.oldInput].block === e.id) { + // This block was connected to the old parent's input. + oldParent.inputs[e.oldInput].block = null; + } else if (oldParent.next === e.id) { + // This block was connected to the old parent's next connection. + oldParent.next = null; + } + this._blocks[e.id].parent = null; + didChange = true; + } + + // Is this block a top-level block? + if (typeof e.newParent === 'undefined') { + this._addScript(e.id); + } else { + // Remove script, if one exists. + this._deleteScript(e.id); + // Otherwise, try to connect it in its new place. + if (typeof e.newInput === 'undefined') { + // Moved to the new parent's next connection. + this._blocks[e.newParent].next = e.id; + } else { + // Moved to the new parent's input. + // Don't obscure the shadow block. + let oldShadow = null; + if (this._blocks[e.newParent].inputs.hasOwnProperty(e.newInput)) { + oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow; + } + + // If the block being attached is itself a shadow, make sure to set + // both block and shadow to that blocks ID. This happens when adding + // inputs to a custom procedure. + if (this._blocks[e.id].shadow) oldShadow = e.id; + + this._blocks[e.newParent].inputs[e.newInput] = { + name: e.newInput, + block: e.id, + shadow: oldShadow + }; + } + this._blocks[e.id].parent = e.newParent; + didChange = true; + } + this.resetCache(); + + if (didChange) this.emitProjectChanged(); + } + + + /** + * Block management: run all blocks. + * @param {!object} runtime Runtime to run all blocks in. + */ + runAllMonitored (runtime) { + if (this._cache._monitored === null) { + this._cache._monitored = Object.keys(this._blocks) + .filter(blockId => this.getBlock(blockId).isMonitored) + .map(blockId => { + const targetId = this.getBlock(blockId).targetId; + return { + blockId, + target: targetId ? runtime.getTargetById(targetId) : null + }; + }); + } + + const monitored = this._cache._monitored; + for (let i = 0; i < monitored.length; i++) { + const {blockId, target} = monitored[i]; + runtime.addMonitorScript(blockId, target); + } + } + + /** + * Block management: delete blocks and their associated scripts. Does nothing if a block + * with the given ID does not exist. + * @param {!string} blockId Id of block to delete + * @param {boolean} preserveStack If we should reconect the bottom blocks to the top block + */ + deleteBlock (blockId, preserveStack) { + // @todo In runtime, stop threads running on this script. + + // Get block + const block = this._blocks[blockId]; + if (!block) { + // No block with the given ID exists + return; + } + + // Delete children + if (block.next !== null && !preserveStack) { + this.deleteBlock(block.next); + } + + if (preserveStack) { + const parent = this._blocks[block.parent]; + const next = this._blocks[block.next]; + const input = parent?.inputs + ? [...Object.entries(parent.inputs)] + .find(ent => ent[1].block === blockId)?.[1] + : null; + if (parent && !input) parent.next = block.next; + if (next) next.parent = block.parent; + if (next && input) input.block = block.next; + } + + // Delete inputs (including branches) + for (const input in block.inputs) { + // If it's null, the block in this input moved away. + if (block.inputs[input].block !== null) { + this.deleteBlock(block.inputs[input].block); + } + // Delete obscured shadow blocks. + if (block.inputs[input].shadow !== null && + block.inputs[input].shadow !== block.inputs[input].block) { + this.deleteBlock(block.inputs[input].shadow); + } + } + + if (!preserveStack) { + // Delete any script starting with this block. + this._deleteScript(blockId); + } + const i = this._scripts.indexOf(blockId); + if (preserveStack && i > -1) { + const next = this._blocks[block.next]; + if (next) { + this._scripts.push(next.id); + next.topLevel = true; + next.x = block.x; + next.y = block.y; + } + this._scripts.splice(i, 1); + } + + // Delete block itself. + delete this._blocks[blockId]; + + this.resetCache(); + this.emitProjectChanged(); + } + + /** + * Returns a map of all references to variables or lists from blocks + * in this block container. + * @param {Array} optBlocks Optional list of blocks to constrain the search to. + * This is useful for getting variable/list references for a stack of blocks instead + * of all blocks on the workspace + * @param {?boolean} optIncludeBroadcast Optional whether to include broadcast fields. + * @return {object} A map of variable ID to a list of all variable references + * for that ID. A variable reference contains the field referencing that variable + * and also the type of the variable being referenced. + */ + getAllVariableAndListReferences (optBlocks, optIncludeBroadcast) { + const blocks = optBlocks ? optBlocks : this._blocks; + const allReferences = Object.create(null); + for (const blockId in blocks) { + let varOrListField = null; + let varType = null; + if (blocks[blockId].fields.VARIABLE) { + varOrListField = blocks[blockId].fields.VARIABLE; + varType = Variable.SCALAR_TYPE; + } else if (blocks[blockId].fields.LIST) { + varOrListField = blocks[blockId].fields.LIST; + varType = Variable.LIST_TYPE; + } else if (optIncludeBroadcast && blocks[blockId].fields.BROADCAST_OPTION) { + varOrListField = blocks[blockId].fields.BROADCAST_OPTION; + varType = Variable.BROADCAST_MESSAGE_TYPE; + } + if (varOrListField) { + const currVarId = varOrListField.id; + if (allReferences[currVarId]) { + allReferences[currVarId].push({ + referencingField: varOrListField, + type: varType + }); + } else { + allReferences[currVarId] = [{ + referencingField: varOrListField, + type: varType + }]; + } + } + } + return allReferences; + } + + /** + * Keep blocks up to date after a variable gets renamed. + * @param {string} varId The id of the variable that was renamed + * @param {string} newName The new name of the variable that was renamed + */ + updateBlocksAfterVarRename (varId, newName) { + const blocks = this._blocks; + for (const blockId in blocks) { + let varOrListField = null; + if (blocks[blockId].fields.VARIABLE) { + varOrListField = blocks[blockId].fields.VARIABLE; + } else if (blocks[blockId].fields.LIST) { + varOrListField = blocks[blockId].fields.LIST; + } + if (varOrListField) { + const currFieldId = varOrListField.id; + if (varId === currFieldId) { + varOrListField.value = newName; + } + } + } + } + + /** + * Keep blocks up to date after they are shared between targets. + * @param {boolean} isStage If the new target is a stage. + */ + updateTargetSpecificBlocks (isStage) { + const blocks = this._blocks; + for (const blockId in blocks) { + if (isStage && blocks[blockId].opcode === 'event_whenthisspriteclicked') { + blocks[blockId].opcode = 'event_whenstageclicked'; + } else if (!isStage && blocks[blockId].opcode === 'event_whenstageclicked') { + blocks[blockId].opcode = 'event_whenthisspriteclicked'; + } + } + } + + /** + * Update blocks after a sound, costume, or backdrop gets renamed. + * Any block referring to the old name of the asset should get updated + * to refer to the new name. + * @param {string} oldName The old name of the asset that was renamed. + * @param {string} newName The new name of the asset that was renamed. + * @param {string} assetType String representation of the kind of asset + * that was renamed. This can be one of 'sprite','costume', 'sound', or + * 'backdrop'. + */ + updateAssetName (oldName, newName, assetType) { + let getAssetField; + if (assetType === 'costume') { + getAssetField = this._getCostumeField.bind(this); + } else if (assetType === 'sound') { + getAssetField = this._getSoundField.bind(this); + } else if (assetType === 'backdrop') { + getAssetField = this._getBackdropField.bind(this); + } else if (assetType === 'sprite') { + getAssetField = this._getSpriteField.bind(this); + } else { + return; + } + const blocks = this._blocks; + for (const blockId in blocks) { + const assetField = getAssetField(blockId); + if (assetField && assetField.value === oldName) { + assetField.value = newName; + } + } + this.resetCache(); + } + + /** + * Update sensing_of blocks after a variable gets renamed. + * @param {string} oldName The old name of the variable that was renamed. + * @param {string} newName The new name of the variable that was renamed. + * @param {string} targetName The name of the target the variable belongs to. + * @return {boolean} Returns true if any of the blocks were updated. + */ + updateSensingOfReference (oldName, newName, targetName) { + const blocks = this._blocks; + let blockUpdated = false; + for (const blockId in blocks) { + const block = blocks[blockId]; + if (block.opcode === 'sensing_of' && + block.fields.PROPERTY.value === oldName && + // If block and shadow are different, it means a block is inserted to OBJECT, and should be ignored. + block.inputs.OBJECT.block === block.inputs.OBJECT.shadow) { + const inputBlock = this.getBlock(block.inputs.OBJECT.block); + if (inputBlock.fields.OBJECT.value === targetName) { + block.fields.PROPERTY.value = newName; + blockUpdated = true; + } + } + } + if (blockUpdated) this.resetCache(); + return blockUpdated; + } + + /** + * Helper function to retrieve a costume menu field from a block given its id. + * @param {string} blockId A unique identifier for a block + * @return {?object} The costume menu field of the block with the given block id. + * Null if either a block with the given id doesn't exist or if a costume menu field + * does not exist on the block with the given id. + */ + _getCostumeField (blockId) { + const block = this.getBlock(blockId); + if (block && block.fields.hasOwnProperty('COSTUME')) { + return block.fields.COSTUME; + } + return null; + } + + /** + * Helper function to retrieve a sound menu field from a block given its id. + * @param {string} blockId A unique identifier for a block + * @return {?object} The sound menu field of the block with the given block id. + * Null, if either a block with the given id doesn't exist or if a sound menu field + * does not exist on the block with the given id. + */ + _getSoundField (blockId) { + const block = this.getBlock(blockId); + if (block && block.fields.hasOwnProperty('SOUND_MENU')) { + return block.fields.SOUND_MENU; + } + return null; + } + + /** + * Helper function to retrieve a backdrop menu field from a block given its id. + * @param {string} blockId A unique identifier for a block + * @return {?object} The backdrop menu field of the block with the given block id. + * Null, if either a block with the given id doesn't exist or if a backdrop menu field + * does not exist on the block with the given id. + */ + _getBackdropField (blockId) { + const block = this.getBlock(blockId); + if (block && block.fields.hasOwnProperty('BACKDROP')) { + return block.fields.BACKDROP; + } + return null; + } + + /** + * Helper function to retrieve a sprite menu field from a block given its id. + * @param {string} blockId A unique identifier for a block + * @return {?object} The sprite menu field of the block with the given block id. + * Null, if either a block with the given id doesn't exist or if a sprite menu field + * does not exist on the block with the given id. + */ + _getSpriteField (blockId) { + const block = this.getBlock(blockId); + if (!block) { + return null; + } + const spriteMenuNames = ['TOWARDS', 'TO', 'OBJECT', 'VIDEOONMENU2', + 'DISTANCETOMENU', 'TOUCHINGOBJECTMENU', 'CLONE_OPTION']; + for (let i = 0; i < spriteMenuNames.length; i++) { + const menuName = spriteMenuNames[i]; + if (block.fields.hasOwnProperty(menuName)) { + return block.fields[menuName]; + } + } + return null; + } + + // --------------------------------------------------------------------- + + /** + * Encode all of `this._blocks` as an XML string usable + * by a Blockly/scratch-blocks workspace. + * @param {object} comments Map of comments referenced by id + * @return {string} String of XML representing this object's blocks. + */ + toXML (comments) { + return this._scripts.map(script => this.blockToXML(script, comments)).join(); + } + + /** + * Recursively encode an individual block and its children + * into a Blockly/scratch-blocks XML string. + * @param {!string} blockId ID of block to encode. + * @param {object} comments Map of comments referenced by id + * @return {string} String of XML representing this block and any children. + */ + blockToXML (blockId, comments) { + const block = this._blocks[blockId]; + // block should exist, but currently some blocks' next property point + // to a blockId for non-existent blocks. Until we track down that behavior, + // this early exit allows the project to load. + if (!block) return; + // Encode properties of this block. + const tagName = (block.shadow) ? 'shadow' : 'block'; + let xmlString = + `<${tagName} + id="${xmlEscape(block.id)}" + type="${xmlEscape(block.opcode)}" + ${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''} + >`; + const commentId = block.comment; + if (commentId) { + if (comments) { + if (comments.hasOwnProperty(commentId)) { + xmlString += comments[commentId].toXML(); + } else { + log.warn(`Could not find comment with id: ${commentId} in provided comment descriptions.`); + } + } else { + log.warn(`Cannot serialize comment with id: ${commentId}; no comment descriptions provided.`); + } + } + // Add any mutation. Must come before inputs. + if (block.mutation) { + xmlString += this.mutationToXML(block.mutation); + } + // Add any inputs on this block. + for (const input in block.inputs) { + if (!block.inputs.hasOwnProperty(input)) continue; + const blockInput = block.inputs[input]; + // Only encode a value tag if the value input is occupied. + if (blockInput.block || blockInput.shadow) { + xmlString += ``; + if (blockInput.block) { + xmlString += this.blockToXML(blockInput.block, comments); + } + if (blockInput.shadow && blockInput.shadow !== blockInput.block) { + // Obscured shadow. + xmlString += this.blockToXML(blockInput.shadow, comments); + } + xmlString += ''; + } + } + // Add any fields on this block. + for (const field in block.fields) { + if (!block.fields.hasOwnProperty(field)) continue; + const blockField = block.fields[field]; + xmlString += `${value}`; + } + // Add blocks connected to the next connection. + if (block.next) { + xmlString += `${this.blockToXML(block.next, comments)}`; + } + xmlString += ``; + return xmlString; + } + + /** + * Recursively encode a mutation object to XML. + * @param {!object} mutation Object representing a mutation. + * @return {string} XML string representing a mutation. + */ + mutationToXML (mutation) { + if (typeof mutation === 'string') return xmlEscape(mutation) + let mutationString = `<${mutation.tagName}`; + for (const prop in mutation) { + if (prop === 'children' || prop === 'tagName') continue; + let mutationValue = (typeof mutation[prop] === 'string') ? + xmlEscape(mutation[prop]) : mutation[prop]; + + // Handle dynamic extension blocks + if (prop === 'blockInfo') { + mutationValue = xmlEscape(JSON.stringify(mutation[prop])); + } + + mutationString += ` ${prop}="${mutationValue}"`; + } + mutationString += '>'; + for (let i = 0; i < mutation.children.length; i++) { + mutationString += this.mutationToXML(mutation.children[i]); + } + mutationString += ``; + return mutationString; + } + + // --------------------------------------------------------------------- + /** + * Helper to serialize block fields and input fields for reporting new monitors + * @param {!object} block Block to be paramified. + * @return {!object} object of param key/values. + */ + _getBlockParams (block) { + const params = {}; + for (const key in block.fields) { + params[key] = block.fields[key].value; + } + for (const inputKey in block.inputs) { + const inputBlock = this._blocks[block.inputs[inputKey].block]; + for (const key in inputBlock.fields) { + params[key] = inputBlock.fields[key].value; + } + } + return params; + } + + /** + * Helper to get the corresponding internal procedure definition block + * @param {!object} defineBlock Outer define block. + * @return {!object} internal definition block which has the mutation. + */ + _getCustomBlockInternal (defineBlock) { + if (defineBlock.inputs && defineBlock.inputs.custom_block) { + return this._blocks[defineBlock.inputs.custom_block.block]; + } + } + + /** + * Helper to add a stack to `this._scripts`. + * @param {?string} topBlockId ID of block that starts the script. + */ + _addScript (topBlockId) { + const i = this._scripts.indexOf(topBlockId); + if (i > -1) return; // Already in scripts. + this._scripts.push(topBlockId); + // Update `topLevel` property on the top block. + this._blocks[topBlockId].topLevel = true; + } + + /** + * Helper to remove a script from `this._scripts`. + * @param {?string} topBlockId ID of block that starts the script. + */ + _deleteScript (topBlockId) { + const i = this._scripts.indexOf(topBlockId); + if (i > -1) this._scripts.splice(i, 1); + // Update `topLevel` property on the top block. + if (this._blocks[topBlockId]) this._blocks[topBlockId].topLevel = false; + } +} + +/** + * A private method shared with execute to build an object containing the block + * information execute needs and that is reset when other cached Blocks info is + * reset. + * @param {Blocks} blocks Blocks containing the expected blockId + * @param {string} blockId blockId for the desired execute cache + * @param {function} CacheType constructor for cached block information + * @return {object} execute cache object + */ +BlocksExecuteCache.getCached = function (blocks, blockId, CacheType) { + let cached = blocks._cache._executeCached[blockId]; + if (typeof cached !== 'undefined') { + return cached; + } + + const block = blocks.getBlock(blockId); + if (typeof block === 'undefined') return null; + + if (typeof CacheType === 'undefined') { + cached = { + id: blockId, + opcode: blocks.getOpcode(block), + fields: blocks.getFields(block), + inputs: blocks.getInputs(block), + mutation: blocks.getMutation(block) + }; + } else { + cached = new CacheType(blocks, { + id: blockId, + opcode: blocks.getOpcode(block), + fields: blocks.getFields(block), + inputs: blocks.getInputs(block), + mutation: blocks.getMutation(block) + }); + } + + blocks._cache._executeCached[blockId] = cached; + return cached; +}; + +/** + * Cache class constructor for runtime. Used to consider what threads should + * start based on hat data. + * @type {function} + */ +const RuntimeScriptCache = BlocksRuntimeCache._RuntimeScriptCache; + +/** + * Get an array of scripts from a block container prefiltered to match opcode. + * @param {Blocks} blocks - Container of blocks + * @param {string} opcode - Opcode to filter top blocks by + * @returns {Array.} - Array of RuntimeScriptCache cache + * objects + */ +BlocksRuntimeCache.getScripts = function (blocks, opcode) { + let scripts = blocks._cache.scripts[opcode]; + if (!scripts) { + scripts = blocks._cache.scripts[opcode] = []; + + const allScripts = blocks._scripts; + for (let i = 0; i < allScripts.length; i++) { + const topBlockId = allScripts[i]; + const block = blocks.getBlock(topBlockId); + if (block.opcode === opcode) { + scripts.push(new RuntimeScriptCache(blocks, topBlockId)); + } + } + } + return scripts; +}; + +module.exports = Blocks; diff --git a/local-scratch-vm/src/engine/comment.js b/local-scratch-vm/src/engine/comment.js new file mode 100644 index 0000000000000000000000000000000000000000..ac644e770eafbffce4d4d7455d6dfa7c1ddcb406 --- /dev/null +++ b/local-scratch-vm/src/engine/comment.js @@ -0,0 +1,56 @@ +/** + * @fileoverview + * Object representing a Scratch Comment (block or workspace). + */ + +const uid = require('../util/uid'); +const xmlEscape = require('../util/xml-escape'); + +class Comment { + /** + * @param {string} id Id of the comment. + * @param {string} text Text content of the comment. + * @param {number} x X position of the comment on the workspace. + * @param {number} y Y position of the comment on the workspace. + * @param {number} width The width of the comment when it is full size. + * @param {number} height The height of the comment when it is full size. + * @param {boolean} minimized Whether the comment is minimized. + * @constructor + */ + constructor (id, text, x, y, width, height, minimized) { + this.id = id || uid(); + this.text = text; + this.x = x; + this.y = y; + this.width = Math.max(Number(width), Comment.MIN_WIDTH); + this.height = Math.max(Number(height), Comment.MIN_HEIGHT); + this.minimized = minimized || false; + this.blockId = null; + } + + toXML () { + return `${xmlEscape(this.text)}`; + } + + // TODO choose min and defaults for width and height + static get MIN_WIDTH () { + return 20; + } + + static get MIN_HEIGHT () { + return 20; + } + + static get DEFAULT_WIDTH () { + return 100; + } + + static get DEFAULT_HEIGHT () { + return 100; + } + +} + +module.exports = Comment; diff --git a/local-scratch-vm/src/engine/execute.js b/local-scratch-vm/src/engine/execute.js new file mode 100644 index 0000000000000000000000000000000000000000..c874b892915e1b41e7cb61512b39228c455d9822 --- /dev/null +++ b/local-scratch-vm/src/engine/execute.js @@ -0,0 +1,586 @@ +const BlockUtility = require('./block-utility'); +const BlocksExecuteCache = require('./blocks-execute-cache'); +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. + */ +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, blockCached, lastOperation) { + const currentBlockId = blockCached.id; + const opcode = blockCached.opcode; + const isHat = blockCached._isHat; + + thread.pushReportedValue(resolvedValue); + if (isHat) { + // Hat predicate was evaluated. + if (thread.stackClick) { + thread.status = Thread.STATUS_RUNNING; + } else 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 + const hasOldEdgeValue = thread.target.hasEdgeActivatedValue(currentBlockId); + const oldEdgeValue = thread.target.updateEdgeActivatedValue( + currentBlockId, + resolvedValue + ); + + const edgeWasActivated = hasOldEdgeValue ? (!oldEdgeValue && resolvedValue) : resolvedValue; + if (edgeWasActivated) { + thread.status = Thread.STATUS_RUNNING; + } else { + sequencer.retireThread(thread); + } + } else if (resolvedValue) { + // Predicate returned true: allow the script to run. + thread.status = Thread.STATUS_RUNNING; + } else { + // Predicate returned false: do not allow script to run + sequencer.retireThread(thread); + } + } else { + // In a non-hat, report the value visually if necessary if + // at the top of the thread stack. + if (lastOperation && 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: resolvedValue + })); + } + } + // Finished any yields. + thread.status = Thread.STATUS_RUNNING; + } +}; + +const handlePromise = (primitiveReportedValue, sequencer, thread, blockCached, lastOperation) => { + if (thread.status === Thread.STATUS_RUNNING) { + // Primitive returned a promise; automatically yield thread. + thread.status = Thread.STATUS_PROMISE_WAIT; + } + // Promise handlers + primitiveReportedValue.then(resolvedValue => { + handleReport(resolvedValue, sequencer, thread, blockCached, lastOperation); + // If it's a command block or a top level reporter in a stackClick. + // TW: Don't mangle the stack when we just finished executing a hat block. + // Hat block is always the top and first block of the script. There are no loops to find. + if (lastOperation && (!blockCached._isHat || thread.stackClick)) { + 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); + } + }, rejectionReason => { + // Promise rejected: the primitive had some error. + // Log it and proceed. + log.warn('Primitive rejected promise: ', rejectionReason); + thread.status = Thread.STATUS_RUNNING; + thread.popStack(); + }); +}; + +/** + * A execute.js internal representation of a block to reduce the time spent in + * execute as the same blocks are called the most. + * + * With the help of the Blocks class create a mutable copy of block + * information. The members of BlockCached derived values of block information + * that does not need to be reevaluated until a change in Blocks. Since Blocks + * handles where the cache instance is stored, it drops all cache versions of a + * block when any change happens to it. This way we can quickly execute blocks + * and keep perform the right action according to the current block information + * in the editor. + * + * @param {Blocks} blockContainer the related Blocks instance + * @param {object} cached default set of cached values + */ +class BlockCached { + constructor (blockContainer, cached) { + /** + * Block id in its parent set of blocks. + * @type {string} + */ + this.id = cached.id; + + /** + * Block operation code for this block. + * @type {string} + */ + this.opcode = cached.opcode; + + /** + * Original block object containing argument values for static fields. + * @type {object} + */ + this.fields = cached.fields; + + /** + * Original block object containing argument values for executable inputs. + * @type {object} + */ + this.inputs = cached.inputs; + + /** + * Procedure mutation. + * @type {?object} + */ + this.mutation = cached.mutation; + + /** + * The profiler the block is configured with. + * @type {?Profiler} + */ + this._profiler = null; + + /** + * Profiler information frame. + * @type {?ProfilerFrame} + */ + this._profilerFrame = null; + + /** + * Is the opcode a hat (event responder) block. + * @type {boolean} + */ + this._isHat = false; + + /** + * The block opcode's implementation function. + * @type {?function} + */ + this._blockFunction = null; + + /** + * Is the block function defined for this opcode? + * @type {boolean} + */ + this._definedBlockFunction = false; + + /** + * Is this block a block with no function but a static value to return. + * @type {boolean} + */ + this._isShadowBlock = false; + + /** + * The static value of this block if it is a shadow block. + * @type {?any} + */ + this._shadowValue = null; + + /** + * A copy of the block's fields that may be modified. + * @type {object} + */ + this._fields = Object.assign({}, this.fields); + + /** + * A copy of the block's inputs that may be modified. + * @type {object} + */ + this._inputs = Object.assign({}, this.inputs); + + /** + * An arguments object for block implementations. All executions of this + * specific block will use this objecct. + * @type {object} + */ + this._argValues = { + mutation: this.mutation + }; + + /** + * The inputs key the parent refers to this BlockCached by. + * @type {string} + */ + this._parentKey = null; + + /** + * The target object where the parent wants the resulting value stored + * with _parentKey as the key. + * @type {object} + */ + this._parentValues = null; + + /** + * A sequence of non-shadow operations that can must be performed. This + * list recreates the order this block and its children are executed. + * Since the order is always the same we can safely store that order + * and iterate over the operations instead of dynamically walking the + * tree every time. + * @type {Array} + */ + this._ops = []; + + const {runtime} = blockUtility.sequencer; + + const {opcode, fields, inputs} = this; + + // Assign opcode isHat and blockFunction data to avoid dynamic lookups. + this._isHat = runtime.getIsHat(opcode); + this._blockFunction = runtime.getOpcodeFunction(opcode); + this._definedBlockFunction = typeof this._blockFunction !== 'undefined'; + + // Store the current shadow value if there is a shadow value. + const fieldKeys = Object.keys(fields); + this._isShadowBlock = ( + !this._definedBlockFunction && + fieldKeys.length === 1 && + Object.keys(inputs).length === 0 + ); + this._shadowValue = this._isShadowBlock && fields[fieldKeys[0]].value; + + // Store the static fields onto _argValues. + for (const fieldName in fields) { + const field = fields[fieldName]; + if (typeof field.variableType === 'string') { + this._argValues[fieldName] = { + id: field.id, + name: field.value + }; + } else { + this._argValues[fieldName] = field.value; + } + } + + // Remove custom_block. It is not part of block execution. + delete this._inputs.custom_block; + + if ('BROADCAST_INPUT' in this._inputs) { + // BROADCAST_INPUT is called BROADCAST_OPTION in the args and is an + // object with an unchanging shape. + this._argValues.BROADCAST_OPTION = { + id: null, + name: null + }; + + // We can go ahead and compute BROADCAST_INPUT if it is a shadow + // value. + const broadcastInput = this._inputs.BROADCAST_INPUT; + 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; + this._argValues.BROADCAST_OPTION.id = broadcastField.id; + this._argValues.BROADCAST_OPTION.name = broadcastField.value; + + // Evaluating BROADCAST_INPUT here we do not need to do so + // later. + delete this._inputs.BROADCAST_INPUT; + } + } + + // Cache all input children blocks in the operation lists. The + // operations can later be run in the order they appear in correctly + // executing the operations quickly in a flat loop instead of needing to + // recursivly iterate them. + for (const inputName in this._inputs) { + const input = this._inputs[inputName]; + if (input.block) { + const inputCached = BlocksExecuteCache.getCached(blockContainer, input.block, BlockCached); + + if (inputCached._isHat) { + continue; + } + + this._ops.push(...inputCached._ops); + inputCached._parentKey = inputName; + inputCached._parentValues = this._argValues; + + // Shadow values are static and do not change, go ahead and + // store their value on args. + if (inputCached._isShadowBlock) { + this._argValues[inputName] = inputCached._shadowValue; + } + } + } + + // The final operation is this block itself. At the top most block is a + // command block or a block that is being run as a monitor. + if (this._definedBlockFunction) { + this._ops.push(this); + } + } +} + +/** + * Initialize a BlockCached instance so its command/hat + * block and reporters can be profiled during execution. + * @param {Profiler} profiler - The profiler that is currently enabled. + * @param {BlockCached} blockCached - The blockCached instance to profile. + */ +const _prepareBlockProfiling = function (profiler, blockCached) { + blockCached._profiler = profiler; + + if (blockFunctionProfilerId === -1) { + blockFunctionProfilerId = profiler.idByName(blockFunctionProfilerFrame); + } + + const ops = blockCached._ops; + for (let i = 0; i < ops.length; i++) { + ops[i]._profilerFrame = profiler.frame(blockFunctionProfilerId, ops[i].opcode); + } +}; + +/** + * Execute a block. + * @param {!Sequencer} sequencer Which sequencer is executing. + * @param {!Thread} thread Thread which to read and execute. + */ +const execute = function (sequencer, thread) { + const runtime = sequencer.runtime; + + // store sequencer and thread so block functions can access them through + // convenience methods. + blockUtility.sequencer = sequencer; + blockUtility.thread = thread; + + // Current block to execute is the one on the top of the stack. + const currentBlockId = thread.peekStack(); + const currentStackFrame = thread.peekStackFrame(); + + let blockContainer = thread.blockContainer; + let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); + if (blockCached === null) { + blockContainer = runtime.flyoutBlocks; + blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); + // Stop if block or target no longer exists. + if (blockCached === null) { + // No block found: stop the thread; script no longer exists. + sequencer.retireThread(thread); + return; + } + } + + const ops = blockCached._ops; + const length = ops.length; + let i = 0; + + if (currentStackFrame.reported !== null) { + const reported = currentStackFrame.reported; + // Reinstate all the previous values. + for (; i < reported.length; i++) { + const {opCached: oldOpCached, inputValue} = reported[i]; + + const opCached = ops.find(op => op.id === oldOpCached); + + if (opCached) { + const inputName = opCached._parentKey; + const argValues = opCached._parentValues; + + if (inputName === 'BROADCAST_INPUT') { + // Something is plugged into the broadcast input. + // Cast it to a string. We don't need an id here. + argValues.BROADCAST_OPTION.id = null; + argValues.BROADCAST_OPTION.name = cast.toString(inputValue); + } else { + argValues[inputName] = inputValue; + } + } + } + + // Find the last reported block that is still in the set of operations. + // This way if the last operation was removed, we'll find the next + // candidate. If an earlier block that was performed was removed then + // we'll find the index where the last operation is now. + if (reported.length > 0) { + const lastExisting = reported.reverse().find(report => ops.find(op => op.id === report.opCached)); + if (lastExisting) { + i = ops.findIndex(opCached => opCached.id === lastExisting.opCached) + 1; + } else { + i = 0; + } + } + + // The reporting block must exist and must be the next one in the sequence of operations. + if (thread.justReported !== null && ops[i] && ops[i].id === currentStackFrame.reporting) { + const opCached = ops[i]; + const inputValue = thread.justReported; + + thread.justReported = null; + + const inputName = opCached._parentKey; + const argValues = opCached._parentValues; + + if (inputName === 'BROADCAST_INPUT') { + // Something is plugged into the broadcast input. + // Cast it to a string. We don't need an id here. + argValues.BROADCAST_OPTION.id = null; + argValues.BROADCAST_OPTION.name = cast.toString(inputValue); + } else { + argValues[inputName] = inputValue; + } + + i += 1; + } + + currentStackFrame.reporting = null; + currentStackFrame.reported = null; + } + + const start = i; + + for (; i < length; i++) { + const lastOperation = i === length - 1; + const opCached = ops[i]; + + const blockFunction = opCached._blockFunction; + + // Update values for arguments (inputs). + const argValues = opCached._argValues; + + // Fields are set during opCached initialization. + + // Blocks should glow when a script is starting, + // not after it has finished (see #1404). + // Only blocks in blockContainers that don't forceNoGlow + // should request a glow. + if (!blockContainer.forceNoGlow) { + thread.requestScriptGlowInFrame = true; + } + + // Inputs are set during previous steps in the loop. + + const primitiveReportedValue = blockFunction(argValues, blockUtility); + + // If it's a promise, wait until promise resolves. + if (isPromise(primitiveReportedValue)) { + handlePromise(primitiveReportedValue, sequencer, thread, opCached, lastOperation); + + // Store the already reported values. They will be thawed into the + // future versions of the same operations by block id. The reporting + // operation if it is promise waiting will set its parent value at + // that time. + thread.justReported = null; + currentStackFrame.reporting = ops[i].id; + currentStackFrame.reported = ops.slice(0, i).map(reportedCached => { + const inputName = reportedCached._parentKey; + const reportedValues = reportedCached._parentValues; + + if (inputName === 'BROADCAST_INPUT') { + return { + opCached: reportedCached.id, + inputValue: reportedValues[inputName].BROADCAST_OPTION.name + }; + } + return { + opCached: reportedCached.id, + inputValue: reportedValues[inputName] + }; + }); + + // We are waiting for a promise. Stop running this set of operations + // and continue them later after thawing the reported values. + break; + } else if (thread.status === Thread.STATUS_RUNNING) { + if (lastOperation) { + handleReport(primitiveReportedValue, sequencer, thread, opCached, lastOperation); + } else { + // By definition a block that is not last in the list has a + // parent. + const inputName = opCached._parentKey; + const parentValues = opCached._parentValues; + + if (inputName === 'BROADCAST_INPUT') { + // Something is plugged into the broadcast input. + // Cast it to a string. We don't need an id here. + parentValues.BROADCAST_OPTION.id = null; + parentValues.BROADCAST_OPTION.name = cast.toString(primitiveReportedValue); + } else { + parentValues[inputName] = primitiveReportedValue; + } + } + } else if (thread.status === Thread.STATUS_DONE) { + // Nothing else to execute. + break; + } + } + + if (runtime.profiler !== null) { + if (blockCached._profiler !== runtime.profiler) { + _prepareBlockProfiling(runtime.profiler, blockCached); + } + // Determine the index that is after the last executed block. `i` is + // currently the block that was just executed. `i + 1` will be the block + // after that. `length` with the min call makes sure we don't try to + // reference an operation outside of the set of operations. + const end = Math.min(i + 1, length); + for (let p = start; p < end; p++) { + ops[p]._profilerFrame.count += 1; + } + } +}; + +module.exports = execute; diff --git a/local-scratch-vm/src/engine/monitor-record.js b/local-scratch-vm/src/engine/monitor-record.js new file mode 100644 index 0000000000000000000000000000000000000000..1259d5252907b9bfccc63f5dd4e5e8696ba5d55e --- /dev/null +++ b/local-scratch-vm/src/engine/monitor-record.js @@ -0,0 +1,23 @@ +const {Record} = require('immutable'); + +const MonitorRecord = Record({ + id: null, // Block Id + /** 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, + mode: 'default', + sliderMin: 0, + sliderMax: 100, + isDiscrete: true, + x: null, // (x: null, y: null) Indicates that the monitor should be auto-positioned + y: null, + width: 0, + height: 0, + visible: true +}); + +module.exports = MonitorRecord; diff --git a/local-scratch-vm/src/engine/mutation-adapter.js b/local-scratch-vm/src/engine/mutation-adapter.js new file mode 100644 index 0000000000000000000000000000000000000000..63e11c9755af8decdeb4d3c1fee4a97e464f5740 --- /dev/null +++ b/local-scratch-vm/src/engine/mutation-adapter.js @@ -0,0 +1,64 @@ +/** + * 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) { + if (dom.tagName === '#text') return dom.textContent + const parseChildren = (obj, dom) => { + for (let i = 0; i < dom.children.length; i++) { + obj.children.push( + mutatorTagToObject(dom.children[i]) + ); + } + return obj.children[0]; + }; + let obj = Object.create(null); + obj.tagName = dom.tagName; + obj.children = []; + if (!dom.tagName) { + console.warn('invalid dom; skiping to reading children'); + obj = parseChildren(obj, dom); + return obj; + } + for (let idx = 0; idx < dom.attributes.length; idx++) { + const attrib = dom.attributes[idx]; + const attribName = attrib.name; + if (attribName === 'xmlns') continue; + obj[attribName] = attrib.value; + // Note: the capitalization of block info in the following lines is important. + // The lowercase is read in from xml which normalizes case. The VM uses camel case everywhere else. + if (attribName === 'blockinfo') { + obj.blockInfo = JSON.parse(obj.blockinfo); + delete obj.blockinfo; + } + } + + parseChildren(obj, dom); + 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. + */ +const mutationAdpater = function (mutation) { + let mutationParsed; + // Check if the mutation is already parsed; if not, parse it. + if (typeof mutation === 'object') { + mutationParsed = mutation; + } else { + const parser = new DOMParser(); + const doc = parser.parseFromString(mutation, "application/xml"); + mutationParsed = doc; + if (mutationParsed.nodeName === '#document') { + mutationParsed = mutationParsed.children[0]; + } + } + + return mutatorTagToObject(mutationParsed); +}; + +module.exports = mutationAdpater; diff --git a/local-scratch-vm/src/engine/profiler.js b/local-scratch-vm/src/engine/profiler.js new file mode 100644 index 0000000000000000000000000000000000000000..bfb47e617584a888df29badaf914315e9b5731c5 --- /dev/null +++ b/local-scratch-vm/src/engine/profiler.js @@ -0,0 +1,390 @@ +/** + * @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.} + */ +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; + + /** + * A summarized count of the number of calls to this frame. + * @type {number} + */ + this.count = 0; + } +} + +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 = []; + + /** + * An array of frames incremented on demand instead as part of start + * and stop. + * @type {Array.} + */ + this.increments = []; + + /** + * An array of profiler frames separated by counter argument. Generally + * for Scratch these frames are separated by block function opcode. + * This tracks each time an opcode is called. + * @type {Array.} + */ + this.counters = []; + + /** + * A frame with no id or argument. + * @type {ProfilerFrame} + */ + this.nullFrame = new ProfilerFrame(-1); + + /** + * A cache of ProfilerFrames to reuse when reporting the recorded + * frames in records. + * @type {Array.} + */ + 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()); + } + + /** + * Increment the number of times this symbol is called. + * @param {number} id The id returned by idByName for a name symbol. + */ + increment (id) { + if (!this.increments[id]) { + this.increments[id] = new ProfilerFrame(-1); + this.increments[id].id = id; + } + this.increments[id].count += 1; + } + + /** + * Find or create a ProfilerFrame-like object whose counter can be + * incremented outside of the Profiler. + * @param {number} id The id returned by idByName for a name symbol. + * @param {*} arg The argument for a frame that identifies it in addition + * to the id. + * @return {{count: number}} A ProfilerFrame-like whose count should be + * incremented for each call. + */ + frame (id, arg) { + for (let i = 0; i < this.counters.length; i++) { + if (this.counters[i].id === id && this.counters[i].arg === arg) { + return this.counters[i]; + } + } + + const newCounter = new ProfilerFrame(-1); + newCounter.id = id; + newCounter.arg = arg; + this.counters.push(newCounter); + return newCounter; + } + + /** + * 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 frame occured once. + frame.count = 1; + + this.onFrame(frame); + + i += STOP_SIZE; + } else { + this.records.length = 0; + throw new Error('Unable to decode Profiler records.'); + } + } + + for (let j = 0; j < this.increments.length; j++) { + if (this.increments[j] && this.increments[j].count > 0) { + this.onFrame(this.increments[j]); + this.increments[j].count = 0; + } + } + + for (let k = 0; k < this.counters.length; k++) { + if (this.counters[k].count > 0) { + this.onFrame(this.counters[k]); + this.counters[k].count = 0; + } + } + + 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; diff --git a/local-scratch-vm/src/engine/runtime.js b/local-scratch-vm/src/engine/runtime.js new file mode 100644 index 0000000000000000000000000000000000000000..fcbcf535e46cac8ccd7290a512eb407d0b7f7d40 --- /dev/null +++ b/local-scratch-vm/src/engine/runtime.js @@ -0,0 +1,3975 @@ +const EventEmitter = require('events'); +const {OrderedMap} = require('immutable'); +const ExtendedJSON = require('@turbowarp/json'); + +const ArgumentType = require('../extension-support/argument-type'); +const Blocks = require('./blocks'); +const BlocksRuntimeCache = require('./blocks-runtime-cache'); +const BlockType = require('../extension-support/block-type'); +const BlockShape = require('../extension-support/block-shape'); +const Profiler = require('./profiler'); +const Sequencer = require('./sequencer'); +const execute = require('./execute.js'); +const compilerExecute = require('../compiler/jsexecute'); +const ScratchBlocksConstants = require('./scratch-blocks-constants'); +const TargetType = require('../extension-support/target-type'); +const Thread = require('./thread'); +const log = require('../util/log'); +const maybeFormatMessage = require('../util/maybe-format-message'); +const StageLayering = require('./stage-layering'); +const Variable = require('./variable'); +const xmlEscape = require('../util/xml-escape'); +const ScratchLinkWebSocket = require('../util/scratch-link-websocket'); +const FontManager = require('./tw-font-manager'); +const { validateJSON } = require('../util/json-block-utilities'); +const Color = require('../util/color'); +const TabManager = require('../extension-support/pm-tab-manager'); +const ModalManager = require('../extension-support/pm-modal-manager'); +const MathUtil = require('../util/math-util'); + +// Virtual I/O devices. +const Clock = require('../io/clock'); +const Cloud = require('../io/cloud'); +const Keyboard = require('../io/keyboard'); +const Mouse = require('../io/mouse'); +const MouseWheel = require('../io/mouseWheel'); +const UserData = require('../io/userData'); +const Video = require('../io/video'); +const Touch = require('../io/touch'); + +const StringUtil = require('../util/string-util'); +const uid = require('../util/uid'); + +const coreVariableTypes = [ + Variable.SCALAR_TYPE, + Variable.LIST_TYPE, + Variable.BROADCAST_MESSAGE_TYPE +]; +const defaultBlockPackages = { + scratch3_control: require('../blocks/scratch3_control'), + scratch3_event: require('../blocks/scratch3_event'), + scratch3_looks: require('../blocks/scratch3_looks'), + scratch3_motion: require('../blocks/scratch3_motion'), + scratch3_operators: require('../blocks/scratch3_operators'), + scratch3_sound: require('../blocks/scratch3_sound'), + scratch3_sensing: require('../blocks/scratch3_sensing'), + scratch3_data: require('../blocks/scratch3_data'), + scratch3_procedures: require('../blocks/scratch3_procedures'), + pm_liveTests: require('../blocks/pm_live tests') +}; + +const interpolate = require('./tw-interpolate'); +const FrameLoop = require('./tw-frame-loop'); + +const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69']; + +const COMMENT_CONFIG_MAGIC = ' // _twconfig_'; + +/** + * Information used for converting Scratch argument types into scratch-blocks data. + * @type {object.} + */ +const ArgumentTypeMap = (() => { + const map = {}; + map[ArgumentType.ANGLE] = { + shadow: { + type: 'math_angle', + // We specify fieldNames here so that we can pick + // create and populate a field with the defaultValue + // specified in the extension. + // When the `fieldName` property is not specified, + // the will be left out of the XML and + // the scratch-blocks defaults for that field will be + // used instead (e.g. default of 0 for number fields) + fieldName: 'NUM' + } + }; + map[ArgumentType.COLOR] = { + shadow: { + type: 'colour_picker', + fieldName: 'COLOUR' + } + }; + map[ArgumentType.NUMBER] = { + shadow: { + type: 'math_number', + fieldName: 'NUM' + } + }; + map[ArgumentType.STRING] = { + shadow: { + type: 'text', + fieldName: 'TEXT' + } + }; + map[ArgumentType.BOOLEAN] = { + check: 'Boolean' + }; + map[ArgumentType.MATRIX] = { + shadow: { + type: 'matrix', + fieldName: 'MATRIX' + } + }; + map[ArgumentType.NOTE] = { + shadow: { + type: 'note', + fieldName: 'NOTE' + } + }; + map[ArgumentType.IMAGE] = { + // Inline images are weird because they're not actually "arguments". + // They are more analagous to the label on a block. + fieldType: 'field_image' + }; + map[ArgumentType.POLYGON] = { + check: 'math_polygon', + shadow: { + type: 'polygon' + } + }; + map[ArgumentType.COSTUME] = { + shadow: { + type: 'looks_costume', + fieldName: 'COSTUME' + } + }; + map[ArgumentType.SOUND] = { + shadow: { + type: 'sound_sounds_menu', + fieldName: 'SOUND_MENU' + } + }; + // VARIABLE, LIST and BROADCAST are actually fields + // they'll be handled similarly to IMAGE + map[ArgumentType.VARIABLE] = { + fieldType: "field_variable", + fieldName: "VARIABLE" + }; + map[ArgumentType.LIST] = { + fieldType: "field_variable", + fieldName: "LIST", + variableType: 'list' + }; + map[ArgumentType.BROADCAST] = { + fieldType: "field_variable", + fieldName: "BROADCAST", + variableType: 'broadcast_msg' + }; + map[ArgumentType.SEPERATOR] = { + fieldType: 'field_vertical_separator' + }; + + return map; +})(); + +/** + * A pair of functions used to manage the cloud variable limit, + * to be used when adding (or attempting to add) or removing a cloud variable. + * @typedef {object} CloudDataManager + * @property {function} canAddCloudVariable A function to call to check that + * a cloud variable can be added. + * @property {function} addCloudVariable A function to call to track a new + * cloud variable on the runtime. + * @property {function} removeCloudVariable A function to call when + * removing an existing cloud variable. + * @property {function} hasCloudVariables A function to call to check that + * the runtime has any cloud variables. + * @property {function} getNumberOfCloudVariables A function that returns the + * number of cloud variables in the project. + */ + +/** + * Creates and manages cloud variable limit in a project, + * and returns two functions to be used to add a new + * cloud variable (while checking that it can be added) + * and remove an existing cloud variable. + * These are to be called whenever attempting to create or delete + * a cloud variable. + * @param {Object} cloudOptions + * @param {number} cloudOptions.limit Maximum number of cloud variables + * @return {CloudDataManager} The functions to be used when adding or removing a + * cloud variable. + */ +const cloudDataManager = cloudOptions => { + let count = 0; + + const canAddCloudVariable = () => count < cloudOptions.limit; + + const addCloudVariable = () => { + count++; + }; + + const removeCloudVariable = () => { + count--; + }; + + const hasCloudVariables = () => count > 0; + + const getNumberOfCloudVariables = () => count; + + return { + canAddCloudVariable, + addCloudVariable, + removeCloudVariable, + hasCloudVariables, + getNumberOfCloudVariables + }; +}; + +/** + * Numeric ID for Runtime._step in Profiler instances. + * @type {number} + */ +let stepProfilerId = -1; + +/** + * Numeric ID for Sequencer.stepThreads in Profiler instances. + * @type {number} + */ +let stepThreadsProfilerId = -1; + +/** + * Numeric ID for RenderWebGL.draw in Profiler instances. + * @type {number} + */ +let rendererDrawProfilerId = -1; + +/** + * Manages targets, scripts, and the sequencer. + * @constructor + */ +class Runtime extends EventEmitter { + constructor () { + super(); + + /** + * Target management and storage. + * @type {Array.} + */ + this.targets = []; + + /** + * Targets in reverse order of execution. Shares its order with drawables. + * @type {Array.} + */ + this.executableTargets = []; + + /** + * A list of threads that are currently running in the VM. + * Threads are added when execution starts and pruned when execution ends. + * @type {Array.} + */ + this.threads = []; + + this.threadMap = new Map(); + + /** @type {!Sequencer} */ + this.sequencer = new Sequencer(this); + + /** + * Storage container for flyout blocks. + * These will execute on `_editingTarget.` + * @type {!Blocks} + */ + this.flyoutBlocks = new Blocks(this, true /* force no glow */); + + /** + * Storage container for monitor blocks. + * These will execute on a target maybe + * @type {!Blocks} + */ + this.monitorBlocks = new Blocks(this, true /* force no glow */); + + /** + * Currently known editing target for the VM. + * @type {?Target} + */ + this._editingTarget = null; + + /** + * Map to look up a block primitive's implementation function by its opcode. + * This is a two-step lookup: package name first, then primitive name. + * @type {Object.} + */ + this._primitives = {}; + + /** + * Map to look up all block information by extended opcode. + * @type {Array.} + * @private + */ + this._blockInfo = []; + + /** + * Map to look up hat blocks' metadata. + * Keys are opcode for hat, values are metadata objects. + * @type {Object.} + */ + this._hats = {}; + + /** + * A list of script block IDs that were glowing during the previous frame. + * @type {!Array.} + */ + this._scriptGlowsPreviousFrame = []; + + /** + * Number of non-monitor threads running during the previous frame. + * @type {number} + */ + this._nonMonitorThreadCount = 0; + + /** + * All threads that finished running and were removed from this.threads + * by behaviour in Sequencer.stepThreads. + * @type {Array} + */ + this._lastStepDoneThreads = null; + + /** + * pm: The current tab manager for this runtime. + */ + this.tabManager = new TabManager(this); + + /** + * pm: The current modal manager for this runtime. + */ + this.modalManager = new ModalManager(this); + + /** + * Currently known number of clones, used to enforce clone limit. + * @type {number} + */ + this._cloneCounter = 0; + + /** + * Flag to emit a targets update at the end of a step. When target data + * changes, this flag is set to true. + * @type {boolean} + */ + this._refreshTargets = false; + + /** + * Map to look up all monitor block information by opcode. + * @type {object} + * @private + */ + this.monitorBlockInfo = {}; + + /** + * Ordered map of all monitors, which are MonitorReporter objects. + */ + this._monitorState = OrderedMap({}); + + /** + * Monitor state from last tick + */ + this._prevMonitorState = OrderedMap({}); + + /** + * Whether the project is in "turbo mode." + * @type {Boolean} + */ + this.turboMode = false; + + /** + * tw: Responsible for managing the VM's many timers. + */ + this.frameLoop = new FrameLoop(this); + + /** + * Current length of a step. + * Changes as mode switches, and used by the sequencer to calculate + * WORK_TIME. + * @type {!number} + */ + this.currentStepTime = 1000 / 30; + + // Set an intial value for this.currentMSecs + this.updateCurrentMSecs(); + + /** + * Whether any primitive has requested a redraw. + * Affects whether `Sequencer.stepThreads` will yield + * after stepping each thread. + * Reset on every frame. + * @type {boolean} + */ + this.redrawRequested = false; + + // Register all given block packages. + this._registerBlockPackages(); + + // Register and initialize "IO devices", containers for processing + // I/O related data. + /** @type {Object.} */ + this.ioDevices = { + clock: new Clock(this), + cloud: new Cloud(this), + keyboard: new Keyboard(this), + mouse: new Mouse(this), + mouseWheel: new MouseWheel(this), + userData: new UserData(), + video: new Video(this), + touch: new Touch(this) + }; + + /** + * A list of extensions, used to manage hardware connection. + */ + this.peripheralExtensions = {}; + + /** + * A runtime profiler that records timed events for later playback to + * diagnose Scratch performance. + * @type {Profiler} + */ + this.profiler = null; + + this.cloudOptions = { + limit: 10 + }; + + this.extensionRuntimeOptions = { + javascriptUnsandboxed: false + }; + + const newCloudDataManager = cloudDataManager(this.cloudOptions); + + /** + * Check wether the runtime has any cloud data. + * @type {function} + * @return {boolean} Whether or not the runtime currently has any + * cloud variables. + */ + this.hasCloudData = newCloudDataManager.hasCloudVariables; + + /** + * A function which checks whether a new cloud variable can be added + * to the runtime. + * @type {function} + * @return {boolean} Whether or not a new cloud variable can be added + * to the runtime. + */ + this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable; + + /** + * A function which returns the number of cloud variables in the runtime. + * @returns {number} + */ + this.getNumberOfCloudVariables = newCloudDataManager.getNumberOfCloudVariables; + + /** + * A function that tracks a new cloud variable in the runtime, + * updating the cloud variable limit. Calling this function will + * emit a cloud data update event if this is the first cloud variable + * being added. + * @type {function} + */ + this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager); + + /** + * A function which updates the runtime's cloud variable limit + * when removing a cloud variable and emits a cloud update event + * if the last of the cloud variables is being removed. + * @type {function} + */ + this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager); + + /** + * A string representing the origin of the current project from outside of the + * Scratch community, such as CSFirst. + * @type {?string} + */ + this.origin = null; + + this._stageTarget = null; + + this.addonBlocks = {}; + + this.stageWidth = Runtime.STAGE_WIDTH; + this.stageHeight = Runtime.STAGE_HEIGHT; + + this.runtimeOptions = { + maxClones: Runtime.MAX_CLONES, + miscLimits: true, + fencing: true, + dangerousOptimizations: false, + disableOffscreenRendering: false + }; + + this.compilerOptions = { + enabled: true, + warpTimer: false + }; + + this.optimizationUtil = { + sin: new Array(360), + cos: new Array(360) + }; + for (let i = 0; i < 360; i++) { + this.optimizationUtil.sin[i] = Math.round(Math.sin((Math.PI * i) / 180) * 1e10) / 1e10; + this.optimizationUtil.cos[i] = Math.round(Math.cos((Math.PI * i) / 180) * 1e10) / 1e10; + } + + this.debug = false; + + this._lastStepTime = Date.now(); + this.interpolationEnabled = false; + this.interpolate = interpolate; + + this._defaultStoredSettings = this._generateAllProjectOptions(); + + /** + * TW: We support a "packaged runtime" mode. This can be used when: + * - there will never be an editor attached such as scratch-gui or scratch-blocks + * - the project will never be exported with saveProjectSb3() + * - original costume and sound data is not needed + * In this mode, the runtime is able to discard large amounts of data and avoid some processing + * to make projects load faster and use less memory. + * This is not designed to protect projects from copying as someone can still copy the data that + * gets fed into the runtime in the first place. + * This mode is used by the PenguinMod Packager and the TurboWarp Packager. + */ + this.isPackaged = false; + + /** + * PM: In the packager, the Project Permission Manager can be disabled. + * This option is used by the PenguinMod Packager. + */ + this.isProjectPermissionManagerDisabled = false; + + /** + * Contains information about the external communication methods that the scripts inside the project + * can use to send data from inside the project to an external server. + * Do not update this directly. Use Runtime.setExternalCommunicationMethod() instead. + */ + this.externalCommunicationMethods = { + cloudVariables: false, + customExtensions: false + }; + this.on(Runtime.HAS_CLOUD_DATA_UPDATE, enabled => { + this.setExternalCommunicationMethod('cloudVariables', enabled); + }); + + // pm: remove listener warning + this.setMaxListeners(50); + + /** + * If set to true, features such as reading colors from the user's webcam will be disabled + * when the project has access to any external communication method to protect user privacy. + * Requires TurboWarp/scratch-render. + * Do not update this directly. Use Runtime.setEnforcePrivacy() instead. + */ + this.enforcePrivacy = true; + + /** + * Internal map of opaque identifiers to the callback to run that function. + * @type {Map} + */ + this.extensionButtons = new Map(); + + /** + * Contains the audio context and gain node for each extension that registers them. + * Used to make sure the extensions respect addons or the pause button. + * @type {Map} + */ + this._extensionAudioObjects = new Map(); + + /** + * Responsible for managing custom fonts. + */ + this.fontManager = new FontManager(this); + + this.cameraStates = { + default: { + pos: [0, 0], + dir: 0, + scale: 1 + } + }; + + // list of variable types declared by extensions + this._extensionVariables = {}; + // lists all custom serializers + this.serializers = { + 'pm-rendered-target': { + serialize: target => ({ id: target.id, name: target.getName() }), + deserialize: ({ id, name }) => this.getTargetById(id) ?? this.getSpriteTargetByName(name) + }, + 'pm-costume-asset': { + serialize: asset => ({ id: asset.assetId, name: asset.name }), + deserialize: ({ assetId, name }) => { + for (let i = 0; i < this.targets.length; i++) { + const assets = this.targets[i].getCostumes(); + const found = assets.find(asset => asset.assetId === assetId || asset.name === name); + if (found) return found; + } + } + }, + 'pm-sound-asset': { + serialize: asset => ({ id: asset.assetId, name: asset.name }), + deserialize: ({ assetId, name }) => { + for (let i = 0; i < this.targets.length; i++) { + const assets = this.targets[i].getSounds(); + const found = assets.find(asset => asset.assetId === assetId || asset.name === name); + if (found) return found; + } + } + } + }; + + /** + * An object to contain runtime variables from the + * LilyMakesThings Thread Variables extension + * @type {Object} + */ + this.variables = Object.create(null); + } + + /** + * Width of the stage, in pixels. + * @const {number} + */ + static get STAGE_WIDTH () { + // tw: stage size is set per-runtime, this is only the initial value + return 480; + } + + /** + * Height of the stage, in pixels. + * @const {number} + */ + static get STAGE_HEIGHT () { + // tw: stage size is set per-runtime, this is only the initial value + return 360; + } + + /** + * Event name for glowing a script. + * @const {string} + */ + static get SCRIPT_GLOW_ON () { + return 'SCRIPT_GLOW_ON'; + } + + /** + * Event name for unglowing a script. + * @const {string} + */ + static get SCRIPT_GLOW_OFF () { + return 'SCRIPT_GLOW_OFF'; + } + + /** + * Event name for glowing a block. + * @const {string} + */ + static get BLOCK_GLOW_ON () { + return 'BLOCK_GLOW_ON'; + } + + /** + * Event name for unglowing a block. + * @const {string} + */ + static get BLOCK_GLOW_OFF () { + return 'BLOCK_GLOW_OFF'; + } + + /** + * Event name for a cloud data update + * to this project. + * @const {string} + */ + static get HAS_CLOUD_DATA_UPDATE () { + return 'HAS_CLOUD_DATA_UPDATE'; + } + + /** + * Event name for turning on turbo mode. + * @const {string} + */ + static get TURBO_MODE_ON () { + return 'TURBO_MODE_ON'; + } + + /** + * Event name for turning off turbo mode. + * @const {string} + */ + static get TURBO_MODE_OFF () { + return 'TURBO_MODE_OFF'; + } + + /** + * Event name for runtime options changing. + * @const {string} + */ + static get RUNTIME_OPTIONS_CHANGED () { + return 'RUNTIME_OPTIONS_CHANGED'; + } + + /** + * Event name for compiler options changing. + * @const {string} + */ + static get COMPILER_OPTIONS_CHANGED () { + return 'COMPILER_OPTIONS_CHANGED'; + } + + /** + * Event name for framerate changing. + * @const {string} + */ + static get FRAMERATE_CHANGED () { + return 'FRAMERATE_CHANGED'; + } + + /** + * Event name for interpolation changing. + * @const {string} + */ + static get INTERPOLATION_CHANGED () { + return 'INTERPOLATION_CHANGED'; + } + + /** + * Event called before interpolation data is set. + */ + static get BEFORE_INTERPOLATE () { + return 'BEFORE_INTERPOLATE'; + } + + /** + * Event called after interpolation data is set. + */ + static get AFTER_INTERPOLATE () { + return 'AFTER_INTERPOLATE'; + } + + /** + * Event called before any block is executed. + */ + static get BEFORE_EXECUTE () { + return 'BEFORE_EXECUTE'; + } + + /** + * Event called after every block in the project has been executed. + */ + static get AFTER_EXECUTE () { + return 'AFTER_EXECUTE'; + } + + /** + * Event name for stage size changing. + * @const {string} + */ + static get STAGE_SIZE_CHANGED () { + return 'STAGE_SIZE_CHANGED'; + } + + /** + * Event name when the mouse is scrolled + * @const {string} + */ + static get MOUSE_SCROLLED () { + return 'MOUSE_SCROLLED'; + } + + /** + * Event name for compiler errors. + * @const {string} + */ + static get COMPILE_ERROR () { + return 'COMPILE_ERROR'; + } + + /** + * Event name when the project is started (threads may not necessarily be + * running). + * @const {string} + */ + static get PROJECT_START () { + return 'PROJECT_START'; + } + + /** + * Event name when the project is started (but before it runs stopAll) + * @const {string} + */ + static get PROJECT_START_BEFORE_RESET () { + return 'PROJECT_START_BEFORE_RESET'; + } + + /** + * Event name when threads start running. + * Used by the UI to indicate running status. + * @const {string} + */ + static get PROJECT_RUN_START () { + return 'PROJECT_RUN_START'; + } + + /** + * Event name when threads stop running + * Used by the UI to indicate not-running status. + * @const {string} + */ + static get PROJECT_RUN_STOP () { + return 'PROJECT_RUN_STOP'; + } + + /** + * Event name for project being stopped or restarted by the user. + * Used by blocks that need to reset state. + * @const {string} + */ + static get PROJECT_STOP_ALL () { + return 'PROJECT_STOP_ALL'; + } + + /** + * Event name for target being stopped by a stop for target call. + * Used by blocks that need to stop individual targets. + * @const {string} + */ + static get STOP_FOR_TARGET () { + return 'STOP_FOR_TARGET'; + } + + /** + * Event name for visual value report. + * @const {string} + */ + static get VISUAL_REPORT () { + return 'VISUAL_REPORT'; + } + + /** + * Event name for when a block errors. + * @const {string} + */ + static get BLOCK_STACK_ERROR () { + return 'BLOCK_STACK_ERROR'; + } + + /** + * Event name for project loaded report. + * @const {string} + */ + static get PROJECT_LOADED () { + return 'PROJECT_LOADED'; + } + + /** + * Event name for report that a change was made that can be saved + * @const {string} + */ + static get PROJECT_CHANGED () { + return 'PROJECT_CHANGED'; + } + + /** + * Event name for report that a change was made to an extension in the toolbox. + * @const {string} + */ + static get TOOLBOX_EXTENSIONS_NEED_UPDATE () { + return 'TOOLBOX_EXTENSIONS_NEED_UPDATE'; + } + + /** + * Event name for targets update report. + * @const {string} + */ + static get TARGETS_UPDATE () { + return 'TARGETS_UPDATE'; + } + + /** + * Event name for monitors update. + * @const {string} + */ + static get MONITORS_UPDATE () { + return 'MONITORS_UPDATE'; + } + + /** + * Event name for block drag update. + * @const {string} + */ + static get BLOCK_DRAG_UPDATE () { + return 'BLOCK_DRAG_UPDATE'; + } + + /** + * Event name for block drag end. + * @const {string} + */ + static get BLOCK_DRAG_END () { + return 'BLOCK_DRAG_END'; + } + + /** + * Event name for reporting that an extension was added. + * @const {string} + */ + static get EXTENSION_ADDED () { + return 'EXTENSION_ADDED'; + } + + /** + * Event name for reporting that an extension was removed + * @const {string} + */ + static get EXTENSION_REMOVED () { + return 'EXTENSION_REMOVED'; + } + + /** + * Event name for reporting that an extension as asked for a custom field to be added + * @const {string} + */ + static get EXTENSION_FIELD_ADDED () { + return 'EXTENSION_FIELD_ADDED'; + } + + /** + * Event name for updating the available set of peripheral devices. + * This causes the peripheral connection modal to update a list of + * available peripherals. + * @const {string} + */ + static get PERIPHERAL_LIST_UPDATE () { + return 'PERIPHERAL_LIST_UPDATE'; + } + + /** + * Event name for when the user picks a bluetooth device to connect to + * via Companion Device Manager (CDM) + * @const {string} + */ + static get USER_PICKED_PERIPHERAL () { + return 'USER_PICKED_PERIPHERAL'; + } + + /** + * Event name for reporting that a peripheral has connected. + * This causes the status button in the blocks menu to indicate 'connected'. + * @const {string} + */ + static get PERIPHERAL_CONNECTED () { + return 'PERIPHERAL_CONNECTED'; + } + + /** + * Event name for reporting that a peripheral has been intentionally disconnected. + * This causes the status button in the blocks menu to indicate 'disconnected'. + * @const {string} + */ + static get PERIPHERAL_DISCONNECTED () { + return 'PERIPHERAL_DISCONNECTED'; + } + + /** + * Event name for reporting that a peripheral has encountered a request error. + * This causes the peripheral connection modal to switch to an error state. + * @const {string} + */ + static get PERIPHERAL_REQUEST_ERROR () { + return 'PERIPHERAL_REQUEST_ERROR'; + } + + /** + * Event name for reporting that a peripheral connection has been lost. + * This causes a 'peripheral connection lost' error alert to display. + * @const {string} + */ + static get PERIPHERAL_CONNECTION_LOST_ERROR () { + return 'PERIPHERAL_CONNECTION_LOST_ERROR'; + } + + /** + * Event name for reporting that a peripheral has not been discovered. + * This causes the peripheral connection modal to show a timeout state. + * @const {string} + */ + static get PERIPHERAL_SCAN_TIMEOUT () { + return 'PERIPHERAL_SCAN_TIMEOUT'; + } + + /** + * Event name to indicate that the microphone is being used to stream audio. + * @const {string} + */ + static get MIC_LISTENING () { + return 'MIC_LISTENING'; + } + + /** + * Event name for reporting that blocksInfo was updated. + * @const {string} + */ + static get BLOCKSINFO_UPDATE () { + return 'BLOCKSINFO_UPDATE'; + } + + /** + * Event name when the runtime tick loop has been started. + * @const {string} + */ + static get RUNTIME_STARTED () { + return 'RUNTIME_STARTED'; + } + + /** + * Event name when the runtime tick loop has been stopped. + * @const {string} + */ + static get RUNTIME_STOPPED () { + return 'RUNTIME_STOPPED'; + } + + /** + * Event name when the runtime is paused temporarily. + * @const {string} + */ + static get RUNTIME_PAUSED () { + return 'RUNTIME_PAUSED'; + } + + /** + * Event name when the runtime is about to be paused temporarily. + * Fires before runtime.paused = true. + * @const {string} + */ + static get RUNTIME_PRE_PAUSED () { + return 'RUNTIME_PRE_PAUSED'; + } + + /** + * Event name when the runtime is unpaused. + * @const {string} + */ + static get RUNTIME_UNPAUSED () { + return 'RUNTIME_UNPAUSED'; + } + + /** + * Event name when the runtime dispose has been called. + * @const {string} + */ + static get RUNTIME_DISPOSED () { + return 'RUNTIME_DISPOSED'; + } + + /** + * Event name when _step() has been called. + * @const {string} + */ + static get RUNTIME_STEP_START () { + return 'RUNTIME_STEP_START'; + } + + /** + * Event name when _step() has finished all processing within the function. + * @const {string} + */ + static get RUNTIME_STEP_END () { + return 'RUNTIME_STEP_END'; + } + + /** + * Event name when an editor tab is created. + * @const {string} + */ + static get EDITOR_TABS_NEW () { + return 'EDITOR_TABS_NEW'; + } + + /** + * Event name when editor tabs need to be updated. + * @const {string} + */ + static get EDITOR_TABS_UPDATE () { + return 'EDITOR_TABS_UPDATE'; + } + + /** + * Event name for reporting that a block was updated and needs to be rerendered. + * @const {string} + */ + static get BLOCKS_NEED_UPDATE () { + return 'BLOCKS_NEED_UPDATE'; + } + + /** + * Event name for camera movements. + * @const {string} + */ + static get CAMERA_CHANGED () { + return 'CAMERA_CHANGED'; + } + + /** + * Event name for the starting of hats. + * @const {string} + */ + + static get HATS_STARTED () { + return 'HATS_STARTED' + } + + /** + * How rapidly we try to step threads by default, in ms. + */ + static get THREAD_STEP_INTERVAL () { + // tw: not used, only exists for compatibility + return 1000 / 60; + } + + /** + * In compatibility mode, how rapidly we try to step threads, in ms. + */ + static get THREAD_STEP_INTERVAL_COMPATIBILITY () { + // tw: not used, only exists for compatibility + return 1000 / 30; + } + + /** + * How many clones can be created at a time. + * @const {number} + */ + static get MAX_CLONES () { + // tw: clone limit is set per-runtime in runtimeOptions, this is only the initial value + return 300; + } + + // ----------------------------------------------------------------------------- + // ----------------------------------------------------------------------------- + + // Helper function for initializing the addCloudVariable function + _initializeAddCloudVariable (newCloudDataManager) { + // The addCloudVariable function + return (() => { + const hadCloudVarsBefore = this.hasCloudData(); + newCloudDataManager.addCloudVariable(); + if (!hadCloudVarsBefore && this.hasCloudData()) { + this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, true); + } + }); + } + + // Helper function for initializing the removeCloudVariable function + _initializeRemoveCloudVariable (newCloudDataManager) { + return (() => { + const hadCloudVarsBefore = this.hasCloudData(); + newCloudDataManager.removeCloudVariable(); + if (hadCloudVarsBefore && !this.hasCloudData()) { + this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, false); + } + }); + } + + /** + * Register default block packages with this runtime. + * @todo Prefix opcodes with package name. + * @private + */ + _registerBlockPackages () { + for (const packageName in defaultBlockPackages) { + if (defaultBlockPackages.hasOwnProperty(packageName)) { + // @todo pass a different runtime depending on package privilege? + const packageObject = new (defaultBlockPackages[packageName])(this); + // Collect primitives from package. + if (packageObject.getPrimitives) { + const packagePrimitives = packageObject.getPrimitives(); + for (const op in packagePrimitives) { + if (packagePrimitives.hasOwnProperty(op)) { + this._primitives[op] = + packagePrimitives[op].bind(packageObject); + } + } + } + // Collect hat metadata from package. + if (packageObject.getHats) { + const packageHats = packageObject.getHats(); + for (const hatName in packageHats) { + if (packageHats.hasOwnProperty(hatName)) { + this._hats[hatName] = packageHats[hatName]; + } + } + } + // Collect monitored from package. + if (packageObject.getMonitored) { + this.monitorBlockInfo = Object.assign({}, this.monitorBlockInfo, packageObject.getMonitored()); + } + + this.compilerRegisterExtension(packageName, packageObject); + } + } + } + + compilerRegisterExtension (name, extensionObject) { + this[`ext_${name}`] = extensionObject; + } + registerCompiledExtensionBlocks (extensionId, information) { + if (!information) return; + if (!information.ir) return; + if (!information.js) return; + + // Used for extension's compiled blocks. + // Importing the generators here avoids circular dependency issues + const JSGenerator = require('../compiler/jsgen'); + const IRGenerator = require('../compiler/irgen'); + + IRGenerator.setExtensionIr(extensionId, information.ir); + JSGenerator.setExtensionJs(extensionId, information.js); + } + + /** + * Allows AudioContexts and GainNodes from an extension to respect addons and runtime pausing by default. + * If audioContext is not supplied, recording addon + pause button will not work with the extension this way. + * If gainNode is not supplied, recording addon + volume slider will not work with the extension this way. + * @param {string} extensionId The extension's ID. May be used internally in the future, or by other extensions. + * @param {AudioContext} audioContext The AudioContext being used in the extension. + * @param {GainNode} gainNode The GainNode that is connected to the AudioContext. All other nodes in the extension should be connected to this GainNode, and this GainNode should be connected to the destination of the AudioContext. + */ + registerExtensionAudioContext(extensionId, audioContext, gainNode) { + if (typeof extensionId !== "string") throw new TypeError('Extension ID must be string'); + if (!extensionId) throw new Error('No extension ID specified'); // empty string + + const obj = {}; + if (audioContext) { + obj.audioContext = audioContext; + } + if (gainNode) { + obj.gainNode = gainNode; + } + this._extensionAudioObjects.set(extensionId, obj); + } + + getMonitorState () { + return this._monitorState; + } + + /** + * Generate an extension-specific menu ID. + * @param {string} menuName - the name of the menu. + * @param {string} extensionId - the ID of the extension hosting the menu. + * @returns {string} - the constructed ID. + * @private + */ + _makeExtensionMenuId (menuName, extensionId) { + return `${extensionId}_menu_${menuName}`; + } + + /** + * Create a context ("args") object for use with `formatMessage` on messages which might be target-specific. + * @param {Target} [target] - the target to use as context. If a target is not provided, default to the current + * editing target or the stage. + */ + makeMessageContextForTarget (target) { + const context = {}; + target = target || this.getEditingTarget() || this.getTargetForStage(); + if (target) { + context.targetType = (target.isStage ? TargetType.STAGE : TargetType.SPRITE); + } + } + + /** + * Register the primitives provided by an extension. + * @param {ExtensionMetadata} extensionInfo - information about the extension (id, blocks, etc.) + * @private + */ + _registerExtensionPrimitives (extensionInfo) { + const categoryInfo = { + id: extensionInfo.id, + name: maybeFormatMessage(extensionInfo.name), + showStatusButton: extensionInfo.showStatusButton, + blockIconURI: extensionInfo.blockIconURI, + menuIconURI: extensionInfo.menuIconURI + }; + + if (extensionInfo.color1) { + const color1 = Color.hexToRgb(extensionInfo.color1); + categoryInfo.color1 = extensionInfo.color1; + categoryInfo.color2 = extensionInfo.color2; + if (!extensionInfo.color2) { + const mixed = Color.mixRgb(color1, Color.RGB_BLACK, 0.1); + categoryInfo.color2 = Color.rgbToHex(mixed); + } + categoryInfo.color3 = extensionInfo.color3; + if (!extensionInfo.color3) { + const mixed = Color.mixRgb(color1, Color.RGB_BLACK, 0.2); + categoryInfo.color3 = Color.rgbToHex(mixed); + } + } else { + categoryInfo.color1 = defaultExtensionColors[0]; + categoryInfo.color2 = defaultExtensionColors[1]; + categoryInfo.color3 = defaultExtensionColors[2]; + } + + if (extensionInfo.isDynamic) { + categoryInfo.isDynamic = extensionInfo.isDynamic; + categoryInfo.orderBlocks = extensionInfo.orderBlocks; + } + + categoryInfo.tbShow = extensionInfo.tbShow || false + + this._blockInfo.push(categoryInfo); + + this._fillExtensionCategory(categoryInfo, extensionInfo); + + for (const fieldTypeName in categoryInfo.customFieldTypes) { + if (extensionInfo.customFieldTypes.hasOwnProperty(fieldTypeName)) { + const fieldTypeInfo = categoryInfo.customFieldTypes[fieldTypeName]; + + // Emit events for custom field types from extension + this.emit(Runtime.EXTENSION_FIELD_ADDED, { + name: `field_${fieldTypeInfo.extendedName}`, + implementation: fieldTypeInfo.fieldImplementation + }); + } + } + + this.emit(Runtime.EXTENSION_ADDED, categoryInfo); + } + + /** + * Reregister the primitives for an extension + * @param {ExtensionMetadata} extensionInfo - new info (results of running getInfo) for an extension + * @private + */ + _refreshExtensionPrimitives (extensionInfo) { + const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id); + if (categoryInfo) { + categoryInfo.name = maybeFormatMessage(extensionInfo.name); + this._fillExtensionCategory(categoryInfo, extensionInfo); + + this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo); + } + } + + _removeExtensionPrimitive(extensionId) { + const extIdx = this._blockInfo.findIndex(ext => ext.id === extensionId); + const info = this._blockInfo[extIdx]; + this._blockInfo.splice(extIdx, 1); + this.emit(Runtime.EXTENSION_REMOVED); + // cleanup blocks + for (const target of this.targets) { + for (const blockId in target.blocks._blocks) { + const {opcode} = target.blocks.getBlock(blockId); + if (info.blocks.find(block => block.json?.type === opcode)) { + target.blocks.deleteBlock(blockId, true); + } + } + } + this.emit(Runtime.BLOCKS_NEED_UPDATE); + } + + /** + * Read extension information, convert menus, blocks and custom field types + * and store the results in the provided category object. + * @param {CategoryInfo} categoryInfo - the category to be filled + * @param {ExtensionMetadata} extensionInfo - the extension metadata to read + * @private + */ + _fillExtensionCategory (categoryInfo, extensionInfo) { + categoryInfo.blocks = []; + categoryInfo.customFieldTypes = {}; + categoryInfo.menus = []; + categoryInfo.menuInfo = {}; + + for (const menuName in extensionInfo.menus) { + if (extensionInfo.menus.hasOwnProperty(menuName)) { + const menuInfo = extensionInfo.menus[menuName]; + const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuInfo, categoryInfo); + categoryInfo.menus.push(convertedMenu); + categoryInfo.menuInfo[menuName] = menuInfo; + } + } + for (const fieldTypeName in extensionInfo.customFieldTypes) { + if (extensionInfo.customFieldTypes.hasOwnProperty(fieldTypeName)) { + const fieldType = extensionInfo.customFieldTypes[fieldTypeName]; + const fieldTypeInfo = this._buildCustomFieldInfo( + fieldTypeName, + fieldType, + extensionInfo.id, + categoryInfo + ); + + categoryInfo.customFieldTypes[fieldTypeName] = fieldTypeInfo; + } + } + + if (extensionInfo.docsURI) { + const xml = '`; + const block = { + info: {}, + xml + }; + categoryInfo.blocks.push(block); + } + + for (const blockInfo of extensionInfo.blocks) { + try { + const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo); + categoryInfo.blocks.push(convertedBlock); + if (convertedBlock.json) { + const opcode = convertedBlock.json.type; + if (blockInfo.blockType !== BlockType.EVENT && blockInfo.blockType !== BlockType.BUTTON) { + this._primitives[opcode] = convertedBlock.info.func; + } + if (blockInfo.blockType === BlockType.EVENT || blockInfo.blockType === BlockType.HAT) { + this._hats[opcode] = { + edgeActivated: blockInfo.isEdgeActivated, + restartExistingThreads: blockInfo.shouldRestartExistingThreads + }; + } + } + } catch (e) { + log.error('Error parsing block: ', {block: blockInfo, error: e}); + } + } + } + + /** + * Convert the given extension menu items into the scratch-blocks style of list of pairs. + * If the menu is dynamic (e.g. the passed in argument is a function), return the input unmodified. + * @param {object} menuItems - an array of menu items or a function to retrieve such an array + * @returns {object} - an array of 2 element arrays or the original input function + * @private + */ + _convertMenuItems (menuItems) { + if (Array.isArray(menuItems)) { + const extensionMessageContext = this.makeMessageContextForTarget(); + return menuItems.map(item => { + const formattedItem = maybeFormatMessage(item, extensionMessageContext); + switch (typeof formattedItem) { + case 'string': + return [formattedItem, formattedItem]; + case 'object': + if (Array.isArray(item)) return item.slice(0, 2); + return [maybeFormatMessage(item.text, extensionMessageContext), item.value]; + default: + throw new Error(`Can't interpret menu item: ${JSON.stringify(item)}`); + } + }); + } + return menuItems; + } + + /** + * Build the scratch-blocks JSON for a menu. Note that scratch-blocks treats menus as a special kind of block. + * @param {string} menuName - the name of the menu + * @param {object} menuInfo - a description of this menu and its items + * @property {*} items - an array of menu items or a function to retrieve such an array + * @property {boolean} [acceptReporters] - if true, allow dropping reporters onto this menu + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {object} - a JSON-esque object ready for scratch-blocks' consumption + * @private + */ + _buildMenuForScratchBlocks (menuName, menuInfo, categoryInfo) { + const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id); + const menuItems = this._convertMenuItems(menuInfo.items); + return { + json: { + message0: '%1', + type: menuId, + inputsInline: true, + output: 'String', + colour: menuInfo.isTypeable + ? '#FFFFFF' + : categoryInfo.color1, + colourSecondary: menuInfo.isTypeable + ? '#FFFFFF' + : categoryInfo.color2, + colourTertiary: menuInfo.isTypeable + ? '#FFFFFF' + : categoryInfo.color3, + outputShape: menuInfo.acceptReporters || menuInfo.isTypeable ? + ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE, + args0: [ + (typeof menuInfo.variableType !== 'undefined' ? + { + type: 'field_variable', + name: menuName, + variableTypes: [menuInfo.variableType === 'scalar' + ? Variable.SCALAR_TYPE + : menuInfo.variableType] + } : (menuInfo.isTypeable ? + { + type: menuInfo.isNumeric + ? 'field_numberdropdown' + : 'field_textdropdown', + name: menuName, + options: menuItems + } : { + type: 'field_dropdown', + name: menuName, + options: menuItems + })) + ] + } + }; + } + + _buildCustomFieldInfo (fieldName, fieldInfo, extensionId, categoryInfo) { + const extendedName = `${extensionId}_${fieldName}`; + return { + fieldName: fieldName, + extendedName: extendedName, + argumentTypeInfo: { + shadow: { + type: extendedName, + fieldName: `field_${extendedName}` + } + }, + scratchBlocksDefinition: this._buildCustomFieldTypeForScratchBlocks( + extendedName, + fieldInfo.output, + fieldInfo.outputShape, + categoryInfo + ), + fieldImplementation: fieldInfo.implementation + }; + } + + /** + * Build the scratch-blocks JSON needed for a fieldType. + * Custom field types need to be namespaced to the extension so that extensions can't interfere with each other + * @param {string} fieldName - The name of the field + * @param {string} output - The output of the field + * @param {number} outputShape - Shape of the field (from ScratchBlocksConstants) + * @param {object} categoryInfo - The category the field belongs to (Used to set its colors) + * @returns {object} - Object to be inserted into scratch-blocks + */ + _buildCustomFieldTypeForScratchBlocks (fieldName, output, outputShape, categoryInfo) { + return { + json: { + type: fieldName, + message0: '%1', + inputsInline: true, + output: output, + colour: categoryInfo.color1, + colourSecondary: categoryInfo.color2, + colourTertiary: categoryInfo.color3, + outputShape: outputShape, + args0: [ + { + name: `field_${fieldName}`, + type: `field_${fieldName}` + } + ] + } + }; + } + + /** + * Convert ExtensionBlockMetadata into data ready for scratch-blocks. + * @param {ExtensionBlockMetadata} blockInfo - the block info to convert + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {ConvertedBlockInfo} - the converted & original block information + * @private + */ + _convertForScratchBlocks (blockInfo, categoryInfo) { + if (blockInfo === '---') { + return this._convertSeparatorForScratchBlocks(blockInfo); + } + + if (blockInfo.blockType === BlockType.LABEL) + return this._convertLabelForScratchBlocks(blockInfo); + + if (blockInfo.blockType === BlockType.BUTTON) { + return this._convertButtonForScratchBlocks(blockInfo); + } + + if (blockInfo.blockType === BlockType.XML) { + return this._convertXmlForScratchBlocks(blockInfo); + } + + return this._convertBlockForScratchBlocks(blockInfo, categoryInfo); + } + + /** + * Convert ExtensionBlockMetadata into scratch-blocks JSON & XML, and generate a proxy function. + * @param {ExtensionBlockMetadata} blockInfo - the block to convert + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {ConvertedBlockInfo} - the converted & original block information + * @private + */ + _convertBlockForScratchBlocks (blockInfo, categoryInfo) { + const extendedOpcode = `${categoryInfo.id}_${blockInfo.opcode}`; + + const blockJSON = { + type: extendedOpcode, + inputsInline: true, + category: categoryInfo.name, + extensions: blockInfo.extensions ?? [], + colour: blockInfo.color1 ?? categoryInfo.color1, + colourSecondary: blockInfo.color2 ?? categoryInfo.color2, + colourTertiary: blockInfo.color3 ?? categoryInfo.color3, + canDragDuplicate: blockInfo.canDragDuplicate === true + }; + const context = { + // TODO: store this somewhere so that we can map args appropriately after translation. + // This maps an arg name to its relative position in the original (usually English) block text. + // When displaying a block in another language we'll need to run a `replace` action similar to the one + // below, but each `[ARG]` will need to be replaced with the number in this map. + argsMap: {}, + blockJSON, + categoryInfo, + blockInfo, + inputList: [] + }; + + // If an icon for the extension exists, prepend it to each block, with a vertical separator. + // We can overspecify an icon for each block, but if no icon exists on a block, fall back to + // the category block icon. + const iconURI = blockInfo.blockIconURI || categoryInfo.blockIconURI; + + if (iconURI) { + if (!blockJSON.extensions.includes('scratch_extension')) { + blockJSON.extensions.push('scratch_extension'); + } + blockJSON.message0 = '%1 %2'; + const iconJSON = { + type: 'field_image', + src: iconURI, + width: 40, + height: 40 + }; + const separatorJSON = { + type: 'field_vertical_separator' + }; + blockJSON.args0 = [ + iconJSON, + separatorJSON + ]; + } + + let notchAccepts = blockInfo.notchAccepts ?? 'normal' + + switch (blockInfo.blockType) { + case BlockType.COMMAND: + blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + blockJSON.previousStatement = notchAccepts; // null = available connection; undefined = hat + if (!blockInfo.isTerminal) { + blockJSON.nextStatement = notchAccepts; // null = available connection; undefined = terminal + } + break; + case BlockType.REPORTER: + blockJSON.output = blockInfo.allowDropAnywhere ? null : 'String'; // TODO: distinguish number & string here? + blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_ROUND; + break; + case BlockType.BOOLEAN: + blockJSON.output = 'Boolean'; + blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_HEXAGONAL; + break; + case BlockType.HAT: + case BlockType.EVENT: + if (!blockInfo.hasOwnProperty('isEdgeActivated')) { + // if absent, this property defaults to true + blockInfo.isEdgeActivated = true; + } + blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + blockJSON.nextStatement = notchAccepts; // null = available connection; undefined = terminal + break; + case BlockType.CONDITIONAL: + case BlockType.LOOP: + blockInfo.branchCount = blockInfo.branchCount ?? 1; + blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + blockJSON.previousStatement = notchAccepts; // null = available connection; undefined = hat + if (!blockInfo.isTerminal) { + blockJSON.nextStatement = notchAccepts; // null = available connection; undefined = terminal + } + break; + } + + blockInfo.branches = blockInfo.branches || Array(Math.max(blockInfo.branchCount || 0, 0)).fill({}) + + const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text]; + let inTextNum = 0; // text for the next block "arm" is blockText[inTextNum] + let inBranchNum = 0; // how many branches have we placed into the JSON so far? + let outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}` + const convertPlaceholders = this._convertPlaceholders.bind(this, context); + const extensionMessageContext = this.makeMessageContextForTarget(); + + // alternate between a block "arm" with text on it and an open slot for a substack + while (inTextNum < blockText.length || inBranchNum < blockInfo.branches.length) { + if (inTextNum < blockText.length) { + context.outLineNum = outLineNum; + const lineText = maybeFormatMessage(blockText[inTextNum], extensionMessageContext); + const convertedText = lineText.replace(/\[(.+?)]/g, convertPlaceholders); + if (blockJSON[`message${outLineNum}`]) { + blockJSON[`message${outLineNum}`] += convertedText; + } else { + blockJSON[`message${outLineNum}`] = convertedText; + } + ++inTextNum; + ++outLineNum; + } + if (inBranchNum < blockInfo.branches.length) { + blockJSON[`message${outLineNum}`] = '%1'; + blockJSON[`args${outLineNum}`] = [{ + type: 'input_statement', + name: `SUBSTACK${inBranchNum > 0 ? inBranchNum + 1 : ''}`, + check: blockInfo.branches[inBranchNum].accepts ?? 'normal' + }]; + ++inBranchNum; + ++outLineNum; + } + } + + if (blockInfo.blockType === BlockType.REPORTER || blockInfo.blockType === BlockType.BOOLEAN) { + if (!blockInfo.disableMonitor && context.inputList.length === 0) { + blockJSON.checkboxInFlyout = true; + } + } + if (blockInfo.blockType === BlockType.LOOP || ('branchIndicator' in blockInfo || 'branchIconURI' in blockInfo)) { + // Add icon to the bottom right of a loop block + blockJSON[`lastDummyAlign${outLineNum}`] = 'RIGHT'; + blockJSON[`message${outLineNum}`] = '%1'; + blockJSON[`args${outLineNum}`] = [{ + type: 'field_image', + src: blockInfo.branchIndicator ?? blockInfo.branchIconURI ?? './static/blocks-media/repeat.svg', + width: blockInfo.branchIndicatorWidth ?? 24, + height: blockInfo.branchIndicatorHeight ?? 24, + alt: '*', // TODO remove this since we don't use collapsed blocks in scratch + flip_rtl: true + }]; + ++outLineNum; + } + + if (Array.isArray(blockInfo.alignments)) { + let idx = 0; + // i love for (const of) + for (const alignment of blockInfo.alignments) { + if (typeof alignment === "string") { + blockJSON[`lastDummyAlign${idx}`] = alignment.toUpperCase(); + } + idx++; + } + } + + if (typeof blockInfo.blockShape === 'number') { + blockJSON.outputShape = blockInfo.blockShape; + } + if (blockInfo.forceOutputType) { + blockJSON.output = blockInfo.forceOutputType; + } + + const mutation = blockInfo.isDynamic + ? `` + : ''; + const inputs = context.inputList.join(''); + const blockXML = `${mutation}${inputs}`; + + return { + info: context.blockInfo, + json: context.blockJSON, + xml: blockXML + }; + } + + /** + * Generate a separator between blocks categories or sub-categories. + * @param {ExtensionBlockMetadata} blockInfo - the block to convert + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {ConvertedBlockInfo} - the converted & original block information + * @private + */ + _convertSeparatorForScratchBlocks (blockInfo) { + return { + info: blockInfo, + xml: '' + }; + } + + /** + * Generate generate the xml for a toolbox lable. + * @param {ExtensionBlockMetadata} blockInfo - the block to convert + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {ConvertedBlockInfo} - the converted & original block information + * @private + */ + _convertLabelForScratchBlocks (blockInfo) { + const text = xmlEscape.escapeAttribute(blockInfo.text); + return { + info: blockInfo, + xml: `` + }; + } + + /** + * Convert a button for scratch-blocks. A button has no opcode but specifies a callback name in the `func` field. + * @param {ExtensionBlockMetadata} buttonInfo - the button to convert + * @property {string} func - the callback name + * @param {CategoryInfo} categoryInfo - the category for this button + * @returns {ConvertedBlockInfo} - the converted & original button information + * @private + */ + _convertButtonForScratchBlocks (buttonInfo) { + const extensionMessageContext = this.makeMessageContextForTarget(); + const buttonText = xmlEscape.escapeAttribute(maybeFormatMessage(buttonInfo.text, extensionMessageContext)); + const callback = xmlEscape.escapeAttribute(buttonInfo.opcode + ? buttonInfo.opcode + : buttonInfo.func); + + return { + info: buttonInfo, + xml: `` + }; + } + + _convertXmlForScratchBlocks (xmlInfo) { + return { + info: xmlInfo, + xml: xmlInfo.xml + }; + } + + /** + * Helper for _convertPlaceholdes which handles inline images which are a specialized case of block "arguments". + * @param {object} argInfo Metadata about the inline image as specified by the extension + * @return {object} JSON blob for a scratch-blocks image field. + * @private + */ + _constructInlineImageJson (argInfo) { + if (!argInfo.dataURI) { + log.warn('Missing data URI in extension block with argument type IMAGE'); + } + return { + type: 'field_image', + src: argInfo.dataURI || '', + // TODO these probably shouldn't be hardcoded...? + width: argInfo.width ?? 24, + height: argInfo.height ?? 24, + // Whether or not the inline image should be flipped horizontally + // in RTL languages. Defaults to false, indicating that the + // image will not be flipped. + flip_rtl: argInfo.flipRTL || false + }; + } + + /** + * Helper for _convertPlaceholdes which handles variable dropdowns + * which are a specialized case of block "arguments". + * @param {object} argInfo Metadata about the variable dropdown + * @return {object} JSON blob for a scratch-blocks variable field. + * @private + */ + _constructVariableDropdown(argInfo, placeholder) { + // console.log(argInfo, placeholder); + const isList = argInfo.type === 'list'; + const isBroadcast = argInfo.type === 'broadcast'; + return { + type: 'field_variable', + name: placeholder, + variableTypes: isList ? ['list'] : (isBroadcast ? ['broadcast_msg'] : ['']), + variable: isBroadcast ? 'message1' : null + }; + } + + /** + * Helper for _convertForScratchBlocks which handles linearization of argument placeholders. Called as a callback + * from string#replace. In addition to the return value the JSON and XML items in the context will be filled. + * @param {object} context - information shared with _convertForScratchBlocks about the block, etc. + * @param {string} match - the overall string matched by the placeholder regex, including brackets: '[FOO]'. + * @param {string} placeholder - the name of the placeholder being matched: 'FOO'. + * @return {string} scratch-blocks placeholder for the argument: '%1'. + * @private + */ + _convertPlaceholders (context, match, placeholder) { + // Determine whether the argument type is one of the known standard field types + const argInfo = context.blockInfo.arguments[placeholder] || {}; + let argTypeInfo = ArgumentTypeMap[argInfo.type] || {}; + + // Field type not a standard field type, see if extension has registered custom field type + if (!ArgumentTypeMap[argInfo.type] && context.categoryInfo.customFieldTypes[argInfo.type]) { + argTypeInfo = context.categoryInfo.customFieldTypes[argInfo.type].argumentTypeInfo; + } + + // Start to construct the scratch-blocks style JSON defining how the block should be + // laid out + let argJSON; + + // Most field types are inputs (slots on the block that can have other blocks plugged into them) + // check if this is not one of those cases. E.g. an inline image on a block. + if (argTypeInfo.fieldType === 'field_image') { + argJSON = this._constructInlineImageJson(argInfo); + } else if (argTypeInfo.fieldType === 'field_variable') { + argJSON = this._constructVariableDropdown(argInfo, placeholder); + } else if (argTypeInfo.fieldType === 'field_vertical_separator') { + argJSON = { + type: 'field_vertical_separator', + }; + } else { + // Construct input value + + // Layout a block argument (e.g. an input slot on the block) + argJSON = { + type: 'input_value', + name: placeholder + }; + + const defaultValue = + typeof argInfo.defaultValue === 'undefined' ? '' : + xmlEscape.escapeAttribute(maybeFormatMessage( + argInfo.defaultValue, this.makeMessageContextForTarget()).toString()); + + if (argTypeInfo.check || argInfo.check) { + // Right now the only type of 'check' we have specifies that the + // input slot on the block accepts Boolean reporters, so it should be + // shaped like a hexagon + argJSON.check = argInfo.check || argTypeInfo.check; + } + if (argInfo.shape) { + argJSON.shape = argInfo.shape; + } + + let valueName; + let shadowType; + let blockType; + let fieldName; + let variableID; + let variableName; + let variableType; + if (argInfo.menu) { + const menuInfo = context.categoryInfo.menuInfo[argInfo.menu]; + if (menuInfo.acceptReporters || menuInfo.isTypeable) { + valueName = placeholder; + shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id); + fieldName = argInfo.menu; + } else if (typeof menuInfo.variableType !== 'undefined') { + const args = Object.keys(context.blockInfo.arguments); + const blockText = context.blockInfo.text.toString(); + const isVariableGetter = args.length === 1 && blockText.length === args[0].length + 2; + if (isVariableGetter) { + context.blockJSON.extensions ??= []; + context.blockJSON.extensions.push('contextMenu_getVariableBlockAnyType'); + } + argJSON.type = isVariableGetter + ? 'field_variable_getter' + : 'field_variable'; + argJSON.variableTypes = [menuInfo.variableType === 'scalar' + ? Variable.SCALAR_TYPE + : menuInfo.variableType]; + argJSON.variableType = argJSON.variableTypes[0]; + valueName = null; + shadowType = null; + fieldName = placeholder; + variableType = menuInfo.variableType === 'scalar' + ? Variable.SCALAR_TYPE + : menuInfo.variableType + const defaultVar = argInfo.defaultValue ?? []; + variableID = defaultVar[0]; + variableName = defaultVar[1]; + } else { + argJSON.type = 'field_dropdown'; + argJSON.options = this._convertMenuItems(menuInfo.items); + valueName = null; + shadowType = null; + fieldName = placeholder; + } + } else { + valueName = placeholder; + shadowType = (argTypeInfo.shadow && argTypeInfo.shadow.type) || null; + fieldName = (argTypeInfo.shadow && argTypeInfo.shadow.fieldName) || null; + } + // TODO: Allow fillIn to work with non-shadow. + if (argInfo.fillIn/* && argInfo.fillInShadow*/) { + shadowType = `${context.categoryInfo.id}_${argInfo.fillIn}`; + }/* else if (argInfo.fillIn) { + blockType = `${context.categoryInfo.id}_${argInfo.fillIn}`; + }*/ + + // is the ScratchBlocks name for a block input. + if (valueName) { + context.inputList.push(``); + } + + // The is a placeholder for a reporter and is visible when there's no reporter in this input. + // Boolean inputs don't need to specify a shadow in the XML. + if (shadowType) { + context.inputList.push(``); + } + + // TODO: This doesnt seem to work properly with fillIn. Default to shadow for now. + if (blockType) { + context.inputList.push(``); + } + + if (shadowType === 'polygon') { + // eslint-disable-next-line max-len + context.inputList.push(``); + } + + // A displays a dynamic value: a user-editable text field, a drop-down menu, etc. + // Leave out the field if defaultValue or fieldName are not specified + if (fieldName && !variableID) { + if ((defaultValue) || ((argInfo.type === "string") && (!argInfo.menu))) { + context.inputList.push(`${defaultValue}`); + } + } + + if (variableID) { + // eslint-disable-next-line max-len + context.inputList.push(`${variableName}`); + } + + if (blockType) { + context.inputList.push(''); + } + + if (shadowType) { + context.inputList.push(''); + } + + if (valueName) { + context.inputList.push(''); + } + } + + const argsName = `args${context.outLineNum}`; + const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []); + if (argJSON) blockArgs.push(argJSON); + const argNum = blockArgs.length; + context.argsMap[placeholder] = argNum; + + return `%${argNum}`; + } + + /** + * @returns {Array.} scratch-blocks XML for each category of extension blocks, in category order. + * @param {?Target} [target] - the active editing target (optional) + * @property {string} id - the category / extension ID + * @property {string} xml - the XML text for this category, starting with `` and ending with `` + */ + getBlocksXML (target) { + return this._blockInfo.map(categoryInfo => { + const {name, color1, color2} = categoryInfo; + let orderBlocks = categoryInfo.orderBlocks; + // Filter out blocks that aren't supposed to be shown on this target, as determined by the block info's + // `hideFromPalette` and `filter` properties. + const paletteBlocks = categoryInfo.blocks.filter(block => { + let blockFilterIncludesTarget = true; + // If an editing target is not passed, include all blocks + // If the block info doesn't include a `filter` property, always include it + if (target && block.info.filter) { + blockFilterIncludesTarget = block.info.filter.includes( + target.isStage ? TargetType.STAGE : TargetType.SPRITE + ); + } + // If the block info's `hideFromPalette` is true, then filter out this block + return blockFilterIncludesTarget && !block.info.hideFromPalette; + }); + + orderBlocks = orderBlocks + ? orderBlocks + : blocks => blocks; + + const colorXML = `colour="${xmlEscape(color1)}" secondaryColour="${xmlEscape(color2)}"`; + + // Use a menu icon if there is one. Otherwise, use the block icon. If there's no icon, + // the category menu will show its default colored circle. + let menuIconURI = ''; + if (categoryInfo.menuIconURI) { + menuIconURI = categoryInfo.menuIconURI; + } else if (categoryInfo.blockIconURI) { + menuIconURI = categoryInfo.blockIconURI; + } + const menuIconXML = menuIconURI ? + `iconURI="${xmlEscape(menuIconURI)}"` : ''; + + let statusButtonXML = ''; + if (categoryInfo.showStatusButton) { + statusButtonXML = 'showStatusButton="true"'; + } + + let xml = ``; + xml += orderBlocks(paletteBlocks.map(block => block.xml)).join(''); + xml += ''; + + return { + id: categoryInfo.id, + xml + }; + }); + } + + /** + * @returns {Array.} - an array containing the scratch-blocks JSON information for each dynamic block. + */ + getBlocksJSON () { + return this._blockInfo.reduce( + (result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []); + } + + /** + * Get a scratch link socket. + * @param {string} type Either BLE or BT + * @returns {ScratchLinkSocket} The scratch link socket. + */ + getScratchLinkSocket (type) { + const factory = this._linkSocketFactory || this._defaultScratchLinkSocketFactory; + return factory(type); + } + + /** + * Configure how ScratchLink sockets are created. Factory must consume a "type" parameter + * either BT or BLE. + * @param {Function} factory The new factory for creating ScratchLink sockets. + */ + configureScratchLinkSocketFactory (factory) { + this._linkSocketFactory = factory; + } + + /** + * The default scratch link socket creator, using websockets to the installed device manager. + * @param {string} type Either BLE or BT + * @returns {ScratchLinkSocket} The new scratch link socket (a WebSocket object) + */ + _defaultScratchLinkSocketFactory (type) { + return new ScratchLinkWebSocket(type); + } + + /** + * Register an extension that communications with a hardware peripheral by id, + * to have access to it and its peripheral functions in the future. + * @param {string} extensionId - the id of the extension. + * @param {object} extension - the extension to register. + */ + registerPeripheralExtension (extensionId, extension) { + this.peripheralExtensions[extensionId] = extension; + } + + /** + * Tell the specified extension to scan for a peripheral. + * @param {string} extensionId - the id of the extension. + */ + scanForPeripheral (extensionId) { + if (this.peripheralExtensions[extensionId]) { + this.peripheralExtensions[extensionId].scan(); + } + } + + /** + * Connect to the extension's specified peripheral. + * @param {string} extensionId - the id of the extension. + * @param {number} peripheralId - the id of the peripheral. + */ + connectPeripheral (extensionId, peripheralId) { + if (this.peripheralExtensions[extensionId]) { + this.peripheralExtensions[extensionId].connect(peripheralId); + } + } + + /** + * Disconnect from the extension's connected peripheral. + * @param {string} extensionId - the id of the extension. + */ + disconnectPeripheral (extensionId) { + if (this.peripheralExtensions[extensionId]) { + this.peripheralExtensions[extensionId].disconnect(); + } + } + + /** + * Returns whether the extension has a currently connected peripheral. + * @param {string} extensionId - the id of the extension. + * @return {boolean} - whether the extension has a connected peripheral. + */ + getPeripheralIsConnected (extensionId) { + let isConnected = false; + if (this.peripheralExtensions[extensionId]) { + isConnected = this.peripheralExtensions[extensionId].isConnected(); + } + return isConnected; + } + + /** + * Emit an event to indicate that the microphone is being used to stream audio. + * @param {boolean} listening - true if the microphone is currently listening. + */ + emitMicListening (listening) { + this.emit(Runtime.MIC_LISTENING, listening); + } + + /** + * Retrieve the function associated with the given opcode. + * @param {!string} opcode The opcode to look up. + * @return {Function} The function which implements the opcode. + */ + getOpcodeFunction (opcode) { + return this._primitives[opcode]; + } + + /** + * Return whether an opcode represents a hat block. + * @param {!string} opcode The opcode to look up. + * @return {boolean} True if the op is known to be a hat. + */ + getIsHat (opcode) { + return this._hats.hasOwnProperty(opcode); + } + + /** + * Return whether an opcode represents an edge-activated hat block. + * @param {!string} opcode The opcode to look up. + * @return {boolean} True if the op is known to be a edge-activated hat. + */ + getIsEdgeActivatedHat (opcode) { + return this._hats.hasOwnProperty(opcode) && + this._hats[opcode].edgeActivated; + } + + + /** + * Attach the audio engine + * @param {!AudioEngine} audioEngine The audio engine to attach + */ + attachAudioEngine (audioEngine) { + this.audioEngine = audioEngine; + } + + /** + * Attach the renderer + * @param {!RenderWebGL} renderer The renderer to attach + */ + attachRenderer (renderer) { + this.renderer = renderer; + this.renderer.setLayerGroupOrdering(StageLayering.LAYER_GROUPS); + this.renderer.offscreenTouching = !this.runtimeOptions.fencing; + this.renderer.renderOffscreen = this.runtimeOptions.disableOffscreenRendering; + this.updatePrivacy(); + } + + /** + * registers a custom serializer to allow saving custom data into standard variables + * @param {Function} validate validates if a given chunk of data is correctly for this serializer + * @param {Function} serialize the function to be ran on serialized data in variables. + * @param {Function} deserialize the function to be ran on serialized data in variables + */ + registerSerializer (id, serialize, deserialize) { + if (typeof serialize !== 'function') { + throw new TypeError('serialize must be of type function'); + } + if (typeof deserialize !== 'function') { + throw new TypeError('deserialize must be of type function'); + } + this.serializers[id] = { + serialize, + deserialize + }; + } + + /** + * Register a variable type + * @param {string} type the type name of this variable + * @param {ObjectConstructor} varClass the class to handle the data this variable contains + */ + registerVariable (type, varClass) { + this._extensionVariables[type] = varClass; + } + + /** + * Remove a variable type + * @param {string} type the type name of this variable + */ + unregisterVariable (type) { + const variable = this._extensionVariables[type]; + if (!variable) throw new Error(`can not remove a variable type that does not exist. removing ${type}`); + delete this._extensionVariables[type]; + } + + /** + * create a new variable instance + * @param {string} type variable type + * @param {...any} args the arguments to pass down to the variable + * @returns {Variable|InstanceType} the new variable instance + */ + newVariableInstance (type, ...args) { + if (coreVariableTypes.includes(type)) { + args.splice(2, 0, type); + return new Variable(...args); + } + const variable = this._extensionVariables[type]; + // return a fake variable, this is because of loading variables from the sb3 parser + if (!variable) return { + type, + value: args, + mustRecreate: true + }; + return new variable(this, ...args); + } + + /** + * Set the bitmap adapter for the VM/runtime, which converts scratch 2 + * bitmaps to scratch 3 bitmaps. (Scratch 3 bitmaps are all bitmap resolution 2) + * @param {!function} bitmapAdapter The adapter to attach + */ + attachV2BitmapAdapter (bitmapAdapter) { + this.v2BitmapAdapter = bitmapAdapter; + } + + /** + * Attach the storage module + * @param {!ScratchStorage} storage The storage module to attach + */ + attachStorage (storage) { + this.storage = storage; + + if (this.isPackaged) { + // In packaged runtime mode, generating real asset IDs is a waste of time. + // We do still want to preserve every asset having a unique ID. + const originalCreateAsset = storage.createAsset; + let assetIdCounter = 0; + // eslint-disable-next-line no-unused-vars + storage.createAsset = function packagedCreateAsset (assetType, dataFormat, data, assetId, generateId) { + if (!assetId) { + assetId = (++assetIdCounter).toString(); + } + return originalCreateAsset.call( + this, + assetType, + dataFormat, + data, + assetId, + // Never generate real asset ID + false + ); + }; + } + } + + // ----------------------------------------------------------------------------- + // ----------------------------------------------------------------------------- + + /** + * Create a thread and push it to the list of threads. + * @param {!string} id ID of block that starts the stack. + * @param {!Target} target Target to run thread on. + * @param {?object} opts optional arguments + * @param {?boolean} opts.stackClick true if the script was activated by clicking on the stack + * @param {?boolean} opts.updateMonitor true if the script should update a monitor value + * @param {?Target} opts.targetBlockLocation where the blocks are located + * @return {!Thread} The newly created thread. + */ + _pushThread (id, target, opts) { + const thread = new Thread(id); + thread.target = target; + thread.stackClick = Boolean(opts && opts.stackClick); + thread.updateMonitor = Boolean(opts && opts.updateMonitor); + thread.blockContainer = thread.updateMonitor ? + this.monitorBlocks : + ((opts && opts.targetBlockLocation) || target.blocks); + + thread.pushStack(id); + this.threads.push(thread); + if (!thread.stackClick && !thread.updateMonitor) { + this.threadMap.set(thread.getId(), thread); + } + + // tw: compile new threads. Do not attempt to compile monitor threads. + if (!(opts && opts.updateMonitor) && this.compilerOptions.enabled) { + thread.tryCompile(); + } + + return thread; + } + + /** + * Stop a thread: stop running it immediately, and remove it from the thread list later. + * @param {!Thread} thread Thread object to remove from actives + */ + _stopThread (thread) { + // Mark the thread for later removal + thread.isKilled = true; + // Inform sequencer to stop executing that thread. + this.sequencer.retireThread(thread); + } + + /** + * Restart a thread in place, maintaining its position in the list of threads. + * This is used by `startHats` to and is necessary to ensure 2.0-like execution order. + * Test project: https://scratch.mit.edu/projects/130183108/ + * @param {!Thread} thread Thread object to restart. + * @return {Thread} The restarted thread. + */ + _restartThread (thread) { + const newThread = new Thread(thread.topBlock); + newThread.target = thread.target; + newThread.stackClick = thread.stackClick; + newThread.updateMonitor = thread.updateMonitor; + newThread.blockContainer = thread.blockContainer; + newThread.pushStack(thread.topBlock); + // tw: when a thread is restarted, we have to check whether the previous script was attempted to be compiled. + if (thread.triedToCompile && this.compilerOptions.enabled) { + newThread.tryCompile(); + } + if (!newThread.stackClick && !newThread.updateMonitor) { + this.threadMap.set(newThread.getId(), newThread); + } + const i = this.threads.indexOf(thread); + if (i > -1) { + this.threads[i] = newThread; + return newThread; + } + this.threads.push(thread); + return thread; + } + + emitCompileError (target, error) { + this.emit(Runtime.COMPILE_ERROR, target, error); + } + + /** + * Return whether a thread is currently active/running. + * @param {?Thread} thread Thread object to check. + * @return {boolean} True if the thread is active/running. + */ + isActiveThread (thread) { + return ( + ( + thread.stack.length > 0 && + thread.status !== Thread.STATUS_DONE) && + this.threads.indexOf(thread) > -1); + } + + /** + * Return whether a thread is waiting for more information or done. + * @param {?Thread} thread Thread object to check. + * @return {boolean} True if the thread is waiting + */ + isWaitingThread (thread) { + return ( + thread.status === Thread.STATUS_PROMISE_WAIT || + thread.status === Thread.STATUS_YIELD_TICK || + !this.isActiveThread(thread) + ); + } + + /** + * Toggle a script. + * @param {!string} topBlockId ID of block that starts the script. + * @param {?object} opts optional arguments to toggle script + * @param {?string} opts.target target ID for target to run script on. If not supplied, uses editing target. + * @param {?boolean} opts.stackClick true if the user activated the stack by clicking, false if not. This + * determines whether we show a visual report when turning on the script. + */ + toggleScript (topBlockId, opts) { + opts = Object.assign({ + target: this._editingTarget, + stackClick: false + }, opts); + // Remove any existing thread. + for (let i = 0; i < this.threads.length; i++) { + // Toggling a script that's already running turns it off + if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE) { + const blockContainer = opts.target.blocks; + const opcode = blockContainer.getOpcode(blockContainer.getBlock(topBlockId)); + + if (this.getIsEdgeActivatedHat(opcode) && this.threads[i].stackClick !== opts.stackClick) { + // Allow edge activated hat thread stack click to coexist with + // edge activated hat thread that runs every frame + continue; + } + this._stopThread(this.threads[i]); + return; + } + } + // Otherwise add it. + this._pushThread(topBlockId, opts.target, opts); + } + + /** + * Enqueue a script that when finished will update the monitor for the block. + * @param {!string} topBlockId ID of block that starts the script. + * @param {?Target} optTarget target Target to run script on. If not supplied, uses editing target. + */ + addMonitorScript (topBlockId, optTarget) { + if (!optTarget) optTarget = this._editingTarget; + for (let i = 0; i < this.threads.length; i++) { + // Don't re-add the script if it's already running + if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE && + this.threads[i].updateMonitor) { + return; + } + } + // Otherwise add it. + this._pushThread(topBlockId, optTarget, {updateMonitor: true}); + } + + /** + * Run a function `f` for all scripts in a workspace. + * `f` will be called with two parameters: + * - the top block ID of the script. + * - the target that owns the script. + * @param {!Function} f Function to call for each script. + * @param {Target=} optTarget Optionally, a target to restrict to. + */ + allScriptsDo (f, optTarget) { + let targets = this.executableTargets; + if (optTarget) { + targets = [optTarget]; + } + for (let t = targets.length - 1; t >= 0; t--) { + const target = targets[t]; + const scripts = target.blocks.getScripts(); + for (let j = 0; j < scripts.length; j++) { + const topBlockId = scripts[j]; + f(topBlockId, target); + } + } + } + + allScriptsByOpcodeDo (opcode, f, optTarget) { + let targets = this.executableTargets; + if (optTarget) { + targets = [optTarget]; + } + for (let t = targets.length - 1; t >= 0; t--) { + const target = targets[t]; + const scripts = BlocksRuntimeCache.getScripts(target.blocks, opcode); + for (let j = 0; j < scripts.length; j++) { + f(scripts[j], target); + } + } + } + + /** + * Start all relevant hats. + * @param {!string} requestedHatOpcode 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.} List of threads started by this function. + */ + startHats (requestedHatOpcode, optMatchFields, optTarget) { + if (!this._hats.hasOwnProperty(requestedHatOpcode)) { + // No known hat with this opcode. + return; + } + const instance = this; + const newThreads = []; + // Look up metadata for the relevant hat. + const hatMeta = instance._hats[requestedHatOpcode]; + + for (const opts in optMatchFields) { + if (!optMatchFields.hasOwnProperty(opts)) continue; + optMatchFields[opts] = optMatchFields[opts].toUpperCase(); + } + + // tw: By assuming that all new threads will not interfere with eachother, we can optimize the loops + // inside the allScriptsByOpcodeDo callback below. + const startingThreadListLength = this.threads.length; + + // Consider all scripts, looking for hats with opcode `requestedHatOpcode`. + this.allScriptsByOpcodeDo(requestedHatOpcode, (script, target) => { + const { + blockId: topBlockId, + fieldsOfInputs: hatFields + } = script; + + // Match any requested fields. + // For example: ensures that broadcasts match. + // This needs to happen before the block is evaluated + // (i.e., before the predicate can be run) because "broadcast and wait" + // needs to have a precise collection of started threads. + for (const matchField in optMatchFields) { + if (hatFields[matchField].value !== optMatchFields[matchField]) { + // Field mismatch. + return; + } + } + + if (hatMeta.restartExistingThreads) { + // If `restartExistingThreads` is true, we should stop + // any existing threads starting with the top block. + const existingThread = this.threadMap.get(Thread.getIdFromTargetAndBlock(target, topBlockId)); + if (existingThread) { + newThreads.push(this._restartThread(existingThread)); + return; + } + } else { + // If `restartExistingThreads` is false, we should + // give up if any threads with the top block are running. + for (let j = 0; j < startingThreadListLength; j++) { + if (this.threads[j].target === target && + this.threads[j].topBlock === topBlockId && + // stack click threads and hat threads can coexist + !this.threads[j].stackClick && + this.threads[j].status !== Thread.STATUS_DONE) { + // Some thread is already running. + return; + } + } + } + // Start the thread with this top block. + newThreads.push(this._pushThread(topBlockId, target)); + }, optTarget); + // For compatibility with Scratch 2, edge triggered hats need to be processed before + // threads are stepped. See ScratchRuntime.as for original implementation + newThreads.forEach(thread => { + // just incase, pause any new threads that appear while we are paused + if (this.paused) thread.pause(); + if (thread.isCompiled) { + if (thread.executableHat) { + // It is quite likely that we are currently executing a block, so make sure + // that we leave the compiler's state intact at the end. + compilerExecute.saveGlobalState(); + compilerExecute(thread); + compilerExecute.restoreGlobalState(); + } + } else { + execute(this.sequencer, thread); + thread.goToNextBlock(); + } + }); + this.emit(Runtime.HATS_STARTED, requestedHatOpcode, optMatchFields, optTarget, newThreads); + return newThreads; + } + + + /** + * Dispose all targets. Return to clean state. + */ + dispose () { + this.stopAll(); + // Deleting each target's variable's monitors. + this.targets.forEach(target => { + if (target.isOriginal) target.deleteMonitors(); + }); + + this.targets.map(this.disposeTarget, this); + // tw: explicitly emit a MONITORS_UPDATE instead of relying on implicit behavior of _step() + const emptyMonitorState = OrderedMap({}); + if (!emptyMonitorState.equals(this._monitorState)) { + this._monitorState = emptyMonitorState; + this.emit(Runtime.MONITORS_UPDATE, this._monitorState); + } + this.emit(Runtime.RUNTIME_DISPOSED); + this.ioDevices.clock.resetProjectTimer(); + this.fontManager.clear(); + // @todo clear out extensions? turboMode? etc. + + // *********** Cloud ******************* + + // If the runtime currently has cloud data, + // emit a has cloud data update event resetting + // it to false + if (this.hasCloudData()) { + this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, false); + } + + this.ioDevices.cloud.clear(); + + // Reset runtime cloud data info + const newCloudDataManager = cloudDataManager(this.cloudOptions); + this.hasCloudData = newCloudDataManager.hasCloudVariables; + this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable; + this.getNumberOfCloudVariables = newCloudDataManager.getNumberOfCloudVariables; + this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager); + this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager); + } + + /** + * Add a target to the runtime. This tracks the sprite pane + * ordering of the target. The target still needs to be put + * into the correct execution order after calling this function. + * @param {Target} target target to add + */ + addTarget (target) { + for (const varId in target.variables) { + const variable = target.variables[varId]; + if (variable.mustRecreate) { + // variable must be fully created now as we couldnt earlier + const newVar = this.newVariableInstance(variable.type, ...variable.value); + // variable type doesnt exist, remove variable entirely + if (newVar.mustRecreate) { + delete target.variable[varId]; + continue; + } + target.variables[varId] = newVar; + } + } + this.targets.push(target); + this.executableTargets.push(target); + if (target.isStage && !this._stageTarget) { + this._stageTarget = target; + } + } + + /** + * Move a target in the execution order by a relative amount. + * + * A positve number will make the target execute earlier. A negative number + * will make the target execute later in the order. + * + * @param {Target} executableTarget target to move + * @param {number} delta number of positions to move target by + * @returns {number} new position in execution order + */ + moveExecutable (executableTarget, delta) { + const oldIndex = this.executableTargets.indexOf(executableTarget); + this.executableTargets.splice(oldIndex, 1); + let newIndex = oldIndex + delta; + if (newIndex > this.executableTargets.length) { + newIndex = this.executableTargets.length; + } + if (newIndex <= 0) { + if (this.executableTargets.length > 0 && this.executableTargets[0].isStage) { + newIndex = 1; + } else { + newIndex = 0; + } + } + this.executableTargets.splice(newIndex, 0, executableTarget); + return newIndex; + } + + /** + * Set a target to execute at a specific position in the execution order. + * + * Infinity will set the target to execute first. 0 will set the target to + * execute last (before the stage). + * + * @param {Target} executableTarget target to move + * @param {number} newIndex position in execution order to place the target + * @returns {number} new position in the execution order + */ + setExecutablePosition (executableTarget, newIndex) { + const oldIndex = this.executableTargets.indexOf(executableTarget); + return this.moveExecutable(executableTarget, newIndex - oldIndex); + } + + /** + * Remove a target from the execution set. + * @param {Target} executableTarget target to remove + */ + removeExecutable (executableTarget) { + const oldIndex = this.executableTargets.indexOf(executableTarget); + if (oldIndex > -1) { + this.executableTargets.splice(oldIndex, 1); + } + } + + /** + * Dispose of a target. + * @param {!Target} disposingTarget Target to dispose of. + */ + disposeTarget (disposingTarget) { + this.targets = this.targets.filter(target => { + if (disposingTarget !== target) return true; + // Allow target to do dispose actions. + target.dispose(); + // Remove from list of targets. + return false; + }); + if (this._stageTarget === disposingTarget) { + this._stageTarget = null; + } + } + + /** + * Stop any threads acting on the target. + * @param {!Target} target Target to stop threads for. + * @param {Thread=} optThreadException Optional thread to skip. + */ + stopForTarget (target, optThreadException) { + // Emit stop event to allow blocks to clean up any state. + this.emit(Runtime.STOP_FOR_TARGET, target, optThreadException); + + // Stop any threads on the target. + for (let i = 0; i < this.threads.length; i++) { + if (this.threads[i] === optThreadException) { + continue; + } + if (this.threads[i].target === target) { + this._stopThread(this.threads[i]); + } + } + } + + /** + * Start all threads that start with the green flag. + */ + greenFlag() { + this.emit(Runtime.PROJECT_START_BEFORE_RESET); + this.stopAll(); + this.emit(Runtime.PROJECT_START); + this.updateCurrentMSecs(); + this.ioDevices.clock.resetProjectTimer(); + this.targets.forEach(target => target.clearEdgeActivatedValues()); + // Inform all targets of the green flag. + for (let i = 0; i < this.targets.length; i++) { + this.targets[i].onGreenFlag(); + } + this.startHats('event_whenflagclicked'); + } + + /** + * Pause running scripts + */ + pause() { + if (this.paused) return; + this.emit(Runtime.RUNTIME_PRE_PAUSED); + this.paused = true; + // pause all audio contexts (that includes exts with their own AC or gain node) + this.audioEngine.audioContext.suspend(); + for (const audioData of this._extensionAudioObjects.values()) { + if (audioData.audioContext) { + audioData.audioContext.suspend(); + } + } + + this.ioDevices.clock.pause(); + for (const thread of this.threads) { + thread.pause(); + } + this.emit(Runtime.RUNTIME_PAUSED); + } + + /** + * Unpause running scripts + */ + play() { + if (!this.paused) return; + this.paused = false; + // resume all audio contexts (that includes exts with their own AC or gain node) + this.audioEngine.audioContext.resume(); + for (const audioData of this._extensionAudioObjects.values()) { + if (audioData.audioContext) { + audioData.audioContext.resume(); + } + } + + this.ioDevices.clock.resume(); + for (const thread of this.threads) { + thread.play(); + } + this.emit(Runtime.RUNTIME_UNPAUSED); + } + + /** + * Stop "everything." + */ + stopAll () { + // unpause everything before we destroy all the threads + this.play(); + // Emit stop event to allow blocks to clean up any state. + this.emit(Runtime.PROJECT_STOP_ALL); + // clear runtime variables + this.variables = Object.create(null); + + // Dispose all clones. + const newTargets = []; + for (let i = 0; i < this.targets.length; i++) { + this.targets[i].onStopAll(); + if (this.targets[i].hasOwnProperty('isOriginal') && + !this.targets[i].isOriginal) { + this.targets[i].dispose(); + } else { + newTargets.push(this.targets[i]); + } + } + this.targets = newTargets; + // Dispose of the active thread. + if (this.sequencer.activeThread !== null) { + this._stopThread(this.sequencer.activeThread); + } + // Remove all remaining threads from executing in the next tick. + this.threads = []; + this.threadMap.clear(); + } + + _renderInterpolatedPositions () { + const frameStarted = this._lastStepTime; + const now = Date.now(); + const timeSinceStart = now - frameStarted; + const progressInFrame = Math.min(1, Math.max(0, timeSinceStart / this.currentStepTime)); + + interpolate.interpolate(this, progressInFrame); + + if (this.renderer) { + this.renderer.draw(); + } + } + + updateThreadMap () { + this.threadMap.clear(); + for (const thread of this.threads) { + if (!thread.stackClick && !thread.updateMonitor) { + this.threadMap.set(thread.getId(), thread); + } + } + } + + /** + * Repeatedly run `sequencer.stepThreads` and filter out + * inactive threads after each iteration. + */ + _step () { + // pm: RUNTIME_STEP_START runs before BEFORE_EXECUTE + // this runs before any processing of this new step + this.frameLoop._stepCounter++; + this.emit(Runtime.RUNTIME_STEP_START); + + if (this.interpolationEnabled) { + interpolate.setupInitialState(this); + } + + if (this.profiler !== null) { + if (stepProfilerId === -1) { + stepProfilerId = this.profiler.idByName('Runtime._step'); + } + this.profiler.start(stepProfilerId); + } + + // Clean up threads that were told to stop during or since the last step + this.threads = this.threads.filter(thread => !thread.isKilled); + this.updateThreadMap(); + + // Find all edge-activated hats, and add them to threads to be evaluated. + for (const hatType in this._hats) { + if (!this._hats.hasOwnProperty(hatType)) continue; + const hat = this._hats[hatType]; + if (hat.edgeActivated) { + this.startHats(hatType); + } + } + this.redrawRequested = false; + this._pushMonitors(); + if (this.profiler !== null) { + if (stepThreadsProfilerId === -1) { + stepThreadsProfilerId = this.profiler.idByName('Sequencer.stepThreads'); + } + this.profiler.start(stepThreadsProfilerId); + } + this.emit(Runtime.BEFORE_EXECUTE); + const doneThreads = this.sequencer.stepThreads(); + if (this.profiler !== null) { + this.profiler.stop(); + } + this.emit(Runtime.AFTER_EXECUTE); + this._updateGlows(doneThreads); + // Add done threads so that even if a thread finishes within 1 frame, the green + // flag will still indicate that a script ran. + this._emitProjectRunStatus( + this.threads.length + doneThreads.length - + this._getMonitorThreadCount([...this.threads, ...doneThreads])); + // Store threads that completed this iteration for testing and other + // internal purposes. + this._lastStepDoneThreads = doneThreads; + if (this.renderer) { + // @todo: Only render when this.redrawRequested or clones rendered. + if (this.profiler !== null) { + if (rendererDrawProfilerId === -1) { + rendererDrawProfilerId = this.profiler.idByName('RenderWebGL.draw'); + } + this.profiler.start(rendererDrawProfilerId); + } + // tw: do not draw if document is hidden or a rAF loop is running + // Checking for the animation frame loop is more reliable than using + // interpolationEnabled in some edge cases + if (!document.hidden && !this.frameLoop._interpolationAnimation) { + this.renderer.draw(); + } + if (this.profiler !== null) { + this.profiler.stop(); + } + } + + if (this._refreshTargets) { + this.emit(Runtime.TARGETS_UPDATE, false /* Don't emit project changed */); + this._refreshTargets = false; + } + + let forceUpd = false; + // if a custom type set _monitorUpToDate to false on an existing instance, we need to report that update to the gui + if (this._monitorState.some(item => + typeof item.get('value') === 'object' && + '_monitorUpToDate' in item.get('value') && + !item.get('value')._monitorUpToDate + )) { + const old = this._monitorState; + // make a new instance so redux detects this as different later on + this._monitorState = this._monitorState.toOrderedMap(); + if (!(old !== this._monitorState)) // why wont redux just accept the fucking value + throw new Error('Expected OrderedMap.toOrderedMap() to produce a truly unique value'); + forceUpd = true; + } + if (!this._prevMonitorState.equals(this._monitorState) || forceUpd) { + this.emit(Runtime.MONITORS_UPDATE, this._monitorState); + this._prevMonitorState = this._monitorState; + } + + if (this.profiler !== null) { + this.profiler.stop(); + this.profiler.reportFrames(); + } + + if (this.interpolationEnabled) { + this._lastStepTime = Date.now(); + } + + // pm: RUNTIME_STEP_END runs after AFTER_EXECUTE + this.emit(Runtime.RUNTIME_STEP_END); + } + + /** + * Get the number of threads in the given array that are monitor threads (threads + * that update monitor values, and don't count as running a script). + * @param {!Array.} threads The set of threads to look through. + * @return {number} The number of monitor threads in threads. + */ + _getMonitorThreadCount (threads) { + let count = 0; + threads.forEach(thread => { + if (thread.updateMonitor) count++; + }); + return count; + } + + /** + * Queue monitor blocks to sequencer to be run. + */ + _pushMonitors () { + this.monitorBlocks.runAllMonitored(this); + } + + /** + * Set the current editing target known by the runtime. + * @param {!Target} editingTarget New editing target. + */ + setEditingTarget (editingTarget) { + const oldEditingTarget = this._editingTarget; + this._editingTarget = editingTarget; + // Script glows must be cleared. + this._scriptGlowsPreviousFrame = []; + this._updateGlows(); + + if (oldEditingTarget !== this._editingTarget) { + this.requestToolboxExtensionsUpdate(); + } + } + + /** + * Set whether we are in 30 TPS compatibility mode. + * @param {boolean} compatibilityModeOn True iff in compatibility mode. + */ + setCompatibilityMode (compatibilityModeOn) { + // tw: "compatibility mode" is replaced with a generic framerate setter, + // but this method is kept for compatibility + if (compatibilityModeOn) { + this.setFramerate(30); + } else { + this.setFramerate(60); + } + } + + /** + * tw: Change runtime target frames per second + * @param {number} framerate Target frames per second + */ + setFramerate (framerate) { + // Setting framerate to anything greater than this is unnecessary and can break the sequencer + // Additionally, the JS spec says intervals can't run more than once every 4ms (250/s) anyways + if (framerate > 250) framerate = 250; + // Convert negative framerates to 1FPS + // Note that 0 is a special value which means "matching device screen refresh rate" + if (framerate < 0) framerate = 1; + this.frameLoop.setFramerate(framerate); + this.emit(Runtime.FRAMERATE_CHANGED, framerate); + } + + /** + * tw: Enable or disable interpolation. + * @param {boolean} interpolationEnabled True if interpolation should be enabled. + */ + setInterpolation (interpolationEnabled) { + this.interpolationEnabled = interpolationEnabled; + this.frameLoop.setInterpolation(this.interpolationEnabled); + this.emit(Runtime.INTERPOLATION_CHANGED, interpolationEnabled); + } + + /** + * tw: Update runtime options + * @param {*} runtimeOptions New options + */ + setRuntimeOptions (runtimeOptions) { + this.runtimeOptions = Object.assign({}, this.runtimeOptions, runtimeOptions); + this.emit(Runtime.RUNTIME_OPTIONS_CHANGED, this.runtimeOptions); + if (this.renderer) { + this.renderer.offscreenTouching = !this.runtimeOptions.fencing; + // if these miss match then update (do full rerender as the state drastically changes output) + if (this.runtimeOptions.disableOffscreenRendering === this.renderer.renderOffscreen) { + this.renderer.setRenderOffscreen(!this.runtimeOptions.disableOffscreenRendering); + } + } + } + + /** + * tw: Update compiler options + * @param {*} compilerOptions New options + */ + setCompilerOptions (compilerOptions) { + this.compilerOptions = Object.assign({}, this.compilerOptions, compilerOptions); + this.resetAllCaches(); + this.emit(Runtime.COMPILER_OPTIONS_CHANGED, this.compilerOptions); + } + + /** + * Change width and height of stage. This will also inform the renderer of the new stage size. + * @param {number} width New stage width + * @param {number} height New stage height + */ + setStageSize (width, height) { + width = Math.round(Math.max(1, width)); + height = Math.round(Math.max(1, height)); + if (this.stageWidth !== width || this.stageHeight !== height) { + const deltaX = width - this.stageWidth; + const deltaY = height - this.stageHeight; + // Preserve monitor location relative to the center of the stage + if (this._monitorState.size > 0) { + const offsetX = deltaX / 2; + const offsetY = deltaY / 2; + for (const monitor of this._monitorState.valueSeq()) { + const newMonitor = monitor + .set('x', monitor.get('x') + offsetX) + .set('y', monitor.get('y') + offsetY); + this.requestUpdateMonitor(newMonitor); + } + this.emit(Runtime.MONITORS_UPDATE, this._monitorState); + } + + this.stageWidth = width; + this.stageHeight = height; + if (this.renderer) { + this.renderer.setStageSize( + -width / 2, + width / 2, + -height / 2, + height / 2 + ); + } + this.emit(Runtime.STAGE_SIZE_CHANGED, width, height); + } + } + + // eslint-disable-next-line no-unused-vars + setInEditor (inEditor) { + // no-op + } + + /** + * TW: Enable "packaged runtime" mode. This is a one-way operation. + */ + convertToPackagedRuntime () { + if (this.storage) { + throw new Error('convertToPackagedRuntime must be called before attachStorage'); + } + + this.isPackaged = true; + } + + /** + * tw: Reset the cache of all block containers. + */ + resetAllCaches () { + for (const target of this.targets) { + if (target.isOriginal) { + target.blocks.resetCache(); + } + } + this.flyoutBlocks.resetCache(); + this.monitorBlocks.resetCache(); + } + + /** + * Add an "addon block" + * @param {object} options Options object + * @param {string} options.procedureCode The ID of the block + * @param {function} options.callback The callback, called with (args, BlockUtility). May return a promise. + * @param {string[]} options.arguments Names of the arguments accepted + * @param {boolean} [hidden] True to not include this block in the block palette + */ + addAddonBlock (options) { + const procedureCode = options.procedureCode; + const names = options.arguments; + const ids = options.arguments.map((_, i) => `arg${i}`); + const defaults = options.arguments.map(() => ''); + this.addonBlocks[procedureCode] = { + namesIdsDefaults: [names, ids, defaults], + ...options + }; + + if (!options.hidden) { + const ID = 'a-b'; + let blockInfo = this._blockInfo.find(i => i.id === ID); + if (!blockInfo) { + // eslint-disable-next-line max-len + const ICON = ''; + blockInfo = { + id: ID, + name: 'Addons', + color1: '#29beb8', + color2: '#3aa8a4', + color3: '#3aa8a4', + menuIconURI: `data:image/svg+xml;,${encodeURIComponent(ICON)}`, + blocks: [], + customFieldTypes: {}, + menus: [] + }; + this._blockInfo.unshift(blockInfo); + } + blockInfo.blocks.push({ + info: {}, + xml: + '' + }); + } + + this.resetAllCaches(); + } + + getAddonBlock (procedureCode) { + if (Object.prototype.hasOwnProperty.call(this.addonBlocks, procedureCode)) { + return this.addonBlocks[procedureCode]; + } + return null; + } + + findProjectOptionsComment () { + const target = this.getTargetForStage(); + const comments = target.comments; + for (const comment of Object.values(comments)) { + if (comment.text.includes(COMMENT_CONFIG_MAGIC)) { + return comment; + } + } + return null; + } + + parseProjectOptions () { + const comment = this.findProjectOptionsComment(); + if (!comment) return; + const lineWithMagic = comment.text.split('\n').find(i => i.endsWith(COMMENT_CONFIG_MAGIC)); + if (!lineWithMagic) { + log.warn('Config comment does not contain valid line'); + return; + } + + const jsonText = lineWithMagic.slice(0, lineWithMagic.length - COMMENT_CONFIG_MAGIC.length); + let parsed; + try { + parsed = ExtendedJSON.parse(jsonText); + if (!parsed || typeof parsed !== 'object') { + throw new Error('Invalid object'); + } + } catch (e) { + log.warn('Config comment has invalid JSON', e); + return; + } + + if (typeof parsed.framerate === 'number') { + this.setFramerate(parsed.framerate); + } + if (parsed.turbo) { + this.turboMode = true; + this.emit(Runtime.TURBO_MODE_ON); + } + if (parsed.interpolation) { + this.setInterpolation(true); + } + if (parsed.runtimeOptions) { + this.setRuntimeOptions(parsed.runtimeOptions); + } + if (typeof parsed.hq === 'boolean' && this.renderer) { + this.renderer.setUseHighQualityRender(parsed.hq); + } + const storedWidth = +parsed.width || this.stageWidth; + const storedHeight = +parsed.height || this.stageHeight; + if (storedWidth !== this.stageWidth || storedHeight !== this.stageHeight) { + this.setStageSize(storedWidth, storedHeight); + } + } + + _generateAllProjectOptions () { + return { + framerate: this.frameLoop.framerate, + runtimeOptions: this.runtimeOptions, + interpolation: this.interpolationEnabled, + turbo: this.turboMode, + hq: this.renderer ? this.renderer.useHighQualityRender : true, + width: this.stageWidth, + height: this.stageHeight + }; + } + + generateDifferingProjectOptions () { + const difference = (oldObject, newObject) => { + const result = {}; + for (const key of Object.keys(newObject)) { + const newValue = newObject[key]; + const oldValue = oldObject[key]; + if (typeof newValue === 'object' && newValue) { + const valueDiffering = difference(oldValue, newValue); + if (Object.keys(valueDiffering).length > 0) { + result[key] = valueDiffering; + } + } else if (newValue !== oldValue) { + result[key] = newValue; + } + } + return result; + }; + return difference(this._defaultStoredSettings, this._generateAllProjectOptions()); + } + + storeProjectOptions () { + const options = this.generateDifferingProjectOptions(); + // TODO: translate + const text = `Configuration for https://penguinmod.com/\nYou can move, resize, and minimize this comment, but don't edit it by hand. This comment can be deleted to remove the stored settings.\n${ExtendedJSON.stringify(options)}${COMMENT_CONFIG_MAGIC}`; + const existingComment = this.findProjectOptionsComment(); + if (existingComment) { + existingComment.text = text; + } else { + const target = this.getTargetForStage(); + // TODO: smarter position logic + target.createComment(uid(), null, text, 50, 50, 350, 170, false); + } + this.emitProjectChanged(); + } + + /** + * Eagerly (re)compile all scripts within this project. + */ + precompile () { + this.allScriptsDo((topBlockId, target) => { + const topBlock = target.blocks.getBlock(topBlockId); + if (this.getIsHat(topBlock.opcode)) { + const thread = new Thread(topBlockId); + thread.target = target; + thread.blockContainer = target.blocks; + thread.tryCompile(); + } + }); + } + + enableDebug () { + this.resetAllCaches(); + this.debug = true; + } + + /** + * Emit glows/glow clears for scripts after a single tick. + * Looks at `this.threads` and notices which have turned on/off new glows. + * @param {Array.=} optExtraThreads Optional list of inactive threads. + */ + _updateGlows (optExtraThreads) { + const searchThreads = []; + searchThreads.push.apply(searchThreads, this.threads); + if (optExtraThreads) { + searchThreads.push.apply(searchThreads, optExtraThreads); + } + // Set of scripts that request a glow this frame. + const requestedGlowsThisFrame = []; + // Final set of scripts glowing during this frame. + const finalScriptGlows = []; + // Find all scripts that should be glowing. + for (let i = 0; i < searchThreads.length; i++) { + const thread = searchThreads[i]; + const target = thread.target; + if (target === this._editingTarget) { + const blockForThread = thread.blockGlowInFrame; + if (thread.requestScriptGlowInFrame || thread.stackClick) { + let script = target.blocks.getTopLevelScript(blockForThread); + if (!script) { + // Attempt to find in flyout blocks. + script = this.flyoutBlocks.getTopLevelScript( + blockForThread + ); + } + if (script) { + requestedGlowsThisFrame.push(script); + } + } + } + } + // Compare to previous frame. + for (let j = 0; j < this._scriptGlowsPreviousFrame.length; j++) { + const previousFrameGlow = this._scriptGlowsPreviousFrame[j]; + if (requestedGlowsThisFrame.indexOf(previousFrameGlow) < 0) { + // Glow turned off. + this.glowScript(previousFrameGlow, false); + } else { + // Still glowing. + finalScriptGlows.push(previousFrameGlow); + } + } + for (let k = 0; k < requestedGlowsThisFrame.length; k++) { + const currentFrameGlow = requestedGlowsThisFrame[k]; + if (this._scriptGlowsPreviousFrame.indexOf(currentFrameGlow) < 0) { + // Glow turned on. + this.glowScript(currentFrameGlow, true); + finalScriptGlows.push(currentFrameGlow); + } + } + this._scriptGlowsPreviousFrame = finalScriptGlows; + } + + /** + * Emit run start/stop after each tick. Emits when `this.threads.length` goes + * between non-zero and zero + * + * @param {number} nonMonitorThreadCount The new nonMonitorThreadCount + */ + _emitProjectRunStatus (nonMonitorThreadCount) { + if (this._nonMonitorThreadCount === 0 && nonMonitorThreadCount > 0) { + this.emit(Runtime.PROJECT_RUN_START); + } + if (this._nonMonitorThreadCount > 0 && nonMonitorThreadCount === 0) { + this.emit(Runtime.PROJECT_RUN_STOP); + } + this._nonMonitorThreadCount = nonMonitorThreadCount; + } + + /** + * "Quiet" a script's glow: stop the VM from generating glow/unglow events + * about that script. Use when a script has just been deleted, but we may + * still be tracking glow data about it. + * @param {!string} scriptBlockId Id of top-level block in script to quiet. + */ + quietGlow (scriptBlockId) { + const index = this._scriptGlowsPreviousFrame.indexOf(scriptBlockId); + if (index > -1) { + this._scriptGlowsPreviousFrame.splice(index, 1); + } + } + + /** + * Emit feedback for block glowing (used in the sequencer). + * @param {?string} blockId ID for the block to update glow + * @param {boolean} isGlowing True to turn on glow; false to turn off. + */ + glowBlock (blockId, isGlowing) { + if (isGlowing) { + this.emit(Runtime.BLOCK_GLOW_ON, {id: blockId}); + } else { + this.emit(Runtime.BLOCK_GLOW_OFF, {id: blockId}); + } + } + + /** + * Emit feedback for script glowing. + * @param {?string} topBlockId ID for the top block to update glow + * @param {boolean} isGlowing True to turn on glow; false to turn off. + */ + glowScript (topBlockId, isGlowing) { + if (isGlowing) { + this.emit(Runtime.SCRIPT_GLOW_ON, {id: topBlockId}); + } else { + this.emit(Runtime.SCRIPT_GLOW_OFF, {id: topBlockId}); + } + } + + /** + * Emit whether blocks are being dragged over gui + * @param {boolean} areBlocksOverGui True if blocks are dragged out of blocks workspace, false otherwise + */ + emitBlockDragUpdate (areBlocksOverGui) { + this.emit(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui); + } + + /** + * Emit event to indicate that the block drag has ended with the blocks outside the blocks workspace + * @param {Array.} blocks The set of blocks dragged to the GUI + * @param {string} topBlockId The original id of the top block being dragged + */ + emitBlockEndDrag (blocks, topBlockId) { + this.emit(Runtime.BLOCK_DRAG_END, blocks, topBlockId); + } + + /** + * Emit value for reporter to show in the blocks. + * @param {string} blockId ID for the block. + * @param {string} value Value to show associated with the block. + */ + visualReport (blockId, value) { + this.emit(Runtime.VISUAL_REPORT, {id: blockId, value}); + } + + /** + * Add a monitor to the state. If the monitor already exists in the state, + * updates those properties that are defined in the given monitor record. + * @param {!MonitorRecord} monitor Monitor to add. + */ + requestAddMonitor (monitor) { + const id = monitor.get('id'); + if (!this.requestUpdateMonitor(monitor)) { // update monitor if it exists in the state + // if the monitor did not exist in the state, add it + this._monitorState = this._monitorState.set(id, monitor); + } + } + + /** + * Update a monitor in the state and report success/failure of update. + * @param {!Map} monitor Monitor values to update. Values on the monitor with overwrite + * values on the old monitor with the same ID. If a value isn't defined on the new monitor, + * the old monitor will keep its old value. + * @return {boolean} true if monitor exists in the state and was updated, false if it did not exist. + */ + requestUpdateMonitor (monitor) { + const id = monitor.get('id'); + if (this._monitorState.has(id)) { + this._monitorState = + // Use mergeWith here to prevent undefined values from overwriting existing ones + this._monitorState.set(id, this._monitorState.get(id).mergeWith((prev, next) => { + if (typeof next === 'undefined' || next === null) { + return prev; + } + return next; + }, monitor)); + return true; + } + return false; + } + + /** + * Removes a monitor from the state. Does nothing if the monitor already does + * not exist in the state. + * @param {!string} monitorId ID of the monitor to remove. + */ + requestRemoveMonitor (monitorId) { + this._monitorState = this._monitorState.delete(monitorId); + } + + /** + * Hides a monitor and returns success/failure of action. + * @param {!string} monitorId ID of the monitor to hide. + * @return {boolean} true if monitor exists and was updated, false otherwise + */ + requestHideMonitor (monitorId) { + return this.requestUpdateMonitor(new Map([ + ['id', monitorId], + ['visible', false] + ])); + } + + /** + * Shows a monitor and returns success/failure of action. + * not exist in the state. + * @param {!string} monitorId ID of the monitor to show. + * @return {boolean} true if monitor exists and was updated, false otherwise + */ + requestShowMonitor (monitorId) { + return this.requestUpdateMonitor(new Map([ + ['id', monitorId], + ['visible', true] + ])); + } + + /** + * Removes all monitors with the given target ID from the state. Does nothing if + * the monitor already does not exist in the state. + * @param {!string} targetId Remove all monitors with given target ID. + */ + requestRemoveMonitorByTargetId (targetId) { + this._monitorState = this._monitorState.filterNot(value => value.targetId === targetId); + } + + /** + * Get a target by its id. + * @param {string} targetId Id of target to find. + * @return {?Target} The target, if found. + */ + getTargetById (targetId) { + for (let i = 0; i < this.targets.length; i++) { + const target = this.targets[i]; + if (target.id === targetId) { + return target; + } + } + } + + /** + * Get the first original (non-clone-block-created) sprite given a name. + * @param {string} spriteName Name of sprite to look for. + * @return {?Target} Target representing a sprite of the given name. + */ + getSpriteTargetByName (spriteName) { + const json = validateJSON(spriteName); + if (json.id) return this.getTargetById(json.id); + for (let i = 0; i < this.targets.length; i++) { + const target = this.targets[i]; + if (target.isStage) { + continue; + } + if (target.sprite && target.sprite.name === spriteName) { + return target; + } + } + } + + /** + * Get a target by its drawable id. + * @param {number} drawableID drawable id of target to find + * @return {?Target} The target, if found + */ + getTargetByDrawableId (drawableID) { + for (let i = 0; i < this.targets.length; i++) { + const target = this.targets[i]; + if (target.drawableID === drawableID) return target; + } + } + + /** + * Update the clone counter to track how many clones are created. + * @param {number} changeAmount How many clones have been created/destroyed. + */ + changeCloneCounter (changeAmount) { + this._cloneCounter += changeAmount; + } + + /** + * Return whether there are clones available. + * @return {boolean} True until the number of clones hits runtimeOptions.maxClones + */ + clonesAvailable () { + return this._cloneCounter < this.runtimeOptions.maxClones; + } + + /** + * Report that the project has loaded in the Virtual Machine. + * and also handle the parsing of custom values to allow for + * minimal code when making cross-target refences + */ + emitProjectLoaded () { + for (const target of this.targets) { + for (const varId in target.variables) { + const variable = target.variables[varId]; + if (variable.type === Variable.LIST_TYPE) { + for (const idx in variable.value) { + const item = variable.value[idx]; + if (item.customType) { + const {deserialize} = this.serializers[item.typeId]; + variable.value[idx] = deserialize(item.serialized, target); + } + } + } + if (variable.value?.customType) { + const customData = variable.value; + const {deserialize} = this.serializers[customData.typeId]; + variable.value = deserialize(customData.serialized, target); + } + } + } + this.emit(Runtime.PROJECT_LOADED); + } + + /** + * Report that the project has changed in a way that would affect serialization + */ + emitProjectChanged () { + this.emit(Runtime.PROJECT_CHANGED); + } + + /** + * Report that a new target has been created, possibly by cloning an existing target. + * @param {Target} newTarget - the newly created target. + * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any. + * @fires Runtime#targetWasCreated + */ + fireTargetWasCreated (newTarget, sourceTarget) { + this.emit('targetWasCreated', newTarget, sourceTarget); + } + + /** + * Report that a clone target is being removed. + * @param {Target} target - the target being removed + * @fires Runtime#targetWasRemoved + */ + fireTargetWasRemoved (target) { + this.emit('targetWasRemoved', target); + } + + /** + * Get the branch for a particular C-shaped block, and it's target. + * @param {?string} id ID for block to get the branch for. + * @param {?string} branchId Which branch to select (e.g. for if-else). + * @return {?string} ID of block in the branch. + */ + getBranchAndTarget (id, branchId) { + for (const target of this.targets) { + const result = target.blocks.getBranch(id, branchId); + if (result) { + return [result, target]; + } + } + return null; + } + + /** + * gets a screen, if no screen can be found it will create one + * @param {string} screen the screen to get + * @returns {Object} the screen state object + */ + getCamera(screen) { + if (typeof this.cameraStates[screen] !== 'object') { + this.cameraStates[screen] = { + pos: [0, 0], + dir: 0, + scale: 1 + }; + } + return this.cameraStates[screen]; + } + + /** + * assign new camera state options + * @param {string} screen the screen + * @param {Object} state the state to apply to the screen + * @param {boolean} silent if we should emit an event because of this change + */ + updateCamera(screen, state, silent) { + if (state.dir) state.dir = MathUtil.wrapClamp(state.dir, -179, 180); + if (typeof this.cameraStates[screen] !== 'object') { + this.cameraStates[screen] = { + pos: [0, 0], + dir: 0, + scale: 1 + }; + } + this.cameraStates[screen] = state = + Object.assign(this.cameraStates[screen], state); + if (!silent ?? state.silent) this.emitCameraChanged(screen); + } + emitCameraChanged(screen) { + for (let i = 0; i < this.targets.length; i++) + if (this.targets[i].cameraBound === screen) + this.targets[i].cameraUpdateEvent(); + this.emit(Runtime.CAMERA_CHANGED, screen); + this.requestRedraw(); + } + + /** + * Get a target representing the Scratch stage, if one exists. + * @return {?Target} The target, if found. + */ + getTargetForStage () { + if (this._stageTarget) { + return this._stageTarget; + } + for (let i = 0; i < this.targets.length; i++) { + const target = this.targets[i]; + if (target.isStage) { + this._stageTarget = target; + return target; + } + } + } + + /** + * Get the editing target. + * @return {?Target} The editing target. + */ + getEditingTarget () { + return this._editingTarget; + } + + getAllVarNamesOfType (varType) { + let varNames = []; + for (const target of this.targets) { + const targetVarNames = target.getAllVariableNamesInScopeByType(varType, true); + varNames = varNames.concat(targetVarNames); + } + return varNames; + } + + /** + * Get the label or label function for an opcode + * @param {string} extendedOpcode - the opcode you want a label for + * @return {object} - object with label and category + * @property {string} category - the category for this opcode + * @property {Function} [labelFn] - function to generate the label for this opcode + * @property {string} [label] - the label for this opcode if `labelFn` is absent + */ + getLabelForOpcode (extendedOpcode) { + const [category, opcode] = StringUtil.splitFirst(extendedOpcode, '_'); + if (!(category && opcode)) return; + + const categoryInfo = this._blockInfo.find(ci => ci.id === category); + if (!categoryInfo) return; + + const block = categoryInfo.blocks.find(b => b.info.opcode === opcode); + if (!block) return; + + // TODO: we may want to format the label in a locale-specific way. + return { + category: 'extension', // This assumes that all extensions have the same monitor color. + label: `${categoryInfo.name}: ${block.info.text}` + }; + } + + /** + * Create a new global variable avoiding conflicts with other variable names. + * @param {string} variableName The desired variable name for the new global variable. + * This can be turned into a fresh name as necessary. + * @param {string} optVarId An optional ID to use for the variable. A new one will be generated + * if a falsey value for this parameter is provided. + * @param {string} optVarType The type of the variable to create. Defaults to Variable.SCALAR_TYPE. + * @return {Variable} The new variable that was created. + */ + createNewGlobalVariable (variableName, optVarId, optVarType) { + const varType = (typeof optVarType === 'string') ? optVarType : Variable.SCALAR_TYPE; + const allVariableNames = this.getAllVarNamesOfType(varType); + const newName = StringUtil.unusedName(variableName, allVariableNames); + + const variable = this.newVariableInstance(varType, optVarId || uid(), newName); + const stage = this.getTargetForStage(); + stage.variables[variable.id] = variable; + return variable; + } + + /** + * Tell the runtime to request a redraw. + * Use after a clone/sprite has completed some visible operation on the stage. + */ + requestRedraw () { + this.redrawRequested = true; + } + + /** + * Emit a targets update at the end of the step if the provided target is + * the original sprite + * @param {!Target} target Target requesting the targets update + */ + requestTargetsUpdate (target) { + if (!target.isOriginal) return; + this._refreshTargets = true; + } + + /** + * Emit an event that indicates that the blocks on the workspace need updating. + */ + requestBlocksUpdate () { + this.emit(Runtime.BLOCKS_NEED_UPDATE); + } + + /** + * Emit an event that indicates that the toolbox extension blocks need updating. + */ + requestToolboxExtensionsUpdate () { + this.emit(Runtime.TOOLBOX_EXTENSIONS_NEED_UPDATE); + } + + /** + * Set up timers to repeatedly step in a browser. + */ + start () { + // Do not start if we are already running + if (this.frameLoop.running) return; + this.frameLoop.start(); + this.emit(Runtime.RUNTIME_STARTED); + } + + /** + * tw: Stop the tick loop + * Note: This only stops the loop. It will not stop any threads the next time the VM starts + */ + stop () { + if (!this.frameLoop.running) { + return; + } + this.frameLoop.stop(); + this.emit(Runtime.RUNTIME_STOPPED); + } + + /** + * Turn on profiling. + * @param {Profiler/FrameCallback} onFrame A callback handle passed a + * profiling frame when the profiler reports its collected data. + */ + enableProfiling (onFrame) { + if (Profiler.available()) { + this.profiler = new Profiler(onFrame); + } + } + + /** + * Turn off profiling. + */ + disableProfiling () { + this.profiler = null; + } + + /** + * Update a millisecond timestamp value that is saved on the Runtime. + * This value is helpful in certain instances for compatibility with Scratch 2, + * which sometimes uses a `currentMSecs` timestamp value in Interpreter.as + */ + updateCurrentMSecs () { + this.currentMSecs = Date.now(); + } + + updatePrivacy () { + const enforceRestrictions = ( + this.enforcePrivacy && + Object.values(this.externalCommunicationMethods).some(i => i) + ); + if (this.renderer && this.renderer.setPrivateSkinAccess) { + this.renderer.setPrivateSkinAccess(!enforceRestrictions); + } + } + + /** + * @param {boolean} enabled True if restrictions should be enforced to protect user privacy. + */ + setEnforcePrivacy (enabled) { + this.enforcePrivacy = enabled; + this.updatePrivacy(); + } + + /** + * @param {string} method Name of the method in Runtime.externalCommunicationMethods + * @param {boolean} enabled True if the feature is enabled. + */ + setExternalCommunicationMethod (method, enabled) { + if (!Object.prototype.hasOwnProperty.call(this.externalCommunicationMethods, method)) { + throw new Error(`Unknown method: ${method}`); + } + this.externalCommunicationMethods[method] = enabled; + this.updatePrivacy(); + } +} + +/** + * Event fired after a new target has been created, possibly by cloning an existing target. + * + * @event Runtime#targetWasCreated + * @param {Target} newTarget - the newly created target. + * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any. + */ + +module.exports = Runtime; diff --git a/local-scratch-vm/src/engine/scratch-blocks-constants.js b/local-scratch-vm/src/engine/scratch-blocks-constants.js new file mode 100644 index 0000000000000000000000000000000000000000..ee558549705e5d3d200c6a3a0c05947f7a5f663c --- /dev/null +++ b/local-scratch-vm/src/engine/scratch-blocks-constants.js @@ -0,0 +1,27 @@ +/** + * These constants are copied from scratch-blocks/core/constants.js + * @TODO find a way to require() these straight from scratch-blocks... maybe make a scratch-blocks/dist/constants.js? + * @readonly + * @enum {int} + */ +const ScratchBlocksConstants = { + /** + * ENUM for output shape: hexagonal (booleans/predicates). + * @const + */ + OUTPUT_SHAPE_HEXAGONAL: 1, + + /** + * ENUM for output shape: rounded (numbers). + * @const + */ + OUTPUT_SHAPE_ROUND: 2, + + /** + * ENUM for output shape: squared (any/all values; strings). + * @const + */ + OUTPUT_SHAPE_SQUARE: 3 +}; + +module.exports = ScratchBlocksConstants; diff --git a/local-scratch-vm/src/engine/sequencer.js b/local-scratch-vm/src/engine/sequencer.js new file mode 100644 index 0000000000000000000000000000000000000000..9072a0a381ee0367b95c9c397b2aed4f5a997839 --- /dev/null +++ b/local-scratch-vm/src/engine/sequencer.js @@ -0,0 +1,378 @@ +const Timer = require('../util/timer'); +const Thread = require('./thread'); +const execute = require('./execute.js'); +const compilerExecute = require('../compiler/jsexecute'); + +/** + * 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} + */ +let stepThreadProfilerId = -1; + +/** + * Profiler frame ID for stepThreadsInnerProfilerFrame. + * @type {number} + */ +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; + + this.activeThread = null; + } + + /** + * 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.} List of inactive threads after stepping. + */ + stepThreads () { + // Work time is 75% of the thread stepping interval. + const WORK_TIME = 0.75 * this.runtime.currentStepTime; + // For compatibility with Scatch 2, update the millisecond clock + // on the Runtime once per step (see Interpreter.as in Scratch 2 + // for original use of `currentMSecs`) + this.runtime.updateCurrentMSecs(); + // 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 = []; + // 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; + let stoppedThread = false; + // Attempt to run each thread one time. + const threads = this.runtime.threads; + for (let i = 0; i < threads.length; i++) { + const activeThread = this.activeThread = threads[i]; + // Check if the thread is done so it is not executed. + if (activeThread.stack.length === 0 || + activeThread.status === Thread.STATUS_DONE) { + // Finished with this thread. + stoppedThread = true; + continue; + } + if (activeThread.status === Thread.STATUS_PAUSED) { + if (activeThread.timer && !activeThread.timer._pausedTime) { + activeThread.timer.pause(); + } + 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. + if (this.runtime.profiler !== null) { + if (stepThreadProfilerId === -1) { + stepThreadProfilerId = this.runtime.profiler.idByName(stepThreadProfilerFrame); + } + + // Increment the number of times stepThread is called. + this.runtime.profiler.increment(stepThreadProfilerId); + } + this.stepThread(activeThread); + activeThread.warpTimer = null; + } + if (activeThread.status === Thread.STATUS_RUNNING) { + numActiveThreads++; + } + // Check if the thread completed while it just stepped to make + // sure we remove it before the next iteration of all threads. + if (activeThread.stack.length === 0 || + activeThread.status === Thread.STATUS_DONE) { + // Finished with this thread. + stoppedThread = true; + } + } + // 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`. + if (stoppedThread) { + let nextActiveThread = 0; + for (let i = 0; i < this.runtime.threads.length; i++) { + const thread = this.runtime.threads[i]; + if (thread.stack.length !== 0 && + thread.status !== Thread.STATUS_DONE) { + this.runtime.threads[nextActiveThread] = thread; + nextActiveThread++; + } else { + this.runtime.threadMap.delete(thread.getId()); + doneThreads.push(thread); + } + } + this.runtime.threads.length = nextActiveThread; + } + } + + this.activeThread = null; + + return doneThreads; + } + + /** + * Step the requested thread for as long as necessary. + * @param {!Thread} thread Thread object to step. + */ + stepThread (thread) { + if (thread.isCompiled) { + compilerExecute(thread); + return; + } + + let currentBlockId = thread.peekStack(); + if (!currentBlockId) { + // A "null block" - empty branch. + thread.popStack(); + + // Did the null follow a hat block? + if (thread.stack.length === 0) { + thread.status = Thread.STATUS_DONE; + return; + } + } + // Save the current block ID to notice if we did control flow. + while ((currentBlockId = 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. + if (this.runtime.profiler !== null) { + if (executeProfilerId === -1) { + executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame); + } + + // Increment the number of times execute is called. + this.runtime.profiler.increment(executeProfilerId); + } + if (thread.target === null) { + this.retireThread(thread); + } else { + execute(this, thread); + } + thread.blockGlowInFrame = currentBlockId; + // If the thread has yielded or is waiting, yield to other threads. + if (thread.status === Thread.STATUS_YIELD) { + // Mark as running for next iteration. + thread.status = Thread.STATUS_RUNNING; + // In warp mode, yielded blocks are re-executed immediately. + if (isWarpMode && + thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) { + continue; + } + return; + } else if (thread.status === Thread.STATUS_PROMISE_WAIT) { + // A promise was returned by the primitive. Yield the thread + // until the promise resolves. Promise resolution should reset + // thread.status to Thread.STATUS_RUNNING. + return; + } else if (thread.status === Thread.STATUS_YIELD_TICK) { + // stepThreads will reset the thread to Thread.STATUS_RUNNING + return; + } else if (thread.status === Thread.STATUS_DONE) { + // Nothing more to execute. + return; + } + // If no control flow has happened, switch to next block. + if (thread.peekStack() === currentBlockId) { + thread.goToNextBlock(); + } + // If no next block has been found at this point, look on the stack. + while (!thread.peekStack()) { + thread.popStack(); + + if (thread.stack.length === 0) { + // No more stack to run! + thread.status = Thread.STATUS_DONE; + return; + } + + 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; + } + // Get next block of existing block on the stack. + thread.goToNextBlock(); + } + } + } + + /** + * 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; + } + // 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 { + // 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. + */ + retireThread (thread) { + thread.stack = []; + thread.stackFrame = []; + thread.requestScriptGlowInFrame = false; + thread.status = Thread.STATUS_DONE; + if (thread.isCompiled) { + thread.procedures = null; + thread.generator = null; + } + } +} + +module.exports = Sequencer; diff --git a/local-scratch-vm/src/engine/stage-layering.js b/local-scratch-vm/src/engine/stage-layering.js new file mode 100644 index 0000000000000000000000000000000000000000..a30aa96f7e374b912fb888bbc2efb22d6b7e0588 --- /dev/null +++ b/local-scratch-vm/src/engine/stage-layering.js @@ -0,0 +1,39 @@ +class StageLayering { + static get BACKGROUND_LAYER () { + return 'background'; + } + + static get VIDEO_LAYER () { + return 'video'; + } + + static get PEN_LAYER () { + return 'pen'; + } + + static get SPRITE_LAYER () { + return 'sprite'; + } + + // Order of layer groups relative to each other, + static get LAYER_GROUPS () { + return [ + StageLayering.BACKGROUND_LAYER, + StageLayering.VIDEO_LAYER, + StageLayering.PEN_LAYER, + StageLayering.SPRITE_LAYER + ]; + } + + // Order of layer groups relative to each other, + static get LAYER_GROUPS_PEN () { + return [ + StageLayering.BACKGROUND_LAYER, + StageLayering.VIDEO_LAYER, + StageLayering.SPRITE_LAYER, + StageLayering.PEN_LAYER + ]; + } +} + +module.exports = StageLayering; diff --git a/local-scratch-vm/src/engine/target.js b/local-scratch-vm/src/engine/target.js new file mode 100644 index 0000000000000000000000000000000000000000..c7930844a02dad2a9f2dcd46816daa7c44ce7db9 --- /dev/null +++ b/local-scratch-vm/src/engine/target.js @@ -0,0 +1,794 @@ +const EventEmitter = require('events'); + +const Blocks = require('./blocks'); +const Variable = require('../engine/variable'); +const Comment = require('../engine/comment'); +const uid = require('../util/uid'); +const {Map} = require('immutable'); +const log = require('../util/log'); +const StringUtil = require('../util/string-util'); +const VariableUtil = require('../util/variable-util'); + +/** + * @fileoverview + * A Target is an abstract "code-running" object for the Scratch VM. + * Examples include sprites/clones or potentially physical-world devices. + */ + +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(runtime); + } + + /** + * 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 id. + * @type {Object.} + */ + this.variables = {}; + /** + * Dictionary of comments for this target. + * Key is the comment id. + * @type {Object.} + */ + this.comments = {}; + /** + * 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.} + */ + this._customState = {}; + + /** + * Currently known values for edge-activated hats. + * Keys are block ID for the hat; values are the currently known values. + * @type {Object.} + */ + this._edgeActivatedHatValues = {}; + } + + /** + * Called when the project receives a "green flag." + * @abstract + */ + onGreenFlag () {} + + /** + * Return a human-readable name for this target. + * Target implementations should override this. + * @abstract + * @returns {string} Human-readable name for the target. + */ + getName () { + return this.id; + } + + /** + * Update an edge-activated hat block value. + * @param {!string} blockId ID of hat to store value for. + * @param {*} newValue Value to store for edge-activated hat. + * @return {*} The old value for the edge-activated hat. + */ + updateEdgeActivatedValue (blockId, newValue) { + const oldValue = this._edgeActivatedHatValues[blockId]; + this._edgeActivatedHatValues[blockId] = newValue; + return oldValue; + } + + hasEdgeActivatedValue (blockId) { + return this._edgeActivatedHatValues.hasOwnProperty(blockId); + } + + /** + * Clear all edge-activaed hat values. + */ + clearEdgeActivatedValues () { + this._edgeActivatedHatValues = {}; + } + + /** + * Look up a variable object, first by id, and then by name if the id is not found. + * Create a new variable if both lookups fail. + * @param {string} id Id of the variable. + * @param {string} name Name of the variable. + * @return {!Variable} Variable object. + */ + lookupOrCreateVariable (id, name) { + let variable = this.lookupVariableById(id); + if (variable) return variable; + + variable = this.lookupVariableByNameAndType(name, Variable.SCALAR_TYPE); + 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; + } + + /** + * 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. + */ + 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 variables = Object.values(this.variables); + return variables.find(varData => { + return ( + varData.type === Variable.BROADCAST_MESSAGE_TYPE && + varData.name.toLowerCase() === name.toLowerCase() + ); + }); + } + + /** + * 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 && stage.variables.hasOwnProperty(id)) { + return stage.variables[id]; + } + } + } + + /** + * Look up a variable object by its name and variable type. + * Search begins with local variables; then global variables if a local one + * was not found. + * @param {string} name Name of the variable. + * @param {string} type Type of the variable. Defaults to Variable.SCALAR_TYPE. + * @param {?bool} skipStage Optional flag to skip checking the stage + * @return {?Variable} Variable object if found, or null if not. + */ + lookupVariableByNameAndType(name, type, skipStage) { + if (typeof name !== 'string') return; + if (typeof type !== 'string') type = Variable.SCALAR_TYPE; + skipStage = skipStage || false; + + // Search variables in the current target + const variables = Object.values(this.variables); + const foundInCurrent = variables.find(varData => varData.name === name && varData.type === type); + if (foundInCurrent) return foundInCurrent; + + // Search variables in the stage if applicable + if (!skipStage && this.runtime && !this.isStage) { + const stage = this.runtime.getTargetForStage(); + if (stage) { + const stageVariables = Object.values(stage.variables); + const foundInStage = stageVariables.find(varData => varData.name === name && varData.type === type); + if (foundInStage) return foundInStage; + } + } + return null; + } + + /** + * 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) { + let list = this.lookupVariableById(id); + if (list) return list; + + list = this.lookupVariableByNameAndType(name, Variable.LIST_TYPE); + 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' + * @param {boolean} isCloud Whether the variable to create has the isCloud flag set. + * Additional checks are made that the variable can be created as a cloud variable. + */ + createVariable (id, name, type, isCloud) { + if (!this.variables.hasOwnProperty(id)) { + const newVariable = this.runtime.newVariableInstance(type, id, name, false); + if (isCloud && this.isStage && this.runtime.canAddCloudVariable()) { + newVariable.isCloud = true; + this.runtime.addCloudVariable(); + this.runtime.ioDevices.cloud.requestCreateVariable(newVariable); + } + this.variables[id] = newVariable; + } + } + + /** + * Creates a comment with the given properties. + * @param {string} id Id of the comment. + * @param {string} blockId Optional id of the block the comment is attached + * to if it is a block comment. + * @param {string} text The text the comment contains. + * @param {number} x The x coordinate of the comment on the workspace. + * @param {number} y The y coordinate of the comment on the workspace. + * @param {number} width The width of the comment when it is full size + * @param {number} height The height of the comment when it is full size + * @param {boolean} minimized Whether the comment is minimized. + */ + createComment (id, blockId, text, x, y, width, height, minimized) { + if (!this.comments.hasOwnProperty(id)) { + const newComment = new Comment(id, text, x, y, + width, height, minimized); + if (blockId) { + newComment.blockId = blockId; + const blockWithComment = this.blocks.getBlock(blockId); + if (blockWithComment) { + blockWithComment.comment = id; + } else { + log.warn(`Could not find block with id ${blockId + } associated with commentId: ${id}`); + } + } + this.comments[id] = newComment; + } + } + + /** + * Renames the variable with the given id to newName. + * @param {string} id Id of variable to rename. + * @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) { + const oldName = variable.name; + variable.name = newName; + + if (this.runtime) { + if (variable.isCloud && this.isStage) { + this.runtime.ioDevices.cloud.requestRenameVariable(oldName, newName); + } + + if (variable.type === Variable.SCALAR_TYPE) { + // sensing__of may be referencing to this variable. + // Change the reference. + let blockUpdated = false; + this.runtime.targets.forEach(t => { + blockUpdated = t.blocks.updateSensingOfReference( + oldName, + newName, + this.isStage ? '_stage_' : this.getName() + ) || blockUpdated; + }); + // Request workspace change only if sensing_of blocks were actually updated. + if (blockUpdated) this.runtime.requestBlocksUpdate(); + } + + const blocks = this.runtime.monitorBlocks; + blocks.changeBlock({ + id: id, + element: 'field', + name: variable.type === Variable.LIST_TYPE ? 'LIST' : 'VARIABLE', + value: id + }, this.runtime); + const monitorBlock = blocks.getBlock(variable.id); + if (monitorBlock) { + this.runtime.requestUpdateMonitor(Map({ + id: id, + params: blocks._getBlockParams(monitorBlock) + })); + } + } + + } + } + } + + /** + * Removes the variable with the given id from the dictionary of variables. + * @param {string} id Id of variable to delete. + */ + deleteVariable (id) { + if (this.variables.hasOwnProperty(id)) { + // Get info about the variable before deleting it + const deletedVariableName = this.variables[id].name; + const deletedVariableWasCloud = this.variables[id].isCloud; + delete this.variables[id]; + if (this.runtime) { + if (deletedVariableWasCloud && this.isStage) { + this.runtime.ioDevices.cloud.requestDeleteVariable(deletedVariableName); + this.runtime.removeCloudVariable(); + } + this.runtime.monitorBlocks.deleteBlock(id); + this.runtime.requestRemoveMonitor(id); + } + } + } + + /** + * Remove this target's monitors from the runtime state and remove the + * target-specific monitored blocks (e.g. local variables, global variables for the stage, x-position). + * NOTE: This does not delete any of the stage monitors like backdrop name. + */ + deleteMonitors () { + this.runtime.requestRemoveMonitorByTargetId(this.id); + let targetSpecificMonitorBlockIds; + if (this.isStage) { + // This only deletes global variables and not other stage monitors like backdrop number. + targetSpecificMonitorBlockIds = Object.keys(this.variables); + } else { + targetSpecificMonitorBlockIds = Object.keys(this.runtime.monitorBlocks._blocks) + .filter(key => this.runtime.monitorBlocks._blocks[key].targetId === this.id); + } + for (const blockId of targetSpecificMonitorBlockIds) { + this.runtime.monitorBlocks.deleteBlock(blockId); + } + } + + /** + * Create a clone of the variable with the given id from the dictionary of + * this target's variables. + * @param {string} id Id of variable to duplicate. + * @param {boolean=} optKeepOriginalId Optional flag to keep the original variable ID + * for the duplicate variable. This is necessary when cloning a sprite, for example. + * @return {?Variable} The duplicated variable, or null if + * the original variable was not found. + */ + duplicateVariable (id, optKeepOriginalId) { + if (this.variables.hasOwnProperty(id)) { + const originalVariable = this.variables[id]; + const newVariable = this.runtime.newVariableInstance( + originalVariable.type, + optKeepOriginalId ? id : null, // conditionally keep original id or generate a new one + originalVariable.name, + originalVariable.isCloud + ); + if (newVariable.type === Variable.LIST_TYPE) { + newVariable.value = originalVariable.value.slice(0); + } else { + newVariable.value = originalVariable.value; + } + return newVariable; + } + return null; + } + + /** + * Duplicate the dictionary of this target's variables as part of duplicating. + * this target or making a clone. + * @param {object=} optBlocks Optional block container for the target being duplicated. + * If provided, new variables will be generated with new UIDs and any variable references + * in this blocks container will be updated to refer to the corresponding new IDs. + * @return {object} The duplicated dictionary of variables + */ + duplicateVariables (optBlocks) { + let allVarRefs; + if (optBlocks) { + allVarRefs = optBlocks.getAllVariableAndListReferences(); + } + return Object.keys(this.variables).reduce((accum, varId) => { + const newVariable = this.duplicateVariable(varId, !optBlocks); + accum[newVariable.id] = newVariable; + if (optBlocks && allVarRefs) { + const currVarRefs = allVarRefs[varId]; + if (currVarRefs) { + this.mergeVariables(varId, newVariable.id, currVarRefs); + } + } + return accum; + }, {}); + } + + /** + * 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 = {}; + + if (this.runtime) { + this.runtime.removeExecutable(this); + } + } + + // Variable Conflict Resolution Helpers + + /** + * Get the names of all the variables of the given type that are in scope for this target. + * For targets that are not the stage, this includes any target-specific + * variables as well as any stage variables unless the skipStage flag is true. + * For the stage, this is all stage variables. + * @param {string} type The variable type to search for; defaults to Variable.SCALAR_TYPE + * @param {?bool} skipStage Optional flag to skip the stage. + * @return {Array} A list of variable names + */ + getAllVariableNamesInScopeByType (type, skipStage) { + if (typeof type !== 'string') type = Variable.SCALAR_TYPE; + skipStage = skipStage || false; + const targetVariables = Object.values(this.variables) + .filter(v => v.type === type) + .map(variable => variable.name); + if (skipStage || this.isStage || !this.runtime) { + return targetVariables; + } + const stage = this.runtime.getTargetForStage(); + const stageVariables = stage.getAllVariableNamesInScopeByType(type); + return targetVariables.concat(stageVariables); + } + + /** + * Merge variable references with another variable. + * @param {string} idToBeMerged ID of the variable whose references need to be updated + * @param {string} idToMergeWith ID of the variable that the old references should be replaced with + * @param {?Array} optReferencesToUpdate Optional context of the change. + * Defaults to all the blocks in this target. + * @param {?string} optNewName New variable name to merge with. The old + * variable name in the references being updated should be replaced with this new name. + * If this parameter is not provided or is '', no name change occurs. + */ + mergeVariables (idToBeMerged, idToMergeWith, optReferencesToUpdate, optNewName) { + const referencesToChange = optReferencesToUpdate || + // TODO should there be a separate helper function that traverses the blocks + // for all references for a given ID instead of doing the below..? + this.blocks.getAllVariableAndListReferences()[idToBeMerged]; + + VariableUtil.updateVariableIdentifiers(referencesToChange, idToMergeWith, optNewName); + } + + /** + * Share a local variable (and given references for that variable) to the stage. + * @param {string} varId The ID of the variable to share. + * @param {Array} varRefs The list of variable references being shared, + * that reference the given variable ID. The names and IDs of these variable + * references will be updated to refer to the new (or pre-existing) global variable. + */ + shareLocalVariableToStage (varId, varRefs) { + if (!this.runtime) return; + const variable = this.variables[varId]; + if (!variable) { + log.warn(`Cannot share a local variable to the stage if it's not local.`); + return; + } + const stage = this.runtime.getTargetForStage(); + // If a local var is being shared with the stage, + // sharing will make the variable global, resulting in a conflict + // with the existing local variable. Preemptively Resolve this conflict + // by renaming the new global variable. + + // First check if we've already done the local to global transition for this + // variable. If we have, merge it with the global variable we've already created. + const varIdForStage = `StageVarFromLocal_${varId}`; + let stageVar = stage.lookupVariableById(varIdForStage); + // If a global var doesn't already exist, create a new one with a fresh name. + // Use the ID we created above so that we can lookup this new variable in the + // future if we decide to share this same variable again. + if (!stageVar) { + const varName = variable.name; + const varType = variable.type; + + const newStageName = `Stage: ${varName}`; + stageVar = this.runtime.createNewGlobalVariable(newStageName, varIdForStage, varType); + } + // Update all variable references to use the new name and ID + this.mergeVariables(varId, stageVar.id, varRefs, stageVar.name); + } + + /** + * Share a local variable with a sprite, merging with one of the same name and + * type if it already exists on the sprite, or create a new one. + * @param {string} varId Id of the variable to share + * @param {Target} sprite The sprite to share the variable with + * @param {Array} varRefs A list of all the variable references currently being shared. + */ + shareLocalVariableToSprite (varId, sprite, varRefs) { + if (!this.runtime) return; + if (this.isStage) return; + const variable = this.variables[varId]; + if (!variable) { + log.warn(`Tried to call 'shareLocalVariableToSprite' with a non-local variable.`); + return; + } + const varName = variable.name; + const varType = variable.type; + // Check if the receiving sprite already has a variable of the same name and type + // and use the existing variable, otherwise create a new one. + const existingLocalVar = sprite.lookupVariableByNameAndType(varName, varType); + let newVarId; + if (existingLocalVar) { + newVarId = existingLocalVar.id; + } else { + const newVar = this.runtime.newVariableInstance(varType, null, varName); + newVarId = newVar.id; + sprite.variables[newVarId] = newVar; + } + + // Merge with the local variable on the new sprite. + this.mergeVariables(varId, newVarId, varRefs); + } + + /** + * Given a list of variable referencing fields, shares those variables with + * the target with the provided id, resolving any variable conflicts that arise + * using the following rules: + * + * If this target is the stage, exit. There are no conflicts that arise + * from sharing variables from the stage to another sprite. The variables + * already exist globally, so no further action is needed. + * + * If a variable being referenced is a global variable, do nothing. The + * global variable already exists so no further action is needed. + * + * If a variable being referenced is local, and + * 1) The receiving target is a sprite: + * create a new local variable or merge with an existing local variable + * of the same name and type. Update all the referencing fields + * for the original variable to reference the new variable. + * 2) The receiving target is the stage: + * Create a new global variable with a fresh name and update all the referencing + * fields to reference the new variable. + * + * @param {Array} blocks The blocks containing + * potential conflicting references to variables. + * @param {Target} receivingTarget The target receiving the variables + */ + resolveVariableSharingConflictsWithTarget (blocks, receivingTarget) { + if (this.isStage) return; + + // Get all the variable references in the given list of blocks + const allVarListRefs = this.blocks.getAllVariableAndListReferences(blocks); + + // For all the variables being referenced, check for which ones are local + // to this target, and resolve conflicts based on whether the receiving target + // is a sprite (with a conflicting local variable) or whether it is + // the stage (which cannot have local variables) + for (const varId in allVarListRefs) { + const currVar = this.variables[varId]; + if (!currVar) continue; // The current variable is global, there shouldn't be any conflicts here, skip it. + + // Get the list of references for the current variable id + const currVarListRefs = allVarListRefs[varId]; + + if (receivingTarget.isStage) { + this.shareLocalVariableToStage(varId, currVarListRefs); + } else { + this.shareLocalVariableToSprite(varId, receivingTarget, currVarListRefs); + } + } + } + + /** + * Fixes up variable references in this target avoiding conflicts with + * pre-existing variables in the same scope. + * This is used when uploading this target as a new sprite into an existing + * project, where the new sprite may contain references + * to variable names that already exist as global variables in the project + * (and thus are in scope for variable references in the given sprite). + * + * If this target has a block that references an existing global variable and that + * variable *does not* exist in this target (e.g. it was a global variable in the + * project the sprite was originally exported from), merge the variables. This entails + * fixing the variable references in this sprite to reference the id of the pre-existing global variable. + * + * If this target has a block that references an existing global variable and that + * variable does exist in the target itself (e.g. it's a local variable in the sprite being uploaded), + * then the local variable is renamed to distinguish itself from the pre-existing variable. + * All blocks that reference the local variable will be updated to use the new name. + */ + // TODO (#1360) This function is too long, add some helpers for the different chunks and cases... + fixUpVariableReferences () { + if (!this.runtime) return; // There's no runtime context to conflict with + if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded) + const stage = this.runtime.getTargetForStage(); + if (!stage || !stage.variables) return; + + const renameConflictingLocalVar = (id, name, type) => { + const conflict = stage.lookupVariableByNameAndType(name, type); + if (conflict) { + const newName = StringUtil.unusedName( + `${this.getName()}: ${name}`, + this.getAllVariableNamesInScopeByType(type)); + this.renameVariable(id, newName); + return newName; + } + return null; + }; + + const allReferences = this.blocks.getAllVariableAndListReferences(); + const unreferencedLocalVarIds = []; + if (Object.keys(this.variables).length > 0) { + for (const localVarId in this.variables) { + if (!this.variables.hasOwnProperty(localVarId)) continue; + if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId); + } + } + const conflictIdsToReplace = Object.create(null); + const conflictNamesToReplace = Object.create(null); + + // Cache the list of all variable names by type so that we don't need to + // re-calculate this in every iteration of the following loop. + const varNamesByType = {}; + const allVarNames = type => { + const namesOfType = varNamesByType[type]; + if (namesOfType) return namesOfType; + varNamesByType[type] = this.runtime.getAllVarNamesOfType(type); + return varNamesByType[type]; + }; + + for (const varId in allReferences) { + // We don't care about which var ref we get, they should all have the same var info + const varRef = allReferences[varId][0]; + const varName = varRef.referencingField.value; + const varType = varRef.type; + if (this.lookupVariableById(varId)) { + // Found a variable with the id in either the target or the stage, + // figure out which one. + if (this.variables.hasOwnProperty(varId)) { + // If the target has the variable, then check whether the stage + // has one with the same name and type. If it does, then rename + // this target specific variable so that there is a distinction. + const newVarName = renameConflictingLocalVar(varId, varName, varType); + + if (newVarName) { + // We are not calling this.blocks.updateBlocksAfterVarRename + // here because it will search through all the blocks. We already + // have access to all the references for this var id. + allReferences[varId].map(ref => { + ref.referencingField.value = newVarName; + return ref; + }); + } + } + } else { + // We didn't find the referenced variable id anywhere, + // Treat it as a reference to a global variable (from the original + // project this sprite was exported from). + // Check for whether a global variable of the same name and type exists, + // and if so, track it to merge with the existing global in a second pass of the blocks. + const existingVar = stage.lookupVariableByNameAndType(varName, varType); + if (existingVar) { + if (!conflictIdsToReplace[varId]) { + conflictIdsToReplace[varId] = existingVar.id; + } + } else { + // A global variable with the same name did not already exist, + // create a new one such that it does not conflict with any + // names of local variables of the same type. + const allNames = allVarNames(varType); + const freshName = StringUtil.unusedName(varName, allNames); + stage.createVariable(varId, freshName, varType); + if (!conflictNamesToReplace[varId]) { + conflictNamesToReplace[varId] = freshName; + } + } + } + } + // Rename any local variables that were missed above because they aren't + // referenced by any blocks + for (const id in unreferencedLocalVarIds) { + const varId = unreferencedLocalVarIds[id]; + const name = this.variables[varId].name; + const type = this.variables[varId].type; + renameConflictingLocalVar(varId, name, type); + } + // Handle global var conflicts with existing global vars (e.g. a sprite is uploaded, and has + // blocks referencing some variable that the sprite does not own, and this + // variable conflicts with a global var) + // In this case, we want to merge the new variable referenes with the + // existing global variable + for (const conflictId in conflictIdsToReplace) { + const existingId = conflictIdsToReplace[conflictId]; + const referencesToUpdate = allReferences[conflictId]; + this.mergeVariables(conflictId, existingId, referencesToUpdate); + } + + // Handle global var conflicts existing local vars (e.g a sprite is uploaded, + // and has blocks referencing some variable that the sprite does not own, and this + // variable conflcits with another sprite's local var). + // In this case, we want to go through the variable references and update + // the name of the variable in that reference. + for (const conflictId in conflictNamesToReplace) { + const newName = conflictNamesToReplace[conflictId]; + const referencesToUpdate = allReferences[conflictId]; + referencesToUpdate.map(ref => { + ref.referencingField.value = newName; + return ref; + }); + } + } + +} + +module.exports = Target; diff --git a/local-scratch-vm/src/engine/thread.js b/local-scratch-vm/src/engine/thread.js new file mode 100644 index 0000000000000000000000000000000000000000..3253293650ec48b519d2513bf9e5687338845148 --- /dev/null +++ b/local-scratch-vm/src/engine/thread.js @@ -0,0 +1,563 @@ +const log = require('../util/log'); + +/** + * Recycle bin for empty stackFrame objects + * @type Array<_StackFrame> + */ +const _stackFrameFreeList = []; + +/** + * A frame used for each level of the stack. A general purpose + * place to store a bunch of execution context and parameters + * @param {boolean} warpMode Whether this level of the stack is warping + * @constructor + * @private + */ +class _StackFrame { + constructor (warpMode) { + /** + * Whether this level of the stack is a loop. + * @type {boolean} + */ + this.isLoop = false; + + /** + * Whether this level is in warp mode. Is set by some legacy blocks and + * "turbo mode" + * @type {boolean} + */ + this.warpMode = warpMode; + + /** + * Reported value from just executed block. + * @type {Any} + */ + this.justReported = null; + + /** + * The active block that is waiting on a promise. + * @type {string} + */ + this.reporting = ''; + + /** + * Persists reported inputs during async block. + * @type {Object} + */ + this.reported = null; + + /** + * Name of waiting reporter. + * @type {string} + */ + this.waitingReporter = null; + + /** + * Procedure parameters. + * @type {Object} + */ + this.params = null; + + /** + * A context passed to block implementations. + * @type {Object} + */ + this.executionContext = null; + + /** + * Internal block object being executed. This is *not* the same as the object found + * in target.blocks. + * @type {object} + */ + this.op = null; + } + + /** + * Reset all properties of the frame to pristine null and false states. + * Used to recycle. + * @return {_StackFrame} this + */ + reset () { + + this.isLoop = false; + this.warpMode = false; + this.justReported = null; + this.reported = null; + this.waitingReporter = null; + this.params = null; + this.executionContext = null; + this.op = null; + + return this; + } + + /** + * Reuse an active stack frame in the stack. + * @param {?boolean} warpMode defaults to current warpMode + * @returns {_StackFrame} this + */ + reuse (warpMode = this.warpMode) { + this.reset(); + this.warpMode = Boolean(warpMode); + return this; + } + + /** + * Create or recycle a stack frame object. + * @param {boolean} warpMode Enable warpMode on this frame. + * @returns {_StackFrame} The clean stack frame with correct warpMode setting. + */ + static create (warpMode) { + const stackFrame = _stackFrameFreeList.pop(); + if (typeof stackFrame !== 'undefined') { + stackFrame.warpMode = Boolean(warpMode); + return stackFrame; + } + return new _StackFrame(warpMode); + } + + /** + * Put a stack frame object into the recycle bin for reuse. + * @param {_StackFrame} stackFrame The frame to reset and recycle. + */ + static release (stackFrame) { + if (typeof stackFrame !== 'undefined') { + _stackFrameFreeList.push(stackFrame.reset()); + } + } +} + +/** + * A thread is a running stack context and all the metadata needed. + * @param {?string} firstBlock First block to execute in the thread. + * @constructor + */ +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.} + */ + this.stack = []; + + /** + * Stack frames for the thread. Store metadata for the executing blocks. + * @type {Array.<_StackFrame>} + */ + 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; + + /** + * The Blocks this thread will execute. + * @type {Blocks} + */ + this.blockContainer = 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; + + this.justReported = null; + + this.triedToCompile = false; + + this.isCompiled = false; + + // compiler data + // these values only make sense if isCompiled == true + this.timer = null; + /** + * The thread's generator. + * @type {Generator} + */ + this.generator = null; + /** + * @type {Object.} + */ + this.procedures = null; + this.executableHat = false; + this.compatibilityStackFrame = null; + + /** + * Thread vars: for allowing a compiled version of the + * LilyMakesThings Thread Variables extension + * @type {Object} + */ + this.variables = Object.create(null); + } + + /** + * 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 + */ + static get STATUS_RUNNING () { + return 0; // used by compiler + } + + /** + * Threads are in this state when a primitive is waiting on a promise; + * execution is paused until the promise changes thread status. + * @const + */ + static get STATUS_PROMISE_WAIT () { + return 1; // used by compiler + } + + /** + * Thread status for yield. + * @const + */ + static get STATUS_YIELD () { + return 2; // used by compiler + } + + /** + * Thread status for a single-tick yield. This will be cleared when the + * thread is resumed. + * @const + */ + static get STATUS_YIELD_TICK () { + return 3; // used by compiler + } + + /** + * Thread status for a paused thread. + * Thread is in this state when it has been told to pause and needs to pause + * any new yields from the compiler + * @const + */ + static get STATUS_PAUSED () { + return 5; + } + + /** + * Thread status for a finished/done thread. + * Thread is in this state when there are no more blocks to execute. + * @const + */ + static get STATUS_DONE () { + return 4; // used by compiler + } + + /** + * @param {Target} target The target running the thread. + * @param {string} topBlock ID of the thread's top block. + * @returns {string} A unique ID for this target and thread. + */ + static getIdFromTargetAndBlock (target, topBlock) { + // & should never appear in any IDs, so we can use it as a separator + return `${target.id}&${topBlock}`; + } + + getId () { + return Thread.getIdFromTargetAndBlock(this.target, this.topBlock); + } + + /** + * Push stack and update stack frames appropriately. + * @param {string} blockId Block ID to push to stack. + */ + 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) { + const parent = this.stackFrames[this.stackFrames.length - 1]; + this.stackFrames.push(_StackFrame.create(typeof parent !== 'undefined' && parent.warpMode)); + } + } + + /** + * 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. + */ + reuseStackForNextBlock (blockId) { + this.stack[this.stack.length - 1] = blockId; + this.stackFrames[this.stackFrames.length - 1].reuse(); + } + + /** + * Pop last block on the stack and its stack frame. + * @return {string} Block ID popped from the stack. + */ + popStack () { + _StackFrame.release(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); + // Reporter form of procedures_call + if (this.peekStackFrame().waitingReporter) break; + + // Command form of procedures_call + if (typeof block !== 'undefined' && block.opcode === 'procedures_call') { + // By definition, if we get here, the procedure is done, so skip ahead so + // the arguments won't be re-evaluated and then discarded as frozen state + // about which arguments have been evaluated is lost. + // This fixes https://github.com/TurboWarp/scratch-vm/issues/201 + this.goToNextBlock(); + 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) { + this.justReported = typeof value === 'undefined' ? null : value; + } + + /** + * Initialize procedure parameters on this stack frame. + */ + initParams () { + const stackFrame = this.peekStackFrame(); + if (stackFrame.params === null) { + stackFrame.params = {}; + } + } + + /** + * pause this thread + */ + pause () { + this.originalStatus = this.status; + this.status = Thread.STATUS_PAUSED; + if (this.timer) this.timer.pause(); + } + + /** + * unpause this thread + */ + play () { + this.status = this.originalStatus; + if (this.timer) this.timer.play(); + } + + + /** + * 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 === null) { + continue; + } + if (Object.prototype.hasOwnProperty.call(frame.params, paramName)) { + return frame.params[paramName]; + } + return null; + } + return null; + } + + getAllparams () { + const stackFrame = this.peekStackFrame(); + return stackFrame.params; + } + + /** + * 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.stackFrames.length - 1; + for (let i = sp - 1; i >= 0; i--) { + const block = this.target.blocks.getBlock(this.stackFrames[i].op.id) || + this.target.runtime.flyoutBlocks.getBlock(this.stackFrames[i].op.id); + if (block.opcode === 'procedures_call' && + block.mutation.proccode === procedureCode) { + return true; + } + if (--callCount < 0) return false; + } + return false; + } + + /** + * Attempt to compile this thread. + */ + tryCompile () { + if (!this.blockContainer) { + return; + } + + // importing the compiler here avoids circular dependency issues + const compile = require('../compiler/compile'); + + this.triedToCompile = true; + + // stackClick === true disables hat block generation + // It would be great to cache these separately, but for now it's easiest to just disable them to avoid + // cached versions of scripts breaking projects. + const canCache = !this.stackClick; + + const topBlock = this.topBlock; + // Flyout blocks are stored in a special block container. + const blocks = this.blockContainer.getBlock(topBlock) ? this.blockContainer : this.target.runtime.flyoutBlocks; + const cachedResult = canCache && blocks.getCachedCompileResult(topBlock); + // If there is a cached error, do not attempt to recompile. + if (cachedResult && !cachedResult.success) { + return; + } + + let result; + if (cachedResult) { + result = cachedResult.value; + } else { + try { + result = compile(this); + if (canCache) { + blocks.cacheCompileResult(topBlock, result); + } + } catch (error) { + log.error('cannot compile script', this.target.getName(), error); + if (canCache) { + blocks.cacheCompileError(topBlock, error); + } + this.target.runtime.emitCompileError(this.target, error); + return; + } + } + + this.procedures = {}; + for (const procedureCode of Object.keys(result.procedures)) { + this.procedures[procedureCode] = result.procedures[procedureCode](this); + } + + this.generator = result.startingFunction(this)(); + + this.executableHat = result.executableHat; + + if (!this.blockContainer.forceNoGlow) { + this.blockGlowInFrame = this.topBlock; + this.requestScriptGlowInFrame = true; + } + + this.isCompiled = true; + } +} + +// For extensions +Thread._StackFrame = _StackFrame; + +module.exports = Thread; diff --git a/local-scratch-vm/src/engine/tw-font-manager.js b/local-scratch-vm/src/engine/tw-font-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..285a23c057e0ebd11755e246cd5f3420e6a0f282 --- /dev/null +++ b/local-scratch-vm/src/engine/tw-font-manager.js @@ -0,0 +1,230 @@ +const EventEmitter = require('events'); +const AssetUtil = require('../util/tw-asset-util'); +const StringUtil = require('../util/string-util'); +const log = require('../util/log'); + +/** + * @typedef InternalFont + * @property {boolean} system True if the font is built in to the system + * @property {string} family The font's name + * @property {string} fallback Fallback font family list + * @property {Asset} [asset] scratch-storage asset if system: false + */ + +class FontManager extends EventEmitter { + /** + * @param {Runtime} runtime + */ + constructor(runtime) { + super(); + this.runtime = runtime; + /** @type {Array} */ + this.fonts = []; + } + + /** + * @param {string} family An unknown font family + * @returns {boolean} true if the family is valid + */ + isValidFamily(family) { + return /^[-\w ]+$/.test(family); + } + + /** + * @param {string} family + * @returns {boolean} + */ + hasFont(family) { + return !!this.fonts.find(i => i.family === family); + } + + /** + * @param {string} family + * @returns {boolean} + */ + getSafeName(family) { + family = family.replace(/[^-\w ]/g, ''); + return StringUtil.unusedName(family, this.fonts.map(i => i.family)); + } + + changed() { + this.emit('change'); + } + + /** + * @param {string} family + * @param {string} fallback + */ + addSystemFont(family, fallback) { + if (!this.isValidFamily(family)) { + throw new Error('Invalid family'); + } + this.fonts.push({ + system: true, + family, + fallback + }); + this.changed(); + } + + /** + * @param {string} family + * @param {string} fallback + * @param {Asset} asset scratch-storage asset + */ + addCustomFont(family, fallback, asset) { + if (!this.isValidFamily(family)) { + throw new Error('Invalid family'); + } + + this.fonts.push({ + system: false, + family, + fallback, + asset + }); + + this.updateRenderer(); + this.changed(); + } + + /** + * @returns {Array<{system: boolean; name: string; family: string; data: Uint8Array | null; format: string | null}>} + */ + getFonts() { + return this.fonts.map(font => ({ + system: font.system, + name: font.family, + family: `"${font.family}", ${font.fallback}`, + data: font.asset ? font.asset.data : null, + format: font.asset ? font.asset.dataFormat : null + })); + } + + /** + * @param {number} index Corresponds to index from getFonts() + */ + deleteFont(index) { + const [removed] = this.fonts.splice(index, 1); + if (!removed.system) { + this.updateRenderer(); + } + this.changed(); + } + + clear() { + const hadNonSystemFont = this.fonts.some(i => !i.system); + this.fonts = []; + if (hadNonSystemFont) { + this.updateRenderer(); + } + this.changed(); + } + + updateRenderer() { + if (!this.runtime.renderer || !this.runtime.renderer.setCustomFonts) { + return; + } + + const fontfaces = {}; + for (const font of this.fonts) { + if (!font.system) { + const uri = font.asset.encodeDataURI(); + const fontface = `@font-face { font-family: "${font.family}"; src: url("${uri}"); }`; + const family = `"${font.family}", ${font.fallback}`; + fontfaces[family] = fontface; + } + } + this.runtime.renderer.setCustomFonts(fontfaces); + } + + /** + * Get data to save in project.json and sb3 files. + */ + serializeJSON() { + if (this.fonts.length === 0) { + return null; + } + + return this.fonts.map(font => { + const serialized = { + system: font.system, + family: font.family, + fallback: font.fallback + }; + + if (!font.system) { + const asset = font.asset; + serialized.md5ext = `${asset.assetId}.${asset.dataFormat}`; + } + + return serialized; + }); + } + + /** + * @returns {Asset[]} list of scratch-storage assets + */ + serializeAssets() { + return this.fonts + .filter(i => !i.system) + .map(i => i.asset); + } + + /** + * @param {unknown} json + * @param {JSZip} [zip] + * @param {boolean} [keepExisting] + * @returns {Promise} + */ + async deserialize(json, zip, keepExisting) { + if (!keepExisting) { + this.clear(); + } + + if (!Array.isArray(json)) { + return; + } + + for (const font of json) { + if (!font || typeof font !== 'object') { + continue; + } + + try { + const system = font.system; + const family = font.family; + const fallback = font.fallback; + if ( + typeof system !== 'boolean' || + typeof family !== 'string' || + typeof fallback !== 'string' || + this.hasFont(family) + ) { + continue; + } + + if (system) { + this.addSystemFont(family, fallback); + } else { + const md5ext = font.md5ext; + if (typeof md5ext !== 'string') { + continue; + } + + const asset = await AssetUtil.getByMd5ext( + this.runtime, + zip, + this.runtime.storage.AssetType.Font, + md5ext + ); + this.addCustomFont(family, fallback, asset); + } + } catch (e) { + log.error('could not add font', e); + } + } + } +} + +module.exports = FontManager; \ No newline at end of file diff --git a/local-scratch-vm/src/engine/tw-frame-loop.js b/local-scratch-vm/src/engine/tw-frame-loop.js new file mode 100644 index 0000000000000000000000000000000000000000..9a4be2235947b07f17fb04f39c90f873c9345130 --- /dev/null +++ b/local-scratch-vm/src/engine/tw-frame-loop.js @@ -0,0 +1,95 @@ +// Due to the existence of features such as interpolation and "0 FPS" being treated as "screen refresh rate", +// The VM loop logic has become much more complex + +// Use setTimeout to polyfill requestAnimationFrame in Node.js environments +const _requestAnimationFrame = typeof requestAnimationFrame === 'function' ? + requestAnimationFrame : + (f => setTimeout(f, 1000 / 60)); +const _cancelAnimationFrame = typeof requestAnimationFrame === 'function' ? + cancelAnimationFrame : + clearTimeout; + +const animationFrameWrapper = callback => { + let id; + const handle = () => { + id = _requestAnimationFrame(handle); + callback(); + }; + const cancel = () => _cancelAnimationFrame(id); + id = _requestAnimationFrame(handle); + return { + cancel + }; +}; + +class FrameLoop { + constructor (runtime) { + this.runtime = runtime; + this.running = false; + this.setFramerate(30); + this.setInterpolation(false); + + this.stepCallback = this.stepCallback.bind(this); + this.interpolationCallback = this.interpolationCallback.bind(this); + + this._stepInterval = null; + this._interpolationAnimation = null; + this._stepAnimation = null; + this._stepCounter = 0; + } + + setFramerate (fps) { + this.framerate = fps; + this._restart(); + } + + setInterpolation (interpolation) { + this.interpolation = interpolation; + this._restart(); + } + + stepCallback () { + this.runtime._step(); + } + + interpolationCallback () { + this.runtime._renderInterpolatedPositions(); + } + + _restart () { + if (this.running) { + this.stop(); + this.start(); + } + } + + start () { + this.running = true; + if (this.framerate === 0) { + this._stepAnimation = animationFrameWrapper(this.stepCallback); + this.runtime.currentStepTime = 1000 / 60; + } else { + // Interpolation should never be enabled when framerate === 0 as that's just redundant + if (this.interpolation) { + this._interpolationAnimation = animationFrameWrapper(this.interpolationCallback); + } + this._stepInterval = setInterval(this.stepCallback, 1000 / this.framerate); + this.runtime.currentStepTime = 1000 / this.framerate; + } + } + + stop () { + this.running = false; + clearInterval(this._stepInterval); + if (this._interpolationAnimation) { + this._interpolationAnimation.cancel(); + } + if (this._stepAnimation) { + this._stepAnimation.cancel(); + } + this._interpolationAnimation = null; + this._stepAnimation = null; + } +} + +module.exports = FrameLoop; diff --git a/local-scratch-vm/src/engine/tw-interpolate.js b/local-scratch-vm/src/engine/tw-interpolate.js new file mode 100644 index 0000000000000000000000000000000000000000..ded5d12832d584d270c537b66c0175d391d7c7c1 --- /dev/null +++ b/local-scratch-vm/src/engine/tw-interpolate.js @@ -0,0 +1,160 @@ +const { translateForCamera } = require('../util/pos-math'); + +/** + * Prepare the targets of a runtime for interpolation. + * @param {Runtime} runtime The Runtime with targets to prepare for interpolation. + */ +const setupInitialState = runtime => { + const renderer = runtime.renderer; + + for (const target of runtime.targets) { + const directionAndScale = target._getRenderedDirectionAndScale(); + let camData = { ...runtime.getCamera(target.cameraBound) }; + camData.dir = camData.dir / 180; + camData.scale = 1 + ((camData.scale - 1) / 100); + + // If sprite may have been interpolated in the previous frame, reset its renderer state. + if (renderer && target.interpolationData) { + const drawableID = target.drawableID; + renderer.updateDrawablePosition(drawableID, [target.x - camData.pos[0], target.y - camData.pos[1]]); + renderer.updateDrawableDirectionScale( + drawableID, + directionAndScale.direction - camData.dir, + [directionAndScale.scale[0] * camData.scale, directionAndScale.scale[1] * camData.scale] + ); + renderer.updateDrawableEffect(drawableID, 'ghost', target.effects.ghost); + } + + if (target.visible && !target.isStage) { + target.interpolationData = { + x: target.x - camData.pos[0], + y: target.y - camData.pos[1], + direction: directionAndScale.direction - camData.dir, + scale: [directionAndScale.scale[0] * camData.scale, directionAndScale.scale[1] * camData.scale], + costume: target.currentCostume, + ghost: target.effects.ghost + }; + } else { + target.interpolationData = null; + } + } +}; + +/** + * Interpolate the position of targets. + * @param {Runtime} runtime The Runtime with targets to interpolate. + * @param {number} time Relative time in the frame in [0-1]. + */ +const interpolate = (runtime, time) => { + const renderer = runtime.renderer; + if (!renderer) { + return; + } + + for (const target of runtime.targets) { + // interpolationData is the initial state at the start of the frame (time 0) + // the state on the target itself is the state at the end of the frame (time 1) + const interpolationData = target.interpolationData; + if (!interpolationData) { + continue; + } + + // Don't waste time interpolating sprites that are hidden. + if ( + !target.visible || + /* special thanks to CST and Cubester for this new check */ + (target.effects.ghost === 100 && interpolationData.ghost === 100) + ) { + continue; + } + + runtime.emit(runtime.constructor.BEFORE_INTERPOLATE, target); + let camData = { ...runtime.getCamera(target.cameraBound) }; + camData.scale = 1 + ((camData.scale - 1) / 100); + const drawableID = target.drawableID; + + // Position interpolation. + const xDistance = target.x - interpolationData.x - camData.pos[0]; + const yDistance = target.y - interpolationData.y - camData.pos[1]; + const absoluteXDistance = Math.abs(xDistance); + const absoluteYDistance = Math.abs(yDistance); + if (absoluteXDistance > 0.1 || absoluteYDistance > 0.1) { + const drawable = renderer._allDrawables[drawableID]; + // Large movements are likely intended to be instantaneous. + // getAABB is less accurate than getBounds, but it's much faster + const bounds = drawable.getAABB(); + const tolerance = Math.min(240, Math.max(50, 1.5 * (bounds.width + bounds.height))); + const distance = Math.sqrt((absoluteXDistance ** 2) + (absoluteYDistance ** 2)); + if (distance < tolerance) { + const newX = interpolationData.x + (xDistance * time); + const newY = interpolationData.y + (yDistance * time); + renderer.updateDrawablePosition(drawableID, [newX, newY]); + } + } + + // Effect interpolation. + const ghostChange = target.effects.ghost - interpolationData.ghost; + const absoluteGhostChange = Math.abs(ghostChange); + // Large changes are likely intended to be instantaneous. + if (absoluteGhostChange > 0 && absoluteGhostChange < 25) { + const newGhost = target.effects.ghost + (ghostChange * time); + renderer.updateDrawableEffect(drawableID, 'ghost', newGhost); + } + + // Interpolate scale and direction. + const costumeUnchanged = interpolationData.costume === target.currentCostume; + if (costumeUnchanged) { + let {direction, scale} = target._getRenderedDirectionAndScale(); + direction = direction - (camData.dir / 180); + let updateDrawableDirectionScale = false; + + // Interpolate direction. + if (direction !== interpolationData.direction) { + // Perfect 90 degree angles should not be interpolated. + // eg. the foreground tile clones in https://scratch.mit.edu/projects/60917032/ + if (direction % 90 !== 0 || interpolationData.direction % 90 !== 0) { + const currentRadians = direction * Math.PI / 180; + const startingRadians = interpolationData.direction * Math.PI / 180; + direction = Math.atan2( + (Math.sin(currentRadians) * time) + (Math.sin(startingRadians) * (1 - time)), + (Math.cos(currentRadians) * time) + (Math.cos(startingRadians) * (1 - time)) + ) * 180 / Math.PI; + updateDrawableDirectionScale = true; + } + } + + // Interpolate scale. + const startingScale = interpolationData.scale; + scale[0] = scale[0] * camData.scale; + scale[1] = scale[1] * camData.scale; + if (scale[0] !== startingScale[0] || scale[1] !== startingScale[1]) { + // Do not interpolate size when the sign of either scale differs. + if ( + Math.sign(scale[0]) === Math.sign(startingScale[0]) && + Math.sign(scale[1]) === Math.sign(startingScale[1]) + ) { + const changeX = scale[0] - startingScale[0]; + const changeY = scale[1] - startingScale[1]; + const absoluteChangeX = Math.abs(changeX); + const absoluteChangeY = Math.abs(changeY); + // Large changes are likely intended to be instantaneous. + if (absoluteChangeX < 100 && absoluteChangeY < 100) { + scale[0] = (startingScale[0] + (changeX * time)); + scale[1] = (startingScale[1] + (changeY * time)); + updateDrawableDirectionScale = true; + } + } + } + + if (updateDrawableDirectionScale) { + renderer.updateDrawableDirectionScale(drawableID, direction, scale); + } + } + runtime.emit(runtime.constructor.AFTER_INTERPOLATE, target); + } +}; + +module.exports = { + setupInitialState, + interpolate +}; diff --git a/local-scratch-vm/src/engine/variable.js b/local-scratch-vm/src/engine/variable.js new file mode 100644 index 0000000000000000000000000000000000000000..eb27f551b1ae483a71c5b733d5e79647aeb2f2f5 --- /dev/null +++ b/local-scratch-vm/src/engine/variable.js @@ -0,0 +1,70 @@ +/** + * @fileoverview + * Object representing a Scratch variable. + */ + +const uid = require('../util/uid'); +const xmlEscape = require('../util/xml-escape'); + +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: + console.warn(`Invalid variable type: ${this.type}`); + } + } + + toXML (isLocal) { + isLocal = (isLocal === true); + return `${xmlEscape(this.name)}`; + } + + /** + * Type representation for scalar variables. + * This is currently represented as '' + * for compatibility with blockly. + * @const {string} + */ + static get SCALAR_TYPE () { + return ''; // used by compiler + } + + /** + * Type representation for list variables. + * @const {string} + */ + static get LIST_TYPE () { + return 'list'; // used by compiler + } + + /** + * Type representation for list variables. + * @const {string} + */ + static get BROADCAST_MESSAGE_TYPE () { + return 'broadcast_msg'; + } +} + +module.exports = Variable; diff --git a/local-scratch-vm/src/extension-support/argument-alignment.js b/local-scratch-vm/src/extension-support/argument-alignment.js new file mode 100644 index 0000000000000000000000000000000000000000..6aa3517729f5243054cf6bab66df0ac222a8de27 --- /dev/null +++ b/local-scratch-vm/src/extension-support/argument-alignment.js @@ -0,0 +1,27 @@ +/** + * Types of argument alignments + * @enum {string?} + */ +const ArgumentAlignment = { + /** + * Default alignment + */ + DEFAULT: null, + + /** + * Left alignment (usually default) + */ + LEFT: 'LEFT', + + /** + * Center alignment (used by the variable getter blocks) + */ + CENTER: 'CENTRE', + + /** + * Right alignment (used by loop indicators) + */ + RIGHT: 'RIGHT', +}; + +module.exports = ArgumentAlignment; diff --git a/local-scratch-vm/src/extension-support/argument-type.js b/local-scratch-vm/src/extension-support/argument-type.js new file mode 100644 index 0000000000000000000000000000000000000000..b573a6d9f89240d10a6ec1ded07ae48e1268b3d1 --- /dev/null +++ b/local-scratch-vm/src/extension-support/argument-type.js @@ -0,0 +1,88 @@ +/** + * Block argument types + * @enum {string} + */ +const ArgumentType = { + /** + * Numeric value with angle picker + */ + ANGLE: 'angle', + + /** + * Boolean value with hexagonal placeholder + */ + BOOLEAN: 'Boolean', + + /** + * Numeric value with color picker + */ + COLOR: 'color', + + /** + * Numeric value with text field + */ + NUMBER: 'number', + + /** + * String value with text field + */ + STRING: 'string', + + /** + * String value with matrix field + */ + MATRIX: 'matrix', + + /** + * MIDI note number with note picker (piano) field + */ + NOTE: 'note', + + /** + * Inline image on block (as part of the label) + */ + IMAGE: 'image', + + /** + * pm: creates an input with n x,y inputs + */ + POLYGON: 'polygon', + + /** + * Costume menu (taken from tw) + */ + COSTUME: 'costume', + + /** + * Sound menu (taken from tw) + */ + SOUND: 'sound', + + /** + * pm: Variable menu + * @deprecated Not functioning as intended + * @todo Fix args returning variable value instead of object + */ + VARIABLE: 'variable', + + /** + * pm: List menu + * @deprecated Not functioning as intended + * @todo Fix menu resetting on update & args returning "[object Object]" instead of object + */ + LIST: 'list', + + /** + * pm: Broadcast menu + * @deprecated Not functioning as intended + * @todo Fix menu resetting on update + */ + BROADCAST: 'broadcast', + + /** + * pm: Vertical seperator + */ + SEPERATOR: 'seperator' +}; + +module.exports = ArgumentType; diff --git a/local-scratch-vm/src/extension-support/block-shape.js b/local-scratch-vm/src/extension-support/block-shape.js new file mode 100644 index 0000000000000000000000000000000000000000..289b88d723341624ecac1a042c0d2145288d6cab --- /dev/null +++ b/local-scratch-vm/src/extension-support/block-shape.js @@ -0,0 +1,32 @@ +/** + * Types of block shapes + * @enum {number} + */ +const BlockShape = { + /** + * Output shape: hexagonal (booleans/predicates). + */ + HEXAGONAL: 1, + + /** + * Output shape: rounded (numbers). + */ + ROUND: 2, + + /** + * Output shape: squared (any/all values; strings). + */ + SQUARE: 3, + + /** + * Output shape: leaf-ed (custom shape thatl ooks cool). + */ + LEAF: 4, + + /** + * Output shape: plus (custom). + */ + PLUS: 5, +}; + +module.exports = BlockShape; diff --git a/local-scratch-vm/src/extension-support/block-type.js b/local-scratch-vm/src/extension-support/block-type.js new file mode 100644 index 0000000000000000000000000000000000000000..5345e40689c509937bbd2a2989459df41ffb812e --- /dev/null +++ b/local-scratch-vm/src/extension-support/block-type.js @@ -0,0 +1,61 @@ +/** + * Types of block + * @enum {string} + */ +const BlockType = { + /** + * Boolean reporter with hexagonal shape + */ + BOOLEAN: 'Boolean', + + /** + * A button (not an actual block) for some special action, like making a variable + */ + BUTTON: 'button', + + /** + * A text label (not an actual block) for adding comments or labling blocks + */ + LABEL: 'label', + + + /** + * Command block + */ + COMMAND: 'command', + + /** + * Specialized command block which may or may not run a child branch + * The thread continues with the next block whether or not a child branch ran. + */ + CONDITIONAL: 'conditional', + + /** + * Specialized hat block with no implementation function + * This stack only runs if the corresponding event is emitted by other code. + */ + EVENT: 'event', + + /** + * Hat block which conditionally starts a block stack + */ + HAT: 'hat', + + /** + * Specialized command block which may or may not run a child branch + * If a child branch runs, the thread evaluates the loop block again. + */ + LOOP: 'loop', + + /** + * General reporter with numeric or string value + */ + REPORTER: 'reporter', + + /** + * Arbitrary scratch-blocks XML. + */ + XML: 'xml' +}; + +module.exports = BlockType; diff --git a/local-scratch-vm/src/extension-support/define-messages.js b/local-scratch-vm/src/extension-support/define-messages.js new file mode 100644 index 0000000000000000000000000000000000000000..0ca9b4b5baacc1f4bd937c6e086a974e879b2068 --- /dev/null +++ b/local-scratch-vm/src/extension-support/define-messages.js @@ -0,0 +1,18 @@ +/** + * @typedef {object} MessageDescriptor + * @property {string} id - the translator-friendly unique ID of this message. + * @property {string} default - the message text in the default language (English). + * @property {string} [description] - a description of this message to help translators understand the context. + */ + +/** + * This is a hook for extracting messages from extension source files. + * This function simply returns the message descriptor map object that's passed in. + * @param {object.} messages - the messages to be defined + * @return {object.} - the input, unprocessed + */ +const defineMessages = function (messages) { + return messages; +}; + +module.exports = defineMessages; diff --git a/local-scratch-vm/src/extension-support/extension-addon-switchers.js b/local-scratch-vm/src/extension-support/extension-addon-switchers.js new file mode 100644 index 0000000000000000000000000000000000000000..81889c098d43c4e962280aa8dbf55f4a55688499 --- /dev/null +++ b/local-scratch-vm/src/extension-support/extension-addon-switchers.js @@ -0,0 +1,152 @@ +const switches = {}; + +// use extension ID +const extensions = { + jgFiles: require("../extensions/jg_files/switches.json"), +} + +const noopSwitch = { + isNoop: true +} + +function opcodeToLabel(opcode) { + return String(opcode).match(/([A-Z]?[^A-Z]*)/g).slice(0, -1).join(" ").toLowerCase(); +} + +Object.getOwnPropertyNames(extensions).forEach(extID => { + const extension = extensions[extID]; + Object.getOwnPropertyNames(extension).forEach(b => { + const block = extension[b]; + for (let i = 0; i < block.length; i++) { + const item = block[i]; + if (item === "hide") { + continue; + } + if ( + item === "same" + || item === "noop" + || item === "normal" + || item === "" + || item === null + ) { + block[i] = noopSwitch; + continue; + } + if (typeof item === "string") { + block[i] = { + opcode: `${extID}_${item}`, + msg: `${opcodeToLabel(item)}` + } + continue; + } + if (!block[i].msg) { + block[i].msg = opcodeToLabel(block[i].opcode); + } + if (block[i].opcode) { + if (String(block[i].opcode).startsWith(extID + "_")) continue; + block[i].opcode = `${extID}_${block[i].opcode}`; + } + } + extension[b] = block.filter(v => v !== "hide") + }) + switches[extID] = extension; +}) + +function getSwitches({runtime}) { + var _switches = switches; + for (let ext of runtime._blockInfo) { + if (ext.id in _switches) continue; + _switches[ext.id] = {}; + for (let block of ext.blocks) { + var blockswitches = block.info.switches; + if (!blockswitches) continue; + let opcode = block.info.opcode; + _switches[ext.id][opcode] = blockswitches.map(current => { + if (typeof current === "string") { + current = {opcode: current} + } else if (typeof current !== "object") { + return noopSwitch; + } + + if ("isNoop" in current && current.isNoop) { + return { + isNoop: true, + msg: current.overwriteText ?? block.info.switchText ?? block.info.text + }; + } + + if (!("opcode" in current)) { + return noopSwitch; + } + + let get_block = ext.blocks.filter(e => e.info.opcode === current.opcode); + if (get_block.length === 0) { // block doesn't exist. + return noopSwitch; + } + get_block = get_block[0]; + + let createInputs = {}; + let currargs = current.createArguments ?? {}; + + const parser = new DOMParser(); + + parser.parseFromString(get_block.xml, "text/xml") + .querySelectorAll(`[type="${get_block.json.type}"] > value`) + .forEach(el => { + let name = el.getAttribute("name"); + if (Object.keys(block.info.arguments).includes(name)) return; + + let shadowType = el.getElementsByTagName("shadow")[0].getAttribute("type"); + + let value = (currargs[name] ?? get_block.info.arguments[name].defaultValue ?? "").toString(); + + createInputs[name] = { + shadowType, + value + }; + }); + + const splitInputs = Object.keys(block.info.arguments) + .filter(arg => !Object.keys(get_block.info.arguments).includes(arg) && !Object.keys(current.remapArguments ?? {}).includes(arg)); + + const remapShadowType = {}; + + parser.parseFromString(block.xml, "text/xml") + .querySelectorAll(`[type="${block.json.type}"] > value`) + .forEach(el => { + let name = el.getAttribute("name"); + if (!(name in get_block.info.arguments)) return; + let shadowType = el.getElementsByTagName("shadow")[0].getAttribute("type"); + remapShadowType[name] = shadowType; + }); + + parser.parseFromString(get_block.xml, "text/xml") + .querySelectorAll(`[type="${get_block.json.type}"] > value`) + .forEach(el => { + let name = el.getAttribute("name"); + if (!(name in remapShadowType)) return; + let shadowType = el.getElementsByTagName("shadow")[0].getAttribute("type"); + + if (remapShadowType[name] == shadowType) { + delete remapShadowType[name]; + return; + } + remapShadowType[name] = shadowType; + }); + + return { + opcode: `${ext.id}_${current.opcode}`, + remapInputName: current.remapArguments ?? {}, + createInputs, + splitInputs, + remapShadowType, + mapFieldValues: current.remapMenus ?? {}, + msg: current.overwriteText ?? get_block.info.switchText ?? get_block.info.text, + }; + }); + } + } + return _switches; +} + +module.exports = getSwitches; diff --git a/local-scratch-vm/src/extension-support/extension-manager.js b/local-scratch-vm/src/extension-support/extension-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..f84a344577fcafb5241d82ad9fe961ece11253ec --- /dev/null +++ b/local-scratch-vm/src/extension-support/extension-manager.js @@ -0,0 +1,964 @@ +const dispatch = require('../dispatch/central-dispatch'); +const log = require('../util/log'); +const maybeFormatMessage = require('../util/maybe-format-message'); + +const BlockType = require('./block-type'); +const SecurityManager = require('./tw-security-manager'); +const Cast = require('../util/cast'); + +const AddonSwitches = require('./extension-addon-switchers'); + +const urlParams = new URLSearchParams(location.search); + +const IsLocal = String(window.location.href).startsWith(`http://localhost:`); +const IsLiveTests = urlParams.has('livetests'); + +// thhank yoh random stack droverflwo person +async function sha256(source) { + const sourceBytes = new TextEncoder().encode(source); + const digest = await crypto.subtle.digest("SHA-256", sourceBytes); + const resultBytes = [...new Uint8Array(digest)]; + return resultBytes.map(x => x.toString(16).padStart(2, '0')).join(""); +} + +// 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 defaultBuiltinExtensions = { + // This is an example that isn't loaded with the other core blocks, + // but serves as a reference for loading core blocks as extensions. + coreExample: () => require('../blocks/scratch3_core_example'), + // These are the non-core built-in extensions. + pen: () => require('../extensions/scratch3_pen'), + wedo2: () => require('../extensions/scratch3_wedo2'), + music: () => require('../extensions/scratch3_music'), + microbit: () => require('../extensions/scratch3_microbit'), + text2speech: () => require('../extensions/scratch3_text2speech'), + translate: () => require('../extensions/scratch3_translate'), + videoSensing: () => require('../extensions/scratch3_video_sensing'), + ev3: () => require('../extensions/scratch3_ev3'), + makeymakey: () => require('../extensions/scratch3_makeymakey'), + boost: () => require('../extensions/scratch3_boost'), + gdxfor: () => require('../extensions/scratch3_gdx_for'), + text: () => require('../extensions/scratchLab_animatedText'), + + // garbomuffin: *silence* + // tw: core extension + tw: () => require('../extensions/tw'), + // twFiles: replaces jgFiles as it works better on other devices + twFiles: () => require('../extensions/tw_files'), + + // pm: category expansions & seperations go here + // pmMotionExpansion: extra motion blocks that were in the category & new ones that werent + pmMotionExpansion: () => require("../extensions/pm_motionExpansion"), + // pmOperatorsExpansion: extra operators that were in the category & new ones that werent + pmOperatorsExpansion: () => require("../extensions/pm_operatorsExpansion"), + // pmSensingExpansion: extra sensing blocks that were in the category & new ones that werent + pmSensingExpansion: () => require("../extensions/pm_sensingExpansion"), + // pmControlsExpansion: extra control blocks that were in the category & new ones that werent + pmControlsExpansion: () => require("../extensions/pm_controlsExpansion"), + // pmEventsExpansion: extra event blocks that were in the category & new ones that werent + pmEventsExpansion: () => require("../extensions/pm_eventsExpansion"), + + // pmInlineBlocks: seperates the inline function block to prevent confusled + pmInlineBlocks: () => require("../extensions/pm_inlineblocks"), + + // jg: jeremyes esxsitenisonsnsn + // jgFiles: support for reading user files + jgFiles: () => require('../extensions/jg_files'), + // jgWebsiteRequests: fetch GET and POST requests to apis & websites + jgWebsiteRequests: () => require("../extensions/jg_websiteRequests"), + // jgJSON: handle JSON objects + jgJSON: () => require("../extensions/jg_json"), + // jgJSONParsed: handle JSON objects BETTER + // jgJSONParsed: () => require("../extensions/jg_jsonParsed"), + // jgRuntime: edit stage and other stuff + jgRuntime: () => require("../extensions/jg_runtime"), + // jgPrism: blocks for specific use cases or major convenience + jgPrism: () => require("../extensions/jg_prism"), + // jgIframe: my last call for help (for legal reasons this is a joke) + jgIframe: () => require("../extensions/jg_iframe"), + // jgExtendedAudio: ok this is my real last call for help (for legal reasons this is a joj) + jgExtendedAudio: () => require("../extensions/jg_audio"), + // jgScratchAuthenticate: easy to add its one block lol! + jgScratchAuthenticate: () => require("../extensions/jg_scratchAuth"), + // JgPermissionBlocks: someones gonna get mad at me for this one i bet + JgPermissionBlocks: () => require("../extensions/jg_permissions"), + // jgClones: funny clone manager + jgClones: () => require("../extensions/jg_clones"), + // jgTween: epic animation + jgTween: () => require("../extensions/jg_tween"), + // jgDebugging: epic animation + jgDebugging: () => require("../extensions/jg_debugging"), + // jgEasySave: easy save stuff + jgEasySave: () => require("../extensions/jg_easySave"), + // jgPackagerApplications: uuhhhhhhh packager + jgPackagerApplications: () => require("../extensions/jg_packagerApplications"), + // jgTailgating: follow sprites like in an RPG + jgTailgating: () => require("../extensions/jg_tailgating"), + // jgScripts: what you know about rollin down in the + jgScripts: () => require("../extensions/jg_scripts"), + // jg3d: damn daniel + jg3d: () => require("../extensions/jg_3d"), + // jg3dVr: epic + jg3dVr: () => require("../extensions/jg_3dVr"), + // jgVr: excuse to use vr headset lol! + jgVr: () => require("../extensions/jg_vr"), + // jgInterfaces: easier UI + jgInterfaces: () => require("../extensions/jg_interfaces"), + // jgCostumeDrawing: draw on costumes + // hiding so fir doesnt touch + // jgCostumeDrawing: () => require("../extensions/jg_costumeDrawing"), + // jgJavascript: this is like the 3rd time we have implemented JS blocks man + jgJavascript: () => require("../extensions/jg_javascript"), + // jgPathfinding: EZ pathfinding for beginners :D hopefully + jgPathfinding: () => require("../extensions/jg_pathfinding"), + // jgAnimation: animate idk + jgAnimation: () => require("../extensions/jg_animation"), + + // jgStorage: event extension requested by Fir & silvxrcat + jgStorage: () => require("../extensions/jg_storage"), + // jgTimers: event extension requested by Arrow + jgTimers: () => require("../extensions/jg_timers"), + // jgAdvancedText: event extension requested by silvxrcat + // hiding so fir doesnt touch + // jgAdvancedText: () => require("../extensions/jg_advancedText"), + + // jgDev: test extension used for making core blocks + jgDev: () => require("../extensions/jg_dev"), + // jgDooDoo: test extension used for making test extensions + jgDooDoo: () => require("../extensions/jg_doodoo"), + // jgBestExtension: great extension used for making great extensions + jgBestExtension: () => require("../extensions/jg_bestextensioin"), + // jgChristmas: Christmas extension used for making Christmas extensions + jgChristmas: () => require("../extensions/jg_christmas"), + + // jw: hello it is i jwklong + // jwUnite: literal features that should of been added in the first place + jwUnite: () => require("../extensions/jw_unite"), + // jwProto: placeholders, labels, defenitons, we got em + jwProto: () => require("../extensions/jw_proto"), + // jwPostLit: postlit real???? + jwPostLit: () => require("../extensions/jw_postlit"), + // jwReflex: vector positioning (UNRELEASED, DO NOT ADD TO GUI) + jwReflex: () => require("../extensions/jw_reflex"), + // Blockly 2: a faithful recreation of the original blockly blocks + blockly2math: () => require("../extensions/blockly-2/math.js"), + // jwXml: hi im back haha have funny xml + jwXml: () => require("../extensions/jw_xml"), + // vector type blah blah blah + jwVector: () => require("../extensions/jwVector"), + // my own array system yipee + jwArray: () => require("../extensions/jwArray"), + // mid extension but i need it + jwTargets: () => require("../extensions/jwTargets"), + // cool new physics extension + jwPsychic: () => require("../extensions/jwPsychic"), + // test ext for lambda functions or something + jwLambda: () => require("../extensions/jwLambda"), + // omega num port for penguinmod + jwNum: () => require("../extensions/jwNum"), + // good color utilties + jwColor: () => require("../extensions/jwColor"), + + // jw: They'll think its made by jwklong >:) + // (but it's not (yet (maybe (probably not (but its made by ianyourgod))))) + // this is the real jwklong speaking, one word shall be said about this: A N G E R Y + // Structs: hehe structs for oop (look at c) + jwStructs: () => require("../extensions/jw_structs"), + // mikedev: ghytfhygfvbl + // cl: () => require("../extensions/cl"), + Gamepad: () => require("../extensions/GamepadExtension"), + + // theshovel: ... + // theshovelcanvaseffects: ... + theshovelcanvaseffects: () => require("../extensions/theshovel_canvasEffects"), + // shovellzcompresss: ... + shovellzcompresss: () => require("../extensions/theshovel_lzString"), + // shovelColorPicker: ... + shovelColorPicker: () => require("../extensions/theshovel_colorPicker"), + // shovelcss: ... + shovelcss: () => require("../extensions/theshovel_customStyles"), + // profanityAPI: ... + profanityAPI: () => require("../extensions/theshovel_profanity"), + + // gsa: fill out your introduction stupet!!! + // no >:( + // canvas: kinda obvius if you know anything about html canvases + canvas: () => require('../extensions/gsa_canvas_old'), + // the replacment for the above extension + newCanvas: () => require('../extensions/gsa_canvas'), + // tempVars: fill out your introduction stupet!!! + tempVars: () => require('../extensions/gsa_tempVars'), + // colors: fill out your introduction stupet!!! + colors: () => require('../extensions/gsa_colorUtilBlocks'), + // Camera: camera + pmCamera: () => require('../extensions/pm_camera'), + + // sharkpool: insert sharkpools epic introduction here + // sharkpoolPrinting: ... + sharkpoolPrinting: () => require("../extensions/sharkpool_printing"), + + // silvxrcat: ... + // oddMessage: ... + oddMessage: () => require("../extensions/silvxrcat_oddmessages"), + + // TW extensions + + // lms: ... + // lmsutilsblocks: ... + lmsutilsblocks: () => require('../extensions/lmsutilsblocks'), + lmsTempVars2: () => require('../extensions/lily_tempVars2'), + + // xeltalliv: ... + // xeltallivclipblend: ... + xeltallivclipblend: () => require('../extensions/xeltalliv_clippingblending'), + + // DT: ... + // DTcameracontrols: ... + DTcameracontrols: () => require('../extensions/dt_cameracontrols'), + + // griffpatch: ... + // griffpatch: () => require('../extensions/griffpatch_box2d') + + // iyg: erm a crep, erm a werdohhhh + // iygPerlin: + iygPerlin: () => require('../extensions/iyg_perlin_noise'), + // fr: waw 3d physics!! + // fr3d: + fr3d: () => require('../extensions/fr_3d') +}; + +const coreExtensionList = Object.getOwnPropertyNames(defaultBuiltinExtensions); + +const preload = []; + +if (IsLocal || IsLiveTests) { + preload.push("jgDev"); +} + +/** + * @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} ConvertedBlockInfo - Raw extension block data paired with processed data ready for scratch-blocks + * @property {ExtensionBlockMetadata} info - the raw block info + * @property {object} json - the scratch-blocks JSON definition for this block + * @property {string} xml - the scratch-blocks XML definition for this block + */ + +/** + * @typedef {object} CategoryInfo - Information about a block category + * @property {string} id - the unique ID of this category + * @property {string} name - the human-readable name of this category + * @property {string|undefined} blockIconURI - optional URI for the block icon image + * @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.} blocks - the blocks, separators, etc. in this category + * @property {Array.} menus - the menus provided by 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 + */ + +const createExtensionService = extensionManager => { + const service = {}; + service.registerExtensionServiceSync = extensionManager.registerExtensionServiceSync.bind(extensionManager); + service.allocateWorker = extensionManager.allocateWorker.bind(extensionManager); + service.onWorkerInit = extensionManager.onWorkerInit.bind(extensionManager); + service.registerExtensionService = extensionManager.registerExtensionService.bind(extensionManager); + return service; +}; + +class ExtensionManager { + constructor(vm) { + /** + * 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.} + */ + this.pendingExtensions = []; + + /** + * Map of worker ID to workers which have been allocated but have not yet finished initialization. + * @type {Array.} + */ + this.pendingWorkers = []; + + /** + * Map of worker ID to the URL where it was loaded from. + * @type {Array} + */ + this.workerURLs = []; + + /** + * Map of loaded extension URLs/IDs to service names. + * @type {Map.} + * @private + */ + this._loadedExtensions = new Map(); + + /** + * Responsible for determining security policies related to custom extensions. + */ + this.securityManager = new SecurityManager(); + + /** + * @type {VirtualMachine} + */ + this.vm = vm; + + /** + * 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 = vm.runtime; + + this.loadingAsyncExtensions = 0; + this.asyncExtensionsLoadedCallbacks = []; + + this.builtinExtensions = Object.assign({}, defaultBuiltinExtensions); + + dispatch.setService('extensions', createExtensionService(this)).catch(e => { + log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`); + }); + + preload.forEach(value => { + this.loadExtensionURL(value); + }); + + this.extUrlCodes = {}; + // extensions that the user has stated (when they where loaded) that they do not wnat updated + this.keepOlder = []; + // map of all new shas so we know when a new code update has happened and so ask the user about it + this.extensionHashes = {}; + } + + getCoreExtensionList() { + return coreExtensionList; + } + getBuiltInExtensionsList() { + return this.builtinExtensions; + } + + getAddonBlockSwitches() { + return AddonSwitches(this.vm); + } + + /** + * 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); + } + + /** + * Determine whether an extension with a given ID is built in to the VM, such as pen. + * Note that "core extensions" like motion will return false here. + * @param {string} extensionId + * @returns {boolean} + */ + isBuiltinExtension(extensionId) { + return Object.prototype.hasOwnProperty.call(this.builtinExtensions, extensionId); + } + + /** + * Synchronously load an internal extension (core or non-core) by ID. This call will + * fail if the provided id is not does not match an internal extension. + * @param {string} extensionId - the ID of an internal extension + */ + loadExtensionIdSync(extensionId) { + if (!this.isBuiltinExtension(extensionId)) { + log.warn(`Could not find extension ${extensionId} in the built in extensions.`); + return; + } + + /** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */ + if (this.isExtensionLoaded(extensionId)) { + const message = `Rejecting attempt to load a second extension with ID ${extensionId}`; + log.warn(message); + return; + } + + const extension = this.builtinExtensions[extensionId](); + const extensionInstance = new extension(this.runtime); + const serviceName = this._registerInternalExtension(extensionInstance); + // devs are stupid so uh + // get the ACTUAL id of the ext so that saving/loading doesnt error + const realId = extensionInstance.getInfo().id; + this._loadedExtensions.set(extensionId, serviceName); + this.runtime.compilerRegisterExtension(realId, extensionInstance); + } + + addBuiltinExtension (extensionId, extensionClass) { + this.builtinExtensions[extensionId] = () => extensionClass; + } + + _isValidExtensionURL(extensionURL) { + try { + const parsedURL = new URL(extensionURL); + return ( + parsedURL.protocol === 'https:' || + parsedURL.protocol === 'http:' || + parsedURL.protocol === 'data:' || + parsedURL.protocol === 'file:' + ); + } catch (e) { + return false; + } + } + + /** + * Load an extension by URL or internal extension ID + * @param {string} normalURL - the URL for the extension to load OR the ID of an internal extension + * @param {string|null} oldHash - included when loading, contains the known hash that is from the loaded file so it can be compared with the one gotten over the url + * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure + */ + async loadExtensionURL(extensionURL, oldHash = '') { + if (this.isBuiltinExtension(extensionURL)) { + this.loadExtensionIdSync(extensionURL); + return [extensionURL]; + } + + if (this.isExtensionURLLoaded(extensionURL)) { + // Extension is already loaded. + return []; + } + + if (!this._isValidExtensionURL(extensionURL)) { + throw new Error(`Invalid extension URL: ${extensionURL}`); + } + + if (extensionURL.includes("penguinmod.site")) { + alert("Extensions using penguinmod.site are deprecated, please swap them over to use penguinmod.com instead.") + } + const normalURL = extensionURL.replace("penguinmod.site", "penguinmod.com"); + + this.runtime.setExternalCommunicationMethod('customExtensions', true); + + this.loadingAsyncExtensions++; + + const sandboxMode = await this.securityManager.getSandboxMode(normalURL); + const rewritten = await this.securityManager.rewriteExtensionURL(normalURL); + const blob = (await fetch(rewritten).then(req => req.blob())) + const blobUrl = URL.createObjectURL(blob) + const newHash = await new Promise(resolve => { + const reader = new FileReader() + reader.onload = async ({ target: { result } }) => { + console.log(result) + this.extUrlCodes[extensionURL] = result + resolve(await sha256(result)) + } + reader.onerror = err => { + console.error('couldnt read the contents of url', url, err) + } + reader.readAsText(blob) + }) + this.extensionHashes[extensionURL] = newHash + if (oldHash && oldHash !== newHash && this.securityManager.shouldUseLocal(extensionURL)) return Promise.reject('useLocal') + + if (sandboxMode === 'unsandboxed') { + const { load } = require('./tw-unsandboxed-extension-runner'); + const extensionObjects = await load(blobUrl, this.vm) + .catch(error => this._failedLoadingExtensionScript(error)); + const fakeWorkerId = this.nextExtensionWorker++; + const returnedIDs = []; + this.workerURLs[fakeWorkerId] = normalURL; + + for (const extensionObject of extensionObjects) { + const extensionInfo = extensionObject.getInfo(); + const serviceName = `unsandboxed.${fakeWorkerId}.${extensionInfo.id}`; + dispatch.setServiceSync(serviceName, extensionObject); + dispatch.callSync('extensions', 'registerExtensionServiceSync', serviceName); + this._loadedExtensions.set(extensionInfo.id, serviceName); + returnedIDs.push(extensionInfo.id); + this.runtime.compilerRegisterExtension(extensionInfo.id, extensionObject); + } + + this._finishedLoadingExtensionScript(); + return returnedIDs; + } + + /* eslint-disable max-len */ + let ExtensionWorker; + if (sandboxMode === 'worker') { + ExtensionWorker = require('worker-loader?name=js/extension-worker/extension-worker.[hash].js!./extension-worker'); + } else if (sandboxMode === 'iframe') { + ExtensionWorker = (await import(/* webpackChunkName: "iframe-extension-worker" */ './tw-iframe-extension-worker')).default; + } else { + throw new Error(`Invalid sandbox mode: ${sandboxMode}`); + } + /* eslint-enable max-len */ + + return new Promise((resolve, reject) => { + this.pendingExtensions.push({ extensionURL: blobUrl, resolve, reject }); + dispatch.addWorker(new ExtensionWorker()); + }).catch(error => this._failedLoadingExtensionScript(error)); + } + + /** + * Wait until all async extensions have loaded + * @returns {Promise} resolved when all async extensions have loaded + */ + allAsyncExtensionsLoaded() { + if (this.loadingAsyncExtensions === 0) { + return; + } + return new Promise((resolve, reject) => { + this.asyncExtensionsLoadedCallbacks.push({ + resolve, + reject + }); + }); + } + + /** + * Regenerate blockinfo for all loaded dynamic extensions + * @returns {Promise} resolved once all the extensions have been reinitialized + */ + refreshDynamicCategorys() { + if (!this._loadedExtensions) return Promise.reject('_loadedExtensions is not readable yet'); + const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName => + dispatch.call(serviceName, 'getInfo') + .then(info => { + info = this._prepareExtensionInfo(serviceName, info); + if (!info.isDynamic) return; + dispatch.call('runtime', '_refreshExtensionPrimitives', info); + }) + .catch(e => { + log.error(`Failed to refresh built-in extension primitives: ${e}`); + }) + ); + return Promise.all(allPromises); + } + + /** + * Regenerate blockinfo for any loaded extensions + * @returns {Promise} resolved once all the extensions have been reinitialized + */ + refreshBlocks() { + const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName => + dispatch.call(serviceName, 'getInfo') + .then(info => { + info = this._prepareExtensionInfo(serviceName, info); + dispatch.call('runtime', '_refreshExtensionPrimitives', info); + }) + .catch(e => { + log.error(`Failed to refresh built-in extension primitives: ${e}`); + }) + ); + return Promise.all(allPromises); + } + + prepareSwap(id) { + const serviceName = this._loadedExtensions.get(id); + dispatch.call(serviceName, 'dispose'); + delete dispatch.services[serviceName]; + delete this.runtime[`ext_${id}`]; + + this._loadedExtensions.delete(id); + const workerId = +serviceName.split('.')[1]; + delete this.workerURLs[workerId]; + } + removeExtension(id) { + const serviceName = this._loadedExtensions.get(id); + dispatch.call(serviceName, 'dispose'); + delete dispatch.services[serviceName]; + delete this.runtime[`ext_${id}`]; + + this._loadedExtensions.delete(id); + const workerId = +serviceName.split('.')[1]; + delete this.workerURLs[workerId]; + dispatch.call('runtime', '_removeExtensionPrimitive', id); + this.refreshBlocks(); + } + + allocateWorker() { + const id = this.nextExtensionWorker++; + const workerInfo = this.pendingExtensions.shift(); + this.pendingWorkers[id] = workerInfo; + this.workerURLs[id] = workerInfo.extensionURL; + return [id, workerInfo.extensionURL]; + } + + /** + * Synchronously collect extension metadata from the specified service and begin the extension registration process. + * @param {string} serviceName - the name of the service hosting the extension. + */ + registerExtensionServiceSync(serviceName) { + const info = dispatch.callSync(serviceName, 'getInfo'); + this._registerExtensionInfo(serviceName, info); + } + + /** + * 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._loadedExtensions.set(info.id, serviceName); + this._registerExtensionInfo(serviceName, info); + this._finishedLoadingExtensionScript(); + }); + } + + _finishedLoadingExtensionScript() { + this.loadingAsyncExtensions--; + if (this.loadingAsyncExtensions === 0) { + this.asyncExtensionsLoadedCallbacks.forEach(i => i.resolve()); + this.asyncExtensionsLoadedCallbacks = []; + } + } + + _failedLoadingExtensionScript(error) { + // Don't set the current extension counter to 0, otherwise it will go negative if another + // extension finishes or fails to load. + this.loadingAsyncExtensions--; + this.asyncExtensionsLoadedCallbacks.forEach(i => i.reject(error)); + this.asyncExtensionsLoadedCallbacks = []; + // Re-throw error so the promise still rejects. + throw error; + } + + /** + * 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(); + } + } + + /** + * Register an internal (non-Worker) extension object + * @param {object} extensionObject - the extension object to register + * @returns {string} The name of the registered extension service + */ + _registerInternalExtension(extensionObject) { + const extensionInfo = extensionObject.getInfo(); + const fakeWorkerId = this.nextExtensionWorker++; + const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`; + dispatch.setServiceSync(serviceName, extensionObject); + dispatch.callSync('extensions', 'registerExtensionServiceSync', 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}:`, e); + }); + } + + /** + * 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); + if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) { + throw new Error('Invalid extension id'); + } + extensionInfo.name = extensionInfo.name || extensionInfo.id; + extensionInfo.blocks = extensionInfo.blocks || []; + extensionInfo.targetTypes = extensionInfo.targetTypes || []; + extensionInfo.menus = extensionInfo.menus || {}; + extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus); + extensionInfo.blocks = extensionInfo.blocks.reduce((results, blockInfo) => { + try { + let result; + switch (blockInfo) { + case '---': // separator + result = '---'; + break; + default: // an ExtensionBlockMetadata object + result = this._prepareBlockInfo(serviceName, blockInfo, extensionInfo.menus); + break; + } + results.push(result); + } catch (e) { + // TODO: more meaningful error reporting + log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`); + } + return results; + }, []); + return extensionInfo; + } + + /** + * Prepare extension menus. e.g. setup binding for dynamic menu functions. + * @param {string} serviceName - the name of the service hosting this extension block + * @param {Array.} menus - the menu defined by the extension. + * @returns {Array.} - a menuInfo object with all preprocessing done. + * @private + */ + _prepareMenuInfo(serviceName, menus) { + const menuNames = Object.getOwnPropertyNames(menus); + for (let i = 0; i < menuNames.length; i++) { + const menuName = menuNames[i]; + let menuInfo = menus[menuName]; + + // If the menu description is in short form (items only) then normalize it to general form: an object with + // its items listed in an `items` property. + if (!menuInfo.items && (typeof menuInfo.variableType !== 'string')) { + menuInfo = { + items: menuInfo + }; + menus[menuName] = menuInfo; + } + // If `items` is a string, it should be the name of a function in the extension object. Calling the + // function should return an array of items to populate the menu when it is opened. + if (typeof menuInfo.items === 'string') { + const menuItemFunctionName = menuInfo.items; + const serviceObject = dispatch.services[serviceName]; + // Bind the function here so we can pass a simple item generation function to Scratch Blocks later. + menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName); + } + } + return menus; + } + + /** + * Fetch the items for a particular extension menu, providing the target ID for context. + * @param {object} extensionObject - the extension object providing the menu. + * @param {string} menuItemFunctionName - the name of the menu function to call. + * @returns {Array} menu items ready for scratch-blocks. + * @private + */ + _getExtensionMenuItems(extensionObject, menuItemFunctionName) { + // Fetch the items appropriate for the target currently being edited. This assumes that menus only + // collect items when opened by the user while editing a particular target. + const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage(); + const editingTargetID = editingTarget ? editingTarget.id : null; + const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget); + + // TODO: Fix this to use dispatch.call when extensions are running in workers. + const menuFunc = extensionObject[menuItemFunctionName]; + const menuItems = menuFunc.call(extensionObject, editingTargetID).map( + item => { + item = maybeFormatMessage(item, extensionMessageContext); + switch (typeof item) { + case 'object': + if (Array.isArray(item)) return item.slice(0, 2); + return [ + maybeFormatMessage(item.text, extensionMessageContext), + item.value + ]; + case 'string': + return [item, item]; + default: + return item; + } + }); + + if (!menuItems || menuItems.length < 1) { + throw new Error(`Extension menu returned no items: ${menuItemFunctionName}`); + } + return menuItems; + } + + _normalize(thing, to) { + switch (to) { + case 'string': return Cast.toString(thing); + case 'bigint': + case 'number': return Cast.toNumber(thing); + case 'boolean': return Cast.toBoolean(thing); + case 'function': return new Function(thing); + default: return Cast.toString(thing); + } + } + + /** + * Apply defaults for optional block fields. + * @param {string} serviceName - the name of the service hosting this extension block + * @param {ExtensionBlockMetadata} blockInfo - the block info from the extension + * @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields. + * @private + */ + _prepareBlockInfo(serviceName, blockInfo, menus) { + if (blockInfo.blockType === BlockType.XML) { + blockInfo = Object.assign({}, blockInfo); + blockInfo.xml = String(blockInfo.xml) || ''; + return blockInfo; + } + + blockInfo = Object.assign({}, { + blockType: BlockType.COMMAND, + terminal: false, + blockAllThreads: false, + arguments: {} + }, blockInfo); + blockInfo.text = blockInfo.text || blockInfo.opcode; + + switch (blockInfo.blockType) { + case BlockType.EVENT: + if (blockInfo.func) { + log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`); + } + break; + case BlockType.BUTTON: + if (!blockInfo.opcode && !blockInfo.func) { + throw new Error(`Missing opcode or func for button: ${blockInfo.text}`); + } + + if (blockInfo.func && !blockInfo.opcode) { + blockInfo.opcode = blockInfo.func; + } + const funcName = blockInfo.opcode; + const callBlockFunc = (...args) => dispatch.call(serviceName, funcName, ...args); + + blockInfo.func = callBlockFunc; + break; + case BlockType.LABEL: + break; + default: { + if (!blockInfo.opcode) { + throw new Error('Missing opcode for block'); + } + + const funcName = blockInfo.func || blockInfo.opcode; + + const getBlockInfo = blockInfo.isDynamic ? + args => args && args.mutation && args.mutation.blockInfo : + () => blockInfo; + const callBlockFunc = (() => { + if (dispatch._isRemoteService(serviceName)) { + return (args, util, realBlockInfo) => + dispatch.call(serviceName, funcName, args, util, realBlockInfo) + .then(result => { + // Scratch is only designed to handle these types. + // If any other value comes in such as undefined, null, an object, etc. + // we'll convert it to a string to avoid undefined behavior. + if ( + typeof result === 'number' || + typeof result === 'string' || + typeof result === 'boolean' + ) { + return result; + } + return `${result}`; + }) + // When an error happens, instead of returning undefined, we'll return a stringified + // version of the error so that it can be debugged. + .catch(err => { + // We want the full error including stack to be printed but the log helper + // messes with that. + // eslint-disable-next-line no-console + console.error('Custom extension block error', err); + return `${err}`; + }); + } + + // avoid promise latency if we can call direct + const serviceObject = dispatch.services[serviceName]; + if (!serviceObject[funcName]) { + // The function might show up later as a dynamic property of the service object + log.warn(`Could not find extension block function called ${funcName}`); + } + return (args, util, realBlockInfo) => + serviceObject[funcName](args, util, realBlockInfo); + })(); + + blockInfo.func = (args, util, visualReport) => { + const normal = { + 'angle': "number", + 'Boolean': "boolean", + 'color': "string", + 'number': "number", + 'string': "string", + 'matrix': "string", + 'note': "number", + 'image': "string", + 'polygon': "object", + // normalization exceptions + 'list': "exception", + 'broadcast': "exception" + }; + const realBlockInfo = getBlockInfo(args); + for (const arg in realBlockInfo.arguments) { + const expected = normal[realBlockInfo.arguments[arg].type]; + if (realBlockInfo.arguments[arg].exemptFromNormalization === true) continue; + if (expected === 'exception') continue; + if (!expected) continue; + // stupidly long check but :Trollhands + // if this argument is for a variable dropdown, do not type cast it + // as variable dropdowns report an object and not something we can or should cast + if (typeof menus[realBlockInfo.arguments[arg].menu]?.variableType === 'string') continue; + if (!(typeof args[arg] === expected)) args[arg] = this._normalize(args[arg], expected); + } + // TODO: filter args using the keys of realBlockInfo.arguments? maybe only if sandboxed? + const returnValue = callBlockFunc(args, util, realBlockInfo); + if (!visualReport && (returnValue?.value ?? false)) return returnValue.value; + return returnValue; + }; + break; + } + } + + return blockInfo; + } + + extensionUrlFromId(extId) { + for (const [extensionId, serviceName] of this._loadedExtensions.entries()) { + if (extensionId !== extId) continue; + // Service names for extension workers are in the format "extension.WORKER_ID.EXTENSION_ID" + const workerId = +serviceName.split('.')[1]; + return this.workerURLs[workerId]; + } + } + getExtensionURLs() { + const extensionURLs = {}; + for (const [extensionId, serviceName] of this._loadedExtensions.entries()) { + // Service names for extension workers are in the format "extension.WORKER_ID.EXTENSION_ID" + const workerId = +serviceName.split('.')[1]; + const extensionURL = this.workerURLs[workerId]; + if (typeof extensionURL === 'string') { + extensionURLs[extensionId] = extensionURL; + } + } + return extensionURLs; + } + + isExtensionURLLoaded (url) { + return this.workerURLs.includes(url); + } +} + +module.exports = ExtensionManager; diff --git a/local-scratch-vm/src/extension-support/extension-metadata.js b/local-scratch-vm/src/extension-support/extension-metadata.js new file mode 100644 index 0000000000000000000000000000000000000000..837f0086bd94e0667cecabc556f394e938890dfc --- /dev/null +++ b/local-scratch-vm/src/extension-support/extension-metadata.js @@ -0,0 +1,74 @@ +/** + * @typedef {object} ExtensionMetadata + * All the metadata needed to register an extension. + * @property {string} id - a unique alphanumeric identifier for this extension. No special characters allowed. + * @property {string} [name] - the human-readable name of this extension. + * @property {string} [blockIconURI] - URI for an image to be placed on each block in this extension. Data URI ok. + * @property {string} [menuIconURI] - URI for an image to be placed on this extension's category menu item. Data URI ok. + * @property {string} [docsURI] - link to documentation content for this extension. + * @property {Array.} blocks - the blocks provided by this extension, plus separators. + * @property {Object.} [menus] - map of menu name to metadata for each of this extension's menus. + */ + +/** + * @typedef {object} ExtensionBlockMetadata + * All the metadata needed to register an extension block. + * @property {string} opcode - a unique alphanumeric identifier for this block. No special characters allowed. + * @property {string} [func] - the name of the function implementing this block. Can be shared by other blocks/opcodes. + * @property {BlockType} blockType - the type of block (command, reporter, etc.) being described. + * @property {string} text - the text on the block, with [PLACEHOLDERS] for arguments. + * @property {Boolean} [hideFromPalette] - true if this block should not appear in the block palette. + * @property {Boolean} [isTerminal] - true if the block ends a stack - no blocks can be connected after it. + * @property {Boolean} [disableMonitor] - true if this block is a reporter but should not allow a monitor. + * @property {ReporterScope} [reporterScope] - if this block is a reporter, this is the scope/context for its value. + * @property {Boolean} [isEdgeActivated] - sets whether a hat block is edge-activated. + * @property {Boolean} [shouldRestartExistingThreads] - sets whether a hat/event block should restart existing threads. + * @property {int} [branchCount] - for flow control blocks, the number of branches/substacks for this block. + * @property {Object.} [arguments] - map of argument placeholder to metadata about each arg. + * @property {Array.} [switches] - array of the opcodes that this block is able to be swapped to. + * @property {string} [switchText] - text used for block switching + */ + +/** + * @typedef {object} ExtensionArgumentMetadata + * All the metadata needed to register an argument for an extension block. + * @property {ArgumentType} type - the type of the argument (number, string, etc.) + * @property {*} [defaultValue] - the default value of this argument. + * @property {string} [menu] - the name of the menu to use for this argument, if any. + */ + +/** + * @typedef {ExtensionDynamicMenu|ExtensionMenuItems} ExtensionMenuMetadata + * All the metadata needed to register an extension drop-down menu. + */ + +/** + * @typedef {string} ExtensionDynamicMenu + * The string name of a function which returns menu items. + * @see {ExtensionMenuItems} - the type of data expected to be returned by the specified function. + */ + +/** + * @typedef {Array.} ExtensionMenuItems + * Items in an extension menu. + */ + +/** + * @typedef {string} ExtensionMenuItemSimple + * A menu item for which the label and value are identical strings. + */ + +/** + * @typedef {object} ExtensionMenuItemComplex + * A menu item for which the label and value can differ. + * @property {*} value - the value of the block argument when this menu item is selected. + * @property {string} text - the human-readable label of this menu item in the menu. + */ + +/** + * @typedef {object} ExtensionBlockSwitchElement + * A menu item for which the label and value can differ. + * @property {string} opcode - the opcode to switch to. + * @property {bool} isNoop - if this switch should be a noop. + * @property {Object.} remapArguments - map of current block's arguments to this block's arguments. +*/ diff --git a/local-scratch-vm/src/extension-support/extension-worker.js b/local-scratch-vm/src/extension-support/extension-worker.js new file mode 100644 index 0000000000000000000000000000000000000000..08450ad46f91f480969a65cc41ffaee06a5a7f6e --- /dev/null +++ b/local-scratch-vm/src/extension-support/extension-worker.js @@ -0,0 +1,101 @@ +/* eslint-env worker */ + +const ScratchCommon = require('./tw-extension-api-common'); +const createScratchX = require('./tw-scratchx-compatibility-layer'); +const dispatch = require('../dispatch/worker-dispatch'); +const log = require('../util/log'); +const {isWorker} = require('./tw-extension-worker-context'); +const createTranslate = require('./tw-l10n'); + +const translate = createTranslate(null); + +const loadScripts = url => { + if (isWorker) { + importScripts(url); + } else { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.onload = () => resolve(); + script.onerror = () => { + reject(new Error(`Error in sandboxed script: ${url}. Check the console for more information.`)); + }; + script.src = url; + document.body.appendChild(script); + }); + } +}; + +class ExtensionWorker { + constructor () { + this.nextExtensionId = 0; + + this.initialRegistrations = []; + + this.firstRegistrationPromise = new Promise(resolve => { + this.firstRegistrationCallback = resolve; + }); + + dispatch.waitForConnection.then(() => { + dispatch.call('extensions', 'allocateWorker').then(async x => { + const [id, extension] = x; + this.workerId = id; + + try { + await loadScripts(extension); + await this.firstRegistrationPromise; + + const initialRegistrations = this.initialRegistrations; + this.initialRegistrations = null; + + Promise.all(initialRegistrations).then(() => dispatch.call('extensions', 'onWorkerInit', id)); + } catch (e) { + log.error(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.firstRegistrationCallback(); + this.initialRegistrations.push(promise); + } + return promise; + } +} + +global.Scratch = global.Scratch || {}; +Object.assign(global.Scratch, ScratchCommon, { + canFetch: () => Promise.resolve(true), + fetch: (url, options) => fetch(url, options), + canOpenWindow: () => Promise.resolve(false), + openWindow: () => Promise.reject(new Error('Scratch.openWindow not supported in sandboxed extensions')), + canRedirect: () => Promise.resolve(false), + redirect: () => Promise.reject(new Error('Scratch.redirect not supported in sandboxed extensions')), + canRecordAudio: () => Promise.resolve(false), + canRecordVideo: () => Promise.resolve(false), + canReadClipboard: () => Promise.resolve(false), + canNotify: () => Promise.resolve(false), + canGeolocate: () => Promise.resolve(false), + canEmbed: () => Promise.resolve(false), + canUnsandbox: () => Promise.resolve(false), + translate +}); + +/** + * Expose only specific parts of the worker to extensions. + */ +const extensionWorker = new ExtensionWorker(); +global.Scratch.extensions = { + register: extensionWorker.register.bind(extensionWorker) +}; + +global.ScratchExtensions = createScratchX(global.Scratch); diff --git a/local-scratch-vm/src/extension-support/pm-modal-manager.js b/local-scratch-vm/src/extension-support/pm-modal-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..8142ac1fadfd188af900d58dd1475efe042791d2 --- /dev/null +++ b/local-scratch-vm/src/extension-support/pm-modal-manager.js @@ -0,0 +1,29 @@ +class ModalManager { + constructor (runtime) { + this.runtime = runtime; + this.modals = {}; + this._updateId = 0; + } + updateModalComponents () { + this._updateId++; + if (this._updateId > 1000000) { + this._updateId = 0; + } + } + + createModal (id, config) { + this.modals[id] = { + ...config, + id + }; + this.updateModalComponents(); + } + deleteModal (id) { + if (id in this.modals) { + delete this.modals[id]; + } + this.updateModalComponents(); + } +} + +module.exports = ModalManager; \ No newline at end of file diff --git a/local-scratch-vm/src/extension-support/pm-tab-manager.js b/local-scratch-vm/src/extension-support/pm-tab-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..724e11c66c604146ea2189006919981aa80a9d80 --- /dev/null +++ b/local-scratch-vm/src/extension-support/pm-tab-manager.js @@ -0,0 +1,62 @@ +class EditorTab { + constructor (runtime, extensionId, tabId, name, uri) { + this.runtime = runtime; + this.extensionId_ = extensionId; + this.tabId_ = tabId; + this.element_ = null; + + this.name = name; + this.icon = uri; + } + + setName (name) { + this.name = name; + this.update(); + } + setIcon (iconUri) { + this.icon = iconUri; + this.update(); + } + + setDOM (element) { + this.element_ = element; + this.update(); + } + update () { + this.runtime.emit('EDITOR_TABS_UPDATE'); + } +} + +/** + * Class responsible for managing tabs created by extensions. + * These tabs are editor tabs near the Code, Costumes and Sounds tabs. + */ +class TabManager { + constructor (runtime) { + this.runtime = runtime; + this.tabs_ = {}; + } + + /** + * Register a new editor tab. + * @param {string} extensionId ID of this extension. + * @param {string} tabId ID for this tab. + * @param {string} name Name of the editor tab. + * @returns {EditorTab} + */ + register (extensionId, tabId, name, uri) { + const fullTabId = `${extensionId}_${tabId}`; + // check if this tab exists + if (fullTabId in this.tabs_) { + console.warn('Tab', tabId, 'for', extensionId, 'already exists.'); + return this.tabs_[fullTabId]; + } + // create tab + const tab = new EditorTab(this.runtime, extensionId, tabId, name, uri); + this.tabs_[fullTabId] = tab; + this.runtime.emit('EDITOR_TABS_NEW', tab); + return tab; + } +} + +module.exports = TabManager; \ No newline at end of file diff --git a/local-scratch-vm/src/extension-support/reporter-scope.js b/local-scratch-vm/src/extension-support/reporter-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..6157d1c5f7c4be2b185f72dd68b6f22de8f625f2 --- /dev/null +++ b/local-scratch-vm/src/extension-support/reporter-scope.js @@ -0,0 +1,18 @@ +/** + * Indicate the scope for a reporter's value. + * @enum {string} + */ +const ReporterScope = { + /** + * This reporter's value is global and does not depend on context. + */ + GLOBAL: 'global', + + /** + * This reporter's value is specific to a particular target/sprite. + * Another target may have a different value or may not even have a value. + */ + TARGET: 'target' +}; + +module.exports = ReporterScope; diff --git a/local-scratch-vm/src/extension-support/target-type.js b/local-scratch-vm/src/extension-support/target-type.js new file mode 100644 index 0000000000000000000000000000000000000000..26db0594dc901403dc539eacb3aafcde44e72fe2 --- /dev/null +++ b/local-scratch-vm/src/extension-support/target-type.js @@ -0,0 +1,17 @@ +/** + * Default types of Target supported by the VM + * @enum {string} + */ +const TargetType = { + /** + * Rendered target which can move, change costumes, etc. + */ + SPRITE: 'sprite', + + /** + * Rendered target which cannot move but can change backdrops + */ + STAGE: 'stage' +}; + +module.exports = TargetType; diff --git a/local-scratch-vm/src/extension-support/tw-extension-api-common.js b/local-scratch-vm/src/extension-support/tw-extension-api-common.js new file mode 100644 index 0000000000000000000000000000000000000000..a7bbf70527f3e49d8b989e09a1b21df290abe49b --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-extension-api-common.js @@ -0,0 +1,21 @@ +const ArgumentType = require('./argument-type'); +const ArgumentAlignment = require('./argument-alignment'); +const BlockType = require('./block-type'); +const BlockShape = require('./block-shape'); +const TargetType = require('./target-type'); +const Cast = require('../util/cast'); +const Clone = require('../util/clone'); +const Color = require('../util/color'); + +const Scratch = { + ArgumentType, + ArgumentAlignment, + BlockType, + BlockShape, + TargetType, + Cast, + Clone, + Color +}; + +module.exports = Scratch; diff --git a/local-scratch-vm/src/extension-support/tw-extension-worker-context.js b/local-scratch-vm/src/extension-support/tw-extension-worker-context.js new file mode 100644 index 0000000000000000000000000000000000000000..185e0f0971e229be18ee81426dbea903d6e09ee4 --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-extension-worker-context.js @@ -0,0 +1,5 @@ +module.exports = { + isWorker: true, + // centralDispatchService is the object to call postMessage() on to send a message to parent. + centralDispatchService: self +}; diff --git a/local-scratch-vm/src/extension-support/tw-iframe-extension-worker-entry.js b/local-scratch-vm/src/extension-support/tw-iframe-extension-worker-entry.js new file mode 100644 index 0000000000000000000000000000000000000000..ddd6d41c560a52a000fecf0adf77e99a80b2744a --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-iframe-extension-worker-entry.js @@ -0,0 +1,29 @@ +const context = require('./tw-extension-worker-context'); + +const jQuery = require('./tw-jquery-shim'); +global.$ = jQuery; +global.jQuery = jQuery; + +const id = window.__WRAPPED_IFRAME_ID__; + +context.isWorker = false; +context.centralDispatchService = { + postMessage (message, transfer) { + const data = { + vmIframeId: id, + message + }; + if (transfer) { + window.parent.postMessage(data, '*', transfer); + } else { + window.parent.postMessage(data, '*'); + } + } +}; + +require('./extension-worker'); + +window.parent.postMessage({ + vmIframeId: id, + ready: true +}, '*'); diff --git a/local-scratch-vm/src/extension-support/tw-iframe-extension-worker.js b/local-scratch-vm/src/extension-support/tw-iframe-extension-worker.js new file mode 100644 index 0000000000000000000000000000000000000000..782ee449fd4ecbc17a87c9836ba487ff235b37ce --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-iframe-extension-worker.js @@ -0,0 +1,96 @@ +const uid = require('../util/uid'); +const frameSource = require('./tw-load-script-as-plain-text!./tw-iframe-extension-worker-entry'); + +const none = "'none'"; +const featurePolicy = { + 'accelerometer': none, + 'ambient-light-sensor': none, + 'battery': none, + 'camera': none, + 'display-capture': none, + 'document-domain': none, + 'encrypted-media': none, + 'fullscreen': none, + 'geolocation': none, + 'gyroscope': none, + 'magnetometer': none, + 'microphone': none, + 'midi': none, + 'payment': none, + 'picture-in-picture': none, + 'publickey-credentials-get': none, + 'speaker-selection': none, + 'usb': none, + 'vibrate': none, + 'vr': none, + 'screen-wake-lock': none, + 'web-share': none, + 'interest-cohort': none +}; + +const generateAllow = () => Object.entries(featurePolicy) + .map(([name, permission]) => `${name} ${permission}`) + .join('; '); + +class IframeExtensionWorker { + constructor () { + this.id = uid(); + this.isRemote = true; + this.ready = false; + this.queuedMessages = []; + + this.iframe = document.createElement('iframe'); + this.iframe.className = 'tw-custom-extension-frame'; + this.iframe.dataset.id = this.id; + this.iframe.style.display = 'none'; + this.iframe.setAttribute('aria-hidden', 'true'); + this.iframe.sandbox = 'allow-scripts'; + this.iframe.allow = generateAllow(); + document.body.appendChild(this.iframe); + + window.addEventListener('message', this._onWindowMessage.bind(this)); + const blob = new Blob([ + // eslint-disable-next-line max-len + `` + ], { + type: 'text/html; charset=utf-8' + }); + this.iframe.src = URL.createObjectURL(blob); + } + + _onWindowMessage (e) { + if (!e.data || e.data.vmIframeId !== this.id) { + return; + } + if (e.data.ready) { + this.ready = true; + for (const {data, transfer} of this.queuedMessages) { + this.postMessage(data, transfer); + } + this.queuedMessages.length = 0; + } + if (e.data.message) { + this.onmessage({ + data: e.data.message + }); + } + } + + onmessage () { + // Should be overridden + } + + postMessage (data, transfer) { + if (this.ready) { + if (transfer) { + this.iframe.contentWindow.postMessage(data, '*', transfer); + } else { + this.iframe.contentWindow.postMessage(data, '*'); + } + } else { + this.queuedMessages.push({data, transfer}); + } + } +} + +module.exports = IframeExtensionWorker; diff --git a/local-scratch-vm/src/extension-support/tw-jquery-shim.js b/local-scratch-vm/src/extension-support/tw-jquery-shim.js new file mode 100644 index 0000000000000000000000000000000000000000..9d1f8897df6b4e84f54d18c9c52485f7dc07223f --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-jquery-shim.js @@ -0,0 +1,112 @@ +/** + * @fileoverview + * Many ScratchX extensions require jQuery to do things like loading scripts and making requests. + * The real jQuery is pretty large and we'd rather not bring in everything, so this file reimplements + * small stubs of a few jQuery methods. + * It's just supposed to be enough to make existing ScratchX extensions work, nothing more. + */ + +const log = require('../util/log'); + +const jQuery = () => { + throw new Error('Not implemented'); +}; + +jQuery.getScript = (src, callback) => { + const script = document.createElement('script'); + script.src = src; + if (callback) { + // We don't implement callback arguments. + script.onload = () => callback(); + } + document.body.appendChild(script); +}; + +/** + * @param {Record|undefined} obj + * @returns {URLSearchParams} + */ +const objectToQueryString = obj => { + const params = new URLSearchParams(); + if (obj) { + for (const key of Object.keys(obj)) { + params.set(key, obj[key]); + } + } + return params; +}; + +let jsonpCallback = 0; + +jQuery.ajax = async (arg1, arg2) => { + let options = {}; + + if (arg1 && arg2) { + options = arg2; + options.url = arg1; + } else if (arg1) { + options = arg1; + } + + const urlParameters = objectToQueryString(options.data); + const getFinalURL = () => { + const query = urlParameters.toString(); + let url = options.url; + if (query) { + url += `?${query}`; + } + // Forcibly upgrade all HTTP requests to HTTPS so that they don't error on HTTPS sites + // All the extensions we care about work fine with this + if (url.startsWith('http://')) { + url = url.replace('http://', 'https://'); + } + return url; + }; + + const successCallback = result => { + if (options.success) { + options.success(result); + } + }; + const errorCallback = error => { + log.error(error); + if (options.error) { + // The error object we provide here might not match what jQuery provides but it's enough to + // prevent extensions from throwing errors trying to access properties. + options.error(error); + } + }; + + try { + if (options.dataType === 'jsonp') { + const callbackName = `_jsonp_callback${jsonpCallback++}`; + global[callbackName] = data => { + delete global[callbackName]; + successCallback(data); + }; + + const callbackParameterName = options.jsonp || 'callback'; + urlParameters.set(callbackParameterName, callbackName); + + jQuery.getScript(getFinalURL()); + return; + } + + if (options.dataType === 'script') { + jQuery.getScript(getFinalURL(), successCallback); + return; + } + + const res = await fetch(getFinalURL(), { + headers: options.headers + }); + // dataType defaults to "Intelligent Guess (xml, json, script, or html)" + // It happens that all the ScratchX extensions we care about either set dataType to "json" or + // leave it blank and implicitly request JSON, so this works good enough for now. + successCallback(await res.json()); + } catch (e) { + errorCallback(e); + } +}; + +module.exports = jQuery; diff --git a/local-scratch-vm/src/extension-support/tw-l10n.js b/local-scratch-vm/src/extension-support/tw-l10n.js new file mode 100644 index 0000000000000000000000000000000000000000..74b4fe28cb0a8d7ace1ce62bc0847538d7f2e601 --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-l10n.js @@ -0,0 +1,61 @@ +const formatMessage = require('format-message'); + +/** + * @param {VM|null} vm + * @returns {object} + */ +const createTranslate = vm => { + const namespace = formatMessage.namespace(); + + const translate = (message, args) => { + if (message && typeof message === 'object') { + // already in the expected format + } else if (typeof message === 'string') { + message = { + default: message + }; + } else { + throw new Error('unsupported data type in translate()'); + } + return namespace(message, args); + }; + + const generateId = defaultMessage => `_${defaultMessage}`; + + const getLocale = () => { + if (vm) return vm.getLocale(); + if (typeof navigator !== 'undefined') return navigator.language; + return 'en'; + }; + + let storedTranslations = {}; + translate.setup = newTranslations => { + if (newTranslations) { + storedTranslations = newTranslations; + } + namespace.setup({ + locale: getLocale(), + missingTranslation: 'ignore', + generateId, + translations: storedTranslations + }); + }; + + Object.defineProperty(translate, 'language', { + configurable: true, + enumerable: true, + get: () => getLocale() + }); + + translate.setup({}); + + if (vm) { + vm.on('LOCALE_CHANGED', () => { + translate.setup(null); + }); + } + + return translate; +}; + +module.exports = createTranslate; \ No newline at end of file diff --git a/local-scratch-vm/src/extension-support/tw-load-script-as-plain-text.js b/local-scratch-vm/src/extension-support/tw-load-script-as-plain-text.js new file mode 100644 index 0000000000000000000000000000000000000000..c93203ea6c61fce1c68c28b9d61ce23f337b46bc --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-load-script-as-plain-text.js @@ -0,0 +1,20 @@ +// Based on https://github.com/webpack-contrib/worker-loader/tree/v2.0.0 + +const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); + +module.exports.pitch = function (request) { + // Technically this loader does work in other environments, but our use case does not want that. + if (this.target !== 'web') { + return 'throw new Error("Not supported in non-web environment");'; + } + this.cacheable(false); + const callback = this.async(); + const compiler = this._compilation.createChildCompiler('extension worker', {}); + new SingleEntryPlugin(this.context, `!!${request}`, 'extension worker').apply(compiler); + compiler.runAsChild((err, entries, compilation) => { + if (err) return callback(err); + const file = entries[0].files[0]; + const source = `module.exports = ${JSON.stringify(compilation.assets[file].source())};`; + return callback(null, source); + }); +}; diff --git a/local-scratch-vm/src/extension-support/tw-scratchx-compatibility-layer.js b/local-scratch-vm/src/extension-support/tw-scratchx-compatibility-layer.js new file mode 100644 index 0000000000000000000000000000000000000000..bceae527aaefc737f90a3ec1bdc1d81608f7f8f5 --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-scratchx-compatibility-layer.js @@ -0,0 +1,223 @@ +// ScratchX API Documentation: https://github.com/LLK/scratchx/wiki/ + +const ArgumentType = require('./argument-type'); +const BlockType = require('./block-type'); + +const { + argumentIndexToId, + generateExtensionId +} = require('./tw-scratchx-utilities'); + +/** + * @typedef ScratchXDescriptor + * @property {unknown[][]} blocks + * @property {Record} [menus] + * @property {string} [url] + * @property {string} [displayName] + */ + +/** + * @typedef ScratchXStatus + * @property {0|1|2} status 0 is red/error, 1 is yellow/not ready, 2 is green/ready + * @property {string} msg + */ + +const parseScratchXBlockType = type => { + if (type === '' || type === ' ' || type === 'w') { + return { + type: BlockType.COMMAND, + async: type === 'w' + }; + } + if (type === 'r' || type === 'R') { + return { + type: BlockType.REPORTER, + async: type === 'R' + }; + } + if (type === 'b') { + return { + type: BlockType.BOOLEAN, + // ScratchX docs don't seem to mention boolean reporters that wait + async: false + }; + } + if (type === 'h') { + return { + type: BlockType.HAT, + async: false + }; + } + throw new Error(`Unknown ScratchX block type: ${type}`); +}; + +const isScratchCompatibleValue = v => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'; + +/** + * @param {string} argument ScratchX argument with leading % removed. + * @param {unknown} defaultValue Default value, if any + */ +const parseScratchXArgument = (argument, defaultValue) => { + const result = {}; + const hasDefaultValue = isScratchCompatibleValue(defaultValue); + if (hasDefaultValue) { + result.defaultValue = defaultValue; + } + // TODO: ScratchX docs don't mention support for boolean arguments? + if (argument === 's') { + result.type = ArgumentType.STRING; + if (!hasDefaultValue) { + result.defaultValue = ''; + } + } else if (argument === 'n') { + result.type = ArgumentType.NUMBER; + if (!hasDefaultValue) { + result.defaultValue = 0; + } + } else if (argument[0] === 'm') { + result.type = ArgumentType.STRING; + const split = argument.split(/\.|:/); + const menuName = split[1]; + result.menu = menuName; + } else { + throw new Error(`Unknown ScratchX argument type: ${argument}`); + } + return result; +}; + +const wrapScratchXFunction = (originalFunction, argumentCount, async) => args => { + // Convert Scratch 3's argument object to an argument list expected by ScratchX + const argumentList = []; + for (let i = 0; i < argumentCount; i++) { + argumentList.push(args[argumentIndexToId(i)]); + } + if (async) { + return new Promise(resolve => { + originalFunction(...argumentList, resolve); + }); + } + return originalFunction(...argumentList); +}; + +/** + * @param {string} name + * @param {ScratchXDescriptor} descriptor + * @param {Record unknown>} functions + */ +const convert = (name, descriptor, functions) => { + const extensionId = generateExtensionId(name); + const info = { + id: extensionId, + name: descriptor.displayName || name, + blocks: [], + color1: '#4a4a5e', + color2: '#31323f', + color3: '#191a21' + }; + const scratch3Extension = { + getInfo: () => info, + _getStatus: functions._getStatus + }; + + if (descriptor.url) { + info.docsURI = descriptor.url; + } + + for (const blockDescriptor of descriptor.blocks) { + if (blockDescriptor.length === 1) { + // Separator + info.blocks.push('---'); + continue; + } + const scratchXBlockType = blockDescriptor[0]; + const blockText = blockDescriptor[1]; + const functionName = blockDescriptor[2]; + const defaultArgumentValues = blockDescriptor.slice(3); + + let scratchText = ''; + const argumentInfo = []; + const blockTextParts = blockText.split(/%([\w.:]+)/g); + for (let i = 0; i < blockTextParts.length; i++) { + const part = blockTextParts[i]; + const isArgument = i % 2 === 1; + if (isArgument) { + parseScratchXArgument(part); + const argumentIndex = Math.floor(i / 2).toString(); + const argumentDefaultValue = defaultArgumentValues[argumentIndex]; + const argumentId = argumentIndexToId(argumentIndex); + argumentInfo[argumentId] = parseScratchXArgument(part, argumentDefaultValue); + scratchText += `[${argumentId}]`; + } else { + scratchText += part; + } + } + + const scratch3BlockType = parseScratchXBlockType(scratchXBlockType); + const blockInfo = { + opcode: functionName, + blockType: scratch3BlockType.type, + text: scratchText, + arguments: argumentInfo + }; + info.blocks.push(blockInfo); + + const originalFunction = functions[functionName]; + const argumentCount = argumentInfo.length; + scratch3Extension[functionName] = wrapScratchXFunction( + originalFunction, + argumentCount, + scratch3BlockType.async + ); + } + + const menus = descriptor.menus; + if (menus) { + const scratch3Menus = {}; + for (const menuName of Object.keys(menus) || {}) { + const menuItems = menus[menuName]; + const menuInfo = { + items: menuItems + }; + scratch3Menus[menuName] = menuInfo; + } + info.menus = scratch3Menus; + } + + return scratch3Extension; +}; + +const extensionNameToExtension = new Map(); + +/** + * @param {*} Scratch Scratch 3.0 extension API object + * @returns {*} ScratchX-compatible API object + */ +const createScratchX = Scratch => { + const register = (name, descriptor, functions) => { + const scratch3Extension = convert(name, descriptor, functions); + extensionNameToExtension.set(name, scratch3Extension); + Scratch.extensions.register(scratch3Extension); + }; + + /** + * @param {string} extensionName + * @returns {ScratchXStatus} + */ + const getStatus = extensionName => { + const extension = extensionNameToExtension.get(extensionName); + if (extension) { + return extension._getStatus(); + } + return { + status: 0, + msg: 'does not exist' + }; + }; + + return { + register, + getStatus + }; +}; + +module.exports = createScratchX; diff --git a/local-scratch-vm/src/extension-support/tw-scratchx-utilities.js b/local-scratch-vm/src/extension-support/tw-scratchx-utilities.js new file mode 100644 index 0000000000000000000000000000000000000000..29aa601558bfd9e2160fde16ef775721111113a8 --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-scratchx-utilities.js @@ -0,0 +1,25 @@ +/** + * @fileoverview + * General ScratchX-related utilities used in multiple places. + * Changing these functions may break projects. + */ + +/** + * @param {string} scratchXName + * @returns {string} + */ +const generateExtensionId = scratchXName => { + const sanitizedName = scratchXName.replace(/[^a-z0-9]/gi, '').toLowerCase(); + return `sbx${sanitizedName}`; +}; + +/** + * @param {number} i 0-indexed index of argument in list + * @returns {string} Scratch 3 argument name + */ +const argumentIndexToId = i => i.toString(); + +module.exports = { + generateExtensionId, + argumentIndexToId +}; diff --git a/local-scratch-vm/src/extension-support/tw-security-manager.js b/local-scratch-vm/src/extension-support/tw-security-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..5a8112804d7add9c99ae37da1ffb2843c41f06a3 --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-security-manager.js @@ -0,0 +1,169 @@ +/* eslint-disable no-unused-vars */ + +/** + * Responsible for determining various policies related to custom extension security. + * The default implementation prevents automatic extension loading, but grants any + * loaded extensions the maximum possible capabilities so as to retain compatibility + * with a vanilla scratch-vm. You may override properties of an instance of this class + * to customize the security policies as you see fit, for example: + * ```js + * vm.securityManager.getSandboxMode = (url) => { + * if (url.startsWith("https://example.com/")) { + * return "unsandboxed"; + * } + * return "iframe"; + * }; + * vm.securityManager.canAutomaticallyLoadExtension = (url) => { + * return confirm("Automatically load extension: " + url); + * }; + * vm.securityManager.canFetch = (url) => { + * return url.startsWith('https://turbowarp.org/'); + * }; + * vm.securityManager.canOpenWindow = (url) => { + * return url.startsWith('https://turbowarp.org/'); + * }; + * vm.securityManager.canRedirect = (url) => { + * return url.startsWith('https://turbowarp.org/'); + * }; + * ``` + */ +class SecurityManager { + /** + * Determine the typeof sandbox to use for a certain custom extension. + * @param {string} extensionURL The URL of the custom extension. + * @returns {'worker'|'iframe'|'unsandboxed'|Promise<'worker'|'iframe'|'unsandboxed'>} + */ + getSandboxMode (extensionURL) { + // Default to worker for Scratch compatibility + return Promise.resolve('worker'); + } + + /** + * Determine whether a custom extension that was stored inside a project may be + * loaded. You could, for example, ask the user to confirm loading an extension + * before resolving. + * @param {string} extensionURL The URL of the custom extension. + * @returns {Promise|boolean} + */ + canLoadExtensionFromProject (extensionURL) { + // Default to false for security + return Promise.resolve(false); + } + + /** + * Allows last-minute changing the real URL of the extension that gets loaded. + * @param {*} extensionURL The URL requested to be loaded. + * @returns {Promise|string} The URL to actually load. + */ + rewriteExtensionURL (extensionURL) { + return Promise.resolve(extensionURL); + } + + /** + * Determine whether an extension is allowed to fetch a remote resource URL. + * This only applies to unsandboxed extensions that use the appropriate Scratch.* APIs. + * Sandboxed extensions ignore this entirely as there is no way to force them to use our APIs. + * data: and blob: URLs are always allowed (this method is never called). + * @param {string} resourceURL + * @returns {Promise|boolean} + */ + canFetch (resourceURL) { + // By default, allow any requests. + return Promise.resolve(true); + } + + /** + * Determine whether an extension is allowed to open a new window or tab to a given URL. + * This only applies to unsandboxed extensions. Sandboxed extensions are unable to open windows. + * javascript: URLs are always rejected (this method is never called). + * @param {string} websiteURL + * @returns {Promise|boolean} + */ + canOpenWindow (websiteURL) { + // By default, allow all. + return Promise.resolve(true); + } + + /** + * Determine whether an extension is allowed to redirect the current tab to a given URL. + * This only applies to unsandboxed extensions. Sandboxed extensions are unable to redirect the parent + * window, but are free to redirect their own sandboxed window. + * javascript: URLs are always rejected (this method is never called). + * @param {string} websiteURL + * @returns {Promise|boolean} + */ + canRedirect (websiteURL) { + // By default, allow all. + return Promise.resolve(true); + } + + /** + * Determine whether an extension is allowed to record audio from the user's microphone. + * This could include raw audio data or a transcriptions. + * Note that, even if this returns true, success is not guaranteed. + * @returns {Promise|boolean} + */ + canRecordAudio () { + return Promise.resolve(true); + } + + /** + * Determine whether an extension is allowed to record video from the user's camera. + * Note that, even if this returns true, success is not guaranteed. + * @returns {Promise|boolean} + */ + canRecordVideo () { + return Promise.resolve(true); + } + + /** + * Determine whether an extension is allowed to read values from the user's clipboard + * without user interaction. + * Note that, even if this returns true, success is not guaranteed. + * @returns {Promise|boolean} + */ + canReadClipboard () { + return Promise.resolve(true); + } + + /** + * Determine whether an extension is allowed to show notifications. + * Note that, even if this returns true, success is not guaranteed. + * @returns {Promise|boolean} + */ + canNotify () { + return Promise.resolve(true); + } + + /** + * Determine whether an extension is allowed to find the user's precise location using GPS + * and other techniques. Note that, even if this returns true, success is not guaranteed. + * @returns {Promise|boolean} + */ + canGeolocate () { + return Promise.resolve(true); + } + + /** + * Determine whether an extension is allowed to embed content from a given URL. + * @param {string} documentURL The URL of the embed. + * @returns {Promise|boolean} + */ + canEmbed (documentURL) { + return Promise.resolve(true); + } + + /** + * pm: Used to prompt the user if they would like to unsandbox a feature in the extension. + * @returns {Promise|boolean} + */ + canUnsandbox() { + return Promise.resolve(false); + } + + shouldUseLocal(refrenceName) { + return Promise.resolve(!confirm(`it seems that the extension ${refrenceName} has been updated, use the up-to-date code?`)) + } +} + +module.exports = SecurityManager; diff --git a/local-scratch-vm/src/extension-support/tw-unsandboxed-extension-runner.js b/local-scratch-vm/src/extension-support/tw-unsandboxed-extension-runner.js new file mode 100644 index 0000000000000000000000000000000000000000..44b3a4cb11876b7d07d8fe4b23b944fbb44b0a84 --- /dev/null +++ b/local-scratch-vm/src/extension-support/tw-unsandboxed-extension-runner.js @@ -0,0 +1,168 @@ +const ScratchCommon = require('./tw-extension-api-common'); +const createScratchX = require('./tw-scratchx-compatibility-layer'); +const AsyncLimiter = require('../util/async-limiter'); +const createTranslate = require('./tw-l10n'); + +/** + * Parse a URL object or return null. + * @param {string} url + * @returns {URL|null} + */ +const parseURL = url => { + try { + return new URL(url, location.href); + } catch (e) { + return null; + } +}; + +/** + * Sets up the global.Scratch API for an unsandboxed extension. + * @param {VirtualMachine} vm + * @returns {Promise} Resolves with a list of extension objects when Scratch.extensions.register is called. + */ +const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => { + const extensionObjects = []; + const register = extensionObject => { + extensionObjects.push(extensionObject); + resolve(extensionObjects); + }; + + // Create a new copy of global.Scratch for each extension + const Scratch = Object.assign({}, global.Scratch || {}, ScratchCommon); + Scratch.extensions = { + unsandboxed: true, + isPenguinMod: true, + register + }; + Scratch.vm = vm; + Scratch.renderer = vm.runtime.renderer; + + Scratch.canFetch = async url => { + const parsed = parseURL(url); + if (!parsed) { + return false; + } + // Always allow protocols that don't involve a remote request. + if (parsed.protocol === 'blob:' || parsed.protocol === 'data:') { + return true; + } + return vm.securityManager.canFetch(parsed.href); + }; + + Scratch.canOpenWindow = async url => { + const parsed = parseURL(url); + if (!parsed) { + return false; + } + // Always reject protocols that would allow code execution. + // eslint-disable-next-line no-script-url + if (parsed.protocol === 'javascript:') { + return false; + } + return vm.securityManager.canOpenWindow(parsed.href); + }; + + Scratch.canRedirect = async url => { + const parsed = parseURL(url); + if (!parsed) { + return false; + } + // Always reject protocols that would allow code execution. + // eslint-disable-next-line no-script-url + if (parsed.protocol === 'javascript:') { + return false; + } + return vm.securityManager.canRedirect(parsed.href); + }; + + Scratch.fetch = async (url, options) => { + const actualURL = url instanceof Request ? url.url : url; + if (!await Scratch.canFetch(actualURL)) { + throw new Error(`Permission to fetch ${actualURL} rejected.`); + } + return fetch(url, options); + }; + + Scratch.openWindow = async (url, features) => { + if (!await Scratch.canOpenWindow(url)) { + throw new Error(`Permission to open tab ${url} rejected.`); + } + return window.open(url, '_blank', features); + }; + + Scratch.redirect = async url => { + if (!await Scratch.canRedirect(url)) { + throw new Error(`Permission to redirect to ${url} rejected.`); + } + location.href = url; + }; + + Scratch.canRecordAudio = async () => vm.securityManager.canRecordAudio(); + + Scratch.canRecordVideo = async () => vm.securityManager.canRecordVideo(); + + Scratch.canReadClipboard = async () => vm.securityManager.canReadClipboard(); + + Scratch.canNotify = async () => vm.securityManager.canNotify(); + + Scratch.canGeolocate = async () => vm.securityManager.canGeolocate(); + + Scratch.canEmbed = async url => { + const parsed = parseURL(url); + if (!parsed) { + return false; + } + return vm.securityManager.canEmbed(parsed.href); + }; + + Scratch.canUnsandbox = async () => vm.securityManager.canUnsandbox(); + + Scratch.translate = createTranslate(vm); + + global.Scratch = Scratch; + global.ScratchExtensions = createScratchX(Scratch); + + vm.emit('CREATE_UNSANDBOXED_EXTENSION_API', Scratch); +}); + +/** + * Disable the existing global.Scratch unsandboxed extension APIs. + * This helps debug poorly designed extensions. + */ +const teardownUnsandboxedExtensionAPI = () => { + // We can assume global.Scratch already exists. + global.Scratch.extensions.register = () => { + throw new Error('Too late to register new extensions.'); + }; +}; + +/** + * Load an unsandboxed extension from an arbitrary URL. This is dangerous. + * @param {string} extensionURL + * @param {Virtualmachine} vm + * @returns {Promise} Resolves with a list of extension objects if the extension was loaded successfully. + */ +const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, reject) => { + setupUnsandboxedExtensionAPI(vm).then(resolve); + + const script = document.createElement('script'); + script.onerror = () => { + reject(new Error(`Error in unsandboxed script ${extensionURL}. Check the console for more information.`)); + }; + script.src = extensionURL; + document.body.appendChild(script); +}).then(objects => { + teardownUnsandboxedExtensionAPI(); + return objects; +}); + +// Because loading unsandboxed extensions requires messing with global state (global.Scratch), +// only let one extension load at a time. +const limiter = new AsyncLimiter(loadUnsandboxedExtension, 1); +const load = (extensionURL, vm) => limiter.do(extensionURL, vm); + +module.exports = { + setupUnsandboxedExtensionAPI, + load +}; diff --git a/local-scratch-vm/src/extensions/GamepadExtension/index.js b/local-scratch-vm/src/extensions/GamepadExtension/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2c5d16b86d3a4f4f776cdb531f49c8b5f09e8883 --- /dev/null +++ b/local-scratch-vm/src/extensions/GamepadExtension/index.js @@ -0,0 +1,484 @@ +// Some parts of this scripts are based on or designed to be compatible-ish with: +// https://arpruss.github.io/gamepad.js (MIT Licensed) + +const ExtensionApi = require("../../util/custom-ext-api-to-core.js"); +const Scratch = new ExtensionApi(true); + + const AXIS_DEADZONE = 0.1; + const BUTTON_DEADZONE = 0.05; + + /** + * @param {number|'any'} index 1-indexed index + * @returns {Gamepad[]} + */ + const getGamepads = (index) => { + if (index === 'any') { + return navigator.getGamepads().filter(i => i); + } + const gamepad = navigator.getGamepads()[index - 1]; + if (gamepad) { + return [gamepad]; + } + return []; + }; + + /** + * @param {Gamepad} gamepad + * @param {number|'any'} buttonIndex 1-indexed index + * @returns {boolean} false if button does not exist + */ + const isButtonPressed = (gamepad, buttonIndex) => { + if (buttonIndex === 'any') { + return gamepad.buttons.some(i => i.pressed); + } + const button = gamepad.buttons[buttonIndex - 1]; + if (!button) { + return false; + } + return button.pressed; + }; + + /** + * @param {Gamepad} gamepad + * @param {number} buttonIndex 1-indexed index + * @returns {number} 0 if button does not exist + */ + const getButtonValue = (gamepad, buttonIndex) => { + const button = gamepad.buttons[buttonIndex - 1]; + if (!button) { + return 0; + } + const value = button.value; + if (value < BUTTON_DEADZONE) { + return 0; + } + return value; + }; + + /** + * @param {Gamepad} gamepad + * @param {number} axisIndex 1-indexed index + * @returns {number} 0 if axis does not exist + */ + const getAxisValue = (gamepad, axisIndex) => { + const axisValue = gamepad.axes[axisIndex - 1]; + if (typeof axisValue !== 'number') { + return 0; + } + if (Math.abs(axisValue) < AXIS_DEADZONE) { + return 0; + } + return axisValue; + }; + + class GamepadExtension { + getInfo() { + return { + id: 'Gamepad', + name: 'Gamepad', + blocks: [ + { + opcode: 'gamepadConnected', + blockType: Scratch.BlockType.BOOLEAN, + text: 'is gamepad [pad] connected?', + arguments: { + pad: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + } + } + }, + { + opcode: 'buttonDown', + blockType: Scratch.BlockType.BOOLEAN, + text: 'button [b] on pad [i] pressed?', + arguments: { + b: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'buttonMenu' + }, + i: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + } + } + }, + { + opcode: 'buttonValue', + blockType: Scratch.BlockType.REPORTER, + text: 'value of button [b] on pad [i]', + arguments: { + b: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'buttonMenu' + }, + i: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + } + } + }, + { + opcode: 'axisValue', + blockType: Scratch.BlockType.REPORTER, + text: 'value of axis [b] on pad [i]', + arguments: { + b: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'axisMenu' + }, + i: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + }, + }, + }, + + '---', + + { + opcode: 'axisDirection', + blockType: Scratch.BlockType.REPORTER, + text: 'direction of axes [axis] on pad [pad]', + arguments: { + axis: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'axesGroupMenu' + }, + pad: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + } + } + }, + { + opcode: 'axisMagnitude', + blockType: Scratch.BlockType.REPORTER, + text: 'magnitude of axes [axis] on pad [pad]', + arguments: { + axis: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'axesGroupMenu' + }, + pad: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + } + } + }, + + /* + { + opcode: 'buttonPressedReleased', + blockType: Scratch.BlockType.HAT, + text: 'button [b] [pr] of pad [i]', + arguments: { + b: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1' + }, + pr: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'pressReleaseMenu' + }, + i: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + }, + }, + }, + + { + opcode: 'axisMoved', + blockType: Scratch.BlockType.HAT, + text: 'axis [b] of pad [i] moved', + arguments: { + b: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1' + }, + i: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + }, + }, + }, + */ + + '---', + + { + opcode: 'rumble', + blockType: Scratch.BlockType.COMMAND, + text: 'rumble strong [s] and weak [w] for [t] sec. on pad [i]', + arguments: { + s: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0.25' + }, + w: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0.5' + }, + t: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0.25' + }, + i: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1', + menu: 'padMenu' + }, + }, + }, + ], + menus: { + padMenu: { + acceptReporters: true, + items: [ + { + text: 'any', + value: 'any' + }, + { + text: '1', + value: '1' + }, + { + text: '2', + value: '2' + }, + { + text: '3', + value: '3' + }, + { + text: '4', + value: '4' + } + ], + }, + buttonMenu: { + acceptReporters: true, + items: [ + // Based on an Xbox controller + { + text: 'any', + value: 'any' + }, + { + text: 'A (1)', + value: '1' + }, + { + text: 'B (2)', + value: '2' + }, + { + text: 'X (3)', + value: '3' + }, + { + text: 'Y (4)', + value: '4' + }, + { + text: 'Left bumper (5)', + value: '5' + }, + { + text: 'Right bumper (6)', + value: '6' + }, + { + text: 'Left trigger (7)', + value: '7' + }, + { + text: 'Right trigger (8)', + value: '8' + }, + { + text: 'Select/View (9)', + value: '9' + }, + { + text: 'Start/Menu (10)', + value: '10' + }, + { + text: 'Left stick (11)', + value: '11' + }, + { + text: 'Right stick (12)', + value: '12' + }, + { + text: 'D-pad up (13)', + value: '13' + }, + { + text: 'D-pad down (14)', + value: '14' + }, + { + text: 'D-pad left (15)', + value: '15' + }, + { + text: 'D-pad right (16)', + value: '16' + }, + ] + }, + axisMenu: { + acceptReporters: true, + items: [ + // Based on an Xbox controller + { + text: 'Left stick horizontal (1)', + value: '1' + }, + { + text: 'Left stick vertical (2)', + value: '2' + }, + { + text: 'Right stick horizontal (3)', + value: '3' + }, + { + text: 'Right stick vertical (4)', + value: '4' + } + ] + }, + axesGroupMenu: { + acceptReporters: true, + items: [ + // Based on an Xbox controller + { + text: 'Left stick (1 & 2)', + value: '1' + }, + { + text: 'Right stick (3 & 4)', + value: '3' + } + ] + }, + /* + pressReleaseMenu: [ + { + text: 'press', + value: 1 + }, + { + text: 'release', + value: 0 + } + ], + */ + } + }; + } + + gamepadConnected ({pad}) { + return getGamepads(pad).length > 0; + } + + buttonDown ({b, i}) { + for (const gamepad of getGamepads(i)) { + if (isButtonPressed(gamepad, b)) { + return true; + } + } + return false; + } + + buttonValue ({b, i}) { + let greatestButton = 0; + for (const gamepad of getGamepads(i)) { + const value = getButtonValue(gamepad, b); + if (value > greatestButton) { + greatestButton = value; + } + } + return greatestButton; + } + + axisValue ({b, i}) { + let greatestAxis = 0; + for (const gamepad of getGamepads(i)) { + const axis = getAxisValue(gamepad, b); + if (Math.abs(axis) > Math.abs(greatestAxis)) { + greatestAxis = axis; + } + } + return greatestAxis; + } + + axisDirection ({axis, pad}) { + let greatestMagnitude = 0; + let direction = 90; + for (const gamepad of getGamepads(pad)) { + const horizontalAxis = getAxisValue(gamepad, axis); + const verticalAxis = getAxisValue(gamepad, +axis + 1); + const magnitude = Math.sqrt(horizontalAxis ** 2 + verticalAxis ** 2); + if (magnitude > greatestMagnitude) { + greatestMagnitude = magnitude; + direction = Math.atan2(verticalAxis, horizontalAxis) * 180 / Math.PI + 90; + if (direction < 0) { + direction += 360; + } + } + } + return direction; + } + + axisMagnitude ({axis, pad}) { + let greatestMagnitude = 0; + for (const gamepad of getGamepads(pad)) { + const horizontalAxis = getAxisValue(gamepad, axis); + const verticalAxis = getAxisValue(gamepad, +axis + 1); + const magnitude = Math.sqrt(horizontalAxis ** 2 + verticalAxis ** 2); + if (magnitude > greatestMagnitude) { + greatestMagnitude = magnitude; + } + } + return greatestMagnitude; + } + + rumble ({s, w, t, i}) { + const gamepads = getGamepads(i); + for (const gamepad of gamepads) { + // @ts-ignore + if (gamepad.vibrationActuator) { + // @ts-ignore + gamepad.vibrationActuator.playEffect('dual-rumble', { + startDelay: 0, + duration: t * 1000, + weakMagnitude: w, + strongMagnitude: s + }); + } + } + } + } + + module.exports = GamepadExtension; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/TEST_EXTENSION/index.js b/local-scratch-vm/src/extensions/TEST_EXTENSION/index.js new file mode 100644 index 0000000000000000000000000000000000000000..29567fe00a43ae0c5dd1f5b851daf506c5d4f8a0 --- /dev/null +++ b/local-scratch-vm/src/extensions/TEST_EXTENSION/index.js @@ -0,0 +1,24 @@ +//THIS IS A TEST EXTENSION, AND AT NO POINT SHOULD IT BE ADDED TO THE EXTENSION MENU OR THE IMPORT LIST + +class TESTEXTENSION { + getInfo() { + return { + id: 'TESTEXTENSION', // change this if you make an actual extension! + name: 'TESTEXTENSION', + blocks: [ + { + opcode: 'logBranch', + blockType: Scratch.BlockType.COMMAND, + branchCount: 1, + text: 'Log value of first block in branch', + } + ] + }; + } + logBranch(args, util) { + if (util.thread.peekStack() && util.target.blocks.getBlock(util.thread.peekStack()) && util.target.blocks.getBlock(util.thread.peekStack()).inputs.SUBSTACK.block) { + console.log(util.target.blocks.getBlock(util.thread.peekStack()).inputs.SUBSTACK.block) + } + } +} +Scratch.extensions.register(new TESTEXTENSION()); diff --git a/local-scratch-vm/src/extensions/TEST_EXTENSION/pm.js b/local-scratch-vm/src/extensions/TEST_EXTENSION/pm.js new file mode 100644 index 0000000000000000000000000000000000000000..e9498484e5d5061c70a361fc69d5fb8184c44158 --- /dev/null +++ b/local-scratch-vm/src/extensions/TEST_EXTENSION/pm.js @@ -0,0 +1,25 @@ +//THIS IS A TEST EXTENSION, AND AT NO POINT SHOULD IT BE ADDED TO THE EXTENSION MENU OR THE IMPORT LIST +const BlockType = require("../../extension-support/block-type") + +class TESTEXTENSION { + getInfo() { + return { + id: 'TESTEXTENSION', // change this if you make an actual extension! + name: 'TESTEXTENSION', + blocks: [ + { + opcode: 'logBranch', + blockType: BlockType.COMMAND, + branchCount: 1, + text: 'Log value of first block in branch', + } + ] + }; + } + logBranch(args, util) { + if (util.thread.peekStack() && util.target.blocks.getBlock(util.thread.peekStack()) && util.target.blocks.getBlock(util.thread.peekStack()).inputs.SUBSTACK.block) { + console.log(util.target.blocks.getBlock(util.thread.peekStack()).inputs.SUBSTACK.block) + } + } +} +module.exports = TESTEXTENSION diff --git a/local-scratch-vm/src/extensions/blockly-2/math.js b/local-scratch-vm/src/extensions/blockly-2/math.js new file mode 100644 index 0000000000000000000000000000000000000000..eba97769da74e87163f13aaf8fd8436a7689daca --- /dev/null +++ b/local-scratch-vm/src/extensions/blockly-2/math.js @@ -0,0 +1,316 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +// const Cast = require('../../util/cast'); + +//const blockIconURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAMAAAD6TlWYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAPUExURQAAAP+xNQDiGgCU/wAAAJEQGGoAAAAFdFJOU/////8A+7YOUwAAAAlwSFlzAAAOwwAADsMBx2+oZAAABA5JREFUeF7t0EtuW0EUA9F8vP81Z8JRAwzbLuk5COoMBb1LdP34EGJAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEHos4M+HZfbtDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0KPBfxfGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEZsBfh/z8z/r9SfnsywwIGRAyIGRAyICQASEDQp8OeMrfvk06vEzOXjPgIWevGfCQs9cMeMjZawY85Ow1Ax5y9poBDzl7zYCHnL2GA57y2dvlvW+TmcmARWYmAxaZmQxYZGYyYJGZyYBFZiYDFpmZDFhkZnp5wFPOvFze+TaZmQxYZGYyYJGZyYBFZiYDFpmZDFhkZjJgkZnJgEVmprcHPOXsl+V9j8lsZcAhs5UBh8xWBhwyWxlwyGxlwCGzlQGHzFYGHDJbPR7wlJlreddjMlsZcMhsZcAhs5UBh8xWBhwyWxlwyGxlwCGzlQGHzFbfHvCU2SrvekxmKwMOma0MOGS2MuCQ2cqAQ2YrAw6ZrQw4ZLYy4JDZyoBDZisDDpmtDDhktjLgkNnKgENmKwMOma0MOGS2MuCQ2erbA2bmWt71mMxWBhwyWxlwyGxlwCGzlQGHzFYGHDJbGXDIbGXAIbPV4wFz9svyrsdktjLgkNnKgENmKwMOma0MOGS2MuCQ2cqAQ2YrAw6Zrd4eMGdeLu97m8xMBiwyMxmwyMxkwCIzkwGLzEwGLDIzGbDIzGTAIjPTywPms7fLO98mM5MBi8xMBiwyMxmwyMxkwCIzkwGLzEwGLDIzGbDIzIQD5m/fJu99mZy9ZsBDzl4z4CFnrxnwkLPXDHjI2WsGPOTsNQMecvaaAQ85e+3TAfPzPysdruWzLzMgZEDIgJABIQNCBoQMCM2A+jsDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgMjHxx+IPExM0h8siAAAAABJRU5ErkJggg==" + +/** + * Class for Blocky2 blocks + * @constructor + */ +class Blockly2Math { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'blockly2math', + name: 'Math', + //blockIconURI: blockIconURI, + color1: '#5b67a5', + color2: '#444d7c', + blocks: [ + { + opcode: 'Number', + text: formatMessage({ + id: 'blockly2math.blocks.Number', + default: '[NUMBER]', + description: 'Define a number' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + NUMBER: { + type: ArgumentType.NUMBER, + defaultValue: 123 + } + } + }, + { + opcode: 'Operation', + text: formatMessage({ + id: 'blockly2math.blocks.Operation', + default: '[ONE][OP][TWO]', + description: 'Perform a basic math operation' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + ONE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + OP: { + type: ArgumentType.STRING, + defaultValue: "+", + menu: "Operation" + }, + TWO: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'AdvancedOperation', + text: formatMessage({ + id: 'blockly2math.blocks.AdvancedOperation', + default: '[OP][ONE]', + description: 'Perform a advanced math operation' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + ONE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + OP: { + type: ArgumentType.STRING, + defaultValue: "square root", + menu: "AdvancedOperation" + }, + } + }, + { + opcode: 'Function', + text: formatMessage({ + id: 'blockly2math.blocks.Function', + default: '[OP][ONE]', + description: 'Perform a math function' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + ONE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + OP: { + type: ArgumentType.STRING, + defaultValue: "sin", + menu: "Function" + }, + } + }, + { + opcode: 'Constant', + text: formatMessage({ + id: 'blockly2math.blocks.Constant', + default: '[CONST]', + description: 'Retrieve a constant' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + CONST: { + type: ArgumentType.STRING, + defaultValue: "π", + menu: "Constant" + }, + } + }, + { + opcode: 'IsOption', + text: formatMessage({ + id: 'blockly2math.blocks.IsOption', + default: '[ONE] is [OPTION]?', + description: 'Check if number match condition' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + arguments: { + ONE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + OPTION: { + type: ArgumentType.STRING, + defaultValue: "even", + menu: "IsOption" + }, + } + }, + { + opcode: 'IsOption2', + text: formatMessage({ + id: 'blockly2math.blocks.IsOption2', + default: '[ONE] is [OPTION] [TWO]?', + description: 'Check if numbers match condition' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + arguments: { + ONE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + TWO: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + OPTION: { + type: ArgumentType.STRING, + defaultValue: "even", + menu: "IsOption2" + }, + } + }, + ], + menus: { + Operation: [ + "+", + "-", + "×", + "÷", + "^" + ], + AdvancedOperation: [ + "square root", + "absolute", + "-", + "ln", + "log10", + "e^", + "10^" + ], + Function: [ + "sin", + "cos", + "tan", + "asin", + "acos", + "atan" + ], + Constant: [ + "π", + "e", + "φ", + "sqrt(2)", + "sqrt(½)", + "∞" + ], + IsOption: [ + "even", + "odd", + "prime", + "whole", + "positive", + "negative", + ], + IsOption2: [ + "divisible by" + ] + } + }; + } + + Number(args, util) { + return Number(args.NUMBER) + } + + Operation(args, util) { + switch (String(args.OP)) { + case "+": return Number(args.ONE) + Number(args.TWO) + case "-": return Number(args.ONE) - Number(args.TWO) + case "×": return Number(args.ONE) * Number(args.TWO) + case "÷": return Number(args.ONE) / Number(args.TWO) + case "^": return Number(args.ONE) ** Number(args.TWO) + default: return Number(args.ONE) + } + } + + AdvancedOperation(args, util) { + switch (String(args.OP)) { + case "square root": return Math.sqrt(Number(args.ONE)) + case "absolute": return Math.abs(Number(args.ONE)) + case "-": return 0 - Number(args.ONE) + case "ln": return Math.log(Number(args.ONE)) + case "log10": return Math.log10(Number(args.ONE)) + case "e^": return Math.exp(Number(args.ONE)) + case "10^": return Math.pow(10, Number(args.ONE)) + default: return Number(args.ONE) + } + } + + Function(args, util) { + switch (String(args.OP)) { + case "sin": return Math.sin(Number(args.ONE) / 180 * Math.PI) + case "tan": return Math.tan(Number(args.ONE) / 180 * Math.PI) + case "cos": return Math.cos(Number(args.ONE) / 180 * Math.PI) + case "asin": return Math.asin(Number(args.ONE)) / Math.PI * 180 + case "atan": return Math.atan(Number(args.ONE)) / Math.PI * 180 + case "acos": return Math.acos(Number(args.ONE)) / Math.PI * 180 + default: return Number(args.ONE) + } + } + + Constant(args, util) { + switch (String(args.CONST)) { + case "π": return Math.PI + case "e": return Math.E + case "φ": return (1 + Math.sqrt(5)) / 2 + case "sqrt(2)": return Math.SQRT2 + case "sqrt(½)": return Math.SQRT1_2 + case "∞": return Infinity + default: return 0 + } + } + + IsOption(args, util) { + switch (String(args.OPTION)) { + case "even": return Number(args.ONE) % 2 == 0 + case "odd": return Number(args.ONE) % 2 == 1 + case "prime": return this._isprime(Number(args.ONE)) + case "whole": return Number(args.ONE) % 1 == 0 + case "positive": return Number(args.ONE) > 0 + case "negative": return Number(args.ONE) < 0 + default: return false + } + } + + IsOption2(args, util) { + switch (String(args.OPTION)) { + case "divisible by": return Number(args.ONE) % Number(args.TWO) == 0 + default: return false + } + } + + _isprime(n) { + if (n == 2 || n == 3) { + return true; + } + + if (isNaN(n) || n <= 1 || n % 1 !== 0 || n % 2 === 0 || n % 3 === 0) { + return false; + } + + for (var x = 6; x <= Math.sqrt(n) + 1; x += 6) { + if (n % (x - 1) === 0 || n % (x + 1) === 0) { + return false; + } + } + return true; + } +} + +module.exports = Blockly2Math; diff --git a/local-scratch-vm/src/extensions/dt_cameracontrols/index.js b/local-scratch-vm/src/extensions/dt_cameracontrols/index.js new file mode 100644 index 0000000000000000000000000000000000000000..58edff377b9949b592ea4065cbfb47f2fe4292a2 --- /dev/null +++ b/local-scratch-vm/src/extensions/dt_cameracontrols/index.js @@ -0,0 +1,392 @@ +// Created by DT-is-not-available +// https://github.com/DT-is-not-available/ +// +// 99% of the code here was not created by a PenguinMod developer! +// Look above for proper crediting :) + +const ExtensionApi = require("../../util/custom-ext-api-to-core.js"); +const Scratch = new ExtensionApi(true); + +const icon = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyMjUuMzU0OCIgaGVpZ2h0PSIyMjUuMzU0OCIgdmlld0JveD0iMCwwLDIyNS4zNTQ4LDIyNS4zNTQ4Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTg3LjMyMjkzLC0zNy4zMjI1OSkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTg3LjMyMjk0LDE1MGMwLC02Mi4yMzAwMSA1MC40NDczOSwtMTEyLjY3NzQgMTEyLjY3NzQsLTExMi42Nzc0YzYyLjIzMDAxLDAgMTEyLjY3NzQsNTAuNDQ3MzkgMTEyLjY3NzQsMTEyLjY3NzRjMCw2Mi4yMzAwMSAtNTAuNDQ3MzksMTEyLjY3NzQgLTExMi42Nzc0LDExMi42Nzc0Yy02Mi4yMzAwMSwwIC0xMTIuNjc3NCwtNTAuNDQ3MzkgLTExMi42Nzc0LC0xMTIuNjc3NHoiIGZpbGw9IiNmZjRkYTciIHN0cm9rZS13aWR0aD0iMCIvPjxnPjxwYXRoIGQ9Ik0zMTcuMTAyOSw4MC44MTA4N2MyMS44OTI0LDAgMzkuNjYyMDcsMTcuNzM3MjMgMzkuNjYyMDcsMzkuNjM0NGMwLDEyLjMwNTE3IC01LjYxMTQ4LDIzLjI5NjIyIC0xNC40MDA4OCwzMC41NjgyNGg4Ljc3MDMydjY4LjE3NTYzaC0xMTQuMTMzMjV2LTU1Ljc5ODg5Yy0xNC4zMzQwOCwtMy41MjgxNyAtMjQuOTYxNTMsLTE2LjQ1NzQ3IC0yNC45NjE1MywtMzEuODgwNDRjMCwtMTguMTM5IDE0LjY5NjczLC0zMi44MzQ3OCAzMi44MzQ3OCwtMzIuODM0NzhjMTIuMDM3OTUsMCAyMi41NTY2MSw2LjQ3ODAxIDI4LjI3MjExLDE2LjEzMzk1bDQuODYxMzcsLTAuOTI0NzVjMy4xMjkyNiwtMTguNzY2OTYgMTkuNDM5NzYsLTMzLjA3MzM2IDM5LjA5OTAyLC0zMy4wNzMzNnpNMjc2LjIxODUxLDE0MS4yOTE3MWMtMS4xMDAzNSwzLjUzMzg5IC0yLjc2OTQ3LDYuODEyMDMgLTQuOTIwNTQsOS43MjE3OWgyMC41NDc3NGMtMy42ODc1NCwtMy4wNDgxNCAtNi44MDI0OCwtNi43NjUyNyAtOS4xODU0NSwtMTAuOTQ0Mjl6IiBmaWxsPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjEiLz48cGF0aCBkPSJNMzM2LjU3NTI5LDExOS41MjgxNWMwLDExLjA2ODM1IC04Ljk3MjY0LDIwLjA0MDk5IC0yMC4wNDA5OSwyMC4wNDA5OWMtMTEuMDY4MzUsMCAtMjAuMDQwOTksLTguOTcyNjQgLTIwLjA0MDk5LC0yMC4wNDA5OWMwLC0xMS4wNjgzNSA4Ljk3MjY0LC0yMC4wNDA5OSAyMC4wNDA5OSwtMjAuMDQwOTljMTEuMDY4MzUsMCAyMC4wNDA5OSw4Ljk3MjY0IDIwLjA0MDk5LDIwLjA0MDk5eiIgZmlsbD0iI2ZmNGRhNyIgc3Ryb2tlLXdpZHRoPSIwLjUiLz48cGF0aCBkPSJNMjYxLjE4MywxMzAuMDI1ODFjMCw4Ljk2MDA0IC03LjI2MzYyLDE2LjIyMzY2IC0xNi4yMjM2NiwxNi4yMjM2NmMtOC45NjAwNCwwIC0xNi4yMjM2NiwtNy4yNjM2MiAtMTYuMjIzNjYsLTE2LjIyMzY2YzAsLTguOTYwMDQgNy4yNjM2MiwtMTYuMjIzNjYgMTYuMjIzNjYsLTE2LjIyMzY2YzguOTYwMDQsMCAxNi4yMjM2Niw3LjI2MzYyIDE2LjIyMzY2LDE2LjIyMzY2eiIgZmlsbD0iI2ZmNGRhNyIgc3Ryb2tlLXdpZHRoPSIwLjUiLz48cGF0aCBkPSJNMzg3Ljk2MDM5LDE0NS44NTcyNHY2MC41MTA0M2wtMjEuNjAzMjYsLTEzLjQ3MjE3aC0xNi44OTgzNHYtMzMuNTY2MDdoMTYuODk4MzRsMjEuNTk5MTcsLTEzLjQ3MTEyeiIgZmlsbD0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIxIi8+PC9nPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjExMi42NzcwNjU6MTEyLjY3NzQwNS0tPg=='; +const CW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIUAAAB+CAYAAAAKj9LmAAAAAXNSR0IArs4c6QAABqdJREFUeF7tneuV1DAMhZdSoBWoB2qBeqAVKAWOOQRCNvHV27Kt/bsex7r6ciV7MjNvXupvWgW+v//8sy3+3bdPbyyDMJ3McmHauQ7BqPNYC0u9LnccissijumhQCJxRb8bbyG0dh3cODVrng4KrjjaZGSARBqzFIz0UEgF8YDhaU6p+JQ1auOXrC0tFFoxKIJ7jJEk4WkdVhpw15QKCisRPJLNnZObiOv8llpw15ICCksBKMl7+/Xj72E/PnyhDFeN4SbkuBjS5IjhGN+LhbuGoVCgwKnZuApEfR0aZwkNJzFIl6d4rcAYAgUKGiXLCwJ0XQt3QXAgbXqxTwkFCriXlJEgPK1L4yR3cCB9KBo8rQnBeI4xzClQwHfCU0Sg3N0RY6SAHMlC+lC1sHCLEChQwNekUQWISDb3GhI4GhhII6om6aFAga4Ewx08EkC0jpkaCg4Q1LuAe9dmGa+BQ6KNtq9wKR9UICQBZ0k0dx0SMKT6pIKCCkMTVBowNxnZxlPh0OijLSFmTkEFQhNstgRr1oPg0OiUAooCQobHU/I0QKBjb8p5hdopKEBYBCmTPf+rrmBYaaXpK1RQFBB20LUkWgGBjuORW4ihKCDsgPCYSdNXuEFhSb2HaDvMKS0hIiiQSxQQOZALg6KAyJFwyiqkJYTlFAUEJRV5xgyHokpGHhjQWUX7f28HQnaKcol8SaesSNJXkKAoICjy5xwzBIoqGzlh0JQQ6BQ9lyggcgMhPdnsQjFL2UDvOFJStzLg3BIihmK0iBYg9GAZHR8FZOoYMyiylg1vGK5CrwAH97zi0SmyQRENw0pwmECRCYjRMKwCB6eE3DpFFiiyAXEGZLayooJiBSDQQyTn5KId1irNKKeEvHKKDFBwHYIDQS/JEkBmcQwXKCKCHwXDHShcQCL0oW5Bn8ZRS8h/TjHSJThAWDkDRWQOHNnBMIUiIlgKFJEwXIFBcERoRIG4N2YqKLID0YReGYrr8xV/y8eo0lFAaO9/3uspbgGh8LTFAoKXUIvR00MxsodYpWRcQSJDMaJ0IJcYDQSCwtNBLRyBuy099xW/y0c0FLMD0TTbFgqvwDmna553zdPcK+w0NFvTrlPsCMXqQDRYUF8RDkVml9gBiB4UR1/R/ao+D6eYFQoPLUaURhUUHiLMCsTMjSV3F9J2fY9OUVD8k9NDi1EucVy311ekgCLzmcSKQKBms6AYcE4z2iVQXxEGxYz9xKouUVAQbsunrejKUPTAGO4UmfuJFXcdqNFs/y8o/qgU/f4PwcDchqBSXlAQoDiyM3s5QW9EwhNNawHQebvbbUGcGB1xE6eZfljo4VV2KFo2dwfj6O+qfFzu7V3BODf8BcWN4e8GxnUHWFA8dAG7gHF3JFBQEFrD1QBBZ0MFBQGK3YYUFLtlnBBvQUEQabchw6E4TtF2Ez5zvKHPaM5wgJU5WVFrC32au6CISqvuOgWFTr8lX50CiuorcrEV/lnSKiG5ALhbTfj3UxQUBcUrBdBTP/klW3+F4U7RJC23yA0WhKItP+oprGo4c8BC+h5Nayh6blFgjAejoBifg3QrIEHhUULKLdKx8HdB5C9sjy4hVUbGQUOGotxiXJKirzwciioj0SnH10vxy0Dok0vomUIcZo3gKJACCuQW1V9wUqofm+rXBssx9Am1mCEVFOUYFinVz5HyF4yRY1Q50Se+N0NKKCiOUWD4gdGFol125Jd5lGP4JV7sFAgKrwOt84IpYJRr2MIDnQKB4XH8fQ2RCsbxujrX0EFCgmJGMMo95GCYQBFRRo4Qua5R7sGHgwwFcosZwLiTp0rNa1VYUCAwIvoLSRPKuVcKkpcXUygi3aLg4KDOG8uGArnFKDDadaX9Rk+yHZ1DBEVmMLQNafUdgvJxFg19F1R0j9G747UuspNjiJ3iSMBMYNxBw4FlFzDcoRjZY/DaK1pPsgMYaigo/cVMYKCGtaBg3GqojBQYDDEHDzVxCmp/cYzL1IA+6b/zp+NNoaCWkllcY9dPx5tDsRIYBYVDHaP0Gdld4w6M1ZtNF6fgHHBl7zMKCge34JSTjK5RUDhBwQUjCxzVUzgCISknGcpKQREEhcQ1RjlHQREIBfew67q0iMOvOrwaAISmpJxf6wHIzkA0bd23pFTeqGcaaD4tJOit9NXPKFJBYeUcT9AgWBAMbd4dgEgLhbbnQG4i+f8uQKSHIgscOwExDRTepaXnHLsBMSUUkYDsCMT0UNzd4Ra7mF1hOPRMsyWVNH/c1/SA2R2Es5a/ACFtcn/xxMQMAAAAAElFTkSuQmCC'; +const CCW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIYAAAB9CAYAAABnLBtLAAAAAXNSR0IArs4c6QAABotJREFUeF7tnWuW5TQMhJmlwFZgPcNaYD3DVmApzAmHzEmnY+thvRxV/01iy6XPJSU3fe+Xnwz+/v71j3+pYX756/cv1DkVjnPWcsS5y3q0mi4liyviGVwVMaVxS8Wtsk5p3Nfz1WBoxY0WTRvniqj3a6PXbBG7CoxVsT2FWo3NQlRqDM/1U3Nzj6eA4VGjdwBiJycRg2GZgNWdYxkLdyd5nbeqhXVcpmD8/O3rh/j++e3PabxaMd4ERFUXEYExS8gdinPBlnBYAjGKd3XnUevljq/dNNzxqfPcwTgCoMSiRFgFwgsCSlzO2qkxKG2o67XHTcDgCK+BYwUITkxa0bTXURrMxo0GJAwMzu65Ll4DRUUYRsnWQBIJBxsMTX9xF4US41j424GQapLVnIaCwXENiU3v5BDUuqhNEw3IMhia5EhFuIuimZNKTJXjEm08S0sKGFrneDMQKyXGA5AtwOgExBWQTPdggWHReD5ZNbXwrkBo3cPSOZbAsEjcCA6Lsav0DRZxUJvonMMKjnQwnvoNQPGMUiQcJcA44QAQPG/hALLqHCQYXv0FTwKctfLkdAUOgLExe5RzpIAB269BlBccascAGDXA4Dws1DjHFAz0F3WST0Vi7RwAg1J8o+OWcKjAQBmpSQvAqJmXElFZwQHHKJFO2yBmcHAb0SEYaDxtkxU92iocYjA69BeUHT8luZou1Boo52gPBiWgdqdXAGXFNdqC4QXEHaRsQLRwPILx5v4iCogqgACMiednwVClF9HAIXKMbFvU1PtKUFzjj9QSYFyUXwGC6tjvgGr+SeoYozIcnxzjDf2FBgopDDP3koASBYfUNV4HhgQKSxieQOECkg3Hkw5sMKKC1/QRnHcSruN6A6EpNRH6SlzjNWBw4IgGQgpIJhx3bQCG1qKU11HlxRsOrmt8AKND45ntGgdPmXCYguFNsXLzDS+jGlDA8fyleVddWI6xGxg79BuUc3hqPto4LcAAHGMf5pSTH47xhv7iLgVKihyO0zVeDQZcwxEMz1pn3XSOxqvuHBl3KVSfQTrGG8DYwTmiSznVZ/wHRnRQUU4h6Tkq38J6bE6AIfgoPhuO6A06KydTx/AgNcstznmpnZIZH8DIVJ/40vuqruGxSeEYDyBSXXkWu5GuMQUjMpAssZ/mBRjjnws5XLNdj0H1GlVLyRG3dTlBKdmolMweH5QBw4PSCuWk8p3JNmCcibSmNQOQ6o/GT01GfZ91DshSQj39zEhixpzZ/UUVMI44yM9KMhKUMWcVKCqUkg9gdHcNgPFxO7LfEs/YxRFzVgKiZCm5J4F6RyAiad5zVISiQin58YBrloC3AVIVhmsOSt2VeO9OjM9XAGDwtWp1JsBolW7+YgEGX6s2Z0Z+2s168tlG+eILBRjFE5QVXhQY1AeJ5A/ZZAnUdd4K/cWnR+Jdk1Fp3QCjUjaKxBJVRo7lUq82opQUgSLyUTjAKJR0KpQKbnHE+Om/3anAcdxXgQpgDL84xXfpGH2kQCQUnDKCu5IirEaCQT2/OCVB85kMRyQUXLeAYxSG4gjN+q1wgJGccO70VdziejeCUsLNntN50VBI3AKlxCnp1LDU65KRJeTJLQAGlUGH4xlQzNwCYDgkWTrkLlDAMaSZXTifgsLrLkTjFgBjIdGSS3eDAmBIsqs4lwOEp1No3QJgKJLNvWRnKAAGN8vC86pDMboTuS4Tn5UIkz47nQuEd/lYKSHn+gCGARgSIHaAAqVkAQopDBFAUE7BKSFwDAUUGhjOaTwecz8tgfu+BbV8lJKBQisQXIeMAoJyC+nXPwAMxs9dUrtrdLwKFJISglLyEiAop9BA0bb5tCoTWSXjOq/Xd5e2KyXWUESWC89m8z52KzAsoMgGIcItWpUSKRSVAJA6hbavuM7TxjEoMKqDcCaN6iksoGjjGIBCfsPdwjEy3siWp2J+RZRTtHmOASh0iL7eMaK+oUYnP31VtFO0d4zqzSYHCKtG8wlPOAa9acPPyIaixV3JTqWEC4SnU6CUfPsa7gSjCSsBATCKgFERCpSSJM+QwHCGKH3RZnVpbZvPQ7joO5MdgGhTSo6FZjegOwEBMP5XwMs1NDBklY2WzzFmjnEVZBWQFRAqAdHKMbhwcPoOCwDuOzS6seQ0pq9vPq8iUB+/cwSzPKciEO0c41xwNhyVYbhC38oxMuHYBYi2jhEFx24g3EtkS8fw6Dt2BwFgTLpHTv/xNgBGcnwHa6xrfvicCcEAAAAASUVORK5CYII='; + +const vm = Scratch.vm; + +let cameraX = 0; +let cameraY = 0; +let cameraZoom = 100; +let cameraDirection = 90; +let cameraBG = '#ffffff'; + +vm.runtime.runtimeOptions.fencing = false; +vm.renderer.offscreenTouching = true; + +function updateCamera(x = cameraX, y = cameraY, scale = cameraZoom / 100, rot = -cameraDirection + 90) { + rot = rot / 180 * Math.PI; + let s = Math.sin(rot) * scale; + let c = Math.cos(rot) * scale; + let w = vm.runtime.stageWidth / 2; + let h = vm.runtime.stageHeight / 2; + vm.renderer._projection = [ + c / w, -s / h, 0, 0, + s / w, c / h, 0, 0, + 0, 0, -1, 0, + (c * -x + s * -y) / w, (c * -y - s * -x) / h, 0, 1 + ]; + vm.renderer.dirty = true; +} + +// tell resize to update camera as well +vm.runtime.on('STAGE_SIZE_CHANGED', _ => updateCamera()); + +// fix mouse positions +let oldSX = vm.runtime.ioDevices.mouse.getScratchX; +let oldSY = vm.runtime.ioDevices.mouse.getScratchY; + +vm.runtime.ioDevices.mouse.getScratchX = function (...a) { + return (oldSX.apply(this, a) + cameraX) / cameraZoom * 100; +}; +vm.runtime.ioDevices.mouse.getScratchY = function (...a) { + return (oldSY.apply(this, a) + cameraY) / cameraZoom * 100; +}; + +class Camera { + + getInfo() { + return { + + id: 'DTcameracontrols', + name: 'Camera', + + color1: '#ff4da7', + color2: '#de4391', + color3: '#c83c82', + + menuIconURI: icon, + + blocks: [ + { + opcode: 'moveSteps', + blockType: Scratch.BlockType.COMMAND, + text: 'move camera [val] steps', + arguments: { + val: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10 + }, + } + }, + { + opcode: 'rotateCW', + blockType: Scratch.BlockType.COMMAND, + text: 'turn camera [image] [val] degrees', + arguments: { + image: { + type: Scratch.ArgumentType.IMAGE, + dataURI: CW + }, + val: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 15 + } + } + }, + { + opcode: 'rotateCCW', + blockType: Scratch.BlockType.COMMAND, + text: 'turn camera [image] [val] degrees', + arguments: { + image: { + type: Scratch.ArgumentType.IMAGE, + dataURI: CCW + }, + val: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 15 + } + } + }, + '---', + { + opcode: 'goTo', + blockType: Scratch.BlockType.COMMAND, + text: 'move camera to [sprite]', + arguments: { + sprite: { + type: Scratch.ArgumentType.STRING, + menu: "sprites", + }, + } + }, + { + opcode: 'setBoth', + blockType: Scratch.BlockType.COMMAND, + text: 'set camera to x: [x] y: [y]', + arguments: { + x: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0 + }, + y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0 + }, + } + }, + '---', + { + opcode: 'setDirection', + blockType: Scratch.BlockType.COMMAND, + text: 'set camera direction to [val]', + arguments: { + val: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 90 + } + } + }, + { + opcode: 'pointTowards', + blockType: Scratch.BlockType.COMMAND, + text: 'point camera towards [sprite]', + arguments: { + sprite: { + type: Scratch.ArgumentType.STRING, + menu: "sprites", + }, + } + }, + '---', + { + opcode: 'changeX', + blockType: Scratch.BlockType.COMMAND, + text: 'change camera x by [val]', + arguments: { + val: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: 'setX', + blockType: Scratch.BlockType.COMMAND, + text: 'set camera x to [val]', + arguments: { + val: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'changeY', + blockType: Scratch.BlockType.COMMAND, + text: 'change camera y by [val]', + arguments: { + val: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: 'setY', + blockType: Scratch.BlockType.COMMAND, + text: 'set camera y to [val]', + arguments: { + val: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + '---', + { + opcode: 'getX', + blockType: Scratch.BlockType.REPORTER, + text: 'camera x', + }, + { + opcode: 'getY', + blockType: Scratch.BlockType.REPORTER, + text: 'camera y', + }, + { + opcode: 'getDirection', + blockType: Scratch.BlockType.REPORTER, + text: 'camera direction', + }, + '---', + { + opcode: 'changeZoom', + blockType: Scratch.BlockType.COMMAND, + text: 'change camera zoom by [val]', + arguments: { + val: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: 'setZoom', + blockType: Scratch.BlockType.COMMAND, + text: 'set camera zoom to [val] %', + arguments: { + val: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'getZoom', + blockType: Scratch.BlockType.REPORTER, + text: 'camera zoom', + }, + '---', + { + opcode: 'setCol', + blockType: Scratch.BlockType.COMMAND, + text: 'set background color to [val]', + arguments: { + val: { + type: Scratch.ArgumentType.COLOR + } + } + }, + { + opcode: 'getCol', + blockType: Scratch.BlockType.REPORTER, + text: 'background color', + }, + ], + menus: { + sprites: { + items: 'getSprites', + acceptReporters: true, + } + }, + }; + } + + getSprites() { + let sprites = []; + Scratch.vm.runtime.targets.forEach(e => { + if (e.isOriginal && !e.isStage) sprites.push(e.sprite.name); + }); + if (sprites.length === 0) { + sprites.push('no sprites exist'); + } + return sprites; + } + + setBoth(args, util) { + cameraX = +args.x; + cameraY = +args.y; + updateCamera(); + vm.runtime.requestRedraw(); + } + changeZoom(args, util) { + cameraZoom += +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + setZoom(args, util) { + cameraZoom = +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + changeX(args, util) { + cameraX += +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + setX(args, util) { + cameraX = +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + changeY(args, util) { + cameraY += +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + setY(args, util) { + cameraY = +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + setDirection(args, util) { + cameraDirection = +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + rotateCW(args, util) { + cameraDirection = cameraDirection + +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + rotateCCW(args, util) { + cameraDirection = cameraDirection - +args.val; + updateCamera(); + vm.runtime.requestRedraw(); + } + getX() { + return cameraX; + } + getY() { + return cameraY; + } + getZoom() { + return cameraZoom; + } + getDirection() { + return cameraDirection; + } + setCol(args, util) { + cameraBG = Scratch.Cast.toString(args.val); + const rgb = Scratch.Cast.toRgbColorList(args.val); + Scratch.vm.renderer.setBackgroundColor(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255); + updateCamera(); + vm.runtime.requestRedraw(); + } + getCol() { + return cameraBG; + } + moveSteps(args) { + let dir = (-cameraDirection + 90) * Math.PI / 180; + cameraX += args.val * Math.cos(dir); + cameraY += args.val * Math.sin(dir); + updateCamera(); + vm.runtime.requestRedraw(); + } + goTo(args, util) { + const target = Scratch.Cast.toString(args.sprite); + const sprite = vm.runtime.getSpriteTargetByName(target); + if (!sprite) return; + cameraX = Math.round(sprite.x); + cameraY = Math.round(sprite.y); + updateCamera(); + vm.runtime.requestRedraw(); + } + pointTowards(args, util) { + const target = Scratch.Cast.toString(args.sprite); + const sprite = vm.runtime.getSpriteTargetByName(target); + if (!sprite) return; + let targetX = sprite.x; + let targetY = sprite.y; + const dx = targetX - cameraX; + const dy = targetY - cameraY; + cameraDirection = 90 - this.radToDeg(Math.atan2(dy, dx)); + updateCamera(); + vm.runtime.requestRedraw(); + } + radToDeg(rad) { + return rad * 180 / Math.PI; + } +} + +module.exports = Camera; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/fr_3d/cannon.min.js b/local-scratch-vm/src/extensions/fr_3d/cannon.min.js new file mode 100644 index 0000000000000000000000000000000000000000..01a53bc6d0e69aa76d5609aef38d58f6741849df --- /dev/null +++ b/local-scratch-vm/src/extensions/fr_3d/cannon.min.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2015 cannon.js Authors + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&false)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.CANNON=e()}}(function(){return function e(f,n,o){function d(t,l){if(!n[t]){if(!f[t]){var u="function"==typeof require&&require;if(!l&&u)return u(t,!0);if(i)return i(t,!0);throw new Error("Cannot find module '"+t+"'")}var p=n[t]={exports:{}};f[t][0].call(p.exports,function(e){var n=f[t][1][e];return d(n?n:e)},p,p.exports,e,f,n,o)}return n[t].exports}for(var i="function"==typeof require&&require,t=0;t (http://steffe.se)",keywords:["cannon.js","cannon","physics","engine","3d"],main:"./build/cannon.js",engines:{node:"*"},repository:{type:"git",url:"https://github.com/schteppe/cannon.js.git"},bugs:{url:"https://github.com/schteppe/cannon.js/issues"},licenses:[{type:"MIT"}],devDependencies:{jshint:"latest","uglify-js":"latest",nodeunit:"^0.9.0",grunt:"~0.4.0","grunt-contrib-jshint":"~0.1.1","grunt-contrib-nodeunit":"^0.4.1","grunt-contrib-concat":"~0.1.3","grunt-contrib-uglify":"^0.5.1","grunt-browserify":"^2.1.4","grunt-contrib-yuidoc":"^0.5.2",browserify:"*"},dependencies:{}}},{}],2:[function(e,f){f.exports={version:e("../package.json").version,AABB:e("./collision/AABB"),ArrayCollisionMatrix:e("./collision/ArrayCollisionMatrix"),Body:e("./objects/Body"),Box:e("./shapes/Box"),Broadphase:e("./collision/Broadphase"),Constraint:e("./constraints/Constraint"),ContactEquation:e("./equations/ContactEquation"),Narrowphase:e("./world/Narrowphase"),ConeTwistConstraint:e("./constraints/ConeTwistConstraint"),ContactMaterial:e("./material/ContactMaterial"),ConvexPolyhedron:e("./shapes/ConvexPolyhedron"),Cylinder:e("./shapes/Cylinder"),DistanceConstraint:e("./constraints/DistanceConstraint"),Equation:e("./equations/Equation"),EventTarget:e("./utils/EventTarget"),FrictionEquation:e("./equations/FrictionEquation"),GSSolver:e("./solver/GSSolver"),GridBroadphase:e("./collision/GridBroadphase"),Heightfield:e("./shapes/Heightfield"),HingeConstraint:e("./constraints/HingeConstraint"),LockConstraint:e("./constraints/LockConstraint"),Mat3:e("./math/Mat3"),Material:e("./material/Material"),NaiveBroadphase:e("./collision/NaiveBroadphase"),ObjectCollisionMatrix:e("./collision/ObjectCollisionMatrix"),Pool:e("./utils/Pool"),Particle:e("./shapes/Particle"),Plane:e("./shapes/Plane"),PointToPointConstraint:e("./constraints/PointToPointConstraint"),Quaternion:e("./math/Quaternion"),Ray:e("./collision/Ray"),RaycastVehicle:e("./objects/RaycastVehicle"),RaycastResult:e("./collision/RaycastResult"),RigidVehicle:e("./objects/RigidVehicle"),RotationalEquation:e("./equations/RotationalEquation"),RotationalMotorEquation:e("./equations/RotationalMotorEquation"),SAPBroadphase:e("./collision/SAPBroadphase"),SPHSystem:e("./objects/SPHSystem"),Shape:e("./shapes/Shape"),Solver:e("./solver/Solver"),Sphere:e("./shapes/Sphere"),SplitSolver:e("./solver/SplitSolver"),Spring:e("./objects/Spring"),Trimesh:e("./shapes/Trimesh"),Vec3:e("./math/Vec3"),Vec3Pool:e("./utils/Vec3Pool"),World:e("./world/World")}},{"../package.json":1,"./collision/AABB":3,"./collision/ArrayCollisionMatrix":4,"./collision/Broadphase":5,"./collision/GridBroadphase":6,"./collision/NaiveBroadphase":7,"./collision/ObjectCollisionMatrix":8,"./collision/Ray":9,"./collision/RaycastResult":10,"./collision/SAPBroadphase":11,"./constraints/ConeTwistConstraint":12,"./constraints/Constraint":13,"./constraints/DistanceConstraint":14,"./constraints/HingeConstraint":15,"./constraints/LockConstraint":16,"./constraints/PointToPointConstraint":17,"./equations/ContactEquation":19,"./equations/Equation":20,"./equations/FrictionEquation":21,"./equations/RotationalEquation":22,"./equations/RotationalMotorEquation":23,"./material/ContactMaterial":24,"./material/Material":25,"./math/Mat3":27,"./math/Quaternion":28,"./math/Vec3":30,"./objects/Body":31,"./objects/RaycastVehicle":32,"./objects/RigidVehicle":33,"./objects/SPHSystem":34,"./objects/Spring":35,"./shapes/Box":37,"./shapes/ConvexPolyhedron":38,"./shapes/Cylinder":39,"./shapes/Heightfield":40,"./shapes/Particle":41,"./shapes/Plane":42,"./shapes/Shape":43,"./shapes/Sphere":44,"./shapes/Trimesh":45,"./solver/GSSolver":46,"./solver/Solver":47,"./solver/SplitSolver":48,"./utils/EventTarget":49,"./utils/Pool":51,"./utils/Vec3Pool":54,"./world/Narrowphase":55,"./world/World":56}],3:[function(e,f){function n(e){e=e||{},this.lowerBound=new o,e.lowerBound&&this.lowerBound.copy(e.lowerBound),this.upperBound=new o,e.upperBound&&this.upperBound.copy(e.upperBound)}{var o=e("../math/Vec3");e("../utils/Utils")}f.exports=n;var d=new o;n.prototype.setFromPoints=function(e,f,n,o){var i=this.lowerBound,t=this.upperBound,l=n;i.copy(e[0]),l&&l.vmult(i,i),t.copy(i);for(var u=1;ut.x&&(t.x=p.x),p.xt.y&&(t.y=p.y),p.yt.z&&(t.z=p.z),p.zf&&(this.lowerBound.x=f);var n=e.upperBound.x;this.upperBound.xf&&(this.lowerBound.y=f);var n=e.upperBound.y;this.upperBound.yf&&(this.lowerBound.z=f);var n=e.upperBound.z;this.upperBound.z=d.x&&f.y<=o.y&&n.y>=d.y&&f.z<=o.z&&n.z>=d.z},n.prototype.getCorners=function(e,f,n,o,d,i,t,l){var u=this.lowerBound,p=this.upperBound;e.copy(u),f.set(p.x,u.y,u.z),n.set(p.x,p.y,u.z),o.set(u.x,p.y,p.z),d.set(p.x,u.y,u.z),i.set(u.x,p.y,u.z),t.set(u.x,u.y,p.z),l.copy(p)};var i=[new o,new o,new o,new o,new o,new o,new o,new o];n.prototype.toLocalFrame=function(e,f){var n=i,o=n[0],d=n[1],t=n[2],l=n[3],u=n[4],p=n[5],s=n[6],y=n[7];this.getCorners(o,d,t,l,u,p,s,y);for(var c=0;8!==c;c++){var a=n[c];e.pointToLocal(a,a)}return f.setFromPoints(n)},n.prototype.toWorldFrame=function(e,f){var n=i,o=n[0],d=n[1],t=n[2],l=n[3],u=n[4],p=n[5],s=n[6],y=n[7];this.getCorners(o,d,t,l,u,p,s,y);for(var c=0;8!==c;c++){var a=n[c];e.pointToWorld(a,a)}return f.setFromPoints(n)}},{"../math/Vec3":30,"../utils/Utils":53}],4:[function(e,f){function n(){this.matrix=[]}f.exports=n,n.prototype.get=function(e,f){if(e=e.index,f=f.index,f>e){var n=f;f=e,e=n}return this.matrix[(e*(e+1)>>1)+f-1]},n.prototype.set=function(e,f,n){if(e=e.index,f=f.index,f>e){var o=f;f=e,e=o}this.matrix[(e*(e+1)>>1)+f-1]=n?1:0},n.prototype.reset=function(){for(var e=0,f=this.matrix.length;e!==f;e++)this.matrix[e]=0},n.prototype.setNumObjects=function(e){this.matrix.length=e*(e-1)>>1}},{}],5:[function(e,f){function n(){this.world=null,this.useBoundingBoxes=!1,this.dirty=!0}{var o=e("../objects/Body"),d=e("../math/Vec3"),i=e("../math/Quaternion");e("../shapes/Shape"),e("../shapes/Plane")}f.exports=n,n.prototype.collisionPairs=function(){throw new Error("collisionPairs not implemented for this BroadPhase class!")};var t=o.STATIC|o.KINEMATIC;n.prototype.needBroadphaseCollision=function(e,f){return 0===(e.collisionFilterGroup&f.collisionFilterMask)||0===(f.collisionFilterGroup&e.collisionFilterMask)?!1:0===(e.type&t)&&e.sleepState!==o.SLEEPING||0===(f.type&t)&&f.sleepState!==o.SLEEPING?!0:!1},n.prototype.intersectionTest=function(e,f,n,o){this.useBoundingBoxes?this.doBoundingBoxBroadphase(e,f,n,o):this.doBoundingSphereBroadphase(e,f,n,o)};{var l=new d;new d,new i,new d}n.prototype.doBoundingSphereBroadphase=function(e,f,n,o){var d=l;f.position.vsub(e.position,d);var i=Math.pow(e.boundingRadius+f.boundingRadius,2),t=d.norm2();i>t&&(n.push(e),o.push(f))},n.prototype.doBoundingBoxBroadphase=function(e,f,n,o){e.aabbNeedsUpdate&&e.computeAABB(),f.aabbNeedsUpdate&&f.computeAABB(),e.aabb.overlaps(f.aabb)&&(n.push(e),o.push(f))};var u={keys:[]},p=[],s=[];n.prototype.makePairsUnique=function(e,f){for(var n=u,o=p,d=s,i=e.length,t=0;t!==i;t++)o[t]=e[t],d[t]=f[t];e.length=0,f.length=0;for(var t=0;t!==i;t++){var l=o[t].id,y=d[t].id,c=y>l?l+","+y:y+","+l;n[c]=t,n.keys.push(c)}for(var t=0;t!==n.keys.length;t++){var c=n.keys.pop(),a=n[c];e.push(o[a]),f.push(d[a]),delete n[c]}},n.prototype.setWorld=function(){};var y=new d;n.boundingSphereCheck=function(e,f){var n=y;return e.position.vsub(f.position,n),Math.pow(e.shape.boundingSphereRadius+f.shape.boundingSphereRadius,2)>n.norm2()},n.prototype.aabbQuery=function(){return console.warn(".aabbQuery is not implemented in this Broadphase subclass."),[]}},{"../math/Quaternion":28,"../math/Vec3":30,"../objects/Body":31,"../shapes/Plane":42,"../shapes/Shape":43}],6:[function(e,f){function n(e,f,n,i,t){o.apply(this),this.nx=n||10,this.ny=i||10,this.nz=t||10,this.aabbMin=e||new d(100,100,100),this.aabbMax=f||new d(-100,-100,-100);var l=this.nx*this.ny*this.nz;if(0>=l)throw"GridBroadphase: Each dimension's n must be >0";this.bins=[],this.binLengths=[],this.bins.length=l,this.binLengths.length=l;for(var u=0;l>u;u++)this.bins[u]=[],this.binLengths[u]=0}f.exports=n;var o=e("./Broadphase"),d=e("../math/Vec3"),i=e("../shapes/Shape");n.prototype=new o,n.prototype.constructor=n;{var t=new d;new d}n.prototype.collisionPairs=function(e,f,n){function o(e,f,n,o,d,i,t){var l=(e-g)*v|0,u=(f-x)*A|0,p=(n-j)*C|0,b=I((o-g)*v),m=I((d-x)*A),N=I((i-j)*C);0>l?l=0:l>=s&&(l=s-1),0>u?u=0:u>=y&&(u=y-1),0>p?p=0:p>=c&&(p=c-1),0>b?b=0:b>=s&&(b=s-1),0>m?m=0:m>=y&&(m=y-1),0>N?N=0:N>=c&&(N=c-1),l*=a,u*=r,p*=w,b*=a,m*=r,N*=w;for(var O=l;b>=O;O+=a)for(var h=u;m>=h;h+=r)for(var k=p;N>=k;k+=w){var q=O+h+k;E[q][F[q]++]=t}}for(var d=e.numObjects(),l=e.bodies,u=this.aabbMax,p=this.aabbMin,s=this.nx,y=this.ny,c=this.nz,a=y*c,r=c,w=1,b=u.x,m=u.y,N=u.z,g=p.x,x=p.y,j=p.z,v=s/(b-g),A=y/(m-x),C=c/(N-j),O=(b-g)/s,h=(m-x)/y,k=(N-j)/c,q=.5*Math.sqrt(O*O+h*h+k*k),z=i.types,B=z.SPHERE,D=z.PLANE,E=(z.BOX,z.COMPOUND,z.CONVEXPOLYHEDRON,this.bins),F=this.binLengths,G=this.bins.length,H=0;H!==G;H++)F[H]=0;for(var I=Math.ceil,p=Math.min,u=Math.max,H=0;H!==d;H++){var J=l[H],K=J.shape;switch(K.type){case B:var L=J.position.x,M=J.position.y,P=J.position.z,Q=K.radius;o(L-Q,M-Q,P-Q,L+Q,M+Q,P+Q,J);break;case D:K.worldNormalNeedsUpdate&&K.computeWorldNormal(J.quaternion);var R=K.worldNormal,S=g+.5*O-J.position.x,T=x+.5*h-J.position.y,U=j+.5*k-J.position.z,V=t;V.set(S,T,U);for(var W=0,X=0;W!==s;W++,X+=a,V.y=T,V.x+=O)for(var Y=0,Z=0;Y!==y;Y++,Z+=r,V.z=U,V.y+=h)for(var $=0,_=0;$!==c;$++,_+=w,V.z+=k)if(V.dot(R)1)for(var nf=E[H],W=0;W!==ff;W++)for(var J=nf[W],Y=0;Y!==W;Y++){var of=nf[Y];this.needBroadphaseCollision(J,of)&&this.intersectionTest(J,of,f,n)}}this.makePairsUnique(f,n)}},{"../math/Vec3":30,"../shapes/Shape":43,"./Broadphase":5}],7:[function(e,f){function n(){o.apply(this)}f.exports=n;var o=e("./Broadphase"),d=e("./AABB");n.prototype=new o,n.prototype.constructor=n,n.prototype.collisionPairs=function(e,f,n){var o,d,i,t,l=e.bodies,u=l.length;for(o=0;o!==u;o++)for(d=0;d!==o;d++)i=l[o],t=l[d],this.needBroadphaseCollision(i,t)&&this.intersectionTest(i,t,f,n)};new d;n.prototype.aabbQuery=function(e,f,n){n=n||[];for(var o=0;oe){var n=f;f=e,e=n}return e+"-"+f in this.matrix},n.prototype.set=function(e,f,n){if(e=e.id,f=f.id,f>e){var o=f;f=e,e=o}n?this.matrix[e+"-"+f]=!0:delete this.matrix[e+"-"+f]},n.prototype.reset=function(){this.matrix={}},n.prototype.setNumObjects=function(){}},{}],9:[function(e,f){function n(e,f){this.from=e?e.clone():new i,this.to=f?f.clone():new i,this._direction=new i,this.precision=1e-4,this.checkCollisionResponse=!0,this.skipBackfaces=!1,this.collisionFilterMask=-1,this.collisionFilterGroup=-1,this.mode=n.ANY,this.result=new u,this.hasHit=!1,this.callback=function(){}}function o(e,f,n,o){o.vsub(f,G),n.vsub(f,a),e.vsub(f,r);var d,i,t=G.dot(G),l=G.dot(a),u=G.dot(r),p=a.dot(a),s=a.dot(r);return(d=p*u-l*s)>=0&&(i=t*s-l*u)>=0&&t*p-l*l>d+i}function d(e,f,n){n.vsub(e,G);var o=G.dot(f);f.mult(o,H),H.vadd(e,H);var d=n.distanceTo(H);return d}f.exports=n;var i=e("../math/Vec3"),t=e("../math/Quaternion"),l=e("../math/Transform"),u=(e("../shapes/ConvexPolyhedron"),e("../shapes/Box"),e("../collision/RaycastResult")),p=e("../shapes/Shape"),s=e("../collision/AABB");n.prototype.constructor=n,n.CLOSEST=1,n.ANY=2,n.ALL=4;var y=new s,c=[];n.prototype.intersectWorld=function(e,f){return this.mode=f.mode||n.ANY,this.result=f.result||new u,this.skipBackfaces=!!f.skipBackfaces,this.collisionFilterMask="undefined"!=typeof f.collisionFilterMask?f.collisionFilterMask:-1,this.collisionFilterGroup="undefined"!=typeof f.collisionFilterGroup?f.collisionFilterGroup:-1,f.from&&this.from.copy(f.from),f.to&&this.to.copy(f.to),this.callback=f.callback||function(){},this.hasHit=!1,this.result.reset(),this._updateDirection(),this.getAABB(y),c.length=0,e.broadphase.aabbQuery(e,y,c),this.intersectBodies(c),this.hasHit};var a=new i,r=new i;n.pointInTriangle=o;var w=new i,b=new t;n.prototype.intersectBody=function(e,f){f&&(this.result=f,this._updateDirection());var n=this.checkCollisionResponse;if((!n||e.collisionResponse)&&0!==(this.collisionFilterGroup&e.collisionFilterMask)&&0!==(e.collisionFilterGroup&this.collisionFilterMask))for(var o=w,d=b,i=0,t=e.shapes.length;t>i;i++){var l=e.shapes[i];if((!n||l.collisionResponse)&&(e.quaternion.mult(e.shapeOrientations[i],d),e.quaternion.vmult(e.shapeOffsets[i],o),o.vadd(e.position,o),this.intersectShape(l,d,o,e),this.result._shouldStop))break}},n.prototype.intersectBodies=function(e,f){f&&(this.result=f,this._updateDirection());for(var n=0,o=e.length;!this.result._shouldStop&&o>n;n++)this.intersectBody(e[n])},n.prototype._updateDirection=function(){this.to.vsub(this.from,this._direction),this._direction.normalize()},n.prototype.intersectShape=function(e,f,n,o){var i=this.from,t=d(i,this._direction,n);if(!(t>e.boundingSphereRadius)){var l=this[e.type];l&&l.call(this,e,f,n,o)}};{var m=(new i,new i,new i),N=new i,g=new i,x=new i;new i,new u}n.prototype.intersectBox=function(e,f,n,o){return this.intersectConvex(e.convexPolyhedronRepresentation,f,n,o)},n.prototype[p.types.BOX]=n.prototype.intersectBox,n.prototype.intersectPlane=function(e,f,n,o){var d=this.from,t=this.to,l=this._direction,u=new i(0,0,1);f.vmult(u,u);var p=new i;d.vsub(n,p);var s=p.dot(u);t.vsub(n,p);var y=p.dot(u);if(!(s*y>0||d.distanceTo(t)c)&&(c=p[0]),(null===y||p[1]a)&&(a=p[1])),null!==s){var w=[];e.getRectMinMax(s,y,c,a,w);for(var b=(w[0],w[1],s);c>=b;b++)for(var m=y;a>=m;m++){if(this.result._shouldStop)return;if(e.getConvexTrianglePillar(b,m,!1),l.pointToWorldFrame(o,f,e.pillarOffset,t),this.intersectConvex(e.pillarConvex,f,t,d,j),this.result._shouldStop)return;e.getConvexTrianglePillar(b,m,!0),l.pointToWorldFrame(o,f,e.pillarOffset,t),this.intersectConvex(e.pillarConvex,f,t,d,j)}}},n.prototype[p.types.HEIGHTFIELD]=n.prototype.intersectHeightfield;var v=new i,A=new i;n.prototype.intersectSphere=function(e,f,n,o){var d=this.from,i=this.to,t=e.radius,l=Math.pow(i.x-d.x,2)+Math.pow(i.y-d.y,2)+Math.pow(i.z-d.z,2),u=2*((i.x-d.x)*(d.x-n.x)+(i.y-d.y)*(d.y-n.y)+(i.z-d.z)*(d.z-n.z)),p=Math.pow(d.x-n.x,2)+Math.pow(d.y-n.y,2)+Math.pow(d.z-n.z,2)-Math.pow(t,2),s=Math.pow(u,2)-4*l*p,y=v,c=A;if(!(0>s))if(0===s)d.lerp(i,s,y),y.vsub(n,c),c.normalize(),this.reportIntersection(c,y,e,o,-1);else{var a=(-u-Math.sqrt(s))/(2*l),r=(-u+Math.sqrt(s))/(2*l);if(a>=0&&1>=a&&(d.lerp(i,a,y),y.vsub(n,c),c.normalize(),this.reportIntersection(c,y,e,o,-1)),this.result._shouldStop)return;r>=0&&1>=r&&(d.lerp(i,r,y),y.vsub(n,c),c.normalize(),this.reportIntersection(c,y,e,o,-1))}},n.prototype[p.types.SPHERE]=n.prototype.intersectSphere;var C=new i,O=(new i,new i,new i);n.prototype.intersectConvex=function(e,f,n,d,i){for(var t=C,l=O,u=i&&i.faceList||null,p=e.faces,s=e.vertices,y=e.faceNormals,c=this._direction,a=this.from,r=this.to,w=a.distanceTo(r),b=u?u.length:p.length,j=this.result,v=0;!j._shouldStop&&b>v;v++){var A=u?u[v]:v,h=p[A],k=y[A],q=f,z=n;l.copy(s[h[0]]),q.vmult(l,l),l.vadd(z,l),l.vsub(a,l),q.vmult(k,t);var B=c.dot(t);if(!(Math.abs(B)D)){c.mult(D,m),m.vadd(a,m),N.copy(s[h[0]]),q.vmult(N,N),z.vadd(N,N);for(var E=1;!j._shouldStop&&Ew||this.reportIntersection(t,m,e,d,A)}}}}},n.prototype[p.types.CONVEXPOLYHEDRON]=n.prototype.intersectConvex;var h=new i,k=new i,q=new i,z=new i,B=new i,D=new i,E=(new s,[]),F=new l;n.prototype.intersectTrimesh=function(e,f,n,d,i){var t=h,u=E,p=F,s=O,y=k,c=q,a=z,r=D,w=B,b=(i&&i.faceList||null,e.indices),j=(e.vertices,e.faceNormals,this.from),v=this.to,A=this._direction;p.position.copy(n),p.quaternion.copy(f),l.vectorToLocalFrame(n,f,A,y),l.pointToLocalFrame(n,f,j,c),l.pointToLocalFrame(n,f,v,a);var C=c.distanceSquared(a);e.tree.rayQuery(this,p,u);for(var G=0,H=u.length;!this.result._shouldStop&&G!==H;G++){var I=u[G];e.getNormal(I,t),e.getVertex(b[3*I],N),N.vsub(c,s);var J=y.dot(t),K=t.dot(s)/J;if(!(0>K)){y.scale(K,m),m.vadd(c,m),e.getVertex(b[3*I+1],g),e.getVertex(b[3*I+2],x);var L=m.distanceSquared(c);!o(m,g,N,x)&&!o(m,N,g,x)||L>C||(l.vectorToWorldFrame(f,t,w),l.pointToWorldFrame(n,f,m,r),this.reportIntersection(w,r,e,d,I))}}u.length=0},n.prototype[p.types.TRIMESH]=n.prototype.intersectTrimesh,n.prototype.reportIntersection=function(e,f,o,d,i){var t=this.from,l=this.to,u=t.distanceTo(f),p=this.result;if(!(this.skipBackfaces&&e.dot(this._direction)>0))switch(p.hitFaceIndex="undefined"!=typeof i?i:-1,this.mode){case n.ALL:this.hasHit=!0,p.set(t,l,e,f,o,d,u),p.hasHit=!0,this.callback(p);break;case n.CLOSEST:(uf;f++){for(var o=e[f],d=f-1;d>=0&&!(e[d].aabb.lowerBound.x<=o.aabb.lowerBound.x);d--)e[d+1]=e[d];e[d+1]=o}return e},n.insertionSortY=function(e){for(var f=1,n=e.length;n>f;f++){for(var o=e[f],d=f-1;d>=0&&!(e[d].aabb.lowerBound.y<=o.aabb.lowerBound.y);d--)e[d+1]=e[d];e[d+1]=o}return e},n.insertionSortZ=function(e){for(var f=1,n=e.length;n>f;f++){for(var o=e[f],d=f-1;d>=0&&!(e[d].aabb.lowerBound.z<=o.aabb.lowerBound.z);d--)e[d+1]=e[d];e[d+1]=o}return e},n.prototype.collisionPairs=function(e,f,o){var d,i,t=this.axisList,l=t.length,u=this.axisIndex;for(this.dirty&&(this.sortList(),this.dirty=!1),d=0;d!==l;d++){var p=t[d];for(i=d+1;l>i;i++){var s=t[i];if(this.needBroadphaseCollision(p,s)){if(!n.checkBounds(p,s,u))break;this.intersectionTest(p,s,f,o)}}}},n.prototype.sortList=function(){for(var e=this.axisList,f=this.axisIndex,o=e.length,d=0;d!==o;d++){var i=e[d];i.aabbNeedsUpdate&&i.computeAABB()}0===f?n.insertionSortX(e):1===f?n.insertionSortY(e):2===f&&n.insertionSortZ(e)},n.checkBounds=function(e,f,n){var o,d;0===n?(o=e.position.x,d=f.position.x):1===n?(o=e.position.y,d=f.position.y):2===n&&(o=e.position.z,d=f.position.z);var i=e.boundingRadius,t=f.boundingRadius,l=o+i,u=d-t;return l>u},n.prototype.autoDetectAxis=function(){for(var e=0,f=0,n=0,o=0,d=0,i=0,t=this.axisList,l=t.length,u=1/l,p=0;p!==l;p++){var s=t[p],y=s.position.x;e+=y,f+=y*y;var c=s.position.y;n+=c,o+=c*c;var a=s.position.z;d+=a,i+=a*a}var r=f-e*e*u,w=o-n*n*u,b=i-d*d*u;this.axisIndex=r>w?r>b?0:2:w>b?1:2},n.prototype.aabbQuery=function(e,f,n){n=n||[],this.dirty&&(this.sortList(),this.dirty=!1);var o=this.axisIndex,d="x";1===o&&(d="y"),2===o&&(d="z");for(var i=this.axisList,t=(f.lowerBound[d],f.upperBound[d],0);td;d++)for(var i=0;3>i;i++){for(var t=0,l=0;3>l;l++)t+=e.elements[d+3*l]*this.elements[l+3*i];o.elements[d+3*i]=t}return o},n.prototype.scale=function(e,f){f=f||new n;for(var o=this.elements,d=f.elements,i=0;3!==i;i++)d[3*i+0]=e.x*o[3*i+0],d[3*i+1]=e.y*o[3*i+1],d[3*i+2]=e.z*o[3*i+2];return f},n.prototype.solve=function(e,f){f=f||new o;for(var n=3,d=4,i=[],t=0;n*d>t;t++)i.push(0);var t,l;for(t=0;3>t;t++)for(l=0;3>l;l++)i[t+d*l]=this.elements[t+3*l];i[3]=e.x,i[7]=e.y,i[11]=e.z;var u,p,s=3,y=s,c=4;do{if(t=y-s,0===i[t+d*t])for(l=t+1;y>l;l++)if(0!==i[t+d*l]){u=c;do p=c-u,i[p+d*t]+=i[p+d*l];while(--u);break}if(0!==i[t+d*t])for(l=t+1;y>l;l++){var a=i[t+d*l]/i[t+d*t];u=c;do p=c-u,i[p+d*l]=t>=p?0:i[p+d*l]-i[p+d*t]*a;while(--u)}}while(--s);if(f.z=i[2*d+3]/i[2*d+2],f.y=(i[1*d+3]-i[1*d+2]*f.z)/i[1*d+1],f.x=(i[0*d+3]-i[0*d+2]*f.z-i[0*d+1]*f.y)/i[0*d+0],isNaN(f.x)||isNaN(f.y)||isNaN(f.z)||1/0===f.x||1/0===f.y||1/0===f.z)throw"Could not solve equation! Got x=["+f.toString()+"], b=["+e.toString()+"], A=["+this.toString()+"]";return f},n.prototype.e=function(e,f,n){return void 0===n?this.elements[f+3*e]:void(this.elements[f+3*e]=n)},n.prototype.copy=function(e){for(var f=0;fn;n++)e+=this.elements[n]+f;return e},n.prototype.reverse=function(e){e=e||new n;for(var f=3,o=6,d=[],i=0;f*o>i;i++)d.push(0);var i,t;for(i=0;3>i;i++)for(t=0;3>t;t++)d[i+o*t]=this.elements[i+3*t];d[3]=1,d[9]=0,d[15]=0,d[4]=0,d[10]=1,d[16]=0,d[5]=0,d[11]=0,d[17]=1;var l,u,p=3,s=p,y=o;do{if(i=s-p,0===d[i+o*i])for(t=i+1;s>t;t++)if(0!==d[i+o*t]){l=y;do u=y-l,d[u+o*i]+=d[u+o*t];while(--l);break}if(0!==d[i+o*i])for(t=i+1;s>t;t++){var c=d[i+o*t]/d[i+o*i];l=y;do u=y-l,d[u+o*t]=i>=u?0:d[u+o*t]-d[u+o*i]*c;while(--l)}}while(--p);i=2;do{t=i-1;do{var c=d[i+o*t]/d[i+o*i];l=o;do u=o-l,d[u+o*t]=d[u+o*t]-d[u+o*i]*c;while(--l)}while(t--)}while(--i);i=2;do{var c=1/d[i+o*i];l=o;do u=o-l,d[u+o*i]=d[u+o*i]*c;while(--l)}while(i--);i=2;do{t=2;do{if(u=d[f+t+o*i],isNaN(u)||1/0===u)throw"Could not reverse! A=["+this.toString()+"]";e.e(i,t,u)}while(t--)}while(i--);return e},n.prototype.setRotationFromQuaternion=function(e){var f=e.x,n=e.y,o=e.z,d=e.w,i=f+f,t=n+n,l=o+o,u=f*i,p=f*t,s=f*l,y=n*t,c=n*l,a=o*l,r=d*i,w=d*t,b=d*l,m=this.elements;return m[0]=1-(y+a),m[1]=p-b,m[2]=s+w,m[3]=p+b,m[4]=1-(u+a),m[5]=c-r,m[6]=s-w,m[7]=c+r,m[8]=1-(u+y),this},n.prototype.transpose=function(e){e=e||new n;for(var f=e.elements,o=this.elements,d=0;3!==d;d++)for(var i=0;3!==i;i++)f[3*d+i]=o[3*i+d];return e}},{"./Vec3":30}],28:[function(e,f){function n(e,f,n,o){this.x=void 0!==e?e:0,this.y=void 0!==f?f:0,this.z=void 0!==n?n:0,this.w=void 0!==o?o:1}f.exports=n;var o=e("./Vec3");n.prototype.set=function(e,f,n,o){this.x=e,this.y=f,this.z=n,this.w=o},n.prototype.toString=function(){return this.x+","+this.y+","+this.z+","+this.w},n.prototype.toArray=function(){return[this.x,this.y,this.z,this.w]},n.prototype.setFromAxisAngle=function(e,f){var n=Math.sin(.5*f);this.x=e.x*n,this.y=e.y*n,this.z=e.z*n,this.w=Math.cos(.5*f)},n.prototype.toAxisAngle=function(e){e=e||new o,this.normalize();var f=2*Math.acos(this.w),n=Math.sqrt(1-this.w*this.w);return.001>n?(e.x=this.x,e.y=this.y,e.z=this.z):(e.x=this.x/n,e.y=this.y/n,e.z=this.z/n),[e,f]};var d=new o,i=new o;n.prototype.setFromVectors=function(e,f){if(e.isAntiparallelTo(f)){var n=d,o=i;e.tangents(n,o),this.setFromAxisAngle(n,Math.PI)}else{var t=e.cross(f);this.x=t.x,this.y=t.y,this.z=t.z,this.w=Math.sqrt(Math.pow(e.norm(),2)*Math.pow(f.norm(),2))+e.dot(f),this.normalize()}};var t=new o,l=new o,u=new o;n.prototype.mult=function(e,f){f=f||new n;var o=this.w,d=t,i=l,p=u;return d.set(this.x,this.y,this.z),i.set(e.x,e.y,e.z),f.w=o*e.w-d.dot(i),d.cross(i,p),f.x=o*i.x+e.w*d.x+p.x,f.y=o*i.y+e.w*d.y+p.y,f.z=o*i.z+e.w*d.z+p.z,f},n.prototype.inverse=function(e){var f=this.x,o=this.y,d=this.z,i=this.w;e=e||new n,this.conjugate(e);var t=1/(f*f+o*o+d*d+i*i);return e.x*=t,e.y*=t,e.z*=t,e.w*=t,e},n.prototype.conjugate=function(e){return e=e||new n,e.x=-this.x,e.y=-this.y,e.z=-this.z,e.w=this.w,e},n.prototype.normalize=function(){var e=Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w);0===e?(this.x=0,this.y=0,this.z=0,this.w=0):(e=1/e,this.x*=e,this.y*=e,this.z*=e,this.w*=e)},n.prototype.normalizeFast=function(){var e=(3-(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w))/2;0===e?(this.x=0,this.y=0,this.z=0,this.w=0):(this.x*=e,this.y*=e,this.z*=e,this.w*=e)},n.prototype.vmult=function(e,f){f=f||new o;var n=e.x,d=e.y,i=e.z,t=this.x,l=this.y,u=this.z,p=this.w,s=p*n+l*i-u*d,y=p*d+u*n-t*i,c=p*i+t*d-l*n,a=-t*n-l*d-u*i;return f.x=s*p+a*-t+y*-u-c*-l,f.y=y*p+a*-l+c*-t-s*-u,f.z=c*p+a*-u+s*-l-y*-t,f},n.prototype.copy=function(e){return this.x=e.x,this.y=e.y,this.z=e.z,this.w=e.w,this},n.prototype.toEuler=function(e,f){f=f||"YZX";var n,o,d,i=this.x,t=this.y,l=this.z,u=this.w;switch(f){case"YZX":var p=i*t+l*u;if(p>.499&&(n=2*Math.atan2(i,u),o=Math.PI/2,d=0),-.499>p&&(n=-2*Math.atan2(i,u),o=-Math.PI/2,d=0),isNaN(n)){var s=i*i,y=t*t,c=l*l;n=Math.atan2(2*t*u-2*i*l,1-2*y-2*c),o=Math.asin(2*p),d=Math.atan2(2*i*u-2*t*l,1-2*s-2*c)}break;default:throw new Error("Euler order "+f+" not supported yet.")}e.y=n,e.z=o,e.x=d},n.prototype.setFromEuler=function(e,f,n,o){o=o||"XYZ";var d=Math.cos(e/2),i=Math.cos(f/2),t=Math.cos(n/2),l=Math.sin(e/2),u=Math.sin(f/2),p=Math.sin(n/2);return"XYZ"===o?(this.x=l*i*t+d*u*p,this.y=d*u*t-l*i*p,this.z=d*i*p+l*u*t,this.w=d*i*t-l*u*p):"YXZ"===o?(this.x=l*i*t+d*u*p,this.y=d*u*t-l*i*p,this.z=d*i*p-l*u*t,this.w=d*i*t+l*u*p):"ZXY"===o?(this.x=l*i*t-d*u*p,this.y=d*u*t+l*i*p,this.z=d*i*p+l*u*t,this.w=d*i*t-l*u*p):"ZYX"===o?(this.x=l*i*t-d*u*p,this.y=d*u*t+l*i*p,this.z=d*i*p-l*u*t,this.w=d*i*t+l*u*p):"YZX"===o?(this.x=l*i*t+d*u*p,this.y=d*u*t+l*i*p,this.z=d*i*p-l*u*t,this.w=d*i*t-l*u*p):"XZY"===o&&(this.x=l*i*t-d*u*p,this.y=d*u*t-l*i*p,this.z=d*i*p+l*u*t,this.w=d*i*t+l*u*p),this},n.prototype.clone=function(){return new n(this.x,this.y,this.z,this.w)}},{"./Vec3":30}],29:[function(e,f){function n(e){e=e||{},this.position=new o,e.position&&this.position.copy(e.position),this.quaternion=new d,e.quaternion&&this.quaternion.copy(e.quaternion)}var o=e("./Vec3"),d=e("./Quaternion");f.exports=n;var i=new d;n.pointToLocalFrame=function(e,f,n,d){var d=d||new o;return n.vsub(e,d),f.conjugate(i),i.vmult(d,d),d},n.prototype.pointToLocal=function(e,f){return n.pointToLocalFrame(this.position,this.quaternion,e,f)},n.pointToWorldFrame=function(e,f,n,d){var d=d||new o;return f.vmult(n,d),d.vadd(e,d),d},n.prototype.pointToWorld=function(e,f){return n.pointToWorldFrame(this.position,this.quaternion,e,f)},n.prototype.vectorToWorldFrame=function(e,f){var f=f||new o;return this.quaternion.vmult(e,f),f},n.vectorToWorldFrame=function(e,f,n){return e.vmult(f,n),n},n.vectorToLocalFrame=function(e,f,n,d){var d=d||new o;return f.w*=-1,f.vmult(n,d),f.w*=-1,d}},{"./Quaternion":28,"./Vec3":30}],30:[function(e,f){function n(e,f,n){this.x=e||0,this.y=f||0,this.z=n||0}f.exports=n;var o=e("./Mat3");n.ZERO=new n(0,0,0),n.UNIT_X=new n(1,0,0),n.UNIT_Y=new n(0,1,0),n.UNIT_Z=new n(0,0,1),n.prototype.cross=function(e,f){var o=e.x,d=e.y,i=e.z,t=this.x,l=this.y,u=this.z;return f=f||new n,f.x=l*i-u*d,f.y=u*o-t*i,f.z=t*d-l*o,f},n.prototype.set=function(e,f,n){return this.x=e,this.y=f,this.z=n,this},n.prototype.setZero=function(){this.x=this.y=this.z=0},n.prototype.vadd=function(e,f){return f?(f.x=e.x+this.x,f.y=e.y+this.y,f.z=e.z+this.z,void 0):new n(this.x+e.x,this.y+e.y,this.z+e.z)},n.prototype.vsub=function(e,f){return f?(f.x=this.x-e.x,f.y=this.y-e.y,f.z=this.z-e.z,void 0):new n(this.x-e.x,this.y-e.y,this.z-e.z)},n.prototype.crossmat=function(){return new o([0,-this.z,this.y,this.z,0,-this.x,-this.y,this.x,0])},n.prototype.normalize=function(){var e=this.x,f=this.y,n=this.z,o=Math.sqrt(e*e+f*f+n*n);if(o>0){var d=1/o;this.x*=d,this.y*=d,this.z*=d}else this.x=0,this.y=0,this.z=0;return o},n.prototype.unit=function(e){e=e||new n;var f=this.x,o=this.y,d=this.z,i=Math.sqrt(f*f+o*o+d*d);return i>0?(i=1/i,e.x=f*i,e.y=o*i,e.z=d*i):(e.x=1,e.y=0,e.z=0),e},n.prototype.norm=function(){var e=this.x,f=this.y,n=this.z;return Math.sqrt(e*e+f*f+n*n)},n.prototype.length=n.prototype.norm,n.prototype.norm2=function(){return this.dot(this)},n.prototype.lengthSquared=n.prototype.norm2,n.prototype.distanceTo=function(e){var f=this.x,n=this.y,o=this.z,d=e.x,i=e.y,t=e.z;return Math.sqrt((d-f)*(d-f)+(i-n)*(i-n)+(t-o)*(t-o))},n.prototype.distanceSquared=function(e){var f=this.x,n=this.y,o=this.z,d=e.x,i=e.y,t=e.z;return(d-f)*(d-f)+(i-n)*(i-n)+(t-o)*(t-o)},n.prototype.mult=function(e,f){f=f||new n;var o=this.x,d=this.y,i=this.z;return f.x=e*o,f.y=e*d,f.z=e*i,f},n.prototype.scale=n.prototype.mult,n.prototype.dot=function(e){return this.x*e.x+this.y*e.y+this.z*e.z},n.prototype.isZero=function(){return 0===this.x&&0===this.y&&0===this.z},n.prototype.negate=function(e){return e=e||new n,e.x=-this.x,e.y=-this.y,e.z=-this.z,e};var d=new n,i=new n;n.prototype.tangents=function(e,f){var n=this.norm();if(n>0){var o=d,t=1/n;o.set(this.x*t,this.y*t,this.z*t);var l=i;Math.abs(o.x)<.9?(l.set(1,0,0),o.cross(l,e)):(l.set(0,1,0),o.cross(l,e)),o.cross(e,f)}else e.set(1,0,0),f.set(0,1,0)},n.prototype.toString=function(){return this.x+","+this.y+","+this.z},n.prototype.toArray=function(){return[this.x,this.y,this.z]},n.prototype.copy=function(e){return this.x=e.x,this.y=e.y,this.z=e.z,this},n.prototype.lerp=function(e,f,n){var o=this.x,d=this.y,i=this.z;n.x=o+(e.x-o)*f,n.y=d+(e.y-d)*f,n.z=i+(e.z-i)*f},n.prototype.almostEquals=function(e,f){return void 0===f&&(f=1e-6),Math.abs(this.x-e.x)>f||Math.abs(this.y-e.y)>f||Math.abs(this.z-e.z)>f?!1:!0},n.prototype.almostZero=function(e){return void 0===e&&(e=1e-6),Math.abs(this.x)>e||Math.abs(this.y)>e||Math.abs(this.z)>e?!1:!0};var t=new n;n.prototype.isAntiparallelTo=function(e,f){return this.negate(t),t.almostEquals(e,f)},n.prototype.clone=function(){return new n(this.x,this.y,this.z)}},{"./Mat3":27}],31:[function(e,f){function n(e){e=e||{},o.apply(this),this.id=n.idCounter++,this.world=null,this.preStep=null,this.postStep=null,this.vlambda=new d,this.collisionFilterGroup="number"==typeof e.collisionFilterGroup?e.collisionFilterGroup:1,this.collisionFilterMask="number"==typeof e.collisionFilterMask?e.collisionFilterMask:1,this.collisionResponse=!0,this.position=new d,e.position&&this.position.copy(e.position),this.previousPosition=new d,this.initPosition=new d,this.velocity=new d,e.velocity&&this.velocity.copy(e.velocity),this.initVelocity=new d,this.force=new d;var f="number"==typeof e.mass?e.mass:0;this.mass=f,this.invMass=f>0?1/f:0,this.material=e.material||null,this.linearDamping="number"==typeof e.linearDamping?e.linearDamping:.01,this.type=0>=f?n.STATIC:n.DYNAMIC,typeof e.type==typeof n.STATIC&&(this.type=e.type),this.allowSleep="undefined"!=typeof e.allowSleep?e.allowSleep:!0,this.sleepState=0,this.sleepSpeedLimit="undefined"!=typeof e.sleepSpeedLimit?e.sleepSpeedLimit:.1,this.sleepTimeLimit="undefined"!=typeof e.sleepTimeLimit?e.sleepTimeLimit:1,this.timeLastSleepy=0,this._wakeUpAfterNarrowphase=!1,this.torque=new d,this.quaternion=new t,e.quaternion&&this.quaternion.copy(e.quaternion),this.initQuaternion=new t,this.angularVelocity=new d,e.angularVelocity&&this.angularVelocity.copy(e.angularVelocity),this.initAngularVelocity=new d,this.interpolatedPosition=new d,this.interpolatedQuaternion=new t,this.shapes=[],this.shapeOffsets=[],this.shapeOrientations=[],this.inertia=new d,this.invInertia=new d,this.invInertiaWorld=new i,this.invMassSolve=0,this.invInertiaSolve=new d,this.invInertiaWorldSolve=new i,this.fixedRotation="undefined"!=typeof e.fixedRotation?e.fixedRotation:!1,this.angularDamping="undefined"!=typeof e.angularDamping?e.angularDamping:.01,this.aabb=new l,this.aabbNeedsUpdate=!0,this.wlambda=new d,e.shape&&this.addShape(e.shape),this.updateMassProperties()}f.exports=n;var o=e("../utils/EventTarget"),d=(e("../shapes/Shape"),e("../math/Vec3")),i=e("../math/Mat3"),t=e("../math/Quaternion"),l=(e("../material/Material"),e("../collision/AABB")),u=e("../shapes/Box");n.prototype=new o,n.prototype.constructor=n,n.DYNAMIC=1,n.STATIC=2,n.KINEMATIC=4,n.AWAKE=0,n.SLEEPY=1,n.SLEEPING=2,n.idCounter=0,n.prototype.wakeUp=function(){var e=this.sleepState;this.sleepState=0,e===n.SLEEPING&&this.dispatchEvent({type:"wakeup"})},n.prototype.sleep=function(){this.sleepState=n.SLEEPING,this.velocity.set(0,0,0),this.angularVelocity.set(0,0,0)},n.sleepyEvent={type:"sleepy"},n.sleepEvent={type:"sleep"},n.prototype.sleepTick=function(e){if(this.allowSleep){var f=this.sleepState,o=this.velocity.norm2()+this.angularVelocity.norm2(),d=Math.pow(this.sleepSpeedLimit,2);f===n.AWAKE&&d>o?(this.sleepState=n.SLEEPY,this.timeLastSleepy=e,this.dispatchEvent(n.sleepyEvent)):f===n.SLEEPY&&o>d?this.wakeUp():f===n.SLEEPY&&e-this.timeLastSleepy>this.sleepTimeLimit&&(this.sleep(),this.dispatchEvent(n.sleepEvent))}},n.prototype.updateSolveMassProperties=function(){this.sleepState===n.SLEEPING||this.type===n.KINEMATIC?(this.invMassSolve=0,this.invInertiaSolve.setZero(),this.invInertiaWorldSolve.setZero()):(this.invMassSolve=this.invMass,this.invInertiaSolve.copy(this.invInertia),this.invInertiaWorldSolve.copy(this.invInertiaWorld))},n.prototype.pointToLocalFrame=function(e,f){var f=f||new d;return e.vsub(this.position,f),this.quaternion.conjugate().vmult(f,f),f},n.prototype.vectorToLocalFrame=function(e,f){var f=f||new d;return this.quaternion.conjugate().vmult(e,f),f},n.prototype.pointToWorldFrame=function(e,f){var f=f||new d;return this.quaternion.vmult(e,f),f.vadd(this.position,f),f},n.prototype.vectorToWorldFrame=function(e,f){var f=f||new d;return this.quaternion.vmult(e,f),f};var p=new d,s=new t;n.prototype.addShape=function(e,f,n){var o=new d,i=new t;return f&&o.copy(f),n&&i.copy(n),this.shapes.push(e),this.shapeOffsets.push(o),this.shapeOrientations.push(i),this.updateMassProperties(),this.updateBoundingRadius(),this.aabbNeedsUpdate=!0,this},n.prototype.updateBoundingRadius=function(){for(var e=this.shapes,f=this.shapeOffsets,n=e.length,o=0,d=0;d!==n;d++){var i=e[d];i.updateBoundingSphereRadius();var t=f[d].norm(),l=i.boundingSphereRadius;t+l>o&&(o=t+l)}this.boundingRadius=o};var y=new l;n.prototype.computeAABB=function(){for(var e=this.shapes,f=this.shapeOffsets,n=this.shapeOrientations,o=e.length,d=p,i=s,t=this.quaternion,l=this.aabb,u=y,c=0;c!==o;c++){var a=e[c];n[c].mult(t,i),i.vmult(f[c],d),d.vadd(this.position,d),a.calculateWorldAABB(d,i,u.lowerBound,u.upperBound),0===c?l.copy(u):l.extend(u)}this.aabbNeedsUpdate=!1};{var c=new i,a=new i;new i}n.prototype.updateInertiaWorld=function(e){var f=this.invInertia;if(f.x!==f.y||f.y!==f.z||e){var n=c,o=a;n.setRotationFromQuaternion(this.quaternion),n.transpose(o),n.scale(f,n),n.mmult(o,this.invInertiaWorld)}else;};var r=new d,w=new d;n.prototype.applyForce=function(e,f){if(this.type===n.DYNAMIC){var o=r;f.vsub(this.position,o);var d=w;o.cross(e,d),this.force.vadd(e,this.force),this.torque.vadd(d,this.torque)}};var b=new d,m=new d;n.prototype.applyLocalForce=function(e,f){if(this.type===n.DYNAMIC){var o=b,d=m;this.vectorToWorldFrame(e,o),this.pointToWorldFrame(f,d),this.applyForce(o,d)}};var N=new d,g=new d,x=new d;n.prototype.applyImpulse=function(e,f){if(this.type===n.DYNAMIC){var o=N;f.vsub(this.position,o);var d=g;d.copy(e),d.mult(this.invMass,d),this.velocity.vadd(d,this.velocity);var i=x;o.cross(e,i),this.invInertiaWorld.vmult(i,i),this.angularVelocity.vadd(i,this.angularVelocity)}};var j=new d,v=new d;n.prototype.applyLocalImpulse=function(e,f){if(this.type===n.DYNAMIC){var o=j,d=v;this.vectorToWorldFrame(e,o),this.pointToWorldFrame(f,d),this.applyImpulse(o,d)}};var A=new d;n.prototype.updateMassProperties=function(){var e=A;this.invMass=this.mass>0?1/this.mass:0;var f=this.inertia,n=this.fixedRotation;this.computeAABB(),e.set((this.aabb.upperBound.x-this.aabb.lowerBound.x)/2,(this.aabb.upperBound.y-this.aabb.lowerBound.y)/2,(this.aabb.upperBound.z-this.aabb.lowerBound.z)/2),u.calculateInertia(e,this.mass,f),this.invInertia.set(f.x>0&&!n?1/f.x:0,f.y>0&&!n?1/f.y:0,f.z>0&&!n?1/f.z:0),this.updateInertiaWorld(!0)},n.prototype.getVelocityAtWorldPoint=function(e,f){var n=new d;return e.vsub(this.position,n),this.angularVelocity.cross(n,f),this.velocity.vadd(f,f),f}},{"../collision/AABB":3,"../material/Material":25,"../math/Mat3":27,"../math/Quaternion":28,"../math/Vec3":30,"../shapes/Box":37,"../shapes/Shape":43,"../utils/EventTarget":49}],32:[function(e,f){function n(e){this.chassisBody=e.chassisBody,this.wheelInfos=[],this.sliding=!1,this.world=null,this.indexRightAxis="undefined"!=typeof e.indexRightAxis?e.indexRightAxis:1,this.indexForwardAxis="undefined"!=typeof e.indexForwardAxis?e.indexForwardAxis:0,this.indexUpAxis="undefined"!=typeof e.indexUpAxis?e.indexUpAxis:2}function o(e,f,n,o,i){var t=0,l=n,u=x,p=j,s=v;e.getVelocityAtWorldPoint(l,u),f.getVelocityAtWorldPoint(l,p),u.vsub(p,s);var y=o.dot(s),c=d(e,n,o),a=d(f,n,o),r=1,w=r/(c+a);return t=-y*w,t>i&&(t=i),-i>t&&(t=-i),t}function d(e,f,n){var o=A,d=C,i=O,t=h;return f.vsub(e.position,o),o.cross(n,d),e.invInertiaWorld.vmult(d,t),t.cross(o,i),e.invMass+n.dot(i)}function i(e,f,n,o,d,i){var t=d.norm2();if(t>1.1)return 0;var l=k,u=q,p=z;e.getVelocityAtWorldPoint(f,l),n.getVelocityAtWorldPoint(o,u),l.vsub(u,p);var s=d.dot(p),y=.2,c=1/(e.invMass+n.invMass),i=-y*s*c;return i}var t=(e("./Body"),e("../math/Vec3")),l=e("../math/Quaternion"),u=(e("../collision/RaycastResult"),e("../collision/Ray")),p=e("../objects/WheelInfo");f.exports=n;{var s=(new t,new t,new t,new t),y=new t,c=new t;new u}n.prototype.addWheel=function(e){e=e||{};var f=new p(e),n=this.wheelInfos.length;return this.wheelInfos.push(f),n},n.prototype.setSteeringValue=function(e,f){var n=this.wheelInfos[f];n.steering=e};new t;n.prototype.applyEngineForce=function(e,f){this.wheelInfos[f].engineForce=e},n.prototype.setBrake=function(e,f){this.wheelInfos[f].brake=e},n.prototype.addToWorld=function(e){this.constraints;e.add(this.chassisBody);var f=this;this.preStepCallback=function(){f.updateVehicle(e.dt)},e.addEventListener("preStep",this.preStepCallback),this.world=e},n.prototype.getVehicleAxisWorld=function(e,f){f.set(0===e?1:0,1===e?1:0,2===e?1:0),this.chassisBody.vectorToWorldFrame(f,f)},n.prototype.updateVehicle=function(e){for(var f=this.wheelInfos,n=f.length,o=this.chassisBody,d=0;n>d;d++)this.updateWheelTransform(d);this.currentVehicleSpeedKmHour=3.6*o.velocity.norm();var i=new t;this.getVehicleAxisWorld(this.indexForwardAxis,i),i.dot(o.velocity)<0&&(this.currentVehicleSpeedKmHour*=-1);for(var d=0;n>d;d++)this.castRay(f[d]);this.updateSuspension(e);for(var l=new t,u=new t,d=0;n>d;d++){var p=f[d],s=p.suspensionForce;s>p.maxSuspensionForce&&(s=p.maxSuspensionForce),p.raycastResult.hitNormalWorld.scale(s*e,l),p.raycastResult.hitPointWorld.vsub(o.position,u),o.applyImpulse(l,p.raycastResult.hitPointWorld)}this.updateFriction(e);var y=new t,c=new t,a=new t;for(d=0;n>d;d++){var p=f[d];o.getVelocityAtWorldPoint(p.chassisConnectionPointWorld,a);var r=1;switch(this.indexUpAxis){case 1:r=-1}if(p.isInContact){this.getVehicleAxisWorld(this.indexForwardAxis,c);var w=c.dot(p.raycastResult.hitNormalWorld);p.raycastResult.hitNormalWorld.scale(w,y),c.vsub(y,c);var b=c.dot(a);p.deltaRotation=r*b*e/p.radius}!p.sliding&&p.isInContact||0===p.engineForce||!p.useCustomSlidingRotationalSpeed||(p.deltaRotation=(p.engineForce>0?1:-1)*p.customSlidingRotationalSpeed*e),Math.abs(p.brake)>Math.abs(p.engineForce)&&(p.deltaRotation=0),p.rotation+=p.deltaRotation,p.deltaRotation*=.99}},n.prototype.updateSuspension=function(){for(var e=this.chassisBody,f=e.mass,n=this.wheelInfos,o=n.length,d=0;o>d;d++){var i=n[d];if(i.isInContact){var t,l=i.suspensionRestLength,u=i.suspensionLength,p=l-u;t=i.suspensionStiffness*p*i.clippedInvContactDotSuspension;var s,y=i.suspensionRelativeVelocity;s=0>y?i.dampingCompression:i.dampingRelaxation,t-=s*y,i.suspensionForce=t*f,i.suspensionForce<0&&(i.suspensionForce=0)}else i.suspensionForce=0}},n.prototype.removeFromWorld=function(e){this.constraints;e.remove(this.chassisBody),e.removeEventListener("preStep",this.preStepCallback),this.world=null};var a=new t,r=new t;n.prototype.castRay=function(e){var f=a,n=r;this.updateWheelTransformWorld(e);var o=this.chassisBody,d=-1,i=e.suspensionRestLength+e.radius;e.directionWorld.scale(i,f);var l=e.chassisConnectionPointWorld;l.vadd(f,n);var u=e.raycastResult;u.reset();var p=o.collisionResponse;o.collisionResponse=!1,this.world.rayTest(l,n,u),o.collisionResponse=p;var s=u.body;if(e.raycastResult.groundObject=0,s){d=u.distance,e.raycastResult.hitNormalWorld=u.hitNormalWorld,e.isInContact=!0;var y=u.distance;e.suspensionLength=y-e.radius;var c=e.suspensionRestLength-e.maxSuspensionTravel,w=e.suspensionRestLength+e.maxSuspensionTravel;e.suspensionLengthw&&(e.suspensionLength=w,e.raycastResult.reset());var b=e.raycastResult.hitNormalWorld.dot(e.directionWorld),m=new t;o.getVelocityAtWorldPoint(e.raycastResult.hitPointWorld,m);var N=e.raycastResult.hitNormalWorld.dot(m);if(b>=-.1)e.suspensionRelativeVelocity=0,e.clippedInvContactDotSuspension=10;else{var g=-1/b;e.suspensionRelativeVelocity=N*g,e.clippedInvContactDotSuspension=g}}else e.suspensionLength=e.suspensionRestLength+0*e.maxSuspensionTravel,e.suspensionRelativeVelocity=0,e.directionWorld.scale(-1,e.raycastResult.hitNormalWorld),e.clippedInvContactDotSuspension=1;return d},n.prototype.updateWheelTransformWorld=function(e){e.isInContact=!1;var f=this.chassisBody;f.pointToWorldFrame(e.chassisConnectionPointLocal,e.chassisConnectionPointWorld),f.vectorToWorldFrame(e.directionLocal,e.directionWorld),f.vectorToWorldFrame(e.axleLocal,e.axleWorld)},n.prototype.updateWheelTransform=function(e){var f=s,n=y,o=c,d=this.wheelInfos[e];this.updateWheelTransformWorld(d),d.directionLocal.scale(-1,f),n.copy(d.axleLocal),f.cross(n,o),o.normalize(),n.normalize();var i=d.steering,t=new l;t.setFromAxisAngle(f,i);var u=new l;u.setFromAxisAngle(n,d.rotation);var p=d.worldTransform.quaternion;this.chassisBody.quaternion.mult(t,p),p.mult(u,p),p.normalize();var a=d.worldTransform.position;a.copy(d.directionWorld),a.scale(d.suspensionLength,a),a.vadd(d.chassisConnectionPointWorld,a)};var w=[new t(1,0,0),new t(0,1,0),new t(0,0,1)];n.prototype.getWheelTransformWorld=function(e){return this.wheelInfos[e].worldTransform};var b=new t,m=[],N=[],g=1;n.prototype.updateFriction=function(e){for(var f=b,n=this.wheelInfos,d=n.length,l=this.chassisBody,u=N,p=m,s=0,y=0;d>y;y++){var c=n[y],a=c.raycastResult.body;a&&s++,c.sideImpulse=0,c.forwardImpulse=0,u[y]||(u[y]=new t),p[y]||(p[y]=new t)}for(var y=0;d>y;y++){var c=n[y],a=c.raycastResult.body;if(a){var r=p[y],x=this.getWheelTransformWorld(y);x.vectorToWorldFrame(w[this.indexRightAxis],r);var j=c.raycastResult.hitNormalWorld,v=r.dot(j);j.scale(v,f),r.vsub(f,r),r.normalize(),j.cross(r,u[y]),u[y].normalize(),c.sideImpulse=i(l,c.raycastResult.hitPointWorld,a,c.raycastResult.hitPointWorld,r),c.sideImpulse*=g}}var A=1,C=.5;this.sliding=!1;for(var y=0;d>y;y++){var c=n[y],a=c.raycastResult.body,O=0;if(c.slipInfo=1,a){var h=0,k=c.brake?c.brake:h;O=o(l,a,c.raycastResult.hitPointWorld,u[y],k),O+=c.engineForce*e;var q=k/O;c.slipInfo*=q}if(c.forwardImpulse=0,c.skidInfo=1,a){c.skidInfo=1;var z=c.suspensionForce*e*c.frictionSlip,B=z,D=z*B;c.forwardImpulse=O;var E=c.forwardImpulse*C,F=c.sideImpulse*A,G=E*E+F*F;if(c.sliding=!1,G>D){this.sliding=!0,c.sliding=!0;var q=z/Math.sqrt(G);c.skidInfo*=q}}}if(this.sliding)for(var y=0;d>y;y++){var c=n[y];0!==c.sideImpulse&&c.skidInfo<1&&(c.forwardImpulse*=c.skidInfo,c.sideImpulse*=c.skidInfo)}for(var y=0;d>y;y++){var c=n[y],H=new t;if(H.copy(c.raycastResult.hitPointWorld),0!==c.forwardImpulse){var I=new t;u[y].scale(c.forwardImpulse,I),l.applyImpulse(I,H)}if(0!==c.sideImpulse){var a=c.raycastResult.body,J=new t;J.copy(c.raycastResult.hitPointWorld);var K=new t;p[y].scale(c.sideImpulse,K),l.pointToLocalFrame(H,H),H["xyz"[this.indexUpAxis]]*=c.rollInfluence,l.pointToWorldFrame(H,H),l.applyImpulse(K,H),K.scale(-1,K),a.applyImpulse(K,J)}}};var x=new t,j=new t,v=new t,A=new t,C=new t,O=new t,h=new t,k=new t,q=new t,z=new t},{"../collision/Ray":9,"../collision/RaycastResult":10,"../math/Quaternion":28,"../math/Vec3":30,"../objects/WheelInfo":36,"./Body":31}],33:[function(e,f){function n(e){if(this.wheelBodies=[],this.coordinateSystem="undefined"==typeof e.coordinateSystem?new t(1,2,3):e.coordinateSystem.clone(),this.chassisBody=e.chassisBody,!this.chassisBody){var f=new i(new t(5,2,.5));this.chassisBody=new o(1,f)}this.constraints=[],this.wheelAxes=[],this.wheelForces=[]}var o=e("./Body"),d=e("../shapes/Sphere"),i=e("../shapes/Box"),t=e("../math/Vec3"),l=e("../constraints/HingeConstraint");f.exports=n,n.prototype.addWheel=function(e){e=e||{};var f=e.body;f||(f=new o(1,new d(1.2))),this.wheelBodies.push(f),this.wheelForces.push(0);var n=(new t,"undefined"!=typeof e.position?e.position.clone():new t),i=new t;this.chassisBody.pointToWorldFrame(n,i),f.position.set(i.x,i.y,i.z);var u="undefined"!=typeof e.axis?e.axis.clone():new t(0,1,0);this.wheelAxes.push(u);var p=new l(this.chassisBody,f,{pivotA:n,axisA:u,pivotB:t.ZERO,axisB:u,collideConnected:!1});return this.constraints.push(p),this.wheelBodies.length-1},n.prototype.setSteeringValue=function(e,f){var n=this.wheelAxes[f],o=Math.cos(e),d=Math.sin(e),i=n.x,t=n.y;this.constraints[f].axisA.set(o*i-d*t,d*i+o*t,0)},n.prototype.setMotorSpeed=function(e,f){var n=this.constraints[f];n.enableMotor(),n.motorTargetVelocity=e},n.prototype.disableMotor=function(e){var f=this.constraints[e]; +f.disableMotor()};var u=new t;n.prototype.setWheelForce=function(e,f){this.wheelForces[f]=e},n.prototype.applyWheelForce=function(e,f){var n=this.wheelAxes[f],o=this.wheelBodies[f],d=o.torque;n.scale(e,u),o.vectorToWorldFrame(u,u),d.vadd(u,d)},n.prototype.addToWorld=function(e){for(var f=this.constraints,n=this.wheelBodies.concat([this.chassisBody]),o=0;othis.particles.length&&this.neighbors.pop())};var d=new o;n.prototype.getNeighbors=function(e,f){for(var n=this.particles.length,o=e.id,i=this.smoothingRadius*this.smoothingRadius,t=d,l=0;l!==n;l++){var u=this.particles[l];u.position.vsub(e.position,t),o!==u.id&&t.norm2()=-.1)this.suspensionRelativeVelocity=0,this.clippedInvContactDotSuspension=10;else{var d=-1/n;this.suspensionRelativeVelocity=o*d,this.clippedInvContactDotSuspension=d}}else f.suspensionLength=this.suspensionRestLength,this.suspensionRelativeVelocity=0,f.directionWorld.scale(-1,f.hitNormalWorld),this.clippedInvContactDotSuspension=1}},{"../collision/RaycastResult":10,"../math/Transform":29,"../math/Vec3":30,"../utils/Utils":53}],37:[function(e,f){function n(e){o.call(this),this.type=o.types.BOX,this.halfExtents=e,this.convexPolyhedronRepresentation=null,this.updateConvexPolyhedronRepresentation(),this.updateBoundingSphereRadius()}f.exports=n;var o=e("./Shape"),d=e("../math/Vec3"),i=e("./ConvexPolyhedron");n.prototype=new o,n.prototype.constructor=n,n.prototype.updateConvexPolyhedronRepresentation=function(){var e=this.halfExtents.x,f=this.halfExtents.y,n=this.halfExtents.z,o=d,t=[new o(-e,-f,-n),new o(e,-f,-n),new o(e,f,-n),new o(-e,f,-n),new o(-e,-f,n),new o(e,-f,n),new o(e,f,n),new o(-e,f,n)],l=[[3,2,1,0],[4,5,6,7],[5,4,0,1],[2,3,7,6],[0,4,7,3],[1,2,6,5]],u=([new o(0,0,1),new o(0,1,0),new o(1,0,0)],new i(t,l));this.convexPolyhedronRepresentation=u,u.material=this.material},n.prototype.calculateLocalInertia=function(e,f){return f=f||new d,n.calculateInertia(this.halfExtents,e,f),f},n.calculateInertia=function(e,f,n){var o=e;n.x=1/12*f*(2*o.y*2*o.y+2*o.z*2*o.z),n.y=1/12*f*(2*o.x*2*o.x+2*o.z*2*o.z),n.z=1/12*f*(2*o.y*2*o.y+2*o.x*2*o.x)},n.prototype.getSideNormals=function(e,f){var n=e,o=this.halfExtents;if(n[0].set(o.x,0,0),n[1].set(0,o.y,0),n[2].set(0,0,o.z),n[3].set(-o.x,0,0),n[4].set(0,-o.y,0),n[5].set(0,0,-o.z),void 0!==f)for(var d=0;d!==n.length;d++)f.vmult(n[d],n[d]);return n},n.prototype.volume=function(){return 8*this.halfExtents.x*this.halfExtents.y*this.halfExtents.z},n.prototype.updateBoundingSphereRadius=function(){this.boundingSphereRadius=this.halfExtents.norm()};{var t=new d;new d}n.prototype.forEachWorldCorner=function(e,f,n){for(var o=this.halfExtents,d=[[o.x,o.y,o.z],[-o.x,o.y,o.z],[-o.x,-o.y,o.z],[-o.x,-o.y,-o.z],[o.x,-o.y,-o.z],[o.x,o.y,-o.z],[-o.x,o.y,-o.z],[o.x,-o.y,o.z]],i=0;it;t++){var i=l[t];f.vmult(i,i),e.vadd(i,i);var u=i.x,p=i.y,s=i.z;u>o.x&&(o.x=u),p>o.y&&(o.y=p),s>o.z&&(o.z=s),ua&&(a=w,c=r)}for(var b=[],m=n.faces[c],N=m.length,g=0;N>g;g++){var x=n.vertices[m[g]],j=new d;j.copy(x),i.vmult(j,j),o.vadd(j,j),b.push(j)}c>=0&&this.clipFaceAgainstHull(t,e,f,b,l,u,s)};var s=new d,y=new d,c=new d,a=new d,r=new d,w=new d;n.prototype.findSeparatingAxis=function(e,f,n,o,d,i,t,l){var u=s,p=y,b=c,m=a,N=r,g=w,x=Number.MAX_VALUE,j=this,v=0;if(j.uniqueAxes)for(var A=0;A!==j.uniqueAxes.length;A++){n.vmult(j.uniqueAxes[A],u);var C=j.testSepAxis(u,e,f,n,o,d);if(C===!1)return!1;x>C&&(x=C,i.copy(u))}else for(var O=t?t.length:j.faces.length,A=0;O>A;A++){var h=t?t[A]:A;u.copy(j.faceNormals[h]),n.vmult(u,u);var C=j.testSepAxis(u,e,f,n,o,d);if(C===!1)return!1;x>C&&(x=C,i.copy(u))}if(e.uniqueAxes)for(var A=0;A!==e.uniqueAxes.length;A++){d.vmult(e.uniqueAxes[A],p),v++;var C=j.testSepAxis(p,e,f,n,o,d);if(C===!1)return!1;x>C&&(x=C,i.copy(p))}else for(var k=l?l.length:e.faces.length,A=0;k>A;A++){var h=l?l[A]:A;p.copy(e.faceNormals[h]),d.vmult(p,p),v++;var C=j.testSepAxis(p,e,f,n,o,d);if(C===!1)return!1;x>C&&(x=C,i.copy(p))}for(var q=0;q!==j.uniqueEdges.length;q++){n.vmult(j.uniqueEdges[q],m);for(var z=0;z!==e.uniqueEdges.length;z++)if(d.vmult(e.uniqueEdges[z],N),m.cross(N,g),!g.almostZero()){g.normalize();var B=j.testSepAxis(g,e,f,n,o,d);if(B===!1)return!1;x>B&&(x=B,i.copy(g))}}return o.vsub(f,b),b.dot(i)>0&&i.negate(i),!0};var b=[],m=[];n.prototype.testSepAxis=function(e,f,o,d,i,t){var l=this;n.project(l,e,o,d,b),n.project(f,e,i,t,m);var u=b[0],p=b[1],s=m[0],y=m[1];if(y>u||p>s)return!1;var c=u-y,a=s-p,r=a>c?c:a;return r};var N=new d,g=new d;n.prototype.calculateLocalInertia=function(e,f){this.computeLocalAABB(N,g);var n=g.x-N.x,o=g.y-N.y,d=g.z-N.z;f.x=1/12*e*(2*o*2*o+2*d*2*d),f.y=1/12*e*(2*n*2*n+2*d*2*d),f.z=1/12*e*(2*o*2*o+2*n*2*n)},n.prototype.getPlaneConstantOfFace=function(e){var f=this.faces[e],n=this.faceNormals[e],o=this.vertices[f[0]],d=-n.dot(o);return d};var x=new d,j=new d,v=new d,A=new d,C=new d,O=new d,h=new d,k=new d;n.prototype.clipFaceAgainstHull=function(e,f,n,o,d,i,t){for(var l=x,u=j,p=v,s=A,y=C,c=O,a=h,r=k,w=this,b=[],m=o,N=b,g=-1,q=Number.MAX_VALUE,z=0;zB&&(q=B,g=z)}if(!(0>g)){var D=w.faces[g];D.connectedFaces=[];for(var E=0;EH;H++){var I=w.vertices[D[H]],J=w.vertices[D[(H+1)%G]];I.vsub(J,u),p.copy(u),n.vmult(p,p),f.vadd(p,p),s.copy(this.faceNormals[g]),n.vmult(s,s),f.vadd(s,s),p.cross(s,y),y.negate(y),c.copy(I),n.vmult(c,c),f.vadd(c,c);var K,L=(-c.dot(y),D.connectedFaces[H]);a.copy(this.faceNormals[L]);var M=this.getPlaneConstantOfFace(L);r.copy(a),n.vmult(r,r);var K=M-r.dot(f);for(this.clipFaceAgainstPlane(m,N,r,K);m.length;)m.shift();for(;N.length;)m.push(N.shift())}a.copy(this.faceNormals[g]);var M=this.getPlaneConstantOfFace(g);r.copy(a),n.vmult(r,r);for(var K=M-r.dot(f),E=0;E=P&&(console.log("clamped: depth="+P+" to minDist="+(d+"")),P=d),i>=P){var Q=m[E];if(0>=P){var R={point:Q,normal:r,depth:P};t.push(R)}}}}},n.prototype.clipFaceAgainstPlane=function(e,f,n,o){var i,t,l=e.length;if(2>l)return f;var u=e[e.length-1],p=e[0];i=n.dot(u)+o;for(var s=0;l>s;s++){if(p=e[s],t=n.dot(p)+o,0>i)if(0>t){var y=new d;y.copy(p),f.push(y)}else{var y=new d;u.lerp(p,i/(i-t),y),f.push(y)}else if(0>t){var y=new d;u.lerp(p,i/(i-t),y),f.push(y),f.push(p)}u=p,i=t}return f},n.prototype.computeWorldVertices=function(e,f){for(var n=this.vertices.length;this.worldVertices.lengthd;d++){var i=o[d];i.xf.x&&(f.x=i.x),i.yf.y&&(f.y=i.y),i.zf.z&&(f.z=i.z)}},n.prototype.computeWorldFaceNormals=function(e){for(var f=this.faceNormals.length;this.worldFaceNormals.lengthe&&(e=d)}this.boundingSphereRadius=Math.sqrt(e)};var q=new d;n.prototype.calculateWorldAABB=function(e,f,n,o){for(var d,i,t,l,u,p,s=this.vertices.length,y=this.vertices,c=0;s>c;c++){q.copy(y[c]),f.vmult(q,q),e.vadd(q,q);var a=q;a.xl||void 0===l)&&(l=a.x),a.yu||void 0===u)&&(u=a.y),a.zp||void 0===p)&&(p=a.z)}n.set(d,i,t),o.set(l,u,p)},n.prototype.volume=function(){return 4*Math.PI*this.boundingSphereRadius/3},n.prototype.getAveragePointLocal=function(e){e=e||new d;for(var f=this.vertices.length,n=this.vertices,o=0;f>o;o++)e.vadd(n[o],e);return e.mult(1/f,e),e},n.prototype.transformAllPoints=function(e,f){var n=this.vertices.length,o=this.vertices;if(f){for(var d=0;n>d;d++){var i=o[d];f.vmult(i,i)}for(var d=0;dd;d++){var i=o[d];i.vadd(e,i)}};var z=new d,B=new d,D=new d;n.prototype.pointIsInside=function(e){var f=this.vertices.length,n=this.vertices,o=this.faces,d=this.faceNormals,i=null,t=this.faces.length,l=z;this.getAveragePointLocal(l);for(var u=0;t>u;u++){var f=(this.faces[u].length,d[u]),p=n[o[u][0]],s=B;e.vsub(p,s);var y=f.dot(s),c=D;l.vsub(p,c);var a=f.dot(c);if(0>y&&a>0||y>0&&0>a)return!1}return i?1:-1};var E=(new d,new d),F=new d;n.project=function(e,f,n,o,d){var t=e.vertices.length,l=E,u=0,p=0,s=F,y=e.vertices;s.setZero(),i.vectorToLocalFrame(n,o,f,l),i.pointToLocalFrame(n,o,s,s);var c=s.dot(l);p=u=y[0].dot(l);for(var a=1;t>a;a++){var r=y[a].dot(l);r>u&&(u=r),p>r&&(p=r)}if(p-=c,u-=c,p>u){var w=p;p=u,u=w}d[0]=u,d[1]=p}},{"../math/Quaternion":28,"../math/Transform":29,"../math/Vec3":30,"./Shape":43}],39:[function(e,f){function n(e,f,n,t){var l=t,u=[],p=[],s=[],y=[],c=[],a=Math.cos,r=Math.sin;u.push(new d(f*a(0),f*r(0),.5*-n)),y.push(0),u.push(new d(e*a(0),e*r(0),.5*n)),c.push(1);for(var w=0;l>w;w++){var b=2*Math.PI/l*(w+1),m=2*Math.PI/l*(w+.5);l-1>w?(u.push(new d(f*a(b),f*r(b),.5*-n)),y.push(2*w+2),u.push(new d(e*a(b),e*r(b),.5*n)),c.push(2*w+3),s.push([2*w+2,2*w+3,2*w+1,2*w])):s.push([0,1,2*w+1,2*w]),(l%2===1||l/2>w)&&p.push(new d(a(m),r(m),0))}s.push(c),p.push(new d(0,0,1));for(var N=[],w=0;wd&&(f=d)}this.minValue=f},n.prototype.updateMaxValue=function(){for(var e=this.data,f=e[0][0],n=0;n!==e.length;n++)for(var o=0;o!==e[n].length;o++){var d=e[n][o];d>f&&(f=d)}this.maxValue=f},n.prototype.setHeightValueAtIndex=function(e,f,n){var o=this.data;o[e][f]=n,this.clearCachedConvexTrianglePillar(e,f,!1),e>0&&(this.clearCachedConvexTrianglePillar(e-1,f,!0),this.clearCachedConvexTrianglePillar(e-1,f,!1)),f>0&&(this.clearCachedConvexTrianglePillar(e,f-1,!0),this.clearCachedConvexTrianglePillar(e,f-1,!1)),f>0&&e>0&&this.clearCachedConvexTrianglePillar(e-1,f-1,!0)},n.prototype.getRectMinMax=function(e,f,n,o,d){d=d||[];for(var i=this.data,t=this.minValue,l=e;n>=l;l++)for(var u=f;o>=u;u++){var p=i[l][u];p>t&&(t=p)}d[0]=this.minValue,d[1]=t},n.prototype.getIndexOfPosition=function(e,f,n,o){var d=this.elementSize,i=this.data,t=Math.floor(e/d),l=Math.floor(f/d);return n[0]=t,n[1]=l,o&&(0>t&&(t=0),0>l&&(l=0),t>=i.length-1&&(t=i.length-1),l>=i[0].length-1&&(l=i[0].length-1)),0>t||0>l||t>=i.length-1||l>=i[0].length-1?!1:!0},n.prototype.getHeightAt=function(e,f,n){var o=[];this.getIndexOfPosition(e,f,o,n);var d=[];return this.getRectMinMax(o[0],o[1]+1,o[0],o[1]+1,d),(d[0]+d[1])/2},n.prototype.getCacheConvexTrianglePillarKey=function(e,f,n){return e+"_"+f+"_"+(n?1:0)},n.prototype.getCachedConvexTrianglePillar=function(e,f,n){return this._cachedPillars[this.getCacheConvexTrianglePillarKey(e,f,n)]},n.prototype.setCachedConvexTrianglePillar=function(e,f,n,o,d){this._cachedPillars[this.getCacheConvexTrianglePillarKey(e,f,n)]={convex:o,offset:d}},n.prototype.clearCachedConvexTrianglePillar=function(e,f,n){delete this._cachedPillars[this.getCacheConvexTrianglePillarKey(e,f,n)]},n.prototype.getConvexTrianglePillar=function(e,f,n){var o=this.pillarConvex,t=this.pillarOffset;if(this.cacheEnabled){var l=this.getCachedConvexTrianglePillar(e,f,n);if(l)return this.pillarConvex=l.convex,void(this.pillarOffset=l.offset);o=new d,t=new i,this.pillarConvex=o,this.pillarOffset=t}var l=this.data,u=this.elementSize,p=o.faces;o.vertices.length=6;for(var s=0;6>s;s++)o.vertices[s]||(o.vertices[s]=new i);p.length=5;for(var s=0;5>s;s++)p[s]||(p[s]=[]);var y=o.vertices,c=(Math.min(l[e][f],l[e+1][f],l[e][f+1],l[e+1][f+1])-this.minValue)/2+this.minValue;n?(t.set((e+.75)*u,(f+.75)*u,c),y[0].set(.25*u,.25*u,l[e+1][f+1]-c),y[1].set(-.75*u,.25*u,l[e][f+1]-c),y[2].set(.25*u,-.75*u,l[e+1][f]-c),y[3].set(.25*u,.25*u,-c-1),y[4].set(-.75*u,.25*u,-c-1),y[5].set(.25*u,-.75*u,-c-1),p[0][0]=0,p[0][1]=1,p[0][2]=2,p[1][0]=5,p[1][1]=4,p[1][2]=3,p[2][0]=2,p[2][1]=5,p[2][2]=3,p[2][3]=0,p[3][0]=3,p[3][1]=4,p[3][2]=1,p[3][3]=0,p[4][0]=1,p[4][1]=4,p[4][2]=5,p[4][3]=2):(t.set((e+.25)*u,(f+.25)*u,c),y[0].set(-.25*u,-.25*u,l[e][f]-c),y[1].set(.75*u,-.25*u,l[e+1][f]-c),y[2].set(-.25*u,.75*u,l[e][f+1]-c),y[3].set(-.25*u,-.25*u,-c-1),y[4].set(.75*u,-.25*u,-c-1),y[5].set(-.25*u,.75*u,-c-1),p[0][0]=0,p[0][1]=1,p[0][2]=2,p[1][0]=5,p[1][1]=4,p[1][2]=3,p[2][0]=0,p[2][1]=2,p[2][2]=5,p[2][3]=3,p[3][0]=1,p[3][1]=0,p[3][2]=3,p[3][3]=4,p[4][0]=4,p[4][1]=5,p[4][2]=2,p[4][3]=1),o.computeNormals(),o.computeEdges(),o.updateBoundingSphereRadius(),this.setCachedConvexTrianglePillar(e,f,n,o,t)},n.prototype.calculateLocalInertia=function(e,f){return f=f||new i,f.set(0,0,0),f},n.prototype.volume=function(){return Number.MAX_VALUE},n.prototype.calculateWorldAABB=function(e,f,n,o){n.set(-Number.MAX_VALUE,-Number.MAX_VALUE,-Number.MAX_VALUE),o.set(Number.MAX_VALUE,Number.MAX_VALUE,Number.MAX_VALUE)},n.prototype.updateBoundingSphereRadius=function(){var e=this.data,f=this.elementSize;this.boundingSphereRadius=new i(e.length*f,e[0].length*f,Math.max(Math.abs(this.maxValue),Math.abs(this.minValue))).norm()}},{"../math/Vec3":30,"../utils/Utils":53,"./ConvexPolyhedron":38,"./Shape":43}],41:[function(e,f){function n(){o.call(this),this.type=o.types.PARTICLE}f.exports=n;var o=e("./Shape"),d=e("../math/Vec3");n.prototype=new o,n.prototype.constructor=n,n.prototype.calculateLocalInertia=function(e,f){return f=f||new d,f.set(0,0,0),f},n.prototype.volume=function(){return 0},n.prototype.updateBoundingSphereRadius=function(){this.boundingSphereRadius=0},n.prototype.calculateWorldAABB=function(e,f,n,o){n.copy(e),o.copy(e)}},{"../math/Vec3":30,"./Shape":43}],42:[function(e,f){function n(){o.call(this),this.type=o.types.PLANE,this.worldNormal=new d,this.worldNormalNeedsUpdate=!0,this.boundingSphereRadius=Number.MAX_VALUE}f.exports=n;var o=e("./Shape"),d=e("../math/Vec3");n.prototype=new o,n.prototype.constructor=n,n.prototype.computeWorldNormal=function(e){var f=this.worldNormal;f.set(0,0,1),e.vmult(f,f),this.worldNormalNeedsUpdate=!1},n.prototype.calculateLocalInertia=function(e,f){return f=f||new d},n.prototype.volume=function(){return Number.MAX_VALUE};var i=new d;n.prototype.calculateWorldAABB=function(e,f,n,o){i.set(0,0,1),f.vmult(i,i);var d=Number.MAX_VALUE;n.set(-d,-d,-d),o.set(d,d,d),1===i.x&&(o.x=e.x),1===i.y&&(o.y=e.y),1===i.z&&(o.z=e.z),-1===i.x&&(n.x=e.x),-1===i.y&&(n.y=e.y),-1===i.z&&(n.z=e.z)},n.prototype.updateBoundingSphereRadius=function(){this.boundingSphereRadius=Number.MAX_VALUE}},{"../math/Vec3":30,"./Shape":43}],43:[function(e,f){function n(){this.id=n.idCounter++,this.type=0,this.boundingSphereRadius=0,this.collisionResponse=!0,this.material=null}f.exports=n;{var n=e("./Shape");e("../math/Vec3"),e("../math/Quaternion"),e("../material/Material")}n.prototype.constructor=n,n.prototype.updateBoundingSphereRadius=function(){throw"computeBoundingSphereRadius() not implemented for shape type "+this.type},n.prototype.volume=function(){throw"volume() not implemented for shape type "+this.type},n.prototype.calculateLocalInertia=function(){throw"calculateLocalInertia() not implemented for shape type "+this.type},n.idCounter=0,n.types={SPHERE:1,PLANE:2,BOX:4,COMPOUND:8,CONVEXPOLYHEDRON:16,HEIGHTFIELD:32,PARTICLE:64,CYLINDER:128,TRIMESH:256}},{"../material/Material":25,"../math/Quaternion":28,"../math/Vec3":30,"./Shape":43}],44:[function(e,f){function n(e){if(o.call(this),this.radius=void 0!==e?Number(e):1,this.type=o.types.SPHERE,this.radius<0)throw new Error("The sphere radius cannot be negative.");this.updateBoundingSphereRadius()}f.exports=n;var o=e("./Shape"),d=e("../math/Vec3");n.prototype=new o,n.prototype.constructor=n,n.prototype.calculateLocalInertia=function(e,f){f=f||new d;var n=2*e*this.radius*this.radius/5;return f.x=n,f.y=n,f.z=n,f},n.prototype.volume=function(){return 4*Math.PI*this.radius/3},n.prototype.updateBoundingSphereRadius=function(){this.boundingSphereRadius=this.radius},n.prototype.calculateWorldAABB=function(e,f,n,o){for(var d=this.radius,i=["x","y","z"],t=0;td?d+"_"+i:i+"_"+d;e[f]=!0},n=0;nn.x&&(n.x=d.x),d.yn.y&&(n.y=d.y),d.zn.z&&(n.z=d.z)},n.prototype.updateAABB=function(){this.computeLocalAABB(this.aabb)},n.prototype.updateBoundingSphereRadius=function(){for(var e=0,f=this.vertices,n=new d,o=0,i=f.length/3;o!==i;o++){this.getVertex(o,n);var t=n.norm2();t>e&&(e=t)}this.boundingSphereRadius=Math.sqrt(e)};var g=(new d,new i),x=new t;n.prototype.calculateWorldAABB=function(e,f,n,o){var d=g,i=x;d.position=e,d.quaternion=f,this.aabb.toWorldFrame(d,i),n.copy(i.lowerBound),o.copy(i.upperBound)},n.prototype.volume=function(){return 4*Math.PI*this.boundingSphereRadius/3},n.createTorus=function(e,f,o,d,i){e=e||1,f=f||.5,o=o||8,d=d||6,i=i||2*Math.PI;for(var t=[],l=[],u=0;o>=u;u++)for(var p=0;d>=p;p++){var s=p/d*i,y=u/o*Math.PI*2,c=(e+f*Math.cos(y))*Math.cos(s),a=(e+f*Math.cos(y))*Math.sin(s),r=f*Math.sin(y);t.push(c,a,r)}for(var u=1;o>=u;u++)for(var p=1;d>=p;p++){var w=(d+1)*u+p-1,b=(d+1)*(u-1)+p-1,m=(d+1)*(u-1)+p,N=(d+1)*u+p;l.push(w,b,N),l.push(b,m,N)}return new n(t,l)}},{"../collision/AABB":3,"../math/Quaternion":28,"../math/Transform":29,"../math/Vec3":30,"../utils/Octree":50,"./Shape":43}],46:[function(e,f){function n(){o.call(this),this.iterations=10,this.tolerance=1e-7}f.exports=n;var o=(e("../math/Vec3"),e("../math/Quaternion"),e("./Solver"));n.prototype=new o;var d=[],i=[],t=[];n.prototype.solve=function(e,f){var n,o,l,u,p,s,y=0,c=this.iterations,a=this.tolerance*this.tolerance,r=this.equations,w=r.length,b=f.bodies,m=b.length,N=e;if(0!==w)for(var g=0;g!==m;g++)b[g].updateSolveMassProperties();var x=i,j=t,v=d; +x.length=w,j.length=w,v.length=w;for(var g=0;g!==w;g++){var A=r[g];v[g]=0,j[g]=A.computeB(N),x[g]=1/A.computeC()}if(0!==w){for(var g=0;g!==m;g++){var C=b[g],O=C.vlambda,h=C.wlambda;O.set(0,0,0),h&&h.set(0,0,0)}for(y=0;y!==c;y++){u=0;for(var k=0;k!==w;k++){var A=r[k];n=j[k],o=x[k],s=v[k],p=A.computeGWlambda(),l=o*(n-p-A.eps*s),s+lA.maxForce&&(l=A.maxForce-s),v[k]+=l,u+=l>0?l:-l,A.addToWlambda(l)}if(a>u*u)break}for(var g=0;g!==m;g++){var C=b[g],q=C.velocity,z=C.angularVelocity;q.vadd(C.vlambda,q),z&&z.vadd(C.wlambda,z)}}return y}},{"../math/Quaternion":28,"../math/Vec3":30,"./Solver":47}],47:[function(e,f){function n(){this.equations=[]}f.exports=n,n.prototype.solve=function(){return 0},n.prototype.addEquation=function(e){e.enabled&&this.equations.push(e)},n.prototype.removeEquation=function(e){var f=this.equations,n=f.indexOf(e);-1!==n&&f.splice(n,1)},n.prototype.removeAllEquations=function(){this.equations.length=0}},{}],48:[function(e,f){function n(e){for(l.call(this),this.iterations=10,this.tolerance=1e-7,this.subsolver=e,this.nodes=[],this.nodePool=[];this.nodePool.length<128;)this.nodePool.push(this.createNode())}function o(e){for(var f=e.length,n=0;n!==f;n++){var o=e[n];if(!(o.visited||o.body.type&c))return o}return!1}function d(e,f,n,d){for(a.push(e),e.visited=!0,f(e,n,d);a.length;)for(var i,t=a.pop();i=o(t.children);)i.visited=!0,f(i,n,d),a.push(i)}function i(e,f,n){f.push(e.body);for(var o=e.eqs.length,d=0;d!==o;d++){var i=e.eqs[d];-1===n.indexOf(i)&&n.push(i)}}function t(e,f){return f.id-e.id}f.exports=n;var l=(e("../math/Vec3"),e("../math/Quaternion"),e("./Solver")),u=e("../objects/Body");n.prototype=new l;var p=[],s=[],y={bodies:[]},c=u.STATIC,a=[];n.prototype.createNode=function(){return{body:null,children:[],eqs:[],visited:!1}},n.prototype.solve=function(e,f){for(var n=p,l=this.nodePool,u=f.bodies,c=this.equations,a=c.length,r=u.length,w=this.subsolver;l.lengthb;b++)n[b]=l[b];for(var b=0;b!==r;b++){var m=n[b];m.body=u[b],m.children.length=0,m.eqs.length=0,m.visited=!1}for(var N=0;N!==a;N++){var g=c[N],b=u.indexOf(g.bi),x=u.indexOf(g.bj),j=n[b],v=n[x];j.children.push(v),j.eqs.push(g),v.children.push(j),v.eqs.push(g)}var A,C=0,O=s;w.tolerance=this.tolerance,w.iterations=this.iterations;for(var h=y;A=o(n);){O.length=0,h.bodies.length=0,d(A,i,h.bodies,O);var k=O.length;O=O.sort(t);for(var b=0;b!==k;b++)w.addEquation(O[b]);{w.solve(e,h)}w.removeAllEquations(),C++}return C}},{"../math/Quaternion":28,"../math/Vec3":30,"../objects/Body":31,"./Solver":47}],49:[function(e,f){var n=function(){};f.exports=n,n.prototype={constructor:n,addEventListener:function(e,f){void 0===this._listeners&&(this._listeners={});var n=this._listeners;return void 0===n[e]&&(n[e]=[]),-1===n[e].indexOf(f)&&n[e].push(f),this},hasEventListener:function(e,f){if(void 0===this._listeners)return!1;var n=this._listeners;return void 0!==n[e]&&-1!==n[e].indexOf(f)?!0:!1},removeEventListener:function(e,f){if(void 0===this._listeners)return this;var n=this._listeners;if(void 0===n[e])return this;var o=n[e].indexOf(f);return-1!==o&&n[e].splice(o,1),this},dispatchEvent:function(e){if(void 0===this._listeners)return this;var f=this._listeners,n=f[e.type];if(void 0!==n){e.target=this;for(var o=0,d=n.length;d>o;o++)n[o].call(this,e)}return this}}},{}],50:[function(e,f){function n(e){e=e||{},this.root=e.root||null,this.aabb=e.aabb?e.aabb.clone():new d,this.data=[],this.children=[]}function o(e,f){f=f||{},f.root=null,f.aabb=e,n.call(this,f),this.maxDepth="undefined"!=typeof f.maxDepth?f.maxDepth:8}var d=e("../collision/AABB"),i=e("../math/Vec3");f.exports=o,o.prototype=new n,n.prototype.reset=function(){this.children.length=this.data.length=0},n.prototype.insert=function(e,f,n){var o=this.data;if(n=n||0,!this.aabb.contains(e))return!1;var d=this.children;if(n<(this.maxDepth||this.root.maxDepth)){var i=!1;d.length||(this.subdivide(),i=!0);for(var t=0;8!==t;t++)if(d[t].insert(e,f,n+1))return!0;i&&(d.length=0)}return o.push(f),!0};var t=new i;n.prototype.subdivide=function(){var e=this.aabb,f=e.lowerBound,o=e.upperBound,l=this.children;l.push(new n({aabb:new d({lowerBound:new i(0,0,0)})}),new n({aabb:new d({lowerBound:new i(1,0,0)})}),new n({aabb:new d({lowerBound:new i(1,1,0)})}),new n({aabb:new d({lowerBound:new i(1,1,1)})}),new n({aabb:new d({lowerBound:new i(0,1,1)})}),new n({aabb:new d({lowerBound:new i(0,0,1)})}),new n({aabb:new d({lowerBound:new i(1,0,1)})}),new n({aabb:new d({lowerBound:new i(0,1,0)})})),o.vsub(f,t),t.scale(.5,t);for(var u=this.root||this,p=0;8!==p;p++){var s=l[p];s.root=u;var y=s.aabb.lowerBound;y.x*=t.x,y.y*=t.y,y.z*=t.z,y.vadd(f,y),y.vadd(t,s.aabb.upperBound)}},n.prototype.aabbQuery=function(e,f){for(var n=(this.data,this.children,[this]);n.length;){var o=n.pop();o.aabb.overlaps(e)&&Array.prototype.push.apply(f,o.data),Array.prototype.push.apply(n,o.children)}return f};var l=new d;n.prototype.rayQuery=function(e,f,n){return e.getAABB(l),l.toLocalFrame(f,l),this.aabbQuery(l,n),n},n.prototype.removeEmptyNodes=function(){for(var e=[this];e.length;){for(var f=e.pop(),n=f.children.length-1;n>=0;n--)f.children[n].data.length||f.children.splice(n,1);Array.prototype.push.apply(e,f.children)}}},{"../collision/AABB":3,"../math/Vec3":30}],51:[function(e,f){function n(){this.objects=[],this.type=Object}f.exports=n,n.prototype.release=function(){for(var e=arguments.length,f=0;f!==e;f++)this.objects.push(arguments[f])},n.prototype.get=function(){return 0===this.objects.length?this.constructObject():this.objects.pop()},n.prototype.constructObject=function(){throw new Error("constructObject() not implemented in this Pool subclass yet!")}},{}],52:[function(e,f){function n(){this.data={keys:[]}}f.exports=n,n.prototype.get=function(e,f){if(e>f){var n=f;f=e,e=n}return this.data[e+"-"+f]},n.prototype.set=function(e,f,n){if(e>f){var o=f;f=e,e=o}var d=e+"-"+f;this.get(e,f)||this.data.keys.push(d),this.data[d]=n},n.prototype.reset=function(){for(var e=this.data,f=e.keys;f.length>0;){var n=f.pop();delete e[n]}}},{}],53:[function(e,f){function n(){}f.exports=n,n.defaults=function(e,f){e=e||{};for(var n in f)n in e||(e[n]=f[n]);return e}},{}],54:[function(e,f){function n(){d.call(this),this.type=o}f.exports=n;var o=e("../math/Vec3"),d=e("./Pool");n.prototype=new d,n.prototype.constructObject=function(){return new o}},{"../math/Vec3":30,"./Pool":51}],55:[function(e,f){function n(e){this.contactPointPool=[],this.frictionEquationPool=[],this.result=[],this.frictionResult=[],this.v3pool=new s,this.world=e,this.currentContactMaterial=null,this.enableFrictionReduction=!1}function o(e,f,n){for(var o=null,d=e.length,i=0;i!==d;i++){var t=e[i],l=M;e[(i+1)%d].vsub(t,l);var u=P;l.cross(f,u);var p=Q;n.vsub(t,p);var s=u.dot(p);if(!(null===o||s>0&&o===!0||0>=s&&o===!1))return!1;null===o&&(o=s>0)}return!0}f.exports=n;var d=e("../collision/AABB"),i=e("../shapes/Shape"),t=e("../collision/Ray"),l=e("../math/Vec3"),u=e("../math/Transform"),p=(e("../shapes/ConvexPolyhedron"),e("../math/Quaternion")),s=(e("../solver/Solver"),e("../utils/Vec3Pool")),y=e("../equations/ContactEquation"),c=e("../equations/FrictionEquation");n.prototype.createContactEquation=function(e,f,n,o,d,i){var t;this.contactPointPool.length?(t=this.contactPointPool.pop(),t.bi=e,t.bj=f):t=new y(e,f),t.enabled=e.collisionResponse&&f.collisionResponse&&n.collisionResponse&&o.collisionResponse;var l=this.currentContactMaterial;t.restitution=l.restitution,t.setSpookParams(l.contactEquationStiffness,l.contactEquationRelaxation,this.world.dt);var u=n.material||e.material,p=o.material||f.material;return u&&p&&u.restitution>=0&&p.restitution>=0&&(t.restitution=u.restitution*p.restitution),t.si=d||n,t.sj=i||o,t},n.prototype.createFrictionEquationsFromContact=function(e,f){var n=e.bi,o=e.bj,d=e.si,i=e.sj,t=this.world,l=this.currentContactMaterial,u=l.friction,p=d.material||n.material,s=i.material||o.material;if(p&&s&&p.friction>=0&&s.friction>=0&&(u=p.friction*s.friction),u>0){var y=u*t.gravity.length(),a=n.invMass+o.invMass;a>0&&(a=1/a);var r=this.frictionEquationPool,w=r.length?r.pop():new c(n,o,y*a),b=r.length?r.pop():new c(n,o,y*a);return w.bi=b.bi=n,w.bj=b.bj=o,w.minForce=b.minForce=-y*a,w.maxForce=b.maxForce=y*a,w.ri.copy(e.ri),w.rj.copy(e.rj),b.ri.copy(e.ri),b.rj.copy(e.rj),e.ni.tangents(w.t,b.t),w.setSpookParams(l.frictionEquationStiffness,l.frictionEquationRelaxation,t.dt),b.setSpookParams(l.frictionEquationStiffness,l.frictionEquationRelaxation,t.dt),w.enabled=b.enabled=e.enabled,f.push(w,b),!0}return!1};var a=new l,r=new l,w=new l;n.prototype.createFrictionFromAverage=function(e){var f=this.result[this.result.length-1];if(this.createFrictionEquationsFromContact(f,this.frictionResult)&&1!==e){var n=this.frictionResult[this.frictionResult.length-2],o=this.frictionResult[this.frictionResult.length-1];a.setZero(),r.setZero(),w.setZero();for(var d=f.bi,i=(f.bj,0);i!==e;i++)f=this.result[this.result.length-1-i],f.bodyA!==d?(a.vadd(f.ni,a),r.vadd(f.ri,r),w.vadd(f.rj,w)):(a.vsub(f.ni,a),r.vadd(f.rj,r),w.vadd(f.ri,w));var t=1/e;r.scale(t,n.ri),w.scale(t,n.rj),o.ri.copy(n.ri),o.rj.copy(n.rj),a.normalize(),a.tangents(n.t,o.t)}};var b=new l,m=new l,N=new p,g=new p;n.prototype.getContacts=function(e,f,n,o,d,i,t){this.contactPointPool=d,this.frictionEquationPool=t,this.result=o,this.frictionResult=i;for(var l=N,u=g,p=b,s=m,y=0,c=e.length;y!==c;y++){var a=e[y],r=f[y],w=null;a.material&&r.material&&(w=n.getContactMaterial(a.material,r.material)||null);for(var x=0;xj.boundingSphereRadius+A.boundingSphereRadius)){var C=null;j.material&&A.material&&(C=n.getContactMaterial(j.material,A.material)||null),this.currentContactMaterial=C||w||n.defaultContactMaterial;var O=this[j.type|A.type];O&&(j.type=w){var b=this.createContactEquation(t,p,e,f);b.ni.copy(y);var m=v;y.scale(r.dot(y),m),s.vsub(m,m),b.ri.copy(m),b.ri.vsub(t.position,b.ri),b.rj.copy(s),b.rj.vsub(p.position,b.rj),this.result.push(b),this.createFrictionEquationsFromContact(b,this.frictionResult)}}};var A=new l,C=new l,O=(new l,new l),h=new l,k=new l,q=new l,z=new l,B=new l,D=new l,E=new l,F=new l,G=new l,H=new l,I=new d,J=[];n.prototype[i.types.SPHERE|i.types.TRIMESH]=n.prototype.sphereTrimesh=function(e,f,n,o,d,i,l,p){var s=k,y=q,c=z,a=B,r=D,w=E,b=I,m=h,N=C,g=J;u.pointToLocalFrame(o,i,n,r);var x=e.radius;b.lowerBound.set(r.x-x,r.y-x,r.z-x),b.upperBound.set(r.x+x,r.y+x,r.z+x),f.getTrianglesInAABB(b,g);for(var j=O,v=e.radius*e.radius,K=0;KL;L++)if(f.getVertex(f.indices[3*g[K]+L],j),j.vsub(r,N),N.norm2()<=v){m.copy(j),u.pointToWorldFrame(o,i,m,j),j.vsub(n,N);var M=this.createContactEquation(l,p,e,f);M.ni.copy(N),M.ni.normalize(),M.ri.copy(M.ni),M.ri.scale(e.radius,M.ri),M.ri.vadd(n,M.ri),M.ri.vsub(l.position,M.ri),M.rj.copy(j),M.rj.vsub(p.position,M.rj),this.result.push(M),this.createFrictionEquationsFromContact(M,this.frictionResult)}for(var K=0;KL;L++){f.getVertex(f.indices[3*g[K]+L],s),f.getVertex(f.indices[3*g[K]+(L+1)%3],y),y.vsub(s,c),r.vsub(y,w);var P=w.dot(c);r.vsub(s,w);var Q=w.dot(c);if(Q>0&&0>P){r.vsub(s,w),a.copy(c),a.normalize(),Q=w.dot(a),a.scale(Q,w),w.vadd(s,w);var R=w.distanceTo(r);if(RC&&C>0){var O=T,h=U;O.copy(p[(x+1)%3]),h.copy(p[(x+2)%3]);var k=O.norm(),q=h.norm();O.normalize(),h.normalize();var z=R.dot(O),B=R.dot(h);if(k>z&&z>-k&&q>B&&B>-q){var D=Math.abs(C-A-s);(null===g||g>D)&&(g=D,m=z,N=B,w=A,c.copy(v),a.copy(O),r.copy(h),b++)}}}if(b){y=!0;var E=this.createContactEquation(t,l,e,f);c.mult(-s,E.ri),E.ni.copy(c),E.ni.negate(E.ni),c.mult(w,c),a.mult(m,a),c.vadd(a,c),r.mult(N,r),c.vadd(r,E.rj),E.ri.vadd(n,E.ri),E.ri.vsub(t.position,E.ri),E.rj.vadd(o,E.rj),E.rj.vsub(l.position,E.rj),this.result.push(E),this.createFrictionEquationsFromContact(E,this.frictionResult)}for(var F=u.get(),G=W,H=0;2!==H&&!y;H++)for(var I=0;2!==I&&!y;I++)for(var J=0;2!==J&&!y;J++)if(F.set(0,0,0),H?F.vadd(p[0],F):F.vsub(p[0],F),I?F.vadd(p[1],F):F.vsub(p[1],F),J?F.vadd(p[2],F):F.vsub(p[2],F),o.vadd(F,G),G.vsub(n,G),G.norm2()_){y=!0;var ef=this.createContactEquation(t,l,e,f);L.vadd(M,ef.rj),ef.rj.copy(ef.rj),D.negate(ef.ni),ef.ni.normalize(),ef.ri.copy(ef.rj),ef.ri.vadd(o,ef.ri),ef.ri.vsub(n,ef.ri),ef.ri.normalize(),ef.ri.mult(s,ef.ri),ef.ri.vadd(n,ef.ri),ef.ri.vsub(t.position,ef.ri),ef.rj.vadd(o,ef.rj),ef.rj.vsub(l.position,ef.rj),this.result.push(ef),this.createFrictionEquationsFromContact(ef,this.frictionResult)}}u.release(K,L,E,M,D)};var $=new l,_=new l,ef=new l,ff=new l,nf=new l,of=new l,df=new l,tf=new l,lf=new l,uf=new l;n.prototype[i.types.SPHERE|i.types.CONVEXPOLYHEDRON]=n.prototype.sphereConvex=function(e,f,n,d,i,t,l,u){var p=this.v3pool;n.vsub(d,$);for(var s=f.faceNormals,y=f.faces,c=f.vertices,a=e.radius,r=0;r!==c.length;r++){var w=c[r],b=nf;t.vmult(w,b),d.vadd(b,b);var m=ff;if(b.vsub(n,m),m.norm2()k&&q.dot(A)>0){for(var z=[],B=0,D=v.length;B!==D;B++){var E=p.get();t.vmult(c[v[B]],E),d.vadd(E,E),z.push(E)}if(o(z,A,n)){g=!0;var N=this.createContactEquation(l,u,e,f);A.mult(-a,N.ri),A.negate(N.ni);var F=p.get();A.mult(-k,F);var G=p.get();A.mult(-a,G),n.vsub(d,N.rj),N.rj.vadd(G,N.rj),N.rj.vadd(F,N.rj),N.rj.vadd(d,N.rj),N.rj.vsub(u.position,N.rj),N.ri.vadd(n,N.ri),N.ri.vsub(l.position,N.ri),p.release(F),p.release(G),this.result.push(N),this.createFrictionEquationsFromContact(N,this.frictionResult);for(var B=0,H=z.length;B!==H;B++)p.release(z[B]);return}for(var B=0;B!==v.length;B++){var I=p.get(),J=p.get();t.vmult(c[v[(B+1)%v.length]],I),t.vmult(c[v[(B+2)%v.length]],J),d.vadd(I,I),d.vadd(J,J);var K=_;J.vsub(I,K);var L=ef;K.unit(L);var M=p.get(),P=p.get();n.vsub(I,P);var Q=P.dot(L);L.mult(Q,M),M.vadd(I,M);var R=p.get();if(M.vsub(n,R),Q>0&&Q*Q=a){var r=this.createContactEquation(t,l,e,f),w=cf;p.mult(p.dot(y),w),u.vsub(w,w),w.vsub(n,r.ri),r.ni.copy(p),u.vsub(o,r.rj),r.ri.vadd(n,r.ri),r.ri.vsub(t.position,r.ri),r.rj.vadd(o,r.rj),r.rj.vsub(l.position,r.rj),this.result.push(r),s++,this.enableFrictionReduction||this.createFrictionEquationsFromContact(r,this.frictionResult)}}this.enableFrictionReduction&&s&&this.createFrictionFromAverage(s)};var af=new l,rf=new l;n.prototype[i.types.CONVEXPOLYHEDRON]=n.prototype.convexConvex=function(e,f,n,o,d,i,t,l,u,p,s,y){var c=af;if(!(n.distanceTo(o)>e.boundingSphereRadius+f.boundingSphereRadius)&&e.findSeparatingAxis(f,n,d,o,i,c,s,y)){var a=[],r=rf;e.clipAgainstHull(n,d,f,o,i,c,-100,100,a);for(var w=0,b=0;b!==a.length;b++){var m=this.createContactEquation(t,l,e,f,u,p),N=m.ri,g=m.rj;c.negate(m.ni),a[b].normal.negate(r),r.mult(a[b].depth,r),a[b].point.vadd(r,N),g.copy(a[b].point),N.vsub(n,N),g.vsub(o,g),N.vadd(n,N),N.vsub(t.position,N),g.vadd(o,g),g.vsub(l.position,g),this.result.push(m),w++,this.enableFrictionReduction||this.createFrictionEquationsFromContact(m,this.frictionResult)}this.enableFrictionReduction&&w&&this.createFrictionFromAverage(w)}};var wf=new l,bf=new l,mf=new l;n.prototype[i.types.PLANE|i.types.PARTICLE]=n.prototype.planeParticle=function(e,f,n,o,d,i,t,l){var u=wf;u.set(0,0,1),t.quaternion.vmult(u,u);var p=bf;o.vsub(t.position,p);var s=u.dot(p);if(0>=s){var y=this.createContactEquation(l,t,f,e);y.ni.copy(u),y.ni.negate(y.ni),y.ri.set(0,0,0);var c=mf;u.mult(u.dot(o),c),o.vsub(c,c),y.rj.copy(c),this.result.push(y),this.createFrictionEquationsFromContact(y,this.frictionResult)}};var Nf=new l;n.prototype[i.types.PARTICLE|i.types.SPHERE]=n.prototype.sphereParticle=function(e,f,n,o,d,i,t,l){var u=Nf;u.set(0,0,1),o.vsub(n,u);var p=u.norm2();if(p<=e.radius*e.radius){var s=this.createContactEquation(l,t,f,e);u.normalize(),s.rj.copy(u),s.rj.mult(e.radius,s.rj),s.ni.copy(u),s.ni.negate(s.ni),s.ri.set(0,0,0),this.result.push(s),this.createFrictionEquationsFromContact(s,this.frictionResult)}};var gf=new p,xf=new l,jf=(new l,new l),vf=new l,Af=new l;n.prototype[i.types.PARTICLE|i.types.CONVEXPOLYHEDRON]=n.prototype.convexParticle=function(e,f,n,o,d,i,t,l){var u=-1,p=jf,s=Af,y=null,c=0,a=xf;if(a.copy(o),a.vsub(n,a),d.conjugate(gf),gf.vmult(a,a),e.pointIsInside(a)){e.worldVerticesNeedsUpdate&&e.computeWorldVertices(n,d),e.worldFaceNormalsNeedsUpdate&&e.computeWorldFaceNormals(d);for(var r=0,w=e.faces.length;r!==w;r++){var b=[e.worldVertices[e.faces[r][0]]],m=e.worldFaceNormals[r];o.vsub(b[0],vf);var N=-m.dot(vf);(null===y||Math.abs(N)b||0>N||w>p.length||m>p[0].length)){0>w&&(w=0),0>b&&(b=0),0>m&&(m=0),0>N&&(N=0),w>=p.length&&(w=p.length-1),b>=p.length&&(b=p.length-1),N>=p[0].length&&(N=p[0].length-1),m>=p[0].length&&(m=p[0].length-1);var g=[];f.getRectMinMax(w,m,b,N,g);var x=g[0],j=g[1];if(!(r.z-y>j||r.z+yv;v++)for(var A=m;N>A;A++)f.getConvexTrianglePillar(v,A,!1),u.pointToWorldFrame(o,i,f.pillarOffset,c),n.distanceTo(c)w||0>m||r>p.length||m>p[0].length)){0>r&&(r=0),0>w&&(w=0),0>b&&(b=0),0>m&&(m=0),r>=p.length&&(r=p.length-1),w>=p.length&&(w=p.length-1),m>=p[0].length&&(m=p[0].length-1),b>=p[0].length&&(b=p[0].length-1);var N=[];f.getRectMinMax(r,b,w,m,N);var g=N[0],x=N[1];if(!(a.z-s>x||a.z+sv;v++)for(var A=b;m>A;A++){var C=j.length;f.getConvexTrianglePillar(v,A,!1),u.pointToWorldFrame(o,i,f.pillarOffset,c),n.distanceTo(c)2)return}}}},{"../collision/AABB":3,"../collision/Ray":9,"../equations/ContactEquation":19,"../equations/FrictionEquation":21,"../math/Quaternion":28,"../math/Transform":29,"../math/Vec3":30,"../shapes/ConvexPolyhedron":38,"../shapes/Shape":43,"../solver/Solver":47,"../utils/Vec3Pool":54}],56:[function(e,f){function n(){u.apply(this),this.dt=-1,this.allowSleep=!1,this.contacts=[],this.frictionEquations=[],this.quatNormalizeSkip=0,this.quatNormalizeFast=!1,this.time=0,this.stepnumber=0,this.default_dt=1/60,this.nextId=0,this.gravity=new d,this.broadphase=new m,this.bodies=[],this.solver=new t,this.constraints=[],this.narrowphase=new l(this),this.collisionMatrix=new p,this.collisionMatrixPrevious=new p,this.materials=[],this.contactmaterials=[],this.contactMaterialTable=new a,this.defaultMaterial=new s("default"),this.defaultContactMaterial=new y(this.defaultMaterial,this.defaultMaterial,{friction:.3,restitution:0}),this.doProfiling=!1,this.profile={solve:0,makeContactConstraints:0,broadphase:0,integrate:0,narrowphase:0},this.subsystems=[],this.addBodyEvent={type:"addBody",body:null},this.removeBodyEvent={type:"removeBody",body:null}}f.exports=n;var o=e("../shapes/Shape"),d=e("../math/Vec3"),i=e("../math/Quaternion"),t=e("../solver/GSSolver"),l=(e("../utils/Vec3Pool"),e("../equations/ContactEquation"),e("../equations/FrictionEquation"),e("./Narrowphase")),u=e("../utils/EventTarget"),p=e("../collision/ArrayCollisionMatrix"),s=e("../material/Material"),y=e("../material/ContactMaterial"),c=e("../objects/Body"),a=e("../utils/TupleDictionary"),r=e("../collision/RaycastResult"),w=e("../collision/AABB"),b=e("../collision/Ray"),m=e("../collision/NaiveBroadphase");n.prototype=new u;var N=(new w,new b);if(n.prototype.getContactMaterial=function(e,f){return this.contactMaterialTable.get(e.id,f.id)},n.prototype.numObjects=function(){return this.bodies.length},n.prototype.collisionMatrixTick=function(){var e=this.collisionMatrixPrevious;this.collisionMatrixPrevious=this.collisionMatrix,this.collisionMatrix=e,this.collisionMatrix.reset()},n.prototype.add=n.prototype.addBody=function(e){-1===this.bodies.indexOf(e)&&(e.index=this.bodies.length,this.bodies.push(e),e.world=this,e.initPosition.copy(e.position),e.initVelocity.copy(e.velocity),e.timeLastSleepy=this.time,e instanceof c&&(e.initAngularVelocity.copy(e.angularVelocity),e.initQuaternion.copy(e.quaternion)),this.collisionMatrix.setNumObjects(this.bodies.length),this.addBodyEvent.body=e,this.dispatchEvent(this.addBodyEvent))},n.prototype.addConstraint=function(e){this.constraints.push(e)},n.prototype.removeConstraint=function(e){var f=this.constraints.indexOf(e);-1!==f&&this.constraints.splice(f,1)},n.prototype.rayTest=function(e,f,n){n instanceof r?this.raycastClosest(e,f,{skipBackfaces:!0},n):this.raycastAll(e,f,{skipBackfaces:!0},n)},n.prototype.raycastAll=function(e,f,n,o){return n.mode=b.ALL,n.from=e,n.to=f,n.callback=o,N.intersectWorld(this,n)},n.prototype.raycastAny=function(e,f,n,o){return n.mode=b.ANY,n.from=e,n.to=f,n.result=o,N.intersectWorld(this,n)},n.prototype.raycastClosest=function(e,f,n,o){return n.mode=b.CLOSEST,n.from=e,n.to=f,n.result=o,N.intersectWorld(this,n)},n.prototype.remove=function(e){e.world=null;var f=this.bodies.length-1,n=this.bodies,o=n.indexOf(e);if(-1!==o){n.splice(o,1);for(var d=0;d!==n.length;d++)n[d].index=d;this.collisionMatrix.setNumObjects(f),this.removeBodyEvent.body=e,this.dispatchEvent(this.removeBodyEvent)}},n.prototype.removeBody=n.prototype.remove,n.prototype.addMaterial=function(e){this.materials.push(e)},n.prototype.addContactMaterial=function(e){this.contactmaterials.push(e),this.contactMaterialTable.set(e.materials[0].id,e.materials[1].id,e)},"undefined"==typeof performance&&(performance={}),!performance.now){var g=Date.now();performance.timing&&performance.timing.navigationStart&&(g=performance.timing.navigationStart),performance.now=function(){return Date.now()-g}}var x=new d;n.prototype.step=function(e,f,n){if(n=n||10,f=f||0,0===f)this.internalStep(e),this.time+=e;else{var o=Math.floor((this.time+f)/e)-Math.floor(this.time/e);o=Math.min(o,n);for(var d=performance.now(),i=0;i!==o&&(this.internalStep(e),!(performance.now()-d>1e3*e));i++);this.time+=f;for(var t=this.time%e,l=t/e,u=x,p=this.bodies,s=0;s!==p.length;s++){var y=p[s];y.type!==c.STATIC&&y.sleepState!==c.SLEEPING?(y.position.vsub(y.previousPosition,u),u.scale(l,u),y.position.vadd(u,y.interpolatedPosition)):(y.interpolatedPosition.copy(y.position),y.interpolatedQuaternion.copy(y.quaternion))}}};var j={type:"postStep"},v={type:"preStep"},A={type:"collide",body:null,contact:null},C=[],O=[],h=[],k=[],q=(new d,new d,new d,new d,new d,new d,new d,new d,new d,new i,new i),z=new i,B=new d;n.prototype.internalStep=function(e){this.dt=e;var f,n=this.contacts,d=h,i=k,t=this.numObjects(),l=this.bodies,u=this.solver,p=this.gravity,s=this.doProfiling,y=this.profile,a=c.DYNAMIC,r=this.constraints,w=O,b=(p.norm(),p.x),m=p.y,N=p.z,g=0;for(s&&(f=performance.now()),g=0;g!==t;g++){var x=l[g];if(x.type&a){var D=x.force,E=x.mass;D.x+=E*b,D.y+=E*m,D.z+=E*N}}for(var g=0,F=this.subsystems.length;g!==F;g++)this.subsystems[g].update();s&&(f=performance.now()),d.length=0,i.length=0,this.broadphase.collisionPairs(this,d,i),s&&(y.broadphase=performance.now()-f);var G=r.length;for(g=0;g!==G;g++){var H=r[g];if(!H.collideConnected)for(var I=d.length-1;I>=0;I-=1)(H.bodyA===d[I]&&H.bodyB===i[I]||H.bodyB===d[I]&&H.bodyA===i[I])&&(d.splice(I,1),i.splice(I,1))}this.collisionMatrixTick(),s&&(f=performance.now());var J=C,K=n.length;for(g=0;g!==K;g++)J.push(n[g]);n.length=0;var L=this.frictionEquations.length;for(g=0;g!==L;g++)w.push(this.frictionEquations[g]);this.frictionEquations.length=0,this.narrowphase.getContacts(d,i,this,n,J,this.frictionEquations,w),s&&(y.narrowphase=performance.now()-f),s&&(f=performance.now());for(var g=0;g=0&&R.material.friction>=0&&(S=x.material.friction*R.material.friction),x.material.restitution>=0&&R.material.restitution>=0&&(H.restitution=x.material.restitution*R.material.restitution)),u.addEquation(H),x.allowSleep&&x.type===c.DYNAMIC&&x.sleepState===c.SLEEPING&&R.sleepState===c.AWAKE&&R.type!==c.STATIC){var T=R.velocity.norm2()+R.angularVelocity.norm2(),U=Math.pow(R.sleepSpeedLimit,2); +T>=2*U&&(x._wakeUpAfterNarrowphase=!0)}if(R.allowSleep&&R.type===c.DYNAMIC&&R.sleepState===c.SLEEPING&&x.sleepState===c.AWAKE&&x.type!==c.STATIC){var V=x.velocity.norm2()+x.angularVelocity.norm2(),W=Math.pow(x.sleepSpeedLimit,2);V>=2*W&&(R._wakeUpAfterNarrowphase=!0)}this.collisionMatrix.set(x,R,!0),this.collisionMatrixPrevious.get(x,R)||(A.body=R,A.contact=H,x.dispatchEvent(A),A.body=x,R.dispatchEvent(A))}for(s&&(y.makeContactConstraints=performance.now()-f,f=performance.now()),g=0;g!==t;g++){var x=l[g];x._wakeUpAfterNarrowphase&&(x.wakeUp(),x._wakeUpAfterNarrowphase=!1)}var G=r.length;for(g=0;g!==G;g++){var H=r[g];H.update();for(var I=0,X=H.equations.length;I!==X;I++){var Y=H.equations[I];u.addEquation(Y)}}u.solve(e,this),s&&(y.solve=performance.now()-f),u.removeAllEquations();var Z=Math.pow;for(g=0;g!==t;g++){var x=l[g];if(x.type&a){var $=Z(1-x.linearDamping,e),_=x.velocity;_.mult($,_);var ef=x.angularVelocity;if(ef){var ff=Z(1-x.angularDamping,e);ef.mult(ff,ef)}}}for(this.dispatchEvent(v),g=0;g!==t;g++){var x=l[g];x.preStep&&x.preStep.call(x)}s&&(f=performance.now());{var nf=q,of=z,df=this.stepnumber,tf=c.DYNAMIC|c.KINEMATIC,lf=df%(this.quatNormalizeSkip+1)===0,uf=this.quatNormalizeFast,pf=.5*e;o.types.PLANE,o.types.CONVEXPOLYHEDRON}for(g=0;g!==t;g++){var sf=l[g],yf=sf.force,cf=sf.torque;if(sf.type&tf&&sf.sleepState!==c.SLEEPING){var af=sf.velocity,rf=sf.angularVelocity,wf=sf.position,bf=sf.quaternion,mf=sf.invMass,Nf=sf.invInertiaWorld;af.x+=yf.x*mf*e,af.y+=yf.y*mf*e,af.z+=yf.z*mf*e,sf.angularVelocity&&(Nf.vmult(cf,B),B.mult(e,B),B.vadd(rf,rf)),wf.x+=af.x*e,wf.y+=af.y*e,wf.z+=af.z*e,sf.angularVelocity&&(nf.set(rf.x,rf.y,rf.z,0),nf.mult(bf,of),bf.x+=pf*of.x,bf.y+=pf*of.y,bf.z+=pf*of.z,bf.w+=pf*of.w,lf&&(uf?bf.normalizeFast():bf.normalize())),sf.aabb&&(sf.aabbNeedsUpdate=!0),sf.updateInertiaWorld&&sf.updateInertiaWorld()}}for(this.clearForces(),this.broadphase.dirty=!0,s&&(y.integrate=performance.now()-f),this.time+=e,this.stepnumber+=1,this.dispatchEvent(j),g=0;g!==t;g++){var x=l[g],gf=x.postStep;gf&&gf.call(x)}if(this.allowSleep)for(g=0;g!==t;g++)l[g].sleepTick(this.time)},n.prototype.clearForces=function(){for(var e=this.bodies,f=e.length,n=0;n!==f;n++){{var o=e[n];o.force,o.torque}o.force.set(0,0,0),o.torque.set(0,0,0)}}},{"../collision/AABB":3,"../collision/ArrayCollisionMatrix":4,"../collision/NaiveBroadphase":7,"../collision/Ray":9,"../collision/RaycastResult":10,"../equations/ContactEquation":19,"../equations/FrictionEquation":21,"../material/ContactMaterial":24,"../material/Material":25,"../math/Quaternion":28,"../math/Vec3":30,"../objects/Body":31,"../shapes/Shape":43,"../solver/GSSolver":46,"../utils/EventTarget":49,"../utils/TupleDictionary":52,"../utils/Vec3Pool":54,"./Narrowphase":55}]},{},[2])(2)}); \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/fr_3d/icon.png b/local-scratch-vm/src/extensions/fr_3d/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c63cdd0af8518895ce50d77231a991a99a05b6f1 Binary files /dev/null and b/local-scratch-vm/src/extensions/fr_3d/icon.png differ diff --git a/local-scratch-vm/src/extensions/fr_3d/index.js b/local-scratch-vm/src/extensions/fr_3d/index.js new file mode 100644 index 0000000000000000000000000000000000000000..caaecbc2e9c4fa4cc36d93f80c017c53c7aa80e2 --- /dev/null +++ b/local-scratch-vm/src/extensions/fr_3d/index.js @@ -0,0 +1,144 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const CANNON = require('./cannon.min.js'); +const Icon = require('./icon.png'); + + + +/** + * Class for 3d Physics blocks + */ +class Fr3DBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + */ + this.runtime = runtime; + this.CANNON = CANNON + // Create the Cannon.js world before initializing other properties + this.world = new this.CANNON.World(); + + this._3d = {}; + this.Three = {}; + + if (!vm.runtime.ext_jg3d) { + vm.extensionManager.loadExtensionURL('jg3d') + .then(() => { + this._3d = vm.runtime.ext_jg3d; + this.Three = this._3d.three; + }) + } else { + this._3d = vm.runtime.ext_jg3d; + this.Three = this._3d.three; + } + } + /** + * metadata for this extension and its blocks. + * @returns {object} + */ + getInfo() { + return { + id: 'fr3d', + name: '3D Physics', + color1: '#D066FE', + color2: '#8000BC', + blockIconURI: Icon, + blocks: [ + { + opcode: 'step', + text: 'step simulation', + blockType: BlockType.COMMAND, + }, + { + opcode: 'addp', + text: 'enable physics for [NAME1]', + blockType: BlockType.COMMAND, + arguments: { + NAME1: { type: ArgumentType.STRING, defaultValue: "Object1" } + } + }, + { + opcode: 'rmp', + text: 'disable physics for [NAME1]', + blockType: BlockType.COMMAND, + arguments: { + NAME1: { type: ArgumentType.STRING, defaultValue: "Object1" } + } + } + ] + }; + } + createShapeFromGeometry(geometry) { + if (geometry instanceof this.Three.BufferGeometry) { + const vertices = geometry.attributes.position.array; + const indices = []; + + for (let i = 0; i < vertices.length / 3; i++) { + indices.push(i); + } + + return new this.CANNON.Trimesh(vertices, indices); + } else if (geometry instanceof this.Three.Geometry) { + return new this.CANNON.ConvexPolyhedron( + geometry.vertices.map((v) => new this.CANNON.Vec3(v.x, v.y, v.z)), + geometry.faces.map((f) => [f.a, f.b, f.c]), + ); + } else { + console.warn('Unsupported geometry type for collision shape creation:', geometry.type); + return null; + } + } + + enablePhysicsForObject(objectName) { + if (!this._3d.scene) return; + const object = this._3d.scene.getObjectByName(objectName); + if (!object || !this._3d.scene) return; + + const shape = this.createShapeFromGeometry(object.geometry); + + if (!shape) { + console.warn('Failed to create a valid shape for the object:', object.name); + return; + } + + const body = new this.CANNON.Body({ + mass: 1, // You might want to adjust mass based on object size/type + }); + + body.addShape(shape); + this.world.addBody(body); // Add the body to the Cannon.js world + + object.userData.physicsBody = body; + } + + disablePhysicsForObject(objectName) { + const object = this._3d.scene.getObjectByName(objectName); + if (!object || !object.userData || !object.userData.physicsBody) return; + + this.world.removeBody(object.userData.physicsBody); // Remove from world + delete object.userData.physicsBody; + } + step() { + // Step the Cannon.js world to simulate physics + this.world.step(1/60); // Update at 60 fps (adjust timestep as needed) + + // Update Three.js object positions and rotations from physics bodies + this._3d.scene.traverse((object) => { + if (object.userData && object.userData.physicsBody) { + object.position.copy(object.userData.physicsBody.position); + object.quaternion.copy(object.userData.physicsBody.quaternion); + } + }); + } + addp(args) { + this.enablePhysicsForObject(Cast.toString(args.NAME1)) + } + + rmp(args) { + this.disablePhysicsForObject(Cast.toString(args.NAME1)) + } +} + +module.exports = Fr3DBlocks; diff --git a/local-scratch-vm/src/extensions/gameutils/index.js b/local-scratch-vm/src/extensions/gameutils/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/local-scratch-vm/src/extensions/gsa_canvas/canvasData.js b/local-scratch-vm/src/extensions/gsa_canvas/canvasData.js new file mode 100644 index 0000000000000000000000000000000000000000..cbf0236e8542eee9f7b7cd4f60b6c4142973a60d --- /dev/null +++ b/local-scratch-vm/src/extensions/gsa_canvas/canvasData.js @@ -0,0 +1,181 @@ +const xmlEscape = require('../../util/xml-escape'); +const uid = require('../../util/uid'); +const StageLayering = require('../../engine/stage-layering'); + +class CanvasVar { + static customId = 'canvasData' + + /** + * initiats the variable + * @param {Runtime} runtime the runtime this canvas exists inside + * @param {string} id this canvas's id + * @param {string} name the name of this canvas + * @param {[number,number]|string|Image} [img=[1, 1]] optionally the image to be loaded into this canvas + */ + constructor (runtime, id, name, img = [1, 1]) { + this.id = id ?? uid(); + this.name = name; + this.type = 'canvas'; + this.customId = CanvasVar.customId; + this.runtime = runtime; + this.renderer = runtime.renderer; + this.canvas = document.createElement('canvas'); + this._costumeDrawer = this.renderer.createDrawable(StageLayering.SPRITE_LAYER); + this._skinId = this.renderer.createBitmapSkin(this.canvas, 1); + this._monitorUpToDate = false; + this._cachedMonContent = [null, 0]; + this._cameraStuff = { + x: 0, + y: 0, + rotation: 0, + scaleX: 1, + scaleY: 1 + }; + // img is just a size to be given to the canvas + if (Array.isArray(img)) { + this.size = img; + return; + } + if (img) this.loadImage(img); + } + + serialize(canvas) { + const instance = canvas ?? this; + return [instance.id, instance.name, instance.canvas.toDataURL()]; + } + getSnapshot() { + const snap = new Image(); + snap.src = this.canvas.toDataURL(); + return snap; + } + toReporterContent() { + return this.canvas; + } + toMonitorContent() { + if (!this._monitorUpToDate) { + this._cachedMonContent = this.getSnapshot(); + this._monitorUpToDate = true; + } + + return this._cachedMonContent; + } + toListEditor() { + return this.toString(); + } + fromListEditor(edit) { + if (this.toString() !== edit) { + this.loadImage(edit); + } + return this; + } + toString() { + return this.canvas.toDataURL(); + } + toXML(isLocal) { + return `${xmlEscape(this.name)}`; + } + toToolboxDefault(fieldName) { + return `${xmlEscape(this.name)}`; + } + + get size() { + return [this.canvas.width, this.canvas.height]; + } + set size(size) { + this.canvas.width = size[0]; + this.canvas.height = size[1]; + } + + /** + * load an image onto the 2d canvas + * @param {Image} img the image to load onto the 2d canvas + */ + async loadImage(img) { + // we where not given something we can use imediatly :( + if (img instanceof Image && !img.complete) { + await new Promise(resolve => { + img.onload = resolve; + img.onerror = resolve; + }); + } + if (typeof img === 'string') { + await new Promise(resolve => { + const src = img; + img = new Image(); + img.onload = resolve; + img.onerror = resolve; + img.src = src; + }); + } + + this.canvas.width = img.width; + this.canvas.height = img.height; + const ctx = this.canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + // do this cause we just added new content + this.updateCanvasContentRenders(); + } + + stampDrawable(id, x, y) { + // drawable doesnt exist, we will get an error if we try to access this drawable + if (!this.renderer._allDrawables[id]) return; + const drawable = this.renderer.extractDrawableScreenSpace(id); + // never got any data, ignore request + if (!drawable) return; + const ctx = this.canvas.getContext('2d'); + ctx.putImageData(drawable.imageData, x, y); + } + + stampCostume(target, costumeName, x, y) { + const skin = costumeName !== '__current__' + ? (() => { + const costumeIdx = target.getCostumeIndexByName(costumeName); + const costumeList = target.getCostumes(); + const costume = costumeList[costumeIdx]; + + return this.renderer._allSkins[costume.skinId]; + })() + : this.renderer._allDrawables[target.drawableID].skin; + const ctx = this.canvas.getContext('2d'); + + // draw svg skins loaded image element + if (skin._svgImage) { + ctx.drawImage(skin._svgImage, x, y); + return; + } + // draw the generated content of TextCostumeSkin or TextBubbleSkin directly + if (skin._canvas) { + ctx.drawImage(skin._canvas, x, y); + return; + } + // shit, alright we cant just goofy ahh our way through this + // we need to somehow request some form of image that we can just draw to the canvas + // from either the webgl texture that the skin gives us or the sprite + /** + * TODO: please if someone could make this not shitty ass and make it just draw a + * fucking webgl texture directly that would be amazing + */ + this.renderer.updateDrawableSkinId(this._costumeDrawer, skin.id); + this.stampDrawable(this._costumeDrawer); + } + + updateCanvasContentRenders() { + this._monitorUpToDate = false; + // if width or height are smaller then one, replace them with one + const width = Math.max(this.canvas.width, 1); + const height = Math.max(this.canvas.height, 1); + const ctx = this.canvas.getContext('2d'); + + const printSkin = this.renderer._allSkins[this._skinId]; + const imageData = ctx.getImageData(0, 0, width, height); + printSkin._setTexture(imageData); + } + + applyCanvasToTarget(target) { + this.renderer.updateDrawableSkinId(target.drawableID, this._skinId); + this.runtime.requestRedraw(); + } +} + +module.exports = CanvasVar; diff --git a/local-scratch-vm/src/extensions/gsa_canvas/index.js b/local-scratch-vm/src/extensions/gsa_canvas/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8af4d2ceaa2c383302b97752dc6680ef96a11cd2 --- /dev/null +++ b/local-scratch-vm/src/extensions/gsa_canvas/index.js @@ -0,0 +1,2281 @@ +/* eslint-disable no-multi-spaces */ +/* eslint-disable no-invalid-this */ +/* eslint-disable no-undef */ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const MathUtil = require('../../util/math-util'); +const CanvasVar = require('./canvasData'); +const uid = require('../../util/uid'); + +const sanitize = string => { + if (typeof string !== 'string') { + log.warn(`sanitize got unexpected type: ${typeof string}`); + string = '' + string; + } + return JSON.stringify(string).slice(1, -1); +}; +const DefaultDrawImage = 'https://studio.penguinmod.com/favicon.ico'; +const canvasPropInfos = [ + ['compositing method', 'globalCompositeOperation', [ + ['source over', 'source-over'], + ['source in', 'source-in'], + ['source out', 'source-out'], + ['source atop', 'source-atop'], + ['destination over', 'destination-over'], + ['destination in', 'destination-in'], + ['destination out', 'destination-out'], + ['destination atop', 'destination-atop'], + ['lighter', 'lighter'], + ['copy', 'copy'], + ['xor', 'xor'], + ['multiply', 'multiply'], + ['screen', 'screen'], + ['overlay', 'overlay'], + ['darken', 'darken'], + ['lighten', 'lighten'], + ['color dodge', 'color-dodge'], + ['color burn', 'color-burn'], + ['hard light', 'hard-light'], + ['soft light', 'soft-light'], + ['difference', 'difference'], + ['exclusion', 'exclusion'], + ['hue', 'hue'], + ['saturation', 'saturation'], + ['color', 'color'], + ['luminosity', 'luminosity'] + ], 'source-over'], + ['CSS filter', 'filter', ArgumentType.STRING, 'none'], + ['font', 'font', ArgumentType.STRING, ''], + ['font kerning method', 'fontKerning', [ + ['browser defined', 'auto'], + ['font defined', 'normal'], + ['none', 'none'] + ], 'normal'], + ['font stretch', 'fontStretch', [ + ['ultra condensed', 'ultra-condensed'], + ['extra condensed', 'extra-condensed'], + ['condensed', 'condensed'], + ['normal', 'normal'], + ['semi expanded', 'semi-expanded'], + ['expanded', 'expanded'], + ['extra expanded', 'extra-expanded'], + ['ultra expanded', 'ultra-expanded'] + ], 'normal'], + ['font case sizing', 'fontVariantCaps', [ + ['normal', 'normal'], + ['uni-case', 'unicase'], + ['titling-case', 'titling-caps'], + ['smaller uppercase', 'small-caps'], + ['smaller cased characters', 'all-small-caps'], + ['petite uppercase', 'petite-caps'], + ['petite cased characters', 'all-petite-caps'] + ], 'normal'], + ['transparency', 'globalAlpha', ArgumentType.NUMBER, '0'], + ['image smoothing', 'imageSmoothingEnabled', ArgumentType.BOOLEAN, ''], + ['image smoothing quality', 'imageSmoothingQuality', [ + ['low', 'low'], + ['medium', 'medium'], + ['high', 'high'] + ], 'low'], + ['letter spacing', 'letterSpacing', ArgumentType.NUMBER, '0'], + ['line cap shape', 'lineCap', [ + ['sharp', 'butt'], + ['round', 'round'], + ['square', 'square'] + ], 'butt'], + ['line dash offset', 'lineDashOffset', ArgumentType.NUMBER, '0'], + ['line join shape', 'lineJoin', [ + ['round', 'round'], + ['beveled', 'bevel'], + ['sharp', 'miter'] + ], 'miter'], + ['line size', 'lineWidth', ArgumentType.NUMBER, '1'], + ['sharp line join limit', 'miterLimit', ArgumentType.NUMBER, '10'], + ['shadow blur', 'shadowBlur', ArgumentType.NUMBER, '0'], + ['shadow color', 'shadowColor', ArgumentType.COLOR, null], + ['shadow X offset', 'shadowOffsetX', ArgumentType.NUMBER, '0'], + ['shadow Y offset', 'shadowOffsetY', ArgumentType.NUMBER, '0'], + ['line color', 'strokeStyle', ArgumentType.COLOR, null], + ['text horizontal alignment', 'textAlign', [ + ['start', 'start'], + ['left', 'left'], + ['center', 'center'], + ['right', 'right'], + ['end', 'end'] + ], 'start'], + ['text vertical alignment', 'textBaseline', [ + ['top', 'top'], + ['hanging', 'hanging'], + ['middle', 'middle'], + ['alphabetic', 'alphabetic'], + ['ideographic', 'ideographic'], + ['bottom', 'bottom'] + ], 'alphabetic'], + ['text rendering optimisation', 'textRendering', [ + ['auto', 'auto'], + ['render speed', 'optimizeSpeed'], + ['legibility', 'optimizeLegibility'], + ['geometric precision', 'geometricPrecision'] + ], 'auto'], + ['word spacing', 'wordSpacing', ArgumentType.NUMBER, '0'] +]; + +/** + * Class + * @constructor + */ +class canvas { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + this.lastVars = []; + this.preloadedImages = {}; + this.propList = []; + this.sbInfo = {}; + for (const item of canvasPropInfos) { + this.propList.push(item.slice(0, 2)); + const info = { + isDummy: false, + default: item[3], + type: item[2] + }; + switch (item[2]) { + case ArgumentType.STRING: + info.shadow = 'text'; + break; + case ArgumentType.NUMBER: + info.shadow = 'math_number'; + break; + case ArgumentType.BOOLEAN: + info.check = 'Boolean'; + break; + case ArgumentType.COLOR: + info.shadow = 'colour_picker'; + break; + default: + info.isDummy = true; + info.options = item[2]; + } + this.sbInfo[item[1]] = info; + } + this.runtime.registerVariable('canvas', CanvasVar); + this.runtime.registerSerializer( + CanvasVar.customId, + canvas => canvas.id, + (varId, target) => { + let variable = target.variables[varId]; + if (!variable) { + for (const target of this.runtime.targets) { + if (target.variables[varId]) { + variable = target.variables[varId]; + break; + } + } + } + return variable; + } + ); + this.runtime.registerCompiledExtensionBlocks('newCanvas', this.getCompileInfo()); + + const updateVariables = type => { + if (type === 'canvas') { + this.runtime.vm.emitWorkspaceUpdate(); + } + }; + this.runtime.on('variableCreate', updateVariables); + this.runtime.on('variableChange', updateVariables); + this.runtime.on('variableDelete', updateVariables); + let infoObj = {}; + Object.defineProperty(ScratchBlocks.Blocks, 'newCanvas_setProperty', { + set: block => { + this._implementSBInfo(block); + infoObj = block; + }, + get: () => infoObj + }); + } + + orderCategoryBlocks(blocks) { + const button = blocks[0]; + const varBlock = blocks[1]; + const variables = [button]; + delete blocks[0]; + delete blocks[1]; + + const stage = this.runtime.getTargetForStage(); + const target = this.runtime.vm.editingTarget; + const stageVars = Object.values(stage.variables) + .filter(variable => variable.type === 'canvas') + .map(variable => variable.toToolboxDefault('canvas')) + .map(xml => varBlock.replace('>', `>${xml}`)); + const privateVars = Object.values(target.variables) + .filter(variable => variable.type === 'canvas') + .map(variable => variable.toToolboxDefault('canvas')) + .map(xml => varBlock.replace('>', `>${xml}`)); + + if (stageVars.length) { + variables.push(``); + variables.push(...stageVars); + } + if (privateVars.length && target.id !== stage.id) { + variables.push(``); + variables.push(...privateVars); + } + if (stageVars.length || privateVars.length) { + variables.push(...blocks); + } + + return variables; + } + + _implementSBInfo(block) { + const info = this.sbInfo; + block.renderInput = function(item) { + if (!item) item = this.getFieldValue('prop'); + const existingInput = this.getInput('value'); + const isInputCurrentlyUsed = existingInput.type !== ScratchBlocks.DUMMY_INPUT + && !existingInput.connection.targetBlock()?.isShadow?.(); + const target = info[item]; + if (this.lastItem === item || (isInputCurrentlyUsed && !target.isDummy)) return; + this.removeInput('value'); + if (target.isDummy) { + const inp = this.appendDummyInput('value'); + const field = new ScratchBlocks.FieldDropdown(target.options); + inp.appendField(field, 'value'); + field.setValue(target.default); + return; + } + + const inp = this.appendValueInput('value'); + inp.setCheck(target.check); + if (target.shadow && !this.isInsertionMarker()) { + const shadow = this.workspace.newBlock(target.shadow); + shadow.setShadow(true); + shadow.initSvg(); + shadow.inputList[0].fieldRow[0].setValue(target.default); + inp.connection.connect(shadow.outputConnection); + shadow.render(false); + } + }; + const oldInit = block.init; + block.init = function() { + oldInit.apply(this); + this.appendDummyInput('value'); + const dropdownField = this.getField('prop'); + dropdownField.setValidator(item => { + this.renderInput(item); + return item; + }); + this.renderInput(); + }; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'newCanvas', + name: 'html canvas', + color1: '#0069c2', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks.bind(this), + blocks: [ + { + opcode: 'createNewCanvas', + blockType: BlockType.BUTTON, + text: 'create new canvas' + }, + { + opcode: 'canvasGetter', + blockType: BlockType.REPORTER, + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + }, + text: '[canvas]' + }, + { + blockType: BlockType.LABEL, + text: "stylizing" + }, + { + opcode: 'setSize', + text: 'set width: [width] height: [height] of [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setProperty', + text: 'set [prop] of [canvas] to ', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + prop: { + type: ArgumentType.STRING, + menu: 'canvasProps' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'getProperty', + text: 'get [prop] of [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + prop: { + type: ArgumentType.STRING, + menu: 'canvasProps' + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'dash', + blockType: BlockType.COMMAND, + text: 'set line dash to [dashing] in [canvas]', + arguments: { + dashing: { + type: ArgumentType.STRING, + defaultValue: '[10, 10]' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + blockType: BlockType.LABEL, + text: "direct drawing" + }, + { + opcode: 'clearCanvas', + text: 'clear canvas [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'clearAria', + text: 'clear area at x: [x] y: [y] with width: [width] height: [height] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'drawText', + text: 'draw text [text] at [x] [y] onto [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + text: { + type: ArgumentType.STRING, + defaultValue: 'photos printed' + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'drawTextWithCap', + text: 'draw text [text] at [x] [y] with size cap [cap] onto [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + text: { + type: ArgumentType.STRING, + defaultValue: 'photos printed' + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + cap: { + type: ArgumentType.NUMBER, + defauleValue: '10' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'outlineText', + text: 'draw text outline for [text] at [x] [y] onto [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + text: { + type: ArgumentType.STRING, + defaultValue: 'photos printed' + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'outlineTextWithCap', + text: 'draw text outline for [text] at [x] [y] with size cap [cap] onto [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + text: { + type: ArgumentType.STRING, + defaultValue: 'photos printed' + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + cap: { + type: ArgumentType.NUMBER, + defauleValue: '10' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'drawRect', + text: 'draw rectangle at x: [x] y: [y] with width: [width] height: [height] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'outlineRect', + text: 'draw rectangle outline at x: [x] y: [y] with width: [width] height: [height] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'preloadUriImage', + blockType: BlockType.COMMAND, + text: 'preload image [URI] as [NAME]', + arguments: { + URI: { + type: ArgumentType.STRING, + exemptFromNormalization: true, + defaultValue: DefaultDrawImage + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "preloaded image" + } + } + }, + { + opcode: 'unloadUriImage', + blockType: BlockType.COMMAND, + text: 'unload image [NAME]', + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "preloaded image" + } + } + }, + { + opcode: 'getWidthOfPreloaded', + blockType: BlockType.REPORTER, + text: 'get width of [name]', + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: "preloaded image" + } + } + }, + { + opcode: 'getHeightOfPreloaded', + blockType: BlockType.REPORTER, + text: 'get height of [name]', + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: "preloaded image" + } + } + }, + { + opcode: 'drawUriImage', + blockType: BlockType.COMMAND, + text: 'draw image [URI] at x:[X] y:[Y] onto canvas [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + URI: { + type: ArgumentType.STRING, + exemptFromNormalization: true, + defaultValue: DefaultDrawImage + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'drawUriImageWHR', + blockType: BlockType.COMMAND, + text: 'draw image [URI] at x:[X] y:[Y] width:[WIDTH] height:[HEIGHT] pointed at: [ROTATE] onto canvas [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + URI: { + type: ArgumentType.STRING, + exemptFromNormalization: true, + defaultValue: DefaultDrawImage + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 64 + }, + HEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 64 + }, + ROTATE: { + type: ArgumentType.ANGLE, + defaultValue: 90 + } + } + }, + { + opcode: 'drawUriImageWHCX1Y1X2Y2R', + blockType: BlockType.COMMAND, + text: 'draw image [URI] at x:[X] y:[Y] width:[WIDTH] height:[HEIGHT] cropping from x:[CROPX] y:[CROPY] width:[CROPW] height:[CROPH] pointed at: [ROTATE] onto canvas [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + }, + URI: { + type: ArgumentType.STRING, + exemptFromNormalization: true, + defaultValue: DefaultDrawImage + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 64 + }, + HEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 64 + }, + CROPX: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + CROPY: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + CROPW: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + CROPH: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + ROTATE: { + type: ArgumentType.ANGLE, + defaultValue: 90 + } + } + }, + { + blockType: BlockType.LABEL, + text: "path drawing" + }, + { + opcode: 'beginPath', + blockType: BlockType.COMMAND, + text: 'begin path drawing on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'moveTo', + blockType: BlockType.COMMAND, + text: 'move pen to x:[x] y:[y] on [canvas]', + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'lineTo', + blockType: BlockType.COMMAND, + text: 'add line going to x:[x] y:[y] on [canvas]', + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'arcTo', + blockType: BlockType.COMMAND, + text: 'add arc going to x:[x] y:[y] on [canvas] with control points [controlPoints] and radius [radius]', + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + controlPoints: { + type: ArgumentType.POLYGON, + nodes: 2 + }, + radius: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + "---", + { + opcode: 'addRect', + blockType: BlockType.COMMAND, + text: 'add a rectangle at x:[x] y:[y] with width:[width] height:[height] to [canvas]', + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: 10 + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: 10 + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'addEllipse', + blockType: BlockType.COMMAND, + text: 'add a ellipse at x:[x] y:[y] with width:[width] height:[height] pointed towards [dir] to [canvas]', + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: 10 + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: 10 + }, + dir: { + type: ArgumentType.ANGLE, + defaultValue: 90 + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'addEllipseStartStop', + blockType: BlockType.COMMAND, + text: 'add a ellipse with starting rotation [start] and ending rotation [end] at x:[x] y:[y] with width:[width] height:[height] pointed towards [dir] to [canvas]', + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: 10 + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: 10 + }, + start: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + end: { + type: ArgumentType.NUMBER, + defaultValue: '360' + }, + dir: { + type: ArgumentType.ANGLE, + defaultValue: 90 + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + "---", + { + opcode: 'closePath', + blockType: BlockType.COMMAND, + text: 'attempt to close any open path in [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'stroke', + blockType: BlockType.COMMAND, + text: 'draw outline for current path in [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'fill', + blockType: BlockType.COMMAND, + text: 'draw fill for current path in [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + blockType: BlockType.LABEL, + text: "transforms" + }, + { + opcode: 'saveTransform', + blockType: BlockType.COMMAND, + text: 'save [canvas]\'s transform', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'restoreTransform', + blockType: BlockType.COMMAND, + text: 'reset to [canvas]\'s saved transform', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + "---", + { + opcode: 'turnRotationLeft', + blockType: BlockType.COMMAND, + text: 'turn left [degrees] in [canvas]', + arguments: { + degrees: { + type: ArgumentType.NUMBER, + defaultValue: '90' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'turnRotationRight', + blockType: BlockType.COMMAND, + text: 'turn right [degrees] in [canvas]', + arguments: { + degrees: { + type: ArgumentType.NUMBER, + defaultValue: '90' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'setRotation', + blockType: BlockType.COMMAND, + text: 'set rotation to [degrees] in [canvas]', + arguments: { + degrees: { + type: ArgumentType.ANGLE, + defaultValue: '90' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + "---", + { + opcode: 'setTranslateXY', + blockType: BlockType.COMMAND, + text: 'set translation X: [x] Y: [y] on [canvas]', + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'changeTranslateXY', + blockType: BlockType.COMMAND, + text: 'change translation X: [x] Y: [y] on [canvas]', + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + "---", + { + opcode: 'changeTranslateX', + blockType: BlockType.COMMAND, + text: 'change X translation by [amount] on [canvas]', + arguments: { + amount: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'setTranslateX', + blockType: BlockType.COMMAND, + text: 'set X scaler to [amount] on [canvas]', + arguments: { + amount: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'changeTranslateY', + blockType: BlockType.COMMAND, + text: 'change Y translation by [amount] on [canvas]', + arguments: { + amount: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'setTranslateY', + blockType: BlockType.COMMAND, + text: 'set Y translation by [amount] on [canvas]', + arguments: { + amount: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + "---", + { + opcode: 'changeScaleXY', + blockType: BlockType.COMMAND, + text: 'change XY scaler by [percent]% on [canvas]', + arguments: { + percent: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'setScaleXY', + blockType: BlockType.COMMAND, + text: 'set XY scaler to [percent]% on [canvas]', + arguments: { + percent: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'changeScaleX', + blockType: BlockType.COMMAND, + text: 'change X scaler by [percent]% on [canvas]', + arguments: { + percent: { + type: ArgumentType.NUMBER, + defaultValue: '10' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'setScaleX', + blockType: BlockType.COMMAND, + text: 'set X scaler to [percent]% on [canvas]', + arguments: { + percent: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'changeScaleY', + blockType: BlockType.COMMAND, + text: 'change Y scaler by [percent]% on [canvas]', + arguments: { + percent: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'setScaleY', + blockType: BlockType.COMMAND, + text: 'set Y scaler to [percent]% on [canvas]', + arguments: { + percent: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + "---", + { + opcode: 'resetTransform', + blockType: BlockType.COMMAND, + text: 'clear transform in [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'loadTransform', + blockType: BlockType.COMMAND, + text: 'set new transform [transform] on [canvas]', + arguments: { + transform: { + type: ArgumentType.STRING, + defaultValue: '[1, 0, 0, 1, 0, 0]' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'getTransform', + blockType: BlockType.REPORTER, + text: 'get current transform in [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + blockType: BlockType.LABEL, + text: "utilizing" + }, + { + opcode: 'putOntoSprite', + blockType: BlockType.COMMAND, + text: 'set this sprites costume to [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'getDataURI', + blockType: BlockType.REPORTER, + text: 'get data URL of [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'getWidthOfCanvas', + blockType: BlockType.REPORTER, + text: 'get width of [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'getHeightOfCanvas', + blockType: BlockType.REPORTER, + text: 'get height of [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + }, + { + opcode: 'getDrawnWidthOfText', + blockType: BlockType.REPORTER, + text: 'get [dimension] of text [text] when drawn to [canvas]', + arguments: { + dimension: { + type: ArgumentType.STRING, + menu: 'textDimension' + }, + text: { + type: ArgumentType.STRING, + defaultValue: 'bogos binted' + }, + canvas: { + type: ArgumentType.STRING, + menu: 'canvas' + } + } + } + ], + menus: { + textDimension: { + items: [ + 'width', + 'height', + ['bounding box left', 'actualBoundingBoxLeft'], + ['bounding box right', 'actualBoundingBoxRight'], + ['bounding box ascent', 'actualBoundingBoxAscent'], + ['bounding box descent', 'actualBoundingBoxDescent'], + ['font bounding box ascent', 'fontBoundingBoxAscent'], + ['font bounding box descent', 'fontBoundingBoxDescent'] + // maby add the other ones but the em ones be hella spotty + ] + }, + canvas: { + variableType: 'canvas' + }, + canvasProps: { + items: this.propList + } + } + }; + } + + createNewCanvas() { + // expect the global ScratchBlocks from inside the window + ScratchBlocks.prompt('New Canvas name:', '', + (name, additionalVars, {scope}) => { + name = ScratchBlocks.Variables.validateScalarVarOrListName_(name, + ScratchBlocks.getMainWorkspace(), additionalVars, false, + 'canvas', 'A Canvas named "%1" already exists.'); + if (!name) return; + + const target = scope + ? this.runtime.getTargetForStage() + : this.runtime.vm.editingTarget; + target.createVariable(uid(), name, 'canvas'); + this.runtime.vm.emitWorkspaceUpdate(); + }, 'New Canvas', 'canvas'); + } + + /** + * This function is used for any compiled blocks in the extension if they exist. + * Data in this function is given to the IR & JS generators. + * Data must be valid otherwise errors may occur. + * @returns {object} functions that create data for compiled blocks. + */ + getCompileInfo() { + return { + ir: { + canvasGetter: (generator, block) => ({ + kind: 'input', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + setSize: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + width: generator.descendInputOfBlock(block, 'width'), + height: generator.descendInputOfBlock(block, 'height') + }), + setProperty: (generator, block) => ({ + kind: 'stack', + isField: !!block.fields.value, + prop: block.fields.prop.value, + value: block.fields?.value?.value ?? generator.descendInputOfBlock(block, 'value'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + getProperty: (generator, block) => ({ + kind: 'input', + prop: block.fields.prop.value, + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + dash: (generator, block) => ({ + kind: 'stack', + dashing: generator.descendInputOfBlock(block, 'dashing'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + clearCanvas: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + clearAria: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + width: generator.descendInputOfBlock(block, 'width'), + height: generator.descendInputOfBlock(block, 'height') + }), + drawText: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + text: generator.descendInputOfBlock(block, 'text') + }), + drawTextWithCap: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + text: generator.descendInputOfBlock(block, 'text'), + cap: generator.descendInputOfBlock(block, 'cap') + }), + outlineText: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + text: generator.descendInputOfBlock(block, 'text') + }), + outlineTextWithCap: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + text: generator.descendInputOfBlock(block, 'text'), + cap: generator.descendInputOfBlock(block, 'cap') + }), + drawRect: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + width: generator.descendInputOfBlock(block, 'width'), + height: generator.descendInputOfBlock(block, 'height') + }), + outlineRect: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + width: generator.descendInputOfBlock(block, 'width'), + height: generator.descendInputOfBlock(block, 'height') + }), + preloadUriImage: (generator, block) => ({ + kind: 'stack', + URI: generator.descendInputOfBlock(block, 'URI'), + NAME: generator.descendInputOfBlock(block, 'NAME') + }), + unloadUriImage: (generator, block) => ({ + kind: 'stack', + NAME: generator.descendInputOfBlock(block, 'NAME') + }), + getWidthOfPreloaded: (generator, block) => ({ + kind: 'input', + name: generator.descendInputOfBlock(block, 'name') + }), + getHeightOfPreloaded: (generator, block) => ({ + kind: 'input', + name: generator.descendInputOfBlock(block, 'name') + }), + drawUriImage: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + URI: generator.descendInputOfBlock(block, 'URI'), + X: generator.descendInputOfBlock(block, 'X'), + Y: generator.descendInputOfBlock(block, 'Y') + }), + drawUriImageWHR: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + URI: generator.descendInputOfBlock(block, 'URI'), + X: generator.descendInputOfBlock(block, 'X'), + Y: generator.descendInputOfBlock(block, 'Y'), + WIDTH: generator.descendInputOfBlock(block, 'WIDTH'), + HEIGHT: generator.descendInputOfBlock(block, 'HEIGHT'), + ROTATE: generator.descendInputOfBlock(block, 'ROTATE') + }), + drawUriImageWHCX1Y1X2Y2R: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas'), + URI: generator.descendInputOfBlock(block, 'URI'), + X: generator.descendInputOfBlock(block, 'X'), + Y: generator.descendInputOfBlock(block, 'Y'), + WIDTH: generator.descendInputOfBlock(block, 'WIDTH'), + HEIGHT: generator.descendInputOfBlock(block, 'HEIGHT'), + CROPX: generator.descendInputOfBlock(block, 'CROPX'), + CROPY: generator.descendInputOfBlock(block, 'CROPY'), + CROPW: generator.descendInputOfBlock(block, 'CROPW'), + CROPH: generator.descendInputOfBlock(block, 'CROPH'), + ROTATE: generator.descendInputOfBlock(block, 'ROTATE') + }), + getWidthOfCanvas: (generator, block) => ({ + kind: 'input', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + getHeightOfCanvas: (generator, block) => ({ + kind: 'input', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + beginPath: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + moveTo: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + lineTo: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + arcTo: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + controlPoints: generator.descendInputOfBlock(block, 'controlPoints'), + radius: generator.descendInputOfBlock(block, 'radius'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + addRect: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + width: generator.descendInputOfBlock(block, 'width'), + height: generator.descendInputOfBlock(block, 'height'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + addEllipse: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + width: generator.descendInputOfBlock(block, 'width'), + height: generator.descendInputOfBlock(block, 'height'), + dir: generator.descendInputOfBlock(block, 'dir'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + addEllipseStartStop: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + width: generator.descendInputOfBlock(block, 'width'), + height: generator.descendInputOfBlock(block, 'height'), + start: generator.descendInputOfBlock(block, 'start'), + end: generator.descendInputOfBlock(block, 'end'), + dir: generator.descendInputOfBlock(block, 'dir'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + stroke: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + fill: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + saveTransform: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + restoreTransform: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + turnRotationLeft: (generator, block) => ({ + kind: 'stack', + degrees: generator.descendInputOfBlock(block, 'degrees'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + turnRotationRight: (generator, block) => ({ + kind: 'stack', + degrees: generator.descendInputOfBlock(block, 'degrees'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + setRotation: (generator, block) => ({ + kind: 'stack', + degrees: generator.descendInputOfBlock(block, 'degrees'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + setTranslateXY: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + changeTranslateXY: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'x'), + y: generator.descendInputOfBlock(block, 'y'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + changeTranslateX: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'amount'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + setTranslateX: (generator, block) => ({ + kind: 'stack', + x: generator.descendInputOfBlock(block, 'amount'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + changeTranslateY: (generator, block) => ({ + kind: 'stack', + y: generator.descendInputOfBlock(block, 'amount'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + setTranslateY: (generator, block) => ({ + kind: 'stack', + y: generator.descendInputOfBlock(block, 'amount'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + changeScaleXY: (generator, block) => ({ + kind: 'stack', + scale: generator.descendInputOfBlock(block, 'percent'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + setScaleXY: (generator, block) => ({ + kind: 'stack', + scale: generator.descendInputOfBlock(block, 'percent'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + changeScaleX: (generator, block) => ({ + kind: 'stack', + scale: generator.descendInputOfBlock(block, 'percent'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + setScaleX: (generator, block) => ({ + kind: 'stack', + scale: generator.descendInputOfBlock(block, 'percent'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + changeScaleY: (generator, block) => ({ + kind: 'stack', + scale: generator.descendInputOfBlock(block, 'percent'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + setScaleY: (generator, block) => ({ + kind: 'stack', + scale: generator.descendInputOfBlock(block, 'percent'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + resetTransform: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + loadTransform: (generator, block) => ({ + kind: 'stack', + transform: generator.descendInputOfBlock(block, 'transform'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + getTransform: (generator, block) => ({ + kind: 'input', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + putOntoSprite: (generator, block) => ({ + kind: 'stack', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + getDataURI: (generator, block) => ({ + kind: 'input', + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }), + getDrawnWidthOfText: (generator, block) => ({ + kind: 'input', + prop: block.fields.dimension.value, + text: generator.descendInputOfBlock(block, 'text'), + canvas: generator.descendVariable(block, 'canvas', 'canvas') + }) + }, + js: { + canvasGetter: (node, compiler, {TypedInput, TYPE_UNKNOWN}) => + new TypedInput(compiler.referenceVariable(node.canvas), TYPE_UNKNOWN), + setSize: (node, compiler) => { + console.log(node); + const canvas = compiler.referenceVariable(node.canvas); + const width = compiler.descendInput(node.width).asNumber(); + const height = compiler.descendInput(node.height).asNumber(); + + compiler.source += `${canvas}.canvas.width = ${width};\n`; + compiler.source += `${canvas}.canvas.height = ${height};\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + setProperty: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const val = node.isField + ? node.value + : compiler.descendInput(node.value); + + compiler.source += `${ctx}.${node.prop} = `; + const target = this.sbInfo[node.prop]; + switch (target.type) { + case ArgumentType.STRING: + compiler.source += val.asString(); + break; + case ArgumentType.NUMBER: + compiler.source += val.asNumber(); + break; + case ArgumentType.BOOLEAN: + compiler.source += val.asBoolean(); + break; + case ArgumentType.COLOR: + compiler.source += val.asString(); + break; + default: + compiler.source += `"${sanitize(val)}"`; + } + compiler.source += ';\n'; + }, + getProperty: (node, compiler, {TypedInput, TYPE_NUMBER, TYPE_STRING, TYPE_BOOLEAN, TYPE_UNKNOWN}) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + let type = TYPE_UNKNOWN; + const target = this.sbInfo[node.prop]; + switch (target.type) { + case ArgumentType.STRING: + type = TYPE_STRING; + break; + case ArgumentType.NUMBER: + type = TYPE_NUMBER; + break; + case ArgumentType.BOOLEAN: + type = TYPE_BOOLEAN; + break; + case ArgumentType.COLOR: + type = TYPE_STRING; + break; + default: + type = TYPE_STRING; + } + return new TypedInput(`${ctx}.${node.prop}`, type); + }, + dash: (node, compiler, {ConstantInput}) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const arrInp = compiler.descendInput(node.dashing); + const isConstant = arrInp instanceof ConstantInput; + + compiler.source += `${ctx}.setLineDash(`; + if (!isConstant) compiler.source += `parseJSONSafe(`; + compiler.source += isConstant + ? arrInp.constantValue + : arrInp.asUnknown(); + if (!isConstant) compiler.source += ')'; + compiler.source += ');\n'; + }, + clearCanvas: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + compiler.source += `${ctx}.clearRect(0, 0, ${canvas}.canvas.width, ${canvas}.canvas.height);\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + clearAria: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const width = compiler.descendInput(node.width).asNumber(); + const height = compiler.descendInput(node.height).asNumber(); + + compiler.source += `${ctx}.clearRect(${x}, ${y}, ${width}, ${height});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + drawText: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const text = compiler.descendInput(node.text).asString(); + + compiler.source += `${ctx}.fillText(${text}, ${x}, ${y});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + drawTextWithCap: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const text = compiler.descendInput(node.text).asString(); + const cap = compiler.descendInput(node.cap).asNumber(); + + compiler.source += `${ctx}.fillText(${text}, ${x}, ${y}, ${cap});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + outlineText: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const text = compiler.descendInput(node.text).asString(); + + compiler.source += `${ctx}.strokeText(${text}, ${x}, ${y});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + outlineTextWithCap: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const text = compiler.descendInput(node.text).asString(); + const cap = compiler.descendInput(node.cap).asNumber(); + + compiler.source += `${ctx}.strokeText(${text}, ${x}, ${y}, ${cap});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + drawRect: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const width = compiler.descendInput(node.width).asNumber(); + const height = compiler.descendInput(node.height).asNumber(); + + compiler.source += `${ctx}.fillRect(${x}, ${y}, ${width}, ${height});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + outlineRect: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const width = compiler.descendInput(node.width).asNumber(); + const height = compiler.descendInput(node.height).asNumber(); + + compiler.source += `${ctx}.strokeRect(${x}, ${y}, ${width}, ${height});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + preloadUriImage: (node, compiler) => { + const allPreloaded = compiler.evaluateOnce('{}'); + const preloadName = compiler.descendInput(node.NAME).asString(); + const preloadUri = compiler.descendInput(node.URI).asUnknown(); + + compiler.source += `${allPreloaded}[${preloadName}] = yield* waitPromise(`; + compiler.source += `resolveImageURL(${preloadUri})`; + compiler.source += ');\n'; + }, + unloadUriImage: (node, compiler) => { + const allPreloaded = compiler.evaluateOnce('{}'); + const preloadName = compiler.descendInput(node.NAME).asString(); + + compiler.source += `if (${allPreloaded}[${preloadName}]) {`; + compiler.source += `${allPreloaded}[${preloadName}].remove();\n`; + compiler.source += `delete ${allPreloaded}[${preloadName}];\n`; + compiler.source += '}'; + }, + getWidthOfPreloaded: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const allPreloaded = compiler.evaluateOnce('{}'); + const preloadName = compiler.descendInput(node.name).asString(); + return new TypedInput(`${allPreloaded}[${preloadName}]?.width ?? 0`, TYPE_NUMBER); + }, + getHeightOfPreloaded: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const allPreloaded = compiler.evaluateOnce('{}'); + const preloadName = compiler.descendInput(node.name).asString(); + return new TypedInput(`${allPreloaded}[${preloadName}]?.height ?? 0`, TYPE_NUMBER); + }, + drawUriImage: (node, compiler) => { + const allPreloaded = compiler.evaluateOnce('{}'); + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const uri = compiler.descendInput(node.URI).asUnknown(); + const x = compiler.descendInput(node.X).asNumber(); + const y = compiler.descendInput(node.Y).asNumber(); + + compiler.source += `${ctx}.drawImage(`; + compiler.source += `${allPreloaded}[${uri}]`; + compiler.source += `? ${allPreloaded}[${uri}]`; + compiler.source += `: yield* waitPromise(resolveImageURL(${uri}))`; + compiler.source += `, ${x}, ${y});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + drawUriImageWHR: (node, compiler) => { + const allPreloaded = compiler.evaluateOnce('{}'); + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const uri = compiler.descendInput(node.URI).asUnknown(); + const x = compiler.descendInput(node.X).asNumber(); + const y = compiler.descendInput(node.Y).asNumber(); + const width = compiler.descendInput(node.WIDTH).asNumber(); + const height = compiler.descendInput(node.HEIGHT).asNumber(); + const dir = compiler.descendInput(node.ROTATE).asNumber(); + + compiler.source += `${ctx}.drawImage(`; + compiler.source += `${allPreloaded}[${uri}] ? `; + compiler.source += `${allPreloaded}[${uri}] : `; + compiler.source += `yield* waitPromise(resolveImageURL(${uri}))`; + compiler.source += `, ${x}, ${y}, ${width}, ${height}, ${dir});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + drawUriImageWHCX1Y1X2Y2R: (node, compiler) => { + const allPreloaded = compiler.evaluateOnce('{}'); + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const uri = compiler.descendInput(node.URI).asUnknown(); + const x = compiler.descendInput(node.X).asNumber(); + const y = compiler.descendInput(node.Y).asNumber(); + const width = compiler.descendInput(node.WIDTH).asNumber(); + const height = compiler.descendInput(node.HEIGHT).asNumber(); + const dir = compiler.descendInput(node.ROTATE).asNumber(); + const cropX = compiler.descendInput(node.CROPX).asNumber(); + const cropY = compiler.descendInput(node.CROPY).asNumber(); + const cropWidth = compiler.descendInput(node.CROPW).asNumber(); + const cropHeight = compiler.descendInput(node.CROPH).asNumber(); + + compiler.source += `${ctx}.drawImage(`; + compiler.source += `${allPreloaded}[${uri}] ? `; + compiler.source += `${allPreloaded}[${uri}] : `; + compiler.source += `yield* waitPromise(resolveImageURL(${uri}))`; + compiler.source += `, ${x}, ${y}, ${width}, ${height}, ${dir}, `; + compiler.source += `${cropX}, ${cropY}, ${cropWidth}, ${cropHeight});\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + getWidthOfCanvas: (node, compiler, {TYPE_NUMBER, TypedInput}) => { + const canvas = compiler.referenceVariable(node.canvas); + return new TypedInput(`${canvas}.canvas.width`, TYPE_NUMBER); + }, + getHeightOfCanvas: (node, compiler, {TYPE_NUMBER, TypedInput}) => { + const canvas = compiler.referenceVariable(node.canvas); + return new TypedInput(`${canvas}.canvas.height`, TYPE_NUMBER); + }, + beginPath: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + compiler.source += `${ctx}.beginPath();\n`; + }, + moveTo: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + + compiler.source += `${ctx}.moveTo(${x}, ${y});\n`; + }, + lineTo: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + + compiler.source += `${ctx}.lineTo(${x}, ${y});\n`; + }, + arcTo: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const controlPoints = compiler.descendInput(node.controlPoints).asUnknown(); + const radius = compiler.descendInput(node.radius).asNumber(); + + compiler.source += `${ctx}.arcTo(${x}, ${y}, ...${controlPoints}, ${radius});\n`; + }, + addRect: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const width = compiler.descendInput(node.width).asNumber(); + const height = compiler.descendInput(node.height).asNumber(); + + compiler.source += `${ctx}.rect(${x}, ${y}, ${width}, ${height});\n`; + }, + addEllipse: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const width = compiler.descendInput(node.width).asNumber(); + const height = compiler.descendInput(node.height).asNumber(); + const dir = compiler.descendInput(node.dir).asNumber(); + + compiler.source += `${ctx}.ellipse(${x}, ${y}, ${width}, ${height}`; + compiler.source += `, (${dir} - 90) * Math.PI / 180, 0, 2 * Math.PI);\n`; + }, + addEllipseStartStop: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + const width = compiler.descendInput(node.width).asNumber(); + const height = compiler.descendInput(node.height).asNumber(); + const dir = compiler.descendInput(node.dir).asNumber(); + const start = compiler.descendInput(node.start).asNumber(); + const end = compiler.descendInput(node.end).asNumber(); + + compiler.source += `${ctx}.ellipse(${x}, ${y}, ${width}, ${height}, `; + compiler.source += `(${dir} - 90) * Math.PI / 180, (${start} - 90) * Math.PI / 180, (${end} - 90) * Math.PI / 180);\n`; + }, + closePath: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + compiler.soource += `${ctx}.closePath()`; + }, + stroke: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + compiler.source += `${ctx}.stroke();\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + fill: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + compiler.source += `${ctx}.fill();\n`; + compiler.source += `${canvas}.updateCanvasContentRenders();\n`; + }, + saveTransform: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + compiler.source += `${ctx}.save();\n`; + }, + restoreTransform: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + compiler.source += `${ctx}.restore();\n`; + }, + turnRotationLeft: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const degrees = compiler.descendInput(node.degrees).asNumber(); + + compiler.source += `${ctx}.rotate(`; + compiler.source += `(${canvas}._cameraStuff.rotation -= ${degrees}) * Math.PI / 180`; + compiler.source += `);\n`; + }, + turnRotationRight: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const degrees = compiler.descendInput(node.degrees).asNumber(); + + compiler.source += `${ctx}.rotate(`; + compiler.source += `(${canvas}._cameraStuff.rotation += ${degrees}) * Math.PI / 180`; + compiler.source += `);\n`; + }, + setRotation: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const degrees = compiler.descendInput(node.degrees).asNumber(); + + compiler.source += `${ctx}.rotate(`; + compiler.source += `((${canvas}._cameraStuff.rotation = ${degrees}) - 90) * Math.PI / 180`; + compiler.source += `);\n`; + }, + setTranslateXY: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + + compiler.source += `${ctx}.translate(`; + compiler.source += `${canvas}._cameraStuff.x = ${x},`; + compiler.source += `${canvas}._cameraStuff.y = ${y}`; + compiler.source += `);\n`; + }, + changeTranslateXY: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + const y = compiler.descendInput(node.y).asNumber(); + + compiler.source += `${ctx}.translate(`; + compiler.source += `${canvas}._cameraStuff.x += ${x},`; + compiler.source += `${canvas}._cameraStuff.y += ${y}`; + compiler.source += `);\n`; + }, + changeTranslateX: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + + compiler.source += `${ctx}.translate(`; + compiler.source += `${canvas}._cameraStuff.x += ${x},`; + compiler.source += `${canvas}._cameraStuff.y`; + compiler.source += `);\n`; + }, + setTranslateX: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const x = compiler.descendInput(node.x).asNumber(); + + compiler.source += `${ctx}.translate(`; + compiler.source += `${canvas}._cameraStuff.x = ${x},`; + compiler.source += `${canvas}._cameraStuff.y`; + compiler.source += `);\n`; + }, + changeTranslateY: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const y = compiler.descendInput(node.y).asNumber(); + + compiler.source += `${ctx}.translate(`; + compiler.source += `${canvas}._cameraStuff.x,`; + compiler.source += `${canvas}._cameraStuff.y += ${y}`; + compiler.source += `);\n`; + }, + setTranslateY: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const y = compiler.descendInput(node.y).asNumber(); + + compiler.source += `${ctx}.translate(`; + compiler.source += `${canvas}._cameraStuff.x,`; + compiler.source += `${canvas}._cameraStuff.y = ${y}`; + compiler.source += `);\n`; + }, + changeScaleXY: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const scale = compiler.descendInput(node.scale).asNumber(); + + compiler.source += `${ctx}.scale(`; + compiler.source += `${canvas}._cameraStuff.scaleX += (${scale} / 100),`; + compiler.source += `${canvas}._cameraStuff.scaleY += (${scale} / 100)`; + compiler.source += `);\n`; + }, + setScaleXY: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const scale = compiler.descendInput(node.scale).asNumber(); + + compiler.source += `${ctx}.scale(`; + compiler.source += `${canvas}._cameraStuff.scaleX = (${scale} / 100),`; + compiler.source += `${canvas}._cameraStuff.scaleY = (${scale} / 100)`; + compiler.source += `);\n`; + }, + changeScaleX: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const scale = compiler.descendInput(node.scale).asNumber(); + + compiler.source += `${ctx}.scale(`; + compiler.source += `${canvas}._cameraStuff.scaleX += (${scale} / 100),`; + compiler.source += `${canvas}._cameraStuff.scaleY`; + compiler.source += `);\n`; + }, + setScaleX: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const scale = compiler.descendInput(node.scale).asNumber(); + + compiler.source += `${ctx}.scale(`; + compiler.source += `${canvas}._cameraStuff.scaleX = (${scale} / 100),`; + compiler.source += `${canvas}._cameraStuff.scaleY`; + compiler.source += `);\n`; + }, + changeScaleY: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const scale = compiler.descendInput(node.scale).asNumber(); + + compiler.source += `${ctx}.scale(`; + compiler.source += `${canvas}._cameraStuff.scaleX,`; + compiler.source += `${canvas}._cameraStuff.scaleY += (${scale} / 100)`; + compiler.source += `);\n`; + }, + setScaleY: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const scale = compiler.descendInput(node.scale).asNumber(); + + compiler.source += `${ctx}.scale(`; + compiler.source += `${canvas}._cameraStuff.scaleX,`; + compiler.source += `${canvas}._cameraStuff.scaleY = (${scale} / 100)`; + compiler.source += `);\n`; + }, + resetTransform: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + compiler.source += `${ctx}.resetTransform();\n`; + }, + loadTransform: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const transform = compiler.descendInput(node.transform).asString(); + + compiler.source += `${ctx}.setTransform(`; + compiler.source += `parseJSONSafe(${transform})`; + compiler.source += `);\n`; + }, + getTransform: (node, compiler, { TypedInput, TYPE_STRING }) => { + const canvas = compiler.referenceVariable(node.canvas); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + + let content = '(() => {'; + content += `const transform = ${ctx}.getTransform(); `; + content += 'return JSON.stringify(['; + content += 'transform.a, transform.b, transform.c, '; + content += 'transform.d, transform.e, transform.f'; + content += '])})()'; + + return new TypedInput(content, TYPE_STRING); + }, + putOntoSprite: (node, compiler) => { + const canvas = compiler.referenceVariable(node.canvas); + + compiler.source += `${canvas}.applyCanvasToTarget(target);\n`; + }, + getDataURI: (node, compiler, {TypedInput, TYPE_STRING}) => { + const canvas = compiler.referenceVariable(node.canvas); + return new TypedInput(`${canvas}.toString()`, TYPE_STRING); + }, + getDrawnWidthOfText: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const canvas = compiler.referenceVariable(node.canvas); + const text = compiler.descendInput(node.text).asString(); + const ctx = compiler.evaluateOnce(`${canvas}.canvas.getContext('2d')`); + const cache = compiler.evaluateOnce(`{}`); + + let code = `(text => {`; + code += `let textMeasure = ${cache}[text + ${ctx}.font]`; + code += `if (!textMeasure) {`; + code += `textMeasure = ${ctx}.measureText(text);\n`; + code += `${cache}[text + ${ctx}.font] = textMeasure;\n`; + code += '}\n'; + code += 'return textMeasure.'; + switch (node.prop) { + case 'height': + code += `actualBoundingBoxAscent + textMeasure.actualBoundingBoxDescent`; + break; + default: + code += node.prop; + } + code += `;})(${text})`; + + return new TypedInput(code, TYPE_NUMBER); + } + } + }; + } + + getOrCreateVariable(target, id, name) { + const stage = this.runtime.getTargetForStage(); + const variable = target.variables[id] ?? stage.variables[id]; + if (!variable) { + return target.createVariable(id, name); + } + return variable; + } + // display monitors + canvasGetter(args, util) { + const canvasObj = this.getOrCreateVariable(util.target, args.canvas.id, args.canvas.name); + return canvasObj; + } + getProperty(args, util) { + const canvasObj = this.getOrCreateVariable(util.target, args.canvas.id, args.canvas.name); + const ctx = canvasObj.canvas.getContext('2d'); + + return ctx[args.prop]; + } + getDataURI(args, util) { + const canvasObj = this.getOrCreateVariable(util.target, args.canvas.id, args.canvas.name); + return canvasObj.toString(); + } + getWidthOfPreloaded ({ name }) { + if (!this.preloadedImages.hasOwnProperty(name)) return 0; + return this.preloadedImages[name].width; + } + getHeightOfPreloaded ({ name }) { + if (!this.preloadedImages.hasOwnProperty(name)) return 0; + return this.preloadedImages[name].height; + } + getWidthOfCanvas({ canvas }, util) { + const canvasObj = this.getOrCreateVariable(util.target, canvas.id, canvas.name); + return canvasObj.size[0]; + } + getHeightOfCanvas({ canvas }, util) { + const canvasObj = this.getOrCreateVariable(util.target, canvas.id, canvas.name); + return canvasObj.size[1]; + } +} + +module.exports = canvas; diff --git a/local-scratch-vm/src/extensions/gsa_canvas_old/canvasStorage.js b/local-scratch-vm/src/extensions/gsa_canvas_old/canvasStorage.js new file mode 100644 index 0000000000000000000000000000000000000000..e08aec527312065d0b39fe788708dad083ee7cea --- /dev/null +++ b/local-scratch-vm/src/extensions/gsa_canvas_old/canvasStorage.js @@ -0,0 +1,87 @@ +const uid = require('../../util/uid'); + +class canvasStorage { + /** + * initiats the storage + */ + constructor () { + this.canvases = {}; + } + + attachRuntime (runtime) { + this.runtime = runtime; + } + + /** + * gets a canvas with a given id + * @param {string} id the id of the canvas to get + * @returns {Object} the canvas object with this id + */ + getCanvas (id) { + return this.canvases[id]; + } + + /** + * deletes a canvas with a given id + * @param {string} id the canvas id to delete + * @returns {Object} the deleted canvas + */ + deleteCanvas (id) { + const orignal = this.canvases[id]; + delete this.canvases[id]; + return orignal; + } + + /** + * creates a new canvas + * @param {string} name the name to give the new canvas + * @param {number} width the width of the canvas + * @param {number} height the height of the canvas + * @param {string} opt_id the id of the canvas + * @returns {Object} the new canvas object + */ + newCanvas (name, width, height, opt_id) { + width = width || this.runtime.stageWidth; + height = height || this.runtime.stageHeight; + + const id = opt_id || uid(); + const element = document.createElement('canvas'); + element.id = id; + element.width = width; + element.height = height; + + const skin = !this.runtime.renderer + ? null + : this.runtime.renderer.createBitmapSkin(element, 1); + + const data = { + name: name, + id: id, + element: element, + skinId: skin, + width: width, + height: height, + context: element.getContext('2d') + }; + this.canvases[id] = data; + return data; + } + + /** + * gets or creates a canvas with name equal to + * @param {String} name the name of the canvas + */ + getCanvasByName (name) { + return Object.values(this.canvases).find(canvas => canvas.name === name); + } + + /** + * gets all canvases + * @returns {Array} + */ + getAllCanvases () { + return Object.values(this.canvases); + } +} + +module.exports = canvasStorage; diff --git a/local-scratch-vm/src/extensions/gsa_canvas_old/index.js b/local-scratch-vm/src/extensions/gsa_canvas_old/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ac5790dfd3c129b434b931e34578a9e88d90527a --- /dev/null +++ b/local-scratch-vm/src/extensions/gsa_canvas_old/index.js @@ -0,0 +1,492 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Color = require('../../util/color'); +const cstore = require('./canvasStorage'); +const Cast = require('../../util/cast'); +const store = new cstore(); + +/** + * Class + * @constructor + */ +class canvas { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + store.attachRuntime(runtime); + } + + static get canvasStorageHeader() { + return 'canvases: '; + } + + deserialize(data) { + store.canvases = {}; + for (const canvas of data) { + store.newCanvas(canvas.name, canvas.width, canvas.height, canvas.id); + } + } + + serialize() { + return store.getAllCanvases() + .map(variable => ({ + name: variable.name, + width: variable.width, + height: variable.height, + id: variable.id + })); + } + + readAsImageElement(src) { + return new Promise((resolve, reject) => { + const image = new Image(); + image.onload = function () { + resolve(image); + image.onload = null; + image.onerror = null; + }; + image.onerror = function () { + reject(new Error('Costume load failed. Asset could not be read.')); + image.onload = null; + image.onerror = null; + }; + image.src = src; + }); + } + + orderCategoryBlocks(blocks) { + const button = blocks[0]; + const varBlock = blocks[1]; + delete blocks[0]; + delete blocks[1]; + // create the variable block xml's + const varBlocks = store.getAllCanvases().map(canvas => varBlock + .replace('{canvasId}', canvas.id)); + if (!varBlocks.length) { + return [button]; + } + // push the button to the top of the var list + varBlocks + .reverse() + .push(button); + // merge the category blocks and variable blocks into one block list + blocks = varBlocks + .reverse() + .concat(blocks); + return blocks; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'canvas', + name: 'html canvas', + color1: '#0069c2', + color2: '#0060B4', + color3: '#0060B4', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [ + { + opcode: 'createNewCanvas', + blockType: BlockType.BUTTON, + text: 'create new canvas' + }, + { + opcode: 'canvasGetter', + blockType: BlockType.REPORTER, + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: '{canvasId}' + } + }, + text: '[canvas]' + }, + { + blockType: BlockType.LABEL, + text: "config" + }, + { + opcode: 'setGlobalCompositeOperation', + text: 'set composite operation of [canvas] to [CompositeOperation]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + CompositeOperation: { + type: ArgumentType.STRING, + menu: 'CompositeOperation', + defaultValue: "" + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setSize', + text: 'set width: [width] height: [height] of [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setTransparency', + text: 'set transparency of [canvas] to [transparency]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + transparency: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setFill', + text: 'set fill color of [canvas] to [color]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + color: { + type: ArgumentType.COLOR + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setBorderColor', + text: 'set border color of [canvas] to [color]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + color: { + type: ArgumentType.COLOR + } + }, + blockType: BlockType.COMMAND + }, + { + blockType: BlockType.LABEL, + text: "drawing" + }, + { + opcode: 'clearCanvas', + text: 'clear canvas [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'clearAria', + text: 'clear area at x: [x] y: [y] with width: [width] height: [height] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'drawRect', + text: 'draw rectangle at x: [x] y: [y] with width: [width] height: [height] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'drawImage', + text: 'draw image [src] at x: [x] y: [y] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + src: { + type: ArgumentType.STRING, + defaultValue: 'https://studio.penguinmod.com/favicon.ico' + } + }, + blockType: BlockType.COMMAND + } + ], + menus: { + canvas: 'getCanvasMenuItems', + CompositeOperation: { + items: [ + { + "text": "source-over", + "value": "source-over" + }, + { + "text": "source-in", + "value": "source-in" + }, + { + "text": "source-out", + "value": "source-out" + }, + { + "text": "source-atop", + "value": "source-atop" + }, + { + "text": "destination-over", + "value": "destination-over" + }, + { + "text": "destination-in", + "value": "destination-in" + }, + { + "text": "destination-out", + "value": "destination-out" + }, + { + "text": "destination-atop", + "value": "destination-atop" + }, + { + "text": "lighter", + "value": "lighter" + }, + { + "text": "copy", + "value": "copy" + }, + { + "text": "xor", + "value": "xor" + }, + { + "text": "multiply", + "value": "multiply" + }, + { + "text": "screen", + "value": "screen" + }, + { + "text": "overlay", + "value": "overlay" + }, + { + "text": "darken", + "value": "darken" + }, + { + "text": "lighten", + "value": "lighten" + }, + { + "text": "color-dodge", + "value": "color-dodge" + }, + { + "text": "color-burn", + "value": "color-burn" + }, + { + "text": "hard-light", + "value": "hard-light" + }, + { + "text": "soft-light", + "value": "soft-light" + }, + { + "text": "difference", + "value": "difference" + }, + { + "text": "exclusion", + "value": "exclusion" + }, + { + "text": "hue", + "value": "hue" + }, + { + "text": "saturation", + "value": "saturation" + }, + { + "text": "color", + "value": "color" + }, + { + "text": "luminosity", + "value": "luminosity" + } + ] + } + } + }; + } + + createNewCanvas() { + const newCanvas = prompt('canvas name?', 'newCanvas'); + // if this camvas already exists, remove it to minimize confusion + if (!newCanvas) return alert('Canceled') + if (store.getCanvasByName(newCanvas)) return; + store.newCanvas(newCanvas); + vm.emitWorkspaceUpdate(); + this.serialize(); + } + + getCanvasMenuItems() { + const canvases = store.getAllCanvases(); + if (canvases.length < 1) return [{ text: '', value: '' }]; + return canvases.map(canvas => ({ + text: canvas.name, + value: canvas.id + })); + } + + canvasGetter(args) { + const canvasObj = store.getCanvas(args.canvas); + return canvasObj.element.toDataURL(); + } + + setGlobalCompositeOperation(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.globalCompositeOperation = args.CompositeOperation; + } + + setBorderColor(args) { + const color = Cast.toString(args.color); + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.strokeStyle = color; + } + + setFill(args) { + const color = Cast.toString(args.color); + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.fillStyle = color; + } + + setSize(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.element.width = args.width; + canvasObj.element.height = args.height; + canvasObj.context = canvasObj.element.getContext('2d'); + } + + drawRect(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.fillRect(args.x, args.y, args.width, args.height); + } + + drawImage(args) { + return new Promise(resolve => { + const canvasObj = store.getCanvas(args.canvas); + const image = new Image(); + image.onload = () => { + canvasObj.context.drawImage(image, args.x, args.y); + resolve(); + }; + image.src = args.src; + }); + } + + clearAria(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.clearRect(args.x, args.y, args.width, args.height); + } + + clearCanvas(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.clearRect(0, 0, canvasObj.width, canvasObj.height); + } + + setTransparency(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.globalAlpha = args.transparency / 100; + } +} + +module.exports = canvas; diff --git a/local-scratch-vm/src/extensions/gsa_colorUtilBlocks/index.js b/local-scratch-vm/src/extensions/gsa_colorUtilBlocks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..43b7789bcc992d791de99c6e454985b1f724dd68 --- /dev/null +++ b/local-scratch-vm/src/extensions/gsa_colorUtilBlocks/index.js @@ -0,0 +1,424 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Color = require('../../util/color'); +const {validateJSON} = require('../../util/json-block-utilities'); +const Cast = require('../../util/cast'); + +/** + * Class for TurboWarp blocks + * @constructor + */ +class colorBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + deafultHsv = '{"h": 360, "s": 1, "v": 1}'; + deafultRgb = '{"r": 255, "g": 0, "b": 0}'; + deafultHex = '#ff0000'; + deafultDecimal = '16711680'; + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'colors', + name: 'Colors', + color1: '#ff4c4c', + color2: '#e64444', + blocks: [ + { + opcode: 'colorPicker', + text: '[OUTPUT] of [COLOR]', + disableMonitor: true, + arguments: { + OUTPUT: { + type: ArgumentType.STRING, + menu: "outputColorType" + }, + COLOR: { + type: ArgumentType.COLOR + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'defaultBlack', + text: 'black', + disableMonitor: true, + blockType: BlockType.REPORTER + }, + { + opcode: 'defaultWhite', + text: 'white', + disableMonitor: true, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: 'RGB' + }, + { + opcode: 'rgbToDecimal', + text: 'rgb [color] to decimal', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultRgb + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'rgbToHex', + text: 'rgb [color] to hex', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultRgb + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'rgbToHsv', + text: 'rgb [color] to hsv', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultRgb + } + }, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: 'Hex' + }, + { + opcode: 'hexToDecimal', + text: 'hex [color] to decimal', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultHex + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'hexToRgb', + text: 'hex [color] to rgb', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultHex + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'hexToHsv', + text: 'hex [color] to hsv', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultHex + } + }, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: 'Decimal' + }, + { + opcode: 'decimalToHex', + text: 'decimal [color] to hex', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultDecimal + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'decimalToRgb', + text: 'decimal [color] to rgb', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultDecimal + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'decimalToHsv', + text: 'decimal [color] to hsv', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultDecimal + } + }, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: 'HSV' + }, + { + opcode: 'hsvToHex', + text: 'hsv [color] to hex', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultHsv + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'hsvToRgb', + text: 'hsv [color] to rgb', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultHsv + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'hsvToDecimal', + text: 'hsv [color] to decimal', + arguments: { + color: { + type: ArgumentType.STRING, + defaultValue: this.deafultHsv + } + }, + blockType: BlockType.REPORTER + }, + "---", + { + blockType: BlockType.LABEL, + text: 'Other' + }, + { + opcode: 'csbMaker', + text: 'color: [h] saturation: [s] brightness: [v] transparency: [a]', + arguments: { + h: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + s: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + v: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + a: { + type: ArgumentType.NUMBER, + defaultValue: '50' + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'hsvMaker', + text: 'h: [h] s: [s] v: [v] a: [a]', + arguments: { + h: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + s: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + v: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + a: { + type: ArgumentType.NUMBER, + defaultValue: '50' + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'rgbMaker', + text: 'r: [r] g: [g] b: [b] a: [a]', + arguments: { + r: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + g: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + b: { + type: ArgumentType.NUMBER, + defaultValue: '50' + }, + a: { + type: ArgumentType.NUMBER, + defaultValue: '50' + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'mixColors', + text: 'mix [color1] [color2] by [percent]', + arguments: { + color1: { + type: ArgumentType.STRING, + defaultValue: this.deafultRgb + }, + color2: { + type: ArgumentType.STRING, + defaultValue: this.deafultRgb + }, + percent: { + type: ArgumentType.NUMBER, + defaultValue: '0.5' + } + }, + blockType: BlockType.REPORTER + } + ], + menus: { + outputColorType: { + items: [ + { text: 'decimal', value: "decimal" }, + { text: 'rgb', value: "rgb" }, + { text: 'hsv', value: "hsv" }, + { text: 'hex', value: "hex" } + ], + acceptReporters: true + } + } + }; + } + + defaultBlack () { + return JSON.stringify(Color.RGB_BLACK); + } + defaultWhite () { + return JSON.stringify(Color.RGB_WHITE); + } + + colorPicker (args) { + const color = Color.hexToDecimal(args.COLOR); + const argsColor = { color: color }; + switch (Cast.toString(args.OUTPUT).toLowerCase()) { + case "rgb": + return this.decimalToRgb(argsColor); + case "hsv": + return this.decimalToHsv(argsColor); + case "hex": + // todo: args.COLOR is already hex now + return this.decimalToHex(argsColor); + default: + return color; + } + } + + csbMaker (args) { + const color = { + h: args.h * 360 / 100, + s: args.s / 100, + v: args.v / 100 + }; + if (!isNaN(args.a)) color.a = args.a / 100; + return JSON.stringify(color); + } + hsvMaker (args) { + const color = { + h: args.h, + s: args.s, + v: args.v + }; + if (!isNaN(args.a)) color.a = args.a; + return JSON.stringify(color); + } + rgbMaker (args) { + const color = { + r: args.r, + g: args.g, + b: args.b + }; + if (!isNaN(args.a)) color.a = args.a; + return JSON.stringify(color); + } + mixColors (args) { + const color1 = validateJSON(args.color1).object; + const color2 = validateJSON(args.color2).object; + return JSON.stringify(Color.mixRgb(color1, color2, args.percent)); + } + + rgbToDecimal (args) { + const color = validateJSON(args.color).object; + return Color.rgbToDecimal(color); + } + rgbToHex (args) { + const color = validateJSON(args.color).object; + return Color.rgbToHex(color); + } + rgbToHsv (args) { + const color = validateJSON(args.color).object; + return JSON.stringify(Color.rgbToHsv(color)); + } + hexToDecimal (args) { + const color = args.color; + return Color.hexToDecimal(color); + } + hexToRgb (args) { + const color = Color.hexToRgb(args.color); + return JSON.stringify(color); + } + hexToHsv (args) { + const color = Color.hexToRgb(args.color); + return JSON.stringify(Color.rgbToHsv(color)); + } + decimalToHex (args) { + const color = Number(args.color); + return Color.decimalToHex(color); + } + decimalToRgb (args) { + const color = Color.decimalToRgb(Number(args.color)); + return JSON.stringify(color); + } + decimalToHsv (args) { + const color = Color.decimalToRgb(Number(args.color)); + return JSON.stringify(Color.rgbToHsv(color)); + } + hsvToHex (args) { + const color = Color.hsvToRgb(validateJSON(args.color).object); + return Color.rgbToHex(color); + } + hsvToRgb (args) { + const color = Color.hsvToRgb(validateJSON(args.color).object); + return JSON.stringify(color); + } + hsvToDecimal (args) { + const color = Color.hsvToRgb(validateJSON(args.color).object); + return Color.rgbToDecimal(color); + } +} + +module.exports = colorBlocks; diff --git a/local-scratch-vm/src/extensions/gsa_objectVars/index.js b/local-scratch-vm/src/extensions/gsa_objectVars/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d40a2a076b693eb834b3f44fbd206f420808a3e8 --- /dev/null +++ b/local-scratch-vm/src/extensions/gsa_objectVars/index.js @@ -0,0 +1,487 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Color = require('../../util/color'); +const cstore = require('./objectStorage'); +const Cast = require('../../util/cast'); +const store = new cstore(); + +/** + * Class + * @constructor + */ +class canvas { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + store.attachRuntime(runtime); + } + + deserialize(data) { + store.canvases = {}; + for (const canvas of data) { + store.newCanvas(canvas.name, canvas.width, canvas.height, canvas.id); + } + } + + serialize() { + return store.getAllCanvases() + .map(variable => ({ + name: variable.name, + width: variable.width, + height: variable.height, + id: variable.id + })); + } + + readAsImageElement(src) { + return new Promise((resolve, reject) => { + const image = new Image(); + image.onload = function () { + resolve(image); + image.onload = null; + image.onerror = null; + }; + image.onerror = function () { + reject(new Error('Costume load failed. Asset could not be read.')); + image.onload = null; + image.onerror = null; + }; + image.src = src; + }); + } + + orderCategoryBlocks(blocks) { + const button = blocks[0]; + const varBlock = blocks[1]; + delete blocks[0]; + delete blocks[1]; + // create the variable block xml's + const varBlocks = store.getAllCanvases().map(canvas => varBlock + .replace('{canvasId}', canvas.id)); + if (!varBlocks.length) { + return [button]; + } + // push the button to the top of the var list + varBlocks + .reverse() + .push(button); + // merge the category blocks and variable blocks into one block list + blocks = varBlocks + .reverse() + .concat(blocks); + return blocks; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'canvas', + name: 'html canvas', + color1: '#0069c2', + color2: '#0060B4', + color3: '#0060B4', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [ + { + opcode: 'createNewCanvas', + blockType: BlockType.BUTTON, + text: 'create new canvas' + }, + { + opcode: 'canvasGetter', + blockType: BlockType.REPORTER, + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: '{canvasId}' + } + }, + text: '[canvas]' + }, + { + blockType: BlockType.LABEL, + text: "config" + }, + { + opcode: 'setGlobalCompositeOperation', + text: 'set composite operation of [canvas] to [CompositeOperation]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + CompositeOperation: { + type: ArgumentType.STRING, + menu: 'CompositeOperation', + defaultValue: "" + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setSize', + text: 'set width: [width] height: [height] of [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setTransparency', + text: 'set transparency of [canvas] to [transparency]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + transparency: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setFill', + text: 'set fill color of [canvas] to [color]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + color: { + type: ArgumentType.COLOR + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setBorderColor', + text: 'set border color of [canvas] to [color]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + color: { + type: ArgumentType.COLOR + } + }, + blockType: BlockType.COMMAND + }, + { + blockType: BlockType.LABEL, + text: "drawing" + }, + { + opcode: 'clearCanvas', + text: 'clear canvas [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'clearAria', + text: 'clear area at x: [x] y: [y] with width: [width] height: [height] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'drawRect', + text: 'draw rectangle at x: [x] y: [y] with width: [width] height: [height] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + width: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageWidth + }, + height: { + type: ArgumentType.NUMBER, + defaultValue: this.runtime.stageHeight + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'drawImage', + text: 'draw image [src] at x: [x] y: [y] on [canvas]', + arguments: { + canvas: { + type: ArgumentType.STRING, + menu: 'canvas', + defaultValue: "" + }, + x: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + src: { + type: ArgumentType.STRING, + defaultValue: 'https://studio.penguinmod.com/favicon.ico' + } + }, + blockType: BlockType.COMMAND + } + ], + menus: { + canvas: 'getCanvasMenuItems', + CompositeOperation: { + items: [ + { + "text": "source-over", + "value": "source-over" + }, + { + "text": "source-in", + "value": "source-in" + }, + { + "text": "source-out", + "value": "source-out" + }, + { + "text": "source-atop", + "value": "source-atop" + }, + { + "text": "destination-over", + "value": "destination-over" + }, + { + "text": "destination-in", + "value": "destination-in" + }, + { + "text": "destination-out", + "value": "destination-out" + }, + { + "text": "destination-atop", + "value": "destination-atop" + }, + { + "text": "lighter", + "value": "lighter" + }, + { + "text": "copy", + "value": "copy" + }, + { + "text": "xor", + "value": "xor" + }, + { + "text": "multiply", + "value": "multiply" + }, + { + "text": "screen", + "value": "screen" + }, + { + "text": "overlay", + "value": "overlay" + }, + { + "text": "darken", + "value": "darken" + }, + { + "text": "lighten", + "value": "lighten" + }, + { + "text": "color-dodge", + "value": "color-dodge" + }, + { + "text": "color-burn", + "value": "color-burn" + }, + { + "text": "hard-light", + "value": "hard-light" + }, + { + "text": "soft-light", + "value": "soft-light" + }, + { + "text": "difference", + "value": "difference" + }, + { + "text": "exclusion", + "value": "exclusion" + }, + { + "text": "hue", + "value": "hue" + }, + { + "text": "saturation", + "value": "saturation" + }, + { + "text": "color", + "value": "color" + }, + { + "text": "luminosity", + "value": "luminosity" + } + ] + } + } + }; + } + + createNewCanvas() { + const newCanvas = prompt('canvas name?', 'newCanvas'); + // if this camvas already exists, remove it to minimize confusion + if (store.getCanvasByName(newCanvas)) return; + store.newCanvas(newCanvas); + vm.emitWorkspaceUpdate(); + this.serialize(); + } + + getCanvasMenuItems() { + const canvases = store.getAllCanvases(); + if (canvases.length < 1) return [{ text: '', value: '' }]; + return canvases.map(canvas => ({ + text: canvas.name, + value: canvas.id + })); + } + + canvasGetter(args) { + const canvasObj = store.getCanvas(args.canvas); + return canvasObj.element.toDataURL(); + } + + setGlobalCompositeOperation(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.globalCompositeOperation = args.CompositeOperation; + } + + setBorderColor(args) { + const color = Cast.toString(args.color); + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.strokeStyle = color; + } + + setFill(args) { + const color = Cast.toString(args.color); + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.fillStyle = color; + } + + setSize(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.element.width = args.width; + canvasObj.element.height = args.height; + canvasObj.context = canvasObj.element.getContext('2d'); + } + + drawRect(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.fillRect(args.x, args.y, args.width, args.height); + } + + drawImage(args) { + return new Promise(resolve => { + const canvasObj = store.getCanvas(args.canvas); + const image = new Image(); + image.onload = () => { + canvasObj.context.drawImage(image, args.x, args.y); + resolve(); + }; + image.src = args.src; + }); + } + + clearAria(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.clearRect(args.x, args.y, args.width, args.height); + } + + clearCanvas(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.clearRect(0, 0, canvasObj.width, canvasObj.height); + } + + setTransparency(args) { + const canvasObj = store.getCanvas(args.canvas); + canvasObj.context.globalAlpha = args.transparency / 100; + } +} + +module.exports = canvas; diff --git a/local-scratch-vm/src/extensions/gsa_objectVars/objectStorage.js b/local-scratch-vm/src/extensions/gsa_objectVars/objectStorage.js new file mode 100644 index 0000000000000000000000000000000000000000..e08aec527312065d0b39fe788708dad083ee7cea --- /dev/null +++ b/local-scratch-vm/src/extensions/gsa_objectVars/objectStorage.js @@ -0,0 +1,87 @@ +const uid = require('../../util/uid'); + +class canvasStorage { + /** + * initiats the storage + */ + constructor () { + this.canvases = {}; + } + + attachRuntime (runtime) { + this.runtime = runtime; + } + + /** + * gets a canvas with a given id + * @param {string} id the id of the canvas to get + * @returns {Object} the canvas object with this id + */ + getCanvas (id) { + return this.canvases[id]; + } + + /** + * deletes a canvas with a given id + * @param {string} id the canvas id to delete + * @returns {Object} the deleted canvas + */ + deleteCanvas (id) { + const orignal = this.canvases[id]; + delete this.canvases[id]; + return orignal; + } + + /** + * creates a new canvas + * @param {string} name the name to give the new canvas + * @param {number} width the width of the canvas + * @param {number} height the height of the canvas + * @param {string} opt_id the id of the canvas + * @returns {Object} the new canvas object + */ + newCanvas (name, width, height, opt_id) { + width = width || this.runtime.stageWidth; + height = height || this.runtime.stageHeight; + + const id = opt_id || uid(); + const element = document.createElement('canvas'); + element.id = id; + element.width = width; + element.height = height; + + const skin = !this.runtime.renderer + ? null + : this.runtime.renderer.createBitmapSkin(element, 1); + + const data = { + name: name, + id: id, + element: element, + skinId: skin, + width: width, + height: height, + context: element.getContext('2d') + }; + this.canvases[id] = data; + return data; + } + + /** + * gets or creates a canvas with name equal to + * @param {String} name the name of the canvas + */ + getCanvasByName (name) { + return Object.values(this.canvases).find(canvas => canvas.name === name); + } + + /** + * gets all canvases + * @returns {Array} + */ + getAllCanvases () { + return Object.values(this.canvases); + } +} + +module.exports = canvasStorage; diff --git a/local-scratch-vm/src/extensions/gsa_tempVars/index.js b/local-scratch-vm/src/extensions/gsa_tempVars/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ccba8124d335b4182887ea56db14c37ee6c659bf --- /dev/null +++ b/local-scratch-vm/src/extensions/gsa_tempVars/index.js @@ -0,0 +1,255 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +/** + * Class + * @constructor + */ +class tempVars { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + } + + getThreadVars (thread) { + if (!thread.tempVars) { + thread.tempVars = Object.create(null); + } + return thread.tempVars; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'tempVars', + name: 'Temporary Variables', + color1: '#0069c2', + color2: '#0060B4', + color3: '#0060B4', + blocks: [ + { + opcode: 'setVariable', + text: 'set [name] to [value]', + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'Variable' + }, + value: { + type: ArgumentType.STRING, + defaultValue: 'Value' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'changeVariable', + text: 'change [name] by [value]', + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'Variable' + }, + value: { + type: ArgumentType.NUMBER, + defaultValue: '1' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'getVariable', + text: 'get [name]', + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'Variable' + } + }, + allowDropAnywhere: true, + blockType: BlockType.REPORTER + }, + '---', + { + opcode: 'deleteVariable', + text: 'delete [name]', + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'Variable' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'deleteAllVariables', + text: 'delete all variables', + blockType: BlockType.COMMAND + }, + { + opcode: 'variableExists', + text: 'variable [name] exists?', + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'Variable' + } + }, + disableMonitor: true, + blockType: BlockType.BOOLEAN + }, + { + opcode: 'allVariables', + text: 'current variables', + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'Variable' + } + }, + disableMonitor: true, + blockType: BlockType.REPORTER + }, + '---', + { + opcode: 'forEachTempVar', + text: 'for each [NAME] in [REPEAT]', + branchCount: 1, + blockType: BlockType.LOOP, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Variable' + }, + REPEAT: { + type: ArgumentType.NUMBER, + defaultValue: 10 + } + } + } + ] + }; + } + /** + * This function is used for any compiled blocks in the extension if they exist. + * Data in this function is given to the IR & JS generators. + * Data must be valid otherwise errors may occur. + * @returns {object} functions that create data for compiled blocks. + * @deprecated nolonger in use as all of this is now inside the compiler + */ + getCompileInfoOld() { + return { + ir: { + forEachTempVar: (generator, block) => { + generator.analyzeLoop(); + return { + kind: 'stack', + name: generator.descendInputOfBlock(block, 'NAME'), + repeat: generator.descendInputOfBlock(block, 'REPEAT'), + do: generator.descendSubstack(block, 'SUBSTACK') + }; + } + }, + js: { + forEachTempVar: (node, compiler, imports) => { + const index = compiler.localVariables.next(); + const name = compiler.localVariables.next(); + const inputName = compiler.descendInput(node.name).asString(); + const realName = `('threadVar_' + ${inputName})`; + compiler.source += `var ${index} = 0; `; + compiler.source += `var ${name} = ${realName}; `; + compiler.source += `while (${index} < ${compiler.descendInput(node.repeat).asNumber()}) { `; + compiler.source += `${index}++; `; + compiler.source += `if (!thread.tempVars) { `; + compiler.source += `thread.tempVars = {}; `; + compiler.source += `}\n`; + compiler.source += `thread.tempVars[${name}] = ${index};\n`; + compiler.descendStack(node.do, new imports.Frame(true)); + compiler.yieldLoop(); + compiler.source += '}\n'; + } + } + } + } + + setVariable (args, util) { + const tempVars = this.getThreadVars(util.thread); + const name = `threadVar_${args.name}`; + tempVars[name] = args.value; + } + + changeVariable (args, util) { + const tempVars = this.getThreadVars(util.thread); + const name = `threadVar_${args.name}`; + const oldNum = Number(tempVars[name]); + const newNum = oldNum + args.value; + if (!oldNum) { + tempVars[name] = Number(args.value); + return; + } + tempVars[name] = newNum; + } + + getVariable (args, util) { + const tempVars = this.getThreadVars(util.thread); + const name = `threadVar_${args.name}`; + const value = tempVars[name]; + if (!value) return ''; + return value; + } + + deleteVariable (args, util) { + const tempVars = this.getThreadVars(util.thread); + const name = `threadVar_${args.name}`; + if (!(name in tempVars)) return; + delete tempVars[name]; + } + + deleteAllVariables (_, util) { + // resets the vars + util.thread.tempVars = Object.create(null); + } + + variableExists (args, util) { + const tempVars = this.getThreadVars(util.thread); + const name = `threadVar_${args.name}`; + return (name in tempVars); + } + + allVariables (_, util) { + const tempVars = this.getThreadVars(util.thread); + const keys = Object.keys(tempVars); + const mapped = keys.map(name => name.replace('threadVar_', '')); + return JSON.stringify(mapped); + } + + forEachTempVar (args, util) { + // compiled blocks need an interpreter version + // for edge activated hats + const count = Cast.toNumber(args.REPEAT); + const name = Cast.toString(args.NAME); + // Initialize loop + if (typeof util.stackFrame.loopCounter === 'undefined') { + util.stackFrame.loopCounter = count; + } + // 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) { + const i = (count - (util.stackFrame.loopCounter)) - 1; + this.setVariable({ name: name, value: i + 1 }, util); + util.startBranch(1, true); + } + } +} + +module.exports = tempVars; diff --git a/local-scratch-vm/src/extensions/iyg_perlin_noise/index.js b/local-scratch-vm/src/extensions/iyg_perlin_noise/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6255820ab8ec5a59f8dee6f8299f7322c83463fc --- /dev/null +++ b/local-scratch-vm/src/extensions/iyg_perlin_noise/index.js @@ -0,0 +1,462 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const MersenneTwister = require('mersenne-twister'); +const { createNoise3D } = require('simplex-noise'); +/** + * Class for perlin noise extension. + * @constructor + */ + +// noise generation code from p5.js + +class iygPerlin { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + this.noise; + this.seed = 123; + this.size = 50; + this.generator = new MersenneTwister(this.seed); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'iygPerlin', + name: 'Perlin Noise', + color1: '#525252', + color2: '#636363', + blocks: [ + // Hidden + { + opcode: 'GetNoise', + blockType: BlockType.REPORTER, + text: formatMessage({ + id: 'iygPerlin.GetNoise', + default: 'Get perlin noise with seed [SEED] and octave [OCTAVE] at x [X], y [Y], and z [Z]', + description: 'Get seeded perlin noise at a specified x and y and z.' + }), + arguments: { + SEED: { + type: ArgumentType.NUMBER, + defaultValue: 123 + }, + OCTAVE: { + type: ArgumentType.NUMBER, + defaultValue: 4 + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Z: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + }, + hideFromPalette: true + }, + // Hidden + { + opcode: 'GetRandomNoise', + blockType: BlockType.REPORTER, + text: formatMessage({ + id: 'iygPerlin.GetRandomNoise', + default: 'Get noise with seed [SEED] at x [X], y [Y], and z [Z]', + description: 'Get seeded noise with a specified seed at a specified x and y and z.' + }), + arguments: { + SEED: { + type: ArgumentType.NUMBER, + defaultValue: 123 + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Z: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + }, + hideFromPalette: true + }, + // Hidden + { + opcode: 'GeneratePerlinNoise', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'iygPerlin.GeneratePerlinNoise', + default: 'Pre-generate perlin noise with seed [SEED] and octave [OCTAVE]', + description: 'Pre-generate seeded perlin noise.' + }), + arguments: { + SEED: { + type: ArgumentType.NUMBER, + defaultValue: 123 + }, + OCTAVE: { + type: ArgumentType.NUMBER, + defaultValue: 4 + } + }, + hideFromPalette: true + }, + // Hidden + { + opcode: 'GenerateRandomNoise', + blockType: BlockType.COMMAND, + hideFromPalette: true, + text: formatMessage({ + id: 'iygPerlin.GenerateRandomNoise', + default: 'not needed [SEED] [SIZE]', + description: 'Pre-generate seeded noise.' + }), + arguments: { + SEED: { + type: ArgumentType.NUMBER, + defaultValue: 123 + }, + SIZE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + }, + }, + // Hidden + { + opcode: 'getSimplexNoise', + blockType: BlockType.REPORTER, + hideFromPalette: true, + text: formatMessage({ + id: 'iygPerlin.getSimplexNoise', + default: 'Get simplex noise with seed [SEED] at x [X], y [Y], and z [Z]', + description: 'Get simplex noise with a specified seed at a specified x and y and z.' + }), + arguments: { + SEED: { + type: ArgumentType.NUMBER, + defaultValue: 123 + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Z: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + + // End of hidden stuff + + { + opcode: 'GetNoiseV2', + blockType: BlockType.REPORTER, + text: formatMessage({ + id: 'iygPerlin.GetNoiseV2', + default: 'Get perlin noise with seed [SEED] and octave [OCTAVE] at x [X], y [Y], and z [Z]', + description: 'Get seeded perlin noise at a specified x and y and z.' + }), + arguments: { + SEED: { + type: ArgumentType.NUMBER, + defaultValue: 123 + }, + OCTAVE: { + type: ArgumentType.NUMBER, + defaultValue: 4 + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Z: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + }, + }, + { + opcode: 'GetRandomNoiseV2', + blockType: BlockType.REPORTER, + text: formatMessage({ + id: 'iygPerlin.GetRandomNoiseV2', + default: 'Get random noise with seed [SEED] at x [X], y [Y], and z [Z]', + description: 'Get seeded random noise with a specified seed at a specified x and y and z.' + }), + arguments: { + SEED: { + type: ArgumentType.NUMBER, + defaultValue: 123 + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Z: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'GeneratePerlinNoiseV2', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'iygPerlin.GeneratePerlinNoiseV2', + default: 'Pre-generate perlin noise with seed [SEED] and octave [OCTAVE]', + description: 'Pre-generate seeded perlin noise.' + }), + arguments: { + SEED: { + type: ArgumentType.NUMBER, + defaultValue: 123 + }, + OCTAVE: { + type: ArgumentType.NUMBER, + defaultValue: 4 + } + } + }, + ] + }; + } + + goodSeedRandom() { + this.generator.init_seed(this.seed); + let result = this.generator.random_incl(); + this.seed = result * 4294967296; + return result; + } + + dumbSeedRandom() { + this.generator.init_seed(this.seed); + let result = this.generator.random_incl(); + this.seed = (1664525*this.seed + 1013904223) % 4294967296; + return result; + } + + GeneratePerlinNoise(args, util) { + args.X = 0; + args.Y = 0; + args.Z = 0; + this.GetNoise(args, util); + } + + GenerateRandomNoise(args, util) { + let seed = args.SEED; + let size = args.SIZE; + + if (this.noise == null || seed != this.seed) { + this.noise = new Array(size); + this.seed = seed; + for (let i = 0; i < size; i++) { + this.noise[i] = new Array(size); + for (let j = 0; j < size; j++) { + this.noise[i][j] = new Array(size); + for (let k = 0; k < size; k++) { + this.noise[i][j][k] = this.dumbSeedRandom(); + } + } + } + this.seed = seed; + this.prev_seed = seed; + this.size = size; + } + + if (size > this.size && seed == this.seed) { + this.seed = this.prev_seed; + for (let i = this.size; i < size+1; i++) { + this.noise[i] = new Array(size); + for (let j = this.size; j < size+1; j++) { + this.noise[i][j] = new Array(size); + for (let k = this.size; k < size+1; k++) { + this.noise[i][j][k] = this.dumbSeedRandom(); + } + } + } + } + } + + GetRandomNoise(args, util) { + let seed = args.SEED; + let x = args.X; + let y = args.Y; + let z = args.Z; + let pre_seed = this.seed; + this.seed = seed+x+y*1000+z*10000; + let result = this.dumbSeedRandom(); + this.seed = pre_seed; + return result; + } + + generatePerlin(seed, perlin_octaves, rand, x, y, z) { + let perlin_amp_falloff = 0.5; + const scaled_cosine = i => 0.5 * (1.0 - Math.cos(i * Math.PI)); + const PERLIN_SIZE = 4095; + const PERLIN_YWRAPB = 4; + const PERLIN_YWRAP = 1 << PERLIN_YWRAPB; + const PERLIN_ZWRAPB = 8; + const PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB; + + if (this.perlin == null || seed != this.seed) { + this.perlin = new Array(PERLIN_SIZE + 1); + this.seed = seed; + for (let i = 0; i < PERLIN_SIZE + 1; i++) { + this.perlin[i] = rand(); + } + this.seed = seed; + } + + + if (x < 0) { + x = -x; + } + if (y < 0) { + y = -y; + } + if (z < 0) { + z = -z; + } + + let xi = Math.floor(x), + yi = Math.floor(y), + zi = Math.floor(z); + let xf = x - xi; + let yf = y - yi; + let zf = z - zi; + let rxf, ryf; + + let r = 0; + let ampl = 0.5; + + let n1, n2, n3; + + for (let o = 0; o < perlin_octaves; o++) { + let of = xi + (yi << PERLIN_YWRAPB) + (zi << PERLIN_ZWRAPB); + + rxf = scaled_cosine(xf); + ryf = scaled_cosine(yf); + + n1 = this.perlin[of & PERLIN_SIZE]; + n1 += rxf * (this.perlin[(of + 1) & PERLIN_SIZE] - n1); + n2 = this.perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE]; + n2 += rxf * (this.perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n2); + n1 += ryf * (n2 - n1); + + of += PERLIN_ZWRAP; + n2 = this.perlin[of & PERLIN_SIZE]; + n2 += rxf * (this.perlin[(of + 1) & PERLIN_SIZE] - n2); + n3 = this.perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE]; + n3 += rxf * (this.perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n3); + n2 += ryf * (n3 - n2); + + n1 += scaled_cosine(zf) * (n2 - n1); + + r += n1 * ampl; + ampl *= perlin_amp_falloff; + xi <<= 1; + xf *= 2; + yi <<= 1; + yf *= 2; + zi <<= 1; + zf *= 2; + + if (xf >= 1.0) { + xi++; + xf--; + } + if (yf >= 1.0) { + yi++; + yf--; + } + if (zf >= 1.0) { + zi++; + zf--; + } + } + return r % 1.0; + } + + GetNoise(args, util) { + let seed = args.SEED; + let perlin_octaves = ((args.OCTAVE === Infinity) ? 4 : args.OCTAVE); + let x = args.X + .5; + let y = args.Y + .5; + let z = args.Z + .5; + + return this.generatePerlin(seed, perlin_octaves, this.dumbSeedRandom.bind(this), x, y, z); + } + + // ----- V2 ----- + GetNoiseV2(args, util) { + let seed = args.SEED; + let perlin_octaves = ((args.OCTAVE === Infinity) ? 4 : args.OCTAVE); + let x = args.X + .5; + let y = args.Y + .5; + let z = args.Z + .5; + + return this.generatePerlin(seed, perlin_octaves, this.goodSeedRandom.bind(this), x, y, z); + } + + // ----- V2 ----- + GetRandomNoiseV2(args, util) { + let seed = args.SEED; + let x = args.X; + let y = args.Y; + let z = args.Z; + let pre_seed = this.seed; + this.seed = seed + (x * 743) + (y * 942 ) + (z * 645); + let result = this.goodSeedRandom.bind(this)(); + this.seed = pre_seed; + return result; + } + + // ----- V2 ----- + GeneratePerlinNoiseV2(args, util) { + args.X = 0; + args.Y = 0; + args.Z = 0; + this.GetNoiseV2(args, util); + } + + getSimplexNoise(args) { + const seed = args.SEED; + const x = args.X; + const y = args.Y; + const z = args.Z; + this.generator.init_seed(seed); + const noise = createNoise3D(this.generator.random_incl.bind(this.generator)); + return noise(x, y, z); + } +} + +module.exports = iygPerlin; diff --git a/local-scratch-vm/src/extensions/jg_3d/camera.png b/local-scratch-vm/src/extensions/jg_3d/camera.png new file mode 100644 index 0000000000000000000000000000000000000000..f9060c9cd46899466b8f9930560af80278ec0fee Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/camera.png differ diff --git a/local-scratch-vm/src/extensions/jg_3d/cube.png b/local-scratch-vm/src/extensions/jg_3d/cube.png new file mode 100644 index 0000000000000000000000000000000000000000..c70ad7268b03e690576fc3b49787066b8abd2108 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/cube.png differ diff --git a/local-scratch-vm/src/extensions/jg_3d/index.js b/local-scratch-vm/src/extensions/jg_3d/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e5857c044fec35dcd6189c0976250f57ab227811 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_3d/index.js @@ -0,0 +1,983 @@ +const Cast = require('../../util/cast'); +const Clone = require('../../util/clone'); +const ExtensionInfo = require("./info"); +const Three = require("three"); +const BufferGeometryUtils = require('three/examples/jsm/utils/BufferGeometryUtils'); +const { ConvexGeometry } = require('three/examples/jsm/geometries/ConvexGeometry'); +const { OBJLoader } = require('three/examples/jsm/loaders/OBJLoader.js'); +const { GLTFLoader } = require('three/examples/jsm/loaders/GLTFLoader.js'); +const { FBXLoader } = require('three/examples/jsm/loaders/FBXLoader.js'); +const Color = require('../../util/color'); + +const MeshLoaders = { + OBJ: new OBJLoader(), + GLTF: new GLTFLoader(), + FBX: new FBXLoader(), +} +function toRad(deg) { + return deg * (Math.PI / 180); +} +function toDeg(rad) { + return rad * (180 / Math.PI); +} +function normalize(vec) { + const length = Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z); + return new Three.Vector3(vec.x / length, vec.y / length, vec.z / length); +} +function toDegRounding(rad) { + const result = toDeg(rad); + if (!String(result).includes('.')) return result; + const split = String(result).split('.'); + const endingDecimals = split[1].substring(0, 3); + if ((endingDecimals === '999') && (split[1].charAt(3) === '9')) return Number(split[0]) + 1; + return Number(split[0] + '.' + endingDecimals); +} + +/** + * Class for 3D blocks + * @constructor + */ +class Jg3DBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.three = Three; + // expose addons and that for the funnis + this.BufferGeometryUtils = BufferGeometryUtils; + this.ConvexGeometry = ConvexGeometry; + this.OBJLoader = OBJLoader; + this.GLTFLoader = GLTFLoader; + this.FBXLoader = FBXLoader; + + // prism has screenshots, lets tell it to use OUR canvas for them + this.runtime.prism_screenshot_checkForExternalCanvas = true; + this.runtime.prism_screenshot_externalCanvas = null; + + // Three.js requirements + /** + * @type {Three.Scene} + */ + this.scene = null; + /** + * @type {Three.Camera} + */ + this.camera = null; + /** + * @type {Three.WebGLRenderer} + */ + this.renderer = null; + + this.existingSceneObjects = []; + this.existingSceneLights = []; + + // extras + this.lastStageSizeWhenRendering = { + width: 0, + height: 0 + } + + this.savedMeshes = {}; + this.sceneLayer = "front"; + this.lastStageColor = [255, 255, 255, 0]; + + // event recievers + // stop button clicked or project restarted, dispose of all objects + this.runtime.on('PROJECT_STOP_ALL', () => { + this.dispose(); + this.sceneLayer = "front"; + this.updateScratchCanvasRelayering(); + }); + } + + /** + * Dispose of the scene, camera & renderer (and any objects) + */ + dispose() { + this.existingSceneObjects = []; + this.existingSceneLights = []; + if (this.scene) { + this.scene.remove(); + this.scene = null; + } + if (this.camera) { + this.camera.remove(); + this.camera = null; + } + if (this.renderer) { + if (this.renderer.domElement) { + this.renderer.domElement.remove(); + } + this.renderer.dispose(); + this.renderer = null; + this.runtime.prism_screenshot_externalCanvas = null; + } + } + /** + * Displays a message for stack blocks. + * @param {BlockUtility} util + */ + stackWarning(util, message) { + if (!util) return; + if (!util.thread) return; + if (!util.thread.stackClick) return; + const block = util.thread.blockGlowInFrame; + this.runtime.visualReport(block, message); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return ExtensionInfo; + } + + // utilities + getScratchCanvas() { + return this.runtime.renderer.canvas; + } + restyleExternalCanvas(canvas) { + canvas.style.position = "absolute"; // position above canvas without pushing it down + canvas.style.width = "100%"; + canvas.style.height = "100%"; + // we have no reason to register clicks on the three.js canvas, + // so make it click on the scratch canvas instead + canvas.style.pointerEvents = "none"; + } + appendElementAboveScratchCanvas(element) { + element.style.zIndex = 450; + if (this.sceneLayer === 'back') { + element.style.zIndex = 0; + } + this.getScratchCanvas().parentElement.prepend(element); + } + updateScratchCanvasRelayering() { + const canvas = this.getScratchCanvas(); + canvas.style.backgroundColor = "transparent"; + canvas.style.position = "relative"; // allows zIndex changes + if (Cast.toNumber(canvas.style.zIndex) < 1) { + canvas.style.zIndex = 1; + } + + // _backgroundColor4f[3] controls opacity + let lastOpacity = this.runtime.renderer._backgroundColor4f[3]; + if (this.sceneLayer === 'front') { + this.runtime.renderer.setBackgroundColor( + this.lastStageColor[0], + this.lastStageColor[1], + this.lastStageColor[2], + 1 + ); + } + if (this.sceneLayer === 'back') { + if ( + this.runtime.renderer._backgroundColor4f[0] !== this.lastStageColor[0] + || this.runtime.renderer._backgroundColor4f[1] !== this.lastStageColor[1] + || this.runtime.renderer._backgroundColor4f[2] !== this.lastStageColor[2] + ) { + // color likely changed to sum else + console.log("updated stage color"); + this.lastStageColor = this.runtime.renderer._backgroundColor4f; + } + this.runtime.renderer.setBackgroundColor(0, 0, 0, 0); + } + // update if changed + if (lastOpacity !== this.runtime.renderer._backgroundColor4f[3]) { + this.runtime.renderer.dirty = true; + } + } + needsToResizeCanvas() { + const stage = { + width: this.runtime.stageWidth, + height: this.runtime.stageHeight + } + return stage !== this.lastStageSizeWhenRendering; + } + mergeVertices(geometry) { + const vertices = geometry.attributes.position.array; + const vertexMap = {}; + const mergedVertices = []; + const newIndices = []; + let newIndex = 0; + + for (let i = 0; i < vertices.length; i += 3) { + const x = vertices[i]; + const y = vertices[i + 1]; + const z = vertices[i + 2]; + const key = `${x},${y},${z}`; + + if (vertexMap[key] === undefined) { + vertexMap[key] = newIndex; + mergedVertices.push(x, y, z); + newIndices.push(newIndex); + newIndex++; + } else { + newIndices.push(vertexMap[key]); + } + } + + geometry.setAttribute('position', new Three.Float32BufferAttribute(mergedVertices, 3)); + geometry.setIndex(new Three.Uint32BufferAttribute(newIndices, 1)); + + return geometry; + } + + performRaycast(raycaster, object) { + const geometry = object.geometry; + + const mergedGeometry = this.mergeVertices(geometry); + + const boundingGeometry = new Three.BufferGeometry().copy(mergedGeometry); + boundingGeometry.computeBoundingBox(); + boundingGeometry.boundingBox.applyMatrix4(object.matrixWorld); + + const intersection = raycaster.intersectObject(object, true); + + return intersection.length > 0; + } + + initialize() { + // dispose of the previous scene + this.dispose(); + this.scene = new Three.Scene(); + this.renderer = new Three.WebGLRenderer({ preserveDrawingBuffer: true, alpha: true }); + this.renderer.penguinMod = { + backgroundColor: 0x000000, + backgroundOpacity: 1 + } + this.renderer.setClearColor(0x000000, 1); + // add renderer canvas ontop of scratch canvas + const canvas = this.renderer.domElement; + this.runtime.prism_screenshot_externalCanvas = canvas; + + this.restyleExternalCanvas(canvas); + this.appendElementAboveScratchCanvas(canvas); + this.updateScratchCanvasRelayering(); + /* dev: test rendering by drawing a cube and see if it appears + // const geometry = new Three.BoxGeometry(1, 1, 1); + // const material = new Three.MeshBasicMaterial({ color: 0x00ff00 }); + // const cube = new Three.Mesh(geometry, material); + // this.scene.add(cube) + + dev update: it worked W + */ + } + render() { + if (!this.renderer) return; + if (!this.scene) return; + if (!this.camera) return; + if (this.needsToResizeCanvas()) { + this.lastStageSizeWhenRendering = { + width: this.runtime.stageWidth, + height: this.runtime.stageHeight + } + /* + multiply sizes because the stage looks like doo doo xd + we dont need to worry about multiplying 1920 * 2 since projects + shouldnt be using that large of a stage but instead a smaller size + with the same aspect ratio, penguinmod even says that + */ + this.renderer.setSize(this.lastStageSizeWhenRendering.width * 2, this.lastStageSizeWhenRendering.height * 2); + this.restyleExternalCanvas(this.renderer.domElement); + } + // when switching between project page & editor, we need to move the canvas again since it gets lost + /* todo: create layers so that iframe appears above 3d every time this is done */ + this.appendElementAboveScratchCanvas(this.renderer.domElement); + this.updateScratchCanvasRelayering(); + return new Promise((resolve) => { + // we do this to avoid HUGE lag when not waiting 1 tick + // and because it waits if the tab isnt focused + requestAnimationFrame(() => { + // renderer might not exist anymore + if (!this.renderer) return; + resolve(this.renderer.render(this.scene, this.camera)); + }) + }) + } + + setCameraPerspective2(args) { + if (this.camera) { + // remove existing camera + this.camera.remove(); + this.camera = null; + } + + const fov = Cast.toNumber(args.FOV); + const aspect = Cast.toNumber(args.AR); + const near = Cast.toNumber(args.NEAR); + const far = Cast.toNumber(args.FAR); + + this.camera = new Three.PerspectiveCamera(fov, aspect, near, far); + } + setCameraPerspective1(args) { + /* todo: make near and far be the same as the existing camera if there is one */ + const near = 0.1; + const far = 1000; + return this.setCameraPerspective2({ + FOV: args.FOV, + AR: args.AR, + NEAR: near, + FAR: far + }) + } + setCameraPerspective0(args) { + /* todo: make ar, near and far be the same as the existing camera if there is one */ + const ar = this.runtime.stageWidth / this.runtime.stageHeight; + const near = 0.1; + const far = 1000; + return this.setCameraPerspective2({ + FOV: args.FOV, + AR: ar, + NEAR: near, + FAR: far + }) + } + + setCameraPosition(args) { + if (!this.camera) return; + const position = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y), + z: Cast.toNumber(args.Z), + } + this.camera.position.set(position.x, position.y, position.z); + } + setCameraRotation(args) { + if (!this.camera) return; + const rotation = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y), + z: Cast.toNumber(args.Z), + } + // const euler = new Three.Euler(toRad(rotation.x), toRad(rotation.y), toRad(rotation.z)); + // this.camera.setRotationFromEuler(euler); + const euler = new Three.Euler(0, 0, 0); + this.camera.setRotationFromEuler(euler); + this.camera.rotateY(toRad(rotation.y)); + this.camera.rotateX(toRad(rotation.x)); + this.camera.rotateZ(toRad(rotation.z)); + } + getCameraPosition(args) { + if (!this.camera) return ""; + const v = args.VECTOR3; + if (!v) return ""; + if (!["x", "y", "z"].includes(v)) return ""; + return Cast.toNumber(this.camera.position[v]); + } + getCameraRotation(args) { + if (!this.camera) return ""; + const v = args.VECTOR3; + if (!v) return ""; + if (!["x", "y", "z"].includes(v)) return ""; + const rotation = Cast.toNumber(this.camera.rotation[v]); + // rotation is in radians, convert to degrees but round it + // a bit so that we get 46 instead of 45.999999999999996 + return toDegRounding(rotation); + } + + setSceneLayer(args) { + if (!this.renderer) return; + let lastSceneLayer = this.sceneLayer; + this.sceneLayer = "front"; + if (Cast.toString(args.SIDE) === 'back') { + this.sceneLayer = "back"; + } + if (this.sceneLayer !== lastSceneLayer) { + this.lastStageColor = this.runtime.renderer._backgroundColor4f; + } + this.appendElementAboveScratchCanvas(this.renderer.domElement); + this.updateScratchCanvasRelayering(); + } + setSceneBackgroundColor(args) { + if (!this.renderer) return; + const rgb = Cast.toRgbColorObject(args.COLOR); + const color = Color.rgbToDecimal(rgb); + this.renderer.penguinMod.backgroundColor = color; + this.renderer.setClearColor(color, this.renderer.penguinMod.backgroundOpacity); + } + setSceneBackgroundOpacity(args) { + if (!this.renderer) return; + let opacity = Cast.toNumber(args.OPACITY); + if (opacity > 100) opacity = 100; + if (opacity < 0) opacity = 0; + const backgroundOpac = 1 - (opacity / 100); + this.renderer.penguinMod.backgroundOpacity = backgroundOpac; + this.renderer.setClearColor(this.renderer.penguinMod.backgroundColor, backgroundOpac); + } + show3d() { + this.renderer.domElement.style.display = "" + } + hide3d() { + this.renderer.domElement.style.display = "none" + } + is3dVisible() { + return this.renderer.domElement.style.display === "" || this.renderer.domElement.style.display === "absolute" + } + + getCameraZoom() { + if (!this.camera) return ""; + return Cast.toNumber(this.camera.zoom) * 100; + } + setCameraZoom(args) { + if (!this.camera) return; + this.camera.zoom = Cast.toNumber(args.ZOOM) / 100; + this.camera.updateProjectionMatrix(); + } + + getCameraClipPlane(args) { + if (!this.camera) return ""; + const plane = args.CLIPPLANE; + if (!["near", "far"].includes(plane)) return ""; + return this.camera[plane]; + } + + getCameraAspectRatio() { + if (!this.camera) return ""; + return Cast.toNumber(this.camera.aspect); + } + getCameraFov() { + if (!this.camera) return ""; + return Cast.toNumber(this.camera.fov); + } + + isCameraPerspective() { + if (!this.camera) return false; + return Cast.toBoolean(this.camera.isPerspectiveCamera); + } + isCameraOrthographic() { + if (!this.camera) return false; + return Cast.toBoolean(!this.camera.isPerspectiveCamera); + } + + doesObjectExist(args) { + if (!this.scene) return false; + const name = Cast.toString(args.NAME); + // !! is easier to type than if (...) { return true; } return false; + return !!this.scene.getObjectByName(name); + } + + createGameObject(args, util, type) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + if (this.scene.getObjectByName(name)) return this.stackWarning(util, 'An object with this name already exists!'); + const position = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y), + z: Cast.toNumber(args.Z), + }; + let object; + switch (type) { + case 'sphere': { + const geometry = new Three.SphereGeometry(1); + const material = new Three.MeshStandardMaterial({ color: 0xffffff }); + const sphere = new Three.Mesh(geometry, material); + object = sphere; + break; + } + case 'plane': { + const geometry = new Three.PlaneGeometry(1, 1); + const material = new Three.MeshStandardMaterial({ color: 0xffffff }); + const plane = new Three.Mesh(geometry, material); + object = plane; + break; + } + case 'mesh': { + const url = Cast.toString(args.URL); + // switch loaders based on file type + let fileType = 'obj'; + switch (Cast.toString(args.FILETYPE)) { + case '.glb / .gltf': + fileType = 'glb'; + break; + case '.fbx': + fileType = 'fbx'; + break; + } + // we need to do a promise here so that stack continues on load + return new Promise((resolve) => { + let loader = MeshLoaders.OBJ; + switch (fileType) { + case 'glb': + loader = MeshLoaders.GLTF; + break; + case 'fbx': + loader = MeshLoaders.FBX; + break; + } + if (url in this.savedMeshes) { + const mesh = this.savedMeshes[url]; + object = mesh.clone(); + object.name = name; + this.existingSceneObjects.push(name); + object.isPenguinMod = true; + object.isMeshObj = true; + object.position.set(position.x, position.y, position.z); + this.scene.add(object); + resolve(); + return; + } + else { + loader.load(url, (object) => { + // success + if (loader === MeshLoaders.GLTF) { + object = object.scene; + } + if (loader === MeshLoaders.OBJ) { + const material = new Three.MeshStandardMaterial({ color: 0xffffff }); + material.wireframe = false; + this.updateMaterialOfObjObject(object, material); + this.savedMeshes[url] = object; + } + object.name = name; + console.log(object); + this.existingSceneObjects.push(name); + object.isPenguinMod = true; + object.isMeshObj = true; + object.position.set(position.x, position.y, position.z); + this.scene.add(object); + resolve(); + }, () => { }, (error) => { + console.warn('Failed to load 3D mesh obj;', error); + this.stackWarning(util, 'Failed to get the 3D mesh!'); + resolve(); + }) + } + }); + } + case 'light': { + const type = Cast.toString(args.LIGHTTYPE); + // switch type because there are different types of lights + let light; + switch (type) { + default: { + light = new Three.PointLight(0xffffff, 1, 100); + break; + } + } + object = light; + this.existingSceneLights.push(name); + break; + } + default: { + const geometry = new Three.BoxGeometry(1, 1, 1); + const material = new Three.MeshStandardMaterial({ color: 0xffffff }); + const cube = new Three.Mesh(geometry, material); + object = cube; + break; + } + } + object.name = name; + this.existingSceneObjects.push(name); + object.isPenguinMod = true; + object.position.set(position.x, position.y, position.z); + this.scene.add(object); + } + createCubeObject(args, util) { + this.createGameObject(args, util, 'cube'); + } + createSphereObject(args, util) { + this.createGameObject(args, util, 'sphere'); + } + createPlaneObject(args, util) { + this.createGameObject(args, util, 'plane'); + } + createMeshObject(args, util) { + this.createGameObject(args, util, 'mesh'); + } + createMeshObjectFileTyped(args, util) { + this.createGameObject(args, util, 'mesh'); + } + createLightObject(args, util) { + this.createGameObject(args, util, 'light'); + } + + getMaterialOfObjObject(object) { + let material; + object.traverse((child) => { + if (child instanceof Three.Mesh) { + material = child.material; + } + }); + return material; + } + updateMaterialOfObjObject(object, material) { + object.traverse((child) => { + if (child instanceof Three.Mesh) { + child.material = material; + } + }); + } + + setObjectPosition(args) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + const position = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y), + z: Cast.toNumber(args.Z), + }; + const object = this.scene.getObjectByName(name); + if (!object) return; + object.position.set(position.x, position.y, position.z); + } + setObjectRotation(args) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + const rotation = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y), + z: Cast.toNumber(args.Z), + }; + const object = this.scene.getObjectByName(name); + if (!object) return; + // const euler = new Three.Euler(toRad(rotation.x), toRad(rotation.y), toRad(rotation.z)); + // object.setRotationFromEuler(euler); + const euler = new Three.Euler(0, 0, 0); + object.setRotationFromEuler(euler); + object.rotateY(toRad(rotation.y)); + object.rotateX(toRad(rotation.x)); + object.rotateZ(toRad(rotation.z)); + } + setObjectSize(args) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + const size = { + x: Cast.toNumber(args.X) / 100, + y: Cast.toNumber(args.Y) / 100, + z: Cast.toNumber(args.Z) / 100, + }; + const object = this.scene.getObjectByName(name); + if (!object) return; + object.scale.set(size.x, size.y, size.z); + } + moveObjectUnits(args) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + const object = this.scene.getObjectByName(name); + if (!object) return; + + const amount = Cast.toNumber(args.AMOUNT); + const direction = new Three.Vector3(); + object.getWorldDirection(direction); + object.position.add(direction.multiplyScalar(amount)); + } + + getObjectPosition(args) { + if (!this.scene) return ""; + const name = Cast.toString(args.NAME); + const object = this.scene.getObjectByName(name); + if (!object) return ''; + const v = args.VECTOR3; + if (!v) return ""; + if (!["x", "y", "z"].includes(v)) return ""; + return Cast.toNumber(object.position[v]); + } + getObjectRotation(args) { + if (!this.scene) return ""; + const name = Cast.toString(args.NAME); + const object = this.scene.getObjectByName(name); + if (!object) return ''; + const v = args.VECTOR3; + if (!v) return ""; + if (!["x", "y", "z"].includes(v)) return ""; + const rotation = Cast.toNumber(object.rotation[v]); + // rotation is in radians, convert to degrees but round it + // a bit so that we get 46 instead of 45.999999999999996 + return toDegRounding(rotation); + } + getObjectSize(args) { + if (!this.scene) return ""; + const name = Cast.toString(args.NAME); + const object = this.scene.getObjectByName(name); + if (!object) return ''; + const v = args.VECTOR3; + if (!v) return ""; + if (!["x", "y", "z"].includes(v)) return ""; + return Cast.toNumber(object.scale[v]) * 100; + } + getObjectColor(args) { + if (!this.scene) return ""; + const name = Cast.toString(args.NAME); + const object = this.scene.getObjectByName(name); + if (!object) return ''; + return "#" + object.material.color.getHexString() + } + deleteObject(args) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + const object = this.scene.getObjectByName(name); + if (!object) return; + const isLight = object.isLight; + object.clear(); + this.scene.remove(object); + const idx = this.existingSceneObjects.indexOf(name); + this.existingSceneObjects.splice(idx, 1); + if (isLight) { + const lidx = this.existingSceneLights.indexOf(name); + this.existingSceneLights.splice(lidx, 1); + } + } + setObjectColor(args) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + const rgb = Cast.toRgbColorObject(args.COLOR); + const color = Color.rgbToDecimal(rgb); + const object = this.scene.getObjectByName(name); + if (!object) return; + if (object.isLight) { + object.color.set(color); + return + } + if (object.isMeshObj) { + const material = this.getMaterialOfObjObject(object); + if (!material) return; + material.color.set(color); + this.updateMaterialOfObjObject(object, material); + return; + } + object.material.color.set(color); + } + setObjectShading(args) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + const on = Cast.toString(args.ONOFF) === 'on'; + const object = this.scene.getObjectByName(name); + if (!object) return; + if (object.isLight) return; + if (object.isMeshObj) { + const material = this.getMaterialOfObjObject(object); + if (!material) return; + const color = '#' + material.color.getHexString(); + let newMat; + if (on) { + newMat = new Three.MeshStandardMaterial({ color: color }); + } else { + newMat = new Three.MeshBasicMaterial({ color: color }); + } + newMat.color.set(color); + this.updateMaterialOfObjObject(object, newMat); + return; + } + const color = '#' + object.material.color.getHexString(); + if (on) { + object.material = new Three.MeshStandardMaterial({ color: color }); + } else { + object.material = new Three.MeshBasicMaterial({ color: color }); + } + } + setObjectWireframe(args) { + if (!this.scene) return; + const name = Cast.toString(args.NAME); + const on = Cast.toString(args.ONOFF) === 'on'; + const object = this.scene.getObjectByName(name); + if (!object) return; + if (object.isLight) return; + if (object.isMeshObj) { + const material = this.getMaterialOfObjObject(object); + if (!material) return; + material.wireframe = on; + this.updateMaterialOfObjObject(object, material); + return; + } + object.material.wireframe = on; + } + + existingObjectsArray(args) { + const listType = Cast.toString(args.OBJECTLIST); + const validOptions = ["objects", "physical objects", "lights"]; + if (!validOptions.includes(listType)) return '[]'; + switch (listType) { + case 'objects': + return JSON.stringify(this.existingSceneObjects); + case 'lights': + return JSON.stringify(this.existingSceneLights); + case 'physical objects': { + const physical = this.existingSceneObjects.filter(objectName => { + return !this.existingSceneLights.includes(objectName); + }); + return JSON.stringify(physical); + } + default: + return '[]'; + } + } + + objectTouchingObject(args) { + if (!this.scene) return false; + const name1 = Cast.toString(args.NAME1); + const name2 = Cast.toString(args.NAME2); + const object1 = this.scene.getObjectByName(name1); + const object2 = this.scene.getObjectByName(name2); + if (!object1) return false; + if (!object2) return false; + if (object1.isLight) return false; // currently lights are not supported for collisions + if (object2.isLight) return false; // currently lights are not supported for collisions + const box1 = new Three.Box3().setFromObject(object1); + const box2 = new Three.Box3().setFromObject(object2); + const collision = box1.intersectsBox(box2); + return collision; + } + + pointTowardsObject(args) { + if (!this.scene) return false; + const name1 = Cast.toString(args.NAME1); + const name2 = Cast.toString(args.NAME2); + const object1 = this.scene.getObjectByName(name1); + const object2 = this.scene.getObjectByName(name2); + if (!object1) return false; + if (!object2) return false; + object1.lookAt(object2.position); + } + pointTowardsXYZ(args) { + if (!this.scene) return false; + const name = Cast.toString(args.NAME); + const object = this.scene.getObjectByName(name); + if (!object) return false; + const position = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y), + z: Cast.toNumber(args.Z) + }; + object.lookAt(position.x, position.y, position.z); + } + + MoveCameraBy(args) { + if (!this.camera) return; + const amount = Cast.toNumber(args.AMOUNT); + // comment so it updates bc github was having problems + const direction = new Three.Vector3(); + this.camera.getWorldDirection(direction); + this.camera.position.add(direction.multiplyScalar(amount)); + } + + changeCameraPosition(args) { + if (!this.camera) return; + this.camera.position.x += Cast.toNumber(args.X); + this.camera.position.y += Cast.toNumber(args.Y); + this.camera.position.z += Cast.toNumber(args.Z); + } + + changeCameraRotation(args) { + if (!this.camera) return; + this.camera.rotation.x += Cast.toNumber(args.X); + this.camera.rotation.y += Cast.toNumber(args.Y); + this.camera.rotation.z += Cast.toNumber(args.Z); + } + + raycastResultToReadable(result) { + const newResult = Clone.simple(result).map((intersection) => { + console.log(intersection.object.object.name); + return intersection.object.object.name; + }) + return newResult; + } + + rayCollision(args) { + if (!this.scene) return ''; + const ray = new Three.Raycaster(); + const origin = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y), + z: Cast.toNumber(args.Z), + }; + const direction = normalize({ + x: toRad(Cast.toNumber(args.DX)), + y: toRad(Cast.toNumber(args.DY)), + z: toRad(Cast.toNumber(args.DZ)), + }); + ray.set(new Three.Vector3(origin.x, origin.y, origin.z), new Three.Vector3(direction.x, direction.y, direction.z)); + const intersects = ray.intersectObjects(this.scene.children, true); + if (intersects.length === 0) return ''; + const first = intersects[0]; + return first.object.name; + } + rayCollisionCamera() { + if (!this.scene) return ''; + if (!this.camera) return ''; + const ray = new Three.Raycaster(); + ray.setFromCamera(new Three.Vector2(), this.camera); + const intersects = ray.intersectObjects(this.scene.children, true); + if (intersects.length === 0) return ''; + const first = intersects[0]; + return first.object.name; + } + rayCollisionArray(args) { + if (!this.scene) return '[]'; + const ray = new Three.Raycaster(); + const origin = { + x: Cast.toNumber(args.X), + y: Cast.toNumber(args.Y), + z: Cast.toNumber(args.Z), + }; + const direction = normalize({ + x: toRad(Cast.toNumber(args.DX)), + y: toRad(Cast.toNumber(args.DY)), + z: toRad(Cast.toNumber(args.DZ)), + }); + ray.set(new Three.Vector3(origin.x, origin.y, origin.z), new Three.Vector3(direction.x, direction.y, direction.z)); + const intersects = ray.intersectObjects(this.scene.children, true); + if (intersects.length === 0) return '[]'; + //const result = this.raycastResultToReadable(intersects); + return JSON.stringify(intersects); + } + rayCollisionCameraArray() { + if (!this.scene) return '[]'; + if (!this.camera) return '[]'; + const ray = new Three.Raycaster(); + ray.setFromCamera(new Three.Vector2(), this.camera); + const intersects = ray.intersectObjects(this.scene.children, true); + if (intersects.length === 0) return '[]'; + //const result = this.raycastResultToReadable(intersects); + return JSON.stringify(intersects); + } + + rayCollisionDistance(args) { + if (!this.scene) return ''; + const origin = new Three.Vector3( + Cast.toNumber(args.X), + Cast.toNumber(args.Y), + Cast.toNumber(args.Z), + ); + const direction = normalize(new Three.Vector3( + toRad(Cast.toNumber(args.DX)), + toRad(Cast.toNumber(args.DY)), + toRad(Cast.toNumber(args.DZ)), + )); + const ray = new Three.Raycaster(origin, direction, 0, args.DIS); + const intersects = ray.intersectObjects(this.scene.children, true); + if (intersects.length === 0) return ''; + const first = intersects[0]; + return first.object.name; + } + rayCollisionArrayDistance(args) { + if (!this.scene) return '[]'; + const origin = new Three.Vector3( + Cast.toNumber(args.X), + Cast.toNumber(args.Y), + Cast.toNumber(args.Z), + ); + const direction = normalize(new Three.Vector3( + toRad(Cast.toNumber(args.DX)), + toRad(Cast.toNumber(args.DY)), + toRad(Cast.toNumber(args.DZ)), + )); + const ray = new Three.Raycaster(origin, direction, 0, args.DIS); + const intersects = ray.intersectObjects(this.scene.children, true); + if (intersects.length === 0) return '[]'; + const result = this.raycastResultToReadable(intersects); + return JSON.stringify(result); + } + getObjectParent(args) { + if (!this.scene) return ''; + const name = Cast.toString(args.NAME); + const object = this.scene.getObjectByName(name); + if (!object) return ''; + if (!object.parent) return ''; + return object.parent.name; + } +} + +module.exports = Jg3DBlocks; diff --git a/local-scratch-vm/src/extensions/jg_3d/info.js b/local-scratch-vm/src/extensions/jg_3d/info.js new file mode 100644 index 0000000000000000000000000000000000000000..04ff6ec554392e5614e799cd82dcbee75428ade3 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_3d/info.js @@ -0,0 +1,440 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); + +const Icons = { + Cube: require('./cube.png'), + Sphere: require('./sphere.png'), + Plane: require('./plane.png'), + Light: require('./light.png'), + OBJ: require('./obj.png'), + Camera: require('./camera.png'), + Touching: require('./touching.png'), + Wireframe: require('./wireframe.png'), + Raycast: require('./raycast.png'), +} + +const blockIconURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAT6SURBVHhe7ZtdqBVVFMfXnHuvXG9i9/ZhWYGVYWoggqGVFSV9ywUpohch+lARFXwRAu1BEB97kNAKheiphyAKEqKXICIyKuhFS0Qh+yKvZqLXj+sZ//+99syZU/ecMx975pzDzA/WzFpzzszstWatvfeZmSMV3WXArkvHOMSPyFJIKRiChI6PL2kKAsWDFEqRJ6SDhgGcdeoAFG4ZRCNeNpujFNauIk40AblBVZH6fpx0mrPW6wjMOmson0OeUTU/anadB9sgvMbG+fdfhYGrPp3zpIaW8PMDr9kNIk9DuP+LxsqJPDLgRsgpVUXunyfy3ZtQkpwJbt/9hsjx8CiG6yAXVHWH6wCEdT42InJ6D5QsZ8DRZm8SOXfJ2orTNrs62J+QW1QVmXxHZJj9vSPOToqMbraG8gfkNlWzkbUPWA/hVTfO/7gTBurYpfPk+pl63P2v2A0icyE87xPGykDGBFWW3ynyLeu8CHDWFbtEDp2wtpLaj6Q78vsYsJQhTGQvvwvFVSElZAjD5lTYGkPiliTZ4STkdlVxIT7A4orqXQUeeBhiIxyGLFa1M3H6gNUQprtxft9aGJzF9YLzBC1je7Zx1qAsgrC9DxurA+0yYAxyWlWRNfi58jF74i6leyzg9sLtIj//ZW1lFHJW1f/Tyh1uN9U1MkPk/F67pV9AIGZsQJJetXab1rf6gAk+OPUZ5ucc4fuUr4+hDnYb9RBkhdH+Q9s+YGA+FisRUFZUH1FnezFTWLlK7XbEmgh57E4ozUNO70HHMT+sJZgjxp8J8uCPQB4wVu/Bn2B0PGG2Jp8K8y4ey4J9aw9QH8aCE2N01mlIHgCLdx8WCIQ0etpCMf0SHK9xsM5A6gCEPGolYeqlJXDcc/Jb0EUACLMAnaT/kJp54c9x53iAmwBYzO0u9g/skBxSH8SCVz2HpwhOAxDgLcSCgch4dJ+ZxTq/We08yCUAAd6DWLCjTDp/YJ0j1b071MyTXAMQgvmDv8zqHTDlk2I8T0sxAQAex2uWRYtOrM5xnHWecjxPS2EBCPDuwoKBsPMHn+XBOnfcccal8AAEeJw74MeWF95j6g5dC0CvUAXArktLFQC7Li1VAOy6tFQBsOvSUgXArktLFQC7Li3tA8B7cf3MrM5PdFt9YzYkfKTs/4JFHo/F+ODV+YtvYACONd4XIC0j0SoD/oWE92a8BSL3PAmloNtUWVjwenznSbsS4CNy7mxuYh37FcYikYkLndOqG5ycQPueEzn6u92AAoB0bGwSbw5CnlUVycD0/Uf11LgoAVxCr/mN4rchW1TtTJrLeRli3gQcRpFM/kQtJRkDUFuNC9Fclon9STMMsm8wL0BfRCj4EOSlrbQKAg4v3YzzIt0jzs+EpKrNVDtF4NsC36gq8ulekfEYb2WEJM2A4Zp4q5qGo3shHKNSkzUAAXwva46quDK/YXFO9bbEDcAIGvqY1RUG3cmjWFcBCAiT8qYxkb/D3GhBjADMel7k/EVrKE7bnKYPaAcbN4/KqTMw0D+89yGt5GzZh/1R5xHn50JcXzD3B4zwAuQjVTGt/B7TS/7lIco0GfDFDyJP7bCGwkcoX6nqnjwDENA0UPnHsQj+ABENAPpx73GrK8ihxn+N+h2WGgNhZPF8jGBHIF9CDop/62jjMyuuS7NnQH/ecPSTPUNRpyml4S1I1PGNkIqKiqIRuQYHeAEU9sBGCQAAAABJRU5ErkJggg=="; + +const seperator = "---"; + +function infoMenu(array) { + return { + acceptReporters: true, + items: array.map(item => ({ text: item, value: item })) + } +} +function infoLabel(text) { + return { + blockType: BlockType.LABEL, + text: text + } +} +function infoArgument(value, extra, extra2) { + switch (typeof value) { + case "number": + return { type: ArgumentType.NUMBER, defaultValue: value }; + case "boolean": + return { type: ArgumentType.BOOLEAN, defaultValue: value }; + case "string": + switch (value) { + case "COLOR": + return { type: ArgumentType.COLOR }; + case "ANGLE": + return { type: ArgumentType.ANGLE }; + case "MATRIX": + return { type: ArgumentType.MATRIX }; + case "NOTE": + return { type: ArgumentType.NOTE, defaultValue: 60 }; + case "POLYGON": + return { type: ArgumentType.POLYGON, nodes: extra }; + case "IMAGE": + return { type: ArgumentType.IMAGE, dataURI: extra, alt: extra2 }; + default: + return { type: ArgumentType.STRING, defaultValue: value }; + } + } +} +function infoArgumentMenu(type, menu) { + return { + type: type, + menu: menu + } +} + +function createCommandBlock(opcode, text, args, icon, hidden) { + const obj = { + opcode: opcode, + text: text ? text : opcode, + blockType: BlockType.COMMAND + } + if (args) { + obj.arguments = args; + } + if (icon) { + obj.blockIconURI = icon; + } + if (hidden === true) { + obj.hideFromPalette = true; + } + return obj; +} +function createReporterBlock(opcode, text, args, icon, disablemonitor) { + const obj = { + opcode: opcode, + text: text ? text : opcode, + blockType: BlockType.REPORTER + } + if (typeof disablemonitor === 'boolean') { + obj.disableMonitor = disablemonitor; + } + if (args) { + obj.arguments = args; + } + if (icon) { + obj.blockIconURI = icon; + } + return obj; +} +function createBooleanBlock(opcode, text, args, icon) { + const obj = { + opcode: opcode, + text: text ? text : opcode, + blockType: BlockType.BOOLEAN, + disableMonitor: true + } + if (args) { + obj.arguments = args; + } + if (icon) { + obj.blockIconURI = icon; + } + return obj; +} + +module.exports = { + id: 'jg3d', + name: '3D', + color1: '#B100FE', + color2: '#8600C3', + color3: '#5B0088', + blockIconURI: blockIconURI, + blocks: [ + infoLabel("Initializing your scene"), + + createCommandBlock('initialize', 'create 3D scene'), + createCommandBlock('dispose', 'remove 3D scene'), + seperator, + createCommandBlock( + 'setCameraPerspective0', + 'set scene camera to perspective camera with fov: [FOV]', + { + FOV: infoArgument(70) + }, + Icons.Camera + ), + createCommandBlock( + 'setCameraPerspective1', + 'set scene camera to perspective camera with fov: [FOV] aspect ratio: [AR]', + { + FOV: infoArgument(70), + AR: infoArgument(480 / 360) + }, + Icons.Camera + ), + createCommandBlock( + 'setCameraPerspective2', + 'set scene camera to perspective camera with fov: [FOV] aspect ratio: [AR] and only render objects within [NEAR] and [FAR] units of the camera', + { + FOV: infoArgument(70), + AR: infoArgument(480 / 360), + NEAR: infoArgument(0.1), + FAR: infoArgument(1000) + }, + Icons.Camera + ), + seperator, + createCommandBlock('setCameraOrthographic0', 'set scene camera to orthographic camera', null, Icons.Camera), + createCommandBlock( + 'setCameraOrthographic1', + 'set scene camera to orthographic camera with left plane: [LEFT] right plane: [RIGHT] top plane: [TOP] bottom plane: [BOTTOM]', + { + LEFT: infoArgument(-480 / 2), + RIGHT: infoArgument(480 / 2), + TOP: infoArgument(360 / 2), + BOTTOM: infoArgument(-360 / 2) + }, + Icons.Camera + ), + createCommandBlock( + 'setCameraOrthographic2', + 'set scene camera to orthographic camera with left plane: [LEFT] right plane: [RIGHT] top plane: [TOP] bottom plane: [BOTTOM] and only render objects within [NEAR] and [FAR] units of the camera', + { + LEFT: infoArgument(-480 / 2), + RIGHT: infoArgument(480 / 2), + TOP: infoArgument(360 / 2), + BOTTOM: infoArgument(-360 / 2), + NEAR: infoArgument(1), + FAR: infoArgument(1000) + }, + Icons.Camera + ), + seperator, + createCommandBlock('render'), + + infoLabel("Scene customization"), + + createCommandBlock('setSceneLayer', "move 3D scene layer to [SIDE]", { + SIDE: infoArgumentMenu(ArgumentType.STRING, "frontBack") + }), + createCommandBlock('setSceneBackgroundColor', "set background color to [COLOR]", { + COLOR: infoArgument("COLOR") + }), + createCommandBlock('setSceneBackgroundOpacity', "set background transparency to [OPACITY]%", { + OPACITY: infoArgument(100) + }), + createCommandBlock("show3d", "show 3D scene", {}), + createCommandBlock("hide3d", "hide 3D scene", {}), + createBooleanBlock("is3dVisible", "is 3D scene visible?", {}), + + infoLabel("Camera controls"), + + createCommandBlock( + 'MoveCameraBy', + 'move camera by [AMOUNT]', + { + AMOUNT: infoArgument(10) + }, + Icons.Camera + ), + createCommandBlock( + 'setCameraPosition', + 'set camera position to x: [X] y: [Y] z: [Z]', + { + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0) + }, + Icons.Camera + ), + createCommandBlock( + 'changeCameraPosition', + 'change camera position by x: [X] y: [Y] z: [Z]', + { + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0) + }, + Icons.Camera + ), + createCommandBlock( + 'setCameraRotation', + 'set camera rotation to x: [X] y: [Y] z: [Z]', + { + X: infoArgument('ANGLE'), + Y: infoArgument('ANGLE'), + Z: infoArgument('ANGLE') + }, + Icons.Camera + ), + createCommandBlock( + 'changeCameraRotation', + 'change camera rotation by x: [X] y: [Y] z: [Z]', + { + X: infoArgument('ANGLE'), + Y: infoArgument('ANGLE'), + Z: infoArgument('ANGLE') + }, + Icons.Camera + ), + createCommandBlock('setCameraZoom', 'set camera zoom to [ZOOM]%', { + ZOOM: infoArgument(100) + }, Icons.Camera), + createReporterBlock("getCameraClipPlane", "camera [CLIPPLANE]", { + CLIPPLANE: infoArgumentMenu(ArgumentType.STRING, "clippingPlanes") + }, Icons.Camera), + createReporterBlock("getCameraPosition", "camera [VECTOR3] position", { + VECTOR3: infoArgumentMenu(ArgumentType.STRING, "vector3") + }, Icons.Camera), + createReporterBlock("getCameraRotation", "camera [VECTOR3] rotation", { + VECTOR3: infoArgumentMenu(ArgumentType.STRING, "vector3") + }, Icons.Camera), + createReporterBlock("getCameraAspectRatio", "camera aspect ratio", null, Icons.Camera), + createReporterBlock("getCameraZoom", "camera zoom", null, Icons.Camera), + createReporterBlock("getCameraFov", "camera fov", null, Icons.Camera), + seperator, + createBooleanBlock("isCameraPerspective", "is scene camera a perspective camera?", null, Icons.Camera), + createBooleanBlock("isCameraOrthographic", "is scene camera an orthographic camera?", null, Icons.Camera), + + infoLabel("Objects"), + + createBooleanBlock("doesObjectExist", "object named [NAME] exists?", { + NAME: infoArgument("Object1") + }), + createReporterBlock("existingObjectsArray", "existing [OBJECTLIST]", { + OBJECTLIST: infoArgumentMenu(ArgumentType.STRING, "objectTypeList") + }), + seperator, + createCommandBlock('createCubeObject', 'create cube named [NAME] at x: [X] y: [Y] z: [Z]', { + NAME: infoArgument("Object1"), + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0) + }, Icons.Cube), + createCommandBlock('createSphereObject', 'create sphere named [NAME] at x: [X] y: [Y] z: [Z]', { + NAME: infoArgument("Object1"), + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0) + }, Icons.Sphere), + createCommandBlock('createPlaneObject', 'create plane named [NAME] at x: [X] y: [Y] z: [Z]', { + NAME: infoArgument("Object1"), + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0) + }, Icons.Plane), + createCommandBlock('createMeshObject', 'create mesh named [NAME] with .obj data: [URL] at x: [X] y: [Y] z: [Z]', { + NAME: infoArgument("Object1"), + URL: infoArgument("data:text/plain;base64,"), + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0) + }, Icons.OBJ, true), + createCommandBlock('createMeshObjectFileTyped', 'create mesh named [NAME] with [FILETYPE] data: [URL] at x: [X] y: [Y] z: [Z]', { + NAME: infoArgument("Object1"), + FILETYPE: infoArgumentMenu(ArgumentType.STRING, "meshFileTypes"), + URL: infoArgument("data:text/plain;base64,"), + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0) + }, Icons.OBJ), + createCommandBlock('createLightObject', 'create [LIGHTTYPE] light named [NAME] at x: [X] y: [Y] z: [Z]', { + LIGHTTYPE: infoArgumentMenu(ArgumentType.STRING, "lightType"), + NAME: infoArgument("Light1"), + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0) + }, Icons.Light), + seperator, + createCommandBlock('moveObjectUnits', 'move object named [NAME] by [AMOUNT]', { + NAME: infoArgument("Object1"), + AMOUNT: infoArgument(10) + }), + createCommandBlock("setObjectPosition", "move object named [NAME] to x: [X] y: [Y] z: [Z]", { + NAME: infoArgument("Object1"), + X: infoArgument(1), + Y: infoArgument(1), + Z: infoArgument(1) + }), + createCommandBlock("setObjectRotation", "set rotation of object named [NAME] to x: [X] y: [Y] z: [Z]", { + NAME: infoArgument("Object1"), + X: infoArgument('ANGLE'), + Y: infoArgument('ANGLE'), + Z: infoArgument('ANGLE') + }), + createCommandBlock("setObjectSize", "set size of object named [NAME] to x: [X]% y: [Y]% z: [Z]%", { + NAME: infoArgument("Object1"), + X: infoArgument(100), + Y: infoArgument(100), + Z: infoArgument(100) + }), + createCommandBlock('pointTowardsObject', 'point object named [NAME1] towards object named [NAME2]', { + NAME1: infoArgument("Object1"), + NAME2: infoArgument("Object2"), + }), + createCommandBlock('pointTowardsXYZ', 'point object named [NAME] towards x: [X] y: [Y] z: [Z]', { + NAME: infoArgument("Object1"), + X: infoArgument(31), + Y: infoArgument(26), + Z: infoArgument(47), + }), + createReporterBlock("getObjectPosition", "[VECTOR3] position of object named [NAME]", { + VECTOR3: infoArgumentMenu(ArgumentType.STRING, "vector3"), + NAME: infoArgument("Object1"), + }), + createReporterBlock("getObjectRotation", "[VECTOR3] rotation of object named [NAME]", { + VECTOR3: infoArgumentMenu(ArgumentType.STRING, "vector3"), + NAME: infoArgument("Object1"), + }), + createReporterBlock("getObjectSize", "[VECTOR3] size of object named [NAME]", { + VECTOR3: infoArgumentMenu(ArgumentType.STRING, "vector3"), + NAME: infoArgument("Object1"), + }), + createReporterBlock("getObjectColor", "hex color of object named [NAME]", { + NAME: infoArgument("Object1"), + }), + createReporterBlock("getObjectParent", "parent of object named [NAME]", { + NAME: infoArgument("Object1"), + }), + seperator, + createBooleanBlock("objectTouchingObject", "object [NAME1] touching object [NAME2]?", { + NAME1: infoArgument("Object1"), + NAME2: infoArgument("Object2"), + }, Icons.Touching), + seperator, + createCommandBlock("deleteObject", "remove object named [NAME]", { + NAME: infoArgument("Object1") + }), + createCommandBlock("setObjectColor", "recolor object named [NAME] to [COLOR]", { + NAME: infoArgument("Object1"), + COLOR: infoArgument("COLOR"), + }), + createCommandBlock("setObjectShading", "turn [ONOFF] shading on object named [NAME]", { + ONOFF: infoArgumentMenu(ArgumentType.STRING, "onoff"), + NAME: infoArgument("Object1"), + }), + createCommandBlock("setObjectWireframe", "turn [ONOFF] wireframe view on object named [NAME]", { + ONOFF: infoArgumentMenu(ArgumentType.STRING, "onoff"), + NAME: infoArgument("Object1"), + }, Icons.Wireframe), + seperator, + createReporterBlock("rayCollision", "first object in raycast from x: [X] y: [Y] z: [Z] with direction x: [DX] y: [DY] z: [DZ]", { + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0), + DX: infoArgument(0), + DY: infoArgument(0), + DZ: infoArgument(0), + }, Icons.Raycast, true), + createReporterBlock("rayCollisionArray", "raycast result from x: [X] y: [Y] z: [Z] with direction x: [DX] y: [DY] z: [DZ]", { + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0), + DX: infoArgument(0), + DY: infoArgument(0), + DZ: infoArgument(0), + }, Icons.Raycast, true), + createReporterBlock("rayCollisionDistance", "first object in raycast from x: [X] y: [Y] z: [Z] with direction x: [DX] y: [DY] z: [DZ] with a max distance of [DIS]", { + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0), + DX: infoArgument(0), + DY: infoArgument(0), + DZ: infoArgument(0), + DIS: infoArgument(10) + }, Icons.Raycast, true), + createReporterBlock("rayCollisionArrayDistance", "raycast result from x: [X] y: [Y] z: [Z] with direction x: [DX] y: [DY] z: [DZ] with a max distance of [DIS]", { + X: infoArgument(0), + Y: infoArgument(0), + Z: infoArgument(0), + DX: infoArgument(0), + DY: infoArgument(0), + DZ: infoArgument(0), + DIS: infoArgument(10) + }, Icons.Raycast, true), + createReporterBlock("rayCollisionCamera", "first object from raycast in camera center", { + }, Icons.Raycast, true), + createReporterBlock("rayCollisionCameraArray", "raycast result starting from the camera center", { + }, Icons.Raycast, true) + ], + menus: { + cameraType: infoMenu(["perspective", "orthographic"]), + lightType: infoMenu(["point"]), + clippingPlanes: infoMenu(["near", "far"]), + frontBack: infoMenu(["front", "back"]), + vector3: infoMenu(["x", "y", "z"]), + vector2: infoMenu(["x", "y"]), + onoff: infoMenu(["on", "off"]), + objectTypeList: infoMenu(["objects", "physical objects", "lights"]), + meshFileTypes: infoMenu([".obj", ".glb / .gltf", ".fbx"]) + } +} \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_3d/light.png b/local-scratch-vm/src/extensions/jg_3d/light.png new file mode 100644 index 0000000000000000000000000000000000000000..57cb44cbe7c3114b9ddc717839b8db8e7b77d202 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/light.png differ diff --git a/local-scratch-vm/src/extensions/jg_3d/obj.png b/local-scratch-vm/src/extensions/jg_3d/obj.png new file mode 100644 index 0000000000000000000000000000000000000000..8017a7a14b4abe2092c4f1aa45b31f28060565a8 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/obj.png differ diff --git a/local-scratch-vm/src/extensions/jg_3d/plane.png b/local-scratch-vm/src/extensions/jg_3d/plane.png new file mode 100644 index 0000000000000000000000000000000000000000..3b411622d8721a1130c5eb7edd921caab9f80a3c Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/plane.png differ diff --git a/local-scratch-vm/src/extensions/jg_3d/raycast.png b/local-scratch-vm/src/extensions/jg_3d/raycast.png new file mode 100644 index 0000000000000000000000000000000000000000..8e5f06842a7727b3b025c90712778c1c10bacc0e Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/raycast.png differ diff --git a/local-scratch-vm/src/extensions/jg_3d/sphere.png b/local-scratch-vm/src/extensions/jg_3d/sphere.png new file mode 100644 index 0000000000000000000000000000000000000000..b680b5d6a64375cea8a7bd7ffcc20cb25468a930 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/sphere.png differ diff --git a/local-scratch-vm/src/extensions/jg_3d/touching.png b/local-scratch-vm/src/extensions/jg_3d/touching.png new file mode 100644 index 0000000000000000000000000000000000000000..f38fbc9abaedcc9710f976bff6e257afcdec79a6 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/touching.png differ diff --git a/local-scratch-vm/src/extensions/jg_3d/wireframe.png b/local-scratch-vm/src/extensions/jg_3d/wireframe.png new file mode 100644 index 0000000000000000000000000000000000000000..b60b8de67d84a60d914e63ed9525159f62bdddf4 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3d/wireframe.png differ diff --git a/local-scratch-vm/src/extensions/jg_3dVr/controller.png b/local-scratch-vm/src/extensions/jg_3dVr/controller.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b19a9ddaaf5988b9d50e9648e986db7ce558a0 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3dVr/controller.png differ diff --git a/local-scratch-vm/src/extensions/jg_3dVr/headset.pdn b/local-scratch-vm/src/extensions/jg_3dVr/headset.pdn new file mode 100644 index 0000000000000000000000000000000000000000..ccb0056b9affd1c0034bd2f44d51131e335b1fb7 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_3dVr/headset.pdn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c75486986bfd5a36c0bd7574aefa45d0daed5689c5d172d277ce0c909b85837b +size 123021 diff --git a/local-scratch-vm/src/extensions/jg_3dVr/icon.png b/local-scratch-vm/src/extensions/jg_3dVr/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..355f22e4d3a457e89618413cd177bdbe826872ab Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_3dVr/icon.png differ diff --git a/local-scratch-vm/src/extensions/jg_3dVr/index.js b/local-scratch-vm/src/extensions/jg_3dVr/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6883cbefe07e7022c8e9f6e17686d60daf89d2be --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_3dVr/index.js @@ -0,0 +1,634 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const Icon = require('./icon.png'); +const IconController = require('./controller.png'); + +const SESSION_TYPE = "immersive-vr"; + +// thanks to twoerner94 for quaternion-to-euler on npm +function quaternionToEuler(quat) { + const q0 = quat[0]; + const q1 = quat[1]; + const q2 = quat[2]; + const q3 = quat[3]; + + const Rx = Math.atan2(2 * (q0 * q1 + q2 * q3), 1 - (2 * (q1 * q1 + q2 * q2))); + const Ry = Math.asin(2 * (q0 * q2 - q3 * q1)); + const Rz = Math.atan2(2 * (q0 * q3 + q1 * q2), 1 - (2 * (q2 * q2 + q3 * q3))); + + return [Rx, Ry, Rz]; +}; + +function toRad(deg) { + return deg * (Math.PI / 180); +} +function toDeg(rad) { + return rad * (180 / Math.PI); +} +function toDegRounding(rad) { + const result = toDeg(rad); + if (!String(result).includes('.')) return result; + const split = String(result).split('.'); + const endingDecimals = split[1].substring(0, 3); + if ((endingDecimals === '999') && (split[1].charAt(3) === '9')) return Number(split[0]) + 1; + return Number(split[0] + '.' + endingDecimals); +} + +/** + * Class for 3D VR blocks + */ +class Jg3DVrBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + */ + this.runtime = runtime; + this.open = false; + this._3d = {}; + this.three = {}; + // We'll store a wake lock reference here: + this.wakeLock = null; + + if (!this.runtime.ext_jg3d) { + vm.extensionManager.loadExtensionURL('jg3d') + .then(() => { + this._3d = this.runtime.ext_jg3d; + this.three = this._3d.three; + }); + } else { + this._3d = this.runtime.ext_jg3d; + this.three = this._3d.three; + } + } + /** + * Metadata for this extension and its blocks. + * @returns {object} + */ + getInfo() { + return { + id: 'jg3dVr', + name: '3D VR', + color1: '#B100FE', + color2: '#8000BC', + blockIconURI: Icon, + blocks: [ + // CORE + { + opcode: 'isSupported', + text: 'is vr supported?', + blockType: BlockType.BOOLEAN, + disableMonitor: true + }, + { + opcode: 'createSession', + text: 'create vr session', + blockType: BlockType.COMMAND + }, + { + opcode: 'closeSession', + text: 'close vr session', + blockType: BlockType.COMMAND + }, + { + opcode: 'isOpened', + text: 'is vr open?', + blockType: BlockType.BOOLEAN, + disableMonitor: true + }, + '---', + { + opcode: 'attachObject', + text: 'attach camera to object named [OBJECT]', + blockType: BlockType.COMMAND, + arguments: { + OBJECT: { + type: ArgumentType.STRING, + defaultValue: "Object1" + } + } + }, + { + opcode: 'detachObject', + text: 'detach camera from object', + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'getControllerPosition', + text: 'controller #[INDEX] position [VECTOR3]', + blockType: BlockType.REPORTER, + blockIconURI: IconController, + disableMonitor: true, + arguments: { + INDEX: { + type: ArgumentType.NUMBER, + menu: 'count' + }, + VECTOR3: { + type: ArgumentType.STRING, + menu: 'vector3' + } + } + }, + { + opcode: 'getControllerRotation', + text: 'controller #[INDEX] rotation [VECTOR3]', + blockType: BlockType.REPORTER, + blockIconURI: IconController, + disableMonitor: true, + arguments: { + INDEX: { + type: ArgumentType.NUMBER, + menu: 'count' + }, + VECTOR3: { + type: ArgumentType.STRING, + menu: 'vector3' + } + } + }, + { + opcode: 'getControllerSide', + text: 'side of controller #[INDEX]', + blockType: BlockType.REPORTER, + blockIconURI: IconController, + disableMonitor: true, + arguments: { + INDEX: { + type: ArgumentType.NUMBER, + menu: 'count' + } + } + }, + '---', + { + opcode: 'getControllerStick', + text: 'joystick axis [XY] of controller #[INDEX]', + blockType: BlockType.REPORTER, + blockIconURI: IconController, + disableMonitor: true, + arguments: { + XY: { + type: ArgumentType.STRING, + menu: 'vector2' + }, + INDEX: { + type: ArgumentType.NUMBER, + menu: 'count' + } + } + }, + { + opcode: 'getControllerTrig', + text: 'analog value of [TRIGGER] trigger on controller #[INDEX]', + blockType: BlockType.REPORTER, + blockIconURI: IconController, + disableMonitor: true, + arguments: { + TRIGGER: { + type: ArgumentType.STRING, + menu: 'trig' + }, + INDEX: { + type: ArgumentType.NUMBER, + menu: 'count' + } + } + }, + { + opcode: 'getControllerButton', + text: 'button [BUTTON] on controller #[INDEX] pressed?', + blockType: BlockType.BOOLEAN, + blockIconURI: IconController, + disableMonitor: true, + arguments: { + BUTTON: { + type: ArgumentType.STRING, + menu: 'butt' + }, + INDEX: { + type: ArgumentType.NUMBER, + menu: 'count' + } + } + }, + { + opcode: 'getControllerTouching', + text: '[BUTTON] on controller #[INDEX] touched?', + blockType: BlockType.BOOLEAN, + blockIconURI: IconController, + disableMonitor: true, + arguments: { + BUTTON: { + type: ArgumentType.STRING, + menu: 'buttAll' + }, + INDEX: { + type: ArgumentType.NUMBER, + menu: 'count' + } + } + }, + ], + menus: { + vector3: { + acceptReporters: true, + items: [ + "x", + "y", + "z", + ].map(item => ({ text: item, value: item })) + }, + vector2: { + acceptReporters: true, + items: [ + "x", + "y", + ].map(item => ({ text: item, value: item })) + }, + butt: { + acceptReporters: true, + items: [ + "a", + "b", + "x", + "y", + "joystick", + ].map(item => ({ text: item, value: item })) + }, + trig: { + acceptReporters: true, + items: [ + "back", + "side", + ].map(item => ({ text: item, value: item })) + }, + buttAll: { + acceptReporters: true, + items: [ + "a button", + "b button", + "x button", + "y button", + "joystick", + "back trigger", + "side trigger", + ].map(item => ({ text: item, value: item })) + }, + count: { + acceptReporters: true, + items: [ + "1", + "2", + ].map(item => ({ text: item, value: item })) + }, + } + }; + } + + // util + _getRenderer() { + if (!this._3d) return; + return this._3d.renderer; + } + _getGamepad(indexFrom1) { + const index = Cast.toNumber(indexFrom1) - 1; + + const three = this._3d; + if (!three.scene) return; + const renderer = this._getRenderer(); + if (!renderer) return; + const session = renderer.xr.getSession(); + if (!session) return; + + const sources = session.inputSources; + const controller = sources[index]; + if (!controller) return; + + const gamepad = controller.gamepad; + return gamepad; + } + _getController(index) { + const renderer = this._getRenderer(); + if (!renderer) return null; + // try to use grip first (which typically has position/quaternion) + const grip = renderer.xr.getControllerGrip(index); + return grip || renderer.xr.getController(index); + } + _getInputSource(index) { + const renderer = this._getRenderer(); + if (!renderer) return null; + const session = renderer.xr.getSession(); + if (!session) return null; + const sources = session.inputSources; + return sources[index] || null; + } + + _disposeImmersive() { + this.session = null; + const renderer = this._getRenderer(); + if (!renderer) return; + renderer.xr.enabled = false; + // Clear the animation loop so Three.js stops calling it + renderer.setAnimationLoop(null); + } + + async _requestWakeLock() { + if ('wakeLock' in navigator) { + try { + // Request a screen wake lock to prevent idling + this.wakeLock = await navigator.wakeLock.request('screen'); + this.wakeLock.addEventListener('release', () => { + console.log('Wake Lock was released'); + }); + console.log('Wake Lock is active'); + } catch (err) { + console.error('Failed to acquire wake lock:', err); + } + } + } + + _releaseWakeLock() { + if (this.wakeLock) { + this.wakeLock.release(); + this.wakeLock = null; + } + } + + async _createImmersive() { + if (!('xr' in navigator)) return false; + const renderer = this._getRenderer(); + if (!renderer) return false; + + const sessionInit = { + optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] + }; + const session = await navigator.xr.requestSession(SESSION_TYPE, sessionInit); + this.session = session; + this.open = true; + + renderer.xr.enabled = true; + await renderer.xr.setSession(session); + + // Request the wake lock so that the session keeps updating even when idle + await this._requestWakeLock(); + + // When session ends, reset state and release wake lock. + session.addEventListener("end", () => { + this.open = false; + this._disposeImmersive(); + this._releaseWakeLock(); + }); + + // Request a reference space (store it so we can use it for the poses) + session.requestReferenceSpace("local").then(space => { + this.localSpace = space; + }); + + // Use Three.js's setAnimationLoop to drive the render loop + renderer.setAnimationLoop((time, frame) => { + if (!this.open) return; + const threed = this._3d; + if (!threed.camera || !threed.scene) return; + + // Render the scene + renderer.render(threed.scene, threed.camera); + + // Update controller poses if available + if (this.localSpace && frame) { + this.controllerPoses = {}; + const sources = session.inputSources; + for (let i = 0; i < sources.length; i++) { + const inputSource = sources[i]; + const pose = frame.getPose(inputSource.targetRaySpace, this.localSpace); + if (pose) { + this.controllerPoses[i] = { + position: pose.transform.position, // {x, y, z} + orientation: pose.transform.orientation // {x, y, z, w} + }; + } + } + } + }); + + return session; + } + + // blocks + isSupported() { + if (!('xr' in navigator)) return false; + return navigator.xr.isSessionSupported(SESSION_TYPE); + } + isOpened() { + return this.open; + } + + createSession() { + if (this.open) return; + if (this.session) return; + return this._createImmersive(); + } + closeSession() { + this.open = false; + if (!this.session) return; + return this.session.end(); + } + + // extra: attach/detach camera to/from an object in the scene + attachObject(args) { + const three = this._3d; + if (!three.scene) return; + if (!three.camera) return; + const name = Cast.toString(args.OBJECT); + const object = three.scene.getObjectByName(name); + if (!object) return; + object.add(three.camera); + } + detachObject() { + const three = this._3d; + if (!three.scene) return; + if (!three.camera) return; + three.scene.add(three.camera); + } + + // Controller input blocks follow + getControllerPosition(args) { + if (!this._3d || !this._3d.scene) return ""; + + const index = Cast.toNumber(args.INDEX) - 1; + const v = args.VECTOR3; + if (!v || !["x", "y", "z"].includes(v)) return ""; + + // Use stored pose information if available + if (this.controllerPoses && this.controllerPoses[index]) { + return Cast.toNumber(this.controllerPoses[index].position[v]); + } + + const renderer = this._getRenderer(); + if (!renderer) return ""; + const controller = this._getController(index); + if (!controller) return ""; + controller.updateMatrixWorld(true); + + // Fallback: get world position via Three.js + const Vector3 = (this.three && this.three.three && this.three.three.Vector3) + ? this.three.three.Vector3 + : this.three.Vector3; + const position = new Vector3(); + controller.getWorldPosition(position); + return Cast.toNumber(position[v]); + } + + getControllerRotation(args) { + if (!this._3d || !this._3d.scene) return ""; + + const index = Cast.toNumber(args.INDEX) - 1; + const v = args.VECTOR3; + if (!v || !["x", "y", "z"].includes(v)) return ""; + + // Use stored orientation if available + if (this.controllerPoses && this.controllerPoses[index]) { + const o = this.controllerPoses[index].orientation; + + const Quaternion = (this.three && this.three.three && this.three.three.Quaternion) + ? this.three.three.Quaternion + : this.three.Quaternion; + const quaternion = new Quaternion(o.x, o.y, o.z, o.w); + + const Euler = (this.three && this.three.three && this.three.three.Euler) + ? this.three.three.Euler + : this.three.Euler; + const euler = new Euler(0, 0, 0, 'YXZ'); + euler.setFromQuaternion(quaternion, 'YXZ'); + return toDegRounding(euler[v]); + } + + const renderer = this._getRenderer(); + if (!renderer) return ""; + const controller = this._getController(index); + if (!controller) return ""; + controller.updateMatrixWorld(true); + + const Quaternion = (this.three && this.three.three && this.three.three.Quaternion) + ? this.three.three.Quaternion + : this.three.Quaternion; + const quaternion = new Quaternion(); + controller.getWorldQuaternion(quaternion); + + const Euler = (this.three && this.three.three && this.three.three.Euler) + ? this.three.three.Euler + : this.three.Euler; + const euler = new Euler(0, 0, 0, 'YXZ'); + euler.setFromQuaternion(quaternion, 'YXZ'); + return toDegRounding(euler[v]); + } + + getControllerSide(args) { + const three = this._3d; + if (!three.scene) return ""; + const renderer = this._getRenderer(); + if (!renderer) return ""; + const session = renderer.xr.getSession(); + if (!session) return ""; + + const sources = session.inputSources; + const index = Cast.toNumber(args.INDEX) - 1; + const controller = sources[index]; + if (!controller) return ""; + + return controller.handedness; + } + getControllerStick(args) { + const gamepad = this._getGamepad(args.INDEX); + if (!gamepad) return 0; + // For 'y', use axis index 3, otherwise default to index 2. + if (Cast.toString(args.XY) === "y") { + return gamepad.axes[3]; + } else { + return gamepad.axes[2]; + } + } + getControllerTrig(args) { + const gamepad = this._getGamepad(args.INDEX); + if (!gamepad) return 0; + if (Cast.toString(args.TRIGGER) === "side") { + return gamepad.buttons[1] ? gamepad.buttons[1].value : 0; + } else { + return gamepad.buttons[0] ? gamepad.buttons[0].value : 0; + } + } + getControllerButton(args) { + const gamepad = this._getGamepad(args.INDEX); + if (!gamepad) return false; + const inputSource = this._getInputSource(Cast.toNumber(args.INDEX) - 1); + let handedness = 'right'; + if (inputSource && inputSource.handedness) { + handedness = inputSource.handedness; + } + + const button = Cast.toString(args.BUTTON).toLowerCase(); + if (handedness === 'right') { + switch (button) { + case 'a': + return gamepad.buttons[4] && gamepad.buttons[4].pressed; + case 'b': + return gamepad.buttons[5] && gamepad.buttons[5].pressed; + case 'joystick': + return gamepad.buttons[3] && gamepad.buttons[3].pressed; + } + } else if (handedness === 'left') { + switch (button) { + case 'x': + return gamepad.buttons[4] && gamepad.buttons[4].pressed; + case 'y': + return gamepad.buttons[5] && gamepad.buttons[5].pressed; + case 'joystick': + return gamepad.buttons[3] && gamepad.buttons[3].pressed; + } + } + return false; + } + getControllerTouching(args) { + const gamepad = this._getGamepad(args.INDEX); + if (!gamepad) return false; + const inputSource = this._getInputSource(Cast.toNumber(args.INDEX) - 1); + let handedness = 'right'; + if (inputSource && inputSource.handedness) { + handedness = inputSource.handedness; + } + + const button = Cast.toString(args.BUTTON).toLowerCase(); + if (handedness === 'right') { + switch (button) { + case 'a button': + return gamepad.buttons[4] && gamepad.buttons[4].touched; + case 'b button': + return gamepad.buttons[5] && gamepad.buttons[5].touched; + case 'joystick': + return gamepad.buttons[3] && gamepad.buttons[3].touched; + case 'back trigger': + return gamepad.buttons[0] && gamepad.buttons[0].touched; + case 'side trigger': + return gamepad.buttons[1] && gamepad.buttons[1].touched; + } + } else if (handedness === 'left') { + switch (button) { + case 'x button': + return gamepad.buttons[4] && gamepad.buttons[4].touched; + case 'y button': + return gamepad.buttons[5] && gamepad.buttons[5].touched; + case 'joystick': + return gamepad.buttons[3] && gamepad.buttons[3].touched; + case 'back trigger': + return gamepad.buttons[0] && gamepad.buttons[0].touched; + case 'side trigger': + return gamepad.buttons[1] && gamepad.buttons[1].touched; + } + } + return false; + } +} + +module.exports = Jg3DVrBlocks; diff --git a/local-scratch-vm/src/extensions/jg_animation/index.js b/local-scratch-vm/src/extensions/jg_animation/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4ca08c83e798adfd5e6e06a2462dae332ee17a7e --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_animation/index.js @@ -0,0 +1,1309 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const Clone = require('../../util/clone'); + +const getStateOfSprite = (target) => { + return { + x: target.x, + y: target.y, + size: target.size, + stretch: Clone.simple(target.stretch), // array + transform: Clone.simple(target.transform), // array + direction: target.direction, + rotationStyle: target.rotationStyle, + visible: target.visible, + effects: Clone.simple(target.effects || {}), // object + currentCostume: target.currentCostume, + tintColor: target.tintColor + }; +}; +const setStateOfSprite = (target, state) => { + target.setXY(state.x, state.y); + target.setSize(state.size); + target.setStretch(...state.stretch); + target.setTransform(state.transform); + target.setDirection(state.direction); + target.setRotationStyle(state.rotationStyle); + target.setVisible(state.visible); + if (state.effects) { + for (const effect in state.effects) { + target.setEffect(effect, state.effects[effect]); + } + } + target.setCostume(state.currentCostume); +}; + +// i've decided to tell ChatGPT to generate these due to some conditions: +// - the color util does NOT have these implemented +// - we know hsvToDecimal will ONLY get an HSV generated by decimalToHSV, and we know hsvToDecimal will have decimals in it's params +// - these functions need to be as performant as possible (i dont know how to do that, so the AI may know better) +// we already only run these if we really need to anyways, as it will be slow +// +// i could be completely wrong and these functions suck, but i dont really have any way of judging that +// this seems to be good for now, we only use them for tintColor anyways to make sure its not a mess +function decimalToHSV(decimalColor, hsv = { h: 0, s: 0, v: 0 }) { + const r = (decimalColor >> 16) & 255; + const g = (decimalColor >> 8) & 255; + const b = decimalColor & 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + + let h; + + // Calculate hue + if (delta === 0) { + h = 0; + } else if (max === r) { + h = (0.5 + ((g - b) / delta) % 6) | 0; + } else if (max === g) { + h = (0.5 + ((b - r) / delta + 2)) | 0; + } else { + h = (0.5 + ((r - g) / delta + 4)) | 0; + } + + hsv.h = (0.5 + (h * 60 + 360) % 360) | 0; + hsv.s = max === 0 ? 0 : (delta / max); + hsv.v = max / 255; + + return hsv; +} +function hsvToDecimal(h, s, v) { + const c = v * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = v - c; + + let r, g, b; + + if (h < 60) { + [r, g, b] = [c, x, 0]; + } else if (h < 120) { + [r, g, b] = [x, c, 0]; + } else if (h < 180) { + [r, g, b] = [0, c, x]; + } else if (h < 240) { + [r, g, b] = [0, x, c]; + } else if (h < 300) { + [r, g, b] = [x, 0, c]; + } else { + [r, g, b] = [c, 0, x]; + } + + const decimalR = (0.5 + (r + m) * 255) | 0; + const decimalG = (0.5 + (g + m) * 255) | 0; + const decimalB = (0.5 + (b + m) * 255) | 0; + + return (decimalR << 16) | (decimalG << 8) | decimalB; +} + +/** + * @param {number} time should be 0-1 + * @param {number} a value at 0 + * @param {number} b value at 1 + * @returns {number} + */ +const interpolate = (time, a, b) => { + // don't restrict range of time as some easing functions are expected to go outside the range + const multiplier = b - a; + const result = time * multiplier + a; + return result; +}; + +const snap = (x) => 1; +const snapcenter = (x) => Math.round(x); +const snapend = (x) => Math.ceil(x); + +const linear = (x) => x; + +const sine = (x, dir) => { + switch (dir) { + case "in": { + return 1 - Math.cos((x * Math.PI) / 2); + } + case "out": { + return Math.sin((x * Math.PI) / 2); + } + case "in out": { + return -(Math.cos(Math.PI * x) - 1) / 2; + } + default: + return 0; + } +}; + +const quad = (x, dir) => { + switch (dir) { + case "in": { + return x * x; + } + case "out": { + return 1 - (1 - x) * (1 - x); + } + case "in out": { + return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; + } + default: + return 0; + } +}; + +const cubic = (x, dir) => { + switch (dir) { + case "in": { + return x * x * x; + } + case "out": { + return 1 - Math.pow(1 - x, 3); + } + case "in out": { + return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; + } + default: + return 0; + } +}; + +const quart = (x, dir) => { + switch (dir) { + case "in": { + return x * x * x * x; + } + case "out": { + return 1 - Math.pow(1 - x, 4); + } + case "in out": { + return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2; + } + default: + return 0; + } +}; + +const quint = (x, dir) => { + switch (dir) { + case "in": { + return x * x * x * x * x; + } + case "out": { + return 1 - Math.pow(1 - x, 5); + } + case "in out": { + return x < 0.5 + ? 16 * x * x * x * x * x + : 1 - Math.pow(-2 * x + 2, 5) / 2; + } + default: + return 0; + } +}; + +const expo = (x, dir) => { + switch (dir) { + case "in": { + return x === 0 ? 0 : Math.pow(2, 10 * x - 10); + } + case "out": { + return x === 1 ? 1 : 1 - Math.pow(2, -10 * x); + } + case "in out": { + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 + ? Math.pow(2, 20 * x - 10) / 2 + : (2 - Math.pow(2, -20 * x + 10)) / 2; + } + default: + return 0; + } +}; + +const circ = (x, dir) => { + switch (dir) { + case "in": { + return 1 - Math.sqrt(1 - Math.pow(x, 2)); + } + case "out": { + return Math.sqrt(1 - Math.pow(x - 1, 2)); + } + case "in out": { + return x < 0.5 + ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 + : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2; + } + default: + return 0; + } +}; + +const back = (x, dir) => { + switch (dir) { + case "in": { + const c1 = 1.70158; + const c3 = c1 + 1; + return c3 * x * x * x - c1 * x * x; + } + case "out": { + const c1 = 1.70158; + const c3 = c1 + 1; + return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); + } + case "in out": { + const c1 = 1.70158; + const c2 = c1 * 1.525; + return x < 0.5 + ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2 + : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; + } + default: + return 0; + } +}; + +const elastic = (x, dir) => { + switch (dir) { + case "in": { + const c4 = (2 * Math.PI) / 3; + return x === 0 + ? 0 + : x === 1 + ? 1 + : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4); + } + case "out": { + const c4 = (2 * Math.PI) / 3; + return x === 0 + ? 0 + : x === 1 + ? 1 + : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; + } + case "in out": { + const c5 = (2 * Math.PI) / 4.5; + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 + ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + + 1; + } + default: + return 0; + } +}; + +const bounce = (x, dir) => { + switch (dir) { + case "in": { + return 1 - bounce(1 - x, "out"); + } + case "out": { + const n1 = 7.5625; + const d1 = 2.75; + if (x < 1 / d1) { + return n1 * x * x; + } else if (x < 2 / d1) { + return n1 * (x -= 1.5 / d1) * x + 0.75; + } else if (x < 2.5 / d1) { + return n1 * (x -= 2.25 / d1) * x + 0.9375; + } else { + return n1 * (x -= 2.625 / d1) * x + 0.984375; + } + } + case "in out": { + return x < 0.5 + ? (1 - bounce(1 - 2 * x, "out")) / 2 + : (1 + bounce(2 * x - 1, "out")) / 2; + } + default: + return 0; + } +}; + +const EasingMethods = { + linear, + sine, + quad, + cubic, + quart, + quint, + expo, + circ, + back, + elastic, + bounce, + snap, + snapcenter, + snapend, +}; + +class AnimationExtension { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + this.animations = Object.create(null); + this.progressingTargets = []; + this.progressingTargetData = Object.create(null); + + this.runtime.on('RUNTIME_PRE_PAUSED', () => { + for (const targetId in this.progressingTargetData) { + const targetData = this.progressingTargetData[targetId]; + for (const animationName in targetData) { + const animationData = targetData[animationName]; + animationData.projectPaused = true; + } + } + this.runtime.updateCurrentMSecs(); + this.runtime.emit('ANIMATIONS_FORCE_STEP'); + }); + this.runtime.on('RUNTIME_UNPAUSED', () => { + this.runtime.updateCurrentMSecs(); // currentMSecs is the same as when we originally paused, fix that + for (const targetId in this.progressingTargetData) { + const targetData = this.progressingTargetData[targetId]; + for (const animationName in targetData) { + const animationData = targetData[animationName]; + animationData.projectPaused = false; + } + } + }); + } + + now() { + return this.runtime.currentMSecs; + } + + deserialize(data) { + this.animations = data; + } + serialize() { + return this.animations; + } + orderCategoryBlocks(blocks) { + const buttons = { + create: blocks[0], + delete: blocks[1] + }; + const varBlock = blocks[2]; + blocks.splice(0, 3); + // create the variable block xml's + const varBlocks = Object.keys(this.animations) + .map(animationName => varBlock.replace('{animationId}', animationName)); + if (varBlocks.length <= 0) { + return [buttons.create]; + } + // push the button to the top of the var list + varBlocks.reverse(); + varBlocks.push(buttons.delete); + varBlocks.push(buttons.create); + // merge the category blocks and variable blocks into one block list + blocks = varBlocks + .reverse() + .concat(blocks); + return blocks; + } + + getInfo() { + return { + id: "jgAnimation", + name: "Animation", + isDynamic: true, + orderBlocks: this.orderCategoryBlocks.bind(this), + blocks: [ + { opcode: 'createAnimation', text: 'New Animation', blockType: BlockType.BUTTON, }, + { opcode: 'deleteAnimation', text: 'Delete an Animation', blockType: BlockType.BUTTON, }, + { + opcode: 'getAnimation', text: '[ANIMATION]', blockType: BlockType.REPORTER, + arguments: { + ANIMATION: { menu: 'animations', defaultValue: '{animationId}', type: ArgumentType.STRING, } + }, + }, + { text: "Animations", blockType: BlockType.LABEL, }, + { + opcode: "playAnimation", + blockType: BlockType.COMMAND, + text: "play [ANIM] [OFFSET] and [FORWARDS] after last keyframe", + arguments: { + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + OFFSET: { + type: ArgumentType.STRING, + menu: 'offsetMenu', + }, + FORWARDS: { + type: ArgumentType.STRING, + menu: 'forwardsMenu', + }, + }, + }, + { + opcode: "pauseAnimation", + blockType: BlockType.COMMAND, + text: "pause [ANIM]", + arguments: { + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "unpauseAnimation", + blockType: BlockType.COMMAND, + text: "unpause [ANIM]", + arguments: { + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "stopAnimation", + blockType: BlockType.COMMAND, + text: "stop [ANIM]", + arguments: { + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { text: "Keyframes", blockType: BlockType.LABEL, }, + { + opcode: "addStateKeyframe", + blockType: BlockType.COMMAND, + text: "add current state with [EASING] [DIRECTION] as keyframe with duration [LENGTH] in animation [ANIM]", + arguments: { + EASING: { + type: ArgumentType.STRING, + menu: 'easingMode', + }, + DIRECTION: { + type: ArgumentType.STRING, + menu: 'easingDir', + }, + LENGTH: { + type: ArgumentType.NUMBER, + defaultValue: 1, + }, + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "addJSONKeyframe", + blockType: BlockType.COMMAND, + text: "add keyframe JSON [JSON] as keyframe in animation [ANIM]", + arguments: { + JSON: { + type: ArgumentType.STRING, + defaultValue: '{}', + }, + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "setStateKeyframe", + blockType: BlockType.COMMAND, + text: "set keyframe [IDX] in animation [ANIM] to current state with [EASING] [DIRECTION] and duration [LENGTH] ", + arguments: { + IDX: { + type: ArgumentType.NUMBER, + defaultValue: '1', + }, + EASING: { + type: ArgumentType.STRING, + menu: 'easingMode', + }, + DIRECTION: { + type: ArgumentType.STRING, + menu: 'easingDir', + }, + LENGTH: { + type: ArgumentType.NUMBER, + defaultValue: 1, + }, + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "setJSONKeyframe", + blockType: BlockType.COMMAND, + text: "set keyframe [IDX] in animation [ANIM] to JSON [JSON]", + arguments: { + IDX: { + type: ArgumentType.NUMBER, + defaultValue: '1', + }, + JSON: { + type: ArgumentType.STRING, + defaultValue: '{}', + }, + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "deleteKeyframe", + blockType: BlockType.COMMAND, + text: "delete keyframe [IDX] from [ANIM]", + arguments: { + IDX: { + type: ArgumentType.NUMBER, + defaultValue: '1', + }, + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "deleteAllKeyframes", + blockType: BlockType.COMMAND, + text: "delete all keyframes [ANIM]", + arguments: { + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "getKeyframe", + blockType: BlockType.REPORTER, + text: "get keyframe [IDX] from [ANIM]", + arguments: { + IDX: { + type: ArgumentType.NUMBER, + defaultValue: '1', + }, + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "getKeyframeCount", + blockType: BlockType.REPORTER, + disableMonitor: true, + text: "amount of keyframes in [ANIM]", + arguments: { + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "isPausedAnimation", + blockType: BlockType.BOOLEAN, + disableMonitor: true, + hideFromPalette: true, + text: "is [ANIM] paused?", + arguments: { + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "isPropertyAnimation", + blockType: BlockType.BOOLEAN, + disableMonitor: true, + text: "is [ANIM] [ANIMPROP]?", + arguments: { + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + ANIMPROP: { + type: ArgumentType.STRING, + menu: 'animationDataProperty', + }, + }, + }, + { text: "Operations", blockType: BlockType.LABEL, }, + { + opcode: "goToKeyframe", + blockType: BlockType.COMMAND, + text: "go to keyframe [IDX] in [ANIM]", + arguments: { + IDX: { + type: ArgumentType.NUMBER, + defaultValue: '1', + }, + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + { + opcode: "snapToKeyframe", + blockType: BlockType.COMMAND, + text: "snap to keyframe [IDX] in [ANIM]", + arguments: { + IDX: { + type: ArgumentType.NUMBER, + defaultValue: '1', + }, + ANIM: { + type: ArgumentType.STRING, + menu: 'animations', + }, + }, + }, + ], + menus: { + animations: '_animationsMenu', + easingMode: { + acceptReporters: true, + items: Object.keys(EasingMethods), + }, + easingDir: { + acceptReporters: true, + items: ["in", "out", "in out"], + }, + animationDataProperty: { + acceptReporters: false, + items: ["playing", "paused"], + }, + offsetMenu: { + acceptReporters: false, + items: [ + { text: "relative to current state", value: "relative" }, + { text: "snapped to first keyframe", value: "snapped" } + ], + }, + forwardsMenu: { + acceptReporters: false, + items: [ + { text: "stay", value: "stay" }, + { text: "reset to original state", value: "reset" }, + ], + }, + } + }; + } + + _animationsMenu() { + const animations = Object.keys(this.animations); + if (animations.length <= 0) { + return [ + { + text: '', + value: '' + } + ]; + } + return animations.map(animation => ({ + text: animation, + value: animation + })); + } + _parseKeyframeOrKeyframes(string) { + let json; + try { + json = JSON.parse(string); + } catch { + json = {}; + } + if (typeof json !== 'object') { + return {}; + } + if (Array.isArray(json)) { + for (const item of json) { + if (typeof item !== 'object') { + return {}; + } + } + } + return json; + } + _tweenValue(start, end, easeMethod, easeDirection, progress) { + if (!Object.prototype.hasOwnProperty.call(EasingMethods, easeMethod)) { + // Unknown method + return start; + } + const easingFunction = EasingMethods[easeMethod]; + + const tweened = easingFunction(progress, easeDirection); + return interpolate(tweened, start, end); + } + _progressAnimation(target, startState, endState, mode, direction, progress) { + const tweenNum = (start, end) => { + return this._tweenValue(start, end, mode, direction, progress); + }; + const staticValue = tweenNum(0, 1); + target.setXY( + tweenNum(startState.x, endState.x), + tweenNum(startState.y, endState.y) + ); + target.setSize(tweenNum(startState.size, endState.size)); + target.setStretch( + tweenNum(startState.stretch[0], endState.stretch[0]), + tweenNum(startState.stretch[1], endState.stretch[1]) + ); + target.setTransform([ + tweenNum(startState.transform[0], endState.transform[0]), + tweenNum(startState.transform[1], endState.transform[1]) + ]); + target.setDirection(tweenNum(startState.direction, endState.direction)); + target.setRotationStyle(Math.round(staticValue) === 0 ? startState.rotationStyle : endState.rotationStyle); + target.setVisible(Math.round(staticValue) === 0 ? startState.visible : endState.visible); + for (const effect in startState.effects) { + if (effect === 'tintColor' && startState.effects.tintColor !== endState.effects.tintColor) { + const startHsv = decimalToHSV(startState.effects.tintColor - 1); + const endHsv = decimalToHSV(endState.effects.tintColor - 1); + const currentHsv = { + h: tweenNum(startHsv.h, endHsv.h), + s: tweenNum(startHsv.s, endHsv.s), + v: tweenNum(startHsv.v, endHsv.v), + }; + target.setEffect('tintColor', hsvToDecimal(currentHsv.h, currentHsv.s, currentHsv.v)); + continue; + } + target.setEffect(effect, tweenNum(startState.effects[effect], endState.effects[effect])); + } + target.setCostume(Math.round(staticValue) === 0 ? startState.currentCostume : endState.currentCostume); + } + + createAnimation() { + const newAnimation = prompt('Create animation named:', 'animation ' + (Object.keys(this.animations).length + 1)); + if (!newAnimation) return; + if (newAnimation in this.animations) return alert(`"${newAnimation}" is taken!`); + this.animations[newAnimation] = { + keyframes: [] + }; + vm.emitWorkspaceUpdate(); + this.serialize(); + } + deleteAnimation() { + const animationName = prompt('Which animation would you like to delete?'); + if (animationName in this.animations) { + for (const target of this.runtime.targets) { + this.stopAnimation({ + ANIM: animationName + }, { + target + }); + } + delete this.animations[animationName]; + } + vm.emitWorkspaceUpdate(); + this.serialize(); + } + + getAnimation(args) { + const animationName = Cast.toString(args.ANIMATION); + if (!(animationName in this.animations)) return '{}'; + return JSON.stringify(this.animations[animationName]); + } + + addKeyframe(animation, state) { + if (!(animation in this.animations)) { + return; + } + this.animations[animation].keyframes.push(state); + } + setKeyframe(animation, state, idx) { + if (!(animation in this.animations)) { + return; + } + const keyframes = this.animations[animation].keyframes; + if (idx > keyframes.length - 1) { + return; + } + if (idx < 0) { + return; + } + keyframes[idx] = state; + } + addStateKeyframe(args, util) { + const animationName = Cast.toString(args.ANIM); + const state = getStateOfSprite(util.target); + this.addKeyframe(animationName, { + ...state, + easingMode: Cast.toString(args.EASING), + easingDir: Cast.toString(args.DIRECTION), + keyframeLength: Cast.toNumber(args.LENGTH) + }); + } + addJSONKeyframe(args) { + const animationName = Cast.toString(args.ANIM); + const parsedKeyframe = this._parseKeyframeOrKeyframes(args.JSON); + if (Array.isArray(parsedKeyframe)) { + for (const keyframe of parsedKeyframe) { + this.addKeyframe(animationName, keyframe); + } + } else { + this.addKeyframe(animationName, parsedKeyframe); + } + } + setStateKeyframe(args, util) { + const animationName = Cast.toString(args.ANIM); + const index = Cast.toNumber(args.IDX) - 1; + const state = getStateOfSprite(util.target); + this.setKeyframe(animationName, { + ...state, + easingMode: Cast.toString(args.EASING), + easingDir: Cast.toString(args.DIRECTION), + keyframeLength: Cast.toNumber(args.LENGTH) + }, index); + } + setJSONKeyframe(args) { + const animationName = Cast.toString(args.ANIM); + const index = Cast.toNumber(args.IDX) - 1; + const parsedKeyframe = this._parseKeyframeOrKeyframes(args.JSON); + if (Array.isArray(parsedKeyframe)) { + return; + } else { + this.setKeyframe(animationName, parsedKeyframe, index); + } + } + deleteKeyframe(args) { + const animationName = Cast.toString(args.ANIM); + const idx = Cast.toNumber(args.IDX); + if (!(animationName in this.animations)) { + return; + } + this.animations[animationName].keyframes.splice(idx - 1, 1); + } + deleteAllKeyframes(args) { + const animationName = Cast.toString(args.ANIM); + if (!(animationName in this.animations)) { + return; + } + this.animations[animationName].keyframes = []; + } + getKeyframe(args) { + const animationName = Cast.toString(args.ANIM); + const idx = Cast.toNumber(args.IDX) - 1; + if (!(animationName in this.animations)) { + return '{}'; + } + const animation = this.animations[animationName]; + const keyframe = animation.keyframes[idx]; + if (!keyframe) return '{}'; + return JSON.stringify(keyframe); + } + getKeyframeCount(args) { + const animationName = Cast.toString(args.ANIM); + if (!(animationName in this.animations)) { + return '{}'; + } + const animation = this.animations[animationName]; + return animation.keyframes.length; + } + + goToKeyframe(args, util) { + const animationName = Cast.toString(args.ANIM); + const idx = Cast.toNumber(args.IDX) - 1; + if (!(animationName in this.animations)) { + return; + } + const animation = this.animations[animationName]; + const keyframe = animation.keyframes[idx]; + if (!keyframe) return; + // start animating + const spriteTarget = util.target; + const currentState = getStateOfSprite(spriteTarget); + const startTime = this.now(); + const endTime = this.now() + (keyframe.keyframeLength * 1000); // 2.65s should be 2650ms + if (endTime <= startTime) { + // this frame is instant + setStateOfSprite(spriteTarget, keyframe); + return; + } + // this will run each step + let finishedAnim = false; + const frameHandler = () => { + const currentTime = this.now(); + if (currentTime >= endTime) { + this.runtime.off('RUNTIME_STEP_START', frameHandler); + setStateOfSprite(spriteTarget, keyframe); + finishedAnim = true; + return; + } + const progress = (currentTime - startTime) / (endTime - startTime); + this._progressAnimation(spriteTarget, currentState, keyframe, keyframe.easingMode, keyframe.easingDir, progress); + }; + frameHandler(); + this.runtime.once('PROJECT_STOP_ALL', () => { + if (!finishedAnim) { + // finishedAnim is only true if we already removed it + this.runtime.off('RUNTIME_STEP_START', frameHandler); + } + }); + this.runtime.on('RUNTIME_STEP_START', frameHandler); + } + snapToKeyframe(args, util) { + const animationName = Cast.toString(args.ANIM); + const idx = Cast.toNumber(args.IDX) - 1; + if (!(animationName in this.animations)) { + return; + } + const animation = this.animations[animationName]; + const keyframe = animation.keyframes[idx]; + if (!keyframe) return; + setStateOfSprite(util.target, keyframe); + } + + // MULTIPLE ANIMATIONS CAN PLAY AT ONCE ON THE SAME SPRITE! remember this + playAnimation(args, util) { + const spriteTarget = util.target; + const id = spriteTarget.id; + const animationName = Cast.toString(args.ANIM); + const isRelative = args.OFFSET !== 'snapped'; + const isForwards = args.FORWARDS !== 'reset'; + if (!(animationName in this.animations)) { + return; + } + const animation = this.animations[animationName]; + const firstKeyframe = animation.keyframes[0]; + // check if we are unpausing + let existingAnimationState = this.progressingTargetData[id]; + if (this.progressingTargets.includes(id) && existingAnimationState && existingAnimationState[animationName]) { + // we are playing this animation already? + const animationState = existingAnimationState[animationName]; + if (animationState.paused) { + animationState.paused = false; + return; + } + if (!animationState.forceStop) { + return; // this animation isnt stopped, still actively playing + } else { + // force an animation update to fully cancel the animation + // console.log('before', performance.now()); + this.runtime.emit('ANIMATIONS_FORCE_SPECIFIC_STEP', id, animationName); + // console.log('after', performance.now()); + } + } + // we can start initializing our animation, but first check if we can skip a lot of work here + if (!firstKeyframe) { + return; + } + // there are a couple cases where we can do nothing or do little to nothing + // relative mode basically ignores the first keyframe, we only care about things after + // if we are relative, if we are ignoring the first keyframe and the second keyframe doesnt exist, we can just do nothing + // forwards mode entails we want to stay in the state that the last keyframe put us in, the name comes from what CSS calls it + // if we are relative and we arent going forwards, then nothing should happen (second keyframe doesnt exist and we ignored the first) + // if we arent relative and we arent going forwards, then nothing should happen (we shouldnt be in the state of the first keyframe) + const secondKeyframe = animation.keyframes[1]; + if (!secondKeyframe) { + if (isForwards && !isRelative) { + // we really should only do this if we arent relative & we should stay in this state when the animation ends + setStateOfSprite(spriteTarget, firstKeyframe); + } + // we are relative OR we shouldnt stay in the state of the last keyframe + return; + } + // initialize for animation + if (!this.progressingTargets.includes(id)) { + // we are playing any animation, so we need to say we are animating atm + this.progressingTargets.push(id); + } + if (!existingAnimationState) { + // we are playing any animation, initialize data + const data = Object.create(null); + this.progressingTargetData[id] = data; + existingAnimationState = this.progressingTargetData[id]; + } + // set our data + existingAnimationState[animationName] = {}; + const animationState = existingAnimationState[animationName]; + animationState.forceStop = false; + animationState.paused = false; + animationState.projectPaused = false; + // we can start animating now + // some of our math needs to allow our offset if we are in relative mode + // there are some exceptions to relative mode: + // - tintColor should only be the current state's color until a keyframe changes it from the first + // - some effects should act like multipliers and others should add to each keyframes effects + // - rotation mode shoould only be the current state's rotation mode on the first keyframe + // - costume should stay the same until the animation changes the costume from the first keyframe + const finalAnimation = Clone.simple(animation); + // patchy fix, but it makes the animation actually be timed properly + finalAnimation.keyframes[0].keyframeLength = 0.001; // 1ms + const initialState = getStateOfSprite(spriteTarget); + const fakeEffects = { tintColor: 0xffffff + 1 }; + if (isRelative) { + // update the keyframes of the animation + let initialCostume = firstKeyframe.currentCostume ?? initialState.currentCostume; + let initialRotation = firstKeyframe.rotationStyle ?? initialState.rotationStyle; + let initialTintColor = (firstKeyframe.effects || fakeEffects).tintColor ?? fakeEffects.tintColor; + let shouldUpdateCostume = false; + let shouldUpdateRotationStyle = false; + let shouldUpdateTintColor = false; + for (const keyframe of finalAnimation.keyframes) { + // offset based on initial position + keyframe.x -= firstKeyframe.x; + keyframe.y -= firstKeyframe.y; + keyframe.size /= firstKeyframe.size / 100; + keyframe.stretch = [keyframe.stretch[0] / (firstKeyframe.stretch[0] / 100), keyframe.stretch[1] / (firstKeyframe.stretch[1] / 100)]; + keyframe.transform = [keyframe.transform[0] - firstKeyframe.transform[0], keyframe.transform[1] - firstKeyframe.transform[1]]; + keyframe.direction -= firstKeyframe.direction - 90; + // change regulars + keyframe.x += initialState.x; + keyframe.y += initialState.y; + keyframe.size *= initialState.size / 100; + keyframe.stretch = [keyframe.stretch[0] * (initialState.stretch[0] / 100), keyframe.stretch[1] * (initialState.stretch[1] / 100)]; + keyframe.transform = [keyframe.transform[0] + initialState.transform[0], keyframe.transform[1] + initialState.transform[1]]; + keyframe.direction += initialState.direction - 90; + // exceptions + if (!shouldUpdateCostume) { + shouldUpdateCostume = initialCostume !== keyframe.currentCostume; + } + if (!shouldUpdateRotationStyle) { + shouldUpdateRotationStyle = initialRotation !== keyframe.rotationStyle; + } + if (!shouldUpdateTintColor) { + shouldUpdateTintColor = initialTintColor !== (keyframe.effects || fakeEffects).tintColor; + } + // handle exceptions + if (!shouldUpdateCostume) { + keyframe.currentCostume = initialState.currentCostume; + } + if (!shouldUpdateRotationStyle) { + keyframe.rotationStyle = initialState.rotationStyle; + } + if (!shouldUpdateTintColor) { + if (!keyframe.effects) keyframe.effects = {}; + keyframe.effects.tintColor = initialState.effects.tintColor; + } + for (const effect in keyframe.effects) { + if (effect === 'tintColor') continue; + const value = keyframe.effects[effect]; + const initValue = initialState.effects[effect]; + switch (effect) { + case 'ghost': + // 0 for invis, 1 for visible + const newGhost = (1 - (value / 100)) * (1 - (initValue / 100)); + keyframe.effects[effect] = (1 - newGhost) * 100; + break; + default: + keyframe.effects[effect] += initialState.effects[effect]; + break; + } + } + } + } + if (!isRelative) { + setStateOfSprite(spriteTarget, firstKeyframe); + } + // play animation + const stopAllHandler = () => { + animationState.forceStop = true; + this.runtime.emit('ANIMATIONS_FORCE_SPECIFIC_STEP', id, animationName); + } + const forceSpecificStepHandler = (targetId, targetAnimationName) => { + if (targetId !== id) return; + if (targetAnimationName !== animationName) return; + // yep he's talking to us + // console.log('forced step', targetId, targetAnimationName, animationState.forceStop); + // console.log('during', performance.now()); + frameHandler(); + }; + const animationEnded = (forceStop) => { + if (!isForwards) { + setStateOfSprite(spriteTarget, initialState); + } else if (!forceStop) { + const lastKeyframe = finalAnimation.keyframes[finalAnimation.keyframes.length - 1]; + setStateOfSprite(spriteTarget, lastKeyframe); + } + this.runtime.off('RUNTIME_STEP_START', frameHandler); + this.runtime.off('ANIMATIONS_FORCE_STEP', frameHandler); + this.runtime.off('ANIMATIONS_FORCE_SPECIFIC_STEP', forceSpecificStepHandler); + this.runtime.off('PROJECT_STOP_ALL', stopAllHandler); + // remove our registered data + const totalSpriteData = this.progressingTargetData[id]; + if (totalSpriteData) { + if (totalSpriteData[animationName]) { + delete totalSpriteData[animationName]; + } + const totalAnimationsPlaying = Object.keys(totalSpriteData); + if (totalAnimationsPlaying.length <= 0) { + delete this.progressingTargetData[id]; + if (this.progressingTargets.includes(id)) { + const idx = this.progressingTargets.indexOf(id); + this.progressingTargets.splice(idx, 1); + } + } + } + }; + let startTime = this.now(); + // calculate length + let animationLength = 0; + let keyframeStartTimes = []; + let keyframeEndTimes = []; + let _lastKeyframeTime = 0; + for (const keyframe of finalAnimation.keyframes) { + animationLength += keyframe.keyframeLength * 1000; + keyframeStartTimes.push(startTime + _lastKeyframeTime); + keyframeEndTimes.push(startTime + (keyframe.keyframeLength * 1000) + _lastKeyframeTime); + _lastKeyframeTime += keyframe.keyframeLength * 1000; + } + // get timings & info + let currentKeyframe = 0; // updates at the end of a frame + let currentState = getStateOfSprite(spriteTarget); // updates at the end of a frame + const lastKeyframe = finalAnimation.keyframes.length - 1; + let endTime = this.now() + animationLength; + let isPaused = false; + let pauseStartTime = 0; + const frameHandler = () => { + const currentTime = this.now(); + if (animationState.forceStop) { + // prematurely end the animation + animationEnded(true); + return; + } + if (animationState.paused || animationState.projectPaused) { + isPaused = true; + if (pauseStartTime === 0) { + pauseStartTime = this.now(); + } + } + if (isPaused) { + // check if still paused & handle if not + if (!animationState.paused && !animationState.projectPaused) { + isPaused = false; + const pauseTime = this.now() - pauseStartTime; // amount of time we were paused for + startTime += pauseTime; + endTime += pauseTime; + keyframeStartTimes = keyframeStartTimes.map(time => time + pauseTime); + keyframeEndTimes = keyframeEndTimes.map(time => time + pauseTime); + pauseStartTime = 0; + } + if (isPaused) { + return; + } + } + if (currentTime >= endTime) { + animationEnded(); + return; + } + const keyframe = finalAnimation.keyframes[currentKeyframe]; + const keyframeStart = keyframeStartTimes[currentKeyframe]; + const keyframeEnd = keyframeEndTimes[currentKeyframe]; + // const animationProgress = (currentTime - startTime) / (endTime - startTime); + const keyframeProgress = (currentTime - keyframeStart) / (keyframeEnd - keyframeStart); + if (keyframeProgress > 1) { + if (currentKeyframe + 1 > lastKeyframe) { + return animationEnded(); + } + setStateOfSprite(spriteTarget, keyframe); + currentState = getStateOfSprite(spriteTarget); + currentKeyframe += 1; + // wait another step to continue the next frame + return; + } + this._progressAnimation(spriteTarget, currentState, keyframe, keyframe.easingMode, keyframe.easingDir, keyframeProgress); + }; + frameHandler(); + this.runtime.once('PROJECT_STOP_ALL', stopAllHandler); + this.runtime.on('RUNTIME_STEP_START', frameHandler); + this.runtime.on('ANIMATIONS_FORCE_STEP', frameHandler); + this.runtime.on('ANIMATIONS_FORCE_SPECIFIC_STEP', forceSpecificStepHandler); + } + pauseAnimation(args, util) { + const id = util.target.id; + if (!this.progressingTargets.includes(id)) return; // we arent doing ANY animation + const animationName = Cast.toString(args.ANIM); + if (!(animationName in this.animations)) { + return; + } + const info = this.progressingTargetData[id]; + if (!info) return; + if (!(animationName in info)) { + return; + } + info[animationName].paused = true; + } + unpauseAnimation(args, util) { + const id = util.target.id; + if (!this.progressingTargets.includes(id)) return; // we arent doing ANY animation + const animationName = Cast.toString(args.ANIM); + if (!(animationName in this.animations)) { + return; + } + const info = this.progressingTargetData[id]; + if (!info) return; + if (!(animationName in info)) { + return; + } + info[animationName].paused = false; + } + stopAnimation(args, util) { + const id = util.target.id; + if (!this.progressingTargets.includes(id)) return; // we arent doing ANY animation + const animationName = Cast.toString(args.ANIM); + if (!(animationName in this.animations)) { + return; + } + const info = this.progressingTargetData[id]; + if (!info) return; + if (!(animationName in info)) { + return; + } + info[animationName].forceStop = true; + this.runtime.emit('ANIMATIONS_FORCE_SPECIFIC_STEP', id, animationName); + } + + isPausedAnimation(args, util) { // HIDDEN FROM PALETTE + const id = util.target.id; + if (!this.progressingTargets.includes(id)) return false; // we arent doing ANY animation + const animationName = Cast.toString(args.ANIM); + if (!(animationName in this.animations)) { + return false; + } + const info = this.progressingTargetData[id]; + if (!info) return; + if (!(animationName in info)) { + return false; + } + return info[animationName].paused; + } + isPropertyAnimation(args, util) { + const id = util.target.id; + if (!this.progressingTargets.includes(id)) return false; // we arent doing ANY animation (we arent paused OR playing) + const animationName = Cast.toString(args.ANIM); + const animationDataProp = Cast.toString(args.ANIMPROP); + if (!(animationName in this.animations)) { + return false; // (we arent paused OR playing) + } + const info = this.progressingTargetData[id]; + if (!info) return false; // (we arent paused OR playing) + if (!(animationName in info)) { + return false; // (we arent paused OR playing) + } + if (animationDataProp === 'paused') { + return info[animationName].paused; + } + return true; // data exists, therefore we are playing the animation currently + } +} + +module.exports = AnimationExtension; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_audio/helper.js b/local-scratch-vm/src/extensions/jg_audio/helper.js new file mode 100644 index 0000000000000000000000000000000000000000..6e25d577217c8c2debc60a37ffffcad98442d30b --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_audio/helper.js @@ -0,0 +1,435 @@ +const Cast = require("../../util/cast"); +const Timer = require("./timer"); + +function MathOver(number, max) { + let num = number; + while (num > max) { + num -= max; + } + return num; +} +function Clamp(number, min, max) { + return Math.min(Math.max(number, min), max); +} + +class AudioSource { + /** + * @param {AudioContext} audioContext + * @param {object} audioGroup + * @param {AudioBuffer} source + * @param {object} data + * @param {object} parent + */ + constructor(audioContext, audioGroup, source, data, parent, runtime) { + if (source == null) source = ""; + if (data == null) data = {}; + this.runtime = runtime; + + this.src = source; + this.duration = source.duration; + this.originAudioName = ""; + + this.volume = data.volume ?? 1; + this.speed = data.speed ?? 1; + this.pitch = data.pitch ?? 0; + this.pan = data.pan ?? 0; + this.looping = data.looping ?? false; + + this.startPosition = data.startPosition ?? 0; + this.endPosition = data.endPosition ?? Infinity; + this.loopStartPosition = data.loopStartPosition ?? 0; + this.loopEndPosition = data.loopEndPosition ?? Infinity; + + this.resumeSpot = 0; + this.paused = false; + this.notPlaying = true; + this.parent = parent; + + this._audioNode = null; + this._audioContext = audioContext; + this._audioGroup = audioGroup; + + this._audioPanner = this._audioContext.createPanner(); + this._audioGainNode = this._audioContext.createGain(); + this._audioAnalyzerNode = this._audioContext.createAnalyser(); + + this._audioPanner.panningModel = 'equalpower'; + this._audioGainNode.gain.value = 1; + + this._audioGainNode.connect(this._audioPanner); + this._audioPanner.connect(this._audioAnalyzerNode); + this._audioAnalyzerNode.connect(parent.audioGlobalVolumeNode); + + this._originalConfig = data; + this._playingSrc = null; + + this._timer = new Timer(runtime, audioContext); + this._disposed = false; + } + + play(atTime) { + if (!this.src) throw "Cannot play an empty audio source"; + try { + if (this._audioNode) { + this._audioNode.onended = null; + this._audioNode.stop(); + } + } catch { + // ... idk + } finally { + this._audioNode = null; + } + + const source = this._audioContext.createBufferSource(); + this._audioNode = source; + this.update(); + + source.buffer = this.src; + source.connect(this._audioGainNode); + this._playingSrc = source.buffer; + + if (!this.paused) { + this._timer.reset(); + this._timer.setTime(Clamp(atTime ?? this.startPosition, 0, this.duration) * 1000); + this._timer.start(); + } else { + this.resumeSpot = this.getTimePosition(); + this._timer.start(); + } + + // we need to know when the sound starts, so we know how long to play for + // we also need to change endTimePos if we are looping + let startTimePos = this.resumeSpot; + let endTimePos = this.endPosition; + if (this.paused) { + this.paused = false; + } else { + startTimePos = atTime ?? this.startPosition; + } + if (this.looping) { + endTimePos = this.loopEndPosition; + } + + // dont play the sound if the playback duration is less than 1 sample frame, otherwise the ended event will not fire + this.notPlaying = false; + const playbackDuration = Clamp(endTimePos - startTimePos, 0, this.duration); + if (playbackDuration < 1 / this.src.sampleRate) { + this._onNodeStop(true); + } else { + source.start(0, Clamp(startTimePos, 0, this.duration), playbackDuration); + + source.onended = () => { + this._onNodeStop(); + } + } + } + stop() { + this.notPlaying = true; + this.paused = false; + this._timer.stop(); + try { + if (this._audioNode) { + this._audioNode.stop(); + } + } catch { + // ... idk + } finally { + this._audioNode = null; + } + } + pause() { + if (!this._audioNode) return; + this.paused = true; + this.notPlaying = true; + this._timer.pause(); + + // onended is already ignored when paused, and stopped nodes cannot restart + this._audioNode.onended = null; + this._audioNode.stop(); + this._audioNode = null; + } + + update() { + if (!this._audioNode) return; + const audioNode = this._audioNode; + const audioGroup = this._audioGroup; + const audioGainNode = this._audioGainNode; + const audioPanner = this._audioPanner; + + // we need to manually calculate detune to prevent problems when using playbackRate for other things + audioNode.playbackRate.value = this.speed * Math.pow(2, this.pitch / 1200); + audioGainNode.gain.value = this.volume; + + audioNode.playbackRate.value *= audioGroup.globalSpeed * Math.pow(2, audioGroup.globalPitch / 1200); + audioGainNode.gain.value *= audioGroup.globalVolume; + this._timer.speed = audioNode.playbackRate.value; + + const pan = Clamp(this.pan + audioGroup.globalPan, -1, 1); + audioPanner.positionX.value = pan; + audioPanner.positionY.value = 0; + audioPanner.positionZ.value = 1 - Math.abs(pan); + } + dispose() { + this._disposed = true; + this._timer.dispose(); + this.stop(); + } + clone() { + const newSource = new AudioSource(this._audioContext, this._audioGroup, this.src, this._originalConfig, this.parent, this.runtime); + return newSource; + } + reverse() { + if (!this.src) throw "Cannot reverse an empty audio source"; + + const buffer = this.src; + const reversedBuffer = this._audioContext.createBuffer( + buffer.numberOfChannels, + buffer.length, + buffer.sampleRate + ); + + for (let channel = 0; channel < buffer.numberOfChannels; channel++) { + const sourceData = buffer.getChannelData(channel); + const destinationData = reversedBuffer.getChannelData(channel); + + for (let i = 0; i < buffer.length; i++) { + destinationData[i] = sourceData[buffer.length - 1 - i]; + } + } + this.src = reversedBuffer; + } + + setTimePosition(newSeconds) { + if (!this._audioNode && !this.paused) return; + const src = this._getActiveSource(); + newSeconds = Clamp(newSeconds, 0, src.duration); + if (this.paused) { + // only update the time + this._timer.setTime(newSeconds * 1000); + return; + } + + this._timer.setTime(newSeconds * 1000); + this.play(newSeconds); + } + + getVolume() { + const analyserNode = this._audioAnalyzerNode; + + const bufferLength = analyserNode.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + analyserNode.getByteTimeDomainData(dataArray); + + let sumSquares = 0.0; + for (let i = 0; i < bufferLength; i++) { + const sample = (dataArray[i] / 128.0) - 1.0; + sumSquares += sample * sample; + } + const volume = Math.sqrt(sumSquares / bufferLength); + return volume; + } + getFrequency() { + const analyserNode = this._audioAnalyzerNode; + const src = this._getActiveSource(); + + const bufferLength = analyserNode.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + analyserNode.getByteFrequencyData(dataArray); + + // find the max frequency + let maxIndex = 0; + for (let i = 1; i < bufferLength; i++) { + if (dataArray[i] > dataArray[maxIndex]) { + maxIndex = i; + } + } + + // return the dominant freq + const nyquist = src.sampleRate / 2; + return maxIndex * nyquist / bufferLength; + } + getTimePosition() { + const src = this._getActiveSource(); + return Clamp(this._timer.getTime(true), 0, src.duration); + } + + _getActiveSource() { + if (this._audioNode) return this._playingSrc; + return this.src; + } + _onNodeStop(didNotPlay) { + if (this.paused || !this._audioNode) return; + if (!didNotPlay) { + if (this.looping && !this.notPlaying) { + this.play(this.loopStartPosition || 0); + return; + } + } + + this._audioNode.onended = null; + this.notPlaying = true; + this._audioNode = null; + this._timer.stop(); + } +} +class AudioExtensionHelper { + constructor(runtime) { + /** + * The runtime that the helper will use for all functions. + * @type {runtime} + */ + this.runtime = runtime; + this.audioGroups = {}; + this.audioContext = new AudioContext(); + this.audioGlobalVolumeNode = this.audioContext.createGain(); + + this.audioGlobalVolumeNode.gain.value = 1; + this.audioGlobalVolumeNode.connect(this.audioContext.destination); + } + + /** + * Creates a new AudioGroup. + * @type {string} AudioGroup name + * @type {object} AudioGroup settings (optional) + * @type {object[]} AudioGroup sources (optional) + */ + AddAudioGroup(name, data, sources) { + if (data == null) data = {}; + this.audioGroups[name] = { + id: name, + sources: (sources == null ? {} : sources), + globalVolume: (data.globalVolume == null ? 1 : data.globalVolume), + globalSpeed: (data.globalSpeed == null ? 1 : data.globalSpeed), + globalPitch: (data.globalPitch == null ? 0 : data.globalPitch), + globalPan: (data.globalPan == null ? 0 : data.globalPan) + }; + return this.audioGroups[name]; + } + /** + * Deletes an AudioGroup by name. + * @type {string} + */ + DeleteAudioGroup(name) { + const audioGroup = this.audioGroups[name]; + if (!audioGroup) return; + this.DisposeAudioGroupSources(audioGroup); + delete this.audioGroups[name]; + } + /** + * Gets an AudioGroup by name. + * @type {string} + */ + GetAudioGroup(name) { + return this.audioGroups[name]; + } + /** + * Gets all AudioGroups and returns them in an array. + */ + GetAllAudioGroups() { + return Object.values(this.audioGroups); + } + /** + * Gets all AudioSources in an AudioGroup and updates them. + * @type {AudioGroup} + */ + UpdateAudioGroupSources(audioGroup) { + const audioSources = this.GrabAllGrabAudioSources(audioGroup); + for (let i = 0; i < audioSources.length; i++) { + const source = audioSources[i]; + source.update(); + } + } + /** + * Gets all AudioSources in an AudioGroup and disposes them. + * @type {AudioGroup} + */ + DisposeAudioGroupSources(audioGroup) { + const audioSources = this.GrabAllGrabAudioSources(audioGroup); + for (let i = 0; i < audioSources.length; i++) { + const source = audioSources[i]; + source.dispose(); + } + } + + /** + * Creates a new AudioSource inside of an AudioGroup. + * @type {AudioGroup} AudioSource parent + * @type {string} AudioSource name + * @type {string} AudioSource source (optional) + * @type {object} AudioSource settings (optional) + */ + AppendAudioSource(parent, name, src, settings) { + const group = typeof parent == "string" ? this.GetAudioGroup(parent) : parent; + if (!group) return; + group.sources[name] = new AudioSource(this.audioContext, group, src, settings, this, this.runtime); + return group.sources[name]; + } + /** + * Deletes an AudioSource by name. + * @type {AudioGroup} AudioSource parent + * @type {string} + */ + RemoveAudioSource(parent, name) { + const group = typeof parent == "string" ? this.GetAudioGroup(parent) : parent; + if (!group) return; + const audioSource = group.sources[name]; + if (!audioSource) return; + + audioSource.dispose(); + delete group.sources[name]; + } + /** + * Gets an AudioSource by name. + * @type {AudioGroup} AudioSource parent + * @type {string} + */ + GrabAudioSource(audioGroup, name) { + const group = typeof audioGroup == "string" ? this.GetAudioGroup(audioGroup) : audioGroup; + if (!group) return; + return group.sources[name]; + } + /** + * Gets all AudioSources and returns them in an array. + * @type {AudioGroup} AudioSource parent + */ + GrabAllGrabAudioSources(audioGroup) { + const group = typeof audioGroup == "string" ? this.GetAudioGroup(audioGroup) : audioGroup; + if (!group) return []; + return Object.values(group.sources); + } + + /** + * Finds a sound with the specified ID in the sound list. + * @type {Array} soundList + * @type {string} Sound ID + */ + FindSoundBySoundId(soundList, id) { + for (let i = 0; i < soundList.length; i++) { + const sound = soundList[i]; + if (sound.soundId == id) return sound; + } + return null; + } + /** + * Finds a sound with the specified name in the sound list. + * @type {Array} soundList + * @type {string} Sound name + */ + FindSoundByName(soundList, name) { + for (let i = 0; i < soundList.length; i++) { + const sound = soundList[i]; + if (sound.name == name) return sound; + } + return null; + } + /** + * Clamps numbers to stay inbetween 2 values. + * @type {number} + */ + Clamp(number, min, max) { + return Math.min(Math.max(number, min), max); + } +} + +module.exports.Helper = AudioExtensionHelper +module.exports.AudioSource = AudioSource \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_audio/index.js b/local-scratch-vm/src/extensions/jg_audio/index.js new file mode 100644 index 0000000000000000000000000000000000000000..cd8d11e08cf7cdc0788c7816f8138ce04eda46e3 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_audio/index.js @@ -0,0 +1,641 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +const HelperTool = require('./helper'); +const Helper = new HelperTool.Helper(); + +/** + * Class for AudioGroups & AudioSources + * @constructor + */ +class AudioExtension { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + this.helper = Helper; + this.helper.runtime = this.runtime; + + this.runtime.on('PROJECT_STOP_ALL', () => { + for (const audioGroupName in Helper.audioGroups) { + const audioGroup = Helper.GetAudioGroup(audioGroupName); + for (const sourceName in audioGroup.sources) { + audioGroup.sources[sourceName].stop(); + } + } + }); + + this.runtime.registerExtensionAudioContext("jgExtendedAudio", this.helper.audioContext, this.helper.audioGlobalVolumeNode); + } + + deserialize(data) { + for (const audioGroup in Helper.audioGroups) { + Helper.DeleteAudioGroup(audioGroup); + } + Helper.audioGroups = {}; + for (const audioGroup of data) { + Helper.AddAudioGroup(audioGroup.id, audioGroup); + } + } + + serialize() { + return Helper.GetAllAudioGroups().map(audioGroup => ({ + id: audioGroup.id, + sources: {}, + globalVolume: audioGroup.globalVolume, + globalSpeed: audioGroup.globalSpeed, + globalPitch: audioGroup.globalPitch, + globalPan: audioGroup.globalPan + })); + } + + orderCategoryBlocks(blocks) { + const buttons = { + create: blocks[0], + delete: blocks[1] + }; + const varBlock = blocks[2]; + blocks.splice(0, 3); + // create the variable block xml's + const varBlocks = Helper.GetAllAudioGroups().map(audioGroup => varBlock.replace('{audioGroupId}', audioGroup.id)); + if (!varBlocks.length) { + return [buttons.create]; + } + // push the button to the top of the var list + varBlocks.reverse(); + varBlocks.push(buttons.delete); + varBlocks.push(buttons.create); + // merge the category blocks and variable blocks into one block list + blocks = varBlocks + .reverse() + .concat(blocks); + return blocks; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgExtendedAudio', + name: 'Sound Systems', + color1: '#E256A1', + color2: '#D33388', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [ + { opcode: 'createAudioGroup', text: 'New Audio Group', blockType: BlockType.BUTTON, }, + { opcode: 'deleteAudioGroup', text: 'Remove an Audio Group', blockType: BlockType.BUTTON, }, + { + opcode: 'audioGroupGet', text: '[AUDIOGROUP]', blockType: BlockType.REPORTER, + arguments: { + AUDIOGROUP: { menu: 'audioGroup', defaultValue: '{audioGroupId}', type: ArgumentType.STRING, } + }, + }, + { text: "Operations", blockType: BlockType.LABEL, }, + { + opcode: 'audioGroupSetVolumeSpeedPitchPan', text: 'set [AUDIOGROUP] [VSPP] to [VALUE]%', blockType: BlockType.COMMAND, + arguments: { + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + VSPP: { type: ArgumentType.STRING, menu: 'vspp', defaultValue: "" }, + VALUE: { type: ArgumentType.NUMBER, defaultValue: 100 }, + }, + }, + { + opcode: 'audioGroupGetModifications', text: '[AUDIOGROUP] [OPTION]', blockType: BlockType.REPORTER, disableMonitor: true, + arguments: { + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + OPTION: { type: ArgumentType.STRING, menu: 'audioGroupOptions', defaultValue: "" }, + }, + }, + "---", + { + opcode: 'audioSourceCreate', text: '[CREATEOPTION] audio source named [NAME] in [AUDIOGROUP]', blockType: BlockType.COMMAND, + arguments: { + CREATEOPTION: { type: ArgumentType.STRING, menu: 'createOptions', defaultValue: "" }, + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + }, + }, + { + opcode: 'audioSourceDuplicate', text: 'duplicate audio source from [NAME] to [COPY] in [AUDIOGROUP]', blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + COPY: { type: ArgumentType.STRING, defaultValue: "AudioSource2" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + }, + }, + { + opcode: 'audioSourceReverse', text: 'reverse audio source used in [NAME] in [AUDIOGROUP]', blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + COPY: { type: ArgumentType.STRING, defaultValue: "AudioSource2" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + }, + }, + { + opcode: 'audioSourceDeleteAll', text: '[DELETEOPTION] all audio sources in [AUDIOGROUP]', blockType: BlockType.COMMAND, + arguments: { + DELETEOPTION: { type: ArgumentType.STRING, menu: 'deleteOptions', defaultValue: "" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + }, + }, + "---", + { + opcode: 'audioSourceSetScratch', text: 'set audio source [NAME] in [AUDIOGROUP] to use [SOUND]', blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + SOUND: { type: ArgumentType.STRING, menu: 'sounds', defaultValue: "" }, + }, + }, + { + opcode: 'audioSourceSetUrl', text: 'set audio source [NAME] in [AUDIOGROUP] to use [URL]', blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + URL: { type: ArgumentType.STRING, defaultValue: "https://extensions.turbowarp.org/meow.mp3" }, + }, + }, + { + opcode: 'audioSourcePlayerOption', text: '[PLAYEROPTION] audio source [NAME] in [AUDIOGROUP]', blockType: BlockType.COMMAND, + arguments: { + PLAYEROPTION: { type: ArgumentType.STRING, menu: 'playerOptions', defaultValue: "" }, + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + }, + }, + "---", + { + opcode: 'audioSourceSetLoop', text: 'set audio source [NAME] in [AUDIOGROUP] to [LOOP]', blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + LOOP: { type: ArgumentType.STRING, menu: 'loop', defaultValue: "loop" }, + }, + }, + { + opcode: 'audioSourceSetTime2', text: 'set audio source [NAME] [TIMEPOS] position in [AUDIOGROUP] to [TIME] seconds', blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + TIMEPOS: { type: ArgumentType.STRING, menu: 'timePosition' }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + TIME: { type: ArgumentType.NUMBER, defaultValue: 0.3 }, + }, + }, + { + opcode: 'audioSourceSetVolumeSpeedPitchPan', text: 'set audio source [NAME] [VSPP] in [AUDIOGROUP] to [VALUE]%', blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + VSPP: { type: ArgumentType.STRING, menu: 'vspp', defaultValue: "" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + VALUE: { type: ArgumentType.NUMBER, defaultValue: 100 }, + }, + }, + "---", + { + opcode: 'audioSourceGetModificationsBoolean', text: 'audio source [NAME] [OPTION] in [AUDIOGROUP]', blockType: BlockType.BOOLEAN, disableMonitor: true, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + OPTION: { type: ArgumentType.STRING, menu: 'audioSourceOptionsBooleans', defaultValue: "" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + }, + }, + { + opcode: 'audioSourceGetModificationsNormal', text: 'audio source [NAME] [OPTION] in [AUDIOGROUP]', blockType: BlockType.REPORTER, disableMonitor: true, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + OPTION: { type: ArgumentType.STRING, menu: 'audioSourceOptions', defaultValue: "" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + }, + }, + // deleted blocks + { + opcode: 'audioSourceSetTime', text: 'set audio source [NAME] start position in [AUDIOGROUP] to [TIME] seconds', blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "AudioSource1" }, + AUDIOGROUP: { type: ArgumentType.STRING, menu: 'audioGroup', defaultValue: "" }, + TIME: { type: ArgumentType.NUMBER, defaultValue: 0.3 }, + }, + hideFromPalette: true, + }, + ], + menus: { + audioGroup: 'fetchAudioGroupMenu', + sounds: 'fetchScratchSoundMenu', + // specific menus + vspp: { + acceptReporters: true, + items: [ + { text: "volume", value: "volume" }, + { text: "speed", value: "speed" }, + { text: "detune", value: "pitch" }, + { text: "pan", value: "pan" }, + ] + }, + playerOptions: { + acceptReporters: true, + items: [ + { text: "play", value: "play" }, + { text: "pause", value: "pause" }, + { text: "stop", value: "stop" }, + ] + }, + loop: { + acceptReporters: true, + items: [ + { text: "loop", value: "loop" }, + { text: "not loop", value: "not loop" }, + ] + }, + timePosition: { + acceptReporters: true, + items: [ + { text: "time", value: "time" }, + { text: "start", value: "start" }, + { text: "end", value: "end" }, + { text: "start loop", value: "start loop" }, + { text: "end loop", value: "end loop" }, + ] + }, + deleteOptions: { + acceptReporters: true, + items: [ + { text: "delete", value: "delete" }, + { text: "play", value: "play" }, + { text: "pause", value: "pause" }, + { text: "stop", value: "stop" }, + ] + }, + createOptions: { + acceptReporters: true, + items: [ + { text: "create", value: "create" }, + { text: "delete", value: "delete" }, + ] + }, + // audio group stuff + audioGroupOptions: { + acceptReporters: true, + items: [ + { text: "volume", value: "volume" }, + { text: "speed", value: "speed" }, + { text: "detune", value: "pitch" }, + { text: "pan", value: "pan" }, + ] + }, + // audio source stuff + audioSourceOptionsBooleans: { + acceptReporters: true, + items: [ + { text: "playing", value: "playing" }, + { text: "paused", value: "paused" }, + { text: "looping", value: "looping" }, + ] + }, + audioSourceOptions: { + acceptReporters: true, + items: [ + { text: "volume", value: "volume" }, + { text: "speed", value: "speed" }, + { text: "detune", value: "pitch" }, + { text: "pan", value: "pan" }, + { text: "time position", value: "time position" }, + { text: "output volume", value: "output volume" }, + { text: "start position", value: "start position" }, + { text: "end position", value: "end position" }, + { text: "start loop position", value: "start loop position" }, + { text: "end loop position", value: "end loop position" }, + { text: "sound length", value: "sound length" }, + { text: "origin sound", value: "origin sound" }, + + // see https://stackoverflow.com/a/54567527 as to why this is not a menu option + // { text: "dominant frequency", value: "dominant frequency" }, + ] + } + } + }; + } + + createAudioGroup() { + const newGroup = prompt('Set a name for this Audio Group:', 'audio group ' + (Helper.GetAllAudioGroups().length + 1)); + if (!newGroup) return alert('Canceled') + if (Helper.GetAudioGroup(newGroup)) return alert(`"${newGroup}" is taken!`); + Helper.AddAudioGroup(newGroup); + vm.emitWorkspaceUpdate(); + this.serialize(); + } + deleteAudioGroup() { + const group = prompt('Which audio group would you like to delete?'); + // helper deals with audio groups that dont exist, so we just call the function with no check + Helper.DeleteAudioGroup(group); + vm.emitWorkspaceUpdate(); + this.serialize(); + } + + fetchAudioGroupMenu() { + const audioGroups = Helper.GetAllAudioGroups(); + if (audioGroups.length <= 0) { + return [ + { + text: '', + value: '' + } + ]; + } + return audioGroups.map(audioGroup => ({ + text: audioGroup.id, + value: audioGroup.id + })); + } + fetchScratchSoundMenu() { + const sounds = vm.editingTarget.sprite.sounds; // this function only gets used in the editor so we are safe to use editingTarget + if (sounds.length <= 0) return [{ text: '', value: '' }]; + return sounds.map(sound => ({ + text: sound.name, + value: sound.name + })); + } + + audioGroupGet(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + return JSON.stringify(Object.getOwnPropertyNames(audioGroup.sources)); + } + + audioGroupSetVolumeSpeedPitchPan(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + switch (args.VSPP) { + case "volume": + audioGroup.globalVolume = Helper.Clamp(Cast.toNumber(args.VALUE) / 100, 0, 1); + break; + case "speed": + audioGroup.globalSpeed = Helper.Clamp(Cast.toNumber(args.VALUE) / 100, 0, Infinity); + break; + case "detune": + case "pitch": + audioGroup.globalPitch = Cast.toNumber(args.VALUE); + break; + case "pan": + audioGroup.globalPan = Helper.Clamp(Cast.toNumber(args.VALUE), -100, 100) / 100; + break; + } + Helper.UpdateAudioGroupSources(audioGroup); + } + + audioSourceCreate(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + switch (args.CREATEOPTION) { + case "create": + Helper.RemoveAudioSource(audioGroup, args.NAME); + Helper.AppendAudioSource(audioGroup, args.NAME); + break; + case "delete": + Helper.RemoveAudioSource(audioGroup, args.NAME); + break; + } + } + audioSourceDuplicate(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + const origin = Cast.toString(args.NAME); + const newName = Cast.toString(args.COPY); + if (!audioGroup) return; + const audioSource = Helper.GrabAudioSource(audioGroup, origin); + if (!audioSource) return; + Helper.RemoveAudioSource(audioGroup, newName); + audioGroup.sources[newName] = audioSource.clone(); + } + audioSourceReverse(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + const target = Cast.toString(args.NAME); + if (!audioGroup) return; + const audioSource = Helper.GrabAudioSource(audioGroup, target); + if (!audioSource) return; + audioSource.reverse(); + } + audioSourceDeleteAll(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + + for (const sourceName in audioGroup.sources) { + switch (args.DELETEOPTION) { + case "delete": + Helper.RemoveAudioSource(audioGroup, sourceName); + break; + case "play": + audioGroup.sources[sourceName].play(); + break; + case "pause": + audioGroup.sources[sourceName].pause(); + break; + case "stop": + audioGroup.sources[sourceName].stop(); + break; + } + } + } + + audioSourceSetScratch(args, util) { + return new Promise((resolve, reject) => { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return resolve(); + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return resolve(); + const sound = Helper.FindSoundByName(util.target.sprite.sounds, args.SOUND); + if (!sound) return resolve(); + let canUse = true; + try { + // eslint-disable-next-line no-unused-vars + util.target.sprite.soundBank.getSoundPlayer(sound.soundId).buffer; + } catch { + canUse = false; + } + if (!canUse) return resolve(); + const buffer = util.target.sprite.soundBank.getSoundPlayer(sound.soundId).buffer + audioSource.duration = buffer.duration; + audioSource.src = buffer; + audioSource.originAudioName = `${args.SOUND}`; + resolve(); + }) + } + audioSourceSetUrl(args, util) { + return new Promise((resolve, reject) => { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return resolve(); + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return resolve(); + fetch(args.URL).then(response => response.arrayBuffer().then(arrayBuffer => { + Helper.audioContext.decodeAudioData(arrayBuffer, buffer => { + audioSource.duration = buffer.duration; + audioSource.src = buffer; + audioSource.originAudioName = `${args.URL}`; + resolve(); + }, resolve); + }).catch(resolve)).catch(err => { + // this is not a url, try some other stuff instead + const sound = Helper.FindSoundByName(util.target.sprite.sounds, args.URL); + if (sound) { + // this is a scratch sound name + let canUse = true; + try { + // eslint-disable-next-line no-unused-vars + util.target.sprite.soundBank.getSoundPlayer(sound.soundId).buffer; + } catch { + canUse = false; + } + if (!canUse) return resolve(); + const buffer = util.target.sprite.soundBank.getSoundPlayer(sound.soundId).buffer + audioSource.duration = buffer.duration; + audioSource.src = buffer; + audioSource.originAudioName = `${args.URL}`; + return resolve(); + } + console.warn(err); + return resolve(); + }); + }) + } + + audioSourcePlayerOption(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return; + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return; + if (!["play", "pause", "stop"].includes(args.PLAYEROPTION)) return; + audioSource[args.PLAYEROPTION](); + } + audioSourceSetLoop(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return; + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return; + if (!["loop", "not loop"].includes(args.LOOP)) return; + audioSource.looping = args.LOOP == "loop"; + } + audioSourceSetTime(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return; + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return; + audioSource.startPosition = Cast.toNumber(args.TIME); + } + audioSourceSetTime2(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return; + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return; + + switch (args.TIMEPOS) { + case "start": + audioSource.startPosition = Cast.toNumber(args.TIME); + break; + case "end": + audioSource.endPosition = Cast.toNumber(args.TIME); + break; + case "start loop": + audioSource.loopStartPosition = Cast.toNumber(args.TIME); + break; + case "end loop": + audioSource.loopEndPosition = Cast.toNumber(args.TIME); + break; + case "time": + audioSource.setTimePosition(Cast.toNumber(args.TIME)); + break; + } + } + audioSourceSetVolumeSpeedPitchPan(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return; + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return; + switch (args.VSPP) { + case "volume": + audioSource.volume = Helper.Clamp(Cast.toNumber(args.VALUE) / 100, 0, 1); + break; + case "speed": + audioSource.speed = Helper.Clamp(Cast.toNumber(args.VALUE) / 100, 0, Infinity); + break; + case "detune": + case "pitch": + audioSource.pitch = Cast.toNumber(args.VALUE); + break; + case "pan": + audioSource.pan = Helper.Clamp(Cast.toNumber(args.VALUE), -100, 100) / 100; + break; + } + Helper.UpdateAudioGroupSources(audioGroup); + } + + audioGroupGetModifications(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + switch (args.OPTION) { + case "volume": + return audioGroup.globalVolume * 100; + case "speed": + return audioGroup.globalSpeed * 100; + case "detune": + case "pitch": + return audioGroup.globalPitch; + case "pan": + return audioGroup.globalPan * 100; + default: + return 0; + } + } + audioSourceGetModificationsBoolean(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return false; + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return false; + switch (args.OPTION) { + case "playing": + return ((!audioSource.paused) && (!audioSource.notPlaying)); + case "paused": + return audioSource.paused; + case "looping": + return audioSource.looping; + default: + return false; + } + } + audioSourceGetModificationsNormal(args) { + const audioGroup = Helper.GetAudioGroup(args.AUDIOGROUP); + if (!audioGroup) return ""; + const audioSource = Helper.GrabAudioSource(audioGroup, args.NAME); + if (!audioSource) return ""; + switch (args.OPTION) { + case "volume": + return audioSource.volume * 100; + case "speed": + return audioSource.speed * 100; + case "detune": + case "pitch": + return audioSource.pitch; + case "pan": + return audioSource.pan * 100; + case "start position": + return audioSource.startPosition; + case "end position": + return audioSource.endPosition; + case "start loop position": + return audioSource.loopStartPosition; + case "end loop position": + return audioSource.loopEndPosition; + case "time position": + return audioSource.getTimePosition(); + case "sound length": + return audioSource.duration; + case "origin sound": + return audioSource.originAudioName; + case "output volume": + return audioSource.getVolume() * 100; + case "dominant frequency": + return audioSource.getFrequency(); + default: + return ""; + } + } +} + +module.exports = AudioExtension; diff --git a/local-scratch-vm/src/extensions/jg_audio/timer.js b/local-scratch-vm/src/extensions/jg_audio/timer.js new file mode 100644 index 0000000000000000000000000000000000000000..5b20aef4509f9eaeb5792da9a3e708f7b231cfb2 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_audio/timer.js @@ -0,0 +1,72 @@ +class Timer { + /** + * @param {Runtime} runtime + * @param {AudioContext} audioContext + */ + constructor(runtime, audioContext) { + this.runtime = runtime; + this.audioContext = audioContext; + this._disposed = false; + + this.paused = false; + this.stopped = true; + + this._value = 0; + this.speed = 1; + + this._lastUpdateReal = Date.now(); + this._lastUpdateProcessed = Date.now(); + + this._boundFunc = this.update.bind(this); + this.runtime.on("RUNTIME_STEP_START", this._boundFunc); + } + + start() { + this.paused = false; + this.stopped = false; + } + + pause() { + this.paused = true; + } + + stop() { + this.paused = false; + this.stopped = true; + } + + reset() { + this._value = 0; + this.paused = false; + this.stopped = true; + } + + update() { + if (this.stopped || this.paused || this._disposed || this.audioContext.state !== "running") { + this._lastUpdateReal = Date.now(); + return; + } + + this._value += (Date.now() - this._lastUpdateReal) * this.speed; + + this._lastUpdateReal = Date.now(); + this._lastUpdateProcessed = Date.now(); + } + dispose() { + if (this._disposed) return; + this._disposed = true; + this.runtime.off("RUNTIME_STEP_START", this._boundFunc); + } + + getTime(inSeconds) { + const divisor = inSeconds ? 1000 : 1; + return this._value / divisor; + } + setTime(ms) { + this._lastUpdateReal = Date.now(); + this._lastUpdateProcessed = Date.now(); + this._value = ms; + } +} + +module.exports = Timer; diff --git a/local-scratch-vm/src/extensions/jg_bestextensioin/index.js b/local-scratch-vm/src/extensions/jg_bestextensioin/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6223c44018e416b9ddb3056c8e96d99256308251 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_bestextensioin/index.js @@ -0,0 +1,93 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); + +/** + * Class for blocks + * @constructor + */ +class JgBestExtensionBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * @type {HTMLVideoElement} + */ + this.videoElement = null; + + this.runtime.on('PROJECT_STOP_ALL', () => { + if (!this.videoElement) return; + this.videoElement.remove(); + this.videoElement = null; + }); + this.runtime.on('RUNTIME_PAUSED', () => { + if (!this.videoElement) return; + this.videoElement.pause(); + }); + this.runtime.on('RUNTIME_UNPAUSED', () => { + if (!this.videoElement) return; + this.videoElement.play(); + }); + this.runtime.on('BEFORE_EXECUTE', () => { + this.setVolumeProperly(); + }); + } + + setVolumeProperly() { + if (!this.videoElement) return; + try { + this.videoElement.volume = this.runtime.audioEngine.inputNode.gain.value * 0.5; + } catch { + // well that sucks + } + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgBestExtension', + name: 'the great', + color1: '#ff0000', + color2: '#00ff00', + color3: '#0000ff', + blocks: [ + { + opcode: 'ohioBlock', + text: 'absolutely delectable!', + blockType: BlockType.COMMAND, + disableMonitor: false + } + ] + }; + } + + ohioBlock() { + if (this.videoElement) return; + + const canvas = this.runtime.renderer.canvas; + if (!canvas) return; + if (!canvas.parentElement) return; + + const video = document.createElement("video"); + video.style = 'width: 100%; height: 100%; z-index: 10000; position: absolute; left: 0; top: 0;'; + video.innerHTML = '' + + ''; + this.videoElement = video; + canvas.parentElement.appendChild(video); + + this.setVolumeProperly(); + video.play(); + + video.onended = () => { + video.remove(); + this.videoElement = null; + }; + } +} + +module.exports = JgBestExtensionBlocks; diff --git a/local-scratch-vm/src/extensions/jg_christmas/icon.png b/local-scratch-vm/src/extensions/jg_christmas/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ab21ac4a40be9b5e326e3b26d7906a9d22a5869d Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_christmas/icon.png differ diff --git a/local-scratch-vm/src/extensions/jg_christmas/index.js b/local-scratch-vm/src/extensions/jg_christmas/index.js new file mode 100644 index 0000000000000000000000000000000000000000..07bdadea3645344dde741aeda0791cf7e29ecd9a --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_christmas/index.js @@ -0,0 +1,215 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const uid = require('../../util/uid'); + +const Textures = { + Snow: require('./snow.png'), + Light: require('./light.png'), + Present: require('./present.png'), +}; + +/** + * Class for Extension blocks + * @constructor + */ +class Extension { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * @type {HTMLDivElement} + */ + this.mainContainer = null; + /** + * @type {HTMLCanvasElement} + */ + this.mainCanvas = null; + /** + * canvas context + * @type {CanvasRenderingContext2D} + */ + this.ctx = null; + + this.initialize(); + + this.snowParticles = {}; + this.lights = {}; + this.runtime.on('RUNTIME_STEP_START', () => { + const viewBox = this.mainContainer.getBoundingClientRect(); + for (const particleId in this.snowParticles) { + const particle = this.snowParticles[particleId]; + const element = particle.element; + element.style.left = `calc(${particle.origin}% + ${particle.x}px)`; + particle.x -= 3; + const y = Cast.toNumber(element.style.top.replace('px', '')) + particle.speed; + element.style.top = `${y}px`; + if (element.getBoundingClientRect().right < 0 || y > viewBox.height) { + element.remove(); + delete this.snowParticles[particleId]; + } + } + this.drawLightBackground(); + }); + this.runtime.on('PROJECT_STOP_ALL', () => { + this.ctx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height); + this.clearSnow(); + this.removeLights(); + }); + } + + initialize() { + const mainContainer = document.body.appendChild(document.createElement("div")); + mainContainer.style = 'position: absolute;' + + 'left: 0; top: 0; width: 100%; height: 100%;' + + 'pointer-events: none; overflow: hidden;' + + 'z-index: 1000009;'; + this.mainContainer = mainContainer; + const mainCanvas = mainContainer.appendChild(document.createElement("canvas")); + mainCanvas.style = 'position: absolute;' + + 'left: 0; top: 0; width: 100%; height: 100%;' + + 'pointer-events: none; background: none;' + + 'border: 0; margin: 0; padding: 0;' + + 'z-index: 999999;'; + this.mainCanvas = mainCanvas; + mainCanvas.width = 1280; + mainCanvas.height = 720; + const canvasContext = mainCanvas.getContext('2d'); + this.ctx = canvasContext; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgChristmas', + name: 'Christmas', + color1: '#ff0000', + color2: '#00ff00', + blockIconURI: require('./icon.png'), + blocks: [ + { + opcode: 'snow', + text: 'snow', + blockType: BlockType.COMMAND + }, + { + opcode: 'clearSnow', + text: 'clear snow', + blockType: BlockType.COMMAND + }, + // { + // opcode: 'addPresent', + // text: 'add present', + // blockType: BlockType.COMMAND + // }, + // { + // opcode: 'removePresents', + // text: 'remove all presents', + // blockType: BlockType.COMMAND + // }, + { + opcode: 'addLight', + text: 'add light', + blockType: BlockType.COMMAND + }, + { + opcode: 'removeLights', + text: 'remove all lights', + blockType: BlockType.COMMAND + }, + ] + }; + } + + snow() { + const snowImage = this.mainContainer.appendChild(document.createElement("img")); + const size = Math.round(8 + (Math.random() * 16)); + const opacity = 0.5 + (Math.random() / 2); + const originX = Math.random() * 150; + snowImage.style = 'position: absolute;' + + `left: ${originX}%; top: -${size}px; width: ${size}px; height: ${size}px;` + + `pointer-events: none; opacity: ${opacity};` + + 'z-index: 1000008;'; + snowImage.src = Textures.Snow; + const id = uid(); + this.snowParticles[id] = { + element: snowImage, + origin: originX, + x: 0, + size: size, + speed: 2 + Math.random() * 6 + }; + } + removeLights() { + this.ctx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height); + for (const particleId in this.lights) { + const particle = this.lights[particleId]; + const element = particle.element; + element.remove(); + delete this.lights[particleId]; + } + } + addLight() { + const lightImage = this.mainContainer.appendChild(document.createElement("img")); + const viewBox = this.mainContainer.getBoundingClientRect(); + const originX = Math.random() * viewBox.width; + const originY = Math.random() * viewBox.height; + const direction = Math.random() * 360; + lightImage.style = 'position: absolute;' + + `left: 0; top: 0; width: 70px; height: 70px;` + + `transform: translate(${originX}px, ${originY}px) rotate(${direction}deg);` + + `transform-origin: 34px 41px; pointer-events: none;` + + 'z-index: 1000005;'; + lightImage.src = Textures.Light; + const id = uid(); + this.lights[id] = { + element: lightImage, + x: originX, + y: originY + }; + this.drawLightBackground(); + let filterGreen = false; + setInterval(() => { + lightImage.style.filter = filterGreen ? 'hue-rotate(90deg) brightness(1.5)' : ''; + filterGreen = !filterGreen; + }, 700); + } + drawLightBackground() { + const canvas = this.mainCanvas; + const viewBox = canvas.getBoundingClientRect(); + const ctx = this.ctx; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.strokeStyle = '#3D5C3A'; + ctx.lineWidth = 1; + + ctx.moveTo(0, 70); + ctx.beginPath(); + for (const particleId in this.lights) { + const particle = this.lights[particleId]; + ctx.lineTo(((particle.x + 34) / viewBox.width) * 1280, ((particle.y + 41) / viewBox.height) * 720); + ctx.moveTo(((particle.x + 34) / viewBox.width) * 1280, ((particle.y + 41) / viewBox.height) * 720); + ctx.stroke(); + } + } + + clearSnow() { + for (const particleId in this.snowParticles) { + const particle = this.snowParticles[particleId]; + const element = particle.element; + element.remove(); + delete this.snowParticles[particleId]; + } + } + addPresent() { + + } +} + +module.exports = Extension; diff --git a/local-scratch-vm/src/extensions/jg_christmas/light.png b/local-scratch-vm/src/extensions/jg_christmas/light.png new file mode 100644 index 0000000000000000000000000000000000000000..53a85f1f6e46ca2d32e3412307a141dffe8a2dfe Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_christmas/light.png differ diff --git a/local-scratch-vm/src/extensions/jg_christmas/present.png b/local-scratch-vm/src/extensions/jg_christmas/present.png new file mode 100644 index 0000000000000000000000000000000000000000..1be68ee7a5afed3807ba64352167eb095b71c4ce Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_christmas/present.png differ diff --git a/local-scratch-vm/src/extensions/jg_christmas/snow.png b/local-scratch-vm/src/extensions/jg_christmas/snow.png new file mode 100644 index 0000000000000000000000000000000000000000..2f9eea93920f6b4057aa2fc02dadc7e5b19867d4 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_christmas/snow.png differ diff --git a/local-scratch-vm/src/extensions/jg_clones/index.js b/local-scratch-vm/src/extensions/jg_clones/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5b77531c14549a6e1c009125ce1bd9e1238a5d4f --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_clones/index.js @@ -0,0 +1,448 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +// const Cast = require('../../util/cast'); + +/** + * Class for CloneTool blocks + * @constructor + */ +class JgCloneToolBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgClones', + name: 'Clone Communication', + color1: '#FFAB19', + color2: '#EC9C13', + blocks: [ + { + blockType: BlockType.LABEL, + text: "Main Sprite Communication" + }, + { + opcode: 'getCloneWithVariableSetTo', + text: formatMessage({ + id: 'jgClones.blocks.getCloneWithVariableSetTo', + default: 'get [DATA] of clone with [VAR] set to [VALUE]', + description: 'Block that returns the value of the item picked within a clone with a variable set to a certain value.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + DATA: { type: ArgumentType.STRING, menu: 'spriteData' }, + VAR: { type: ArgumentType.STRING, menu: 'spriteVariables' }, + VALUE: { type: ArgumentType.STRING, defaultValue: '0' }, + } + }, + { + opcode: 'getCloneVariableWithVariableSetTo', + text: formatMessage({ + id: 'jgClones.blocks.getCloneVariableWithVariableSetTo', + default: 'get [VAR1] of clone with [VAR2] set to [VALUE]', + description: 'Block that returns the value of the variable picked within a clone with a variable set to a certain value.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + VAR1: { type: ArgumentType.STRING, menu: 'spriteVariables' }, + VAR2: { type: ArgumentType.STRING, menu: 'spriteVariables' }, + VALUE: { type: ArgumentType.STRING, defaultValue: '0' }, + } + }, + // { + // opcode: 'setValueOnCloneWithVariableSetTo', + // text: formatMessage({ + // id: 'jgClones.blocks.setValueOnCloneWithVariableSetTo', + // default: 'set [DATA] to [VALUE1] on clone with [VAR] set to [VALUE2]', + // description: 'Block that sets the value of the item picked within a clone with a variable set to a certain value.' + // }), + // blockType: BlockType.COMMAND, + // arguments: { + // DATA: { type: ArgumentType.STRING, menu: 'spriteData' }, + // VALUE1: { type: ArgumentType.STRING, defaultValue: '0' }, + // VAR: { type: ArgumentType.STRING, menu: 'spriteVariables' }, + // VALUE2: { type: ArgumentType.STRING, defaultValue: '0' }, + // } + // }, + { + opcode: 'setVariableOnCloneWithVariableSetTo', + text: formatMessage({ + id: 'jgClones.blocks.setVariableOnCloneWithVariableSetTo', + default: 'set [VAR1] to [VALUE1] on clone with [VAR2] set to [VALUE2]', + description: 'Block that sets a variable within a clone with a variable set to a certain value.' + }), + blockType: BlockType.COMMAND, + arguments: { + VAR1: { type: ArgumentType.STRING, menu: 'spriteVariables' }, + VALUE1: { type: ArgumentType.STRING, defaultValue: '0' }, + VAR2: { type: ArgumentType.STRING, menu: 'spriteVariables' }, + VALUE2: { type: ArgumentType.STRING, defaultValue: '0' }, + } + }, + "---", + "---", + { + blockType: BlockType.LABEL, + text: "Clone Communication" + }, + { + opcode: 'getMainSpriteData', + text: formatMessage({ + id: 'jgClones.blocks.getMainSpriteData', + default: 'get [DATA] of main sprite', + description: 'Block that returns the value of the item picked on the main sprite.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + DATA: { type: ArgumentType.STRING, menu: 'spriteData' } + } + }, + { + opcode: 'getVariableOnMainSprite', + text: formatMessage({ + id: 'jgClones.blocks.getVariableOnMainSprite', + default: 'get [VAR] of main sprite', + description: 'Block that returns the value of the variable picked on the main sprite.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + VAR: { type: ArgumentType.STRING, menu: 'spriteVariables' }, + } + }, + // { + // opcode: 'setValueOnMainSprite', + // text: formatMessage({ + // id: 'jgClones.blocks.setValueOnMainSprite', + // default: 'set [DATA] to [VALUE] on main sprite', + // description: 'Block that sets the value of the item picked within the main sprite.' + // }), + // blockType: BlockType.COMMAND, + // arguments: { + // DATA: { type: ArgumentType.STRING, menu: 'spriteData' }, + // VALUE: { type: ArgumentType.STRING, defaultValue: '0' } + // } + // }, + { + opcode: 'setVariableOnMainSprite', + text: formatMessage({ + id: 'jgClones.blocks.setVariableOnMainSprite', + default: 'set [VAR] to [VALUE] on main sprite', + description: 'Block that sets a variable within the main sprite.' + }), + blockType: BlockType.COMMAND, + arguments: { + VAR: { type: ArgumentType.STRING, menu: 'spriteVariables' }, + VALUE: { type: ArgumentType.STRING, defaultValue: '0' } + } + }, + "---", + "---", + { + blockType: BlockType.LABEL, + text: "Other" + }, + { + opcode: 'getIsClone', + text: formatMessage({ + id: 'jgClones.blocks.getIsClone', + default: 'is clone?', + description: 'Block that returns whether the current sprite is a clone or not.' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN + }, + { + opcode: 'clonesInSprite', + text: formatMessage({ + id: 'jgClones.blocks.clonesInSprite', + default: 'clone count of [SPRITE]', + description: 'Block that returns the amount of clones of this sprite that currently exist.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + SPRITE: { type: ArgumentType.STRING, menu: 'sprites' }, + } + } + ], + menus: { + sprites: "getSpriteMenu", + spriteVariables: "getSpriteVariablesMenu", + spriteData: { + acceptReporters: true, + items: [ + // motion + "x position", + "y position", + "direction", + "rotation style", + // looks (excluding effects) + "visible", + "costume number", + "costume name", + "size", + "x stretch", + "y stretch", + // sound + "volume", + // sensing + "draggable", + // music (doesnt seem to work) + // "tempo", + + // effects + "color effect", + "fisheye effect", + "whirl effect", + "pixelate effect", + "mosaic effect", + "brightness effect", + "ghost effect", + "saturation effect", + "red effect", + "green effect", + "blue effect", + "opaque effect", + ].map(item => ({ text: item, value: item })) + } + } + }; + } + // utilities + getClones (sprite) { + // i call this the stair step null check + if (!sprite.clones) return []; + const clones = sprite.clones + return clones.filter(clone => clone.isOriginal === false); + } + getTargetBySpriteName (name) { + const targets = this.runtime.targets; + const sprites = targets.filter(target => target.isOriginal).filter(target => target.sprite.name == name); + return sprites[0]; + } + getTargetClonesByVariableSetToValue (target, variableName, value) { + const clones = this.getClones(target.sprite); + const cloneList = clones.filter(clone => { + const variables = Object.getOwnPropertyNames(clone.variables).map(variableId => { + return clone.variables[variableId]; + }); + const variable = variables.filter(variable => variable.name == variableName)[0]; + if (!variable) return false; + if (String(variable.value) != String(value)) return false; + return true; + }); + return cloneList; + } + getTargetCloneByVariableSetToValue (target, variableName, value) { + const clones = this.getTargetClonesByVariableSetToValue(target, variableName, value); + if (!clones) return; + return clones[0]; + } + menuOptionToTargetProperty (option) { + switch (option) { + case "x position": + return "x"; + case "y position": + return "y"; + case "x stretch": + return "xStretch"; + case "y stretch": + return "yStretch"; + case "rotation style": + return "rotationStyle"; + case "costume number": + return "currentCostume"; + case "costume name": + return "costumeName"; + default: + return option; + } + } + getMainSprite (target) { + if (!target.sprite) return; + const clones = target.sprite.clones; + const mainSprites = clones.filter(clone => clone.isOriginal); + return mainSprites[0]; + } + + // menus + getSpriteMenu () { + const targets = this.runtime.targets; + const emptyMenu = [{ text: "", value: "" }]; + if (!targets) return emptyMenu; + const menu = targets.filter(target => target.isOriginal && (!target.isStage)).map(target => ({ text: target.sprite.name, value: target.sprite.name })); + return (menu.length > 0) ? menu : emptyMenu; + } + getSpriteVariablesMenu () { + const target = vm.editingTarget; + const emptyMenu = [{ text: "", value: "" }]; + if (!target) return emptyMenu; + if (!target.variables) return emptyMenu; + const menu = Object.getOwnPropertyNames(target.variables).map(variableId => { + const variable = target.variables[variableId] + return { + text: variable.name, + value: variable.name, + } + }); + // check if menu has 0 items because pm throws an error if theres no items + return (menu.length > 0) ? menu : emptyMenu; + } + + // blocks + // other blocks + getIsClone (_, util) { + return !util.target.isOriginal; + } + clonesInSprite (args) { + const target = this.getTargetBySpriteName(args.SPRITE); + if (!target) return 0; + const clones = this.getClones(target.sprite); + return clones.length; + } + + // main sprite communication + getCloneWithVariableSetTo (args, util) { + const target = util.target; + const clone = this.getTargetCloneByVariableSetToValue(target, args.VAR, String(args.VALUE)); + if (!clone) return ""; + const property = this.menuOptionToTargetProperty(args.DATA); + switch (property) { + case "currentCostume": + return clone.currentCostume + 1; + case "costumeName": + return clone.sprite.costumes_[clone.currentCostume].name; + case "xStretch": + return clone.stretch[0]; + case "yStretch": + return clone.stretch[1]; + case "color effect": + return clone.effects.color; + case "fisheye effect": + return clone.effects.fisheye; + case "whirl effect": + return clone.effects.whirl; + case "pixelate effect": + return clone.effects.pixelate; + case "mosaic effect": + return clone.effects.mosaic; + case "brightness effect": + return clone.effects.brightness; + case "ghost effect": + return clone.effects.ghost; + case "red effect": + return clone.effects.red; + case "green effect": + return clone.effects.green; + case "blue effect": + return clone.effects.blue; + case "opaque effect": + return clone.effects.opaque; + case "saturation effect": + return clone.effects.saturation; + default: + return clone[property]; + } + } + getCloneVariableWithVariableSetTo (args, util) { + const target = util.target; + const clone = this.getTargetCloneByVariableSetToValue(target, args.VAR2, String(args.VALUE)); + if (!clone) return ""; + const variables = {}; + Object.getOwnPropertyNames(clone.variables).forEach(id => { + variables[clone.variables[id].name] = clone.variables[id].value; + }); + return variables[args.VAR1]; + } + setVariableOnCloneWithVariableSetTo (args, util) { + const target = util.target; + const clones = this.getTargetClonesByVariableSetToValue(target, args.VAR2, String(args.VALUE2)); + clones.forEach(clone => { + Object.getOwnPropertyNames(clone.variables).forEach(variableId => { + const variable = clone.variables[variableId]; + if (variable.name !== args.VAR1) return; + const value = isNaN(Number(args.VALUE1)) ? String(args.VALUE1) : Number(args.VALUE1); + variable.value = value; + }); + }); + } + + // clone communication + getMainSpriteData (args, util) { + const target = util.target; + const mainSprite = this.getMainSprite(target); + if (!mainSprite) return ""; + const property = this.menuOptionToTargetProperty(args.DATA); + switch (property) { + case "currentCostume": + return mainSprite.currentCostume + 1; + case "costumeName": + return mainSprite.sprite.costumes_[mainSprite.currentCostume].name; + case "xStretch": + return mainSprite.stretch[0]; + case "yStretch": + return mainSprite.stretch[1]; + case "color effect": + return mainSprite.effects.color; + case "fisheye effect": + return mainSprite.effects.fisheye; + case "whirl effect": + return mainSprite.effects.whirl; + case "pixelate effect": + return mainSprite.effects.pixelate; + case "mosaic effect": + return mainSprite.effects.mosaic; + case "brightness effect": + return mainSprite.effects.brightness; + case "ghost effect": + return mainSprite.effects.ghost; + case "red effect": + return mainSprite.effects.red; + case "green effect": + return mainSprite.effects.green; + case "blue effect": + return mainSprite.effects.blue; + case "opaque effect": + return mainSprite.effects.opaque; + case "saturation effect": + return mainSprite.effects.saturation; + default: + return mainSprite[property]; + } + } + getVariableOnMainSprite(args, util) { + const target = util.target; + const mainSprite = this.getMainSprite(target); + if (!mainSprite) return ""; + const variables = {}; + Object.getOwnPropertyNames(mainSprite.variables).forEach(id => { + variables[mainSprite.variables[id].name] = mainSprite.variables[id].value; + }); + return variables[args.VAR]; + } + setVariableOnMainSprite (args, util) { + const target = util.target; + const mainSprite = this.getMainSprite(target); + Object.getOwnPropertyNames(mainSprite.variables).forEach(variableId => { + const variable = mainSprite.variables[variableId]; + if (variable.name !== args.VAR) return; + const value = isNaN(Number(args.VALUE)) ? String(args.VALUE) : Number(args.VALUE); + variable.value = value; + }); + } +} + +module.exports = JgCloneToolBlocks; diff --git a/local-scratch-vm/src/extensions/jg_debugging/index.js b/local-scratch-vm/src/extensions/jg_debugging/index.js new file mode 100644 index 0000000000000000000000000000000000000000..30ec4f03ae4b7a9cb9e2c7b064bf52799b66ad16 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_debugging/index.js @@ -0,0 +1,395 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const xmlEscape = require('../../util/xml-escape'); + +/** + * @param {string} line The line with the quote + * @param {number} index The point where the quote appears + */ +const isEscapedQuote = (line, index) => { + const quote = line.charAt(index); + if (quote !== '"') return false; + let lastIndex = index - 1; + let escaped = false; + while (line.charAt(lastIndex) === "\\") { + escaped = !escaped; + lastIndex -= 1; + } + return escaped; +} +const CommandDescriptions = { + "help": "List all commands and how to use them.\n\tSpecify a command after to only include that explanation.", + "exit": "Closes the debugger.", + "start": "Restarts the project like the flag was clicked.", + "stop": "Stops the project.", + "pause": "Pauses the project.", + "resume": "Resumes the project.", + "broadcast": "Starts a broadcast by name.", + "getvar": "Gets the value of a variable by name.\n\tAdd a sprite name to specify a variable in a sprite.", + "setvar": "Sets the value of a variable by name.\n\tAdd a sprite name to specify a variable in a sprite.", + "getlist": "Gets the value of a list by name.\n\tReturns an array.\n\tAdd a sprite name to specify a list in a sprite.", + "setlist": "Sets the value of a list by name.\n\tThe list will be set to the array specified.\n\tUse a sprite name as the first parameter instead to specify a list in a sprite.", +}; + +/** + * Class for Debugging blocks + * @constructor + */ +class jgDebuggingBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * The console element. + * @type {HTMLDivElement} + */ + this.console = document.body.appendChild(document.createElement("div")); + this.console.style = 'display: none;' + + 'position: absolute; left: 40px; top: 40px;' + + 'resize: both; border-radius: 8px;' + + 'box-shadow: 0px 0px 10px black; border: 1px solid rgba(0, 0, 0, 0.15);' + + 'background: black; font-family: monospace;' + + 'min-height: 3rem; min-width: 128px; width: 480px; height: 480px;' + + 'overflow: hidden; z-index: 1000000;'; + + this.consoleHeader = this.console.appendChild(document.createElement("div")); + this.consoleHeader.style = 'width: 100%; height: 2rem;' + + 'position: absolute; left: 0px; top: 0px;' + + 'display: flex; flex-direction: column; align-items: center;' + + 'justify-content: center; color: white; cursor: move;' + + 'background: #333333; z-index: 1000001; user-select: none;'; + this.consoleHeader.innerHTML = '

Debugger

'; + + this.consoleLogs = this.console.appendChild(document.createElement("div")); + this.consoleLogs.style = 'width: 100%; height: calc(100% - 3rem);' + + 'position: absolute; left: 0px; top: 2rem;' + + 'color: white; cursor: text; overflow: auto;' + + 'background: transparent; outline: unset !important;' + + 'border: 0; margin: 0; padding: 0; font-family: monospace;' + + 'display: flex; flex-direction: column; align-items: flex-start;' + + 'z-index: 1000005; user-select: text;'; + + this.consoleBar = this.console.appendChild(document.createElement("div")); + this.consoleBar.style = 'width: 100%; height: 1rem;' + + 'position: absolute; left: 0px; bottom: 0px;' + + 'display: flex; flex-direction: row;' + + 'color: white; cursor: text; background: black;' + + 'z-index: 1000001; user-select: none;'; + + this.consoleBarInput = this.consoleBar.appendChild(document.createElement("input")); + this.consoleBarInput.style = 'width: calc(100% - 16px); height: 100%;' + + 'position: absolute; left: 16px; top: 0px;' + + 'border: 0; padding: 0; margin: 0; font-family: monospace;' + + 'color: white; cursor: text; background: black;' + + 'z-index: 1000003; user-select: none; outline: unset !important;'; + const consoleBarIndicator = this.consoleBar.appendChild(document.createElement("div")); + consoleBarIndicator.style = 'width: 16px; height: 100%;' + + 'position: absolute; left: 0px; top: 0px;' + + 'color: white; cursor: text;' + + 'z-index: 1000002; user-select: none;'; + consoleBarIndicator.innerHTML = '>'; + consoleBarIndicator.onclick = () => { + this.consoleBarInput.focus(); + }; + // this.consoleLogs.onclick = () => { + // this.consoleBarInput.focus(); + // }; + + this.consoleBarInput.onkeydown = (e) => { + if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; + if (e.key.toLowerCase() !== "enter") return; + const command = this.consoleBarInput.value; + this.consoleBarInput.value = ""; + this._addLog(`> ${command}`, "opacity: 0.7;"); + let parsed = {}; + try { + parsed = this._parseCommand(command); + } catch (err) { + this._addLog(`${err}`, "color: red;"); + return; + } + console.log(parsed); + this._runCommand(parsed); + }; + + // setup events for moving the console around + let mouseDown = false; + let clickDifferenceX = 0; + let clickDifferenceY = 0; + // let oldConsoleHeight = 480; + this.consoleHeader.onmousedown = (e) => { + // if (e.button === 2) { + // e.preventDefault(); + // let newHeight = getComputedStyle(this.consoleHeader, null).height; + // if (this.console.style.height === newHeight) { + // newHeight = oldConsoleHeight; + // } else { + // oldConsoleHeight = this.console.style.height; + // } + // this.console.style.height = newHeight; + // return; + // } + if (e.button !== 0) return; + mouseDown = true; + e.preventDefault(); + const rect = this.console.getBoundingClientRect(); + clickDifferenceX = e.clientX - rect.left; + clickDifferenceY = e.clientY - rect.top; + }; + document.addEventListener('mousemove', (e) => { + if (!mouseDown) { + return; + } + e.preventDefault(); + this.console.style.left = `${e.clientX - clickDifferenceX}px`; + this.console.style.top = `${e.clientY - clickDifferenceY}px`; + }); + document.addEventListener('mouseup', (e) => { + if (!mouseDown) { + return; + } + mouseDown = false; + }); + + this._logs = []; + this.commandSet = {}; + this.commandExplanations = {}; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgDebugging', + name: 'Debugging', + color1: '#878787', + color2: '#757575', + blocks: [ + { + opcode: 'openDebugger', + text: 'open debugger', + blockType: BlockType.COMMAND + }, + { + opcode: 'closeDebugger', + text: 'close debugger', + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'log', + text: 'log [INFO]', + blockType: BlockType.COMMAND, + arguments: { + INFO: { + type: ArgumentType.STRING, + defaultValue: "Hello!" + } + } + }, + { + opcode: 'warn', + text: 'warn [INFO]', + blockType: BlockType.COMMAND, + arguments: { + INFO: { + type: ArgumentType.STRING, + defaultValue: "Warning" + } + } + }, + { + opcode: 'error', + text: 'error [INFO]', + blockType: BlockType.COMMAND, + arguments: { + INFO: { + type: ArgumentType.STRING, + defaultValue: "Error" + } + } + }, + ] + }; + } + + _addLog(log, style) { + const logElement = this.consoleLogs.appendChild(document.createElement("p")); + this._logs.push(log); + logElement.style = 'white-space: break-spaces;'; + if (style) { + logElement.style = `white-space: break-spaces; ${style}`; + } + logElement.innerHTML = xmlEscape(log); + this.consoleLogs.scrollBy(0, 1000000); + } + _parseCommand(command) { + const rawCommand = Cast.toString(command); + const data = { + command: '', + args: [] + }; + let chunk = ''; + let readingCommand = true; + let isInString = false; + let idx = -1; // idx gets added to at the start since there a bunch of continue statemnets + for (const letter of rawCommand.split('')) { + idx++; + if (readingCommand) { + if (letter === ' ' || letter === '\t') { + if (chunk.length <= 0) { + throw new SyntaxError('No command before white-space'); + } + data.command = chunk; + chunk = ''; + readingCommand = false; + continue; + } + chunk += letter; + continue; + } + // we are reading args + if (!isInString) { + if (letter !== '"') { + if (letter === ' ' || letter === '\t') { + data.args.push(chunk); + chunk = ''; + continue; + } + chunk += letter; + continue; + } else { + if (chunk.length > 0) { + // ex: run thing"Hello!" + throw new SyntaxError("Cannot prefix string argument"); + } + isInString = true; + continue; + } + } + // we are inside of a string + if (letter === '"' && !isEscapedQuote(rawCommand, idx)) { + isInString = false; + data.args.push(JSON.parse(`"${chunk}"`)); + chunk = ''; + } else { + chunk += letter; + } + } + // reached end of the command + if (isInString) throw new SyntaxError('String never terminates in command'); + if (readingCommand && chunk.length > 0) { + data.command = chunk; + readingCommand = false; + } else if (chunk.length > 0) { + data.args.push(chunk); + } + return data; + } + _runCommand(parsedCommand) { + if (!parsedCommand) return; + if (!parsedCommand.command) return; + const command = parsedCommand.command; + const args = parsedCommand.args; + switch (command) { + case 'help': { + if (args.length > 0) { + const command = args[0]; + let explanation = "No description defined for this command."; + if (command in this.commandExplanations) { + explanation = this.commandExplanations[command]; + } else if (command in CommandDescriptions) { + explanation = CommandDescriptions[command]; + } + this._addLog(`- Command: ${command}\n${explanation}`); + break; + } + const commadnDescriptions = { + ...this.commandExplanations, + ...CommandDescriptions, + }; + let log = ""; + for (const commandName in commadnDescriptions) { + log += `${commandName} - ${commadnDescriptions[commandName]}\n`; + } + this._addLog(log); + break; + } + case 'exit': + this.closeDebugger(); + break; + default: + if (!(command in this.commandSet)) { + this._addLog(`Command "${command}" not found. Check "help" for command list.`, "color: red;"); + break; + } + try { + this.commandSet[command](...args); + } catch (err) { + this._addLog(`Error: ${err}`, "color: red;"); + } + break; + } + } + _findBlockFromId(id, target) { + if (!target) return; + if (!target.blocks) return; + if (!target.blocks._blocks) return; + const block = target.blocks._blocks[id]; + return block; + } + + openDebugger() { + this.console.style.display = ''; + } + closeDebugger() { + this.console.style.display = 'none'; + } + + log(args) { + const text = Cast.toString(args.INFO); + console.log(text); + this._addLog(text); + } + warn(args) { + const text = Cast.toString(args.INFO); + console.warn(text); + this._addLog(text, "color: yellow;"); + } + error(args, util) { + // create error stack + const stack = []; + const target = util.target; + const thread = util.thread; + if (thread.stackClick) { + stack.push('clicked blocks'); + } + const commandBlockId = thread.peekStack(); + const block = this._findBlockFromId(commandBlockId, target); + if (block) { + stack.push(`block ${block.opcode}`); + } else { + stack.push(`block ${commandBlockId}`); + } + const eventBlock = this._findBlockFromId(thread.topBlock, target); + if (eventBlock) { + stack.push(`event ${eventBlock.opcode}`); + } else { + stack.push(`event ${thread.topBlock}`); + } + stack.push(`sprite ${target.sprite.name}`); + + const text = `Error: ${Cast.toString(args.INFO)}` + + `\n${stack.map(text => (`\tat ${text}`)).join("\n")}`; + console.error(text); + this._addLog(text, "color: red;"); + } +} + +module.exports = jgDebuggingBlocks; diff --git a/local-scratch-vm/src/extensions/jg_dev/index.js b/local-scratch-vm/src/extensions/jg_dev/index.js new file mode 100644 index 0000000000000000000000000000000000000000..45f7465f5a19569d0417157c1944215c517c8bae --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_dev/index.js @@ -0,0 +1,712 @@ +const BlockType = require('../../extension-support/block-type'); +const BlockShape = require('../../extension-support/block-shape'); +const ArgumentType = require('../../extension-support/argument-type'); +const ArgumentAlignment = require('../../extension-support/argument-alignment'); +const Cast = require('../../util/cast'); +const MathUtil = require('../../util/math-util'); +const test_indicator = require('./test_indicator.png'); + +const pathToMedia = 'static/blocks-media'; + +/** + * Class for Dev blocks + * @constructor + */ +class JgDevBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + // register compiled blocks + this.runtime.registerCompiledExtensionBlocks('jgDev', this.getCompileInfo()); + } + + // util + + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgDev', + name: 'Test Extension', + color1: '#4275f5', + color2: '#425df5', + blocks: [ + { + opcode: 'stopSound', + text: 'stop sound [ID]', + blockType: BlockType.COMMAND, + arguments: { + ID: { type: ArgumentType.STRING, defaultValue: "id" } + } + }, + { + opcode: 'starttimeSound', + text: 'start sound [ID] at seconds [SEX]', + blockType: BlockType.COMMAND, + arguments: { + ID: { type: ArgumentType.SOUND, defaultValue: "name or index" }, + SEX: { type: ArgumentType.NUMBER, defaultValue: 0 } + } + }, + { + opcode: 'transitionSound', + text: 'set sound [ID] volume transition to seconds [SEX]', + blockType: BlockType.COMMAND, + arguments: { + ID: { type: ArgumentType.SOUND, defaultValue: "sound to set fade out effect on" }, + SEX: { type: ArgumentType.NUMBER, defaultValue: 1 } + } + }, + { + opcode: 'logArgs1', + text: 'costume input [INPUT] sound input [INPUT2]', + blockType: BlockType.REPORTER, + arguments: { + INPUT: { type: ArgumentType.COSTUME }, + INPUT2: { type: ArgumentType.SOUND } + } + }, + { + opcode: 'logArgs2', + text: 'variable input [INPUT] list input [INPUT2]', + blockType: BlockType.REPORTER, + arguments: { + INPUT: { type: ArgumentType.VARIABLE }, + INPUT2: { type: ArgumentType.LIST } + } + }, + { + opcode: 'logArgs3', + text: 'broadcast input [INPUT]', + blockType: BlockType.REPORTER, + arguments: { + INPUT: { type: ArgumentType.BROADCAST } + } + }, + { + opcode: 'logArgs4', + text: 'color input [INPUT]', + blockType: BlockType.REPORTER, + arguments: { + INPUT: { type: ArgumentType.COLOR } + } + }, + { + opcode: 'setEffectName', + text: 'set [EFFECT] to [VALUE]', + blockType: BlockType.COMMAND, + arguments: { + EFFECT: { type: ArgumentType.STRING, defaultValue: "color" }, + VALUE: { type: ArgumentType.NUMBER, defaultValue: 0 } + } + }, + { + opcode: 'setBlurEffect', + text: 'set blur [PX]px', + blockType: BlockType.COMMAND, + arguments: { + PX: { type: ArgumentType.NUMBER, defaultValue: 0 } + } + }, + { + opcode: 'restartFromTheTop', + text: 'restart from the top [ICON]', + blockType: BlockType.COMMAND, + isTerminal: true, + arguments: { + ICON: { + type: ArgumentType.IMAGE, + dataURI: pathToMedia + "/repeat.svg" + } + } + }, + { + opcode: 'doodooBlockLolol', + text: 'ignore blocks inside [INPUT]', + branchCount: 1, + blockType: BlockType.CONDITIONAL, + arguments: { + INPUT: { type: ArgumentType.BOOLEAN } + } + }, + { + opcode: 'ifFalse', + text: 'if [INPUT] is false', + branchCount: 1, + blockType: BlockType.CONDITIONAL, + arguments: { + INPUT: { type: ArgumentType.BOOLEAN } + } + }, + { + opcode: 'multiplyTest', + text: 'multiply [VAR] by [MULT] then', + branchCount: 1, + blockType: BlockType.CONDITIONAL, + arguments: { + VAR: { type: ArgumentType.STRING, menu: "variable" }, + MULT: { type: ArgumentType.NUMBER, defaultValue: 4 } + } + }, + { + opcode: 'compiledIfNot', + text: 'if not [CONDITION] then (compiled)', + branchCount: 1, + blockType: BlockType.CONDITIONAL, + arguments: { + CONDITION: { type: ArgumentType.BOOLEAN } + } + }, + { + opcode: 'compiledReturn', + text: 'return [RETURN]', + blockType: BlockType.COMMAND, + isTerminal: true, + arguments: { + RETURN: { type: ArgumentType.STRING, defaultValue: '1' } + } + }, + { + opcode: 'compiledOutput', + text: 'compiled code', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'branchNewThread', + text: 'new thread', + branchCount: 1, + blockType: BlockType.CONDITIONAL + }, + { + opcode: 'whatthescallop', + text: 'bruh [numtypeableDropdown] [typeableDropdown] overriden: [overridennumtypeableDropdown] [overridentypeableDropdown]', + arguments: { + numtypeableDropdown: { + menu: 'numericTypeableTest' + }, + typeableDropdown: { + menu: 'typeableTest' + }, + overridennumtypeableDropdown: { + menu: 'numericTypeableTest', + defaultValue: 5 + }, + overridentypeableDropdown: { + menu: 'typeableTest', + defaultValue: 'your mom' + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'booleanMonitor', + text: 'boolean monitor', + blockType: BlockType.BOOLEAN + }, + { + opcode: 'ifFalseReturned', + text: 'if [INPUT] is false (return)', + branchCount: 1, + blockType: BlockType.CONDITIONAL, + arguments: { + INPUT: { type: ArgumentType.BOOLEAN } + } + }, + { + opcode: 'turbrowaorploop', + blockType: BlockType.LOOP, + text: 'my repeat [TIMES]', + arguments: { + TIMES: { + type: ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: 'alignmentTestate', + blockType: BlockType.CONDITIONAL, + text: [ + 'this block tests alignments', + 'left', + 'middle', + 'right' + ], + alignments: [ + null, + null, + ArgumentAlignment.LEFT, + null, + ArgumentAlignment.CENTER, + null, + ArgumentAlignment.RIGHT + ], + branchCount: 3 + }, + { + opcode: 'squareReporter', + text: 'square boy', + blockType: BlockType.REPORTER, + blockShape: BlockShape.SQUARE + }, + { + opcode: 'branchIndicatorTest', + text: 'this has a custom branchIndicator', + branchCount: 1, + blockType: BlockType.CONDITIONAL, + branchIndicator: test_indicator + }, + { + opcode: 'givesAnError', + text: 'throw an error', + blockType: BlockType.COMMAND + }, + { + opcode: 'hiddenBoolean', + text: 'im actually a boolean output', + blockType: BlockType.REPORTER, + forceOutputType: 'Boolean', + disableMonitor: true + }, + { + opcode: 'varvarvavvarvarvar', + text: 'varibles!?!?!??!?!?!?!?!!!?!?! [variable]', + arguments: { + variable: { + menu: 'variableInternal' + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'green', + text: 'im literally just green', + blockType: BlockType.REPORTER, + color1: '#00ff00', + color2: '#000000', + color3: '#000000', + disableMonitor: true + }, + { + opcode: 'duplicato', + text: 'duplicato', + blockType: BlockType.REPORTER, + canDragDuplicate: true, + disableMonitor: true, + hideFromPalette: true + }, + { + opcode: 'theheheuoihew9h9', + blockType: BlockType.COMMAND, + text: 'This block will appear in the penguinmod wiki [SEP] [DUPLIC]', + arguments: { + SEP: { + type: ArgumentType.SEPERATOR, + }, + DUPLIC: { + type: ArgumentType.STRING, + fillIn: 'duplicato', + } + } + }, + { + opcode: 'costumeTypeTest', + blockType: BlockType.REPORTER, + text: 'test custom type updating/rendering (new instance)' + }, + { + opcode: 'costumeTypeTestSame', + blockType: BlockType.REPORTER, + text: 'test custom type updating/rendering (same instance)' + }, + { + opcode: 'spriteDefaultType', + blockType: BlockType.REPORTER, + text: 'get this target' + }, + { + opcode: 'spriteDefaultTypeOther', + blockType: BlockType.REPORTER, + text: 'get stage target' + }, + { + opcode: 'costumeDefaultType', + blockType: BlockType.REPORTER, + text: 'get current costume' + }, + { + opcode: 'soundDefaultType', + blockType: BlockType.REPORTER, + text: 'get first sound' + } + ], + menus: { + variableInternal: { + variableType: 'scalar' + }, + variable: "getVariablesMenu", + numericTypeableTest: { + items: [ + 'item1', + 'item2', + 'item3' + ], + isTypeable: true, + isNumeric: true + }, + typeableTest: { + items: [ + 'item1', + 'item2', + 'item3' + ], + isTypeable: true, + isNumeric: false + } + } + }; + } + spriteDefaultType(args, util) { + return util.target; + } + spriteDefaultTypeOther(args, util) { + return this.runtime.getTargetForStage(); + } + costumeDefaultType(args, util) { + return util.target.getCostumeType(util.target.currentCostume); + } + soundDefaultType(args, util) { + return util.target.getSoundType(0); + } + costumeTypeTest() { + return { + _monitorUpToDate: false, + costumId: 'thing', + num: Math.sin(Date.now() / 1000), + toReporterContent() { + const el = document.createElement('span'); + el.style.color = '#F00'; + el.textContent = this.num; + return el; + }, + toMonitorContent() { + this._monitorUpToDate = true; + const el = document.createElement('span'); + el.style.color = '#0F0'; + el.textContent = this.num; + return el; + }, + toListItem() { + this._monitorUpToDate = true; + const el = document.createElement('span'); + el.style.color = '#00F'; + el.textContent = this.num; + return el; + }, + toListEditor() { + return `[num ${this.num}]`; + }, + fromListEditor(thing) { + this.num = Number(thing.slice(5, -1)); + return this; + } + }; + } + costumeTypeTestSame() { + if (!this.custom) this.custom = this.costumeTypeTest(); + this.custom.num = Math.sin(Date.now() / 1000); + this.custom._monitorUpToDate = false; + return this.custom; + } + /** + * This function is used for any compiled blocks in the extension if they exist. + * Data in this function is given to the IR & JS generators. + * Data must be valid otherwise errors may occur. + * @returns {object} functions that create data for compiled blocks. + */ + getCompileInfo() { + return { + ir: { + compiledIfNot: (generator, block) => ({ + kind: 'stack', /* this gets replaced but we still need to say what type of block this is */ + condition: generator.descendInputOfBlock(block, 'CONDITION'), + whenTrue: generator.descendSubstack(block, 'SUBSTACK'), + whenFalse: [] + }), + compiledReturn: (generator, block) => ({ + kind: 'stack', + return: generator.descendInputOfBlock(block, 'RETURN') + }), + restartFromTheTop: () => ({ + kind: 'stack' + }), + compiledOutput: () => ({ + kind: 'input' /* input is output :troll: (it makes sense in the ir & jsgen implementation ok) */ + }) + }, + js: { + compiledIfNot: (node, compiler, imports) => { + compiler.source += `if (!(${compiler.descendInput(node.condition).asBoolean()})) {\n`; + compiler.descendStack(node.whenTrue, new imports.Frame(false)); + // only add the else branch if it won't be empty + // this makes scripts have a bit less useless noise in them + if (node.whenFalse.length) { + compiler.source += `} else {\n`; + compiler.descendStack(node.whenFalse, new imports.Frame(false)); + } + compiler.source += `}\n`; + }, + compiledReturn: (node, compiler) => { + compiler.source += `return ${compiler.descendInput(node.return).asString()};`; + }, + restartFromTheTop: (_, compiler) => { + compiler.source += `runtime._restartThread(thread);`; + compiler.source += `return;`; + }, + compiledOutput: (_, compiler, imports) => { + const code = Cast.toString(compiler.source); + return new imports.TypedInput(JSON.stringify(code), imports.TYPE_STRING); + } + } + }; + } + + varvarvavvarvarvar(args) { + return JSON.stringify(args); + } + + // menu + getVariablesMenu() { + // menus can only be opened in the editor so use editingTarget + const target = vm.editingTarget; + const emptyMenu = [{ text: "", value: "" }]; + if (!target) return emptyMenu; + if (!target.variables) return emptyMenu; + const menu = Object.getOwnPropertyNames(target.variables).map(variableId => { + const variable = target.variables[variableId]; + return { + text: variable.name, + value: variable.name + }; + }); + // check if menu has 0 items because pm throws an error if theres no items + return (menu.length > 0) ? menu : emptyMenu; + } + + branchIndicatorTest() { + return; // dude logs wont shut up because i didnt define this func + } + + // util + _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; + } + + // blocks + + branchNewThread(_, util) { + // CubesterYT probably + if (util.thread.target.blocks.getBranch(util.thread.peekStack(), 0)) { + util.sequencer.runtime._pushThread( + util.thread.target.blocks.getBranch(util.thread.peekStack(), 0), + util.target, + {} + ); + } + } + + booleanMonitor() { + return Math.round(Math.random()) == 1; + } + + stopSound(args, util) { + const target = util.target; + const sprite = target.sprite; + if (!sprite) return; + + const soundBank = sprite.soundBank; + if (!soundBank) return; + + const id = Cast.toString(args.ID); + soundBank.stop(target, id); + } + starttimeSound(args, util) { + const id = Cast.toString(args.ID); + const index = this._getSoundIndex(id, util); + if (index < 0) return; + + const target = util.target; + const sprite = target.sprite; + if (!sprite) return; + if (!sprite.sounds) return; + + const { soundId } = sprite.sounds[index]; + + const soundBank = sprite.soundBank; + if (!soundBank) return; + + soundBank.playSound(target, soundId, Cast.toNumber(args.SEX)); + } + transitionSound(args, util) { + const id = Cast.toString(args.ID); + const index = this._getSoundIndex(id, util); + if (index < 0) return; + + const target = util.target; + const sprite = target.sprite; + if (!sprite) return; + if (!sprite.sounds) return; + + const { soundId } = sprite.sounds[index]; + + const soundBank = sprite.soundBank; + if (!soundBank) return; + + soundBank.soundPlayers[soundId].stopFadeDecay = Cast.toNumber(args.SEX); + } + + green() { + return 'g'; + } + + logArgs1(args) { + console.log(args); + return JSON.stringify(args); + } + logArgs2(args) { + console.log(args); + return JSON.stringify(args); + } + logArgs3(args) { + console.log(args); + return JSON.stringify(args); + } + logArgs4(args) { + console.log(args); + return JSON.stringify(args); + } + + setEffectName(args, util) { + const PX = Cast.toNumber(args.VALUE); + util.target.setEffect(args.EFFECT, PX); + } + setBlurEffect(args, util) { + const PX = Cast.toNumber(args.PX); + util.target.setEffect("blur", PX); + } + + doodooBlockLolol(args, util) { + if (args.INPUT === true) return; + console.log(args, util); + util.startBranch(1, false); + console.log(util.target.getCurrentCostume()); + } + + ifFalse(args, util) { + console.log(args, util); + if (!args.INPUT) { + util.startBranch(1, false); + } + } + ifFalseReturned(args) { + if (!args.INPUT) { + return 1; + } + } + turbrowaorploop ({TIMES}, util) { + const times = Math.round(Cast.toNumber(TIMES)); + if (typeof util.stackFrame.loopCounter === 'undefined') { + util.stackFrame.loopCounter = times; + } + util.stackFrame.loopCounter--; + if (util.stackFrame.loopCounter >= 0) { + return true; + } + } + // compiled blocks should have interpreter versions + compiledIfNot(args, util) { + const condition = Cast.toBoolean(args.CONDITION); + if (!condition) { + util.startBranch(1, false); + } + } + compiledReturn() { + return 'noop'; + } + restartFromTheTop() { + return 'noop'; + } + compiledOutput() { + return ''; + } + + hiddenBoolean() { + return true; + } + + multiplyTest(args, util) { + const target = util.target; + Object.getOwnPropertyNames(target.variables).forEach(variableId => { + const variable = target.variables[variableId]; + if (variable.name !== Cast.toString(args.VAR)) return; + console.log(variable); + if (typeof variable.value !== 'number') { + variable.value = 0; + } + variable.value *= Cast.toNumber(args.MULT); + }); + } + + whatthescallop(args) { + return JSON.stringify(args); + } + + squareReporter() { + return 0; + } + alignmentTestate() { + return; + } + givesAnError() { + throw new Error('woah an error'); + } +} + +module.exports = JgDevBlocks; diff --git a/local-scratch-vm/src/extensions/jg_dev/test_indicator.png b/local-scratch-vm/src/extensions/jg_dev/test_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..ac152e0c3fbd8f1fed89f60bac0109af58e92942 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_dev/test_indicator.png differ diff --git a/local-scratch-vm/src/extensions/jg_doodoo/index.js b/local-scratch-vm/src/extensions/jg_doodoo/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c5c557e80ba866ffa90002bfdc001f3fd266c1b4 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_doodoo/index.js @@ -0,0 +1,209 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const MathUtil = require('../../util/math-util'); +const ArrayBufferTool = require('../../util/array buffer'); +const PermissionAsk = require('../../util/ask-for-permision'); +const AsyncLimiter = require('../../util/async-limiter'); +const Base64Util = require('../../util/base64-util'); +const Clone = require('../../util/clone'); +const CustomExtToCore = require('../../util/custom-ext-api-to-core'); +const FetchTimeout = require('../../util/fetch-with-timeout'); +const MonitorId = require('../../util/get-monitor-id'); +const JavascriptContainer = require('../../util/javascript-container'); +const JSONBlockUtil = require('../../util/json-block-utilities'); +const JSONRPC = require('../../util/jsonrpc'); +const Log = require('../../util/log'); + +const randomNumber = (start, end) => { + return start + Math.round(Math.random() * (end - start)); +} + +const CHARCODEAMOUNT = 16384; +const IP_ADDRESS = `${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}`; + +/** + * Class for blocks + * @constructor + */ +class JgDooDooBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgDooDoo', + name: 'doo doo', + color1: '#59C059', + color2: '#46B946', + color3: '#389438', + blocks: [ + { + opcode: 'returnSelectedCharacter', + text: '[CHAR]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + CHAR: { type: ArgumentType.STRING, menu: "funny" } + } + }, + { + text: 'ip addresses are fake', + blockType: BlockType.LABEL, + }, + { + text: '(sorry not sorry)', + blockType: BlockType.LABEL, + }, + { + opcode: 'fullNameIp', + text: 'ip address of [NAME]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "gloobert dooben" } + } + }, + { + opcode: 'randomStartupIp', + text: 'ip address', + blockType: BlockType.REPORTER, + disableMonitor: false + }, + { + opcode: 'chicago', + text: 'chicago', + blockType: BlockType.REPORTER, + disableMonitor: false + }, + '---', + { + opcode: 'doodoo', + text: 'go to x: 17 y: 36', + blockType: BlockType.COMMAND, + disableMonitor: false + }, + { + opcode: 'visualReportbad', + text: 'give me admin on PenguinMod', + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'launchroblox', + text: 'launch roblox', + blockType: BlockType.COMMAND + }, + { + opcode: 'launchrobloxgame', + text: 'open roblox game id: [ID]', + blockType: BlockType.COMMAND, + arguments: { + ID: { + type: ArgumentType.NUMBER, + defaultValue: 11219669059 + } + } + }, + ], + menus: { + funny: "getAllCharacters" + } + }; + } + + // menus + getAllCharacters() { + const charArray = []; + for (let i = 8; i < (CHARCODEAMOUNT + 1); i++) { + charArray.push(String.fromCharCode(i)); + } + return charArray.map(item => ({ text: item, value: item })) + } + + // util + + // blocks + returnSelectedCharacter(args) { + return Cast.toString(args.CHAR); + } + randomStartupIp() { + return IP_ADDRESS; + } + chicago() { + return 'Chicago, IL'; + } + doodoo(_, util) { + util.target.setXY(17, 36); + } + visualReportbad(_, util) { + if (!util.thread) return; + this.runtime.visualReport(util.thread.topBlock, "no"); + } + fullNameIp(args) { + return new Promise((resolve, reject) => { + const name = Cast.toString(args.NAME).toLowerCase().replace(/[^A-Za-z ]+/gmi, ""); + if (!name) return resolve("A name is required"); + if (!name.includes(" ")) return resolve("2nd name required"); + const splitName = name.split(" "); + if ((splitName[0].length <= 0) || (splitName[1].length <= 0)) { + return resolve("Put the first and second name"); + } + setTimeout(() => { + const array = []; + const nameValues = { + first: 0, + last: 0 + } + + splitName[0].split("").forEach(char => { + nameValues.first += String(char).charCodeAt(0) * 1.53; + }) + splitName[1].split("").forEach(char => { + nameValues.last += String(char).charCodeAt(0) * 1.35; + }) + + nameValues.first = Math.ceil(nameValues.first) % 253; + nameValues.last = Math.floor(nameValues.last) % 235; + + array.push(nameValues.first); + array.push(Math.round(nameValues.first / 3)); + array.push(nameValues.last); + array.push(Math.floor(nameValues.last / 2)); + + return resolve(array.join(".")); + }, 300 + Math.round(Math.random() * 1200)); + }) + } + launchroblox() { + if (!confirm('Launch Roblox?')) return; + const element = document.createElement("a"); + element.href = "roblox:"; + element.target = "_blank"; + element.style = "display: none;"; + document.body.appendChild(element); + element.click(); + element.remove(); + } + launchrobloxgame(args) { + if (!confirm('Launch Roblox?')) return; + const id = Cast.toString(args.ID); + const element = document.createElement("a"); + element.href = `roblox://placeID=${id}`; + element.target = "_blank"; + element.style = "display: none;"; + document.body.appendChild(element); + element.click(); + element.remove(); + } +} + +module.exports = JgDooDooBlocks; diff --git a/local-scratch-vm/src/extensions/jg_easySave/index.js b/local-scratch-vm/src/extensions/jg_easySave/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a3d30d93f3f4753146e9835f62fdee92cec97e00 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_easySave/index.js @@ -0,0 +1,112 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +/** + * Class for EasySave blocks + * @constructor + */ +class jgEasySaveBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgEasySave', + name: 'Easy Save', + color1: '#48a3d4', + color2: '#3d89b3', + blocks: [ + { + blockType: BlockType.LABEL, + text: "Saving" + }, + { + opcode: 'addVarToSave', + text: 'add value of variable [VAR] to save', + blockType: BlockType.COMMAND, + arguments: { + VAR: { + menu: "variable" + } + } + }, + { + opcode: 'addListToSave', + text: 'add value of list [LIST] to save', + blockType: BlockType.COMMAND, + arguments: { + LIST: { + menu: "list" + } + } + }, + { + blockType: BlockType.LABEL, + text: "Loading" + }, + ], + menus: { + variable: { + acceptReporters: false, + items: "getVariables", + }, + list: { + acceptReporters: false, + items: "getLists", + }, + } + }; + } + + getVariables() { + const variables = + // @ts-expect-error + typeof Blockly === "undefined" + ? [] + : // @ts-expect-error + Blockly.getMainWorkspace() + .getVariableMap() + .getVariablesOfType("") + .map((model) => ({ + text: model.name, + value: model.getId(), + })); + if (variables.length > 0) { + return variables; + } else { + return [{ text: "", value: "" }]; + } + } + getLists() { + // using blockly causes unstable behavior + // https://discord.com/channels/1033551490331197462/1038251742439149661/1202846831994863627 + const globalLists = Object.values(this.runtime.getTargetForStage().variables) + .filter((x) => x.type == "list"); + const localLists = Object.values(this.runtime.vm.editingTarget.variables) + .filter((x) => x.type == "list"); + const uniqueLists = [...new Set([...globalLists, ...localLists])]; + if (uniqueLists.length === 0) return [{ text: "", value: "" }]; + return uniqueLists.map((i) => ({ text: i.name, value: i.id })); + } + + addVarToSave(args, util) { + const variable = util.target.lookupVariableById(args.VAR); + console.log(variable); + } + addListToSave(args, util) { + console.log(args.LIST); + const variable = util.target.lookupVariableById(args.LIST); + console.log(variable); + } +} + +module.exports = jgEasySaveBlocks; diff --git a/local-scratch-vm/src/extensions/jg_files/index.js b/local-scratch-vm/src/extensions/jg_files/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c6f6e852f27a3d317b42514b6bb179c75a2b94a4 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_files/index.js @@ -0,0 +1,308 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const { validateArray } = require('../../util/json-block-utilities'); +const AHHHHHHHHHHHHHH = require('../../util/array buffer'); +const BufferStuff = new AHHHHHHHHHHHHHH(); + +/** + * Class for File blocks + * @constructor + */ +class JgFilesBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'jgFiles', + name: 'Files (legacy)', + color1: '#ffbb00', + color2: '#ffaa00', + // docsURI: 'https://docs.turbowarp.org/blocks', + blocks: [ + { + opcode: 'isFileReaderSupported', + text: 'can files be used?', + disableMonitor: false, + blockType: BlockType.BOOLEAN + }, + { + opcode: 'askUserForFileOfType', + text: 'ask user for a file of type [FILE_TYPE]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + FILE_TYPE: { + type: ArgumentType.STRING, + defaultValue: 'txt savefile' + } + } + }, + { + opcode: 'askUserForFileOfTypeAsArrayBuffer', + text: 'ask user for an array buffer file of type [FILE_TYPE]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + FILE_TYPE: { + type: ArgumentType.STRING, + defaultValue: 'txt savefile' + } + } + }, + { + opcode: 'askUserForFileOfTypeAsDataUri', + text: 'ask user for a data uri file of type [FILE_TYPE]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + FILE_TYPE: { + type: ArgumentType.STRING, + defaultValue: 'png' + } + } + }, + { + opcode: 'downloadFile', + text: 'download content [FILE_CONTENT] as file name [FILE_NAME]', + blockType: BlockType.COMMAND, + arguments: { + FILE_CONTENT: { + type: ArgumentType.STRING, + defaultValue: 'Hello!' + }, + FILE_NAME: { + type: ArgumentType.STRING, + defaultValue: 'text.txt' + } + } + }, + { + opcode: 'downloadFileDataUri', + text: 'download data uri [FILE_CONTENT] as file name [FILE_NAME]', + blockType: BlockType.COMMAND, + arguments: { + FILE_CONTENT: { + type: ArgumentType.STRING, + defaultValue: 'data:image/png;base64,' + }, + FILE_NAME: { + type: ArgumentType.STRING, + defaultValue: 'content.png' + } + } + }, + { + opcode: 'downloadFileBuffer', + text: 'download array buffer [FILE_CONTENT] as file name [FILE_NAME]', + blockType: BlockType.COMMAND, + arguments: { + FILE_CONTENT: { + type: ArgumentType.STRING, + defaultValue: '[]' + }, + FILE_NAME: { + type: ArgumentType.STRING, + defaultValue: 'data.bin' + } + } + } + ] + }; + } + + isFileReaderSupported () { + return (window.FileReader !== null) && (window.document !== null); + } + + dataURLtoBlob(dataurl) { + var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], + bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + return new Blob([u8arr], { type: mime }); + } + + __askUserForFile (acceptTypes) { + try { + return new Promise(resolve => { + const fileReader = new FileReader(); + fileReader.onload = e => { + resolve(e.target.result); + }; + const input = document.createElement("input"); + input.type = "file"; + if (acceptTypes !== null) { + input.accept = acceptTypes; + } + input.style.display = "none"; + document.body.append(input); + input.onchange = () => { + const file = input.files[0]; + if (!file) { + resolve(""); + return; + } + fileReader.readAsText(file); + + input.remove(); + }; + input.onblur = () => { + input.onchange(); + }; + input.focus(); + input.click(); + }); + } catch (e) { + return; + } + } + __askUserForFilearraybuffer (acceptTypes) { + try { + return new Promise(resolve => { + const fileReader = new FileReader(); + fileReader.onload = e => { + resolve(JSON.stringify(BufferStuff.bufferToArray(e.target.result))); + }; + const input = document.createElement("input"); + input.type = "file"; + if (acceptTypes !== null) { + input.accept = acceptTypes; + } + input.style.display = "none"; + document.body.append(input); + input.onchange = () => { + const file = input.files[0]; + if (!file) { + resolve(""); + return; + } + fileReader.readAsArrayBuffer(file); + + input.remove(); + }; + input.onblur = () => { + input.onchange(); + }; + input.focus(); + input.click(); + }); + } catch (e) { + return; + } + } + __askUserForFiledatauri (acceptTypes) { + try { + return new Promise(resolve => { + const fileReader = new FileReader(); + fileReader.onload = e => { + resolve(e.target.result); + }; + const input = document.createElement("input"); + input.type = "file"; + if (acceptTypes !== null) { + input.accept = acceptTypes; + } + input.style.display = "none"; + document.body.append(input); + input.onchange = () => { + const file = input.files[0]; + if (!file) { + resolve(""); + return; + } + fileReader.readAsDataURL(file); + + input.remove(); + }; + input.onblur = () => { + input.onchange(); + }; + input.focus(); + input.click(); + }); + } catch (e) { + return; + } + } + + askUserForFileOfType (args) { + const fileTypesAllowed = []; + const input = args.FILE_TYPE + .toLowerCase() + .replace(/.,/gmi, ""); + if (input === "any") return this.__askUserForFile(null); + input.split(" ").forEach(type => { + fileTypesAllowed.push(`.${type}`); + }); + return this.__askUserForFile(fileTypesAllowed.join(","), false); + } + askUserForFileOfTypeAsArrayBuffer (args) { + const fileTypesAllowed = []; + const input = args.FILE_TYPE + .toLowerCase() + .replace(/.,/gmi, ""); + if (input === "any") return this.__askUserForFilearraybuffer(null); + input.split(" ").forEach(type => { + fileTypesAllowed.push(`.${type}`); + }); + return this.__askUserForFilearraybuffer(fileTypesAllowed.join(",")); + } + askUserForFileOfTypeAsDataUri (args) { + const fileTypesAllowed = []; + const input = args.FILE_TYPE + .toLowerCase() + .replace(/.,/gmi, ""); + if (input === "any") return this.__askUserForFiledatauri(null); + input.split(" ").forEach(type => { + fileTypesAllowed.push(`.${type}`); + }); + return this.__askUserForFiledatauri(fileTypesAllowed.join(",")); + } + + downloadFile (args, _, __, downloadArray, downloadBase64) { + let content = ""; + let fileName = "text.txt"; + + content = String(args.FILE_CONTENT) || content; + fileName = String(args.FILE_NAME) || fileName; + + const array = validateArray(args.FILE_CONTENT); + if (array.isValid && downloadArray) { + content = BufferStuff.arrayToBuffer(array.array); + } + + let blob; + if (downloadBase64) { + blob = this.dataURLtoBlob(content); + } else { + blob = new Blob([content]); + } + const a = document.createElement("a"); + a.style.display = "none"; + document.body.append(a); + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = fileName; + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + } + downloadFileDataUri(args) { + return this.downloadFile(args, null, null, false, true); + } + downloadFileBuffer(args) { + return this.downloadFile(args, null, null, true, false); + } +} + +module.exports = JgFilesBlocks; diff --git a/local-scratch-vm/src/extensions/jg_files/packager-friendly.js b/local-scratch-vm/src/extensions/jg_files/packager-friendly.js new file mode 100644 index 0000000000000000000000000000000000000000..6a67df094d0885046ba595b30b9e01d167673c75 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_files/packager-friendly.js @@ -0,0 +1,148 @@ +const { validateArray } = require('../../util/json-block-utilities') +class JgFilesBlocks { + constructor(runtime, id) { + //cq: ignore + //ext stuff + this.runtime = runtime; + this.menuIconURI = null; + this.blockIconURI = null; + this.colorScheme = ["#ffbb00", "ffaa00"]; + } + + getInfo() { + return { + id: 'JgFilesBlocks', + name: 'Files', + blockIconURI: this.blockIconURI, + menuIconURI: this.menuIconURI, + color1: this.colorScheme[0], + color2: this.colorScheme[1], + blocks: [ + { + opcode: 'isFileReaderSupported', + text: 'can files be used?', + disableMonitor: false, + blockType: Scratch.BlockType.BOOLEAN + }, + { + opcode: 'askUserForFile', + text: 'ask user for a file', + disableMonitor: true, + blockType: Scratch.BlockType.REPORTER + }, + { + opcode: 'askUserForFileOfType', + text: 'ask user for a file of type [FILE_TYPE]', + disableMonitor: true, + blockType: Scratch.BlockType.REPORTER, + arguments: { + FILE_TYPE: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'any' + } + } + }, + { + opcode: 'downloadFile', + text: 'download content [FILE_CONTENT] as file name [FILE_NAME]', + blockType: Scratch.BlockType.COMMAND, + arguments: { + FILE_CONTENT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Hello!' + }, + FILE_NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'text.txt' + } + } + }, + ] + } + } + isFileReaderSupported() { + return (window.FileReader != null) && (window.document != null); + } + __askUserForFile(acceptTypes) { + return new Promise((resolve, _) => { + const fileReader = new FileReader(); + fileReader.onload = (e) => { + resolve(e.target.result); + } + const input = document.createElement("input"); + input.type = "file"; + if (acceptTypes != null) { + input.accept = acceptTypes + } + input.style.display = "none"; + document.body.append(input); + input.onchange = () => { + const file = input.files[0]; + if (!file) { + resolve("[]"); + return; + } else { + fileReader.readAsArrayBuffer(file) + } + input.remove(); + } + input.onblur = () => { + input.onchange(); + } + input.focus(); + input.click(); + }) + } + askUserForFile() { + return this.__askUserForFile(null); + } + askUserForFileOfType(args) { + const fileTypesAllowed = []; + const input = String(args.FILE_TYPE).toLowerCase().replace(/.,/gmi, ""); + if (input == "any") + return this.__askUserForFile(null); + input.split(" ").forEach(type => { + fileTypesAllowed.push("." + type); + }) + return this.__askUserForFile(fileTypesAllowed.join(",")); + } + downloadFile(args) { + let content = ""; + let fileName = "text.txt"; + content = String(args.FILE_CONTENT) || content; + fileName = String(args.FILE_NAME) || fileName; + + const array = validateArray(args.FILE_CONTENT) + if (array.length > 0 && typeof array[0] == 'number') { + content = array + } + + const blob = new Blob([content]); + const a = document.createElement("a"); + a.style.display = "none"; + document.body.append(a); + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = fileName; + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + } +}; +// Scratch.extensions.register(new JgFilesBlocks()); +(function () { + var extensionClass = JgFilesBlocks; + if (typeof window === "undefined" || !window.vm) { + console.error("JgFilesBlocks is not supported in this environment."); + } else { + var extensionInstance = new extensionClass( + window.vm.extensionManager.runtime + ); + var serviceName = + window.vm.extensionManager._registerInternalExtension(extensionInstance); + window.vm.extensionManager._loadedExtensions.set( + extensionInstance.getInfo().id, + serviceName + ); + console.info("JgFilesBlocks has loaded."); + }}); diff --git a/local-scratch-vm/src/extensions/jg_files/switches.json b/local-scratch-vm/src/extensions/jg_files/switches.json new file mode 100644 index 0000000000000000000000000000000000000000..f10577be2236345dbd39896c5addf509e8088385 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_files/switches.json @@ -0,0 +1,32 @@ +{ + "askUserForFileOfType": [ + "hide", + "askUserForFileOfTypeAsArrayBuffer", + "askUserForFileOfTypeAsDataUri" + ], + "askUserForFileOfTypeAsArrayBuffer": [ + "askUserForFileOfType", + "hide", + "askUserForFileOfTypeAsDataUri" + ], + "askUserForFileOfTypeAsDataUri": [ + "askUserForFileOfType", + "askUserForFileOfTypeAsArrayBuffer", + "hide" + ], + "downloadFile": [ + "hide", + "downloadFileDataUri", + "downloadFileBuffer" + ], + "downloadFileDataUri": [ + "downloadFile", + "hide", + "downloadFileBuffer" + ], + "downloadFileBuffer": [ + "downloadFile", + "downloadFileDataUri", + "hide" + ] +} \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_files/test.js b/local-scratch-vm/src/extensions/jg_files/test.js new file mode 100644 index 0000000000000000000000000000000000000000..21f002eb81f6e883717d72208f3533838fcca51d --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_files/test.js @@ -0,0 +1,52 @@ +class YourExt { + constructor(runtime, id) { + //ext stuff + this.runtime = runtime; + this.menuIconURI = null; + this.blockIconURI = null; + this.colorScheme = ["#41e2d0", "#0DA57A"]; + } + getInfo() { + return { + id: "gameutils", + name: "GameUtils", + blockIconURI: this.blockIconURI, + menuIconURI: this.menuIconURI, + color1: this.colorScheme[0], + color2: this.colorScheme[1], + blocks: [{ + "opcode": "LoadProject", + "blockType": "command", + "text": "Load Project from [url]", + "arguments": { + "url": { + "type": "string", + "defaultValue": "" + } + } + }, ] + } + } + async LoadProject(args, util) { + try { + const req = await fetch(args.ul) + if (req.status == 200) { + vm.loadProject(await req.blob()) + } else { + console.error("Could not load project") + } + } catch (e) { + console.error(e) + } + } + } + (function() { + var extensionClass = YourExt; + if (typeof window === "undefined" || !window.vm) { + Scratch.extensions.register(new extensionClass()); + } else { + var extensionInstance = new extensionClass(window.vm.extensionManager.runtime); + var serviceName = window.vm.extensionManager._registerInternalExtension(extensionInstance); + window.vm.extensionManager._loadedExtensions.set(extensionInstance.getInfo().id, serviceName); + }; + })() \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_iframe/index.js b/local-scratch-vm/src/extensions/jg_iframe/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a1e2e204da8de2304b76430a18f3de2a45e99cd5 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_iframe/index.js @@ -0,0 +1,827 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const ProjectPermissionManager = require('../../util/project-permissions'); +const Color = require('../../util/color'); +const Cast = require('../../util/cast'); + +const EffectOptions = { + acceptReporters: true, + items: [ + { text: "color", value: "color" }, + { text: "grayscale", value: "grayscale" }, + { text: "brightness", value: "brightness" }, + { text: "contrast", value: "contrast" }, + { text: "ghost", value: "ghost" }, + { text: "blur", value: "blur" }, + { text: "invert", value: "invert" }, + { text: "saturate", value: "saturate" }, + { text: "sepia", value: "sepia" } + ] +}; + +const urlToReportUrl = (url) => { + let urlObject; + try { + urlObject = new URL(url); + } catch { + // we cant really throw an error in this state since it halts any blocks + // or return '' since thatll just confuse the api likely + // so just use example.com + return 'example.com'; + } + // use host name + return urlObject.hostname; +}; + +// to avoid taking 1290 years for each url set +// we save the ones that we already checked +const safeOriginUrls = {}; + +/** + * uhhhhhhhhhh + * @param {Array} array the array + * @param {*} value the value + * @returns {Object} an object + */ +const ArrayToValue = (array, value) => { + const object = {}; + array.forEach(item => { + object[String(item)] = value; + }); + return object; +}; + +const isUrlRatedSafe = (url) => { + return new Promise((resolve) => { + const saveUrl = urlToReportUrl(url); + if (safeOriginUrls.hasOwnProperty(saveUrl)) { + return resolve(safeOriginUrls[saveUrl]); + } + + fetch(`https://pm-bapi.vercel.app/api/safeurl?url=${saveUrl}`).then(res => { + if (!res.ok) { + resolve(true); + return; + } + res.json().then(status => { + safeOriginUrls[saveUrl] = status.safe; + resolve(status.safe); + }).catch(() => resolve(true)); + }).catch(() => resolve(true)); + }) +} + +/** + * Class for IFRAME blocks + * @constructor + */ +class JgIframeBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + this.createdIframe = null; + this.iframeSettings = { + x: 0, + y: 0, + rotation: 90, + width: 480, + height: 360, + color: '#ffffff', + opacity: 0, + clickable: true + }; + this.iframeFilters = ArrayToValue(EffectOptions.items.map(item => item.value), 0); + this.iframeLoadedValue = false; + this.displayWebsiteUrl = ""; + this.runtime.on('PROJECT_STOP_ALL', () => { + // stop button clicked so delete the iframe + this.RemoveIFrame(); + }); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgIframe', + name: 'IFrame', + color1: '#F36518', + color2: '#E64D18', + blocks: [ + { + opcode: 'createIframeElement', + text: formatMessage({ + id: 'jgIframe.blocks.createIframeElement', + default: 'set new iframe', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND + }, + { + opcode: 'deleteIframeElement', + text: formatMessage({ + id: 'jgIframe.blocks.deleteIframeElement', + default: 'delete iframe', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND + }, + { + opcode: 'iframeElementExists', + text: formatMessage({ + id: 'jgIframe.blocks.iframeElementExists', + default: 'iframe exists?', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.BOOLEAN, + disableMonitor: true, + }, + "---", + "---", + { + opcode: 'whenIframeIsLoaded', + text: formatMessage({ + id: 'jgIframe.blocks.whenIframeIsLoaded', + default: 'when iframe loads site', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.HAT + }, + { + opcode: 'setIframeUrl', + text: formatMessage({ + id: 'jgIframe.blocks.setIframeUrl', + default: 'set iframe url to [URL]', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: "https://www.example.com" + } + } + }, + { + opcode: 'setIframePosLeft', + text: formatMessage({ + id: 'jgIframe.blocks.setIframePosLeft', + default: 'set iframe x to [X]', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND, + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'setIframePosTop', + text: formatMessage({ + id: 'jgIframe.blocks.setIframePosTop', + default: 'set iframe y to [Y]', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND, + arguments: { + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'setIframeSizeWidth', + text: formatMessage({ + id: 'jgIframe.blocks.setIframeSizeWidth', + default: 'set iframe width to [WIDTH]', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND, + arguments: { + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 480 + } + } + }, + { + opcode: 'setIframeSizeHeight', + text: formatMessage({ + id: 'jgIframe.blocks.setIframeSizeHeight', + default: 'set iframe height to [HEIGHT]', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND, + arguments: { + HEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 360 + } + } + }, + { + opcode: 'setIframeRotation', + text: formatMessage({ + id: 'jgIframe.blocks.setIframeRotation', + default: 'point iframe in direction [ROTATE]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + ROTATE: { + type: ArgumentType.ANGLE, + defaultValue: 90 + } + } + }, + { + opcode: 'setIframeBackgroundColor', + text: formatMessage({ + id: 'jgIframe.blocks.setIframeBackgroundColor', + default: 'set iframe background color to [COLOR]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + COLOR: { + type: ArgumentType.COLOR + } + } + }, + { + opcode: 'setIframeBackgroundOpacity', + text: formatMessage({ + id: 'jgIframe.blocks.setIframeBackgroundOpacity', + default: 'set iframe background transparency to [GHOST]%', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + GHOST: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'setIframeClickable', + text: formatMessage({ + id: 'jgIframe.blocks.setIframeClickable', + default: 'toggle iframe to be [USABLE]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + USABLE: { + type: ArgumentType.STRING, + menu: 'iframeClickable' + } + } + }, + { + opcode: 'showIframeElement', + text: formatMessage({ + id: 'jgIframe.blocks.showIframeElement', + default: 'show iframe', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND + }, + { + opcode: 'hideIframeElement', + text: formatMessage({ + id: 'jgIframe.blocks.hideIframeElement', + default: 'hide iframe', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.COMMAND + }, + { + opcode: 'getIframeLeft', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeLeft', + default: 'iframe x', + description: '' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getIframeTop', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeTop', + default: 'iframe y', + description: '' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getIframeWidth', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeWidth', + default: 'iframe width', + description: '' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getIframeHeight', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeHeight', + default: 'iframe height', + description: '' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getIframeRotation', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeRotation', + default: 'iframe rotation', + description: '' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getIframeBackgroundColor', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeBackgroundColor', + default: 'iframe background color', + description: '' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getIframeBackgroundOpacity', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeBackgroundOpacity', + default: 'iframe background transparency', + description: '' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getIframeTargetUrl', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeTargetUrl', + default: 'iframe target url', + description: '' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'iframeElementIsHidden', + text: formatMessage({ + id: 'jgIframe.blocks.iframeElementIsHidden', + default: 'iframe is hidden?', + description: 'im too lazy to write these anymore tbh' + }), + blockType: BlockType.BOOLEAN, + disableMonitor: true, + }, + { + opcode: 'getIframeClickable', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeClickable', + default: 'iframe is interactable?', + description: '' + }), + blockType: BlockType.BOOLEAN, + disableMonitor: true, + }, + "---", + "---", + // effects YAYYAYAWOOHOOO YEEAAAAAAAAA + { + opcode: 'iframeElementSetEffect', + text: formatMessage({ + id: 'jgIframe.blocks.iframeElementSetEffect', + default: 'set [EFFECT] effect on iframe to [AMOUNT]', + description: 'YAYYAYAWOOHOOO YEEAAAAAAAAAYAYYAYAWOOHOOO YEEAAAAAAAAA' + }), + blockType: BlockType.COMMAND, + arguments: { + EFFECT: { + type: ArgumentType.STRING, + menu: 'effects', + defaultValue: "color" + }, + AMOUNT: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'iframeElementChangeEffect', + text: formatMessage({ + id: 'jgIframe.blocks.iframeElementChangeEffect', + default: 'change [EFFECT] effect on iframe by [AMOUNT]', + description: 'YAYYAYAWOOHOOO YEEAAAAAAAAAYAYYAYAWOOHOOO YEEAAAAAAAAA' + }), + blockType: BlockType.COMMAND, + arguments: { + EFFECT: { + type: ArgumentType.STRING, + menu: 'effects', + defaultValue: "color" + }, + AMOUNT: { + type: ArgumentType.NUMBER, + defaultValue: 25 + } + } + }, + { + opcode: 'iframeElementClearEffects', + text: formatMessage({ + id: 'jgIframe.blocks.iframeElementClearEffects', + default: 'clear iframe effects', + description: 'YAYYAYAWOOHOOO YEEAAAAAAAAAYAYYAYAWOOHOOO YEEAAAAAAAAA' + }), + blockType: BlockType.COMMAND + }, + { + opcode: 'getIframeEffectAmount', + text: formatMessage({ + id: 'jgIframe.blocks.getIframeEffectAmount', + default: 'iframe [EFFECT]', + description: 'YAYYAYAWOOHOOO YEEAAAAAAAAAYAYYAYAWOOHOOO YEEAAAAAAAAA' + }), + blockType: BlockType.REPORTER, + arguments: { + EFFECT: { + type: ArgumentType.STRING, + menu: 'effects', + defaultValue: "color" + } + } + }, + "---" + ], + menus: { + effects: EffectOptions, + iframeClickable: { + acceptReporters: true, + items: [ + 'interactable', + 'non-interactable' + ] + } + } + }; + } + // permissions + async IsWebsiteAllowed(url) { + if (ProjectPermissionManager.IsDataUrl(url)) return true; + if (!ProjectPermissionManager.IsUrlSafe(url)) return false; + const safe = await isUrlRatedSafe(url); + return safe; + } + + // utilities + GetCurrentCanvas() { + return this.runtime.renderer.canvas; + } + SetNewIFrame() { + const iframe = document.createElement("iframe"); + iframe.onload = () => { + this.iframeLoadedValue = true; + }; + this.createdIframe = iframe; + return iframe; + } + RemoveIFrame() { + if (this.createdIframe) { + this.createdIframe.remove(); + this.createdIframe = null; + } + } + GetIFrameState() { + if (this.createdIframe) { + return true; + } + return false; + } + SetIFramePosition(iframe, x, y, width, height, rotation) { + const frame = iframe; + const stage = { + width: this.runtime.stageWidth, + height: this.runtime.stageHeight + }; + frame.style.position = "absolute"; // position above canvas without pushing it down + frame.style.width = `${(width / stage.width) * 100}%`; // convert pixel size to percentage for full screen + frame.style.height = `${(height / stage.height) * 100}%`; + frame.style.transformOrigin = "center center"; // rotation and translation begins at center + + // epic maths to place x and y at the center + let xpos = ((((stage.width / 2) - (width / 2)) + x) / stage.width) * 100; + let ypos = ((((stage.height / 2) - (height / 2)) - y) / stage.height) * 100; + + frame.style.left = `${xpos}%`; + frame.style.top = `${ypos}%`; + frame.style.transform = `rotate(${rotation - 90}deg)`; + this.iframeSettings = { + ...this.iframeSettings, + x: x, + y: y, + rotation: rotation, + width: width, + height: height + }; + + // when switching between project page & editor, we need to place the iframe again since it gets lost + if (iframe.parentElement !== this.GetCurrentCanvas().parentElement) { + /* todo: create layers so that iframe appears above 3d every time this is done */ + this.GetCurrentCanvas().parentElement.prepend(iframe); + } + } + SetIFrameColors(iframe, color, opacity) { + const frame = iframe; + + const rgb = Cast.toRgbColorObject(color); + const hex = Color.rgbToHex(rgb); + + frame.style.backgroundColor = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity * 100}%)`; + this.iframeSettings = { + ...this.iframeSettings, + color: hex, + opacity: Cast.toNumber(opacity) + }; + + // when switching between project page & editor, we need to place the iframe again since it gets lost + if (iframe.parentElement !== this.GetCurrentCanvas().parentElement) { + /* todo: create layers so that iframe appears above 3d every time this is done */ + this.GetCurrentCanvas().parentElement.prepend(iframe); + } + } + SetIFrameClickable(iframe, clickable) { + const frame = iframe; + + frame.style.pointerEvents = Cast.toBoolean(clickable) ? '' : 'none'; + this.iframeSettings = { + ...this.iframeSettings, + clickable: Cast.toBoolean(clickable) + }; + + // when switching between project page & editor, we need to place the iframe again since it gets lost + if (iframe.parentElement !== this.GetCurrentCanvas().parentElement) { + /* todo: create layers so that iframe appears above 3d every time this is done */ + this.GetCurrentCanvas().parentElement.prepend(iframe); + } + } + GenerateCssFilter(color, grayscale, brightness, contrast, ghost, blur, invert, saturate, sepia) { + return `hue-rotate(${(color / 200) * 360}deg) ` + // scratch color effect goes back to normal color at 200 + `grayscale(${grayscale}%) ` + + `brightness(${brightness + 100}%) ` + // brightness at 0 will be 100 + `contrast(${contrast + 100}%) ` + // same thing here + `opacity(${100 - ghost}%) ` + // opacity at 0 will be 100 but opacity at 100 will be 0 + `blur(${blur}px) ` + + `invert(${invert}%) ` + // invert is actually a percentage lolol! + `saturate(${saturate + 100}%) ` + // saturation at 0 will be 100 + `sepia(${sepia}%)`; + } + ApplyFilterOptions(iframe) { + iframe.style.filter = this.GenerateCssFilter( + this.iframeFilters.color, + this.iframeFilters.grayscale, + this.iframeFilters.brightness, + this.iframeFilters.contrast, + this.iframeFilters.ghost, + this.iframeFilters.blur, + this.iframeFilters.invert, + this.iframeFilters.saturate, + this.iframeFilters.sepia, + ); + } + + createIframeElement() { + this.RemoveIFrame(); + const iframe = this.SetNewIFrame(); + iframe.style.zIndex = 500; + iframe.style.borderWidth = "0px"; + iframe.src = "data:text/html;base64,PERPQ1RZUEUgaHRtbD4KPGh0bWwgbGFuZz0iZW4tVVMiPgo8aGVhZD48L2hlYWQ+Cjxib2R5PjxoMT5IZWxsbyE8L2gxPjxwPllvdSd2ZSBqdXN0IGNyZWF0ZWQgYW4gaWZyYW1lIGVsZW1lbnQuPGJyPlVzZSB0aGlzIHRvIGVtYmVkIHdlYnNpdGVzIHdpdGggdGhlaXIgVVJMcy4gTm90ZSB0aGF0IHNvbWUgd2Vic2l0ZXMgbWlnaHQgbm90IGFsbG93IGlmcmFtZXMgdG8gd29yayBmb3IgdGhlaXIgd2Vic2l0ZS48L3A+PC9ib2R5Pgo8L2h0bWw+"; + this.displayWebsiteUrl = iframe.src; + // positions iframe to fit stage + this.SetIFramePosition(iframe, 0, 0, this.runtime.stageWidth, this.runtime.stageHeight, 90); + // reset color & opacity + this.SetIFrameColors(iframe, '#ffffff', 0); + // reset other stuff + this.SetIFrameClickable(iframe, true); + // reset filters + this.iframeFilters = ArrayToValue(EffectOptions.items.map(item => item.value), 0); // reset all filter stuff + this.GetCurrentCanvas().parentElement.prepend(iframe); // adds the iframe above the canvas + return iframe; + } + deleteIframeElement() { + this.RemoveIFrame(); + } + iframeElementExists() { + return this.GetIFrameState(); + } + setIframeUrl(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + let usingProxy = false; + let checkingUrl = args.URL; + if (Cast.toString(args.URL).startsWith("proxy://")) { + // use the penguin mod proxy but still say we are on proxy:// since its what the user input + // replace proxy:// with https:// though since we are still using the https protocol + usingProxy = true; + checkingUrl = Cast.toString(args.URL).replace("proxy://", "https://"); + } + if (Cast.toString(args.URL) === 'about:blank') { + this.createdIframe.src = "about:blank"; + this.displayWebsiteUrl = "about:blank"; + return; + } + this.IsWebsiteAllowed(checkingUrl).then(safe => { + if (!safe) { // website isnt in the permitted sites list? + this.createdIframe.src = "about:blank"; + this.displayWebsiteUrl = args.URL; + return; + } + this.createdIframe.src = (usingProxy ? `https://detaproxy-1-s1965152.deta.app/?url=${Cast.toString(args.URL).replace("proxy://", "https://")}` : args.URL); + // tell the user we are on proxy:// still since it looks nicer than the disgusting deta url + this.displayWebsiteUrl = (usingProxy ? `${Cast.toString(this.createdIframe.src).replace("https://detaproxy-1-s1965152.deta.app/?url=https://", "proxy://")}` : this.createdIframe.src); + }); + } + setIframePosLeft(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + this.SetIFramePosition(iframe, + Cast.toNumber(args.X), + this.iframeSettings.y, + this.iframeSettings.width, + this.iframeSettings.height, + this.iframeSettings.rotation, + ); + } + setIframePosTop(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + this.SetIFramePosition(iframe, + this.iframeSettings.x, + Cast.toNumber(args.Y), + this.iframeSettings.width, + this.iframeSettings.height, + this.iframeSettings.rotation, + ); + } + setIframeSizeWidth(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + this.SetIFramePosition(iframe, + this.iframeSettings.x, + this.iframeSettings.y, + Cast.toNumber(args.WIDTH), + this.iframeSettings.height, + this.iframeSettings.rotation, + ); + } + setIframeSizeHeight(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + this.SetIFramePosition(iframe, + this.iframeSettings.x, + this.iframeSettings.y, + this.iframeSettings.width, + Cast.toNumber(args.HEIGHT), + this.iframeSettings.rotation, + ); + } + setIframeRotation(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + this.SetIFramePosition(iframe, + this.iframeSettings.x, + this.iframeSettings.y, + this.iframeSettings.width, + this.iframeSettings.height, + Cast.toNumber(args.ROTATE), + ); + } + setIframeBackgroundColor(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + this.SetIFrameColors(iframe, args.COLOR, this.iframeSettings.opacity); + } + setIframeBackgroundOpacity(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + let opacity = Cast.toNumber(args.GHOST); + if (opacity > 100) opacity = 100; + if (opacity < 0) opacity = 0; + opacity /= 100; + opacity = 1 - opacity; + this.SetIFrameColors(iframe, this.iframeSettings.color, opacity); + } + setIframeClickable(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + let clickable = false; + if (Cast.toString(args.USABLE).toLowerCase() === 'interactable') { + clickable = true; + } + if (Cast.toString(args.USABLE).toLowerCase() === 'on') { + clickable = true; + } + if (Cast.toString(args.USABLE).toLowerCase() === 'enabled') { + clickable = true; + } + if (Cast.toString(args.USABLE).toLowerCase() === 'true') { + clickable = true; + } + this.SetIFrameClickable(iframe, clickable); + } + showIframeElement() { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + iframe.style.display = ""; + } + hideIframeElement() { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + const iframe = this.createdIframe; + iframe.style.display = "none"; + } + + getIframeLeft() { + if (!this.GetIFrameState()) return 0; // iframe doesnt exist, stop + return this.iframeSettings.x; + } + getIframeTop() { + if (!this.GetIFrameState()) return 0; // iframe doesnt exist, stop + return this.iframeSettings.y; + } + getIframeWidth() { + if (!this.GetIFrameState()) return 480; // iframe doesnt exist, stop + return this.iframeSettings.width; + } + getIframeHeight() { + if (!this.GetIFrameState()) return 360; // iframe doesnt exist, stop + return this.iframeSettings.height; + } + getIframeRotation() { + if (!this.GetIFrameState()) return 90; // iframe doesnt exist, stop + return this.iframeSettings.rotation; + } + getIframeTargetUrl() { + if (!this.GetIFrameState()) return ''; // iframe doesnt exist, stop + return this.displayWebsiteUrl; + } + getIframeBackgroundColor() { + if (!this.GetIFrameState()) return '#ffffff'; // iframe doesnt exist, stop + const rawColor = this.iframeSettings.color; + const rgb = Cast.toRgbColorObject(rawColor); + const hex = Color.rgbToHex(rgb); + return hex; + } + getIframeBackgroundOpacity() { + if (!this.GetIFrameState()) return 100; // iframe doesnt exist, stop + const rawOpacity = this.iframeSettings.opacity; + return (1 - rawOpacity) * 100; + } + getIframeClickable() { + if (!this.GetIFrameState()) return true; // iframe doesnt exist, stop + return this.iframeSettings.clickable; + } + iframeElementIsHidden() { + if (!this.GetIFrameState()) return false; // iframe doesnt exist, stop + return this.createdIframe.style.display === "none"; + } + + whenIframeIsLoaded() { + const value = this.iframeLoadedValue; + this.iframeLoadedValue = false; + return value; + } + + // effect functions lolol + iframeElementSetEffect(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + this.iframeFilters[args.EFFECT] = Cast.toNumber(args.AMOUNT); + this.ApplyFilterOptions(this.createdIframe); + } + iframeElementChangeEffect(args) { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + this.iframeFilters[args.EFFECT] += Cast.toNumber(args.AMOUNT); + this.ApplyFilterOptions(this.createdIframe); + } + iframeElementClearEffects() { + if (!this.GetIFrameState()) return; // iframe doesnt exist, stop + this.iframeFilters = ArrayToValue(EffectOptions.items.map(item => item.value), 0); // reset all values to 0 + this.ApplyFilterOptions(this.createdIframe); + } + getIframeEffectAmount(args) { + if (!this.GetIFrameState()) return 0; // iframe doesnt exist, stop + return this.iframeFilters[args.EFFECT]; + } +} + +module.exports = JgIframeBlocks; diff --git a/local-scratch-vm/src/extensions/jg_interfaces/box.png b/local-scratch-vm/src/extensions/jg_interfaces/box.png new file mode 100644 index 0000000000000000000000000000000000000000..251eb3a05c15490d96e484aae010fdfaac429963 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/box.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/button.png b/local-scratch-vm/src/extensions/jg_interfaces/button.png new file mode 100644 index 0000000000000000000000000000000000000000..ae6f63f10ed4ee89d9c8f8f5640e24f97c3921b6 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/button.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/checkbox.png b/local-scratch-vm/src/extensions/jg_interfaces/checkbox.png new file mode 100644 index 0000000000000000000000000000000000000000..2c3b1a12bffe091171ebdf3053f1bb1e148e11ab Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/checkbox.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/dropdown.png b/local-scratch-vm/src/extensions/jg_interfaces/dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..4efed32f76855b89616f17318176880a146fe504 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/dropdown.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/helper.js b/local-scratch-vm/src/extensions/jg_interfaces/helper.js new file mode 100644 index 0000000000000000000000000000000000000000..092872e907de88da4ad7868652efffe15f1cb56c --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_interfaces/helper.js @@ -0,0 +1,79 @@ +class Button { + constructor(addToClient, { id, label, shown }) { + this.id = id; + this.label = label; + this._element = document.createElement("button"); + this._element.style = `position:absolute;left:0%;top:0%` + if (shown === false) { + this._element.style.display = "none"; + } + if (label) { + this._element.innerText = label; + } else { + this._element.innerText = "Button"; + } + + addToClient.AddToCanvas(this._element); + if (addToClient.buttons[id]) { + addToClient.buttons[id].dispose(); + delete addToClient.buttons[id]; + } + addToClient.buttons[id] = this; + } + show() { + this._element.style.display = ""; + } + hide() { + this._element.style.display = "none"; + } + + dispose() { + this._element.remove(); + } +} + +class UI { + constructor(runtime) { + this.runtime = runtime; + this._div = document.createElement("div"); + this.Realign(); + + this.buttons = {}; + } + + static Button = Button; + + /** + * If we switch from editor to project page, our element div gets hidden away somewhere. + * This function puts it back in place. + */ + Realign() { + this._div.style = `position: absolute;left: 0px;width: 100%;height: 100%;top: 0px;z-index: 1000;` + if (!this.runtime.renderer) return; + this.runtime.renderer.canvas.parentElement.prepend(this._div); + } + /** + * Append an element to the div containing all UI elements. + * @param {Element} element The element to add to the div. + */ + AddToCanvas(element) { + this._div.append(element); + this.Realign(); + } + + /** + * Dispose of all UI elements. + */ + DisposeAll() { + const buttons = Object.values(this.buttons); + const elements = [].concat(buttons); + + elements.forEach(element => { + element.dispose(); + }) + + this.Realign(); + } +} + +module.exports = UI; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_interfaces/index.js b/local-scratch-vm/src/extensions/jg_interfaces/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dcc512f0c0847abec5f3b53faa90d0afe2ad078a --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_interfaces/index.js @@ -0,0 +1,229 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const UI = require('./helper.js'); +const Cast = require('../../util/cast'); +const Icons = { + Mouse: require('./mouse.png'), + Button: require('./button.png'), + Text: require('./text.png'), + Textarea: require('./textarea.png'), + Box: require('./box.png'), + ScrollingBox: require('./scrollingbox.png'), + Checkbox: require('./checkbox.png'), + Dropdown: require('./dropdown.png'), + Multiselect: require('./multiselect.png'), + Slider: require('./slider.png'), +} + +/** + * Class + * @constructor + */ +class jgAdvancedText { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + this.UIClient = new UI(runtime); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgInterfaces', + name: 'Interfaces', + color1: '#ac96b5', + color2: '#8e7a96', + blocks: [ + { + opcode: 'createButton', + text: 'create button named: [NAME] with text: [TEXT]', + blockIconURI: Icons.Button, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Button' + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: 'Click me' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'createTextInput', + text: 'create text input named: [NAME] with placeholder: [PLACEHOLDER] and default: [DEFAULT]', + blockIconURI: Icons.Text, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'TextInput' + }, + PLACEHOLDER: { + type: ArgumentType.STRING, + defaultValue: 'Type here...' + }, + DEFAULT: { + type: ArgumentType.STRING, + defaultValue: ' ' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'createTextBox', + text: 'create textbox named: [NAME] with placeholder: [PLACEHOLDER] and default: [DEFAULT] being resizable? [RESIZE]', + blockIconURI: Icons.Textarea, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Textbox' + }, + PLACEHOLDER: { + type: ArgumentType.STRING, + defaultValue: 'Type here...' + }, + DEFAULT: { + type: ArgumentType.STRING, + defaultValue: ' ' + }, + RESIZE: { + type: ArgumentType.BOOLEAN + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'createDropdown', + text: 'create dropdown menu named: [NAME] with label: [LABEL]', + blockIconURI: Icons.Dropdown, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Dropdown' + }, + LABEL: { + type: ArgumentType.STRING, + defaultValue: 'Click me' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'createCheckbox', + text: 'create checkbox named: [NAME] with label: [LABEL]', + blockIconURI: Icons.Checkbox, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Checkbox' + }, + LABEL: { + type: ArgumentType.STRING, + defaultValue: ' ' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'createSlider', + text: 'create slider named: [NAME] minimum number: [MIN] maximum number: [MAX]', + blockIconURI: Icons.Slider, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Slider' + }, + MIN: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + MAX: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'createScrollingArea', + text: 'create scrolling box named: [NAME]', + blockIconURI: Icons.ScrollingBox, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Scroll area' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'createMultiselect', + text: 'create multiselect box named: [NAME]', + blockIconURI: Icons.Multiselect, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Multi-select' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'createGroup', + text: 'create group box named: [NAME]', + blockIconURI: Icons.Box, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Box' + } + }, + blockType: BlockType.COMMAND + }, + ] + }; + } + + // util + createElement(type, properties) { + const element = new UI.Button(this.UIClient, properties); + } + + // blocks + createButton(args) { + this.createElement('Button', { + id: Cast.toString(args.NAME), + label: Cast.toString(args.TEXT), + shown: true + }) + } + createTextInput(args) { + + } + createTextBox(args) { + + } + createDropdown(args) { + + } + createCheckbox(args) { + + } + createScrollingArea(args) { + + } + createMultiselect(args) { + + } + createGroup(args) { + + } +} + +module.exports = jgAdvancedText; diff --git a/local-scratch-vm/src/extensions/jg_interfaces/mouse.pdn b/local-scratch-vm/src/extensions/jg_interfaces/mouse.pdn new file mode 100644 index 0000000000000000000000000000000000000000..a8c50a1f8f409938eb5200646351827916b78cab Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/mouse.pdn differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/mouse.png b/local-scratch-vm/src/extensions/jg_interfaces/mouse.png new file mode 100644 index 0000000000000000000000000000000000000000..67cfafa92af67853a0c087cb1201f2b0d3617b43 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/mouse.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/multiselect.png b/local-scratch-vm/src/extensions/jg_interfaces/multiselect.png new file mode 100644 index 0000000000000000000000000000000000000000..23c5ff620c6441b0c4ce7b93b6c400b25dd6dd6b Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/multiselect.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/scrollingbox.png b/local-scratch-vm/src/extensions/jg_interfaces/scrollingbox.png new file mode 100644 index 0000000000000000000000000000000000000000..26e4f7e071b0b4b8a35da3043f449bfebd7d40ed Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/scrollingbox.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/slider.png b/local-scratch-vm/src/extensions/jg_interfaces/slider.png new file mode 100644 index 0000000000000000000000000000000000000000..3cd4804d941ab9ea743eb24c3cbb9e653104de9a Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/slider.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/text.png b/local-scratch-vm/src/extensions/jg_interfaces/text.png new file mode 100644 index 0000000000000000000000000000000000000000..9fa950316d54063b5082f0ffc491efd4cdd80328 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/text.png differ diff --git a/local-scratch-vm/src/extensions/jg_interfaces/textarea.png b/local-scratch-vm/src/extensions/jg_interfaces/textarea.png new file mode 100644 index 0000000000000000000000000000000000000000..46a8804af22584bd2642fb1b46f3681cbc7bfadc Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_interfaces/textarea.png differ diff --git a/local-scratch-vm/src/extensions/jg_javascript/index.js b/local-scratch-vm/src/extensions/jg_javascript/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b618e416e8e984f78e72b3242c7d6f5c3fccfd5f --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_javascript/index.js @@ -0,0 +1,186 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const SandboxRunner = require('../../util/sandboxed-javascript-runner'); +const Cast = require('../../util/cast'); + +/** + * Class + * oh yea you cant access util in the runner anymore + * im not adding it because im done with implementing eval in PM since it was done like 3 times + * @constructor + */ +class jgJavascript { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + this.runningEditorUnsandboxed = false; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgJavascript', + name: 'JavaScript', + isDynamic: true, + // color1: '#EFC900', look like doo doo + blocks: [ + { + opcode: 'unsandbox', + text: 'Run Unsandboxed', + blockType: BlockType.BUTTON, + hideFromPalette: this.runningEditorUnsandboxed + }, + { + opcode: 'sandbox', + text: 'Run Sandboxed', + blockType: BlockType.BUTTON, + hideFromPalette: !this.runningEditorUnsandboxed + }, + { + opcode: 'javascriptHat', + text: 'when javascript [CODE] == true', + blockType: BlockType.HAT, + hideFromPalette: !this.runningEditorUnsandboxed, // this block seems to cause strange behavior because of how sandboxed eval is done + arguments: { + CODE: { + type: ArgumentType.STRING, + defaultValue: "Math.round(Math.random()) === 1" + } + } + }, + { + opcode: 'javascriptStack', + text: 'javascript [CODE]', + blockType: BlockType.COMMAND, + arguments: { + CODE: { + type: ArgumentType.STRING, + defaultValue: "alert('Hello!')" + } + } + }, + { + opcode: 'javascriptString', + text: 'javascript [CODE]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + CODE: { + type: ArgumentType.STRING, + defaultValue: "Math.random()" + } + } + }, + { + opcode: 'javascriptBool', + text: 'javascript [CODE]', + blockType: BlockType.BOOLEAN, + disableMonitor: true, + arguments: { + CODE: { + type: ArgumentType.STRING, + defaultValue: "Math.round(Math.random()) === 1" + } + } + }, + { + blockType: BlockType.LABEL, + text: 'You can run unsandboxed', + hideFromPalette: !this.runningEditorUnsandboxed + }, + { + blockType: BlockType.LABEL, + text: 'when packaging the project.', + hideFromPalette: !this.runningEditorUnsandboxed + }, + { + blockType: BlockType.LABEL, + text: '⠀', + hideFromPalette: !this.runningEditorUnsandboxed + }, + { + blockType: BlockType.LABEL, + text: 'Player Options >', + hideFromPalette: !this.runningEditorUnsandboxed + }, + { + blockType: BlockType.LABEL, + text: 'Remove sandbox on the JavaScript Ext.', + hideFromPalette: !this.runningEditorUnsandboxed + }, + ] + }; + } + + async unsandbox() { + const unsandbox = await this.runtime.vm.securityManager.canUnsandbox('JavaScript'); + if (!unsandbox) return; + this.runningEditorUnsandboxed = true; + this.runtime.vm.emitWorkspaceUpdate(); + } + sandbox() { + this.runningEditorUnsandboxed = false; + this.runtime.vm.emitWorkspaceUpdate(); + } + + // util + evaluateCode(code, args, util, realBlockInfo) { + // used for packager + if (this.runtime.extensionRuntimeOptions.javascriptUnsandboxed === true || this.runningEditorUnsandboxed) { + let result; + try { + // eslint-disable-next-line no-eval + result = eval(code); + } catch (err) { + result = err; + } + return result; + } + // we are not packaged + return new Promise((resolve) => { + SandboxRunner.execute(code).then(result => { + // result is { value: any, success: boolean } + // in PM, we always ignore errors + return resolve(result.value); + }) + }) + } + + // blocks + javascriptStack(args, util, realBlockInfo) { + const code = Cast.toString(args.CODE); + return this.evaluateCode(code, args, util, realBlockInfo); + } + javascriptString(args, util, realBlockInfo) { + const code = Cast.toString(args.CODE); + return this.evaluateCode(code, args, util, realBlockInfo); + } + javascriptBool(args, util, realBlockInfo) { + const code = Cast.toString(args.CODE); + const possiblePromise = this.evaluateCode(code, args, util, realBlockInfo); + if (possiblePromise && typeof possiblePromise.then === 'function') { + return (async () => { + const value = await possiblePromise; + return Boolean(value); // this is a JavaScript extension, we should use the JavaScript way of determining booleans + })(); + } + return Boolean(possiblePromise); + } + javascriptHat(...args) { + if (!this.runtime.extensionRuntimeOptions.javascriptUnsandboxed && !this.runningEditorUnsandboxed) { + return false; // we will cause issues otherwise, edging hats cause weird issues when waiting for promises each frame + } + const possiblePromise = this.javascriptBool(...args); + if (possiblePromise && typeof possiblePromise.then === 'function') { + return false; // we will cause issues otherwise, edging hats cause weird issues when waiting for promises each frame + } + return possiblePromise; + } +} + +module.exports = jgJavascript; diff --git a/local-scratch-vm/src/extensions/jg_json/index.js b/local-scratch-vm/src/extensions/jg_json/index.js new file mode 100644 index 0000000000000000000000000000000000000000..202bb2cc65edd226050206a7326ffa947c1c7b33 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_json/index.js @@ -0,0 +1,824 @@ +/* eslint-disable no-undef */ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const { + validateJSON, + validateArray, + stringToEqivalint, + valueToString, + validateRegex +} = require('../../util/json-block-utilities'); + +// const Cast = require('../../util/cast'); + +/** + * Class for JSON blocks + * @constructor + */ +class JgJSONBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'jgJSON', + name: 'JSON', + color1: '#0FBD8C', + color2: '#0EAF82', + blocks: [ + { + opcode: 'json_validate', + blockType: BlockType.BOOLEAN, + arguments: { + json: { + type: ArgumentType.STRING, + defaultValue: "{}" + } + }, + text: 'is json [json] valid?' + }, + "---", + { + opcode: 'getValueFromJSON', + text: formatMessage({ + id: 'jgJSON.blocks.getValueFromJSON', + default: 'get [VALUE] from [JSON]', + description: 'Gets a value from a JSON object.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + VALUE: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.getValueFromJSON_value', + default: 'key', + description: 'The name of the item you want to get from the JSON.' + }) + }, + JSON: { + type: ArgumentType.STRING, + defaultValue: '{"key": "value"}' + } + } + }, + { + opcode: 'getTreeValueFromJSON', + text: 'get path [VALUE] from [JSON]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + VALUE: { + type: ArgumentType.STRING, + defaultValue: 'first/second' + }, + JSON: { + type: ArgumentType.STRING, + defaultValue: '{"first": {"second": 2, "third": 3}}' + } + } + }, + { + opcode: 'setValueToKeyInJSON', + text: formatMessage({ + id: 'jgJSON.blocks.setValueToKeyInJSON', + default: 'set [KEY] to [VALUE] in [JSON]', + description: 'Returns the JSON with the key set to the value.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + VALUE: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_value', + default: 'value', + description: 'The value of the key you are setting.' + }) + }, + KEY: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_key', + default: 'key', + description: 'The key you are setting in the JSON.' + }) + }, + JSON: { + type: ArgumentType.STRING, + defaultValue: "{}" + } + } + }, + { + opcode: 'json_delete', + blockType: BlockType.REPORTER, + arguments: { + json: { + type: ArgumentType.STRING, + defaultValue: "{}" + }, + key: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_key', + default: 'key', + description: 'The key you are setting in the JSON.' + }) + } + }, + text: 'in json [json] delete key [key]' + }, + { + opcode: 'json_values', + blockType: BlockType.REPORTER, + arguments: { + json: { + type: ArgumentType.STRING, + defaultValue: "{}" + } + }, + text: 'get all values from json [json]' + }, + { + opcode: 'json_keys', + blockType: BlockType.REPORTER, + arguments: { + json: { + type: ArgumentType.STRING, + defaultValue: "{}" + } + }, + text: 'get all keys from json [json]' + }, + { + opcode: 'json_has', + blockType: BlockType.BOOLEAN, + arguments: { + json: { + type: ArgumentType.STRING, + defaultValue: "{}" + }, + key: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_key', + default: 'key', + description: 'The key you are setting in the JSON.' + }) + } + }, + text: 'json [json] has key [key] ?' + }, + { + opcode: 'json_combine', + blockType: BlockType.REPORTER, + arguments: { + one: { + type: ArgumentType.STRING, + defaultValue: "{}" + }, + two: { + type: ArgumentType.STRING, + defaultValue: "{}" + } + }, + text: 'combine json [one] and json [two]' + }, + { + blockType: BlockType.LABEL, + text: "Arrays" + }, + { + opcode: 'json_array_validate', + blockType: BlockType.BOOLEAN, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[]" + } + }, + text: 'is array [array] valid?' + }, + { + opcode: 'json_array_split', + blockType: BlockType.REPORTER, + arguments: { + text: { + type: ArgumentType.STRING, + defaultValue: "A, B, C" + }, + delimeter: { + type: ArgumentType.STRING, + defaultValue: ', ' + } + }, + text: 'create an array from text [text] with delimeter [delimeter]' + }, + { + opcode: 'json_array_join', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + delimeter: { + type: ArgumentType.STRING, + defaultValue: ', ' + } + }, + text: 'create text from array [array] with delimeter [delimeter]' + }, + "---", + { + opcode: 'json_array_push', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + item: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_value', + default: 'value', + description: 'The value of the key you are setting.' + }) + } + }, + text: 'in array [array] add [item]' + }, + "---", + { + opcode: 'json_array_concatLayer1', + blockType: BlockType.REPORTER, + arguments: { + array1: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + array2: { + type: ArgumentType.STRING, + defaultValue: "[\"D\", \"E\", \"F\"]" + } + }, + text: 'add items from array [array2] to array [array1]' + }, + { + opcode: 'json_array_concatLayer2', + blockType: BlockType.REPORTER, + arguments: { + array1: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + array2: { + type: ArgumentType.STRING, + defaultValue: "[\"D\", \"E\", \"F\"]" + }, + array3: { + type: ArgumentType.STRING, + defaultValue: "[\"G\", \"H\", \"I\"]" + } + }, + text: 'add items from array [array2] and array [array3] to array [array1]' + }, + "---", + { + opcode: 'json_array_delete', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + index: { + type: ArgumentType.NUMBER, + defaultValue: 2 + } + }, + text: 'in array [array] delete [index]' + }, + { + opcode: 'json_array_reverse', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + } + }, + text: 'reverse array [array]' + }, + { + opcode: 'json_array_insert', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + index: { + type: ArgumentType.NUMBER, + defaultValue: 2 + }, + value: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_value', + default: 'value', + description: 'The value of the key you are setting.' + }) + } + }, + text: 'in array [array] insert [value] at [index]' + }, + { + opcode: 'json_array_set', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + index: { + type: ArgumentType.NUMBER, + defaultValue: 2 + }, + value: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_value', + default: 'value', + description: 'The value of the key you are setting.' + }) + } + }, + text: 'in array [array] set [index] to [value]' + }, + "---", + { + opcode: 'json_array_get', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + index: { + type: ArgumentType.NUMBER, + defaultValue: 2 + } + }, + text: 'in array [array] get [index]' + }, + { + opcode: 'json_array_indexofNostart', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + value: { + type: ArgumentType.STRING, + defaultValue: "value" + } + }, + text: 'in array [array] get index of [value]' + }, + { + opcode: 'json_array_indexof', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + number: { + type: ArgumentType.NUMBER, + defaultValue: 2 + }, + value: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_value', + default: 'value', + description: 'The value of the key you are setting.' + }) + } + }, + text: 'in array [array] from [number] get index of [value]' + }, + { + opcode: 'json_array_length', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + } + }, + text: 'length of array [array]' + }, + { + opcode: 'json_array_contains', + blockType: BlockType.BOOLEAN, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + value: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgJSON.setValueToKeyInJSON_value', + default: 'value', + description: 'The value of the key you are setting.' + }) + } + }, + text: 'array [array] contains [value] ?' + }, + "---", + { + opcode: 'json_array_flat', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[[\"A\", \"B\"], [\"C\", \"D\"]]" + }, + layer: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + text: 'flatten nested array [array] by [layer] layers' + }, + "---", + { + opcode: 'json_array_getrange', + blockType: BlockType.REPORTER, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + }, + index1: { + type: ArgumentType.NUMBER, + defaultValue: 2 + }, + index2: { + type: ArgumentType.NUMBER, + defaultValue: 2 + } + }, + text: 'in array [array] get all items from [index1] to [index2]' + }, + "---", + { + opcode: 'json_array_isempty', + blockType: BlockType.BOOLEAN, + arguments: { + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + } + }, + text: 'is array [array] empty?' + }, + "---", + { + opcode: 'json_array_listtoarray', + blockType: BlockType.REPORTER, + arguments: { + list: { + type: ArgumentType.STRING, + defaultValue: 'select a list', + menu: 'lists' + } + }, + hideFromPalette: true, + text: 'get contents of list [list] as array' + }, + { + opcode: 'json_array_tolist', + blockType: BlockType.COMMAND, + arguments: { + list: { + type: ArgumentType.STRING, + defaultValue: 'select a list', + menu: 'lists' + }, + array: { + type: ArgumentType.STRING, + defaultValue: "[\"A\", \"B\", \"C\"]" + } + }, + hideFromPalette: true, + text: 'set contents of list [list] to contents of array [array]' + } + ], + menus: { + lists: 'getAllLists' + } + }; + } + + getAllLists () { + const variables = [].concat( + Object.values(vm.runtime.getTargetForStage().variables), + Object.values(vm.editingTarget.variables) + ); + const lists = variables.filter(i => i.type === 'list'); + if (lists.length === 0) { + return [ + { + text: 'select a list', + value: 'select a list' + } + ]; + } + return lists.map(i => ({ + text: i.name, + value: JSON.stringify({ + id: i.id, + name: i.name + }) + })); + } + + getValueFromJSON (args) { + const key = args.VALUE; + const json = validateJSON(args.JSON).object; + + return valueToString(json[key]); + } + getTreeValueFromJSON (args) { + const tree = Cast.toString(args.VALUE); + let _json; + if (Cast.toString(args.JSON).startsWith('[')) { + _json = validateArray(args.JSON).array; + } else { + _json = validateJSON(args.JSON).object; + } + const json = _json; + + if (!tree.includes('/')) { + // if we dont have a slash, treat it like + // the get value block + if (Array.isArray(json)) { + return this.json_array_get({ + array: Cast.toString(args.JSON), + index: Cast.toNumber(args.VALUE) + }); + } + return this.getValueFromJSON(args); + } + + let value = ''; + let currentObject = json; + const treeSplit = tree.split('/'); + for (const key of treeSplit) { + value = ''; + // check for array so we can do "object/array/3/value" + if (Array.isArray(currentObject)) { + currentObject = currentObject[Cast.toNumber(key)]; + value = currentObject; + continue; + } + + if (typeof currentObject === 'object') { + currentObject = currentObject[key]; + value = currentObject; + } else { + value = currentObject; + } + } + + if (typeof value === "undefined") return ''; + + return valueToString(value); + } + setValueToKeyInJSON (args) { + const json = validateJSON(args.JSON).object; + const key = args.KEY; + const value = args.VALUE; + + json[key] = stringToEqivalint(value); + + return JSON.stringify(json); + } + + json_has (args) { + const json = validateJSON(args.json).object; + const key = args.key; + + return json.hasOwnProperty(key); + } + + json_delete (args) { + const json = validateJSON(args.json).object; + const key = args.key; + + if (!json.hasOwnProperty(key)) return JSON.stringify(json); + + delete json[key]; + + return JSON.stringify(json); + } + + json_values (args) { + const json = validateJSON(args.json).object; + + return JSON.stringify(Object.values(json)); + } + + json_keys (args) { + const json = validateJSON(args.json).object; + + return JSON.stringify(Object.keys(json)); + } + + json_array_length (args) { + const array = validateArray(args.array).array; + + return array.length; + } + + json_array_isempty (args) { + const array = validateArray(args.array).array; + + return !array.length; + } + + json_array_contains (args) { + const array = validateArray(args.array).array; + const value = args.value; + + return array.includes(stringToEqivalint(value)); + } + + json_array_reverse (args) { + const array = validateArray(args.array).array; + + return JSON.stringify(array.reverse()); + } + + json_array_indexof (args) { + const array = validateArray(args.array).array; + const number = args.number; + const value = args.value; + + return array.indexOf(stringToEqivalint(value), number); + } + + json_array_indexofNostart (args) { + const array = validateArray(args.array).array; + const value = args.value; + + return array.indexOf(stringToEqivalint(value)); + } + + json_array_set (args) { + const array = validateArray(args.array).array; + const index = args.index; + const value = args.value; + + array[index] = stringToEqivalint(value); + + return JSON.stringify(array); + } + + json_array_insert (args) { + const array = validateArray(args.array).array; + const index = args.index; + const value = args.value; + + array.splice(index, 0, stringToEqivalint(value)); + + return JSON.stringify(array); + } + + json_array_get (args) { + const array = validateArray(args.array).array; + const index = args.index; + + return valueToString(array[index]); + } + + json_array_getrange (args) { + const array = validateArray(args.array).array; + const index1 = args.index1; + const index2 = args.index2; + + return JSON.stringify(array.slice(index1, index2)); + } + + json_array_push (args) { + const array = validateArray(args.array).array; + const value = args.item; + + array.push(stringToEqivalint(value)); + + return JSON.stringify(array); + } + + json_array_concatLayer1 (args) { + const array1 = validateArray(args.array1).array; + const array2 = validateArray(args.array2).array; + + const array = array1.concat(array2); + + return JSON.stringify(array); + } + + json_array_concatLayer2 (args) { + const array1 = validateArray(args.array1).array; + const array2 = validateArray(args.array2).array; + const array3 = validateArray(args.array3).array; + + const array = array1.concat(array2, array3); + + return JSON.stringify(array); + } + + json_array_flat (args) { + const array = validateArray(args.array).array; + const depth = Cast.toNumber(args.layer); + + const flattened = array.flat(depth); + + return JSON.stringify(flattened); + } + + json_array_tolist (args, util) { + let list; + try { + list = JSON.parse(args.list); + } catch { + return; + } + const array = validateArray(args.array).array; + const content = util.target.lookupOrCreateList(list.id, list.name); + + content.value = array.map(x => valueToString(x)); + } + + json_array_listtoarray (args, util) { + let list; + try { + list = JSON.parse(args.list); + } catch { + return; + } + const content = util.target.lookupOrCreateList(list.id, list.name).value; + + return JSON.stringify(content.map(x => stringToEqivalint(x))); + } + + json_array_delete (args) { + const array = validateArray(args.array).array; + const index = args.index; + + array.splice(index, 1); + + return JSON.stringify(array); + } + + json_array_split (args) { + return JSON.stringify(args.text.split(args.delimeter)); + } + json_array_join (args) { + return validateArray(args.array).array.join(args.delimeter); + } + + json_validate (args) { + return validateJSON(args.json).isValid; + } + json_array_validate (args) { + return validateArray(args.array).isValid; + } + + json_combine (args) { + const one = validateJSON(args.one).object; + const two = validateJSON(args.two).object; + + return JSON.stringify(Object.assign(one, two)); + } +} + +module.exports = JgJSONBlocks; diff --git a/local-scratch-vm/src/extensions/jg_nineslices/index.js b/local-scratch-vm/src/extensions/jg_nineslices/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/local-scratch-vm/src/extensions/jg_packagerApplications/icon.svg b/local-scratch-vm/src/extensions/jg_packagerApplications/icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..67e298b1e104e7389e690cd23038e51709861786 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_packagerApplications/icon.svg differ diff --git a/local-scratch-vm/src/extensions/jg_packagerApplications/index.js b/local-scratch-vm/src/extensions/jg_packagerApplications/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a9c8ee2aaad8fafc54c0e15e665800ee36f98894 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_packagerApplications/index.js @@ -0,0 +1,261 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const Icon = require('./icon.svg'); + +class JgPackagerApplicationsBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + */ + this.runtime = runtime; + } + + /** + * metadata for this extension and its blocks. + * @returns {object} + */ + getInfo() { + return { + id: "jgPackagerApplications", + name: "Packager Applications", + color1: "#66b8ff", + color2: "#5092cc", + blockIconURI: Icon, + blocks: [ + { + opcode: "isPackaged", + blockType: BlockType.BOOLEAN, + text: "is packaged?" + }, + { + opcode: "moveWindow", + blockType: BlockType.COMMAND, + text: "move window to x: [X] y: [Y]", + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: "setX", + blockType: BlockType.COMMAND, + text: "set window x to [X]", + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: "changeX", + blockType: BlockType.COMMAND, + text: "change window x by [X]", + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: "setY", + blockType: BlockType.COMMAND, + text: "set window y to [Y]", + arguments: { + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: "changeY", + blockType: BlockType.COMMAND, + text: "change window y by [Y]", + arguments: { + Y: { + type: ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: "windowX", + blockType: BlockType.REPORTER, + text: "window x" + }, + { + opcode: "windowY", + blockType: BlockType.REPORTER, + text: "window y" + }, + "---", + { + opcode: "resizeWindow", + blockType: BlockType.COMMAND, + text: "set window size to width: [WIDTH] height: [HEIGHT]", + arguments: { + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 640 + }, + HEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 360 + } + } + }, + { + opcode: "windowWidth", + blockType: BlockType.REPORTER, + text: "window width" + }, + { + opcode: "windowHeight", + blockType: BlockType.REPORTER, + text: "window height" + }, + "---", + { + opcode: "enableFullscreen", + blockType: BlockType.COMMAND, + text: "enable fullscreen" + }, + { + opcode: "exitFullscreen", + blockType: BlockType.COMMAND, + text: "exit fullscreen" + }, + { + opcode: "isFullscreen", + blockType: BlockType.BOOLEAN, + text: "in fullscreen?" + }, + { + opcode: "screenWidth", + blockType: BlockType.REPORTER, + text: "screen width" + }, + { + opcode: "screenHeight", + blockType: BlockType.REPORTER, + text: "screen height" + }, + "---", + { + opcode: "setWindowName", + blockType: BlockType.COMMAND, + text: "set window name to [NAME]", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "My Cool Game" + } + } + }, + { + opcode: "getWindowName", + blockType: BlockType.REPORTER, + text: "window name" + }, + { + opcode: "isFocused", + blockType: BlockType.BOOLEAN, + text: "is user using this window?" + }, + { + opcode: "closeWindow", + blockType: BlockType.COMMAND, + isTerminal: true, + text: "close window" + }, + ] + }; + } + + // blocks + isPackaged() { + return this.runtime.isPackaged; + } + moveWindow(args) { + const x = Cast.toNumber(args.X); + const y = Cast.toNumber(args.Y); + window.moveTo(x, y); + } + setX(args) { + const x = Cast.toNumber(args.X); + const y = window.screenY; + window.moveTo(x, y); + } + changeX(args) { + const x = Cast.toNumber(args.X); + window.moveBy(x, 0); + } + setY(args) { + const x = window.screenX; + const y = Cast.toNumber(args.Y); + window.moveTo(x, y); + } + changeY(args) { + const y = Cast.toNumber(args.Y); + window.moveBy(0, y); + } + windowX() { + return window.screenLeft; + } + windowY() { + return window.screenTop; + } + resizeWindow(args) { + const width = Cast.toNumber(args.WIDTH); + const height = Cast.toNumber(args.HEIGHT); + window.resizeTo(width, height); + } + windowWidth() { + return window.outerWidth; + } + windowHeight() { + return window.outerHeight; + } + screenWidth() { + return screen.width; + } + screenHeight() { + return screen.height; + } + enableFullscreen() { + document.documentElement.requestFullscreen(); + } + exitFullscreen() { + document.exitFullscreen(); + } + isFullscreen() { + if (document.fullscreenElement) { + return true; + } + return false; + } + setWindowName(args) { + const name = Cast.toString(args.NAME); + document.title = name; + } + getWindowName() { + return document.title; + } + isFocused() { + return document.hasFocus(); + } + closeWindow() { + window.close(); + } +} + +module.exports = JgPackagerApplicationsBlocks; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_packagerApplications/test.html b/local-scratch-vm/src/extensions/jg_packagerApplications/test.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/local-scratch-vm/src/extensions/jg_pathfinding/index.js b/local-scratch-vm/src/extensions/jg_pathfinding/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a593371722c9a10e6713bead8f82bab627fd6688 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_pathfinding/index.js @@ -0,0 +1,307 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const formatMessage = require('format-message'); +const Cast = require('../../util/cast'); +const Clone = require('../../util/clone'); +const Pathfinding = require('pathfinding'); +const Nodes = require('./nodes'); +const Seperator = require('./seperator'); +const Map = require('./map'); + +/* + develope test + do the Seperator functions work? + +const _testGrid = [ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0], +]; +console.log(_testGrid); +const _marginGrid = Seperator.marginGrid(_testGrid, 4); +console.log(_marginGrid); +const _padGrid = Seperator.padGrid(_marginGrid, 3, 3); +console.log(_padGrid); + + develope test RESULTS + no not really for some reason + one of the functions adds like 60 items to a row + but only the rows that were created inside of that function + + ok now they work lol +*/ + +/* + develope test + do the Map functions work? + +const _testMap = new Map(); +console.log(Clone.simple(_testMap)); +_testMap.add(0, 0, 50, 50); +_testMap.add(-50, 0, 0, -50); +console.log(Clone.simple(_testMap)); +console.log(_testMap.toGrid()); + + develope test RESULTS + i put the offset x and y in the wrong order lol + somehow Math.abs stopped working + nvm i just messed somethin up with the offsets and X was negative + ok somehow its sitll not working even though grid is the right size + seems like after creating blank tiles, the walls create too many tiles + oh i was literally just using 2 variables i shouldnt have been using + and now grid is blank brh + + nevermind i was reading the wrong grid + Map class should fully work +*/ + +/** + * Class for Pathfinding blocks + * @constructor + */ +class JgPathfindingBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + /** + * The current map and it's boxes. + */ + this.map = new Map(); + /** + * The current settings for the pathfinding character. + */ + this.pather = { + x: 0, + y: 0, + width: 1, + height: 1 + } + /** + * The current result of the pathfinding operation. + */ + this.pathNodes = new Nodes(); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgPathfinding', + name: 'Pathfinding', + color1: '#5386E2', + color2: '#4169B1', + blocks: [ + { + opcode: 'createBlockadeAt', + text: formatMessage({ + id: 'jgPathfinding.blocks.createBlockadeAt', + default: 'create blockade at x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]', + description: "Block that creates a blockade in the pathfinding area." + }), + arguments: { + X1: { type: ArgumentType.NUMBER, defaultValue: -70 }, + Y1: { type: ArgumentType.NUMBER, defaultValue: 20 }, + X2: { type: ArgumentType.NUMBER, defaultValue: 70 }, + Y2: { type: ArgumentType.NUMBER, defaultValue: -20 }, + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'clearBlockades', + text: formatMessage({ + id: 'jgPathfinding.blocks.clearBlockades', + default: 'clear blockades', + description: "Block that removes all blockades in the pathfinding area." + }), + blockType: BlockType.COMMAND + }, + { + opcode: 'setPatherXY', + text: formatMessage({ + id: 'jgPathfinding.blocks.setPatherXY', + default: 'set pather starting x: [X] y: [Y]', + description: "Block that sets the starting position for the pather." + }), + arguments: { + X: { type: ArgumentType.NUMBER, defaultValue: 0 }, + Y: { type: ArgumentType.NUMBER, defaultValue: 120 }, + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'setWidthHeight', + text: formatMessage({ + id: 'jgPathfinding.blocks.setWidthHeight', + default: 'set pather width: [WIDTH] height: [HEIGHT]', + description: "Block that sets the width and height of the path follower. This allows sprites to avoid clipping inside walls on the way to the destination." + }), + arguments: { + WIDTH: { type: ArgumentType.NUMBER, defaultValue: 55 }, + HEIGHT: { type: ArgumentType.NUMBER, defaultValue: 95 }, + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'pathToSpot', + text: formatMessage({ + id: 'jgPathfinding.blocks.pathToSpot', + default: 'find path to x: [X] y: [Y] around blockades', + description: "Block that finds a path around blockades in the pathfinding area to get to a location." + }), + arguments: { + X: { type: ArgumentType.NUMBER, defaultValue: 60 }, + Y: { type: ArgumentType.NUMBER, defaultValue: -60 }, + }, + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'setListToPath', + text: formatMessage({ + id: 'jgPathfinding.blocks.setListToPath', + default: 'set [LIST] to current path', + description: "Block that sets a list to the current path." + }), + arguments: { + LIST: { type: ArgumentType.LIST }, + }, + hideFromPalette: true, + blockType: BlockType.COMMAND + }, + { + opcode: 'getPathAs', + text: formatMessage({ + id: 'jgPathfinding.blocks.getPathAs', + default: 'current path as [TYPE]', + description: "Block that returns the current path in a certain way." + }), + arguments: { + TYPE: { type: ArgumentType.STRING, menu: "pathReturnType" }, + }, + disableMonitor: true, + blockType: BlockType.REPORTER + }, + ], + menus: { + // lists: "menuLists", + pathReturnType: { + acceptReporters: true, + items: [ + "json arrays", + "json array with objects", + "json object", + "comma seperated list", + ].map(item => ({ text: item, value: item })) + } + } + }; + } + // menus + + // blocks + createBlockadeAt(args) { + const x1 = Cast.toNumber(args.X1); + const y1 = Cast.toNumber(args.Y1); + const x2 = Cast.toNumber(args.X2); + const y2 = Cast.toNumber(args.Y2); + // add to map + this.map.add(x1, y1, x2, y2); + } + clearBlockades() { + this.map.clear(); + } + + setPatherXY(args) { + const x = Cast.toNumber(args.X); + const y = Cast.toNumber(args.Y); + this.pather.x = x; + this.pather.y = y; + } + setWidthHeight(args) { + const width = Cast.toNumber(args.WIDTH); + const height = Cast.toNumber(args.HEIGHT); + this.pather.width = width; + this.pather.height = height; + } + + pathToSpot(args) { + const goalX = Cast.toNumber(args.X); + const goalY = Cast.toNumber(args.Y); + const exported = this.map.toGrid(); + // add margins + const stageSize = { + width: this.runtime.stageWidth, + height: this.runtime.stageHeight + }; + const marginSize = (stageSize.height > stageSize.width ? stageSize.height : stageSize.width) + + (this.pather.height > this.pather.width ? this.pather.height : this.pather.width) + + 24; + // use the margins & add padding to the walls for the final matrix + const marginMatrix = Seperator.marginGrid(exported.grid, marginSize); + const matrix = Seperator.padGrid(marginMatrix, this.pather.width, this.pather.height); + // get proper offsets + const offset = exported.offset; + offset.left -= marginSize; + offset.top += marginSize; + // setup pathfinding + const grid = new Pathfinding.Grid(matrix); + const finder = new Pathfinding.AStarFinder({ + allowDiagonal: true, + dontCrossCorners: true + }); + // set real starts and goals + // this is based on the offset + const realPositions = Clone.simple({ + start: { + x: this.pather.x - offset.left, + y: Math.abs(this.pather.y - offset.top), + }, + end: { + x: goalX - offset.left, + y: Math.abs(goalY - offset.top), + } + }); + const path = Pathfinding.Util.compressPath(finder.findPath( + realPositions.start.x, + realPositions.start.y, + realPositions.end.x, + realPositions.end.y, + grid + )); + // we need to do more offsetting for the resulting path + // also need to convert it to nodes + const newPath = new Nodes(); + for (const node of path) { + const x = node[0] + offset.left; + const y = 0 - (node[1] - offset.top); + newPath.push([x, y]); + } + this.pathNodes = newPath; + } + + setListToPath(args, util) { + console.log(args); + const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); + list.value = push(args.ITEM); + } + getPathAs(args) { + const switchh = Cast.toString(args.TYPE).toLowerCase(); + switch (switchh) { + case 'json array with objects': + return JSON.stringify(this.pathNodes.getObjects()); + case 'json object': + return JSON.stringify(this.pathNodes.getAsObject()); + case 'comma seperated list': + return this.pathNodes.getCommaSeperated(); + default: + return JSON.stringify(this.pathNodes.getRaw()); + } + } +} + +module.exports = JgPathfindingBlocks; diff --git a/local-scratch-vm/src/extensions/jg_pathfinding/map.js b/local-scratch-vm/src/extensions/jg_pathfinding/map.js new file mode 100644 index 0000000000000000000000000000000000000000..276568506f4ee35896cdadc8215cb1467a249a3e --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_pathfinding/map.js @@ -0,0 +1,147 @@ +const Cast = require('../../util/cast'); +const Clone = require('../../util/clone'); + +const blankGridReturn = { + grid: [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0] + ], + offset: { + left: 0, + top: 0 + } +}; + +class Map { + constructor() { + this.boxes = []; + } + static new(...args) { + return new Map(...args); + } + + add(x1, y1, x2, y2) { + // cast & round + x1 = Math.round(Cast.toNumber(x1)); + y1 = Math.round(Cast.toNumber(y1)); + x2 = Math.round(Cast.toNumber(x2)); + y2 = Math.round(Cast.toNumber(y2)); + + const position = { + x1: x1, + y1: y1, + x2: x2, + y2: y2, + }; + if (x2 < x1) { + // x1 should be less + position.x1 = x2; + position.x2 = x1; + } + if (y2 > y1) { + // y1 should be greater + position.y1 = y2; + position.y2 = y1; + } + const box = { + x: position.x1, + y: position.x2, + width: position.x2 - position.x1, + height: position.y1 - position.y2 + }; + this.boxes.push(box); + return box; + } + clear() { + this.boxes = []; + } + + toGrid() { + // no tiles if theres no boxes + if (!this.boxes) return blankGridReturn; + if (this.boxes.length <= 0) return blankGridReturn; + + const grid = []; + // find highest y + let highestY = -Infinity; + for (const box of this.boxes) { + if (box.y > highestY) highestY = box.y; + } + // find lowest y & its block height + let lowestY = Infinity; + let lowestHeight = 0; + for (const box of this.boxes) { + if (box.y < lowestY) { + lowestY = box.y; + lowestHeight = box.height; + } + } + // for simplicity, we just fill this array & then clone it to make our rows + const baseRow = []; + // get grid width + // find lowest x + let lowestX = Infinity; + for (const box of this.boxes) { + if (box.x < lowestX) lowestX = box.x; + } + // find highest x & its block width + let highestX = -Infinity; + let highestWidth = 0; + for (const box of this.boxes) { + if (box.x > highestX) { + highestX = box.x; + highestWidth = box.width; + } + } + // based on x numbers, add 0s to base row + const xtoxwidth = (highestX - lowestX) + highestWidth; + for (let i = 0; i < xtoxwidth; i++) { + baseRow.push(0); + } + // based on y numbers, add rows to grid + const ytoyheight = (highestY - lowestY) + lowestHeight; + for (let i = 0; i < ytoyheight; i++) { + // add a clone so modifiying a row doesnt modify all rows + const clone = Clone.simple(baseRow); + grid.push(clone); + } + + // console.log(Clone.simple(grid)); + + // fill the walls + // we need the offset since we are converting scratch coords to grid indexes + const offset = Clone.simple({ + top: highestY, + left: lowestX + }); + for (const box of this.boxes) { + // offsetX is straight forward + // offsetY makes everything except the highest box be a negative Y + // so we correct for that + const offsetX = box.x - offset.left; + const offsetY = Math.abs(box.y - offset.top); + let tileIdx = offsetX; + let rowIdx = offsetY; + // repeat (height) { repeat (width) { tileIdx++ } rowIdx++; tileIdx = offsetX; } + // the for loops could probably be better optimized but i dont wanna focus on that until the code actually works + for (let i = 0; i < box.height; i++) { + for (let j = 0; j < box.width; j++) { + // find row & then find tile to set to 1 + const row = grid[rowIdx]; + row[tileIdx] = 1; + tileIdx++; + } + rowIdx++; + tileIdx = offsetX; + } + } + + return { + grid: grid, + offset: offset + } + } +} + +module.exports = Map; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_pathfinding/nodes.js b/local-scratch-vm/src/extensions/jg_pathfinding/nodes.js new file mode 100644 index 0000000000000000000000000000000000000000..ca8492c07adc983c66c03eac3369c615680ca8f0 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_pathfinding/nodes.js @@ -0,0 +1,56 @@ +const Clone = require('../../util/clone'); + +class Nodes { + constructor(...args) { + if (Array.isArray(args)) { + if (Array.isArray(args[0])) { + this._nodes = args; + return; + } + } + this._nodes = []; + for (let node of args) { + if (!Array.isArray(node)) node = []; + if (typeof node[0] !== 'number') node[0] = 0; + if (typeof node[1] !== 'number') node[1] = 0; + this._nodes.push([ + Cast.toNumber(node[0]), + Cast.toNumber(node[1]) + ]); + } + } + + push(node) { + this._nodes.push(node); + } + + getRaw() { + return Clone.simple(this._nodes); + } + getAsObject() { + const nodes = this.getRaw(); + const object = {}; + let idx = 0; + for (const node of nodes) { + const key = Cast.toString(idx + 1); + object[key] = { x: node[0], y: node[1] }; + idx++; + } + return object; + } + getObjects() { + const nodes = this.getRaw(); + const newArray = []; + for (const node of nodes) { + newArray.push({ x: node[0], y: node[1] }); + } + return newArray; + } + getCommaSeperated() { + const nodes = this.getRaw(); + const flattened = nodes.flat(Infinity); + return flattened.join(','); + } +} + +module.exports = Nodes; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_pathfinding/seperator.js b/local-scratch-vm/src/extensions/jg_pathfinding/seperator.js new file mode 100644 index 0000000000000000000000000000000000000000..cc904266e2186fc9339fe2527f80833d14776c46 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_pathfinding/seperator.js @@ -0,0 +1,184 @@ +const Cast = require('../../util/cast'); +const Clone = require('../../util/clone'); + +class Seperator { + static _createArrayOfLength(length, item) { + length = Cast.toNumber(length); + if (length <= 0) return []; + if (!isFinite(length)) return []; + const newArray = Array.from(Array(length).keys()).map(() => { + return item; + }); + return Clone.simple(newArray); + } + static _validateUnsignedInteger(int) { + int = Cast.toNumber(int); + if (int < 0) int = 0; + if (!isFinite(int)) int = 0; + return Math.round(int); + } + static _splitNumber(num) { + const number = Math.round(Cast.toNumber(num)); + if (number === 0) return [0, 0]; + if (!isFinite(number)) return [0, 0]; + return [ + Math.ceil(number / 2), + Math.floor(number / 2) + ]; + } + /** + * Adds extra space in a grid so large pathers can still get around objects. + * @param {Grid} grid The grid that needs extra spacing + * @param {number} amount Amount of extra tiles you want to add on each side + * @returns The new grid with it's margins + */ + static marginGrid(grid, amount) { + // amount must be round & not inf or less than 0 + amount = Seperator._validateUnsignedInteger(amount); + + const newGrid = Clone.simple(grid); + // console.log(Clone.simple(newGrid), newGrid[0].length); + const gridLength = Seperator._validateUnsignedInteger(newGrid[0] ? newGrid[0].length : 0); + const fillerRow = Seperator._createArrayOfLength(gridLength, 0); + // add 0s to the top and bottom + for (let i = 0; i < amount; i++) { + // we need to push new copies + // otherwise we store the same array + // but in multiple indexes + const first = Clone.simple(fillerRow); + const last = Clone.simple(fillerRow); + newGrid.unshift(first); + newGrid.push(last); + } + // console.log(Clone.simple(newGrid), amount); + // add 0s to the individual rows to expand the sides + for (const row of newGrid) { + for (let i = 0; i < amount; i++) { + row.unshift(0); + row.push(0); + } + } + return newGrid; + } + /** + * Thickens the walls in the grid. Unlike marginGrid, this actually cuts the numbers in half properly. + * Odd numbers are split, see below which sides get the larger split. + * @param {Grid} grid The grid that needs thicker walls + * @param {number} width The width that will be thickened. Odd numbers will thicken the right, then the left. + * @param {number} height The height that will be thickened. Odd numbers will thicken the top, then the bottom. + * @returns The new grid with the padded walls. + */ + static padGrid(grid, width, height) { + // split the width & height numbers so we can thicken the walls with them + // we need to subtract 1 without going negative so that wall thickness is reasonable + const reasonableW = Math.max(0, Cast.toNumber(width) - 1); + const reasonableH = Math.max(0, Cast.toNumber(height) - 1); + const splitW = Seperator._splitNumber(reasonableW); + const splitH = Seperator._splitNumber(reasonableH); + // we need the original grid so we dont thicken walls after we already thickened them + const originalGrid = Clone.simple(grid); + const newGrid = Clone.simple(grid); + // go through each row & tile of the grid + const idx = { + row: 0, + tile: 0 + } + for (const row of newGrid) { + for (const _ of row) { + // if not a wall, increment index & continue to next iteration + if (originalGrid[idx.row][idx.tile] <= 0) { + idx.tile++; + continue; + } + // console.log('can'); + // we are a wall, thicken + // thicken horizontally first as its the easiest + for (let i = 0; i < splitW[0]; i++) { + // right + const nextTile = idx.tile + (i + 1); + // dont continue if there is no next tile + // this can happen if we reach the boundary + // of the grid + if (typeof row[nextTile] !== 'number') continue; + // set next tile to a wall + row[nextTile] = 1; + } + for (let i = 0; i < splitW[1]; i++) { + // left + const nextTile = idx.tile - (i + 1); + // dont continue if there is no next tile + // this can happen if we reach the boundary + // of the grid + if (typeof row[nextTile] !== 'number') continue; + // set next tile to a wall + row[nextTile] = 1; + } + // thicken vertically + for (let i = 0; i < splitH[0]; i++) { + // top + const nextRow = idx.row - (i + 1); + // dont continue if there is no next row + // this can happen if we reach the boundary + // of the grid + if (!originalGrid[nextRow]) continue; + // get next row + const foundRow = newGrid[nextRow]; + // set tile to a wall + foundRow[idx.tile] = 1; + } + for (let i = 0; i < splitH[1]; i++) { + // bottom + const nextRow = idx.row + (i + 1); + // dont continue if there is no next row + // this can happen if we reach the boundary + // of the grid + if (!originalGrid[nextRow]) continue; + // get next row + const foundRow = newGrid[nextRow]; + // set tile to a wall + foundRow[idx.tile] = 1; + } + // if width & height are greater than 0, thicken diagonally + // this is the hardest one to do because we need to modify + // horizontal values in the vertical arrays + // we stack a for loop in a for loop in a for loop in a for loop + for (let i = 0; i < 2; i++) { + const isBottom = i === 1; + for (let j = 0; j < splitH[isBottom ? 1 : 0]; j++) { + // vertical + let nextRow = idx.row - (j + 1); + if (isBottom) nextRow = idx.row + (j + 1); + // dont continue if there is no next row + // this can happen if we reach the boundary + // of the grid + if (!originalGrid[nextRow]) continue; + // get next row + const foundRow = newGrid[nextRow]; + for (let k = 0; k < 2; k++) { + const isLeft = k === 1; + for (let l = 0; l < splitW[isLeft ? 1 : 0]; l++) { + // horizontal + let nextTile = idx.tile + (l + 1); + if (isLeft) nextTile = idx.tile - (l + 1); + // dont continue if there is no next tile + // this can happen if we reach the boundary + // of the grid + if (typeof foundRow[nextTile] !== 'number') continue; + // set next tile to a wall + foundRow[nextTile] = 1; + } + } + } + } + // increment index + idx.tile++; + } + // increment index and reset tile idx + idx.row++; + idx.tile = 0; + } + return newGrid; + } +} + +module.exports = Seperator; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_permissions/index.js b/local-scratch-vm/src/extensions/jg_permissions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b731920fdec2305da5cbd36c03b5f204836f81a7 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_permissions/index.js @@ -0,0 +1,84 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const ProjectPermissionManager = require('../../util/project-permissions'); + +/** + * Class for Permission blocks + * @constructor + */ +class JgPermissionBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'JgPermissionBlocks', + name: 'Permissions', + color1: '#00C4FF', + color2: '#0093FF', + blocks: [ + { + blockType: BlockType.LABEL, + text: "This extension is deprecated." + }, + // tw says deleting menu elements is unsafe + // blocks below this are hidden + { hideFromPalette: true, opcode: 'requestPermission', text: 'request [PERMISSION] permission', disableMonitor: false, blockType: BlockType.BOOLEAN, arguments: { PERMISSION: { type: ArgumentType.STRING, menu: 'permissions', defaultValue: "javascript" } } }, + { hideFromPalette: true, opcode: 'requestPermission2', text: 'request [PERMISSION] permission', disableMonitor: false, blockType: BlockType.BOOLEAN, arguments: { PERMISSION: { type: ArgumentType.STRING, menu: 'permissions2' } } }, + { hideFromPalette: true, opcode: 'requestAllPermission', text: 'request all permissions', disableMonitor: false, blockType: BlockType.BOOLEAN }, + { hideFromPalette: true, opcode: 'requestSitePermission', text: 'request permission to show [URL]', disableMonitor: false, blockType: BlockType.BOOLEAN, arguments: { URL: { type: ArgumentType.STRING, defaultValue: "https://www.example.com" } } }, + ], + menus: { + // tw says deleting menu elements is unsafe + // menus below this are hidden + permissions: "fetchPermissionsList", + permissions2: "fetchPermissionsList2" + } + }; + } + + fetchPermissionsList() { + return Object.getOwnPropertyNames(ProjectPermissionManager.permissions).filter(name => typeof ProjectPermissionManager.permissions[name] === "boolean").map(permissionName => ({ + text: permissionName, + value: permissionName + })); + } + + fetchPermissionsList2() { + // tw says deleting menu elements is unsafe + return Object.getOwnPropertyNames(ProjectPermissionManager.permissions).filter(name => typeof ProjectPermissionManager.permissions[name] === "boolean").filter(name => name !== "javascript").map(permissionName => ({ + text: permissionName, + value: permissionName + })); + } + + requestPermission(args) { + const permission = args.PERMISSION; + if (ProjectPermissionManager.permissions[permission] == true) return true; + return ProjectPermissionManager.RequestPermission(permission); + } + requestPermission2(args) { + // tw says deleting menu elements is unsafe + const permission = args.PERMISSION; + if (ProjectPermissionManager.permissions[permission] == true) return true; + return ProjectPermissionManager.RequestPermission(permission); + } + requestAllPermission() { + return ProjectPermissionManager.RequestAllPermissions(); + } + requestSitePermission(args) { + const site = args.URL; + if (ProjectPermissionManager.permissions.limitedWebsites[site] == true) return true; + return ProjectPermissionManager.RequestPermission("limitedWebsite", site); + } +} + +module.exports = JgPermissionBlocks; diff --git a/local-scratch-vm/src/extensions/jg_prism/beatgammit-deflate.js b/local-scratch-vm/src/extensions/jg_prism/beatgammit-deflate.js new file mode 100644 index 0000000000000000000000000000000000000000..f8d196ff0d59ce451511cc5cdc64e20c0d93c089 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_prism/beatgammit-deflate.js @@ -0,0 +1,1678 @@ +/* constant parameters */ +var WSIZE = 32768, // Sliding Window size + STORED_BLOCK = 0, + STATIC_TREES = 1, + DYN_TREES = 2, + + /* for deflate */ + DEFAULT_LEVEL = 6, + FULL_SEARCH = false, + INBUFSIZ = 32768, // Input buffer size + //INBUF_EXTRA = 64, // Extra buffer + OUTBUFSIZ = 1024 * 8, + window_size = 2 * WSIZE, + MIN_MATCH = 3, + MAX_MATCH = 258, + BITS = 16, + // for SMALL_MEM + LIT_BUFSIZE = 0x2000, + // HASH_BITS = 13, + //for MEDIUM_MEM + // LIT_BUFSIZE = 0x4000, + // HASH_BITS = 14, + // for BIG_MEM + // LIT_BUFSIZE = 0x8000, + HASH_BITS = 15, + DIST_BUFSIZE = LIT_BUFSIZE, + HASH_SIZE = 1 << HASH_BITS, + HASH_MASK = HASH_SIZE - 1, + WMASK = WSIZE - 1, + NIL = 0, // Tail of hash chains + TOO_FAR = 4096, + MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1, + MAX_DIST = WSIZE - MIN_LOOKAHEAD, + SMALLEST = 1, + MAX_BITS = 15, + MAX_BL_BITS = 7, + LENGTH_CODES = 29, + LITERALS = 256, + END_BLOCK = 256, + L_CODES = LITERALS + 1 + LENGTH_CODES, + D_CODES = 30, + BL_CODES = 19, + REP_3_6 = 16, + REPZ_3_10 = 17, + REPZ_11_138 = 18, + HEAP_SIZE = 2 * L_CODES + 1, + H_SHIFT = parseInt((HASH_BITS + MIN_MATCH - 1) / MIN_MATCH, 10), + + /* variables */ + free_queue, + qhead, + qtail, + initflag, + outbuf = null, + outcnt, + outoff, + complete, + window, + d_buf, + l_buf, + prev, + bi_buf, + bi_valid, + block_start, + ins_h, + hash_head, + prev_match, + match_available, + match_length, + prev_length, + strstart, + match_start, + eofile, + lookahead, + max_chain_length, + max_lazy_match, + compr_level, + good_match, + nice_match, + dyn_ltree, + dyn_dtree, + static_ltree, + static_dtree, + bl_tree, + l_desc, + d_desc, + bl_desc, + bl_count, + heap, + heap_len, + heap_max, + depth, + length_code, + dist_code, + base_length, + base_dist, + flag_buf, + last_lit, + last_dist, + last_flags, + flags, + flag_bit, + opt_len, + static_len, + deflate_data, + deflate_pos; + +if (LIT_BUFSIZE > INBUFSIZ) { + console.error("error: INBUFSIZ is too small"); +} +if ((WSIZE << 1) > (1 << BITS)) { + console.error("error: WSIZE is too large"); +} +if (HASH_BITS > BITS - 1) { + console.error("error: HASH_BITS is too large"); +} +if (HASH_BITS < 8 || MAX_MATCH !== 258) { + console.error("error: Code too clever"); +} + +/* objects (deflate) */ + +function DeflateCT() { + this.fc = 0; // frequency count or bit string + this.dl = 0; // father node in Huffman tree or length of bit string +} + +function DeflateTreeDesc() { + this.dyn_tree = null; // the dynamic tree + this.static_tree = null; // corresponding static tree or NULL + this.extra_bits = null; // extra bits for each code or NULL + this.extra_base = 0; // base index for extra_bits + this.elems = 0; // max number of elements in the tree + this.max_length = 0; // max bit length for the codes + this.max_code = 0; // largest code with non zero frequency +} + +/* Values for max_lazy_match, good_match and max_chain_length, depending on +* the desired pack level (0..9). The values given below have been tuned to +* exclude worst case performance for pathological files. Better values may be +* found for specific files. +*/ +function DeflateConfiguration(a, b, c, d) { + this.good_length = a; // reduce lazy search above this match length + this.max_lazy = b; // do not perform lazy search above this match length + this.nice_length = c; // quit search above this match length + this.max_chain = d; +} + +function DeflateBuffer() { + this.next = null; + this.len = 0; + this.ptr = []; // new Array(OUTBUFSIZ); // ptr.length is never read + this.off = 0; +} + +/* constant tables */ +var extra_lbits = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0]; +var extra_dbits = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13]; +var extra_blbits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 7]; +var bl_order = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; +var configuration_table = [ + new DeflateConfiguration(0, 0, 0, 0), + new DeflateConfiguration(4, 4, 8, 4), + new DeflateConfiguration(4, 5, 16, 8), + new DeflateConfiguration(4, 6, 32, 32), + new DeflateConfiguration(4, 4, 16, 16), + new DeflateConfiguration(8, 16, 32, 32), + new DeflateConfiguration(8, 16, 128, 128), + new DeflateConfiguration(8, 32, 128, 256), + new DeflateConfiguration(32, 128, 258, 1024), + new DeflateConfiguration(32, 258, 258, 4096) +]; + + +/* routines (deflate) */ + +function deflate_start(level) { + var i; + + if (!level) { + level = DEFAULT_LEVEL; + } else if (level < 1) { + level = 1; + } else if (level > 9) { + level = 9; + } + + compr_level = level; + initflag = false; + eofile = false; + if (outbuf !== null) { + return; + } + + free_queue = qhead = qtail = null; + outbuf = []; // new Array(OUTBUFSIZ); // outbuf.length never called + window = []; // new Array(window_size); // window.length never called + d_buf = []; // new Array(DIST_BUFSIZE); // d_buf.length never called + l_buf = []; // new Array(INBUFSIZ + INBUF_EXTRA); // l_buf.length never called + prev = []; // new Array(1 << BITS); // prev.length never called + + dyn_ltree = []; + for (i = 0; i < HEAP_SIZE; i++) { + dyn_ltree[i] = new DeflateCT(); + } + dyn_dtree = []; + for (i = 0; i < 2 * D_CODES + 1; i++) { + dyn_dtree[i] = new DeflateCT(); + } + static_ltree = []; + for (i = 0; i < L_CODES + 2; i++) { + static_ltree[i] = new DeflateCT(); + } + static_dtree = []; + for (i = 0; i < D_CODES; i++) { + static_dtree[i] = new DeflateCT(); + } + bl_tree = []; + for (i = 0; i < 2 * BL_CODES + 1; i++) { + bl_tree[i] = new DeflateCT(); + } + l_desc = new DeflateTreeDesc(); + d_desc = new DeflateTreeDesc(); + bl_desc = new DeflateTreeDesc(); + bl_count = []; // new Array(MAX_BITS+1); // bl_count.length never called + heap = []; // new Array(2*L_CODES+1); // heap.length never called + depth = []; // new Array(2*L_CODES+1); // depth.length never called + length_code = []; // new Array(MAX_MATCH-MIN_MATCH+1); // length_code.length never called + dist_code = []; // new Array(512); // dist_code.length never called + base_length = []; // new Array(LENGTH_CODES); // base_length.length never called + base_dist = []; // new Array(D_CODES); // base_dist.length never called + flag_buf = []; // new Array(parseInt(LIT_BUFSIZE / 8, 10)); // flag_buf.length never called +} + +function deflate_end() { + free_queue = qhead = qtail = null; + outbuf = null; + window = null; + d_buf = null; + l_buf = null; + prev = null; + dyn_ltree = null; + dyn_dtree = null; + static_ltree = null; + static_dtree = null; + bl_tree = null; + l_desc = null; + d_desc = null; + bl_desc = null; + bl_count = null; + heap = null; + depth = null; + length_code = null; + dist_code = null; + base_length = null; + base_dist = null; + flag_buf = null; +} + +function reuse_queue(p) { + p.next = free_queue; + free_queue = p; +} + +function new_queue() { + var p; + + if (free_queue !== null) { + p = free_queue; + free_queue = free_queue.next; + } else { + p = new DeflateBuffer(); + } + p.next = null; + p.len = p.off = 0; + + return p; +} + +function head1(i) { + return prev[WSIZE + i]; +} + +function head2(i, val) { + return (prev[WSIZE + i] = val); +} + +/* put_byte is used for the compressed output, put_ubyte for the +* uncompressed output. However unlzw() uses window for its +* suffix table instead of its output buffer, so it does not use put_ubyte +* (to be cleaned up). +*/ +function put_byte(c) { + outbuf[outoff + outcnt++] = c; + if (outoff + outcnt === OUTBUFSIZ) { + qoutbuf(); + } +} + +/* Output a 16 bit value, lsb first */ +function put_short(w) { + w &= 0xffff; + if (outoff + outcnt < OUTBUFSIZ - 2) { + outbuf[outoff + outcnt++] = (w & 0xff); + outbuf[outoff + outcnt++] = (w >>> 8); + } else { + put_byte(w & 0xff); + put_byte(w >>> 8); + } +} + +/* ========================================================================== +* Insert string s in the dictionary and set match_head to the previous head +* of the hash chain (the most recent string with same hash key). Return +* the previous length of the hash chain. +* IN assertion: all calls to to INSERT_STRING are made with consecutive +* input characters and the first MIN_MATCH bytes of s are valid +* (except for the last MIN_MATCH-1 bytes of the input file). +*/ +function INSERT_STRING() { + ins_h = ((ins_h << H_SHIFT) ^ (window[strstart + MIN_MATCH - 1] & 0xff)) & HASH_MASK; + hash_head = head1(ins_h); + prev[strstart & WMASK] = hash_head; + head2(ins_h, strstart); +} + +/* Send a code of the given tree. c and tree must not have side effects */ +function SEND_CODE(c, tree) { + send_bits(tree[c].fc, tree[c].dl); +} + +/* Mapping from a distance to a distance code. dist is the distance - 1 and +* must not have side effects. dist_code[256] and dist_code[257] are never +* used. +*/ +function D_CODE(dist) { + return (dist < 256 ? dist_code[dist] : dist_code[256 + (dist >> 7)]) & 0xff; +} + +/* ========================================================================== +* Compares to subtrees, using the tree depth as tie breaker when +* the subtrees have equal frequency. This minimizes the worst case length. +*/ +function SMALLER(tree, n, m) { + return tree[n].fc < tree[m].fc || (tree[n].fc === tree[m].fc && depth[n] <= depth[m]); +} + +/* ========================================================================== +* read string data +*/ +function read_buff(buff, offset, n) { + var i; + for (i = 0; i < n && deflate_pos < deflate_data.length; i++) { + buff[offset + i] = deflate_data[deflate_pos++] & 0xff; + } + return i; +} + +/* ========================================================================== +* Initialize the "longest match" routines for a new file +*/ +function lm_init() { + var j; + + // Initialize the hash table. */ + for (j = 0; j < HASH_SIZE; j++) { + // head2(j, NIL); + prev[WSIZE + j] = 0; + } + // prev will be initialized on the fly */ + + // Set the default configuration parameters: + max_lazy_match = configuration_table[compr_level].max_lazy; + good_match = configuration_table[compr_level].good_length; + if (!FULL_SEARCH) { + nice_match = configuration_table[compr_level].nice_length; + } + max_chain_length = configuration_table[compr_level].max_chain; + + strstart = 0; + block_start = 0; + + lookahead = read_buff(window, 0, 2 * WSIZE); + if (lookahead <= 0) { + eofile = true; + lookahead = 0; + return; + } + eofile = false; + // Make sure that we always have enough lookahead. This is important + // if input comes from a device such as a tty. + while (lookahead < MIN_LOOKAHEAD && !eofile) { + fill_window(); + } + + // If lookahead < MIN_MATCH, ins_h is garbage, but this is + // not important since only literal bytes will be emitted. + ins_h = 0; + for (j = 0; j < MIN_MATCH - 1; j++) { + // UPDATE_HASH(ins_h, window[j]); + ins_h = ((ins_h << H_SHIFT) ^ (window[j] & 0xff)) & HASH_MASK; + } +} + +/* ========================================================================== +* Set match_start to the longest match starting at the given string and +* return its length. Matches shorter or equal to prev_length are discarded, +* in which case the result is equal to prev_length and match_start is +* garbage. +* IN assertions: cur_match is the head of the hash chain for the current +* string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1 +*/ +function longest_match(cur_match) { + var chain_length = max_chain_length; // max hash chain length + var scanp = strstart; // current string + var matchp; // matched string + var len; // length of current match + var best_len = prev_length; // best match length so far + + // Stop when cur_match becomes <= limit. To simplify the code, + // we prevent matches with the string of window index 0. + var limit = (strstart > MAX_DIST ? strstart - MAX_DIST : NIL); + + var strendp = strstart + MAX_MATCH; + var scan_end1 = window[scanp + best_len - 1]; + var scan_end = window[scanp + best_len]; + + var i, broke; + + // Do not waste too much time if we already have a good match: */ + if (prev_length >= good_match) { + chain_length >>= 2; + } + + // Assert(encoder->strstart <= window_size-MIN_LOOKAHEAD, "insufficient lookahead"); + + do { + // Assert(cur_match < encoder->strstart, "no future"); + matchp = cur_match; + + // Skip to next match if the match length cannot increase + // or if the match length is less than 2: + if (window[matchp + best_len] !== scan_end || + window[matchp + best_len - 1] !== scan_end1 || + window[matchp] !== window[scanp] || + window[++matchp] !== window[scanp + 1]) { + continue; + } + + // The check at best_len-1 can be removed because it will be made + // again later. (This heuristic is not always a win.) + // It is not necessary to compare scan[2] and match[2] since they + // are always equal when the other bytes match, given that + // the hash keys are equal and that HASH_BITS >= 8. + scanp += 2; + matchp++; + + // We check for insufficient lookahead only every 8th comparison; + // the 256th check will be made at strstart+258. + while (scanp < strendp) { + broke = false; + for (i = 0; i < 8; i += 1) { + scanp += 1; + matchp += 1; + if (window[scanp] !== window[matchp]) { + broke = true; + break; + } + } + + if (broke) { + break; + } + } + + len = MAX_MATCH - (strendp - scanp); + scanp = strendp - MAX_MATCH; + + if (len > best_len) { + match_start = cur_match; + best_len = len; + if (FULL_SEARCH) { + if (len >= MAX_MATCH) { + break; + } + } else { + if (len >= nice_match) { + break; + } + } + + scan_end1 = window[scanp + best_len - 1]; + scan_end = window[scanp + best_len]; + } + } while ((cur_match = prev[cur_match & WMASK]) > limit && --chain_length !== 0); + + return best_len; +} + +/* ========================================================================== +* Fill the window when the lookahead becomes insufficient. +* Updates strstart and lookahead, and sets eofile if end of input file. +* IN assertion: lookahead < MIN_LOOKAHEAD && strstart + lookahead > 0 +* OUT assertions: at least one byte has been read, or eofile is set; +* file reads are performed for at least two bytes (required for the +* translate_eol option). +*/ +function fill_window() { + var n, m; + + // Amount of free space at the end of the window. + var more = window_size - lookahead - strstart; + + // If the window is almost full and there is insufficient lookahead, + // move the upper half to the lower one to make room in the upper half. + if (more === -1) { + // Very unlikely, but possible on 16 bit machine if strstart == 0 + // and lookahead == 1 (input done one byte at time) + more--; + } else if (strstart >= WSIZE + MAX_DIST) { + // By the IN assertion, the window is not empty so we can't confuse + // more == 0 with more == 64K on a 16 bit machine. + // Assert(window_size == (ulg)2*WSIZE, "no sliding with BIG_MEM"); + + // System.arraycopy(window, WSIZE, window, 0, WSIZE); + for (n = 0; n < WSIZE; n++) { + window[n] = window[n + WSIZE]; + } + + match_start -= WSIZE; + strstart -= WSIZE; /* we now have strstart >= MAX_DIST: */ + block_start -= WSIZE; + + for (n = 0; n < HASH_SIZE; n++) { + m = head1(n); + head2(n, m >= WSIZE ? m - WSIZE : NIL); + } + for (n = 0; n < WSIZE; n++) { + // If n is not on any hash chain, prev[n] is garbage but + // its value will never be used. + m = prev[n]; + prev[n] = (m >= WSIZE ? m - WSIZE : NIL); + } + more += WSIZE; + } + // At this point, more >= 2 + if (!eofile) { + n = read_buff(window, strstart + lookahead, more); + if (n <= 0) { + eofile = true; + } else { + lookahead += n; + } + } +} + +/* ========================================================================== +* Processes a new input file and return its compressed length. This +* function does not perform lazy evaluationof matches and inserts +* new strings in the dictionary only for unmatched strings or for short +* matches. It is used only for the fast compression options. +*/ +function deflate_fast() { + while (lookahead !== 0 && qhead === null) { + var flush; // set if current block must be flushed + + // Insert the string window[strstart .. strstart+2] in the + // dictionary, and set hash_head to the head of the hash chain: + INSERT_STRING(); + + // Find the longest match, discarding those <= prev_length. + // At this point we have always match_length < MIN_MATCH + if (hash_head !== NIL && strstart - hash_head <= MAX_DIST) { + // To simplify the code, we prevent matches with the string + // of window index 0 (in particular we have to avoid a match + // of the string with itself at the start of the input file). + match_length = longest_match(hash_head); + // longest_match() sets match_start */ + if (match_length > lookahead) { + match_length = lookahead; + } + } + if (match_length >= MIN_MATCH) { + // check_match(strstart, match_start, match_length); + + flush = ct_tally(strstart - match_start, match_length - MIN_MATCH); + lookahead -= match_length; + + // Insert new strings in the hash table only if the match length + // is not too large. This saves time but degrades compression. + if (match_length <= max_lazy_match) { + match_length--; // string at strstart already in hash table + do { + strstart++; + INSERT_STRING(); + // strstart never exceeds WSIZE-MAX_MATCH, so there are + // always MIN_MATCH bytes ahead. If lookahead < MIN_MATCH + // these bytes are garbage, but it does not matter since + // the next lookahead bytes will be emitted as literals. + } while (--match_length !== 0); + strstart++; + } else { + strstart += match_length; + match_length = 0; + ins_h = window[strstart] & 0xff; + // UPDATE_HASH(ins_h, window[strstart + 1]); + ins_h = ((ins_h << H_SHIFT) ^ (window[strstart + 1] & 0xff)) & HASH_MASK; + + //#if MIN_MATCH !== 3 + // Call UPDATE_HASH() MIN_MATCH-3 more times + //#endif + + } + } else { + // No match, output a literal byte */ + flush = ct_tally(0, window[strstart] & 0xff); + lookahead--; + strstart++; + } + if (flush) { + flush_block(0); + block_start = strstart; + } + + // Make sure that we always have enough lookahead, except + // at the end of the input file. We need MAX_MATCH bytes + // for the next match, plus MIN_MATCH bytes to insert the + // string following the next match. + while (lookahead < MIN_LOOKAHEAD && !eofile) { + fill_window(); + } + } +} + +function deflate_better() { + // Process the input block. */ + while (lookahead !== 0 && qhead === null) { + // Insert the string window[strstart .. strstart+2] in the + // dictionary, and set hash_head to the head of the hash chain: + INSERT_STRING(); + + // Find the longest match, discarding those <= prev_length. + prev_length = match_length; + prev_match = match_start; + match_length = MIN_MATCH - 1; + + if (hash_head !== NIL && prev_length < max_lazy_match && strstart - hash_head <= MAX_DIST) { + // To simplify the code, we prevent matches with the string + // of window index 0 (in particular we have to avoid a match + // of the string with itself at the start of the input file). + match_length = longest_match(hash_head); + // longest_match() sets match_start */ + if (match_length > lookahead) { + match_length = lookahead; + } + + // Ignore a length 3 match if it is too distant: */ + if (match_length === MIN_MATCH && strstart - match_start > TOO_FAR) { + // If prev_match is also MIN_MATCH, match_start is garbage + // but we will ignore the current match anyway. + match_length--; + } + } + // If there was a match at the previous step and the current + // match is not better, output the previous match: + if (prev_length >= MIN_MATCH && match_length <= prev_length) { + var flush; // set if current block must be flushed + + // check_match(strstart - 1, prev_match, prev_length); + flush = ct_tally(strstart - 1 - prev_match, prev_length - MIN_MATCH); + + // Insert in hash table all strings up to the end of the match. + // strstart-1 and strstart are already inserted. + lookahead -= prev_length - 1; + prev_length -= 2; + do { + strstart++; + INSERT_STRING(); + // strstart never exceeds WSIZE-MAX_MATCH, so there are + // always MIN_MATCH bytes ahead. If lookahead < MIN_MATCH + // these bytes are garbage, but it does not matter since the + // next lookahead bytes will always be emitted as literals. + } while (--prev_length !== 0); + match_available = false; + match_length = MIN_MATCH - 1; + strstart++; + if (flush) { + flush_block(0); + block_start = strstart; + } + } else if (match_available) { + // If there was no match at the previous position, output a + // single literal. If there was a match but the current match + // is longer, truncate the previous match to a single literal. + if (ct_tally(0, window[strstart - 1] & 0xff)) { + flush_block(0); + block_start = strstart; + } + strstart++; + lookahead--; + } else { + // There is no previous match to compare with, wait for + // the next step to decide. + match_available = true; + strstart++; + lookahead--; + } + + // Make sure that we always have enough lookahead, except + // at the end of the input file. We need MAX_MATCH bytes + // for the next match, plus MIN_MATCH bytes to insert the + // string following the next match. + while (lookahead < MIN_LOOKAHEAD && !eofile) { + fill_window(); + } + } +} + +function init_deflate() { + if (eofile) { + return; + } + bi_buf = 0; + bi_valid = 0; + ct_init(); + lm_init(); + + qhead = null; + outcnt = 0; + outoff = 0; + + if (compr_level <= 3) { + prev_length = MIN_MATCH - 1; + match_length = 0; + } else { + match_length = MIN_MATCH - 1; + match_available = false; + } + + complete = false; +} + +/* ========================================================================== +* Same as above, but achieves better compression. We use a lazy +* evaluation for matches: a match is finally adopted only if there is +* no better match at the next window position. +*/ +function deflate_internal(buff, off, buff_size) { + var n; + + if (!initflag) { + init_deflate(); + initflag = true; + if (lookahead === 0) { // empty + complete = true; + return 0; + } + } + + n = qcopy(buff, off, buff_size); + if (n === buff_size) { + return buff_size; + } + + if (complete) { + return n; + } + + if (compr_level <= 3) { + // optimized for speed + deflate_fast(); + } else { + deflate_better(); + } + + if (lookahead === 0) { + if (match_available) { + ct_tally(0, window[strstart - 1] & 0xff); + } + flush_block(1); + complete = true; + } + + return n + qcopy(buff, n + off, buff_size - n); +} + +function qcopy(buff, off, buff_size) { + var n, i, j; + + n = 0; + while (qhead !== null && n < buff_size) { + i = buff_size - n; + if (i > qhead.len) { + i = qhead.len; + } + // System.arraycopy(qhead.ptr, qhead.off, buff, off + n, i); + for (j = 0; j < i; j++) { + buff[off + n + j] = qhead.ptr[qhead.off + j]; + } + + qhead.off += i; + qhead.len -= i; + n += i; + if (qhead.len === 0) { + var p; + p = qhead; + qhead = qhead.next; + reuse_queue(p); + } + } + + if (n === buff_size) { + return n; + } + + if (outoff < outcnt) { + i = buff_size - n; + if (i > outcnt - outoff) { + i = outcnt - outoff; + } + // System.arraycopy(outbuf, outoff, buff, off + n, i); + for (j = 0; j < i; j++) { + buff[off + n + j] = outbuf[outoff + j]; + } + outoff += i; + n += i; + if (outcnt === outoff) { + outcnt = outoff = 0; + } + } + return n; +} + +/* ========================================================================== +* Allocate the match buffer, initialize the various tables and save the +* location of the internal file attribute (ascii/binary) and method +* (DEFLATE/STORE). +*/ +function ct_init() { + var n; // iterates over tree elements + var bits; // bit counter + var length; // length value + var code; // code value + var dist; // distance index + + if (static_dtree[0].dl !== 0) { + return; // ct_init already called + } + + l_desc.dyn_tree = dyn_ltree; + l_desc.static_tree = static_ltree; + l_desc.extra_bits = extra_lbits; + l_desc.extra_base = LITERALS + 1; + l_desc.elems = L_CODES; + l_desc.max_length = MAX_BITS; + l_desc.max_code = 0; + + d_desc.dyn_tree = dyn_dtree; + d_desc.static_tree = static_dtree; + d_desc.extra_bits = extra_dbits; + d_desc.extra_base = 0; + d_desc.elems = D_CODES; + d_desc.max_length = MAX_BITS; + d_desc.max_code = 0; + + bl_desc.dyn_tree = bl_tree; + bl_desc.static_tree = null; + bl_desc.extra_bits = extra_blbits; + bl_desc.extra_base = 0; + bl_desc.elems = BL_CODES; + bl_desc.max_length = MAX_BL_BITS; + bl_desc.max_code = 0; + + // Initialize the mapping length (0..255) -> length code (0..28) + length = 0; + for (code = 0; code < LENGTH_CODES - 1; code++) { + base_length[code] = length; + for (n = 0; n < (1 << extra_lbits[code]); n++) { + length_code[length++] = code; + } + } + // Assert (length === 256, "ct_init: length !== 256"); + + // Note that the length 255 (match length 258) can be represented + // in two different ways: code 284 + 5 bits or code 285, so we + // overwrite length_code[255] to use the best encoding: + length_code[length - 1] = code; + + // Initialize the mapping dist (0..32K) -> dist code (0..29) */ + dist = 0; + for (code = 0; code < 16; code++) { + base_dist[code] = dist; + for (n = 0; n < (1 << extra_dbits[code]); n++) { + dist_code[dist++] = code; + } + } + // Assert (dist === 256, "ct_init: dist !== 256"); + // from now on, all distances are divided by 128 + for (dist >>= 7; code < D_CODES; code++) { + base_dist[code] = dist << 7; + for (n = 0; n < (1 << (extra_dbits[code] - 7)); n++) { + dist_code[256 + dist++] = code; + } + } + // Assert (dist === 256, "ct_init: 256+dist !== 512"); + + // Construct the codes of the static literal tree + for (bits = 0; bits <= MAX_BITS; bits++) { + bl_count[bits] = 0; + } + n = 0; + while (n <= 143) { + static_ltree[n++].dl = 8; + bl_count[8]++; + } + while (n <= 255) { + static_ltree[n++].dl = 9; + bl_count[9]++; + } + while (n <= 279) { + static_ltree[n++].dl = 7; + bl_count[7]++; + } + while (n <= 287) { + static_ltree[n++].dl = 8; + bl_count[8]++; + } + // Codes 286 and 287 do not exist, but we must include them in the + // tree construction to get a canonical Huffman tree (longest code + // all ones) + gen_codes(static_ltree, L_CODES + 1); + + // The static distance tree is trivial: */ + for (n = 0; n < D_CODES; n++) { + static_dtree[n].dl = 5; + static_dtree[n].fc = bi_reverse(n, 5); + } + + // Initialize the first block of the first file: + init_block(); +} + +/* ========================================================================== +* Initialize a new block. +*/ +function init_block() { + var n; // iterates over tree elements + + // Initialize the trees. + for (n = 0; n < L_CODES; n++) { + dyn_ltree[n].fc = 0; + } + for (n = 0; n < D_CODES; n++) { + dyn_dtree[n].fc = 0; + } + for (n = 0; n < BL_CODES; n++) { + bl_tree[n].fc = 0; + } + + dyn_ltree[END_BLOCK].fc = 1; + opt_len = static_len = 0; + last_lit = last_dist = last_flags = 0; + flags = 0; + flag_bit = 1; +} + +/* ========================================================================== +* Restore the heap property by moving down the tree starting at node k, +* exchanging a node with the smallest of its two sons if necessary, stopping +* when the heap property is re-established (each father smaller than its +* two sons). +* +* @param tree- tree to restore +* @param k- node to move down +*/ +function pqdownheap(tree, k) { + var v = heap[k], + j = k << 1; // left son of k + + while (j <= heap_len) { + // Set j to the smallest of the two sons: + if (j < heap_len && SMALLER(tree, heap[j + 1], heap[j])) { + j++; + } + + // Exit if v is smaller than both sons + if (SMALLER(tree, v, heap[j])) { + break; + } + + // Exchange v with the smallest son + heap[k] = heap[j]; + k = j; + + // And continue down the tree, setting j to the left son of k + j <<= 1; + } + heap[k] = v; +} + +/* ========================================================================== +* Compute the optimal bit lengths for a tree and update the total bit length +* for the current block. +* IN assertion: the fields freq and dad are set, heap[heap_max] and +* above are the tree nodes sorted by increasing frequency. +* OUT assertions: the field len is set to the optimal bit length, the +* array bl_count contains the frequencies for each bit length. +* The length opt_len is updated; static_len is also updated if stree is +* not null. +*/ +function gen_bitlen(desc) { // the tree descriptor + var tree = desc.dyn_tree; + var extra = desc.extra_bits; + var base = desc.extra_base; + var max_code = desc.max_code; + var max_length = desc.max_length; + var stree = desc.static_tree; + var h; // heap index + var n, m; // iterate over the tree elements + var bits; // bit length + var xbits; // extra bits + var f; // frequency + var overflow = 0; // number of elements with bit length too large + + for (bits = 0; bits <= MAX_BITS; bits++) { + bl_count[bits] = 0; + } + + // In a first pass, compute the optimal bit lengths (which may + // overflow in the case of the bit length tree). + tree[heap[heap_max]].dl = 0; // root of the heap + + for (h = heap_max + 1; h < HEAP_SIZE; h++) { + n = heap[h]; + bits = tree[tree[n].dl].dl + 1; + if (bits > max_length) { + bits = max_length; + overflow++; + } + tree[n].dl = bits; + // We overwrite tree[n].dl which is no longer needed + + if (n > max_code) { + continue; // not a leaf node + } + + bl_count[bits]++; + xbits = 0; + if (n >= base) { + xbits = extra[n - base]; + } + f = tree[n].fc; + opt_len += f * (bits + xbits); + if (stree !== null) { + static_len += f * (stree[n].dl + xbits); + } + } + if (overflow === 0) { + return; + } + + // This happens for example on obj2 and pic of the Calgary corpus + + // Find the first bit length which could increase: + do { + bits = max_length - 1; + while (bl_count[bits] === 0) { + bits--; + } + bl_count[bits]--; // move one leaf down the tree + bl_count[bits + 1] += 2; // move one overflow item as its brother + bl_count[max_length]--; + // The brother of the overflow item also moves one step up, + // but this does not affect bl_count[max_length] + overflow -= 2; + } while (overflow > 0); + + // Now recompute all bit lengths, scanning in increasing frequency. + // h is still equal to HEAP_SIZE. (It is simpler to reconstruct all + // lengths instead of fixing only the wrong ones. This idea is taken + // from 'ar' written by Haruhiko Okumura.) + for (bits = max_length; bits !== 0; bits--) { + n = bl_count[bits]; + while (n !== 0) { + m = heap[--h]; + if (m > max_code) { + continue; + } + if (tree[m].dl !== bits) { + opt_len += (bits - tree[m].dl) * tree[m].fc; + tree[m].fc = bits; + } + n--; + } + } +} + +/* ========================================================================== +* Generate the codes for a given tree and bit counts (which need not be +* optimal). +* IN assertion: the array bl_count contains the bit length statistics for +* the given tree and the field len is set for all tree elements. +* OUT assertion: the field code is set for all tree elements of non +* zero code length. +* @param tree- the tree to decorate +* @param max_code- largest code with non-zero frequency +*/ +function gen_codes(tree, max_code) { + var next_code = []; // new Array(MAX_BITS + 1); // next code value for each bit length + var code = 0; // running code value + var bits; // bit index + var n; // code index + + // The distribution counts are first used to generate the code values + // without bit reversal. + for (bits = 1; bits <= MAX_BITS; bits++) { + code = ((code + bl_count[bits - 1]) << 1); + next_code[bits] = code; + } + + // Check that the bit counts in bl_count are consistent. The last code + // must be all ones. + // Assert (code + encoder->bl_count[MAX_BITS]-1 === (1<> 1; n >= 1; n--) { + pqdownheap(tree, n); + } + + // Construct the Huffman tree by repeatedly combining the least two + // frequent nodes. + do { + n = heap[SMALLEST]; + heap[SMALLEST] = heap[heap_len--]; + pqdownheap(tree, SMALLEST); + + m = heap[SMALLEST]; // m = node of next least frequency + + // keep the nodes sorted by frequency + heap[--heap_max] = n; + heap[--heap_max] = m; + + // Create a new node father of n and m + tree[node].fc = tree[n].fc + tree[m].fc; + // depth[node] = (char)(MAX(depth[n], depth[m]) + 1); + if (depth[n] > depth[m] + 1) { + depth[node] = depth[n]; + } else { + depth[node] = depth[m] + 1; + } + tree[n].dl = tree[m].dl = node; + + // and insert the new node in the heap + heap[SMALLEST] = node++; + pqdownheap(tree, SMALLEST); + + } while (heap_len >= 2); + + heap[--heap_max] = heap[SMALLEST]; + + // At this point, the fields freq and dad are set. We can now + // generate the bit lengths. + gen_bitlen(desc); + + // The field len is now set, we can generate the bit codes + gen_codes(tree, max_code); +} + +/* ========================================================================== +* Scan a literal or distance tree to determine the frequencies of the codes +* in the bit length tree. Updates opt_len to take into account the repeat +* counts. (The contribution of the bit length codes will be added later +* during the construction of bl_tree.) +* +* @param tree- the tree to be scanned +* @param max_code- and its largest code of non zero frequency +*/ +function scan_tree(tree, max_code) { + var n, // iterates over all tree elements + prevlen = -1, // last emitted length + curlen, // length of current code + nextlen = tree[0].dl, // length of next code + count = 0, // repeat count of the current code + max_count = 7, // max repeat count + min_count = 4; // min repeat count + + if (nextlen === 0) { + max_count = 138; + min_count = 3; + } + tree[max_code + 1].dl = 0xffff; // guard + + for (n = 0; n <= max_code; n++) { + curlen = nextlen; + nextlen = tree[n + 1].dl; + if (++count < max_count && curlen === nextlen) { + continue; + } else if (count < min_count) { + bl_tree[curlen].fc += count; + } else if (curlen !== 0) { + if (curlen !== prevlen) { + bl_tree[curlen].fc++; + } + bl_tree[REP_3_6].fc++; + } else if (count <= 10) { + bl_tree[REPZ_3_10].fc++; + } else { + bl_tree[REPZ_11_138].fc++; + } + count = 0; prevlen = curlen; + if (nextlen === 0) { + max_count = 138; + min_count = 3; + } else if (curlen === nextlen) { + max_count = 6; + min_count = 3; + } else { + max_count = 7; + min_count = 4; + } + } +} + +/* ========================================================================== +* Send a literal or distance tree in compressed form, using the codes in +* bl_tree. +* +* @param tree- the tree to be scanned +* @param max_code- and its largest code of non zero frequency +*/ +function send_tree(tree, max_code) { + var n; // iterates over all tree elements + var prevlen = -1; // last emitted length + var curlen; // length of current code + var nextlen = tree[0].dl; // length of next code + var count = 0; // repeat count of the current code + var max_count = 7; // max repeat count + var min_count = 4; // min repeat count + + // tree[max_code+1].dl = -1; */ /* guard already set */ + if (nextlen === 0) { + max_count = 138; + min_count = 3; + } + + for (n = 0; n <= max_code; n++) { + curlen = nextlen; + nextlen = tree[n + 1].dl; + if (++count < max_count && curlen === nextlen) { + continue; + } else if (count < min_count) { + do { + SEND_CODE(curlen, bl_tree); + } while (--count !== 0); + } else if (curlen !== 0) { + if (curlen !== prevlen) { + SEND_CODE(curlen, bl_tree); + count--; + } + // Assert(count >= 3 && count <= 6, " 3_6?"); + SEND_CODE(REP_3_6, bl_tree); + send_bits(count - 3, 2); + } else if (count <= 10) { + SEND_CODE(REPZ_3_10, bl_tree); + send_bits(count - 3, 3); + } else { + SEND_CODE(REPZ_11_138, bl_tree); + send_bits(count - 11, 7); + } + count = 0; + prevlen = curlen; + if (nextlen === 0) { + max_count = 138; + min_count = 3; + } else if (curlen === nextlen) { + max_count = 6; + min_count = 3; + } else { + max_count = 7; + min_count = 4; + } + } +} + +/* ========================================================================== +* Construct the Huffman tree for the bit lengths and return the index in +* bl_order of the last bit length code to send. +*/ +function build_bl_tree() { + var max_blindex; // index of last bit length code of non zero freq + + // Determine the bit length frequencies for literal and distance trees + scan_tree(dyn_ltree, l_desc.max_code); + scan_tree(dyn_dtree, d_desc.max_code); + + // Build the bit length tree: + build_tree(bl_desc); + // opt_len now includes the length of the tree representations, except + // the lengths of the bit lengths codes and the 5+5+4 bits for the counts. + + // Determine the number of bit length codes to send. The pkzip format + // requires that at least 4 bit length codes be sent. (appnote.txt says + // 3 but the actual value used is 4.) + for (max_blindex = BL_CODES - 1; max_blindex >= 3; max_blindex--) { + if (bl_tree[bl_order[max_blindex]].dl !== 0) { + break; + } + } + // Update opt_len to include the bit length tree and counts */ + opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4; + // Tracev((stderr, "\ndyn trees: dyn %ld, stat %ld", + // encoder->opt_len, encoder->static_len)); + + return max_blindex; +} + +/* ========================================================================== +* Send the header for a block using dynamic Huffman trees: the counts, the +* lengths of the bit length codes, the literal tree and the distance tree. +* IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4. +*/ +function send_all_trees(lcodes, dcodes, blcodes) { // number of codes for each tree + var rank; // index in bl_order + + // Assert (lcodes >= 257 && dcodes >= 1 && blcodes >= 4, "not enough codes"); + // Assert (lcodes <= L_CODES && dcodes <= D_CODES && blcodes <= BL_CODES, "too many codes"); + // Tracev((stderr, "\nbl counts: ")); + send_bits(lcodes - 257, 5); // not +255 as stated in appnote.txt + send_bits(dcodes - 1, 5); + send_bits(blcodes - 4, 4); // not -3 as stated in appnote.txt + for (rank = 0; rank < blcodes; rank++) { + // Tracev((stderr, "\nbl code %2d ", bl_order[rank])); + send_bits(bl_tree[bl_order[rank]].dl, 3); + } + + // send the literal tree + send_tree(dyn_ltree, lcodes - 1); + + // send the distance tree + send_tree(dyn_dtree, dcodes - 1); +} + +/* ========================================================================== +* Determine the best encoding for the current block: dynamic trees, static +* trees or store, and output the encoded block to the zip file. +*/ +function flush_block(eof) { // true if this is the last block for a file + var opt_lenb, static_lenb, // opt_len and static_len in bytes + max_blindex, // index of last bit length code of non zero freq + stored_len, // length of input block + i; + + stored_len = strstart - block_start; + flag_buf[last_flags] = flags; // Save the flags for the last 8 items + + // Construct the literal and distance trees + build_tree(l_desc); + // Tracev((stderr, "\nlit data: dyn %ld, stat %ld", + // encoder->opt_len, encoder->static_len)); + + build_tree(d_desc); + // Tracev((stderr, "\ndist data: dyn %ld, stat %ld", + // encoder->opt_len, encoder->static_len)); + // At this point, opt_len and static_len are the total bit lengths of + // the compressed block data, excluding the tree representations. + + // Build the bit length tree for the above two trees, and get the index + // in bl_order of the last bit length code to send. + max_blindex = build_bl_tree(); + + // Determine the best encoding. Compute first the block length in bytes + opt_lenb = (opt_len + 3 + 7) >> 3; + static_lenb = (static_len + 3 + 7) >> 3; + + // Trace((stderr, "\nopt %lu(%lu) stat %lu(%lu) stored %lu lit %u dist %u ", opt_lenb, encoder->opt_len, static_lenb, encoder->static_len, stored_len, encoder->last_lit, encoder->last_dist)); + + if (static_lenb <= opt_lenb) { + opt_lenb = static_lenb; + } + if (stored_len + 4 <= opt_lenb && block_start >= 0) { // 4: two words for the lengths + // The test buf !== NULL is only necessary if LIT_BUFSIZE > WSIZE. + // Otherwise we can't have processed more than WSIZE input bytes since + // the last block flush, because compression would have been + // successful. If LIT_BUFSIZE <= WSIZE, it is never too late to + // transform a block into a stored block. + send_bits((STORED_BLOCK << 1) + eof, 3); /* send block type */ + bi_windup(); /* align on byte boundary */ + put_short(stored_len); + put_short(~stored_len); + + // copy block + /* + p = &window[block_start]; + for (i = 0; i < stored_len; i++) { + put_byte(p[i]); + } + */ + for (i = 0; i < stored_len; i++) { + put_byte(window[block_start + i]); + } + } else if (static_lenb === opt_lenb) { + send_bits((STATIC_TREES << 1) + eof, 3); + compress_block(static_ltree, static_dtree); + } else { + send_bits((DYN_TREES << 1) + eof, 3); + send_all_trees(l_desc.max_code + 1, d_desc.max_code + 1, max_blindex + 1); + compress_block(dyn_ltree, dyn_dtree); + } + + init_block(); + + if (eof !== 0) { + bi_windup(); + } +} + +/* ========================================================================== +* Save the match info and tally the frequency counts. Return true if +* the current block must be flushed. +* +* @param dist- distance of matched string +* @param lc- (match length - MIN_MATCH) or unmatched char (if dist === 0) +*/ +function ct_tally(dist, lc) { + l_buf[last_lit++] = lc; + if (dist === 0) { + // lc is the unmatched char + dyn_ltree[lc].fc++; + } else { + // Here, lc is the match length - MIN_MATCH + dist--; // dist = match distance - 1 + // Assert((ush)dist < (ush)MAX_DIST && (ush)lc <= (ush)(MAX_MATCH-MIN_MATCH) && (ush)D_CODE(dist) < (ush)D_CODES, "ct_tally: bad match"); + + dyn_ltree[length_code[lc] + LITERALS + 1].fc++; + dyn_dtree[D_CODE(dist)].fc++; + + d_buf[last_dist++] = dist; + flags |= flag_bit; + } + flag_bit <<= 1; + + // Output the flags if they fill a byte + if ((last_lit & 7) === 0) { + flag_buf[last_flags++] = flags; + flags = 0; + flag_bit = 1; + } + // Try to guess if it is profitable to stop the current block here + if (compr_level > 2 && (last_lit & 0xfff) === 0) { + // Compute an upper bound for the compressed length + var out_length = last_lit * 8; + var in_length = strstart - block_start; + var dcode; + + for (dcode = 0; dcode < D_CODES; dcode++) { + out_length += dyn_dtree[dcode].fc * (5 + extra_dbits[dcode]); + } + out_length >>= 3; + // Trace((stderr,"\nlast_lit %u, last_dist %u, in %ld, out ~%ld(%ld%%) ", encoder->last_lit, encoder->last_dist, in_length, out_length, 100L - out_length*100L/in_length)); + if (last_dist < parseInt(last_lit / 2, 10) && out_length < parseInt(in_length / 2, 10)) { + return true; + } + } + return (last_lit === LIT_BUFSIZE - 1 || last_dist === DIST_BUFSIZE); + // We avoid equality with LIT_BUFSIZE because of wraparound at 64K + // on 16 bit machines and because stored blocks are restricted to + // 64K-1 bytes. +} + +/* ========================================================================== +* Send the block data compressed using the given Huffman trees +* +* @param ltree- literal tree +* @param dtree- distance tree +*/ +function compress_block(ltree, dtree) { + var dist; // distance of matched string + var lc; // match length or unmatched char (if dist === 0) + var lx = 0; // running index in l_buf + var dx = 0; // running index in d_buf + var fx = 0; // running index in flag_buf + var flag = 0; // current flags + var code; // the code to send + var extra; // number of extra bits to send + + if (last_lit !== 0) { + do { + if ((lx & 7) === 0) { + flag = flag_buf[fx++]; + } + lc = l_buf[lx++] & 0xff; + if ((flag & 1) === 0) { + SEND_CODE(lc, ltree); /* send a literal byte */ + // Tracecv(isgraph(lc), (stderr," '%c' ", lc)); + } else { + // Here, lc is the match length - MIN_MATCH + code = length_code[lc]; + SEND_CODE(code + LITERALS + 1, ltree); // send the length code + extra = extra_lbits[code]; + if (extra !== 0) { + lc -= base_length[code]; + send_bits(lc, extra); // send the extra length bits + } + dist = d_buf[dx++]; + // Here, dist is the match distance - 1 + code = D_CODE(dist); + // Assert (code < D_CODES, "bad d_code"); + + SEND_CODE(code, dtree); // send the distance code + extra = extra_dbits[code]; + if (extra !== 0) { + dist -= base_dist[code]; + send_bits(dist, extra); // send the extra distance bits + } + } // literal or match pair ? + flag >>= 1; + } while (lx < last_lit); + } + + SEND_CODE(END_BLOCK, ltree); +} + +/* ========================================================================== +* Send a value on a given number of bits. +* IN assertion: length <= 16 and value fits in length bits. +* +* @param value- value to send +* @param length- number of bits +*/ +var Buf_size = 16; // bit size of bi_buf +function send_bits(value, length) { + // If not enough room in bi_buf, use (valid) bits from bi_buf and + // (16 - bi_valid) bits from value, leaving (width - (16-bi_valid)) + // unused bits in value. + if (bi_valid > Buf_size - length) { + bi_buf |= (value << bi_valid); + put_short(bi_buf); + bi_buf = (value >> (Buf_size - bi_valid)); + bi_valid += length - Buf_size; + } else { + bi_buf |= value << bi_valid; + bi_valid += length; + } +} + +/* ========================================================================== +* Reverse the first len bits of a code, using straightforward code (a faster +* method would use a table) +* IN assertion: 1 <= len <= 15 +* +* @param code- the value to invert +* @param len- its bit length +*/ +function bi_reverse(code, len) { + var res = 0; + do { + res |= code & 1; + code >>= 1; + res <<= 1; + } while (--len > 0); + return res >> 1; +} + +/* ========================================================================== +* Write out any remaining bits in an incomplete byte. +*/ +function bi_windup() { + if (bi_valid > 8) { + put_short(bi_buf); + } else if (bi_valid > 0) { + put_byte(bi_buf); + } + bi_buf = 0; + bi_valid = 0; +} + +function qoutbuf() { + var q, i; + if (outcnt !== 0) { + q = new_queue(); + if (qhead === null) { + qhead = qtail = q; + } else { + qtail = qtail.next = q; + } + q.len = outcnt - outoff; + // System.arraycopy(outbuf, outoff, q.ptr, 0, q.len); + for (i = 0; i < q.len; i++) { + q.ptr[i] = outbuf[outoff + i]; + } + outcnt = outoff = 0; + } +} + +function deflate(arr, level) { + var i, j, buff; + + deflate_data = arr; + deflate_pos = 0; + if (typeof level === "undefined") { + level = DEFAULT_LEVEL; + } + deflate_start(level); + + buff = []; + + do { + i = deflate_internal(buff, buff.length, 1024); + } while (i > 0); + + deflate_data = null; // G.C. + return buff; +} + +module.exports = deflate; +module.exports.DEFAULT_LEVEL = DEFAULT_LEVEL; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_prism/beatgammit-inflate.js b/local-scratch-vm/src/extensions/jg_prism/beatgammit-inflate.js new file mode 100644 index 0000000000000000000000000000000000000000..494e862d7b0a7a0036e918aee1f2c350d0c41d15 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_prism/beatgammit-inflate.js @@ -0,0 +1,794 @@ +/* constant parameters */ +var WSIZE = 32768, // Sliding Window size + STORED_BLOCK = 0, + STATIC_TREES = 1, + DYN_TREES = 2, + + /* for inflate */ + lbits = 9, // bits in base literal/length lookup table + dbits = 6, // bits in base distance lookup table + + /* variables (inflate) */ + slide, + wp, // current position in slide + fixed_tl = null, // inflate static + fixed_td, // inflate static + fixed_bl, // inflate static + fixed_bd, // inflate static + bit_buf, // bit buffer + bit_len, // bits in bit buffer + method, + eof, + copy_leng, + copy_dist, + tl, // literal length decoder table + td, // literal distance decoder table + bl, // number of bits decoded by tl + bd, // number of bits decoded by td + + inflate_data, + inflate_pos, + + + /* constant tables (inflate) */ + MASK_BITS = [ + 0x0000, + 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff, + 0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff + ], + // Tables for deflate from PKZIP's appnote.txt. + // Copy lengths for literal codes 257..285 + cplens = [ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 + ], + /* note: see note #13 above about the 258 in this list. */ + // Extra bits for literal codes 257..285 + cplext = [ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 99, 99 // 99==invalid + ], + // Copy offsets for distance codes 0..29 + cpdist = [ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577 + ], + // Extra bits for distance codes + cpdext = [ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13 + ], + // Order of the bit length code lengths + border = [ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 + ]; +/* objects (inflate) */ + +function HuftList() { + this.next = null; + this.list = null; +} + +function HuftNode() { + this.e = 0; // number of extra bits or operation + this.b = 0; // number of bits in this code or subcode + + // union + this.n = 0; // literal, length base, or distance base + this.t = null; // (HuftNode) pointer to next level of table +} + +/* +* @param b- code lengths in bits (all assumed <= BMAX) +* @param n- number of codes (assumed <= N_MAX) +* @param s- number of simple-valued codes (0..s-1) +* @param d- list of base values for non-simple codes +* @param e- list of extra bits for non-simple codes +* @param mm- maximum lookup bits +*/ +function HuftBuild(b, n, s, d, e, mm) { + this.BMAX = 16; // maximum bit length of any code + this.N_MAX = 288; // maximum number of codes in any set + this.status = 0; // 0: success, 1: incomplete table, 2: bad input + this.root = null; // (HuftList) starting table + this.m = 0; // maximum lookup bits, returns actual + + /* Given a list of code lengths and a maximum table size, make a set of + tables to decode that set of codes. Return zero on success, one if + the given code set is incomplete (the tables are still built in this + case), two if the input is invalid (all zero length codes or an + oversubscribed set of lengths), and three if not enough memory. + The code with value 256 is special, and the tables are constructed + so that no bits beyond that code are fetched when that code is + decoded. */ + var a; // counter for codes of length k + var c = []; + var el; // length of EOB code (value 256) + var f; // i repeats in table every f entries + var g; // maximum code length + var h; // table level + var i; // counter, current code + var j; // counter + var k; // number of bits in current code + var lx = []; + var p; // pointer into c[], b[], or v[] + var pidx; // index of p + var q; // (HuftNode) points to current table + var r = new HuftNode(); // table entry for structure assignment + var u = []; + var v = []; + var w; + var x = []; + var xp; // pointer into x or c + var y; // number of dummy codes added + var z; // number of entries in current table + var o; + var tail; // (HuftList) + + tail = this.root = null; + + // bit length count table + for (i = 0; i < this.BMAX + 1; i++) { + c[i] = 0; + } + // stack of bits per table + for (i = 0; i < this.BMAX + 1; i++) { + lx[i] = 0; + } + // HuftNode[BMAX][] table stack + for (i = 0; i < this.BMAX; i++) { + u[i] = null; + } + // values in order of bit length + for (i = 0; i < this.N_MAX; i++) { + v[i] = 0; + } + // bit offsets, then code stack + for (i = 0; i < this.BMAX + 1; i++) { + x[i] = 0; + } + + // Generate counts for each bit length + el = n > 256 ? b[256] : this.BMAX; // set length of EOB code, if any + p = b; pidx = 0; + i = n; + do { + c[p[pidx]]++; // assume all entries <= BMAX + pidx++; + } while (--i > 0); + if (c[0] === n) { // null input--all zero length codes + this.root = null; + this.m = 0; + this.status = 0; + return; + } + + // Find minimum and maximum length, bound *m by those + for (j = 1; j <= this.BMAX; j++) { + if (c[j] !== 0) { + break; + } + } + k = j; // minimum code length + if (mm < j) { + mm = j; + } + for (i = this.BMAX; i !== 0; i--) { + if (c[i] !== 0) { + break; + } + } + g = i; // maximum code length + if (mm > i) { + mm = i; + } + + // Adjust last length count to fill out codes, if needed + for (y = 1 << j; j < i; j++, y <<= 1) { + if ((y -= c[j]) < 0) { + this.status = 2; // bad input: more codes than bits + this.m = mm; + return; + } + } + if ((y -= c[i]) < 0) { + this.status = 2; + this.m = mm; + return; + } + c[i] += y; + + // Generate starting offsets into the value table for each length + x[1] = j = 0; + p = c; + pidx = 1; + xp = 2; + while (--i > 0) { // note that i == g from above + x[xp++] = (j += p[pidx++]); + } + + // Make a table of values in order of bit lengths + p = b; pidx = 0; + i = 0; + do { + if ((j = p[pidx++]) !== 0) { + v[x[j]++] = i; + } + } while (++i < n); + n = x[g]; // set n to length of v + + // Generate the Huffman codes and for each, make the table entries + x[0] = i = 0; // first Huffman code is zero + p = v; pidx = 0; // grab values in bit order + h = -1; // no tables yet--level -1 + w = lx[0] = 0; // no bits decoded yet + q = null; // ditto + z = 0; // ditto + + // go through the bit lengths (k already is bits in shortest code) + for (null; k <= g; k++) { + a = c[k]; + while (a-- > 0) { + // here i is the Huffman code of length k bits for value p[pidx] + // make tables up to required level + while (k > w + lx[1 + h]) { + w += lx[1 + h]; // add bits already decoded + h++; + + // compute minimum size table less than or equal to *m bits + z = (z = g - w) > mm ? mm : z; // upper limit + if ((f = 1 << (j = k - w)) > a + 1) { // try a k-w bit table + // too few codes for k-w bit table + f -= a + 1; // deduct codes from patterns left + xp = k; + while (++j < z) { // try smaller tables up to z bits + if ((f <<= 1) <= c[++xp]) { + break; // enough codes to use up j bits + } + f -= c[xp]; // else deduct codes from patterns + } + } + if (w + j > el && w < el) { + j = el - w; // make EOB code end at table + } + z = 1 << j; // table entries for j-bit table + lx[1 + h] = j; // set table size in stack + + // allocate and link in new table + q = []; + for (o = 0; o < z; o++) { + q[o] = new HuftNode(); + } + + if (!tail) { + tail = this.root = new HuftList(); + } else { + tail = tail.next = new HuftList(); + } + tail.next = null; + tail.list = q; + u[h] = q; // table starts after link + + /* connect to last table, if there is one */ + if (h > 0) { + x[h] = i; // save pattern for backing up + r.b = lx[h]; // bits to dump before this table + r.e = 16 + j; // bits in this table + r.t = q; // pointer to this table + j = (i & ((1 << w) - 1)) >> (w - lx[h]); + u[h - 1][j].e = r.e; + u[h - 1][j].b = r.b; + u[h - 1][j].n = r.n; + u[h - 1][j].t = r.t; + } + } + + // set up table entry in r + r.b = k - w; + if (pidx >= n) { + r.e = 99; // out of values--invalid code + } else if (p[pidx] < s) { + r.e = (p[pidx] < 256 ? 16 : 15); // 256 is end-of-block code + r.n = p[pidx++]; // simple code is just the value + } else { + r.e = e[p[pidx] - s]; // non-simple--look up in lists + r.n = d[p[pidx++] - s]; + } + + // fill code-like entries with r // + f = 1 << (k - w); + for (j = i >> w; j < z; j += f) { + q[j].e = r.e; + q[j].b = r.b; + q[j].n = r.n; + q[j].t = r.t; + } + + // backwards increment the k-bit code i + for (j = 1 << (k - 1); (i & j) !== 0; j >>= 1) { + i ^= j; + } + i ^= j; + + // backup over finished tables + while ((i & ((1 << w) - 1)) !== x[h]) { + w -= lx[h]; // don't need to update q + h--; + } + } + } + + /* return actual size of base table */ + this.m = lx[1]; + + /* Return true (1) if we were given an incomplete table */ + this.status = ((y !== 0 && g !== 1) ? 1 : 0); +} + + +/* routines (inflate) */ + +function GET_BYTE() { + if (inflate_data.length === inflate_pos) { + return -1; + } + return inflate_data[inflate_pos++] & 0xff; +} + +function NEEDBITS(n) { + while (bit_len < n) { + bit_buf |= GET_BYTE() << bit_len; + bit_len += 8; + } +} + +function GETBITS(n) { + return bit_buf & MASK_BITS[n]; +} + +function DUMPBITS(n) { + bit_buf >>= n; + bit_len -= n; +} + +function inflate_codes(buff, off, size) { + // inflate (decompress) the codes in a deflated (compressed) block. + // Return an error code or zero if it all goes ok. + var e; // table entry flag/number of extra bits + var t; // (HuftNode) pointer to table entry + var n; + + if (size === 0) { + return 0; + } + + // inflate the coded data + n = 0; + for (; ;) { // do until end of block + NEEDBITS(bl); + t = tl.list[GETBITS(bl)]; + e = t.e; + while (e > 16) { + if (e === 99) { + return -1; + } + DUMPBITS(t.b); + e -= 16; + NEEDBITS(e); + t = t.t[GETBITS(e)]; + e = t.e; + } + DUMPBITS(t.b); + + if (e === 16) { // then it's a literal + wp &= WSIZE - 1; + buff[off + n++] = slide[wp++] = t.n; + if (n === size) { + return size; + } + continue; + } + + // exit if end of block + if (e === 15) { + break; + } + + // it's an EOB or a length + + // get length of block to copy + NEEDBITS(e); + copy_leng = t.n + GETBITS(e); + DUMPBITS(e); + + // decode distance of block to copy + NEEDBITS(bd); + t = td.list[GETBITS(bd)]; + e = t.e; + + while (e > 16) { + if (e === 99) { + return -1; + } + DUMPBITS(t.b); + e -= 16; + NEEDBITS(e); + t = t.t[GETBITS(e)]; + e = t.e; + } + DUMPBITS(t.b); + NEEDBITS(e); + copy_dist = wp - t.n - GETBITS(e); + DUMPBITS(e); + + // do the copy + while (copy_leng > 0 && n < size) { + copy_leng--; + copy_dist &= WSIZE - 1; + wp &= WSIZE - 1; + buff[off + n++] = slide[wp++] = slide[copy_dist++]; + } + + if (n === size) { + return size; + } + } + + method = -1; // done + return n; +} + +function inflate_stored(buff, off, size) { + /* "decompress" an inflated type 0 (stored) block. */ + var n; + + // go to byte boundary + n = bit_len & 7; + DUMPBITS(n); + + // get the length and its complement + NEEDBITS(16); + n = GETBITS(16); + DUMPBITS(16); + NEEDBITS(16); + if (n !== ((~bit_buf) & 0xffff)) { + return -1; // error in compressed data + } + DUMPBITS(16); + + // read and output the compressed data + copy_leng = n; + + n = 0; + while (copy_leng > 0 && n < size) { + copy_leng--; + wp &= WSIZE - 1; + NEEDBITS(8); + buff[off + n++] = slide[wp++] = GETBITS(8); + DUMPBITS(8); + } + + if (copy_leng === 0) { + method = -1; // done + } + return n; +} + +function inflate_fixed(buff, off, size) { + // decompress an inflated type 1 (fixed Huffman codes) block. We should + // either replace this with a custom decoder, or at least precompute the + // Huffman tables. + + // if first time, set up tables for fixed blocks + if (!fixed_tl) { + var i; // temporary variable + var l = []; // 288 length list for huft_build (initialized below) + var h; // HuftBuild + + // literal table + for (i = 0; i < 144; i++) { + l[i] = 8; + } + for (null; i < 256; i++) { + l[i] = 9; + } + for (null; i < 280; i++) { + l[i] = 7; + } + for (null; i < 288; i++) { // make a complete, but wrong code set + l[i] = 8; + } + fixed_bl = 7; + + h = new HuftBuild(l, 288, 257, cplens, cplext, fixed_bl); + if (h.status !== 0) { + console.error("HufBuild error: " + h.status); + return -1; + } + fixed_tl = h.root; + fixed_bl = h.m; + + // distance table + for (i = 0; i < 30; i++) { // make an incomplete code set + l[i] = 5; + } + fixed_bd = 5; + + h = new HuftBuild(l, 30, 0, cpdist, cpdext, fixed_bd); + if (h.status > 1) { + fixed_tl = null; + console.error("HufBuild error: " + h.status); + return -1; + } + fixed_td = h.root; + fixed_bd = h.m; + } + + tl = fixed_tl; + td = fixed_td; + bl = fixed_bl; + bd = fixed_bd; + return inflate_codes(buff, off, size); +} + +function inflate_dynamic(buff, off, size) { + // decompress an inflated type 2 (dynamic Huffman codes) block. + var i; // temporary variables + var j; + var l; // last length + var n; // number of lengths to get + var t; // (HuftNode) literal/length code table + var nb; // number of bit length codes + var nl; // number of literal/length codes + var nd; // number of distance codes + var ll = []; + var h; // (HuftBuild) + + // literal/length and distance code lengths + for (i = 0; i < 286 + 30; i++) { + ll[i] = 0; + } + + // read in table lengths + NEEDBITS(5); + nl = 257 + GETBITS(5); // number of literal/length codes + DUMPBITS(5); + NEEDBITS(5); + nd = 1 + GETBITS(5); // number of distance codes + DUMPBITS(5); + NEEDBITS(4); + nb = 4 + GETBITS(4); // number of bit length codes + DUMPBITS(4); + if (nl > 286 || nd > 30) { + return -1; // bad lengths + } + + // read in bit-length-code lengths + for (j = 0; j < nb; j++) { + NEEDBITS(3); + ll[border[j]] = GETBITS(3); + DUMPBITS(3); + } + for (null; j < 19; j++) { + ll[border[j]] = 0; + } + + // build decoding table for trees--single level, 7 bit lookup + bl = 7; + h = new HuftBuild(ll, 19, 19, null, null, bl); + if (h.status !== 0) { + return -1; // incomplete code set + } + + tl = h.root; + bl = h.m; + + // read in literal and distance code lengths + n = nl + nd; + i = l = 0; + while (i < n) { + NEEDBITS(bl); + t = tl.list[GETBITS(bl)]; + j = t.b; + DUMPBITS(j); + j = t.n; + if (j < 16) { // length of code in bits (0..15) + ll[i++] = l = j; // save last length in l + } else if (j === 16) { // repeat last length 3 to 6 times + NEEDBITS(2); + j = 3 + GETBITS(2); + DUMPBITS(2); + if (i + j > n) { + return -1; + } + while (j-- > 0) { + ll[i++] = l; + } + } else if (j === 17) { // 3 to 10 zero length codes + NEEDBITS(3); + j = 3 + GETBITS(3); + DUMPBITS(3); + if (i + j > n) { + return -1; + } + while (j-- > 0) { + ll[i++] = 0; + } + l = 0; + } else { // j === 18: 11 to 138 zero length codes + NEEDBITS(7); + j = 11 + GETBITS(7); + DUMPBITS(7); + if (i + j > n) { + return -1; + } + while (j-- > 0) { + ll[i++] = 0; + } + l = 0; + } + } + + // build the decoding tables for literal/length and distance codes + bl = lbits; + h = new HuftBuild(ll, nl, 257, cplens, cplext, bl); + if (bl === 0) { // no literals or lengths + h.status = 1; + } + if (h.status !== 0) { + if (h.status !== 1) { + return -1; // incomplete code set + } + // **incomplete literal tree** + } + tl = h.root; + bl = h.m; + + for (i = 0; i < nd; i++) { + ll[i] = ll[i + nl]; + } + bd = dbits; + h = new HuftBuild(ll, nd, 0, cpdist, cpdext, bd); + td = h.root; + bd = h.m; + + if (bd === 0 && nl > 257) { // lengths but no distances + // **incomplete distance tree** + return -1; + } + /* + if (h.status === 1) { + // **incomplete distance tree** + } + */ + if (h.status !== 0) { + return -1; + } + + // decompress until an end-of-block code + return inflate_codes(buff, off, size); +} + +function inflate_start() { + if (!slide) { + slide = []; // new Array(2 * WSIZE); // slide.length is never called + } + wp = 0; + bit_buf = 0; + bit_len = 0; + method = -1; + eof = false; + copy_leng = copy_dist = 0; + tl = null; +} + +function inflate_internal(buff, off, size) { + // decompress an inflated entry + var n, i; + + n = 0; + while (n < size) { + if (eof && method === -1) { + return n; + } + + if (copy_leng > 0) { + if (method !== STORED_BLOCK) { + // STATIC_TREES or DYN_TREES + while (copy_leng > 0 && n < size) { + copy_leng--; + copy_dist &= WSIZE - 1; + wp &= WSIZE - 1; + buff[off + n++] = slide[wp++] = slide[copy_dist++]; + } + } else { + while (copy_leng > 0 && n < size) { + copy_leng--; + wp &= WSIZE - 1; + NEEDBITS(8); + buff[off + n++] = slide[wp++] = GETBITS(8); + DUMPBITS(8); + } + if (copy_leng === 0) { + method = -1; // done + } + } + if (n === size) { + return n; + } + } + + if (method === -1) { + if (eof) { + break; + } + + // read in last block bit + NEEDBITS(1); + if (GETBITS(1) !== 0) { + eof = true; + } + DUMPBITS(1); + + // read in block type + NEEDBITS(2); + method = GETBITS(2); + DUMPBITS(2); + tl = null; + copy_leng = 0; + } + + switch (method) { + case STORED_BLOCK: + i = inflate_stored(buff, off + n, size - n); + break; + + case STATIC_TREES: + if (tl) { + i = inflate_codes(buff, off + n, size - n); + } else { + i = inflate_fixed(buff, off + n, size - n); + } + break; + + case DYN_TREES: + if (tl) { + i = inflate_codes(buff, off + n, size - n); + } else { + i = inflate_dynamic(buff, off + n, size - n); + } + break; + + default: // error + i = -1; + break; + } + + if (i === -1) { + if (eof) { + return 0; + } + return -1; + } + n += i; + } + return n; +} + +function inflate(arr) { + var buff = [], i; + + inflate_start(); + inflate_data = arr; + inflate_pos = 0; + + do { + i = inflate_internal(buff, buff.length, 1024); + } while (i > 0); + inflate_data = null; // G.C. + return buff; +} + +module.exports = inflate; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_prism/index.js b/local-scratch-vm/src/extensions/jg_prism/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7a9e774564712886100514f694d8ecade57b6b22 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_prism/index.js @@ -0,0 +1,805 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const ProjectPermissionManager = require('../../util/project-permissions'); +const SandboxRunner = require('../../util/sandboxed-javascript-runner'); +const beatgammit = { + deflate: require('./beatgammit-deflate'), + inflate: require('./beatgammit-inflate') +}; +const { + validateArray +} = require('../../util/json-block-utilities'); +const ArrayBufferUtil = require('../../util/array buffer'); +const BufferParser = new ArrayBufferUtil(); +const Cast = require('../../util/cast'); +// const Cast = require('../../util/cast'); + +const warningIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAC8SURBVDhPpZPBDYMwDEWhJw4MQzdgG0bi0APDlHuPZRv6X2xUaqJWpE8y2Pk/JkRJFVnXtVOMiqdig5yxzm1HJDZu+gWexqcZDCjuqHtcRo/gfTdRkf2yy7kGMG4i/5wlGYSXObqL9MFsRQw06C0voq9ZhxcHasH7m4cV/AUNFkuLWGgwW17EzB5wPB9Wn+aanmoysVGRJAovI5PLydAqzh7l1mWDAUV2JQE8n5P3SORo3xTxOjMWrnNVvQChGZRpEqnWPQAAAABJRU5ErkJggg=="; + +/** + * Class for Prism blocks + * @constructor + */ +class JgPrismBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + this.audioPlayer = new Audio(); + this.isJSPermissionGranted = false; + this.isCameraScreenshotEnabled = false; + + this.mouseScrollDelta = { x: 0, y: 0, z: 0 }; + addEventListener("wheel", e => { + this.mouseScrollDelta.x = e.deltaX; + this.mouseScrollDelta.y = e.deltaY; + this.mouseScrollDelta.z = e.deltaZ; + }); + setInterval(() => { + this.mouseScrollDelta = { x: 0, y: 0, z: 0 }; + }, 65); + + this.encodeCharacterLength = 6; + } + + + /** + * dummy function for reseting user provided permisions when a save is loaded + */ + deserialize() { + this.isJSPermissionGranted = false; + this.isCameraScreenshotEnabled = false; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgPrism', + name: 'Prism', + color1: '#BC7FFF', + color2: '#AD66FF', + blocks: [ + { + opcode: 'playAudioFromUrl', + text: formatMessage({ + id: 'jgPrism.blocks.playAudioFromUrl', + default: 'play audio from [URL]', + description: 'Plays sound from a URL.' + }), + blockType: BlockType.COMMAND, + hideFromPalette: true, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: 'https://extensions.turbowarp.org/meow.mp3' + } + } + }, + { + opcode: 'setAudioToLooping', + text: formatMessage({ + id: 'jgPrism.blocks.setAudioToLooping', + default: 'set audio to loop', + description: 'Sets the audio to be looping.' + }), + hideFromPalette: true, + blockType: BlockType.COMMAND + }, + { + opcode: 'setAudioToNotLooping', + text: formatMessage({ + id: 'jgPrism.blocks.setAudioToNotLooping', + default: 'set audio to not loop', + description: 'Sets the audio to not be looping.' + }), + hideFromPalette: true, + blockType: BlockType.COMMAND + }, + { + opcode: 'pauseAudio', + text: formatMessage({ + id: 'jgPrism.blocks.pauseAudio', + default: 'pause audio', + description: 'Pauses the audio player.' + }), + hideFromPalette: true, + blockType: BlockType.COMMAND + }, + { + opcode: 'playAudio', + text: formatMessage({ + id: 'jgPrism.blocks.playAudio', + default: 'resume audio', + description: 'Resumes the audio player.' + }), + hideFromPalette: true, + blockType: BlockType.COMMAND + }, + { + opcode: 'setAudioPlaybackSpeed', + text: formatMessage({ + id: 'jgPrism.blocks.setAudioPlaybackSpeed', + default: 'set audio speed to [SPEED]%', + description: 'Sets the speed of the audio player.' + }), + hideFromPalette: true, + blockType: BlockType.COMMAND, + arguments: { + SPEED: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'getAudioPlaybackSpeed', + text: formatMessage({ + id: 'jgRuntime.blocks.getAudioPlaybackSpeed', + default: 'audio speed', + description: 'Block that returns the playback speed of the audio player.' + }), + hideFromPalette: true, + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'setAudioPosition', + text: formatMessage({ + id: 'jgPrism.blocks.setAudioPosition', + default: 'set audio position to [POSITION] seconds', + description: 'Sets the position of the current audio in the audio player.' + }), + blockType: BlockType.COMMAND, + hideFromPalette: true, + arguments: { + POSITION: { + type: ArgumentType.NUMBER, + defaultValue: 5 + } + } + }, + { + opcode: 'getAudioPosition', + text: 'audio position', + disableMonitor: false, + hideFromPalette: true, + blockType: BlockType.REPORTER + }, + { + opcode: 'setAudioVolume', + text: formatMessage({ + id: 'jgPrism.blocks.setAudioVolume', + default: 'set audio volume to [VOLUME]%', + description: 'Sets the volume of the current audio in the audio player.' + }), + blockType: BlockType.COMMAND, + hideFromPalette: true, + arguments: { + VOLUME: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'getAudioVolume', + text: formatMessage({ + id: 'jgRuntime.blocks.getAudioVolume', + default: 'audio volume', + description: 'Block that returns the volume of the audio player.' + }), + disableMonitor: false, + hideFromPalette: true, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: "Data URIs" + }, + { + opcode: 'screenshotStage', + text: formatMessage({ + id: 'jgRuntime.blocks.screenshotStage', + default: 'screenshot the stage', + description: 'Block that screenshots the stage and returns a Data URI of it.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'dataUriOfCostume', + text: formatMessage({ + id: 'jgRuntime.blocks.dataUriOfCostume', + default: 'data url of costume #[INDEX]', + description: 'Block that returns a Data URI of the costume at the index.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + // these blocks will be replaced in the future + // hideFromPalette: true, + arguments: { + INDEX: { + type: ArgumentType.NUMBER, + defaultValue: "1" + } + } + }, + { + opcode: 'dataUriFromImageUrl', + text: formatMessage({ + id: 'jgRuntime.blocks.dataUriFromImageUrl', + default: 'data url of image at url: [URL]', + description: 'Block that returns a Data URI of the content fetched from the URL.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + // these blocks will be replaced in the future + // hideFromPalette: true, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: "url" + } + } + }, + { + opcode: 'dataUriFromArrayBuffer', + text: formatMessage({ + id: 'jgRuntime.blocks.dataUriFromArrayBuffer', + default: 'convert array buffer [BUFFER] to data url', + description: 'Block that returns a Data URI from an array buffer.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + BUFFER: { + type: ArgumentType.STRING, + defaultValue: "[72,101,108,108,111]" + } + } + }, + { + opcode: 'arrayBufferFromDataUri', + text: formatMessage({ + id: 'jgRuntime.blocks.arrayBufferFromDataUri', + default: 'convert data url [URL] to array buffer', + description: 'Block that returns an array buffer from a Data URL.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: "data:text/plain;base64,SGVsbG8=" + } + } + }, + // { + // blockType: BlockType.LABEL, + // text: "More Mouse Inputs" + // }, + { + opcode: 'currentMouseScrollX', + text: formatMessage({ + id: 'jgRuntime.blocks.currentMouseScrollX', + default: 'mouse scroll x', + description: 'im too lazy to write these anymore tbh' + }), + disableMonitor: false, + hideFromPalette: true, + blockIconURI: warningIcon, + blockType: BlockType.REPORTER + }, + { + opcode: 'currentMouseScroll', + text: formatMessage({ + id: 'jgRuntime.blocks.currentMouseScroll', + default: 'mouse scroll y', + description: 'im too lazy to write these anymore tbh' + }), + disableMonitor: false, + hideFromPalette: true, + blockIconURI: warningIcon, + blockType: BlockType.REPORTER + }, + { + opcode: 'currentMouseScrollZ', + text: formatMessage({ + id: 'jgRuntime.blocks.currentMouseScrollZ', + default: 'mouse scroll z', + description: 'im too lazy to write these anymore tbh' + }), + disableMonitor: false, + hideFromPalette: true, + blockIconURI: warningIcon, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: "Base64" + }, + { + opcode: 'base64Encode', + text: formatMessage({ + id: 'jgRuntime.blocks.base64Encode', + default: 'base64 encode [TEXT]', + description: 'Block that encodes and returns the result of it.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "abc" + } + } + }, + { + opcode: 'base64Decode', + text: formatMessage({ + id: 'jgRuntime.blocks.base64Decode', + default: 'base64 decode [TEXT]', + description: 'Block that decodes and returns the result of it.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "YWJj" + } + } + }, + // { + // blockType: BlockType.LABEL, + // text: "String Character Codes" + // }, + { + opcode: 'fromCharacterCodeString', + text: formatMessage({ + id: 'jgRuntime.blocks.fromCharacterCodeString', + default: 'character from character code [TEXT]', + description: 'Block that decodes and returns the result of it.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + hideFromPalette: true, + blockIconURI: warningIcon, + arguments: { + TEXT: { + type: ArgumentType.NUMBER, + defaultValue: 97 + } + } + }, + { + opcode: 'toCharacterCodeString', + text: formatMessage({ + id: 'jgRuntime.blocks.toCharacterCodeString', + default: 'character code of [TEXT]', + description: 'Block that encodes and returns the result of it.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + hideFromPalette: true, + blockIconURI: warningIcon, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "a" + } + } + }, + "---", + "---", + { + blockType: BlockType.LABEL, + text: "JS Deflate by BeatGammit" + }, + { + opcode: 'lib_deflate_deflateArray', + text: formatMessage({ + id: 'jgRuntime.blocks.lib_deflate_deflateArray', + default: 'deflate [ARRAY]', + description: 'abc' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + ARRAY: { + type: ArgumentType.STRING, + defaultValue: "[]" + } + } + }, + { + opcode: 'lib_deflate_inflateArray', + text: formatMessage({ + id: 'jgRuntime.blocks.lib_deflate_inflateArray', + default: 'inflate [ARRAY]', + description: 'abc' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + ARRAY: { + type: ArgumentType.STRING, + defaultValue: "[]" + } + } + }, + { + blockType: BlockType.LABEL, + text: "Numerical Encoding by cs2627883" + }, + { + opcode: 'NumericalEncode', + blockType: BlockType.REPORTER, + text: 'encode [DATA] to number', + arguments: { + DATA: { + type: ArgumentType.STRING, + defaultValue: 'Hello!' + } + } + }, + { + opcode: 'NumericalDecode', + blockType: BlockType.REPORTER, + text: 'decode [ENCODED] from number', + arguments: { + ENCODED: { + type: ArgumentType.STRING, + defaultValue: '000072000101000108000108000111000033' + } + } + }, + // "---", + // { + // blockType: BlockType.LABEL, + // text: "Deprecated blocks" + // }, + // "---", + // { + // blockType: BlockType.LABEL, + // text: "Don't use any of the blocks below." + // }, + // { + // blockType: BlockType.LABEL, + // text: "They will be removed soon." + // }, + // "---", + // { + // blockType: BlockType.LABEL, + // text: "(Deprecated) JavaScript" + // }, + { + opcode: 'evaluate', + text: formatMessage({ + id: 'jgRuntime.blocks.evaluate', + default: 'eval [JAVASCRIPT]', + description: 'Block that runs JavaScript code.' + }), + blockType: BlockType.COMMAND, + blockIconURI: warningIcon, + hideFromPalette: true, + arguments: { + JAVASCRIPT: { + type: ArgumentType.STRING, + defaultValue: "console.log('Hello!')" + } + } + }, + { + opcode: 'evaluate2', + text: formatMessage({ + id: 'jgRuntime.blocks.evaluate2', + default: 'eval [JAVASCRIPT]', + description: 'Block that runs JavaScript code and returns the result of it.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true, + blockIconURI: warningIcon, + hideFromPalette: true, + arguments: { + JAVASCRIPT: { + type: ArgumentType.STRING, + defaultValue: "Math.random()" + } + } + }, + { + opcode: 'evaluate3', + text: formatMessage({ + id: 'jgRuntime.blocks.evaluate3', + default: 'eval [JAVASCRIPT]', + description: 'Block that runs JavaScript code.' + }), + blockType: BlockType.HAT, + blockIconURI: warningIcon, + hideFromPalette: true, + arguments: { + JAVASCRIPT: { + type: ArgumentType.STRING, + defaultValue: "Math.round(Math.random()) == 1" + } + } + } + ] + }; + } + playAudioFromUrl(args) { + if (!this.audioPlayer) this.audioPlayer = new Audio(); + this.audioPlayer.pause(); + this.audioPlayer.src = `${args.URL}`; + this.audioPlayer.currentTime = 0; + this.audioPlayer.play(); + } + setAudioToLooping() { + this.audioPlayer.loop = true; + } + setAudioToNotLooping() { + this.audioPlayer.loop = false; + } + pauseAudio() { + this.audioPlayer.pause(); + } + playAudio() { + this.audioPlayer.play(); + } + setAudioPlaybackSpeed(args) { + this.audioPlayer.playbackRate = (isNaN(Number(args.SPEED)) ? 100 : Number(args.SPEED)) / 100; + } + getAudioPlaybackSpeed() { + return this.audioPlayer.playbackRate * 100; + } + setAudioPosition(args) { + this.audioPlayer.currentTime = isNaN(Number(args.POSITION)) ? 0 : Number(args.POSITION); + } + getAudioPosition() { + return this.audioPlayer.currentTime; + } + setAudioVolume(args) { + this.audioPlayer.volume = (isNaN(Number(args.VOLUME)) ? 100 : Number(args.VOLUME)) / 100; + } + getAudioVolume() { + return this.audioPlayer.volume * 100; + } + // eslint-disable-next-line no-unused-vars + evaluate(args, util, realBlockInfo) { + return new Promise((resolve, reject) => { + // if (!(this.isJSPermissionGranted)) { + // this.isJSPermissionGranted = ProjectPermissionManager.RequestPermission("javascript"); + // if (!this.isJSPermissionGranted) return; + // } + // // otherwise + SandboxRunner.execute(String(args.JAVASCRIPT)).then(result => { + if (!result.success) { + alert(result.value); + console.error(result.value); + return; + } + resolve(result.value) + }) + }) + } + // eslint-disable-next-line no-unused-vars + evaluate2(args, util, realBlockInfo) { + return new Promise((resolve, reject) => { + // if (!(this.isJSPermissionGranted)) { + // this.isJSPermissionGranted = ProjectPermissionManager.RequestPermission("javascript"); + // if (!this.isJSPermissionGranted) return ""; + // } + // // otherwise + SandboxRunner.execute(String(args.JAVASCRIPT)).then(result => { + if (!result.success) { + console.error(result.value); + } + resolve(result.value) + }) + // let result = ""; + // try { + // // eslint-disable-next-line no-eval + // result = eval(String(args.JAVASCRIPT)); + // } catch (e) { + // result = e; + // console.error(e); + // } + // return result; + // return ""; + }) + } + // eslint-disable-next-line no-unused-vars + evaluate3(args, util, realBlockInfo) { + return new Promise((resolve, reject) => { + SandboxRunner.execute(String(args.JAVASCRIPT)).then(result => { + if (!result.success) { + console.error(result.value); + } + resolve(result.value === true) + }) + }) + // if (!(this.isJSPermissionGranted)) { + // this.isJSPermissionGranted = ProjectPermissionManager.RequestPermission("javascript"); + // if (!this.isJSPermissionGranted) return false; + // } + // // otherwise + // let result = true; + // try { + // // eslint-disable-next-line no-eval + // result = eval(String(args.JAVASCRIPT)); + // } catch (e) { + // result = false; + // console.error(e); + // } + // // // otherwise + // return result === true; + // return false; + } + screenshotStage() { + // should we look for an external canvas + if (this.runtime.prism_screenshot_checkForExternalCanvas) { + // if so, does one exist (this will check for more than 1 in the future) + if (this.runtime.prism_screenshot_externalCanvas) { + // we dont need to check camera permissions since external canvases + // will never have the ability to get camera data + return this.runtime.prism_screenshot_externalCanvas.toDataURL(); + } + } + // DO NOT REMOVE, USER HAS NOT GIVEN PERMISSION TO SAVE CAMERA IMAGES. + if (this.runtime.ext_videoSensing || this.runtime.ioDevices.video.provider.enabled) { + // user's camera is on, ask for permission to take a picture of them + if (!(this.isCameraScreenshotEnabled)) { + this.isCameraScreenshotEnabled = ProjectPermissionManager.RequestPermission("cameraPictures"); + if (!this.isCameraScreenshotEnabled) return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/j//z8ABf4C/qc1gYQAAAAASUVORK5CYII="; // 1 pixel of white + } + } + return new Promise(resolve => { + vm.renderer.requestSnapshot(uri => { + resolve(uri); + }); + }); + } + dataUriOfCostume(args, util) { + const index = Number(args.INDEX); + if (isNaN(index)) return ""; + if (index < 1) return ""; + + const target = util.target; + // eslint-disable-next-line no-undefined + if (target.sprite.costumes[index - 1] === undefined || target.sprite.costumes[index - 1] === null) return ""; + const dataURI = target.sprite.costumes[index - 1].asset.encodeDataURI(); + return String(dataURI); + } + dataUriFromImageUrl(args) { + return new Promise(resolve => { + if (window && !window.FileReader) return resolve(""); + if (window && !window.fetch) return resolve(""); + fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent(String(args.URL))}`).then(r => { + r.blob().then(blob => { + const reader = new FileReader(); + reader.onload = e => { + resolve(e.target.result); + }; + reader.readAsDataURL(blob); + }) + .catch(() => { + resolve(""); + }); + }) + .catch(() => { + resolve(""); + }); + }); + } + + dataUriFromArrayBuffer(args) { + const array = validateArray(args.BUFFER); + if (!array.isValid) return 'data:text/plain;base64,'; + const buffer = BufferParser.arrayToBuffer(array.array); + let binary = ''; + let bytes = new Uint8Array(buffer); + let len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + // use "application/octet-stream", we have no idea what the buffer actually contains + return `data:application/octet-stream;base64,${btoa(binary)}`; + } + arrayBufferFromDataUri(args) { + const dataUrl = Cast.toString(args.URL); + return new Promise((resolve) => { + fetch(dataUrl).then(res => { + res.arrayBuffer().then(buffer => { + const array = BufferParser.bufferToArray(buffer); + resolve(JSON.stringify(array)); + }).catch(() => { + resolve('[]'); + }); + }).catch(() => { + resolve('[]'); + }); + }); + } + + currentMouseScrollX() { + return this.mouseScrollDelta.x; + } + currentMouseScroll() { + return this.mouseScrollDelta.y; + } + currentMouseScrollZ() { + return this.mouseScrollDelta.z; + } + base64Encode(args) { + let result = ""; + try { + result = btoa(String(args.TEXT)); + } catch { + // what a shame + } + return result; + } + base64Decode(args) { + let result = ""; + try { + result = atob(String(args.TEXT)); + } catch { + // what a shame + } + return result; + } + fromCharacterCodeString(args) { + return String.fromCharCode(args.TEXT); + } + toCharacterCodeString(args) { + return String(args.TEXT).charCodeAt(0); + } + lib_deflate_deflateArray(args) { + const array = validateArray(args.ARRAY).array; + + return JSON.stringify(beatgammit.deflate(array)); + } + lib_deflate_inflateArray(args) { + const array = validateArray(args.ARRAY).array; + + return JSON.stringify(beatgammit.inflate(array)); + } + + NumericalEncode(args) { + const toencode = String(args.DATA); + let encoded = ""; + for (let i = 0; i < toencode.length; ++i) { + // Get char code of character + let encodedchar = String(toencode.charCodeAt(i)); + // Pad encodedchar with 0s to ensure all encodedchars are the same length + encodedchar = "0".repeat(this.encodeCharacterLength - encodedchar.length) + encodedchar; + encoded += encodedchar; + } + return encoded; + } + NumericalDecode(args) { + const todecode = String(args.ENCODED); + if (todecode == "") { + return ""; + } + let decoded = ""; + // Create regex to split by char length + const regex = new RegExp('.{1,' + this.encodeCharacterLength + '}', 'g'); + // Split into array of characters + let encodedchars = todecode.match(regex); + for (let i = 0; i < encodedchars.length; i++) { + // Get character from char code + let decodedchar = String.fromCharCode(encodedchars[i]); + decoded += decodedchar; + } + return decoded; + } +} + +module.exports = JgPrismBlocks; diff --git a/local-scratch-vm/src/extensions/jg_runtime/index.js b/local-scratch-vm/src/extensions/jg_runtime/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e7f780c0d3f85d161e90cfd49c1ade8bad3bf7ab --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_runtime/index.js @@ -0,0 +1,1238 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const BufferUtil = new (require('../../util/array buffer')); +const Cast = require('../../util/cast'); +const Color = require('../../util/color'); + +// ShovelUtils +let fps = 0; + +/** + * Class for Runtime blocks + * @constructor + */ +class JgRuntimeBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + // SharkPool + this.pausedScripts = Object.create(null); + + // ShovelUtils + // Based on from https://www.growingwiththeweb.com/2017/12/fast-simple-js-fps-counter.html + const times = []; + fps = this.runtime.frameLoop.framerate; + this.runtime.on('RUNTIME_STEP_START', () => { + const now = performance.now(); + while (times.length > 0 && times[0] <= now - 1000) { times.shift() } + times.push(now); + fps = times.length; + }); + this.runtime.on('PROJECT_STOP_ALL', () => { this.pausedScripts = Object.create(null) }); + } + + _typeIsBitmap(type) { + return ( + type === 'image/png' || type === 'image/bmp' || type === 'image/jpg' || type === 'image/jpeg' || + type === 'image/jfif' || type === 'image/webp' || type === 'image/gif' + ); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgRuntime', + name: 'Runtime', + color1: '#777777', + color2: '#6a6a6a', + blocks: [ + { + opcode: 'addSpriteUrl', + text: 'add sprite from [URL]', + blockType: BlockType.COMMAND, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: `https://corsproxy.io/?${encodeURIComponent('https://penguinmod.com/Sprite1.pms')}` + } + } + }, + { + opcode: 'addCostumeUrl', + text: 'add costume [name] from [URL]', + blockType: BlockType.COMMAND, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: `https://corsproxy.io/?${encodeURIComponent('https://penguinmod.com/navicon.png')}` + }, + name: { + type: ArgumentType.STRING, + defaultValue: 'penguinmod' + } + } + }, + { + opcode: 'addCostumeUrlForceMime', + text: 'add [costtype] costume [name] from [URL]', + blockType: BlockType.COMMAND, + arguments: { + costtype: { + type: ArgumentType.STRING, + menu: "costumeMimeType" + }, + URL: { + type: ArgumentType.STRING, + defaultValue: `https://corsproxy.io/?${encodeURIComponent('https://penguinmod.com/navicon.png')}` + }, + name: { + type: ArgumentType.STRING, + defaultValue: 'penguinmod' + } + } + }, + { + opcode: 'addSoundUrl', + text: 'add sound [NAME] from [URL]', + blockType: BlockType.COMMAND, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: 'https://extensions.turbowarp.org/meow.mp3' + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: 'Meow' + } + } + }, + { + opcode: 'loadProjectDataUrl', + text: 'load project from [URL]', + blockType: BlockType.COMMAND, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: '' + } + } + }, + { + opcode: 'getIndexOfCostume', + text: 'get costume index of [costume]', + blockType: BlockType.REPORTER, + arguments: { + costume: { + type: ArgumentType.STRING, + defaultValue: "costume1" + } + } + }, + { + opcode: 'getIndexOfSound', + text: 'get sound index of [NAME]', + blockType: BlockType.REPORTER, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "Pop" + } + } + }, + { + opcode: 'getProjectDataUrl', + text: 'get data url of project', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + '---', + { + opcode: 'setStageSize', + text: formatMessage({ + id: 'jgRuntime.blocks.setStageSize', + default: 'set stage width: [WIDTH] height: [HEIGHT]', + description: 'Sets the width and height of the stage.' + }), + blockType: BlockType.COMMAND, + arguments: { + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 480 + }, + HEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 360 + } + } + }, + { + opcode: 'getStageWidth', + text: formatMessage({ + id: 'jgRuntime.blocks.getStageWidth', + default: 'stage width', + description: 'Block that returns the width of the stage.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'getStageHeight', + text: formatMessage({ + id: 'jgRuntime.blocks.getStageHeight', + default: 'stage height', + description: 'Block that returns the height of the stage.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + '---', + { + opcode: 'updateRuntimeConfig', + text: formatMessage({ + id: 'jgRuntime.blocks.updateRuntimeConfig', + default: 'set [OPTION] to [ENABLED]', + description: 'Block that enables or disables configuration on the runtime like high quality pen or turbo mode.' + }), + disableMonitor: false, + blockType: BlockType.COMMAND, + arguments: { + OPTION: { + menu: 'runtimeConfig' + }, + ENABLED: { + menu: 'onoff' + } + } + }, + { + opcode: 'changeRenderingCapping', + text: formatMessage({ + id: 'jgRuntime.blocks.changeRenderingCapping', + default: 'change render setting [OPTION] to [CAPPED]', + description: 'Block that updates configuration on the renderer like resolution for certain content.' + }), + disableMonitor: false, + blockType: BlockType.COMMAND, + arguments: { + OPTION: { + menu: 'renderConfigCappable' + }, + CAPPED: { + menu: 'cappableSettings' + } + } + }, + { + opcode: 'setRenderingNumber', + text: formatMessage({ + id: 'jgRuntime.blocks.setRenderingNumber', + default: 'set render setting [OPTION] to [NUM]', + description: 'Block that sets configuration on the renderer like resolution for certain content.' + }), + disableMonitor: false, + blockType: BlockType.COMMAND, + arguments: { + OPTION: { + menu: 'renderConfigNumber' + }, + NUM: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'runtimeConfigEnabled', + text: formatMessage({ + id: 'jgRuntime.blocks.runtimeConfigEnabled', + default: '[OPTION] enabled?', + description: 'Block that returns whether a runtime option like Turbo Mode is enabled on the project or not.' + }), + disableMonitor: false, + blockType: BlockType.BOOLEAN, + arguments: { + OPTION: { + menu: 'runtimeConfig' + } + } + }, + { + opcode: 'turboModeEnabled', + text: formatMessage({ + id: 'jgRuntime.blocks.turboModeEnabled', + default: 'turbo mode enabled?', + description: 'Block that returns whether Turbo Mode is enabled on the project or not.' + }), + disableMonitor: false, + hideFromPalette: true, + blockType: BlockType.BOOLEAN + }, + '---', + { + opcode: 'setMaxClones', + text: formatMessage({ + id: 'jgRuntime.blocks.setMaxClones', + default: 'set max clones to [MAX]', + description: 'Block that enables or disables configuration on the runtime like high quality pen or turbo mode.' + }), + disableMonitor: false, + blockType: BlockType.COMMAND, + arguments: { + MAX: { + menu: 'cloneLimit', + defaultValue: 300 + } + } + }, + { + opcode: 'maxAmountOfClones', + text: formatMessage({ + id: 'jgRuntime.blocks.maxAmountOfClones', + default: 'max clone count', + description: 'Block that returns the maximum amount of clones that may exist.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'amountOfClones', + text: formatMessage({ + id: 'jgRuntime.blocks.amountOfClones', + default: 'clone count', + description: 'Block that returns the amount of clones that currently exist.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'getIsClone', + text: formatMessage({ + id: 'jgRuntime.blocks.getIsClone', + default: 'is clone?', + description: 'Block that returns whether the sprite is a clone or not.' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN + }, + '---', + { + opcode: 'setMaxFrameRate', + text: formatMessage({ + id: 'jgRuntime.blocks.setMaxFrameRate', + default: 'set max framerate to: [FRAMERATE]', + description: 'Sets the max allowed framerate.' + }), + blockType: BlockType.COMMAND, + arguments: { + FRAMERATE: { + type: ArgumentType.NUMBER, + defaultValue: 30 + } + } + }, + { + opcode: 'getMaxFrameRate', + text: formatMessage({ + id: 'jgRuntime.blocks.getMaxFrameRate', + default: 'max framerate', + description: 'Block that returns the amount of FPS allowed.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'getFrameRate', + text: formatMessage({ + id: 'jgRuntime.blocks.getFrameRate', + default: 'framerate', + description: 'Block that returns the amount of FPS.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + '---', + { + opcode: 'setBackgroundColor', + text: formatMessage({ + id: 'jgRuntime.blocks.setBackgroundColor', + default: 'set stage background color to [COLOR]', + description: 'Sets the background color of the stage.' + }), + blockType: BlockType.COMMAND, + arguments: { + COLOR: { + type: ArgumentType.COLOR + } + } + }, + { + opcode: 'getBackgroundColor', + text: formatMessage({ + id: 'jgRuntime.blocks.getBackgroundColor', + default: 'stage background color', + description: 'Block that returns the stage background color in HEX.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + "---", + { + opcode: "pauseScript", + blockType: BlockType.COMMAND, + text: "pause this script using name: [NAME]", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "my script", + }, + } + }, + { + opcode: "unpauseScript", + blockType: BlockType.COMMAND, + text: "unpause script named: [NAME]", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "my script", + }, + } + }, + { + opcode: "isScriptPaused", + blockType: BlockType.BOOLEAN, + text: "is script named [NAME] paused?", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "my script", + }, + } + }, + "---", + { + opcode: 'variables_createVariable', + text: 'create variable named [NAME] for [SCOPE]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "my variable" }, + SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } + } + }, + { + opcode: 'variables_createCloudVariable', + text: 'create cloud variable named [NAME]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "cloud variable" }, + } + }, + { + opcode: 'variables_createList', + text: 'create list named [NAME] for [SCOPE]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "list" }, + SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } + } + }, + { + opcode: 'variables_getVariable', + text: 'get value of variable named [NAME] in [SCOPE]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "my variable" }, + SCOPE: { type: ArgumentType.STRING, menu: "variableTypes" } + } + }, + { + opcode: 'variables_getList', + text: 'get array of list named [NAME] in [SCOPE]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "list" }, + SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } + } + }, + { + opcode: 'variables_existsVariable', + text: 'variable named [NAME] exists in [SCOPE]?', + disableMonitor: true, + blockType: BlockType.BOOLEAN, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "my variable" }, + SCOPE: { type: ArgumentType.STRING, menu: "variableTypes" } + } + }, + { + opcode: 'variables_existsList', + text: 'list named [NAME] exists in [SCOPE]?', + disableMonitor: true, + blockType: BlockType.BOOLEAN, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "list" }, + SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } + } + }, + "---", + { + opcode: 'getDataOption', + text: formatMessage({ + id: 'jgRuntime.blocks.getDataOption', + default: 'get binary data of [OPTION] named [NAME]', + description: 'Block that returns the binary data of a sprite, sound or costume.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER, + arguments: { + OPTION: { + type: ArgumentType.STRING, + menu: "objectType" + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "Sprite1" + } + } + }, + { + opcode: 'getDataUriOption', + text: formatMessage({ + id: 'jgRuntime.blocks.getDataUriOption', + default: 'get data uri of [OPTION] named [NAME]', + description: 'Block that returns the data URI of a sprite, sound or costume.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER, + arguments: { + OPTION: { + type: ArgumentType.STRING, + menu: "objectType" + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "Sprite1" + } + } + }, + "---", + { + opcode: 'getAllSprites', + text: 'get all sprites', + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'getAllCostumes', + text: 'get all costumes', + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'getAllSounds', + text: 'get all sounds', + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'getAllFonts', + text: 'get all fonts', + disableMonitor: false, + blockType: BlockType.REPORTER + }, + "---", + { + opcode: 'getAllVariables', + text: 'get all variables [ALLSCOPE]', + disableMonitor: false, + blockType: BlockType.REPORTER, + arguments: { + ALLSCOPE: { + type: ArgumentType.STRING, + menu: "allVariableType" + } + } + }, + { + opcode: 'getAllLists', + text: 'get all lists [ALLSCOPE]', + disableMonitor: false, + blockType: BlockType.REPORTER, + arguments: { + ALLSCOPE: { + type: ArgumentType.STRING, + menu: "allVariableScope" + } + } + }, + "---", + { + blockType: BlockType.LABEL, + text: "Potentially Dangerous" + }, + { + opcode: 'deleteCostume', + text: formatMessage({ + id: 'jgRuntime.blocks.deleteCostume', + default: 'delete costume at index [COSTUME]', + description: 'Deletes a costume at the specified index.' + }), + blockType: BlockType.COMMAND, + arguments: { + COSTUME: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'deleteSound', + text: formatMessage({ + id: 'jgRuntime.blocks.deleteSound', + default: 'delete sound at index [SOUND]', + description: 'Deletes a sound at the specified index.' + }), + blockType: BlockType.COMMAND, + arguments: { + SOUND: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + "---", + { + opcode: 'variables_deleteVariable', + text: 'delete variable named [NAME] in [SCOPE]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "my variable" }, + SCOPE: { type: ArgumentType.STRING, menu: "variableTypes" } + } + }, + { + opcode: 'variables_deleteList', + text: 'delete list named [NAME] in [SCOPE]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "list" }, + SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } + } + }, + "---", + { + opcode: 'deleteSprite', + text: formatMessage({ + id: 'jgRuntime.blocks.deleteSprite', + default: 'delete sprite named [NAME]', + description: 'Deletes a sprite with the specified name.' + }), + blockType: BlockType.COMMAND, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "Sprite1" + } + } + }, + ], + menus: { + objectType: { + acceptReporters: true, + items: [ + "sprite", "costume", "sound" + ].map(item => ({ text: item, value: item })) + }, + variableScope: { + acceptReporters: true, + items: [ + "all sprites", "this sprite" + ].map(item => ({ text: item, value: item })) + }, + allVariableScope: { + acceptReporters: true, + items: [ + "for all sprites", "in every sprite", "in this sprite" + ].map(item => ({ text: item, value: item })) + }, + allVariableType: { + acceptReporters: true, + items: [ + "for all sprites", "in every sprite", + "in this sprite", "in the cloud" + ].map(item => ({ text: item, value: item })) + }, + variableTypes: { + acceptReporters: true, + items: [ + "all sprites", "this sprite", "cloud" + ].map(item => ({ text: item, value: item })) + }, + cloneLimit: { + items: [ + '100', '128', '300', '500', + '1000', '1024', '5000', + '10000', '16384', 'Infinity' + ], + isTypeable: true, + isNumeric: true + }, + runtimeConfig: { + acceptReporters: true, + items: [ + "turbo mode", + "high quality pen", + "offscreen sprites", + "remove miscellaneous limits", + "disable offscreen rendering", + "interpolation", + "warp timer" + ] + }, + renderConfigCappable: { + acceptReporters: true, + items: ["animated text resolution"] + }, + renderConfigNumber: { + acceptReporters: true, + items: ["animated text resolution"] + }, + onoff: ["on", "off"], + costumeMimeType: ["png", "bmp", "jpg", "jpeg", "jfif", "webp", "gif", "vector"], + cappableSettings: ["uncapped", "capped", "fixed"] + } + }; + } + // utils + _generateScratchId() { + const soup = "!#%()*+,-./:;=?@[]^_`{|}~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const id = []; + for (let i = 0; i < 20; i++) { id[i] = soup.charAt(Math.random() * soup.length) } + return id.join(""); + } + + // blocks + addCostumeUrl(args, util) { + const targetId = util.target.id; + return new Promise(resolve => { + fetch(args.URL, { method: 'GET' }).then(x => x.blob().then(blob => { + const costumeHasForcedMime = !!args.costtype; + const costumeForcedMimeBitmap = args.costtype !== "vector"; + if (!( + (this._typeIsBitmap(blob.type)) || (blob.type === 'image/svg+xml') + ) && !costumeHasForcedMime) { + resolve(); + throw new Error(`Invalid mime type: "${blob.type}"`); + } + const assetType = (costumeHasForcedMime ? costumeForcedMimeBitmap : this._typeIsBitmap(blob.type)) ? this.runtime.storage.AssetType.ImageBitmap : this.runtime.storage.AssetType.ImageVector; + const dataType = costumeHasForcedMime ? (costumeForcedMimeBitmap ? args.costtype : 'svg') : (blob.type === 'image/svg+xml' ? 'svg' : blob.type.split('/')[1]); + blob.arrayBuffer().then(buffer => { + const data = costumeHasForcedMime ? (!costumeForcedMimeBitmap ? buffer : new Uint8Array(buffer)) : (dataType === 'image/svg+xml' + ? buffer : new Uint8Array(buffer)); + const asset = this.runtime.storage.createAsset(assetType, dataType, data, null, true); + const name = `${asset.assetId}.${asset.dataFormat}`; + const spriteJson = { asset: asset, md5ext: name, name: args.name }; + const request = vm.addCostume(name, spriteJson, targetId); + if (request.then) request.then(resolve); + else resolve(); + }) + .catch(err => { + console.error(`Failed to Load Costume: ${err}`); + resolve(); + }); + })); + }); + } + addCostumeUrlForceMime(args, util) { + this.addCostumeUrl(args, util); + } + deleteCostume(args, util) { + const index = Math.round(Cast.toNumber(args.COSTUME)) - 1; + if (index < 0) return; + util.target.deleteCostume(index); + } + deleteSound(args, util) { + const index = Math.round(Cast.toNumber(args.SOUND)) - 1; + if (index < 0) return; + util.target.deleteSound(index); + } + getIndexOfCostume(args, util) { return util.target.getCostumeIndexByName(args.costume) + 1 } + getIndexOfSound(args, util) { + let index = 0; + const sounds = util.target.getSounds(); + for (let i = 0; i < sounds.length; i++) { + if (sounds[i].name === args.NAME) index = i + 1; + } + return index; + } + setStageSize(args) { + if (vm) vm.setStageSize( + Math.max(1, Cast.toNumber(args.WIDTH)), Math.max(1, Cast.toNumber(args.HEIGHT)) + ); + } + turboModeEnabled() { return this.runtime.turboMode } + amountOfClones() { return this.runtime._cloneCounter } + getStageWidth() { return this.runtime.stageWidth } + getStageHeight() { return this.runtime.stageHeight } + getMaxFrameRate() { return this.runtime.frameLoop.framerate } + getIsClone(_, util) { return !(util.target.isOriginal) } + + changeRenderingCapping(args) { + const option = Cast.toString(args.OPTION).toLowerCase(); + const capping = Cast.toString(args.CAPPED).toLowerCase(); + switch (option) { + case "animated text resolution": { + this.runtime.renderer.customRenderConfig.textCostumeResolution.fixed = false; + this.runtime.renderer.customRenderConfig.textCostumeResolution.capped = false; + if (capping === "fixed") this.runtime.renderer.customRenderConfig.textCostumeResolution.fixed = true; + else if (capping === "capped") this.runtime.renderer.customRenderConfig.textCostumeResolution.capped = true; + break; + } + } + this.runtime.renderer.dirty = true; + this.runtime.requestRedraw(); + } + setRenderingNumber(args) { + const option = Cast.toString(args.OPTION).toLowerCase(); + const number = Cast.toNumber(args.NUM); + switch (option) { + case "animated text resolution": { + this.runtime.renderer.customRenderConfig.textCostumeResolution.value = number; + break; + } + case "max texture scale for new svg images": { + this.runtime.renderer.setMaxTextureDimension(number); + break; + } + } + this.runtime.renderer.dirty = true; + this.runtime.requestRedraw(); + } + + updateRuntimeConfig(args) { + const enabled = Cast.toString(args.ENABLED).toLowerCase() === 'on'; + switch (Cast.toString(args.OPTION).toLowerCase()) { + case 'turbo mode': return vm.setTurboMode(enabled); + case "high quality pen": return this.runtime.renderer.setUseHighQualityRender(enabled); + case "offscreen sprites": return this.runtime.setRuntimeOptions({ fencing: !enabled }); + case "remove miscellaneous limits": return this.runtime.setRuntimeOptions({ miscLimits: !enabled }); + case "disable offscreen rendering": return this.runtime.setRuntimeOptions({ disableOffscreenRendering: enabled }); + case "interpolation": return vm.setInterpolation(enabled); + case "warp timer": return this.runtime.setCompilerOptions({ warpTimer: enabled }); + } + } + runtimeConfigEnabled(args) { + switch (Cast.toString(args.OPTION).toLowerCase()) { + case 'turbo mode': return this.runtime.turboMode; + case "high quality pen": return this.runtime.renderer.useHighQualityRender; + case "offscreen sprites": return !this.runtime.runtimeOptions.fencing; + case "remove miscellaneous limits": return !this.runtime.runtimeOptions.miscLimits; + case "disable offscreen rendering": return this.runtime.runtimeOptions.disableOffscreenRendering; + case "interpolation": return this.runtime.interpolationEnabled; + case "warp timer": return this.runtime.compilerOptions.warpTimer; + default: return false; + } + } + setMaxClones(args) { + const limit = Math.round(Cast.toNumber(args.MAX)); + this.runtime.vm.setRuntimeOptions({ maxClones: limit }); + } + maxAmountOfClones() { return this.runtime.runtimeOptions.maxClones } + setBackgroundColor(args) { + let RGB; + if (typeof args.COLOR === "number") { + RGB = Cast.toRgbColorObject(args.COLOR); + this.runtime.renderer.setBackgroundColor(RGB.r / 255, RGB.g / 255, RGB.b / 255); + } else { + RGB = Cast.toString(args.COLOR); + RGB = RGB.startsWith("#") ? RGB.slice(1) : RGB; + this.runtime.renderer.setBackgroundColor( + parseInt(RGB.slice(0, 2), 16) / 255, + parseInt(RGB.slice(2, 4), 16) / 255, + parseInt(RGB.slice(4, 6), 16) / 255, + RGB.length === 8 ? parseInt(RGB.slice(6, 8), 16) / 255 : 1 + ) + } + } + getBackgroundColor() { + const colorArray = this.runtime.renderer._backgroundColor3b; + const colorObject = { + r: Math.round(Cast.toNumber(colorArray[0])), + g: Math.round(Cast.toNumber(colorArray[1])), + b: Math.round(Cast.toNumber(colorArray[2])) + }; + const hex = Color.rgbToHex(colorObject); + return hex; + } + + // SharkPool, edited by JeremyGamer13 + pauseScript(args, util) { + const scriptName = Cast.toString(args.NAME); + const state = util.stackFrame.pausedScript; + if (!state) { + this.pausedScripts[scriptName] = true; + util.stackFrame.pausedScript = scriptName; + util.yield(); + } else if (state in this.pausedScripts) { + util.yield(); + } + } + unpauseScript(args) { + const scriptName = Cast.toString(args.NAME); + if (scriptName in this.pausedScripts) { + delete this.pausedScripts[scriptName]; + } + } + isScriptPaused(args) { + const scriptName = Cast.toString(args.NAME); + return scriptName in this.pausedScripts; + } + + setMaxFrameRate(args) { + let frameRate = Cast.toNumber(args.FRAMERATE); + this.runtime.frameLoop.setFramerate(frameRate); + } + deleteSprite(args) { + const target = this.runtime.getSpriteTargetByName(args.NAME); + if (!target) return; + vm.deleteSpriteInternal(target.id); + } + + getDataOption(args, util) { + switch (args.OPTION) { + case "sprite": { + const sprites = this.runtime.targets.filter(target => target.isOriginal); + const sprite = sprites.filter(sprite => sprite.sprite.name === args.NAME)[0]; + if (!sprite) return "[]"; + return new Promise(resolve => { + vm.exportSprite(sprite.id).then(blob => { + blob.arrayBuffer().then(arrayBuffer => { + const array = BufferUtil.bufferToArray(arrayBuffer); + const stringified = JSON.stringify(array); + resolve(stringified); + }).catch(() => resolve("[]")); + }).catch(() => resolve("[]")); + }); + } + case "costume": { + const costumes = util.target.getCostumes(); + const index = util.target.getCostumeIndexByName(args.NAME); + if (!costumes[index]) return "[]"; + const costume = costumes[index]; + const data = costume.asset.data; + const array = BufferUtil.bufferToArray(data.buffer); + return JSON.stringify(array); + } + case "sound": { + const sounds = util.target.getSounds(); + const index = this.getIndexOfSound(args, util) - 1; + if (!sounds[index]) return "[]"; + const sound = sounds[index]; + const data = sound.asset.data; + const array = BufferUtil.bufferToArray(data.buffer); + return JSON.stringify(array); + } + default: return "[]"; + } + } + getDataUriOption(args, util) { + switch (args.OPTION) { + case "sprite": { + const sprites = this.runtime.targets.filter(target => target.isOriginal); + const sprite = sprites.filter(sprite => sprite.sprite.name === args.NAME)[0]; + if (!sprite) return ""; + return new Promise(resolve => { + vm.exportSprite(sprite.id).then(blob => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => resolve(""); + reader.onabort = () => resolve(""); + reader.readAsDataURL(blob); + }).catch(() => resolve("")); + }); + } + case "costume": { + const costumes = util.target.getCostumes(); + const index = util.target.getCostumeIndexByName(args.NAME); + if (!costumes[index]) return ""; + const costume = costumes[index]; + return costume.asset.encodeDataURI(); + } + case "sound": { + const sounds = util.target.getSounds(); + const index = this.getIndexOfSound(args, util) - 1; + if (!sounds[index]) return ""; + const sound = sounds[index]; + return sound.asset.encodeDataURI(); + } + default: return ""; + } + } + getAllSprites() { + return JSON.stringify(this.runtime.targets.filter(target => target.isOriginal && !target.isStage).map(target => target.sprite.name)); + } + getAllCostumes(_, util) { + const costumes = util.target.getCostumes(); + return JSON.stringify(costumes.map(costume => costume.name)); + } + getAllSounds(_, util) { + const sounds = util.target.getSounds(); + return JSON.stringify(sounds.map(sound => sound.name)); + } + getAllFonts() { + const fonts = this.runtime.fontManager.getFonts(); + return JSON.stringify(fonts.map(font => font.name)); + } + + loadProjectDataUrl(args) { + const url = Cast.toString(args.URL); + if (typeof ScratchBlocks !== "undefined") { + // We are in the editor. Ask before loading a new project to avoid unrecoverable data loss. + if (!confirm(`Runtime Extension - Editor: Are you sure you want to load a new project?\nEverything in the current project will be permanently deleted.`)) { + return; + } + } + console.log("Loading project from custom source..."); + fetch(url) + .then((r) => r.arrayBuffer()) + .then((buffer) => vm.loadProject(buffer)) + .then(() => { + console.log("Loaded project!"); + vm.greenFlag(); + }) + .catch((error) => { + console.log("Error loading custom project;", error); + }); + } + getProjectDataUrl() { + return new Promise((resolve) => { + const failingUrl = 'data:application/octet-stream;base64,'; + vm.saveProjectSb3().then(blob => { + const fileReader = new FileReader(); + fileReader.onload = () => { resolve(fileReader.result); }; + fileReader.onerror = () => { resolve(failingUrl) } + fileReader.readAsDataURL(blob); + }).catch(() => { resolve(failingUrl) }); + }); + } + + getAllVariables(args, util) { + switch (args.ALLSCOPE) { + case "for all sprites": { + const stage = this.runtime.getTargetForStage(); + if (!stage) return "[]"; + const variables = stage.variables; + if (!variables) return "[]"; + return JSON.stringify(Object.values(variables).filter(v => v.type !== "list").map(v => v.name)); + } + case "in every sprite": { + const targets = this.runtime.targets; + if (!targets) return "[]"; + const variables = targets.filter(t => t.isOriginal).map(t => t.variables); + if (!variables) return "[]"; + return JSON.stringify(variables.map(v => Object.values(v)).map(v => v.filter(v => v.type !== "list").map(v => v.name)).flat(1)); + } + case "in this sprite": { + const target = util.target; + if (!target) return "[]"; + const variables = target.variables; + if (!variables) return "[]"; + return JSON.stringify(Object.values(variables).filter(v => v.type !== "list").map(v => v.name)); + } + case "in the cloud": { + const stage = this.runtime.getTargetForStage(); + if (!stage) return "[]"; + const variables = stage.variables; + if (!variables) return "[]"; + return JSON.stringify(Object.values(variables).filter(v => v.type !== "list").filter(v => v.isCloud === true).map(v => v.name)); + } + default: return "[]"; + } + } + getAllLists(args, util) { + switch (args.ALLSCOPE) { + case "for all sprites": { + const stage = this.runtime.getTargetForStage(); + if (!stage) return "[]"; + const variables = stage.variables; + if (!variables) return "[]"; + return JSON.stringify(Object.values(variables).filter(v => v.type === "list").map(v => v.name)); + } + case "in every sprite": { + const targets = this.runtime.targets; + if (!targets) return "[]"; + const variables = targets.filter(t => t.isOriginal).map(t => t.variables); + if (!variables) return "[]"; + return JSON.stringify(variables.map(v => Object.values(v)).map(v => v.filter(v => v.type === "list").map(v => v.name)).flat(1)); + } + case "in this sprite": { + const target = util.target; + if (!target) return "[]"; + const variables = target.variables; + if (!variables) return "[]"; + return JSON.stringify(Object.values(variables).filter(v => v.type === "list").map(v => v.name)); + } + default: return "[]"; + } + } + + // ShovelUtils + getFrameRate() { return fps } + addSoundUrl(args, util) { + const targetId = util.target.id; + return new Promise((resolve) => { + fetch(args.URL) + .then((r) => r.arrayBuffer()) + .then((arrayBuffer) => { + const storage = this.runtime.storage; + const asset = new storage.Asset( + storage.AssetType.Sound, null, storage.DataFormat.MP3, + new Uint8Array(arrayBuffer), true + ); + resolve(vm.addSound({ + md5: asset.assetId + '.' + asset.dataFormat, + asset: asset, name: args.NAME + }, targetId)); + }).catch(resolve); + }) + } + + // GameUtils + addSpriteUrl(args) { + return new Promise((resolve) => { + fetch(args.URL).then(response => { + response.arrayBuffer().then(arrayBuffer => { + vm.addSprite(arrayBuffer).finally(resolve); + }).catch(resolve); + }).catch(resolve); + }); + } + + // variables + variables_createVariable(args, util) { + const variableName = args.NAME; + switch (args.SCOPE) { + case "all sprites": return this.runtime.createNewGlobalVariable(variableName); + case "this sprite": return util.target.createVariable(this._generateScratchId(), variableName, ""); + } + } + variables_createCloudVariable(args) { + const variableName = `☁ ${args.NAME}`; + const stage = this.runtime.getTargetForStage(); + if (!stage) return; + const id = this._generateScratchId(); + stage.createVariable(id, variableName, "", true); + } + variables_createList(args, util) { + const variableName = args.NAME; + switch (args.SCOPE) { + case "all sprites": return this.runtime.createNewGlobalVariable(variableName, null, "list"); + case "this sprite": return util.target.createVariable(this._generateScratchId(), variableName, "list"); + } + } + variables_getVariable(args, util) { + const variableName = args.NAME; + let target; + let isCloud = false; + if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); + else if (args.SCOPE === "this sprite") target = util.target; + else if (args.SCOPE === "cloud") { + target = this.runtime.getTargetForStage(); + isCloud = true; + } else return ""; + const variables = Object.values(target.variables).filter(variable => variable.type !== "list").filter(variable => { + if (variable.isCloud) return String(variable.name).replace("☁ ", "") === variableName; + if (isCloud) return false; // above check should have already told us its a cloud variable + return variable.name === variableName; + }); + if (!variables) return ""; + const variable = variables[0]; + if (!variable) return ""; + return variable.value; + } + variables_getList(args, util) { + const variableName = args.NAME; + let target; + if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); + else if (args.SCOPE === "this sprite") target = util.target; + else return "[]"; + const variables = Object.values(target.variables).filter(v => v.type === "list").filter(v => v.name === variableName); + if (!variables) return "[]"; + const variable = variables[0]; + if (!variable) return "[]"; + return JSON.stringify(variable.value); + } + variables_deleteVariable(args, util) { + const variableName = args.NAME; + let target, isCloud = false; + if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); + else if (args.SCOPE === "this sprite") target = util.target; + else if (args.SCOPE === "cloud") { + target = this.runtime.getTargetForStage(); + isCloud = true; + } else return; + const variables = Object.values(target.variables).filter(v => v.type !== "list").filter(variable => { + if (variable.isCloud) return String(variable.name).replace("☁ ", "") === variableName; + if (isCloud) return false; // above check should have already told us its a cloud variable + return variable.name === variableName; + }); + if (!variables) return; + const variable = variables[0]; + if (!variable) return; + return target.deleteVariable(variable.id); + } + variables_deleteList(args, util) { + const variableName = args.NAME; + let target; + if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); + else if (args.SCOPE === "this sprite") target = util.target; + else return; + const variables = Object.values(target.variables).filter(v => v.type === "list").filter(v => v.name === variableName); + if (!variables) return; + const variable = variables[0]; + if (!variable) return; + return target.deleteVariable(variable.id); + } + variables_existsVariable(args, util) { + const variableName = args.NAME; + let target, isCloud = false; + if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); + else if (args.SCOPE === "this sprite") target = util.target; + else if (args.SCOPE === "cloud") { + target = this.runtime.getTargetForStage(); + isCloud = true; + } else return false; + const variables = Object.values(target.variables).filter(v => v.type !== "list").filter(variable => { + if (variable.isCloud) return String(variable.name).replace("☁ ", "") === variableName; + if (isCloud) return false; // above check should have already told us its a cloud variable + return variable.name === variableName; + }); + if (!variables) return false; + const variable = variables[0]; + if (!variable) return false; + return true; + } + variables_existsList(args, util) { + const variableName = args.NAME; + let target; + if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); + else if (args.SCOPE === "this sprite") target = util.target; + else return false; + const variables = Object.values(target.variables).filter(v => v.type === "list").filter(v => v.name === variableName); + if (!variables) return false; + const variable = variables[0]; + if (!variable) return false; + return true; + } +} + +module.exports = JgRuntimeBlocks; diff --git a/local-scratch-vm/src/extensions/jg_scratchAuth/icon.svg b/local-scratch-vm/src/extensions/jg_scratchAuth/icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..0b1c7f29b4d485033dff056516c72a1df81df669 Binary files /dev/null and b/local-scratch-vm/src/extensions/jg_scratchAuth/icon.svg differ diff --git a/local-scratch-vm/src/extensions/jg_scratchAuth/index.js b/local-scratch-vm/src/extensions/jg_scratchAuth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d4242cde20e5122cbabf5de4f85329ea9fe74f94 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_scratchAuth/index.js @@ -0,0 +1,315 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const Legacy = require('./legacy'); + +const Icon = require("./icon.svg"); + +/** + * Class for Scratch Authentication blocks + * @constructor + */ +let currentPrivateCode = ''; +class JgScratchAuthenticateBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.promptStatus = { + inProgress: false, + blocked: false, + completed: false, + userClosed: false, + }; + this.loginInfo = {}; + + // legacy + this.keepAllowingAuthBlock = true; + this.disableConfirmationShown = false; + } + + + /** + * dummy function for reseting user provided permisions when a save is loaded + */ + deserialize() { + this.disableConfirmationShown = false; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgScratchAuthenticate', + name: 'Scratch Auth', + color1: '#FFA01C', + color2: '#ff8C00', + blockIconURI: Icon, + // TODO: docs doesnt exist, make some docs + // docsURI: 'https://docs.penguinmod.com/extensions/scratch-auth', + blocks: [ + // LEGACY BLOCK + { + opcode: 'authenticate', + text: formatMessage({ + id: 'jgScratchAuthenticate.blocks.authenticate', + default: 'get scratch username and set sign in location name to [NAME]', + description: "Block that returns the user's name on Scratch." + }), + disableMonitor: true, + hideFromPalette: true, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "PenguinMod" } + }, + blockType: BlockType.REPORTER + }, + // NEW BLOCKS + { + opcode: 'showPrompt', + text: formatMessage({ + id: 'jgScratchAuthenticate.blocks.showPrompt', + default: 'show login message as [NAME]', + description: "Block that shows the Log in menu from Scratch Authentication." + }), + arguments: { + NAME: { + type: ArgumentType.STRING, + menu: 'loginLocation' + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'getPromptStatus', + text: formatMessage({ + id: 'jgScratchAuthenticate.blocks.promptStatus', + default: 'login prompt [STATUS]?', + description: "The status of the login prompt for Scratch Authentication." + }), + arguments: { + STATUS: { + type: ArgumentType.STRING, + menu: "promptStatus" + } + }, + disableMonitor: true, + blockType: BlockType.BOOLEAN + }, + { + opcode: 'privateCode', + text: formatMessage({ + id: 'jgScratchAuthenticate.blocks.privateCode', + default: 'authentication code', + description: "The login code when Scratch Authentication closes the login prompt." + }), + disableMonitor: true, + blockType: BlockType.REPORTER + }, + { + opcode: 'serverRedirectLocation', + text: formatMessage({ + id: 'jgScratchAuthenticate.blocks.serverRedirectLocation', + default: 'redirect location', + description: "The redirect location when Scratch Authentication closes the login prompt." + }), + disableMonitor: true, + blockType: BlockType.REPORTER + }, + '---', + { + text: formatMessage({ + id: 'jgScratchAuthenticate.labels.loginInfo1', + default: 'The blocks below invalidate', + description: "Label to denote that blocks invalidate the Scratch Auth private code below this label" + }), + blockType: BlockType.LABEL + }, + { + text: formatMessage({ + id: 'jgScratchAuthenticate.labels.loginInfo2', + default: 'the authentication code from above.', + description: "Label to denote that blocks invalidate the Scratch Auth private code below this label" + }), + blockType: BlockType.LABEL + }, + { + opcode: 'validLogin', + text: formatMessage({ + id: 'jgScratchAuthenticate.blocks.validLogin', + default: 'login is valid?', + description: "Whether or not the authentication was valid." + }), + disableMonitor: true, + // this doesnt seem to be important, + // login should always be valid when checking on client-side + hideFromPalette: true, + blockType: BlockType.BOOLEAN + }, + { + opcode: 'scratchUsername', + text: formatMessage({ + id: 'jgScratchAuthenticate.blocks.scratchUsername', + default: 'scratch username', + description: "The username that was logged in." + }), + disableMonitor: true, + blockType: BlockType.REPORTER + }, + ], + menus: { + loginLocation: { + items: '_getLoginLocations', + isTypeable: true, + }, + promptStatus: [ + { text: 'in progress', value: 'inProgress' }, + { text: 'blocked', value: 'blocked' }, + { text: 'complete', value: 'completed' }, + { text: 'closed by the user', value: 'userClosed' }, + ] + } + }; + } + + // menus + _getLoginLocations() { + const nameSplit = document.title.split(" - "); + nameSplit.pop(); + const projectName = Cast.toString(nameSplit.join(" - ")); + return [ + projectName === 'PenguinMod' ? 'Project' : projectName, + 'PenguinMod', + 'Game', + ]; + } + + // util + async parseLoginCode_() { + if (!currentPrivateCode) throw new Error('Private code not present'); + const req = await fetch(`https://pm-bapi.vercel.app/api/verifyToken?privateCode=${currentPrivateCode}`); + const json = await req.json(); + this.loginInfo = { + valid: json.valid, + username: json.username + }; + return this.loginInfo; + } + + // blocks + showPrompt(args) { + // reset + this.promptStatus = { + inProgress: true, + blocked: false, + completed: false, + userClosed: false, + }; + this.loginInfo = {}; + + const loginLocation = Cast.toString(args.NAME); + const sanitizedName = encodeURIComponent(loginLocation.substring(0, 256).replace(/[^a-zA-Z0-9 _\-\.\[\]\(\)]+/gmi, "")); + const waitingLink = `https://studio.penguinmod.com/scratchAuthExt.html?openLocation=${encodeURIComponent(window.origin)}`; + + // listen for events before opening + let login; + let finished = false; + const listener = (event) => { + if (event.origin !== (new URL(waitingLink)).origin) { + return; + } + if (!(event.data && event.data.scratchauthd1)) { + return; + } + + const data = event.data.scratchauthd1; + + const privateCode = data.pv; + currentPrivateCode = privateCode; + + // update status + this.promptStatus.inProgress = false; + this.promptStatus.completed = true; + + finished = true; + window.removeEventListener("message", listener); + login.close(); + }; + window.addEventListener("message", listener); + + // open prompt + login = window.open( + `https://auth.itinerary.eu.org/auth/?redirect=${btoa(waitingLink)}${sanitizedName.length > 0 ? `&name=${sanitizedName}` : ""}`, + "Scratch Authentication", + `scrollbars=yes,resizable=yes,status=no,location=yes,toolbar=no,menubar=no,width=768,height=512,left=200,top=200` + ); + if (!login) { + // popup was blocked most likely + this.promptStatus.inProgress = false; + this.promptStatus.blocked = true; + return; + } + + // .onclose doesnt work on most platforms it seems + // so just set interval + const closedInterval = setInterval(() => { + if (!login.closed) return; + + this.promptStatus.inProgress = false; + if (!finished) { + this.promptStatus.userClosed = true; + } + window.removeEventListener("message", listener); + clearInterval(closedInterval); + }, 500); + } + privateCode() { + const code = currentPrivateCode; + currentPrivateCode = ''; + return code; + } + serverRedirectLocation() { + const waitingLink = `https://studio.penguinmod.com/scratchAuthExt.html?openLocation=${window.origin}`; + return waitingLink; + } + getPromptStatus(args) { + const option = Cast.toString(args.STATUS); + if (!(option in this.promptStatus)) return false; + return this.promptStatus[option]; + } + + // parsing privat4e code blocks + async validLogin() { + if (Object.keys(this.loginInfo).length <= 0) { + try { + await this.parseLoginCode_(); + } catch { + // just say invalid if we cant parse + return false; + } + } + return !!this.loginInfo.valid; + } + async scratchUsername() { + if (Object.keys(this.loginInfo).length <= 0) { + try { + await this.parseLoginCode_(); + } catch { + // just say no username if we cant parse + return ''; + } + } + return Cast.toString(this.loginInfo.username); + } + + // legacy block + authenticate(...args) { + return Legacy.authenticate(this, ...args); + } +} + +module.exports = JgScratchAuthenticateBlocks; diff --git a/local-scratch-vm/src/extensions/jg_scratchAuth/legacy.js b/local-scratch-vm/src/extensions/jg_scratchAuth/legacy.js new file mode 100644 index 0000000000000000000000000000000000000000..d7672e84463235ef6a64a4d047b121f5389a9d1c --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_scratchAuth/legacy.js @@ -0,0 +1,82 @@ +const ProjectPermissionManager = require('../../util/project-permissions'); + +const authenticate = (thisObject, args) => { + if (!thisObject.keepAllowingAuthBlock) { // user closed popup before it was finished + if (!thisObject.disableConfirmationShown) { // we didnt ask them to confirm yet or they only declined it once, so we let them know every time + const areYouSure = ProjectPermissionManager.RequestPermission("scratchSignIn"); + if (!areYouSure) { // they clicked no, dont show confirmation again + thisObject.disableConfirmationShown = true; + return "The user has declined the ability to authenticate."; + } + } else { // they already clicked no before + return "The user has declined the ability to authenticate."; + } + } + return new Promise(resolve => { + const sanitizedName = encodeURIComponent(String(args.NAME).substring(0, 256).replace(/[^a-zA-Z0-9 _-]+/gmi, "_")); + const waitingLink = `${window.location.origin}/wait.html`; + const login = window.open( + `https://auth.itinerary.eu.org/auth/?redirect=${btoa(waitingLink)}&name=${sanitizedName.length > 0 ? sanitizedName : "PenguinMod"}`, + "Scratch Authentication", + `scrollbars=yes,resizable=yes,status=no,location=yes,toolbar=no,menubar=no,width=768,height=512,left=200,top=200` + ); + if (!login) { + resolve("Authentication failed to appear."); // popup was blocked most likely + // reminder for future me to make an iframe appear if the window failed to appear + } + let cantAccessAnymore = false; + let finished = false; // finished will be set to true if we got the username or something went wrong + let interval = null; // goofy activity + interval = setInterval(() => { + if (login?.closed && (!finished)) { + thisObject.keepAllowingAuthBlock = false; + clearInterval(interval); + try { + login.close(); + } catch { + // what a shame we couldnt close the window that doesnt exist anymore + } + resolve(""); + } + try { + const query = login.location.search; + if (!cantAccessAnymore) return; + const parameters = new URLSearchParams(query); + const privateCode = parameters.get("privateCode"); + if (!privateCode) { + finished = true; + clearInterval(interval); + login.close(); + resolve(""); + } + clearInterval(interval); + fetch(`https://pm-bapi.vercel.app/api/verifyToken?privateCode=${privateCode}`).then(res => res.json().then(json => { + finished = true; + login.close(); + if (json.valid != true) { + resolve(""); + } + resolve(String(json.username)); + }) + .catch(() => { + finished = true; + login.close(); + resolve(""); + })) + .catch(() => { + finished = true; + login.close(); + resolve(""); + }); + } catch { + // due to strange chrome bug, window still has the previous url on it so we need to wait until we switch to the auth site + cantAccessAnymore = true; + // now we cant access the location yet since the user hasnt left the authentication site + } + }, 10); + }); +}; + +module.exports = { + authenticate +} \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_scripts/index.js b/local-scratch-vm/src/extensions/jg_scripts/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e956f374e3f5b32f55eb9d0dfeea2a78bda27f69 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_scripts/index.js @@ -0,0 +1,251 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +class JgScriptsBlocks { + /** + * Class for Script blocks + * @constructor + */ + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + this.scripts = {}; + + this.runtime.on("PROJECT_START", () => { this.scripts = {} }); + this.runtime.on("PROJECT_STOP_ALL", () => { this.scripts = {} }); + } + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: "jgScripts", + name: "Scripts", + color1: "#8c8c8c", + color2: "#7a7a7a", + blocks: [ + { + opcode: "createScript", + blockType: BlockType.COMMAND, + text: "create script named [NAME]", + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "Script1" } + }, + }, + { + opcode: "deleteScript", + blockType: BlockType.COMMAND, + text: "delete script named [NAME]", + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "Script1" } + }, + }, + { + opcode: "deleteAll", + blockType: BlockType.COMMAND, + text: "delete all scripts" + }, + { + opcode: "allScripts", + blockType: BlockType.REPORTER, + text: "all scripts" + }, + { + opcode: "scriptExists", + blockType: BlockType.BOOLEAN, + text: "script named [NAME] exists?", + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "Script1" } + }, + }, + "---", + { + opcode: "addBlocksTo", + blockType: BlockType.COMMAND, + text: ["add blocks", "to script [NAME]"], + branchCount: 1, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "Script1" } + }, + }, + { + opcode: "JGreturn", + text: "return [THING]", + blockType: BlockType.COMMAND, + isTerminal: true, + arguments: { + THING: { type: ArgumentType.STRING, defaultValue: "1" } + }, + }, + "---", + { + opcode: "scriptData", + text: "script data", + blockType: BlockType.REPORTER, + allowDropAnywhere: true, + disableMonitor: true + }, + "---", + { + opcode: "runBlocks", + text: "run script [NAME] in [SPRITE]", + blockType: BlockType.LOOP, + branchCount: 0, + branchIconURI: "", + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "Script1" }, + SPRITE: { type: ArgumentType.STRING, menu: "TARGETS" } + }, + }, + { + opcode: "runBlocksData", + text: "run script [NAME] in [SPRITE] with data [DATA]", + blockType: BlockType.LOOP, + branchCount: 0, + branchIconURI: "", + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "Script1" }, + SPRITE: { type: ArgumentType.STRING, menu: "TARGETS" }, + DATA: { type: ArgumentType.STRING, defaultValue: "data" } + }, + }, + "---", + { + opcode: "reportBlocks", + text: "run script [NAME] in [SPRITE]", + blockType: BlockType.REPORTER, + allowDropAnywhere: true, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "Script1" }, + SPRITE: { type: ArgumentType.STRING, menu: "TARGETS" } + }, + }, + { + opcode: "reportBlocksData", + text: "run script [NAME] in [SPRITE] with data [DATA]", + blockType: BlockType.REPORTER, + allowDropAnywhere: true, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "Script1" }, + SPRITE: { type: ArgumentType.STRING, menu: "TARGETS" }, + DATA: { type: ArgumentType.STRING, defaultValue: "data" } + }, + } + ], + menus: { + TARGETS: { acceptReporters: true, items: "getTargets" } + }, + }; + } + + getTargets() { + const spriteNames = [ + { text: "myself", value: "_myself_" }, + { text: "Stage", value: "_stage_" } + ]; + const targets = this.runtime.targets; + for (let index = 1; index < targets.length; index++) { + const target = targets[index]; + if (target.isOriginal) spriteNames.push({ + text: target.getName(), value: target.getName() + }); + } + return spriteNames.length > 0 ? spriteNames : [""]; + } + + createScript(args) { this.scripts[Cast.toString(args.NAME)] = { blocks: [] } } + + deleteScript(args) { delete this.scripts[Cast.toString(args.NAME)] } + + deleteAll() { this.scripts = {} } + + allScripts() { return JSON.stringify(Object.keys(this.scripts)) } + + scriptExists(args) { return Cast.toBoolean(this.scripts[args.NAME]) } + + addBlocksTo(args, util) { + const name = Cast.toString(args.NAME); + const branch = util.thread.target.blocks.getBranch(util.thread.peekStack(), 1); + if (branch && this.scripts[name] !== undefined) { + this.scripts[name].blocks.push({ stack : branch, target : util.target }); + } + } + + JGreturn(args, util) { util.thread.report = Cast.toString(args.THING) } + + scriptData(args, util) { + const data = util.thread.scriptData; + return data ? data : ""; + } + + runBlocksData(args, util) { this.runBlocks(args, util) } + runBlocks(args, util) { + const target = args.SPRITE === "_myself_" ? util.target : + args.SPRITE === "_stage_" ? this.runtime.getTargetForStage() : this.runtime.getSpriteTargetByName(args.SPRITE); + const name = Cast.toString(args.NAME); + const data = args.DATA ? Cast.toString(args.DATA) : ""; + if (this.scripts[name] === undefined || !target) return; + + if (util.stackFrame.JGindex === undefined) util.stackFrame.JGindex = 0; + if (util.stackFrame.JGthread === undefined) util.stackFrame.JGthread = ""; + const blocks = this.scripts[name].blocks; + const index = util.stackFrame.JGindex; + const thread = util.stackFrame.JGthread; + if (!thread && index < blocks.length) { + const thisStack = blocks[index]; + if (thisStack.target.blocks.getBlock(thisStack.stack) !== undefined) { + util.stackFrame.JGthread = this.runtime._pushThread(thisStack.stack, thisStack.target, { stackClick: false }); + util.stackFrame.JGthread.scriptData = data; + util.stackFrame.JGthread.target = target; + util.stackFrame.JGthread.tryCompile(); // update thread + } + util.stackFrame.JGindex = util.stackFrame.JGindex + 1; + } + + if (util.stackFrame.JGthread && this.runtime.isActiveThread(util.stackFrame.JGthread)) util.startBranch(1, true); + else util.stackFrame.JGthread = ""; + if (util.stackFrame.JGindex < blocks.length) util.startBranch(1, true); + } + + reportBlocksData(args, util) { return this.reportBlocks(args, util) || "" } + reportBlocks(args, util) { + const target = args.SPRITE === "_myself_" ? util.target : + args.SPRITE === "_stage_" ? this.runtime.getTargetForStage() : this.runtime.getSpriteTargetByName(args.SPRITE); + const name = Cast.toString(args.NAME); + const data = args.DATA ? Cast.toString(args.DATA) : ""; + if (this.scripts[name] === undefined || !target) return; + + if (util.stackFrame.JGindex === undefined) util.stackFrame.JGindex = 0; + if (util.stackFrame.JGthread === undefined) util.stackFrame.JGthread = ""; + const blocks = this.scripts[name].blocks; + const index = util.stackFrame.JGindex; + const thread = util.stackFrame.JGthread; + if (!thread && index < blocks.length) { + const thisStack = blocks[index]; + if (thisStack.target.blocks.getBlock(thisStack.stack) !== undefined) { + util.stackFrame.JGthread = this.runtime._pushThread(thisStack.stack, thisStack.target, { stackClick: false }); + util.stackFrame.JGthread.scriptData = data; + util.stackFrame.JGthread.target = target; + util.stackFrame.JGthread.tryCompile(); // update thread + } + util.stackFrame.JGindex = util.stackFrame.JGindex + 1; + } + + if (util.stackFrame.JGthread && this.runtime.isActiveThread(util.stackFrame.JGthread)) util.yield(); + else { + if (util.stackFrame.JGthread.report !== undefined) { + util.stackFrame.JGreport = util.stackFrame.JGthread.report; + util.stackFrame.JGindex = blocks.length + 1; + } + util.stackFrame.JGthread = ""; + } + if (util.stackFrame.JGindex < blocks.length) util.yield(); + return util.stackFrame.JGreport || ""; + } +} + +module.exports = JgScriptsBlocks; diff --git a/local-scratch-vm/src/extensions/jg_shaders/index.js b/local-scratch-vm/src/extensions/jg_shaders/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b27202eb312d854b0f2a5d5e72ba3e4688d38432 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_shaders/index.js @@ -0,0 +1,65 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +/** + * Class for Shaders blocks + * @constructor + */ +class jgShadersBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgShaders', + name: 'Shaders', + blocks: [ + { + opcode: 'enableShader', + text: 'enable [SHADER]', + blockType: BlockType.COMMAND, + arguments: { + SHADER: { + menu: "shaders" + } + } + }, + { + opcode: 'disableShader', + text: 'disable [SHADER]', + blockType: BlockType.COMMAND, + arguments: { + SHADER: { + menu: "shaders" + } + } + }, + ], + menus: { + shaders: { + items: [ + 'bloom' + ] + }, + } + }; + } + + enableShader(args) { + const shader = Cast.toString(args.SHADER).toLowerCase(); + } + disableShader(args) { + const shader = Cast.toString(args.SHADER).toLowerCase(); + } +} + +module.exports = jgShadersBlocks; diff --git a/local-scratch-vm/src/extensions/jg_storage/index.js b/local-scratch-vm/src/extensions/jg_storage/index.js new file mode 100644 index 0000000000000000000000000000000000000000..052a16bea0c48b4eca99ddb3e721c32ccadb272e --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_storage/index.js @@ -0,0 +1,486 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const uid = require('../../util/uid'); + +/** + * Class for storage blocks + * @constructor + */ +class JgStorageBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.currentServer = "https://storage-ext.penguinmod.com/"; + this.usePenguinMod = true; + this.useGlobal = true; + this.waitingForResponse = false; + this.serverFailedResponse = false; + this.serverError = ""; + + this.uniquePrefix = "u" + uid(); + // A value stored in the PMP of the project. + // This value should always be globally unique to + // every project. + // The chance that 2 projects have the same "unique" + // prefix is about very small. + // The u at the start is to make sure that it can never + // be mistaken for a project id. + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgStorage', + name: 'Storage', + color1: '#76A8FE', + color2: '#538EFC', + docsURI: 'https://docs.penguinmod.com/extensions/storage', + blocks: [ + { + blockType: BlockType.LABEL, + text: "Local Storage" + }, + { + opcode: 'getValue', + text: 'get [KEY]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + }, + } + }, + { + opcode: 'setValue', + text: 'set [KEY] to [VALUE]', + blockType: BlockType.COMMAND, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "value" + }, + } + }, + { + opcode: 'deleteValue', + text: 'delete [KEY]', + blockType: BlockType.COMMAND, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + } + } + }, + { + opcode: 'getKeys', + text: 'get all stored names', + disableMonitor: true, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: "Local Uploaded Project Storage" + }, + { + opcode: 'getProjectValue', + text: 'get uploaded project [KEY]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + }, + } + }, + { + opcode: 'setProjectValue', + text: 'set uploaded project [KEY] to [VALUE]', + blockType: BlockType.COMMAND, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "value" + }, + } + }, + { + opcode: 'deleteProjectValue', + text: 'delete uploaded project [KEY]', + blockType: BlockType.COMMAND, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + } + } + }, + { + opcode: 'getProjectKeys', + text: 'get all stored names in this uploaded project', + disableMonitor: true, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: "Local Project Storage" + }, + { + opcode: 'getUniqueValue', + text: 'get local project [KEY]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + }, + } + }, + { + opcode: 'setUniqueValue', + text: 'set local project [KEY] to [VALUE]', + blockType: BlockType.COMMAND, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "value" + }, + } + }, + { + opcode: 'deleteUniqueValue', + text: 'delete local project [KEY]', + blockType: BlockType.COMMAND, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + } + } + }, + { + opcode: 'getUniqueKeys', + text: 'get all stored names in this local project', + disableMonitor: true, + blockType: BlockType.REPORTER + }, + { + blockType: BlockType.LABEL, + text: "Server Storage" + }, + { + opcode: 'isGlobalServer', + text: 'is using global server?', + disableMonitor: true, + blockType: BlockType.BOOLEAN + }, + { + opcode: 'useCertainServer', + text: 'set server to [SERVER] server', + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + SERVER: { + type: ArgumentType.STRING, + menu: "serverType" + }, + } + }, + { + opcode: 'waitingForConnection', + text: 'waiting for server to respond?', + disableMonitor: true, + blockType: BlockType.BOOLEAN + }, + { + opcode: 'connectionFailed', + text: 'server failed to respond?', + disableMonitor: true, + blockType: BlockType.BOOLEAN + }, + { + opcode: 'serverErrorOutput', + text: 'server error', + disableMonitor: false, + blockType: BlockType.REPORTER + }, + "---", + { + opcode: 'getServerValue', + text: 'get server [KEY]', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + }, + } + }, + { + opcode: 'setServerValue', + text: 'set server [KEY] to [VALUE]', + blockType: BlockType.COMMAND, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "value" + }, + } + }, + { + opcode: 'deleteServerValue', + text: 'delete server [KEY]', + blockType: BlockType.COMMAND, + arguments: { + KEY: { + type: ArgumentType.STRING, + defaultValue: "key" + } + } + } + ], + menus: { + serverType: { + acceptReporters: true, + items: [ + "project", + "global" + ].map(item => ({ text: item, value: item })) + } + } + }; + } + // Storage + serialize() { + return { uniqueId: this.uniquePrefix } + } + + deserialize(data) { + this.uniquePrefix = data.uniqueId; + } + + // utilities + /** + * @returns {string} Prefix for any keys saved + */ + getPrefix(projectId) { + return `PM_PROJECTSTORAGE_EXT_${projectId == null ? "" : `${projectId}_`}`; + } + getAllKeys(projectId) { + return Object.keys(localStorage).filter(key => key.startsWith(this.getPrefix(projectId))).map(key => key.replace(this.getPrefix(projectId), "")); + } + getProjectId() { + /* todo: get the project id in a like 190x better way lol */ + const hash = String(window.location.hash).replace(/#/gmi, ""); + return Cast.toNumber(hash); + } + + runPenguinWebRequest(url, options, ifFailReturn) { + this.waitingForResponse = true; + this.serverFailedResponse = false; + this.serverError = ""; + return new Promise((resolve) => { + let promise = null; + if (options !== null) { + promise = fetch(url, options); + } else { + promise = fetch(url); + } + promise.then(response => { + response.text().then(text => { + if (!response.ok) { + this.waitingForResponse = false; + this.serverFailedResponse = true; + this.serverError = Cast.toString(text); + if (ifFailReturn !== null) { + return resolve(ifFailReturn); + } + resolve(text); + return; + } + this.waitingForResponse = false; + this.serverFailedResponse = false; + this.serverError = ""; + resolve(text); + }).catch(err => { + this.waitingForResponse = false; + this.serverFailedResponse = true; + this.serverError = Cast.toString(err); + if (ifFailReturn !== null) { + return resolve(ifFailReturn); + } + resolve(err); + }) + }).catch(err => { + this.waitingForResponse = false; + this.serverFailedResponse = true; + this.serverError = Cast.toString(err); + if (ifFailReturn !== null) { + return resolve(ifFailReturn); + } + resolve(err); + }) + }) + } + + getCurrentServer() { + return `https://storage-ext.penguinmod.com/` + } + + // blocks + getKeys() { + return JSON.stringify(this.getAllKeys()); + } + getValue(args) { + const key = this.getPrefix() + Cast.toString(args.KEY); + + const returned = localStorage.getItem(key); + if (returned === null) return ""; + return Cast.toString(returned); + } + setValue(args) { + const key = this.getPrefix() + Cast.toString(args.KEY); + const value = Cast.toString(args.VALUE); + + return localStorage.setItem(key, value); + } + deleteValue(args) { + const key = this.getPrefix() + Cast.toString(args.KEY); + + return localStorage.removeItem(key); + } + + // project blocks + getProjectKeys() { + return JSON.stringify(this.getAllKeys(this.getProjectId())); + } + getProjectValue(args) { + const key = this.getPrefix(this.getProjectId()) + Cast.toString(args.KEY); + + const returned = localStorage.getItem(key); + if (returned === null) return ""; + return Cast.toString(returned); + } + setProjectValue(args) { + const key = this.getPrefix(this.getProjectId()) + Cast.toString(args.KEY); + const value = Cast.toString(args.VALUE); + + return localStorage.setItem(key, value); + } + deleteProjectValue(args) { + const key = this.getPrefix(this.getProjectId()) + Cast.toString(args.KEY); + + return localStorage.removeItem(key); + } + + // global unique blocks + getUniqueKeys() { + return JSON.stringify(this.getAllKeys(this.uniquePrefix)); + } + getUniqueValue(args) { + const key = this.getPrefix(this.uniquePrefix) + Cast.toString(args.KEY); + + const returned = localStorage.getItem(key); + if (returned === null) return ""; + return Cast.toString(returned); + } + setUniqueValue(args) { + const key = this.getPrefix(this.uniquePrefix) + Cast.toString(args.KEY); + const value = Cast.toString(args.VALUE); + + return localStorage.setItem(key, value); + } + deleteUniqueValue(args) { + const key = this.getPrefix(this.uniquePrefix) + Cast.toString(args.KEY); + + return localStorage.removeItem(key); + } + + // server blocks + isGlobalServer() { + return this.useGlobal; + } + useCertainServer(args) { + const serverType = Cast.toString(args.SERVER).toLowerCase(); + if (["project", "global"].includes(serverType)) { + // this is a menu option + this.currentServer = "https://storage-ext.penguinmod.com/"; + this.usePenguinMod = true; + this.useGlobal = serverType === "global"; + } else { + // this is a url + this.currentServer = Cast.toString(args.SERVER); + if (!this.currentServer.endsWith("/")) { + this.currentServer += "/"; + } + this.usePenguinMod = false; + this.useGlobal = true; + } + // now lets wait until the server responds saying it is online + return this.runPenguinWebRequest(this.currentServer); + } + waitingForConnection() { + return this.waitingForResponse; + } + connectionFailed() { + return this.serverFailedResponse; + } + serverErrorOutput() { + return this.serverError; + } + + getServerValue(args) { + const key = Cast.toString(args.KEY); + + return this.runPenguinWebRequest(`${this.currentServer}get?key=${key}${this.useGlobal ? "" : `&project=${this.getProjectId()}`}`, null, ""); + } + setServerValue(args) { + const key = Cast.toString(args.KEY); + const value = Cast.toString(args.VALUE); + + return this.runPenguinWebRequest(`${this.currentServer}set?key=${key}${this.useGlobal ? "" : `&project=${this.getProjectId()}`}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + "value": value + }) + }); + } + deleteServerValue(args) { + const key = Cast.toString(args.KEY); + + return this.runPenguinWebRequest(`${this.currentServer}delete?key=${key}${this.useGlobal ? "" : `&project=${this.getProjectId()}`}`, { + method: "DELETE" + }); + } +} + +module.exports = JgStorageBlocks; diff --git a/local-scratch-vm/src/extensions/jg_tailgating/index.js b/local-scratch-vm/src/extensions/jg_tailgating/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0ad2c539f8b7a6c0a29baac854bab13286a11db5 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_tailgating/index.js @@ -0,0 +1,221 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +class TailgatingExtension { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.trackers = Object.create(null); + this.maxSaving = Object.create(null); + this.positions = Object.create(null); + + const shouldSaveNewPosition = (positionsList, tracker) => { + const firstPos = positionsList[0]; + if (typeof firstPos !== "object") return true; + if (firstPos.x !== tracker.x || firstPos.y !== tracker.y) { + return true; + } + return false; + }; + + this.runtime.on('RUNTIME_STEP_START', () => { + for (const trackerName in this.trackers) { + const tracker = this.trackers[trackerName]; + // happens when sprite is deleted or clone is deleted + if (tracker.isDisposed) { + this.stopTrackingSprite({ NAME: trackerName }); + continue; + } + // 0 positions should be saved, so just dont make them at all + const positions = this.positions[trackerName]; + const maxPositions = this.maxSaving[trackerName]; + if (maxPositions <= 0) continue; + // only track new positions when they have changed + // we have no reason to track the same position multiple times (that would make this ext useless) + if (shouldSaveNewPosition(positions, tracker)) { + // console.log('saved new pos for', trackerName); + positions.unshift({ x: tracker.x, y: tracker.y }); + } + this.positions[trackerName] = positions.slice(0, maxPositions); + } + }); + } + + getInfo() { + return { + id: "jgTailgating", + name: "Tailgating", + blocks: [ + { + opcode: "startTrackingSprite", + blockType: BlockType.COMMAND, + text: "start tracking [SPRITE] as [NAME]", + arguments: { + SPRITE: { + type: ArgumentType.STRING, + menu: "spriteMenu", + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "leader", + } + }, + }, + { + opcode: "stopTrackingSprite", + blockType: BlockType.COMMAND, + text: "stop tracking [NAME]", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "leader", + } + }, + }, + '---', + { + opcode: "followSprite", + blockType: BlockType.COMMAND, + text: "follow [INDEX] positions behind [NAME]", + arguments: { + INDEX: { + type: ArgumentType.NUMBER, + defaultValue: 20, + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "leader", + } + }, + }, + { + opcode: "savePositionsBehindSprite", + blockType: BlockType.COMMAND, + text: "set max saved positions behind [NAME] to [MAX]", + arguments: { + MAX: { + type: ArgumentType.NUMBER, + defaultValue: 20, + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "leader", + } + }, + }, + { + opcode: "getSpriteFollowPos", + blockType: BlockType.REPORTER, + disableMonitor: true, + text: "get position [INDEX] behind [NAME]", + arguments: { + INDEX: { + type: ArgumentType.NUMBER, + defaultValue: 20, + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "leader", + } + }, + }, + ], + menus: { + spriteMenu: '_getSpriteMenu' + }, + }; + } + + // menus + _getSpriteMenu() { + const emptyMenu = [{ text: '', value: '' }]; + const sprites = []; + if (this.runtime.vm.editingTarget && !this.runtime.vm.editingTarget.isStage) { + sprites.push({ text: 'this sprite', value: '_myself_' }); + } + for (const target of this.runtime.targets) { + if (!target.isOriginal) continue; + if (target.isStage) continue; + if (this.runtime.vm.editingTarget && this.runtime.vm.editingTarget.id === target.id) continue; + const name = target.getName(); + sprites.push({ + text: name, + value: name + }); + } + return sprites.length > 0 ? sprites : emptyMenu; + } + + // blocks + startTrackingSprite(args, util) { + const spriteName = Cast.toString(args.SPRITE); + const trackerName = Cast.toString(args.NAME); + const pickedSprite = spriteName === '_myself_' ? util.target : this.runtime.getSpriteTargetByName(spriteName); + if (!pickedSprite) return; + this.trackers[trackerName] = pickedSprite; + this.positions[trackerName] = []; + if (!(trackerName in this.maxSaving)) { + this.maxSaving[trackerName] = 20; + } + } + stopTrackingSprite(args) { + const trackerName = Cast.toString(args.NAME); + delete this.trackers[trackerName]; + this.positions[trackerName] = []; + } + + followSprite(args, util) { + const trackerName = Cast.toString(args.NAME); + const index = Cast.toNumber(args.INDEX); + const spritePositions = this.positions[trackerName]; + if (!spritePositions) return; + let position = spritePositions[index]; + if (typeof position !== "object") { + // this index position was not found + // use the last one in the list instead + + // if there is nothing in the list, dont do anything + if (spritePositions.length <= 0) return; + position = spritePositions[spritePositions.length - 1]; + } + util.target.setXY(position.x, position.y); + } + getSpriteFollowPos(args) { + const trackerName = Cast.toString(args.NAME); + const index = Cast.toNumber(args.INDEX); + const spritePositions = this.positions[trackerName]; + if (!spritePositions) return '{}'; + let position = spritePositions[index]; + if (typeof position !== "object") { + // this index position was not found + // use the last one in the list instead + + // if there is nothing in the list, dont do anything + if (spritePositions.length <= 0) return '{}'; + position = spritePositions[spritePositions.length - 1]; + } + return JSON.stringify({ + x: position.x, + y: position.y + }); + } + savePositionsBehindSprite(args, util) { + const trackerName = Cast.toString(args.NAME); + const maxPositions = Cast.toNumber(args.MAX); + let max = Math.round(maxPositions); + if (max <= 0) { + max = 0; + } + if (max > 0) { + max++; + } + this.maxSaving[trackerName] = max; + } +} + +module.exports = TailgatingExtension; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_timers/Timer.js b/local-scratch-vm/src/extensions/jg_timers/Timer.js new file mode 100644 index 0000000000000000000000000000000000000000..44a0e348d5125c6b65893b66ca1918243f1192f1 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_timers/Timer.js @@ -0,0 +1,58 @@ +class Timer { + constructor (startingTime, pausingTime) { + this.startTime = startingTime ? startingTime : Date.now(); + if (pausingTime) { + this.pauseTime = pausingTime; + } + this.stopped = true; + } + + start (vmUnpause = false) { + const paused = (this.pauseTime !== null); + // check if we are stopped or paused before continuing + if (!(this.stopped || paused) || (paused && (vmUnpause && !this.vmPaused))) return; + if (this.stopped) { + this.startTime = Date.now(); + } else { + // we are unpausing + this.startTime += Date.now() - this.pauseTime; + } + this.vmPaused = false; + this.pauseTime = null; + this.stopped = false; + } + pause (vmPause = false) { + const paused = (this.pauseTime !== null); + if (paused) return; + this.vmPaused = vmPause; + this.pauseTime = Date.now(); + } + stop () { + if (this.stopped) return; + this.stopped = true; + this.pauseTime = Date.now(); + } + + reset () { + this.stopped = true; + this.pauseTime = Date.now(); + this.startTime = Date.now(); + } + + add (seconds) { + this.startTime -= seconds; + } + + getTime (inSeconds) { + const paused = (this.pauseTime !== null); + + const pausedTime = Number(this.pauseTime) - this.startTime; + const normalTime = Date.now() - this.startTime; + + const divisor = inSeconds ? 1000 : 1; + + return (paused ? pausedTime : normalTime) / divisor; + } +} + +module.exports = Timer; diff --git a/local-scratch-vm/src/extensions/jg_timers/index.js b/local-scratch-vm/src/extensions/jg_timers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e6ae71239cfb3d344716682e613f56a6adf765b2 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_timers/index.js @@ -0,0 +1,248 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const Timer = require('./Timer'); + +/** + * Class for Timers blocks + * @constructor + */ +class JgTimersBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + this.timers = {}; + // pause/unpause timers when the project pauses + runtime.on("RUNTIME_PAUSED", () => { + this._getTimersArray().forEach(timer => timer.instance.pause(true)); + }); + runtime.on("RUNTIME_UNPAUSED", () => { + this._getTimersArray().forEach(timer => timer.instance.start(true)); + }); + } + + // util + + _getTimersArray() { + return Object.values(this.timers); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgTimers', + name: 'Multiple Timers', + color1: '#0093FE', + color2: '#1177FC', + blocks: [ + { + opcode: 'createTimer', + text: 'create timer named [NAME]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + { + opcode: 'deleteTimer', + text: 'delete timer named [NAME]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + { + opcode: 'deleteAllTimer', + text: 'delete all timers', + blockType: BlockType.COMMAND + }, + + { text: "Values", blockType: BlockType.LABEL, }, + + { + opcode: 'getTimer', + text: 'get timer named [NAME]', + blockType: BlockType.REPORTER, + disableMonitor: false, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + { + opcode: 'getTimerData', + text: 'get [DATA] of timer named [NAME]', + blockType: BlockType.REPORTER, + disableMonitor: false, + arguments: { + DATA: { type: ArgumentType.STRING, menu: "timerData" }, + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + { + opcode: 'existsTimer', + text: 'timer named [NAME] exists?', + blockType: BlockType.BOOLEAN, + disableMonitor: false, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + + { + opcode: 'getAllTimer', + text: 'get all timers', + blockType: BlockType.REPORTER, + disableMonitor: false + }, + + { text: "Operations", blockType: BlockType.LABEL, }, + + { + opcode: 'startTimer', + text: 'start timer [NAME]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + { + opcode: 'pauseTimer', + text: 'pause timer [NAME]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + { + opcode: 'stopTimer', + text: 'stop timer [NAME]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + { + opcode: 'resetTimer', + text: 'reset timer [NAME]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + { + opcode: 'addTimer', + text: 'add [SECONDS] seconds to timer [NAME]', + blockType: BlockType.COMMAND, + arguments: { + SECONDS: { type: ArgumentType.NUMBER, defaultValue: 5 }, + NAME: { type: ArgumentType.STRING, defaultValue: "timer" } + } + }, + ], + menus: { + timerData: { + acceptReporters: true, + items: [ + "milliseconds", + "minutes", + "hours", + // haha funny options + "days", + "weeks", + "years" + ].map(item => ({ text: item, value: item })) + } + } + }; + } + + // blocks + + createTimer(args) { + const timer = this.timers[args.NAME]; + if (timer) return; + this.timers[args.NAME] = { + name: Cast.toString(args.NAME), + instance: new Timer() + }; + } + deleteTimer(args) { + const timer = this.timers[args.NAME]; + if (!timer) return; + delete this.timers[args.NAME]; + } + deleteAllTimer() { + this.timers = {}; + } + + getTimer(args) { + const timer = this.timers[args.NAME]; + if (!timer) return ""; + const time = timer.instance.getTime(true); + return Cast.toNumber(time); + } + getTimerData(args) { + const timer = this.timers[args.NAME]; + if (!timer) return ""; + const seconds = Cast.toNumber(timer.instance.getTime(true)); + switch (args.DATA) { + case "milliseconds": + return seconds * 1000; + case "minutes": + return Math.floor(seconds / 60); + case "hours": + return Math.floor(seconds / 3600); + case "days": + return Math.floor(seconds / 86400); + case "weeks": + return Math.floor(seconds / 604800); + case "years": + return Math.floor(seconds / 31536000); + default: + return seconds; + } + } + existsTimer(args) { + const timer = this.timers[args.NAME]; + if (!timer) return false; + return true; + } + getAllTimer() { + return JSON.stringify(this._getTimersArray().map(timer => timer.name)); + } + + startTimer(args) { + const timer = this.timers[args.NAME]; + if (!timer) return; + timer.instance.start(); + } + pauseTimer(args) { + const timer = this.timers[args.NAME]; + if (!timer) return; + timer.instance.pause(); + } + stopTimer(args) { + const timer = this.timers[args.NAME]; + if (!timer) return; + timer.instance.stop(); + } + resetTimer(args) { + const timer = this.timers[args.NAME]; + if (!timer) return; + timer.instance.reset(); + } + + addTimer(args) { + const timer = this.timers[args.NAME]; + if (!timer) return; + const seconds = Cast.toNumber(args.SECONDS); + timer.instance.add(seconds * 1000); + } +} + +module.exports = JgTimersBlocks; diff --git a/local-scratch-vm/src/extensions/jg_tween/index.js b/local-scratch-vm/src/extensions/jg_tween/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7387c06de201d1c5ced1bb64f9d84d75d3f0cd30 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_tween/index.js @@ -0,0 +1,522 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +/** + * @param {number} time should be 0-1 + * @param {number} a value at 0 + * @param {number} b value at 1 + * @returns {number} + */ +const interpolate = (time, a, b) => { + // don't restrict range of time as some easing functions are expected to go outside the range + const multiplier = b - a; + return time * multiplier + a; +}; + +const linear = x => x; + +const sine = (x, dir) => { + switch (dir) { + case "in": return 1 - Math.cos((x * Math.PI) / 2); + case "out": return Math.sin((x * Math.PI) / 2); + case "in out": return -(Math.cos(Math.PI * x) - 1) / 2; + default: return 0; + } +}; + +const quad = (x, dir) => { + switch (dir) { + case "in": return x * x; + case "out": return 1 - (1 - x) * (1 - x); + case "in out": return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; + default: return 0; + } +}; + +const cubic = (x, dir) => { + switch (dir) { + case "in": return x * x * x; + case "out": return 1 - Math.pow(1 - x, 3); + case "in out": return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; + default: return 0; + } +}; + +const quart = (x, dir) => { + switch (dir) { + case "in": return x * x * x * x; + case "out": return 1 - Math.pow(1 - x, 4); + case "in out": return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2; + default: return 0; + } +}; + +const quint = (x, dir) => { + switch (dir) { + case "in": return x * x * x * x * x; + case "out": return 1 - Math.pow(1 - x, 5); + case "in out": return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2; + default: + return 0; + } +}; + +const expo = (x, dir) => { + switch (dir) { + case "in": return x === 0 ? 0 : Math.pow(2, 10 * x - 10); + case "out": return x === 1 ? 1 : 1 - Math.pow(2, -10 * x); + case "in out": return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 + ? Math.pow(2, 20 * x - 10) / 2 : (2 - Math.pow(2, -20 * x + 10)) / 2; + default: return 0; + } +}; + +const circ = (x, dir) => { + switch (dir) { + case "in": return 1 - Math.sqrt(1 - Math.pow(x, 2)); + case "out": return Math.sqrt(1 - Math.pow(x - 1, 2)); + case "in out": return x < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 + : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2; + default: return 0; + } +}; + +const back = (x, dir) => { + const c1 = 1.70158; + const c2 = c1 * 1.525; + const c3 = c1 + 1; + switch (dir) { + case "in": return c3 * x * x * x - c1 * x * x; + case "out": return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); + case "in out": return x < 0.5 ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2 + : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; + default: return 0; + } +}; + +const elastic = (x, dir) => { + const c4 = (2 * Math.PI) / 3; + const c5 = (2 * Math.PI) / 4.5; + switch (dir) { + case "in": return x === 0 ? 0 : x === 1 ? 1 : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4); + case "out": return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; + case "in out": return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 + ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1; + default: return 0; + } +}; + +const bounce = (x, dir) => { + switch (dir) { + case "in": return 1 - bounce(1 - x, "out"); + case "out": { + const n1 = 7.5625, d1 = 2.75; + if (x < 1 / d1) return n1 * x * x; + else if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + 0.75; + else if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + 0.9375; + return n1 * (x -= 2.625 / d1) * x + 0.984375; + } + case "in out": return x < 0.5 ? (1 - bounce(1 - 2 * x, "out")) / 2 : (1 + bounce(2 * x - 1, "out")) / 2; + default: return 0; + } +}; + +const EasingMethods = { + linear, sine, quad, cubic, quart, + quint, expo, circ, back, elastic, bounce +}; + +class Tween { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + getInfo() { + return { + id: "jgTween", + name: "Tweening", + blocks: [ + { + opcode: "tweenValue", + text: formatMessage({ + id: 'jgTween.blocks.tweenValue', + default: '[MODE] ease [DIRECTION] [START] to [END] by [AMOUNT]%', + description: 'Block for easing a value with a certain mode and direction by a certain amount.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + MODE: { + type: ArgumentType.STRING, + menu: "modes" + }, + DIRECTION: { + type: ArgumentType.STRING, + menu: "direction" + }, + START: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + END: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + AMOUNT: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: "tweenVariable", + text: "tween variable [VAR] to [VALUE] over [SEC] seconds using [MODE] ease [DIRECTION]", + blockType: BlockType.COMMAND, + arguments: { + VAR: { + type: ArgumentType.STRING, + menu: "vars" + }, + VALUE: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + SEC: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + MODE: { + type: ArgumentType.STRING, + menu: "modes" + }, + DIRECTION: { + type: ArgumentType.STRING, + menu: "direction" + } + } + }, + { + opcode: "tweenXY", + text: "tween to x: [X] y: [Y] over [SEC] seconds using [MODE] ease [DIRECTION]", + blockType: BlockType.COMMAND, + arguments: { + PROPERTY: { + type: ArgumentType.STRING, + menu: "properties" + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + SEC: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + MODE: { + type: ArgumentType.STRING, + menu: "modes" + }, + DIRECTION: { + type: ArgumentType.STRING, + menu: "direction" + } + } + }, + { + opcode: "tweenProperty", + text: "tween [PROPERTY] to [VALUE] over [SEC] seconds using [MODE] ease [DIRECTION]", + blockType: BlockType.COMMAND, + arguments: { + PROPERTY: { + type: ArgumentType.STRING, + menu: "properties" + }, + VALUE: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + SEC: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + MODE: { + type: ArgumentType.STRING, + menu: "modes" + }, + DIRECTION: { + type: ArgumentType.STRING, + menu: "direction" + } + } + }, + "---", + { + opcode: "tweenVariableCancel", + text: "cancel tween for variable [VAR]", + blockType: BlockType.COMMAND, + arguments: { + VAR: { + type: ArgumentType.STRING, + menu: "vars" + } + } + }, + { + opcode: "tweenPropertyCancel", + text: "cancel tween for [PROPERTY]", + blockType: BlockType.COMMAND, + arguments: { + PROPERTY: { + type: ArgumentType.STRING, + menu: "properties" + } + } + }, + "---", + { + opcode: "tweenC", blockType: BlockType.LOOP, + text: "[MODE] ease [DIRECTION] [CHANGE] [START] to [END] in [SEC] secs", + arguments: { + MODE: { + type: ArgumentType.STRING, + menu: "modes", + }, + DIRECTION: { + type: ArgumentType.STRING, + menu: "direction", + }, + CHANGE: { + type: ArgumentType.STRING, + fillIn: "tweenVal" + }, + START: { + type: ArgumentType.NUMBER, + defaultValue: 0, + }, + END: { + type: ArgumentType.NUMBER, + defaultValue: 100, + }, + SEC: { + type: ArgumentType.NUMBER, + defaultValue: 1, + }, + } + }, + { + opcode: "tweenVal", blockType: BlockType.REPORTER, + text: "tween value", canDragDuplicate: true, hideFromPalette: true + }, + ], + menus: { + modes: { + acceptReporters: true, + items: Object.keys(EasingMethods) + }, + direction: { + acceptReporters: true, + items: ["in", "out", "in out"] + }, + vars: { + acceptReporters: false, // for Scratch parity + items: "getVariables" + }, + properties: { + acceptReporters: true, + items: ["x position", "y position", "direction", "size"] + } + } + }; + } + + getVariables() { + const variables = + // @ts-expect-error + typeof Blockly === "undefined" ? [] : + // @ts-expect-error + Blockly.getMainWorkspace() + .getVariableMap().getVariablesOfType("") + .map(model => ({ text: model.name, value: model.getId() })); + if (variables.length > 0) return variables; + return [{ text: "", value: "" }]; + + } + + tweenValue(args) { + const easeMethod = Cast.toString(args.MODE); + const easeDirection = Cast.toString(args.DIRECTION); + const start = Cast.toNumber(args.START); + const end = Cast.toNumber(args.END); + const progress = Cast.toNumber(args.AMOUNT) / 100; + + if (!Object.prototype.hasOwnProperty.call(EasingMethods, easeMethod)) { + // Unknown method + return start; + } + const easingFunction = EasingMethods[easeMethod]; + + const tweened = easingFunction(progress, easeDirection); + return interpolate(tweened, start, end); + } + + _tweenValue(args, util, id, valueArgName, currentValue, propertyName) { + // Only use args on first run. For later executions grab everything from stackframe. + // This ensures that if the arguments change, the tweening won't change. This matches + // the vanilla Scratch glide blocks. + const state = util.stackFrame[id]; + + if (!state) { + // First run, need to start timer + util.yield(); + + if (util.stackTimerNeedsInit()) { + const durationMS = Math.max(0, 1000 * Cast.toNumber(args.SEC)); + util.startStackTimer(durationMS); + } + const easeMethod = Cast.toString(args.MODE); + const easeDirection = Cast.toString(args.DIRECTION); + const start = currentValue; + const end = Cast.toNumber(args[valueArgName]); + + let easingFunction; + if (Object.prototype.hasOwnProperty.call(EasingMethods, easeMethod)) easingFunction = EasingMethods[easeMethod]; + else easingFunction = EasingMethods.linear; + + util.stackFrame[id] = { + easingFunction, easeDirection, + start, end, propertyName + }; + return start; + } else if (util.stackTimerFinished()) { + // Done + return util.stackFrame[id].end; + } + // Still running + util.yield(); + + const progress = util.stackFrame.timer.timeElapsed() / util.stackFrame.duration; + const tweened = state.easingFunction(progress, state.easeDirection); + return interpolate(tweened, state.start, state.end); + } + + tweenVariable(args, util) { + const variable = util.target.lookupVariableById(args.VAR); + + if (util.stackFrame[""] && util.stackFrame[""].cancelled) { + return; + } + + const value = this._tweenValue(args, util, "", "VALUE", variable.value, args.VAR); + if (variable && variable.type === "") variable.value = value; + } + + tweenXY(args, util) { + const stateX = util.stackFrame["x"] || {}; + const stateY = util.stackFrame["y"] || {}; + + const x = stateX.cancelled ? util.target.x : this._tweenValue(args, util, "x", "X", util.target.x, "x position"); + const y = stateY.cancelled ? util.target.y : this._tweenValue(args, util, "y", "Y", util.target.y, "y position"); + util.target.setXY(x, y); + } + + tweenProperty(args, util) { + let currentValue = 0; + if (args.PROPERTY === "x position") currentValue = util.target.x; + else if (args.PROPERTY === "y position") currentValue = util.target.y; + else if (args.PROPERTY === "direction") currentValue = util.target.direction; + else if (args.PROPERTY === "size") currentValue = util.target.size; + + if (util.stackFrame[""] && util.stackFrame[""].cancelled) { + return; + } + + const value = this._tweenValue(args, util, "", "VALUE", currentValue, args.PROPERTY); + + if (args.PROPERTY === "x position") util.target.setXY(value, util.target.y); + else if (args.PROPERTY === "y position") util.target.setXY(util.target.x, value); + else if (args.PROPERTY === "direction") util.target.setDirection(value); + else if (args.PROPERTY === "size") util.target.setSize(value); + } + + tweenVariableCancel(args, util) { + const property = args.VAR; + this.tweenPropertyCancel({ + PROPERTY: property + }, util); + } + tweenPropertyCancel(args, util) { + const property = args.PROPERTY; + const id = util.target.id; + + // supposedly for i loop is faster (garbo seemed to say this before too?) + for (let i = 0; i < this.runtime.threads.length; i++) { + const thread = this.runtime.threads[i]; + if (!thread.target) continue; + if (thread.target.id !== id) continue; + // some threads dont have a stackFrame from util + if (!thread.compatibilityStackFrame) continue; + // x position and y position should also cancel the tweenXY block + const propertyFrame = thread.compatibilityStackFrame[""] || + (property === "x position" ? thread.compatibilityStackFrame["x"] : null) || + (property === "y position" ? thread.compatibilityStackFrame["y"] : null); + // this thread did not have a property tween + if (!propertyFrame) continue; + // check if the property being tweened is the one we are cancelling + if (propertyFrame.propertyName !== property) continue; + propertyFrame.cancelled = true; + } + } + + tweenC(args, util) { + const id = "loopedVal"; + const state = util.stackFrame[id]; + if (!state) { + if (util.stackTimerNeedsInit()) { + const durationMS = Math.max(0, 1000 * Cast.toNumber(args.SEC)); + util.startStackTimer(durationMS); + } + const easeMethod = Cast.toString(args.MODE); + const easeDirection = Cast.toString(args.DIRECTION); + const start = Cast.toNumber(args.START); + const end = Cast.toNumber(args.END); + const params = util.thread.tweenValue; + if (typeof params === "undefined") util.thread.stackFrames[0].tweenValue = start; + let easingFunction; + if (Object.prototype.hasOwnProperty.call(EasingMethods, easeMethod)) easingFunction = EasingMethods[easeMethod]; + else easingFunction = EasingMethods.linear; + + util.stackFrame[id] = { + easingFunction, easeDirection, + start, end, + }; + util.startBranch(1, true); + } else if (util.stackTimerFinished()) { + util.thread.stackFrames[0].tweenValue = util.stackFrame[id].end; + if (util.stackFrame[id].canContinue !== "stop") { + util.stackFrame[id].canContinue = "stop"; + util.startBranch(1, true); + } + } else { + const progress = util.stackFrame.timer.timeElapsed() / util.stackFrame.duration; + const tweened = state.easingFunction(progress, state.easeDirection); + util.thread.stackFrames[0].tweenValue = interpolate(tweened, state.start, state.end); + if (util.stackFrame[id].canContinue !== "stop") util.startBranch(1, true); + } + } + + tweenVal(_, util) { + return util.thread.stackFrames[0].tweenValue ?? ""; + } +} + +module.exports = Tween; diff --git a/local-scratch-vm/src/extensions/jg_tween/turbowarp.js b/local-scratch-vm/src/extensions/jg_tween/turbowarp.js new file mode 100644 index 0000000000000000000000000000000000000000..6e5d60ced117abb50795c7bb6ead266244d8b3fe --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_tween/turbowarp.js @@ -0,0 +1,311 @@ +(function (Scratch) { + 'use strict'; + + /* + * extension thumbnail if needed + * (thumbnail is created by JeremyGamer13 + * incase this info is required for licensing) + * data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAF0CAMAAADIPrc+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAD5UExURQ+9jAuUbguUbQygdwygdg2rfw2pfQyccw60hQ6xgw65ifr8/P////7+/g61hrDc0Lff1G/AqX7Gsgyacs3p4TypilKzlwyYcQyXcA63hx+ceTWmhtTs5dzv6g2ofOTz7ur28g2mei2jgQ2keQyedUuwk4XJtpLPvprSwr7i2A67igyZcQ2jeYvMuaHVxlm2mxOXct3w6uv287Hd0M7p4ZPQvtXs5g2qfvL59wuWb/P5+HfDrZvTww2nfBmadQ68iyWffQ2vgvv9/IzMug+Wbw66ig64iEOsjguVbmi9pQ6zhGG5oA2tgA2ugeXz77/j2aLWxwyddMbm3TiKPaIAAAAJcEhZcwAADsMAAA7DAcdvqGQAAA9pSURBVHhe7d17txTFFYZxGRTB4A1FwUsMoIkBo4CKigJe0GjUaPz+HybTVU9fqruqL3PmkPTM8/tLqdp7Zq39rj5z5vTlKUmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJElS35lNin+WdneWMPU8zbK0g2eIUdY5NknLPEuCip5lozTfZKwqRkvLzIpVxWhpvvOkZpYLFEkTSMxslEljniMuXX+62Hief+p6gVKpiKy0XiRRHS+x1KJYKuh9an+ZKA1cYgNeoVzKIid4lRRlvcom0EDKSI5Xl0lQ0WtsDDxmqeh1QhKQnlFsDa7QROq5SkQqbxCdCW+yveIXWsojIBVyMwMFFdpICeJRGf3UnnqLkgqNpI4LpGOLzMxE0dYztJJahGNrwfGq8jZlW7SSGn8mG5vNOwRmtr9QuNlco5lUIxqbzXXiskD7hRbNJNwgGUs/YEWUbjbv0k6KCMZuueoki3ZS0Byw3iMpC71MuYcsJYjFrgcsD1nKag5YfyUni/2NBh6y1EEodj9gechSDpnYvE9KdvB3Whgs1W7eIhMnOGC1h6wP6Kpjdv4KcYjIyE5o4SHr6A3v9/EPMrKTD2lisI5Z/kpnIrIjmvgHw2NVuDPRFgnZEU08ZB2jF5h91kckZEfNV1m8lo7FNQZfQkB2RhuDdVReYeojyMfOaGOwjsdUqm7fIRsnQjODdSTG73V1+S6xODk6GqxjcI5hZ31MIvaErgbr4OXuSFT7hDTsEZ0N1oFL/1rTtecjVY3uBuuQFQ9Wb5KCU8ArGKzDVfot8FMicDp4EYN1qJhv3z3mf2p4HYN1kD5juj2nnqotXspgHaD8z8BT+A0wh1czWAeHwaYuMfZT9zkvaLAODGNNzLx/2l7wkgbrsDDUxP7+WjMHL7r5gnekA8BMu57kwarS3HOGt6T1Y6Jdi38LvF/dMob/3kl83S3elNaOeXYx67neo2wvwfLuyYeBcXZ8yaRnuUdRxD/uhBYesA4Cw+xY8DMwDVXAyg5oYLAOAaPsYMozUNDzNquLtbeL5L1ptRhkB0Oe9BX7M9ixGOXmavWYYwcjnpI8AWfgeXYtRbnBWrnmNh4NBjyBzSPYuBDF5mrdHjDFFvMd95DNoyaf+ZXT3CnSYK3YI2bYYryjvmZv3oX2R+vCxwdU2kcI8Ba1QoywxXTHjHxarx80yP9tUbMAhVuxl9ZncIUEsx1xn50Z3adX8k9b1M1G2RattDbfMMAGox1xnZ1D9KzxrxVKZ6KoQiutDONrTF4Y/y0bB27QsYulCuWzUFKhkdaF6TW+Y7JFX7Kxr/Ao1PMsV+gwAwUVnyq3Rp3HCQaPGWwR+/pol9O9ycNbdJnQeaScjxtfJYbXYLAld9nW8z3dCtgVzPsKns0BTbQi/bs7TpxznP924QeajWBnRLMRbIxooRVhdLWJWzpm/xiY+7Sewe5o4kv4f7ItooHWo//VFXMteMyurh/pNAMVNZpmsKFGudaDydWYawGbEldpNA9FjZfonGieb1KjWKtxhslh/HGCbOo6Q5/5hhdT/0R7DL/F8PfB1WFyNUabx56uz2izCLV9t9944zb/2Ueh1qJ3uBq9Xxp7uuiyHPUzUaTVYHA1EpQ1PNXqRFdhdb+En/AvSrQWvc86Y19d/cye1sxvF8pmRstYrQ6TqxGhnI/Z0ir8MXCZGdEyVqvTu4v2t2QoY3gCwyN6nBwNC9ikFWF0NTKUw44WHfbkaboO+EzxFfqB4WHkcMWOFh326Rdad+z0LYb+115nfCBDGZ+yozFx8sJJ3HwuuMn/an0ICUZuxcCOxq80kIZ6P3jIUAYbGmdpIGWQEpTvS9teIRot/5OgjgkxASHKYEODcinnV2ISfU2IhtjQoFzKSk/o+zcpGviNDbVrlEtZ5ASkaIj1BtVSVvoYuN6pda1LbKhRLeVdIygRKRpiveYVohpHUECKBj5ivUaxVEBQop+J0QDrNWqlEpISlU7o613C4NkFmvAuUYmI0QDLNWqlEpISlR6s1DuP4Ry1UglRiUrPAme5RqlURFQiYtR3h2V8Q6lU8j1ZichRH6s1SqWi9CkA5KiPVXhSsCaRleg3ctTT+9ROpVRGViJy1McqPI9B0whLRI76WAWF0gjCEpGjnvT041sUSmXJU5YeEqQeVkGhNOIGaQkKTwFgFRRKIwhLRI56vmM18lO7ZiAtEUHqYRHUSWNIS0SQelgEddIY0hIRpNQ9FiNPP9YcxCXIP2WJRVAmjblKXIKvSFKKRVAnjUlyRZB6WIweUCeN+YC8BAQpld5OlDJp1HSu0uu7KJNG/UheApKUYi3yMSKaZWGuqJLGkZfgOklKsRhRJY0jL0E2V+mpolRJ48hL8DtRSiQPLfF2opqHwATZXLEWeX2X5iEwwX2ilGAtokiaQGACc6V9ITABSUrcZy2iSJpAYAKilEhOkvFUUc1EYgKilLjMWuBDSzQTiQmIUiLJFTXSFBITEKUESxE10hQSExClBEsRNdIUEhMQpQRLwY/USFOITECUEiwFniSjuYhMQJQSLAXnqZGmEJmAKHXdZSkwV5qLyARkqSvJFSXSJCITkKWu31kKKJEmEZmALHWZK+3iJpEJyFJXcjEONdIUc6XT8A2RCchS122WKleokaaYK52GqcudWQk+oEaaYq50Gq6QmcptstTFUuDFOJrLXOk0EJngI7LUxVJwkxppCpEJzJX2hcgEucudWQookSYRmcBcaV+ITHCXLHWxFFAiTTlPZAJzpT1JckWUEiwF1EhTXiEyAVFKsBRQI02ZvLsoSwE10hQSExGlBEsBNdIUEhMRpQRLATXSFBITXCZKCdYCaqQJv5KYwFxpT66RmOAeUUqwFlAkTSAw0eTdRSmSJhCYiCQlkruLUiRNIDARUUqYKy2XXIyTzZWXO2u5swQm+JkoJcyVliMv0adEKXGdxYAqaRx5iUhSylxpOfISkaQUa4F3F9UszxKYiCSlWAvMlWYhL1HuYpw0V17urFnIS/QxSUqxGJgrzfGAvEQEqYfF4Cp10hjiAoKU+orFwFxpDuICkpT6jsWAMmnMM8Ql+oQkpViMqJPGkBYQpB4WI+qkMaQFBKmHxYg6aURyqujmO4LUw2pEoTSCsIAc9STPdr5BoTSCtIAg9TxkNXhAoVR2i7RELxOkHlYjCqURhAXkqI/ViEKpLP3Ubq60H2QF2VNFL178jeWISqnoM7ICctTHauSznTWJrNTIUR+r0feUSiXp9V2bO+Soj+WIUqmIqNSIUV9ykoy50pRzRAWFT+3pt6LmSlNISo0YDbAcvUutVPAuUcGXxKjvLusRtVIJSakRo4GfWY+olQoISi1/fdcW66BYyktPPy4frr5lPfIkGY0jKLVLxGjgJzZEfiuqUeSkQYqGWAfVUlbvPIbNb6Ro4N9siK5QLmWRkwYpGvqaDdGvlEs5xKRBiDLYAMqlnDPEpFY4/XjrEjuiM9RLGcntRCuEKIMN+IUG0lDy0JIKGcpILvDyx6BGfE9IGqXzGLbYgdfpoBXpf+bhn/eP/i0ylJF+1775gQ5ai6eZXM/TLO8TrVtkKIcdNTpoJXpXL6TOsWk/HtG19S0Zyugdrvb7TnTakmd25+zvEpgLdGzlbycasaVGD63DZKwq+4nWB3RrZR9agvSEPi/wWpdZsaqcfK5X6NR6SISy2FOjiVbhPFOb5QJFu6FJFwnK+pg98Lv2VWFqs1G2XO6XAxKUx54abbQKV5la158Y7Nbz/FPXeUqX6X89VuFF8t5jEzxcrQpTa73EWDteYqlF8Xy5+I7HysPVqqWPPCpea9U7r2DpZ/gfKet6TOcCdtU8oW9dGBveYqhZb7EJNJjhBhWJ1+ha8BHbarTSSiTfMlxmqEWvsTGYeczK/gjc/E7Hgt5XV5uzNNNKvM7gAoY6iq3BjB9Og5MXort0K2Fbg25aixcYXOU6Q51wme2Vb2hTwrY+OhU9Zl/tZN+b6X+AyVUY6gwUVGiTM/xjYFT85aCWPGWpQkOtB5OrvMpYZ3ibkgqN+rKf1isjJzBEd9jYoKNWpHNQYawzUbT1DK26WBqa8cOWnY2pH7b6f8Twtka/ZRjqHLNoVRv5U/Z9ikews+FXV6v0Z8a32bzDYGf7C4XJ7TjGzo/4isIxbG3RVyvD+DabN5nsAu3Xl6FV6ZN6NOv3Tfa2HoXOWp32ZDtGuwilm81/cn+s6Rg91arB5pZPWVotJrhbrjJRyGLzBDa3bvEetT7NAWvib3YlL1NeNrcx2zt4i1ojZrjrAWvqkDXn03rA/g7eoFapOWD9lQEv9j4NMtgxAwUdvD+tFGPc/YBVOmTdY3WG3q0YKrw7rRaD3HzIkHfwBy1aC0J18eKXFHXw3rReDxjlCQ5Y6SHrPf5tLsq6eGtaM2a5l2C9NuOPNanMz0BjdRiY5uYfjHonHz4s3n92zBu8dhdvSyvXPAqQWT85/XOOA96VVo+BPvFgpU+BA+9JB4CRlh9Ucxp614+Bd6SDwFCf4AHrE14x5Y1jDgxzfVLByv0WuPUZ70YHg8k+kWAVUuXPwEPEbE8/WINLbuDPwMPEeE85WG/yKgPP8TZ0aBjwKQard7+0Dq+QOGDM+JSClf8NMPKZlQeNKZ9CsMpHqi3von3oGPR+g3W3+JkqeJbX1gFj1nsL1p3bNCzxt8DjwLj3Eazxw1Rgqo4GEz9xsGgzxgsEjwlD3/xBQHb0OW2KXuD1dCSY+0kPWTTJ8+6OR+g/DP+0guVvgMeKACy539rQOzRJeaQ6ZoTgZIcsWrSu+Jnq2DVPDSAjO6FFcOsmnXXcyMNegkVLaS8/C2lgsNRBJhbffrTVfnSnpbRFKHY/ZFFurpQgFZvN8wRlKcoNllLEYtdDFsXmSj3kYqebJnfvFEk7CQRjpz9Ft48upJlUIxlbpGUBCrdoJjWIxhZxmY2yLVpJLbJRITAzUVShldRBOCpEZhZKKjSSuroPWCU0M1BQyT1VTmpP+Nua+bed7mMwvUpCBSQk+JzojEpOc6eJNEBEIsIzgo0RLaQMQhJNPFkwvYaQBlIWMamRoQw21CiXCghK40WClHiRxQbFUtHwYc4/EScMn03o74Oa4Qvi0vf4Mf/R9wWF0jgCMxNF0qSrZGYGLx7UEjOjZay01IxoGSvthPwUsEla7iwhGvBmHzqhX4hSh+fHaG8eBfyPJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmStFpPPfVfkojW95ZrqUIAAAAASUVORK5CYII= + */ + + const EasingMethods = [ + "linear", + "sine", + "quad", + "cubic", + "quart", + "quint", + "expo", + "circ", + "back", + "elastic", + "bounce" + ]; + + const BlockType = Scratch.BlockType; + const ArgumentType = Scratch.ArgumentType; + const Cast = Scratch.Cast; + + class Tween { + getInfo() { + return { + id: 'tweening', + name: 'Tweening', + blocks: [ + { + opcode: 'tweenValue', + text: '[MODE] ease [DIRECTION] [START] to [END] by [AMOUNT]%', + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + MODE: { type: ArgumentType.STRING, menu: 'modes' }, + DIRECTION: { type: ArgumentType.STRING, menu: 'direction' }, + START: { type: ArgumentType.NUMBER, defaultValue: 0 }, + END: { type: ArgumentType.NUMBER, defaultValue: 100 }, + AMOUNT: { type: ArgumentType.NUMBER, defaultValue: 50 }, + } + } + ], + menus: { + modes: { + acceptReporters: true, + items: EasingMethods.map(item => ({ text: item, value: item })) + }, + direction: { + acceptReporters: true, + items: [ + "in", + "out", + "in out" + ].map(item => ({ text: item, value: item })) + } + } + }; + } + + // utilities + multiplierToNormalNumber(mul, start, end) { + const multiplier = end - start; + const result = (mul * multiplier) + start; + return result; + } + + // blocks + tweenValue(args) { + const easeMethod = Cast.toString(args.MODE); + const easeDirection = Cast.toString(args.DIRECTION); + + const start = Cast.toNumber(args.START); + const end = Cast.toNumber(args.END); + + // easing method does not exist, return starting number + if (!EasingMethods.includes(easeMethod)) return start; + // easing method is not implemented, return starting number + if (!this[easeMethod]) return start; + + const progress = Cast.toNumber(args.AMOUNT) / 100; + + const tweened = this[easeMethod](progress, easeDirection); + return this.multiplierToNormalNumber(tweened, start, end); + } + + // easing functions (placed below blocks for organization) + linear(x) { + // lol + return x; + } + + sine(x, dir) { + switch (dir) { + case "in": { + return 1 - Math.cos((x * Math.PI) / 2); + } + case "out": { + return Math.sin((x * Math.PI) / 2); + } + case "in out": { + return -(Math.cos(Math.PI * x) - 1) / 2; + } + default: + return 0; + } + } + + quad(x, dir) { + switch (dir) { + case "in": { + return x * x; + } + case "out": { + return 1 - (1 - x) * (1 - x); + } + case "in out": { + return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; + } + default: + return 0; + } + } + + cubic(x, dir) { + switch (dir) { + case "in": { + return x * x * x; + } + case "out": { + return 1 - Math.pow(1 - x, 3); + } + case "in out": { + return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; + } + default: + return 0; + } + } + + quart(x, dir) { + switch (dir) { + case "in": { + return x * x * x * x; + } + case "out": { + return 1 - Math.pow(1 - x, 4); + } + case "in out": { + return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2; + } + default: + return 0; + } + } + + quint(x, dir) { + switch (dir) { + case "in": { + return x * x * x * x * x; + } + case "out": { + return 1 - Math.pow(1 - x, 5); + } + case "in out": { + return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2; + } + default: + return 0; + } + } + + expo(x, dir) { + switch (dir) { + case "in": { + return x === 0 ? 0 : Math.pow(2, 10 * x - 10); + } + case "out": { + return x === 1 ? 1 : 1 - Math.pow(2, -10 * x); + } + case "in out": { + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 ? Math.pow(2, 20 * x - 10) / 2 + : (2 - Math.pow(2, -20 * x + 10)) / 2; + } + default: + return 0; + } + } + + circ(x, dir) { + switch (dir) { + case "in": { + return 1 - Math.sqrt(1 - Math.pow(x, 2)); + } + case "out": { + return Math.sqrt(1 - Math.pow(x - 1, 2)); + } + case "in out": { + return x < 0.5 + ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 + : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2; + } + default: + return 0; + } + } + + back(x, dir) { + switch (dir) { + case "in": { + const c1 = 1.70158; + const c3 = c1 + 1; + + return c3 * x * x * x - c1 * x * x; + } + case "out": { + const c1 = 1.70158; + const c3 = c1 + 1; + + return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); + } + case "in out": { + const c1 = 1.70158; + const c2 = c1 * 1.525; + + return x < 0.5 + ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2 + : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; + } + default: + return 0; + } + } + + elastic(x, dir) { + switch (dir) { + case "in": { + const c4 = (2 * Math.PI) / 3; + + return x === 0 + ? 0 + : x === 1 + ? 1 + : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4); + } + case "out": { + const c4 = (2 * Math.PI) / 3; + + return x === 0 + ? 0 + : x === 1 + ? 1 + : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; + } + case "in out": { + const c5 = (2 * Math.PI) / 4.5; + + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 + ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1; + } + default: + return 0; + } + } + + bounce(x, dir) { + switch (dir) { + case "in": { + return 1 - this.bounce(1 - x, "out"); + } + case "out": { + const n1 = 7.5625; + const d1 = 2.75; + + if (x < 1 / d1) { + return n1 * x * x; + } else if (x < 2 / d1) { + return n1 * (x -= 1.5 / d1) * x + 0.75; + } else if (x < 2.5 / d1) { + return n1 * (x -= 2.25 / d1) * x + 0.9375; + } else { + return n1 * (x -= 2.625 / d1) * x + 0.984375; + } + } + case "in out": { + return x < 0.5 + ? (1 - this.bounce(1 - 2 * x, "out")) / 2 + : (1 + this.bounce(2 * x - 1, "out")) / 2; + } + default: + return 0; + } + } + } + + Scratch.extensions.register(new Tween()); +})(Scratch); \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jg_vr/index.js b/local-scratch-vm/src/extensions/jg_vr/index.js new file mode 100644 index 0000000000000000000000000000000000000000..123a7ce0ad8c226434daf140837bc31187ffba10 --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_vr/index.js @@ -0,0 +1,422 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +const SESSION_TYPE = "immersive-vr"; + +// WebXR unfortunately does not give us Euler angles easily +// so lets do it ourselves +// thanks to twoerner94 for quaternion-to-euler on npm +function quaternionToEuler(quat) { + const q0 = quat[0]; + const q1 = quat[1]; + const q2 = quat[2]; + const q3 = quat[3]; + + const Rx = Math.atan2(2 * (q0 * q1 + q2 * q3), 1 - (2 * (q1 * q1 + q2 * q2))); + const Ry = Math.asin(2 * (q0 * q2 - q3 * q1)); + const Rz = Math.atan2(2 * (q0 * q3 + q1 * q2), 1 - (2 * (q2 * q2 + q3 * q3))); + + const euler = [Rx, Ry, Rz]; + + return euler; +}; + +function toRad(deg) { + return deg * (Math.PI / 180); +} +function toDeg(rad) { + return rad * (180 / Math.PI); +} + +/** + * Class of 2025 + * @constructor + */ +class jgVr { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + this.open = false; + this.session = null; + + this.view = null; + this.localSpace = null; + + /** + * If true, VR sessions will begin split + * If false, VR sessions will begin with no split + */ + this.splitState = false; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jgVr', + name: 'Virtual Reality', + color1: '#3888cf', + color2: '#2f72ad', + blocks: [ + // CORE + { + opcode: 'isSupported', + text: 'is vr supported?', + blockType: BlockType.BOOLEAN, + disableMonitor: true + }, + { + opcode: 'createSession', + text: 'create vr session', + blockType: BlockType.COMMAND + }, + { + opcode: 'closeSession', + text: 'close vr session', + blockType: BlockType.COMMAND + }, + { + opcode: 'isOpened', + text: 'is vr open?', + blockType: BlockType.BOOLEAN, + disableMonitor: true + }, + '---', // SCREEN SPLITTING SETTINGS + { + opcode: 'enableDisableSplitting', + text: 'turn auto-splitting [ONOFF]', + blockType: BlockType.COMMAND, + arguments: { + ONOFF: { + type: ArgumentType.STRING, + menu: 'onoff' + } + } + }, + { + opcode: 'splittingOffset', + text: 'set auto-split offset to [PX] pixels', + blockType: BlockType.COMMAND, + arguments: { + PX: { + type: ArgumentType.NUMBER, + defaultValue: 40 + } + } + }, + { + opcode: 'placement169', + text: '[SIDE] x placement', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + SIDE: { + type: ArgumentType.STRING, + menu: 'side' + } + } + }, + '---', // HEADSET POSITION + { + opcode: 'headsetPosition', + text: 'headset position [VECTOR3]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + VECTOR3: { + type: ArgumentType.STRING, + menu: 'vector3' + } + } + }, + { + opcode: 'headsetRotation', + text: 'headset rotation [VECTOR3]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + VECTOR3: { + type: ArgumentType.STRING, + menu: 'vector3' + } + } + }, + '---', // CONTROLLER INPUT + { + opcode: 'controllerPosition', + text: 'controller #[COUNT] position [VECTOR3]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + COUNT: { + type: ArgumentType.NUMBER, + menu: 'count' + }, + VECTOR3: { + type: ArgumentType.STRING, + menu: 'vector3' + } + } + }, + { + opcode: 'controllerRotation', + text: 'controller #[COUNT] rotation [VECTOR3]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + COUNT: { + type: ArgumentType.NUMBER, + menu: 'count' + }, + VECTOR3: { + type: ArgumentType.STRING, + menu: 'vector3' + } + } + }, + ], + menus: { + vector3: { + acceptReporters: true, + items: [ + "x", + "y", + "z", + ].map(item => ({ text: item, value: item })) + }, + count: { + acceptReporters: true, + items: [ + "1", + "2", + ].map(item => ({ text: item, value: item })) + }, + side: { + acceptReporters: false, + items: [ + "left", + "right", + ].map(item => ({ text: item, value: item })) + }, + onoff: { + acceptReporters: false, + items: [ + "on", + "off", + ].map(item => ({ text: item, value: item })) + }, + } + }; + } + + // menus + _isVector3Menu(option) { + const normalized = Cast.toString(option).toLowerCase().trim(); + return ['x', 'y', 'z'].includes(normalized); + } + _onOffBoolean(onoff) { + const normalized = Cast.toString(onoff).toLowerCase().trim(); + return normalized === 'on'; + } + + // util + _getCanvas() { + if (!this.runtime) return; + if (!this.runtime.renderer) return; + return this.runtime.renderer.canvas; + } + _getContext() { + if (!this.runtime) return; + if (!this.runtime.renderer) return; + return this.runtime.renderer.gl; + } + _getRenderer() { + if (!this.runtime) return; + return this.runtime.renderer; + } + + _disposeImmersive() { + this.session = null; + const gl = this._getContext(); + if (!gl) return; + // bind frame buffer to canvas + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + // reset renderer info + const renderer = this._getRenderer(); + if (!renderer) return; + renderer.xrEnabled = false; + renderer.xrSplitting = false; + renderer.xrLayer = null; + } + async _createImmersive() { + if (!('xr' in navigator)) return false; + const gl = this._getContext(); + if (!gl) return; + const renderer = this._getRenderer(); + if (!renderer) return; + + await gl.makeXRCompatible(); + const session = await navigator.xr.requestSession(SESSION_TYPE); + this.session = session; + this.open = true; + + renderer.xrEnabled = true; + renderer.xrSplitting = this.splitState; + + // we need to make sure stuff is back to normal once the vr session is done + // but this isnt always triggered by the close session block + // the user can also close it themselves, so we need to handle that + // this is also triggered by the close session block btw so we dont need + // to repeat + session.addEventListener("end", () => { + this.open = false; + this._disposeImmersive(); + }); + + // set render state to use a new layer for the vr session + // renderer will handle this + const layer = new XRWebGLLayer(session, gl, { + alpha: true, + stencil: true, + antialias: false, + }); + session.updateRenderState({ + baseLayer: layer + }); + renderer.xrLayer = layer; + // for debugging & other extensions, never used by the renderer + renderer._xrSession = session; + + // setup render loop + const drawFrame = (_, frame) => { + // breaks the loop once the session has ended + if (!this.open) return; + // get view info + const viewerPose = frame.getViewerPose(this.localSpace); + const transform = viewerPose.transform; + // set view info + this.view = { + position: [ + transform.position.x, + transform.position.y, + transform.position.z + ], + quaternion: [ + transform.orientation.w, + transform.orientation.y, + transform.orientation.x, + transform.orientation.z + ] + } + // force renderer to draw a new frame + // otherwise we would only actually draw outside of this loop + // which just ends up showing nothing + // since rendering only happens in session.requestAnimationFrame + renderer.dirty = true; + renderer.draw(); + // loop again + session.requestAnimationFrame(drawFrame); + } + session.requestAnimationFrame(drawFrame); + + // reference space + session.requestReferenceSpace("local").then(space => { + this.localSpace = space; + // TODO: add "when position reset" hat? + // done with space.addEventListener("reset") + }); + + return session; + } + + // blocks + isSupported() { + if (!('xr' in navigator)) return false; + return navigator.xr.isSessionSupported(SESSION_TYPE); + } + isOpened() { + return this.open; + } + + createSession() { + if (this.open) return; + if (this.session) return; + return this._createImmersive(); + } + closeSession() { + this.open = false; + if (!this.session) return; + return this.session.end(); + } + + // splitting blocks + enableDisableSplitting(args) { + const renderer = this._getRenderer(); + if (!renderer) return; + + const boolean = this._onOffBoolean(args.ONOFF); + this.splitState = boolean; + // setting xrSplitting outside of XR mode WILL work + // so prevent this by just checking if we ARE in XR rendering mode + if (!renderer.xrEnabled) return; + renderer.xrSplitting = this.splitState; + } + splittingOffset(args) { + const renderer = this._getRenderer(); + if (!renderer) return; + + // pixels should be negative + // otherwise we push away from the center + const pixels = Cast.toNumber(args.PX); + renderer.xrSplitOffset = 0 - pixels; + } + + // inputs + headsetPosition(args) { + if (!this.open) return 0; + if (!this.session) return 0; + if (!this.view) return 0; + const vector3 = Cast.toString(args.VECTOR3).toLowerCase().trim(); + if (!this._isVector3Menu(vector3)) return 0; + const axisArray = ['x', 'y', 'z']; + const idx = axisArray.indexOf(vector3); + return this.view.position[idx] * 100; + } + headsetRotation(args) { + if (!this.open) return 0; + if (!this.session) return 0; + if (!this.view) return 0; + const vector3 = Cast.toString(args.VECTOR3).toLowerCase().trim(); + if (!this._isVector3Menu(vector3)) return 0; + const axisArray = ['x', 'y', 'z']; + const idx = axisArray.indexOf(vector3); + const quaternion = this.view.quaternion; + const euler = quaternionToEuler(quaternion); + return toDeg(euler[idx]); + } + + // helper + placement169(args) { + const side = Cast.toString(args.SIDE).toLowerCase().trim(); + + const width = this.runtime.stageWidth; + const multX = width / 640; + + // this was found with experimentation + // please tell me if stuff needs to be added for certain cases + const valueR = ((640 / 4) - 40) * multX; + const valueL = 0 - valueR; + + if (side === 'right') { + return valueR; + } + return valueL; + } +} + +module.exports = jgVr; diff --git a/local-scratch-vm/src/extensions/jg_websiteRequests/index.js b/local-scratch-vm/src/extensions/jg_websiteRequests/index.js new file mode 100644 index 0000000000000000000000000000000000000000..863ea070a0c0bb8998a615b391efba206b56bdfc --- /dev/null +++ b/local-scratch-vm/src/extensions/jg_websiteRequests/index.js @@ -0,0 +1,230 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const AHHHHHHHHHHHHHH = require('../../util/array buffer'); +const BufferStuff = new AHHHHHHHHHHHHHH(); +// const Cast = require('../../util/cast'); + +/** + * Class for Website Request blocks + * @constructor + */ +class JgWebsiteRequestBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'jgWebsiteRequests', + name: 'Website Requests', + color1: '#004299', + color2: '#003478', + blocks: [ + { + opcode: 'encodeTextForURL', + text: formatMessage({ + id: 'jgWebsiteRequests.blocks.encodeTextForURL', + default: 'encode [TEXT] for URL', + description: 'Encodes text to be usable in a URL.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgWebsiteRequests.text_encode_for_url', + default: 'Text here', + description: 'The text to encode.' + }) + } + } + }, + { + opcode: 'decodeUrlForText', + text: formatMessage({ + id: 'jgWebsiteRequests.blocks.decodeUrlForText', + default: 'decode [TEXT] for text', + description: 'Decodes text used in query parameters and other areas.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgWebsiteRequests.text_decode_for_url', + default: 'Text%20here', + description: 'The text to decode.' + }) + } + } + }, + { + opcode: 'getWebsiteContent', + text: formatMessage({ + id: 'jgWebsiteRequests.blocks.getWebsiteContent', + default: 'get [WEBSITE]\'s content', + description: 'Gets the contents of the specified website. Includes HTML if it\'s a normal website.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + WEBSITE: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgWebsiteRequests.website_fetch_content', + default: 'https://www.google.com', + description: 'The website to get the content of.' + }) + } + } + }, + { + opcode: 'getWebsiteBinaryData', + text: formatMessage({ + id: 'jgWebsiteRequests.blocks.getWebsiteBinaryData', + default: 'get binary data from [WEBSITE]', + description: 'Gets the data of the specified website.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + WEBSITE: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgWebsiteRequests.website_fetch_content', + default: 'https://www.google.com', + description: 'The website to get the content of.' + }) + } + } + }, + { + opcode: 'postWithContentToWebsite', + text: formatMessage({ + id: 'jgWebsiteRequests.blocks.postWithContentToWebsite', + default: 'post [CONTENT] as [KEY] to [WEBSITE]', + description: 'Posts to a website using a JSON body with the key text set to the content.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + CONTENT: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgWebsiteRequests.website_post_content', + default: 'value', + description: 'The content of the key to post.' + }) + }, + KEY: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgWebsiteRequests.website_post_key', + default: 'key', + description: 'The key in the request body to post.' + }) + }, + WEBSITE: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jgWebsiteRequests.website_post_website', + default: 'https://httpbin.org/post', + description: 'The website to post the key and content to.' + }) + } + } + } + ] + }; + } + encodeTextForURL (args) { + return encodeURIComponent(String(args.TEXT)); + } + decodeUrlForText (args) { + return decodeURI(String(args.TEXT)); + } + + getWebsiteContent (args) { + return new Promise(resolve => { + if (window && !window.fetch) return resolve(""); + const fetchingUrl = args.WEBSITE.replace("rawRequest()", ""); + fetch(fetchingUrl, {cache: "no-cache"}).then(r => { + r.text().then(text => { + resolve(String(text)); + }) + .catch(() => { + resolve(""); + }); + }) + .catch(() => { + resolve(""); + }); + }); + } + + getWebsiteBinaryData (args) { + return new Promise(resolve => { + if (window && !window.fetch) return resolve("[]"); + const fetchingUrl = args.WEBSITE.replace("rawRequest()", ""); + fetch(fetchingUrl, {cache: "no-cache"}).then(r => { + r.arrayBuffer().then(buffer => { + resolve(String(JSON.stringify(BufferStuff.bufferToArray(buffer)))); + }) + .catch(() => { + resolve("[]"); + }); + }) + .catch(() => { + resolve("[]"); + }); + }); + } + + postWithContentToWebsite (args) { + return new Promise(resolve => { + if (window && !window.fetch) return resolve(""); + const body = {}; + const checking = String(args.CONTENT); + let canJSONParse = true; + try { + JSON.parse(checking); + } catch { + canJSONParse = false; + } + body[String(args.KEY)] = checking === "true" ? true : + checking === "false" ? false : + Number(checking) ? Number(checking) : + checking === "null" ? null : + canJSONParse ? JSON.parse(checking) : + checking; + fetch(args.WEBSITE, { + method: "POST", + headers: { "Content-Type": "application/json" }, + cache: "no-cache", + body: JSON.stringify(body) + }).then(r => { + r.text().then(text => { + resolve(String(text)); + }) + .catch(() => { + resolve(""); + }); + }) + .catch(() => { + resolve(""); + }); + }); + } +} + +module.exports = JgWebsiteRequestBlocks; diff --git a/local-scratch-vm/src/extensions/jwArray/index.js b/local-scratch-vm/src/extensions/jwArray/index.js new file mode 100644 index 0000000000000000000000000000000000000000..300adb4058e97c0b9197085fa0ab051dfd7eae76 --- /dev/null +++ b/local-scratch-vm/src/extensions/jwArray/index.js @@ -0,0 +1,507 @@ +const BlockType = require('../../extension-support/block-type') +const BlockShape = require('../../extension-support/block-shape') +const ArgumentType = require('../../extension-support/argument-type') +const Cast = require('../../util/cast') + +let arrayLimit = 2 ** 32 + +// credit to sharpool because i stole the for each code from his extension haha im soo evil + +/** +* @param {number} x +* @returns {string} +*/ +function formatNumber(x) { + if (x >= 1e6) { + return x.toExponential(4) + } else { + x = Math.floor(x * 1000) / 1000 + return x.toFixed(Math.min(3, (String(x).split('.')[1] || '').length)) + } +} + +function clampIndex(x) { + return Math.min(Math.max(x, 0), arrayLimit) +} + +function span(text) { + let el = document.createElement('span') + el.innerHTML = text + el.style.display = 'hidden' + el.style.whiteSpace = 'nowrap' + el.style.width = '100%' + el.style.textAlign = 'center' + return el +} + +class ArrayType { + customId = "jwArray" + + array = [] + + constructor(array = []) { + this.array = array + } + + static toArray(x) { + if (x instanceof ArrayType) return new ArrayType([...x.array]) + if (x instanceof Array) return new ArrayType([...x]) + if (x === "" || x === null || x === undefined) return new ArrayType() + try { + let parsed = JSON.parse(x) + if (parsed instanceof Array) return new ArrayType(parsed) + } catch {} + return new ArrayType([x]) + } + + static forArray(x) { + if (x instanceof ArrayType) return new ArrayType([...x.array]) + return x + } + + static display(x) { + try { + switch (typeof x) { + case "object": + if (x === null) return "null" + if (typeof x.jwArrayHandler == "function") { + return x.jwArrayHandler() + } + return Cast.toString(x) + case "undefined": + return "null" + case "number": + return formatNumber(x) + case "boolean": + return x ? "true" : "false" + case "string": + return `"${Cast.toString(x)}"` + } + } catch {} + return "?" + } + + jwArrayHandler() { + return `Array<${formatNumber(this.array.length)}>` + } + + toString() { + return JSON.stringify(this.array) + } + toMonitorContent = () => span(this.toString()) + + toReporterContent() { + let root = document.createElement('div') + root.style.display = 'flex' + root.style.flexDirection = 'column' + root.style.justifyContent = 'center' + + let arrayDisplay = span(`[${this.array.slice(0, 50).map(v => ArrayType.display(v)).join(', ')}]`) + arrayDisplay.style.overflow = "hidden" + arrayDisplay.style.whiteSpace = "nowrap" + arrayDisplay.style.textOverflow = "ellipsis" + arrayDisplay.style.maxWidth = "256px" + root.appendChild(arrayDisplay) + + root.appendChild(span(`Length: ${this.array.length}`)) + + return root + } + + get length() { + return this.array.length + } +} + +const jwArray = { + Type: ArrayType, + Block: { + blockType: BlockType.REPORTER, + blockShape: BlockShape.SQUARE, + forceOutputType: "Array", + disableMonitor: true + }, + Argument: { + shape: BlockShape.SQUARE, + check: ["Array"] + } +} + +class Extension { + constructor() { + vm.jwArray = jwArray + vm.runtime.registerSerializer( //this basically copies variable serialization + "jwArray", + v => v.array.map(w => { + if (typeof w == "object" && w != null && w.customId) { + return { + customType: true, + typeId: w.customId, + serialized: vm.runtime.serializers[w.customId].serialize(w) + }; + } + return w + }), + v => new jwArray.Type(v.map(w => { + if (typeof w == "object" && w != null && w.customType) { + return vm.runtime.serializers[w.typeId].deserialize(w.serialized) + } + return w + })) + ); + } + + getInfo() { + return { + id: "jwArray", + name: "Arrays", + color1: "#ff513d", + menuIconURI: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM6Yng9Imh0dHBzOi8vYm94eS1zdmcuY29tIj4KICA8Y2lyY2xlIHN0eWxlPSJzdHJva2Utd2lkdGg6IDJweDsgcGFpbnQtb3JkZXI6IHN0cm9rZTsgZmlsbDogcmdiKDI1NSwgODEsIDYxKTsgc3Ryb2tlOiByZ2IoMjA1LCA1OSwgNDQpOyIgY3g9IjEwIiBjeT0iMTAiIHI9IjkiPjwvY2lyY2xlPgogIDxwYXRoIGQ9Ik0gOC4wNzMgNC4yMiBMIDYuMTQ3IDQuMjIgQyA1LjA4MyA0LjIyIDQuMjIgNS4wODMgNC4yMiA2LjE0NyBMIDQuMjIgMTMuODUzIEMgNC4yMiAxNC45MTkgNS4wODMgMTUuNzggNi4xNDcgMTUuNzggTCA4LjA3MyAxNS43OCBMIDguMDczIDEzLjg1MyBMIDYuMTQ3IDEzLjg1MyBMIDYuMTQ3IDYuMTQ3IEwgOC4wNzMgNi4xNDcgTCA4LjA3MyA0LjIyIFogTSAxMS45MjcgMTMuODUzIEwgMTMuODUzIDEzLjg1MyBMIDEzLjg1MyA2LjE0NyBMIDExLjkyNyA2LjE0NyBMIDExLjkyNyA0LjIyIEwgMTMuODUzIDQuMjIgQyAxNC45MTcgNC4yMiAxNS43OCA1LjA4MyAxNS43OCA2LjE0NyBMIDE1Ljc4IDEzLjg1MyBDIDE1Ljc4IDE0LjkxOSAxNC45MTcgMTUuNzggMTMuODUzIDE1Ljc4IEwgMTEuOTI3IDE1Ljc4IEwgMTEuOTI3IDEzLjg1MyBaIiBmaWxsPSIjZmZmIiBzdHlsZT0iIj48L3BhdGg+Cjwvc3ZnPg==", + blocks: [ + { + opcode: 'blank', + text: 'blank array', + ...jwArray.Block + }, + { + opcode: 'blankLength', + text: 'blank array of length [LENGTH]', + arguments: { + LENGTH: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + ...jwArray.Block + }, + { + opcode: 'fromList', + text: 'array from list [LIST]', + arguments: { + LIST: { + menu: "list" + } + }, + hideFromPalette: true, //doesn't work for some reason + ...jwArray.Block + }, + { + opcode: 'split', + text: 'split [STRING] by [DIVIDER]', + arguments: { + STRING: { + type: ArgumentType.STRING, + defaultValue: "foo" + }, + DIVIDER: { + type: ArgumentType.STRING + } + }, + ...jwArray.Block + }, + "---", + { + opcode: 'get', + text: 'get [INDEX] in [ARRAY]', + blockType: BlockType.REPORTER, + allowDropAnywhere: true, + arguments: { + ARRAY: jwArray.Argument, + INDEX: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'index', + text: 'index of [VALUE] in [ARRAY]', + blockType: BlockType.REPORTER, + arguments: { + ARRAY: jwArray.Argument, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "foo", + exemptFromNormalization: true + } + } + }, + { + opcode: 'has', + text: '[ARRAY] has [VALUE]', + blockType: BlockType.BOOLEAN, + arguments: { + ARRAY: jwArray.Argument, + VALUE: { + type: ArgumentType.STRING, + exemptFromNormalization: true + } + } + }, + { + opcode: 'length', + text: 'length of [ARRAY]', + blockType: BlockType.REPORTER, + arguments: { + ARRAY: jwArray.Argument + } + }, + "---", + { + opcode: 'set', + text: 'set [INDEX] in [ARRAY] to [VALUE]', + arguments: { + ARRAY: jwArray.Argument, + INDEX: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "foo", + exemptFromNormalization: true + } + }, + ...jwArray.Block + }, + { + opcode: 'append', + text: 'append [VALUE] to [ARRAY]', + arguments: { + ARRAY: jwArray.Argument, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "foo", + exemptFromNormalization: true + } + }, + ...jwArray.Block + }, + { + opcode: 'concat', + text: 'merge [ONE] with [TWO]', + arguments: { + ONE: jwArray.Argument, + TWO: jwArray.Argument + }, + ...jwArray.Block + }, + { + opcode: 'fill', + text: 'fill [ARRAY] with [VALUE]', + arguments: { + ARRAY: jwArray.Argument, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "foo", + exemptFromNormalization: true + } + }, + ...jwArray.Block + }, + { + opcode: 'splice', + text: 'splice [ARRAY] at [INDEX] with [ITEMS] items', + arguments: { + ARRAY: jwArray.Argument, + INDEX: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + ITEMS: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + ...jwArray.Block + }, + "---", + { + opcode: 'reverse', + text: 'reverse [ARRAY]', + arguments: { + ARRAY: jwArray.Argument + }, + ...jwArray.Block + }, + "---", + { + opcode: 'forEachI', + text: 'index', + blockType: BlockType.REPORTER, + hideFromPalette: true, + canDragDuplicate: true + }, + { + opcode: 'forEachV', + text: 'value', + blockType: BlockType.REPORTER, + hideFromPalette: true, + allowDropAnywhere: true, + canDragDuplicate: true + }, + { + opcode: 'forEach', + text: 'for [I] [V] of [ARRAY]', + blockType: BlockType.LOOP, + arguments: { + ARRAY: jwArray.Argument, + I: { + fillIn: 'forEachI' + }, + V: { + fillIn: 'forEachV' + } + } + }, + /*{ + opcode: 'forEachBreak', + text: 'break', + blockType: BlockType.COMMAND, + isTerminal: true + }*/ + ], + menus: { + list: { + acceptReporters: false, + items: "getLists", + }, + } + }; + } + + getLists() { + const globalLists = Object.values(vm.runtime.getTargetForStage().variables) + .filter((x) => x.type == "list"); + const localLists = Object.values(vm.editingTarget.variables) + .filter((x) => x.type == "list"); + const uniqueLists = [...new Set([...globalLists, ...localLists])]; + if (uniqueLists.length === 0) return [{ text: "", value: "" }]; + return uniqueLists.map((v) => ({ text: v.name, value: new jwArray.Type(v.value) })); + } + + blank() { + return new jwArray.Type() + } + + blankLength({LENGTH}) { + LENGTH = clampIndex(Cast.toNumber(LENGTH)) + + return new jwArray.Type(Array(LENGTH).fill(undefined)) + } + + fromList({LIST}) { + return jwArray.Type.toArray(LIST) + } + + split({STRING, DIVIDER}) { + STRING = Cast.toString(STRING) + DIVIDER = Cast.toString(DIVIDER) + + return new jwArray.Type(STRING.split(DIVIDER)) + } + + get({ARRAY, INDEX}) { + ARRAY = jwArray.Type.toArray(ARRAY) + + return ARRAY.array[Cast.toNumber(INDEX)-1] || "" + } + + index({ARRAY, VALUE}) { + ARRAY = jwArray.Type.toArray(ARRAY) + + return ARRAY.array.indexOf(VALUE) + 1 + } + + has({ARRAY, VALUE}) { + ARRAY = jwArray.Type.toArray(ARRAY) + + return ARRAY.array.includes(VALUE) + } + + length({ARRAY}) { + ARRAY = jwArray.Type.toArray(ARRAY) + + return ARRAY.length + } + + set({ARRAY, INDEX, VALUE}) { + ARRAY = jwArray.Type.toArray(ARRAY) + INDEX = Cast.toNumber(INDEX) + + ARRAY.array[clampIndex(Cast.toNumber(INDEX)-1)] = jwArray.Type.forArray(VALUE) + return ARRAY + } + + append({ARRAY, VALUE}) { + ARRAY = jwArray.Type.toArray(ARRAY) + + ARRAY.array.push(jwArray.Type.forArray(VALUE)) + return ARRAY + } + + concat({ONE, TWO}) { + ONE = jwArray.Type.toArray(ONE) + TWO = jwArray.Type.toArray(TWO) + + return new jwArray.Type(ONE.array.concat(TWO.array)) + } + + fill({ARRAY, VALUE}) { + ARRAY = jwArray.Type.toArray(ARRAY) + + ARRAY.array.fill(jwArray.Type.forArray(VALUE)) + return ARRAY + } + + splice({ARRAY, INDEX, ITEMS}) { + ARRAY = jwArray.Type.toArray(ARRAY) + INDEX = Cast.toNumber(INDEX) + ITEMS = Cast.toNumber(ITEMS) + + ARRAY.array.splice(INDEX - 1, ITEMS) + return ARRAY + } + + reverse({ARRAY}) { + ARRAY = jwArray.Type.toArray(ARRAY) + + ARRAY.array.reverse() + return ARRAY + } + + forEachI({}, util) { + let arr = util.thread.stackFrames[0].jwArray + return arr ? Cast.toNumber(arr[0]) + 1 : 0 + } + + forEachV({}, util) { + let arr = util.thread.stackFrames[0].jwArray + return arr ? arr[1] : "" + } + + forEach({ARRAY}, util) { + ARRAY = jwArray.Type.toArray(ARRAY) + + if (util.stackFrame.execute) { + util.stackFrame.index++; + const { index, entry } = util.stackFrame; + if (index > entry.length - 1) return; + util.thread.stackFrames[0].jwArray = entry[index]; + } else { + const entry = Object.entries(ARRAY.array); + if (entry.length === 0) return; + util.stackFrame.entry = entry; + util.stackFrame.execute = true; + util.stackFrame.index = 0; + util.thread.stackFrames[0].jwArray = entry[0]; + } + util.startBranch(1, true); + } + + forEachBreak({}, util) { + util.stackFrame.entry = [] + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jwColor/index.js b/local-scratch-vm/src/extensions/jwColor/index.js new file mode 100644 index 0000000000000000000000000000000000000000..632b9405f46bb0829cece432e99af3909d32c8a0 --- /dev/null +++ b/local-scratch-vm/src/extensions/jwColor/index.js @@ -0,0 +1,461 @@ +const BlockType = require('../../extension-support/block-type') +const BlockShape = require('../../extension-support/block-shape') +const ArgumentType = require('../../extension-support/argument-type') +const Cast = require('../../util/cast') + +/** + * @param {number} x + * @returns {string} + */ +function formatNumber(x) { + if (x >= 1e6) { + return x.toExponential(4) + } else { + x = Math.floor(x * 1000) / 1000 + return x.toFixed(Math.min(1, (String(x).split('.')[1] || '').length)) + } +} + +function span(text) { + let el = document.createElement('span') + el.innerHTML = text + el.style.display = 'hidden' + el.style.whiteSpace = 'nowrap' + el.style.width = '100%' + el.style.textAlign = 'center' + return el +} + +class ColorType { + customId = "jwColor" + + h = 0 + setHue(x) { + this.h = (x % 360) + if (this.h < 0) { + this.h = 360 + this.h + } + } + + s = 0 + setSaturation(x) { + this.s = Math.max(0, Math.min(x, 1)) + } + + v = 0 + setValue(x) { + this.v = Math.max(0, Math.min(x, 1)) + } + + constructor(h = 0, s = 0, v = 0) { + this.setHue(h) + this.setSaturation(s) + this.setValue(v) + } + + static toColor(x) { + if (x instanceof ColorType) return x + if (Number(x) == x) return ColorType.fromDecimal(x) + if (typeof x == 'string') return ColorType.fromHex(x) + return new ColorType() + } + + static fromHex(x) { + if (x.startsWith("#")) x = x.substring(1) + try { + if (x.length === 6 || x.length === 8) { + return ColorType.fromDecimal(Number(`0x${x.slice(0, 6)}`)) + } else if (x.length === 3 || x.length === 4) { + return ColorType.fromDecimal(Number(`0x${x.slice(0, 3).split("").map(v => v + v).join("")}`)) + } + } catch {} + return new ColorType() + } + + static fromRGB(r, g, b) { + r = Math.max(0, Math.min(r / 255, 1)) + g = Math.max(0, Math.min(g / 255, 1)) + b = Math.max(0, Math.min(b / 255, 1)) + + let v = Math.max(r, g, b), c = v - Math.min(r, g, b) + let h = c && ((v == r) ? (g - b) / c : ((v == g) ? 2 + (b - r) / c : 4 + (r - g) / c)) + return new ColorType(60 * (h < 0 ? h + 6 : h), v && c / v, v) + } + + static fromDecimal(d) { + const r = (d >> 16) & 0xFF + const g = (d >> 8) & 0xFF + const b = d & 0xFF + return this.fromRGB(r, g, b) + } + + jwArrayHandler() { + let color = document.createElement('div') + color.style.width = "16px" + color.style.height = "16px" + color.style.border = "1px solid black" + color.style.borderRadius = "4px" + color.style.boxSizing = "border-box" + color.style.backgroundColor = `#${this.toHex()}` + color.style.display = "inline-block" + color.style.verticalAlign = "text-bottom" + + return color.outerHTML + } + + toString() { + return String(this.toDecimal()) + } + toMonitorContent = () => span(this.toString()) + + toReporterContent() { + let root = document.createElement('div') + root.style.display = 'flex' + root.style.width = "200px" + root.style.overflow = "hidden" + let details = document.createElement('div') + details.style.display = 'flex' + details.style.flexDirection = 'column' + details.style.justifyContent = 'center' + details.style.width = "100px" + details.appendChild(span(`H: ${formatNumber(Math.round(this.h))}`)) + details.appendChild(span(`S: ${formatNumber(this.s * 100)}%`)) + details.appendChild(span(`V: ${formatNumber(this.v * 100)}%`)) + root.appendChild(details) + let color = document.createElement('div') + color.style.width = "84px" + color.style.height = "84px" + color.style.margin = "8px" + color.style.border = "2px solid black" + color.style.borderRadius = "8px" + color.style.boxSizing = "border-box" + color.style.backgroundColor = `#${this.toHex()}` + root.appendChild(color) + return root + } + + toRGB() { + let f = (n, k = (n + this.h / 60) % 6) => this.v - this.v * this.s * Math.max(Math.min(k, 4 - k, 1), 0) + return [Math.round(f(5) * 255), Math.round(f(3) * 255), Math.round(f(1) * 255)] + } + + toDecimal() { + let [r, g, b] = this.toRGB() + return r * 0x10000 + g * 0x100 + b * 0x1 + } + + toHex() { + return this.toDecimal().toString(16).padStart(6, "0") + } +} + +const Color = { + Type: ColorType, + Block: { + blockType: BlockType.REPORTER, + forceOutputType: "Color", + disableMonitor: true + }, + Argument: { + type: ArgumentType.COLOR, + defaultValue: "#ff7aab" + } +} + +class Extension { + constructor() { + vm.jwColor = Color + vm.runtime.registerSerializer( + "jwColor", + v => [v.h, v.s, v.v], + v => new Color.Type(...v) + ); + } + + getInfo() { + return { + id: "jwColor", + name: "Color", + color1: "#f04a87", + menuIconURI: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+CiAgPGVsbGlwc2Ugc3R5bGU9ImZpbGw6IHJnYigyNDAsIDc0LCAxMzUpOyBzdHJva2U6IHJnYigyMTYsIDY2LCAxMjIpOyBzdHJva2Utd2lkdGg6IDJweDsgcGFpbnQtb3JkZXI6IHN0cm9rZTsiIGN4PSIxMCIgY3k9IjEwIiByeD0iOSIgcnk9IjkiPjwvZWxsaXBzZT4KICA8cGF0aCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGQ9Ik0gMTIuMTYyIDExLjAxNSBDIDExLjM1OCAxMS44MTkgMTAuNzY1IDEyLjIzMyAxMC4yOTkgMTIuMzkxIEMgMTAuMTYyIDExLjk2OCA5LjkyOSAxMS41NzYgOS42MDEgMTEuMjQ4IEMgOS4yNjIgMTAuOTIgOC44NzEgMTAuNjg3IDguNDQ3IDEwLjUzOCBDIDguNjE3IDEwLjA3MyA5LjAzIDkuNDggOS44MjMgOC42ODcgQyAxMS43MjggNi43NzEgMTUuMTE1IDQuNDMyIDE1Ljc2MSA1LjA3OCBDIDE2LjQwNyA1LjcyMyAxNC4wNjggOS4xMSAxMi4xNjIgMTEuMDE1IFogTSA4LjY1IDE0LjUzOSBDIDguMzM1IDE0Ljg0NCA3LjkyOSAxNSA3LjUyMiAxNS4wMiBMIDcuNTIyIDE1LjAzIEwgNy40MjEgMTUuMDMgQyA0LjY5OCAxNS4xMjggMy41MDkgMTIuMDQ2IDQuNDQ0IDEyLjM2OSBDIDUuNjczIDEyLjc5MiA2LjE3MiAxMi4xODMgNi4xOTEgMTIuMTYzIEMgNi44NzIgMTEuNTE2IDcuOTY5IDExLjUxNiA4LjY1IDEyLjE2MyBDIDkuMzMxIDEyLjgyMSA5LjMzMSAxMy44OTIgOC42NSAxNC41MzkgWiIgaWQ9ImJ1cnNoLWljb24iIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7Ij48L3BhdGg+Cjwvc3ZnPg==", + blocks: [ + { + opcode: 'newColor', + text: 'new color [COLOR]', + arguments: { + COLOR: Color.Argument + }, + ...Color.Block + }, + { + opcode: 'fromRGB', + text: 'from RGB [R] [G] [B]', + arguments: { + R: { + type: ArgumentType.NUMBER, + defaultValue: 255 + }, + G: { + type: ArgumentType.NUMBER, + defaultValue: 122 + }, + B: { + type: ArgumentType.NUMBER, + defaultValue: 171 + } + }, + ...Color.Block + }, + { + opcode: 'fromHSV', + text: 'from HSV [H] [S] [V]', + arguments: { + H: { + type: ArgumentType.NUMBER, + defaultValue: 338 + }, + S: { + type: ArgumentType.NUMBER, + defaultValue: 0.52 + }, + V: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + ...Color.Block + }, + { + opcode: 'fromHex', + text: 'from hex [HEX]', + arguments: { + HEX: { + type: ArgumentType.STRING, + defaultValue: "ff7aab" + } + }, + ...Color.Block + }, + "---", + { + opcode: 'add', + text: '[A] + [B]', + arguments: { + A: Color.Argument, + B: Color.Argument + }, + ...Color.Block + }, + { + opcode: 'sub', + text: '[A] - [B]', + arguments: { + A: Color.Argument, + B: Color.Argument + }, + ...Color.Block + }, + { + opcode: 'mul', + text: '[A] * [B]', + arguments: { + A: Color.Argument, + B: Color.Argument + }, + ...Color.Block + }, + { + opcode: 'interpolate', + text: 'interpolate [A] to [B] by [I] using [OPTION]', + arguments: { + A: Color.Argument, + B: Color.Argument, + I: { + type: ArgumentType.NUMBER, + defaultValue: 0.5 + }, + OPTION: { + menu: "interpolateOption" + } + }, + ...Color.Block + }, + "---", + { + opcode: 'get', + text: 'get [OPTION] [COLOR]', + blockType: BlockType.REPORTER, + arguments: { + COLOR: Color.Argument, + OPTION: { + menu: "propOption" + } + } + }, + { + opcode: 'set', + text: 'set [OPTION] [COLOR] to [VALUE]', + arguments: { + COLOR: Color.Argument, + VALUE: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + OPTION: { + menu: "propOption" + } + }, + ...Color.Block + }, + "---", + { + opcode: 'toDecimal', + text: '[COLOR] to decimal', + blockType: BlockType.REPORTER, + arguments: { + COLOR: Color.Argument + } + }, + { + opcode: 'toHex', + text: '[COLOR] to hexadecimal', + blockType: BlockType.REPORTER, + arguments: { + COLOR: Color.Argument + } + } + ], + menus: { + interpolateOption: { + acceptReporters: true, + items: [ + 'RGB', + 'HSV' + ] + }, + propOption: { + acceptReporters: true, + items: [ + 'red', + 'green', + 'blue', + 'hue', + 'saturation', + 'value' + ] + } + } + }; + } + + newColor({COLOR}) { + return Color.Type.toColor(COLOR) + } + + fromRGB({R, G, B}) { + R = Cast.toNumber(R) + G = Cast.toNumber(G) + B = Cast.toNumber(B) + + return Color.Type.fromRGB(R, G, B) + } + + fromHSV({H, S, V}) { + H = Cast.toNumber(H) + S = Cast.toNumber(S) + V = Cast.toNumber(V) + + return new Color.Type(H, S, V) + } + + fromHex({HEX}) { + HEX = Cast.toString(HEX) + + return Color.Type.fromHex(HEX) + } + + add({A, B}) { + A = Color.Type.toColor(A).toRGB() + B = Color.Type.toColor(B).toRGB() + + return Color.Type.fromRGB(Math.min(255, A[0] + B[0]), Math.min(255, A[1] + B[1]), Math.min(255, A[2] + B[2])) + } + + sub({A, B}) { + A = Color.Type.toColor(A).toRGB() + B = Color.Type.toColor(B).toRGB() + + return Color.Type.fromRGB(A[0] - B[0], A[1] - B[1], A[2] - B[2]) + } + + mul({A, B}) { + A = Color.Type.toColor(A).toRGB() + B = Color.Type.toColor(B).toRGB() + + return Color.Type.fromRGB(A[0] * B[0] / 255, A[1] * B[1] / 255, A[2] * B[2] / 255) + } + + interpolate({A, B, I, OPTION}) { + A = Color.Type.toColor(A) + B = Color.Type.toColor(B) + I = Math.max(0, Math.min(Cast.toNumber(I), 1)) + + switch (OPTION) { + case "RGB": + A = A.toRGB() + B = B.toRGB() + + return Color.Type.fromRGB(A[0] * (1-I) + B[0] * I, A[1] * (1-I) + B[1] * I, A[2] * (1-I) + B[2] * I) + case "HSV": + let hueDifference = Math.abs(A.h - B.h) + if (hueDifference > 180) { + return new Color.Type(A.h * (1-I) - (360 - hueDifference) * I, A.s * (1-I) + B.s * I, A.v * (1-I) + B.v * I) + } else { + return new Color.Type(A.h * (1-I) + B.h * I, A.s * (1-I) + B.s * I, A.v * (1-I) + B.v * I) + } + default: return new Color.Type + } + } + + get({COLOR, OPTION}) { + COLOR = Color.Type.toColor(COLOR) + + switch (OPTION) { + case "red": return COLOR.toRGB()[0] + case "green": return COLOR.toRGB()[1] + case "blue": return COLOR.toRGB()[2] + case "hue": return COLOR.h + case "saturation": return COLOR.s + case "value": return COLOR.v + default: return 0 + } + } + + set({COLOR, VALUE, OPTION}) { + COLOR = Color.Type.toColor(COLOR) + VALUE = Cast.toNumber(VALUE) + + switch (OPTION) { + case "red": return Color.Type.fromRGB(VALUE, COLOR.toRGB()[1], COLOR.toRGB()[2]) + case "green": return Color.Type.fromRGB(COLOR.toRGB()[0], VALUE, COLOR.toRGB()[2]) + case "blue": return Color.Type.fromRGB(COLOR.toRGB()[0], COLOR.toRGB()[1], VALUE) + case "hue": return new Color.Type(VALUE, COLOR.s, COLOR.v) + case "saturation": return new Color.Type(COLOR.h, VALUE, COLOR.v) + case "value": return new Color.Type(COLOR.h, COLOR.s, VALUE) + } + } + + toDecimal({COLOR}) { + COLOR = Color.Type.toColor(COLOR) + + return COLOR.toDecimal() + } + + toHex({COLOR}) { + COLOR = Color.Type.toColor(COLOR) + + return COLOR.toHex() + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jwLambda/index.js b/local-scratch-vm/src/extensions/jwLambda/index.js new file mode 100644 index 0000000000000000000000000000000000000000..735ebcc7999792ab6a5c4d3cc78646ffa1799f44 --- /dev/null +++ b/local-scratch-vm/src/extensions/jwLambda/index.js @@ -0,0 +1,141 @@ +const BlockType = require('../../extension-support/block-type') +const BlockShape = require('../../extension-support/block-shape') +const ArgumentType = require('../../extension-support/argument-type') +const Cast = require('../../util/cast') + +/** + * @param {number} x + * @returns {string} + */ +function formatNumber(x) { + if (x >= 1e6) { + return x.toExponential(4) + } else { + x = Math.floor(x * 1000) / 1000 + return x.toFixed(Math.min(3, (String(x).split('.')[1] || '').length)) + } +} + +function span(text) { + let el = document.createElement('span') + el.innerHTML = text + el.style.display = 'hidden' + el.style.whiteSpace = 'nowrap' + el.style.width = '100%' + el.style.textAlign = 'center' + return el +} + +class LambdaType { + customId = "jwLambda" + + constructor(util) { + this.firstBlockId = util ? util.thread.blockContainer.getBranch(util.thread.peekStack(), 0) : "" + this.targetBlockLocation = util.target + } + + static toLambda(x) { + if (x instanceof LambdaType) return x + return new LambdaType() + } + + jwArrayHandler() { + return 'Lambda' + } + + toString() { + return `Lambda` + } + toMonitorContent = () => span(this.toString()) + toReporterContent = () => span(this.toString()) + + execute(target, arg) { + if (this.firstBlockId) { + let thread = vm.runtime._pushThread(this.firstBlockId, target, {targetBlockLocation: this.targetBlockLocation}) + util.thread.stackFrames[0].jwLambda = arg + } + } +} + +const Lambda = { + Type: LambdaType, + Block: { + blockType: BlockType.REPORTER, + blockShape: BlockShape.SQUARE, + forceOutputType: "Lambda", + disableMonitor: true + }, + Argument: { + shape: BlockShape.SQUARE, + check: ["Lambda"] + } +} + +class Extension { + constructor() { + vm.jwLambda = Lambda + vm.runtime.registerSerializer( + "jwLambda", + v => null, + v => new Lambda.Type("") + ); + } + + getInfo() { + return { + id: "jwLambda", + name: "Lambda", + color1: "#555555", + blocks: [ + { + opcode: 'arg', + text: 'argument', + blockType: BlockType.REPORTER, + hideFromPalette: true, + allowDropAnywhere: true, + canDragDuplicate: true + }, + { + opcode: 'newLambda', + text: 'new lambda [ARG]', + branchCount: 1, + arguments: { + ARG: { + fillIn: 'arg' + } + }, + ...Lambda.Block + }, + { + opcode: 'execute', + text: 'execute [LAMBDA] with [ARG]', + arguments: { + LAMBDA: Lambda.Argument, + ARG: { + type: ArgumentType.String, + defaultValue: "foo", + exemptFromNormalization: true + } + } + } + ] + }; + } + + arg({}, util) { + let lambda = util.thread.stackFrames[0].jwLambda + return lambda ?? "" + } + + newLambda({}, util) { + return new Lambda.Type(util) + } + + execute({LAMBDA, ARG}, util) { + LAMBDA = Lambda.Type.toLambda(LAMBDA) + + LAMBDA.execute(util.target, ARG) + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jwNum/expantanum.js b/local-scratch-vm/src/extensions/jwNum/expantanum.js new file mode 100644 index 0000000000000000000000000000000000000000..c3a3f7dd093aa1afb515a6fc08192035bec8eca2 --- /dev/null +++ b/local-scratch-vm/src/extensions/jwNum/expantanum.js @@ -0,0 +1,2157 @@ +// https://github.com/Naruyoko/ExpantaNum.js/blob/develop/ExpantaNum.js + +//Code snippets and templates from Decimal.js + +;(function (globalScope) { + "use strict"; + + + // -- EDITABLE DEFAULTS -- // + var ExpantaNum = { + + // The maximum number of operators stored in array. + // If the number of operations exceed the limit, then the least significant operations will be discarded. + // This is to prevent long loops and eating away of memory and processing time. + // 1000 means there are at maximum of 1000 elements in array. + // It is not recommended to make this number too big. + // `ExpantaNum.maxOps = 1000;` + maxOps: 1e3, + + // Specify what format is used when serializing for JSON.stringify + // + // JSON 0 JSON object + // STRING 1 String + serializeMode: 0, + + // Deprecated + // Level of debug information printed in console + // + // NONE 0 Show no information. + // NORMAL 1 Show operations. + // ALL 2 Show everything. + debug: 0 + }, + + + // -- END OF EDITABLE DEFAULTS -- // + + + external = true, + + expantaNumError = "[ExpantaNumError] ", + invalidArgument = expantaNumError + "Invalid argument: ", + + isExpantaNum = /^[-\+]*(Infinity|NaN|(J+|J\^\d+ )?(10(\^+|\{[1-9]\d*\})|\(10(\^+|\{[1-9]\d*\})\)\^[1-9]\d* )*((\d+(\.\d*)?|\d*\.\d+)?([Ee][-\+]*))*(0|\d+(\.\d*)?|\d*\.\d+))$/, + + MAX_SAFE_INTEGER = 9007199254740991, + MAX_E = Math.log10(MAX_SAFE_INTEGER), //15.954589770191003 + + // ExpantaNum.prototype object + P={}, + // ExpantaNum static object + Q={}, + // ExpantaNum constants + R={}; + + R.ZERO=0; + R.ONE=1; + R.E=Math.E; + R.LN2=Math.LN2; + R.LN10=Math.LN10; + R.LOG2E=Math.LOG2E; + R.LOG10E=Math.LOG10E; + R.PI=Math.PI; + R.SQRT1_2=Math.SQRT1_2; + R.SQRT2=Math.SQRT2; + R.MAX_SAFE_INTEGER=MAX_SAFE_INTEGER; + R.MIN_SAFE_INTEGER=Number.MIN_SAFE_INTEGER; + R.NaN=Number.NaN; + R.NEGATIVE_INFINITY=Number.NEGATIVE_INFINITY; + R.POSITIVE_INFINITY=Number.POSITIVE_INFINITY; + R.E_MAX_SAFE_INTEGER="e"+MAX_SAFE_INTEGER; + R.EE_MAX_SAFE_INTEGER="ee"+MAX_SAFE_INTEGER; + R.TETRATED_MAX_SAFE_INTEGER="10^^"+MAX_SAFE_INTEGER; + R.GRAHAMS_NUMBER="J^63 10^^^(10^)^7625597484984 3638334640023.7783"; + + + // ExpantaNum prototype methods + + P.absoluteValue=P.abs=function(){ + var x=this.clone(); + x.sign=1; + return x; + }; + Q.absoluteValue=Q.abs=function(x){ + return new ExpantaNum(x).abs(); + }; + P.negate=P.neg=function (){ + var x=this.clone(); + x.sign=x.sign*-1; + return x; + }; + Q.negate=Q.neg=function (x){ + return new ExpantaNum(x).neg(); + }; + P.compareTo=P.cmp=function (other){ + if (!(other instanceof ExpantaNum)) other=new ExpantaNum(other); + if (isNaN(this.array[0][1])||isNaN(other.array[0][1])) return NaN; + if (this.array[0][1]==Infinity&&other.array[0][1]!=Infinity) return this.sign; + if (this.array[0][1]!=Infinity&&other.array[0][1]==Infinity) return -other.sign; + if (this.array.length==1&&this.array[0][1]===0&&other.array.length==1&&other.array[0][1]===0) return 0; + if (this.sign!=other.sign) return this.sign; + var m=this.sign; + var r; + if (this.layer>other.layer) r=1; + else if (this.layerf[0]||e[0]==f[0]&&e[1]>f[1]){ + r=1; + break; + }else if (e[0]other.array.length){ + e=this.array[this.array.length-l]; + if (e[0]>=1||e[1]>10){ + r=1; + }else{ + r=-1; + } + }else{ + e=other.array[other.array.length-l]; + if (e[0]>=1||e[1]>10){ + r=-1; + }else{ + r=1; + } + } + } + } + return r*m; + }; + Q.compare=Q.cmp=function (x,y){ + return new ExpantaNum(x).cmp(y); + }; + P.greaterThan=P.gt=function (other){ + return this.cmp(other)>0; + }; + Q.greaterThan=Q.gt=function (x,y){ + return new ExpantaNum(x).gt(y); + }; + P.greaterThanOrEqualTo=P.gte=function (other){ + return this.cmp(other)>=0; + }; + Q.greaterThanOrEqualTo=Q.gte=function (x,y){ + return new ExpantaNum(x).gte(y); + }; + P.lessThan=P.lt=function (other){ + return this.cmp(other)<0; + }; + Q.lessThan=Q.lt=function (x,y){ + return new ExpantaNum(x).lt(y); + }; + P.lessThanOrEqualTo=P.lte=function (other){ + return this.cmp(other)<=0; + }; + Q.lessThanOrEqualTo=Q.lte=function (x,y){ + return new ExpantaNum(x).lte(y); + }; + P.equalsTo=P.equal=P.eq=function (other){ + return this.cmp(other)===0; + }; + Q.equalsTo=Q.equal=Q.eq=function (x,y){ + return new ExpantaNum(x).eq(y); + }; + P.notEqualsTo=P.notEqual=P.neq=function (other){ + return this.cmp(other)!==0; + }; + Q.notEqualsTo=Q.notEqual=Q.neq=function (x,y){ + return new ExpantaNum(x).neq(y); + }; + P.minimum=P.min=function (other){ + return this.lt(other)?this.clone():new ExpantaNum(other); + }; + Q.minimum=Q.min=function (x,y){ + return new ExpantaNum(x).min(y); + }; + P.maximum=P.max=function (other){ + return this.gt(other)?this.clone():new ExpantaNum(other); + }; + Q.maximum=Q.max=function (x,y){ + return new ExpantaNum(x).max(y); + }; + P.compareTo_tolerance=P.cmp_tolerance=function (other,tolerance){ + if (!(other instanceof ExpantaNum)) other=new ExpantaNum(other); + return this.eq_tolerance(other,tolerance)?0:this.cmp(other); + }; + Q.compare_tolerance=Q.cmp_tolerance=function (x,y,tolerance){ + return new ExpantaNum(x).cmp_tolerance(y,tolerance); + }; + P.greaterThan_tolerance=P.gt_tolerance=function (other,tolerance){ + if (!(other instanceof ExpantaNum)) other=new ExpantaNum(other); + return !this.eq_tolerance(other,tolerance)&&this.gt(other); + }; + Q.greaterThan_tolerance=Q.gt_tolerance=function (x,y,tolerance){ + return new ExpantaNum(x).gt_tolerance(y,tolerance); + }; + P.greaterThanOrEqualTo_tolerance=P.gte_tolerance=function (other,tolerance){ + if (!(other instanceof ExpantaNum)) other=new ExpantaNum(other); + return this.eq_tolerance(other,tolerance)||this.gt(other); + }; + Q.greaterThanOrEqualTo_tolerance=Q.gte_tolerance=function (x,y,tolerance){ + return new ExpantaNum(x).gte_tolerance(y,tolerance); + }; + P.lessThan_tolerance=P.lt_tolerance=function (other,tolerance){ + if (!(other instanceof ExpantaNum)) other=new ExpantaNum(other); + return !this.eq_tolerance(other,tolerance)&&this.lt(other); + }; + Q.lessThan_tolerance=Q.lt_tolerance=function (x,y,tolerance){ + return new ExpantaNum(x).lt_tolerance(y,tolerance); + }; + P.lessThanOrEqualTo_tolerance=P.lte_tolerance=function (other,tolerance){ + if (!(other instanceof ExpantaNum)) other=new ExpantaNum(other); + return this.eq_tolerance(other,tolerance)||this.lt(other); + }; + Q.lessThanOrEqualTo_tolerance=Q.lte_tolerance=function (x,y,tolerance){ + return new ExpantaNum(x).lte_tolerance(y,tolerance); + }; + //From break_eternity.js + //https://github.com/Patashu/break_eternity.js/blob/96901974c175cb28f66c7164a5a205cdda783872/src/index.ts#L2802 + P.equalsTo_tolerance=P.equal_tolerance=P.eq_tolerance=function (other,tolerance){ + if (!(other instanceof ExpantaNum)) other=new ExpantaNum(other); + if (tolerance==null) tolerance=1e-7; + if (this.isNaN()||other.isNaN()||this.isFinite()!=other.isFinite()) return false; + if (this.sign!=other.sign) return false; + if (Math.abs(this.layer-other.layer)>1) return false; + var a,b; + if (this.layer!=other.layer){ + var x,y; + if (this.layer>other.layer) x=this,y=other; + else x=other,y=this; + if (!(x.array.length==2&&x.array[0][0]===0&&x.array[1][0]==1&&x.array[1][1]==1)) return false; + a=x.array[0][1]; + if (y.array[y.array.length-1][1]>=10) b=Math.log10(y.array[y.array.length-1][0]+1); + else b=Math.log10(y.array[y.array.length-1][0]); + }else{ + if (Math.abs(this.array[this.array.length-1][0]-other.array[other.array.length-1][0])>1) return false; + for (var i=1;Math.max(this.array.length,other.array.length)-i>=0;++i){ + var c=this.array[this.array.length-i][0]; + var d=other.array[other.array.length-i][0]; + var x,y,e,f; + if (c!=d){ + if (c>d) x=this,y=other; + else x=other,y=this,c=d; + e=x.array[x.array.length-i][1]; + f=0; + }else{ + x=this; + y=other; + e=x.array[x.array.length-i][1]; + f=y.array[y.array.length-i][1]; + if (x.array.length-i==0){ + a=e; + b=f; + break; + } + } + if (Math.abs(e-f)>1) return false; + if (e!=f){ + if (!(x.array.length-i<2||x.array.length-i==2&&x.array[0][0]===0&&x.array[1][0]==1&&x.array[1][1]==1)) return false; + a=x.array[0][1]; + if (c==1) b=Math.log10(y.operator(0)); + else if (c==2&&y.operator(0)>=1e10) b=Math.log10(y.operator(1)+2); + else if (y.operator(c-2)>=10) b=Math.log10(y.operator(c-1)+1); + else b=Math.log10(y.operator(c-1)); + break; + } + } + } + return Math.abs(a-b)<=tolerance*Math.max(Math.abs(a),Math.abs(b)); + }; + Q.equalsTo_tolerance=Q.equal_tolerance=Q.eq_tolerance=function (x,y,tolerance){ + return new ExpantaNum(x).eq_tolerance(y,tolerance); + }; + P.notEqualsTo_tolerance=P.notEqual_tolerance=P.neq_tolerance=function (other,tolerance){ + return !this.eq_tolerance(other,tolerance); + }; + Q.notEqualsTo_tolerance=Q.notEqual_tolerance=Q.neq_tolerance=function (x,y,tolerance){ + return new ExpantaNum(x).neq_tolerance(y,tolerance); + }; + P.isPositive=P.ispos=function (){ + return this.gt(ExpantaNum.ZERO); + }; + Q.isPositive=Q.ispos=function (x){ + return new ExpantaNum(x).ispos(); + }; + P.isNegative=P.isneg=function (){ + return this.lt(ExpantaNum.ZERO); + }; + Q.isNegative=Q.isneg=function (x){ + return new ExpantaNum(x).isneg(); + }; + P.isNaN=function (){ + return isNaN(this.array[0][1]); + }; + Q.isNaN=function (x){ + return new ExpantaNum(x).isNaN(); + }; + P.isFinite=function (){ + return isFinite(this.array[0][1]); + }; + Q.isFinite=function (x){ + return new ExpantaNum(x).isFinite(); + }; + P.isInfinite=function (){ + return this.array[0][1]==Infinity; + }; + Q.isInfinite=function (x){ + return new ExpantaNum(x).isInfinite(); + }; + P.isInteger=P.isint=function (){ + if (this.sign==-1) return this.abs().isint(); + if (this.gt(ExpantaNum.MAX_SAFE_INTEGER)) return true; + return Number.isInteger(this.toNumber()); + }; + Q.isInteger=Q.isint=function (x){ + return new ExpantaNum(x).isint(); + }; + P.floor=function (){ + if (this.isInteger()) return this.clone(); + return new ExpantaNum(Math.floor(this.toNumber())); + }; + Q.floor=function (x){ + return new ExpantaNum(x).floor(); + }; + P.ceiling=P.ceil=function (){ + if (this.isInteger()) return this.clone(); + return new ExpantaNum(Math.ceil(this.toNumber())); + }; + Q.ceiling=Q.ceil=function (x){ + return new ExpantaNum(x).ceil(); + }; + P.round=function (){ + if (this.isInteger()) return this.clone(); + return new ExpantaNum(Math.round(this.toNumber())); + }; + Q.round=function (x){ + return new ExpantaNum(x).round(); + }; + var debugMessageSent=false; + P.plus=P.add=function (other){ + var x=this.clone(); + other=new ExpantaNum(other); + if (ExpantaNum.debug>=ExpantaNum.NORMAL){ + console.log(this+"+"+other); + if (!debugMessageSent) console.warn(expantaNumError+"Debug output via 'debug' is being deprecated and will be removed in the future!"),debugMessageSent=true; + } + if (x.sign==-1) return x.neg().add(other.neg()).neg(); + if (other.sign==-1) return x.sub(other.neg()); + if (x.eq(ExpantaNum.ZERO)) return other; + if (other.eq(ExpantaNum.ZERO)) return x; + if (x.isNaN()||other.isNaN()||x.isInfinite()&&other.isInfinite()&&x.eq(other.neg())) return ExpantaNum.NaN.clone(); + if (x.isInfinite()) return x; + if (other.isInfinite()) return other; + var p=x.min(other); + var q=x.max(other); + var op0=q.operator(0); + var op1=q.operator(1); + var t; + if (q.gt(ExpantaNum.E_MAX_SAFE_INTEGER)||q.div(p).gt(ExpantaNum.MAX_SAFE_INTEGER)){ + t=q; + }else if (!op1){ + t=new ExpantaNum(x.toNumber()+other.toNumber()); + }else if (op1==1){ + var a=p.operator(1)?p.operator(0):Math.log10(p.operator(0)); + t=new ExpantaNum([a+Math.log10(Math.pow(10,op0-a)+1),1]); + } + p=q=null; + return t; + }; + Q.plus=Q.add=function (x,y){ + return new ExpantaNum(x).add(y); + }; + P.minus=P.sub=function (other){ + var x=this.clone(); + other=new ExpantaNum(other); + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log(x+"-"+other); + if (x.sign==-1) return x.neg().sub(other.neg()).neg(); + if (other.sign==-1) return x.add(other.neg()); + if (x.eq(other)) return ExpantaNum.ZERO.clone(); + if (other.eq(ExpantaNum.ZERO)) return x; + if (x.isNaN()||other.isNaN()||x.isInfinite()&&other.isInfinite()) return ExpantaNum.NaN.clone(); + if (x.isInfinite()) return x; + if (other.isInfinite()) return other.neg(); + var p=x.min(other); + var q=x.max(other); + var n=other.gt(x); + var op0=q.operator(0); + var op1=q.operator(1); + var t; + if (q.gt(ExpantaNum.E_MAX_SAFE_INTEGER)||q.div(p).gt(ExpantaNum.MAX_SAFE_INTEGER)){ + t=q; + t=n?t.neg():t; + }else if (!op1){ + t=new ExpantaNum(x.toNumber()-other.toNumber()); + }else if (op1==1){ + var a=p.operator(1)?p.operator(0):Math.log10(p.operator(0)); + t=new ExpantaNum([a+Math.log10(Math.pow(10,op0-a)-1),1]); + t=n?t.neg():t; + } + p=q=null; + return t; + }; + Q.minus=Q.sub=function (x,y){ + return new ExpantaNum(x).sub(y); + }; + P.times=P.mul=function (other){ + var x=this.clone(); + other=new ExpantaNum(other); + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log(x+"*"+other); + if (x.sign*other.sign==-1) return x.abs().mul(other.abs()).neg(); + if (x.sign==-1) return x.abs().mul(other.abs()); + if (x.isNaN()||other.isNaN()||x.eq(ExpantaNum.ZERO)&&other.isInfinite()||x.isInfinite()&&other.abs().eq(ExpantaNum.ZERO)) return ExpantaNum.NaN.clone(); + if (other.eq(ExpantaNum.ZERO)) return ExpantaNum.ZERO.clone(); + if (other.eq(ExpantaNum.ONE)) return x.clone(); + if (x.isInfinite()) return x; + if (other.isInfinite()) return other; + if (x.max(other).gt(ExpantaNum.EE_MAX_SAFE_INTEGER)) return x.max(other); + var n=x.toNumber()*other.toNumber(); + if (n<=MAX_SAFE_INTEGER) return new ExpantaNum(n); + return ExpantaNum.pow(10,x.log10().add(other.log10())); + }; + Q.times=Q.mul=function (x,y){ + return new ExpantaNum(x).mul(y); + }; + P.divide=P.div=function (other){ + var x=this.clone(); + other=new ExpantaNum(other); + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log(x+"/"+other); + if (x.sign*other.sign==-1) return x.abs().div(other.abs()).neg(); + if (x.sign==-1) return x.abs().div(other.abs()); + if (x.isNaN()||other.isNaN()||x.isInfinite()&&other.isInfinite()||x.eq(ExpantaNum.ZERO)&&other.eq(ExpantaNum.ZERO)) return ExpantaNum.NaN.clone(); + if (other.eq(ExpantaNum.ZERO)) return ExpantaNum.POSITIVE_INFINITY.clone(); + if (other.eq(ExpantaNum.ONE)) return x.clone(); + if (x.eq(other)) return ExpantaNum.ONE.clone(); + if (x.isInfinite()) return x; + if (other.isInfinite()) return ExpantaNum.ZERO.clone(); + if (x.max(other).gt(ExpantaNum.EE_MAX_SAFE_INTEGER)) return x.gt(other)?x.clone():ExpantaNum.ZERO.clone(); + var n=x.toNumber()/other.toNumber(); + if (n<=MAX_SAFE_INTEGER) return new ExpantaNum(n); + var pw=ExpantaNum.pow(10,x.log10().sub(other.log10())); + var fp=pw.floor(); + if (pw.sub(fp).lt(new ExpantaNum(1e-9))) return fp; + return pw; + }; + Q.divide=Q.div=function (x,y){ + return new ExpantaNum(x).div(y); + }; + P.reciprocate=P.rec=function (){ + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log(this+"^-1"); + if (this.isNaN()||this.eq(ExpantaNum.ZERO)) return ExpantaNum.NaN.clone(); + if (this.abs().gt("2e323")) return ExpantaNum.ZERO.clone(); + return new ExpantaNum(1/this); + }; + Q.reciprocate=Q.rec=function (x){ + return new ExpantaNum(x).rec(); + }; + P.modular=P.mod=function (other){ + other=new ExpantaNum(other); + if (other.eq(ExpantaNum.ZERO)) return ExpantaNum.ZERO.clone(); + if (this.sign*other.sign==-1) return this.abs().mod(other.abs()).neg(); + if (this.sign==-1) return this.abs().mod(other.abs()); + return this.sub(this.div(other).floor().mul(other)); + }; + Q.modular=Q.mod=function (x,y){ + return new ExpantaNum(x).mod(y); + }; + //All of these are from Patashu's break_eternity.js + //from HyperCalc source code + var f_gamma=function (n){ + if (!isFinite(n)) return n; + if (n<-50){ + if (n==Math.trunc(n)) return Number.NEGATIVE_INFINITY; + return 0; + } + var scal1=1; + while (n<10){ + scal1=scal1*n; + ++n; + } + n-=1; + var l=0.9189385332046727; //0.5*Math.log(2*Math.PI) + l+=(n+0.5)*Math.log(n); + l-=n; + var n2=n*n; + var np=n; + l+=1/(12*np); + np*=n2; + l-=1/(360*np); + np*=np*n2; + l+=1/(1260*np); + np*=n2; + l-=1/(1680*np); + np*=n2; + l+=1/(1188*np); + np*=n2; + l-=691/(360360*np); + np*=n2; + l+=7/(1092*np); + np*=n2; + l-=3617/(122400*np); + return Math.exp(l)/scal1; + }; + //from HyperCalc source code + P.gamma=function (){ + var x=this.clone(); + if (x.gt(ExpantaNum.TETRATED_MAX_SAFE_INTEGER)) return x; + if (x.gt(ExpantaNum.E_MAX_SAFE_INTEGER)) return ExpantaNum.exp(x); + if (x.gt(ExpantaNum.MAX_SAFE_INTEGER)) return ExpantaNum.exp(ExpantaNum.mul(x,ExpantaNum.ln(x).sub(1))); + var n=x.operator(0); + if (n>1){ + if (n<24) return new ExpantaNum(f_gamma(x.sign*n)); + var t=n-1; + var l=0.9189385332046727; //0.5*Math.log(2*Math.PI) + l+=((t+0.5)*Math.log(t)); + l-=t; + var n2=t*t; + var np=t; + var lm=12*np; + var adj=1/lm; + var l2=l+adj; + if (l2==l) return ExpantaNum.exp(l); + l=l2; + np*=n2; + lm=360*np; + adj=1/lm; + l2=l-adj; + if (l2==l) return ExpantaNum.exp(l); + l=l2; + np*=n2; + lm=1260*np; + var lt=1/lm; + l+=lt; + np*=n2; + lm=1680*np; + lt=1/lm; + l-=lt; + return ExpantaNum.exp(l); + }else return this.rec(); + }; + Q.gamma=function (x){ + return new ExpantaNum(x).gamma(); + }; + //end break_eternity.js excerpt + Q.factorials=[1,1,2,6,24,120,720,5040,40320,362880,3628800,39916800,479001600,6227020800,87178291200,1307674368000,20922789888000,355687428096000,6402373705728000,121645100408832000,2432902008176640000,51090942171709440000,1.1240007277776076800e+21,2.5852016738884978213e+22,6.2044840173323941000e+23,1.5511210043330986055e+25,4.0329146112660565032e+26,1.0888869450418351940e+28,3.0488834461171387192e+29,8.8417619937397018986e+30,2.6525285981219106822e+32,8.2228386541779224302e+33,2.6313083693369351777e+35,8.6833176188118859387e+36,2.9523279903960415733e+38,1.0333147966386145431e+40,3.7199332678990125486e+41,1.3763753091226345579e+43,5.2302261746660111714e+44,2.0397882081197444123e+46,8.1591528324789768380e+47,3.3452526613163807956e+49,1.4050061177528799549e+51,6.0415263063373834074e+52,2.6582715747884488694e+54,1.1962222086548018857e+56,5.5026221598120891536e+57,2.5862324151116817767e+59,1.2413915592536072528e+61,6.0828186403426752249e+62,3.0414093201713375576e+64,1.5511187532873821895e+66,8.0658175170943876846e+67,4.2748832840600254848e+69,2.3084369733924137924e+71,1.2696403353658276447e+73,7.1099858780486348103e+74,4.0526919504877214100e+76,2.3505613312828784949e+78,1.3868311854568983861e+80,8.3209871127413898951e+81,5.0758021387722483583e+83,3.1469973260387939390e+85,1.9826083154044400850e+87,1.2688693218588416544e+89,8.2476505920824715167e+90,5.4434493907744306945e+92,3.6471110918188683221e+94,2.4800355424368305480e+96,1.7112245242814129738e+98,1.1978571669969892213e+100,8.5047858856786230047e+101,6.1234458376886084639e+103,4.4701154615126843855e+105,3.3078854415193862416e+107,2.4809140811395399745e+109,1.8854947016660503806e+111,1.4518309202828587210e+113,1.1324281178206296794e+115,8.9461821307829757136e+116,7.1569457046263805709e+118,5.7971260207473678414e+120,4.7536433370128420198e+122,3.9455239697206587884e+124,3.3142401345653531943e+126,2.8171041143805501310e+128,2.4227095383672734128e+130,2.1077572983795278544e+132,1.8548264225739843605e+134,1.6507955160908460244e+136,1.4857159644817615149e+138,1.3520015276784029158e+140,1.2438414054641308179e+142,1.1567725070816415659e+144,1.0873661566567430754e+146,1.0329978488239059305e+148,9.9167793487094964784e+149,9.6192759682482120384e+151,9.4268904488832479837e+153,9.3326215443944153252e+155,9.3326215443944150966e+157,9.4259477598383598816e+159,9.6144667150351270793e+161,9.9029007164861804721e+163,1.0299016745145628100e+166,1.0813967582402909767e+168,1.1462805637347083683e+170,1.2265202031961380050e+172,1.3246418194518290179e+174,1.4438595832024936625e+176,1.5882455415227430287e+178,1.7629525510902445874e+180,1.9745068572210740115e+182,2.2311927486598137657e+184,2.5435597334721876552e+186,2.9250936934930159967e+188,3.3931086844518980862e+190,3.9699371608087210616e+192,4.6845258497542909237e+194,5.5745857612076058231e+196,6.6895029134491271205e+198,8.0942985252734440920e+200,9.8750442008336010580e+202,1.2146304367025329301e+205,1.5061417415111409314e+207,1.8826771768889261129e+209,2.3721732428800468512e+211,3.0126600184576594309e+213,3.8562048236258040716e+215,4.9745042224772874590e+217,6.4668554892204741474e+219,8.4715806908788206314e+221,1.1182486511960043298e+224,1.4872707060906857134e+226,1.9929427461615187928e+228,2.6904727073180504073e+230,3.6590428819525488642e+232,5.0128887482749919605e+234,6.9177864726194885808e+236,9.6157231969410893532e+238,1.3462012475717525742e+241,1.8981437590761708898e+243,2.6953641378881628530e+245,3.8543707171800730787e+247,5.5502938327393044385e+249,8.0479260574719917061e+251,1.1749972043909107097e+254,1.7272458904546389230e+256,2.5563239178728653927e+258,3.8089226376305697893e+260,5.7133839564458546840e+262,8.6272097742332399855e+264,1.3113358856834524492e+267,2.0063439050956822953e+269,3.0897696138473507759e+271,4.7891429014633940780e+273,7.4710629262828942235e+275,1.1729568794264144743e+278,1.8532718694937349890e+280,2.9467022724950384028e+282,4.7147236359920616095e+284,7.5907050539472189932e+286,1.2296942187394494177e+289,2.0044015765453026266e+291,3.2872185855342959088e+293,5.4239106661315886750e+295,9.0036917057784375454e+297,1.5036165148649991456e+300,2.5260757449731984219e+302,4.2690680090047051083e+304,7.2574156153079990350e+306]; + P.factorial=P.fact=function (){ + var x=this.clone(); + var f=ExpantaNum.factorials; + if (x.lt(ExpantaNum.ZERO)||!x.isint()) return x.add(1).gamma(); + if (x.lte(170)) return new ExpantaNum(f[+x]); + var errorFixer=1; + var e=+x; + if (e<500) e+=163879/209018880*Math.pow(e,5); + if (e<1000) e+=-571/2488320*Math.pow(e,4); + if (e<50000) e+=-139/51840*Math.pow(e,3); + if (e<1e7) e+=1/288*Math.pow(e,2); + if (e<1e20) e+=1/12*e; + return x.div(ExpantaNum.E).pow(x).mul(x.mul(ExpantaNum.PI).mul(2).sqrt()).times(errorFixer); + }; + Q.factorial=Q.fact=function (x){ + return new ExpantaNum(x).fact(); + }; + P.toPower=P.pow=function (other){ + other=new ExpantaNum(other); + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log(this+"^"+other); + if (other.eq(ExpantaNum.ZERO)) return ExpantaNum.ONE.clone(); + if (other.eq(ExpantaNum.ONE)) return this.clone(); + if (other.lt(ExpantaNum.ZERO)) return this.pow(other.neg()).rec(); + if (this.lt(ExpantaNum.ZERO)&&other.isint()){ + if (other.mod(2).lt(ExpantaNum.ONE)) return this.abs().pow(other); + return this.abs().pow(other).neg(); + } + if (this.lt(ExpantaNum.ZERO)) return ExpantaNum.NaN.clone(); + if (this.eq(ExpantaNum.ONE)) return ExpantaNum.ONE.clone(); + if (this.eq(ExpantaNum.ZERO)) return ExpantaNum.ZERO.clone(); + if (this.max(other).gt(ExpantaNum.TETRATED_MAX_SAFE_INTEGER)) return this.max(other); + if (this.eq(10)){ + if (other.gt(ExpantaNum.ZERO)){ + other.operator(1,(other.operator(1)+1)||1); + other.normalize(); + return other; + }else{ + return new ExpantaNum(Math.pow(10,other.toNumber())); + } + } + if (other.lt(ExpantaNum.ONE)) return this.root(other.rec()); + var n=Math.pow(this.toNumber(),other.toNumber()); + if (n<=MAX_SAFE_INTEGER) return new ExpantaNum(n); + return ExpantaNum.pow(10,this.log10().mul(other)); + }; + Q.toPower=Q.pow=function (x,y){ + return new ExpantaNum(x).pow(y); + }; + P.exponential=P.exp=function (){ + return ExpantaNum.pow(Math.E,this); + }; + Q.exponential=Q.exp=function (x){ + return ExpantaNum.pow(Math.E,x); + }; + P.squareRoot=P.sqrt=function (){ + return this.root(2); + }; + Q.squareRoot=Q.sqrt=function (x){ + return new ExpantaNum(x).root(2); + }; + P.cubeRoot=P.cbrt=function (){ + return this.root(3); + }; + Q.cubeRoot=Q.cbrt=function (x){ + return new ExpantaNum(x).root(3); + }; + P.root=function (other){ + other=new ExpantaNum(other); + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log(this+"root"+other); + if (other.eq(ExpantaNum.ONE)) return this.clone(); + if (other.lt(ExpantaNum.ZERO)) return this.root(other.neg()).rec(); + if (other.lt(ExpantaNum.ONE)) return this.pow(other.rec()); + if (this.lt(ExpantaNum.ZERO)&&other.isint()&&other.mod(2).eq(ExpantaNum.ONE)) return this.neg().root(other).neg(); + if (this.lt(ExpantaNum.ZERO)) return ExpantaNum.NaN.clone(); + if (this.eq(ExpantaNum.ONE)) return ExpantaNum.ONE.clone(); + if (this.eq(ExpantaNum.ZERO)) return ExpantaNum.ZERO.clone(); + if (this.max(other).gt(ExpantaNum.TETRATED_MAX_SAFE_INTEGER)) return this.gt(other)?this.clone():ExpantaNum.ZERO.clone(); + return ExpantaNum.pow(10,this.log10().div(other)); + }; + Q.root=function (x,y){ + return new ExpantaNum(x).root(y); + }; + P.generalLogarithm=P.log10=function (){ + var x=this.clone(); + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log("log"+this); + if (x.lt(ExpantaNum.ZERO)) return ExpantaNum.NaN.clone(); + if (x.eq(ExpantaNum.ZERO)) return ExpantaNum.NEGATIVE_INFINITY.clone(); + if (x.lte(ExpantaNum.MAX_SAFE_INTEGER)) return new ExpantaNum(Math.log10(x.toNumber())); + if (!x.isFinite()) return x; + if (x.gt(ExpantaNum.TETRATED_MAX_SAFE_INTEGER)) return x; + x.operator(1,x.operator(1)-1); + return x.normalize(); + }; + Q.generalLogarithm=Q.log10=function (x){ + return new ExpantaNum(x).log10(); + }; + P.logarithm=P.logBase=function (base){ + if (base===undefined) base=Math.E; + return this.log10().div(ExpantaNum.log10(base)); + }; + Q.logarithm=Q.logBase=function (x,base){ + return new ExpantaNum(x).logBase(base); + }; + P.naturalLogarithm=P.log=P.ln=function (){ + return this.logBase(Math.E); + }; + Q.naturalLogarithm=Q.log=Q.ln=function (x){ + return new ExpantaNum(x).ln(); + }; + //All of these are from Patashu's break_eternity.js + var OMEGA=0.56714329040978387299997; //W(1,0) + //from https://math.stackexchange.com/a/465183 + //The evaluation can become inaccurate very close to the branch point + var f_lambertw=function (z,tol,principal){ + if (tol===undefined) tol=1e-10; + if (principal===undefined) principal=true; + var w; + if (!Number.isFinite(z)) return z; + if (principal){ + if (z===0) return z; + if (z===1) return OMEGA; + if (z<10) w=0; + else w=Math.log(z)-Math.log(Math.log(z)); + }else{ + if (z===0) return -Infinity; + if (z<=-0.1) w=-2; + else w=Math.log(-z)-Math.log(-Math.log(-z)); + } + for (var i=0;i<100;++i){ + var wn=(z*Math.exp(-w)+w*w)/(w+1); + if (Math.abs(wn-w)=ExpantaNum.NORMAL) console.log(t+"^^"+other); + var negln; + if (other.isInfinite()&&other.sign>0){ + if (t.gte(Math.exp(1/Math.E))) return ExpantaNum.POSITIVE_INFINITY.clone(); + //Formula for infinite height power tower. + negln = t.ln().neg(); + return negln.lambertw().div(negln); + } + if (other.lte(-2)) return ExpantaNum.NaN.clone(); + if (t.eq(ExpantaNum.ZERO)){ + if (other.eq(ExpantaNum.ZERO)) return ExpantaNum.NaN.clone(); + if (other.mod(2).eq(ExpantaNum.ZERO)) return ExpantaNum.ZERO.clone(); + return ExpantaNum.ONE.clone(); + } + if (t.eq(ExpantaNum.ONE)){ + if (other.eq(ExpantaNum.ONE.neg())) return ExpantaNum.NaN.clone(); + return ExpantaNum.ONE.clone(); + } + if (other.eq(ExpantaNum.ONE.neg())) return ExpantaNum.ZERO.clone(); + if (other.eq(ExpantaNum.ZERO)) return ExpantaNum.ONE.clone(); + if (other.eq(ExpantaNum.ONE)) return t; + if (other.eq(2)) return t.pow(t); + if (t.eq(2)){ + if (other.eq(3)) return new ExpantaNum(16); + if (other.eq(4)) return new ExpantaNum(65536); + } + var m=t.max(other); + if (m.gt("10^^^"+MAX_SAFE_INTEGER)) return m; + if (m.gt(ExpantaNum.TETRATED_MAX_SAFE_INTEGER)||other.gt(ExpantaNum.MAX_SAFE_INTEGER)){ + if (this.lt(Math.exp(1/Math.E))){ + negln = t.ln().neg(); + return negln.lambertw().div(negln); + } + var j=t.slog(10).add(other); + j.operator(2,(j.operator(2)||0)+1); + j.normalize(); + return j; + } + var y=other.toNumber(); + var f=Math.floor(y); + var r=t.pow(y-f); + var l=ExpantaNum.NaN; + for (var i=0,w=new ExpantaNum(ExpantaNum.E_MAX_SAFE_INTEGER);f!==0&&r.lt(w)&&i<100;++i){ + if (f>0){ + r=t.pow(r); + if (l.eq(r)){ + f=0; + break; + } + l=r; + --f; + }else{ + r=r.logBase(t); + if (l.eq(r)){ + f=0; + break; + } + l=r; + ++f; + } + } + if (i==100||this.lt(Math.exp(1/Math.E))) f=0; + r.operator(1,(r.operator(1)+f)||f); + r.normalize(); + return r; + }; + Q.tetrate=Q.tetr=function (x,y,payload){ + return new ExpantaNum(x).tetr(y,payload); + }; + //Implementation of functions from break_eternity.js + P.iteratedexp=function (other,payload){ + return this.tetr(other,payload); + }; + Q.iteratedexp=function (x,y,payload){ + return new ExpantaNum(x).iteratedexp(y,payload); + }; + //This implementation is highly inaccurate and slow, and probably be given custom code + P.iteratedlog=function (base,other){ + if (base===undefined) base=10; + if (other===undefined) other=ExpantaNum.ONE.clone(); + var t=this.clone(); + base=new ExpantaNum(base); + other=new ExpantaNum(other); + if (other.eq(ExpantaNum.ZERO)) return t; + if (other.eq(ExpantaNum.ONE)) return t.logBase(base); + return base.tetr(t.slog(base).sub(other)); + }; + Q.iteratedlog=function (x,y,z){ + return new ExpantaNum(x).iteratedlog(y,z); + }; + P.layeradd=function (other,base){ + if (base===undefined) base=10; + if (other===undefined) other=ExpantaNum.ONE.clone(); + var t=this.clone(); + base=new ExpantaNum(base); + other=new ExpantaNum(other); + return base.tetr(t.slog(base).add(other)); + }; + Q.layeradd=function (x,y,z){ + return new ExpantaNum(x).layeradd(y,z); + }; + P.layeradd10=function (other){ + return this.layeradd(other); + }; + Q.layeradd10=function (x,y){ + return new ExpantaNum(x).layeradd10(y); + }; + //End implementation from break_eternity.js + //All of these are from Patashu's break_eternity.js + //The super square-root function - what number, tetrated to height 2, equals this? + //Other sroots are possible to calculate probably through guess and check methods, this one is easy though. + //https://en.wikipedia.org/wiki/Tetration#Super-root + P.ssqrt=P.ssrt=function (){ + var x=this.clone(); + if (x.lt(Math.exp(-1/Math.E))) return ExpantaNum.NaN.clone(); + if (!x.isFinite()) return x; + if (x.gt(ExpantaNum.TETRATED_MAX_SAFE_INTEGER)) return x; + if (x.gt(ExpantaNum.EE_MAX_SAFE_INTEGER)){ + x.operator(1,x.operator(1)-1); + return x; + } + var l=x.ln(); + return l.div(l.lambertw()); + }; + Q.ssqrt=Q.ssrt=function (x){ + return new ExpantaNum(x).ssqrt(); + }; + //Uses linear approximation + //For more information, please see the break_eternity.js source: + //https://github.com/Patashu/break_eternity.js/blob/848736e3dc37d8e7b5cc238f46e3ddb277d0dce2/src/index.ts#L4008 + P.linear_sroot=function (degree){ + var x=new ExpantaNum(this); + degree=new ExpantaNum(degree); + if (degree.isNaN()) return ExpantaNum.NaN.clone(); + var degreeNum=Number(degree); + if (degreeNum==1) return x; + if (x.eq(ExpantaNum.POSITIVE_INFINITY)) return ExpantaNum.POSITIVE_INFINITY.clone(); + if (!x.isFinite()) return ExpantaNum.NaN.clone(); + if (degreeNum>0&°reeNum<1) return x.root(degree); + if (degreeNum>-2&°reeNum<-1) return degree.add(2).pow(x.rec()); + if (degreeNum<=0) return ExpantaNum.NaN.clone(); + if (degree.gt(ExpantaNum.MAX_SAFE_INTEGER)){ + var xNum=Number(x); + if (xNum1/Math.E) return x.pow(x.rec()); + if (x.gt(ExpantaNum.TETRATED_MAX_SAFE_INTEGER)){ + var nh=x.slog(10).sub(degree); + if (nh.lte(ExpantaNum.ZERO)) return new ExpantaNum(Math.exp(1/Math.E)); + return ExpantaNum.tetr(10,nh); + } + return ExpantaNum.NaN.clone(); + } + if (x.eq(ExpantaNum.ONE)) return ExpantaNum.ONE.clone(); + if (x.lt(ExpantaNum.ZERO)) return ExpantaNum.NaN.clone(); + if (x.eq(ExpantaNum.ZERO)) return ExpantaNum.ZERO.clone(); + if (x.gt(ExpantaNum.ONE)){ + var upperBound; + if (degreeNum<=1) upperBound=x.root(degree); + else if (x.gte(ExpantaNum.tetr(10,degree))) upperBound=x.iteratedlog(10,degreeNum-1); + else upperBound=new ExpantaNum(10); + var lower=ExpantaNum.ZERO; + var layer=upperBound.array[2]||0; + var upper=upperBound.iteratedlog(10,layer); + var guess=upper.div(2); + while (true){ + if (ExpantaNum.iteratedexp(10,layer,guess).tetr(degree).gt(x)) upper=guess; + else lower=guess; + var newguess=lower.add(upper).div(2); + if (newguess.eq(guess)) break; + guess=newguess; + } + return ExpantaNum.iteratedexp(10,layer,guess); + }else{ + var BIG=new ExpantaNum("10^^10"); + var stage=1; + var minimum=BIG; + var maximum=BIG; + var lower=BIG + var upper=new ExpantaNum(1e-16) + var prevspan=ExpantaNum.ZERO; + var difference=BIG; + var upperBound=ExpantaNum.pow(10,upper).rec(); + var distance=ExpantaNum.ZERO; + var prevPoint=upperBound; + var nextPoint=upperBound; + var evenDegree=Math.ceil(degreeNum)%2==0; + var range=0; + var lastValid=BIG; + var infLoopDetector=false; + var previousUpper=ExpantaNum.ZERO; + var decreasingFound=false; + while (stage<4){ + if (stage==2){ + if (evenDegree) break; + lower=BIG; + upper=minimum; + stage=3; + difference=BIG; + lastValid=BIG; + } + infLoopDetector=false; + while (upper.neq(lower)){ + previousUpper=upper; + var up10r=ExpantaNum.pow(10,upper).rec(); + var up10rtd=up10r.tetr(degree); + if (up10rtd.eq(ExpantaNum.ONE)&&up10r.lt(0.4)){ + upperBound=up10r; + prevPoint=up10r; + nextPoint=up10r; + distance=ExpantaNum.ZERO; + range=-1; + if (stage==3) lastValid=upper; + }else if (up10rtd.eq(up10r)&&!evenDegree&&up10r.lt(0.4)){ + upperBound=up10r; + prevPoint=up10r; + nextPoint=up10r; + distance=ExpantaNum.ZERO; + range=0; + }else if (up10rtd.eq(up10r.mul(2).tetr(degree))){ + upperBound=up10r; + prevPoint=ExpantaNum.ZERO; + nextPoint=upperBound.mul(2); + distance=upperBound; + if (evenDegree) range=-1; + else range=0; + }else{ + prevspan=upper.mul(1.2e-16); + upperBound=up10r; + prevPoint=ExpantaNum.pow(10,upper.add(prevspan)).rec(); + distance=upperBound.sub(prevPoint); + nextPoint=upperBound.add(distance); + var ubtd=upperBound.tetr(degree); //upperBound does not change during lifetime + var pptd; + var nptd; + while (prevPoint.gte(upperBound)||nextPoint.lte(upperBound)||(pptd=prevPoint.tetr(degree)).eq(ubtd)||(nptd=nextPoint.tetr(degree)).eq(ubtd)){ + prevspan=prevspan.mul(2); + prevPoint=ExpantaNum.pow(10,upper.add(prevspan)).rec(); + distance=upperBound.sub(prevPoint); + nextPoint=upperBound.add(distance); + } + //pptd and nptd are up-to-date + if (stage==1&&nptd.gt(ubtd)&&pptd.gt(ubtd)||stage==3&&nptd.lt(ubtd)&&pptd.lt(ubtd)) lastValid=upper; + if (nptd.lt(ubtd)) range=-1; + else if (evenDegree) range=1; + else if (stage==3&&upper.gt_tolerance(minimum,1e-8)) range=0; + else{ + while (prevPoint.gte(upperBound)||nextPoint.lte(upperBound)||(pptd=prevPoint.tetr(degree)).eq_tolerance(ubtd,1e-8)||(nptd=nextPoint.tetr(degree)).eq_tolerance(ubtd,1e-8)){ + prevspan=prevspan.mul(2); + prevPoint=ExpantaNum.pow(10,upper.add(prevspan)).rec(); + distance=upperBound.sub(prevPoint); + nextPoint=upperBound.add(distance); + } + //pptd and nptd are up-to-date + if (nptd.sub(ubtd).lt(ubtd.sub(pptd))) range=0; + else range=1; + } + } + if (range==-1) decreasingFound=true; + if (stage==1&&range==1||stage==3&&range!=0){ + if (lower.eq(BIG)) upper=upper.mul(2); + else{ + upper=upper.add(lower).div(2); + if (infLoopDetector&&(range==1&&stage==1||range==-1&&stage==3)) break; + } + }else{ + if (lower.eq(BIG)){ + lower=upper; + upper=upper.div(2); + }else{ + lower=lower.sub(difference); + upper=upper.sub(difference); + if (infLoopDetector&&(range==1&&stage==1||range==-1&&stage==3)) break; + } + } + var newDifference=lower.sub(upper).div(2).abs(); + if (newDifference.gt(difference.mul(1.5))) infLoopDetector=true; + difference=newDifference; + if (upper.gt(1e18)||upper.eq(previousUpper)) break; + } + if (upper.gt(1e18)) break; + if (!decreasingFound) break; + if (lastValid.eq(BIG)) break; + if (stage==1) minimum=lastValid; + else if (stage==3) maximum=lastValid; + stage++; + } + lower=minimum; + upper=new ExpantaNum(1e-18); + var previous=upper; + var guess=ExpantaNum.ZERO; + var loopGoing=true; + while (loopGoing){ + if (lower.eq(BIG)) guess=upper.mul(2); + else guess=lower.add(upper).div(2); + if (ExpantaNum.pow(10,guess).rec().tetr(degree).gt(x)) upper=guess; + else lower=guess; + if (guess.eq(previous)) loopGoing=false; + else previous=guess; + if (upper.gt(1e18)) return ExpantaNum.NaN.clone(); + } + if (guess.neq_tolerance(minimum,1e-15)) return ExpantaNum.pow(10,guess).rec(); + else{ + if (maximum.eq(BIG)) return ExpantaNum.NaN.clone(); + lower=BIG; + upper=maximum; + previous=upper; + guess=ExpantaNum.ZERO; + var loopGoing=true; + while (loopGoing){ + if (lower.eq(BIG)) guess=upper.mul(2); + else guess=lower.add(upper).div(2); + if (ExpantaNum.pow(10,guess).rec().tetr(degree).gt(x)) upper=guess; + else lower=guess; + if (guess.eq(previous)) loopGoing=false; + else previous=guess; + if (upper.gt(1e18)) return ExpantaNum.NaN.clone(); + } + return ExpantaNum.pow(10,guess).rec(); + } + } + }; + Q.linear_sroot=function (x,y){ + return new ExpantaNum(x).linear_sroot(y); + }; + //Super-logarithm, one of tetration's inverses, tells you what size power tower you'd have to tetrate base to to get number. By definition, will never be higher than 1.8e308 in break_eternity.js, since a power tower 1.8e308 numbers tall is the largest representable number. + //Uses linear approximation + //https://en.wikipedia.org/wiki/Super-logarithm + P.slog=function (base){ + if (base===undefined) base=10; + var x=this.clone(); + base=new ExpantaNum(base); + if (x.isNaN()||base.isNaN()||x.isInfinite()&&base.isInfinite()) return ExpantaNum.NaN.clone(); + if (x.isInfinite()) return x; + if (base.isInfinite()) return ExpantaNum.ZERO.clone(); + if (x.eq(ExpantaNum.ZERO)) return ExpantaNum.ONE.neg(); + if (x.eq(ExpantaNum.ONE)) return ExpantaNum.ZERO.clone(); + if (x.eq(base)) return ExpantaNum.ONE.clone(); + if (base.lt(Math.exp(1/Math.E))){ + var a=ExpantaNum.tetr(base,Infinity); + if (x.eq(a)) return ExpantaNum.POSITIVE_INFINITY.clone(); + if (x.gt(a)) return ExpantaNum.NaN.clone(); + } + if (x.max(base).gt("10^^^"+MAX_SAFE_INTEGER)){ + if (x.gt(base)) return x; + return ExpantaNum.ZERO.clone(); + } + if (x.max(base).gt(ExpantaNum.TETRATED_MAX_SAFE_INTEGER)){ + if (x.gt(base)){ + x.operator(2,x.operator(2)-1); + x.normalize(); + return x.sub(x.operator(1)); + } + return ExpantaNum.ZERO.clone(); + } + if (x.lt(ExpantaNum.ZERO)) return base.pow(x).sub(2); //Inversion of x^^y=log_x(2+y) for -23){ + var l=t-3; + r+=l; + x.operator(1,x.operator(1)-l); + } + for (var i=0;i<100;++i){ + if (x.lte(ExpantaNum.ONE)) return new ExpantaNum(r+x.toNumber()-1); + ++r; + x=ExpantaNum.logBase(x,base); + } + return ExpantaNum.NaN.clone(); //Failed to converge + }; + Q.slog=function (x,y){ + return new ExpantaNum(x).slog(y); + }; + //end break_eternity.js excerpt + P.pentate=P.pent=function (other){ + return this.arrow(3)(other); + }; + Q.pentate=Q.pent=function (x,y){ + return ExpantaNum.arrow(x,3,y); + }; + P.penta_log=function (other){ + return this.arrow_height_inverse(3)(other); + }; + Q.penta_log=function (x,y){ + return ExpantaNum.arrow_height_inverse(x,3,y); + }; + //Uses linear approximations for real height + P.arrow=function (arrows){ + var t=this.clone(); + arrows=new ExpantaNum(arrows); + if (!arrows.isint()||arrows.lt(ExpantaNum.ZERO)) return function(other){return ExpantaNum.NaN.clone();}; + if (arrows.eq(ExpantaNum.ZERO)) return function(other){return t.mul(other);}; + if (arrows.eq(ExpantaNum.ONE)) return function(other){return t.pow(other);}; + if (arrows.eq(2)) return function(other,payload){return t.tetr(other,payload);}; + return function (other,payload,depth){ + if (payload===undefined) payload=ExpantaNum.ONE; + if (depth===undefined) depth=0; + other=new ExpantaNum(other); + payload=new ExpantaNum(payload); + if (t.isNaN()||other.isNaN()||payload.isNaN()) return ExpantaNum.NaN.clone(); + if (payload.neq(ExpantaNum.ONE)) other=other.add(payload.arrow_height_inverse(arrows)(t)); + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log(t+"{"+arrows+"}"+other); + if (t.eq(ExpantaNum.ZERO)){ + if (other.eq(ExpantaNum.ONE)) return ExpantaNum.ZERO.clone(); + return ExpantaNum.NaN.clone(); + } + if (t.eq(ExpantaNum.ONE)) return ExpantaNum.ONE.clone(); + if (other.eq(ExpantaNum.ZERO)) return ExpantaNum.ONE.clone(); + if (other.eq(ExpantaNum.ONE)) return t.clone(); + //By induction: See initialization of r in the fallthrough branch + if (other.gt(ExpantaNum.ZERO)&&other.lt(ExpantaNum.ONE)) return t.pow(other); + if (other.gt(ExpantaNum.ONE)&&arrows.gt(ExpantaNum.MAX_SAFE_INTEGER)){ + var r=arrows.clone(); + r.layer++; + return r; + } + var arrowsNum=arrows.toNumber(); + if (other.eq(2)) return t.arrow(arrowsNum-1)(t,ExpantaNum.ONE,depth+1); + if (t.max(other).gt("10{"+(arrowsNum+1)+"}"+MAX_SAFE_INTEGER)) return t.max(other); + if (t.gt("10{"+arrowsNum+"}"+MAX_SAFE_INTEGER)||other.gt(ExpantaNum.MAX_SAFE_INTEGER)){ + var r; + if (t.gt("10{"+arrowsNum+"}"+MAX_SAFE_INTEGER)){ + r=t.clone(); + r.operator(arrowsNum,r.operator(arrowsNum)-1); + r.normalize(); + }else if (t.gt("10{"+(arrowsNum-1)+"}"+MAX_SAFE_INTEGER)){ + r=new ExpantaNum(t.operator(arrowsNum-1)); + }else{ + r=ExpantaNum.ZERO; + } + var j=r.add(other); + j.operator(arrowsNum,(j.operator(arrowsNum)||0)+1); + j.normalize(); + return j; + } + if (depth>=ExpantaNum.maxOps+10){ + return new ExpantaNum([[0,10],[arrowsNum,1]]); + } + var y=other.toNumber(); + var f=Math.floor(y); + var arrows_m1=arrows.sub(ExpantaNum.ONE); + var r=t.arrow(arrows_m1)(y-f,ExpantaNum.ONE,depth+1); + var l=ExpantaNum.NaN; + for (var i=0,m=new ExpantaNum("10{"+(arrowsNum-1)+"}"+MAX_SAFE_INTEGER);f!==0&&r.lt(m)&&i<100;++i){ + if (f>0){ + r=t.arrow(arrows_m1)(r,ExpantaNum.ONE,depth+1); + if (l.eq(r)){ + f=0; + break; + } + l=r; + --f; + }else{ + r=r.arrow_height_inverse(arrows_m1)(t); + if (l.eq(r)){ + f=0; + break; + } + l=r; + ++f; + } + } + if (i==100) f=0; + r.operator(arrowsNum-1,(r.operator(arrowsNum-1)+f)||f); + r.normalize(); + return r; + }; + }; + P.chain=function (other,arrows){ + return this.arrow(arrows)(other); + }; + Q.arrow=function (x,z,y,payload){ + return new ExpantaNum(x).arrow(z)(y,payload); + }; + Q.chain=function (x,y,z){ + return new ExpantaNum(x).arrow(z)(y); + }; + Q.hyper=function (z){ + z=new ExpantaNum(z); + if (z.eq(ExpantaNum.ZERO)) return function(x,y){return new ExpantaNum(y).eq(ExpantaNum.ZERO)?new ExpantaNum(x):new ExpantaNum(x).add(ExpantaNum.ONE);}; + if (z.eq(ExpantaNum.ONE)) return function(x,y){return ExpantaNum.add(x,y);}; + return function(x,y,payload){return new ExpantaNum(x).arrow(z.sub(2))(y,payload);}; + }; + //arrow_height_inverse{z}_x(x{z}y)=y + //See also: https://github.com/Patashu/break_eternity.js/blob/848736e3dc37d8e7b5cc238f46e3ddb277d0dce2/src/index.ts#L4647 + P.arrow_height_inverse=function (arrows){ + var x=this.clone(); + arrows=new ExpantaNum(arrows); + if (!arrows.isint()||arrows.lt(ExpantaNum.ONE)) return function(other){return ExpantaNum.NaN.clone();}; + if (arrows.eq(ExpantaNum.ONE)) return function(base){return x.logBase(base);}; + if (arrows.eq(2)) return function(base){return x.slog(base);}; + return function (base,depth){ + if (base===undefined) base=10; + if (depth===undefined) depth=0; + base=new ExpantaNum(base); + if (x.isNaN()||base.isNaN()||x.isInfinite()&&base.isInfinite()) return ExpantaNum.NaN.clone(); + if (base.lte(ExpantaNum.ONE)) return ExpantaNum.NaN.clone(); + if (x.isInfinite()) return x; + if (base.isInfinite()) return ExpantaNum.ZERO.clone(); + if (x.eq(ExpantaNum.ZERO)) return ExpantaNum.ONE.neg(); + if (x.eq(ExpantaNum.ONE)) return ExpantaNum.ZERO.clone(); + if (x.eq(base)) return ExpantaNum.ONE.clone(); + //Inverse of shortcut for 00) return x; + return ExpantaNum.ONE.clone(); //base{arrows}(1+epsilon) explodes + } + var arrowsNum=arrows.toNumber(); + if (arrowsNum==2&&x.lt(ExpantaNum.ONE.neg())){ + if (x.lt(-2)) return ExpantaNum.NaN.clone(); + var infrcmp=x.cmp(base.arrow(arrows.sub(ExpantaNum.ONE))(x)); + if (infrcmp==0) return ExpantaNum.NEGATIVE_INFINITY.clone(); + if (infrcmp>0) return ExpantaNum.NaN.clone(); + } + if (x.max(base).gt("10{"+(arrowsNum+1)+"}"+MAX_SAFE_INTEGER)){ + if (x.gt(base)) return x; + return ExpantaNum.ZERO.clone(); + } + if (x.max(base).gt("10{"+arrowsNum+"}"+MAX_SAFE_INTEGER)){ + if (x.gt(base)){ + x.operator(arrowsNum,x.operator(arrowsNum)-1); + x.normalize(); + return x.sub(x.operator(arrowsNum-1)); + } + return ExpantaNum.ZERO.clone(); + } + var r=0; + var t=(x.operator(arrowsNum-1)||0)-(base.operator(arrowsNum-1)||0); + if (depth>=ExpantaNum.maxOps+10) return new ExpantaNum(t); + if (t>3){ + var l=t-3; + r+=l; + x.operator(arrowsNum-1,x.operator(arrowsNum-1)-l); + } + var arrows_m1=arrows.sub(ExpantaNum.ONE); + for (var i=0;i<100;++i){ + if (x.lt(ExpantaNum.ZERO)){ + x=base.arrow(arrows_m1)(x); + --r; + }else if (x.lte(ExpantaNum.ONE)){ + return new ExpantaNum(r+x.toNumber()-1); + }else{ + ++r; + x=x.arrow_height_inverse(arrows_m1)(base,depth+1); + } + } + return ExpantaNum.NaN.clone(); //Failed to converge + }; + }; + Q.arrow_height_inverse=function (x,z,y){ + return new ExpantaNum(x).arrow_height_inverse(z)(y); + } + P.expansion=function (other){ + var t=this.clone(); + other=new ExpantaNum(other); + var r; + if (ExpantaNum.debug>=ExpantaNum.NORMAL) console.log("{"+t+","+other+",1,2}"); + if (other.lte(ExpantaNum.ZERO)||!other.isint()) return ExpantaNum.NaN.clone(); + if (other.eq(ExpantaNum.ONE)) return t.clone(); + if (!t.isint()) return ExpantaNum.NaN.clone(); + if (t.eq(2)) return new ExpantaNum(4); + if (other.gt(ExpantaNum.MAX_SAFE_INTEGER)) return ExpantaNum.POSITIVE_INFINITY.clone(); + var f=other.toNumber()-1; + r=t; + for (var i=0;f!==0&&r.lt(ExpantaNum.MAX_SAFE_INTEGER)&&i<100;++i){ + if (f>0){ + r=t.arrow(r)(t); + --f; + } + } + if (i==100) f=0; + r.layer+=f; + r.normalize(); + return r; + }; + Q.expansion=function (x,y){ + return new ExpantaNum(x).expansion(y); + }; + // All of these are from Patashu's break_eternity.js + Q.affordGeometricSeries = function (resourcesAvailable, priceStart, priceRatio, currentOwned) { + /* + If you have resourcesAvailable, the price of something starts at + priceStart, and on each purchase it gets multiplied by priceRatio, + and you have already bought currentOwned, how many of the object + can you buy. + */ + resourcesAvailable=new ExpantaNum(resourcesAvailable); + priceStart=new ExpantaNum(priceStart); + priceRatio=new ExpantaNum(priceRatio); + var actualStart = priceStart.mul(priceRatio.pow(currentOwned)); + return ExpantaNum.floor(resourcesAvailable.div(actualStart).mul(priceRatio.sub(ExpantaNum.ONE)).add(ExpantaNum.ONE).log10().div(priceRatio.log10())); + }; + Q.affordArithmeticSeries = function (resourcesAvailable, priceStart, priceAdd, currentOwned) { + /* + If you have resourcesAvailable, the price of something starts at + priceStart, and on each purchase it gets increased by priceAdd, + and you have already bought currentOwned, how many of the object + can you buy. + */ + resourcesAvailable=new ExpantaNum(resourcesAvailable); + priceStart=new ExpantaNum(priceStart); + priceAdd=new ExpantaNum(priceAdd); + currentOwned=new ExpantaNum(currentOwned); + var actualStart = priceStart.add(currentOwned.mul(priceAdd)); + var b = actualStart.sub(priceAdd.div(2)); + var b2 = b.pow(2); + return b.neg().add(b2.add(priceAdd.mul(resourcesAvailable).mul(2)).sqrt()).div(priceAdd).floor(); + }; + Q.sumGeometricSeries = function (numItems, priceStart, priceRatio, currentOwned) { + /* + If you want to buy numItems of something, the price of something starts at + priceStart, and on each purchase it gets multiplied by priceRatio, + and you have already bought currentOwned, what will be the price of numItems + of something. + */ + priceStart=new ExpantaNum(priceStart); + priceRatio=new ExpantaNum(priceRatio); + return priceStart.mul(priceRatio.pow(currentOwned)).mul(ExpantaNum.sub(ExpantaNum.ONE, priceRatio.pow(numItems))).div(ExpantaNum.sub(ExpantaNum.ONE, priceRatio)); + }; + Q.sumArithmeticSeries = function (numItems, priceStart, priceAdd, currentOwned) { + /* + If you want to buy numItems of something, the price of something starts at + priceStart, and on each purchase it gets increased by priceAdd, + and you have already bought currentOwned, what will be the price of numItems + of something. + */ + numItems=new ExpantaNum(numItems); + priceStart=new ExpantaNum(priceStart); + currentOwned=new ExpantaNum(currentOwned); + var actualStart = priceStart.add(currentOwned.mul(priceAdd)); + + return numItems.div(2).mul(actualStart.mul(2).plus(numItems.sub(ExpantaNum.ONE).mul(priceAdd))); + }; + // Binomial Coefficients n choose k + Q.choose = function (n, k) { + /* + If you have n items and you take k out, + how many ways could you do this? + */ + return new ExpantaNum(n).factorial().div(new ExpantaNum(k).factorial().mul(new ExpantaNum(n).sub(new ExpantaNum(k)).factorial())); + }; + P.choose = function (other) { + return ExpantaNum.choose(this, other); + }; + //end break_eternity.js excerpt + P.normalize=function (){ + var b; + var x=this; + if (ExpantaNum.debug>=ExpantaNum.ALL) console.log(x.toString()); + if (!x.array||!x.array.length) x.array=[[0,0]]; + if (x.sign!=1&&x.sign!=-1){ + if (typeof x.sign!="number") x.sign=Number(x.sign); + x.sign=x.sign<0?-1:1; + } + if (x.layer>MAX_SAFE_INTEGER){ + x.array=[[0,Infinity]]; + x.layer=0; + return x; + } + if (Number.isInteger(x.layer)) x.layer=Math.floor(x.layer); + for (var i=0;i=ExpantaNum.ALL) console.log(x.toString()); + b=false; + x.array.sort(function (a,b){return a[0]>b[0]?1:a[0]ExpantaNum.maxOps) x.array.splice(0,x.array.length-ExpantaNum.maxOps); + if (!x.array.length) x.array=[[0,0]]; + if (x.array[x.array.length-1][0]>MAX_SAFE_INTEGER){ + x.layer++; + x.array=[[0,x.array[x.array.length-1][0]]]; + b=true; + }else if (x.layer&&x.array.length==1&&x.array[0][0]===0){ + x.layer--; + if (x.array[0][1]===0) x.array=[[0,10]]; + else x.array=[[0,10],[Math.round(x.array[0][1]),1]]; + b=true; + } + if (x.array.lengthMAX_SAFE_INTEGER){ + if (x.array.length>=2&&x.array[1][0]==1){ + x.array[1][1]++; + }else{ + x.array.splice(1,0,[1,1]); + } + x.array[0][1]=Math.log10(x.array[0][1]); + b=true; + } + while (x.array.length>=2&&x.array[0][0]===0&&x.array[0][1]1){ + x.array[1][1]--; + }else{ + x.array.splice(1,1); + } + b=true; + } + while (x.array.length>=2&&x.array[0][0]===0&&x.array[0][1]==1&&x.array[1][1]){ + if (x.array[1][1]>1){ + x.array[1][1]--; + }else{ + x.array.splice(1,1); + } + x.array[0][1]=10; + } + if (x.array.length>=2&&x.array[0][0]===0&&x.array[1][0]!=1){ + var p=1; + if (Math.floor(x.array[0][1])) x.array.splice(1,0,[x.array[1][0]-1,Math.floor(x.array[0][1])]),p++; + x.array[0][1]=Math.pow(10,x.array[0][1]-Math.floor(x.array[0][1])); + if (x.array[p][1]>1){ + x.array[p][1]--; + }else{ + x.array.splice(p,1); + } + b=true; + } + for (i=0;iMAX_SAFE_INTEGER){ + if (i!=x.array.length-1&&x.array[i+1][0]==x.array[i][0]+1){ + x.array[i+1][1]++; + }else{ + x.array.splice(i+1,0,[x.array[i][0]+1,1]); + } + x.array.splice(0,i+1,[0,x.array[i][1]+1]); + b=true; + } + } + }while(b); + if (!x.array.length) x.array=[[0,0]]; + return x; + }; + var standardizeMessageSent=false; + P.standardize=function (){ + if (!standardizeMessageSent) console.warn(expantaNumError+"'standardize' method is being deprecated in favor of 'normalize' and will be removed in the future!"),standardizeMessageSent=true; + return this.normalize(); + } + P.toNumber=function (){ + //console.log(this.array); + if (this.sign==-1) return -1*this.abs(); + if (this.layer>0||this.array.length>=2&&(this.array[1][0]>=2||this.array[1][1]>=2||this.array[1][1]==1&&this.array[0][1]>Math.log10(Number.MAX_VALUE))) return Infinity; + if (this.array.length>=2&&this.array[1][1]==1) return Math.pow(10,this.array[0][1]); + return this.array[0][1]; + }; + P.toString=function (){ + if (this.sign==-1) return "-"+this.abs(); + if (isNaN(this.array[0][1])) return "NaN"; + if (!isFinite(this.array[0][1])) return "Infinity"; + var s=""; + if (!this.layer) s+=""; + else if (this.layer<3) s+="J".repeat(this.layer); + else s+="J^"+this.layer+" "; + if (this.array.length>=3||this.array.length==2&&this.array[1][0]>=2){ + for (var i=this.array.length-1;i>=2;--i){ + var e=this.array[i]; + var q=e[0]>=5?"{"+e[0]+"}":"^".repeat(e[0]); + if (e[1]>1) s+="(10"+q+")^"+e[1]+" "; + else if (e[1]==1) s+="10"+q; + } + } + var op0=this.operator(0); + var op1=this.operator(1); + if (!op1) s+=String(op0); + else if (op1<3) s+="e".repeat(op1-1)+Math.pow(10,op0-Math.floor(op0))+"e"+Math.floor(op0); + else if (op1<8) s+="e".repeat(op1)+op0; + else s+="(10^)^"+op1+" "+op0; + return s; + }; + //from break_eternity.js + var decimalPlaces=function decimalPlaces(value,places){ + var len=places+1; + var numDigits=Math.ceil(Math.log10(Math.abs(value))); + if (numDigits<100) numDigits=0; //A hack-y solution to https://github.com/Naruyoko/ExpantaNum.js/issues/22 + var rounded=Math.round(value*Math.pow(10,len-numDigits))*Math.pow(10,numDigits-len); + return parseFloat(rounded.toFixed(Math.max(len-numDigits,0))); + }; + P.toStringWithDecimalPlaces=function (places,applyToOpNums){ + if (this.sign==-1) return "-"+this.abs(); + if (isNaN(this.array[0][1])) return "NaN"; + if (!isFinite(this.array[0][1])) return "Infinity"; + var b=0; + var s=""; + var m=Math.pow(10,places); + if (!this.layer) s+=""; + else if (this.layer<3) s+="J".repeat(this.layer); + else s+="J^"+this.layer+" "; + if (this.array.length>=3||this.array.length==2&&this.array[1][0]>=2){ + for (var i=this.array.length-1;!b&&i>=2;--i){ + var e=this.array[i]; + var w=e[0]; + var x=e[1]; + if (applyToOpNums&&x>=m){ + ++w; + b=x; + x=1; + }else if (applyToOpNums&&this.array[i-1][0]==w-1&&this.array[i-1][1]>=m){ + ++x; + b=this.array[i-1][1]; + } + var q=w>=5?"{"+w+"}":"^".repeat(w); + if (x>1) s+="(10"+q+")^"+x+" "; + else if (x==1) s+="10"+q; + } + } + var k=this.operator(0); + var l=this.operator(1); + if (k>m){ + k=Math.log10(k); + ++l; + } + if (b) s+=decimalPlaces(b,places); + else if (!l) s+=String(decimalPlaces(k,places)); + else if (l<3) s+="e".repeat(l-1)+decimalPlaces(Math.pow(10,k-Math.floor(k)),places)+"e"+decimalPlaces(Math.floor(k),places); + else if (l<8) s+="e".repeat(l)+decimalPlaces(k,places); + else if (applyToOpNums) s+="(10^)^"+decimalPlaces(l,places)+" "+decimalPlaces(k,places); + else s+="(10^)^"+l+" "+decimalPlaces(k,places); + return s; + }; + //these are from break_eternity.js as well + P.toExponential=function (places,applyToOpNums){ + if (this.array.length==1) return (this.sign*this.array[0][1]).toExponential(places); + return this.toStringWithDecimalPlaces(places,applyToOpNums); + }; + P.toFixed=function (places,applyToOpNums){ + if (this.array.length==1) return (this.sign*this.array[0][1]).toFixed(places); + return this.toStringWithDecimalPlaces(places,applyToOpNums); + }; + P.toPrecision=function (places,applyToOpNums){ + if (this.array[0][1]===0) return (this.sign*this.array[0][1]).toFixed(places-1,applyToOpNums); + if (this.array.length==1&&this.array[0][1]<1e-6) return this.toExponential(places-1,applyToOpNums); + if (this.array.length==1&&places>Math.log10(this.array[0][1])) return this.toFixed(places-Math.floor(Math.log10(this.array[0][1]))-1,applyToOpNums); + return this.toExponential(places-1,applyToOpNums); + }; + P.valueOf=function (){ + return this.toString(); + }; + //Note: toArray() would be impossible without changing the layout of the array or lose the information about the sign + P.toJSON=function (){ + if (ExpantaNum.serializeMode==ExpantaNum.JSON){ + var a=[]; + for (var i=0;i=BigInt(1)<BigInt(0)){ + if (input>=BigInt(1)<>cutbits; + return Math.log10(Number(firstbits))+Math.LOG10E/Math.LOG2E*Number(cutbits); + } + Q.fromBigInt=function (input){ + if (typeof input!="bigint") throw Error(invalidArgument+"Expected BigInt"); + var x=new ExpantaNum(); + var abs=input=2&&x.array[1][0]==1){ + x.array[1][1]+=c; + }else{ + x.array.splice(1,0,[1,c]); + } + }else if (arrows==2){ + a=x.array.length>=2&&x.array[1][0]==1?x.array[1][1]:0; + b=x.array[0][1]; + if (b>=1e10) ++a; + if (b>=10) ++a; + x.array[0][1]=a; + if (x.array.length>=2&&x.array[1][0]==1) x.array.splice(1,1); + d=x.getOperatorIndex(2); + if (Number.isInteger(d)) x.array[d][1]+=c; + else x.array.splice(Math.ceil(d),0,[2,c]); + }else{ + a=x.operator(arrows-1); + b=x.operator(arrows-2); + if (b>=10) ++a; + d=x.getOperatorIndex(arrows); + x.array.splice(1,Math.ceil(d)-1); + x.array[0][1]=a; + if (Number.isInteger(d)) x.array[1][1]+=c; + else x.array.splice(1,0,[arrows,c]); + } + }else{ + break; + } + } + a=input.split(/[Ee]/); + b=[x.array[0][1],0]; + c=1; + for (i=a.length-1;i>=0;--i){ + //The things that are already there + if (b[0]=LONG_STRING_MIN_LENGTH) b[0]=Math.log10(b[0])+log10LongString(a[i].substring(0,intPartLen)),b[1]=1; + else if (a[i]) b[0]*=Number(a[i]); + }else{ + d=intPartLen>=LONG_STRING_MIN_LENGTH?log10LongString(a[i].substring(0,intPartLen)):a[i]?Math.log10(Number(a[i])):0; + if (b[1]==1){ + b[0]+=d; + }else if (b[1]==2&&b[0]MAX_SAFE_INTEGER){ + b[0]=Math.log10(b[0]); + b[1]++; + } + } + x.array[0][1]=b[0]; + if (b[1]){ + if (x.array.length>=2&&x.array[1][0]==1) x.array[1][1]+=b[1]; + else x.array.splice(1,0,[1,b[1]]); + } + } + if (negateIt) x.sign*=-1; + x.normalize(); + return x; + }; + Q.fromArray=function (input1,input2,input3){ + var array,layer,sign; + if (input1 instanceof Array&&(input2===undefined||typeof input2=="number")&&(input3===undefined||typeof input3=="number")){ + array=input1; + sign=input2; + layer=input3||0; + }else if (typeof input1=="number"&&input2 instanceof Array&&(input3===undefined||typeof input3=="number")){ + array=input2; + sign=input1; + layer=input3||0; + }else if (typeof input1=="number"&&typeof input2=="number"&&input3 instanceof Array){ + array=input3; + sign=input1; + layer=input2; + }else{ + throw Error(invalidArgument+"Expected an Array [and 1 or 2 Number]"); + } + var x=new ExpantaNum(); + var i; + if (!array.length) x.array=[[0,0]]; + else if (typeof array[0]=="number"){ + x.array=[]; + for (i=0;i=2){ + --t; + } + x.array[i]=[i,t]; + } + } + if (negateIt) x.sign*=-1; + x.normalize(); + return x; + }; + P.getOperatorIndex=function (i){ + if (typeof i!="number") i=Number(i); + if (!isFinite(i)) throw Error(invalidArgument+"Index out of range."); + var a=this.array; + var min=0,max=a.length-1; + if (a[max][0]i) return -0.5; + while (min!=max){ + if (a[min][0]==i) return min; + if (a[max][0]==i) return max; + var mid=Math.floor((min+max)/2); + if (min==mid||a[mid][0]==i){ + min=mid; + break; + } + if (a[mid][0]i) max=mid; + } + return a[min][0]==i?min:min+0.5; + }; + P.getOperator=function (i){ + if (typeof i!="number") i=Number(i); + if (!isFinite(i)) throw Error(invalidArgument+"Index out of range."); + var ai=this.getOperatorIndex(i); + if (Number.isInteger(ai)) return this.array[ai][1]; + else return i===0?10:0; + }; + P.setOperator=function (i,value){ + if (typeof i!="number") i=Number(i); + if (!isFinite(i)) throw Error(invalidArgument+"Index out of range."); + var ai=this.getOperatorIndex(i); + if (Number.isInteger(ai)) this.array[ai][1]=value; + else{ + ai=Math.ceil(ai); + this.array.splice(ai,0,[i,value]); + } + this.normalize(); + }; + P.operator=function (i,value){ + if (value===undefined) return this.getOperator(i); + else this.setOperator(i,value); + }; + P.clone=function (){ + var temp=new ExpantaNum(); + var array=[]; + for (var i=0;i= ps[i + 1] && v <= ps[i + 2]) this[p] = v; + else throw Error(invalidArgument + p + ': ' + v); + } + } + + return this; + } + + + // Create and configure initial ExpantaNum constructor. + ExpantaNum=clone(ExpantaNum); + + ExpantaNum=defineConstants(ExpantaNum); + + ExpantaNum['default']=ExpantaNum.ExpantaNum=ExpantaNum; + + // Export. + + // AMD. + if (typeof define == 'function' && define.amd) { + define(function () { + return ExpantaNum; + }); + // Node and other environments that support module.exports. + } else if (typeof module != 'undefined' && module.exports) { + module.exports = ExpantaNum; + // Browser. + } else { + if (!globalScope) { + globalScope = typeof self != 'undefined' && self && self.self == self + ? self : Function('return this')(); + } + globalScope.ExpantaNum = ExpantaNum; + } +})(this); \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jwNum/index.js b/local-scratch-vm/src/extensions/jwNum/index.js new file mode 100644 index 0000000000000000000000000000000000000000..95fcbaf93f6d15f99a2520690c381dce8cd790e3 --- /dev/null +++ b/local-scratch-vm/src/extensions/jwNum/index.js @@ -0,0 +1,517 @@ +const BlockType = require('../../extension-support/block-type') +const BlockShape = require('../../extension-support/block-shape') +const ArgumentType = require('../../extension-support/argument-type') +const Cast = require('../../util/cast') + +const ExpantaNum = require('./expantanum.js') + +function span(text) { + let el = document.createElement('span') + el.innerHTML = text + el.style.display = 'hidden' + el.style.whiteSpace = 'nowrap' + el.style.width = '100%' + el.style.textAlign = 'center' + return el +} + +class NumType { + customId = "jwNum" + + number = ExpantaNum(0) + + constructor(x) { + this.number = ExpantaNum(x) + } + + static toNum(x) { + if (x instanceof NumType) return new NumType(x.number) + try { + let parsed = JSON.parse(x) + if (typeof parsed == 'object') return new NumType(parsed) + } catch {} + return new NumType(x) + } + + jwArrayHandler() { + return this.number.toStringWithDecimalPlaces(3) + } + + toString() { + return this.number.toStringWithDecimalPlaces(7) + } + toMonitorContent = () => span(this.toString()) + toReporterContent = () => span(this.toString()) +} + +const jwNum = { + Type: NumType, + Block: { + blockType: BlockType.REPORTER, + forceOutputType: "jwNum", + disableMonitor: true + }, + Argument: { + type: ArgumentType.STRING, + defaultValue: "10", + exemptFromNormalization: true + }, + ExpantaNum +} + +class Extension { + constructor() { + vm.jwNum = jwNum + vm.runtime.registerSerializer( + "jwNum", + v => v.number.toJSON(), + v => { + let x = new ExpantaNum(0) + try { + x = ExpantaNum.fromJSON(v) + } catch {} + return new jwNum.Type(x) + } + ) + } + + getInfo() { + return { + id: "jwNum", + name: "Infinity", + color1: "#3bd471", + menuIconURI: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+CiAgPGVsbGlwc2Ugc3R5bGU9InN0cm9rZS13aWR0aDogMnB4OyBwYWludC1vcmRlcjogc3Ryb2tlOyBmaWxsOiByZ2IoNTksIDIxMiwgMTEzKTsgc3Ryb2tlOiByZ2IoNTMsIDE5MCwgMTAyKTsiIGN4PSIxMCIgY3k9IjEwIiByeD0iOSIgcnk9IjkiPjwvZWxsaXBzZT4KICA8cGF0aCBkPSJNIDEzLjU3OCAxMy42MTMgUSAxMi42NDggMTMuNjEzIDExLjk2NCAxMy4zMzYgUSAxMS4yNzkgMTMuMDU4IDEwLjc5IDEyLjYwMiBRIDEwLjMwMSAxMi4xNDYgOS45MjYgMTEuNjE2IFEgOS41NTEgMTEuMDg2IDkuMjI1IDEwLjUzMiBRIDguODk5IDkuOTc4IDguNTMzIDkuNTI5IFEgOC4xNjYgOS4wODEgNy43MSA4LjgwNCBRIDcuMjUzIDguNTI3IDYuNjE3IDguNTI3IFEgNi4xNDUgOC41MjcgNS43NyA4Ljc0NyBRIDUuMzk1IDguOTY3IDUuMTgzIDkuMzI2IFEgNC45NzEgOS42ODQgNC45NzEgMTAuMTU3IFEgNC45NzEgMTAuNjEzIDUuMTgzIDEwLjk4IFEgNS4zOTUgMTEuMzQ3IDUuNzcgMTEuNTY3IFEgNi4xNDUgMTEuNzg3IDYuNjE3IDExLjc4NyBRIDcuMjUzIDExLjc4NyA3LjcxIDExLjUxIFEgOC4xNjYgMTEuMjMzIDguNTMzIDEwLjc4NSBRIDguODk5IDEwLjMzNiA5LjIyNSA5Ljc5OCBRIDkuNTUxIDkuMjYxIDkuOTI2IDguNzE0IFEgMTAuMzAxIDguMTY4IDEwLjc5IDcuNzEyIFEgMTEuMjc5IDcuMjU2IDExLjk2NCA2Ljk3OSBRIDEyLjY0OCA2LjcwMSAxMy41NzggNi43MDEgUSAxNC41NzIgNi43MDEgMTUuMzU0IDcuMTU4IFEgMTYuMTM3IDcuNjE0IDE2LjU5MyA4LjM4IFEgMTcuMDQ5IDkuMTQ2IDE3LjA0OSAxMC4xNDEgUSAxNy4wNDkgMTEuMTM1IDE2LjU5MyAxMS45MTcgUSAxNi4xMzcgMTIuNyAxNS4zNTQgMTMuMTU2IFEgMTQuNTcyIDEzLjYxMyAxMy41NzggMTMuNjEzIFogTSA2LjQ1NCAxMy42MTMgUSA1LjQ2IDEzLjYxMyA0LjY2MSAxMy4xNTYgUSAzLjg2MyAxMi43IDMuNDA2IDExLjkxNyBRIDIuOTUgMTEuMTM1IDIuOTUgMTAuMTU3IFEgMi45NSA5LjE2MyAzLjQwNiA4LjM4OCBRIDMuODYzIDcuNjE0IDQuNjYxIDcuMTU4IFEgNS40NiA2LjcwMSA2LjQ1NCA2LjcwMSBRIDcuMzY3IDYuNzAxIDguMDUyIDYuOTc5IFEgOC43MzYgNy4yNTYgOS4yMjUgNy43MTIgUSA5LjcxNCA4LjE2OCAxMC4wODkgOC43MDYgUSAxMC40NjQgOS4yNDQgMTAuNzk4IDkuNzkgUSAxMS4xMzMgMTAuMzM2IDExLjQ4MyAxMC43ODUgUSAxMS44MzMgMTEuMjMzIDEyLjI5OCAxMS41MSBRIDEyLjc2MyAxMS43ODcgMTMuMzgyIDExLjc4NyBRIDEzLjg1NSAxMS43ODcgMTQuMjMgMTEuNTc1IFEgMTQuNjA0IDExLjM2MyAxNC44MDggMTAuOTg4IFEgMTUuMDEyIDEwLjYxMyAxNS4wMTIgMTAuMTQxIFEgMTUuMDEyIDkuNjg0IDE0LjgwOCA5LjMxOCBRIDE0LjYwNCA4Ljk1MSAxNC4yMyA4LjczOSBRIDEzLjg1NSA4LjUyNyAxMy4zODIgOC41MjcgUSAxMi43NjMgOC41MjcgMTIuMjk4IDguODA0IFEgMTEuODMzIDkuMDgxIDExLjQ4MyA5LjUyOSBRIDExLjEzMyA5Ljk3OCAxMC43OTggMTAuNTI0IFEgMTAuNDY0IDExLjA3IDEwLjA4OSAxMS42MDggUSA5LjcxNCAxMi4xNDYgOS4yMjUgMTIuNjAyIFEgOC43MzYgMTMuMDU4IDguMDUyIDEzLjMzNiBRIDcuMzY3IDEzLjYxMyA2LjQ1NCAxMy42MTMgWiIgc3R5bGU9ImZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgdGV4dC13cmFwLW1vZGU6IG5vd3JhcDsiPjwvcGF0aD4KPC9zdmc+", + blocks: [ + { + opcode: 'add', + text: '[A] + [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'sub', + text: '[A] - [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'mul', + text: '[A] * [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'div', + text: '[A] / [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'pow', + text: '[A] ^ [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'fact', + text: '[A]!', + arguments: { + A: jwNum.Argument + }, + ...jwNum.Block + }, + "---", + { + opcode: 'eq', + text: '[A] = [B]', + blockType: BlockType.BOOLEAN, + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + } + }, + { + opcode: 'gt', + text: '[A] > [B]', + blockType: BlockType.BOOLEAN, + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + } + }, + { + opcode: 'gte', + text: '[A] >= [B]', + blockType: BlockType.BOOLEAN, + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + } + }, + { + opcode: 'lt', + text: '[A] < [B]', + blockType: BlockType.BOOLEAN, + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + } + }, + { + opcode: 'lte', + text: '[A] <= [B]', + blockType: BlockType.BOOLEAN, + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + } + }, + "---", + { + opcode: 'root', + text: 'root [A] [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'ssqrt', + text: 'square super-root [A]', + arguments: { + A: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'log', + text: 'log [A] [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'slog', + text: 'super log [A] [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + "---", + { + opcode: 'mod', + text: '[A] % [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'round', + text: '[A] [B]', + arguments: { + A: { + type: ArgumentType.STRING, + menu: 'round', + defaultValue: 'round' + }, + B: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'isInteger', + text: 'is [A] an integer?', + blockType: BlockType.BOOLEAN, + arguments: { + A: jwNum.Argument + } + }, + "---", + { + opcode: 'hyper', + text: '[A] hyper [B] [C]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument, + C: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'arrow', + text: '[A] arrow [B] [C]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument, + C: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'reverseArrow', + text: '[A] reverse arrow [B] [C]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument, + C: jwNum.Argument + }, + ...jwNum.Block + }, + { + opcode: 'expansion', + text: '[A] expansion [B]', + arguments: { + A: jwNum.Argument, + B: jwNum.Argument + }, + ...jwNum.Block + }, + "---", + { + opcode: 'toString', + text: '[A] to string', + blockType: BlockType.REPORTER, + arguments: { + A: jwNum.Argument + } + }, + { + opcode: 'toStringD', + text: '[A] to string with [B] decimal places', + blockType: BlockType.REPORTER, + arguments: { + A: jwNum.Argument, + B: { + type: ArgumentType.NUMBER, + defaultValue: 20, + } + } + }, + { + opcode: 'toHyperE', + text: '[A] to hyper E', + blockType: BlockType.REPORTER, + arguments: { + A: jwNum.Argument + } + } + ], + menus: { + round: { + acceptReporters: true, + items: [ + 'ceil', + 'round', + 'floor' + ] + }, + } + } + } + + add({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(A.number.add(B.number)) + } + + sub({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(A.number.sub(B.number)) + } + + mul({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(A.number.mul(B.number)) + } + + div({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(A.number.div(B.number)) + } + + pow({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(A.number.pow(B.number)) + } + + fact({A}) { + A = jwNum.Type.toNum(A) + + return new jwNum.Type(A.number.fact()) + } + + eq({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return A.number.eq(B.number) + } + + gt({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return A.number.gt(B.number) + } + + gte({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return A.number.gte(B.number) + } + + lt({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return A.number.lt(B.number) + } + + lte({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return A.number.lte(B.number) + } + + root({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(B.number.root(A.number)) + } + + ssqrt({A}) { + A = jwNum.Type.toNum(A) + + return new jwNum.Type(A.number.ssqrt()) + } + + log({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(B.number.logBase(A.number)) + } + + slog({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(B.number.slog(A.number)) + } + + mod({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(A.number.mod(B.number)) + } + + round({A, B}) { + A = Cast.toString(A).toLowerCase() + B = jwNum.Type.toNum(B) + + switch (A) { + case "ceiling": + case "ceil": + return new jwNum.Type(B.number.ceil()) + case "round": + return new jwNum.Type(B.number.round()) + case "floor": + return new jwNum.Type(B.number.floor()) + default: return new jwNum.Type(B) + } + } + + isInteger({A}) { + A = jwNum.Type.toNum(A) + + return A.number.isint() + } + + hyper({A, B, C}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + C = jwNum.Type.toNum(C) + + return new jwNum.Type(ExpantaNum.hyper(B.number)(A.number, C.number)) + } + + arrow({A, B, C}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + C = jwNum.Type.toNum(C) + + return new jwNum.Type(A.number.arrow(B.number)(C.number)) + } + + reverseArrow({A, B, C}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + C = jwNum.Type.toNum(C) + + return new jwNum.Type(A.number.arrow_height_inverse(B.number)(C.number)) + } + + expansion({A, B}) { + A = jwNum.Type.toNum(A) + B = jwNum.Type.toNum(B) + + return new jwNum.Type(A.number.expansion(B.number)) + } + + toString({A}) { + A = jwNum.Type.toNum(A) + + return A.number.toString() + } + + toStringD({A, B}) { + A = jwNum.Type.toNum(A) + B = Cast.toNumber(B) + + return A.number.toStringWithDecimalPlaces(B) + } + + toHyperE({A}) { + A = jwNum.Type.toNum(A) + + return A.number.toHyperE() + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jwPsychic/index.js b/local-scratch-vm/src/extensions/jwPsychic/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a0dae2c01ad9457f3ff2e13f956f4b1915c2ab1e --- /dev/null +++ b/local-scratch-vm/src/extensions/jwPsychic/index.js @@ -0,0 +1,625 @@ +const BlockType = require('../../extension-support/block-type') +const BlockShape = require('../../extension-support/block-shape') +const ArgumentType = require('../../extension-support/argument-type') +const TargetType = require('../../extension-support/target-type') +const Cast = require('../../util/cast') + +const Matter = require('matter-js') + +let Vector = { + Type: class {}, + Block: {}, + Argument: {} +} + +let jwArray = { + Type: class {}, + Block: {}, + Argument: {} +} + +let Target = { + Type: class {}, + Block: {}, + Argument: {} +} + +class Extension { + constructor() { + if (!vm.jwVector) vm.extensionManager.loadExtensionIdSync('jwVector') + Vector = vm.jwVector + + if (!vm.jwArray) vm.extensionManager.loadExtensionIdSync('jwArray') + jwArray = vm.jwArray + + if (!vm.jwTargets) vm.extensionManager.loadExtensionIdSync('jwTargets') + Target = vm.jwTargets + + this.engine = Matter.Engine.create() + /** @type {Object} */ + this.bodies = {} + /** @type {Matter.Composite?} */ + this.bounds = null + + vm.runtime.on("PROJECT_START", this.reset.bind(this)); + + vm.PsychicDebug = this; + } + + getInfo() { + return { + id: "jwPsychic", + name: "Psychic", + color1: "#b16bed", + menuIconURI: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM6Yng9Imh0dHBzOi8vYm94eS1zdmcuY29tIiB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4Ij48ZGVmcz48Yng6ZXhwb3J0PjxieDpmaWxlIGZvcm1hdD0ic3ZnIi8+PC9ieDpleHBvcnQ+PC9kZWZzPjxlbGxpcHNlIHN0eWxlPSJzdHJva2Utd2lkdGg6IDJweDsgcGFpbnQtb3JkZXI6IHN0cm9rZTsgZmlsbDogcmdiKDE3NywgMTA3LCAyMzcpOyBzdHJva2U6IHJnYigxNTksIDk2LCAyMTMpOyIgY3g9IjEwIiBjeT0iMTAiIHJ4PSI5IiByeT0iOSIvPjxyZWN0IHg9IjQuNjM0IiB5PSIxMC4yMjgiIHdpZHRoPSI0Ljc3IiBoZWlnaHQ9IjQuNzciIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7Ii8+PHJlY3QgeD0iMTAuNTk2IiB5PSIxMC4yMjgiIHdpZHRoPSI0Ljc3IiBoZWlnaHQ9IjQuNzciIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7Ii8+PHJlY3QgeD0iNy42MTUiIHdpZHRoPSI0Ljc3IiBoZWlnaHQ9IjQuNzciIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IiB5PSI0LjI2NyIvPjwvc3ZnPg==", + blocks: [ + { + opcode: 'tick', + text: 'tick', + blockType: BlockType.COMMAND + }, + "---", + { + opcode: 'boundaries', + text: 'set boundaries [OPTION]', + blockType: BlockType.COMMAND, + arguments: { + OPTION: { + type: ArgumentType.STRING, + menu: 'boundariesOption' + } + } + }, + { + opcode: 'setGravity', + text: 'set gravity to [VECTOR]', + blockType: BlockType.COMMAND, + arguments: { + VECTOR: Vector.Argument + } + }, + { + opcode: 'getGravity', + text: 'gravity', + ...Vector.Block + }, + "---", + { + opcode: 'enablePhysics', + text: 'enable physics as [OPTION]', + blockType: BlockType.COMMAND, + arguments: { + OPTION: { + type: ArgumentType.STRING, + menu: 'enablePhysicsOption' + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'disablePhysics', + text: 'disable physics', + blockType: BlockType.COMMAND, + filter: [TargetType.SPRITE] + }, + "---", + { + opcode: 'setPos', + text: 'set position to [VECTOR]', + blockType: BlockType.COMMAND, + arguments: { + VECTOR: Vector.Argument + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getPos', + text: 'position', + filter: [TargetType.SPRITE], + ...Vector.Block + }, + { + opcode: 'setVel', + text: 'set velocity to [VECTOR]', + blockType: BlockType.COMMAND, + arguments: { + VECTOR: Vector.Argument + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getVel', + text: 'velocity', + filter: [TargetType.SPRITE], + ...Vector.Block + }, + { + opcode: 'setRot', + text: 'set rotation to [ANGLE]', + blockType: BlockType.COMMAND, + arguments: { + ANGLE: { + type: ArgumentType.ANGLE, + defaultValue: 90 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getRot', + text: 'rotation', + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + { + opcode: 'setAngVel', + text: 'set angular velocity to [ANGLE]', + blockType: BlockType.COMMAND, + arguments: { + ANGLE: { + type: ArgumentType.ANGLE, + defaultValue: 0 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getAngVel', + text: 'angular velocity', + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + "---", + { + opcode: 'getMass', + text: 'mass', + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + { + opcode: 'setDensity', + text: 'set density to [NUMBER]', + blockType: BlockType.COMMAND, + arguments: { + NUMBER: { + type: ArgumentType.NUMBER, + defaultValue: 0.001 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getDensity', + text: 'density', + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + "---", + { + opcode: 'setStatic', + text: 'set fixed to [BOOLEAN]', + blockType: BlockType.COMMAND, + arguments: { + BOOLEAN: { + type: ArgumentType.BOOLEAN + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getStatic', + text: 'fixed', + blockType: BlockType.BOOLEAN, + filter: [TargetType.SPRITE] + }, + { + opcode: 'setRotatable', + text: 'set rotatable to [BOOLEAN]', + blockType: BlockType.COMMAND, + arguments: { + BOOLEAN: { + type: ArgumentType.BOOLEAN + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getRotatable', + text: 'rotatable', + blockType: BlockType.BOOLEAN, + filter: [TargetType.SPRITE] + }, + "---", + { + opcode: 'setFric', + text: 'set friction to [NUMBER]', + blockType: BlockType.COMMAND, + arguments: { + NUMBER: { + type: ArgumentType.NUMBER, + defaultValue: 0.1 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getFric', + text: 'friction', + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + { + opcode: 'setAirFric', + text: 'set air resistance to [NUMBER]', + blockType: BlockType.COMMAND, + arguments: { + NUMBER: { + type: ArgumentType.NUMBER, + defaultValue: 0.01 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getAirFric', + text: 'air resistance', + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + { + opcode: 'setRest', + text: 'set restitution to [NUMBER]', + blockType: BlockType.COMMAND, + arguments: { + NUMBER: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getRest', + text: 'restitution', + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + "---", + { + opcode: 'getCollides', + text: 'targets colliding with [OPTION]', + arguments: { + OPTION: { + type: ArgumentType.STRING, + menu: 'touchingOption' + } + }, + filter: [TargetType.SPRITE], + ...jwArray.Block + } + ], + menus: { + enablePhysicsOption: [ + 'precise', + 'box', + 'circle' + ], + boundariesOption: [ + 'all', + 'floor', + 'none' + ], + touchingOption: [ + 'body', + 'feet', + 'head' + ] + } + }; + } + + vectorToMatter(vector) { + return Matter.Vector.create(vector.x, -vector.y) + } + + matterToVector(matter) { + return new Vector.Type(matter.x, -matter.y) + } + + angleToMatter(angle) { + return (angle - 90) * Math.PI / 180 + } + + matterToAngle(matter) { + return (matter * 180 / Math.PI) + 90 + } + + reset() { + this.engine = Matter.Engine.create() + this.bodies = {} + this.bounds = null + } + + correctBody(id) { + /** @type {Matter.Body} */ + let body = this.bodies[id] + let target = vm.runtime.getTargetById(id) + + if (target == undefined) { + Matter.Composite.remove(this.engine.world, body) + delete this.bodies[id] + return + } + + Matter.Body.setPosition(body, Matter.Vector.create(target.x, -target.y)) + Matter.Body.setAngle(body, this.angleToMatter(target.direction)) + } + + correctTarget(id) { + /** @type {Matter.Body} */ + let body = this.bodies[id] + let target = vm.runtime.getTargetById(id) + + target.setXY(body.position.x, -body.position.y, false, true) + target.setDirection(this.matterToAngle(body.angle)) + } + + tick() { + let fps = vm.runtime.frameLoop.framerate + if (fps == 0) fps = 60 + + for (let id of Object.keys(this.bodies)) { + this.correctBody(id) + } + + Matter.Engine.update(this.engine, 1000 / fps) + + for (let id of Object.keys(this.bodies)) { + this.correctTarget(id) + } + } + + boundaries({OPTION}) { + if (this.bounds) { + Matter.Composite.remove(this.engine.world, this.bounds) + this.bounds = null + } + + let stageWidth = vm.runtime.stageWidth + let stageHeight = vm.runtime.stageHeight + + this.bounds = Matter.Composite.create() + + switch (OPTION) { + case 'all': + Matter.Composite.add(this.bounds, [ + Matter.Bodies.rectangle(-stageWidth, 0, stageWidth, Number.MAX_SAFE_INTEGER / 2, { isStatic: true }), + Matter.Bodies.rectangle(stageWidth, 0, stageWidth, Number.MAX_SAFE_INTEGER / 2, { isStatic: true }), + Matter.Bodies.rectangle(0, -stageHeight, Number.MAX_SAFE_INTEGER / 2, stageHeight, { isStatic: true }), + ]) + case 'floor': + Matter.Composite.add(this.bounds, Matter.Bodies.rectangle(0, stageHeight, Number.MAX_SAFE_INTEGER / 2, stageHeight, { isStatic: true })) + break + } + + Matter.Composite.add(this.engine.world, this.bounds) + } + + setGravity({VECTOR}) { + let v = Vector.Type.toVector(VECTOR) + this.engine.gravity.x = v.x + this.engine.gravity.y = -v.y + } + + getGravity() { + return this.matterToVector(this.engine.gravity) + } + + enablePhysics({OPTION}, util) { + let target = util.target + let costume = target.getCostumes()[target.currentCostume] + let size = { + x: costume.size[0] * (target.size / 100) * (target.stretch[0] / 100) / costume.bitmapResolution, + y: costume.size[1] * (target.size / 100) * (target.stretch[1] / 100) / costume.bitmapResolution + } + + console.debug(size) + + /** @type {Matter.Body?} */ + let body = null + switch (OPTION) { + case 'precise': + throw "i need to finish precise mb" + break + case 'box': + body = Matter.Bodies.rectangle(target.x, -target.y, size.x, size.y) + break + case 'circle': + body = Matter.Bodies.circle(target.x, -target.y, Math.max(size.x, size.y) / 2) + break + default: + throw "Invalid physics option" + } + + body.label = target.id + + this.bodies[target.id] = body + Matter.Composite.add(this.engine.world, body) + + this.correctBody(target.id) + } + + disablePhysics({}, util) { + let body = this.bodies[util.target.id] + if (!body) return + Matter.Composite.remove(this.engine.world, body) + delete this.bodies[id] + return + } + + setPos({VECTOR}, util) { + let v = Vector.Type.toVector(VECTOR) + util.target.setXY(v.x, v.y) + } + + getPos({}, util) { + let body = this.bodies[util.target.id] + if (!body) return new Vector.Type(util.target.x, util.target.y) + return this.matterToVector(body.position) + } + + setRot({ANGLE}, util) { + let a = Cast.toNumber(ANGLE) + util.target.setDirection(a) + } + + getRot({}, util) { + let body = this.bodies[util.target.id] + if (!body) return util.target.direction + return this.matterToAngle(body.angle) + } + + setVel({VECTOR}, util) { + let body = this.bodies[util.target.id] + if (!body) return + let v = Vector.Type.toVector(VECTOR) + Matter.Body.setVelocity(body, this.vectorToMatter(v)) + } + + getVel({}, util) { + let body = this.bodies[util.target.id] + if (!body) return new Vector.Type(0, 0) + return this.matterToVector(body.velocity) + } + + setAngVel({ANGLE}, util) { + let body = this.bodies[util.target.id] + if (!body) return + Matter.Body.setAngularVelocity(body, Cast.toNumber(ANGLE)) + } + + getAngVel({}, util) { + let body = this.bodies[util.target.id] + if (!body) return 0 + return body.angularVelocity + } + + getMass({}, util) { + let body = this.bodies[util.target.id] + if (!body) return 0 + return body.mass + } + + getDensity({}, util) { + let body = this.bodies[util.target.id] + if (!body) return 0.001 + return body.density + } + + setDensity({NUMBER}, util) { + let body = this.bodies[util.target.id] + if (!body) return + Matter.Body.setDensity(Cast.toNumber(NUMBER)) + } + + getStatic({}, util) { + let body = this.bodies[util.target.id] + if (!body) return false + return body.isStatic + } + + setStatic({BOOLEAN}, util) { + let body = this.bodies[util.target.id] + if (!body) return + body.isStatic = BOOLEAN + } + + getRotatable({}, util) { + let body = this.bodies[util.target.id] + if (!body) return true + return body.inertia !== Infinity + } + + setRotatable({BOOLEAN}, util) { + let body = this.bodies[util.target.id] + if (!body) return + if (BOOLEAN) { + Matter.Body.setVertices(body, body.vertices) + } else { + Matter.Body.setInertia(body, Infinity) + } + } + + setFric({NUMBER}, util) { + let body = this.bodies[util.target.id] + if (!body) return + body.friction = Cast.toNumber(NUMBER) + } + + getFric({}, util) { + let body = this.bodies[util.target.id] + if (!body) return 0.1 + return body.friction + } + + setAirFric({NUMBER}, util) { + let body = this.bodies[util.target.id] + if (!body) return + body.frictionAir = Cast.toNumber(NUMBER) + } + + getAirFric({}, util) { + let body = this.bodies[util.target.id] + if (!body) return 0.01 + return body.frictionAir + } + + setRest({NUMBER}, util) { + let body = this.bodies[util.target.id] + if (!body) return + body.restitution = Cast.toNumber(NUMBER) + } + + getRest({}, util) { + let body = this.bodies[util.target.id] + if (!body) return 0.01 + return body.restitution + } + + getCollides({OPTION}, util) { + let body = this.bodies[util.target.id] + if (!body) return new jwArray.Type() + + let collisions = Matter.Query.collides(body, Object.values(this.bodies).filter(v => v.label !== util.target.id)) + + if (OPTION !== 'body') { + collisions = collisions.filter(v => v.supports[0].x > body.bounds.min.x+1 && v.supports[0].x < body.bounds.max.x-1) + console.debug(collisions) + switch (OPTION) { + case 'feet': + collisions = collisions.filter(v => { + for (let support of v.supports) { + if (support == null) continue + if (support.y > body.bounds.max.y-4) return true + } + }) + break + case 'head': + collisions = collisions.filter(v => { + for (let support of v.supports) { + if (support == null) continue + if (support.y < body.bounds.min.y+4) return true + } + }) + break + } + console.debug(collisions) + } + + let bodies = collisions.map(v => body == v.bodyA ? v.bodyB : v.bodyA) + bodies.filter(v => v.label !== util.target.id) + return new jwArray.Type(bodies.map(v => new Target.Type(v.label))) + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jwTargets/index.js b/local-scratch-vm/src/extensions/jwTargets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..91be5ca6ffee7ede9750c5373957d308654499e2 --- /dev/null +++ b/local-scratch-vm/src/extensions/jwTargets/index.js @@ -0,0 +1,488 @@ +const BlockType = require('../../extension-support/block-type') +const BlockShape = require('../../extension-support/block-shape') +const ArgumentType = require('../../extension-support/argument-type') +const TargetType = require('../../extension-support/target-type') +const Cast = require('../../util/cast') + +function span(text) { + let el = document.createElement('span') + el.innerHTML = text + el.style.display = 'hidden' + el.style.whiteSpace = 'nowrap' + el.style.width = '100%' + el.style.textAlign = 'center' + return el +} + +class jwTargetType { + customId = "jwTargets" + + targetId = "" + + constructor(targetId) { + this.targetId = targetId + } + + static toTarget(x) { + if (x instanceof jwTargetType) return x + if (typeof x == "string") return new jwTargetType(x) + return new jwTargetType("") + } + + jwArrayHandler() { + try { + return `Target<${this.target.sprite.name}>` + } catch { + return `Target` + } + } + + toString() { + return this.targetId + } + toMonitorContent = () => span(this.toString()) + + toReporterContent() { + try { + let target = this.target + let name = target.sprite.name + let isClone = !target.isOriginal + let costumeURI = target.getCostumes()[target.currentCostume].asset.encodeDataURI() + + let root = document.createElement('div') + root.style.display = 'flex' + root.style.flexDirection = 'column' + root.style.justifyContent = 'center' + + let img = document.createElement('img') + img.src = costumeURI + img.style.maxWidth = '150px' + img.style.maxHeight = '150px' + root.appendChild(img) + + root.appendChild(span(`${name}${isClone ? ' (clone)' : ''}`)) + + return root + } catch { + return span("Unknown") + } + } + + get target() { + return vm.runtime.getTargetById(this.targetId) + } +} + +const Target = { + Type: jwTargetType, + Block: { + blockType: BlockType.REPORTER, + forceOutputType: "Target", + disableMonitor: true + }, + Argument: { + check: ["Target"] + } +} + +let jwArray = { + Type: class {}, + Block: {}, + Argument: {} +} + +class Extension { + constructor() { + vm.jwTargets = Target + vm.runtime.registerSerializer( + "jwTargets", + v => v.targetId, + v => new Target.Type(v) + ); + + if (!vm.jwArray) vm.extensionManager.loadExtensionIdSync('jwArray') + jwArray = vm.jwArray + } + + getInfo() { + return { + id: "jwTargets", + name: "Targets", + color1: "#4254f5", + menuIconURI: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM6Yng9Imh0dHBzOi8vYm94eS1zdmcuY29tIj4KICA8Y2lyY2xlIHN0eWxlPSJzdHJva2Utd2lkdGg6IDJweDsgcGFpbnQtb3JkZXI6IHN0cm9rZTsgZmlsbDogcmdiKDY2LCA4NCwgMjQ1KTsgc3Ryb2tlOiByZ2IoNDAsIDU1LCAxOTkpOyIgY3g9IjEwIiBjeT0iMTAiIHI9IjkiPjwvY2lyY2xlPgogIDxwYXRoIGQ9Ik0gMTAgMy4yMjIgQyA0Ljc4MyAzLjIyMiAxLjUyMyA4Ljg3IDQuMTMgMTMuMzg5IEMgNS4zNCAxNS40ODUgNy41OCAxNi43NzggMTAgMTYuNzc4IEMgMTUuMjE4IDE2Ljc3OCAxOC40OCAxMS4xMyAxNS44NjkgNi42MTEgQyAxNC42NjEgNC41MTUgMTIuNDIyIDMuMjIyIDEwIDMuMjIyIE0gMTAgNS40ODEgQyAxMy40NzkgNS40ODEgMTUuNjUzIDkuMjQ4IDEzLjkxMyAxMi4yNTkgQyAxMy4xMDYgMTMuNjU4IDExLjYxNiAxNC41MTkgMTAgMTQuNTE5IEMgNi41MjIgMTQuNTE5IDQuMzUgMTAuNzUyIDYuMDg3IDcuNzQxIEMgNi44OTUgNi4zNDIgOC4zODUgNS40ODEgMTAgNS40ODEgTSAxMCA3Ljc0MSBDIDguMjYyIDcuNzQxIDcuMTczIDkuNjIyIDguMDQ0IDExLjEzIEMgOC40NDggMTEuODI4IDkuMTkzIDEyLjI1OSAxMCAxMi4yNTkgQyAxMS43NCAxMi4yNTkgMTIuODI3IDEwLjM3OCAxMS45NTYgOC44NyBDIDExLjU1MyA4LjE3MiAxMC44MDggNy43NDEgMTAgNy43NDEiIGZpbGw9IiNmZmYiIHN0eWxlPSIiPjwvcGF0aD4KPC9zdmc+", + blocks: [ + { + opcode: 'this', + text: 'this target', + hideFromPalette: true, + ...Target.Block + }, + { + opcode: 'stage', + text: 'stage target', + hideFromPalette: true, + ...Target.Block + }, + { + opcode: 'fromName', + text: '[SPRITE] target', + arguments: { + SPRITE: { + menu: "sprite" + } + }, + ...Target.Block + }, + { + opcode: 'cloneOrigin', + text: 'origin of [TARGET]', + arguments: { + TARGET: Target.Argument + }, + ...Target.Block + }, + '---', + { + opcode: 'get', + text: '[TARGET] [MENU]', + blockType: BlockType.REPORTER, + arguments: { + TARGET: Target.Argument, + MENU: { + menu: "targetProperty", + defaultValue: "name" + } + } + }, + { + opcode: 'set', + text: 'set [TARGET] [MENU] to [VALUE]', + blockType: BlockType.COMMAND, + arguments: { + TARGET: Target.Argument, + MENU: { + menu: "targetPropertySet", + defaultValue: "x" + }, + VALUE: { + type: ArgumentType.STRING, + exemptFromNormalization: true + } + } + }, + '---', + { + opcode: 'isClone', + text: 'is [TARGET] a clone', + blockType: BlockType.BOOLEAN, + arguments: { + TARGET: Target.Argument + } + }, + { + opcode: 'isTouching', + text: 'is [A] touching [B]', + blockType: BlockType.BOOLEAN, + arguments: { + A: Target.Argument, + B: Target.Argument + } + }, + '---', + { + opcode: 'getVar', + text: 'var [NAME] of [TARGET]', + blockType: BlockType.REPORTER, + allowDropAnywhere: true, + arguments: { + TARGET: Target.Argument, + NAME: { + type: ArgumentType.STRING + } + } + }, + { + opcode: 'setVar', + text: 'set var [NAME] of [TARGET] to [VALUE]', + blockType: BlockType.COMMAND, + arguments: { + TARGET: Target.Argument, + NAME: { + type: ArgumentType.STRING + }, + VALUE: { + type: ArgumentType.STRING, + exemptFromNormalization: true + } + } + }, + '---', + { + opcode: 'clone', + text: 'create clone of [TARGET]', + blockType: BlockType.COMMAND, + arguments: { + TARGET: Target.Argument + } + }, + { + opcode: 'cloneR', + text: 'create clone of [TARGET]', + arguments: { + TARGET: Target.Argument + }, + ...Target.Block + }, + '---', + { + opcode: 'all', + text: 'all targets', + ...jwArray.Block + }, + { + opcode: 'touching', + text: 'targets touching [TARGET]', + arguments: { + TARGET: Target.Argument + }, + ...jwArray.Block + }, + { + opcode: 'clones', + text: 'clones of [TARGET]', + arguments: { + TARGET: Target.Argument + }, + ...jwArray.Block + }, + { + opcode: 'arrayHasTarget', + text: '[ARRAY] has clone of [TARGET]', + blockType: BlockType.BOOLEAN, + arguments: { + ARRAY: jwArray.Argument, + TARGET: Target.Argument + } + }, + '---', + { + blockType: BlockType.XML, + xml: `` + } + ], + menus: { + sprite: { + acceptReporters: true, + items: 'getSpriteMenu' + }, + targetProperty: { + acceptReporters: true, + items: [ + "name", + "x", + "y", + "direction", + "size", + "stretch x", + "stretch y", + "costume #", + "costume name", + ] + }, + targetPropertySet: { + acceptReporters: true, + items: [ + "x", + "y", + "direction", + "size", + "stretch x", + "stretch y", + "costume #", + "costume name", + ] + } + } + }; + } + + getSpriteMenu({}) { + let sprites = ["this", "stage"] + for (let target of vm.runtime.targets.filter(v => v !== vm.runtime._stageTarget)) { + if (!sprites.includes(target.sprite.name)) sprites.push(target.sprite.name) + } + return sprites + } + + this({}, util) { + return new Target.Type(util.target.id) + } + + stage() { + return new Target.Type(vm.runtime._stageTarget.id) + } + + fromName({SPRITE}, util) { + SPRITE = Cast.toString(SPRITE) + if (SPRITE == "this") return this.this({}, util) + if (SPRITE == "stage") return this.stage() + let target = vm.runtime.getSpriteTargetByName(SPRITE) + return new Target.Type(target ? target.id : "") + } + + cloneOrigin({TARGET}, util) { + TARGET = Target.Type.toTarget(TARGET) + if (!TARGET.target) return "" + + return this.fromName({SPRITE: TARGET.target.sprite.name}, util) + } + + get({TARGET, MENU}) { + TARGET = Target.Type.toTarget(TARGET) + MENU = Cast.toString(MENU) + + if (!TARGET.target) return "" + + switch(MENU) { + case "x": return TARGET.target.x + case "y": return TARGET.target.y + case "direction": return TARGET.target.direction + case "size": return TARGET.target.size + case "name": return TARGET.target.sprite.name + case "stretch x": return TARGET.target.stretch[0] + case "stretch y": return TARGET.target.stretch[1] + case "costume #": return TARGET.target.currentCostume + 1 + case "costume name": return TARGET.target.getCurrentCostume().name + } + + return "" + } + + set({TARGET, MENU, VALUE}) { + TARGET = Target.Type.toTarget(TARGET) + MENU = Cast.toString(MENU) + + if (!TARGET.target) return + + switch(MENU) { + case "x": + TARGET.target.setXY(Cast.toNumber(VALUE), TARGET.target.y) + break + case "y": + TARGET.target.setXY(TARGET.target.x, Cast.toNumber(VALUE)) + break + case "direction": + TARGET.target.setDirection(Cast.toNumber(VALUE)) + break + case "size": + TARGET.target.setSize(Cast.toNumber(VALUE)) + break + case "stretch x": + TARGET.target.setStretch(Cast.toNumber(VALUE), TARGET.target.stretch[1]) + break + case "stretch y": + TARGET.target.setStretch(TARGET.target.stretch[0], Cast.toNumber(VALUE)) + break + case "costume #": + TARGET.target.setCostume(Cast.toNumber(VALUE) - 1) + break + case "costume name": + let index = TARGET.target.getCostumes().indexOf(TARGET.target.getCostumes().find(v => v.name === Cast.toString(VALUE))) + TARGET.target.setCostume(index) + break + } + } + + isClone({TARGET}) { + TARGET = Target.Type.toTarget(TARGET) + if (!TARGET.target) return false + + return !TARGET.target.isOriginal + } + + isTouching({A, B}) { + A = Target.Type.toTarget(A) + B = Target.Type.toTarget(B) + + if (!A.target) return + + return A.target.isTouchingTarget(B.targetId) + } + + getVar({TARGET, NAME}) { + TARGET = Target.Type.toTarget(TARGET) + NAME = Cast.toString(NAME) + if (!TARGET.target) return "" + + let variable = Object.values(TARGET.target.variables).find(v => v.name == NAME) + if (!variable) return "" + + return variable.value + } + + setVar({TARGET, NAME, VALUE}) { + TARGET = Target.Type.toTarget(TARGET) + NAME = Cast.toString(NAME) + if (!TARGET.target) return + + let variable = Object.values(TARGET.target.variables).find(v => v.name == NAME) + if (!variable) return + + variable.value = VALUE + } + + clone(args) { + this.cloneR(args) + } + + cloneR({TARGET}) { + TARGET = Target.Type.toTarget(TARGET) + if (!TARGET.target) return + + let origin = TARGET.target + let clone = origin.makeClone() + + if (clone) { + vm.runtime.addTarget(clone) + clone.goBehindOther(origin) //mimick clone making from control category + } + + return new Target.Type(clone ? clone.id : "") + } + + all() { + return new jwArray.Type(vm.runtime.targets.map(v => new Target.Type(v.id))) + } + + touching({TARGET}) { + TARGET = Target.Type.toTarget(TARGET) + if (!TARGET.target) return new jwArray.Type + + let targets = vm.runtime.targets + targets = targets.filter(v => v !== TARGET && !v.isStage) + targets = targets.filter(v => v.isTouchingTarget(TARGET.targetId)) + return new jwArray.Type(targets.map(v => new Target.Type(v.id))) + } + + clones({TARGET}) { + TARGET = Target.Type.toTarget(TARGET) + if (TARGET.target) { + return new jwArray.Type(TARGET.target.sprite.clones.filter(v => !v.isOriginal).map(v => new Target.Type(v.id))) + } + return new jwArray.Type() + } + + arrayHasTarget({ARRAY, TARGET}) { + ARRAY = jwArray.Type.toArray(ARRAY) + TARGET = Target.Type.toTarget(TARGET) + if (!TARGET.target) return false + + return ARRAY.array.find(v => { + let target = Target.Type.toTarget(v) + if (!target.target) return false + return target.target.sprite == TARGET.target.sprite + }) !== undefined + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jwVector/index.js b/local-scratch-vm/src/extensions/jwVector/index.js new file mode 100644 index 0000000000000000000000000000000000000000..766b2484738934e6b42b93758c8c314e46289d90 --- /dev/null +++ b/local-scratch-vm/src/extensions/jwVector/index.js @@ -0,0 +1,377 @@ +const BlockType = require('../../extension-support/block-type') +const BlockShape = require('../../extension-support/block-shape') +const ArgumentType = require('../../extension-support/argument-type') +const Cast = require('../../util/cast') + +/** + * @param {number} x + * @returns {string} + */ +function formatNumber(x) { + if (x >= 1e6) { + return x.toExponential(4) + } else { + x = Math.floor(x * 1000) / 1000 + return x.toFixed(Math.min(3, (String(x).split('.')[1] || '').length)) + } +} + +function span(text) { + let el = document.createElement('span') + el.innerHTML = text + el.style.display = 'hidden' + el.style.whiteSpace = 'nowrap' + el.style.width = '100%' + el.style.textAlign = 'center' + return el +} + +class VectorType { + customId = "jwVector" + + constructor(x = 0, y = 0) { + this.x = isNaN(x) ? 0 : x + this.y = isNaN(y) ? 0 : y + } + + static toVector(x) { + if (x instanceof VectorType) return x + if (x instanceof Array && x.length == 2) return new VectorType(x[0], x[1]) + if (String(x).split(',')) return new VectorType(Cast.toNumber(String(x).split(',')[0]), Cast.toNumber(String(x).split(',')[1])) + return new VectorType(0, 0) + } + + jwArrayHandler() { + return 'Vector' + } + + toString() { + return `${this.x},${this.y}` + } + toMonitorContent = () => span(this.toString()) + + toReporterContent() { + let root = document.createElement('div') + root.style.display = 'flex' + root.style.width = "200px" + root.style.overflow = "hidden" + let details = document.createElement('div') + details.style.display = 'flex' + details.style.flexDirection = 'column' + details.style.justifyContent = 'center' + details.style.width = "100px" + details.appendChild(span(`X: ${formatNumber(this.x)}`)) + details.appendChild(span(`Y: ${formatNumber(this.y)}`)) + root.appendChild(details) + let angle = document.createElement('div') + angle.style.width = "100px" + let circle = document.createElement('div') + circle.style.width = "84px" + circle.style.height = "84px" + circle.style.margin = "8px" + circle.style.border = "4px solid black" + circle.style.borderRadius = "100%" + circle.style.boxSizing = "border-box" + circle.style.transform = `rotate(${this.angle}deg)` + let line = document.createElement('div') + line.style.width = "8px" + line.style.height = "50%" + line.style.background = "black" + line.style.position = "absolute" + line.style.left = "calc(50% - 4px)" + circle.appendChild(line) + angle.appendChild(circle) + root.appendChild(angle) + return root + } + + /** @returns {number} */ + get magnitude() { return Math.hypot(this.x, this.y) } + + /** @returns {number} */ + get angle() {return Math.atan2(this.x, this.y) * (180 / Math.PI)} +} + +const Vector = { + Type: VectorType, + Block: { + blockType: BlockType.REPORTER, + blockShape: BlockShape.LEAF, + forceOutputType: "Vector", + disableMonitor: true + }, + Argument: { + shape: BlockShape.LEAF, + check: ["Vector"] + } +} + +class Extension { + constructor() { + vm.jwVector = Vector + vm.runtime.registerSerializer( + "jwVector", + v => [v.x, v.y], + v => new Vector.Type(v[0], v[1]) + ); + } + + getInfo() { + return { + id: "jwVector", + name: "Vector", + color1: "#6babff", + menuIconURI: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM6Yng9Imh0dHBzOi8vYm94eS1zdmcuY29tIj4KICA8ZWxsaXBzZSBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAycHg7IHBhaW50LW9yZGVyOiBzdHJva2U7IGZpbGw6IHJnYigxMDcsIDE3MSwgMjU1KTsgc3Ryb2tlOiByZ2IoNjksIDEyNiwgMjA0KTsiIGN4PSIxMCIgY3k9IjEwIiByeD0iOSIgcnk9IjkiPjwvZWxsaXBzZT4KICA8cGF0aCBkPSJNIDQuMzUyIDEzLjc2NiBDIDQuMzUyIDE0LjgwNSA1LjE5NCAxNS42NDggNi4yMzUgMTUuNjQ4IEwgMTAgMTUuNjQ4IEMgMTEuMDM5IDE1LjY0OCAxMS44ODIgMTQuODA1IDExLjg4MiAxMy43NjYgTCAxMS44ODIgMTAgQyAxMS44ODIgOC45NTkgMTEuMDM5IDguMTE4IDEwIDguMTE4IEwgNi4yMzUgOC4xMTggQyA1LjE5NCA4LjExOCA0LjM1MiA4Ljk1OSA0LjM1MiAxMCBMIDQuMzUyIDEzLjc2NiBNIDguMTE3IDEzLjc2NiBDIDYuNjY4IDEzLjc2NiA1Ljc2MiAxMi4xOTUgNi40ODcgMTAuOTQyIEMgNi44MjIgMTAuMzU4IDcuNDQzIDEwIDguMTE3IDEwIEMgOS41NjcgMTAgMTAuNDcyIDExLjU2OSA5Ljc0NyAxMi44MjQgQyA5LjQxMSAxMy40MDYgOC43ODkgMTMuNzY2IDguMTE3IDEzLjc2NiBNIDcuMTc2IDkuMDU5IEwgOS4wNTggOS4wNTkgTCA5LjA1OCA1LjI5NCBDIDkuMDU4IDQuNTY5IDguMjczIDQuMTE2IDcuNjQ3IDQuNDc5IEMgNy4zNTUgNC42NDYgNy4xNzYgNC45NTcgNy4xNzYgNS4yOTQgTCA3LjE3NiA5LjA1OSBaIE0gMTAuOTQxIDEwLjk0MiBMIDEwLjk0MSAxMi44MjQgTCAxNC43MDYgMTIuODI0IEMgMTUuNDMxIDEyLjgyNCAxNS44ODMgMTIuMDM5IDE1LjUyMSAxMS40MTIgQyAxNS4zNTIgMTEuMTIxIDE1LjA0MSAxMC45NDIgMTQuNzA2IDEwLjk0MiBMIDEwLjk0MSAxMC45NDIgWiIgc3R5bGU9ImZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsiPjwvcGF0aD4KPC9zdmc+", + blocks: [ + { + opcode: 'newVector', + text: 'new vector x: [X] y: [Y]', + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + }, + ...Vector.Block + }, + { + opcode: 'newVectorFromMagnitude', + text: 'new vector magnitude: [X] angle: [Y]', + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + Y: { + type: ArgumentType.ANGLE, + defaultValue: 0 + } + }, + ...Vector.Block + }, + "---", + { + opcode: 'vectorX', + text: '[VECTOR] x', + blockType: BlockType.REPORTER, + arguments: { + VECTOR: Vector.Argument + } + }, + { + opcode: 'vectorY', + text: '[VECTOR] y', + blockType: BlockType.REPORTER, + arguments: { + VECTOR: Vector.Argument + } + }, + "---", + { + opcode: 'add', + text: '[X] + [Y]', + arguments: { + X: Vector.Argument, + Y: Vector.Argument + }, + ...Vector.Block + }, + { + opcode: 'subtract', + text: '[X] - [Y]', + arguments: { + X: Vector.Argument, + Y: Vector.Argument + }, + ...Vector.Block + }, + { + opcode: 'multiplyA', + text: '[X] * [Y]', + arguments: { + X: Vector.Argument, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + ...Vector.Block + }, + { + opcode: 'multiplyB', + text: '[X] * [Y]', + arguments: { + X: Vector.Argument, + Y: Vector.Argument + }, + ...Vector.Block + }, + { + opcode: 'divideA', + text: '[X] / [Y]', + arguments: { + X: Vector.Argument, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + ...Vector.Block + }, + { + opcode: 'divideB', + text: '[X] / [Y]', + arguments: { + X: Vector.Argument, + Y: Vector.Argument + }, + ...Vector.Block + }, + "---", + { + opcode: 'magnitude', + text: 'magnitude of [VECTOR]', + blockType: BlockType.REPORTER, + arguments: { + VECTOR: Vector.Argument + } + }, + { + opcode: 'angle', + text: 'angle of [VECTOR]', + blockType: BlockType.REPORTER, + arguments: { + VECTOR: Vector.Argument + } + }, + { + opcode: 'normalize', + text: 'normalize [VECTOR]', + arguments: { + VECTOR: Vector.Argument + }, + ...Vector.Block + }, + { + opcode: 'absolute', + text: 'absolute [VECTOR]', + arguments: { + VECTOR: Vector.Argument + }, + ...Vector.Block + }, + { + opcode: 'rotate', + text: 'rotate [VECTOR] by [ANGLE]', + arguments: { + VECTOR: Vector.Argument, + ANGLE: { + type: ArgumentType.ANGLE, + defaultValue: 90 + } + }, + ...Vector.Block + }, + ] + }; + } + + newVector(args) { + const X = Cast.toNumber(args.X) + const Y = Cast.toNumber(args.Y) + + return new VectorType(X, Y) + } + + newVectorFromMagnitude(args) { + return this.rotate({VECTOR: new VectorType(0, Cast.toNumber(args.X)), ANGLE: args.Y}) + } + + vectorX(args) { + return VectorType.toVector(args.VECTOR).x + } + + vectorY(args) { + return VectorType.toVector(args.VECTOR).y + } + + add(args) { + const X = VectorType.toVector(args.X) + const Y = VectorType.toVector(args.Y) + + return new VectorType(X.x + Y.x, X.y + Y.y) + } + + subtract(args) { + const X = VectorType.toVector(args.X) + const Y = VectorType.toVector(args.Y) + + return new VectorType(X.x - Y.x, X.y - Y.y) + } + + multiplyA(args) { + const X = VectorType.toVector(args.X) + const Y = Cast.toNumber(args.Y) + + return new VectorType(X.x * Y, X.y * Y) + } + + multiplyB(args) { + const X = VectorType.toVector(args.X) + const Y = VectorType.toVector(args.Y) + + return new VectorType(X.x * Y.x, X.y * Y.y) + } + + divideA(args) { + const X = VectorType.toVector(args.X) + const Y = Cast.toNumber(args.Y) + + return new VectorType(X.x / Y, X.y / Y) + } + + divideB(args) { + const X = VectorType.toVector(args.X) + const Y = VectorType.toVector(args.Y) + + return new VectorType(X.x / Y.x, X.y / Y.y) + } + + magnitude(args) { + return VectorType.toVector(args.VECTOR).magnitude + } + + angle(args) { + return VectorType.toVector(args.VECTOR).angle + } + + normalize(args) { + const v = VectorType.toVector(args.VECTOR) + + return new VectorType(v.x / v.magnitude, v.y / v.magnitude) + } + + absolute(args) { + const v = VectorType.toVector(args.VECTOR) + + return new VectorType(Math.abs(v.x), Math.abs(v.y)) + } + + rotate(args) { + const v = VectorType.toVector(args.VECTOR) + const ANGLE = Cast.toNumber(args.ANGLE) / 180 * -Math.PI + const cos = Math.cos(ANGLE) + const sin = Math.sin(ANGLE) + + return new VectorType( + v.x * cos - v.y * sin, + v.x * sin + v.y * cos + ) + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jw_encrypt/index.js b/local-scratch-vm/src/extensions/jw_encrypt/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3ea5a76f4e57ce60384470d1dc83c39f86f2d624 --- /dev/null +++ b/local-scratch-vm/src/extensions/jw_encrypt/index.js @@ -0,0 +1,65 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); + +/** + * Class for Proto blocks + * @constructor + */ +class jwEncrypt { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + autoLoad: true, + id: 'jwEncrypt', + name: 'Encrypt', + // blockIconURI: blockIconURI, + color1: '#ffdc7a', + color2: '#ffd45e', + blocks: [ + { + opcode: 'encrypt', + text: formatMessage({ + id: 'jwEncrypt.blocks.encrypt', + default: 'encrypt [MENU] [VALUE]', + description: 'Encrypt a string' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + VALUE: { + type: ArgumentType.STRING, + defaultValue: "string" + }, + MENU: { + type: ArgumentType.STRING, + defaultValue: 'base64', + menu: 'encrypt' + } + } + } + ], + menus: { + encrypt: [ + 'base64' + ] + } + }; + } + + encrypt() { + return "test"; + } +} + +module.exports = jwEncrypt; diff --git a/local-scratch-vm/src/extensions/jw_postlit/index.js b/local-scratch-vm/src/extensions/jw_postlit/index.js new file mode 100644 index 0000000000000000000000000000000000000000..be3db33f6ccb38c0da9c267bbe3d1caa61457468 --- /dev/null +++ b/local-scratch-vm/src/extensions/jw_postlit/index.js @@ -0,0 +1,356 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +// const Cast = require('../../util/cast'); + +const proxy = "https://proxy.jwklong.repl.co" +const prefix = "https://postlit.dev/" + +/** + * Class for PostLit blocks + * @constructor + */ +class jwPostLit { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + loginData = { + username: '', + token: '' + } + + latestPost = '' + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jwPostLit', + name: 'postLit', + //blockIconURI: blockIconURI, + color2: '#14f789', + color1: '#0fd173', + blocks: [ + { + opcode: 'categorySignIn', + text: formatMessage({ + id: 'jwPostLit.blocks.categorySignIn', + default: 'Sign In', + description: 'Sign in to postLit.' + }), + blockType: BlockType.LABEL + }, + { + opcode: 'signIn', + text: formatMessage({ + id: 'jwPostLit.blocks.signIn', + default: 'sign in [USER] [PASS]', + description: 'Sign in to postLit.' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + USER: { + type: ArgumentType.STRING, + defaultValue: "username" + }, + PASS: { + type: ArgumentType.STRING, + defaultValue: "password" + } + } + }, + { + opcode: 'currentUsername', + text: formatMessage({ + id: 'jwPostLit.blocks.currentUsername', + default: 'username', + description: 'Username for your postLit account.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'currentToken', + text: formatMessage({ + id: 'jwPostLit.blocks.currentToken', + default: 'token', + description: 'Token for your postLit account.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + { + opcode: 'isSignedIn', + text: formatMessage({ + id: 'jwPostLit.blocks.isSignedIn', + default: 'signed in?', + description: 'Checks if you are currently signed into a postLit account.' + }), + disableMonitor: false, + blockType: BlockType.BOOLEAN + }, + "---", + { + opcode: 'categoryPosts', + text: formatMessage({ + id: 'jwPostLit.blocks.categoryPosts', + default: 'Posts', + description: 'Blocks to create and get data from posts' + }), + blockType: BlockType.LABEL + }, + { + opcode: 'createPost', + text: formatMessage({ + id: 'jwPostLit.blocks.createPost', + default: 'create post [STRING]', + description: 'Create a post.' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + STRING: { + type: ArgumentType.STRING, + defaultValue: "post" + } + } + }, + { + opcode: 'getLatestPost', + text: formatMessage({ + id: 'jwPostLit.blocks.getLatestPost', + default: 'latest post id', + description: 'Gets the ID of the latest post made with the create post block.' + }), + disableMonitor: false, + blockType: BlockType.REPORTER + }, + "---", + { + opcode: 'getPost', + text: formatMessage({ + id: 'jwPostLit.blocks.getPost', + default: 'get post [ID] [WANTS]', + description: 'Gets some data from a post.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + ID: { + type: ArgumentType.STRING, + defaultValue: 'id' + }, + WANTS: { + type: ArgumentType.STRING, + defaultValue: 'json', + menu: 'getPostWants' + }, + } + }, + { + opcode: 'likePost', + text: formatMessage({ + id: 'jwPostLit.blocks.likePost', + default: 'like post [ID]', + description: 'Like a post.' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + ID: { + type: ArgumentType.STRING, + defaultValue: 'id' + }, + } + }, + { + opcode: 'unlikePost', + text: formatMessage({ + id: 'jwPostLit.blocks.unlikePost', + default: 'unlike post [ID]', + description: 'Unlike a post.' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + ID: { + type: ArgumentType.STRING, + defaultValue: 'id' + }, + } + } + ], + menus: { + getPostWants: [ + 'json', + 'author', + 'content', + 'time', + 'comments', + 'likes', + 'likers', + 'reposts' + ] + } + }; + } + + async signIn(args, util) { + const username = String(args.USER) + const password = String(args.PASS) + var response = await fetch(proxy, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + url: prefix + "signin", + method: 'POST', + body: { + username: username, + password: password + } + }) + }) + var data = await response.json() + if (data.success) { + this.loginData = { + username: username, + token: data.token + } + } + } + + currentUsername(args, util) { + return this.loginData.username + } + + currentToken(args, util) { + return this.loginData.token + } + + isSignedIn(args, util) { + return this.loginData.token !== '' + } + + async createPost(args, util) { + const string = String(args.STRING) + var response = await fetch(proxy, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + url: prefix + "post", + method: 'POST', + headers: { + cookie: "token="+this.loginData.token + }, + body: { + content: string + } + }) + }) + const data = await response.json() + if (data.success) { + this.latestPost = data.success.split("/")[2] + } + } + + getLatestPost(args, util) { + return this.latestPost + } + + async getPost(args, util) { + const id = String(args.ID) + const wants = String(args.WANTS) + const url = prefix + "posts/" + id + "/data/" + var response = await fetch(proxy, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + url: url, + headers: { + cookie: "token="+this.loginData.token + }, + }) + }) + const data = await response.json() + switch (wants) { + case 'json': + return JSON.stringify(data) + case 'author': + return data.author + case 'content': + return data.content + case 'time': + return data.time + case 'comments': + return data.comments + case 'likes': + return (data.likes || []).length + case 'likers': + return JSON.stringify(data.likes || []) + case 'reposts': + return data.reposts || 0 + default: + return '' + } + } + + likePost(args, util) { + const id = String(args.ID) + fetch(proxy, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: { + url: prefix + "like", + method: "POST", + headers: { + cookie: "token="+this.loginData.token + }, + body: { + post: id + } + } + }) + } + + unlikePost(args, util) { + const id = String(args.ID) + fetch(proxy, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: { + url: prefix + "unlike", + method: "POST", + headers: { + cookie: "token="+this.loginData.token + }, + body: { + post: id + } + } + }) + } +} + +module.exports = jwPostLit; diff --git a/local-scratch-vm/src/extensions/jw_proto/index.js b/local-scratch-vm/src/extensions/jw_proto/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e468eede43981b9c368390ce5b2a044b8de813c4 --- /dev/null +++ b/local-scratch-vm/src/extensions/jw_proto/index.js @@ -0,0 +1,206 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); + +/** + * Class for Proto blocks + * @constructor + */ +class jwProto { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + // register compiled blocks + this.runtime.registerCompiledExtensionBlocks('jwProto', this.getCompileInfo()); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + autoLoad: true, + id: 'jwProto', + // ok at this point, just make a new extension if you add more stuff + // its been so long that it just wouldnt make sense for this to have other stuff + name: 'Labels', + // blockIconURI: blockIconURI, + color1: '#969696', + color2: '#6e6e6e', + blocks: [ + { + opcode: 'labelHat', + text: formatMessage({ + id: 'jwProto.blocks.labelHat', + default: '// [LABEL]', + description: 'Label for some unused blocks.' + }), + disableMonitor: true, + blockType: BlockType.HAT, + arguments: { + LABEL: { + type: ArgumentType.STRING, + defaultValue: "label" + } + } + }, + { + opcode: 'labelFunction', + text: formatMessage({ + id: 'jwProto.blocks.labelFunction', + default: '// [LABEL]', + description: 'Label for some blocks.' + }), + blockType: BlockType.COMMAND, + branchCount: 1, + arguments: { + LABEL: { + type: ArgumentType.STRING, + defaultValue: "label" + } + } + }, + { + opcode: 'labelCommand', + text: formatMessage({ + id: 'jwProto.blocks.labelCommand', + default: '// [LABEL]', + description: 'Label for labeling.' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + LABEL: { + type: ArgumentType.STRING, + defaultValue: "label" + } + } + }, + { + opcode: 'labelReporter', + text: formatMessage({ + id: 'jwProto.blocks.labelReporter', + default: '[VALUE] // [LABEL]', + description: 'Label for a value.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + LABEL: { + type: ArgumentType.STRING, + defaultValue: "label" + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "value" + } + } + }, + { + opcode: 'labelBoolean', + text: formatMessage({ + id: 'jwProto.blocks.labelBoolean', + default: '[VALUE] // [LABEL]', + description: 'Label for a boolean.' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + arguments: { + LABEL: { + type: ArgumentType.STRING, + defaultValue: "label" + }, + VALUE: { + type: ArgumentType.BOOLEAN + } + } + }, + { + blockType: BlockType.LABEL, + text: "Placeholders" + }, + { + opcode: 'placeholderCommand', + text: formatMessage({ + id: 'jwProto.blocks.placeholderCommand', + default: '...', + description: 'Placeholder for stack blocks.' + }), + blockType: BlockType.COMMAND + }, + { + opcode: 'placeholderReporter', + text: formatMessage({ + id: 'jwProto.blocks.placeholderReporter', + default: '...', + description: 'Placeholder for a value.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER + }, + { + opcode: 'placeholderBoolean', + text: formatMessage({ + id: 'jwProto.blocks.placeholderBoolean', + default: '...', + description: 'Placeholder for a boolean.' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN + }, + ] + }; + } + /** + * This function is used for any compiled blocks in the extension if they exist. + * Data in this function is given to the IR & JS generators. + * Data must be valid otherwise errors may occur. + * @returns {object} functions that create data for compiled blocks. + */ + getCompileInfo() { + return { + ir: { + labelFunction: (generator, block) => ({ + kind: 'stack', + branch: generator.descendSubstack(block, 'SUBSTACK') + }) + }, + js: { + labelFunction: (node, compiler, imports) => { + compiler.descendStack(node.branch, new imports.Frame(false)); + } + } + }; + } + + labelHat() { + return false; + } + labelFunction(_, util) { + util.startBranch(1, false); + } + labelCommand() { + return; + } + labelReporter(args) { + return args.VALUE; + } + labelBoolean(args) { + return args.VALUE; + } + + placeholderCommand() { + return; + } + placeholderReporter() { + return ''; + } + placeholderBoolean() { + return false; + } +} + +module.exports = jwProto; diff --git a/local-scratch-vm/src/extensions/jw_reflex/index.js b/local-scratch-vm/src/extensions/jw_reflex/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8800f5bffd7bd49b1fc420be096219b704e7deec --- /dev/null +++ b/local-scratch-vm/src/extensions/jw_reflex/index.js @@ -0,0 +1,210 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const TargetType = require("../../extension-support/target-type") +// const Cast = require('../../util/cast'); + +//const blockIconURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAMAAAD6TlWYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAPUExURQAAAP+xNQDiGgCU/wAAAJEQGGoAAAAFdFJOU/////8A+7YOUwAAAAlwSFlzAAAOwwAADsMBx2+oZAAABA5JREFUeF7t0EtuW0EUA9F8vP81Z8JRAwzbLuk5COoMBb1LdP34EGJAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEHos4M+HZfbtDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0KPBfxfGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEZsBfh/z8z/r9SfnsywwIGRAyIGRAyICQASEDQp8OeMrfvk06vEzOXjPgIWevGfCQs9cMeMjZawY85Ow1Ax5y9poBDzl7zYCHnL2GA57y2dvlvW+TmcmARWYmAxaZmQxYZGYyYJGZyYBFZiYDFpmZDFhkZnp5wFPOvFze+TaZmQxYZGYyYJGZyYBFZiYDFpmZDFhkZjJgkZnJgEVmprcHPOXsl+V9j8lsZcAhs5UBh8xWBhwyWxlwyGxlwCGzlQGHzFYGHDJbPR7wlJlreddjMlsZcMhsZcAhs5UBh8xWBhwyWxlwyGxlwCGzlQGHzFbfHvCU2SrvekxmKwMOma0MOGS2MuCQ2cqAQ2YrAw6ZrQw4ZLYy4JDZyoBDZisDDpmtDDhktjLgkNnKgENmKwMOma0MOGS2MuCQ2erbA2bmWt71mMxWBhwyWxlwyGxlwCGzlQGHzFYGHDJbGXDIbGXAIbPV4wFz9svyrsdktjLgkNnKgENmKwMOma0MOGS2MuCQ2cqAQ2YrAw6Zrd4eMGdeLu97m8xMBiwyMxmwyMxkwCIzkwGLzEwGLDIzGbDIzGTAIjPTywPms7fLO98mM5MBi8xMBiwyMxmwyMxkwCIzkwGLzEwGLDIzGbDIzIQD5m/fJu99mZy9ZsBDzl4z4CFnrxnwkLPXDHjI2WsGPOTsNQMecvaaAQ85e+3TAfPzPysdruWzLzMgZEDIgJABIQNCBoQMCM2A+jsDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgMjHxx+IPExM0h8siAAAAABJRU5ErkJggg==" + +/** + * Class for Reflex blocks + * @constructor + */ +class jwReflex { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jwReflex', + name: 'Reflex', + //blockIconURI: blockIconURI, + color1: '#000000', //tbd + color2: '#ffffff', // tbd + blocks: [ + { + opcode: 'createFlex', + text: formatMessage({ + id: 'jwReflex.blocks.createFlex', + default: 'create flex', + description: 'Creates the flex. If there is already a flex, nothing happens. You can only remove a flex by reloading the project.' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + filter: [TargetType.SPRITE] + }, + { + opcode: 'updateFlex', + text: formatMessage({ + id: 'jwReflex.blocks.updateFlex', + default: 'update flex', + description: 'Update position of sprite with flex data.' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + filter: [TargetType.SPRITE] + }, + "---", + { + opcode: 'setFlexXY', + text: formatMessage({ + id: 'jwReflex.blocks.setFlexXY', + default: 'set flex pos [FX] [FY]', + description: 'Sets flex position' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + FX: { + type: ArgumentType.NUMBER, + default: 0 + }, + FY: { + type: ArgumentType.NUMBER, + default: 0 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getFlexX', + text: formatMessage({ + id: 'jwReflex.blocks.getFlexX', + default: 'x flex position', + description: 'Gets the flex positon\'s x value' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getFlexY', + text: formatMessage({ + id: 'jwReflex.blocks.getFlexY', + default: 'y flex position', + description: 'Gets the flex positon\'s y value' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + "---", + { + opcode: 'setOffsetXY', + text: formatMessage({ + id: 'jwReflex.blocks.setOffsetXY', + default: 'set offset pos [OX] [OY]', + description: 'Sets offset position' + }), + disableMonitor: true, + blockType: BlockType.COMMAND, + arguments: { + OX: { + type: ArgumentType.NUMBER, + default: 0 + }, + OY: { + type: ArgumentType.NUMBER, + default: 0 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getOffsetX', + text: formatMessage({ + id: 'jwReflex.blocks.getOffsetX', + default: 'x offset position', + description: 'Gets the offset positon\'s x value' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + }, + { + opcode: 'getOffsetY', + text: formatMessage({ + id: 'jwReflex.blocks.getOffsetY', + default: 'y offset position', + description: 'Gets the offset positon\'s y value' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + filter: [TargetType.SPRITE] + } + ] + }; + } + + flexes = {} + + _updateFlex(target) { + const flex = this.flexes[target.getName()] + if (flex && !flex.paused) { + target.setXY((((flex.fx)/2)*vm.runtime.stageWidth)+flex.ox,(((flex.fy)/2)*vm.runtime.stageHeight)-flex.oy) + } + } + + createFlex(args, util) { + if (util.target.isSprite() && !Object.keys(this.flexes).includes(util.target.getName())) { + this.flexes[util.target.getName()] = { + fx: 0, + fy: 0, + ox: 0, + oy: 0, + } + } + console.debug(this.flexes) + } + updateFlex(args, util) { + this._updateFlex(util.target) + } + + setFlexXY(args, util) { + if (Object.keys(this.flexes).includes(util.target.getName())) { + this.flexes[util.target.getName()].fx = Number(args.FX) + this.flexes[util.target.getName()].fy = Number(args.FY) + } + } + getFlexX(args, util) { + if (Object.keys(this.flexes).includes(util.target.getName())) { + return this.flexes[util.target.getName()].fx + } + return 0 + } + getFlexY(args, util) { + if (Object.keys(this.flexes).includes(util.target.getName())) { + return this.flexes[util.target.getName()].fy + } + return 0 + } + + setOffsetXY(args, util) { + if (Object.keys(this.flexes).includes(util.target.getName())) { + this.flexes[util.target.getName()].ox = Number(args.OX) + this.flexes[util.target.getName()].oy = Number(args.OY) + } + } + getOffsetX(args, util) { + if (Object.keys(this.flexes).includes(util.target.getName())) { + return this.flexes[util.target.getName()].ox + } + return 0 + } + getOffsetY(args, util) { + if (Object.keys(this.flexes).includes(util.target.getName())) { + return this.flexes[util.target.getName()].oy + } + return 0 + } +} + +module.exports = jwReflex; diff --git a/local-scratch-vm/src/extensions/jw_structs/index.js b/local-scratch-vm/src/extensions/jw_structs/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6cdf03ed56c9a16e18a299874f7a10d428d33477 --- /dev/null +++ b/local-scratch-vm/src/extensions/jw_structs/index.js @@ -0,0 +1,313 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +// const Cast = require('../../util/cast'); + +/** + * Class for Structs + * @constructor + */ + +class jwStructs { + constructor(runtime) { + console.log("Welcome to the OOP extension!"); + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + this.classes = {}; + this.objects = {}; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + console.log("Getting info for the OOP extension!"); + return { + id: 'jwStructs', + name: 'Structs', + color1: '#7ddcff', + color2: '#4a98ff', + blocks: [ + { + opcode: 'createClass', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.createClass', + default: 'Create class [NAME]', + description: 'Create a class' + }), + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'MyClass' + } + } + }, + { + opcode: 'createClassProperty', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.createClassProperty', + default: 'Create class property [NAME] with value [VALUE] in class [CLASS]', + description: 'Create a class property' + }), + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'myProperty' + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: 'myValue' + }, + CLASS: { + type: ArgumentType.STRING, + defaultValue: 'MyClass' + } + } + }, + "---", + { + opcode: 'newObject', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.newObject', + default: 'Create object [NAME] from class [CLASS]', + description: 'Create a new object' + }), + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'myObject' + }, + CLASS: { + type: ArgumentType.STRING, + defaultValue: 'MyClass' + } + } + }, + { + opcode: 'setObjectProperty', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.setObjectProperty', + default: 'Set property [PROPERTY] of object [OBJECT] to [VALUE]', + description: 'Set a property of an object' + }), + arguments: { + PROPERTY: { + type: ArgumentType.STRING, + defaultValue: 'myProperty' + }, + OBJECT: { + type: ArgumentType.STRING, + defaultValue: 'myObject' + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: 'myValue' + } + } + }, + { + opcode: 'returnObjectProperty', + blockType: BlockType.REPORTER, + text: formatMessage({ + id: 'jwStructs.returnObjectProperty', + default: 'Property [PROPERTY] of object [OBJECT]', + description: 'Return a property of an object' + }), + arguments: { + PROPERTY: { + type: ArgumentType.STRING, + defaultValue: 'myProperty' + }, + OBJECT: { + type: ArgumentType.STRING, + defaultValue: 'myObject' + } + } + }, + "---", + { + opcode: 'createClassMethod', + blockType: BlockType.HAT, + text: formatMessage({ + id: 'jwStructs.createClassMethod', + default: 'When method [NAME] is called in class [CLASS]', + description: 'Create a class method' + }), + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'myMethod' + }, + CLASS: { + type: ArgumentType.STRING, + defaultValue: 'MyClass' + } + } + }, + { + opcode: 'callObjectMethod', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.callObjectMethod', + default: 'Call method [NAME] of object [OBJECT]', + description: 'Call a method of an object' + }), + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: 'myMethod' + }, + OBJECT: { + type: ArgumentType.STRING, + defaultValue: 'myObject' + } + } + }, + "---", + { + opcode: 'deleteClasses', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.deleteClasses', + default: 'Delete all classes', + description: 'Delete all classes' + }) + }, + { + opcode: 'deleteObjects', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.deleteObjects', + default: 'Delete all objects', + description: 'Delete all objects' + }) + }, + { + opcode: 'deleteClass', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.deleteClass', + default: 'Delete class [CLASS]', + description: 'Delete a class' + }), + arguments: { + CLASS: { + type: ArgumentType.STRING, + defaultValue: 'MyClass' + } + } + }, + { + opcode: 'deleteObject', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'jwStructs.deleteObject', + default: 'Delete object [OBJECT]', + description: 'Delete an object' + }), + arguments: { + OBJECT: { + type: ArgumentType.STRING, + defaultValue: 'myObject' + } + } + } + ] + }; + } + + createClass(args,util) { + var name = args.NAME; + if (name in this.classes) { + return; + } + this.classes[name] = { + properties: {}, + methods: {} + }; + } + + createClassProperty(args,util) { + var name = args.NAME; + var value = args.VALUE; + var className = args.CLASS; + if (className in this.classes) { + this.classes[className].properties[name] = value; + } + } + + newObject(args,util) { + var name = args.NAME; + var className = args.CLASS; + if (className in this.classes) { + this.objects[name] = this.classes[className]; + } + } + + setObjectProperty(args,util) { + var property = args.PROPERTY; + var object = args.OBJECT; + var value = args.VALUE; + if (object in this.objects) { + this.objects[object].properties[property] = value; + } + } + + returnObjectProperty(args,util) { + var property = args.PROPERTY; + var object = args.OBJECT; + if (object in this.objects) { + return this.objects[object].properties[property]; + } + } + + createClassMethod(args,util) { + var name = args.NAME; + var className = args.CLASS; + if (className in this.classes) { + this.classes[className].methods[name] = util.stackFrame; + } + } + + callObjectMethod(args,util) { + var name = args.NAME; + var object = args.OBJECT; + if (object in this.objects) { + var method = this.objects[object].methods[name]; + if (method) { + util.startBranch(1,method); + } + } + } + + deleteClasses(args,util) { + this.classes = {}; + } + + deleteObjects(args,util) { + this.objects = {}; + } + + deleteClass(args,util) { + var className = args.CLASS; + if (className in this.classes) { + delete this.classes[className]; + } + } + + deleteObject(args,util) { + var object = args.OBJECT; + if (object in this.objects) { + delete this.objects[object]; + } + } +} + +module.exports = jwStructs; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/jw_unite/index.js b/local-scratch-vm/src/extensions/jw_unite/index.js new file mode 100644 index 0000000000000000000000000000000000000000..91b5971a4d336e6a48cc062b1cf769027becd7f6 --- /dev/null +++ b/local-scratch-vm/src/extensions/jw_unite/index.js @@ -0,0 +1,689 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const { validateRegex } = require('../../util/json-block-utilities') +// const Cast = require('../../util/cast'); + +const blockIconURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAMAAAD6TlWYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAPUExURQAAAP+xNQDiGgCU/wAAAJEQGGoAAAAFdFJOU/////8A+7YOUwAAAAlwSFlzAAAOwwAADsMBx2+oZAAABA5JREFUeF7t0EtuW0EUA9F8vP81Z8JRAwzbLuk5COoMBb1LdP34EGJAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEHos4M+HZfbtDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0KPBfxfGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEZsBfh/z8z/r9SfnsywwIGRAyIGRAyICQASEDQp8OeMrfvk06vEzOXjPgIWevGfCQs9cMeMjZawY85Ow1Ax5y9poBDzl7zYCHnL2GA57y2dvlvW+TmcmARWYmAxaZmQxYZGYyYJGZyYBFZiYDFpmZDFhkZnp5wFPOvFze+TaZmQxYZGYyYJGZyYBFZiYDFpmZDFhkZjJgkZnJgEVmprcHPOXsl+V9j8lsZcAhs5UBh8xWBhwyWxlwyGxlwCGzlQGHzFYGHDJbPR7wlJlreddjMlsZcMhsZcAhs5UBh8xWBhwyWxlwyGxlwCGzlQGHzFbfHvCU2SrvekxmKwMOma0MOGS2MuCQ2cqAQ2YrAw6ZrQw4ZLYy4JDZyoBDZisDDpmtDDhktjLgkNnKgENmKwMOma0MOGS2MuCQ2erbA2bmWt71mMxWBhwyWxlwyGxlwCGzlQGHzFYGHDJbGXDIbGXAIbPV4wFz9svyrsdktjLgkNnKgENmKwMOma0MOGS2MuCQ2cqAQ2YrAw6Zrd4eMGdeLu97m8xMBiwyMxmwyMxkwCIzkwGLzEwGLDIzGbDIzGTAIjPTywPms7fLO98mM5MBi8xMBiwyMxmwyMxkwCIzkwGLzEwGLDIzGbDIzIQD5m/fJu99mZy9ZsBDzl4z4CFnrxnwkLPXDHjI2WsGPOTsNQMecvaaAQ85e+3TAfPzPysdruWzLzMgZEDIgJABIQNCBoQMCM2A+jsDQgaEDAgZEDIgZEDIgJABIQNCBoQMCBkQMiBkQMiAkAEhA0IGhAwIGRAyIGRAyICQASEDQgaEDAgZEDIgZEDIgMjHxx+IPExM0h8siAAAAABJRU5ErkJggg==" + +/** + * Class for Unite blocks + * @constructor + */ +class jwUnite { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + alert('unite is deprecated, please use the blocks in the toolbox') + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'jwUnite', + name: 'Unite', + blockIconURI: blockIconURI, + color1: '#7ddcff', + color2: '#4a98ff', + blocks: [ + { + opcode: 'always', + text: formatMessage({ + id: 'jwUnite.blocks.always', + default: 'always', + description: 'Runs the code every tick' + }), + disableMonitor: true, + blockType: BlockType.EVENT + }, + { + opcode: 'whenanything', + text: formatMessage({ + id: 'jwUnite.blocks.whenanything', + default: 'when [ANYTHING]', + description: 'Runs blocks when set boolean is true' + }), + disableMonitor: true, + blockType: BlockType.HAT, + arguments: { + ANYTHING: { + type: ArgumentType.BOOLEAN, + } + } + }, + "---", + { + opcode: 'getspritewithattrib', + text: formatMessage({ + id: 'jwUnite.blocks.getspritewithattrib', + default: 'get sprite with [var] set to [val]', + description: 'Reports the first sprite with a variable set to a value' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + var: { + type: ArgumentType.STRING, + defaultValue: "my variable" + }, + val: { + type: ArgumentType.STRING, + defaultValue: "0" + } + } + }, + "---", + { + opcode: 'backToGreenFlag', + text: formatMessage({ + id: 'jwUnite.blocks.backToGreenFlag', + default: 'run [FLAG]', + description: 'Acts like a click on the flag has been done.' + }), + terminal: true, + blockType: BlockType.COMMAND, + arguments: { + FLAG: { + type: ArgumentType.IMAGE, + dataURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACJ0lEQVRIid2Uy09TQRSHv2kv1VR5hhQQqwlYGtqCmtbHokhDTNQtEVz4B/gfmBCDXoyGmLj3kbBwY2LY6MpHWJDUoBukKsTKQ6I2paWGBipCo73jBii30BpI78ZfMpkzMznznTOPAwZLbFgt/ZWY011k9gzysSdZLICStdJXkPRjSns41hcH2Y7EjmAVmESKFyjyCaPqr50AzBtWbeAsEKDE0oy7w4rnfAXNbWU0nlY40CQwmX0kY73UBr4QGw5vu5vndg31Zy6wv3OGhecZfQbrajk3Rp2zXRdCqe0Irg5o8k8w+uwWR9VLmE13eaeNgarRqtoQ4jKKdpUy+wLy+3Hg2vaAQlIsbk51ZUjNJ/n8egBv5CBan2Bf5TL1nlkafGkWYz8YeVy94bIjwHpOpTY/vs7Nc1WAHQCNr5sXTLsA/EOaNBagabph8QEyYzAA3QkZkYGeUOAVyTlWUt/I/M5gLa/GpDjYXFryuumPaCsgMWsl+ilEfLoOSQTBClBBuW2GExcXsVi9hQFSF8RWQGT8MNCLwzXAYHc2nNabbQzdu4/TH6ThpBMhbDmef1iKhRgf8iHFo/wAeEpIfUgoZ/bD9SAB1Us42MPUiJOaxhBVh5YwKYKfib3MhZtYXS5B0sl79VUhQH4Nq6vADbzqHaJTbUQn3QjKECKOlG8Iqblh7apUsFayX661gjLgH/zPgPm1PlFMQPaSHa4HTIcnSFW8LSbAcP0F3uGqEimnx6MAAAAASUVORK5CYII=', + alt: 'Blue Flag' + } + } + }, + "---", + { + opcode: 'trueBoolean', + text: formatMessage({ + id: 'jwUnite.blocks.trueBoolean', + default: 'true', + description: 'Returns true' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + }, + { + opcode: 'falseBoolean', + text: formatMessage({ + id: 'jwUnite.blocks.falseBoolean', + default: 'false', + description: 'Returns false' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + }, + { + opcode: 'randomBoolean', + text: formatMessage({ + id: 'jwUnite.blocks.randomBoolean', + default: 'random', + description: 'Returns true or false' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + }, + "---", + { + opcode: 'mobile', + text: formatMessage({ + id: 'jwUnite.blocks.mobile', + default: 'mobile?', + description: 'Returns true if the project is running on a mobile device' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + }, + "---", + { + opcode: 'thing_is_text', + text: formatMessage({ + id: 'jwUnite.blocks.thing_is_text', + default: '[TEXT1] is text?', + description: 'Checks if something is text!' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + arguments: { + TEXT1: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.thing_is_text_whatToCheck', + default: 'world', + description: 'What to check.' + }) + } + } + }, + { + opcode: 'thing_is_number', + text: formatMessage({ + id: 'jwUnite.blocks.thing_is_number', + default: '[TEXT1] is number?', + description: 'Checks if something is a number!' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + arguments: { + TEXT1: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.thing_is_number_whatToCheck', + default: '10', + description: 'What to check.' + }) + } + } + }, + { + opcode: 'if_return_else_return', + text: formatMessage({ + id: 'jwUnite.blocks.if_return_else_return', + default: 'if [boolean] is true [TEXT1] is false [TEXT2]', + description: 'Returns a value based on wether or not the boolean is true or false' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + boolean: { + type: ArgumentType.BOOLEAN + }, + TEXT1: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.if_return_else_return_ifValue', + default: 'foo', + description: 'What to return if the boolean is true.' + }) + }, + TEXT2: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.if_return_else_return_elseValue', + default: 'bar', + description: 'What to return if the boolean is false.' + }) + } + } + }, + { + opcode: 'indexOfTextInText', + text: formatMessage({ + id: 'jwUnite.blocks.indexOfTextInText', + default: 'index of [TEXT1] in [TEXT2]', + description: 'Finds the position of some text in another piece of text.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + TEXT1: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.indexof_textToFind', + default: 'world', + description: 'The text to look for.' + }) + }, + TEXT2: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.indexof_textToSearch', + default: 'Hello world!', + description: 'The text to search in.' + }) + } + } + }, + { + opcode: 'regextest', + text: formatMessage({ + id: 'jwUnite.blocks.regextest', + default: 'test [text] with regex [reg]', + description: 'tests a string to see if its valid for this regex' + }), + disableMonitor: true, + blockType: BlockType.BOOLEAN, + arguments: { + text: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.regextest_text', + default: 'foo bar', + description: 'the text to test' + }) + }, + reg: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.regextest_regex', + default: '/foo/g', + description: 'the regex to test the text with' + }) + } + } + }, + { + opcode: 'regexmatch', + text: formatMessage({ + id: 'jwUnite.blocks.regexmatch', + default: 'match [text] with regex [reg]', + description: 'gets all regex matxhes on a string' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + text: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.regexmatch_text', + default: 'foo bar', + description: 'the text to test' + }) + }, + reg: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.regexmatch_regex', + default: '/foo/g', + description: 'the regex to test the text with' + }) + } + } + }, + { + opcode: 'replaceAll', + text: formatMessage({ + id: 'jwUnite.blocks.replaceAll', + default: 'in [text] replace all [term] with [res]', + description: 'replaces all of somthing with something in a string' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + text: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.replaceAll_text', + default: 'foo bar', + description: 'the text to test' + }) + }, + term: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.replaceAll_replacy', + default: 'foo', + description: 'what text to replace' + }) + }, + res: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.replaceAll_replacer', + default: 'bar', + description: 'the text to replace with' + }) + } + } + }, + { + opcode: 'getLettersFromIndexToIndexInText', + text: formatMessage({ + id: 'jwUnite.blocks.getLettersFromIndexToIndexInText', + default: 'letters from [INDEX1] to [INDEX2] in [TEXT]', + description: 'Gets a part of text using the indexes specified.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + INDEX1: { + type: ArgumentType.NUMBER, + defaultValue: 2 + }, + INDEX2: { + type: ArgumentType.NUMBER, + defaultValue: 3 + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.getLettersFromIndexToIndexInText_text', + default: 'Hello!', + description: 'The text to get a substring from.' + }) + } + } + }, + { + opcode: 'readLineInMultilineText', + text: formatMessage({ + id: 'jwUnite.blocks.readLineInMultilineText', + default: 'read line [LINE] in [TEXT]', + description: 'Reads a certain line in text with multiple lines.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + LINE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'jwUnite.readLineInMultilineText_text', + default: 'Text with multiple lines here', + description: 'The text to read lines from.' + }) + } + } + }, + { + opcode: 'newLine', + text: formatMessage({ + id: 'jwUnite.blocks.newLine', + default: 'newline', + description: 'Represents a new line character.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER + }, + { + opcode: 'stringify', + text: formatMessage({ + id: 'jwUnite.blocks.stringify', + default: '[ONE] as string', + description: 'Represents a new line character.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + ONE: { + type: ArgumentType.STRING, + defaultValue: "foo" + } + }}, + { + opcode: 'lerpFunc', + text: formatMessage({ + id: 'jwUnite.blocks.lerpFunc', + default: 'interpolate [ONE] to [TWO] by [AMOUNT]', + description: 'Linearly interpolates the first number to the second by the amount.' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + ONE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + TWO: { + type: ArgumentType.NUMBER, + defaultValue: 3 + }, + AMOUNT: { + type: ArgumentType.NUMBER, + defaultValue: 0.5 + } + } + }, + { + opcode: 'advMath', + text: formatMessage({ + id: 'jwUnite.blocks.advMath', + default: '[ONE] [OPTION] [TWO]', + description: 'Operators advanced math function but with 2 variables' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + ONE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + }, + OPTION: { + type: ArgumentType.NUMBER, + defaultValue: "^", + menu: 'advMath' + }, + TWO: { + type: ArgumentType.NUMBER, + defaultValue: 2 + } + } + }, + { + opcode: 'constrainnumber', + text: formatMessage({ + id: 'jwUnite.blocks.constrainnumber', + default: 'constrain [inp] min [min] max [max]', + description: 'Constrains a number to a specified minimum and maximum' + }), + disableMonitor: true, + blockType: BlockType.REPORTER, + arguments: { + inp: { + type: ArgumentType.NUMBER, + defaultValue: 50 + }, + min: { + type: ArgumentType.NUMBER, + defaultValue: 1, + }, + max: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + "---", + { + opcode: 'setReplacer', + text: formatMessage({ + id: 'jwUnite.blocks.setReplacer', + default: 'replacer [REPLACER] to [VALUE]', + description: 'Sets a replacer to a value' + }), + arguments: { + REPLACER: { + type: ArgumentType.STRING, + defaultValue: "foo", + }, + VALUE: { + type: ArgumentType.STRING, + defaultValue: "bar" + }, + }, + disableMonitor: true, + blockType: BlockType.COMMAND, + }, + { + opcode: 'replaceWithReplacers', + text: formatMessage({ + id: 'jwUnite.blocks.replaceWithReplacers', + default: 'replace [STRING] with replacers', + description: 'Replaces all replacer names with their respective value' + }), + + arguments: { + STRING: { + type: ArgumentType.STRING, + defaultValue: "Hello {foo}!" + }, + }, + disableMonitor: true, + blockType: BlockType.REPORTER, + }, + ], + menus: { + advMath: [ + '^', + 'root', + 'log' + ]/* + sprites: { + items: 'getAllSprites', + acceptReporters: true + } + */ + } + }; + } + /* + getAllSprites() { + return this.runtime.targets.map(x => { + return { + text: x.sprite ? x.sprite.name : `Unkown ${x.id}`, + value: x.id + } + }) + */ + + replacers = {} + knownLinks = {} + + whenanything(args, util) { + return Boolean(args.ANYTHING || false) + } + + backToGreenFlag(args, util) { + if (vm) vm.greenFlag() + } + + trueBoolean() {return true} + falseBoolean() {return false} + randomBoolean() {return Boolean(Math.round(Math.random()))} + + indexOfTextInText(args, util) { + const lookfor = String(args.TEXT1); + const searchin = String(args.TEXT2); + let index = 0; + if (searchin.includes(lookfor)) { + index = searchin.indexOf(lookfor) + 1; + } + return index; + } + getLettersFromIndexToIndexInText(args, util) { + const index1 = (Number(args.INDEX1) ? Number(args.INDEX1) : 1) - 1; + const index2 = (Number(args.INDEX2) ? Number(args.INDEX2) : 1) - 1; + const string = String(args.TEXT); + const substring = string.substring(index1, index2); + return substring; + } + readLineInMultilineText(args, util) { + const line = (Number(args.LINE) ? Number(args.LINE) : 1) - 1; + const text = String(args.TEXT); + const readline = text.split("\n")[line] || ""; + return readline; + } + newLine() { return "\n" } + stringify(args, util) {return args.ONE} + + lerpFunc(args, util) { + const one = isNaN(Number(args.ONE)) ? 0 : Number(args.ONE); + const two = isNaN(Number(args.TWO)) ? 0 : Number(args.TWO); + const amount = isNaN(Number(args.AMOUNT)) ? 0 : Number(args.AMOUNT); + let lerped = one; + lerped += ((two - one) / (amount / (amount * amount))); + return lerped; + } + advMath(args, util) { + const one = isNaN(Number(args.ONE)) ? 0 : Number(args.ONE) + const two = isNaN(Number(args.TWO)) ? 0 : Number(args.TWO) + const operator = String(args.OPTION) + switch(operator) { + case "^": return one ** two + case "root": return one ** 1/two + case "log": return Math.log(two) / Math.log(one) + default: return 0 + } + } + + setReplacer(args, util) { + this.replacers["{" + String(args.REPLACER) + "}"] = String(args.VALUE || "") + } + replaceWithReplacers(args, util) { + let string = String(args.STRING || "") + for (const replacer of Object.keys(this.replacers)) { + string = string.replaceAll(replacer, this.replacers[replacer]) + } + return string + } + + thing_is_number(args, util) { + // i hate js + // i also hate regex + // so im gonna do this the lazy way + // no. String(Number(value)) === value does infact do the job X) + // also what was originaly here was inificiant as hell + return String(Number(args.TEXT1)) == args.TEXT1 && !isNaN(Number(args.TEXT1)) + } + thing_is_text(args, util) { + // WHY IS NAN NOT EQUAL TO ITSELF + // HOW IS NAN A NUMBER + // because nan is how numbers say the value put into me is not a number + return isNaN(Number(args.TEXT1)) + } + + if_return_else_return(args) { + return args.boolean ? args.TEXT1 : args.TEXT2 + } + mobile(args, util) { + return navigator.userAgent.includes("Mobile") || window.matchMedia("(max-width: 767px)").matches + } + getspritewithattrib(args, util) { + // strip out usless data + const sprites = util.runtime.targets.map(x => { + return { + id: x.id, + name: x.sprite ? x.sprite.name : "Unkown", + variables: Object.values(x.variables).reduce((obj, value) => { + if (!value.name) return obj + obj[value.name] = String(value.value) + return obj + }, {}) + } + }) + // get the target with variable x set to y + let res = "No sprites found" + for ( + // define the index and the sprite + let idx = 1, sprite = sprites[0]; + // standard for loop thing + idx < sprites.length; + // set sprite to a new item + sprite = sprites[idx++] + ) { + if (sprite.variables[args.var] == args.val) { + res = `{"id": "${sprite.id}", "name": "${sprite.name}"}` + break + } + } + + return res + } + + constrainnumber(args) { + return Math.min(Math.max(args.min, args.inp), args.max) + } + + regextest(args) { + if (!validateRegex(args.reg)) return false + const regex = new RegExp(args.reg) + return regex.test(args.text) + } + regexmatch(args) { + if (!validateRegex(args.reg)) return "[]" + const regex = new RegExp(args.reg) + const matches = args.text.match(regex) + return JSON.stringify(matches ? matches : []) + } + replaceAll(args) { + return args.text.replaceAll(args.term, args.res) + } +} + +module.exports = jwUnite; diff --git a/local-scratch-vm/src/extensions/jw_xml/index.js b/local-scratch-vm/src/extensions/jw_xml/index.js new file mode 100644 index 0000000000000000000000000000000000000000..be8a00d900cd50d55a13c7f8e1c1c9c9e509ed27 --- /dev/null +++ b/local-scratch-vm/src/extensions/jw_xml/index.js @@ -0,0 +1,176 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); + +class Extension { + getInfo() { + return { + id: "jwXml", + name: "XML", + color1: "#ffbb3d", + color2: "#cc9837", + blocks: [ + { + opcode: 'createNewXML', + text: "generate xml [ROOT] with:", + arguments: { + ROOT: { + type: ArgumentType.STRING, + defaultValue: "root" + } + }, + blockType: BlockType.CONDITIONAL + }, + { + opcode: 'addText', + text: "add text [TEXT]", + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "foo" + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'addChild', + text: "add child [CHILD]", + arguments: { + CHILD: {} + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'addAttribute', + text: "add attribute [ATT] as [TEXT]", + arguments: { + ATT: { + type: ArgumentType.STRING, + defaultValue: "foo" + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: "bar" + } + }, + blockType: BlockType.COMMAND + }, + { + opcode: 'generated', + text: "xml generated", + blockType: BlockType.REPORTER + }, + { + opcode: 'clear', + text: "clear (ADVANCED)", + blockType: BlockType.COMMAND + }, + "---", + { + opcode: 'getChild', + text: "get child [NUM] from [XML]", + arguments: { + NUM: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'getNamed', + text: "get element [STR] from [XML]", + arguments: { + STR: { + type: ArgumentType.STRING, + defaultValue: "element" + } + }, + blockType: BlockType.REPORTER + }, + { + opcode: 'getAttr', + text: "get attribute [ATT] from [XML]", + arguments: { + ATT: { + type: ArgumentType.STRING, + defaultValue: "attribute" + } + }, + blockType: BlockType.REPORTER + } + ] + }; + } + + xmlsInGeneration = [] + + _XMLToString(xml) { + return xml.outerHTML + } + + _StringToXML(str) { + var div = document.createElement('div'); + div.innerHTML = str.trim(); + return div.firstChild; + } + + createNewXML({ROOT}, util) { + this.xmlsInGeneration.unshift(document.createElement(ROOT)) + util.startBranch(1, false) + } + + addText({TEXT}) { + this.xmlsInGeneration[0].append(TEXT) + } + + addChild({CHILD}) { + CHILD = this._StringToXML(CHILD) + this.xmlsInGeneration[0].appendChild(CHILD) + } + + addAttribute({ATT, TEXT}) { + this.xmlsInGeneration[0].setAttribute(ATT, TEXT) + } + + generated() { + try { + return this._XMLToString(this.xmlsInGeneration[0]) + } catch { + return "" + } + } + + clear() { + this.xmlsInGeneration.shift() + } + + getChild({NUM, XML}) { + try { + NUM -= 1 + XML = this._StringToXML(XML) + return ((typeof XML.childNodes[NUM]) !== 'string' ? this._XMLToString(XML.childNodes[NUM]) : XML.childNodes[NUM]) || "" + } catch { + return "" + } + } + + getNamed({STR, XML}) { + try { + XML = this._StringToXML(XML) + return this._XMLToString(Array.from(XML.children).find((el) => el.localName == "".toLowerCase())) || "" + } catch { + return "" + } + } + + getAttr({ATT, XML}) { + try { + XML = this._StringToXML(XML) + return XML.getAttribute(ATT) || "" + } catch { + return "" + } + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/lily_tempVars2/index.js b/local-scratch-vm/src/extensions/lily_tempVars2/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0e19921c38623fb3fbe8265c54c3d4ede06a4802 --- /dev/null +++ b/local-scratch-vm/src/extensions/lily_tempVars2/index.js @@ -0,0 +1,305 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +// Object.create(null) prevents "variable [toString]" from returning a function +let runtimeVariables = Object.create(null); + +// Credit to skyhigh173 for the idea of this +const label = (name, hidden) => ({ + blockType: BlockType.LABEL, + text: name, + hideFromPalette: hidden +}); + +function resetRuntimeVariables() { + runtimeVariables = Object.create(null); +} + +/** + * Class + * @constructor + */ +class lmsTempVars2 { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: "lmsTempVars2", + name: "Temporary Variables", + color1: "#FF791A", + color2: "#E15D00", + blocks: [ + label("Thread Variables", false), + + { + opcode: "setThreadVariable", + blockType: BlockType.COMMAND, + text: "set thread var [VAR] to [STRING]", + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + }, + STRING: { + type: ArgumentType.STRING, + defaultValue: "0" + } + } + }, + { + opcode: "changeThreadVariable", + blockType: BlockType.COMMAND, + text: "change thread var [VAR] by [NUM]", + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + }, + NUM: { + type: ArgumentType.NUMBER, + defaultValue: "1" + } + } + }, + + "---", + + { + opcode: "getThreadVariable", + blockType: BlockType.REPORTER, + text: "thread var [VAR]", + disableMonitor: true, + allowDropAnywhere: true, + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + } + } + }, + { + opcode: "threadVariableExists", + blockType: BlockType.BOOLEAN, + text: "thread var [VAR] exists?", + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + } + } + }, + + "---", + + { + opcode: "forEachThreadVariable", + blockType: BlockType.LOOP, + text: "for [VAR] in [NUM]", + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "thread variable" + }, + NUM: { + type: ArgumentType.NUMBER, + defaultValue: "10" + } + } + }, + { + opcode: "listThreadVariables", + blockType: BlockType.REPORTER, + text: "active thread variables", + disableMonitor: true + }, + + "---", + + label("Runtime Variables", false), + + { + opcode: "setRuntimeVariable", + blockType: BlockType.COMMAND, + text: "set runtime var [VAR] to [STRING]", + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + }, + STRING: { + type: ArgumentType.STRING, + defaultValue: "0" + } + } + }, + { + opcode: "changeRuntimeVariable", + blockType: BlockType.COMMAND, + text: "change runtime var [VAR] by [NUM]", + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + }, + NUM: { + type: ArgumentType.STRING, + defaultValue: "1" + } + } + }, + + "---", + + { + opcode: "getRuntimeVariable", + blockType: BlockType.REPORTER, + text: "runtime var [VAR]", + disableMonitor: true, + allowDropAnywhere: true, + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + } + } + }, + { + opcode: "runtimeVariableExists", + blockType: BlockType.BOOLEAN, + text: "runtime var [VAR] exists?", + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + } + } + }, + + "---", + + { + opcode: "deleteRuntimeVariable", + blockType: BlockType.COMMAND, + text: "delete runtime var [VAR]", + arguments: { + VAR: { + type: ArgumentType.STRING, + defaultValue: "variable" + } + } + }, + { + opcode: "deleteAllRuntimeVariables", + blockType: BlockType.COMMAND, + text: "delete all runtime variables" + }, + { + opcode: "listRuntimeVariables", + blockType: BlockType.REPORTER, + text: "active runtime variables" + } + ] + }; + } + + /* THREAD VARIABLES */ + + setThreadVariable(args, util) { + const thread = util.thread; + if (!thread.variables) thread.variables = Object.create(null); + const vars = thread.variables; + vars[args.VAR] = args.STRING; + } + + changeThreadVariable(args, util) { + const thread = util.thread; + if (!thread.variables) thread.variables = Object.create(null); + const vars = thread.variables; + const prev = Cast.toNumber(vars[args.VAR]); + const next = Cast.toNumber(args.NUM); + vars[args.VAR] = prev + next; + } + + getThreadVariable(args, util) { + const thread = util.thread; + if (!thread.variables) thread.variables = Object.create(null); + const vars = thread.variables; + const varValue = vars[args.VAR]; + if (typeof varValue === "undefined") return ""; + return varValue; + } + + threadVariableExists(args, util) { + const thread = util.thread; + if (!thread.variables) thread.variables = Object.create(null); + const vars = thread.variables; + const varValue = vars[args.VAR]; + return !(typeof varValue === "undefined"); + } + + forEachThreadVariable(args, util) { + const thread = util.thread; + if (!thread.variables) thread.variables = Object.create(null); + const vars = thread.variables; + if (typeof util.stackFrame.index === "undefined") { + util.stackFrame.index = 0; + } + if (util.stackFrame.index < Number(args.NUM)) { + util.stackFrame.index++; + vars[args.VAR] = util.stackFrame.index; + return true; + } + } + + listThreadVariables(args, util) { + const thread = util.thread; + if (!thread.variables) thread.variables = Object.create(null); + const vars = thread.variables; + return Object.keys(vars).join(","); + } + + /* RUNTIME VARIABLES */ + + setRuntimeVariable(args) { + runtimeVariables[args.VAR] = args.STRING; + } + + changeRuntimeVariable(args) { + const prev = Cast.toNumber(runtimeVariables[args.VAR]); + const next = Cast.toNumber(args.NUM); + runtimeVariables[args.VAR] = prev + next; + } + + getRuntimeVariable(args) { + if (!(args.VAR in runtimeVariables)) return ""; + return runtimeVariables[args.VAR]; + } + + runtimeVariableExists(args) { + return args.VAR in runtimeVariables; + } + + listRuntimeVariables(args, util) { + return Object.keys(this.runtime.variables).join(","); + } + + deleteRuntimeVariable(args) { + Reflect.deleteProperty(runtimeVariables, args.VAR); + } + + deleteAllRuntimeVariables() { + runtimeVariables = Object.create(null); + } +} + +module.exports = lmsTempVars2; diff --git a/local-scratch-vm/src/extensions/lmsutilsblocks/index.js b/local-scratch-vm/src/extensions/lmsutilsblocks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0d3bcf5ec0dc4622a79ea8f1cb4f69b8a053943d --- /dev/null +++ b/local-scratch-vm/src/extensions/lmsutilsblocks/index.js @@ -0,0 +1,1491 @@ +// Created by LilyMakesThings +// https://github.com/LilyMakesThings/ +// +// 99% of the code here was not created by a PenguinMod developer! +// Look above for proper crediting :) + +const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAV4AAAFeCAYAAADNK3caAAAABmJLR0QA/wD/AP+gvaeTAAAv2ElEQVR42u2dB5hUVdKGe3LOuSeCYEJRQWVd+REQQQUDigkMqIioKOquomDAjBgQsyCiIIqomLNgACWNpLG7B0YEYc2JYABB6q+60+AAEzrd0/fc+9XzvA8rq9N9z9T5+nSdCi4XDKbQ2tRRUmUttSrz0KHMMcyZZV4aVuqlm5mHyr30HP/dLP67aj/L+Z9X8p9fMr/4+Y0hP781+Psv/f/u8h3/vY9m8s+dJj9bXkNey3jN+tc+VN6LvCf8ZmAwmLbWsZoSKlZQ6woPdS710CksdMNZ4B5jQX3PL4pbG4imlRDhri730XT+czQL9mC3j46r8FFHdzWl4jcLg8EsYeW15HZ7qYdxivTSZP8pc7NFhTUctvo/NF7jP0fJBwp/sLRzEcXCC2AwmDlGFCNCw8IziL+uP1zuoTn8v9fZUGCDZR2vx2xZE1kbXqN9Za3gMDAYLLRwAX/FlpOs/6v3jxDZgNngD6uMMr4NrKUUeBQMBtvNRBxYKI5mwbiXhWMe8xcENGLIWs7l8MTd/CHWC0IMgznY5AJMLpD8J9oNEEhl/GGciPnSUb5VwBNhMJufajkW2Zs3/IPMFxBAy1DH3zYe4D+PxWkYBrOBSX6qpET5Mw7WQ+SsfxqWzAn+gDwbKWwwmE5GFCc5tJI7i8wDvTMm5ANTPjjbeSgRjg2DWU9sY/jy5shSHz3hLwSAcNmLn5mJpbXUDelqMFiUrcpDxUZlGGK2TosJj3LXUDl2AAym7nQbK/mh/mwEpH05l62SHSFVdK4PKB4bAwYzwaQ8l9O/buAN9xVEB+zCavGNSi+VYKfAYBGwslpq778o+xMCA1pgs5HB4qP9sXNgsBDMyEyQpixe2gZBAcEi/TQkIwKXcTBYCyZpQ0YrRR8tgHiACLFUKhSrVlEydhgM1sCk0IHDCZfxJvkaQgFMwUNrWYCHouk7zPEmXcCkSsk/XQHiAFSwRrrOQYBhzjNOCfNPZ6iDEIAoZkIMRioazEmCuxwbH1iELw0B5lJzbFCY7cw/IqcGGx1Y9RKODwXdsVNhtjC+1GjjrzLD5gY68Fp5De2BnQvT0oqWUprU1LMjb8JmBtpNzfDROL6Ay8ROhukTx+VMBeY7bGCgOTJrbxjivzBLG/dTOJgddRE2LLAZC/mOogN2OMxacVwZFuml0dIxCpsU2JQtEn4o8FA6djws+qdcL3VBehhwUAXcygovHYWdD4uKVS2mbH/XMDSxAc5rwsOZOjzxJA9KAFN5yj0Nl2cA0LdGI3YYzEyT9Bp2tinYcADsfPqtWEY5UAhYxI2/Vh0m8S1sNAAa5Su574BSwCJj3ETEXwiBjAUAmudvyXzAOHpYWFZZS63YmT7BhgIgCOob+beFgsCCNnac85nfsJEACImNHPsdCCWBBXqBlsROMwEbB4CIMFkKjKAssCbNXUPl7CjzsVkAiCiLJGwHhYHtHlrwUVfme2wSAEzhJw499ITSwOqNx1+zUwxH1gIAprNVMoSkgx+Ex9nxXCmIeAkbAgCl/R5eyK+lDCiQE0MLy6mUnWAxNgIAUaGmxEuVUCIniW4ttedP3bVwfgCiyjcVPuoIRXKAcYC/F//CN8DpAbAEv7l9dByUyd6ZC4ONhs5wdgCsdenmo6FQKDtmLtT3W4CTA2DdUuNxyHiwT+aCVKLNgGMDoIX4Pi97FsqlsbmrKZV/me/AoQHQKt1sFma7aWryi+NPz5lwZAC05GPJs4eSaWTSDZ9/cXPhvABoTTXmumliRUupkE+6S+C0ANgCT6WXSqBsFjb5BfEv6nM4KwC2orZ0KZVB4Sxo8ovBTDQAbHvhtlLK/KF0FrLiOirgceteOCgAtmZFlYeKoXgWsKrFlM3TTT+DUwLgCJbx6TcXyhfd4ohM/2A9OCQAzmEe2kpGtzjiQzghAM6j3ENzOIMpDUqo0Np5KJEX/004IADOhe913kV5sSrjJhrovQAAME6+PpqOxjoKjC/SxsLhAAANGANlNNH8/XThaACAXbuaoZ+vKaLrpWPL0MQcANA4W/nC7XgoZSQzGLzUgRd2I5wLANAMG90eOgiKGYmTrkwDxmBKAEBgfMPiWwHlDLdAgsdAw5kAAEGwFAUWoaeNyZy0F+BEAIAQeFk0BEIapJV6aAScBwAQBldDSYMT3SPllhKOAwAIg7+5wKIXFDWQDAYOjPOC/QinAQBEgJ8ra6kVlLUZq1pFybxQC+EsAIAIsqhsLaVAYZsukpgIJwEAmMB4KGzjons+nAMAYGI3s7OhtA0b39TQHrwwG+AcAAAzK9uYtlBcsQ8onhdjHpwCAKCgmc6CjtWUgNOuj26DQwAAlOGhUY4W3QoPdY5ovm7NVip+u47yx79NOTeNp6wrR1PmRddT5rBbKfu6Byl/wjvknvszHA8Ah+f38sn3CEeKbuuVlMULsDqsYPnSzVTwxPuUcd5VlNjhcIpJSSMpNm6W2FhK6tSd8sa9CAcEwLmsqVhGOU7MYngm1EUrfnM5pQ8YSrFZuS0LbTMkH96T3PN+gRMC4EymOK0k+NRQFso9fx2lnXIBueLiwxLchiTsub/xc+GEADjysu1kZ5x0PZTL+XTfBX3Kfd1Hce7KiAluQ1L7DIADAuBMvnVEyIGHVT4Z7OKUfPQNxeUXmyK6BjExxqUcnBAARzLB3iGGWurGD7kt2IVJOeok80TXT+aQ6+CAADiTbTxerIc9u45VUyo/4BdBn3bfX21kIpgtvBLGgAMC4FhW2LKRDl+o3RPKgmTf8LDporsdhBsAcHRhxR32iut+ToeEWiiR0vNkZcKbe8dTcD4AnMsWmWhuD9Ulii2TfpghLkbCvh2UCW/aqRfC+QBwNvNFs/RPH/PRBeEsRHz5HsqEN2Gv9nA8AJzePtJH52gtujJiWfLkwlmExAMPUya8UphRumQTnA8AJwsv1xm0qaNMnU+7d4W7CKl9+qsTXqZoxmI4HwAOR7om6tzcfFO4C5B54Uilwps7ejIcDwCwibMc2ugnvB56NRILkHPL40qFVzqdwekAACy8L+iWs9s9Ug9fMOVjpcKb3LkXHA4AUN+YS5uKNqI4fsM1EXvw+b8avRRUCW9cQQkcDgCwncVapJfx8fzMSD98XEmF0lOve873cDgAQH2WA7ex1eG0WxvpB0/u2kep8BZMmgmHAwBsZ7kM5bXyhdp5Zjx4xuARSoU3+5qxcDYAQMOLtjMtKboyMpnf3EozHjr37meVCm/aSefB0QAADamz5KmX4yBDzHrootc8SoU3cb+D4WgAgJ2LKvgbvaVEl8vrkviNrTHtoWu2UExSsjLhjUlOMUbFw9kAAA1Y3c5DiVbKZLjM7IdO2OcgpademfEGRwMA7JLhMMQSoiufAPyG/mf2A6eeeI5S4c0bOx2OBgDYla/kPiv6mQw+GqjigbOuulup8GZedD2cDACwOz7qb4UOZEtUPGz+4+8qFd6U7ifAwQAAjQnvEq5ZiInmabeXqoctmf2dUuGNL2sFBwMANBXr7R69FDIvvavyYWNzC9WJL/eHcC9YDycDADTGG9EKMezPL75N5cMmHdZD6am3cOoncDAAQGNsK6ul9urDDF56SvXDZgy8Uqnw5tz4CBwMANAUE9WKbi25+UU3q37QnNsmKRXe9DMuhnMBAJpiU6WXSlSGGW6MxoMWvfCZUuFN6tAZzgUAaLqM2EvXqWr9GCulc1G5SeQJwDIJWJXwxmZkcVeibXAwAEDjmuSlVUoapXN58DHRfND41vsoPfWWvL8aDgYAaJIKLx1lvvB6aUY0HzL1mNOUCm/+Q6/CuQAATYcbfDTdVNGt8lAxv9Bf0XzIzGG3qi0d5teDcwEAmmFz0VIqNK9gwkfXRvsh5QSqUnjlhA3HAgA0e+r10H/NulSL4Reoi/YDSsxVaekwx5ThWACAFlhuSv8Grk0+0hIPyFkGsRnZ6sQ3Lo5KF/0BxwIAtJRa1sUM4Z1klQeU/FqVp17JH4ZjAQCaPxTSY5EfZOmln63ygFJRprR0mCvm4FgAgBb4MaJN0vlSrY+VHlB6KKgUXukRAacCAASQWtYrkrm7U6z0cNI1TGnpMHdFg1MBAJQ1zvFPEF5nqTK9hRuMfrnKSodzC+BQAIBA+DUik4j5pu5EKz6gTIhQWjrMEzDgVE3w+d9U9PIyyh0zlWfj3UVZV46mzKE3UfaIccbfFT43n9xzf8Y6Aaf0b+gdiTDDM1Z8OJmJprR0mGe+wal2xv3pT5Rx/tUBTwaJr9qT0voNotzRk6nk42+xhsCuaWVPhSe6aymFf9BGKz6cTAFWKbwy5RhO9Q8FT35ghGBCXc+Y+ARK6XWK8XOwnsBmrJMQrW2yGRqSN3a62tLhE8+BQ/kpmrGYYpJTIra2ie07GT8TawuQ3VAfZnjQqg9W/EatUuFN2OcgOJTEr7iKL2GPfU2oEIynjEHDqaxmK9YZ2CHcMDYc4f3Csg/HGzQmOVWZ8MYkJvFrbnG8Q2Vedoup65zW91xsXGAHakMNM+xp9YdL3O9gtaXDr3mcfdpd/GfAF2lhVQrePAEbF2hPxQpqHcppd5jVHyztpPOUCm/u3c86+0LtifeVjVySjAlsXqB5uOGiUAZavmX1B8u+Zqza0uHBI5wdZrj0ZnUTns+5ApsX6C28Hno1lDSy3y1/Aps0U6nwJnft42hHSu7cS11MPSGRit/5AhsY6MxvQaWVRXugZTAJ/CqFN66kwtHVaRICUJrC12cANi/Q+16E+5gH03v3Hl0eLK7QrVQM3PN/dWbuLl8sqlxng9hYKnpxETYw0JnRwcR3F+Drb+MUTPnYkQ4kPReUC690hju0KzYv0DnOOyeY+O5mXR4s47yrlApB9nUPOtKBMgZfGxXhNfpkTHjHfmvKI6xKPvrGqNgrfL7aoOSD/6GAxH5sqlpFyYGcdrtqdRLjhisqRSDt1AudebHGF4vREt6EvQ80Yszax/uWbqbcO6dQcpdjm54byBV8ccXllNjhcEo7bQjl3DqRit+ug4Dp3Eyqlg4PJL47QqvY40tLlIpA4oGHOdJ55GIxWsJr5FCzYOm8ftIaM77VXqFPu65oY6QzFr+1AmKmH1cHUjjxhm6nCOl0pSzNKS3D+Jqo+jlLZq2l/PFvU9Z/x1DayedT0iFHUHx5a4rNzDFOT3H5xUYln8yjK5j8UWQ/sflCUWXj+UaFp7SKSpds0nLj5T3wUuTK2/nCMfWY05Bqp5NG+eiV5lWX58KXWWioZaAktN1PqQgUv7PSvK8l834xLvBkrlz66RdRUsf/M8Q16Eupfx1ZHzOMRL40v59oiu6O1pxX36PdpjOKfFgsI34ASE2n3DuegrDpwY+irU3qboWH2un4YKl9+isVADnBRKLvgYyNl80jF4SSnRFXVBbZUyJP6YjE5Ay5ULSC8MZm5eqTzseXZOkDLjV3TfhbiHxAQ9g0iPMup72bu1i7QMeHklEzKgVAxtoEswGlhWXefc9T5iU3UkrPk41JDK64OCXvNaVnv/B7YvCFohWE1yjbltaRVv9q+dlvlNLteGUVfnLPAXGzPOc3N1/tER0fKv/RN5Ru/qbEzAgTPD2bsq+9j8VqsHEzrbJ1ZVOnIhH+sLrA8YWiVYRXGrCXzFpjWV+UkUaqu+bJJA8Im9XTB+mBpoWXk311fCi5eFJ60cM3zHKBlX39Q0bKT1KHzk2nB1mA9DMvCyvfNDY901LPk9Z3oGWr++QSUPmHUVKy0aAeAmdpPmruYm29rg8m8T+rCl/UY6P8oRBqE3e5PbfcM0kpscVGBUnDpmh++MrrQ9wsza+NXrBV1lIrnR9M0qsgsk1TOH1haKlQD75syeeRIgTLFPHc/qQRa43memQPvxfiZvULthoqbyzMcILOD2X6DbLmZF11V2g9ePki0arPJI3Zo132Kxem0c5xNi58LxwJcbP6pauXejd2sXa9zg8l42IgsM1cwHQ/IaR1lYtEqz5T4n6HRKWYZXvhTuoJZ1tmLTIvuh7iZnV8dM3uwuuj6VqXZE6bB4Ftrt8BF5mEsq7hlLkqKSW+Z5r6r4ycS5zUqZul1iF75AMQNutnNkxtrFS4VutjPOdOmlEhZKsLtmDXlG/KVeUbh1MkorKUuOTDrylhrwMstw75D70KYbM+NTuJbjsPJfJfbtX9weIr20Jkm6G0emNw3yKmL9DiubJH3q8mXeyVGqNzmBXXoPjN5RA26/OX6wOKbzjqp40dHizlqJMgsBGcniEtCbU4zecWkHvBenPTxSa+Z7l85obVa6XL/oKw6VBz4KXKHcLr9lIPOzyUccMMgW36xBvk5kw/a5g2z5Y55DpTez6r7IAXdPx+r/YQNX0u2Lpq36Nht5zTcS9CYJs5FQWdG92puz7PZ5QSr418dzEu/7ZCulizQ0GPOxOCpssoIB8NbJjRcJsdHko69UNkmzgV7dsh+GpA/gqv0zNKf4yIdhfjtpxa5Gj/506Imj6ZDTc1jPFOtcVD8XgY6VMKoW1kc15+e9C399o9J2dgSL+ESGTIJHc7Tpvnzh//FgRNlxOvl55qmEr2iV0eLPGAf0FoG5ne4J6/LriObzztQstCkSNPDC9H99OfKPGgf2v1zPIhCVHTho8bCu83dnmwtFMugNg2IK6ghPu1Lg2+xzGPGNL1maU1Z2gNgVbW90rWKT+bm0NBzLQKNaw1RFdGD/NfbLPLg+XcNB6Cu/30x+l1oY7/ST3+LG2fO7F9p6BLiWUYZWxuoXbPKhV0EDSt+LtNHSW5KlZQazs9WNHrXmdnL3Bv1pSjT6XCZz4Nb46dBauzghrPdP+MoDqwRb1hfTR6LYOoUOWjKrlYO9ReR3lu3J2T7xyh5fzSxHYdKb3/JZQ3djqVLtwQgRv9LRSTmKR3XJtDBoHkLefc8jhfysVr+5zSHApiphcVPuoownuM3R5MOnHZNV6bdFgP45QjQzILn682hmaaURprh/XKGfVoyy0dde+zzCESiJl2ubw9Xdwj8iy7PZhxitG5oQ2Pc5d5bZKXKgn8clkUkZNsoJVaY6baozEQx2wbWzejpSMXHWj/jNwUSlLfIGbaVa/1F+G93Had3rlu36p19TuFCdIyjPQ3ycTIHjHOGN/invND1Ncv44JrbPMtIfPSm3cWXW4UlNz5aHukCXJTKAiZlpkNl4rw3mLHh7PS10gp15V6+tQ+/Snrijso/+HXqPjdL6PWxLslko/obZ8YOBfUlMz+7p+WjnsfaJ+slZ4nQ8T0FN5R2o50b7H6iHu0SvNvu8ZhzcSqrQ9DvvkfMJSKXv2c4koqbPVcmZeMgojpyYPaT55oNiH+rRWmiUhcfnG9wJ5zhRFTlt61doi3SetIqzeFCSXzI5oTgE1LmeOmUBAxLWevPStZDbNs3f+SCwhSevQN/YKGY8VSQmpcdPF4lYJJs4yyUruuV8GUj5V9M0CRS5jNz/lgASHTsl/De9IScoETHrbopSXG6VQGJDaWoyo34FLxJLfdWVeOpvxHXqfi91Y5zinkw0XV12SIZxin+JQ0oykUhExL5orwLnHkcZ/TjKR/q3veL+je37DXBZ/sleSfcmhG9+q4qJdFw191ZZGEGnxYCKC6u5t7zveUfc1YiGiovYc5BRH+qi2fi/CuxEKA7dVcklusop+EvJZ821DxenZE8r7hs9pSJ8K7FgsB6tsifqGsj8KOYo3zroKQhtL68qkP4bP68pX04v0BCwGMLl0PvKRENJL/fdQ/WSdc3CDz0iCmQYZqbJxZ44B0su9EeNdhMYBR7Tf0JjXxyX6Ddr7QO20IxDSYVLyiMvir3vwiwvsHFgIYXd169otK/wQpn9a5NaNqkrscC3/Vm99FeLdiIYAQ32ovJcKRe/uTu0+8sEO3MEVknH81/FVvtkJ4QX3cadEfxpReJRdDT36w+8Uen3qlmRCENYAPrjFPw2dtILwINQAuaFiortSVh0o2WryhaaxXOp6p/NAIZXgpsF6oAZdrgHJunaiueXcTlYLStlG3DIe4olLKf/QNpS1GpZE7fFb3yzUffY+FAOlnX26JG3mJX+o0Wr3o5WVGzFrZ6XrP/eGvtkgnQwEFYJI6dVfTY4A7vTXblpKr2WT0kQ7jmSQ8Y3xo8aBRVa8rl5DwVzsUUKBkGDCxuQVqhKP3GS3nE3O6mbVnuRXsFGeVhjWqXjvrP3fCX+1QMszHXi8WwtmUfPSNulQonufW4lcxnosWl1dk2QGaEl7Y8V45Xq0yLp0//i34rC2a5Di0LST4h/zxb6tr7nLDw4H1Bebpypa7SOMPg6JXanbu8/ziIqXvQRr7w2ft0BbSIY3QQdNkXXWXuhMbN5gP6AKCZ+bFuSutI7r8XorfXL57NgiPfVJ5mQd/tQVGI/SZWAhnk3r8WepyUHnoZMApbrc9YY0x6q335qb5axrPPT71QmXvI6lTN/irPbIa3pUpw89hMZyNykkQ7gXrA39vNVtZ9PaJ7qSHdh25afsPTb5H+f+VTUvmydXwV1vwjFyuPYyFcDA1WxqdQWdWClbQrSrvez5qopt0yBHNflBIIYOqtRNybp4Af7UDHnpAhPdmLIZzkcsilaW1wTvpNqXpWjs6gHU7zuhf0XyZ9QKl76nwufmW9iX3/F+NcvBdMb4x8O8R+22H8I6SkuFhWAznknv3s8qEI6Xb8aFlXUx4R6nApZ54jvFNoMUY9I2PqHtfUmr92W9R/3Yk07pzbptEGYNHUMrRp1LCvh3qc8BbarDE71/+vYR9DqKUXqcY/33unVOo+I1a54myj4ZKAcWZECDnkjH4WnUxygGXWr6yTqrQAh2bnnby+eou+Crbqr8E4hzlgkkzKePc/1Bih8M5XznVnBBURhal9Ohr9AtxwmSNch+dIcJ7NATIuSR37aOu6orT1kIOibzwmekdwHZt0N7ipSSHTpR9WzjqJGVim/fgy8apNDY9U3mIR4pRJFOk+K0Vtt1zFV46ylX+OR0CAXIucSUVyjZV3tjpYb1X04oq+Gtw9nUPBt2/OCY+QdnaZV5yo7nVizO/MhoUxeUXWyN3mkMXqX0GUNFrHtvtObeXOrgqa6kVBMiZyGWIKyZG3eXQtHlhXkpsi3hDGvn6nHf/jOD7Fz87V6kQ5Y170RzB5fzk9NMvsm4Tev5QTD329GZT+rQr0fdSpatqFSXzP/wNIXIeBZM/UlvuyhOFI1Jpd+XoyMQWs/Oo8Ok5oZ2+R96vdO2K366LbEhh8Z+UOeQ6pelw4ZZrB1r1aHH+blNHSS4x/oevIUTOQ75eK4vdJSVH9PY6965n+FImO/R0scN7UvF7q0Kv9uPMB2Vrl5IW8IVfQB+4k2YZl3XaTfvgb2fBxuEtyBrXduN/+ARC5DxUlruacSsvXdUk9CCiHljcMJ5SjjzRSE8Lu9qv7X7qquf2PzRC+aPbKPOyW4yv7zrPnEs/a5jO++6jf4TXR09DiJyHNCVXVgV2WA8TY9XrjNxS6TmRsMe+xglxe3wwvrTKyNyQ0ICMForI13TOp1U1GFRI6zco/Pe8cIORR22XgZ85N43XM5XMS0/tEF6uXrsVQuTA5ucKJz2knXSefWLjT89WKjLZI8aF/cGUeOBhtpq0LJeiWrbIlKq1BqGGQRAip2U0rFO6UTKH3mSf2Pjwe5WuXcFTH4b+e+ZRSon7HWLLMfdZV9yhY/HEwB3Cy3llPSBGDuvRwAUJKjeJDIS0TRtNzi9VuXYhV3Nxia+qir9okNzlWB3LhY/YIbzlNbQHxMhZ5N33gtpTG9+k22Xt4lvtpXB8fFnI7zN9wFDbiq5xb9Chs37fND1UsUN4O1ZTAv/lFgiSc8i66m61eajvfGGPEA23iVSZFZD8f8eElm7HDWjsLLrhNF2KIptdXCzqamgYegnhNe0iJC3DaGpui4s1PrmrFJeMQcODT7PjSyeVF6fRIuPc/+rmP0tduxoL7zQIkoPaQY6ebItUMvUfWHepjY3zyTXoxkd8Sra76Bpl1A+9opv/TNlNeDm/bCQEyTmUvL/aNg1elF6sHXOaUnFpOEo+oNg9i5ETRFe6mJUu+l03/7l6N+F1++g4CJKzSGjTTs1k4QhUilnmYq18D3Xiwo1rZLxQ4FkMW5X9TqMe3+Um7Brm8B6zm/BKxxyIkcN6NSho9CInk6AGXFr5Yo1zYlV2c0vYc3/Lho+iTcGUj/XzoeVUupvw8m1bDP+fv0KQnEPpkk2mN/M2JjrYpWJt4ntqRxBxvnBQJeAKJx5Hk5Bm90Wfn11NGV+wzYYgOQvp0BVf3tqckS7cdjFS/REscbHGlVJKK7O4/WXA/YGnfuKY027eAy/p6D8fNie8D0GMHHjRxn1yk4/oHfEWfvkPv2ardUrpebJSgcl/7M3AiyW4mbkTRDfxgH/pORzTR/c3Kbzo2eBspNF0REapyyid6x+y3fqovFgzGsfPWhtwu0epcLO98HJbz6IXF+nZlcxD5zUpvCU1tA8ECBS9tNSYPiyjuIO9TErY6wBjsoXt1oX7HqicsRablRv474vFyAmn3cyLb9D3PsVHezYpvP4Ltp8hPqBhFzO5VJLCgbTThhjJ+Yn7HWzEhQURZyndlFzdwmc+tXUsXGkfgkOOCDw75YaHoz4ZIr5qTyNclXbKBcbYe2lgIzH+iJVOd+6lc/Xjj6KtruaM/6XXITgA7FoqPFPtlIUzLwv4vYnYRatJjVTWuef+3GRecc6tEymu0B1eXLfD4UbzeY395yVXS8ZH4mux0QCIbtOZYCYsqG5yLhM+ZOZdwF+zWTQzL7reaF4efEpdfx0r1HaN7/63ReHl0uEu2GgA7CK8Y6YqFbfC6Quteekn2SpBZFvs2rwn6/LbjXuAFmf0cevNvPtn2CNf3keHtSi8ZWspRdqXYbMB0EB475mmtg/Bsr8Cfm+xGVnq2lTydOaIpDDO/Mr4MMsYeCWlHnu68XPlTxk5b0zc0DFlrHE27Rjn3qL4emkeNhsADVLtxr+tLnZ6aNeg3pvKbAsJGcAfguJjV6BW6qG7sWAA7FxeHZueqWa45TVjgxPeEGKnIV/68VQL+ENQjXHuCFh4+V8+GosGwC4tIY8703xx4+KTkllrgnpfKpuexxWUGCmG8IcAP7A91D1g4a1aRcn8H/2OhQOgQS7v23UUk5pubrvDnv2Cr6jjHFqlaWSduoU+gNNZ/BZwfLdBnPdNLBwAu8Z63zJPfOPiqGjG4qDfU3Lno5Xn8EplXeYlo6j4jVr4RdO87ArWONxwGRYOgMZKqpdQfEUbS8xXMxrknHFxVCvX4vKK6rMSTjzHmIMmzyFkXX6b0WGtITmjHqW8e5+j/MffpcLnq6n43S+ptHqjXcMMQ4IXXi+1xSYDoOnLNhGSSF24JXftE9y0iQbk3PaE9n0YJE5tlJ8fdZKRYiZN+iWTROeQRsUKau0Kxfg/rsMmA6AZAV64gYVvktFbIaTpFPzfSA+M0sV/hh57fmuFrZvjxFe2NZrCixgXTptnNCyyvF/w1HZXqMbhhgewuQAIsKEQ9yyQ1pqZF47kC6ju9a0aOW7b6AmPix5kcGbh03Mi8tpx+cWOaYQem5Nv9B8ueHq2dcuEvXRvyMLLqt0bGwqA8NpJSpWWxDMFmRos/xzpyiwZ/ugU4d01wyKY8mplwuujniELrz+tbD02EADWJu++FxwpvDvCNf0GcTOdP6zy+/g16DSy3cINPnoajg2AxWOKfDEXyf63WsaCW+9DRa97rZDNMMkVrnFLs+Ph2ABYH6fMXWs2/puRXX8BF90y4WPCFt52HkrkH/YLHBsAq+cXLzVKjiG+WdGM+/4imumKhPEN3ZNwbAA06CfBjcOdLrzb84LlIjMKv4MJrkgZ/7Bj4dQAaNBPgivBYhISIb4S8+XqQveC9WpTCr3UI2LC27GaEviH/gTHBkCDWO/Zl0N4t48O4o5yCtf+R9cHFO+KpPEPfRxODYAGGQ5cBZewV3sIr59g5sOFWTTxiCvSVlpL3eDUAGgScuAyYrNbWOpCnLvS6K1hem8GD3WOuPDKXHj+4Svg1ADogZz0kOXgn+px7X1mr3etaKTLDONiimvg0ADoQ86ox0Jr3mO3LIfcAmPMvIklwle6zLJWNVTEL/IXHBoAfZAZbhBfjvWOnmzWGm8urqMCl5nGL/IinBkAzXo5jJ1OMSlpjhbe5H8fZdal2nMusw2DMAHQtbKNJ2eUt3au+BqDRNdaO3e3mUu2WH6x1XBkADRMNVv0O2UMHuHYIgsTLtm+FE10qTC+ZLsBTgyAxulmby6ntL4DKSYp2VHCm9Lz5EiHGUa6VFl5LbkloAwHBkBvZGpG1tX3UNLBXSgmPsH+Ob08mDOCTeg3VXmo2KXSpOckHBcAG4UhON2qYOJ7xoRg6W1rV/GV4hLLNcQJomPZfvzC2+CwANi34Y5kQ2RccI0xvl1yYW2RVjZmaiTWZxtXqu3rioZxrPctOCgAzqFk1hrKf+xNyr7hYeNknNr7DErscDgP9izV5sIu6/LbIrEWr7miZZJGAWcEAOyAh3u65/1CJe+vNsbwFD43n/Iff9c4OefcNJ4yL72ZUnqdYsRaoyW8aacODv85fdTVFU3jN7EIDgcACIrP/6aCKR9TSvcT1BdSdO4V7vuvdkXbeAT8WXAkAEDI/SRufESp8CbsdUC4KWSnR114jSbpHloLBwJA46q21zxGT4f0My+j9HOuMP534dRPOHSwVcnrx5fvoU542+4XzntdHfFm52Fcsg2F8wKgXzw29/YnjRE5zXX1kokWErM1870kHdpVnfC2aRdObHewyyomUzU55LAKzgyAJqlib9cZX7kDFqy4eCODoWjGYlPeT+JB/1YnvHvsG/JpN2IThCN46h0MhwbA+kiRRGxGdsiNZtL6DSL3pz9FrnBj6WZjKrAy4d23Q2ixXR8NdFnN/LHelXBsAKxL4fSFERkJFJudZzRZl+yEcN9T7p1T1PZr6NE3lPe5wjKx3d2q2fgTAc4NgFWLH9ZGPH82sX0nKnr187Cq40TEVQqvXB6GENvt77KsEcXxqdcHJwfAYnBjGCn7NUPIpGIt8+IbjMKJYN5T/oR3+PKuUH1ryBHjgl0/j7LWj2HEevvD0QGwFlLia7agyYSL9DMu5obrS5uO5/K037wHXjLtQyAQCp6eHVwM2kP9XJa3+kbpS+HsAFin2Y3qsT9x+cWU2mcAZV11lyH6mUOuM0bvxKRlRLVPg8S3S5f9FUyxxGemTQ+OeDVbLXWDwwNgDVKPPwsj3reXCx/RO9gqtS4unYzf9Aw4PQBRPu2+Ucs5uHEQ3RDiu1ybMM2lm1WsoNb85v+E8wOA064ViElMIvcnPwa6dn9U+ajKpaPxmx8N5wcgOpQu3EAxySkQXT8Scw7itHuzS1fLr6UMfohvsAkAUI80LIfghpTN8L+ipZTm0tnKPXQuNgEA6sm66m4Irp+kTt2DyHmmAS7trT69bCE2AgCKhZdH3EB06/tLBNHcZ6426WMtppd9TgfwA/2FzQCAOvLuex6iK6N++p4b6JptcXvoIJedjB9qDDYDAOpwL1ivzQBK0wo5isoCzmTgC7VbXXazsrWUwg9Xhw0BgMJ0smNPd276WHxC/RSNwNZqedUqSnbZ0WQyJz/gNmwIABQVULyz0shfdaLwZl19T6DrtI37MXR32dn4ISdiQwCg8JLtP3c6TnQzBo8IpsH5oy67W+uVlMUP+zU2BAAKQw59+jtGdIPst/ttxTLKcTnBpM0aNgMACqvYFv1ByV372Ft0Y2Ioc9itwa5NX5eTjJOUp2JDAKAQHtWe1negPS/SkpIpd8zUYDuPPeVymknIAZOJAVAMz0iT+KecDu0iuvGt9+bm60uCXYsv29RRpsuJ5q6lw3kBtmJDAKCW3HumcQOdVFsUR5R+9luwz7+l1EeHuZxsfOq9BRsBAPVIGW18eWs9CyNKKij/oVdDe3Yf3ehyvPHIZKmPxkYAIBqXbr9TxqDhRrGBFrFcrsSTrIXS6o0hPS837Zpj2THtqs3fNH0DNgIAUTr98oj2lCNPtG7slxvdSEqcFISE8Zwb+VK/DRS3gXES80BsAACiLMA8ITil1ymWGRck2Qpp/QYZHwxhP58t2j2aU9U2Ac4PQPQpmbWGMi++gZvMlEZFcBP23J+y/juG3HO+j0xIxUsPQ2GbME7vSOLA9wI4PgBWyf3dYlxiSbMdU0fE8wk78YB/UebQm0JJDWuJeaItUNjmUsxqqJwX6gc4PQDWq34rmDSTMi+6npI7H220XAw1XhvnrjR+hpyq8x95ndxzfzbnffvo+9KlVAZlDaykuLvk2sHZAbB4z9/566jolRpDPHNuedwYn5515WhDUOVPIXvk/ZR71zNU8MT7VPS6l0qXbFL1/rZIR0QoanDx3uFwbABAqPCF/ZVQ0mCN5x7xwk2HAwEAQmCGbWanqTb/ePhlcCIAQBBx3SUFHkqHgoaT31tLbs6/WwuHAgAEwNdyQQ/ljESmA0/+NKpO4FQAgKbZwBfzB0IxI3nZ5qFjkOkAAGiCrW4fHQelNEN8fXQBHAwAsFsGg5cuhkKam+N7DxwNANCA0VBG89PMYvnk+zycDQDAPRimiSZAGBVYOw8l8qK/DscDwNG8gx4MquO9aymFF/4DOB8ADozpckPzoqWUBiWMgsnC81eN2XBEABzFPCmuggJG0WRaMd9ofgZnBMARLOXU0lwonwWsuI4K+BfigVMCYGuWV3moGIpnpTQz7rnJn4Qr4ZwA2JIvypZTKZTOgiafhvwLqoGTAmAjPORDM3PrX7gV8i9rMRwWAFuwWEKJUDYdTr6LKZt/YZ/CaQHQmoWlPsqDommWasbZDu/BeQHQko+QMqapSVULf2K+AicGQCvelAIpKJjGJuXFGCEEgD69F2TPQrnsYDx7iW9GR8GxAbAwPhqHhjc2NP7lDmL+gpMDYCm2op+uzY3DDj35F70ezg6AJdjI4YXeUCYnnHx9tD//wtfA6QGIKl+7vdQBiuSkk69ML/bSIjg/AFFhGQ+xrYASOdAKPJSOjAcAlPOydBWEAjk948FLw3DpBoD5l2jMcNlzEB5YfejBS13YKb7F5gDAFH7keG4PKA1s90s3bjvHDjIXmwSAiFJd4qVKKAysSZMyY0nkxmYBICItHR9DJRosYOPcwrPZcTZg8wAQEutZdAdASWBBm3w9wjBNAIJmHotuGygILHT7gOLlJhZZDwC0yBZmdMdqSoBwwCJz8eahTuxUddhcADTK6goPdYZSwMy4eMuUywJsMgD+QYqQZOoLFAJm7unXRycj5xcA+prpC0WAKTMpefSnnf2NDQgcxjZmMuahwaJm5R76Pxk/jc0IHMIXpR46EjsfFnWrWkXJ/gkXm7ExgW0zFvgbngyRxY6HWcr4JHAgO+cCbFJgM+aW1VJ77HCYdY07L7EAn8LO+hU2LND+8sxHgzEHDaaNuasp1R9++BMbGGjGH1IIkV9LGdjJMD0FuIbK5QbYfxOMTQ2szmuVtdQKOxdmC+OvbEewUy/GxgaWLILw0meSoYOdCrNl/Nfto+NYhJdgswMrwE2gvNKJD3FcmBMEONZ/Abccmx9EiS/9F2dx2JAwpwowmu8AZc1sDMHlrnvYgDBHm3To5xjbRUhBA2YKLn/IX4iWjTBYIydgIwYsjaQhFCAyLDZiuDjhwmAtm/Q2ldQepKGBUJrY8Deo9+RDHDsJBgvBeBO19XdB+wOCAlpAeoVM5g/tdtg5MFgErNJLJXyKuY6/Nq6CwIBdMxTYN0ZWeagYOwUGMysO7KUe/mo4nIIdfLqVyQ9GOAEpYTCYwjjwMsqR1CDehMsgRI5Bcr+Hc3vGQuwAGCy6p+AY/qrZxT8P7keIk+34gU+3j2KYJAxmXRGOMzIi6i/kfoBoacsvEk6SUAJyb2EwHUW4/iS8DmJmedZtF1spqoEDw2CaG4+mT+Kvq714Y9/H1ELkLALP6+Mw0b38u+kpvyN4KgxmY5Oeq3IxJzfjLADrIYLK+F2KG+SCzL2c9oYnwmAOPg37U9RGc0/WOfznJghkxPiTc69n88n2Du6V0B0hBBgM1rhxXX+FjzqyaAzzn4hxSRc4640TLY99kg8zmUANh4LBYMGbNHDnr8V8Gj6PheVB5iP/rTsyD7z0IYvsA7I2CB3AYDDTrbyW3P4QxTB/JV21TYd7bmFxXWk0LZKTLGcdVKyg1vKBBC+AwWCWMKmok1CFv8n7cEln83/9FvHaauHTa7U/tDJaLh7lQ8UQWLRUhMFgOptcLpV4qVKE2Uhv81F/FuRL5SQpYQy+iHrWf+tf7cdjCHa9aP/kF8iNDQRzo//vfmrw73m2//f8897lP5+RcIDxGvJa/Jry2vIe5L3gwgum2v4fWd7IWKJ/Y7MAAAAASUVORK5CYII='; + +const Scratch = { + BlockType: require('../../extension-support/block-type'), + ArgumentType: require('../../extension-support/argument-type'), + TargetType: require('../../extension-support/target-type') +} +const Cast = require('../../util/cast'); + +var vars = {}; +vars['variables'] = {}; + +class LMSUtils { + constructor(runtime) { + this.runtime = runtime; + } + getInfo() { + return { + id: 'lmsutilsblocks', + name: 'LMS Utilities', + color1: '#1cd6ff', + color2: '#1cbbff', + color3: '#1cbbff', + blockIconURI: blockIconURI, + blocks: [ + { + opcode: 'whenBooleanHat', + blockType: Scratch.BlockType.HAT, + text: 'when [INPUT]', + isEdgeActivated: true, + hideFromPalette: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + } + } + }, + { + opcode: 'whenKeyString', + blockType: Scratch.BlockType.HAT, + text: 'when key [KEY_OPTION] pressed', + isEdgeActivated: true, + arguments: { + KEY_OPTION: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'enter' + } + } + }, + + '---', + + { + opcode: 'keyStringPressed', + blockType: Scratch.BlockType.BOOLEAN, + text: 'key [KEY_OPTION] pressed?', + arguments: { + KEY_OPTION: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'enter' + } + } + }, + { + opcode: 'trueFalseBoolean', + blockType: Scratch.BlockType.BOOLEAN, + text: '[TRUEFALSE]', + hideFromPalette: true, + arguments: { + TRUEFALSE: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'true', + menu: 'trueFalseMenu' + } + } + }, + { + opcode: 'stringIf', + blockType: Scratch.BlockType.REPORTER, + text: 'if [BOOLEAN] then [INPUTA]', + disableMonitor: true, + arguments: { + BOOLEAN: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + }, + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + } + } + }, + { + opcode: 'stringIfElse', + blockType: Scratch.BlockType.REPORTER, + text: 'if [BOOLEAN] then [INPUTA] else [INPUTB]', + hideFromPalette: true, + disableMonitor: true, + arguments: { + BOOLEAN: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + }, + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'banana' + } + } + }, + + '---', + + { + opcode: 'getEffectValue', + blockType: Scratch.BlockType.REPORTER, + text: 'effect [INPUT]', + hideFromPalette: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'color', + menu: 'colorMenu' + } + } + }, + { + opcode: 'clonesBeingUsed', + hideFromPalette: true, + blockType: Scratch.BlockType.REPORTER, + text: 'clone count', + }, + { + opcode: 'isClone', + hideFromPalette: true, + blockType: Scratch.BlockType.BOOLEAN, + text: 'is clone?', + filter: [Scratch.TargetType.SPRITE] + }, + { + opcode: 'spriteClicked', + blockType: Scratch.BlockType.BOOLEAN, + text: 'sprite clicked?', + filter: [Scratch.TargetType.SPRITE] + }, + + '---', + + { + opcode: 'lettersToOf', + blockType: Scratch.BlockType.REPORTER, + text: 'letters [INPUTA] to [INPUTB] of [STRING]', + disableMonitor: true, + arguments: { + INPUTA: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1' + }, + INPUTB: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '3' + }, + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'suspicious' + } + } + }, + { + opcode: 'replaceWords', + blockType: Scratch.BlockType.REPORTER, + text: 'replace [INPUTA] with [INPUTB] in [STRING]', + disableMonitor: true, + arguments: { + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Scratch' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Turbowarp' + }, + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Scratch is brilliant!' + } + } + }, + { + opcode: 'findIndexOfString', + blockType: Scratch.BlockType.REPORTER, + text: 'index of [INPUTA] in [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'brilliant' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Turbowarp is brilliant!' + } + } + }, + { + opcode: 'itemOfFromString', + blockType: Scratch.BlockType.REPORTER, + text: 'item [INPUTA] of [INPUTB] split by [INPUTC]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '2' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple|banana' + }, + INPUTC: { + type: Scratch.ArgumentType.STRING, + defaultValue: '|' + } + } + }, + { + opcode: 'stringToUpperCase', + blockType: Scratch.BlockType.REPORTER, + text: '[STRING] to uppercase', + disableMonitor: true, + hideFromPalette: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + } + } + }, + { + opcode: 'stringToLowerCase', + blockType: Scratch.BlockType.REPORTER, + text: '[STRING] to lowercase', + disableMonitor: true, + hideFromPalette: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'APPLE' + } + } + }, + { + opcode: 'reverseString', + blockType: Scratch.BlockType.REPORTER, + text: 'reverse [STRING]', + disableMonitor: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'prawobrut' + } + } + }, + + '---', + + { + opcode: 'norBoolean', + blockType: Scratch.BlockType.BOOLEAN, + text: '[INPUTA] nor [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + }, + INPUTB: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + } + } + }, + { + opcode: 'xorBoolean', + blockType: Scratch.BlockType.BOOLEAN, + text: '[INPUTA] xor [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + }, + INPUTB: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + } + } + }, + { + opcode: 'xnorBoolean', + blockType: Scratch.BlockType.BOOLEAN, + text: '[INPUTA] xnor [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + }, + INPUTB: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + } + } + }, + { + opcode: 'nandBoolean', + blockType: Scratch.BlockType.BOOLEAN, + text: '[INPUTA] nand [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + }, + INPUTB: { + type: Scratch.ArgumentType.BOOLEAN, + defaultValue: '' + } + } + }, + + '---', + + { + opcode: 'stringReporter', + blockType: Scratch.BlockType.REPORTER, + text: '[STRING]', + disableMonitor: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + } + } + }, + { + opcode: 'colourHex', + blockType: Scratch.BlockType.REPORTER, + text: 'color [COLOUR]', + arguments: { + COLOUR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: '#0088ff' + } + } + }, + { + opcode: 'angleReporter', + blockType: Scratch.BlockType.REPORTER, + text: 'angle [ANGLE]', + arguments: { + ANGLE: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: '90' + } + } + }, + { + opcode: 'matrixReporter', + blockType: Scratch.BlockType.REPORTER, + text: 'matrix [MATRIX]', + arguments: { + MATRIX: { + type: Scratch.ArgumentType.MATRIX, + defaultValue: '0101001010000001000101110' + } + } + }, + { + opcode: 'noteReporter', + blockType: Scratch.BlockType.REPORTER, + text: 'note [NOTE]', + arguments: { + NOTE: { + type: Scratch.ArgumentType.NOTE, + defaultValue: '' + } + } + }, + { + opcode: 'newlineCharacter', + blockType: Scratch.BlockType.REPORTER, + text: 'newline character', + hideFromPalette: true, + disableMonitor: true + }, + + '---', + + { + opcode: 'equalsExactly', + blockType: Scratch.BlockType.BOOLEAN, + text: '[ONE] === [TWO]', + arguments: { + ONE: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + }, + TWO: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'banana' + } + } + }, + { + opcode: 'notEqualTo', + blockType: Scratch.BlockType.BOOLEAN, + text: '[INPUTA] ≠ [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'banana' + } + } + }, + { + opcode: 'moreThanEqual', + blockType: Scratch.BlockType.BOOLEAN, + text: '[INPUTA] ≥ [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '16' + }, + INPUTB: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '25' + } + } + }, + { + opcode: 'lessThanEqual', + blockType: Scratch.BlockType.BOOLEAN, + text: '[INPUTA] ≤ [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '16' + }, + INPUTB: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '25' + } + } + }, + { + opcode: 'stringCheckBoolean', + blockType: Scratch.BlockType.BOOLEAN, + text: '[INPUT] is [DROPDOWN]', + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + }, + DROPDOWN: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'text', + menu: 'stringCheckMenu' + } + } + }, + + '---', + + { + opcode: 'encodeToBlock', + blockType: Scratch.BlockType.REPORTER, + text: 'encode [STRING] to [DROPDOWN]', + disableMonitor: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: '' + }, + DROPDOWN: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'base64', + menu: 'conversionMenu' + } + } + }, + { + opcode: 'decodeFromBlock', + blockType: Scratch.BlockType.REPORTER, + text: 'decode [STRING] from [DROPDOWN]', + disableMonitor: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: '' + }, + DROPDOWN: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'base64', + menu: 'conversionMenu' + } + } + }, + + '---', + + { + opcode: 'negativeReporter', + blockType: Scratch.BlockType.REPORTER, + text: '- [INPUT]', + disableMonitor: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '' + } + } + }, + { + opcode: 'exponentBlock', + blockType: Scratch.BlockType.REPORTER, + text: '[INPUTA] ^ [INPUTB]', + disableMonitor: true, + arguments: { + INPUTA: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '' + }, + INPUTB: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '' + } + } + }, + { + opcode: 'rootBlock', + blockType: Scratch.BlockType.REPORTER, + text: '[INPUTA] √ [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '' + }, + INPUTB: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '' + } + } + }, + { + opcode: 'normaliseValue', + blockType: Scratch.BlockType.REPORTER, + text: 'normalise [INPUT]', + disableMonitor: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '100' + } + } + }, + { + opcode: 'clampNumber', + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: true, + text: 'clamp [INPUTA] between [INPUTB] and [INPUTC]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '100' + }, + INPUTB: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '25' + }, + INPUTC: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '50' + } + } + }, + + '---', + + { + opcode: 'setVariableTo', + blockType: Scratch.BlockType.COMMAND, + text: 'set variable [INPUTA] to [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my variable' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: '0' + } + } + }, + { + opcode: 'changeVariableBy', + blockType: Scratch.BlockType.COMMAND, + text: 'change variable [INPUTA] by [INPUTB]', + arguments: { + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my variable' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: '1' + } + } + }, + { + opcode: 'getVariable', + blockType: Scratch.BlockType.REPORTER, + text: 'variable [INPUT]', + disableMonitor: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my variable' + } + } + }, + { + opcode: 'deleteVariable', + blockType: Scratch.BlockType.COMMAND, + text: 'delete variable [INPUT]', + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my variable' + } + } + }, + { + opcode: 'deleteAllVariables', + blockType: Scratch.BlockType.COMMAND, + text: 'delete all variables', + }, + { + opcode: 'listVariables', + blockType: Scratch.BlockType.REPORTER, + text: 'list active variables', + disableMonitor: true, + }, + + '---', + + { + opcode: 'greenFlag', + blockType: Scratch.BlockType.COMMAND, + hideFromPalette: true, + text: 'green flag', + }, + { + opcode: 'setUsername', + blockType: Scratch.BlockType.COMMAND, + text: 'set username to [INPUT]', + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'LilyMakesThings' + } + } + }, + + '---', + + { + opcode: 'setSpriteSVG', + blockType: Scratch.BlockType.COMMAND, + text: 'replace SVG data for costume [INPUTA] with [INPUTB]', + + arguments: { + INPUTA: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '1' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: '' + } + } + }, + + '---', + + { + opcode: 'alertBlock', + blockType: Scratch.BlockType.COMMAND, + text: 'alert [STRING]', + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'A red spy is in the base!' + } + } + }, + { + opcode: 'inputPromptBlock', + blockType: Scratch.BlockType.REPORTER, + text: 'prompt [STRING]', + disableMonitor: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'The code is 1, 1, 1.. err... 1!' + } + } + }, + { + opcode: 'confirmationBlock', + blockType: Scratch.BlockType.BOOLEAN, + text: 'confirm [STRING]', + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Are you the red spy?' + } + } + }, + + { + opcode: 'goToLink', + blockType: Scratch.BlockType.COMMAND, + text: 'open link [INPUT] in new tab', + hideFromPalette: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: '' + } + } + }, + { + opcode: 'redirectToLink', + blockType: Scratch.BlockType.COMMAND, + text: 'redirect to link [INPUT]', + hideFromPalette: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: '' + } + } + }, + + '---', + + { + opcode: 'setClipboard', + blockType: Scratch.BlockType.COMMAND, + text: 'set [STRING] to clipboard', + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple', + } + } + }, + { + opcode: 'readClipboard', + blockType: Scratch.BlockType.REPORTER, + text: 'clipboard' + }, + + '---', + + { + opcode: 'isUserMobile', + blockType: Scratch.BlockType.BOOLEAN, + text: 'is mobile?' + }, + { + opcode: 'screenReporter', + blockType: Scratch.BlockType.REPORTER, + text: 'screen [DROPDOWN]', + disableMonitor: true, + arguments: { + DROPDOWN: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'width', + menu: 'screenReporterMenu' + } + } + }, + { + opcode: 'windowReporter', + blockType: Scratch.BlockType.REPORTER, + text: 'window [DROPDOWN]', + disableMonitor: true, + arguments: { + DROPDOWN: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'width', + menu: 'screenReporterMenu' + } + } + }, + { + opcode: 'osBrowserDetails', + blockType: Scratch.BlockType.REPORTER, + text: '[DROPDOWN]', + disableMonitor: true, + arguments: { + DROPDOWN: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'operating system', + menu: 'osBrowserMenu' + } + } + }, + { + opcode: 'projectURL', + blockType: Scratch.BlockType.REPORTER, + text: 'project URL', + disableMonitor: true, + }, + + '---', + + { + opcode: 'consoleLog', + blockType: Scratch.BlockType.COMMAND, + text: 'console [DROPDOWN] [INPUT]', + disableMonitor: true, + arguments: { + DROPDOWN: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'log', + menu: 'consoleLogMenu' + }, + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Apple' + } + } + }, + { + opcode: 'clearConsole', + blockType: Scratch.BlockType.COMMAND, + text: 'clear console' + }, + + '---', + + { + opcode: 'commentHat', + blockType: Scratch.BlockType.HAT, + text: '// [STRING]', + hideFromPalette: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'comment', + } + } + }, + { + opcode: 'commentCommand', + blockType: Scratch.BlockType.COMMAND, + text: '// [STRING]', + hideFromPalette: true, + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'comment', + } + } + }, + { + opcode: 'commentString', + blockType: Scratch.BlockType.REPORTER, + text: '// [INPUTA] [INPUTB]', + hideFromPalette: true, + disableMonitor: true, + arguments: { + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'comment' + }, + INPUTB: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'input' + } + } + }, + { + opcode: 'commentBool', + blockType: Scratch.BlockType.BOOLEAN, + text: '// [INPUTA] [INPUTB]', + hideFromPalette: true, + arguments: { + INPUTA: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'comment' + }, + INPUTB: { + type: Scratch.ArgumentType.BOOLEAN, + } + } + } + ], + menus: { + conversionMenu: { + acceptReporters: true, + items: [ + { + text: 'base64', + value: 'base64' + }, + { + text: 'binary', + value: 'binary' + } + ] + }, + trueFalseMenu: { + acceptReporters: true, + items: [ + { + text: 'true', + value: 'true' + }, + { + text: 'false', + value: 'false' + }, + { + text: 'random', + value: 'random', + } + ] + }, + screenReporterMenu: { + acceptReporters: true, + items: [ + { + text: 'width', + value: 'width' + }, + { + text: 'height', + value: 'height' + } + ] + }, + windowReporterMenu: { + acceptReporters: true, + items: [ + { + text: 'width', + value: 'width' + }, + { + text: 'height', + value: 'height' + } + ] + }, + stringCheckMenu: { + acceptReporters: true, + items: [ + { + text: 'text', + value: 'text' + }, + { + text: 'number', + value: 'number' + }, + { + text: 'uppercase', + value: 'uppercase' + }, + { + text: 'lowercase', + value: 'lowercase' + } + ] + }, + osBrowserMenu: { + acceptReporters: true, + items: [ + { + text: 'operating system', + value: 'operating system' + }, + { + text: 'browser', + value: 'browser' + } + ] + }, + consoleLogMenu: { + acceptReporters: false, + items: [ + { + text: 'log', + value: 'log' + }, + { + text: 'error', + value: 'error' + }, + { + text: 'warn', + value: 'warn' + } + ] + }, + colorMenu: { + acceptReporters: true, + items: [ + { + text: 'color', + value: 'color' + }, + { + text: 'fisheye', + value: 'fisheye' + }, + { + text: 'whirl', + value: 'whirl' + }, + { + text: 'pixelate', + value: 'pixelate' + }, + { + text: 'mosaic', + value: 'mosaic' + }, + { + text: 'brightness', + value: 'brightness' + }, + { + text: 'ghost', + value: 'ghost' + }, + ] + }, + } + }; + } + + whenBooleanHat(args) { + return args.INPUT; + } + + whenKeyString(args, util) { + return util.ioQuery('keyboard', 'getKeyIsDown', [args.KEY_OPTION]); + } + + equalsExactly(args) { + return args.ONE === args.TWO; + } + + stringReporter(args) { + return args.STRING; + } + + colourHex(args) { + return args.COLOUR; + } + + angleReporter(args) { + return args.ANGLE; + } + + matrixReporter(args) { + return args.MATRIX; + } + + noteReporter(args) { + return args.NOTE; + } + + newlineCharacter() { + return '\n'; + } + + stringIf(args) { + if (args.BOOLEAN) { + return args.INPUTA; + } else { + return ''; + } + } + + stringIfElse(args) { + if (args.BOOLEAN) { + return args.INPUTA; + } else { + return args.INPUTB; + } + } + + lettersToOf(args) { + var string = args.STRING.toString(); + var input1 = args.INPUTA - 1; + var input2 = args.INPUTB; + return string.slice(input1, input2); + } + + replaceWords(args) { + var input1 = args.INPUTA; + var input2 = args.INPUTB; + var string = args.STRING; + return string.replace(input1, input2); + } + + exponentBlock(args) { + return Math.pow(args.INPUTA, args.INPUTB); + } + + rootBlock(args) { + return Math.pow(args.INPUTB, 1 / args.INPUTA); + } + + normaliseValue(args) { + var input1 = args.INPUT; + var input2 = Math.abs(input1); + var output = (input1 / input2); + if (isNaN(output)) { + return '0'; + } else { + return output; + } + } + + stringToUpperCase(args) { + return args.STRING.toUpperCase(); + } + + stringToLowerCase(args) { + return args.STRING.toLowerCase(); + } + + reverseString(args) { + var input = args.STRING; + var splitInput = input.split(''); + var reversedInput = splitInput.reverse(); + var joinedArray = reversedInput.join(''); + return joinedArray; + } + + encodeToBlock(args) { + if (args.STRING === '') { + return ''; + } + if (args.DROPDOWN === 'base64') { + return btoa(args.STRING); + } + if (args.DROPDOWN === 'binary') { + return args.STRING.split('').map(function (char) { + return char.charCodeAt(0).toString(2); + }).join(' '); + } + } + + decodeFromBlock(args) { + if (args.STRING === '') { + return ''; + } + if (args.DROPDOWN === 'base64') { + return atob(args.STRING); + } + if (args.DROPDOWN === 'binary') { + var output = args.STRING.toString(); + return output.split(' ').map((x) => x = String.fromCharCode(parseInt(x, 2))).join(''); + } + } + + trueFalseBoolean(args) { + if (args.TRUEFALSE === 'random') { + return Math.random() > 0.5; + } + if (args.TRUEFALSE === 'true') { + return true; + } else { + return false; + } + } + + isClone(args, util) { + if (util.target.isOriginal) { + return false; + } else { + return true; + } + } + + clonesBeingUsed(args, util) { + return vm.runtime._cloneCounter; + } + + keyStringPressed(args, util) { + return util.ioQuery('keyboard', 'getKeyIsDown', [args.KEY_OPTION]); + } + + spriteClicked(args, util) { + return (util.ioQuery('mouse', 'getIsDown') && util.target.isTouchingObject('_mouse_')); + } + + notEqualTo(args) { + return (args.INPUTA != args.INPUTB); + } + + moreThanEqual(args) { + return (args.INPUTA >= args.INPUTB); + } + + lessThanEqual(args) { + return (args.INPUTA <= args.INPUTB); + } + + stringCheckBoolean(args) { + var input = args.INPUT; + if (args.DROPDOWN === 'text') { + return isNaN(args.INPUT); + } + if (args.DROPDOWN === 'number') { + return !isNaN(args.INPUT); + } + if (args.DROPDOWN === 'uppercase') { + return (args.INPUT == args.INPUT.toUpperCase()); + } + if (args.DROPDOWN === 'lowercase') { + return (args.INPUT == args.INPUT.toLowerCase()); + } + } + + norBoolean(args) { + return !(args.INPUTA || args.INPUTB); + } + + xorBoolean(args) { + return (args.INPUTA !== args.INPUTB); + } + + xnorBoolean(args) { + return (args.INPUTA === args.INPUTB); + } + + nandBoolean(args) { + return !(args.INPUTA && args.INPUTB); + } + + screenReporter(args) { + if (args.DROPDOWN === 'width') { + return screen.width; + } + if (args.DROPDOWN === 'height') { + return screen.height; + } + } + + windowReporter(args) { + if (args.DROPDOWN === 'width') { + return window.innerWidth; + } + if (args.DROPDOWN === 'height') { + return window.innerHeight; + } + } + + osBrowserDetails(args) { + var user = navigator.userAgent; + if (args.DROPDOWN === 'operating system') { + if (user.includes('Mac OS')) { + return 'macOS'; + } + if (user.includes('CrOS')) { + return 'ChromeOS'; + } + if (user.includes('Linux')) { + return 'Linux'; + } + if (user.includes('Windows')) { + return 'Windows'; + } + if (user.includes('iPad')) { + return 'iOS'; + } + if (user.includes('iPod')) { + return 'iOS'; + } + if (user.includes('iPhone')) { + return 'iOS'; + } + if (user.includes('Android')) { + return 'Android'; + } + return 'Other'; + } + if (args.DROPDOWN === 'browser') { + if (user.includes('Chrome')) { + return 'Chrome'; + } + if (user.includes('MSIE')) { + return 'Internet Explorer'; + } + if (user.includes('rv:')) { + return 'Internet Explorer'; + } + if (user.includes('Firefox')) { + return 'Firefox'; + } + if (user.includes('Safari')) { + return 'Safari'; + } + return 'Other'; + } + } + + projectURL() { + return window.location.href; + } + + greenFlag(args, util) { + util.runtime.greenFlag(); + } + + setUsername(args, util) { + util.runtime.vm.postIOData('userData', { + username: args.INPUT, + loggedIn: false, + }); + } + + consoleLog(args) { + if (args.DROPDOWN === 'log') { + console.log(args.INPUT); + } else if (args.DROPDOWN === 'error') { + console.error(args.INPUT); + } else if (args.DROPDOWN === 'warn') { + console.warn(args.INPUT); + } + } + + clearConsole() { + console.clear(); + } + + setClipboard(args) { + navigator.clipboard.writeText(args.STRING); + } + + readClipboard(args) { + if (navigator.clipboard && navigator.clipboard.readText) { + return navigator.clipboard.readText(); + } + return ''; + } + + alertBlock(args) { + alert(args.STRING); + } + + inputPromptBlock(args) { + return prompt(args.STRING); + } + + confirmationBlock(args) { + if (confirm(args.STRING)) { + return true; + } else { + return false; + } + } + + commentHat(args, util) { + return args.INPUT; + } + + commentCommand(args) { + } + + commentString(args) { + return args.INPUTB; + } + + commentBool(args) { + return Cast.toBoolean(args.INPUTB); + } + + getVariable(args) { + if (args.INPUT in vars['variables']) { + return (vars['variables'][args.INPUT]); + } else { + return ''; + } + } + + setVariableTo(args) { + vars['variables'][args.INPUTA] = args.INPUTB; + } + + changeVariableBy(args) { + if (args.INPUTA in vars['variables']) { + var prev = vars['variables'][args.INPUTA]; + var next = args.INPUTB; + vars['variables'][args.INPUTA] = (prev + next); + } else { + vars['variables'][args.INPUTA] = args.INPUTB; + } + } + + listVariables(args, util) { + if (Object.keys(vars['variables']).length) { + var output = Object.keys(vars['variables']); + return output; + } else { + return; + } + } + + deleteVariable(args) { + Reflect.deleteProperty(vars['variables'], args.INPUT); + } + + deleteAllVariables() { + Reflect.deleteProperty(vars, 'variables'); + vars['variables'] = {}; + } + + clampNumber(args) { + var input1 = args.INPUTA; + var input2 = args.INPUTB; + var input3 = args.INPUTC; + return Math.min(Math.max(input1, input2), input3); + } + + findIndexOfString(args) { + var input1 = args.INPUTA; + var input2 = args.INPUTB; + if (input2.includes(input1)) { + return (input2.indexOf(input1) + 1); + } else { + return ''; + } + } + + itemOfFromString(args, util) { + var input1 = (args.INPUTA - 1); + var input2 = String(args.INPUTB); + var input3 = args.INPUTC; + var output = input2.split(input3)[input1] || ''; + return output; + } + + isUserMobile(args, util) { + return navigator.userAgent.includes('Mobile'); + } + + getEffectValue(args, util) { + return util.target.effects[args.INPUT]; + } + + negativeReporter(args) { + return (args.INPUT * -1); + } + + setSpriteSVG(args, util) { + try { + this.runtime.renderer.updateSVGSkin(util.target.sprite.costumes[args.INPUTA - 1].skinId, args.INPUTB); + } catch (error) { + return; + } + vm.emitTargetsUpdate(); + } +} +module.exports = LMSUtils; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/pm_camera/index.js b/local-scratch-vm/src/extensions/pm_camera/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7ee4eaa1c4ab3bd15c9a0f89638df2d1b9de4f3b --- /dev/null +++ b/local-scratch-vm/src/extensions/pm_camera/index.js @@ -0,0 +1,462 @@ +/* eslint-disable space-infix-ops */ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const MathUtil = require('../../util/math-util'); + +// eslint-disable-next-line no-undef +const pathToMedia = 'static/blocks-media'; // ScratchBlocks.mainWorkspace.options.pathToMedia +const stateKey = 'CAMERA_INFO'; +const defaultState = 'default'; + +class PenguinModCamera { + constructor(runtime) { + this.runtime = runtime; + + runtime.setRuntimeOptions({ + fencing: false + }); + runtime.ioDevices.mouse.bindToCamera(0); + } + getCamera(target) { + return this.runtime.getCamera(this.getActiveCamera(target)); + } + updateCamera(target, state) { + this.runtime.updateCamera(this.getActiveCamera(target), state); + } + getActiveCamera(target) { + let cameraState = target._customState[stateKey]; + if (!cameraState) { + cameraState = target.cameraBound || defaultState; + target.setCustomState(stateKey, cameraState); + } + return cameraState; + } + setActiveCamera(target, screen) { + target.setCustomState(stateKey, screen); + } + getInfo() { + return { + id: 'pmCamera', + name: 'Camera', + color1: '#0586FF', + blocks: [ + { + opcode: 'moveSteps', + blockType: BlockType.COMMAND, + text: 'move camera [STEPS] steps', + arguments: { + STEPS: { + type: ArgumentType.NUMBER, + defaultValue: '10' + } + } + }, + { + opcode: 'turnRight', + blockType: BlockType.COMMAND, + text: 'turn camera [DIRECTION] [DEGREES] degrees', + arguments: { + DIRECTION: { + type: ArgumentType.IMAGE, + dataURI: `${pathToMedia}/rotate-right.svg` + }, + DEGREES: { + type: ArgumentType.NUMBER, + defaultValue: '15' + } + } + }, + { + opcode: 'turnLeft', + blockType: BlockType.COMMAND, + text: 'turn camera [DIRECTION] [DEGREES] degrees', + arguments: { + DIRECTION: { + type: ArgumentType.IMAGE, + dataURI: `${pathToMedia}/rotate-left.svg` + }, + DEGREES: { + type: ArgumentType.NUMBER, + defaultValue: '15' + } + } + }, + { + opcode: 'bindTarget', + blockType: BlockType.COMMAND, + text: 'bind [TARGET] to camera [SCREEN]', + arguments: { + TARGET: { + type: ArgumentType.STRING, + menu: 'BINDABLE_TARGETS' + }, + SCREEN: { + type: ArgumentType.STRING, + defaultValue: defaultState + } + } + }, + { + opcode: 'unbindTarget', + blockType: BlockType.COMMAND, + text: 'unbind [TARGET] from the camera', + arguments: { + TARGET: { + type: ArgumentType.STRING, + menu: 'BINDABLE_TARGETS' + } + } + }, + { + opcode: 'setCurrentCamera', + blockType: BlockType.COMMAND, + text: 'set current camera to [SCREEN]', + arguments: { + SCREEN: { + type: ArgumentType.STRING, + defaultValue: defaultState + } + } + }, + { + opcode: 'setRenderImediat', + blockType: BlockType.COMMAND, + text: 'set render mode to [RENDER_MODE]', + arguments: { + RENDER_MODE: { + type: ArgumentType.STRING, + menu: 'RENDER_MODES' + } + } + }, + { + opcode: 'manualRender', + blockType: BlockType.COMMAND, + text: 'render camera' + }, + '---', + { + opcode: 'gotoXY', + blockType: BlockType.COMMAND, + text: 'set camera x: [X] y: [Y]', + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + } + }, + { + opcode: 'setSize', + blockType: BlockType.COMMAND, + text: 'set camera zoom to [ZOOM]%', + arguments: { + ZOOM: { + type: ArgumentType.NUMBER, + defaultValue: '100' + } + } + }, + { + opcode: 'changeSize', + blockType: BlockType.COMMAND, + text: 'change camera zoom by [ZOOM]%', + arguments: { + ZOOM: { + type: ArgumentType.NUMBER, + defaultValue: '10' + } + } + }, + '---', + { + opcode: 'pointTowards', + blockType: BlockType.COMMAND, + text: 'point camera in direction [DIRECTION]', + arguments: { + DIRECTION: { + type: ArgumentType.ANGLE, + defaultValue: '90' + } + } + }, + { + opcode: 'pointTowardsPoint', + blockType: BlockType.COMMAND, + text: 'point camera towards x: [X] y: [Y]', + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: '0' + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + } + }, + '---', + { + opcode: 'changeXpos', + blockType: BlockType.COMMAND, + text: 'change camera x by [X]', + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: '10' + } + } + }, + { + opcode: 'setXpos', + blockType: BlockType.COMMAND, + text: 'set camera x to [X]', + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + } + }, + { + opcode: 'changeYpos', + blockType: BlockType.COMMAND, + text: 'change camera y by [Y]', + arguments: { + Y: { + type: ArgumentType.NUMBER, + defaultValue: '10' + } + } + }, + { + opcode: 'setYpos', + blockType: BlockType.COMMAND, + text: 'set camera y to [Y]', + arguments: { + Y: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + } + }, + '---', + { + opcode: 'xPosition', + blockType: BlockType.REPORTER, + text: 'camera x' + }, + { + opcode: 'yPosition', + blockType: BlockType.REPORTER, + text: 'camera y' + }, + { + opcode: 'direction', + blockType: BlockType.REPORTER, + text: 'camera direction' + }, + { + // theres also a property named "size" so this one is special + opcode: 'getSize', + blockType: BlockType.REPORTER, + text: 'camera zoom' + }, + { + opcode: 'getCurrentCamera', + blockType: BlockType.REPORTER, + text: 'current camera' + } + ], + menus: { + BINDABLE_TARGETS: { + items: 'getBindableTargets', + acceptReports: true + }, + RENDER_MODES: { + items: [ + 'immediate', + 'manual' + ] + } + } + }; + } + getBindableTargets() { + const targets = this.runtime.targets + .filter(target => !target.isStage && target.isOriginal && target.id !== this.runtime.vm.editingTarget) + .map(target => target.getName()); + return [].concat([ + { text: 'this sprite', value: '__MYSELF__' }, + { text: 'mouse-pointer', value: '__MOUSEPOINTER__' }, + { text: 'backdrop', value: '__STAGE__' }, + { text: 'all sprites', value: '__ALL__' } + ], targets); + } + moveSteps({ STEPS }, util) { + const { pos: [x, y], dir } = this.getCamera(util.target); + const radians = MathUtil.degToRad(dir); + const dx = STEPS * Math.cos(radians); + const dy = STEPS * Math.sin(radians); + this.updateCamera(util.target, { pos: [x + dx, y + dy] }); + } + turnRight({ DEGREES }, util) { + const { dir } = this.getCamera(util.target); + this.updateCamera(util.target, { dir: dir - DEGREES }); + } + turnLeft({ DEGREES }, util) { + const { dir } = this.getCamera(util.target); + this.updateCamera(util.target, { dir: dir + DEGREES }); + } + bindTarget({ TARGET, SCREEN }, util) { + if (!SCREEN) throw new Error('target screen MUST not be blank'); + switch (TARGET) { + case '__MYSELF__': + const myself = util.target; + myself.bindToCamera(SCREEN); + this.setActiveCamera(myself, SCREEN); + break; + case '__MOUSEPOINTER__': + util.ioQuery('mouse', 'bindToCamera', [SCREEN]); + break; + /* + case '__PEN__': + const pen = this.runtime.ext_pen; + if (!pen) break; + pen.bindToCamera(SCREEN); + break; + */ + case '__STAGE__': + const stage = this.runtime.getTargetForStage(); + stage.bindToCamera(SCREEN); + break; + case '__ALL__': + for (const target of this.runtime.targets) { + target.bindToCamera(SCREEN); + } + break; + default: + const sprite = this.runtime.getSpriteTargetByName(TARGET); + if (!sprite) throw `unkown target ${TARGET}`; + sprite.bindToCamera(SCREEN); + break; + } + } + unbindTarget({ TARGET }, util) { + switch (TARGET) { + case '__MYSELF__': { + const myself = util.target; + myself.removeCameraBinding(); + break; + } + case '__MOUSEPOINTER__': + util.ioQuery('mouse', 'removeCameraBinding'); + break; + /* + case '__PEN__': { + const pen = this.runtime.ext_pen; + if (!pen) break; + pen.removeCameraBinding(); + break; + } + */ + case '__STAGE__': { + const stage = this.runtime.getTargetForStage(); + stage.removeCameraBinding(); + break; + } + case '__ALL__': + for (const target of this.runtime.targets) { + target.removeCameraBinding(); + } + break; + default: { + const sprite = this.runtime.getSpriteTargetByName(TARGET); + if (!sprite) throw `unkown target ${TARGET}`; + sprite.removeCameraBinding(); + break; + } + } + } + setCurrentCamera({ SCREEN }, util) { + if (!SCREEN) throw new Error('target screen MUST not be blank'); + this.setActiveCamera(util.target, SCREEN); + } + setRenderImediat({ RENDER_MODE }, util) { + // possibly add more render modes? + switch (RENDER_MODE) { + case 'immediate': + this.updateCamera(util.target, { silent: false }); + break; + case 'manual': + this.updateCamera(util.target, { silent: true }); + break; + } + } + manualRender(_, util) { + this.runtime.emitCameraChanged(this.getActiveCamera(util.target)); + } + + gotoXY({ X, Y }, util) { + this.updateCamera(util.target, { pos: [X, Y] }); + } + setSize({ ZOOM }, util) { + this.updateCamera(util.target, { scale: ZOOM / 100 }); + } + changeSize({ ZOOM }, util) { + const { scale } = this.getCamera(util.target); + this.updateCamera(util.target, { scale: (ZOOM / 100) + scale }); + } + + pointTowards({ DIRECTION }, util) { + this.updateCamera(util.target, { dir: DIRECTION -90 }); + } + pointTowardsPoint({ X, Y }, util) { + const { pos: [x, y] } = this.getCamera(util.target); + this.updateCamera(util.target, { dir: MathUtil.radToDeg(Math.atan2(X-x, Y-y)) }); + } + + changeXpos({ X }, util) { + const { pos: [x, y] } = this.getCamera(util.target); + this.updateCamera(util.target, { pos: [X+x, y] }); + } + setXpos({ X }, util) { + const { pos: [_, y] } = this.getCamera(util.target); + this.updateCamera(util.target, { pos: [X, y] }); + } + changeYpos({ Y }, util) { + const { pos: [x, y] } = this.getCamera(util.target); + this.updateCamera(util.target, { pos: [x, Y+y] }); + } + setYpos({ Y }, util) { + const { pos: [x, _] } = this.getCamera(util.target); + this.updateCamera(util.target, { pos: [x, Y] }); + } + + xPosition(_, util) { + const state = this.getCamera(util.target); + return state.pos[0]; + } + yPosition(_, util) { + const state = this.getCamera(util.target); + return state.pos[1]; + } + direction(_, util) { + const state = this.getCamera(util.target); + return state.dir +90; + } + getSize(_, util) { + const state = this.getCamera(util.target); + return state.scale * 100; + } + getCurrentCamera(_, util) { + return this.getActiveCamera(util.target); + } +} + +module.exports = PenguinModCamera; diff --git a/local-scratch-vm/src/extensions/pm_controlsExpansion/async.svg b/local-scratch-vm/src/extensions/pm_controlsExpansion/async.svg new file mode 100644 index 0000000000000000000000000000000000000000..11a16dc3cbb38ed43e67de37da4e71a48a6439d8 Binary files /dev/null and b/local-scratch-vm/src/extensions/pm_controlsExpansion/async.svg differ diff --git a/local-scratch-vm/src/extensions/pm_controlsExpansion/index.js b/local-scratch-vm/src/extensions/pm_controlsExpansion/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fbe7e8e78d33b602b32732b6bde3f7af6e1570d9 --- /dev/null +++ b/local-scratch-vm/src/extensions/pm_controlsExpansion/index.js @@ -0,0 +1,293 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const ArgumentAlignment = require('../../extension-support/argument-alignment'); +const Cast = require('../../util/cast'); +const AsyncIcon = require('./async.svg'); + +const blockSeparator = ''; // At default scale, about 28px +const pathToMedia = 'static/blocks-media'; // ScratchBlocks.mainWorkspace.options.pathToMedia + +const blocks = ` + + + + 1 + + + +%block0> +%block1> + + + + + + 1 + + + + + + +%block3> +${blockSeparator} +%block2> +%block4> +%block5> +${blockSeparator} + + + + + + + 10 + + + + +`; + +/** + * Class of idk + * @constructor + */ +class pmControlsExpansion { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + // register compiled blocks + this.runtime.registerCompiledExtensionBlocks('pmControlsExpansion', this.getCompileInfo()); + } + + orderCategoryBlocks(extensionBlocks) { + let categoryBlocks = blocks; + + let idx = 0; + for (const block of extensionBlocks) { + categoryBlocks = categoryBlocks.replace(`%block${idx}>`, block); + idx++; + } + + return [categoryBlocks]; + } + + /** + * @returns {object} metadata for extension category NOT blocks + * this extension only contains blocks defined elsewhere, + * since we just want to seperate them rather than create + * slow versions of them + */ + getInfo() { + return { + id: 'pmControlsExpansion', + name: 'Controls Expansion', + color1: '#FFAB19', + color2: '#EC9C13', + color3: '#CF8B17', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [ + { + opcode: 'ifElseIf', + text: [ + 'if [CONDITION1] then', + 'else if [CONDITION2] then' + ], + branchCount: 2, + blockType: BlockType.CONDITIONAL, + arguments: { + CONDITION1: { type: ArgumentType.BOOLEAN }, + CONDITION2: { type: ArgumentType.BOOLEAN } + } + }, + { + opcode: 'ifElseIfElse', + text: [ + 'if [CONDITION1] then', + 'else if [CONDITION2] then', + 'else' + ], + branchCount: 3, + blockType: BlockType.CONDITIONAL, + arguments: { + CONDITION1: { type: ArgumentType.BOOLEAN }, + CONDITION2: { type: ArgumentType.BOOLEAN } + } + }, + { + opcode: 'asNewBroadcast', + text: [ + 'new thread', + '[ICON]' + ], + branchCount: 1, + blockType: BlockType.CONDITIONAL, + alignments: [ + null, // text + null, // SUBSTACK + ArgumentAlignment.RIGHT // ICON + ], + arguments: { + ICON: { + type: ArgumentType.IMAGE, + dataURI: AsyncIcon + } + } + }, + { + opcode: 'restartFromTheTop', + text: 'restart from the top [ICON]', + blockType: BlockType.COMMAND, + isTerminal: true, + arguments: { + ICON: { + type: ArgumentType.IMAGE, + dataURI: `${pathToMedia}/repeat.svg` + } + } + }, + { + opcode: 'asNewBroadcastArgs', + text: [ + 'new thread with data [DATA]', + '[ICON]' + ], + branchCount: 1, + blockType: BlockType.CONDITIONAL, + alignments: [ + null, // text + null, // SUBSTACK + ArgumentAlignment.RIGHT // ICON + ], + arguments: { + DATA: { + type: ArgumentType.STRING, + defaultValue: "abc", + }, + ICON: { + type: ArgumentType.IMAGE, + dataURI: AsyncIcon + } + } + }, + { + opcode: 'asNewBroadcastArgBlock', + text: 'thread data', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + ] + }; + } + + /** + * This function is used for any compiled blocks in the extension if they exist. + * Data in this function is given to the IR & JS generators. + * Data must be valid otherwise errors may occur. + * @returns {object} functions that create data for compiled blocks. + */ + getCompileInfo() { + return { + ir: { + ifElseIf: (generator, block) => ({ + kind: 'stack', + condition1: generator.descendInputOfBlock(block, 'CONDITION1'), + condition2: generator.descendInputOfBlock(block, 'CONDITION2'), + whenTrue1: generator.descendSubstack(block, 'SUBSTACK'), + whenTrue2: generator.descendSubstack(block, 'SUBSTACK2') + }), + ifElseIfElse: (generator, block) => ({ + kind: 'stack', + condition1: generator.descendInputOfBlock(block, 'CONDITION1'), + condition2: generator.descendInputOfBlock(block, 'CONDITION2'), + whenTrue1: generator.descendSubstack(block, 'SUBSTACK'), + whenTrue2: generator.descendSubstack(block, 'SUBSTACK2'), + whenTrue3: generator.descendSubstack(block, 'SUBSTACK3') + }), + restartFromTheTop: () => ({ + kind: 'stack' + }) + }, + js: { + ifElseIf: (node, compiler, imports) => { + compiler.source += `if (${compiler.descendInput(node.condition1).asBoolean()}) {\n`; + compiler.descendStack(node.whenTrue1, new imports.Frame(false)); + compiler.source += `} else if (${compiler.descendInput(node.condition2).asBoolean()}) {\n`; + compiler.descendStack(node.whenTrue2, new imports.Frame(false)); + compiler.source += `}\n`; + }, + ifElseIfElse: (node, compiler, imports) => { + compiler.source += `if (${compiler.descendInput(node.condition1).asBoolean()}) {\n`; + compiler.descendStack(node.whenTrue1, new imports.Frame(false)); + compiler.source += `} else if (${compiler.descendInput(node.condition2).asBoolean()}) {\n`; + compiler.descendStack(node.whenTrue2, new imports.Frame(false)); + compiler.source += `} else {\n`; + compiler.descendStack(node.whenTrue3, new imports.Frame(false)); + compiler.source += `}\n`; + }, + restartFromTheTop: (_, compiler) => { + compiler.source += `runtime._restartThread(thread);`; + compiler.source += `return;`; + } + } + }; + } + + ifElseIf (args, util) { + const condition1 = Cast.toBoolean(args.CONDITION1); + const condition2 = Cast.toBoolean(args.CONDITION2); + if (condition1) { + util.startBranch(1, false); + } else if (condition2) { + util.startBranch(2, false); + } + } + + ifElseIfElse (args, util) { + const condition1 = Cast.toBoolean(args.CONDITION1); + const condition2 = Cast.toBoolean(args.CONDITION2); + if (condition1) { + util.startBranch(1, false); + } else if (condition2) { + util.startBranch(2, false); + } else { + util.startBranch(3, false); + } + } + + restartFromTheTop() { + return; // doesnt work in compat mode + } + + // CubesterYT code probably + asNewBroadcast(_, util) { + if (util.thread.target.blocks.getBranch(util.thread.peekStack(), 0)) { + util.sequencer.runtime._pushThread( + util.thread.target.blocks.getBranch(util.thread.peekStack(), 0), + util.target, + {} + ); + } + } + asNewBroadcastArgs(args, util) { + const data = Cast.toString(args.DATA); + if (util.thread.target.blocks.getBranch(util.thread.peekStack(), 0)) { + const thread = util.sequencer.runtime._pushThread( + util.thread.target.blocks.getBranch(util.thread.peekStack(), 0), + util.target, + {} + ); + + thread.__controlx_asNewBroadcastArgs_data = data; + } + } + asNewBroadcastArgBlock(_, util) { + return util.thread.__controlx_asNewBroadcastArgs_data; + } +} + +module.exports = pmControlsExpansion; diff --git a/local-scratch-vm/src/extensions/pm_eventsExpansion/index.js b/local-scratch-vm/src/extensions/pm_eventsExpansion/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3d3760d375d57caac266cde9974eb27b539bd705 --- /dev/null +++ b/local-scratch-vm/src/extensions/pm_eventsExpansion/index.js @@ -0,0 +1,375 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +const blockSeparator = ''; // At default scale, about 28px + +const blocks = ` + + + + + + + + + abc + + + + + + + + +%b5> +${blockSeparator} + + + + + + + + + + + + + + + + + + abc + + + +%b8> +${blockSeparator} +%b2> +%b0> +%b1> +${blockSeparator} + + + + + +` + +/** + * Class of idk + * @constructor + */ +class pmEventsExpansion { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + // every other frame block + this._otherFrame = false; + this.runtime.on('RUNTIME_STEP_START', () => { + this._everyOtherFrame(); + }); + } + + // stepUpdates + _everyOtherFrame() { + if (this._otherFrame) { + this.runtime.startHats('pmEventsExpansion_everyOtherFrame'); + this._otherFrame = false; + } else { + this._otherFrame = true; + } + } + + // order + orderCategoryBlocks(extensionBlocks) { + let categoryBlocks = blocks; + + let idx = 0; + for (const block of extensionBlocks) { + categoryBlocks = categoryBlocks.replace('%b' + idx + '>', block); + idx++; + } + + return [categoryBlocks]; + } + + /** + * @returns {object} metadata for extension + */ + getInfo() { + return { + id: 'pmEventsExpansion', + name: 'Events Expansion', + color1: '#FFBF00', + color2: '#E6AC00', + color3: '#CC9900', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [ + { + opcode: 'everyOtherFrame', + text: 'every other frame', + blockType: BlockType.EVENT, + isEdgeActivated: false + }, + { + opcode: 'neverr', + text: 'never', + blockType: BlockType.EVENT, + isEdgeActivated: false + }, + { + opcode: 'whenSpriteClicked', + text: 'when [SPRITE] clicked', + blockType: BlockType.EVENT, + isEdgeActivated: false, + arguments: { + SPRITE: { + type: ArgumentType.STRING, + menu: "spriteName" + } + } + }, + { + opcode: 'sendWithData', + text: 'broadcast [BROADCAST] with data [DATA]', + blockType: BlockType.COMMAND, + arguments: { + BROADCAST: { + type: ArgumentType.STRING, + defaultValue: "your not supposed to see this?" + }, + DATA: { + type: ArgumentType.STRING, + defaultValue: "abc" + } + } + }, + { + opcode: 'receivedData', + text: 'when I receive [BROADCAST] with data', + blockType: BlockType.EVENT, + isEdgeActivated: false, + arguments: { + BROADCAST: { + type: ArgumentType.STRING, + menu: "broadcastMenu" + } + } + }, + { + opcode: 'isBroadcastReceived', + text: 'is message [BROADCAST] received?', + blockType: BlockType.BOOLEAN, + hideFromPalette: true, + arguments: { + BROADCAST: { + type: ArgumentType.STRING, + defaultValue: "your not supposed to see this?" + } + } + }, + { + opcode: 'recievedDataReporter', + text: 'recieved data', + blockType: BlockType.REPORTER, + allowDropAnywhere: true, + disableMonitor: true + }, + { + opcode: 'broadcastToSprite', + text: 'broadcast [BROADCAST] to [SPRITE]', + blockType: BlockType.COMMAND, + arguments: { + BROADCAST: { + type: ArgumentType.STRING, + defaultValue: "your not supposed to see this?" + }, + SPRITE: { + type: ArgumentType.STRING, + menu: "spriteName" + } + } + }, + { + opcode: 'broadcastFunction', + text: 'broadcast [BROADCAST] and wait', + blockType: BlockType.REPORTER, + disableMonitor: true, + allowDropAnywhere: true, + arguments: { + BROADCAST: { + type: ArgumentType.STRING, + defaultValue: "your not supposed to see this?" + } + } + }, + { + opcode: 'returnFromBroadcastFunc', + text: 'return [VALUE]', + blockType: BlockType.COMMAND, + isTerminal: true, + disableMonitor: true, + arguments: { + VALUE: { + type: ArgumentType.STRING, + defaultValue: "1" + } + } + }, + { + opcode: 'broadcastThreadCount', + text: 'broadcast [BROADCAST] and get # of blocks started', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'broadcastFunctionArgs', + text: 'broadcast [BROADCAST] with data [ARGS] and wait', + blockType: BlockType.REPORTER, + disableMonitor: true, + allowDropAnywhere: true, + arguments: { + BROADCAST: { + type: ArgumentType.STRING, + defaultValue: "your not supposed to see this?" + }, + ARGS: { + type: ArgumentType.STRING, + defaultValue: "abc" + } + } + }, + ], + menus: { + spriteName: "_spriteName", + broadcastMenu: "_broadcastMenu" + } + }; + } + + // menus + _spriteId() { + const emptyMenu = [{ text: '', value: '' }]; + const menu = []; + for (const target of this.runtime.targets) { + if (!target.isOriginal) continue; + if (target.isStage) { + menu.push({ + text: "stage", + value: target.id + }); + continue; + } + menu.push({ + text: target.sprite.name, + value: target.id + }); + } + if (menu.length <= 0) return emptyMenu; + return menu; + } + _spriteName() { + const emptyMenu = [{ text: '', value: '' }]; + const menu = []; + for (const target of this.runtime.targets) { + if (!target.isOriginal) continue; + if (target.isStage) { + menu.push({ + text: "stage", + value: "_stage_" + }); + continue; + } + menu.push({ + text: target.sprite.name, + value: target.sprite.name + }); + } + if (menu.length <= 0) return emptyMenu; + return menu; + } + _broadcastMenu() { + const emptyMenu = [{ text: '', value: '' }]; + const menu = []; + for (const target of this.runtime.targets) { + if (!target.isOriginal) continue; + if (target.isStage) { + menu.push({ + text: "stage", + value: target.id + }); + continue; + } + menu.push({ + text: target.sprite.name, + value: target.id + }); + } + if (menu.length <= 0) return emptyMenu; + return menu; + } + + // blocks + sendWithData(args, util) { + const broadcast = Cast.toString(args.BROADCAST); + const data = Cast.toString(args.DATA); + const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg("", broadcast); + if (broadcastVar) broadcastVar.isSent = true; + + const threads = util.startHats("event_whenbroadcastreceived", { + BROADCAST_OPTION: broadcast + }); + for (const thread of threads) { + thread.__evex_recievedDataa = data; + } + } + broadcastToSprite(args, util) { + const broadcast = Cast.toString(args.BROADCAST); + const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg("", broadcast); + if (broadcastVar) broadcastVar.isSent = true; + + const sprite = Cast.toString(args.SPRITE); + const target = sprite === "_stage_" ? + this.runtime.getTargetForStage() + : this.runtime.getSpriteTargetByName(sprite); + util.startHats("event_whenbroadcastreceived", { + BROADCAST_OPTION: broadcast + }, target); + } + broadcastThreadCount(args, util) { + const broadcast = Cast.toString(args.BROADCAST); + const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg("", broadcast); + if (broadcastVar) broadcastVar.isSent = true; + + const threads = util.startHats("event_whenbroadcastreceived", { + BROADCAST_OPTION: broadcast + }); + return threads.length; + } + recievedDataReporter(_, util) { + return util.thread.__evex_recievedDataa; + } + returnFromBroadcastFunc(args, util) { + util.thread.__evex_returnDataa = args.VALUE; + } + isBroadcastReceived(args, util) { + const broadcast = Cast.toString(args.BROADCAST); + const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg("", broadcast); + return Cast.toBoolean(broadcastVar && broadcastVar.isSent); + } + broadcastFunction() { + return; // compiler block + } + broadcastFunctionArgs() { + return; // compiler block + } +} + +module.exports = pmEventsExpansion; diff --git a/local-scratch-vm/src/extensions/pm_inlineblocks/index.js b/local-scratch-vm/src/extensions/pm_inlineblocks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..75c57e1370d6f5f88694d2b15cfa15481f229039 --- /dev/null +++ b/local-scratch-vm/src/extensions/pm_inlineblocks/index.js @@ -0,0 +1,58 @@ +const blocks = ` + + + + + + 1 + + + + + +` + +/** + * Class of 2024 + * @constructor + */ +class pmInlineBlocks { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + } + + orderCategoryBlocks() { + let categoryBlocks = blocks; + + // let idx = 0; + // for (const block of extensionBlocks) { + // categoryBlocks = categoryBlocks.replace('%b' + idx + '>', block); + // idx++; + // } + + return [categoryBlocks]; + } + + /** + * @returns {object} metadata for deez nuts + * this extension really only exists to seperate the block + */ + getInfo() { + return { + id: 'pmInlineBlocks', + name: 'Inline Blocks', + color1: '#FFAB19', + color2: '#EC9C13', + color3: '#CF8B17', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [] + }; + } +} + +module.exports = pmInlineBlocks; diff --git a/local-scratch-vm/src/extensions/pm_motionExpansion/index.js b/local-scratch-vm/src/extensions/pm_motionExpansion/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3f3f1dc04d9ea3f8dda44f1cae9b93052f60963c --- /dev/null +++ b/local-scratch-vm/src/extensions/pm_motionExpansion/index.js @@ -0,0 +1,430 @@ +// Most of the blocks here are from More Motion by NexusKitten: +// https://scratch.mit.edu/users/NamelessCat/ +// https://github.com/NexusKitten + +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Clone = require('../../util/clone'); +const Cast = require('../../util/cast'); + +const blockSeparator = ''; // At default scale, about 28px + +const blocks = ` +%block6> +%block7> +%block2> +%block3> +${blockSeparator} + + + + 15 + + + + + 0 + + + + + 0 + + + + + + + 15 + + + + + 0 + + + + + 0 + + + + + + + 10 + + + + + 10 + + + +%block1> +${blockSeparator} +%block0> +%block4> +%block5> +` + +/** + * Class of idk + * @constructor + */ +class pmMotionExpansion { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + + this.spriteHomes = {}; + this.cloneHomes = {}; + } + + // cloneHomes contains targetId's which do not save, so dont serialize them + // clones in general dont save anyways so theres no point if we did + deserialize(data) { + this.spriteHomes = data; + } + serialize() { + return this.filterHomes("sprite", this.spriteHomes); + } + + /** + * filter out the homes to only contain existing targets + * @param {string} type clone or sprite + * @param {object} homes sprite or clone homes + * @returns the homes with only the existing targets + */ + filterHomes(type, homes) { + const newHomes = {}; + for (const targetNameOrId in homes) { + let canCopy = true; + if (type === 'clone') { + if (!this.runtime.getTargetById(targetNameOrId)) { + canCopy = false; + } + } else { + if (!this.runtime.getSpriteTargetByName(targetNameOrId)) { + canCopy = false; + } + } + if (canCopy) { + newHomes[targetNameOrId] = homes[targetNameOrId]; + } + } + return newHomes; + } + + orderCategoryBlocks(extensionBlocks) { + if (typeof vm !== "undefined") { + if (vm.editingTarget) { + const target = vm.editingTarget; + if (target.isStage) { + return [``]; + } + } + } + + let categoryBlocks = blocks; + + let idx = 0; + for (const block of extensionBlocks) { + categoryBlocks = categoryBlocks.replace('%block' + idx + '>', block); + idx++; + } + + return [categoryBlocks]; + } + + /** + * @returns {object} metadata for extension + */ + getInfo() { + return { + id: 'pmMotionExpansion', + name: 'Motion Expansion', + color1: '#4C97FF', + color2: '#4280D7', + color3: '#3373CC', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [ + { + opcode: "rotationStyle", + blockType: BlockType.REPORTER, + text: "rotation style", + disableMonitor: true, + }, + { + opcode: "fence", + blockType: BlockType.COMMAND, + text: "manually fence", + }, + { + opcode: "steptowards", + blockType: BlockType.COMMAND, + text: "move [STEPS] steps towards x: [X] y: [Y]", + arguments: { + STEPS: { + type: ArgumentType.NUMBER, + defaultValue: "10", + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + }, + }, + { + opcode: "tweentowards", + blockType: BlockType.COMMAND, + text: "move [PERCENT]% of the way to x: [X] y: [Y]", + arguments: { + PERCENT: { + type: ArgumentType.NUMBER, + defaultValue: "10", + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + }, + }, + { + opcode: "touchingxy", + blockType: BlockType.BOOLEAN, + text: "touching x: [X] y: [Y]?", + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + }, + }, + { + opcode: "touchingrect", + blockType: BlockType.BOOLEAN, + text: "touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?", + arguments: { + X1: { + type: ArgumentType.NUMBER, + defaultValue: "-100", + }, + Y1: { + type: ArgumentType.NUMBER, + defaultValue: "-100", + }, + X2: { + type: ArgumentType.NUMBER, + defaultValue: "100", + }, + Y2: { + type: ArgumentType.NUMBER, + defaultValue: "100", + }, + }, + }, + { + opcode: "setHome", + blockType: BlockType.COMMAND, + text: "set my home", + }, + { + opcode: "gotoHome", + blockType: BlockType.COMMAND, + text: "go to home", + }, + ] + }; + } + + rotationStyle(_, util) { + return util.target.rotationStyle; + } + + fence(_, util) { + const newpos = this.runtime.renderer.getFencedPositionOfDrawable( + util.target.drawableID, + [util.target.x, util.target.y] + ); + util.target.setXY(newpos[0], newpos[1]); + } + + steptowards(args, util) { + const x = Cast.toNumber(args.X); + const y = Cast.toNumber(args.Y); + const steps = Cast.toNumber(args.STEPS); + const val = + steps / Math.sqrt((x - util.target.x) ** 2 + (y - util.target.y) ** 2); + if (val >= 1) { + util.target.setXY(x, y); + } else { + util.target.setXY( + (x - util.target.x) * val + util.target.x, + (y - util.target.y) * val + util.target.y + ); + } + } + + tweentowards(args, util) { + const x = Cast.toNumber(args.X); + const y = Cast.toNumber(args.Y); + const val = Cast.toNumber(args.PERCENT); + // Essentially a smooth glide script. + util.target.setXY( + (x - util.target.x) * (val / 100) + util.target.x, + (y - util.target.y) * (val / 100) + util.target.y + ); + } + + touchingrect(args, util) { + let left = Cast.toNumber(args.X1); + let right = Cast.toNumber(args.X2); + let bottom = Cast.toNumber(args.Y1); + let top = Cast.toNumber(args.Y2); + + // Fix argument order if they got it backwards + if (left > right) { + let temp = left; + left = right; + right = temp; + } + if (bottom > top) { + let temp = bottom; + bottom = top; + bottom = temp; + } + + const drawable = this.runtime.renderer._allDrawables[util.target.drawableID]; + if (!drawable) { + return false; + } + + // See renderer.isTouchingDrawables + + const drawableBounds = drawable.getFastBounds(); + drawableBounds.snapToInt(); + + const Rectangle = this.runtime.renderer.exports.Rectangle; + const containsBounds = new Rectangle(); + containsBounds.initFromBounds(left, right, bottom, top); + containsBounds.snapToInt(); + + if (!containsBounds.intersects(drawableBounds)) { + return false; + } + + drawable.updateCPURenderAttributes(); + + const intersectingBounds = Rectangle.intersect( + drawableBounds, + containsBounds + ); + for (let x = intersectingBounds.left; x < intersectingBounds.right; x++) { + for ( + let y = intersectingBounds.bottom; + y < intersectingBounds.top; + y++ + ) { + // technically should be a twgl vec3, but does not actually need to be + if (drawable.isTouching([x, y])) { + return true; + } + } + } + return false; + } + + touchingxy(args, util) { + const x = Cast.toNumber(args.X); + const y = Cast.toNumber(args.Y); + const drawable = this.runtime.renderer._allDrawables[util.target.drawableID]; + if (!drawable) { + return false; + } + // Position should technically be a twgl vec3, but it doesn't actually need to be + drawable.updateCPURenderAttributes(); + return drawable.isTouching([x, y]); + } + + setHome(_, util) { + const target = util.target; + if (target.isStage) return; + // this is all of the sprite specific data we will save + // variables are a bit too far, and most other data is stage only or shouldnt be overwritten + const savedState = { + x: target.x, + y: target.y, + size: target.size, + stretch: Clone.simple(target.stretch), // array + transform: Clone.simple(target.transform), // array + direction: target.direction, + rotationStyle: target.rotationStyle, + visible: target.visible, + effects: Clone.simple(target.effects), // object + draggable: target.draggable, + currentCostume: target.currentCostume, + tintColor: target.tintColor, + volume: target.volume + }; + if (target.isOriginal) { + const name = target.getName(); + this.spriteHomes[name] = savedState; + this.spriteHomes = this.filterHomes("sprite", this.spriteHomes); + return; + } + this.cloneHomes[target.id] = savedState; + this.cloneHomes = this.filterHomes("clone", this.cloneHomes); + } + gotoHome(_, util) { + const target = util.target; + if (target.isStage) return; + const identifier = target.isOriginal ? target.getName() : target.id; + const homeTable = target.isOriginal ? this.spriteHomes : this.cloneHomes; + // dont do anything if theres no name in here + if (!(identifier in homeTable)) { + return; + } + const homeState = homeTable[identifier]; + if (!homeState) { + return; + } + // set state + target.setXY(homeState.x, homeState.y); + target.setSize(homeState.size); + target.setStretch(...homeState.stretch); + target.setTransform(homeState.transform); + target.setDirection(homeState.direction); + target.setRotationStyle(homeState.rotationStyle); + target.setVisible(homeState.visible); + if (homeState.effects) { + for (const effectName in homeState.effects) { + const value = homeState.effects[effectName]; + target.setEffect(effectName, value); + } + } + target.setDraggable(homeState.draggable); + target.setCostume(homeState.currentCostume); + target.tintColor = homeState.tintColor; // tintColor no longer exists but we'll do this anyways incase it somehow breaks things + target.volume = homeState.volume; + this.runtime.requestRedraw(); + } +} + +module.exports = pmMotionExpansion; diff --git a/local-scratch-vm/src/extensions/pm_operatorsExpansion/index.js b/local-scratch-vm/src/extensions/pm_operatorsExpansion/index.js new file mode 100644 index 0000000000000000000000000000000000000000..57db8988657d3dfb06d77dc748ac936eb07ab97d --- /dev/null +++ b/local-scratch-vm/src/extensions/pm_operatorsExpansion/index.js @@ -0,0 +1,1009 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const createTranslate = require('../../extension-support/tw-l10n'); +const Cast = require('../../util/cast'); +const MathJS = require('./mathjs.js'); + +const blockSeparator = ''; // At default scale, about 28px + +const blocks = ` +%b26> ` +/* left shift */` +%b27> ` +/* right shift */` +%b28> ` +/* binnary and */` +%b29> ` +/* binnary or */` +%b30> ` +/* binnary xor */` +%b31> ` +/* binnary not */` +${blockSeparator} +%b34> ` +/* or if falsey */` +%b35> ` +/* if is true */` +${blockSeparator} + + + + + +${blockSeparator} +%b20> ` +/* evaluate math expression */` + + + + a + + + + + abc abc abc + + + + + + + 1 + + + + + Text with multiple lines here + + + + + + + abcdef + + + + + fgh + + + +${blockSeparator} +%b21> ` +/* set replacer */` +%b22> ` +/* reset replacers */` +%b23> ` +/* use replacers */` +${blockSeparator} +%b24> ` +/* text after () in () */` +%b25> ` +/* text before () in () */` + + + + a + + + + + + + 97 + + + +${blockSeparator} +` +/* new blocks */` +%b18> ` +/* exactly equals */` +${blockSeparator} +%b6> ` +/* part of ratio */` +%b7> ` +/* simplify of ratio */` +${blockSeparator} +%b12> ` +/* is number multiple of number */` +%b15> ` +/* is number even */` +%b13> ` +/* is number int */` +%b14> ` +/* is number prime */` +%b19> ` +/* is number between numbers */` +%b11> ` +/* trunc number */` +${blockSeparator} +%b16> ` +/* reverse text */` +%b17> ` +/* shuffle text */` +${blockSeparator} +%b32> ` +/* speed to pitch */` +%b33> ` +/* pitch to speed */` +${blockSeparator} +` +/* join blocks */` + + + + apple + + + + + banana + + + + + + + apple + + + + + banana + + + + + pear + + + +` +/* extreme join blocks */` +%b0> +%b1> +%b2> +%b3> +%b4> +%b5> +` +/* constants */` +${blockSeparator} +%b8> ` +/* pi */` +%b9> ` +/* euler */` +%b10> ` +/* inf */` +${blockSeparator} +`; + +const translate = createTranslate(vm); +function generateJoin(amount) { + const joinWords = [ + 'apple', + 'banana', + 'pear', + 'orange', + 'mango', + 'strawberry', + 'pineapple', + 'grape', + 'kiwi' + ]; + + const argumentTextArray = []; + const argumentss = {}; + + for (let i = 0; i < amount; i++) { + argumentTextArray.push(`[STRING${i + 1}]`); + argumentss[`STRING${i + 1}`] = { + type: ArgumentType.STRING, + defaultValue: joinWords[i] + ((i === (amount - 1)) ? '' : ' ') + }; + } + + const opcode = `join${amount}`; + const defaultText = `join ${argumentTextArray.join(' ')}`; + + return { + opcode: opcode, + text: translate({ id: opcode, default: defaultText }), + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: argumentss + }; +} + +function generateJoinTranslations(amount, word, type) { + switch (type) { + case 1: + const obj = {}; + for (let i = 0; i < amount; i++) { + let text = `${word} `; + for (let j = 0; j < amount; j++) { + text += `[STRING${j + 1}]`; + } + obj[`join${i + 1}`] = text; + } + return obj; + } +} + +/** + * Class of 2023 + * @constructor + */ +class pmOperatorsExpansion { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + translate.setup({ + "zh-cn": { + ...generateJoinTranslations(9, "连接字符串", 1) + }, + "zh-tw": { + ...generateJoinTranslations(9, "字串組合", 1) + } + }); + this.replacers = Object.create(null); + this.runtime.registerCompiledExtensionBlocks('pmOperatorsExpansion', this.getCompileInfo()); + } + + orderCategoryBlocks(extensionBlocks) { + let categoryBlocks = blocks; + + let idx = 0; + for (const block of extensionBlocks) { + categoryBlocks = categoryBlocks.replace(`%b${idx}>`, block); + idx++; + } + + return [categoryBlocks]; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'pmOperatorsExpansion', + name: 'Operators Expansion', + color1: '#59C059', + color2: '#46B946', + color3: '#389438', + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [ + generateJoin(4), + generateJoin(5), + generateJoin(6), + generateJoin(7), + generateJoin(8), + generateJoin(9), + { + opcode: 'partOfRatio', + text: '[PART] part of ratio [RATIO]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + PART: { + type: ArgumentType.STRING, + menu: "part" + }, + RATIO: { + type: ArgumentType.STRING, + defaultValue: "1:2" + } + } + }, + { + opcode: 'simplifyRatio', + text: 'simplify ratio [RATIO]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + RATIO: { + type: ArgumentType.STRING, + defaultValue: "1:2" + } + } + }, + { + opcode: 'pi', + text: 'π', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'euler', + text: 'e', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'infinity', + text: '∞', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'truncateNumber', + text: 'truncate number [NUM]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + NUM: { + type: ArgumentType.NUMBER, + defaultValue: "2.5" + } + } + }, + { + opcode: 'isNumberMultipleOf', + text: 'is [NUM] multiple of [MULTIPLE]?', + blockType: BlockType.BOOLEAN, + disableMonitor: true, + arguments: { + NUM: { + type: ArgumentType.NUMBER, + defaultValue: "20" + }, + MULTIPLE: { + type: ArgumentType.NUMBER, + defaultValue: "10" + } + } + }, + { + opcode: 'isInteger', + text: 'is [NUM] an integer?', + blockType: BlockType.BOOLEAN, + disableMonitor: true, + arguments: { + NUM: { + type: ArgumentType.NUMBER, + defaultValue: "0.5" + } + } + }, + { + opcode: 'isPrime', + text: 'is [NUM] a prime number?', + blockType: BlockType.BOOLEAN, + disableMonitor: true, + arguments: { + NUM: { + type: ArgumentType.NUMBER, + defaultValue: "13" + } + } + }, + { + opcode: 'isEven', + text: 'is [NUM] even?', + blockType: BlockType.BOOLEAN, + disableMonitor: true, + arguments: { + NUM: { + type: ArgumentType.NUMBER, + defaultValue: "4" + } + } + }, + { + opcode: 'reverseChars', + text: 'reverse [TEXT]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "Hello!" + } + } + }, + { + opcode: 'shuffleChars', + text: 'shuffle [TEXT]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "Hello!" + } + } + }, + { + opcode: 'exactlyEqual', + text: '[ONE] exactly equals [TWO]?', + blockType: BlockType.BOOLEAN, + disableMonitor: true, + arguments: { + ONE: { + type: ArgumentType.STRING, + defaultValue: "a" + }, + TWO: { + type: ArgumentType.STRING, + defaultValue: "b" + } + } + }, + { + opcode: 'betweenNumbers', + text: 'is [NUM] between [MIN] and [MAX]?', + blockType: BlockType.BOOLEAN, + disableMonitor: true, + arguments: { + NUM: { + type: ArgumentType.NUMBER, + defaultValue: 5 + }, + MIN: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + MAX: { + type: ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: 'evaluateMath', + text: 'answer to [EQUATION]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + EQUATION: { + type: ArgumentType.STRING, + defaultValue: "5 * 2" + } + } + }, + { + opcode: 'setReplacer', + text: 'set replacer [REPLACER] to [TEXT]', + blockType: BlockType.COMMAND, + arguments: { + REPLACER: { + type: ArgumentType.STRING, + defaultValue: "${replacer}" + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: "world" + } + } + }, + { + opcode: 'resetReplacers', + text: 'reset replacers', + blockType: BlockType.COMMAND + }, + { + opcode: 'applyReplacers', + text: 'apply replacers to [TEXT]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "Hello ${replacer}!" + } + } + }, + { + opcode: 'textAfter', + text: 'text after [TEXT] in [BASE]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "Hello" + }, + BASE: { + type: ArgumentType.STRING, + defaultValue: "Hello world!" + } + } + }, + { + opcode: 'textBefore', + text: 'text before [TEXT] in [BASE]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "world" + }, + BASE: { + type: ArgumentType.STRING, + defaultValue: "Hello world!" + } + } + }, + { + opcode: 'shiftLeft', + text: '[num1] << [num2]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + num1: { + type: ArgumentType.NUMBER, + defaultValue: "1" + }, + num2: { + type: ArgumentType.NUMBER, + defaultValue: "5" + } + } + }, + { + opcode: 'shiftRight', + text: '[num1] >> [num2]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + num1: { + type: ArgumentType.NUMBER, + defaultValue: "32" + }, + num2: { + type: ArgumentType.NUMBER, + defaultValue: "5" + } + } + }, + { + opcode: 'binnaryAnd', + text: '[num1] & [num2]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + num1: { + type: ArgumentType.NUMBER, + defaultValue: "32" + }, + num2: { + type: ArgumentType.NUMBER, + defaultValue: "5" + } + } + }, + { + opcode: 'binnaryOr', + text: '[num1] | [num2]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + num1: { + type: ArgumentType.NUMBER, + defaultValue: "7" + }, + num2: { + type: ArgumentType.NUMBER, + defaultValue: "8" + } + } + }, + { + opcode: 'binnaryXor', + text: '[num1] ^ [num2]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + num1: { + type: ArgumentType.NUMBER, + defaultValue: "7" + }, + num2: { + type: ArgumentType.NUMBER, + defaultValue: "2" + } + } + }, + { + opcode: 'binnaryNot', + text: '~ [num1]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + num1: { + type: ArgumentType.NUMBER, + defaultValue: "2" + } + } + }, + { + opcode: 'speedToPitch', + text: 'speed [SPEED] to pitch', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + SPEED: { + type: ArgumentType.NUMBER, + defaultValue: "2" + }, + } + }, + { + opcode: 'pitchToSpeed', + text: 'pitch [PITCH] to speed', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + PITCH: { + type: ArgumentType.NUMBER, + defaultValue: "120" + }, + } + }, + { + opcode: 'orIfFalsey', + text: '[ONE] or else [TWO]', + blockType: BlockType.REPORTER, + allowDropAnywhere: true, + disableMonitor: true, + arguments: { + ONE: { + type: ArgumentType.STRING, + defaultValue: "a" + }, + TWO: { + type: ArgumentType.STRING, + defaultValue: "b" + } + } + }, + { + opcode: 'ifIsTruthy', + text: 'if [ONE] is true then [TWO]', + blockType: BlockType.REPORTER, + allowDropAnywhere: true, + disableMonitor: true, + arguments: { + ONE: { + type: ArgumentType.BOOLEAN + }, + TWO: { + type: ArgumentType.STRING, + defaultValue: "perfect!" + } + } + }, + ], + menus: { + part: { + acceptReporters: true, + items: [ + "first", + "last" + ].map(item => ({ text: item, value: item })) + } + } + }; + } + + /** + * This function is used for any compiled blocks in the extension if they exist. + * Data in this function is given to the IR & JS generators. + * Data must be valid otherwise errors may occur. + * @returns {object} functions that create data for compiled blocks. + */ + getCompileInfo() { + return { + ir: { + shiftLeft: (generator, block) => ({ + kind: 'input', + num1: generator.descendInputOfBlock(block, 'num1'), + num2: generator.descendInputOfBlock(block, 'num2') + }), + shiftRight: (generator, block) => ({ + kind: 'input', + num1: generator.descendInputOfBlock(block, 'num1'), + num2: generator.descendInputOfBlock(block, 'num2') + }), + binnaryAnd: (generator, block) => ({ + kind: 'input', + num1: generator.descendInputOfBlock(block, 'num1'), + num2: generator.descendInputOfBlock(block, 'num2') + }), + binnaryOr: (generator, block) => ({ + kind: 'input', + num1: generator.descendInputOfBlock(block, 'num1'), + num2: generator.descendInputOfBlock(block, 'num2') + }), + binnaryXor: (generator, block) => ({ + kind: 'input', + num1: generator.descendInputOfBlock(block, 'num1'), + num2: generator.descendInputOfBlock(block, 'num2') + }), + binnaryNot: (generator, block) => ({ + kind: 'input', + num1: generator.descendInputOfBlock(block, 'num1') + }), + orIfFalsey: (generator, block) => ({ + kind: 'input', + one: generator.descendInputOfBlock(block, 'ONE'), + two: generator.descendInputOfBlock(block, 'TWO') + }), + ifIsTruthy: (generator, block) => ({ + kind: 'input', + one: generator.descendInputOfBlock(block, 'ONE'), + two: generator.descendInputOfBlock(block, 'TWO'), + }), + speedToPitch: (generator, block) => ({ + kind: 'input', + speed: generator.descendInputOfBlock(block, 'SPEED'), + }), + pitchToSpeed: (generator, block) => ({ + kind: 'input', + pitch: generator.descendInputOfBlock(block, 'PITCH'), + }) + }, + js: { + shiftLeft: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const num1 = compiler.descendInput(node.num1).asNumber(); + const num2 = compiler.descendInput(node.num2).asNumber(); + + return new TypedInput(`(${num1} << ${num2})`, TYPE_NUMBER); + }, + shiftRight: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const num1 = compiler.descendInput(node.num1).asNumber(); + const num2 = compiler.descendInput(node.num2).asNumber(); + + return new TypedInput(`(${num1} >> ${num2})`, TYPE_NUMBER); + }, + binnaryAnd: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const num1 = compiler.descendInput(node.num1).asNumber(); + const num2 = compiler.descendInput(node.num2).asNumber(); + + return new TypedInput(`(${num1} & ${num2})`, TYPE_NUMBER); + }, + binnaryOr: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const num1 = compiler.descendInput(node.num1).asNumber(); + const num2 = compiler.descendInput(node.num2).asNumber(); + + return new TypedInput(`(${num1} | ${num2})`, TYPE_NUMBER); + }, + binnaryXor: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const num1 = compiler.descendInput(node.num1).asNumber(); + const num2 = compiler.descendInput(node.num2).asNumber(); + + return new TypedInput(`(${num1} ^ ${num2})`, TYPE_NUMBER); + }, + binnaryNot: (node, compiler, {TypedInput, TYPE_NUMBER}) => { + const num1 = compiler.descendInput(node.num1).asNumber(); + + return new TypedInput(`(~${num1})`, TYPE_NUMBER); + }, + orIfFalsey: (node, compiler, {TypedInput, TYPE_UNKNOWN}) => { + const num1 = compiler.descendInput(node.one).asUnknown(); + const num2 = compiler.descendInput(node.two).asUnknown(); + + return new TypedInput(`(${num1} || ${num2})`, TYPE_UNKNOWN); + }, + ifIsTruthy: (node, compiler, {TypedInput, TYPE_UNKNOWN}) => { + const num1 = compiler.descendInput(node.one).asUnknown(); + const num2 = compiler.descendInput(node.two).asUnknown(); + + return new TypedInput(`(${num1} && ${num2})`, TYPE_UNKNOWN); + }, + speedToPitch: (node, compiler, { TypedInput, TYPE_NUMBER_NAN }) => { + const speed = compiler.descendInput(node.speed).asNumber(); + return new TypedInput(`((1200 * Math.log2(${speed})) / 10)`, TYPE_NUMBER_NAN); + }, + pitchToSpeed: (node, compiler, { TypedInput, TYPE_NUMBER_NAN }) => { + const pitch = compiler.descendInput(node.pitch).asNumber(); + return new TypedInput(`(Math.pow(2, (${pitch} * 10) / 1200))`, TYPE_NUMBER_NAN); + } + } + }; + } + + // util + reduce(numerator, denominator) { + let gcd = function gcd(a, b) { + return b ? gcd(b, a % b) : a; + }; + gcd = gcd(numerator, denominator); + return [numerator / gcd, denominator / gcd]; + } + checkPrime(number) { + number = Math.trunc(number); + if (number <= 1) return false; + for (let i = 2; i < number; i++) { + if (number % i === 0) { + return false; + } + } + return true; + } + + // useful + pi() { + return Math.PI; + } + euler() { + return Math.E; + } + infinity() { + return Infinity; + } + + partOfRatio(args) { + const ratio = Cast.toString(args.RATIO); + const part = Cast.toString(args.PART).toLowerCase(); + + if (!ratio.includes(':')) return ''; + const split = ratio.split(':'); + + const section = split[Number(part === 'last')]; + return Cast.toNumber(section); + } + simplifyRatio(args) { + const ratio = Cast.toString(args.RATIO); + if (!ratio.includes(':')) return ''; + const split = ratio.split(':'); + + const first = Cast.toNumber(split[0]); + const last = Cast.toNumber(split[1]); + + const reduced = this.reduce(first, last); + + return `${Cast.toNumber(reduced[0])}:${Cast.toNumber(reduced[1])}`; + } + + truncateNumber(args) { + const num = Cast.toNumber(args.NUM); + return Math.trunc(num); + } + + isNumberMultipleOf(args) { + const num = Cast.toNumber(args.NUM); + const mult = Cast.toNumber(args.MULTIPLE); + + return (num % mult) === 0; + } + isInteger(args) { + const num = Cast.toNumber(args.NUM); + return Math.trunc(num) === num; + } + isPrime(args) { + const num = Cast.toNumber(args.NUM); + return this.checkPrime(num); + } + isEven(args) { + const num = Cast.toNumber(args.NUM); + return num % 2 == 0; + } + + evaluateMath(args) { + const equation = Cast.toString(args.EQUATION); + // "" is undefined when evalutated + if (equation.trim().length === 0) return 0; + // evalueate + let answer = 0; + try { + answer = MathJS.evaluate(equation); + } catch { + // syntax errors cause real errors + answer = 0; + } + // multiline or semi-colon breaks create a ResultSet, we can get the last item in the set for that + if (typeof answer === "object") { + if ("entries" in answer) { + const answers = answer.entries; + if (answers.length === 0) return 0; + const lastIdx = answers.length - 1; + return Number(answers[lastIdx]); + } + } + // Cast.toNumber converts NaN to 0 + return Number(answer); + } + + exactlyEqual(args) { + // everyone requested this but watch literally no one use it :trollface: + return args.ONE === args.TWO; + } + betweenNumbers(args) { + const number = Cast.toNumber(args.NUM); + let min = Cast.toNumber(args.MIN); + let max = Cast.toNumber(args.MAX); + // check that max isnt less than min + if (max < min) { + const switchover = max; + max = min; + min = switchover; + } + return (number <= max) && (number >= min); + } + + reverseChars(args) { + const text = Cast.toString(args.TEXT); + const split = text.split(''); + return split.reverse().join(''); + } + shuffleChars(args) { + const text = Cast.toString(args.TEXT); + const split = text.split(''); + const shuffled = split.sort(() => Math.random() - 0.5); + return shuffled.join(''); + } + + // join + join4(args) { + return Cast.toString(args.STRING1) + + Cast.toString(args.STRING2) + + Cast.toString(args.STRING3) + + Cast.toString(args.STRING4); + } + join5(args) { + return Cast.toString(args.STRING1) + + Cast.toString(args.STRING2) + + Cast.toString(args.STRING3) + + Cast.toString(args.STRING4) + + Cast.toString(args.STRING5); + } + join6(args) { + return Cast.toString(args.STRING1) + + Cast.toString(args.STRING2) + + Cast.toString(args.STRING3) + + Cast.toString(args.STRING4) + + Cast.toString(args.STRING5) + + Cast.toString(args.STRING6); + } + join7(args) { + return Cast.toString(args.STRING1) + + Cast.toString(args.STRING2) + + Cast.toString(args.STRING3) + + Cast.toString(args.STRING4) + + Cast.toString(args.STRING5) + + Cast.toString(args.STRING6) + + Cast.toString(args.STRING7); + } + join8(args) { + return Cast.toString(args.STRING1) + + Cast.toString(args.STRING2) + + Cast.toString(args.STRING3) + + Cast.toString(args.STRING4) + + Cast.toString(args.STRING5) + + Cast.toString(args.STRING6) + + Cast.toString(args.STRING7) + + Cast.toString(args.STRING8); + } + join9(args) { + return Cast.toString(args.STRING1) + + Cast.toString(args.STRING2) + + Cast.toString(args.STRING3) + + Cast.toString(args.STRING4) + + Cast.toString(args.STRING5) + + Cast.toString(args.STRING6) + + Cast.toString(args.STRING7) + + Cast.toString(args.STRING8) + + Cast.toString(args.STRING9); + } + + setReplacer(args) { + const replacer = Cast.toString(args.REPLACER); + const text = Cast.toString(args.TEXT); + this.replacers[replacer] = text; + } + resetReplacers() { + this.replacers = Object.create(null); + } + applyReplacers(args) { + let text = Cast.toString(args.TEXT); + for (const replacer in this.replacers) { + const replacementText = this.replacers[replacer]; + text = text.replaceAll(replacer, replacementText); + } + return text; + } + + textAfter(args) { + const text = Cast.toString(args.TEXT); + const base = Cast.toString(args.BASE); + const idx = base.indexOf(text); + if (idx < 0) return ''; + return base.substring(idx + text.length); + } + textBefore(args) { + const text = Cast.toString(args.TEXT); + const base = Cast.toString(args.BASE); + const idx = base.indexOf(text); + if (idx < 0) return ''; + return base.substring(0, idx); + } + + // These blocks are compiled + orIfFalsey(args) { return "" } + ifIsTruthy(args) { return "" } + shiftLeft(args) { return "" } + shiftRight(args) { return "" } + binnaryAnd(args) { return false } + binnaryOr(args) { return false } + binnaryXor(args) { return false } + binnaryNot(args) { return false } + speedToPitch(args) { return 0 } + pitchToSpeed(args) { return 1 } +} + +module.exports = pmOperatorsExpansion; diff --git a/local-scratch-vm/src/extensions/pm_operatorsExpansion/mathjs.js b/local-scratch-vm/src/extensions/pm_operatorsExpansion/mathjs.js new file mode 100644 index 0000000000000000000000000000000000000000..9b003deed74642f87c35c15a03b40826c03bc1b5 --- /dev/null +++ b/local-scratch-vm/src/extensions/pm_operatorsExpansion/mathjs.js @@ -0,0 +1,51 @@ +/*! + * decimal.js v10.4.3 + * An arbitrary-precision Decimal type for JavaScript. + * https://github.com/MikeMcl/decimal.js + * Copyright (c) 2022 Michael Mclaughlin + * MIT License + */ + +/*! + * @license Complex.js v2.1.1 12/05/2020 + * + * Copyright (c) 2020, Robert Eisele (robert@xarg.org) + * Dual licensed under the MIT or GPL Version 2 licenses. + **/ + +/*! + * @license Fraction.js v4.3.7 31/08/2023 + * https://www.xarg.org/2014/03/rational-numbers-in-javascript/ + * + * Copyright (c) 2023, Robert Eisele (robert@raw.org) + * Dual licensed under the MIT or GPL Version 2 licenses. + **/ + +/*! + * math.js + * https://github.com/josdejong/mathjs + * + * Math.js is an extensive math library for JavaScript and Node.js, + * It features real and complex numbers, units, matrices, a large set of + * mathematical functions, and a flexible expression parser. + * + * @version 13.2.0 + * @date 2024-10-02 + * + * @license + * Copyright (C) 2013-2024 Jos de Jong + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.math=t():e.math=t()}(this,(()=>(()=>{var e={1977:function(e,t){var r;!function(n){"use strict";var i=Math.cosh||function(e){return Math.abs(e)<1e-9?1-e:.5*(Math.exp(e)+Math.exp(-e))},a=Math.sinh||function(e){return Math.abs(e)<1e-9?e:.5*(Math.exp(e)-Math.exp(-e))},o=function(){throw SyntaxError("Invalid Param")};function u(e,t){var r=Math.abs(e),n=Math.abs(t);return 0===e?Math.log(n):0===t?Math.log(r):r<3e3&&n<3e3?.5*Math.log(e*e+t*t):(e/=2,t/=2,.5*Math.log(e*e+t*t)+Math.LN2)}function s(e,t){if(!(this instanceof s))return new s(e,t);var r=function(e,t){var r={re:0,im:0};if(null==e)r.re=r.im=0;else if(void 0!==t)r.re=e,r.im=t;else switch(typeof e){case"object":if("im"in e&&"re"in e)r.re=e.re,r.im=e.im;else if("abs"in e&&"arg"in e){if(!Number.isFinite(e.abs)&&Number.isFinite(e.arg))return s.INFINITY;r.re=e.abs*Math.cos(e.arg),r.im=e.abs*Math.sin(e.arg)}else if("r"in e&&"phi"in e){if(!Number.isFinite(e.r)&&Number.isFinite(e.phi))return s.INFINITY;r.re=e.r*Math.cos(e.phi),r.im=e.r*Math.sin(e.phi)}else 2===e.length?(r.re=e[0],r.im=e[1]):o();break;case"string":r.im=r.re=0;var n=e.match(/\d+\.?\d*e[+-]?\d+|\d+\.?\d*|\.\d+|./g),i=1,a=0;null===n&&o();for(var u=0;u0&&o();break;case"number":r.im=0,r.re=e;break;default:o()}return isNaN(r.re)||isNaN(r.im),r}(e,t);this.re=r.re,this.im=r.im}s.prototype={re:0,im:0,sign:function(){var e=this.abs();return new s(this.re/e,this.im/e)},add:function(e,t){var r=new s(e,t);return this.isInfinite()&&r.isInfinite()?s.NAN:this.isInfinite()||r.isInfinite()?s.INFINITY:new s(this.re+r.re,this.im+r.im)},sub:function(e,t){var r=new s(e,t);return this.isInfinite()&&r.isInfinite()?s.NAN:this.isInfinite()||r.isInfinite()?s.INFINITY:new s(this.re-r.re,this.im-r.im)},mul:function(e,t){var r=new s(e,t);return this.isInfinite()&&r.isZero()||this.isZero()&&r.isInfinite()?s.NAN:this.isInfinite()||r.isInfinite()?s.INFINITY:0===r.im&&0===this.im?new s(this.re*r.re,0):new s(this.re*r.re-this.im*r.im,this.re*r.im+this.im*r.re)},div:function(e,t){var r=new s(e,t);if(this.isZero()&&r.isZero()||this.isInfinite()&&r.isInfinite())return s.NAN;if(this.isInfinite()||r.isZero())return s.INFINITY;if(this.isZero()||r.isInfinite())return s.ZERO;e=this.re,t=this.im;var n,i,a=r.re,o=r.im;return 0===o?new s(e/a,t/a):Math.abs(a)0)return new s(Math.pow(e,r.re),0);if(0===e)switch((r.re%4+4)%4){case 0:return new s(Math.pow(t,r.re),0);case 1:return new s(0,Math.pow(t,r.re));case 2:return new s(-Math.pow(t,r.re),0);case 3:return new s(0,-Math.pow(t,r.re))}}if(0===e&&0===t&&r.re>0&&r.im>=0)return s.ZERO;var n=Math.atan2(t,e),i=u(e,t);return e=Math.exp(r.re*i-r.im*n),t=r.im*i+r.re*n,new s(e*Math.cos(t),e*Math.sin(t))},sqrt:function(){var e,t,r=this.re,n=this.im,i=this.abs();if(r>=0){if(0===n)return new s(Math.sqrt(r),0);e=.5*Math.sqrt(2*(i+r))}else e=Math.abs(n)/Math.sqrt(2*(i-r));return t=r<=0?.5*Math.sqrt(2*(i-r)):Math.abs(n)/Math.sqrt(2*(i+r)),new s(e,n<0?-t:t)},exp:function(){var e=Math.exp(this.re);return this.im,new s(e*Math.cos(this.im),e*Math.sin(this.im))},expm1:function(){var e=this.re,t=this.im;return new s(Math.expm1(e)*Math.cos(t)+function(e){var t=Math.PI/4;if(-t>e||e>t)return Math.cos(e)-1;var r=e*e;return r*(r*(r*(r*(r*(r*(r*(r/20922789888e3-1/87178291200)+1/479001600)-1/3628800)+1/40320)-1/720)+1/24)-.5)}(t),Math.exp(e)*Math.sin(t))},log:function(){var e=this.re,t=this.im;return new s(u(e,t),Math.atan2(t,e))},abs:function(){return e=this.re,t=this.im,r=Math.abs(e),n=Math.abs(t),r<3e3&&n<3e3?Math.sqrt(r*r+n*n):(r1&&0===t,n=1-e,i=1+e,a=n*n+t*t,o=0!==a?new s((i*n-t*t)/a,(t*n+i*t)/a):new s(-1!==e?e/0:0,0!==t?t/0:0),c=o.re;return o.re=u(o.re,o.im)/2,o.im=Math.atan2(o.im,c)/2,r&&(o.im=-o.im),o},acoth:function(){var e=this.re,t=this.im;if(0===e&&0===t)return new s(0,Math.PI/2);var r=e*e+t*t;return 0!==r?new s(e/r,-t/r).atanh():new s(0!==e?e/0:0,0!==t?-t/0:0).atanh()},acsch:function(){var e=this.re,t=this.im;if(0===t)return new s(0!==e?Math.log(e+Math.sqrt(e*e+1)):1/0,0);var r=e*e+t*t;return 0!==r?new s(e/r,-t/r).asinh():new s(0!==e?e/0:0,0!==t?-t/0:0).asinh()},asech:function(){var e=this.re,t=this.im;if(this.isZero())return s.INFINITY;var r=e*e+t*t;return 0!==r?new s(e/r,-t/r).acosh():new s(0!==e?e/0:0,0!==t?-t/0:0).acosh()},inverse:function(){if(this.isZero())return s.INFINITY;if(this.isInfinite())return s.ZERO;var e=this.re,t=this.im,r=e*e+t*t;return new s(e/r,-t/r)},conjugate:function(){return new s(this.re,-this.im)},neg:function(){return new s(-this.re,-this.im)},ceil:function(e){return e=Math.pow(10,e||0),new s(Math.ceil(this.re*e)/e,Math.ceil(this.im*e)/e)},floor:function(e){return e=Math.pow(10,e||0),new s(Math.floor(this.re*e)/e,Math.floor(this.im*e)/e)},round:function(e){return e=Math.pow(10,e||0),new s(Math.round(this.re*e)/e,Math.round(this.im*e)/e)},equals:function(e,t){var r=new s(e,t);return Math.abs(r.re-this.re)<=s.EPSILON&&Math.abs(r.im-this.im)<=s.EPSILON},clone:function(){return new s(this.re,this.im)},toString:function(){var e=this.re,t=this.im,r="";return this.isNaN()?"NaN":this.isInfinite()?"Infinity":(Math.abs(e){"use strict";var t=Object.assign||function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{},o=a.preserveFormatting,u=void 0!==o&&o,s=a.escapeMapFn,c=void 0===s?i:s,f=String(e),l="",p=c(t({},r),u?t({},n):{}),m=Object.keys(p),h=function(){var e=!1;m.forEach((function(t,r){e||f.length>=t.length&&f.slice(0,t.length)===t&&(l+=p[m[r]],f=f.slice(t.length,f.length),e=!0)})),e||(l+=f.slice(0,1),f=f.slice(1,f.length))};f;)h();return l}},5628:function(e){!function(t){"use strict";var r={s:1,n:0,d:1};function n(e,t){if(isNaN(e=parseInt(e,10)))throw f();return e*t}function i(e,t){if(0===t)throw c();var r=Object.create(s.prototype);r.s=e<0?-1:1;var n=u(e=e<0?-e:e,t);return r.n=e/n,r.d=t/n,r}function a(e){for(var t={},r=e,n=2,i=4;i<=r;){for(;r%n==0;)r/=n,t[n]=(t[n]||0)+1;i+=1+2*n++}return r!==e?r>1&&(t[r]=(t[r]||0)+1):t[e]=(t[e]||0)+1,t}var o=function(e,t){var i,a=0,o=1,u=1,s=0,p=0,m=0,h=1,d=1,v=0,y=1,g=1,x=1,b=1e7;if(null==e);else if(void 0!==t){if(u=(a=e)*(o=t),a%1!=0||o%1!=0)throw l()}else switch(typeof e){case"object":if("d"in e&&"n"in e)a=e.n,o=e.d,"s"in e&&(a*=e.s);else{if(!(0 in e))throw f();a=e[0],1 in e&&(o=e[1])}u=a*o;break;case"number":if(e<0&&(u=e,e=-e),e%1==0)a=e;else if(e>0){for(e>=1&&(e/=d=Math.pow(10,Math.floor(1+Math.log(e)/Math.LN10)));y<=b&&x<=b;){if(e===(i=(v+g)/(y+x))){y+x<=b?(a=v+g,o=y+x):x>y?(a=g,o=x):(a=v,o=y);break}e>i?(v+=g,y+=x):(g+=v,x+=y),y>b?(a=g,o=x):(a=v,o=y)}a*=d}else(isNaN(e)||isNaN(t))&&(o=a=NaN);break;case"string":if(null===(y=e.match(/\d+|./g)))throw f();if("-"===y[v]?(u=-1,v++):"+"===y[v]&&v++,y.length===v+1?p=n(y[v++],u):"."===y[v+1]||"."===y[v]?("."!==y[v]&&(s=n(y[v++],u)),(1+ ++v===y.length||"("===y[v+1]&&")"===y[v+3]||"'"===y[v+1]&&"'"===y[v+3])&&(p=n(y[v],u),h=Math.pow(10,y[v].length),v++),("("===y[v]&&")"===y[v+2]||"'"===y[v]&&"'"===y[v+2])&&(m=n(y[v+1],u),d=Math.pow(10,y[v+1].length)-1,v+=3)):"/"===y[v+1]||":"===y[v+1]?(p=n(y[v],u),h=n(y[v+2],1),v+=3):"/"===y[v+3]&&" "===y[v+1]&&(s=n(y[v],u),p=n(y[v+2],u),h=n(y[v+4],1),v+=5),y.length<=v){u=a=m+(o=h*d)*s+d*p;break}default:throw f()}if(0===o)throw c();r.s=u<0?-1:1,r.n=Math.abs(a),r.d=Math.abs(o)};function u(e,t){if(!e)return t;if(!t)return e;for(;;){if(!(e%=t))return t;if(!(t%=e))return e}}function s(e,t){if(o(e,t),!(this instanceof s))return i(r.s*r.n,r.d);e=u(r.d,r.n),this.s=r.s,this.n=r.n/e,this.d=r.d/e}var c=function(){return new Error("Division by Zero")},f=function(){return new Error("Invalid argument")},l=function(){return new Error("Parameters must be integer")};s.prototype={s:1,n:0,d:1,abs:function(){return i(this.n,this.d)},neg:function(){return i(-this.s*this.n,this.d)},add:function(e,t){return o(e,t),i(this.s*this.n*r.d+r.s*this.d*r.n,this.d*r.d)},sub:function(e,t){return o(e,t),i(this.s*this.n*r.d-r.s*this.d*r.n,this.d*r.d)},mul:function(e,t){return o(e,t),i(this.s*r.s*this.n*r.n,this.d*r.d)},div:function(e,t){return o(e,t),i(this.s*r.s*this.n*r.d,this.d*r.n)},clone:function(){return i(this.s*this.n,this.d)},mod:function(e,t){if(isNaN(this.n)||isNaN(this.d))return new s(NaN);if(void 0===e)return i(this.s*this.n%this.d,1);if(o(e,t),0===r.n&&0===this.d)throw c();return i(this.s*(r.d*this.n)%(r.n*this.d),r.d*this.d)},gcd:function(e,t){return o(e,t),i(u(r.n,this.n)*u(r.d,this.d),r.d*this.d)},lcm:function(e,t){return o(e,t),0===r.n&&0===this.n?i(0,1):i(r.n*this.n,u(r.n,this.n)*u(r.d,this.d))},ceil:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new s(NaN):i(Math.ceil(e*this.s*this.n/this.d),e)},floor:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new s(NaN):i(Math.floor(e*this.s*this.n/this.d),e)},round:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new s(NaN):i(Math.round(e*this.s*this.n/this.d),e)},inverse:function(){return i(this.s*this.d,this.n)},pow:function(e,t){if(o(e,t),1===r.d)return r.s<0?i(Math.pow(this.s*this.d,r.n),Math.pow(this.n,r.n)):i(Math.pow(this.s*this.n,r.n),Math.pow(this.d,r.n));if(this.s<0)return null;var n=a(this.n),u=a(this.d),s=1,c=1;for(var f in n)if("1"!==f){if("0"===f){s=0;break}if(n[f]*=r.n,n[f]%r.d!=0)return null;n[f]/=r.d,s*=Math.pow(f,n[f])}for(var f in u)if("1"!==f){if(u[f]*=r.n,u[f]%r.d!=0)return null;u[f]/=r.d,c*=Math.pow(f,u[f])}return r.s<0?i(c,s):i(s,c)},equals:function(e,t){return o(e,t),this.s*this.n*r.d==r.s*r.n*this.d},compare:function(e,t){o(e,t);var n=this.s*this.n*r.d-r.s*r.n*this.d;return(0=0;o--)a=a.inverse().add(r[o]);if(Math.abs(a.sub(t).valueOf())0&&(r+=t,r+=" ",n%=i),r+=n,r+="/",r+=i),r},toLatex:function(e){var t,r="",n=this.n,i=this.d;return this.s<0&&(r+="-"),1===i?r+=n:(e&&(t=Math.floor(n/i))>0&&(r+=t,n%=i),r+="\\frac{",r+=n,r+="}{",r+=i,r+="}"),r},toContinued:function(){var e,t=this.n,r=this.d,n=[];if(isNaN(t)||isNaN(r))return n;do{n.push(Math.floor(t/r)),e=t%r,t=r,r=e}while(1!==t);return n},toString:function(e){var t=this.n,r=this.d;if(isNaN(t)||isNaN(r))return"NaN";e=e||15;var n=function(e,t){for(;t%2==0;t/=2);for(;t%5==0;t/=5);if(1===t)return 0;for(var r=10%t,n=1;1!==r;n++)if(r=10*r%t,n>2e3)return 0;return n}(0,r),i=function(e,t,r){for(var n=1,i=function(e,t,r){for(var n=1;t>0;e=e*e%r,t>>=1)1&t&&(n=n*e%r);return n}(10,r,t),a=0;a<300;a++){if(n===i)return a;n=10*n%t,i=10*i%t}return 0}(0,r,n),a=this.s<0?"-":"";if(a+=t/r|0,t%=r,(t*=10)&&(a+="."),n){for(var o=i;o--;)a+=t/r|0,t%=r,t*=10;for(a+="(",o=n;o--;)a+=t/r|0,t%=r,t*=10;a+=")"}else for(o=e;t&&o--;)a+=t/r|0,t%=r,t*=10;return a}},Object.defineProperty(s,"__esModule",{value:!0}),s.default=s,s.Fraction=s,e.exports=s}()},3228:e=>{e.exports=function e(t,r){"use strict";var n,i,a=/(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi,o=/(^[ ]*|[ ]*$)/g,u=/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,s=/^0x[0-9a-f]+$/i,c=/^0/,f=function(t){return e.insensitive&&(""+t).toLowerCase()||""+t},l=f(t).replace(o,"")||"",p=f(r).replace(o,"")||"",m=l.replace(a,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0"),h=p.replace(a,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0"),d=parseInt(l.match(s),16)||1!==m.length&&l.match(u)&&Date.parse(l),v=parseInt(p.match(s),16)||d&&p.match(u)&&Date.parse(p)||null;if(v){if(dv)return 1}for(var y=0,g=Math.max(m.length,h.length);yi)return 1}return 0}},6377:(e,t,r)=>{var n=r(4832),i=r(8652),a=r(801),o=r(2030),u=r(3618),s=r(9049),c=r(1971);c.alea=n,c.xor128=i,c.xorwow=a,c.xorshift7=o,c.xor4096=u,c.tychei=s,e.exports=c},4832:function(e,t,r){var n;!function(e,i,a){function o(e){var t,r=this,n=(t=4022871197,function(e){e=String(e);for(var r=0;r>>0,t=(n*=t)>>>0,t+=4294967296*(n-=t)}return 2.3283064365386963e-10*(t>>>0)});r.next=function(){var e=2091639*r.s0+2.3283064365386963e-10*r.c;return r.s0=r.s1,r.s1=r.s2,r.s2=e-(r.c=0|e)},r.c=1,r.s0=n(" "),r.s1=n(" "),r.s2=n(" "),r.s0-=n(e),r.s0<0&&(r.s0+=1),r.s1-=n(e),r.s1<0&&(r.s1+=1),r.s2-=n(e),r.s2<0&&(r.s2+=1),n=null}function u(e,t){return t.c=e.c,t.s0=e.s0,t.s1=e.s1,t.s2=e.s2,t}function s(e,t){var r=new o(e),n=t&&t.state,i=r.next;return i.int32=function(){return 4294967296*r.next()|0},i.double=function(){return i()+11102230246251565e-32*(2097152*i()|0)},i.quick=i,n&&("object"==typeof n&&u(n,r),i.state=function(){return u(r,{})}),i}i&&i.exports?i.exports=s:r.amdD&&r.amdO?void 0===(n=function(){return s}.call(t,r,t,i))||(i.exports=n):this.alea=s}(0,e=r.nmd(e),r.amdD)},9049:function(e,t,r){var n;!function(e,i,a){function o(e){var t=this,r="";t.next=function(){var e=t.b,r=t.c,n=t.d,i=t.a;return e=e<<25^e>>>7^r,r=r-n|0,n=n<<24^n>>>8^i,i=i-e|0,t.b=e=e<<20^e>>>12^r,t.c=r=r-n|0,t.d=n<<16^r>>>16^i,t.a=i-e|0},t.a=0,t.b=0,t.c=-1640531527,t.d=1367130551,e===Math.floor(e)?(t.a=e/4294967296|0,t.b=0|e):r+=e;for(var n=0;n>>0)/4294967296};return i.double=function(){do{var e=((r.next()>>>11)+(r.next()>>>0)/4294967296)/(1<<21)}while(0===e);return e},i.int32=r.next,i.quick=i,n&&("object"==typeof n&&u(n,r),i.state=function(){return u(r,{})}),i}i&&i.exports?i.exports=s:r.amdD&&r.amdO?void 0===(n=function(){return s}.call(t,r,t,i))||(i.exports=n):this.tychei=s}(0,e=r.nmd(e),r.amdD)},8652:function(e,t,r){var n;!function(e,i,a){function o(e){var t=this,r="";t.x=0,t.y=0,t.z=0,t.w=0,t.next=function(){var e=t.x^t.x<<11;return t.x=t.y,t.y=t.z,t.z=t.w,t.w^=t.w>>>19^e^e>>>8},e===(0|e)?t.x=e:r+=e;for(var n=0;n>>0)/4294967296};return i.double=function(){do{var e=((r.next()>>>11)+(r.next()>>>0)/4294967296)/(1<<21)}while(0===e);return e},i.int32=r.next,i.quick=i,n&&("object"==typeof n&&u(n,r),i.state=function(){return u(r,{})}),i}i&&i.exports?i.exports=s:r.amdD&&r.amdO?void 0===(n=function(){return s}.call(t,r,t,i))||(i.exports=n):this.xor128=s}(0,e=r.nmd(e),r.amdD)},3618:function(e,t,r){var n;!function(e,i,a){function o(e){var t=this;t.next=function(){var e,r,n=t.w,i=t.X,a=t.i;return t.w=n=n+1640531527|0,r=i[a+34&127],e=i[a=a+1&127],r^=r<<13,e^=e<<17,r^=r>>>15,e^=e>>>12,r=i[a]=r^e,t.i=a,r+(n^n>>>16)|0},function(e,t){var r,n,i,a,o,u=[],s=128;for(t===(0|t)?(n=t,t=null):(t+="\0",n=0,s=Math.max(s,t.length)),i=0,a=-32;a>>15,n^=n<<4,n^=n>>>13,a>=0&&(o=o+1640531527|0,i=0==(r=u[127&a]^=n+o)?i+1:0);for(i>=128&&(u[127&(t&&t.length||0)]=-1),i=127,a=512;a>0;--a)n=u[i+34&127],r=u[i=i+1&127],n^=n<<13,r^=r<<17,n^=n>>>15,r^=r>>>12,u[i]=n^r;e.w=o,e.X=u,e.i=i}(t,e)}function u(e,t){return t.i=e.i,t.w=e.w,t.X=e.X.slice(),t}function s(e,t){null==e&&(e=+new Date);var r=new o(e),n=t&&t.state,i=function(){return(r.next()>>>0)/4294967296};return i.double=function(){do{var e=((r.next()>>>11)+(r.next()>>>0)/4294967296)/(1<<21)}while(0===e);return e},i.int32=r.next,i.quick=i,n&&(n.X&&u(n,r),i.state=function(){return u(r,{})}),i}i&&i.exports?i.exports=s:r.amdD&&r.amdO?void 0===(n=function(){return s}.call(t,r,t,i))||(i.exports=n):this.xor4096=s}(0,e=r.nmd(e),r.amdD)},2030:function(e,t,r){var n;!function(e,i,a){function o(e){var t=this;t.next=function(){var e,r,n=t.x,i=t.i;return e=n[i],r=(e^=e>>>7)^e<<24,r^=(e=n[i+1&7])^e>>>10,r^=(e=n[i+3&7])^e>>>3,r^=(e=n[i+4&7])^e<<7,e=n[i+7&7],r^=(e^=e<<13)^e<<9,n[i]=r,t.i=i+1&7,r},function(e,t){var r,n=[];if(t===(0|t))n[0]=t;else for(t=""+t,r=0;r0;--r)e.next()}(t,e)}function u(e,t){return t.x=e.x.slice(),t.i=e.i,t}function s(e,t){null==e&&(e=+new Date);var r=new o(e),n=t&&t.state,i=function(){return(r.next()>>>0)/4294967296};return i.double=function(){do{var e=((r.next()>>>11)+(r.next()>>>0)/4294967296)/(1<<21)}while(0===e);return e},i.int32=r.next,i.quick=i,n&&(n.x&&u(n,r),i.state=function(){return u(r,{})}),i}i&&i.exports?i.exports=s:r.amdD&&r.amdO?void 0===(n=function(){return s}.call(t,r,t,i))||(i.exports=n):this.xorshift7=s}(0,e=r.nmd(e),r.amdD)},801:function(e,t,r){var n;!function(e,i,a){function o(e){var t=this,r="";t.next=function(){var e=t.x^t.x>>>2;return t.x=t.y,t.y=t.z,t.z=t.w,t.w=t.v,(t.d=t.d+362437|0)+(t.v=t.v^t.v<<4^e^e<<1)|0},t.x=0,t.y=0,t.z=0,t.w=0,t.v=0,e===(0|e)?t.x=e:r+=e;for(var n=0;n>>4),t.next()}function u(e,t){return t.x=e.x,t.y=e.y,t.z=e.z,t.w=e.w,t.v=e.v,t.d=e.d,t}function s(e,t){var r=new o(e),n=t&&t.state,i=function(){return(r.next()>>>0)/4294967296};return i.double=function(){do{var e=((r.next()>>>11)+(r.next()>>>0)/4294967296)/(1<<21)}while(0===e);return e},i.int32=r.next,i.quick=i,n&&("object"==typeof n&&u(n,r),i.state=function(){return u(r,{})}),i}i&&i.exports?i.exports=s:r.amdD&&r.amdO?void 0===(n=function(){return s}.call(t,r,t,i))||(i.exports=n):this.xorwow=s}(0,e=r.nmd(e),r.amdD)},1971:function(e,t,r){var n;!function(i,a,o){var u,s=256,c=o.pow(s,6),f=o.pow(2,52),l=2*f,p=s-1;function m(e,t,r){var n=[],p=y(v((t=1==t?{entropy:!0}:t||{}).entropy?[e,g(a)]:null==e?function(){try{var e;return u&&(e=u.randomBytes)?e=e(s):(e=new Uint8Array(s),(i.crypto||i.msCrypto).getRandomValues(e)),g(e)}catch(e){var t=i.navigator,r=t&&t.plugins;return[+new Date,i,r,i.screen,g(a)]}}():e,3),n),m=new h(n),x=function(){for(var e=m.g(6),t=c,r=0;e=l;)e/=2,t/=2,r>>>=1;return(e+r)/t};return x.int32=function(){return 0|m.g(4)},x.quick=function(){return m.g(4)/4294967296},x.double=x,y(g(m.S),a),(t.pass||r||function(e,t,r,n){return n&&(n.S&&d(n,m),e.state=function(){return d(m,{})}),r?(o.random=e,t):e})(x,p,"global"in t?t.global:this==o,t.state)}function h(e){var t,r=e.length,n=this,i=0,a=n.i=n.j=0,o=n.S=[];for(r||(e=[r++]);i{function t(){}t.prototype={on:function(e,t,r){var n=this.e||(this.e={});return(n[e]||(n[e]=[])).push({fn:t,ctx:r}),this},once:function(e,t,r){var n=this;function i(){n.off(e,i),t.apply(r,arguments)}return i._=t,this.on(e,i,r)},emit:function(e){for(var t=[].slice.call(arguments,1),r=((this.e||(this.e={}))[e]||[]).slice(),n=0,i=r.length;n{},7061:(e,t,r)=>{var n=r(8698).default;function i(){"use strict";e.exports=i=function(){return r},e.exports.__esModule=!0,e.exports.default=e.exports;var t,r={},a=Object.prototype,o=a.hasOwnProperty,u=Object.defineProperty||function(e,t,r){e[t]=r.value},s="function"==typeof Symbol?Symbol:{},c=s.iterator||"@@iterator",f=s.asyncIterator||"@@asyncIterator",l=s.toStringTag||"@@toStringTag";function p(e,t,r){return Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{p({},"")}catch(t){p=function(e,t,r){return e[t]=r}}function m(e,t,r,n){var i=t&&t.prototype instanceof b?t:b,a=Object.create(i.prototype),o=new _(n||[]);return u(a,"_invoke",{value:F(e,r,o)}),a}function h(e,t,r){try{return{type:"normal",arg:e.call(t,r)}}catch(e){return{type:"throw",arg:e}}}r.wrap=m;var d="suspendedStart",v="suspendedYield",y="executing",g="completed",x={};function b(){}function w(){}function N(){}var D={};p(D,c,(function(){return this}));var E=Object.getPrototypeOf,A=E&&E(E(k([])));A&&A!==a&&o.call(A,c)&&(D=A);var S=N.prototype=b.prototype=Object.create(D);function C(e){["next","throw","return"].forEach((function(t){p(e,t,(function(e){return this._invoke(t,e)}))}))}function M(e,t){function r(i,a,u,s){var c=h(e[i],e,a);if("throw"!==c.type){var f=c.arg,l=f.value;return l&&"object"==n(l)&&o.call(l,"__await")?t.resolve(l.__await).then((function(e){r("next",e,u,s)}),(function(e){r("throw",e,u,s)})):t.resolve(l).then((function(e){f.value=e,u(f)}),(function(e){return r("throw",e,u,s)}))}s(c.arg)}var i;u(this,"_invoke",{value:function(e,n){function a(){return new t((function(t,i){r(e,n,t,i)}))}return i=i?i.then(a,a):a()}})}function F(e,r,n){var i=d;return function(a,o){if(i===y)throw new Error("Generator is already running");if(i===g){if("throw"===a)throw o;return{value:t,done:!0}}for(n.method=a,n.arg=o;;){var u=n.delegate;if(u){var s=O(u,n);if(s){if(s===x)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(i===d)throw i=g,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);i=y;var c=h(e,r,n);if("normal"===c.type){if(i=n.done?g:v,c.arg===x)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(i=g,n.method="throw",n.arg=c.arg)}}}function O(e,r){var n=r.method,i=e.iterator[n];if(i===t)return r.delegate=null,"throw"===n&&e.iterator.return&&(r.method="return",r.arg=t,O(e,r),"throw"===r.method)||"return"!==n&&(r.method="throw",r.arg=new TypeError("The iterator does not provide a '"+n+"' method")),x;var a=h(i,e.iterator,r.arg);if("throw"===a.type)return r.method="throw",r.arg=a.arg,r.delegate=null,x;var o=a.arg;return o?o.done?(r[e.resultName]=o.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,x):o:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,x)}function T(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function B(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function _(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(T,this),this.reset(!0)}function k(e){if(e||""===e){var r=e[c];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var i=-1,a=function r(){for(;++i=0;--i){var a=this.tryEntries[i],u=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var s=o.call(a,"catchLoc"),c=o.call(a,"finallyLoc");if(s&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&o.call(n,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),B(r),x}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var i=n.arg;B(r)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:k(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),x}},r}e.exports=i,e.exports.__esModule=!0,e.exports.default=e.exports},8698:e=>{function t(r){return e.exports=t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e.exports.__esModule=!0,e.exports.default=e.exports,t(r)}e.exports=t,e.exports.__esModule=!0,e.exports.default=e.exports},4687:(e,t,r)=>{var n=r(7061)();e.exports=n;try{regeneratorRuntime=n}catch(e){"object"==typeof globalThis?globalThis.regeneratorRuntime=n:Function("r","regeneratorRuntime = r")(n)}},509:(e,t,r)=>{"use strict";var n=r(9985),i=r(3691),a=TypeError;e.exports=function(e){if(n(e))return e;throw new a(i(e)+" is not a function")}},2655:(e,t,r)=>{"use strict";var n=r(9429),i=r(3691),a=TypeError;e.exports=function(e){if(n(e))return e;throw new a(i(e)+" is not a constructor")}},3550:(e,t,r)=>{"use strict";var n=r(9985),i=String,a=TypeError;e.exports=function(e){if("object"==typeof e||n(e))return e;throw new a("Can't set "+i(e)+" as a prototype")}},7370:(e,t,r)=>{"use strict";var n=r(4201),i=r(5391),a=r(2560).f,o=n("unscopables"),u=Array.prototype;void 0===u[o]&&a(u,o,{configurable:!0,value:i(null)}),e.exports=function(e){u[o][e]=!0}},1514:(e,t,r)=>{"use strict";var n=r(730).charAt;e.exports=function(e,t,r){return t+(r?n(e,t).length:1)}},767:(e,t,r)=>{"use strict";var n=r(3622),i=TypeError;e.exports=function(e,t){if(n(t,e))return e;throw new i("Incorrect invocation")}},5027:(e,t,r)=>{"use strict";var n=r(8999),i=String,a=TypeError;e.exports=function(e){if(n(e))return e;throw new a(i(e)+" is not an object")}},1655:(e,t,r)=>{"use strict";var n=r(3689);e.exports=n((function(){if("function"==typeof ArrayBuffer){var e=new ArrayBuffer(8);Object.isExtensible(e)&&Object.defineProperty(e,"a",{value:8})}}))},2872:(e,t,r)=>{"use strict";var n=r(690),i=r(7578),a=r(6310);e.exports=function(e){for(var t=n(this),r=a(t),o=arguments.length,u=i(o>1?arguments[1]:void 0,r),s=o>2?arguments[2]:void 0,c=void 0===s?r:i(s,r);c>u;)t[u++]=e;return t}},7612:(e,t,r)=>{"use strict";var n=r(2960).forEach,i=r(6834)("forEach");e.exports=i?[].forEach:function(e){return n(this,e,arguments.length>1?arguments[1]:void 0)}},1055:(e,t,r)=>{"use strict";var n=r(4071),i=r(2615),a=r(690),o=r(1228),u=r(3292),s=r(9429),c=r(6310),f=r(6522),l=r(5185),p=r(1664),m=Array;e.exports=function(e){var t=a(e),r=s(this),h=arguments.length,d=h>1?arguments[1]:void 0,v=void 0!==d;v&&(d=n(d,h>2?arguments[2]:void 0));var y,g,x,b,w,N,D=p(t),E=0;if(!D||this===m&&u(D))for(y=c(t),g=r?new this(y):m(y);y>E;E++)N=v?d(t[E],E):t[E],f(g,E,N);else for(w=(b=l(t,D)).next,g=r?new this:[];!(x=i(w,b)).done;E++)N=v?o(b,d,[x.value,E],!0):x.value,f(g,E,N);return g.length=E,g}},4328:(e,t,r)=>{"use strict";var n=r(5290),i=r(7578),a=r(6310),o=function(e){return function(t,r,o){var u,s=n(t),c=a(s),f=i(o,c);if(e&&r!=r){for(;c>f;)if((u=s[f++])!=u)return!0}else for(;c>f;f++)if((e||f in s)&&s[f]===r)return e||f||0;return!e&&-1}};e.exports={includes:o(!0),indexOf:o(!1)}},2960:(e,t,r)=>{"use strict";var n=r(4071),i=r(8844),a=r(4413),o=r(690),u=r(6310),s=r(7120),c=i([].push),f=function(e){var t=1===e,r=2===e,i=3===e,f=4===e,l=6===e,p=7===e,m=5===e||l;return function(h,d,v,y){for(var g,x,b=o(h),w=a(b),N=n(d,v),D=u(w),E=0,A=y||s,S=t?A(h,D):r||p?A(h,0):void 0;D>E;E++)if((m||E in w)&&(x=N(g=w[E],E,b),e))if(t)S[E]=x;else if(x)switch(e){case 3:return!0;case 5:return g;case 6:return E;case 2:c(S,g)}else switch(e){case 4:return!1;case 7:c(S,g)}return l?-1:i||f?f:S}};e.exports={forEach:f(0),map:f(1),filter:f(2),some:f(3),every:f(4),find:f(5),findIndex:f(6),filterReject:f(7)}},9042:(e,t,r)=>{"use strict";var n=r(3689),i=r(4201),a=r(3615),o=i("species");e.exports=function(e){return a>=51||!n((function(){var t=[];return(t.constructor={})[o]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}},6834:(e,t,r)=>{"use strict";var n=r(3689);e.exports=function(e,t){var r=[][e];return!!r&&n((function(){r.call(null,t||function(){return 1},1)}))}},8820:(e,t,r)=>{"use strict";var n=r(509),i=r(690),a=r(4413),o=r(6310),u=TypeError,s=function(e){return function(t,r,s,c){n(r);var f=i(t),l=a(f),p=o(f),m=e?p-1:0,h=e?-1:1;if(s<2)for(;;){if(m in l){c=l[m],m+=h;break}if(m+=h,e?m<0:p<=m)throw new u("Reduce of empty array with no initial value")}for(;e?m>=0:p>m;m+=h)m in l&&(c=r(c,l[m],m,f));return c}};e.exports={left:s(!1),right:s(!0)}},5649:(e,t,r)=>{"use strict";var n=r(7697),i=r(2297),a=TypeError,o=Object.getOwnPropertyDescriptor,u=n&&!function(){if(void 0!==this)return!0;try{Object.defineProperty([],"length",{writable:!1}).length=1}catch(e){return e instanceof TypeError}}();e.exports=u?function(e,t){if(i(e)&&!o(e,"length").writable)throw new a("Cannot set read only .length");return e.length=t}:function(e,t){return e.length=t}},9015:(e,t,r)=>{"use strict";var n=r(7578),i=r(6310),a=r(6522),o=Array,u=Math.max;e.exports=function(e,t,r){for(var s=i(e),c=n(t,s),f=n(void 0===r?s:r,s),l=o(u(f-c,0)),p=0;c{"use strict";var n=r(8844);e.exports=n([].slice)},382:(e,t,r)=>{"use strict";var n=r(9015),i=Math.floor,a=function(e,t){var r=e.length,s=i(r/2);return r<8?o(e,t):u(e,a(n(e,0,s),t),a(n(e,s),t),t)},o=function(e,t){for(var r,n,i=e.length,a=1;a0;)e[n]=e[--n];n!==a++&&(e[n]=r)}return e},u=function(e,t,r,n){for(var i=t.length,a=r.length,o=0,u=0;o{"use strict";var n=r(2297),i=r(9429),a=r(8999),o=r(4201)("species"),u=Array;e.exports=function(e){var t;return n(e)&&(t=e.constructor,(i(t)&&(t===u||n(t.prototype))||a(t)&&null===(t=t[o]))&&(t=void 0)),void 0===t?u:t}},7120:(e,t,r)=>{"use strict";var n=r(5271);e.exports=function(e,t){return new(n(e))(0===t?0:t)}},1228:(e,t,r)=>{"use strict";var n=r(5027),i=r(2125);e.exports=function(e,t,r,a){try{return a?t(n(r)[0],r[1]):t(r)}catch(t){i(e,"throw",t)}}},6431:(e,t,r)=>{"use strict";var n=r(4201)("iterator"),i=!1;try{var a=0,o={next:function(){return{done:!!a++}},return:function(){i=!0}};o[n]=function(){return this},Array.from(o,(function(){throw 2}))}catch(e){}e.exports=function(e,t){try{if(!t&&!i)return!1}catch(e){return!1}var r=!1;try{var a={};a[n]=function(){return{next:function(){return{done:r=!0}}}},e(a)}catch(e){}return r}},6648:(e,t,r)=>{"use strict";var n=r(8844),i=n({}.toString),a=n("".slice);e.exports=function(e){return a(i(e),8,-1)}},926:(e,t,r)=>{"use strict";var n=r(3043),i=r(9985),a=r(6648),o=r(4201)("toStringTag"),u=Object,s="Arguments"===a(function(){return arguments}());e.exports=n?a:function(e){var t,r,n;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=u(e),o))?r:s?a(t):"Object"===(n=a(t))&&i(t.callee)?"Arguments":n}},800:(e,t,r)=>{"use strict";var n=r(5391),i=r(2148),a=r(6045),o=r(4071),u=r(767),s=r(981),c=r(8734),f=r(1934),l=r(7807),p=r(4241),m=r(7697),h=r(5375).fastKey,d=r(618),v=d.set,y=d.getterFor;e.exports={getConstructor:function(e,t,r,f){var l=e((function(e,i){u(e,p),v(e,{type:t,index:n(null),first:void 0,last:void 0,size:0}),m||(e.size=0),s(i)||c(i,e[f],{that:e,AS_ENTRIES:r})})),p=l.prototype,d=y(t),g=function(e,t,r){var n,i,a=d(e),o=x(e,t);return o?o.value=r:(a.last=o={index:i=h(t,!0),key:t,value:r,previous:n=a.last,next:void 0,removed:!1},a.first||(a.first=o),n&&(n.next=o),m?a.size++:e.size++,"F"!==i&&(a.index[i]=o)),e},x=function(e,t){var r,n=d(e),i=h(t);if("F"!==i)return n.index[i];for(r=n.first;r;r=r.next)if(r.key===t)return r};return a(p,{clear:function(){for(var e=d(this),t=e.index,r=e.first;r;)r.removed=!0,r.previous&&(r.previous=r.previous.next=void 0),delete t[r.index],r=r.next;e.first=e.last=void 0,m?e.size=0:this.size=0},delete:function(e){var t=this,r=d(t),n=x(t,e);if(n){var i=n.next,a=n.previous;delete r.index[n.index],n.removed=!0,a&&(a.next=i),i&&(i.previous=a),r.first===n&&(r.first=i),r.last===n&&(r.last=a),m?r.size--:t.size--}return!!n},forEach:function(e){for(var t,r=d(this),n=o(e,arguments.length>1?arguments[1]:void 0);t=t?t.next:r.first;)for(n(t.value,t.key,this);t&&t.removed;)t=t.previous},has:function(e){return!!x(this,e)}}),a(p,r?{get:function(e){var t=x(this,e);return t&&t.value},set:function(e,t){return g(this,0===e?0:e,t)}}:{add:function(e){return g(this,e=0===e?0:e,e)}}),m&&i(p,"size",{configurable:!0,get:function(){return d(this).size}}),l},setStrong:function(e,t,r){var n=t+" Iterator",i=y(t),a=y(n);f(e,t,(function(e,t){v(this,{type:n,target:e,state:i(e),kind:t,last:void 0})}),(function(){for(var e=a(this),t=e.kind,r=e.last;r&&r.removed;)r=r.previous;return e.target&&(e.last=r=r?r.next:e.state.first)?l("keys"===t?r.key:"values"===t?r.value:[r.key,r.value],!1):(e.target=void 0,l(void 0,!0))}),r?"entries":"values",!r,!0),p(t)}}},319:(e,t,r)=>{"use strict";var n=r(9989),i=r(9037),a=r(8844),o=r(5266),u=r(1880),s=r(5375),c=r(8734),f=r(767),l=r(9985),p=r(981),m=r(8999),h=r(3689),d=r(6431),v=r(5997),y=r(3457);e.exports=function(e,t,r){var g=-1!==e.indexOf("Map"),x=-1!==e.indexOf("Weak"),b=g?"set":"add",w=i[e],N=w&&w.prototype,D=w,E={},A=function(e){var t=a(N[e]);u(N,e,"add"===e?function(e){return t(this,0===e?0:e),this}:"delete"===e?function(e){return!(x&&!m(e))&&t(this,0===e?0:e)}:"get"===e?function(e){return x&&!m(e)?void 0:t(this,0===e?0:e)}:"has"===e?function(e){return!(x&&!m(e))&&t(this,0===e?0:e)}:function(e,r){return t(this,0===e?0:e,r),this})};if(o(e,!l(w)||!(x||N.forEach&&!h((function(){(new w).entries().next()})))))D=r.getConstructor(t,e,g,b),s.enable();else if(o(e,!0)){var S=new D,C=S[b](x?{}:-0,1)!==S,M=h((function(){S.has(1)})),F=d((function(e){new w(e)})),O=!x&&h((function(){for(var e=new w,t=5;t--;)e[b](t,t);return!e.has(-0)}));F||((D=t((function(e,t){f(e,N);var r=y(new w,e,D);return p(t)||c(t,r[b],{that:r,AS_ENTRIES:g}),r}))).prototype=N,N.constructor=D),(M||O)&&(A("delete"),A("has"),g&&A("get")),(O||C)&&A(b),x&&N.clear&&delete N.clear}return E[e]=D,n({global:!0,constructor:!0,forced:D!==w},E),v(D,e),x||r.setStrong(D,e,g),D}},8758:(e,t,r)=>{"use strict";var n=r(6812),i=r(9152),a=r(2474),o=r(2560);e.exports=function(e,t,r){for(var u=i(t),s=o.f,c=a.f,f=0;f{"use strict";var n=r(4201)("match");e.exports=function(e){var t=/./;try{"/./"[e](t)}catch(r){try{return t[n]=!1,"/./"[e](t)}catch(e){}}return!1}},1748:(e,t,r)=>{"use strict";var n=r(3689);e.exports=!n((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype}))},1568:(e,t,r)=>{"use strict";var n=r(8844),i=r(4684),a=r(4327),o=/"/g,u=n("".replace);e.exports=function(e,t,r,n){var s=a(i(e)),c="<"+t;return""!==r&&(c+=" "+r+'="'+u(a(n),o,""")+'"'),c+">"+s+""}},7807:e=>{"use strict";e.exports=function(e,t){return{value:e,done:t}}},5773:(e,t,r)=>{"use strict";var n=r(7697),i=r(2560),a=r(5684);e.exports=n?function(e,t,r){return i.f(e,t,a(1,r))}:function(e,t,r){return e[t]=r,e}},5684:e=>{"use strict";e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},6522:(e,t,r)=>{"use strict";var n=r(8360),i=r(2560),a=r(5684);e.exports=function(e,t,r){var o=n(t);o in e?i.f(e,o,a(0,r)):e[o]=r}},2148:(e,t,r)=>{"use strict";var n=r(8702),i=r(2560);e.exports=function(e,t,r){return r.get&&n(r.get,t,{getter:!0}),r.set&&n(r.set,t,{setter:!0}),i.f(e,t,r)}},1880:(e,t,r)=>{"use strict";var n=r(9985),i=r(2560),a=r(8702),o=r(5014);e.exports=function(e,t,r,u){u||(u={});var s=u.enumerable,c=void 0!==u.name?u.name:t;if(n(r)&&a(r,c,u),u.global)s?e[t]=r:o(t,r);else{try{u.unsafe?e[t]&&(s=!0):delete e[t]}catch(e){}s?e[t]=r:i.f(e,t,{value:r,enumerable:!1,configurable:!u.nonConfigurable,writable:!u.nonWritable})}return e}},6045:(e,t,r)=>{"use strict";var n=r(1880);e.exports=function(e,t,r){for(var i in t)n(e,i,t[i],r);return e}},5014:(e,t,r)=>{"use strict";var n=r(9037),i=Object.defineProperty;e.exports=function(e,t){try{i(n,e,{value:t,configurable:!0,writable:!0})}catch(r){n[e]=t}return t}},8494:(e,t,r)=>{"use strict";var n=r(3691),i=TypeError;e.exports=function(e,t){if(!delete e[t])throw new i("Cannot delete property "+n(t)+" of "+n(e))}},7697:(e,t,r)=>{"use strict";var n=r(3689);e.exports=!n((function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]}))},2659:e=>{"use strict";var t="object"==typeof document&&document.all,r=void 0===t&&void 0!==t;e.exports={all:t,IS_HTMLDDA:r}},6420:(e,t,r)=>{"use strict";var n=r(9037),i=r(8999),a=n.document,o=i(a)&&i(a.createElement);e.exports=function(e){return o?a.createElement(e):{}}},5565:e=>{"use strict";var t=TypeError;e.exports=function(e){if(e>9007199254740991)throw t("Maximum allowed index exceeded");return e}},6338:e=>{"use strict";e.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},3265:(e,t,r)=>{"use strict";var n=r(6420)("span").classList,i=n&&n.constructor&&n.constructor.prototype;e.exports=i===Object.prototype?void 0:i},7365:(e,t,r)=>{"use strict";var n=r(71).match(/firefox\/(\d+)/i);e.exports=!!n&&+n[1]},2532:(e,t,r)=>{"use strict";var n=r(8563),i=r(806);e.exports=!n&&!i&&"object"==typeof window&&"object"==typeof document},8563:e=>{"use strict";e.exports="object"==typeof Deno&&Deno&&"object"==typeof Deno.version},7298:(e,t,r)=>{"use strict";var n=r(71);e.exports=/MSIE|Trident/.test(n)},3221:(e,t,r)=>{"use strict";var n=r(71);e.exports=/ipad|iphone|ipod/i.test(n)&&"undefined"!=typeof Pebble},4764:(e,t,r)=>{"use strict";var n=r(71);e.exports=/(?:ipad|iphone|ipod).*applewebkit/i.test(n)},806:(e,t,r)=>{"use strict";var n=r(9037),i=r(6648);e.exports="process"===i(n.process)},7486:(e,t,r)=>{"use strict";var n=r(71);e.exports=/web0s(?!.*chrome)/i.test(n)},71:e=>{"use strict";e.exports="undefined"!=typeof navigator&&String(navigator.userAgent)||""},3615:(e,t,r)=>{"use strict";var n,i,a=r(9037),o=r(71),u=a.process,s=a.Deno,c=u&&u.versions||s&&s.version,f=c&&c.v8;f&&(i=(n=f.split("."))[0]>0&&n[0]<4?1:+(n[0]+n[1])),!i&&o&&(!(n=o.match(/Edge\/(\d+)/))||n[1]>=74)&&(n=o.match(/Chrome\/(\d+)/))&&(i=+n[1]),e.exports=i},7922:(e,t,r)=>{"use strict";var n=r(71).match(/AppleWebKit\/(\d+)\./);e.exports=!!n&&+n[1]},2739:e=>{"use strict";e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},9989:(e,t,r)=>{"use strict";var n=r(9037),i=r(2474).f,a=r(5773),o=r(1880),u=r(5014),s=r(8758),c=r(5266);e.exports=function(e,t){var r,f,l,p,m,h=e.target,d=e.global,v=e.stat;if(r=d?n:v?n[h]||u(h,{}):(n[h]||{}).prototype)for(f in t){if(p=t[f],l=e.dontCallGetSet?(m=i(r,f))&&m.value:r[f],!c(d?f:h+(v?".":"#")+f,e.forced)&&void 0!==l){if(typeof p==typeof l)continue;s(p,l)}(e.sham||l&&l.sham)&&a(p,"sham",!0),o(r,f,p,e)}}},3689:e=>{"use strict";e.exports=function(e){try{return!!e()}catch(e){return!0}}},8678:(e,t,r)=>{"use strict";r(4043);var n=r(6576),i=r(1880),a=r(6308),o=r(3689),u=r(4201),s=r(5773),c=u("species"),f=RegExp.prototype;e.exports=function(e,t,r,l){var p=u(e),m=!o((function(){var t={};return t[p]=function(){return 7},7!==""[e](t)})),h=m&&!o((function(){var t=!1,r=/a/;return"split"===e&&((r={}).constructor={},r.constructor[c]=function(){return r},r.flags="",r[p]=/./[p]),r.exec=function(){return t=!0,null},r[p](""),!t}));if(!m||!h||r){var d=n(/./[p]),v=t(p,""[e],(function(e,t,r,i,o){var u=n(e),s=t.exec;return s===a||s===f.exec?m&&!o?{done:!0,value:d(t,r,i)}:{done:!0,value:u(r,t,i)}:{done:!1}}));i(String.prototype,e,v[0]),i(f,p,v[1])}l&&s(f[p],"sham",!0)}},1594:(e,t,r)=>{"use strict";var n=r(3689);e.exports=!n((function(){return Object.isExtensible(Object.preventExtensions({}))}))},1735:(e,t,r)=>{"use strict";var n=r(7215),i=Function.prototype,a=i.apply,o=i.call;e.exports="object"==typeof Reflect&&Reflect.apply||(n?o.bind(a):function(){return o.apply(a,arguments)})},4071:(e,t,r)=>{"use strict";var n=r(6576),i=r(509),a=r(7215),o=n(n.bind);e.exports=function(e,t){return i(e),void 0===t?e:a?o(e,t):function(){return e.apply(t,arguments)}}},7215:(e,t,r)=>{"use strict";var n=r(3689);e.exports=!n((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")}))},6761:(e,t,r)=>{"use strict";var n=r(8844),i=r(509),a=r(8999),o=r(6812),u=r(6004),s=r(7215),c=Function,f=n([].concat),l=n([].join),p={};e.exports=s?c.bind:function(e){var t=i(this),r=t.prototype,n=u(arguments,1),s=function(){var r=f(n,u(arguments));return this instanceof s?function(e,t,r){if(!o(p,t)){for(var n=[],i=0;i{"use strict";var n=r(7215),i=Function.prototype.call;e.exports=n?i.bind(i):function(){return i.apply(i,arguments)}},1236:(e,t,r)=>{"use strict";var n=r(7697),i=r(6812),a=Function.prototype,o=n&&Object.getOwnPropertyDescriptor,u=i(a,"name"),s=u&&"something"===function(){}.name,c=u&&(!n||n&&o(a,"name").configurable);e.exports={EXISTS:u,PROPER:s,CONFIGURABLE:c}},2743:(e,t,r)=>{"use strict";var n=r(8844),i=r(509);e.exports=function(e,t,r){try{return n(i(Object.getOwnPropertyDescriptor(e,t)[r]))}catch(e){}}},6576:(e,t,r)=>{"use strict";var n=r(6648),i=r(8844);e.exports=function(e){if("Function"===n(e))return i(e)}},8844:(e,t,r)=>{"use strict";var n=r(7215),i=Function.prototype,a=i.call,o=n&&i.bind.bind(a,a);e.exports=n?o:function(e){return function(){return a.apply(e,arguments)}}},6058:(e,t,r)=>{"use strict";var n=r(9037),i=r(9985);e.exports=function(e,t){return arguments.length<2?(r=n[e],i(r)?r:void 0):n[e]&&n[e][t];var r}},1664:(e,t,r)=>{"use strict";var n=r(926),i=r(4849),a=r(981),o=r(9478),u=r(4201)("iterator");e.exports=function(e){if(!a(e))return i(e,u)||i(e,"@@iterator")||o[n(e)]}},5185:(e,t,r)=>{"use strict";var n=r(2615),i=r(509),a=r(5027),o=r(3691),u=r(1664),s=TypeError;e.exports=function(e,t){var r=arguments.length<2?u(e):t;if(i(r))return a(n(r,e));throw new s(o(e)+" is not iterable")}},2643:(e,t,r)=>{"use strict";var n=r(8844),i=r(2297),a=r(9985),o=r(6648),u=r(4327),s=n([].push);e.exports=function(e){if(a(e))return e;if(i(e)){for(var t=e.length,r=[],n=0;n{"use strict";var n=r(509),i=r(981);e.exports=function(e,t){var r=e[t];return i(r)?void 0:n(r)}},7017:(e,t,r)=>{"use strict";var n=r(8844),i=r(690),a=Math.floor,o=n("".charAt),u=n("".replace),s=n("".slice),c=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,f=/\$([$&'`]|\d{1,2})/g;e.exports=function(e,t,r,n,l,p){var m=r+e.length,h=n.length,d=f;return void 0!==l&&(l=i(l),d=c),u(p,d,(function(i,u){var c;switch(o(u,0)){case"$":return"$";case"&":return e;case"`":return s(t,0,r);case"'":return s(t,m);case"<":c=l[s(u,1,-1)];break;default:var f=+u;if(0===f)return i;if(f>h){var p=a(f/10);return 0===p?i:p<=h?void 0===n[p-1]?o(u,1):n[p-1]+o(u,1):i}c=n[f-1]}return void 0===c?"":c}))}},9037:function(e){"use strict";var t=function(e){return e&&e.Math===Math&&e};e.exports=t("object"==typeof globalThis&&globalThis)||t("object"==typeof window&&window)||t("object"==typeof self&&self)||t("object"==typeof global&&global)||function(){return this}()||this||Function("return this")()},6812:(e,t,r)=>{"use strict";var n=r(8844),i=r(690),a=n({}.hasOwnProperty);e.exports=Object.hasOwn||function(e,t){return a(i(e),t)}},7248:e=>{"use strict";e.exports={}},920:e=>{"use strict";e.exports=function(e,t){try{1===arguments.length?console.error(e):console.error(e,t)}catch(e){}}},2688:(e,t,r)=>{"use strict";var n=r(6058);e.exports=n("document","documentElement")},8506:(e,t,r)=>{"use strict";var n=r(7697),i=r(3689),a=r(6420);e.exports=!n&&!i((function(){return 7!==Object.defineProperty(a("div"),"a",{get:function(){return 7}}).a}))},4413:(e,t,r)=>{"use strict";var n=r(8844),i=r(3689),a=r(6648),o=Object,u=n("".split);e.exports=i((function(){return!o("z").propertyIsEnumerable(0)}))?function(e){return"String"===a(e)?u(e,""):o(e)}:o},3457:(e,t,r)=>{"use strict";var n=r(9985),i=r(8999),a=r(9385);e.exports=function(e,t,r){var o,u;return a&&n(o=t.constructor)&&o!==r&&i(u=o.prototype)&&u!==r.prototype&&a(e,u),e}},6738:(e,t,r)=>{"use strict";var n=r(8844),i=r(9985),a=r(4091),o=n(Function.toString);i(a.inspectSource)||(a.inspectSource=function(e){return o(e)}),e.exports=a.inspectSource},5375:(e,t,r)=>{"use strict";var n=r(9989),i=r(8844),a=r(7248),o=r(8999),u=r(6812),s=r(2560).f,c=r(2741),f=r(6062),l=r(7049),p=r(4630),m=r(1594),h=!1,d=p("meta"),v=0,y=function(e){s(e,d,{value:{objectID:"O"+v++,weakData:{}}})},g=e.exports={enable:function(){g.enable=function(){},h=!0;var e=c.f,t=i([].splice),r={};r[d]=1,e(r).length&&(c.f=function(r){for(var n=e(r),i=0,a=n.length;i{"use strict";var n,i,a,o=r(9834),u=r(9037),s=r(8999),c=r(5773),f=r(6812),l=r(4091),p=r(2713),m=r(7248),h="Object already initialized",d=u.TypeError,v=u.WeakMap;if(o||l.state){var y=l.state||(l.state=new v);y.get=y.get,y.has=y.has,y.set=y.set,n=function(e,t){if(y.has(e))throw new d(h);return t.facade=e,y.set(e,t),t},i=function(e){return y.get(e)||{}},a=function(e){return y.has(e)}}else{var g=p("state");m[g]=!0,n=function(e,t){if(f(e,g))throw new d(h);return t.facade=e,c(e,g,t),t},i=function(e){return f(e,g)?e[g]:{}},a=function(e){return f(e,g)}}e.exports={set:n,get:i,has:a,enforce:function(e){return a(e)?i(e):n(e,{})},getterFor:function(e){return function(t){var r;if(!s(t)||(r=i(t)).type!==e)throw new d("Incompatible receiver, "+e+" required");return r}}}},3292:(e,t,r)=>{"use strict";var n=r(4201),i=r(9478),a=n("iterator"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(i.Array===e||o[a]===e)}},2297:(e,t,r)=>{"use strict";var n=r(6648);e.exports=Array.isArray||function(e){return"Array"===n(e)}},9985:(e,t,r)=>{"use strict";var n=r(2659),i=n.all;e.exports=n.IS_HTMLDDA?function(e){return"function"==typeof e||e===i}:function(e){return"function"==typeof e}},9429:(e,t,r)=>{"use strict";var n=r(8844),i=r(3689),a=r(9985),o=r(926),u=r(6058),s=r(6738),c=function(){},f=[],l=u("Reflect","construct"),p=/^\s*(?:class|function)\b/,m=n(p.exec),h=!p.test(c),d=function(e){if(!a(e))return!1;try{return l(c,f,e),!0}catch(e){return!1}},v=function(e){if(!a(e))return!1;switch(o(e)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return h||!!m(p,s(e))}catch(e){return!0}};v.sham=!0,e.exports=!l||i((function(){var e;return d(d.call)||!d(Object)||!d((function(){e=!0}))||e}))?v:d},5266:(e,t,r)=>{"use strict";var n=r(3689),i=r(9985),a=/#|\.prototype\./,o=function(e,t){var r=s[u(e)];return r===f||r!==c&&(i(t)?n(t):!!t)},u=o.normalize=function(e){return String(e).replace(a,".").toLowerCase()},s=o.data={},c=o.NATIVE="N",f=o.POLYFILL="P";e.exports=o},981:e=>{"use strict";e.exports=function(e){return null==e}},8999:(e,t,r)=>{"use strict";var n=r(9985),i=r(2659),a=i.all;e.exports=i.IS_HTMLDDA?function(e){return"object"==typeof e?null!==e:n(e)||e===a}:function(e){return"object"==typeof e?null!==e:n(e)}},3931:e=>{"use strict";e.exports=!1},1245:(e,t,r)=>{"use strict";var n=r(8999),i=r(6648),a=r(4201)("match");e.exports=function(e){var t;return n(e)&&(void 0!==(t=e[a])?!!t:"RegExp"===i(e))}},734:(e,t,r)=>{"use strict";var n=r(6058),i=r(9985),a=r(3622),o=r(9525),u=Object;e.exports=o?function(e){return"symbol"==typeof e}:function(e){var t=n("Symbol");return i(t)&&a(t.prototype,u(e))}},8734:(e,t,r)=>{"use strict";var n=r(4071),i=r(2615),a=r(5027),o=r(3691),u=r(3292),s=r(6310),c=r(3622),f=r(5185),l=r(1664),p=r(2125),m=TypeError,h=function(e,t){this.stopped=e,this.result=t},d=h.prototype;e.exports=function(e,t,r){var v,y,g,x,b,w,N,D=r&&r.that,E=!(!r||!r.AS_ENTRIES),A=!(!r||!r.IS_RECORD),S=!(!r||!r.IS_ITERATOR),C=!(!r||!r.INTERRUPTED),M=n(t,D),F=function(e){return v&&p(v,"normal",e),new h(!0,e)},O=function(e){return E?(a(e),C?M(e[0],e[1],F):M(e[0],e[1])):C?M(e,F):M(e)};if(A)v=e.iterator;else if(S)v=e;else{if(!(y=l(e)))throw new m(o(e)+" is not iterable");if(u(y)){for(g=0,x=s(e);x>g;g++)if((b=O(e[g]))&&c(d,b))return b;return new h(!1)}v=f(e,y)}for(w=A?e.next:v.next;!(N=i(w,v)).done;){try{b=O(N.value)}catch(e){p(v,"throw",e)}if("object"==typeof b&&b&&c(d,b))return b}return new h(!1)}},2125:(e,t,r)=>{"use strict";var n=r(2615),i=r(5027),a=r(4849);e.exports=function(e,t,r){var o,u;i(e);try{if(!(o=a(e,"return"))){if("throw"===t)throw r;return r}o=n(o,e)}catch(e){u=!0,o=e}if("throw"===t)throw r;if(u)throw o;return i(o),r}},974:(e,t,r)=>{"use strict";var n=r(2013).IteratorPrototype,i=r(5391),a=r(5684),o=r(5997),u=r(9478),s=function(){return this};e.exports=function(e,t,r,c){var f=t+" Iterator";return e.prototype=i(n,{next:a(+!c,r)}),o(e,f,!1,!0),u[f]=s,e}},1934:(e,t,r)=>{"use strict";var n=r(9989),i=r(2615),a=r(3931),o=r(1236),u=r(9985),s=r(974),c=r(1868),f=r(9385),l=r(5997),p=r(5773),m=r(1880),h=r(4201),d=r(9478),v=r(2013),y=o.PROPER,g=o.CONFIGURABLE,x=v.IteratorPrototype,b=v.BUGGY_SAFARI_ITERATORS,w=h("iterator"),N="keys",D="values",E="entries",A=function(){return this};e.exports=function(e,t,r,o,h,v,S){s(r,t,o);var C,M,F,O=function(e){if(e===h&&I)return I;if(!b&&e&&e in _)return _[e];switch(e){case N:case D:case E:return function(){return new r(this,e)}}return function(){return new r(this)}},T=t+" Iterator",B=!1,_=e.prototype,k=_[w]||_["@@iterator"]||h&&_[h],I=!b&&k||O(h),R="Array"===t&&_.entries||k;if(R&&(C=c(R.call(new e)))!==Object.prototype&&C.next&&(a||c(C)===x||(f?f(C,x):u(C[w])||m(C,w,A)),l(C,T,!0,!0),a&&(d[T]=A)),y&&h===D&&k&&k.name!==D&&(!a&&g?p(_,"name",D):(B=!0,I=function(){return i(k,this)})),h)if(M={values:O(D),keys:v?I:O(N),entries:O(E)},S)for(F in M)(b||B||!(F in _))&&m(_,F,M[F]);else n({target:t,proto:!0,forced:b||B},M);return a&&!S||_[w]===I||m(_,w,I,{name:h}),d[t]=I,M}},2013:(e,t,r)=>{"use strict";var n,i,a,o=r(3689),u=r(9985),s=r(8999),c=r(5391),f=r(1868),l=r(1880),p=r(4201),m=r(3931),h=p("iterator"),d=!1;[].keys&&("next"in(a=[].keys())?(i=f(f(a)))!==Object.prototype&&(n=i):d=!0),!s(n)||o((function(){var e={};return n[h].call(e)!==e}))?n={}:m&&(n=c(n)),u(n[h])||l(n,h,(function(){return this})),e.exports={IteratorPrototype:n,BUGGY_SAFARI_ITERATORS:d}},9478:e=>{"use strict";e.exports={}},6310:(e,t,r)=>{"use strict";var n=r(3126);e.exports=function(e){return n(e.length)}},8702:(e,t,r)=>{"use strict";var n=r(8844),i=r(3689),a=r(9985),o=r(6812),u=r(7697),s=r(1236).CONFIGURABLE,c=r(6738),f=r(618),l=f.enforce,p=f.get,m=String,h=Object.defineProperty,d=n("".slice),v=n("".replace),y=n([].join),g=u&&!i((function(){return 8!==h((function(){}),"length",{value:8}).length})),x=String(String).split("String"),b=e.exports=function(e,t,r){"Symbol("===d(m(t),0,7)&&(t="["+v(m(t),/^Symbol\(([^)]*)\)/,"$1")+"]"),r&&r.getter&&(t="get "+t),r&&r.setter&&(t="set "+t),(!o(e,"name")||s&&e.name!==t)&&(u?h(e,"name",{value:t,configurable:!0}):e.name=t),g&&r&&o(r,"arity")&&e.length!==r.arity&&h(e,"length",{value:r.arity});try{r&&o(r,"constructor")&&r.constructor?u&&h(e,"prototype",{writable:!1}):e.prototype&&(e.prototype=void 0)}catch(e){}var n=l(e);return o(n,"source")||(n.source=y(x,"string"==typeof t?t:"")),e};Function.prototype.toString=b((function(){return a(this)&&p(this).source||c(this)}),"toString")},1745:e=>{"use strict";var t=Math.expm1,r=Math.exp;e.exports=!t||t(10)>22025.465794806718||t(10)<22025.465794806718||-2e-17!==t(-2e-17)?function(e){var t=+e;return 0===t?t:t>-1e-6&&t<1e-6?t+t*t/2:r(t)-1}:t},4736:e=>{"use strict";var t=Math.log,r=Math.LOG10E;e.exports=Math.log10||function(e){return t(e)*r}},3956:e=>{"use strict";var t=Math.log;e.exports=Math.log1p||function(e){var r=+e;return r>-1e-8&&r<1e-8?r-r*r/2:t(1+r)}},5680:e=>{"use strict";e.exports=Math.sign||function(e){var t=+e;return 0===t||t!=t?t:t<0?-1:1}},8828:e=>{"use strict";var t=Math.ceil,r=Math.floor;e.exports=Math.trunc||function(e){var n=+e;return(n>0?r:t)(n)}},231:(e,t,r)=>{"use strict";var n,i,a,o,u,s=r(9037),c=r(4071),f=r(2474).f,l=r(9886).set,p=r(4410),m=r(4764),h=r(3221),d=r(7486),v=r(806),y=s.MutationObserver||s.WebKitMutationObserver,g=s.document,x=s.process,b=s.Promise,w=f(s,"queueMicrotask"),N=w&&w.value;if(!N){var D=new p,E=function(){var e,t;for(v&&(e=x.domain)&&e.exit();t=D.get();)try{t()}catch(e){throw D.head&&n(),e}e&&e.enter()};m||v||d||!y||!g?!h&&b&&b.resolve?((o=b.resolve(void 0)).constructor=b,u=c(o.then,o),n=function(){u(E)}):v?n=function(){x.nextTick(E)}:(l=c(l,s),n=function(){l(E)}):(i=!0,a=g.createTextNode(""),new y(E).observe(a,{characterData:!0}),n=function(){a.data=i=!i}),N=function(e){D.head||n(),D.add(e)}}e.exports=N},2582:(e,t,r)=>{"use strict";var n=r(509),i=TypeError,a=function(e){var t,r;this.promise=new e((function(e,n){if(void 0!==t||void 0!==r)throw new i("Bad Promise constructor");t=e,r=n})),this.resolve=n(t),this.reject=n(r)};e.exports.f=function(e){return new a(e)}},2124:(e,t,r)=>{"use strict";var n=r(1245),i=TypeError;e.exports=function(e){if(n(e))throw new i("The method doesn't accept regular expressions");return e}},4818:(e,t,r)=>{"use strict";var n=r(9037),i=r(3689),a=r(8844),o=r(4327),u=r(1435).trim,s=r(6350),c=a("".charAt),f=n.parseFloat,l=n.Symbol,p=l&&l.iterator,m=1/f(s+"-0")!=-1/0||p&&!i((function(){f(Object(p))}));e.exports=m?function(e){var t=u(o(e)),r=f(t);return 0===r&&"-"===c(t,0)?-0:r}:f},7897:(e,t,r)=>{"use strict";var n=r(9037),i=r(3689),a=r(8844),o=r(4327),u=r(1435).trim,s=r(6350),c=n.parseInt,f=n.Symbol,l=f&&f.iterator,p=/^[+-]?0x/i,m=a(p.exec),h=8!==c(s+"08")||22!==c(s+"0x16")||l&&!i((function(){c(Object(l))}));e.exports=h?function(e,t){var r=u(o(e));return c(r,t>>>0||(m(p,r)?16:10))}:c},5391:(e,t,r)=>{"use strict";var n,i=r(5027),a=r(8920),o=r(2739),u=r(7248),s=r(2688),c=r(6420),f=r(2713),l="prototype",p="script",m=f("IE_PROTO"),h=function(){},d=function(e){return"<"+p+">"+e+""},v=function(e){e.write(d("")),e.close();var t=e.parentWindow.Object;return e=null,t},y=function(){try{n=new ActiveXObject("htmlfile")}catch(e){}var e,t,r;y="undefined"!=typeof document?document.domain&&n?v(n):(t=c("iframe"),r="java"+p+":",t.style.display="none",s.appendChild(t),t.src=String(r),(e=t.contentWindow.document).open(),e.write(d("document.F=Object")),e.close(),e.F):v(n);for(var i=o.length;i--;)delete y[l][o[i]];return y()};u[m]=!0,e.exports=Object.create||function(e,t){var r;return null!==e?(h[l]=i(e),r=new h,h[l]=null,r[m]=e):r=y(),void 0===t?r:a.f(r,t)}},8920:(e,t,r)=>{"use strict";var n=r(7697),i=r(5648),a=r(2560),o=r(5027),u=r(5290),s=r(300);t.f=n&&!i?Object.defineProperties:function(e,t){o(e);for(var r,n=u(t),i=s(t),c=i.length,f=0;c>f;)a.f(e,r=i[f++],n[r]);return e}},2560:(e,t,r)=>{"use strict";var n=r(7697),i=r(8506),a=r(5648),o=r(5027),u=r(8360),s=TypeError,c=Object.defineProperty,f=Object.getOwnPropertyDescriptor,l="enumerable",p="configurable",m="writable";t.f=n?a?function(e,t,r){if(o(e),t=u(t),o(r),"function"==typeof e&&"prototype"===t&&"value"in r&&m in r&&!r[m]){var n=f(e,t);n&&n[m]&&(e[t]=r.value,r={configurable:p in r?r[p]:n[p],enumerable:l in r?r[l]:n[l],writable:!1})}return c(e,t,r)}:c:function(e,t,r){if(o(e),t=u(t),o(r),i)try{return c(e,t,r)}catch(e){}if("get"in r||"set"in r)throw new s("Accessors not supported");return"value"in r&&(e[t]=r.value),e}},2474:(e,t,r)=>{"use strict";var n=r(7697),i=r(2615),a=r(9556),o=r(5684),u=r(5290),s=r(8360),c=r(6812),f=r(8506),l=Object.getOwnPropertyDescriptor;t.f=n?l:function(e,t){if(e=u(e),t=s(t),f)try{return l(e,t)}catch(e){}if(c(e,t))return o(!i(a.f,e,t),e[t])}},6062:(e,t,r)=>{"use strict";var n=r(6648),i=r(5290),a=r(2741).f,o=r(9015),u="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){return u&&"Window"===n(e)?function(e){try{return a(e)}catch(e){return o(u)}}(e):a(i(e))}},2741:(e,t,r)=>{"use strict";var n=r(4948),i=r(2739).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return n(e,i)}},7518:(e,t)=>{"use strict";t.f=Object.getOwnPropertySymbols},1868:(e,t,r)=>{"use strict";var n=r(6812),i=r(9985),a=r(690),o=r(2713),u=r(1748),s=o("IE_PROTO"),c=Object,f=c.prototype;e.exports=u?c.getPrototypeOf:function(e){var t=a(e);if(n(t,s))return t[s];var r=t.constructor;return i(r)&&t instanceof r?r.prototype:t instanceof c?f:null}},7049:(e,t,r)=>{"use strict";var n=r(3689),i=r(8999),a=r(6648),o=r(1655),u=Object.isExtensible,s=n((function(){u(1)}));e.exports=s||o?function(e){return!!i(e)&&(!o||"ArrayBuffer"!==a(e))&&(!u||u(e))}:u},3622:(e,t,r)=>{"use strict";var n=r(8844);e.exports=n({}.isPrototypeOf)},4948:(e,t,r)=>{"use strict";var n=r(8844),i=r(6812),a=r(5290),o=r(4328).indexOf,u=r(7248),s=n([].push);e.exports=function(e,t){var r,n=a(e),c=0,f=[];for(r in n)!i(u,r)&&i(n,r)&&s(f,r);for(;t.length>c;)i(n,r=t[c++])&&(~o(f,r)||s(f,r));return f}},300:(e,t,r)=>{"use strict";var n=r(4948),i=r(2739);e.exports=Object.keys||function(e){return n(e,i)}},9556:(e,t)=>{"use strict";var r={}.propertyIsEnumerable,n=Object.getOwnPropertyDescriptor,i=n&&!r.call({1:2},1);t.f=i?function(e){var t=n(this,e);return!!t&&t.enumerable}:r},9385:(e,t,r)=>{"use strict";var n=r(2743),i=r(5027),a=r(3550);e.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var e,t=!1,r={};try{(e=n(Object.prototype,"__proto__","set"))(r,[]),t=r instanceof Array}catch(e){}return function(r,n){return i(r),a(n),t?e(r,n):r.__proto__=n,r}}():void 0)},5073:(e,t,r)=>{"use strict";var n=r(3043),i=r(926);e.exports=n?{}.toString:function(){return"[object "+i(this)+"]"}},5899:(e,t,r)=>{"use strict";var n=r(2615),i=r(9985),a=r(8999),o=TypeError;e.exports=function(e,t){var r,u;if("string"===t&&i(r=e.toString)&&!a(u=n(r,e)))return u;if(i(r=e.valueOf)&&!a(u=n(r,e)))return u;if("string"!==t&&i(r=e.toString)&&!a(u=n(r,e)))return u;throw new o("Can't convert object to primitive value")}},9152:(e,t,r)=>{"use strict";var n=r(6058),i=r(8844),a=r(2741),o=r(7518),u=r(5027),s=i([].concat);e.exports=n("Reflect","ownKeys")||function(e){var t=a.f(u(e)),r=o.f;return r?s(t,r(e)):t}},496:(e,t,r)=>{"use strict";var n=r(9037);e.exports=n},9302:e=>{"use strict";e.exports=function(e){try{return{error:!1,value:e()}}catch(e){return{error:!0,value:e}}}},7073:(e,t,r)=>{"use strict";var n=r(9037),i=r(7919),a=r(9985),o=r(5266),u=r(6738),s=r(4201),c=r(2532),f=r(8563),l=r(3931),p=r(3615),m=i&&i.prototype,h=s("species"),d=!1,v=a(n.PromiseRejectionEvent),y=o("Promise",(function(){var e=u(i),t=e!==String(i);if(!t&&66===p)return!0;if(l&&(!m.catch||!m.finally))return!0;if(!p||p<51||!/native code/.test(e)){var r=new i((function(e){e(1)})),n=function(e){e((function(){}),(function(){}))};if((r.constructor={})[h]=n,!(d=r.then((function(){}))instanceof n))return!0}return!t&&(c||f)&&!v}));e.exports={CONSTRUCTOR:y,REJECTION_EVENT:v,SUBCLASSING:d}},7919:(e,t,r)=>{"use strict";var n=r(9037);e.exports=n.Promise},2945:(e,t,r)=>{"use strict";var n=r(5027),i=r(8999),a=r(2582);e.exports=function(e,t){if(n(e),i(t)&&t.constructor===e)return t;var r=a.f(e);return(0,r.resolve)(t),r.promise}},562:(e,t,r)=>{"use strict";var n=r(7919),i=r(6431),a=r(7073).CONSTRUCTOR;e.exports=a||!i((function(e){n.all(e).then(void 0,(function(){}))}))},8055:(e,t,r)=>{"use strict";var n=r(2560).f;e.exports=function(e,t,r){r in e||n(e,r,{configurable:!0,get:function(){return t[r]},set:function(e){t[r]=e}})}},4410:e=>{"use strict";var t=function(){this.head=null,this.tail=null};t.prototype={add:function(e){var t={item:e,next:null},r=this.tail;r?r.next=t:this.head=t,this.tail=t},get:function(){var e=this.head;if(e)return null===(this.head=e.next)&&(this.tail=null),e.item}},e.exports=t},6100:(e,t,r)=>{"use strict";var n=r(2615),i=r(5027),a=r(9985),o=r(6648),u=r(6308),s=TypeError;e.exports=function(e,t){var r=e.exec;if(a(r)){var c=n(r,e,t);return null!==c&&i(c),c}if("RegExp"===o(e))return n(u,e,t);throw new s("RegExp#exec called on incompatible receiver")}},6308:(e,t,r)=>{"use strict";var n,i,a=r(2615),o=r(8844),u=r(4327),s=r(9633),c=r(7901),f=r(3430),l=r(5391),p=r(618).get,m=r(2100),h=r(6422),d=f("native-string-replace",String.prototype.replace),v=RegExp.prototype.exec,y=v,g=o("".charAt),x=o("".indexOf),b=o("".replace),w=o("".slice),N=(i=/b*/g,a(v,n=/a/,"a"),a(v,i,"a"),0!==n.lastIndex||0!==i.lastIndex),D=c.BROKEN_CARET,E=void 0!==/()??/.exec("")[1];(N||E||D||m||h)&&(y=function(e){var t,r,n,i,o,c,f,m=this,h=p(m),A=u(e),S=h.raw;if(S)return S.lastIndex=m.lastIndex,t=a(y,S,A),m.lastIndex=S.lastIndex,t;var C=h.groups,M=D&&m.sticky,F=a(s,m),O=m.source,T=0,B=A;if(M&&(F=b(F,"y",""),-1===x(F,"g")&&(F+="g"),B=w(A,m.lastIndex),m.lastIndex>0&&(!m.multiline||m.multiline&&"\n"!==g(A,m.lastIndex-1))&&(O="(?: "+O+")",B=" "+B,T++),r=new RegExp("^(?:"+O+")",F)),E&&(r=new RegExp("^"+O+"$(?!\\s)",F)),N&&(n=m.lastIndex),i=a(v,M?r:m,B),M?i?(i.input=w(i.input,T),i[0]=w(i[0],T),i.index=m.lastIndex,m.lastIndex+=i[0].length):m.lastIndex=0:N&&i&&(m.lastIndex=m.global?i.index+i[0].length:n),E&&i&&i.length>1&&a(d,i[0],r,(function(){for(o=1;o{"use strict";var n=r(5027);e.exports=function(){var e=n(this),t="";return e.hasIndices&&(t+="d"),e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.dotAll&&(t+="s"),e.unicode&&(t+="u"),e.unicodeSets&&(t+="v"),e.sticky&&(t+="y"),t}},3477:(e,t,r)=>{"use strict";var n=r(2615),i=r(6812),a=r(3622),o=r(9633),u=RegExp.prototype;e.exports=function(e){var t=e.flags;return void 0!==t||"flags"in u||i(e,"flags")||!a(u,e)?t:n(o,e)}},7901:(e,t,r)=>{"use strict";var n=r(3689),i=r(9037).RegExp,a=n((function(){var e=i("a","y");return e.lastIndex=2,null!==e.exec("abcd")})),o=a||n((function(){return!i("a","y").sticky})),u=a||n((function(){var e=i("^r","gy");return e.lastIndex=2,null!==e.exec("str")}));e.exports={BROKEN_CARET:u,MISSED_STICKY:o,UNSUPPORTED_Y:a}},2100:(e,t,r)=>{"use strict";var n=r(3689),i=r(9037).RegExp;e.exports=n((function(){var e=i(".","s");return!(e.dotAll&&e.test("\n")&&"s"===e.flags)}))},6422:(e,t,r)=>{"use strict";var n=r(3689),i=r(9037).RegExp;e.exports=n((function(){var e=i("(?b)","g");return"b"!==e.exec("b").groups.a||"bc"!=="b".replace(e,"$c")}))},4684:(e,t,r)=>{"use strict";var n=r(981),i=TypeError;e.exports=function(e){if(n(e))throw new i("Can't call method on "+e);return e}},4241:(e,t,r)=>{"use strict";var n=r(6058),i=r(2148),a=r(4201),o=r(7697),u=a("species");e.exports=function(e){var t=n(e);o&&t&&!t[u]&&i(t,u,{configurable:!0,get:function(){return this}})}},5997:(e,t,r)=>{"use strict";var n=r(2560).f,i=r(6812),a=r(4201)("toStringTag");e.exports=function(e,t,r){e&&!r&&(e=e.prototype),e&&!i(e,a)&&n(e,a,{configurable:!0,value:t})}},2713:(e,t,r)=>{"use strict";var n=r(3430),i=r(4630),a=n("keys");e.exports=function(e){return a[e]||(a[e]=i(e))}},4091:(e,t,r)=>{"use strict";var n=r(9037),i=r(5014),a="__core-js_shared__",o=n[a]||i(a,{});e.exports=o},3430:(e,t,r)=>{"use strict";var n=r(3931),i=r(4091);(e.exports=function(e,t){return i[e]||(i[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.33.1",mode:n?"pure":"global",copyright:"© 2014-2023 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.33.1/LICENSE",source:"https://github.com/zloirock/core-js"})},6373:(e,t,r)=>{"use strict";var n=r(5027),i=r(2655),a=r(981),o=r(4201)("species");e.exports=function(e,t){var r,u=n(e).constructor;return void 0===u||a(r=n(u)[o])?t:i(r)}},4580:(e,t,r)=>{"use strict";var n=r(3689);e.exports=function(e){return n((function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}))}},730:(e,t,r)=>{"use strict";var n=r(8844),i=r(8700),a=r(4327),o=r(4684),u=n("".charAt),s=n("".charCodeAt),c=n("".slice),f=function(e){return function(t,r){var n,f,l=a(o(t)),p=i(r),m=l.length;return p<0||p>=m?e?"":void 0:(n=s(l,p))<55296||n>56319||p+1===m||(f=s(l,p+1))<56320||f>57343?e?u(l,p):n:e?c(l,p,p+2):f-56320+(n-55296<<10)+65536}};e.exports={codeAt:f(!1),charAt:f(!0)}},534:(e,t,r)=>{"use strict";var n=r(8700),i=r(4327),a=r(4684),o=RangeError;e.exports=function(e){var t=i(a(this)),r="",u=n(e);if(u<0||u===1/0)throw new o("Wrong number of repetitions");for(;u>0;(u>>>=1)&&(t+=t))1&u&&(r+=t);return r}},5984:(e,t,r)=>{"use strict";var n=r(1236).PROPER,i=r(3689),a=r(6350);e.exports=function(e){return i((function(){return!!a[e]()||"​…᠎"!=="​…᠎"[e]()||n&&a[e].name!==e}))}},1435:(e,t,r)=>{"use strict";var n=r(8844),i=r(4684),a=r(4327),o=r(6350),u=n("".replace),s=RegExp("^["+o+"]+"),c=RegExp("(^|[^"+o+"])["+o+"]+$"),f=function(e){return function(t){var r=a(i(t));return 1&e&&(r=u(r,s,"")),2&e&&(r=u(r,c,"$1")),r}};e.exports={start:f(1),end:f(2),trim:f(3)}},146:(e,t,r)=>{"use strict";var n=r(3615),i=r(3689),a=r(9037).String;e.exports=!!Object.getOwnPropertySymbols&&!i((function(){var e=Symbol("symbol detection");return!a(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&n&&n<41}))},3032:(e,t,r)=>{"use strict";var n=r(2615),i=r(6058),a=r(4201),o=r(1880);e.exports=function(){var e=i("Symbol"),t=e&&e.prototype,r=t&&t.valueOf,u=a("toPrimitive");t&&!t[u]&&o(t,u,(function(e){return n(r,this)}),{arity:1})}},6549:(e,t,r)=>{"use strict";var n=r(146);e.exports=n&&!!Symbol.for&&!!Symbol.keyFor},9886:(e,t,r)=>{"use strict";var n,i,a,o,u=r(9037),s=r(1735),c=r(4071),f=r(9985),l=r(6812),p=r(3689),m=r(2688),h=r(6004),d=r(6420),v=r(1500),y=r(4764),g=r(806),x=u.setImmediate,b=u.clearImmediate,w=u.process,N=u.Dispatch,D=u.Function,E=u.MessageChannel,A=u.String,S=0,C={},M="onreadystatechange";p((function(){n=u.location}));var F=function(e){if(l(C,e)){var t=C[e];delete C[e],t()}},O=function(e){return function(){F(e)}},T=function(e){F(e.data)},B=function(e){u.postMessage(A(e),n.protocol+"//"+n.host)};x&&b||(x=function(e){v(arguments.length,1);var t=f(e)?e:D(e),r=h(arguments,1);return C[++S]=function(){s(t,void 0,r)},i(S),S},b=function(e){delete C[e]},g?i=function(e){w.nextTick(O(e))}:N&&N.now?i=function(e){N.now(O(e))}:E&&!y?(o=(a=new E).port2,a.port1.onmessage=T,i=c(o.postMessage,o)):u.addEventListener&&f(u.postMessage)&&!u.importScripts&&n&&"file:"!==n.protocol&&!p(B)?(i=B,u.addEventListener("message",T,!1)):i=M in d("script")?function(e){m.appendChild(d("script"))[M]=function(){m.removeChild(this),F(e)}}:function(e){setTimeout(O(e),0)}),e.exports={set:x,clear:b}},3648:(e,t,r)=>{"use strict";var n=r(8844);e.exports=n(1..valueOf)},7578:(e,t,r)=>{"use strict";var n=r(8700),i=Math.max,a=Math.min;e.exports=function(e,t){var r=n(e);return r<0?i(r+t,0):a(r,t)}},5290:(e,t,r)=>{"use strict";var n=r(4413),i=r(4684);e.exports=function(e){return n(i(e))}},8700:(e,t,r)=>{"use strict";var n=r(8828);e.exports=function(e){var t=+e;return t!=t||0===t?0:n(t)}},3126:(e,t,r)=>{"use strict";var n=r(8700),i=Math.min;e.exports=function(e){return e>0?i(n(e),9007199254740991):0}},690:(e,t,r)=>{"use strict";var n=r(4684),i=Object;e.exports=function(e){return i(n(e))}},8732:(e,t,r)=>{"use strict";var n=r(2615),i=r(8999),a=r(734),o=r(4849),u=r(5899),s=r(4201),c=TypeError,f=s("toPrimitive");e.exports=function(e,t){if(!i(e)||a(e))return e;var r,s=o(e,f);if(s){if(void 0===t&&(t="default"),r=n(s,e,t),!i(r)||a(r))return r;throw new c("Can't convert object to primitive value")}return void 0===t&&(t="number"),u(e,t)}},8360:(e,t,r)=>{"use strict";var n=r(8732),i=r(734);e.exports=function(e){var t=n(e,"string");return i(t)?t:t+""}},3043:(e,t,r)=>{"use strict";var n={};n[r(4201)("toStringTag")]="z",e.exports="[object z]"===String(n)},4327:(e,t,r)=>{"use strict";var n=r(926),i=String;e.exports=function(e){if("Symbol"===n(e))throw new TypeError("Cannot convert a Symbol value to a string");return i(e)}},3691:e=>{"use strict";var t=String;e.exports=function(e){try{return t(e)}catch(e){return"Object"}}},4630:(e,t,r)=>{"use strict";var n=r(8844),i=0,a=Math.random(),o=n(1..toString);e.exports=function(e){return"Symbol("+(void 0===e?"":e)+")_"+o(++i+a,36)}},9525:(e,t,r)=>{"use strict";var n=r(146);e.exports=n&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},5648:(e,t,r)=>{"use strict";var n=r(7697),i=r(3689);e.exports=n&&i((function(){return 42!==Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype}))},1500:e=>{"use strict";var t=TypeError;e.exports=function(e,r){if(e{"use strict";var n=r(9037),i=r(9985),a=n.WeakMap;e.exports=i(a)&&/native code/.test(String(a))},5405:(e,t,r)=>{"use strict";var n=r(496),i=r(6812),a=r(6145),o=r(2560).f;e.exports=function(e){var t=n.Symbol||(n.Symbol={});i(t,e)||o(t,e,{value:a.f(e)})}},6145:(e,t,r)=>{"use strict";var n=r(4201);t.f=n},4201:(e,t,r)=>{"use strict";var n=r(9037),i=r(3430),a=r(6812),o=r(4630),u=r(146),s=r(9525),c=n.Symbol,f=i("wks"),l=s?c.for||c:c&&c.withoutSetter||o;e.exports=function(e){return a(f,e)||(f[e]=u&&a(c,e)?c[e]:l("Symbol."+e)),f[e]}},6350:e=>{"use strict";e.exports="\t\n\v\f\r                 \u2028\u2029\ufeff"},4338:(e,t,r)=>{"use strict";var n=r(9989),i=r(3689),a=r(2297),o=r(8999),u=r(690),s=r(6310),c=r(5565),f=r(6522),l=r(7120),p=r(9042),m=r(4201),h=r(3615),d=m("isConcatSpreadable"),v=h>=51||!i((function(){var e=[];return e[d]=!1,e.concat()[0]!==e})),y=function(e){if(!o(e))return!1;var t=e[d];return void 0!==t?!!t:a(e)};n({target:"Array",proto:!0,arity:1,forced:!v||!p("concat")},{concat:function(e){var t,r,n,i,a,o=u(this),p=l(o,0),m=0;for(t=-1,n=arguments.length;t{"use strict";var n=r(9989),i=r(2960).every;n({target:"Array",proto:!0,forced:!r(6834)("every")},{every:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}})},7895:(e,t,r)=>{"use strict";var n=r(9989),i=r(2872),a=r(7370);n({target:"Array",proto:!0},{fill:i}),a("fill")},8077:(e,t,r)=>{"use strict";var n=r(9989),i=r(2960).filter;n({target:"Array",proto:!0,forced:!r(9042)("filter")},{filter:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}})},5728:(e,t,r)=>{"use strict";var n=r(9989),i=r(2960).find,a=r(7370),o="find",u=!0;o in[]&&Array(1)[o]((function(){u=!1})),n({target:"Array",proto:!0,forced:u},{find:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),a(o)},9693:(e,t,r)=>{"use strict";var n=r(9989),i=r(7612);n({target:"Array",proto:!0,forced:[].forEach!==i},{forEach:i})},7722:(e,t,r)=>{"use strict";var n=r(9989),i=r(1055);n({target:"Array",stat:!0,forced:!r(6431)((function(e){Array.from(e)}))},{from:i})},6801:(e,t,r)=>{"use strict";var n=r(9989),i=r(4328).includes,a=r(3689),o=r(7370);n({target:"Array",proto:!0,forced:a((function(){return!Array(1).includes()}))},{includes:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),o("includes")},7195:(e,t,r)=>{"use strict";var n=r(9989),i=r(6576),a=r(4328).indexOf,o=r(6834),u=i([].indexOf),s=!!u&&1/u([1],1,-0)<0;n({target:"Array",proto:!0,forced:s||!o("indexOf")},{indexOf:function(e){var t=arguments.length>1?arguments[1]:void 0;return s?u(this,e,t)||0:a(this,e,t)}})},3975:(e,t,r)=>{"use strict";r(9989)({target:"Array",stat:!0},{isArray:r(2297)})},752:(e,t,r)=>{"use strict";var n=r(5290),i=r(7370),a=r(9478),o=r(618),u=r(2560).f,s=r(1934),c=r(7807),f=r(3931),l=r(7697),p="Array Iterator",m=o.set,h=o.getterFor(p);e.exports=s(Array,"Array",(function(e,t){m(this,{type:p,target:n(e),index:0,kind:t})}),(function(){var e=h(this),t=e.target,r=e.index++;if(!t||r>=t.length)return e.target=void 0,c(void 0,!0);switch(e.kind){case"keys":return c(r,!1);case"values":return c(t[r],!1)}return c([r,t[r]],!1)}),"values");var d=a.Arguments=a.Array;if(i("keys"),i("values"),i("entries"),!f&&l&&"values"!==d.name)try{u(d,"name",{value:"values"})}catch(e){}},6203:(e,t,r)=>{"use strict";var n=r(9989),i=r(8844),a=r(4413),o=r(5290),u=r(6834),s=i([].join);n({target:"Array",proto:!0,forced:a!==Object||!u("join",",")},{join:function(e){return s(o(this),void 0===e?",":e)}})},886:(e,t,r)=>{"use strict";var n=r(9989),i=r(2960).map;n({target:"Array",proto:!0,forced:!r(9042)("map")},{map:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}})},278:(e,t,r)=>{"use strict";var n=r(9989),i=r(8820).left,a=r(6834),o=r(3615);n({target:"Array",proto:!0,forced:!r(806)&&o>79&&o<83||!a("reduce")},{reduce:function(e){var t=arguments.length;return i(this,e,t,t>1?arguments[1]:void 0)}})},3374:(e,t,r)=>{"use strict";var n=r(9989),i=r(8844),a=r(2297),o=i([].reverse),u=[1,2];n({target:"Array",proto:!0,forced:String(u)===String(u.reverse())},{reverse:function(){return a(this)&&(this.length=this.length),o(this)}})},9730:(e,t,r)=>{"use strict";var n=r(9989),i=r(2297),a=r(9429),o=r(8999),u=r(7578),s=r(6310),c=r(5290),f=r(6522),l=r(4201),p=r(9042),m=r(6004),h=p("slice"),d=l("species"),v=Array,y=Math.max;n({target:"Array",proto:!0,forced:!h},{slice:function(e,t){var r,n,l,p=c(this),h=s(p),g=u(e,h),x=u(void 0===t?h:t,h);if(i(p)&&(r=p.constructor,(a(r)&&(r===v||i(r.prototype))||o(r)&&null===(r=r[d]))&&(r=void 0),r===v||void 0===r))return m(p,g,x);for(n=new(void 0===r?v:r)(y(x-g,0)),l=0;g{"use strict";var n=r(9989),i=r(2960).some;n({target:"Array",proto:!0,forced:!r(6834)("some")},{some:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}})},5137:(e,t,r)=>{"use strict";var n=r(9989),i=r(8844),a=r(509),o=r(690),u=r(6310),s=r(8494),c=r(4327),f=r(3689),l=r(382),p=r(6834),m=r(7365),h=r(7298),d=r(3615),v=r(7922),y=[],g=i(y.sort),x=i(y.push),b=f((function(){y.sort(void 0)})),w=f((function(){y.sort(null)})),N=p("sort"),D=!f((function(){if(d)return d<70;if(!(m&&m>3)){if(h)return!0;if(v)return v<603;var e,t,r,n,i="";for(e=65;e<76;e++){switch(t=String.fromCharCode(e),e){case 66:case 69:case 70:case 72:r=3;break;case 68:case 71:r=4;break;default:r=2}for(n=0;n<47;n++)y.push({k:t+n,v:r})}for(y.sort((function(e,t){return t.v-e.v})),n=0;nc(r)?1:-1}}(e)),r=u(i),n=0;n{"use strict";var n=r(9989),i=r(690),a=r(7578),o=r(8700),u=r(6310),s=r(5649),c=r(5565),f=r(7120),l=r(6522),p=r(8494),m=r(9042)("splice"),h=Math.max,d=Math.min;n({target:"Array",proto:!0,forced:!m},{splice:function(e,t){var r,n,m,v,y,g,x=i(this),b=u(x),w=a(e,b),N=arguments.length;for(0===N?r=n=0:1===N?(r=0,n=b-w):(r=N-2,n=d(h(o(t),0),b-w)),c(b+r-n),m=f(x,n),v=0;vb-n+r;v--)p(x,v-1)}else if(r>n)for(v=b-n;v>w;v--)g=v+r-1,(y=v+n-1)in x?x[g]=x[y]:p(x,g);for(v=0;v{"use strict";var n=r(9989),i=r(8844),a=Date,o=i(a.prototype.getTime);n({target:"Date",stat:!0},{now:function(){return o(new a)}})},8150:(e,t,r)=>{"use strict";var n=r(9989),i=r(3689),a=r(690),o=r(8732);n({target:"Date",proto:!0,arity:1,forced:i((function(){return null!==new Date(NaN).toJSON()||1!==Date.prototype.toJSON.call({toISOString:function(){return 1}})}))},{toJSON:function(e){var t=a(this),r=o(t,"number");return"number"!=typeof r||isFinite(r)?t.toISOString():null}})},24:(e,t,r)=>{"use strict";var n=r(8844),i=r(1880),a=Date.prototype,o="Invalid Date",u="toString",s=n(a[u]),c=n(a.getTime);String(new Date(NaN))!==o&&i(a,u,(function(){var e=c(this);return e==e?s(this):o}))},1517:(e,t,r)=>{"use strict";var n=r(9989),i=r(6761);n({target:"Function",proto:!0,forced:Function.bind!==i},{bind:i})},4284:(e,t,r)=>{"use strict";var n=r(7697),i=r(1236).EXISTS,a=r(8844),o=r(2148),u=Function.prototype,s=a(u.toString),c=/function\b(?:\s|\/\*[\S\s]*?\*\/|\/\/[^\n\r]*[\n\r]+)*([^\s(/]*)/,f=a(c.exec);n&&!i&&o(u,"name",{configurable:!0,get:function(){try{return f(c,s(this))[1]}catch(e){return""}}})},8324:(e,t,r)=>{"use strict";var n=r(9989),i=r(6058),a=r(1735),o=r(2615),u=r(8844),s=r(3689),c=r(9985),f=r(734),l=r(6004),p=r(2643),m=r(146),h=String,d=i("JSON","stringify"),v=u(/./.exec),y=u("".charAt),g=u("".charCodeAt),x=u("".replace),b=u(1..toString),w=/[\uD800-\uDFFF]/g,N=/^[\uD800-\uDBFF]$/,D=/^[\uDC00-\uDFFF]$/,E=!m||s((function(){var e=i("Symbol")("stringify detection");return"[null]"!==d([e])||"{}"!==d({a:e})||"{}"!==d(Object(e))})),A=s((function(){return'"\\udf06\\ud834"'!==d("\udf06\ud834")||'"\\udead"'!==d("\udead")})),S=function(e,t){var r=l(arguments),n=p(t);if(c(n)||void 0!==e&&!f(e))return r[1]=function(e,t){if(c(n)&&(t=o(n,this,h(e),t)),!f(t))return t},a(d,null,r)},C=function(e,t,r){var n=y(r,t-1),i=y(r,t+1);return v(N,e)&&!v(D,i)||v(D,e)&&!v(N,n)?"\\u"+b(g(e,0),16):e};d&&n({target:"JSON",stat:!0,arity:3,forced:E||A},{stringify:function(e,t,r){var n=l(arguments),i=a(E?S:d,null,n);return A&&"string"==typeof i?x(i,w,C):i}})},9322:(e,t,r)=>{"use strict";r(319)("Map",(function(e){return function(){return e(this,arguments.length?arguments[0]:void 0)}}),r(800))},6646:(e,t,r)=>{"use strict";r(9322)},6557:(e,t,r)=>{"use strict";var n=r(9989),i=r(3956),a=Math.acosh,o=Math.log,u=Math.sqrt,s=Math.LN2;n({target:"Math",stat:!0,forced:!a||710!==Math.floor(a(Number.MAX_VALUE))||a(1/0)!==1/0},{acosh:function(e){var t=+e;return t<1?NaN:t>94906265.62425156?o(t)+s:i(t-1+u(t-1)*u(t+1))}})},2428:(e,t,r)=>{"use strict";var n=r(9989),i=Math.asinh,a=Math.log,o=Math.sqrt;n({target:"Math",stat:!0,forced:!(i&&1/i(0)>0)},{asinh:function e(t){var r=+t;return isFinite(r)&&0!==r?r<0?-e(-r):a(r+o(r*r+1)):r}})},5263:(e,t,r)=>{"use strict";var n=r(9989),i=Math.atanh,a=Math.log;n({target:"Math",stat:!0,forced:!(i&&1/i(-0)<0)},{atanh:function(e){var t=+e;return 0===t?t:a((1+t)/(1-t))/2}})},4712:(e,t,r)=>{"use strict";var n=r(9989),i=r(5680),a=Math.abs,o=Math.pow;n({target:"Math",stat:!0},{cbrt:function(e){var t=+e;return i(t)*o(a(t),1/3)}})},7221:(e,t,r)=>{"use strict";var n=r(9989),i=r(1745),a=Math.cosh,o=Math.abs,u=Math.E;n({target:"Math",stat:!0,forced:!a||a(710)===1/0},{cosh:function(e){var t=i(o(e)-1)+1;return(t+1/(t*u*u))*(u/2)}})},4992:(e,t,r)=>{"use strict";var n=r(9989),i=r(1745);n({target:"Math",stat:!0,forced:i!==Math.expm1},{expm1:i})},5239:(e,t,r)=>{"use strict";r(9989)({target:"Math",stat:!0},{log10:r(4736)})},2076:(e,t,r)=>{"use strict";r(9989)({target:"Math",stat:!0},{log1p:r(3956)})},8813:(e,t,r)=>{"use strict";var n=r(9989),i=Math.log,a=Math.LN2;n({target:"Math",stat:!0},{log2:function(e){return i(e)/a}})},6976:(e,t,r)=>{"use strict";r(9989)({target:"Math",stat:!0},{sign:r(5680)})},2700:(e,t,r)=>{"use strict";var n=r(9989),i=r(3689),a=r(1745),o=Math.abs,u=Math.exp,s=Math.E;n({target:"Math",stat:!0,forced:i((function(){return-2e-17!==Math.sinh(-2e-17)}))},{sinh:function(e){var t=+e;return o(t)<1?(a(t)-a(-t))/2:(u(t-1)-u(-t-1))*(s/2)}})},1554:(e,t,r)=>{"use strict";var n=r(9989),i=r(1745),a=Math.exp;n({target:"Math",stat:!0},{tanh:function(e){var t=+e,r=i(t),n=i(-t);return r===1/0?1:n===1/0?-1:(r-n)/(a(t)+a(-t))}})},9288:(e,t,r)=>{"use strict";var n=r(9989),i=r(3931),a=r(7697),o=r(9037),u=r(496),s=r(8844),c=r(5266),f=r(6812),l=r(3457),p=r(3622),m=r(734),h=r(8732),d=r(3689),v=r(2741).f,y=r(2474).f,g=r(2560).f,x=r(3648),b=r(1435).trim,w="Number",N=o[w],D=u[w],E=N.prototype,A=o.TypeError,S=s("".slice),C=s("".charCodeAt),M=c(w,!N(" 0o1")||!N("0b1")||N("+0x1")),F=function(e){var t,r=arguments.length<1?0:N(function(e){var t=h(e,"number");return"bigint"==typeof t?t:function(e){var t,r,n,i,a,o,u,s,c=h(e,"number");if(m(c))throw new A("Cannot convert a Symbol value to a number");if("string"==typeof c&&c.length>2)if(c=b(c),43===(t=C(c,0))||45===t){if(88===(r=C(c,2))||120===r)return NaN}else if(48===t){switch(C(c,1)){case 66:case 98:n=2,i=49;break;case 79:case 111:n=8,i=55;break;default:return+c}for(o=(a=S(c,2)).length,u=0;ui)return NaN;return parseInt(a,n)}return+c}(t)}(e));return p(E,t=this)&&d((function(){x(t)}))?l(Object(r),this,F):r};F.prototype=E,M&&!i&&(E.constructor=F),n({global:!0,constructor:!0,wrap:!0,forced:M},{Number:F});var O=function(e,t){for(var r,n=a?v(t):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,isFinite,isInteger,isNaN,isSafeInteger,parseFloat,parseInt,fromString,range".split(","),i=0;n.length>i;i++)f(t,r=n[i])&&!f(e,r)&&g(e,r,y(t,r))};i&&D&&O(u[w],D),(M||i)&&O(u[w],N)},3584:(e,t,r)=>{"use strict";r(9989)({target:"Number",stat:!0,nonConfigurable:!0,nonWritable:!0},{EPSILON:Math.pow(2,-52)})},5993:(e,t,r)=>{"use strict";r(9989)({target:"Number",stat:!0},{isNaN:function(e){return e!=e}})},7389:(e,t,r)=>{"use strict";var n=r(9989),i=r(8844),a=r(8700),o=r(3648),u=r(534),s=r(3689),c=RangeError,f=String,l=Math.floor,p=i(u),m=i("".slice),h=i(1..toFixed),d=function(e,t,r){return 0===t?r:t%2==1?d(e,t-1,r*e):d(e*e,t/2,r)},v=function(e,t,r){for(var n=-1,i=r;++n<6;)i+=t*e[n],e[n]=i%1e7,i=l(i/1e7)},y=function(e,t){for(var r=6,n=0;--r>=0;)n+=e[r],e[r]=l(n/t),n=n%t*1e7},g=function(e){for(var t=6,r="";--t>=0;)if(""!==r||0===t||0!==e[t]){var n=f(e[t]);r=""===r?n:r+p("0",7-n.length)+n}return r};n({target:"Number",proto:!0,forced:s((function(){return"0.000"!==h(8e-5,3)||"1"!==h(.9,0)||"1.25"!==h(1.255,2)||"1000000000000000128"!==h(0xde0b6b3a7640080,0)}))||!s((function(){h({})}))},{toFixed:function(e){var t,r,n,i,u=o(this),s=a(e),l=[0,0,0,0,0,0],h="",x="0";if(s<0||s>20)throw new c("Incorrect fraction digits");if(u!=u)return"NaN";if(u<=-1e21||u>=1e21)return f(u);if(u<0&&(h="-",u=-u),u>1e-21)if(r=(t=function(e){for(var t=0,r=e;r>=4096;)t+=12,r/=4096;for(;r>=2;)t+=1,r/=2;return t}(u*d(2,69,1))-69)<0?u*d(2,-t,1):u/d(2,t,1),r*=4503599627370496,(t=52-t)>0){for(v(l,0,r),n=s;n>=7;)v(l,1e7,0),n-=7;for(v(l,d(10,n,1),0),n=t-1;n>=23;)y(l,1<<23),n-=23;y(l,1<0?h+((i=x.length)<=s?"0."+p("0",s-i)+x:m(x,0,i-s)+"."+m(x,i-s)):h+x}})},5284:(e,t,r)=>{"use strict";var n=r(9989),i=r(8844),a=r(3689),o=r(3648),u=i(1..toPrecision);n({target:"Number",proto:!0,forced:a((function(){return"1"!==u(1,void 0)}))||!a((function(){u({})}))},{toPrecision:function(e){return void 0===e?u(o(this)):u(o(this),e)}})},1013:(e,t,r)=>{"use strict";r(9989)({target:"Object",stat:!0,sham:!r(7697)},{create:r(5391)})},5082:(e,t,r)=>{"use strict";var n=r(9989),i=r(7697),a=r(8920).f;n({target:"Object",stat:!0,forced:Object.defineProperties!==a,sham:!i},{defineProperties:a})},739:(e,t,r)=>{"use strict";var n=r(9989),i=r(7697),a=r(2560).f;n({target:"Object",stat:!0,forced:Object.defineProperty!==a,sham:!i},{defineProperty:a})},1919:(e,t,r)=>{"use strict";var n=r(9989),i=r(3689),a=r(5290),o=r(2474).f,u=r(7697);n({target:"Object",stat:!0,forced:!u||i((function(){o(1)})),sham:!u},{getOwnPropertyDescriptor:function(e,t){return o(a(e),t)}})},9474:(e,t,r)=>{"use strict";var n=r(9989),i=r(7697),a=r(9152),o=r(5290),u=r(2474),s=r(6522);n({target:"Object",stat:!0,sham:!i},{getOwnPropertyDescriptors:function(e){for(var t,r,n=o(e),i=u.f,c=a(n),f={},l=0;c.length>l;)void 0!==(r=i(n,t=c[l++]))&&s(f,t,r);return f}})},9434:(e,t,r)=>{"use strict";var n=r(9989),i=r(146),a=r(3689),o=r(7518),u=r(690);n({target:"Object",stat:!0,forced:!i||a((function(){o.f(1)}))},{getOwnPropertySymbols:function(e){var t=o.f;return t?t(u(e)):[]}})},8052:(e,t,r)=>{"use strict";var n=r(9989),i=r(3689),a=r(690),o=r(1868),u=r(1748);n({target:"Object",stat:!0,forced:i((function(){o(1)})),sham:!u},{getPrototypeOf:function(e){return o(a(e))}})},9358:(e,t,r)=>{"use strict";var n=r(9989),i=r(690),a=r(300);n({target:"Object",stat:!0,forced:r(3689)((function(){a(1)}))},{keys:function(e){return a(i(e))}})},228:(e,t,r)=>{"use strict";var n=r(3043),i=r(1880),a=r(5073);n||i(Object.prototype,"toString",a,{unsafe:!0})},939:(e,t,r)=>{"use strict";var n=r(9989),i=r(4818);n({global:!0,forced:parseFloat!==i},{parseFloat:i})},2320:(e,t,r)=>{"use strict";var n=r(9989),i=r(7897);n({global:!0,forced:parseInt!==i},{parseInt:i})},1692:(e,t,r)=>{"use strict";var n=r(9989),i=r(2615),a=r(509),o=r(2582),u=r(9302),s=r(8734);n({target:"Promise",stat:!0,forced:r(562)},{all:function(e){var t=this,r=o.f(t),n=r.resolve,c=r.reject,f=u((function(){var r=a(t.resolve),o=[],u=0,f=1;s(e,(function(e){var a=u++,s=!1;f++,i(r,t,e).then((function(e){s||(s=!0,o[a]=e,--f||n(o))}),c)})),--f||n(o)}));return f.error&&c(f.value),r.promise}})},5089:(e,t,r)=>{"use strict";var n=r(9989),i=r(3931),a=r(7073).CONSTRUCTOR,o=r(7919),u=r(6058),s=r(9985),c=r(1880),f=o&&o.prototype;if(n({target:"Promise",proto:!0,forced:a,real:!0},{catch:function(e){return this.then(void 0,e)}}),!i&&s(o)){var l=u("Promise").prototype.catch;f.catch!==l&&c(f,"catch",l,{unsafe:!0})}},6697:(e,t,r)=>{"use strict";var n,i,a,o=r(9989),u=r(3931),s=r(806),c=r(9037),f=r(2615),l=r(1880),p=r(9385),m=r(5997),h=r(4241),d=r(509),v=r(9985),y=r(8999),g=r(767),x=r(6373),b=r(9886).set,w=r(231),N=r(920),D=r(9302),E=r(4410),A=r(618),S=r(7919),C=r(7073),M=r(2582),F="Promise",O=C.CONSTRUCTOR,T=C.REJECTION_EVENT,B=C.SUBCLASSING,_=A.getterFor(F),k=A.set,I=S&&S.prototype,R=S,z=I,q=c.TypeError,j=c.document,P=c.process,L=M.f,U=L,$=!!(j&&j.createEvent&&c.dispatchEvent),H="unhandledrejection",G=function(e){var t;return!(!y(e)||!v(t=e.then))&&t},V=function(e,t){var r,n,i,a=t.value,o=1===t.state,u=o?e.ok:e.fail,s=e.resolve,c=e.reject,l=e.domain;try{u?(o||(2===t.rejection&&X(t),t.rejection=1),!0===u?r=a:(l&&l.enter(),r=u(a),l&&(l.exit(),i=!0)),r===e.promise?c(new q("Promise-chain cycle")):(n=G(r))?f(n,r,s,c):s(r)):c(a)}catch(e){l&&!i&&l.exit(),c(e)}},Z=function(e,t){e.notified||(e.notified=!0,w((function(){for(var r,n=e.reactions;r=n.get();)V(r,e);e.notified=!1,t&&!e.rejection&&Y(e)})))},W=function(e,t,r){var n,i;$?((n=j.createEvent("Event")).promise=t,n.reason=r,n.initEvent(e,!1,!0),c.dispatchEvent(n)):n={promise:t,reason:r},!T&&(i=c["on"+e])?i(n):e===H&&N("Unhandled promise rejection",r)},Y=function(e){f(b,c,(function(){var t,r=e.facade,n=e.value;if(J(e)&&(t=D((function(){s?P.emit("unhandledRejection",n,r):W(H,r,n)})),e.rejection=s||J(e)?2:1,t.error))throw t.value}))},J=function(e){return 1!==e.rejection&&!e.parent},X=function(e){f(b,c,(function(){var t=e.facade;s?P.emit("rejectionHandled",t):W("rejectionhandled",t,e.value)}))},Q=function(e,t,r){return function(n){e(t,n,r)}},K=function(e,t,r){e.done||(e.done=!0,r&&(e=r),e.value=t,e.state=2,Z(e,!0))},ee=function(e,t,r){if(!e.done){e.done=!0,r&&(e=r);try{if(e.facade===t)throw new q("Promise can't be resolved itself");var n=G(t);n?w((function(){var r={done:!1};try{f(n,t,Q(ee,r,e),Q(K,r,e))}catch(t){K(r,t,e)}})):(e.value=t,e.state=1,Z(e,!1))}catch(t){K({done:!1},t,e)}}};if(O&&(z=(R=function(e){g(this,z),d(e),f(n,this);var t=_(this);try{e(Q(ee,t),Q(K,t))}catch(e){K(t,e)}}).prototype,(n=function(e){k(this,{type:F,done:!1,notified:!1,parent:!1,reactions:new E,rejection:!1,state:0,value:void 0})}).prototype=l(z,"then",(function(e,t){var r=_(this),n=L(x(this,R));return r.parent=!0,n.ok=!v(e)||e,n.fail=v(t)&&t,n.domain=s?P.domain:void 0,0===r.state?r.reactions.add(n):w((function(){V(n,r)})),n.promise})),i=function(){var e=new n,t=_(e);this.promise=e,this.resolve=Q(ee,t),this.reject=Q(K,t)},M.f=L=function(e){return e===R||void 0===e?new i(e):U(e)},!u&&v(S)&&I!==Object.prototype)){a=I.then,B||l(I,"then",(function(e,t){var r=this;return new R((function(e,t){f(a,r,e,t)})).then(e,t)}),{unsafe:!0});try{delete I.constructor}catch(e){}p&&p(I,z)}o({global:!0,constructor:!0,wrap:!0,forced:O},{Promise:R}),m(R,F,!1,!0),h(F)},3964:(e,t,r)=>{"use strict";r(6697),r(1692),r(5089),r(8829),r(2092),r(7905)},8829:(e,t,r)=>{"use strict";var n=r(9989),i=r(2615),a=r(509),o=r(2582),u=r(9302),s=r(8734);n({target:"Promise",stat:!0,forced:r(562)},{race:function(e){var t=this,r=o.f(t),n=r.reject,c=u((function(){var o=a(t.resolve);s(e,(function(e){i(o,t,e).then(r.resolve,n)}))}));return c.error&&n(c.value),r.promise}})},2092:(e,t,r)=>{"use strict";var n=r(9989),i=r(2615),a=r(2582);n({target:"Promise",stat:!0,forced:r(7073).CONSTRUCTOR},{reject:function(e){var t=a.f(this);return i(t.reject,void 0,e),t.promise}})},7905:(e,t,r)=>{"use strict";var n=r(9989),i=r(6058),a=r(3931),o=r(7919),u=r(7073).CONSTRUCTOR,s=r(2945),c=i("Promise"),f=a&&!u;n({target:"Promise",stat:!0,forced:a||u},{resolve:function(e){return s(f&&this===c?o:this,e)}})},50:(e,t,r)=>{"use strict";var n=r(9989),i=r(6058),a=r(1735),o=r(6761),u=r(2655),s=r(5027),c=r(8999),f=r(5391),l=r(3689),p=i("Reflect","construct"),m=Object.prototype,h=[].push,d=l((function(){function e(){}return!(p((function(){}),[],e)instanceof e)})),v=!l((function(){p((function(){}))})),y=d||v;n({target:"Reflect",stat:!0,forced:y,sham:y},{construct:function(e,t){u(e),s(t);var r=arguments.length<3?e:u(arguments[2]);if(v&&!d)return p(e,t,r);if(e===r){switch(t.length){case 0:return new e;case 1:return new e(t[0]);case 2:return new e(t[0],t[1]);case 3:return new e(t[0],t[1],t[2]);case 4:return new e(t[0],t[1],t[2],t[3])}var n=[null];return a(h,n,t),new(a(o,e,n))}var i=r.prototype,l=f(c(i)?i:m),y=a(e,l,t);return c(y)?y:l}})},6034:(e,t,r)=>{"use strict";var n=r(9989),i=r(9037),a=r(5997);n({global:!0},{Reflect:{}}),a(i.Reflect,"Reflect",!0)},2003:(e,t,r)=>{"use strict";var n=r(7697),i=r(9037),a=r(8844),o=r(5266),u=r(3457),s=r(5773),c=r(2741).f,f=r(3622),l=r(1245),p=r(4327),m=r(3477),h=r(7901),d=r(8055),v=r(1880),y=r(3689),g=r(6812),x=r(618).enforce,b=r(4241),w=r(4201),N=r(2100),D=r(6422),E=w("match"),A=i.RegExp,S=A.prototype,C=i.SyntaxError,M=a(S.exec),F=a("".charAt),O=a("".replace),T=a("".indexOf),B=a("".slice),_=/^\?<[^\s\d!#%&*+<=>@^][^\s!#%&*+<=>@^]*>/,k=/a/g,I=/a/g,R=new A(k)!==k,z=h.MISSED_STICKY,q=h.UNSUPPORTED_Y;if(o("RegExp",n&&(!R||z||N||D||y((function(){return I[E]=!1,A(k)!==k||A(I)===I||"/a/i"!==String(A(k,"i"))}))))){for(var j=function(e,t){var r,n,i,a,o,c,h=f(S,this),d=l(e),v=void 0===t,y=[],b=e;if(!h&&d&&v&&e.constructor===j)return e;if((d||f(S,e))&&(e=e.source,v&&(t=m(b))),e=void 0===e?"":p(e),t=void 0===t?"":p(t),b=e,N&&"dotAll"in k&&(n=!!t&&T(t,"s")>-1)&&(t=O(t,/s/g,"")),r=t,z&&"sticky"in k&&(i=!!t&&T(t,"y")>-1)&&q&&(t=O(t,/y/g,"")),D&&(a=function(e){for(var t,r=e.length,n=0,i="",a=[],o={},u=!1,s=!1,c=0,f="";n<=r;n++){if("\\"===(t=F(e,n)))t+=F(e,++n);else if("]"===t)u=!1;else if(!u)switch(!0){case"["===t:u=!0;break;case"("===t:M(_,B(e,n+1))&&(n+=2,s=!0),i+=t,c++;continue;case">"===t&&s:if(""===f||g(o,f))throw new C("Invalid capture group name");o[f]=!0,a[a.length]=[f,c],s=!1,f="";continue}s?f+=t:i+=t}return[i,a]}(e),e=a[0],y=a[1]),o=u(A(e,t),h?this:S,j),(n||i||y.length)&&(c=x(o),n&&(c.dotAll=!0,c.raw=j(function(e){for(var t,r=e.length,n=0,i="",a=!1;n<=r;n++)"\\"!==(t=F(e,n))?a||"."!==t?("["===t?a=!0:"]"===t&&(a=!1),i+=t):i+="[\\s\\S]":i+=t+F(e,++n);return i}(e),r)),i&&(c.sticky=!0),y.length&&(c.groups=y)),e!==b)try{s(o,"source",""===b?"(?:)":b)}catch(e){}return o},P=c(A),L=0;P.length>L;)d(j,A,P[L++]);S.constructor=j,j.prototype=S,v(i,"RegExp",j,{constructor:!0})}b("RegExp")},8518:(e,t,r)=>{"use strict";var n=r(7697),i=r(2100),a=r(6648),o=r(2148),u=r(618).get,s=RegExp.prototype,c=TypeError;n&&i&&o(s,"dotAll",{configurable:!0,get:function(){if(this!==s){if("RegExp"===a(this))return!!u(this).dotAll;throw new c("Incompatible receiver, RegExp required")}}})},4043:(e,t,r)=>{"use strict";var n=r(9989),i=r(6308);n({target:"RegExp",proto:!0,forced:/./.exec!==i},{exec:i})},3440:(e,t,r)=>{"use strict";var n=r(7697),i=r(7901).MISSED_STICKY,a=r(6648),o=r(2148),u=r(618).get,s=RegExp.prototype,c=TypeError;n&&i&&o(s,"sticky",{configurable:!0,get:function(){if(this!==s){if("RegExp"===a(this))return!!u(this).sticky;throw new c("Incompatible receiver, RegExp required")}}})},7409:(e,t,r)=>{"use strict";r(4043);var n,i,a=r(9989),o=r(2615),u=r(9985),s=r(5027),c=r(4327),f=(n=!1,(i=/[ac]/).exec=function(){return n=!0,/./.exec.apply(this,arguments)},!0===i.test("abc")&&n),l=/./.test;a({target:"RegExp",proto:!0,forced:!f},{test:function(e){var t=s(this),r=c(e),n=t.exec;if(!u(n))return o(l,t,r);var i=o(n,t,r);return null!==i&&(s(i),!0)}})},2826:(e,t,r)=>{"use strict";var n=r(1236).PROPER,i=r(1880),a=r(5027),o=r(4327),u=r(3689),s=r(3477),c="toString",f=RegExp.prototype[c],l=u((function(){return"/a/b"!==f.call({source:"a",flags:"b"})})),p=n&&f.name!==c;(l||p)&&i(RegExp.prototype,c,(function(){var e=a(this);return"/"+o(e.source)+"/"+o(s(e))}),{unsafe:!0})},7985:(e,t,r)=>{"use strict";r(319)("Set",(function(e){return function(){return e(this,arguments.length?arguments[0]:void 0)}}),r(800))},9649:(e,t,r)=>{"use strict";r(7985)},3843:(e,t,r)=>{"use strict";var n=r(9989),i=r(8844),a=r(2124),o=r(4684),u=r(4327),s=r(7413),c=i("".indexOf);n({target:"String",proto:!0,forced:!s("includes")},{includes:function(e){return!!~c(u(o(this)),u(a(e)),arguments.length>1?arguments[1]:void 0)}})},1694:(e,t,r)=>{"use strict";var n=r(730).charAt,i=r(4327),a=r(618),o=r(1934),u=r(7807),s="String Iterator",c=a.set,f=a.getterFor(s);o(String,"String",(function(e){c(this,{type:s,string:i(e),index:0})}),(function(){var e,t=f(this),r=t.string,i=t.index;return i>=r.length?u(void 0,!0):(e=n(r,i),t.index+=e.length,u(e,!1))}))},2462:(e,t,r)=>{"use strict";var n=r(2615),i=r(8678),a=r(5027),o=r(981),u=r(3126),s=r(4327),c=r(4684),f=r(4849),l=r(1514),p=r(6100);i("match",(function(e,t,r){return[function(t){var r=c(this),i=o(t)?void 0:f(t,e);return i?n(i,t,r):new RegExp(t)[e](s(r))},function(e){var n=a(this),i=s(e),o=r(t,n,i);if(o.done)return o.value;if(!n.global)return p(n,i);var c=n.unicode;n.lastIndex=0;for(var f,m=[],h=0;null!==(f=p(n,i));){var d=s(f[0]);m[h]=d,""===d&&(n.lastIndex=l(i,u(n.lastIndex),c)),h++}return 0===h?null:m}]}))},9588:(e,t,r)=>{"use strict";r(9989)({target:"String",proto:!0},{repeat:r(534)})},7267:(e,t,r)=>{"use strict";var n=r(1735),i=r(2615),a=r(8844),o=r(8678),u=r(3689),s=r(5027),c=r(9985),f=r(981),l=r(8700),p=r(3126),m=r(4327),h=r(4684),d=r(1514),v=r(4849),y=r(7017),g=r(6100),x=r(4201)("replace"),b=Math.max,w=Math.min,N=a([].concat),D=a([].push),E=a("".indexOf),A=a("".slice),S="$0"==="a".replace(/./,"$0"),C=!!/./[x]&&""===/./[x]("a","$0");o("replace",(function(e,t,r){var a=C?"$":"$0";return[function(e,r){var n=h(this),a=f(e)?void 0:v(e,x);return a?i(a,e,n,r):i(t,m(n),e,r)},function(e,i){var o=s(this),u=m(e);if("string"==typeof i&&-1===E(i,a)&&-1===E(i,"$<")){var f=r(t,o,u,i);if(f.done)return f.value}var h=c(i);h||(i=m(i));var v,x=o.global;x&&(v=o.unicode,o.lastIndex=0);for(var S,C=[];null!==(S=g(o,u))&&(D(C,S),x);)""===m(S[0])&&(o.lastIndex=d(u,p(o.lastIndex),v));for(var M,F="",O=0,T=0;T=O&&(F+=A(u,O,k)+B,O=k+_.length)}return F+A(u,O)}]}),!!u((function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$")}))||!S||C)},7729:(e,t,r)=>{"use strict";var n=r(9989),i=r(1568);n({target:"String",proto:!0,forced:r(4580)("sub")},{sub:function(){return i(this,"sub","","")}})},372:(e,t,r)=>{"use strict";var n=r(9989),i=r(8844),a=r(4684),o=r(8700),u=r(4327),s=i("".slice),c=Math.max,f=Math.min;n({target:"String",proto:!0,forced:!"".substr||"b"!=="ab".substr(-1)},{substr:function(e,t){var r,n,i=u(a(this)),l=i.length,p=o(e);return p===1/0&&(p=0),p<0&&(p=c(l+p,0)),(r=void 0===t?l:o(t))<=0||r===1/0||p>=(n=f(p+r,l))?"":s(i,p,n)}})},8436:(e,t,r)=>{"use strict";var n=r(9989),i=r(1435).trim;n({target:"String",proto:!0,forced:r(5984)("trim")},{trim:function(){return i(this)}})},7855:(e,t,r)=>{"use strict";var n=r(9989),i=r(9037),a=r(2615),o=r(8844),u=r(3931),s=r(7697),c=r(146),f=r(3689),l=r(6812),p=r(3622),m=r(5027),h=r(5290),d=r(8360),v=r(4327),y=r(5684),g=r(5391),x=r(300),b=r(2741),w=r(6062),N=r(7518),D=r(2474),E=r(2560),A=r(8920),S=r(9556),C=r(1880),M=r(2148),F=r(3430),O=r(2713),T=r(7248),B=r(4630),_=r(4201),k=r(6145),I=r(5405),R=r(3032),z=r(5997),q=r(618),j=r(2960).forEach,P=O("hidden"),L="Symbol",U="prototype",$=q.set,H=q.getterFor(L),G=Object[U],V=i.Symbol,Z=V&&V[U],W=i.RangeError,Y=i.TypeError,J=i.QObject,X=D.f,Q=E.f,K=w.f,ee=S.f,te=o([].push),re=F("symbols"),ne=F("op-symbols"),ie=F("wks"),ae=!J||!J[U]||!J[U].findChild,oe=function(e,t,r){var n=X(G,t);n&&delete G[t],Q(e,t,r),n&&e!==G&&Q(G,t,n)},ue=s&&f((function(){return 7!==g(Q({},"a",{get:function(){return Q(this,"a",{value:7}).a}})).a}))?oe:Q,se=function(e,t){var r=re[e]=g(Z);return $(r,{type:L,tag:e,description:t}),s||(r.description=t),r},ce=function(e,t,r){e===G&&ce(ne,t,r),m(e);var n=d(t);return m(r),l(re,n)?(r.enumerable?(l(e,P)&&e[P][n]&&(e[P][n]=!1),r=g(r,{enumerable:y(0,!1)})):(l(e,P)||Q(e,P,y(1,{})),e[P][n]=!0),ue(e,n,r)):Q(e,n,r)},fe=function(e,t){m(e);var r=h(t),n=x(r).concat(he(r));return j(n,(function(t){s&&!a(le,r,t)||ce(e,t,r[t])})),e},le=function(e){var t=d(e),r=a(ee,this,t);return!(this===G&&l(re,t)&&!l(ne,t))&&(!(r||!l(this,t)||!l(re,t)||l(this,P)&&this[P][t])||r)},pe=function(e,t){var r=h(e),n=d(t);if(r!==G||!l(re,n)||l(ne,n)){var i=X(r,n);return!i||!l(re,n)||l(r,P)&&r[P][n]||(i.enumerable=!0),i}},me=function(e){var t=K(h(e)),r=[];return j(t,(function(e){l(re,e)||l(T,e)||te(r,e)})),r},he=function(e){var t=e===G,r=K(t?ne:h(e)),n=[];return j(r,(function(e){!l(re,e)||t&&!l(G,e)||te(n,re[e])})),n};c||(C(Z=(V=function(){if(p(Z,this))throw new Y("Symbol is not a constructor");var e=arguments.length&&void 0!==arguments[0]?v(arguments[0]):void 0,t=B(e),r=function(e){var n=void 0===this?i:this;n===G&&a(r,ne,e),l(n,P)&&l(n[P],t)&&(n[P][t]=!1);var o=y(1,e);try{ue(n,t,o)}catch(e){if(!(e instanceof W))throw e;oe(n,t,o)}};return s&&ae&&ue(G,t,{configurable:!0,set:r}),se(t,e)})[U],"toString",(function(){return H(this).tag})),C(V,"withoutSetter",(function(e){return se(B(e),e)})),S.f=le,E.f=ce,A.f=fe,D.f=pe,b.f=w.f=me,N.f=he,k.f=function(e){return se(_(e),e)},s&&(M(Z,"description",{configurable:!0,get:function(){return H(this).description}}),u||C(G,"propertyIsEnumerable",le,{unsafe:!0}))),n({global:!0,constructor:!0,wrap:!0,forced:!c,sham:!c},{Symbol:V}),j(x(ie),(function(e){I(e)})),n({target:L,stat:!0,forced:!c},{useSetter:function(){ae=!0},useSimple:function(){ae=!1}}),n({target:"Object",stat:!0,forced:!c,sham:!s},{create:function(e,t){return void 0===t?g(e):fe(g(e),t)},defineProperty:ce,defineProperties:fe,getOwnPropertyDescriptor:pe}),n({target:"Object",stat:!0,forced:!c},{getOwnPropertyNames:me}),R(),z(V,L),T[P]=!0},6544:(e,t,r)=>{"use strict";var n=r(9989),i=r(7697),a=r(9037),o=r(8844),u=r(6812),s=r(9985),c=r(3622),f=r(4327),l=r(2148),p=r(8758),m=a.Symbol,h=m&&m.prototype;if(i&&s(m)&&(!("description"in h)||void 0!==m().description)){var d={},v=function(){var e=arguments.length<1||void 0===arguments[0]?void 0:f(arguments[0]),t=c(h,this)?new m(e):void 0===e?m():m(e);return""===e&&(d[t]=!0),t};p(v,m),v.prototype=h,h.constructor=v;var y="Symbol(description detection)"===String(m("description detection")),g=o(h.valueOf),x=o(h.toString),b=/^Symbol\((.*)\)[^)]+$/,w=o("".replace),N=o("".slice);l(h,"description",{configurable:!0,get:function(){var e=g(this);if(u(d,e))return"";var t=x(e),r=y?N(t,7,-1):w(t,b,"$1");return""===r?void 0:r}}),n({global:!0,constructor:!0,forced:!0},{Symbol:v})}},8074:(e,t,r)=>{"use strict";var n=r(9989),i=r(6058),a=r(6812),o=r(4327),u=r(3430),s=r(6549),c=u("string-to-symbol-registry"),f=u("symbol-to-string-registry");n({target:"Symbol",stat:!0,forced:!s},{for:function(e){var t=o(e);if(a(c,t))return c[t];var r=i("Symbol")(t);return c[t]=r,f[r]=t,r}})},4254:(e,t,r)=>{"use strict";r(5405)("iterator")},9749:(e,t,r)=>{"use strict";r(7855),r(8074),r(1445),r(8324),r(9434)},1445:(e,t,r)=>{"use strict";var n=r(9989),i=r(6812),a=r(734),o=r(3691),u=r(3430),s=r(6549),c=u("symbol-to-string-registry");n({target:"Symbol",stat:!0,forced:!s},{keyFor:function(e){if(!a(e))throw new TypeError(o(e)+" is not a symbol");if(i(c,e))return c[e]}})},7522:(e,t,r)=>{"use strict";var n=r(9037),i=r(6338),a=r(3265),o=r(7612),u=r(5773),s=function(e){if(e&&e.forEach!==o)try{u(e,"forEach",o)}catch(t){e.forEach=o}};for(var c in i)i[c]&&s(n[c]&&n[c].prototype);s(a)},6265:(e,t,r)=>{"use strict";var n=r(9037),i=r(6338),a=r(3265),o=r(752),u=r(5773),s=r(4201),c=s("iterator"),f=s("toStringTag"),l=o.values,p=function(e,t){if(e){if(e[c]!==l)try{u(e,c,l)}catch(t){e[c]=l}if(e[f]||u(e,f,t),i[t])for(var r in o)if(e[r]!==o[r])try{u(e,r,o[r])}catch(t){e[r]=o[r]}}};for(var m in i)p(n[m]&&n[m].prototype,m);p(a,"DOMTokenList")},9979:(e,t,r)=>{"use strict";var n=r(9989),i=r(2615);n({target:"URL",proto:!0,enumerable:!0},{toJSON:function(){return i(URL.prototype.toString,this)}})},4814:function(e){e.exports=function(){"use strict";function e(){return!0}function t(){return!1}function r(){}const n="Argument is not a typed-function.";return function i(){function a(e){return"object"==typeof e&&null!==e&&e.constructor===Object}const o=[{name:"number",test:function(e){return"number"==typeof e}},{name:"string",test:function(e){return"string"==typeof e}},{name:"boolean",test:function(e){return"boolean"==typeof e}},{name:"Function",test:function(e){return"function"==typeof e}},{name:"Array",test:Array.isArray},{name:"Date",test:function(e){return e instanceof Date}},{name:"RegExp",test:function(e){return e instanceof RegExp}},{name:"Object",test:a},{name:"null",test:function(e){return null===e}},{name:"undefined",test:function(e){return void 0===e}}],u={name:"any",test:e,isAny:!0};let s,c,f=0,l={createCount:0};function p(e){const t=s.get(e);if(t)return t;let r='Unknown type "'+e+'"';const n=e.toLowerCase();let i;for(i of c)if(i.toLowerCase()===n){r+='. Did you mean "'+i+'" ?';break}throw new TypeError(r)}function m(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"any";const r=t?p(t).index:c.length,n=[];for(let t=0;t{const r=s.get(t);return!r.isAny&&r.test(e)}));return t.length?t:["any"]}function v(e){return e&&"function"==typeof e&&"_typedFunctionData"in e}function y(e,t,r){if(!v(e))throw new TypeError(n);const i=r&&r.exact,a=N(Array.isArray(t)?t.join(","):t),o=g(a);if(!i||o in e.signatures){const t=e._typedFunctionData.signatureMap.get(o);if(t)return t}const u=a.length;let s,c;if(i){let t;for(t in s=[],e.signatures)s.push(e._typedFunctionData.signatureMap.get(t))}else s=e._typedFunctionData.signatures;for(let e=0;e!e.has(t.name))))continue}r.push(n)}}if(s=r,0===s.length)break}for(c of s)if(c.params.length<=u)return c;throw new TypeError("Signature not found (signature: "+(e.name||"unnamed")+"("+g(a,", ")+"))")}function g(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:",";return e.map((e=>e.name)).join(t)}function x(e){const t=0===e.indexOf("..."),r=(t?e.length>3?e.slice(3):"any":e).split("|").map((e=>p(e.trim())));let n=!1,i=t?"...":"";return{types:r.map((function(e){return n=e.isAny||n,i+=e.name+"|",{name:e.name,typeIndex:e.index,test:e.test,isAny:e.isAny,conversion:null,conversionIndex:-1}})),name:i.slice(0,-1),hasAny:n,hasConversion:!1,restParam:t}}function b(e){const t=function(e){if(0===e.length)return[];const t=e.map(p);e.length>1&&t.sort(((e,t)=>e.index-t.index));let r=t[0].conversionsTo;if(1===e.length)return r;r=r.concat([]);const n=new Set(e);for(let e=1;ee.name)));let r=e.hasAny,n=e.name;const i=t.map((function(e){const t=p(e.from);return r=t.isAny||r,n+="|"+e.from,{name:e.from,typeIndex:t.index,test:t.test,isAny:t.isAny,conversion:e,conversionIndex:e.index}}));return{types:e.types.concat(i),name:n,hasAny:r,hasConversion:i.length>0,restParam:e.restParam}}function w(e){return e.typeSet||(e.typeSet=new Set,e.types.forEach((t=>e.typeSet.add(t.name)))),e.typeSet}function N(e){const t=[];if("string"!=typeof e)throw new TypeError("Signatures must be strings");const r=e.trim();if(""===r)return t;const n=r.split(",");for(let e=0;e=r+1}}return 0===e.length?function(e){return 0===e.length}:1===e.length?(r=E(e[0]),function(e){return r(e[0])&&1===e.length}):2===e.length?(r=E(e[0]),n=E(e[1]),function(e){return r(e[0])&&n(e[1])&&2===e.length}):(t=e.map(E),function(e){for(let r=0;r{const n=C(e.params,t);let i;for(i of n)r.add(i)})),r.has("any")?["any"]:Array.from(r)}function O(e,t,r){let n,i;const a=e||"unnamed";let o,u=r;for(o=0;o{const n=E(S(r.params,o));(o0){const e=d(t[o]);return n=new TypeError("Unexpected type of argument in function "+a+" (expected: "+i.join(" or ")+", actual: "+e.join(" | ")+", index: "+o+")"),n.data={category:"wrongType",fn:a,index:o,actual:e,expected:i},n}}else u=e}const s=u.map((function(e){return D(e.params)?1/0:e.params.length}));if(t.lengthc)return n=new TypeError("Too many arguments in function "+a+" (expected: "+c+", actual: "+t.length+")"),n.data={category:"tooManyArgs",fn:a,index:t.length,expectedLength:c},n;const f=[];for(let e=0;e0)return 1;const n=B(e)-B(t);return n<0?-1:n>0?1:0}function k(e,t){const r=e.params,n=t.params,i=H(r),a=H(n),o=D(r),u=D(n);if(o&&i.hasAny){if(!u||!a.hasAny)return 1}else if(u&&a.hasAny)return-1;let s,c=0,f=0;for(s of r)s.hasAny&&++c,s.hasConversion&&++f;let l=0,p=0;for(s of n)s.hasAny&&++l,s.hasConversion&&++p;if(c!==l)return c-l;if(o&&i.hasConversion){if(!u||!a.hasConversion)return 1}else if(u&&a.hasConversion)return-1;if(f!==p)return f-p;if(o){if(!u)return 1}else if(u)return-1;const m=(r.length-n.length)*(o?-1:1);if(0!==m)return m;const h=[];let d,v=0;for(let e=0;ee.hasConversion))){const n=D(e),i=e.map(R);r=function(){const e=[],r=n?arguments.length-1:arguments.length;for(let t=0;te.name)).join("|"),hasAny:e.some((e=>e.isAny)),hasConversion:!1,restParam:!0}),u.push(o)}else u=o.types.map((function(e){return{types:[e],name:e.name,hasAny:e.isAny,hasConversion:e.conversion,restParam:!1}}));return i=u,a=function(i){return e(t,r+1,n.concat([i]))},Array.prototype.concat.apply([],i.map(a))}var i,a;return[n]}(e,0,[])}function q(e,t){const r=Math.max(e.length,t.length);for(let n=0;n=n:o?n>=i:n===i}function j(e,t,r){const n=[];let i;for(i of e){let e=r[i];if("number"!=typeof e)throw new TypeError('No definition for referenced signature "'+i+'"');if(e=t[e],"function"!=typeof e)return!1;n.push(e)}return n}function P(e,t,r){const n=function(e){return e.map((e=>Y(e)?Z(e.referToSelf.callback):W(e)?V(e.referTo.references,e.referTo.callback):e))}(e),i=new Array(n.length).fill(!1);let a=!0;for(;a;){a=!1;let e=!0;for(let o=0;o{const n=e[r];if(t.test(n.toString()))throw new SyntaxError("Using `this` to self-reference a function is deprecated since typed-function@3. Use typed.referTo and typed.referToSelf instead.")}))}(n);const i=[],a=[],o={},u=[];let s;for(s in n){if(!Object.prototype.hasOwnProperty.call(n,s))continue;const e=N(s);if(!e)continue;i.forEach((function(t){if(q(t,e))throw new TypeError('Conflicting signatures "'+g(t)+'" and "'+g(e)+'".')})),i.push(e);const t=a.length;a.push(n[s]);const r=e.map(b);let c;for(c of z(r)){const e=g(c);u.push({params:c,name:e,fn:t}),c.every((e=>!e.hasConversion))&&(o[e]=t)}}u.sort(k);const c=P(a,o,ue);let f;for(f in o)Object.prototype.hasOwnProperty.call(o,f)&&(o[f]=c[o[f]]);const p=[],m=new Map;for(f of u)m.has(f.name)||(f.fn=c[f.fn],p.push(f),m.set(f.name,f));const h=p[0]&&p[0].params.length<=2&&!D(p[0].params),d=p[1]&&p[1].params.length<=2&&!D(p[1].params),v=p[2]&&p[2].params.length<=2&&!D(p[2].params),y=p[3]&&p[3].params.length<=2&&!D(p[3].params),x=p[4]&&p[4].params.length<=2&&!D(p[4].params),w=p[5]&&p[5].params.length<=2&&!D(p[5].params),S=h&&d&&v&&y&&x&&w;for(let e=0;ee.test)),ae=p.map((e=>e.implementation)),oe=function(){for(let e=re;eg(N(e)))),t=H(arguments);if("function"!=typeof t)throw new TypeError("Callback function expected as last argument");return V(e,t)},l.referToSelf=Z,l.convert=function(e,t){const r=p(t);if(r.test(e))return e;const n=r.conversionsTo;if(0===n.length)throw new Error("There are no conversions to "+t+" defined.");for(let t=0;tt.from===e.from));if(!r)throw new Error("Attempt to remove nonexistent conversion from "+e.from+" to "+e.to);if(r.convert!==e.convert)throw new Error("Conversion to remove does not match existing conversion");const n=t.conversionsTo.indexOf(r);t.conversionsTo.splice(n,1)},l.resolve=function(e,t){if(!v(e))throw new TypeError(n);const r=e._typedFunctionData.signatures;for(let e=0;e{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e);var n={};return(()=>{"use strict";r.d(n,{default:()=>Jy});var e={};function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t(e)}function i(e){return"number"==typeof e}function a(e){return!(!e||"object"!==t(e)||"function"!=typeof e.constructor)&&(!0===e.isBigNumber&&"object"===t(e.constructor.prototype)&&!0===e.constructor.prototype.isBigNumber||"function"==typeof e.constructor.isDecimal&&!0===e.constructor.isDecimal(e))}function o(e){return e&&"object"===t(e)&&!0===Object.getPrototypeOf(e).isComplex||!1}function u(e){return e&&"object"===t(e)&&!0===Object.getPrototypeOf(e).isFraction||!1}function s(e){return e&&!0===e.constructor.prototype.isUnit||!1}function c(e){return"string"==typeof e}r.r(e),r.d(e,{createAbs:()=>la,createAccessorNode:()=>wp,createAcos:()=>_f,createAcosh:()=>Qf,createAcot:()=>el,createAcoth:()=>rl,createAcsc:()=>il,createAcsch:()=>ol,createAdd:()=>np,createAddScalar:()=>va,createAnd:()=>Mc,createApply:()=>ma,createApplyTransform:()=>vy,createArg:()=>ru,createArrayNode:()=>Dp,createAsec:()=>sl,createAsech:()=>fl,createAsin:()=>pl,createAsinh:()=>ml,createAssignmentNode:()=>Tp,createAtan:()=>hl,createAtan2:()=>vl,createAtanh:()=>gl,createAtomicMass:()=>Zv,createAvogadro:()=>Wv,createBellNumbers:()=>gd,createBigNumberClass:()=>Ir,createBignumber:()=>Ai,createBin:()=>js,createBitAnd:()=>Wo,createBitNot:()=>Jo,createBitOr:()=>Qo,createBitXor:()=>tu,createBlockNode:()=>_p,createBohrMagneton:()=>Cv,createBohrRadius:()=>_v,createBoltzmann:()=>Yv,createBoolean:()=>Ei,createCatalan:()=>bd,createCbrt:()=>ba,createCeil:()=>Ma,createChain:()=>Wm,createChainClass:()=>Lm,createClassicalElectronRadius:()=>kv,createClone:()=>Pn,createColumn:()=>gu,createColumnTransform:()=>yy,createCombinations:()=>Ih,createCombinationsWithRep:()=>qh,createCompare:()=>Oc,createCompareNatural:()=>kc,createCompareText:()=>zc,createCompile:()=>hm,createComplex:()=>Si,createComplexClass:()=>zr,createComposition:()=>Nd,createConcat:()=>vu,createConcatTransform:()=>By,createConditionalNode:()=>Ip,createConductanceQuantum:()=>Mv,createConj:()=>iu,createConstantNode:()=>Hp,createCorr:()=>Th,createCos:()=>bl,createCosh:()=>Nl,createCot:()=>Dl,createCoth:()=>Al,createCoulomb:()=>Av,createCount:()=>bu,createCreateUnit:()=>Tf,createCross:()=>Nu,createCsc:()=>Sl,createCsch:()=>Ml,createCtranspose:()=>vs,createCube:()=>Oa,createCumSum:()=>gh,createCumSumTransform:()=>jy,createDeepEqual:()=>rf,createDenseMatrixClass:()=>qn,createDerivative:()=>Pd,createDet:()=>Ym,createDeuteronMass:()=>Pv,createDiag:()=>Eu,createDiff:()=>Uu,createDiffTransform:()=>ky,createDistance:()=>hh,createDivide:()=>ph,createDivideScalar:()=>Xs,createDot:()=>sp,createDotDivide:()=>fc,createDotMultiply:()=>Oo,createDotPow:()=>sc,createE:()=>av,createEfimovFactor:()=>Vv,createEigs:()=>th,createElectricConstant:()=>Dv,createElectronMass:()=>Iv,createElementaryCharge:()=>Sv,createEqual:()=>jc,createEqualScalar:()=>gi,createEqualText:()=>Uc,createErf:()=>Es,createEvaluate:()=>vm,createExp:()=>Ta,createExpm:()=>nh,createExpm1:()=>_a,createFactorial:()=>Xh,createFalse:()=>Kd,createFaraday:()=>Jv,createFermiCoupling:()=>Rv,createFft:()=>xs,createFibonacciHeapClass:()=>vf,createFilter:()=>Su,createFilterTransform:()=>xy,createFineStructure:()=>zv,createFirstRadiation:()=>Xv,createFix:()=>za,createFlatten:()=>Fu,createFloor:()=>La,createForEach:()=>Tu,createForEachTransform:()=>wy,createFormat:()=>qs,createFraction:()=>Ci,createFractionClass:()=>jr,createFreqz:()=>Vd,createFunctionAssignmentNode:()=>Zp,createFunctionNode:()=>fm,createGamma:()=>Zh,createGasConstant:()=>Kv,createGcd:()=>io,createGetMatrixDataType:()=>ku,createGravitationConstant:()=>xv,createGravity:()=>sy,createHartreeEnergy:()=>qv,createHasNumericValue:()=>oi,createHelp:()=>Vm,createHelpClass:()=>Pm,createHex:()=>Ls,createHypot:()=>ap,createI:()=>mv,createIdentity:()=>Ru,createIfft:()=>ws,createIm:()=>au,createImmutableDenseMatrixClass:()=>mf,createIndex:()=>lp,createIndexClass:()=>hf,createIndexNode:()=>Yp,createIndexTransform:()=>Ny,createInfinity:()=>tv,createIntersect:()=>dh,createInv:()=>Jm,createInverseConductanceQuantum:()=>Fv,createInvmod:()=>Co,createIsInteger:()=>Yn,createIsNaN:()=>pi,createIsNegative:()=>ri,createIsNumeric:()=>ii,createIsPositive:()=>si,createIsPrime:()=>Ws,createIsZero:()=>fi,createKldivergence:()=>Kh,createKlitzing:()=>Bv,createKron:()=>qu,createLN10:()=>sv,createLN2:()=>uv,createLOG10E:()=>fv,createLOG2E:()=>cv,createLarger:()=>Jc,createLargerEq:()=>Kc,createLcm:()=>oo,createLeafCount:()=>Ed,createLeftShift:()=>Dc,createLgamma:()=>Yh,createLog:()=>rc,createLog10:()=>so,createLog1p:()=>ic,createLog2:()=>fo,createLoschmidt:()=>Qv,createLsolve:()=>mc,createLsolveAll:()=>yc,createLup:()=>bm,createLusolve:()=>zm,createLyap:()=>lh,createMad:()=>Dh,createMagneticConstant:()=>Nv,createMagneticFluxQuantum:()=>Ov,createMap:()=>ju,createMapTransform:()=>Dy,createMatrix:()=>Fi,createMatrixClass:()=>Lr,createMatrixFromColumns:()=>zi,createMatrixFromFunction:()=>Ti,createMatrixFromRows:()=>ki,createMax:()=>lf,createMaxTransform:()=>Sy,createMean:()=>bh,createMeanTransform:()=>Cy,createMedian:()=>Nh,createMin:()=>pf,createMinTransform:()=>My,createMod:()=>Ya,createMode:()=>_s,createMolarMass:()=>oy,createMolarMassC12:()=>uy,createMolarPlanckConstant:()=>ey,createMolarVolume:()=>ty,createMultinomial:()=>td,createMultiply:()=>mo,createMultiplyScalar:()=>lo,createNaN:()=>rv,createNeutronMass:()=>Lv,createNode:()=>mp,createNorm:()=>up,createNot:()=>pu,createNthRoot:()=>vo,createNthRoots:()=>oc,createNuclearMagneton:()=>Tv,createNull:()=>ev,createNumber:()=>bi,createNumeric:()=>Ys,createObjectNode:()=>Xp,createOct:()=>Ps,createOnes:()=>$u,createOperatorNode:()=>Kp,createOr:()=>mu,createParenthesisNode:()=>tm,createParse:()=>pm,createParser:()=>xm,createParserClass:()=>ym,createPartitionSelect:()=>sf,createPermutations:()=>nd,createPhi:()=>ov,createPi:()=>nv,createPickRandom:()=>cd,createPinv:()=>Qm,createPlanckCharge:()=>py,createPlanckConstant:()=>bv,createPlanckLength:()=>cy,createPlanckMass:()=>fy,createPlanckTemperature:()=>my,createPlanckTime:()=>ly,createPolynomialRoot:()=>jm,createPow:()=>Qs,createPrint:()=>Hs,createPrintTransform:()=>$y,createProd:()=>Rs,createProtonMass:()=>jv,createQr:()=>wm,createQuantileSeq:()=>Mh,createQuantileSeqTransform:()=>zy,createQuantumOfCirculation:()=>Uv,createRandom:()=>pd,createRandomInt:()=>hd,createRange:()=>Wu,createRangeClass:()=>Pr,createRangeNode:()=>nm,createRangeTransform:()=>Fy,createRationalize:()=>Ud,createRe:()=>ou,createReducedPlanckConstant:()=>wv,createRelationalNode:()=>am,createReplacer:()=>Wd,createReshape:()=>Ju,createResize:()=>Xu,createResolve:()=>Rd,createResultSet:()=>Qe,createReviver:()=>Zd,createRightArithShift:()=>Ac,createRightLogShift:()=>Cc,createRotate:()=>Ku,createRotationMatrix:()=>ts,createRound:()=>tc,createRow:()=>rs,createRowTransform:()=>Oy,createRydberg:()=>$v,createSQRT1_2:()=>lv,createSQRT2:()=>pv,createSackurTetrode:()=>ry,createSchur:()=>ch,createSec:()=>Fl,createSech:()=>Tl,createSecondRadiation:()=>ny,createSetCartesian:()=>ql,createSetDifference:()=>Pl,createSetDistinct:()=>Ul,createSetIntersect:()=>Hl,createSetIsSubset:()=>Vl,createSetMultiplicity:()=>Wl,createSetPowerset:()=>Jl,createSetSize:()=>Ql,createSetSymDifference:()=>ep,createSetUnion:()=>rp,createSign:()=>go,createSimplify:()=>Od,createSimplifyConstant:()=>_d,createSimplifyCore:()=>Id,createSin:()=>Bl,createSinh:()=>kl,createSize:()=>is,createSlu:()=>km,createSmaller:()=>Hc,createSmallerEq:()=>Zc,createSolveODE:()=>Ds,createSort:()=>ff,createSpaClass:()=>yf,createSparse:()=>Ff,createSparseMatrixClass:()=>xi,createSpeedOfLight:()=>gv,createSplitUnit:()=>ji,createSqrt:()=>xo,createSqrtm:()=>ah,createSquare:()=>wo,createSqueeze:()=>os,createStd:()=>Fh,createStdTransform:()=>Iy,createStefanBoltzmann:()=>iy,createStirlingS2:()=>vd,createString:()=>Ni,createSubset:()=>ss,createSubsetTransform:()=>Ty,createSubtract:()=>Do,createSubtractScalar:()=>ga,createSum:()=>vh,createSumTransform:()=>Ry,createSylvester:()=>uh,createSymbolNode:()=>om,createSymbolicEqual:()=>qd,createTan:()=>Il,createTanh:()=>Rl,createTau:()=>iv,createThomsonCrossSection:()=>Hv,createTo:()=>Vs,createTrace:()=>cp,createTranspose:()=>hs,createTrue:()=>Qd,createTypeOf:()=>hi,createTyped:()=>Ve,createUnaryMinus:()=>sa,createUnaryPlus:()=>fa,createUnequal:()=>af,createUnitClass:()=>Af,createUnitFunction:()=>Cf,createUppercaseE:()=>dv,createUppercasePi:()=>hv,createUsolve:()=>dc,createUsolveAll:()=>xc,createVacuumImpedance:()=>Ev,createVariance:()=>Sh,createVarianceTransform:()=>Ly,createVersion:()=>vv,createWeakMixingAngle:()=>Gv,createWienDisplacement:()=>ay,createXgcd:()=>Ao,createXor:()=>hu,createZeros:()=>gs,createZeta:()=>Ts,createZpk2tf:()=>Hd}),r(4043),r(7409),r(9288),r(6801),r(8742),r(228),r(3843),r(8052),r(3975),r(24),r(2003),r(8518),r(3440),r(2826),r(4284);var f=Array.isArray;function l(e){return e&&!0===e.constructor.prototype.isMatrix||!1}function p(e){return Array.isArray(e)||l(e)}function m(e){return e&&e.isDenseMatrix&&!0===e.constructor.prototype.isMatrix||!1}function h(e){return e&&e.isSparseMatrix&&!0===e.constructor.prototype.isMatrix||!1}function d(e){return e&&!0===e.constructor.prototype.isRange||!1}function v(e){return e&&!0===e.constructor.prototype.isIndex||!1}function y(e){return"boolean"==typeof e}function g(e){return e&&!0===e.constructor.prototype.isResultSet||!1}function x(e){return e&&!0===e.constructor.prototype.isHelp||!1}function b(e){return"function"==typeof e}function w(e){return e instanceof Date}function N(e){return e instanceof RegExp}function D(e){return!(!e||"object"!==t(e)||e.constructor!==Object||o(e)||u(e))}function E(e){return null===e}function A(e){return void 0===e}function S(e){return e&&!0===e.isAccessorNode&&!0===e.constructor.prototype.isNode||!1}function C(e){return e&&!0===e.isArrayNode&&!0===e.constructor.prototype.isNode||!1}function M(e){return e&&!0===e.isAssignmentNode&&!0===e.constructor.prototype.isNode||!1}function F(e){return e&&!0===e.isBlockNode&&!0===e.constructor.prototype.isNode||!1}function O(e){return e&&!0===e.isConditionalNode&&!0===e.constructor.prototype.isNode||!1}function T(e){return e&&!0===e.isConstantNode&&!0===e.constructor.prototype.isNode||!1}function B(e){return T(e)||q(e)&&1===e.args.length&&T(e.args[0])&&"-+~".includes(e.op)}function _(e){return e&&!0===e.isFunctionAssignmentNode&&!0===e.constructor.prototype.isNode||!1}function k(e){return e&&!0===e.isFunctionNode&&!0===e.constructor.prototype.isNode||!1}function I(e){return e&&!0===e.isIndexNode&&!0===e.constructor.prototype.isNode||!1}function R(e){return e&&!0===e.isNode&&!0===e.constructor.prototype.isNode||!1}function z(e){return e&&!0===e.isObjectNode&&!0===e.constructor.prototype.isNode||!1}function q(e){return e&&!0===e.isOperatorNode&&!0===e.constructor.prototype.isNode||!1}function j(e){return e&&!0===e.isParenthesisNode&&!0===e.constructor.prototype.isNode||!1}function P(e){return e&&!0===e.isRangeNode&&!0===e.constructor.prototype.isNode||!1}function L(e){return e&&!0===e.isRelationalNode&&!0===e.constructor.prototype.isNode||!1}function U(e){return e&&!0===e.isSymbolNode&&!0===e.constructor.prototype.isNode||!1}function $(e){return e&&!0===e.constructor.prototype.isChain||!1}function H(e){var r=t(e);return"object"===r?null===e?"null":a(e)?"BigNumber":e.constructor&&e.constructor.name?e.constructor.name:"Object":r}var G=r(4814);function V(e){return"boolean"==typeof e||!!isFinite(e)&&e===Math.round(e)}r(6976),r(8813),r(5239),r(2076),r(4712),r(4992),r(4338),r(7267),r(2462),r(939),r(7195),r(886),r(2320),r(6203),r(9730),r(2506),r(3584),r(6557),r(2428),r(5263),r(7221),r(2700),r(1554);var Z=Math.sign||function(e){return e>0?1:e<0?-1:0},W=Math.log2||function(e){return Math.log(e)/Math.LN2},Y=Math.log10||function(e){return Math.log(e)/Math.LN10},J=Math.log1p||function(e){return Math.log(e+1)},X=Math.cbrt||function(e){if(0===e)return e;var t,r=e<0;return r&&(e=-e),t=isFinite(e)?(e/((t=Math.exp(Math.log(e)/3))*t)+2*t)/3:e,r?-t:t},Q=Math.expm1||function(e){return e>=2e-4||e<=-2e-4?Math.exp(e)-1:e+e*e/2+e*e*e/6};function K(e,t,r){var n={2:"0b",8:"0o",16:"0x"}[t],i="";if(r){if(r<1)throw new Error("size must be in greater than 0");if(!V(r))throw new Error("size must be an integer");if(e>Math.pow(2,r-1)-1||e<-Math.pow(2,r-1))throw new Error("Value must be in range [-2^".concat(r-1,", 2^").concat(r-1,"-1]"));if(!V(e))throw new Error("Value must be an integer");e<0&&(e+=Math.pow(2,r)),i="i".concat(r)}var a="";return e<0&&(e=-e,a="-"),"".concat(a).concat(n).concat(e.toString(t)).concat(i)}function ee(e,t){if("function"==typeof t)return t(e);if(e===1/0)return"Infinity";if(e===-1/0)return"-Infinity";if(isNaN(e))return"NaN";var r,n,a="auto";if(t&&(t.notation&&(a=t.notation),i(t)?r=t:i(t.precision)&&(r=t.precision),t.wordSize&&"number"!=typeof(n=t.wordSize)))throw new Error('Option "wordSize" must be a number');switch(a){case"fixed":return re(e,r);case"exponential":return ne(e,r);case"engineering":return function(e,t){if(isNaN(e)||!isFinite(e))return String(e);var r=ie(te(e),t),n=r.exponent,a=r.coefficients,o=n%3==0?n:n<0?n-3-n%3:n-n%3;if(i(t))for(;t>a.length||n-o+1>a.length;)a.push(0);else for(var u=Math.abs(n-o)-(a.length-1),s=0;s0;)f++,c--;var l=a.slice(f).join(""),p=i(t)&&l.length||l.match(/[1-9]/)?"."+l:"",m=a.slice(0,f).join("")+p+"e"+(n>=0?"+":"")+o.toString();return r.sign+m}(e,r);case"bin":return K(e,2,n);case"oct":return K(e,8,n);case"hex":return K(e,16,n);case"auto":return function(e,t,r){if(isNaN(e)||!isFinite(e))return String(e);var n=r&&void 0!==r.lowerExp?r.lowerExp:-3,i=r&&void 0!==r.upperExp?r.upperExp:5,a=te(e),o=t?ie(a,t):a;if(o.exponent=i)return ne(e,t);var u=o.coefficients,s=o.exponent;u.length0?s:0;return c<(u=ae(-s).concat(u)).length-1&&u.splice(c+1,0,"."),o.sign+u.join("")}(e,r,t&&t).replace(/((\.\d*?)(0+))($|e)/,(function(){var e=arguments[2],t=arguments[4];return"."!==e?e+t:t}));default:throw new Error('Unknown notation "'+a+'". Choose "auto", "exponential", "fixed", "bin", "oct", or "hex.')}}function te(e){var t=String(e).toLowerCase().match(/^(-?)(\d+\.?\d*)(e([+-]?\d+))?$/);if(!t)throw new SyntaxError("Invalid number "+e);var r=t[1],n=t[2],i=parseFloat(t[4]||"0"),a=n.indexOf(".");i+=-1!==a?a-1:n.length-1;var o=n.replace(".","").replace(/^0*/,(function(e){return i-=e.length,""})).replace(/0*$/,"").split("").map((function(e){return parseInt(e)}));return 0===o.length&&(o.push(0),i++),{sign:r,coefficients:o,exponent:i}}function re(e,t){if(isNaN(e)||!isFinite(e))return String(e);var r=te(e),n="number"==typeof t?ie(r,r.exponent+1+t):r,i=n.coefficients,a=n.exponent+1,o=a+(t||0);return i.length0?"."+i.join(""):"")+"e"+(a>=0?"+":"")+a}function ie(e,t){for(var r={sign:e.sign,coefficients:e.coefficients,exponent:e.exponent},n=r.coefficients;t<=0;)n.unshift(0),r.exponent++,t++;if(n.length>t&&n.splice(t,n.length-t)[0]>=5){var i=t-1;for(n[i]++;10===n[i];)n.pop(),0===i&&(n.unshift(0),r.exponent++,i++),n[--i]++}return r}function ae(e){for(var t=[],r=0;r=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==r.return||r.return()}finally{if(u)throw a}}}}function je(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r1?t-1:0),n=1;n15)throw new TypeError("Cannot implicitly convert a number with >15 significant digits to BigNumber (value: "+e+"). Use function bignumber(x) to convert to BigNumber.");return new t(e)}},{from:"number",to:"Complex",convert:function(e){return r||We(e),new r(e,0)}},{from:"BigNumber",to:"Complex",convert:function(e){return r||We(e),new r(e.toNumber(),0)}},{from:"Fraction",to:"BigNumber",convert:function(e){throw new TypeError("Cannot implicitly convert a Fraction to BigNumber or vice versa. Use function bignumber(x) to convert to BigNumber or fraction(x) to convert to Fraction.")}},{from:"Fraction",to:"Complex",convert:function(e){return r||We(e),new r(e.valueOf(),0)}},{from:"number",to:"Fraction",convert:function(e){B||Ye(e);var t=new B(e);if(t.valueOf()!==e)throw new TypeError("Cannot implicitly convert a number to a Fraction when there will be a loss of precision (value: "+e+"). Use function fraction(x) to convert to Fraction.");return t}},{from:"string",to:"number",convert:function(e){var t=Number(e);if(isNaN(t))throw new Error('Cannot convert "'+e+'" to a number');return t}},{from:"string",to:"BigNumber",convert:function(e){t||Ze(e);try{return new t(e)}catch(t){throw new Error('Cannot convert "'+e+'" to BigNumber')}}},{from:"string",to:"Fraction",convert:function(e){B||Ye(e);try{return new B(e)}catch(t){throw new Error('Cannot convert "'+e+'" to Fraction')}}},{from:"string",to:"Complex",convert:function(e){r||We(e);try{return new r(e)}catch(t){throw new Error('Cannot convert "'+e+'" to Complex')}}},{from:"boolean",to:"number",convert:function(e){return+e}},{from:"boolean",to:"BigNumber",convert:function(e){return t||Ze(e),new t(+e)}},{from:"boolean",to:"Fraction",convert:function(e){return B||Ye(e),new B(+e)}},{from:"boolean",to:"string",convert:function(e){return String(e)}},{from:"Array",to:"Matrix",convert:function(e){return n||function(){throw new Error("Cannot convert array into a Matrix: no class 'DenseMatrix' provided")}(),new n(e)}},{from:"Matrix",to:"Array",convert:function(e){return e.valueOf()}}]),H.onMismatch=function(e,t,r){var n=H.createError(e,t,r);if(["wrongType","mismatch"].includes(n.data.category)&&1===t.length&&p(t[0])&&r.some((function(e){return!e.params.includes(",")}))){var i=new TypeError("Function '".concat(e,"' doesn't apply to matrices. To call it ")+"elementwise on a matrix 'M', try 'map(M, ".concat(e,")'."));throw i.data=n.data,i}throw n},H.onMismatch=function(e,t,r){var n=H.createError(e,t,r);if(["wrongType","mismatch"].includes(n.data.category)&&1===t.length&&p(t[0])&&r.some((function(e){return!e.params.includes(",")}))){var i=new TypeError("Function '".concat(e,"' doesn't apply to matrices. To call it ")+"elementwise on a matrix 'M', try 'map(M, ".concat(e,")'."));throw i.data=n.data,i}throw n},H}));function Ze(e){throw new Error("Cannot convert value ".concat(e," into a BigNumber: no class 'BigNumber' provided"))}function We(e){throw new Error("Cannot convert value ".concat(e," into a Complex number: no class 'Complex' provided"))}function Ye(e){throw new Error("Cannot convert value ".concat(e," into a Fraction, no class 'Fraction' provided."))}r(8150),r(9979);var Je,Xe,Qe=Ee("ResultSet",[],(function(){function e(t){if(!(this instanceof e))throw new SyntaxError("Constructor must be called with the new operator");this.entries=t||[]}return e.prototype.type="ResultSet",e.prototype.isResultSet=!0,e.prototype.valueOf=function(){return this.entries},e.prototype.toString=function(){return"["+this.entries.join(", ")+"]"},e.prototype.toJSON=function(){return{mathjs:"ResultSet",entries:this.entries}},e.fromJSON=function(t){return new e(t.entries)},e}),{isClass:!0}),Ke=(r(1013),9e15),et=1e9,tt="0123456789abcdef",rt="2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862486334095254650828067566662873690987816894829072083255546808437998948262331985283935053089653777326288461633662222876982198867465436674744042432743651550489343149393914796194044002221051017141748003688084012647080685567743216228355220114804663715659121373450747856947683463616792101806445070648000277502684916746550586856935673420670581136429224554405758925724208241314695689016758940256776311356919292033376587141660230105703089634572075440370847469940168269282808481184289314848524948644871927809676271275775397027668605952496716674183485704422507197965004714951050492214776567636938662976979522110718264549734772662425709429322582798502585509785265383207606726317164309505995087807523710333101197857547331541421808427543863591778117054309827482385045648019095610299291824318237525357709750539565187697510374970888692180205189339507238539205144634197265287286965110862571492198849978748873771345686209167058",nt="3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632789",it={precision:20,rounding:4,modulo:1,toExpNeg:-7,toExpPos:21,minE:-Ke,maxE:Ke,crypto:!1},at=!0,ot="[DecimalError] ",ut=ot+"Invalid argument: ",st=ot+"Precision limit exceeded",ct=ot+"crypto unavailable",ft="[object Decimal]",lt=Math.floor,pt=Math.pow,mt=/^0b([01]+(\.[01]*)?|\.[01]+)(p[+-]?\d+)?$/i,ht=/^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/i,dt=/^0o([0-7]+(\.[0-7]*)?|\.[0-7]+)(p[+-]?\d+)?$/i,vt=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,yt=1e7,gt=7,xt=rt.length-1,bt=nt.length-1,wt={toStringTag:ft};function Nt(e){var t,r,n,i=e.length-1,a="",o=e[0];if(i>0){for(a+=o,t=1;tr)throw Error(ut+e)}function Et(e,t,r,n){var i,a,o,u;for(a=e[0];a>=10;a/=10)--t;return--t<0?(t+=gt,i=0):(i=Math.ceil((t+1)/gt),t%=gt),a=pt(10,gt-t),u=e[i]%a|0,null==n?t<3?(0==t?u=u/100|0:1==t&&(u=u/10|0),o=r<4&&99999==u||r>3&&49999==u||5e4==u||0==u):o=(r<4&&u+1==a||r>3&&u+1==a/2)&&(e[i+1]/a/100|0)==pt(10,t-2)-1||(u==a/2||0==u)&&0==(e[i+1]/a/100|0):t<4?(0==t?u=u/1e3|0:1==t?u=u/100|0:2==t&&(u=u/10|0),o=(n||r<4)&&9999==u||!n&&r>3&&4999==u):o=((n||r<4)&&u+1==a||!n&&r>3&&u+1==a/2)&&(e[i+1]/a/1e3|0)==pt(10,t-3)-1,o}function At(e,t,r){for(var n,i,a=[0],o=0,u=e.length;or-1&&(void 0===a[n+1]&&(a[n+1]=0),a[n+1]+=a[n]/r|0,a[n]%=r)}return a.reverse()}wt.absoluteValue=wt.abs=function(){var e=new this.constructor(this);return e.s<0&&(e.s=1),Ct(e)},wt.ceil=function(){return Ct(new this.constructor(this),this.e+1,2)},wt.clampedTo=wt.clamp=function(e,t){var r=this,n=r.constructor;if(e=new n(e),t=new n(t),!e.s||!t.s)return new n(NaN);if(e.gt(t))throw Error(ut+t);return r.cmp(e)<0?e:r.cmp(t)>0?t:new n(r)},wt.comparedTo=wt.cmp=function(e){var t,r,n,i,a=this,o=a.d,u=(e=new a.constructor(e)).d,s=a.s,c=e.s;if(!o||!u)return s&&c?s!==c?s:o===u?0:!o^s<0?1:-1:NaN;if(!o[0]||!u[0])return o[0]?s:u[0]?-c:0;if(s!==c)return s;if(a.e!==e.e)return a.e>e.e^s<0?1:-1;for(t=0,r=(n=o.length)<(i=u.length)?n:i;tu[t]^s<0?1:-1;return n===i?0:n>i^s<0?1:-1},wt.cosine=wt.cos=function(){var e,t,r=this,n=r.constructor;return r.d?r.d[0]?(e=n.precision,t=n.rounding,n.precision=e+Math.max(r.e,r.sd())+gt,n.rounding=1,r=function(e,t){var r,n,i;if(t.isZero())return t;(n=t.d.length)<32?i=(1/$t(4,r=Math.ceil(n/3))).toString():(r=16,i="2.3283064365386962890625e-10"),e.precision+=r,t=Ut(e,1,t.times(i),new e(1));for(var a=r;a--;){var o=t.times(t);t=o.times(o).minus(o).times(8).plus(1)}return e.precision-=r,t}(n,Ht(n,r)),n.precision=e,n.rounding=t,Ct(2==Xe||3==Xe?r.neg():r,e,t,!0)):new n(1):new n(NaN)},wt.cubeRoot=wt.cbrt=function(){var e,t,r,n,i,a,o,u,s,c,f=this,l=f.constructor;if(!f.isFinite()||f.isZero())return new l(f);for(at=!1,(a=f.s*pt(f.s*f,1/3))&&Math.abs(a)!=1/0?n=new l(a.toString()):(r=Nt(f.d),(a=((e=f.e)-r.length+1)%3)&&(r+=1==a||-2==a?"0":"00"),a=pt(r,1/3),e=lt((e+1)/3)-(e%3==(e<0?-1:2)),(n=new l(r=a==1/0?"5e"+e:(r=a.toExponential()).slice(0,r.indexOf("e")+1)+e)).s=f.s),o=(e=l.precision)+3;;)if(c=(s=(u=n).times(u).times(u)).plus(f),n=St(c.plus(f).times(u),c.plus(s),o+2,1),Nt(u.d).slice(0,o)===(r=Nt(n.d)).slice(0,o)){if("9999"!=(r=r.slice(o-3,o+1))&&(i||"4999"!=r)){+r&&(+r.slice(1)||"5"!=r.charAt(0))||(Ct(n,e+1,1),t=!n.times(n).times(n).eq(f));break}if(!i&&(Ct(u,e+1,0),u.times(u).times(u).eq(f))){n=u;break}o+=4,i=1}return at=!0,Ct(n,e,l.rounding,t)},wt.decimalPlaces=wt.dp=function(){var e,t=this.d,r=NaN;if(t){if(r=((e=t.length-1)-lt(this.e/gt))*gt,e=t[e])for(;e%10==0;e/=10)r--;r<0&&(r=0)}return r},wt.dividedBy=wt.div=function(e){return St(this,new this.constructor(e))},wt.dividedToIntegerBy=wt.divToInt=function(e){var t=this.constructor;return Ct(St(this,new t(e),0,1,1),t.precision,t.rounding)},wt.equals=wt.eq=function(e){return 0===this.cmp(e)},wt.floor=function(){return Ct(new this.constructor(this),this.e+1,3)},wt.greaterThan=wt.gt=function(e){return this.cmp(e)>0},wt.greaterThanOrEqualTo=wt.gte=function(e){var t=this.cmp(e);return 1==t||0===t},wt.hyperbolicCosine=wt.cosh=function(){var e,t,r,n,i,a=this,o=a.constructor,u=new o(1);if(!a.isFinite())return new o(a.s?1/0:NaN);if(a.isZero())return u;r=o.precision,n=o.rounding,o.precision=r+Math.max(a.e,a.sd())+4,o.rounding=1,(i=a.d.length)<32?t=(1/$t(4,e=Math.ceil(i/3))).toString():(e=16,t="2.3283064365386962890625e-10"),a=Ut(o,1,a.times(t),new o(1),!0);for(var s,c=e,f=new o(8);c--;)s=a.times(a),a=u.minus(s.times(f.minus(s.times(f))));return Ct(a,o.precision=r,o.rounding=n,!0)},wt.hyperbolicSine=wt.sinh=function(){var e,t,r,n,i=this,a=i.constructor;if(!i.isFinite()||i.isZero())return new a(i);if(t=a.precision,r=a.rounding,a.precision=t+Math.max(i.e,i.sd())+4,a.rounding=1,(n=i.d.length)<3)i=Ut(a,2,i,i,!0);else{e=(e=1.4*Math.sqrt(n))>16?16:0|e,i=Ut(a,2,i=i.times(1/$t(5,e)),i,!0);for(var o,u=new a(5),s=new a(16),c=new a(20);e--;)o=i.times(i),i=i.times(u.plus(o.times(s.times(o).plus(c))))}return a.precision=t,a.rounding=r,Ct(i,t,r,!0)},wt.hyperbolicTangent=wt.tanh=function(){var e,t,r=this,n=r.constructor;return r.isFinite()?r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+7,n.rounding=1,St(r.sinh(),r.cosh(),n.precision=e,n.rounding=t)):new n(r.s)},wt.inverseCosine=wt.acos=function(){var e,t=this,r=t.constructor,n=t.abs().cmp(1),i=r.precision,a=r.rounding;return-1!==n?0===n?t.isNeg()?Tt(r,i,a):new r(0):new r(NaN):t.isZero()?Tt(r,i+4,a).times(.5):(r.precision=i+6,r.rounding=1,t=t.asin(),e=Tt(r,i+4,a).times(.5),r.precision=i,r.rounding=a,e.minus(t))},wt.inverseHyperbolicCosine=wt.acosh=function(){var e,t,r=this,n=r.constructor;return r.lte(1)?new n(r.eq(1)?0:NaN):r.isFinite()?(e=n.precision,t=n.rounding,n.precision=e+Math.max(Math.abs(r.e),r.sd())+4,n.rounding=1,at=!1,r=r.times(r).minus(1).sqrt().plus(r),at=!0,n.precision=e,n.rounding=t,r.ln()):new n(r)},wt.inverseHyperbolicSine=wt.asinh=function(){var e,t,r=this,n=r.constructor;return!r.isFinite()||r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+2*Math.max(Math.abs(r.e),r.sd())+6,n.rounding=1,at=!1,r=r.times(r).plus(1).sqrt().plus(r),at=!0,n.precision=e,n.rounding=t,r.ln())},wt.inverseHyperbolicTangent=wt.atanh=function(){var e,t,r,n,i=this,a=i.constructor;return i.isFinite()?i.e>=0?new a(i.abs().eq(1)?i.s/0:i.isZero()?i:NaN):(e=a.precision,t=a.rounding,n=i.sd(),Math.max(n,e)<2*-i.e-1?Ct(new a(i),e,t,!0):(a.precision=r=n-i.e,i=St(i.plus(1),new a(1).minus(i),r+e,1),a.precision=e+4,a.rounding=1,i=i.ln(),a.precision=e,a.rounding=t,i.times(.5))):new a(NaN)},wt.inverseSine=wt.asin=function(){var e,t,r,n,i=this,a=i.constructor;return i.isZero()?new a(i):(t=i.abs().cmp(1),r=a.precision,n=a.rounding,-1!==t?0===t?((e=Tt(a,r+4,n).times(.5)).s=i.s,e):new a(NaN):(a.precision=r+6,a.rounding=1,i=i.div(new a(1).minus(i.times(i)).sqrt().plus(1)).atan(),a.precision=r,a.rounding=n,i.times(2)))},wt.inverseTangent=wt.atan=function(){var e,t,r,n,i,a,o,u,s,c=this,f=c.constructor,l=f.precision,p=f.rounding;if(c.isFinite()){if(c.isZero())return new f(c);if(c.abs().eq(1)&&l+4<=bt)return(o=Tt(f,l+4,p).times(.25)).s=c.s,o}else{if(!c.s)return new f(NaN);if(l+4<=bt)return(o=Tt(f,l+4,p).times(.5)).s=c.s,o}for(f.precision=u=l+10,f.rounding=1,e=r=Math.min(28,u/gt+2|0);e;--e)c=c.div(c.times(c).plus(1).sqrt().plus(1));for(at=!1,t=Math.ceil(u/gt),n=1,s=c.times(c),o=new f(c),i=c;-1!==e;)if(i=i.times(s),a=o.minus(i.div(n+=2)),i=i.times(s),void 0!==(o=a.plus(i.div(n+=2))).d[t])for(e=t;o.d[e]===a.d[e]&&e--;);return r&&(o=o.times(2<this.d.length-2},wt.isNaN=function(){return!this.s},wt.isNegative=wt.isNeg=function(){return this.s<0},wt.isPositive=wt.isPos=function(){return this.s>0},wt.isZero=function(){return!!this.d&&0===this.d[0]},wt.lessThan=wt.lt=function(e){return this.cmp(e)<0},wt.lessThanOrEqualTo=wt.lte=function(e){return this.cmp(e)<1},wt.logarithm=wt.log=function(e){var t,r,n,i,a,o,u,s,c=this,f=c.constructor,l=f.precision,p=f.rounding;if(null==e)e=new f(10),t=!0;else{if(r=(e=new f(e)).d,e.s<0||!r||!r[0]||e.eq(1))return new f(NaN);t=e.eq(10)}if(r=c.d,c.s<0||!r||!r[0]||c.eq(1))return new f(r&&!r[0]?-1/0:1!=c.s?NaN:r?0:1/0);if(t)if(r.length>1)a=!0;else{for(i=r[0];i%10==0;)i/=10;a=1!==i}if(at=!1,o=qt(c,u=l+5),n=t?Ot(f,u+10):qt(e,u),Et((s=St(o,n,u,1)).d,i=l,p))do{if(o=qt(c,u+=10),n=t?Ot(f,u+10):qt(e,u),s=St(o,n,u,1),!a){+Nt(s.d).slice(i+1,i+15)+1==1e14&&(s=Ct(s,l+1,0));break}}while(Et(s.d,i+=10,p));return at=!0,Ct(s,l,p)},wt.minus=wt.sub=function(e){var t,r,n,i,a,o,u,s,c,f,l,p,m=this,h=m.constructor;if(e=new h(e),!m.d||!e.d)return m.s&&e.s?m.d?e.s=-e.s:e=new h(e.d||m.s!==e.s?m:NaN):e=new h(NaN),e;if(m.s!=e.s)return e.s=-e.s,m.plus(e);if(c=m.d,p=e.d,u=h.precision,s=h.rounding,!c[0]||!p[0]){if(p[0])e.s=-e.s;else{if(!c[0])return new h(3===s?-0:0);e=new h(m)}return at?Ct(e,u,s):e}if(r=lt(e.e/gt),f=lt(m.e/gt),c=c.slice(),a=f-r){for((l=a<0)?(t=c,a=-a,o=p.length):(t=p,r=f,o=c.length),a>(n=Math.max(Math.ceil(u/gt),o)+2)&&(a=n,t.length=1),t.reverse(),n=a;n--;)t.push(0);t.reverse()}else{for((l=(n=c.length)<(o=p.length))&&(o=n),n=0;n0;--n)c[o++]=0;for(n=p.length;n>a;){if(c[--n](o=(a=Math.ceil(u/gt))>o?a+1:o+1)&&(i=o,r.length=1),r.reverse();i--;)r.push(0);r.reverse()}for((o=c.length)-(i=f.length)<0&&(i=o,r=f,f=c,c=r),t=0;i;)t=(c[--i]=c[i]+f[i]+t)/yt|0,c[i]%=yt;for(t&&(c.unshift(t),++n),o=c.length;0==c[--o];)c.pop();return e.d=c,e.e=Ft(c,n),at?Ct(e,u,s):e},wt.precision=wt.sd=function(e){var t,r=this;if(void 0!==e&&e!==!!e&&1!==e&&0!==e)throw Error(ut+e);return r.d?(t=Bt(r.d),e&&r.e+1>t&&(t=r.e+1)):t=NaN,t},wt.round=function(){var e=this,t=e.constructor;return Ct(new t(e),e.e+1,t.rounding)},wt.sine=wt.sin=function(){var e,t,r=this,n=r.constructor;return r.isFinite()?r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+Math.max(r.e,r.sd())+gt,n.rounding=1,r=function(e,t){var r,n=t.d.length;if(n<3)return t.isZero()?t:Ut(e,2,t,t);r=(r=1.4*Math.sqrt(n))>16?16:0|r,t=Ut(e,2,t=t.times(1/$t(5,r)),t);for(var i,a=new e(5),o=new e(16),u=new e(20);r--;)i=t.times(t),t=t.times(a.plus(i.times(o.times(i).minus(u))));return t}(n,Ht(n,r)),n.precision=e,n.rounding=t,Ct(Xe>2?r.neg():r,e,t,!0)):new n(NaN)},wt.squareRoot=wt.sqrt=function(){var e,t,r,n,i,a,o=this,u=o.d,s=o.e,c=o.s,f=o.constructor;if(1!==c||!u||!u[0])return new f(!c||c<0&&(!u||u[0])?NaN:u?o:1/0);for(at=!1,0==(c=Math.sqrt(+o))||c==1/0?(((t=Nt(u)).length+s)%2==0&&(t+="0"),c=Math.sqrt(t),s=lt((s+1)/2)-(s<0||s%2),n=new f(t=c==1/0?"5e"+s:(t=c.toExponential()).slice(0,t.indexOf("e")+1)+s)):n=new f(c.toString()),r=(s=f.precision)+3;;)if(n=(a=n).plus(St(o,a,r+2,1)).times(.5),Nt(a.d).slice(0,r)===(t=Nt(n.d)).slice(0,r)){if("9999"!=(t=t.slice(r-3,r+1))&&(i||"4999"!=t)){+t&&(+t.slice(1)||"5"!=t.charAt(0))||(Ct(n,s+1,1),e=!n.times(n).eq(o));break}if(!i&&(Ct(a,s+1,0),a.times(a).eq(o))){n=a;break}r+=4,i=1}return at=!0,Ct(n,s,f.rounding,e)},wt.tangent=wt.tan=function(){var e,t,r=this,n=r.constructor;return r.isFinite()?r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+10,n.rounding=1,(r=r.sin()).s=1,r=St(r,new n(1).minus(r.times(r)).sqrt(),e+10,0),n.precision=e,n.rounding=t,Ct(2==Xe||4==Xe?r.neg():r,e,t,!0)):new n(NaN)},wt.times=wt.mul=function(e){var t,r,n,i,a,o,u,s,c,f=this,l=f.constructor,p=f.d,m=(e=new l(e)).d;if(e.s*=f.s,!(p&&p[0]&&m&&m[0]))return new l(!e.s||p&&!p[0]&&!m||m&&!m[0]&&!p?NaN:p&&m?0*e.s:e.s/0);for(r=lt(f.e/gt)+lt(e.e/gt),(s=p.length)<(c=m.length)&&(a=p,p=m,m=a,o=s,s=c,c=o),a=[],n=o=s+c;n--;)a.push(0);for(n=c;--n>=0;){for(t=0,i=s+n;i>n;)u=a[i]+m[n]*p[i-n-1]+t,a[i--]=u%yt|0,t=u/yt|0;a[i]=(a[i]+t)%yt|0}for(;!a[--o];)a.pop();return t?++r:a.shift(),e.d=a,e.e=Ft(a,r),at?Ct(e,l.precision,l.rounding):e},wt.toBinary=function(e,t){return Gt(this,2,e,t)},wt.toDecimalPlaces=wt.toDP=function(e,t){var r=this,n=r.constructor;return r=new n(r),void 0===e?r:(Dt(e,0,et),void 0===t?t=n.rounding:Dt(t,0,8),Ct(r,e+r.e+1,t))},wt.toExponential=function(e,t){var r,n=this,i=n.constructor;return void 0===e?r=Mt(n,!0):(Dt(e,0,et),void 0===t?t=i.rounding:Dt(t,0,8),r=Mt(n=Ct(new i(n),e+1,t),!0,e+1)),n.isNeg()&&!n.isZero()?"-"+r:r},wt.toFixed=function(e,t){var r,n,i=this,a=i.constructor;return void 0===e?r=Mt(i):(Dt(e,0,et),void 0===t?t=a.rounding:Dt(t,0,8),r=Mt(n=Ct(new a(i),e+i.e+1,t),!1,e+n.e+1)),i.isNeg()&&!i.isZero()?"-"+r:r},wt.toFraction=function(e){var t,r,n,i,a,o,u,s,c,f,l,p,m=this,h=m.d,d=m.constructor;if(!h)return new d(m);if(c=r=new d(1),n=s=new d(0),o=(a=(t=new d(n)).e=Bt(h)-m.e-1)%gt,t.d[0]=pt(10,o<0?gt+o:o),null==e)e=a>0?t:c;else{if(!(u=new d(e)).isInt()||u.lt(c))throw Error(ut+u);e=u.gt(t)?a>0?t:c:u}for(at=!1,u=new d(Nt(h)),f=d.precision,d.precision=a=h.length*gt*2;l=St(u,t,0,1,1),1!=(i=r.plus(l.times(n))).cmp(e);)r=n,n=i,i=c,c=s.plus(l.times(i)),s=i,i=t,t=u.minus(l.times(i)),u=i;return i=St(e.minus(r),n,0,1,1),s=s.plus(i.times(c)),r=r.plus(i.times(n)),s.s=c.s=m.s,p=St(c,n,a,1).minus(m).abs().cmp(St(s,r,a,1).minus(m).abs())<1?[c,n]:[s,r],d.precision=f,at=!0,p},wt.toHexadecimal=wt.toHex=function(e,t){return Gt(this,16,e,t)},wt.toNearest=function(e,t){var r=this,n=r.constructor;if(r=new n(r),null==e){if(!r.d)return r;e=new n(1),t=n.rounding}else{if(e=new n(e),void 0===t?t=n.rounding:Dt(t,0,8),!r.d)return e.s?r:e;if(!e.d)return e.s&&(e.s=r.s),e}return e.d[0]?(at=!1,r=St(r,e,0,t,1).times(e),at=!0,Ct(r)):(e.s=r.s,r=e),r},wt.toNumber=function(){return+this},wt.toOctal=function(e,t){return Gt(this,8,e,t)},wt.toPower=wt.pow=function(e){var t,r,n,i,a,o,u=this,s=u.constructor,c=+(e=new s(e));if(!(u.d&&e.d&&u.d[0]&&e.d[0]))return new s(pt(+u,c));if((u=new s(u)).eq(1))return u;if(n=s.precision,a=s.rounding,e.eq(1))return Ct(u,n,a);if((t=lt(e.e/gt))>=e.d.length-1&&(r=c<0?-c:c)<=9007199254740991)return i=kt(s,u,r,n),e.s<0?new s(1).div(i):Ct(i,n,a);if((o=u.s)<0){if(ts.maxE+1||t0?o/0:0):(at=!1,s.rounding=u.s=1,r=Math.min(12,(t+"").length),(i=zt(e.times(qt(u,n+r)),n)).d&&Et((i=Ct(i,n+5,1)).d,n,a)&&(t=n+10,+Nt((i=Ct(zt(e.times(qt(u,t+r)),t),t+5,1)).d).slice(n+1,n+15)+1==1e14&&(i=Ct(i,n+1,0))),i.s=o,at=!0,s.rounding=a,Ct(i,n,a))},wt.toPrecision=function(e,t){var r,n=this,i=n.constructor;return void 0===e?r=Mt(n,n.e<=i.toExpNeg||n.e>=i.toExpPos):(Dt(e,1,et),void 0===t?t=i.rounding:Dt(t,0,8),r=Mt(n=Ct(new i(n),e,t),e<=n.e||n.e<=i.toExpNeg,e)),n.isNeg()&&!n.isZero()?"-"+r:r},wt.toSignificantDigits=wt.toSD=function(e,t){var r=this.constructor;return void 0===e?(e=r.precision,t=r.rounding):(Dt(e,1,et),void 0===t?t=r.rounding:Dt(t,0,8)),Ct(new r(this),e,t)},wt.toString=function(){var e=this,t=e.constructor,r=Mt(e,e.e<=t.toExpNeg||e.e>=t.toExpPos);return e.isNeg()&&!e.isZero()?"-"+r:r},wt.truncated=wt.trunc=function(){return Ct(new this.constructor(this),this.e+1,1)},wt.valueOf=wt.toJSON=function(){var e=this,t=e.constructor,r=Mt(e,e.e<=t.toExpNeg||e.e>=t.toExpPos);return e.isNeg()?"-"+r:r};var St=function(){function e(e,t,r){var n,i=0,a=e.length;for(e=e.slice();a--;)n=e[a]*t+i,e[a]=n%r|0,i=n/r|0;return i&&e.unshift(i),e}function t(e,t,r,n){var i,a;if(r!=n)a=r>n?1:-1;else for(i=a=0;it[i]?1:-1;break}return a}function r(e,t,r,n){for(var i=0;r--;)e[r]-=i,i=e[r]1;)e.shift()}return function(n,i,a,o,u,s){var c,f,l,p,m,h,d,v,y,g,x,b,w,N,D,E,A,S,C,M,F=n.constructor,O=n.s==i.s?1:-1,T=n.d,B=i.d;if(!(T&&T[0]&&B&&B[0]))return new F(n.s&&i.s&&(T?!B||T[0]!=B[0]:B)?T&&0==T[0]||!B?0*O:O/0:NaN);for(s?(m=1,f=n.e-i.e):(s=yt,m=gt,f=lt(n.e/m)-lt(i.e/m)),C=B.length,A=T.length,g=(y=new F(O)).d=[],l=0;B[l]==(T[l]||0);l++);if(B[l]>(T[l]||0)&&f--,null==a?(N=a=F.precision,o=F.rounding):N=u?a+(n.e-i.e)+1:a,N<0)g.push(1),h=!0;else{if(N=N/m+2|0,l=0,1==C){for(p=0,B=B[0],N++;(l1&&(B=e(B,p,s),T=e(T,p,s),C=B.length,A=T.length),E=C,b=(x=T.slice(0,C)).length;b=s/2&&++S;do{p=0,(c=t(B,x,C,b))<0?(w=x[0],C!=b&&(w=w*s+(x[1]||0)),(p=w/S|0)>1?(p>=s&&(p=s-1),1==(c=t(d=e(B,p,s),x,v=d.length,b=x.length))&&(p--,r(d,C=10;p/=10)l++;y.e=l+f*m-1,Ct(y,u?a+y.e+1:a,o,h)}return y}}();function Ct(e,t,r,n){var i,a,o,u,s,c,f,l,p,m=e.constructor;e:if(null!=t){if(!(l=e.d))return e;for(i=1,u=l[0];u>=10;u/=10)i++;if((a=t-i)<0)a+=gt,o=t,s=(f=l[p=0])/pt(10,i-o-1)%10|0;else if((p=Math.ceil((a+1)/gt))>=(u=l.length)){if(!n)break e;for(;u++<=p;)l.push(0);f=s=0,i=1,o=(a%=gt)-gt+1}else{for(f=u=l[p],i=1;u>=10;u/=10)i++;s=(o=(a%=gt)-gt+i)<0?0:f/pt(10,i-o-1)%10|0}if(n=n||t<0||void 0!==l[p+1]||(o<0?f:f%pt(10,i-o-1)),c=r<4?(s||n)&&(0==r||r==(e.s<0?3:2)):s>5||5==s&&(4==r||n||6==r&&(a>0?o>0?f/pt(10,i-o):0:l[p-1])%10&1||r==(e.s<0?8:7)),t<1||!l[0])return l.length=0,c?(t-=e.e+1,l[0]=pt(10,(gt-t%gt)%gt),e.e=-t||0):l[0]=e.e=0,e;if(0==a?(l.length=p,u=1,p--):(l.length=p+1,u=pt(10,gt-a),l[p]=o>0?(f/pt(10,i-o)%pt(10,o)|0)*u:0),c)for(;;){if(0==p){for(a=1,o=l[0];o>=10;o/=10)a++;for(o=l[0]+=u,u=1;o>=10;o/=10)u++;a!=u&&(e.e++,l[0]==yt&&(l[0]=1));break}if(l[p]+=u,l[p]!=yt)break;l[p--]=0,u=1}for(a=l.length;0===l[--a];)l.pop()}return at&&(e.e>m.maxE?(e.d=null,e.e=NaN):e.e0?a=a.charAt(0)+"."+a.slice(1)+_t(n):o>1&&(a=a.charAt(0)+"."+a.slice(1)),a=a+(e.e<0?"e":"e+")+e.e):i<0?(a="0."+_t(-i-1)+a,r&&(n=r-o)>0&&(a+=_t(n))):i>=o?(a+=_t(i+1-o),r&&(n=r-i-1)>0&&(a=a+"."+_t(n))):((n=i+1)0&&(i+1===o&&(a+="."),a+=_t(n))),a}function Ft(e,t){var r=e[0];for(t*=gt;r>=10;r/=10)t++;return t}function Ot(e,t,r){if(t>xt)throw at=!0,r&&(e.precision=r),Error(st);return Ct(new e(rt),t,1,!0)}function Tt(e,t,r){if(t>bt)throw Error(st);return Ct(new e(nt),t,r,!0)}function Bt(e){var t=e.length-1,r=t*gt+1;if(t=e[t]){for(;t%10==0;t/=10)r--;for(t=e[0];t>=10;t/=10)r++}return r}function _t(e){for(var t="";e--;)t+="0";return t}function kt(e,t,r,n){var i,a=new e(1),o=Math.ceil(n/gt+4);for(at=!1;;){if(r%2&&Vt((a=a.times(t)).d,o)&&(i=!0),0===(r=lt(r/2))){r=a.d.length-1,i&&0===a.d[r]&&++a.d[r];break}Vt((t=t.times(t)).d,o)}return at=!0,a}function It(e){return 1&e.d[e.d.length-1]}function Rt(e,t,r){for(var n,i=new e(t[0]),a=0;++a17)return new p(e.d?e.d[0]?e.s<0?0:1/0:1:e.s?e.s<0?0:e:NaN);for(null==t?(at=!1,s=h):s=t,u=new p(.03125);e.e>-2;)e=e.times(u),l+=5;for(s+=n=Math.log(pt(2,l))/Math.LN10*2+5|0,r=a=o=new p(1),p.precision=s;;){if(a=Ct(a.times(e),s,1),r=r.times(++f),Nt((u=o.plus(St(a,r,s,1))).d).slice(0,s)===Nt(o.d).slice(0,s)){for(i=l;i--;)o=Ct(o.times(o),s,1);if(null!=t)return p.precision=h,o;if(!(c<3&&Et(o.d,s-n,m,c)))return Ct(o,p.precision=h,m,at=!0);p.precision=s+=10,r=a=u=new p(1),f=0,c++}o=u}}function qt(e,t){var r,n,i,a,o,u,s,c,f,l,p,m=1,h=e,d=h.d,v=h.constructor,y=v.rounding,g=v.precision;if(h.s<0||!d||!d[0]||!h.e&&1==d[0]&&1==d.length)return new v(d&&!d[0]?-1/0:1!=h.s?NaN:d?0:h);if(null==t?(at=!1,f=g):f=t,v.precision=f+=10,n=(r=Nt(d)).charAt(0),!(Math.abs(a=h.e)<15e14))return c=Ot(v,f+2,g).times(a+""),h=qt(new v(n+"."+r.slice(1)),f-10).plus(c),v.precision=g,null==t?Ct(h,g,y,at=!0):h;for(;n<7&&1!=n||1==n&&r.charAt(1)>3;)n=(r=Nt((h=h.times(e)).d)).charAt(0),m++;for(a=h.e,n>1?(h=new v("0."+r),a++):h=new v(n+"."+r.slice(1)),l=h,s=o=h=St(h.minus(1),h.plus(1),f,1),p=Ct(h.times(h),f,1),i=3;;){if(o=Ct(o.times(p),f,1),Nt((c=s.plus(St(o,new v(i),f,1))).d).slice(0,f)===Nt(s.d).slice(0,f)){if(s=s.times(2),0!==a&&(s=s.plus(Ot(v,f+2,g).times(a+""))),s=St(s,new v(m),f,1),null!=t)return v.precision=g,s;if(!Et(s.d,f-10,y,u))return Ct(s,v.precision=g,y,at=!0);v.precision=f+=10,c=o=h=St(l.minus(1),l.plus(1),f,1),p=Ct(h.times(h),f,1),i=u=1}s=c,i+=2}}function jt(e){return String(e.s*e.s/0)}function Pt(e,t){var r,n,i;for((r=t.indexOf("."))>-1&&(t=t.replace(".","")),(n=t.search(/e/i))>0?(r<0&&(r=n),r+=+t.slice(n+1),t=t.substring(0,n)):r<0&&(r=t.length),n=0;48===t.charCodeAt(n);n++);for(i=t.length;48===t.charCodeAt(i-1);--i);if(t=t.slice(n,i)){if(i-=n,e.e=r=r-n-1,e.d=[],n=(r+1)%gt,r<0&&(n+=gt),ne.constructor.maxE?(e.d=null,e.e=NaN):e.e-1){if(t=t.replace(/(\d)_(?=\d)/g,"$1"),vt.test(t))return Pt(e,t)}else if("Infinity"===t||"NaN"===t)return+t||(e.s=NaN),e.e=NaN,e.d=null,e;if(ht.test(t))r=16,t=t.toLowerCase();else if(mt.test(t))r=2;else{if(!dt.test(t))throw Error(ut+t);r=8}for((a=t.search(/p/i))>0?(s=+t.slice(a+1),t=t.substring(2,a)):t=t.slice(2),o=(a=t.indexOf("."))>=0,n=e.constructor,o&&(a=(u=(t=t.replace(".","")).length)-a,i=kt(n,new n(r),a,2*a)),a=f=(c=At(t,r,yt)).length-1;0===c[a];--a)c.pop();return a<0?new n(0*e.s):(e.e=Ft(c,f),e.d=c,at=!1,o&&(e=St(e,i,4*u)),s&&(e=e.times(Math.abs(s)<54?pt(2,s):_r.pow(2,s))),at=!0,e)}function Ut(e,t,r,n,i){var a,o,u,s,c=e.precision,f=Math.ceil(c/gt);for(at=!1,s=r.times(r),u=new e(n);;){if(o=St(u.times(s),new e(t++*t++),c,1),u=i?n.plus(o):n.minus(o),n=St(o.times(s),new e(t++*t++),c,1),void 0!==(o=u.plus(n)).d[f]){for(a=f;o.d[a]===u.d[a]&&a--;);if(-1==a)break}a=u,u=n,n=o,o=a}return at=!0,o.d.length=f+1,o}function $t(e,t){for(var r=e;--t;)r*=e;return r}function Ht(e,t){var r,n=t.s<0,i=Tt(e,e.precision,1),a=i.times(.5);if((t=t.abs()).lte(a))return Xe=n?4:1,t;if((r=t.divToInt(i)).isZero())Xe=n?3:2;else{if((t=t.minus(r.times(i))).lte(a))return Xe=It(r)?n?2:3:n?4:1,t;Xe=It(r)?n?1:4:n?3:2}return t.minus(i).abs()}function Gt(e,t,r,n){var i,a,o,u,s,c,f,l,p,m=e.constructor,h=void 0!==r;if(h?(Dt(r,1,et),void 0===n?n=m.rounding:Dt(n,0,8)):(r=m.precision,n=m.rounding),e.isFinite()){for(h?(i=2,16==t?r=4*r-3:8==t&&(r=3*r-2)):i=t,(o=(f=Mt(e)).indexOf("."))>=0&&(f=f.replace(".",""),(p=new m(1)).e=f.length-o,p.d=At(Mt(p),10,i),p.e=p.d.length),a=s=(l=At(f,10,i)).length;0==l[--s];)l.pop();if(l[0]){if(o<0?a--:((e=new m(e)).d=l,e.e=a,l=(e=St(e,p,r,n,0,i)).d,a=e.e,c=Je),o=l[r],u=i/2,c=c||void 0!==l[r+1],c=n<4?(void 0!==o||c)&&(0===n||n===(e.s<0?3:2)):o>u||o===u&&(4===n||c||6===n&&1&l[r-1]||n===(e.s<0?8:7)),l.length=r,c)for(;++l[--r]>i-1;)l[r]=0,r||(++a,l.unshift(1));for(s=l.length;!l[s-1];--s);for(o=0,f="";o1)if(16==t||8==t){for(o=16==t?4:3,--s;s%o;s++)f+="0";for(s=(l=At(f,i,t)).length;!l[s-1];--s);for(o=1,f="1.";os)for(a-=s;a--;)f+="0";else at)return e.length=t,!0}function Zt(e){return new this(e).abs()}function Wt(e){return new this(e).acos()}function Yt(e){return new this(e).acosh()}function Jt(e,t){return new this(e).plus(t)}function Xt(e){return new this(e).asin()}function Qt(e){return new this(e).asinh()}function Kt(e){return new this(e).atan()}function er(e){return new this(e).atanh()}function tr(e,t){e=new this(e),t=new this(t);var r,n=this.precision,i=this.rounding,a=n+4;return e.s&&t.s?e.d||t.d?!t.d||e.isZero()?(r=t.s<0?Tt(this,n,i):new this(0)).s=e.s:!e.d||t.isZero()?(r=Tt(this,a,1).times(.5)).s=e.s:t.s<0?(this.precision=a,this.rounding=1,r=this.atan(St(e,t,a,1)),t=Tt(this,a,1),this.precision=n,this.rounding=i,r=e.s<0?r.minus(t):r.plus(t)):r=this.atan(St(e,t,a,1)):(r=Tt(this,a,1).times(t.s>0?.25:.75)).s=e.s:r=new this(NaN),r}function rr(e){return new this(e).cbrt()}function nr(e){return Ct(e=new this(e),e.e+1,2)}function ir(e,t,r){return new this(e).clamp(t,r)}function ar(e){if(!e||"object"!=typeof e)throw Error(ot+"Object expected");var t,r,n,i=!0===e.defaults,a=["precision",1,et,"rounding",0,8,"toExpNeg",-Ke,0,"toExpPos",0,Ke,"maxE",0,Ke,"minE",-Ke,0,"modulo",0,9];for(t=0;t=a[t+1]&&n<=a[t+2]))throw Error(ut+r+": "+n);this[r]=n}if(r="crypto",i&&(this[r]=it[r]),void 0!==(n=e[r])){if(!0!==n&&!1!==n&&0!==n&&1!==n)throw Error(ut+r+": "+n);if(n){if("undefined"==typeof crypto||!crypto||!crypto.getRandomValues&&!crypto.randomBytes)throw Error(ct);this[r]=!0}else this[r]=!1}return this}function or(e){return new this(e).cos()}function ur(e){return new this(e).cosh()}function sr(e,t){return new this(e).div(t)}function cr(e){return new this(e).exp()}function fr(e){return Ct(e=new this(e),e.e+1,3)}function lr(){var e,t,r=new this(0);for(at=!1,e=0;e=429e7?t[a]=crypto.getRandomValues(new Uint32Array(1))[0]:u[a++]=i%1e7;else{if(!crypto.randomBytes)throw Error(ct);for(t=crypto.randomBytes(n*=4);a=214e7?crypto.randomBytes(4).copy(t,a):(u.push(i%1e7),a+=4);a=n/4}else for(;a=10;i/=10)n++;na.maxE?(i.e=NaN,i.d=null):e.e=10;r/=10)t++;return void(at?t>a.maxE?(i.e=NaN,i.d=null):tt.re?1:e.ret.im?1:e.im0?this.step>0?this.start:this.start+(e-1)*this.step:void 0},e.prototype.max=function(){var e=this.size()[0];return e>0?this.step>0?this.start+(e-1)*this.step:this.start:void 0},e.prototype.forEach=function(e){var t=this.start,r=this.step,n=this.end,i=0;if(r>0)for(;tn;)e(t,[i],this),t+=r,i++},e.prototype.map=function(e){var t=[];return this.forEach((function(r,n,i){t[n[0]]=e(r,n,i)})),t},e.prototype.toArray=function(){var e=[];return this.forEach((function(t,r){e[r[0]]=t})),e},e.prototype.valueOf=function(){return this.toArray()},e.prototype.format=function(e){var t=ee(this.start,e);return 1!==this.step&&(t+=":"+ee(this.step,e)),t+":"+ee(this.end,e)},e.prototype.toString=function(){return this.format()},e.prototype.toJSON=function(){return{mathjs:"Range",start:this.start,end:this.end,step:this.step}},e.fromJSON=function(t){return new e(t.start,t.end,t.step)},e}),{isClass:!0})),Lr=Ee("Matrix",[],(function(){function e(){if(!(this instanceof e))throw new SyntaxError("Constructor must be called with the new operator")}return e.prototype.type="Matrix",e.prototype.isMatrix=!0,e.prototype.storage=function(){throw new Error("Cannot invoke storage on a Matrix interface")},e.prototype.datatype=function(){throw new Error("Cannot invoke datatype on a Matrix interface")},e.prototype.create=function(e,t){throw new Error("Cannot invoke create on a Matrix interface")},e.prototype.subset=function(e,t,r){throw new Error("Cannot invoke subset on a Matrix interface")},e.prototype.get=function(e){throw new Error("Cannot invoke get on a Matrix interface")},e.prototype.set=function(e,t,r){throw new Error("Cannot invoke set on a Matrix interface")},e.prototype.resize=function(e,t){throw new Error("Cannot invoke resize on a Matrix interface")},e.prototype.reshape=function(e,t){throw new Error("Cannot invoke reshape on a Matrix interface")},e.prototype.clone=function(){throw new Error("Cannot invoke clone on a Matrix interface")},e.prototype.size=function(){throw new Error("Cannot invoke size on a Matrix interface")},e.prototype.map=function(e,t){throw new Error("Cannot invoke map on a Matrix interface")},e.prototype.forEach=function(e){throw new Error("Cannot invoke forEach on a Matrix interface")},e.prototype[Symbol.iterator]=function(){throw new Error("Cannot iterate a Matrix interface")},e.prototype.toArray=function(){throw new Error("Cannot invoke toArray on a Matrix interface")},e.prototype.valueOf=function(){throw new Error("Cannot invoke valueOf on a Matrix interface")},e.prototype.format=function(e){throw new Error("Cannot invoke format on a Matrix interface")},e.prototype.toString=function(){throw new Error("Cannot invoke toString on a Matrix interface")},e}),{isClass:!0}),Ur=r(4687);function $r(){return $r=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0?"+":"")+n.toString()}(e,r);case"bin":return Zr(e,2,n);case"oct":return Zr(e,8,n);case"hex":return Zr(e,16,n);case"auto":var a=t&&void 0!==t.lowerExp?t.lowerExp:-3,o=t&&void 0!==t.upperExp?t.upperExp:5;if(e.isZero())return"0";var u=e.toSignificantDigits(r),s=u.e;return(s>=a&&sr.truncate?n.substring(0,r.truncate-3)+"...":n}function Xr(e){for(var t=String(e),r="",n=0;n/g,">")}function en(e,t){if(Array.isArray(e)){for(var r="[",n=e.length,i=0;it?1:-1}function rn(e,t,r){if(!(this instanceof rn))throw new SyntaxError("Constructor must be called with the new operator");this.actual=e,this.expected=t,this.relation=r,this.message="Dimension mismatch ("+(Array.isArray(e)?"["+e.join(", ")+"]":e)+" "+(this.relation||"!=")+" "+(Array.isArray(t)?"["+t.join(", ")+"]":t)+")",this.stack=(new Error).stack}function nn(e,t,r){if(!(this instanceof nn))throw new SyntaxError("Constructor must be called with the new operator");this.index=e,arguments.length<3?(this.min=0,this.max=t):(this.min=t,this.max=r),void 0!==this.min&&this.index=this.max?this.message="Index out of range ("+this.index+" > "+(this.max-1)+")":this.message="Index out of range ("+this.index+")",this.stack=(new Error).stack}function an(e){for(var t=[];Array.isArray(e);)t.push(e.length),e=e[0];return t}function on(e,t,r){var n,i=e.length;if(i!==t[r])throw new rn(i,t[r]);if(r")}function un(e,t){if(0===t.length){if(Array.isArray(e))throw new rn(e.length,0)}else on(e,t,0)}function sn(e,t){var r=e.isMatrix?e._size:an(e);t._sourceSize.forEach((function(e,t){if(null!==e&&e!==r[t])throw new rn(e,r[t])}))}function cn(e,t){if(void 0!==e){if(!i(e)||!V(e))throw new TypeError("Index must be an integer (value: "+e+")");if(e<0||"number"==typeof t&&e>=t)throw new nn(e,t)}}function fn(e){for(var t=0;t=0){if(t%r!=0)throw new Error("Could not replace wildcard, since "+t+" is no multiple of "+-r);n[i]=-t/r}return n}function dn(e){return e.reduce((function(e,t){return e*t}),1)}function vn(e,t){for(var r=t||an(e);Array.isArray(e)&&1===e.length;)e=e[0],r.shift();for(var n=r.length;1===r[n-1];)n--;return n1)return e.slice(1).reduce((function(e,r){return On(e,r,t,0)}),e[0]);throw new Error("Wrong number of arguments in function concat")}function Bn(e,t){for(var r=t.length,n=e.length,i=0;i1||e[i]>t[a])throw new Error("shape missmatch: missmatch is found in arg with shape (".concat(e,") not possible to broadcast dimension ").concat(n," with size ").concat(e[i]," to size ").concat(t[a]))}}function _n(e,t){var r=an(e);if(ge(r,t))return e;Bn(r,t);var n,i,a,o=function(){for(var e=arguments.length,t=new Array(e),r=0;ra[f]&&(a[f]=u[c])}for(var l=0;l1&&void 0!==arguments[1]?arguments[1]:{},n=r.hasher,i=r.limit;return i=null==i?Number.POSITIVE_INFINITY:i,n=null==n?JSON.stringify:n,function r(){"object"!==t(r.cache)&&(r.cache={values:new Map,lru:kn(i||Number.POSITIVE_INFINITY)});for(var a=[],o=0;oe.length)&&(t=e.length);for(var r=0,n=new Array(t);rn[a]&&(n[a]=t[a],i=!0);i&&u(e,n,r)}function m(e){return l(e)?m(e.valueOf()):f(e)?e.map(m):e}return r.prototype=new t,r.prototype.createDenseMatrix=function(e,t){return new r(e,t)},Object.defineProperty(r,"name",{value:"DenseMatrix"}),r.prototype.constructor=r,r.prototype.type="DenseMatrix",r.prototype.isDenseMatrix=!0,r.prototype.getDataType=function(){return Mn(this._data,H)},r.prototype.storage=function(){return"dense"},r.prototype.datatype=function(){return this._datatype},r.prototype.create=function(e,t){return new r(e,t)},r.prototype.subset=function(e,t,i){switch(arguments.length){case 1:return function(e,t){if(!v(t))throw new TypeError("Invalid index");if(t.isScalar())return e.get(t.min());var i=t.size();if(i.length!==e._size.length)throw new rn(i.length,e._size.length);for(var a=t.min(),o=t.max(),u=0,s=e._size.length;u");var p=t.max().map((function(e){return e+1}));s(e,p,n);var m=a.length;o(e._data,t,r,m,0)}return e}(this,e,t,i);default:throw new SyntaxError("Wrong number of arguments")}},r.prototype.get=function(e){if(!f(e))throw new TypeError("Array expected");if(e.length!==this._size.length)throw new rn(e.length,this._size.length);for(var t=0;t=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==r.return||r.return()}finally{if(u)throw a}}}}(this._data);try{for(n.s();!(t=n.n()).done;){var i=t.value;e.push(new r([i],this._datatype))}}catch(e){n.e(e)}finally{n.f()}return e},r.prototype.columns=function(){var e=this,t=[],n=this.size();if(2!==n.length)throw new TypeError("Rows can only be returned for a 2D matrix.");for(var i=this._data,a=function(n){var a=i.map((function(e){return[e[n]]}));t.push(new r(a,e._datatype))},o=0;o0?e:0,n=e<0?-e:0,o=this._size[0],u=this._size[1],s=Math.min(o-n,u-t),c=[],f=0;f0?n:0,c=n<0?-n:0,p=e[0],m=e[1],h=Math.min(p-c,m-s);if(f(t)){if(t.length!==h)throw new Error("Invalid value array length");u=function(e){return t[e]}}else if(l(t)){var d=t.size();if(1!==d.length||d[0]!==h)throw new Error("Invalid matrix length");u=function(e){return t.get([e])}}else u=function(){return t};o||(o=a(u(0))?u(0).mul(0):0);var v=[];if(e.length>0){v=ln(v,e,o);for(var y=0;y=n.length)throw new nn(t,n.length);return l(e)?e.create(Vn(e.valueOf(),t,r)):Vn(e,t,r)}function Vn(e,t,r){var n,i,a,o;if(t<=0){if(Array.isArray(e[0])){for(o=Ln(e),i=[],n=0;n0}function Kn(e){return 0===e}function ei(e){return Number.isNaN(e)}Xn.signature=Jn,Qn.signature=Jn,Kn.signature=Jn,ei.signature=Jn;var ti="isNegative",ri=Ee(ti,["typed"],(function(e){var t=e.typed;return t(ti,{number:Xn,BigNumber:function(e){return e.isNeg()&&!e.isZero()&&!e.isNaN()},Fraction:function(e){return e.s<0},Unit:t.referToSelf((function(e){return function(r){return t.find(e,r.valueType())(r.value)}})),"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),ni="isNumeric",ii=Ee(ni,["typed"],(function(e){var t=e.typed;return t(ni,{"number | BigNumber | Fraction | boolean":function(){return!0},"Complex | Unit | string | null | undefined | Node":function(){return!1},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),ai=(r(8436),"hasNumericValue"),oi=Ee(ai,["typed","isNumeric"],(function(e){var t=e.typed,r=e.isNumeric;return t(ai,{boolean:function(){return!0},string:function(e){return e.trim().length>0&&!isNaN(Number(e))},any:function(e){return r(e)}})})),ui="isPositive",si=Ee(ui,["typed"],(function(e){var t=e.typed;return t(ui,{number:Qn,BigNumber:function(e){return!e.isNeg()&&!e.isZero()&&!e.isNaN()},Fraction:function(e){return e.s>0&&e.n>0},Unit:t.referToSelf((function(e){return function(r){return t.find(e,r.valueType())(r.value)}})),"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),ci="isZero",fi=Ee(ci,["typed"],(function(e){var t=e.typed;return t(ci,{number:Kn,BigNumber:function(e){return e.isZero()},Complex:function(e){return 0===e.re&&0===e.im},Fraction:function(e){return 1===e.d&&0===e.n},Unit:t.referToSelf((function(e){return function(r){return t.find(e,r.valueType())(r.value)}})),"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),li="isNaN",pi=Ee(li,["typed"],(function(e){return(0,e.typed)(li,{number:ei,BigNumber:function(e){return e.isNaN()},Fraction:function(e){return!1},Complex:function(e){return e.isNaN()},Unit:function(e){return Number.isNaN(e.value)},"Array | Matrix":function(e){return Hn(e,Number.isNaN)}})})),mi="typeOf",hi=Ee(mi,["typed"],(function(e){return(0,e.typed)(mi,{any:H})}));function di(e,t,r){if(null==r)return e.eq(t);if(e.eq(t))return!0;if(e.isNaN()||t.isNaN())return!1;if(e.isFinite()&&t.isFinite()){var n=e.minus(t).abs();if(n.isZero())return!0;var i=e.constructor.max(e.abs(),t.abs());return n.lte(i.times(r))}return!1}var vi=Ee("compareUnits",["typed"],(function(e){var t=e.typed;return{"Unit, Unit":t.referToSelf((function(e){return function(r,n){if(!r.equalBase(n))throw new Error("Cannot compare units with different base");return t.find(e,[r.valueType(),n.valueType()])(r.value,n.value)}}))}})),yi="equalScalar",gi=Ee(yi,["typed","config"],(function(e){var t=e.typed,r=e.config,n=vi({typed:t});return t(yi,{"boolean, boolean":function(e,t){return e===t},"number, number":function(e,t){return ue(e,t,r.epsilon)},"BigNumber, BigNumber":function(e,t){return e.eq(t)||di(e,t,r.epsilon)},"Fraction, Fraction":function(e,t){return e.equals(t)},"Complex, Complex":function(e,t){return function(e,t,r){return ue(e.re,t.re,r)&&ue(e.im,t.im,r)}(e,t,r.epsilon)}},n)})),xi=(Ee(yi,["typed","config"],(function(e){var t=e.typed,r=e.config;return t(yi,{"number, number":function(e,t){return ue(e,t,r.epsilon)}})})),Ee("SparseMatrix",["typed","equalScalar","Matrix"],(function(e){var t=e.typed,r=e.equalScalar,n=e.Matrix;function o(e,t){if(!(this instanceof o))throw new SyntaxError("Constructor must be called with the new operator");if(t&&!c(t))throw new Error("Invalid datatype: "+t);if(l(e))!function(e,t,r){"SparseMatrix"===t.type?(e._values=t._values?he(t._values):void 0,e._index=he(t._index),e._ptr=he(t._ptr),e._size=he(t._size),e._datatype=r||t._datatype):u(e,t.valueOf(),r||t._datatype)}(this,e,t);else if(e&&f(e.index)&&f(e.ptr)&&f(e.size))this._values=e.values,this._index=e.index,this._ptr=e.ptr,this._size=e.size,this._datatype=t||e.datatype;else if(f(e))u(this,e,t);else{if(e)throw new TypeError("Unsupported type of data ("+H(e)+")");this._values=[],this._index=[],this._ptr=[0],this._size=[0,0],this._datatype=t}}function u(e,n,i){e._values=[],e._index=[],e._ptr=[],e._datatype=i;var a=n.length,o=0,u=r,s=0;if(c(i)&&(u=t.find(r,[i,i])||r,s=t.convert(0,i)),a>0){var l=0;do{e._ptr.push(e._index.length);for(var p=0;pd){for(l=d;lh){if(m){var v=0;for(l=0;ln-1&&(e._values.splice(p,1),e._index.splice(p,1),g++)}e._ptr[l]=e._values.length}return e._size[0]=n,e._size[1]=i,e}function d(e,t,r,n,i){var a,o,u=n[0],s=n[1],c=[];for(a=0;a");if(1===a.length)t.dimension(0).forEach((function(t,i){cn(t),e.set([t,0],r[i[0]],n)}));else{var c=t.dimension(0),f=t.dimension(1);c.forEach((function(t,i){cn(t),f.forEach((function(a,o){cn(a),e.set([t,a],r[i[0]][o[0]],n)}))}))}}return e}(this,e,t,r);default:throw new SyntaxError("Wrong number of arguments")}},o.prototype.get=function(e){if(!f(e))throw new TypeError("Array expected");if(e.length!==this._size.length)throw new rn(e.length,this._size.length);if(!this._values)throw new Error("Cannot invoke get on a Pattern only matrix");var t=e[0],r=e[1];cn(t,this._size[0]),cn(r,this._size[1]);var n=s(t,this._ptr[r],this._ptr[r+1],this._index);return nu-1||o>l-1)&&(h(this,Math.max(a+1,u),Math.max(o+1,l),i),u=this._size[0],l=this._size[1]),cn(a,u),cn(o,l);var v=s(a,this._ptr[o],this._ptr[o+1],this._index);return v=0&&w<=i&&v(e._values[b],w-0,y-0)}else{for(var N={},D=g;D "+(this._values?Jr(this._values[s],e):"X");return i},o.prototype.toString=function(){return Jr(this.toArray())},o.prototype.toJSON=function(){return{mathjs:"SparseMatrix",values:this._values,index:this._index,ptr:this._ptr,size:this._size,datatype:this._datatype}},o.prototype.diagonal=function(e){if(e){if(a(e)&&(e=e.toNumber()),!i(e)||!V(e))throw new TypeError("The parameter k must be an integer number")}else e=0;var t=e>0?e:0,r=e<0?-e:0,n=this._size[0],u=this._size[1],s=Math.min(n-r,u-t),c=[],f=[],l=[];l[0]=0;for(var p=t;p0?u:0,y=u<0?-u:0,g=e[0],x=e[1],b=Math.min(g-y,x-v);if(f(n)){if(n.length!==b)throw new Error("Invalid value array length");d=function(e){return n[e]}}else if(l(n)){var w=n.size();if(1!==w.length||w[0]!==b)throw new Error("Invalid matrix length");d=function(e){return n.get([e])}}else d=function(){return n};for(var N=[],D=[],E=[],A=0;A=0&&S=c||i[l]!==t)){var m=n?n[f]:void 0;i.splice(l,0,t),n&&n.splice(l,0,m),i.splice(l<=f?f+1:f,1),n&&n.splice(l<=f?f+1:f,1)}else if(l=c||i[f]!==e)){var h=n?n[l]:void 0;i.splice(f,0,e),n&&n.splice(f,0,h),i.splice(f<=l?l+1:l,1),n&&n.splice(f<=l?l+1:l,1)}}},o}),{isClass:!0})),bi=Ee("number",["typed"],(function(e){var t=e.typed,r=t("number",{"":function(){return 0},number:function(e){return e},string:function(e){if("NaN"===e)return NaN;var t,r,n=(r=(t=e).match(/(0[box])([0-9a-fA-F]*)\.([0-9a-fA-F]*)/))?{input:t,radix:{"0b":2,"0o":8,"0x":16}[r[1]],integerPart:r[2],fractionalPart:r[3]}:null;if(n)return function(e){for(var t=parseInt(e.integerPart,e.radix),r=0,n=0;nMath.pow(2,i)-1)throw new SyntaxError('String "'.concat(e,'" is out of range'));o>=Math.pow(2,i-1)&&(o-=Math.pow(2,i))}return o},BigNumber:function(e){return e.toNumber()},Fraction:function(e){return e.valueOf()},Unit:t.referToSelf((function(e){return function(t){var r=t.clone();return r.value=e(t.value),r}})),null:function(e){return 0},"Unit, string | Unit":function(e,t){return e.toNumber(t)},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))});return r.fromJSON=function(e){return parseFloat(e.value)},r})),wi="string",Ni=Ee(wi,["typed"],(function(e){var t=e.typed;return t(wi,{"":function(){return""},number:ee,null:function(e){return"null"},boolean:function(e){return e+""},string:function(e){return e},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}})),any:function(e){return String(e)}})})),Di="boolean",Ei=Ee(Di,["typed"],(function(e){var t=e.typed;return t(Di,{"":function(){return!1},boolean:function(e){return e},number:function(e){return!!e},null:function(e){return!1},BigNumber:function(e){return!e.isZero()},string:function(e){var t=e.toLowerCase();if("true"===t)return!0;if("false"===t)return!1;var r=Number(e);if(""!==e&&!isNaN(r))return!!r;throw new Error('Cannot convert "'+e+'" to a boolean')},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),Ai=Ee("bignumber",["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t("bignumber",{"":function(){return new r(0)},number:function(e){return new r(e+"")},string:function(e){var t=e.match(/(0[box][0-9a-fA-F]*)i([0-9]*)/);if(t){var n=t[2],i=r(t[1]),a=new r(2).pow(Number(n));if(i.gt(a.sub(1)))throw new SyntaxError('String "'.concat(e,'" is out of range'));var o=new r(2).pow(Number(n)-1);return i.gte(o)?i.sub(a):i}return new r(e)},BigNumber:function(e){return e},Unit:t.referToSelf((function(e){return function(t){var r=t.clone();return r.value=e(t.value),r}})),Fraction:function(e){return new r(e.n).div(e.d).times(e.s)},null:function(e){return new r(0)},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),Si=Ee("complex",["typed","Complex"],(function(e){var t=e.typed,r=e.Complex;return t("complex",{"":function(){return r.ZERO},number:function(e){return new r(e,0)},"number, number":function(e,t){return new r(e,t)},"BigNumber, BigNumber":function(e,t){return new r(e.toNumber(),t.toNumber())},Fraction:function(e){return new r(e.valueOf(),0)},Complex:function(e){return e.clone()},string:function(e){return r(e)},null:function(e){return r(0)},Object:function(e){if("re"in e&&"im"in e)return new r(e.re,e.im);if("r"in e&&"phi"in e||"abs"in e&&"arg"in e)return new r(e);throw new Error("Expected object with properties (re and im) or (r and phi) or (abs and arg)")},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),Ci=Ee("fraction",["typed","Fraction"],(function(e){var t=e.typed,r=e.Fraction;return t("fraction",{number:function(e){if(!isFinite(e)||isNaN(e))throw new Error(e+" cannot be represented as a fraction");return new r(e)},string:function(e){return new r(e)},"number, number":function(e,t){return new r(e,t)},null:function(e){return new r(0)},BigNumber:function(e){return new r(e.toString())},Fraction:function(e){return e},Unit:t.referToSelf((function(e){return function(t){var r=t.clone();return r.value=e(t.value),r}})),Object:function(e){return new r(e)},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),Mi="matrix",Fi=Ee(Mi,["typed","Matrix","DenseMatrix","SparseMatrix"],(function(e){var t=e.typed,r=(e.Matrix,e.DenseMatrix),n=e.SparseMatrix;return t(Mi,{"":function(){return i([])},string:function(e){return i([],e)},"string, string":function(e,t){return i([],e,t)},Array:function(e){return i(e)},Matrix:function(e){return i(e,e.storage())},"Array | Matrix, string":i,"Array | Matrix, string, string":i});function i(e,t,i){if("dense"===t||"default"===t||void 0===t)return new r(e,i);if("sparse"===t)return new n(e,i);throw new TypeError("Unknown matrix type "+JSON.stringify(t)+".")}})),Oi="matrixFromFunction",Ti=Ee(Oi,["typed","matrix","isZero"],(function(e){var t=e.typed,r=e.matrix,n=e.isZero;return t(Oi,{"Array | Matrix, function, string, string":function(e,t,r,n){return i(e,t,r,n)},"Array | Matrix, function, string":function(e,t,r){return i(e,t,r)},"Matrix, function":function(e,t){return i(e,t,"dense")},"Array, function":function(e,t){return i(e,t,"dense").toArray()},"Array | Matrix, string, function":function(e,t,r){return i(e,r,t)},"Array | Matrix, string, string, function":function(e,t,r,n){return i(e,n,t,r)}});function i(e,t,i,a){var o;return(o=void 0!==a?r(i,a):r(i)).resize(e),o.forEach((function(e,r){var i=t(r);n(i)||o.set(r,i)})),o}}));function Bi(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==r.return||r.return()}finally{if(u)throw a}}}}(e);try{for(a.s();!(t=a.n()).done;){var u=t.value,s=o(u);if(s!==r)throw new TypeError("The vectors had different length: "+(0|r)+" ≠ "+(0|s));i.push(n(u))}}catch(e){a.e(e)}finally{a.f()}return i}function o(e){var t=i(e);if(1===t.length)return t[0];if(2===t.length){if(1===t[0])return t[1];if(1===t[1])return t[0];throw new TypeError("At least one of the arguments is not a vector.")}throw new TypeError("Only one- or two-dimensional vectors are supported.")}}));function Ii(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==r.return||r.return()}finally{if(u)throw a}}}}(e);try{for(u.s();!(a=u.n()).done;){var s=a.value,c=o(s);if(c!==t)throw new TypeError("The vectors had different length: "+(0|t)+" ≠ "+(0|c));for(var f=n(s),l=0;l1&&void 0!==arguments[1]?arguments[1]:2,r=t<0;if(r&&(t=-t),0===t)throw new Error("Root must be non-zero");if(e<0&&Math.abs(t)%2!=1)throw new Error("Root must be odd when a is negative.");if(0===e)return r?1/0:0;if(!isFinite(e))return r?0:e;var n=Math.pow(Math.abs(e),1/t);return n=e<0?-n:n,r?1/n:n}function ra(e){return Z(e)}function na(e){return e*e}function ia(e,t){var r,n,i,a=0,o=1,u=1,s=0;if(!V(e)||!V(t))throw new Error("Parameters in function xgcd must be integer numbers");for(;t;)i=e-(n=Math.floor(e/t))*t,r=a,a=o-n*a,o=r,r=u,u=s-n*u,s=r,e=t,t=i;return e<0?[-e,-o,-s]:[e,e?o:0,s]}function aa(e,t){return e*e<1&&t===1/0||e*e>1&&t===-1/0?0:Math.pow(e,t)}function oa(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;if(!V(t)||t<0||t>15)throw new Error("Number of decimals in function round must be an integer from 0 to 15 inclusive");return parseFloat(re(e,t))}Ui.signature=Pi,$i.signature=Li,Hi.signature=Li,Gi.signature=Li,Vi.signature=Pi,Zi.signature=Pi,Wi.signature=Pi,Yi.signature=Pi,Ji.signature=Pi,Xi.signature=Pi,Qi.signature=Li,Ki.signature=Pi,ea.signature=Pi,ra.signature=Pi,na.signature=Pi,ia.signature=Li,aa.signature=Li;var ua="unaryMinus",sa=Ee(ua,["typed"],(function(e){var t=e.typed;return t(ua,{number:Vi,"Complex | BigNumber | Fraction":function(e){return e.neg()},Unit:t.referToSelf((function(e){return function(r){var n=r.clone();return n.value=t.find(e,n.valueType())(r.value),n}})),"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e,!0)}}))})})),ca="unaryPlus",fa=Ee(ca,["typed","config","BigNumber"],(function(e){var t=e.typed,r=e.config,n=e.BigNumber;return t(ca,{number:Zi,Complex:function(e){return e},BigNumber:function(e){return e},Fraction:function(e){return e},Unit:function(e){return e.clone()},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e,!0)}})),"boolean | string":function(e){return"BigNumber"===r.number?new n(+e):+e}})})),la=Ee("abs",["typed"],(function(e){var t=e.typed;return t("abs",{number:Ui,"Complex | BigNumber | Fraction | Unit":function(e){return e.abs()},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e,!0)}}))})})),pa="apply",ma=Ee(pa,["typed","isInteger"],(function(e){var t=e.typed,r=e.isInteger;return t(pa,{"Array | Matrix, number | BigNumber, function":function(e,t,n){if(!r(t))throw new TypeError("Integer number expected for dimension");var i=Array.isArray(e)?an(e):e.size();if(t<0||t>=i.length)throw new nn(t,i.length);return l(e)?e.create(ha(e.valueOf(),t,n)):ha(e,t,n)}})}));function ha(e,t,r){var n,i,a;if(t<=0){if(Array.isArray(e[0])){for(a=function(e){var t,r,n=e.length,i=e[0].length,a=[];for(r=0;r0?r(f,0,s,s[0],u,n,a):[];return e.createDenseMatrix({data:l,size:he(s),datatype:o})};function r(e,t,n,i,a,o,u){var s=[];if(t===n.length-1)for(var c=0;c0?n(e):r(e)},"number, number":function(e,t){return e>0?n(e,t):r(e,t)}})})),za=Ee(ka,Ia,(function(e){var t=e.typed,r=e.Complex,n=e.matrix,i=e.ceil,a=e.floor,o=e.equalScalar,u=e.zeros,s=e.DenseMatrix,c=Da({typed:t,DenseMatrix:s}),f=Ea({typed:t}),l=Ra({typed:t,ceil:i,floor:a});return t("fix",{number:l.signatures.number,"number, number | BigNumber":l.signatures["number,number"],Complex:function(e){return new r(e.re>0?Math.floor(e.re):Math.ceil(e.re),e.im>0?Math.floor(e.im):Math.ceil(e.im))},"Complex, number":function(e,t){return new r(e.re>0?a(e.re,t):i(e.re,t),e.im>0?a(e.im,t):i(e.im,t))},"Complex, BigNumber":function(e,t){var n=t.toNumber();return new r(e.re>0?a(e.re,n):i(e.re,n),e.im>0?a(e.im,n):i(e.im,n))},BigNumber:function(e){return e.isNegative()?i(e):a(e)},"BigNumber, number | BigNumber":function(e,t){return e.isNegative()?i(e,t):a(e,t)},Fraction:function(e){return e.s<0?e.ceil():e.floor()},"Fraction, number | BigNumber":function(e,t){return e.s<0?i(e,t):a(e,t)},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e,!0)}})),"Array | Matrix, number | BigNumber":t.referToSelf((function(e){return function(t,r){return Hn(t,(function(t){return e(t,r)}),!0)}})),"number | Complex | Fraction | BigNumber, Array":t.referToSelf((function(e){return function(t,r){return f(n(r),t,e,!0).valueOf()}})),"number | Complex | Fraction | BigNumber, Matrix":t.referToSelf((function(e){return function(t,r){return o(t,0)?u(r.size(),r.storage()):"dense"===r.storage()?f(r,t,e,!0):c(r,t,e,!0)}}))})})),qa="floor",ja=["typed","config","round","matrix","equalScalar","zeros","DenseMatrix"],Pa=Ee(qa,["typed","config","round"],(function(e){var t=e.typed,r=e.config,n=e.round;return t(qa,{number:function(e){return ue(e,n(e),r.epsilon)?n(e):Math.floor(e)},"number, number":function(e,t){if(ue(e,n(e,t),r.epsilon))return n(e,t);var i=wa("".concat(e,"e").split("e"),2),a=i[0],o=i[1],u=Math.floor(Number("".concat(a,"e").concat(Number(o)+t))),s=wa("".concat(u,"e").split("e"),2);return a=s[0],o=s[1],Number("".concat(a,"e").concat(Number(o)-t))}})})),La=Ee(qa,ja,(function(e){var t=e.typed,r=e.config,n=e.round,i=e.matrix,a=e.equalScalar,o=e.zeros,u=e.DenseMatrix,s=Na({typed:t,equalScalar:a}),c=Da({typed:t,DenseMatrix:u}),f=Ea({typed:t}),l=Pa({typed:t,config:r,round:n});return t("floor",{number:l.signatures.number,"number,number":l.signatures["number,number"],Complex:function(e){return e.floor()},"Complex, number":function(e,t){return e.floor(t)},"Complex, BigNumber":function(e,t){return e.floor(t.toNumber())},BigNumber:function(e){return di(e,n(e),r.epsilon)?n(e):e.floor()},"BigNumber, BigNumber":function(e,t){return di(e,n(e,t),r.epsilon)?n(e,t):e.toDecimalPlaces(t.toNumber(),kr.ROUND_FLOOR)},Fraction:function(e){return e.floor()},"Fraction, number":function(e,t){return e.floor(t)},"Fraction, BigNumber":function(e,t){return e.floor(t.toNumber())},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e,!0)}})),"Array, number | BigNumber":t.referToSelf((function(e){return function(t,r){return Hn(t,(function(t){return e(t,r)}),!0)}})),"SparseMatrix, number | BigNumber":t.referToSelf((function(e){return function(t,r){return s(t,r,e,!1)}})),"DenseMatrix, number | BigNumber":t.referToSelf((function(e){return function(t,r){return f(t,r,e,!1)}})),"number | Complex | Fraction | BigNumber, Array":t.referToSelf((function(e){return function(t,r){return f(i(r),t,e,!0).valueOf()}})),"number | Complex | Fraction | BigNumber, Matrix":t.referToSelf((function(e){return function(t,r){return a(t,0)?o(r.size(),r.storage()):"dense"===r.storage()?f(r,t,e,!0):c(r,t,e,!0)}}))})}));function Ua(e,t,r){return(t=Me(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}var $a=Ee("matAlgo02xDS0",["typed","equalScalar"],(function(e){var t=e.typed,r=e.equalScalar;return function(e,n,i,a){var o=e._data,u=e._size,s=e._datatype,c=n._values,f=n._index,l=n._ptr,p=n._size,m=n._datatype;if(u.length!==p.length)throw new rn(u.length,p.length);if(u[0]!==p[0]||u[1]!==p[1])throw new RangeError("Dimension mismatch. Matrix A ("+u+") must match Matrix B ("+p+")");if(!c)throw new Error("Cannot perform operation on Dense Matrix and Pattern Sparse Matrix");var h,d=u[0],v=u[1],y=r,g=0,x=i;"string"==typeof s&&s===m&&(h=s,y=t.find(r,[h,h]),g=t.convert(0,h),x=t.find(i,[h,h]));for(var b=[],w=[],N=[],D=0;D0?r(h,0,p,p[0],o,c):[];return e.createDenseMatrix({data:d,size:p,datatype:a})};function r(e,t,n,i,a,o){var u=[];if(t===n.length-1)for(var s=0;s=0?e.mod(t):e.mod(t).add(t).mod(t)}},Wa({typed:t,matrix:i,concat:s})({SS:p,DS:l,SD:f,Ss:m,sS:h}))})),Ja=Ee("matAlgo01xDSid",["typed"],(function(e){var t=e.typed;return function(e,r,n,i){var a=e._data,o=e._size,u=e._datatype,s=r._values,c=r._index,f=r._ptr,l=r._size,p=r._datatype;if(o.length!==l.length)throw new rn(o.length,l.length);if(o[0]!==l[0]||o[1]!==l[1])throw new RangeError("Dimension mismatch. Matrix A ("+o+") must match Matrix B ("+l+")");if(!s)throw new Error("Cannot perform operation on Dense Matrix and Pattern Sparse Matrix");var m,h,d=o[0],v=o[1],y="string"==typeof u&&u===p?u:void 0,g=y?t.find(n,[y,y]):n,x=[];for(m=0;m=0||r.predictable?Ki(e):new n(e,0).log().div(Math.LN10)},Complex:function(e){return new n(e).log().div(Math.LN10)},BigNumber:function(e){return!e.isNegative()||r.predictable?e.log():new n(e.toNumber(),0).log().div(Math.LN10)},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),co="log2",fo=Ee(co,["typed","config","Complex"],(function(e){var t=e.typed,r=e.config,n=e.Complex;return t(co,{number:function(e){return e>=0||r.predictable?ea(e):i(new n(e,0))},Complex:i,BigNumber:function(e){return!e.isNegative()||r.predictable?e.log(2):i(new n(e.toNumber(),0))},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))});function i(e){var t=Math.sqrt(e.re*e.re+e.im*e.im);return new n(Math.log2?Math.log2(t):Math.log(t)/Math.LN2,Math.atan2(e.im,e.re)/Math.LN2)}})),lo=Ee("multiplyScalar",["typed"],(function(e){return(0,e.typed)("multiplyScalar",{"number, number":Gi,"Complex, Complex":function(e,t){return e.mul(t)},"BigNumber, BigNumber":function(e,t){return e.times(t)},"Fraction, Fraction":function(e,t){return e.mul(t)},"number | Fraction | BigNumber | Complex, Unit":function(e,t){return t.multiply(e)},"Unit, number | Fraction | BigNumber | Complex | Unit":function(e,t){return e.multiply(t)}})})),po="multiply",mo=Ee(po,["typed","matrix","addScalar","multiplyScalar","equalScalar","dot"],(function(e){var t=e.typed,r=e.matrix,n=e.addScalar,i=e.multiplyScalar,a=e.equalScalar,o=e.dot,u=Na({typed:t,equalScalar:a}),s=Ea({typed:t});function c(e,t){switch(e.length){case 1:switch(t.length){case 1:if(e[0]!==t[0])throw new RangeError("Dimension mismatch in multiplication. Vectors must have the same length");break;case 2:if(e[0]!==t[0])throw new RangeError("Dimension mismatch in multiplication. Vector length ("+e[0]+") must match Matrix rows ("+t[0]+")");break;default:throw new Error("Can only multiply a 1 or 2 dimensional matrix (Matrix B has "+t.length+" dimensions)")}break;case 2:switch(t.length){case 1:if(e[1]!==t[0])throw new RangeError("Dimension mismatch in multiplication. Matrix columns ("+e[1]+") must match Vector length ("+t[0]+")");break;case 2:if(e[1]!==t[0])throw new RangeError("Dimension mismatch in multiplication. Matrix A columns ("+e[1]+") must match Matrix B rows ("+t[0]+")");break;default:throw new Error("Can only multiply a 1 or 2 dimensional matrix (Matrix B has "+t.length+" dimensions)")}break;default:throw new Error("Can only multiply a 1 or 2 dimensional matrix (Matrix A has "+e.length+" dimensions)")}}var f=t("_multiplyMatrixVector",{"DenseMatrix, any":function(e,r){var a,o=e._data,u=e._size,s=e._datatype,c=r._data,f=r._datatype,l=u[0],p=u[1],m=n,h=i;s&&f&&s===f&&"string"==typeof s&&(a=s,m=t.find(n,[a,a]),h=t.find(i,[a,a]));for(var d=[],v=0;vS)for(var M=0,F=0;F=0||t.predictable?Math.sqrt(e):new n(e,0).sqrt()}})),bo="square",wo=Ee(bo,["typed"],(function(e){return(0,e.typed)(bo,{number:na,Complex:function(e){return e.mul(e)},BigNumber:function(e){return e.times(e)},Fraction:function(e){return e.mul(e)},Unit:function(e){return e.pow(2)}})})),No="subtract",Do=Ee(No,["typed","matrix","equalScalar","subtractScalar","unaryMinus","DenseMatrix","concat"],(function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.subtractScalar,a=(e.unaryMinus,e.DenseMatrix),o=e.concat,u=Ja({typed:t}),s=Ha({typed:t}),c=Ga({typed:t,equalScalar:n}),f=Qa({typed:t,DenseMatrix:a}),l=Da({typed:t,DenseMatrix:a}),p=Wa({typed:t,matrix:r,concat:o});return t(No,{"any, any":i},p({elop:i,SS:c,DS:u,SD:s,Ss:l,sS:f}))})),Eo="xgcd",Ao=Ee(Eo,["typed","config","matrix","BigNumber"],(function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.BigNumber;return t(Eo,{"number, number":function(e,t){var i=ia(e,t);return"Array"===r.matrix?i:n(i)},"BigNumber, BigNumber":function(e,t){var a,o,u,s,c=new i(0),f=new i(1),l=c,p=f,m=f,h=c;if(!e.isInt()||!t.isInt())throw new Error("Parameters in function xgcd must be integer numbers");for(;!t.isZero();)o=e.div(t).floor(),u=e.mod(t),a=l,l=p.minus(o.times(l)),p=a,a=m,m=h.minus(o.times(m)),h=a,e=t,t=u;return s=e.lt(c)?[e.neg(),p.neg(),h.neg()]:[e,e.isZero()?0:p,h],"Array"===r.matrix?s:n(s)}})})),So="invmod",Co=Ee(So,["typed","config","BigNumber","xgcd","equal","smaller","mod","add","isInteger"],(function(e){var t=e.typed,r=(e.config,e.BigNumber),n=e.xgcd,i=e.equal,a=e.smaller,o=e.mod,u=e.add,s=e.isInteger;return t(So,{"number, number":c,"BigNumber, BigNumber":c});function c(e,t){if(!s(e)||!s(t))throw new Error("Parameters in function invmod must be integer numbers");if(e=o(e,t),i(t,0))throw new Error("Divisor must be non zero");var c=n(e,t),f=wa(c=c.valueOf(),2),l=f[0],p=f[1];return i(l,r(1))?(p=o(p,t),a(p,r(0))&&(p=u(p,t)),p):NaN}})),Mo=Ee("matAlgo09xS0Sf",["typed","equalScalar"],(function(e){var t=e.typed,r=e.equalScalar;return function(e,n,i){var a=e._values,o=e._index,u=e._ptr,s=e._size,c=e._datatype,f=n._values,l=n._index,p=n._ptr,m=n._size,h=n._datatype;if(s.length!==m.length)throw new rn(s.length,m.length);if(s[0]!==m[0]||s[1]!==m[1])throw new RangeError("Dimension mismatch. Matrix A ("+s+") must match Matrix B ("+m+")");var d,v=s[0],y=s[1],g=r,x=0,b=i;"string"==typeof c&&c===h&&(d=c,g=t.find(r,[d,d]),x=t.convert(0,d),b=t.find(i,[d,d]));var w,N,D,E,A,S=a&&f?[]:void 0,C=[],M=[],F=S?[]:void 0,O=[];for(N=0;N0;)r(a[--m],o[--h])===d&&(v=v.plus(y)),y=y.times(g);for(;h>0;)r(u,o[--h])===d&&(v=v.plus(y)),y=y.times(g);return s.config({precision:x}),0===d&&(v.s=-v.s),v}function Io(e){for(var t=e.d,r=t[0]+"",n=1;n0)if(++u>c)for(u-=c;u--;)s+="0";else u1&&(null!==f[m+1]&&void 0!==f[m+1]||(f[m+1]=0),f[m+1]+=f[m]>>1,f[m]&=1)}return f.reverse()}function Ro(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function bitXor");var r=e.constructor;if(e.isNaN()||t.isNaN())return new r(NaN);if(e.isZero())return t;if(t.isZero())return e;if(e.eq(t))return new r(0);var n=new r(-1);return e.eq(n)?Bo(t):t.eq(n)?Bo(e):e.isFinite()&&t.isFinite()?ko(e,t,(function(e,t){return e^t})):e.isFinite()||t.isFinite()?new r(e.isNegative()===t.isNegative()?1/0:-1/0):n}function zo(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function leftShift");var r=e.constructor;return e.isNaN()||t.isNaN()||t.isNegative()&&!t.isZero()?new r(NaN):e.isZero()||t.isZero()?e:e.isFinite()||t.isFinite()?t.lt(55)?e.times(Math.pow(2,t.toNumber())+""):e.times(new r(2).pow(t)):new r(NaN)}function qo(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function rightArithShift");var r=e.constructor;return e.isNaN()||t.isNaN()||t.isNegative()&&!t.isZero()?new r(NaN):e.isZero()||t.isZero()?e:t.isFinite()?t.lt(55)?e.div(Math.pow(2,t.toNumber())+"").floor():e.div(new r(2).pow(t)).floor():e.isNegative()?new r(-1):e.isFinite()?new r(0):new r(NaN)}r(3374);var jo="number, number";function Po(e,t){if(!V(e)||!V(t))throw new Error("Integers expected in function bitAnd");return e&t}function Lo(e){if(!V(e))throw new Error("Integer expected in function bitNot");return~e}function Uo(e,t){if(!V(e)||!V(t))throw new Error("Integers expected in function bitOr");return e|t}function $o(e,t){if(!V(e)||!V(t))throw new Error("Integers expected in function bitXor");return e^t}function Ho(e,t){if(!V(e)||!V(t))throw new Error("Integers expected in function leftShift");return e<>t}function Vo(e,t){if(!V(e)||!V(t))throw new Error("Integers expected in function rightLogShift");return e>>>t}Po.signature=jo,Lo.signature="number",Uo.signature=jo,$o.signature=jo,Ho.signature=jo,Go.signature=jo,Vo.signature=jo;var Zo="bitAnd",Wo=Ee(Zo,["typed","matrix","equalScalar","concat"],(function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.concat,a=$a({typed:t,equalScalar:n}),o=ao({typed:t,equalScalar:n}),u=Na({typed:t,equalScalar:n}),s=Wa({typed:t,matrix:r,concat:i});return t(Zo,{"number, number":Po,"BigNumber, BigNumber":To},s({SS:o,DS:a,Ss:u}))})),Yo="bitNot",Jo=Ee(Yo,["typed"],(function(e){var t=e.typed;return t(Yo,{number:Lo,BigNumber:Bo,"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),Xo="bitOr",Qo=Ee(Xo,["typed","matrix","equalScalar","DenseMatrix","concat"],(function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.DenseMatrix,a=e.concat,o=Ja({typed:t}),u=Xa({typed:t,equalScalar:n}),s=Qa({typed:t,DenseMatrix:i}),c=Wa({typed:t,matrix:r,concat:a});return t(Xo,{"number, number":Uo,"BigNumber, BigNumber":_o},c({SS:u,DS:o,Ss:s}))})),Ko=Ee("matAlgo07xSSf",["typed","DenseMatrix"],(function(e){var t=e.typed,r=e.DenseMatrix;return function(e,i,a){var o=e._size,u=e._datatype,s=i._size,c=i._datatype;if(o.length!==s.length)throw new rn(o.length,s.length);if(o[0]!==s[0]||o[1]!==s[1])throw new RangeError("Dimension mismatch. Matrix A ("+o+") must match Matrix B ("+s+")");var f,l,p,m=o[0],h=o[1],d=0,v=a;"string"==typeof u&&u===c&&(f=u,d=t.convert(0,f),v=t.find(a,[f,f]));var y=[];for(l=0;l0&&s>o)throw new nn(s,o+1)}else{var m=he(p).valueOf(),h=an(m);if(f[t]=m,o=s,s=h.length-1,t>0&&s!==o)throw new rn(o+1,s+1)}}if(0===f.length)throw new SyntaxError("At least one matrix expected");for(var d=f.shift();f.length;)d=Tn(d,f.shift(),s);return c?r(d):d},"...string":function(e){return e.join("")}})})),yu="column",gu=Ee(yu,["typed","Index","matrix","range"],(function(e){var t=e.typed,r=e.Index,n=e.matrix,i=e.range;return t(yu,{"Matrix, number":a,"Array, number":function(e,t){return a(n(he(e)),t).valueOf()}});function a(e,t){if(2!==e.size().length)throw new Error("Only two dimensional matrix is supported");cn(t,e.size()[1]);var a=i(0,e.size()[0]),o=new r(a,t),u=e.subset(o);return l(u)?u:n([[u]])}})),xu="count",bu=Ee(xu,["typed","size","prod"],(function(e){var t=e.typed,r=e.size,n=e.prod;return t(xu,{string:function(e){return e.length},"Matrix | Array":function(e){return n(r(e))}})})),wu="cross",Nu=Ee(wu,["typed","matrix","subtract","multiply"],(function(e){var t=e.typed,r=e.matrix,n=e.subtract,i=e.multiply;return t(wu,{"Matrix, Matrix":function(e,t){return r(a(e.toArray(),t.toArray()))},"Matrix, Array":function(e,t){return r(a(e.toArray(),t))},"Array, Matrix":function(e,t){return r(a(e,t.toArray()))},"Array, Array":a});function a(e,t){var r=Math.max(an(e).length,an(t).length);e=vn(e),t=vn(t);var a=an(e),o=an(t);if(1!==a.length||1!==o.length||3!==a[0]||3!==o[0])throw new RangeError("Vectors with length 3 expected (Size A = ["+a.join(", ")+"], B = ["+o.join(", ")+"])");var u=[n(i(e[1],t[2]),i(e[2],t[1])),n(i(e[2],t[0]),i(e[0],t[2])),n(i(e[0],t[1]),i(e[1],t[0]))];return r>1?[u]:u}})),Du="diag",Eu=Ee(Du,["typed","matrix","DenseMatrix","SparseMatrix"],(function(e){var t=e.typed,r=e.matrix,n=e.DenseMatrix,i=e.SparseMatrix;return t(Du,{Array:function(e){return a(e,0,an(e),null)},"Array, number":function(e,t){return a(e,t,an(e),null)},"Array, BigNumber":function(e,t){return a(e,t.toNumber(),an(e),null)},"Array, string":function(e,t){return a(e,0,an(e),t)},"Array, number, string":function(e,t,r){return a(e,t,an(e),r)},"Array, BigNumber, string":function(e,t,r){return a(e,t.toNumber(),an(e),r)},Matrix:function(e){return a(e,0,e.size(),e.storage())},"Matrix, number":function(e,t){return a(e,t,e.size(),e.storage())},"Matrix, BigNumber":function(e,t){return a(e,t.toNumber(),e.size(),e.storage())},"Matrix, string":function(e,t){return a(e,0,e.size(),t)},"Matrix, number, string":function(e,t,r){return a(e,t,e.size(),r)},"Matrix, BigNumber, string":function(e,t,r){return a(e,t.toNumber(),e.size(),r)}});function a(e,t,a,o){if(!V(t))throw new TypeError("Second parameter in function diag must be an integer");var u=t>0?t:0,s=t<0?-t:0;switch(a.length){case 1:return function(e,t,r,a,o,u){var s=[a+o,a+u];if(r&&"sparse"!==r&&"dense"!==r)throw new TypeError("Unknown matrix type ".concat(r,'"'));var c="sparse"===r?i.diagonal(s,e,t):n.diagonal(s,e,t);return null!==r?c:c.valueOf()}(e,t,o,a[0],s,u);case 2:return function(e,t,n,i,a,o){if(l(e)){var u=e.diagonal(t);return null!==n?n!==u.storage()?r(u,n):u:u.valueOf()}for(var s=Math.min(i[0]-a,i[1]-o),c=[],f=0;f=2&&s.push("index: ".concat(H(r))),o.length>=3&&s.push("array: ".concat(H(n))),new TypeError("Function ".concat(i," cannot apply callback arguments ")+"".concat(e.name,"(").concat(s.join(", "),") at index ").concat(JSON.stringify(r)))}throw new TypeError("Function ".concat(i," cannot apply callback arguments ")+"to function ".concat(e.name,": ").concat(a.message))}}}var Su=Ee("filter",["typed"],(function(e){return(0,e.typed)("filter",{"Array, function":Cu,"Matrix, function":function(e,t){return e.create(Cu(e.toArray(),t))},"Array, RegExp":En,"Matrix, RegExp":function(e,t){return e.create(En(e.toArray(),t))}})}));function Cu(e,t){return Dn(e,(function(e,r,n){return Au(t,e,[r],n,"filter")}))}var Mu="flatten",Fu=Ee(Mu,["typed","matrix"],(function(e){var t=e.typed,r=e.matrix;return t(Mu,{Array:function(e){return bn(e)},Matrix:function(e){var t=bn(e.toArray());return r(t)}})})),Ou="forEach",Tu=Ee(Ou,["typed"],(function(e){return(0,e.typed)(Ou,{"Array, function":Bu,"Matrix, function":function(e,t){e.forEach(t)}})}));function Bu(e,t){!function r(n,i){if(!Array.isArray(n))return Au(t,n,i,e,"forEach");Nn(n,(function(e,t){r(e,i.concat(t))}))}(e,[])}var _u="getMatrixDataType",ku=Ee(_u,["typed"],(function(e){return(0,e.typed)(_u,{Array:function(e){return Mn(e,H)},Matrix:function(e){return e.getDataType()}})})),Iu="identity",Ru=Ee(Iu,["typed","config","matrix","BigNumber","DenseMatrix","SparseMatrix"],(function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.BigNumber,o=e.DenseMatrix,u=e.SparseMatrix;return t(Iu,{"":function(){return"Matrix"===r.matrix?n([]):[]},string:function(e){return n(e)},"number | BigNumber":function(e){return c(e,e,"Matrix"===r.matrix?"dense":void 0)},"number | BigNumber, string":function(e,t){return c(e,e,t)},"number | BigNumber, number | BigNumber":function(e,t){return c(e,t,"Matrix"===r.matrix?"dense":void 0)},"number | BigNumber, number | BigNumber, string":function(e,t,r){return c(e,t,r)},Array:function(e){return s(e)},"Array, string":function(e,t){return s(e,t)},Matrix:function(e){return s(e.valueOf(),e.storage())},"Matrix, string":function(e,t){return s(e.valueOf(),t)}});function s(e,t){switch(e.length){case 0:return t?n(t):[];case 1:return c(e[0],e[0],t);case 2:return c(e[0],e[1],t);default:throw new Error("Vector containing two values expected")}}function c(e,t,r){var n=a(e)||a(t)?i:null;if(a(e)&&(e=e.toNumber()),a(t)&&(t=t.toNumber()),!V(e)||e<1)throw new Error("Parameters in function identity must be positive integers");if(!V(t)||t<1)throw new Error("Parameters in function identity must be positive integers");var s=n?new i(1):1,c=n?new n(0):0,f=[e,t];if(r){if("sparse"===r)return u.diagonal(f,s,0,c);if("dense"===r)return o.diagonal(f,s,0,c);throw new TypeError('Unknown matrix type "'.concat(r,'"'))}for(var l=ln([],f,c),p=e2||an(t).length>2)throw new RangeError("Vectors with dimensions greater then 2 are not supported expected (Size x = "+JSON.stringify(e.length)+", y = "+JSON.stringify(t.length)+")");var r=[],i=[];return e.map((function(e){return t.map((function(t){return i=[],r.push(i),e.map((function(e){return t.map((function(t){return i.push(n(e,t))}))}))}))}))&&r}})),ju=Ee("map",["typed"],(function(e){return(0,e.typed)("map",{"Array, function":Pu,"Matrix, function":function(e,t){return e.map(t)}})}));function Pu(e,t){return function r(n,i){return Array.isArray(n)?n.map((function(e,t){return r(e,i.concat(t))})):Au(t,n,i,e,"map")}(e,[])}var Lu="diff",Uu=Ee(Lu,["typed","matrix","subtract","number"],(function(e){var t=e.typed,r=e.matrix,n=e.subtract,i=e.number;return t(Lu,{"Array | Matrix":function(e){return l(e)?r(o(e.toArray())):o(e)},"Array | Matrix, number":function(e,t){if(!V(t))throw new RangeError("Dimension must be a whole number");return l(e)?r(a(e.toArray(),t)):a(e,t)},"Array, BigNumber":t.referTo("Array,number",(function(e){return function(t,r){return e(t,i(r))}})),"Matrix, BigNumber":t.referTo("Matrix,number",(function(e){return function(t,r){return e(t,i(r))}}))});function a(e,t){if(l(e)&&(e=e.toArray()),!Array.isArray(e))throw RangeError("Array/Matrix does not have that many dimensions");if(t>0){var r=[];return e.forEach((function(e){r.push(a(e,t-1))})),r}if(0===t)return o(e);throw RangeError("Cannot have negative dimension")}function o(e){for(var t=[],r=e.length,n=1;n0?u.resize(e,o):u}var s=[];return e.length>0?ln(s,e,o):s}}));function Hu(){throw new Error('No "bignumber" implementation available')}function Gu(){throw new Error('No "fraction" implementation available')}function Vu(){throw new Error('No "matrix" implementation available')}var Zu="range",Wu=Ee(Zu,["typed","config","?matrix","?bignumber","smaller","smallerEq","larger","largerEq","add","isPositive"],(function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.bignumber,a=e.smaller,o=e.smallerEq,u=e.larger,s=e.largerEq,c=e.add,f=e.isPositive;return t(Zu,{string:p,"string, boolean":p,"number, number":function(e,t){return l(m(e,t,1,!1))},"number, number, number":function(e,t,r){return l(m(e,t,r,!1))},"number, number, boolean":function(e,t,r){return l(m(e,t,1,r))},"number, number, number, boolean":function(e,t,r,n){return l(m(e,t,r,n))},"BigNumber, BigNumber":function(e,t){return l(m(e,t,new(0,e.constructor)(1),!1))},"BigNumber, BigNumber, BigNumber":function(e,t,r){return l(m(e,t,r,!1))},"BigNumber, BigNumber, boolean":function(e,t,r){return l(m(e,t,new(0,e.constructor)(1),r))},"BigNumber, BigNumber, BigNumber, boolean":function(e,t,r,n){return l(m(e,t,r,n))},"Unit, Unit, Unit":function(e,t,r){return l(m(e,t,r,!1))},"Unit, Unit, Unit, boolean":function(e,t,r,n){return l(m(e,t,r,n))}});function l(e){return"Matrix"===r.matrix?n?n(e):Vu():e}function p(e,t){var n=function(e){var t=e.split(":").map((function(e){return Number(e)}));if(t.some((function(e){return isNaN(e)})))return null;switch(t.length){case 2:return{start:t[0],end:t[1],step:1};case 3:return{start:t[0],end:t[2],step:t[1]};default:return null}}(e);if(!n)throw new SyntaxError('String "'+e+'" is no valid range');return"BigNumber"===r.number?(void 0===i&&Hu(),l(m(i(n.start),i(n.end),i(n.step)))):l(m(n.start,n.end,n.step,t))}function m(e,t,r,n){for(var i=[],l=f(r)?n?o:a:n?s:u,p=e;l(p,t);)i.push(p),p=c(p,r);return i}})),Yu="reshape",Ju=Ee(Yu,["typed","isInteger","matrix"],(function(e){var t=e.typed,r=e.isInteger;return t(Yu,{"Matrix, Array":function(e,t){return e.reshape(t,!0)},"Array, Array":function(e,t){return t.forEach((function(e){if(!r(e))throw new TypeError("Invalid size for dimension: "+e)})),mn(e,t)}})})),Xu=Ee("resize",["config","matrix"],(function(e){var t=e.config,r=e.matrix;return function(e,n,i){if(2!==arguments.length&&3!==arguments.length)throw new Ka("resize",arguments.length,2,3);if(l(n)&&(n=n.valueOf()),a(n[0])&&(n=n.map((function(e){return a(e)?e.toNumber():e}))),l(e))return e.resize(n,i,!0);if("string"==typeof e)return function(e,t,r){if(void 0!==r){if("string"!=typeof r||1!==r.length)throw new TypeError("Single character expected as defaultValue")}else r=" ";if(1!==t.length)throw new rn(t.length,1);var n=t[0];if("number"!=typeof n||!V(n))throw new TypeError("Invalid size, must contain positive integers (size: "+Jr(t)+")");if(e.length>n)return e.substring(0,n);if(e.length2)throw new RangeError("Vector must be of dimensions 1x".concat(t));if(2===r.length&&1!==r[1])throw new RangeError("Vector must be of dimensions 1x".concat(t));if(r[0]!==t)throw new RangeError("Vector must be of dimensions 1x".concat(t))}})),es="rotationMatrix",ts=Ee(es,["typed","config","multiplyScalar","addScalar","unaryMinus","norm","matrix","BigNumber","DenseMatrix","SparseMatrix","cos","sin"],(function(e){var t=e.typed,r=e.config,n=e.multiplyScalar,i=e.addScalar,o=e.unaryMinus,u=e.norm,s=e.BigNumber,c=e.matrix,f=e.DenseMatrix,l=e.SparseMatrix,p=e.cos,m=e.sin;return t(es,{"":function(){return"Matrix"===r.matrix?c([]):[]},string:function(e){return c(e)},"number | BigNumber | Complex | Unit":function(e){return h(e,"Matrix"===r.matrix?"dense":void 0)},"number | BigNumber | Complex | Unit, string":function(e,t){return h(e,t)},"number | BigNumber | Complex | Unit, Array":function(e,t){var r=c(t);return d(r),g(e,r,void 0)},"number | BigNumber | Complex | Unit, Matrix":function(e,t){d(t);var n=t.storage()||("Matrix"===r.matrix?"dense":void 0);return g(e,t,n)},"number | BigNumber | Complex | Unit, Array, string":function(e,t,r){var n=c(t);return d(n),g(e,n,r)},"number | BigNumber | Complex | Unit, Matrix, string":function(e,t,r){return d(t),g(e,t,r)}});function h(e,t){var r=a(e)?new s(-1):-1,i=p(e),o=m(e);return y([[i,n(r,o)],[o,i]],t)}function d(e){var t=e.size();if(t.length<1||3!==t[0])throw new RangeError("Vector must be of dimensions 1x3")}function v(e){return e.reduce((function(e,t){return n(e,t)}))}function y(e,t){if(t){if("sparse"===t)return new l(e);if("dense"===t)return new f(e);throw new TypeError('Unknown matrix type "'.concat(t,'"'))}return e}function g(e,t,r){var n=u(t);if(0===n)throw new RangeError("Rotation around zero vector");var c=a(e)?s:null,f=c?new c(1):1,l=c?new c(-1):-1,h=c?new c(t.get([0])/n):t.get([0])/n,d=c?new c(t.get([1])/n):t.get([1])/n,g=c?new c(t.get([2])/n):t.get([2])/n,x=p(e),b=i(f,o(x)),w=m(e);return y([[i(x,v([h,h,b])),i(v([h,d,b]),v([l,g,w])),i(v([h,g,b]),v([d,w]))],[i(v([h,d,b]),v([g,w])),i(x,v([d,d,b])),i(v([d,g,b]),v([l,h,w]))],[i(v([h,g,b]),v([l,d,w])),i(v([d,g,b]),v([h,w])),i(x,v([g,g,b]))]],r)}})),rs=Ee("row",["typed","Index","matrix","range"],(function(e){var t=e.typed,r=e.Index,n=e.matrix,i=e.range;return t("row",{"Matrix, number":a,"Array, number":function(e,t){return a(n(he(e)),t).valueOf()}});function a(e,t){if(2!==e.size().length)throw new Error("Only two dimensional matrix is supported");cn(t,e.size()[0]);var a=i(0,e.size()[1]),o=new r(t,a),u=e.subset(o);return l(u)?u:n([[u]])}})),ns="size",is=Ee(ns,["typed","config","?matrix"],(function(e){var t=e.typed,r=e.config,n=e.matrix;return t(ns,{Matrix:function(e){return e.create(e.size())},Array:an,string:function(e){return"Array"===r.matrix?[e.length]:n([e.length])},"number | Complex | BigNumber | Unit | boolean | null":function(e){return"Array"===r.matrix?[]:n?n([]):Vu()}})})),as="squeeze",os=Ee(as,["typed","matrix"],(function(e){var t=e.typed,r=e.matrix;return t(as,{Array:function(e){return vn(he(e))},Matrix:function(e){var t=vn(e.toArray());return Array.isArray(t)?r(t):t},any:function(e){return he(e)}})})),us="subset",ss=Ee(us,["typed","matrix","zeros","add"],(function(e){var t=e.typed,r=e.matrix,n=e.zeros,i=e.add;return t(us,{"Matrix, Index":function(e,t){return fn(t)?r():(sn(e,t),e.subset(t))},"Array, Index":t.referTo("Matrix, Index",(function(e){return function(t,n){var i=e(r(t),n);return n.isScalar()?i:i.valueOf()}})),"Object, Index":ls,"string, Index":cs,"Matrix, Index, any, any":function(e,t,r,a){return fn(t)?e:(sn(e,t),e.clone().subset(t,function(e,t){if("string"==typeof e)throw new Error("can't boradcast a string");if(t._isScalar)return e;var r=t.size();if(!r.every((function(e){return e>0})))return e;try{return i(e,n(r))}catch(t){return e}}(r,t),a))},"Array, Index, any, any":t.referTo("Matrix, Index, any, any",(function(e){return function(t,n,i,a){var o=e(r(t),n,i,a);return o.isMatrix?o.valueOf():o}})),"Array, Index, any":t.referTo("Matrix, Index, any, any",(function(e){return function(t,n,i){return e(r(t),n,i,void 0).valueOf()}})),"Matrix, Index, any":t.referTo("Matrix, Index, any, any",(function(e){return function(t,r,n){return e(t,r,n,void 0)}})),"string, Index, string":fs,"string, Index, string, string":fs,"Object, Index, any":ps})}));function cs(e,t){if(!v(t))throw new TypeError("Index expected");if(fn(t))return"";if(sn(Array.from(e),t),1!==t.size().length)throw new rn(t.size().length,1);var r=e.length;cn(t.min()[0],r),cn(t.max()[0],r);var n=t.dimension(0),i="";return n.forEach((function(t){i+=e.charAt(t)})),i}function fs(e,t,r,n){if(!t||!0!==t.isIndex)throw new TypeError("Index expected");if(fn(t))return e;if(sn(Array.from(e),t),1!==t.size().length)throw new rn(t.size().length,1);if(void 0!==n){if("string"!=typeof n||1!==n.length)throw new TypeError("Single character expected as defaultValue")}else n=" ";var i=t.dimension(0);if(i.size()[0]!==r.length)throw new rn(i.size()[0],r.length);var a=e.length;cn(t.min()[0]),cn(t.max()[0]);for(var o=[],u=0;ua)for(var s=a-1,c=o.length;s0?u.resize(e,o):u}var s=[];return e.length>0?ln(s,e,o):s}})),xs=Ee("fft",["typed","matrix","addScalar","multiplyScalar","divideScalar","exp","tau","i","dotDivide","conj","pow","ceil","log2"],(function(e){var t=e.typed,r=(e.matrix,e.addScalar),n=e.multiplyScalar,i=e.divideScalar,a=e.exp,o=e.tau,u=e.i,s=e.dotDivide,c=e.conj,f=e.pow,l=e.ceil,p=e.log2;return t("fft",{Array:m,Matrix:function(e){return e.create(m(e.toArray()))}});function m(e){var t=an(e);return 1===t.length?d(e,t[0]):h(e.map((function(e){return m(e,t.slice(1))})),0)}function h(e,t){var r=an(e);if(0!==t)return new Array(r[0]).fill(0).map((function(r,n){return h(e[n],t-1)}));if(1===r.length)return d(e);function n(e){var t=an(e);return new Array(t[1]).fill(0).map((function(r,n){return new Array(t[0]).fill(0).map((function(t,r){return e[r][n]}))}))}return n(h(n(e),1))}function d(e){var t=e.length;if(1===t)return[e[0]];if(t%2==0){for(var h=[].concat(Vr(d(e.filter((function(e,t){return t%2==0})))),Vr(d(e.filter((function(e,t){return t%2==1}))))),v=0;v1/4&&(j.push(r(j[U],q)),P.push(r(P[U],o(q,R,V))),U++);var Y=.84*Math.pow(M/W,.2);if(d(Y,F)?Y=F:h(Y,O)&&(Y=O),Y=B?y(Y):Y,q=o(q,Y),A&&h(l(q),A)?q=N?A:g(A):S&&d(l(q),S)&&(q=N?S:g(S)),++$>T)throw new Error("Maximum number of iterations reached, try changing options")}return{t:j,y:P}}}function b(e,t,r,n){return x({a:[[],[.5],[0,3/4],[2/9,1/3,4/9]],c:[null,.5,3/4,1],b:[2/9,1/3,4/9,0],bp:[7/24,1/4,1/3,1/8]})(e,t,r,n)}function w(e,t,r,n){return x({a:[[],[.2],[3/40,9/40],[44/45,-56/15,32/9],[19372/6561,-25360/2187,64448/6561,-212/729],[9017/3168,-355/33,46732/5247,49/176,-5103/18656],[35/384,0,500/1113,125/192,-2187/6784,11/84]],c:[null,.2,.3,.8,8/9,1,1],b:[35/384,0,500/1113,125/192,-2187/6784,11/84,0],bp:[5179/57600,0,7571/16695,393/640,-92097/339200,187/2100,1/40]})(e,t,r,n)}function N(e,t,r,n){var i=n.method?n.method:"RK45",a={RK23:b,RK45:w};if(i.toUpperCase()in a){var o=function(e){for(var t=1;t=Fs?Z(e):t<=As?Z(e)*function(e){var t,r=e*e,n=Cs[0][4]*r,i=r;for(t=0;t<3;t+=1)n=(n+Cs[0][t])*r,i=(i+Ms[0][t])*r;return e*(n+Cs[0][3])/(i+Ms[0][3])}(t):t<=4?Z(e)*(1-function(e){var t,r=Cs[1][8]*e,n=e;for(t=0;t<7;t+=1)r=(r+Cs[1][t])*e,n=(n+Ms[1][t])*e;var i=(r+Cs[1][7])/(n+Ms[1][7]),a=parseInt(16*e)/16,o=(e-a)*(e+a);return Math.exp(-a*a)*Math.exp(-o)*i}(t)):Z(e)*(1-function(e){var t,r=1/(e*e),n=Cs[2][5]*r,i=r;for(t=0;t<4;t+=1)n=(n+Cs[2][t])*r,i=(i+Ms[2][t])*r;var a=r*(n+Cs[2][4])/(i+Ms[2][4]);a=(Ss-a)/e;var o=(e-(r=parseInt(16*e)/16))*(e+r);return Math.exp(-r*r)*Math.exp(-o)*a}(t))},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),As=.46875,Ss=.5641895835477563,Cs=[[3.1611237438705655,113.86415415105016,377.485237685302,3209.3775891384694,.18577770618460315],[.5641884969886701,8.883149794388377,66.11919063714163,298.6351381974001,881.952221241769,1712.0476126340707,2051.0783778260716,1230.3393547979972,2.1531153547440383e-8],[.30532663496123236,.36034489994980445,.12578172611122926,.016083785148742275,.0006587491615298378,.016315387137302097]],Ms=[[23.601290952344122,244.02463793444417,1282.6165260773723,2844.236833439171],[15.744926110709835,117.6939508913125,537.1811018620099,1621.3895745666903,3290.7992357334597,4362.619090143247,3439.3676741437216,1230.3393548037495],[2.568520192289822,1.8729528499234604,.5279051029514285,.06051834131244132,.0023352049762686918]],Fs=Math.pow(2,53),Os="zeta",Ts=Ee(Os,["typed","config","multiply","pow","divide","factorial","equal","smallerEq","isNegative","gamma","sin","subtract","add","?Complex","?BigNumber","pi"],(function(e){var t=e.typed,r=e.config,n=e.multiply,i=e.pow,a=e.divide,o=e.factorial,u=e.equal,s=e.smallerEq,c=e.isNegative,f=e.gamma,l=e.sin,p=e.subtract,m=e.add,h=e.Complex,d=e.BigNumber,v=e.pi;return t(Os,{number:function(e){return y(e,(function(e){return e}),(function(){return 20}))},BigNumber:function(e){return y(e,(function(e){return new d(e)}),(function(){return Math.abs(Math.log10(r.epsilon))}))},Complex:function(e){return 0===e.re&&0===e.im?new h(-.5):1===e.re?new h(NaN,NaN):e.re===1/0&&0===e.im?new h(1):e.im===1/0||e.re===-1/0?new h(NaN,NaN):g(e,(function(e){return e}),(function(e){return Math.round(19.5+.9*Math.abs(e.im))}),(function(e){return e.re}))}});function y(e,t,r){return u(e,0)?t(-.5):u(e,1)?t(NaN):isFinite(e)?g(e,t,r,(function(e){return e})):c(e)?t(NaN):t(1)}function g(e,t,r,o){var u=r(e);if(o(e)>-(u-1)/2)return function(e,t,r){for(var o=a(1,n(x(r(0),t),p(1,i(2,p(1,e))))),u=r(0),c=r(1);s(c,t);c=m(c,1))u=m(u,a(n(Math.pow(-1,c-1),x(c,t)),i(c,e)));return n(o,u)}(e,t(u),t);var c=n(i(2,e),i(t(v),p(e,1)));return c=n(c,l(n(a(t(v),2),e))),c=n(c,f(p(1,e))),n(c,g(p(1,e),t,r,o))}function x(e,t){for(var r=e,u=e;s(u,t);u=m(u,1)){var c=a(n(o(m(t,p(u,1))),i(4,u)),n(o(p(t,u)),o(n(2,u))));r=m(r,c)}return n(t,r)}})),Bs="mode",_s=Ee(Bs,["typed","isNaN","isNumeric"],(function(e){var t=e.typed,r=e.isNaN,n=e.isNumeric;return t(Bs,{"Array | Matrix":i,"...":function(e){return i(e)}});function i(e){if(0===(e=bn(e.valueOf())).length)throw new Error("Cannot calculate mode of an empty array");for(var t={},i=[],a=0,o=0;oa&&(a=t[u],i=[u])}return i}}));function ks(e,t,r){var n;return-1!==String(e).indexOf("Unexpected type")?(n=arguments.length>2?" (type: "+H(r)+", value: "+JSON.stringify(r)+")":" (type: "+e.data.actual+")",new TypeError("Cannot calculate "+t+", unexpected type of argument"+n)):-1!==String(e).indexOf("complex numbers")?(n=arguments.length>2?" (type: "+H(r)+", value: "+JSON.stringify(r)+")":"",new TypeError("Cannot calculate "+t+", no ordering relation is defined for complex numbers"+n)):e}var Is="prod",Rs=Ee(Is,["typed","config","multiplyScalar","numeric"],(function(e){var t=e.typed,r=e.config,n=e.multiplyScalar,i=e.numeric;return t(Is,{"Array | Matrix":a,"Array | Matrix, number | BigNumber":function(e,t){throw new Error("prod(A, dim) is not yet supported")},"...":function(e){return a(e)}});function a(e){var t;if($n(e,(function(e){try{t=void 0===t?e:n(t,e)}catch(t){throw ks(t,"prod",e)}})),"string"==typeof t&&(t=i(t,r.number)),void 0===t)throw new Error("Cannot calculate prod of an empty array");return t}})),zs="format",qs=Ee(zs,["typed"],(function(e){return(0,e.typed)(zs,{any:Jr,"any, Object | function | number":Jr})})),js=Ee("bin",["typed","format"],(function(e){var t=e.typed,r=e.format;return t("bin",{"number | BigNumber":function(e){return r(e,{notation:"bin"})},"number | BigNumber, number":function(e,t){return r(e,{notation:"bin",wordSize:t})}})})),Ps=Ee("oct",["typed","format"],(function(e){var t=e.typed,r=e.format;return t("oct",{"number | BigNumber":function(e){return r(e,{notation:"oct"})},"number | BigNumber, number":function(e,t){return r(e,{notation:"oct",wordSize:t})}})})),Ls=Ee("hex",["typed","format"],(function(e){var t=e.typed,r=e.format;return t("hex",{"number | BigNumber":function(e){return r(e,{notation:"hex"})},"number | BigNumber, number":function(e,t){return r(e,{notation:"hex",wordSize:t})}})})),Us=/\$([\w.]+)/g,$s="print",Hs=Ee($s,["typed"],(function(e){return(0,e.typed)($s,{"string, Object | Array":Gs,"string, Object | Array, number | Object":Gs})}));function Gs(e,t,r){return e.replace(Us,(function(e,n){var i=n.split("."),a=t[i.shift()];for(void 0!==a&&a.isMatrix&&(a=a.toArray());i.length&&void 0!==a;){var o=i.shift();a=o?a[o]:a+"."}return void 0!==a?c(a)?a:Jr(a,r):e}))}var Vs=Ee("to",["typed","matrix","concat"],(function(e){var t=e.typed,r=e.matrix,n=e.concat;return t("to",{"Unit, Unit | string":function(e,t){return e.to(t)}},Wa({typed:t,matrix:r,concat:n})({Ds:!0}))})),Zs="isPrime",Ws=Ee(Zs,["typed"],(function(e){var t=e.typed;return t(Zs,{number:function(e){if(0*e!=0)return!1;if(e<=3)return e>1;if(e%2==0||e%3==0)return!1;for(var t=5;t*t<=e;t+=6)if(e%t==0||e%(t+2)==0)return!1;return!0},BigNumber:function(e){if(0*e.toNumber()!=0)return!1;if(e.lte(3))return e.gt(1);if(e.mod(2).eq(0)||e.mod(3).eq(0))return!1;if(e.lt(Math.pow(2,32))){for(var t=e.toNumber(),r=5;r*r<=t;r+=6)if(t%r==0||t%(r+2)==0)return!1;return!0}function n(e,t,r){for(var n=1;!t.eq(0);)t.mod(2).eq(0)?(t=t.div(2),e=e.mul(e).mod(r)):(t=t.sub(1),n=e.mul(n).mod(r));return n}for(var i=e.constructor.clone({precision:2*e.toFixed(0).length}),a=0,o=(e=new i(e)).sub(1);o.mod(2).eq(0);)o=o.div(2),a+=1;var u=null;if(e.lt("3317044064679887385961981"))u=[2,3,5,7,11,13,17,19,23,29,31,37,41].filter((function(t){return t1&&void 0!==arguments[1]?arguments[1]:"number";if(void 0!==(arguments.length>2?arguments[2]:void 0))throw new SyntaxError("numeric() takes one or two arguments");var r=H(e);if(!(r in i))throw new TypeError("Cannot convert "+e+' of type "'+r+'"; valid input types are '+Object.keys(i).join(", "));if(!(t in a))throw new TypeError("Cannot convert "+e+' to type "'+t+'"; valid output types are '+Object.keys(a).join(", "));return t===r?e:a[t](e)}})),Js="divideScalar",Xs=Ee(Js,["typed","numeric"],(function(e){var t=e.typed;return e.numeric,t(Js,{"number, number":function(e,t){return e/t},"Complex, Complex":function(e,t){return e.div(t)},"BigNumber, BigNumber":function(e,t){return e.div(t)},"Fraction, Fraction":function(e,t){return e.div(t)},"Unit, number | Complex | Fraction | BigNumber | Unit":function(e,t){return e.divide(t)},"number | Fraction | Complex | BigNumber, Unit":function(e,t){return t.divideInto(e)}})})),Qs=Ee("pow",["typed","config","identity","multiply","matrix","inv","fraction","number","Complex"],(function(e){var t=e.typed,r=e.config,n=e.identity,i=e.multiply,a=e.matrix,o=e.inv,u=e.number,s=e.fraction,c=e.Complex;return t("pow",{"number, number":f,"Complex, Complex":function(e,t){return e.pow(t)},"BigNumber, BigNumber":function(e,t){return t.isInteger()||e>=0||r.predictable?e.pow(t):new c(e.toNumber(),0).pow(t.toNumber(),0)},"Fraction, Fraction":function(e,t){var n=e.pow(t);if(null!=n)return n;if(r.predictable)throw new Error("Result of pow is non-rational and cannot be expressed as a fraction");return f(e.valueOf(),t.valueOf())},"Array, number":l,"Array, BigNumber":function(e,t){return l(e,t.toNumber())},"Matrix, number":p,"Matrix, BigNumber":function(e,t){return p(e,t.toNumber())},"Unit, number | BigNumber":function(e,t){return e.pow(t)}});function f(e,t){if(r.predictable&&!V(t)&&e<0)try{var n=s(t),i=u(n);if((t===i||Math.abs((t-i)/t)<1e-14)&&n.d%2==1)return(n.n%2==0?1:-1)*Math.pow(-e,t)}catch(e){}return r.predictable&&(e<-1&&t===1/0||e>-1&&e<0&&t===-1/0)?NaN:V(t)||e>=0||r.predictable?aa(e,t):e*e<1&&t===1/0||e*e>1&&t===-1/0?0:new c(e,0).pow(t,0)}function l(e,t){if(!V(t))throw new TypeError("For A^b, b must be an integer (value is "+t+")");var r=an(e);if(2!==r.length)throw new Error("For A^b, A must be 2 dimensional (A has "+r.length+" dimensions)");if(r[0]!==r[1])throw new Error("For A^b, A must be square (size is "+r[0]+"x"+r[1]+")");if(t<0)try{return l(o(e),-t)}catch(e){if("Cannot calculate inverse, determinant is zero"===e.message)throw new TypeError("For A^b, when A is not invertible, b must be a positive integer (value is "+t+")");throw e}for(var a=n(r[0]).valueOf(),u=e;t>=1;)1==(1&t)&&(a=i(u,a)),t>>=1,u=i(u,u);return a}function p(e,t){return a(l(e.valueOf(),t))}})),Ks="Number of decimals in function round must be an integer",ec="round",tc=Ee(ec,["typed","matrix","equalScalar","zeros","BigNumber","DenseMatrix"],(function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.zeros,a=e.BigNumber,o=e.DenseMatrix,u=Na({typed:t,equalScalar:n}),s=Da({typed:t,DenseMatrix:o}),c=Ea({typed:t});return t(ec,{number:oa,"number, number":oa,"number, BigNumber":function(e,t){if(!t.isInteger())throw new TypeError(Ks);return new a(e).toDecimalPlaces(t.toNumber())},Complex:function(e){return e.round()},"Complex, number":function(e,t){if(t%1)throw new TypeError(Ks);return e.round(t)},"Complex, BigNumber":function(e,t){if(!t.isInteger())throw new TypeError(Ks);var r=t.toNumber();return e.round(r)},BigNumber:function(e){return e.toDecimalPlaces(0)},"BigNumber, BigNumber":function(e,t){if(!t.isInteger())throw new TypeError(Ks);return e.toDecimalPlaces(t.toNumber())},Fraction:function(e){return e.round()},"Fraction, number":function(e,t){if(t%1)throw new TypeError(Ks);return e.round(t)},"Fraction, BigNumber":function(e,t){if(!t.isInteger())throw new TypeError(Ks);return e.round(t.toNumber())},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e,!0)}})),"SparseMatrix, number | BigNumber":t.referToSelf((function(e){return function(t,r){return u(t,r,e,!1)}})),"DenseMatrix, number | BigNumber":t.referToSelf((function(e){return function(t,r){return c(t,r,e,!1)}})),"Array, number | BigNumber":t.referToSelf((function(e){return function(t,n){return c(r(t),n,e,!1).valueOf()}})),"number | Complex | BigNumber | Fraction, SparseMatrix":t.referToSelf((function(e){return function(t,r){return n(t,0)?i(r.size(),r.storage()):s(r,t,e,!0)}})),"number | Complex | BigNumber | Fraction, DenseMatrix":t.referToSelf((function(e){return function(t,r){return n(t,0)?i(r.size(),r.storage()):c(r,t,e,!0)}})),"number | Complex | BigNumber | Fraction, Array":t.referToSelf((function(e){return function(t,n){return c(r(n),t,e,!0).valueOf()}}))})})),rc=Ee("log",["config","typed","divideScalar","Complex"],(function(e){var t=e.typed,r=e.config,n=e.divideScalar,i=e.Complex;return t("log",{number:function(e){return e>=0||r.predictable?function(e,t){return Math.log(e)}(e):new i(e,0).log()},Complex:function(e){return e.log()},BigNumber:function(e){return!e.isNegative()||r.predictable?e.ln():new i(e.toNumber(),0).log()},"any, any":t.referToSelf((function(e){return function(t,r){return n(e(t),e(r))}}))})})),nc="log1p",ic=Ee(nc,["typed","config","divideScalar","log","Complex"],(function(e){var t=e.typed,r=e.config,n=e.divideScalar,i=e.log,a=e.Complex;return t(nc,{number:function(e){return e>=-1||r.predictable?J(e):o(new a(e,0))},Complex:o,BigNumber:function(e){var t=e.plus(1);return!t.isNegative()||r.predictable?t.ln():o(new a(e.toNumber(),0))},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}})),"any, any":t.referToSelf((function(e){return function(t,r){return n(e(t),i(r))}}))});function o(e){var t=e.re+1;return new a(Math.log(Math.sqrt(t*t+e.im*e.im)),Math.atan2(e.im,t))}})),ac="nthRoots",oc=Ee(ac,["config","typed","divideScalar","Complex"],(function(e){var t=e.typed,r=(e.config,e.divideScalar,e.Complex),n=[function(e){return new r(e,0)},function(e){return new r(0,e)},function(e){return new r(-e,0)},function(e){return new r(0,-e)}];function i(e,t){if(t<0)throw new Error("Root must be greater than zero");if(0===t)throw new Error("Root must be non-zero");if(t%1!=0)throw new Error("Root must be an integer");if(0===e||0===e.abs())return[new r(0,0)];var i,a="number"==typeof e;(a||0===e.re||0===e.im)&&(i=a?2*+(e<0):0===e.im?2*+(e.re<0):2*+(e.im<0)+1);for(var o=e.arg(),u=e.abs(),s=[],c=Math.pow(u,1/t),f=0;fd&&(g.push(l[N]),x.push(D))}if(o(y,0))throw new Error("Linear system cannot be solved since matrix is singular");for(var E=n(v,y),A=0,S=x.length;A=0;d--){var v=r[d][0]||0;if(o(v,0))h[d]=[0];else{for(var y=0,g=[],x=[],b=m[d],w=m[d+1]-1;w>=b;w--){var N=p[w];N===d?y=l[w]:N=0;m--){var h=r[m][0]||0,d=void 0;if(o(h,0))d=0;else{var v=p[m][m];if(o(v,0))throw new Error("Linear system cannot be solved since matrix is singular");d=n(h,v);for(var y=m-1;y>=0;y--)r[y]=[a(r[y][0]||0,i(d,p[y][m]))]}l[m]=[d]}return new u({data:l,size:[c,1]})}})),vc="lsolveAll",yc=Ee(vc,["typed","matrix","divideScalar","multiplyScalar","subtractScalar","equalScalar","DenseMatrix"],(function(e){var t=e.typed,r=e.matrix,n=e.divideScalar,i=e.multiplyScalar,a=e.subtractScalar,o=e.equalScalar,u=e.DenseMatrix,s=lc({DenseMatrix:u});return t(vc,{"SparseMatrix, Array | Matrix":function(e,t){return function(e,t){for(var r=[s(e,t,!0)._data.map((function(e){return e[0]}))],c=e._size[0],f=e._size[1],l=e._values,p=e._index,m=e._ptr,h=0;hh&&(g.push(l[D]),x.push(E))}if(o(N,0))if(o(y[h],0)){if(0===v){var A=Vr(y);A[h]=1;for(var S=0,C=x.length;S=0;h--)for(var d=r.length,v=0;v=b;N--){var D=p[N];D===h?w=l[N]:D=0;l--)for(var p=r.length,m=0;m=0;v--)d[v]=a(d[v],c[v][l]);r.push(d)}}else{if(0===m)return[];r.splice(m,1),m-=1,p-=1}else{h[l]=n(h[l],c[l][l]);for(var y=l-1;y>=0;y--)h[y]=a(h[y],i(h[l],c[y][l]))}}return r.map((function(e){return new u({data:e.map((function(e){return[e]})),size:[f,1]})}))}})),bc=Ee("matAlgo08xS0Sid",["typed","equalScalar"],(function(e){var t=e.typed,r=e.equalScalar;return function(e,n,i){var a=e._values,o=e._index,u=e._ptr,s=e._size,c=e._datatype,f=n._values,l=n._index,p=n._ptr,m=n._size,h=n._datatype;if(s.length!==m.length)throw new rn(s.length,m.length);if(s[0]!==m[0]||s[1]!==m[1])throw new RangeError("Dimension mismatch. Matrix A ("+s+") must match Matrix B ("+m+")");if(!a||!f)throw new Error("Cannot perform operation on Pattern Sparse Matrices");var d,v=s[0],y=s[1],g=r,x=0,b=i;"string"==typeof c&&c===h&&(d=c,g=t.find(r,[d,d]),x=t.convert(0,d),b=t.find(i,[d,d]));for(var w,N,D,E,A=[],S=[],C=[],M=[],F=[],O=0;Ot?1:-1},"BigNumber, BigNumber":function(e,t){return di(e,t,r.epsilon)?new a(0):new a(e.cmp(t))},"Fraction, Fraction":function(e,t){return new o(e.compare(t))},"Complex, Complex":function(){throw new TypeError("No ordering relation is defined for complex numbers")}},m,p({SS:f,DS:c,Ss:l}))})),Tc=Ee(Fc,["typed","config"],(function(e){var t=e.typed,r=e.config;return t(Fc,{"number, number":function(e,t){return ue(e,t,r.epsilon)?0:e>t?1:-1}})})),Bc=r(3228),_c="compareNatural",kc=Ee(_c,["typed","compare"],(function(e){var t=e.typed,r=e.compare,n=r.signatures["boolean,boolean"];return t(_c,{"any, any":function e(t,o){var u,s=H(t),c=H(o);if(!("number"!==s&&"BigNumber"!==s&&"Fraction"!==s||"number"!==c&&"BigNumber"!==c&&"Fraction"!==c))return"0"!==(u=r(t,o)).toString()?u>0?1:-1:Bc(s,c);var f=["Array","DenseMatrix","SparseMatrix"];if(f.includes(s)||f.includes(c))return 0!==(u=i(e,t,o))?u:Bc(s,c);if(s!==c)return Bc(s,c);if("Complex"===s)return function(e,t){return e.re>t.re?1:e.ret.im?1:e.imr.length?1:t.lengtht},"BigNumber, BigNumber":function(e,t){return e.gt(t)&&!di(e,t,r.epsilon)},"Fraction, Fraction":function(e,t){return 1===e.compare(t)},"Complex, Complex":function(){throw new TypeError("No ordering relation is defined for complex numbers")}},f,c({SS:u,DS:o,Ss:s}))})),Xc=Ee(Yc,["typed","config"],(function(e){var t=e.typed,r=e.config;return t(Yc,{"number, number":function(e,t){return e>t&&!ue(e,t,r.epsilon)}})})),Qc="largerEq",Kc=Ee(Qc,["typed","config","matrix","DenseMatrix","concat"],(function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.DenseMatrix,a=e.concat,o=Ha({typed:t}),u=Ko({typed:t,DenseMatrix:i}),s=Da({typed:t,DenseMatrix:i}),c=Wa({typed:t,matrix:n,concat:a}),f=vi({typed:t});return t(Qc,ef({typed:t,config:r}),{"boolean, boolean":function(e,t){return e>=t},"BigNumber, BigNumber":function(e,t){return e.gte(t)||di(e,t,r.epsilon)},"Fraction, Fraction":function(e,t){return-1!==e.compare(t)},"Complex, Complex":function(){throw new TypeError("No ordering relation is defined for complex numbers")}},f,c({SS:u,DS:o,Ss:s}))})),ef=Ee(Qc,["typed","config"],(function(e){var t=e.typed,r=e.config;return t(Qc,{"number, number":function(e,t){return e>=t||ue(e,t,r.epsilon)}})})),tf="deepEqual",rf=Ee(tf,["typed","equal"],(function(e){var t=e.typed,r=e.equal;return t(tf,{"any, any":function(e,t){return n(e.valueOf(),t.valueOf())}});function n(e,t){if(Array.isArray(e)){if(Array.isArray(t)){var i=e.length;if(i!==t.length)return!1;for(var a=0;a1)throw new Error("Only one dimensional matrices supported");return s(e.valueOf(),t,r)}if(Array.isArray(e))return s(e,t,r)}function s(e,t,i){if(t>=e.length)throw new Error("k out of bounds");for(var a=0;a=0){var l=e[c];e[c]=e[s],e[s]=l,--c}else++s;i(e[s],f)>0&&--s,t<=s?u=s:o=s+1}return e[t]}})),cf="sort",ff=Ee(cf,["typed","matrix","compare","compareNatural"],(function(e){var t=e.typed,r=e.matrix,n=e.compare,i=e.compareNatural,a=n,o=function(e,t){return-n(e,t)};return t(cf,{Array:function(e){return s(e),e.sort(a)},Matrix:function(e){return c(e),r(e.toArray().sort(a),e.storage())},"Array, function":function(e,t){return s(e),e.sort(t)},"Matrix, function":function(e,t){return c(e),r(e.toArray().sort(t),e.storage())},"Array, string":function(e,t){return s(e),e.sort(u(t))},"Matrix, string":function(e,t){return c(e),r(e.toArray().sort(u(t)),e.storage())}});function u(e){if("asc"===e)return a;if("desc"===e)return o;if("natural"===e)return i;throw new Error('String "asc", "desc", or "natural" expected')}function s(e){if(1!==an(e).length)throw new Error("One dimensional array expected")}function c(e){if(1!==e.size().length)throw new Error("One dimensional matrix expected")}})),lf=Ee("max",["typed","config","numeric","larger"],(function(e){var t=e.typed,r=e.config,n=e.numeric,i=e.larger;return t("max",{"Array | Matrix":o,"Array | Matrix, number | BigNumber":function(e,t){return Gn(e,t.valueOf(),a)},"...":function(e){if(Un(e))throw new TypeError("Scalar values expected in function max");return o(e)}});function a(e,t){try{return i(e,t)?e:t}catch(e){throw ks(e,"max",t)}}function o(e){var t;if($n(e,(function(e){try{isNaN(e)&&"number"==typeof e?t=NaN:(void 0===t||i(e,t))&&(t=e)}catch(t){throw ks(t,"max",e)}})),void 0===t)throw new Error("Cannot calculate max of an empty array");return"string"==typeof t&&(t=n(t,r.number)),t}})),pf=Ee("min",["typed","config","numeric","smaller"],(function(e){var t=e.typed,r=e.config,n=e.numeric,i=e.smaller;return t("min",{"Array | Matrix":o,"Array | Matrix, number | BigNumber":function(e,t){return Gn(e,t.valueOf(),a)},"...":function(e){if(Un(e))throw new TypeError("Scalar values expected in function min");return o(e)}});function a(e,t){try{return i(e,t)?e:t}catch(e){throw ks(e,"min",t)}}function o(e){var t;if($n(e,(function(e){try{isNaN(e)&&"number"==typeof e?t=NaN:(void 0===t||i(e,t))&&(t=e)}catch(t){throw ks(t,"min",e)}})),void 0===t)throw new Error("Cannot calculate min of an empty array");return"string"==typeof t&&(t=n(t,r.number)),t}})),mf=Ee("ImmutableDenseMatrix",["smaller","DenseMatrix"],(function(e){var t=e.smaller,r=e.DenseMatrix;function n(e,t){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(t&&!c(t))throw new Error("Invalid datatype: "+t);if(l(e)||f(e)){var i=new r(e,t);this._data=i._data,this._size=i._size,this._datatype=i._datatype,this._min=null,this._max=null}else if(e&&f(e.data)&&f(e.size))this._data=e.data,this._size=e.size,this._datatype=e.datatype,this._min=void 0!==e.min?e.min:null,this._max=void 0!==e.max?e.max:null;else{if(e)throw new TypeError("Unsupported type of data ("+H(e)+")");this._data=[],this._size=[0],this._datatype=t,this._min=null,this._max=null}}return n.prototype=new r,n.prototype.type="ImmutableDenseMatrix",n.prototype.isImmutableDenseMatrix=!0,n.prototype.subset=function(e){switch(arguments.length){case 1:var t=r.prototype.subset.call(this,e);return l(t)?new n({data:t._data,size:t._size,datatype:t._datatype}):t;case 2:case 3:throw new Error("Cannot invoke set subset on an Immutable Matrix instance");default:throw new SyntaxError("Wrong number of arguments")}},n.prototype.set=function(){throw new Error("Cannot invoke set on an Immutable Matrix instance")},n.prototype.resize=function(){throw new Error("Cannot invoke resize on an Immutable Matrix instance")},n.prototype.reshape=function(){throw new Error("Cannot invoke reshape on an Immutable Matrix instance")},n.prototype.clone=function(){return new n({data:he(this._data),size:he(this._size),datatype:this._datatype})},n.prototype.toJSON=function(){return{mathjs:"ImmutableDenseMatrix",data:this._data,size:this._size,datatype:this._datatype}},n.fromJSON=function(e){return new n(e)},n.prototype.swapRows=function(){throw new Error("Cannot invoke swapRows on an Immutable Matrix instance")},n.prototype.min=function(){if(null===this._min){var e=null;this.forEach((function(r){(null===e||t(r,e))&&(e=r)})),this._min=null!==e?e:void 0}return this._min},n.prototype.max=function(){if(null===this._max){var e=null;this.forEach((function(r){(null===e||t(e,r))&&(e=r)})),this._max=null!==e?e:void 0}return this._max},n}),{isClass:!0}),hf=Ee("Index",["ImmutableDenseMatrix","getMatrixDataType"],(function(e){var t=e.ImmutableDenseMatrix,r=e.getMatrixDataType;function n(e){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");this._dimensions=[],this._sourceSize=[],this._isScalar=!0;for(var t=0,a=arguments.length;t0;){var s=o.right;o.left.right=o.right,o.right.left=o.left,o.left=i,o.right=i.right,i.right=o,o.right.left=o,o.parent=null,o=s,a--}return e.left.right=e.right,e.right.left=e.left,i=e===e.right?null:function(e,i){var a,o=Math.floor(Math.log(i)*n)+1,s=new Array(o),c=0,f=e;if(f)for(c++,f=f.right;f!==e;)c++,f=f.right;for(;c>0;){for(var l=f.degree,p=f.right;a=s[l];){if(r(f.key,a.key)){var m=a;a=f,f=m}u(a,f),s[l]=null,l++}s[l]=f,f=p,c--}e=null;for(var h=0;h=e&&(r(u.value,0)||n(u.key,u.value,this)),(u=i.extractMinimum())&&o.push(u);for(var s=0;s="0"&&e<="9"}function M(){n++,i=r.charAt(n)}function F(e){n=e,i=r.charAt(n)}function O(){var e="",t=n;if("+"===i?M():"-"===i&&(e+=i,M()),!function(e){return e>="0"&&e<="9"||"."===e}(i))return F(t),null;if("."===i){if(e+=i,M(),!C(i))return F(t),null}else{for(;C(i);)e+=i,M();"."===i&&(e+=i,M())}for(;C(i);)e+=i,M();if("E"===i||"e"===i){var r="",a=n;if(r+=i,M(),"+"!==i&&"-"!==i||(r+=i,M()),!C(i))return F(a),e;for(e+=r;C(i);)e+=i,M()}return e}function T(){for(var e="";C(i)||A.isValidAlpha(i);)e+=i,M();var t=e.charAt(0);return A.isValidAlpha(t)?e:null}function B(e){return i===e?(M(),e):null}Object.defineProperty(A,"name",{value:"Unit"}),A.prototype.constructor=A,A.prototype.type="Unit",A.prototype.isUnit=!0,A.parse=function(e,t){if(t=t||{},n=-1,i="","string"!=typeof(r=e))throw new TypeError("Invalid argument in Unit.parse, string expected");var a=new A;a.units=[];var o=1,s=!1;M(),S();var c=O(),f=null;if(c){if("BigNumber"===u.number)f=new N(c);else if("Fraction"===u.number)try{f=new D(c)}catch(e){f=parseFloat(c)}else f=parseFloat(c);S(),B("*")?(o=1,s=!0):B("/")&&(o=-1,s=!0)}for(var l=[],p=1;;){for(S();"("===i;)l.push(o),p*=o,o=1,M(),S();var m;if(!i)break;var h=i;if(null===(m=T()))throw new SyntaxError('Unexpected "'+h+'" in "'+r+'" at index '+n.toString());var d=_(m);if(null===d)throw new SyntaxError('Unit "'+m+'" not found.');var v=o*p;if(S(),B("^")){S();var y=O();if(null===y)throw new SyntaxError('In "'+e+'", "^" must be followed by a floating-point number');v*=y}a.units.push({unit:d.unit,prefix:d.prefix,power:v});for(var g=0;g1||Math.abs(this.units[0].power-1)>1e-15)},A.prototype._normalize=function(e){if(null==e||0===this.units.length)return e;for(var t=e,r=A._getNumberConverter(H(e)),n=0;n1e-12)return!1;return!0},A.prototype.equalBase=function(e){for(var t=0;t1e-12)return!1;return!0},A.prototype.equals=function(e){return this.equalBase(e)&&y(this.value,e.value)},A.prototype.multiply=function(e){for(var t=this.clone(),r=s(e)?e:new A(e),n=0;n1e-12&&(Ne(G,u)?n.push({unit:G[u].unit,prefix:G[u].prefix,power:r.dimensions[o]||0}):a=!0)}n.length1e-12){if(!Ne($.si,n))throw new Error("Cannot express custom unit "+n+" in SI units");t.push({unit:$.si[n].unit,prefix:$.si[n].prefix,power:e.dimensions[r]||0})}}return e.units=t,e.fixPrefix=!0,e.skipAutomaticSimplification=!0,e},A.prototype.formatUnits=function(){for(var e="",t="",r=0,n=0,i=0;i0?(r++,e+=" "+this.units[i].prefix.name+this.units[i].unit.name,Math.abs(this.units[i].power-1)>1e-15&&(e+="^"+this.units[i].power)):this.units[i].power<0&&n++;if(n>0)for(var a=0;a0?(t+=" "+this.units[a].prefix.name+this.units[a].unit.name,Math.abs(this.units[a].power+1)>1e-15&&(t+="^"+-this.units[a].power)):(t+=" "+this.units[a].prefix.name+this.units[a].unit.name,t+="^"+this.units[a].power));e=e.substr(1),t=t.substr(1),r>1&&n>0&&(e="("+e+")"),n>1&&r>0&&(t="("+t+")");var o=e;return r>0&&n>0&&(o+=" / "),o+t},A.prototype.format=function(e){var t=this.skipAutomaticSimplification||null===this.value?this.clone():this.simplify(),r=!1;for(var n in void 0!==t.value&&null!==t.value&&o(t.value)&&(r=Math.abs(t.value.re)<1e-14),t.units)Ne(t.units,n)&&t.units[n].unit&&("VA"===t.units[n].unit.name&&r?t.units[n].unit=P.VAR:"VAR"!==t.units[n].unit.name||r||(t.units[n].unit=P.VA));1!==t.units.length||t.fixPrefix||Math.abs(t.units[0].power-Math.round(t.units[0].power))<1e-14&&(t.units[0].prefix=t._bestPrefix());var i=t._denormalize(t.value),a=null!==t.value?x(i,e||{}):"",u=t.formatUnits();return t.value&&o(t.value)&&(a="("+a+")"),u.length>0&&a.length>0&&(a+=" "),a+u},A.prototype._bestPrefix=function(){if(1!==this.units.length)throw new Error("Can only compute the best prefix for single units with integer powers, like kg, s^2, N^-1, and so forth!");if(Math.abs(this.units[0].power-Math.round(this.units[0].power))>=1e-14)throw new Error("Can only compute the best prefix for single units with integer powers, like kg, s^2, N^-1, and so forth!");var e=null!==this.value?h(this.value):0,t=h(this.units[0].unit.value),r=this.units[0].prefix;if(0===e)return r;var n=this.units[0].power,i=Math.log(e/Math.pow(r.value*t,n))/Math.LN10-1.2;if(i>-2.200001&&i<1.800001)return r;i=Math.abs(i);var a=this.units[0].unit.prefixes;for(var o in a)if(Ne(a,o)){var u=a[o];if(u.scientific){var s=Math.abs(Math.log(e/Math.pow(u.value*t,n))/Math.LN10-1.2);(s0&&!A.isValidAlpha(i)&&!C(i))throw new Error('Invalid unit name (only alphanumeric characters are allowed): "'+e+'"')}}(e);var n,a,o,u=null,s=[],c=0;if(r&&"Unit"===r.type)u=r.clone();else if("string"==typeof r)""!==r&&(n=r);else{if("object"!==t(r))throw new TypeError('Cannot create unit "'+e+'" from "'+r.toString()+'": expecting "string" or "Unit" or "Object"');n=r.definition,a=r.prefixes,c=r.offset,o=r.baseName,r.aliases&&(s=r.aliases.valueOf())}if(s)for(var f=0;f1e-12){h=!1;break}if(h){p=!0,l.base=z[m];break}}if(!p){o=o||e+"_STUFF";var v={dimensions:u.dimensions.slice(0)};v.key=o,z[o]=v,G[o]={unit:l,prefix:I.NONE[""]},l.base=z[o]}}else{if(o=o||e+"_STUFF",R.indexOf(o)>=0)throw new Error('Cannot create new base unit "'+e+'": a base unit with that name already exists (and cannot be overridden)');for(var y in R.push(o),z)Ne(z,y)&&(z[y].dimensions[R.length-1]=0);for(var g={dimensions:[]},x=0;x=-1&&e<=1||r.predictable?Math.acos(e):new n(e,0).acos()},Complex:function(e){return e.acos()},BigNumber:function(e){return e.acos()}})})),kf="number";function If(e){return se(e)}function Rf(e){return Math.atan(1/e)}function zf(e){return isFinite(e)?(Math.log((e+1)/e)+Math.log(e/(e-1)))/2:0}function qf(e){return Math.asin(1/e)}function jf(e){var t=1/e;return Math.log(t+Math.sqrt(t*t+1))}function Pf(e){return Math.acos(1/e)}function Lf(e){var t=1/e,r=Math.sqrt(t*t-1);return Math.log(r+t)}function Uf(e){return ce(e)}function $f(e){return fe(e)}function Hf(e){return 1/Math.tan(e)}function Gf(e){var t=Math.exp(2*e);return(t+1)/(t-1)}function Vf(e){return 1/Math.sin(e)}function Zf(e){return 0===e?Number.POSITIVE_INFINITY:Math.abs(2/(Math.exp(e)-Math.exp(-e)))*Z(e)}function Wf(e){return 1/Math.cos(e)}function Yf(e){return 2/(Math.exp(e)+Math.exp(-e))}function Jf(e){return pe(e)}If.signature=kf,Rf.signature=kf,zf.signature=kf,qf.signature=kf,jf.signature=kf,Pf.signature=kf,Lf.signature=kf,Uf.signature=kf,$f.signature=kf,Hf.signature=kf,Gf.signature=kf,Vf.signature=kf,Zf.signature=kf,Wf.signature=kf,Yf.signature=kf,Jf.signature=kf;var Xf="acosh",Qf=Ee(Xf,["typed","config","Complex"],(function(e){var t=e.typed,r=e.config,n=e.Complex;return t(Xf,{number:function(e){return e>=1||r.predictable?If(e):e<=-1?new n(Math.log(Math.sqrt(e*e-1)-e),Math.PI):new n(e,0).acosh()},Complex:function(e){return e.acosh()},BigNumber:function(e){return e.acosh()}})})),Kf="acot",el=Ee(Kf,["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t(Kf,{number:Rf,Complex:function(e){return e.acot()},BigNumber:function(e){return new r(1).div(e).atan()}})})),tl="acoth",rl=Ee(tl,["typed","config","Complex","BigNumber"],(function(e){var t=e.typed,r=e.config,n=e.Complex,i=e.BigNumber;return t(tl,{number:function(e){return e>=1||e<=-1||r.predictable?zf(e):new n(e,0).acoth()},Complex:function(e){return e.acoth()},BigNumber:function(e){return new i(1).div(e).atanh()}})})),nl="acsc",il=Ee(nl,["typed","config","Complex","BigNumber"],(function(e){var t=e.typed,r=e.config,n=e.Complex,i=e.BigNumber;return t(nl,{number:function(e){return e<=-1||e>=1||r.predictable?qf(e):new n(e,0).acsc()},Complex:function(e){return e.acsc()},BigNumber:function(e){return new i(1).div(e).asin()}})})),al="acsch",ol=Ee(al,["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t(al,{number:jf,Complex:function(e){return e.acsch()},BigNumber:function(e){return new r(1).div(e).asinh()}})})),ul="asec",sl=Ee(ul,["typed","config","Complex","BigNumber"],(function(e){var t=e.typed,r=e.config,n=e.Complex,i=e.BigNumber;return t(ul,{number:function(e){return e<=-1||e>=1||r.predictable?Pf(e):new n(e,0).asec()},Complex:function(e){return e.asec()},BigNumber:function(e){return new i(1).div(e).acos()}})})),cl="asech",fl=Ee(cl,["typed","config","Complex","BigNumber"],(function(e){var t=e.typed,r=e.config,n=e.Complex,i=e.BigNumber;return t(cl,{number:function(e){if(e<=1&&e>=-1||r.predictable){var t=1/e;if(t>0||r.predictable)return Lf(e);var i=Math.sqrt(t*t-1);return new n(Math.log(i-t),Math.PI)}return new n(e,0).asech()},Complex:function(e){return e.asech()},BigNumber:function(e){return new i(1).div(e).acosh()}})})),ll="asin",pl=Ee(ll,["typed","config","Complex"],(function(e){var t=e.typed,r=e.config,n=e.Complex;return t(ll,{number:function(e){return e>=-1&&e<=1||r.predictable?Math.asin(e):new n(e,0).asin()},Complex:function(e){return e.asin()},BigNumber:function(e){return e.asin()}})})),ml=Ee("asinh",["typed"],(function(e){return(0,e.typed)("asinh",{number:Uf,Complex:function(e){return e.asinh()},BigNumber:function(e){return e.asinh()}})})),hl=Ee("atan",["typed"],(function(e){return(0,e.typed)("atan",{number:function(e){return Math.atan(e)},Complex:function(e){return e.atan()},BigNumber:function(e){return e.atan()}})})),dl="atan2",vl=Ee(dl,["typed","matrix","equalScalar","BigNumber","DenseMatrix","concat"],(function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.BigNumber,a=e.DenseMatrix,o=e.concat,u=$a({typed:t,equalScalar:n}),s=Ha({typed:t}),c=Mo({typed:t,equalScalar:n}),f=Na({typed:t,equalScalar:n}),l=Da({typed:t,DenseMatrix:a}),p=Wa({typed:t,matrix:r,concat:o});return t(dl,{"number, number":Math.atan2,"BigNumber, BigNumber":function(e,t){return i.atan2(e,t)}},p({scalar:"number | BigNumber",SS:c,DS:s,SD:u,Ss:f,sS:l}))})),yl="atanh",gl=Ee(yl,["typed","config","Complex"],(function(e){var t=e.typed,r=e.config,n=e.Complex;return t(yl,{number:function(e){return e<=1&&e>=-1||r.predictable?$f(e):new n(e,0).atanh()},Complex:function(e){return e.atanh()},BigNumber:function(e){return e.atanh()}})})),xl=Ee("trigUnit",["typed"],(function(e){var t=e.typed;return{Unit:t.referToSelf((function(e){return function(r){if(!r.hasBase(r.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function cot is no angle");return t.find(e,r.valueType())(r.value)}}))}})),bl=Ee("cos",["typed"],(function(e){var t=e.typed,r=xl({typed:t});return t("cos",{number:Math.cos,"Complex | BigNumber":function(e){return e.cos()}},r)})),wl="cosh",Nl=Ee(wl,["typed"],(function(e){return(0,e.typed)(wl,{number:le,"Complex | BigNumber":function(e){return e.cosh()}})})),Dl=Ee("cot",["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t("cot",{number:Hf,Complex:function(e){return e.cot()},BigNumber:function(e){return new r(1).div(e.tan())}},xl({typed:t}))})),El="coth",Al=Ee(El,["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t(El,{number:Gf,Complex:function(e){return e.coth()},BigNumber:function(e){return new r(1).div(e.tanh())}})})),Sl=Ee("csc",["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t("csc",{number:Vf,Complex:function(e){return e.csc()},BigNumber:function(e){return new r(1).div(e.sin())}},xl({typed:t}))})),Cl="csch",Ml=Ee(Cl,["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t(Cl,{number:Zf,Complex:function(e){return e.csch()},BigNumber:function(e){return new r(1).div(e.sinh())}})})),Fl=Ee("sec",["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t("sec",{number:Wf,Complex:function(e){return e.sec()},BigNumber:function(e){return new r(1).div(e.cos())}},xl({typed:t}))})),Ol="sech",Tl=Ee(Ol,["typed","BigNumber"],(function(e){var t=e.typed,r=e.BigNumber;return t(Ol,{number:Yf,Complex:function(e){return e.sech()},BigNumber:function(e){return new r(1).div(e.cosh())}})})),Bl=Ee("sin",["typed"],(function(e){var t=e.typed,r=xl({typed:t});return t("sin",{number:Math.sin,"Complex | BigNumber":function(e){return e.sin()}},r)})),_l="sinh",kl=Ee(_l,["typed"],(function(e){return(0,e.typed)(_l,{number:Jf,"Complex | BigNumber":function(e){return e.sinh()}})})),Il=Ee("tan",["typed"],(function(e){var t=e.typed,r=xl({typed:t});return t("tan",{number:Math.tan,"Complex | BigNumber":function(e){return e.tan()}},r)})),Rl=Ee("tanh",["typed"],(function(e){return(0,e.typed)("tanh",{number:me,"Complex | BigNumber":function(e){return e.tanh()}})})),zl="setCartesian",ql=Ee(zl,["typed","size","subset","compareNatural","Index","DenseMatrix"],(function(e){var t=e.typed,r=e.size,n=e.subset,i=e.compareNatural,a=e.Index,o=e.DenseMatrix;return t(zl,{"Array | Matrix, Array | Matrix":function(e,t){var u=[];if(0!==n(r(e),new a(0))&&0!==n(r(t),new a(0))){var s=bn(Array.isArray(e)?e:e.toArray()).sort(i),c=bn(Array.isArray(t)?t:t.toArray()).sort(i);u=[];for(var f=0;f0;r--)for(var n=0;ne[n+1].length&&(t=e[n],e[n]=e[n+1],e[n+1]=t);return e}(u)}});function o(e,t){for(var r=[],n=0;nd?m++:h===d&&(c=f(c,l(a[p],s[m])),p++,m++)}return c}});function o(e,t){var r,n,i=u(e),a=u(t);if(1===i.length)r=i[0];else{if(2!==i.length||1!==i[1])throw new RangeError("Expected a column vector, instead got a matrix of size ("+i.join(", ")+")");r=i[0]}if(1===a.length)n=a[0];else{if(2!==a.length||1!==a[1])throw new RangeError("Expected a column vector, instead got a matrix of size ("+a.join(", ")+")");n=a[0]}if(r!==n)throw new RangeError("Vectors must have equal length ("+r+" != "+n+")");if(0===r)throw new RangeError("Cannot calculate the dot product of empty vectors");return r}function u(e){return l(e)?e.size():a(e)}})),cp=Ee("trace",["typed","matrix","add"],(function(e){var t=e.typed,r=e.matrix,n=e.add;return t("trace",{Array:function(e){return i(r(e))},SparseMatrix:function(e){var t=e._values,r=e._index,i=e._ptr,a=e._size,o=a[0],u=a[1];if(o===u){var s=0;if(t.length>0)for(var c=0;cc)break}return s}throw new RangeError("Matrix must be square (size: "+Jr(a)+")")},DenseMatrix:i,any:he});function i(e){var t=e._size,r=e._data;switch(t.length){case 1:if(1===t[0])return he(r[0]);throw new RangeError("Matrix must be square (size: "+Jr(t)+")");case 2:var i=t[0];if(i===t[1]){for(var a=0,o=0;o)'),t+this.index.toHTML(e)}},{key:"_toTex",value:function(e){var t=this.object.toTex(e);return i(this.object)&&(t="\\left(' + object + '\\right)"),t+this.index.toTex(e)}},{key:"toJSON",value:function(){return{mathjs:bp,object:this.object,index:this.index}}}],[{key:"fromJSON",value:function(e){return new o(e.object,e.index)}}]),o}(r);return Ua(a,"name",bp),a}),{isClass:!0,isNode:!0});var Np="ArrayNode",Dp=Ee(Np,["Node"],(function(e){var t=function(e){dp(i,e);var t,r,n=(t=i,r=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=yp(t);if(r){var i=yp(this).constructor;e=Reflect.construct(n,arguments,i)}else e=n.apply(this,arguments);return vp(this,e)});function i(e){var t;if(Ce(this,i),(t=n.call(this)).items=e||[],!Array.isArray(t.items)||!t.items.every(R))throw new TypeError("Array containing Nodes expected");return t}return Oe(i,[{key:"type",get:function(){return Np}},{key:"isArrayNode",get:function(){return!0}},{key:"_compile",value:function(e,t){var r=wn(this.items,(function(r){return r._compile(e,t)}));if("Array"!==e.config.matrix){var n=e.matrix;return function(e,t,i){return n(wn(r,(function(r){return r(e,t,i)})))}}return function(e,t,n){return wn(r,(function(r){return r(e,t,n)}))}}},{key:"forEach",value:function(e){for(var t=0;t['+this.items.map((function(t){return t.toHTML(e)})).join(',')+']'}},{key:"_toTex",value:function(e){return function t(r,n){var i=r.some(C)&&!r.every(C),a=n||i,o=a?"&":"\\\\",u=r.map((function(r){return r.items?t(r.items,!n):r.toTex(e)})).join(o);return i||!a||a&&!n?"\\begin{bmatrix}"+u+"\\end{bmatrix}":u}(this.items,!1)}}],[{key:"fromJSON",value:function(e){return new i(e.items)}}]),i}(e.Node);return Ua(t,"name",Np),t}),{isClass:!0,isNode:!0});function Ep(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r",associativity:"left",associativeWith:[]},"OperatorNode:smallerEq":{op:"<=",associativity:"left",associativeWith:[]},"OperatorNode:largerEq":{op:">=",associativity:"left",associativeWith:[]},RelationalNode:{associativity:"left",associativeWith:[]}},{"OperatorNode:leftShift":{op:"<<",associativity:"left",associativeWith:[]},"OperatorNode:rightArithShift":{op:">>",associativity:"left",associativeWith:[]},"OperatorNode:rightLogShift":{op:">>>",associativity:"left",associativeWith:[]}},{"OperatorNode:to":{op:"to",associativity:"left",associativeWith:[]}},{RangeNode:{}},{"OperatorNode:add":{op:"+",associativity:"left",associativeWith:["OperatorNode:add","OperatorNode:subtract"]},"OperatorNode:subtract":{op:"-",associativity:"left",associativeWith:[]}},{"OperatorNode:multiply":{op:"*",associativity:"left",associativeWith:["OperatorNode:multiply","OperatorNode:divide","Operator:dotMultiply","Operator:dotDivide"]},"OperatorNode:divide":{op:"/",associativity:"left",associativeWith:[],latexLeftParens:!1,latexRightParens:!1,latexParens:!1},"OperatorNode:dotMultiply":{op:".*",associativity:"left",associativeWith:["OperatorNode:multiply","OperatorNode:divide","OperatorNode:dotMultiply","OperatorNode:doDivide"]},"OperatorNode:dotDivide":{op:"./",associativity:"left",associativeWith:[]},"OperatorNode:mod":{op:"mod",associativity:"left",associativeWith:[]}},{"OperatorNode:multiply":{associativity:"left",associativeWith:["OperatorNode:multiply","OperatorNode:divide","Operator:dotMultiply","Operator:dotDivide"]}},{"OperatorNode:unaryPlus":{op:"+",associativity:"right"},"OperatorNode:unaryMinus":{op:"-",associativity:"right"},"OperatorNode:bitNot":{op:"~",associativity:"right"},"OperatorNode:not":{op:"not",associativity:"right"}},{"OperatorNode:pow":{op:"^",associativity:"right",associativeWith:[],latexRightParens:!1},"OperatorNode:dotPow":{op:".^",associativity:"right",associativeWith:[]}},{"OperatorNode:factorial":{op:"!",associativity:"left"}},{"OperatorNode:ctranspose":{op:"'",associativity:"left"}}];function Sp(e,t){if(!t||"auto"!==t)return e;for(var r=e;j(r);)r=r.content;return r}function Cp(e,t,r,n){var i=e;"keep"!==t&&(i=e.getContent());for(var a=i.getIdentifier(),o=null,u=0;u)'),t+r+'='+n}},{key:"_toTex",value:function(e){var t=this.object.toTex(e),r=this.index?this.index.toTex(e):"",n=this.value.toTex(e);return u(this,e&&e.parenthesis,e&&e.implicit)&&(n="\\left(".concat(n,"\\right)")),t+r+":="+n}}],[{key:"fromJSON",value:function(e){return new i(e.object,e.index,e.value)}}]),i}(i);return Ua(s,"name",Op),s}),{isClass:!0,isNode:!0});var Bp="BlockNode",_p=Ee(Bp,["ResultSet","Node"],(function(e){var t=e.ResultSet,r=function(e){dp(a,e);var r,n,i=(r=a,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,t=yp(r);if(n){var i=yp(this).constructor;e=Reflect.construct(t,arguments,i)}else e=t.apply(this,arguments);return vp(this,e)});function a(e){var t;if(Ce(this,a),t=i.call(this),!Array.isArray(e))throw new Error("Array expected");return t.blocks=e.map((function(e){var t=e&&e.node,r=!e||void 0===e.visible||e.visible;if(!R(t))throw new TypeError('Property "node" must be a Node');if("boolean"!=typeof r)throw new TypeError('Property "visible" must be a boolean');return{node:t,visible:r}})),t}return Oe(a,[{key:"type",get:function(){return Bp}},{key:"isBlockNode",get:function(){return!0}},{key:"_compile",value:function(e,r){var n=wn(this.blocks,(function(t){return{evaluate:t.node._compile(e,r),visible:t.visible}}));return function(e,r,i){var a=[];return Nn(n,(function(t){var n=t.evaluate(e,r,i);t.visible&&a.push(n)})),new t(a)}}},{key:"forEach",value:function(e){for(var t=0;t;')})).join('
')}},{key:"_toTex",value:function(e){return this.blocks.map((function(t){return t.node.toTex(e)+(t.visible?"":";")})).join("\\;\\;\n")}}],[{key:"fromJSON",value:function(e){return new a(e.blocks)}}]),a}(e.Node);return Ua(r,"name",Bp),r}),{isClass:!0,isNode:!0});var kp="ConditionalNode",Ip=Ee(kp,["Node"],(function(e){var t=function(e){dp(i,e);var t,r,n=(t=i,r=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=yp(t);if(r){var i=yp(this).constructor;e=Reflect.construct(n,arguments,i)}else e=n.apply(this,arguments);return vp(this,e)});function i(e,t,r){var a;if(Ce(this,i),a=n.call(this),!R(e))throw new TypeError("Parameter condition must be a Node");if(!R(t))throw new TypeError("Parameter trueExpr must be a Node");if(!R(r))throw new TypeError("Parameter falseExpr must be a Node");return a.condition=e,a.trueExpr=t,a.falseExpr=r,a}return Oe(i,[{key:"type",get:function(){return kp}},{key:"isConditionalNode",get:function(){return!0}},{key:"_compile",value:function(e,t){var r=this.condition._compile(e,t),n=this.trueExpr._compile(e,t),i=this.falseExpr._compile(e,t);return function(e,t,u){return function(e){if("number"==typeof e||"boolean"==typeof e||"string"==typeof e)return!!e;if(e){if(a(e))return!e.isZero();if(o(e))return!(!e.re&&!e.im);if(s(e))return!!e.value}if(null==e)return!1;throw new TypeError('Unsupported type of condition "'+H(e)+'"')}(r(e,t,u))?n(e,t,u):i(e,t,u)}}},{key:"forEach",value:function(e){e(this.condition,"condition",this),e(this.trueExpr,"trueExpr",this),e(this.falseExpr,"falseExpr",this)}},{key:"map",value:function(e){return new i(this._ifNode(e(this.condition,"condition",this)),this._ifNode(e(this.trueExpr,"trueExpr",this)),this._ifNode(e(this.falseExpr,"falseExpr",this)))}},{key:"clone",value:function(){return new i(this.condition,this.trueExpr,this.falseExpr)}},{key:"_toString",value:function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=Cp(this,t,e&&e.implicit),n=this.condition.toString(e),i=Cp(this.condition,t,e&&e.implicit);("all"===t||"OperatorNode"===this.condition.type||null!==i&&i<=r)&&(n="("+n+")");var a=this.trueExpr.toString(e),o=Cp(this.trueExpr,t,e&&e.implicit);("all"===t||"OperatorNode"===this.trueExpr.type||null!==o&&o<=r)&&(a="("+a+")");var u=this.falseExpr.toString(e),s=Cp(this.falseExpr,t,e&&e.implicit);return("all"===t||"OperatorNode"===this.falseExpr.type||null!==s&&s<=r)&&(u="("+u+")"),n+" ? "+a+" : "+u}},{key:"toJSON",value:function(){return{mathjs:kp,condition:this.condition,trueExpr:this.trueExpr,falseExpr:this.falseExpr}}},{key:"toHTML",value:function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=Cp(this,t,e&&e.implicit),n=this.condition.toHTML(e),i=Cp(this.condition,t,e&&e.implicit);("all"===t||"OperatorNode"===this.condition.type||null!==i&&i<=r)&&(n='('+n+')');var a=this.trueExpr.toHTML(e),o=Cp(this.trueExpr,t,e&&e.implicit);("all"===t||"OperatorNode"===this.trueExpr.type||null!==o&&o<=r)&&(a='('+a+')');var u=this.falseExpr.toHTML(e),s=Cp(this.falseExpr,t,e&&e.implicit);return("all"===t||"OperatorNode"===this.falseExpr.type||null!==s&&s<=r)&&(u='('+u+')'),n+'?'+a+':'+u}},{key:"_toTex",value:function(e){return"\\begin{cases} {"+this.trueExpr.toTex(e)+"}, &\\quad{\\text{if }\\;"+this.condition.toTex(e)+"}\\\\{"+this.falseExpr.toTex(e)+"}, &\\quad{\\text{otherwise}}\\end{cases}"}}],[{key:"fromJSON",value:function(e){return new i(e.condition,e.trueExpr,e.falseExpr)}}]),i}(e.Node);return Ua(t,"name",kp),t}),{isClass:!0,isNode:!0}),Rp=r(7928),zp={Alpha:"A",alpha:"\\alpha",Beta:"B",beta:"\\beta",Gamma:"\\Gamma",gamma:"\\gamma",Delta:"\\Delta",delta:"\\delta",Epsilon:"E",epsilon:"\\epsilon",varepsilon:"\\varepsilon",Zeta:"Z",zeta:"\\zeta",Eta:"H",eta:"\\eta",Theta:"\\Theta",theta:"\\theta",vartheta:"\\vartheta",Iota:"I",iota:"\\iota",Kappa:"K",kappa:"\\kappa",varkappa:"\\varkappa",Lambda:"\\Lambda",lambda:"\\lambda",Mu:"M",mu:"\\mu",Nu:"N",nu:"\\nu",Xi:"\\Xi",xi:"\\xi",Omicron:"O",omicron:"o",Pi:"\\Pi",pi:"\\pi",varpi:"\\varpi",Rho:"P",rho:"\\rho",varrho:"\\varrho",Sigma:"\\Sigma",sigma:"\\sigma",varsigma:"\\varsigma",Tau:"T",tau:"\\tau",Upsilon:"\\Upsilon",upsilon:"\\upsilon",Phi:"\\Phi",phi:"\\phi",varphi:"\\varphi",Chi:"X",chi:"\\chi",Psi:"\\Psi",psi:"\\psi",Omega:"\\Omega",omega:"\\omega",true:"\\mathrm{True}",false:"\\mathrm{False}",i:"i",inf:"\\infty",Inf:"\\infty",infinity:"\\infty",Infinity:"\\infty",oo:"\\infty",lim:"\\lim",undefined:"\\mathbf{?}"},qp={transpose:"^\\top",ctranspose:"^H",factorial:"!",pow:"^",dotPow:".^\\wedge",unaryPlus:"+",unaryMinus:"-",bitNot:"\\~",not:"\\neg",multiply:"\\cdot",divide:"\\frac",dotMultiply:".\\cdot",dotDivide:".:",mod:"\\mod",add:"+",subtract:"-",to:"\\rightarrow",leftShift:"<<",rightArithShift:">>",rightLogShift:">>>",equal:"=",unequal:"\\neq",smaller:"<",larger:">",smallerEq:"\\leq",largerEq:"\\geq",bitAnd:"\\&",bitXor:"\\underline{|}",bitOr:"|",and:"\\wedge",xor:"\\veebar",or:"\\vee"},jp={abs:{1:"\\left|${args[0]}\\right|"},add:{2:"\\left(${args[0]}".concat(qp.add,"${args[1]}\\right)")},cbrt:{1:"\\sqrt[3]{${args[0]}}"},ceil:{1:"\\left\\lceil${args[0]}\\right\\rceil"},cube:{1:"\\left(${args[0]}\\right)^3"},divide:{2:"\\frac{${args[0]}}{${args[1]}}"},dotDivide:{2:"\\left(${args[0]}".concat(qp.dotDivide,"${args[1]}\\right)")},dotMultiply:{2:"\\left(${args[0]}".concat(qp.dotMultiply,"${args[1]}\\right)")},dotPow:{2:"\\left(${args[0]}".concat(qp.dotPow,"${args[1]}\\right)")},exp:{1:"\\exp\\left(${args[0]}\\right)"},expm1:"\\left(e".concat(qp.pow,"{${args[0]}}-1\\right)"),fix:{1:"\\mathrm{${name}}\\left(${args[0]}\\right)"},floor:{1:"\\left\\lfloor${args[0]}\\right\\rfloor"},gcd:"\\gcd\\left(${args}\\right)",hypot:"\\hypot\\left(${args}\\right)",log:{1:"\\ln\\left(${args[0]}\\right)",2:"\\log_{${args[1]}}\\left(${args[0]}\\right)"},log10:{1:"\\log_{10}\\left(${args[0]}\\right)"},log1p:{1:"\\ln\\left(${args[0]}+1\\right)",2:"\\log_{${args[1]}}\\left(${args[0]}+1\\right)"},log2:"\\log_{2}\\left(${args[0]}\\right)",mod:{2:"\\left(${args[0]}".concat(qp.mod,"${args[1]}\\right)")},multiply:{2:"\\left(${args[0]}".concat(qp.multiply,"${args[1]}\\right)")},norm:{1:"\\left\\|${args[0]}\\right\\|",2:void 0},nthRoot:{2:"\\sqrt[${args[1]}]{${args[0]}}"},nthRoots:{2:"\\{y : $y^{args[1]} = {${args[0]}}\\}"},pow:{2:"\\left(${args[0]}\\right)".concat(qp.pow,"{${args[1]}}")},round:{1:"\\left\\lfloor${args[0]}\\right\\rceil",2:void 0},sign:{1:"\\mathrm{${name}}\\left(${args[0]}\\right)"},sqrt:{1:"\\sqrt{${args[0]}}"},square:{1:"\\left(${args[0]}\\right)^2"},subtract:{2:"\\left(${args[0]}".concat(qp.subtract,"${args[1]}\\right)")},unaryMinus:{1:"".concat(qp.unaryMinus,"\\left(${args[0]}\\right)")},unaryPlus:{1:"".concat(qp.unaryPlus,"\\left(${args[0]}\\right)")},bitAnd:{2:"\\left(${args[0]}".concat(qp.bitAnd,"${args[1]}\\right)")},bitNot:{1:qp.bitNot+"\\left(${args[0]}\\right)"},bitOr:{2:"\\left(${args[0]}".concat(qp.bitOr,"${args[1]}\\right)")},bitXor:{2:"\\left(${args[0]}".concat(qp.bitXor,"${args[1]}\\right)")},leftShift:{2:"\\left(${args[0]}".concat(qp.leftShift,"${args[1]}\\right)")},rightArithShift:{2:"\\left(${args[0]}".concat(qp.rightArithShift,"${args[1]}\\right)")},rightLogShift:{2:"\\left(${args[0]}".concat(qp.rightLogShift,"${args[1]}\\right)")},bellNumbers:{1:"\\mathrm{B}_{${args[0]}}"},catalan:{1:"\\mathrm{C}_{${args[0]}}"},stirlingS2:{2:"\\mathrm{S}\\left(${args}\\right)"},arg:{1:"\\arg\\left(${args[0]}\\right)"},conj:{1:"\\left(${args[0]}\\right)^*"},im:{1:"\\Im\\left\\lbrace${args[0]}\\right\\rbrace"},re:{1:"\\Re\\left\\lbrace${args[0]}\\right\\rbrace"},and:{2:"\\left(${args[0]}".concat(qp.and,"${args[1]}\\right)")},not:{1:qp.not+"\\left(${args[0]}\\right)"},or:{2:"\\left(${args[0]}".concat(qp.or,"${args[1]}\\right)")},xor:{2:"\\left(${args[0]}".concat(qp.xor,"${args[1]}\\right)")},cross:{2:"\\left(${args[0]}\\right)\\times\\left(${args[1]}\\right)"},ctranspose:{1:"\\left(${args[0]}\\right)".concat(qp.ctranspose)},det:{1:"\\det\\left(${args[0]}\\right)"},dot:{2:"\\left(${args[0]}\\cdot${args[1]}\\right)"},expm:{1:"\\exp\\left(${args[0]}\\right)"},inv:{1:"\\left(${args[0]}\\right)^{-1}"},pinv:{1:"\\left(${args[0]}\\right)^{+}"},sqrtm:{1:"{${args[0]}}".concat(qp.pow,"{\\frac{1}{2}}")},trace:{1:"\\mathrm{tr}\\left(${args[0]}\\right)"},transpose:{1:"\\left(${args[0]}\\right)".concat(qp.transpose)},combinations:{2:"\\binom{${args[0]}}{${args[1]}}"},combinationsWithRep:{2:"\\left(\\!\\!{\\binom{${args[0]}}{${args[1]}}}\\!\\!\\right)"},factorial:{1:"\\left(${args[0]}\\right)".concat(qp.factorial)},gamma:{1:"\\Gamma\\left(${args[0]}\\right)"},lgamma:{1:"\\ln\\Gamma\\left(${args[0]}\\right)"},equal:{2:"\\left(${args[0]}".concat(qp.equal,"${args[1]}\\right)")},larger:{2:"\\left(${args[0]}".concat(qp.larger,"${args[1]}\\right)")},largerEq:{2:"\\left(${args[0]}".concat(qp.largerEq,"${args[1]}\\right)")},smaller:{2:"\\left(${args[0]}".concat(qp.smaller,"${args[1]}\\right)")},smallerEq:{2:"\\left(${args[0]}".concat(qp.smallerEq,"${args[1]}\\right)")},unequal:{2:"\\left(${args[0]}".concat(qp.unequal,"${args[1]}\\right)")},erf:{1:"erf\\left(${args[0]}\\right)"},max:"\\max\\left(${args}\\right)",min:"\\min\\left(${args}\\right)",variance:"\\mathrm{Var}\\left(${args}\\right)",acos:{1:"\\cos^{-1}\\left(${args[0]}\\right)"},acosh:{1:"\\cosh^{-1}\\left(${args[0]}\\right)"},acot:{1:"\\cot^{-1}\\left(${args[0]}\\right)"},acoth:{1:"\\coth^{-1}\\left(${args[0]}\\right)"},acsc:{1:"\\csc^{-1}\\left(${args[0]}\\right)"},acsch:{1:"\\mathrm{csch}^{-1}\\left(${args[0]}\\right)"},asec:{1:"\\sec^{-1}\\left(${args[0]}\\right)"},asech:{1:"\\mathrm{sech}^{-1}\\left(${args[0]}\\right)"},asin:{1:"\\sin^{-1}\\left(${args[0]}\\right)"},asinh:{1:"\\sinh^{-1}\\left(${args[0]}\\right)"},atan:{1:"\\tan^{-1}\\left(${args[0]}\\right)"},atan2:{2:"\\mathrm{atan2}\\left(${args}\\right)"},atanh:{1:"\\tanh^{-1}\\left(${args[0]}\\right)"},cos:{1:"\\cos\\left(${args[0]}\\right)"},cosh:{1:"\\cosh\\left(${args[0]}\\right)"},cot:{1:"\\cot\\left(${args[0]}\\right)"},coth:{1:"\\coth\\left(${args[0]}\\right)"},csc:{1:"\\csc\\left(${args[0]}\\right)"},csch:{1:"\\mathrm{csch}\\left(${args[0]}\\right)"},sec:{1:"\\sec\\left(${args[0]}\\right)"},sech:{1:"\\mathrm{sech}\\left(${args[0]}\\right)"},sin:{1:"\\sin\\left(${args[0]}\\right)"},sinh:{1:"\\sinh\\left(${args[0]}\\right)"},tan:{1:"\\tan\\left(${args[0]}\\right)"},tanh:{1:"\\tanh\\left(${args[0]}\\right)"},to:{2:"\\left(${args[0]}".concat(qp.to,"${args[1]}\\right)")},numeric:function(e,t){return e.args[0].toTex()},number:{0:"0",1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)${args[1]}\\right)"},string:{0:'\\mathtt{""}',1:"\\mathrm{string}\\left(${args[0]}\\right)"},bignumber:{0:"0",1:"\\left(${args[0]}\\right)"},complex:{0:"0",1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)+".concat(zp.i,"\\cdot\\left(${args[1]}\\right)\\right)")},matrix:{0:"\\begin{bmatrix}\\end{bmatrix}",1:"\\left(${args[0]}\\right)",2:"\\left(${args[0]}\\right)"},sparse:{0:"\\begin{bsparse}\\end{bsparse}",1:"\\left(${args[0]}\\right)"},unit:{1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)${args[1]}\\right)"}},Pp={deg:"^\\circ"};function Lp(e){return Rp(e,{preserveFormatting:!0})}function Up(e,t){return(t=void 0!==t&&t)?Ne(Pp,e)?Pp[e]:"\\mathrm{"+Lp(e)+"}":Ne(zp,e)?zp[e]:Lp(e)}var $p="ConstantNode",Hp=Ee($p,["Node"],(function(e){var t=function(e){dp(i,e);var t,r,n=(t=i,r=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=yp(t);if(r){var i=yp(this).constructor;e=Reflect.construct(n,arguments,i)}else e=n.apply(this,arguments);return vp(this,e)});function i(e){var t;return Ce(this,i),(t=n.call(this)).value=e,t}return Oe(i,[{key:"type",get:function(){return $p}},{key:"isConstantNode",get:function(){return!0}},{key:"_compile",value:function(e,t){var r=this.value;return function(){return r}}},{key:"forEach",value:function(e){}},{key:"map",value:function(e){return this.clone()}},{key:"clone",value:function(){return new i(this.value)}},{key:"_toString",value:function(e){return Jr(this.value,e)}},{key:"toHTML",value:function(e){var t=this._toString(e);switch(H(this.value)){case"number":case"BigNumber":case"Fraction":return''+t+"";case"string":return''+t+"";case"boolean":return''+t+"";case"null":return''+t+"";case"undefined":return''+t+"";default:return''+t+""}}},{key:"toJSON",value:function(){return{mathjs:$p,value:this.value}}},{key:"_toTex",value:function(e){var t=this._toString(e);switch(H(this.value)){case"string":return"\\mathtt{"+Lp(t)+"}";case"number":case"BigNumber":if(!isFinite(this.value))return this.value.valueOf()<0?"-\\infty":"\\infty";var r=t.toLowerCase().indexOf("e");return-1!==r?t.substring(0,r)+"\\cdot10^{"+t.substring(r+1)+"}":t;case"Fraction":return this.value.toLatex();default:return t}}}],[{key:"fromJSON",value:function(e){return new i(e.value)}}]),i}(e.Node);return Ua(t,"name",$p),t}),{isClass:!0,isNode:!0});function Gp(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==r.return||r.return()}finally{if(u)throw a}}}}(t);try{for(s.s();!(i=s.n()).done;){var c=i.value,f="string"==typeof c?c:c.name;if(u.has(f))throw new Error('Duplicate parameter name "'.concat(f,'"'));u.add(f)}}catch(e){s.e(e)}finally{s.f()}return n.name=e,n.params=t.map((function(e){return e&&e.name||e})),n.types=t.map((function(e){return e&&e.type||"any"})),n.expr=r,n}return Oe(o,[{key:"type",get:function(){return Vp}},{key:"isFunctionAssignmentNode",get:function(){return!0}},{key:"_compile",value:function(e,r){var n=Object.create(r);Nn(this.params,(function(e){n[e]=!0}));var i=this.expr._compile(e,n),a=this.name,o=this.params,u=An(this.types,","),s=a+"("+An(this.params,", ")+")";return function(e,r,n){var c={};c[u]=function(){for(var t=Object.create(r),a=0;a'+Kr(this.params[i])+"");var a=this.expr.toHTML(e);return r(this,t,e&&e.implicit)&&(a='('+a+')'),''+Kr(this.name)+'('+n.join(',')+')='+a}},{key:"_toTex",value:function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",n=this.expr.toTex(e);return r(this,t,e&&e.implicit)&&(n="\\left(".concat(n,"\\right)")),"\\mathrm{"+this.name+"}\\left("+this.params.map(Up).join(",")+"\\right):="+n}}],[{key:"fromJSON",value:function(e){return new o(e.name,e.params,e.expr)}}]),o}(e.Node);return Ua(n,"name",Vp),n}),{isClass:!0,isNode:!0});var Wp="IndexNode",Yp=Ee(Wp,["Node","size"],(function(e){var t=e.Node,r=e.size,n=function(e){dp(a,e);var t,n,i=(t=a,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=yp(t);if(n){var i=yp(this).constructor;e=Reflect.construct(r,arguments,i)}else e=r.apply(this,arguments);return vp(this,e)});function a(e,t){var r;if(Ce(this,a),(r=i.call(this)).dimensions=e,r.dotNotation=t||!1,!Array.isArray(e)||!e.every(R))throw new TypeError('Array containing Nodes expected for parameter "dimensions"');if(r.dotNotation&&!r.isObjectProperty())throw new Error("dotNotation only applicable for object properties");return r}return Oe(a,[{key:"type",get:function(){return Wp}},{key:"isIndexNode",get:function(){return!0}},{key:"_compile",value:function(e,t){var n=wn(this.dimensions,(function(n,i){if(n.filter((function(e){return e.isSymbolNode&&"end"===e.name})).length>0){var a=Object.create(t);a.end=!0;var o=n._compile(e,a);return function(e,t,n){if(!l(n)&&!f(n)&&!c(n))throw new TypeError('Cannot resolve "end": context must be a Matrix, Array, or string but is '+H(n));var a=r(n).valueOf(),u=Object.create(t);return u.end=a[i],o(e,u,n)}}return n._compile(e,t)})),i=Te(e,"index");return function(e,t,r){var a=wn(n,(function(n){return n(e,t,r)}));return i.apply(void 0,Vr(a))}}},{key:"forEach",value:function(e){for(var t=0;t.'+Kr(this.getObjectProperty())+"":'['+t.join(',')+']'}},{key:"_toTex",value:function(e){var t=this.dimensions.map((function(t){return t.toTex(e)}));return this.dotNotation?"."+this.getObjectProperty():"_{"+t.join(",")+"}"}}],[{key:"fromJSON",value:function(e){return new a(e.dimensions,e.dotNotation)}}]),a}(t);return Ua(n,"name",Wp),n}),{isClass:!0,isNode:!0});var Jp="ObjectNode",Xp=Ee(Jp,["Node"],(function(e){var r=function(e){dp(a,e);var r,n,i=(r=a,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,t=yp(r);if(n){var i=yp(this).constructor;e=Reflect.construct(t,arguments,i)}else e=t.apply(this,arguments);return vp(this,e)});function a(e){var r;if(Ce(this,a),(r=i.call(this)).properties=e||{},e&&("object"!==t(e)||!Object.keys(e).every((function(t){return R(e[t])}))))throw new TypeError("Object containing Nodes expected");return r}return Oe(a,[{key:"type",get:function(){return Jp}},{key:"isObjectNode",get:function(){return!0}},{key:"_compile",value:function(e,t){var r={};for(var n in this.properties)if(Ne(this.properties,n)){var i=Xr(n),a=JSON.parse(i),o=Te(this.properties,n);r[a]=o._compile(e,t)}return function(e,t,n){var i={};for(var a in r)Ne(r,a)&&(i[a]=r[a](e,t,n));return i}}},{key:"forEach",value:function(e){for(var t in this.properties)Ne(this.properties,t)&&e(this.properties[t],"properties["+Xr(t)+"]",this)}},{key:"map",value:function(e){var t={};for(var r in this.properties)Ne(this.properties,r)&&(t[r]=this._ifNode(e(this.properties[r],"properties["+Xr(r)+"]",this)));return new a(t)}},{key:"clone",value:function(){var e={};for(var t in this.properties)Ne(this.properties,t)&&(e[t]=this.properties[t]);return new a(e)}},{key:"_toString",value:function(e){var t=[];for(var r in this.properties)Ne(this.properties,r)&&t.push(Xr(r)+": "+this.properties[r].toString(e));return"{"+t.join(", ")+"}"}},{key:"toJSON",value:function(){return{mathjs:Jp,properties:this.properties}}},{key:"toHTML",value:function(e){var t=[];for(var r in this.properties)Ne(this.properties,r)&&t.push(''+Kr(r)+':'+this.properties[r].toHTML(e));return'{'+t.join(',')+'}'}},{key:"_toTex",value:function(e){var t=[];for(var r in this.properties)Ne(this.properties,r)&&t.push("\\mathbf{"+r+":} & "+this.properties[r].toTex(e)+"\\\\");return"\\left\\{\\begin{array}{ll}"+t.join("\n")+"\\end{array}\\right\\}"}}],[{key:"fromJSON",value:function(e){return new a(e.properties)}}]),a}(e.Node);return Ua(r,"name",Jp),r}),{isClass:!0,isNode:!0});var Qp="OperatorNode",Kp=Ee(Qp,["Node"],(function(e){function t(e,r){var n=e;if("auto"===r)for(;j(n);)n=n.content;return!!T(n)||!!q(n)&&t(n.args[0],r)}function r(e,r,n,i,a){var o,u=Cp(e,r,n),s=Mp(e,r);if("all"===r||i.length>2&&"OperatorNode:add"!==e.getIdentifier()&&"OperatorNode:multiply"!==e.getIdentifier())return i.map((function(e){switch(e.getContent().type){case"ArrayNode":case"ConstantNode":case"SymbolNode":case"ParenthesisNode":return!1;default:return!0}}));switch(i.length){case 0:o=[];break;case 1:var c=Cp(i[0],r,n,e);if(a&&null!==c){var f,l;if("keep"===r?(f=i[0].getIdentifier(),l=e.getIdentifier()):(f=i[0].getContent().getIdentifier(),l=e.getContent().getIdentifier()),!1===Ap[u][l].latexLeftParens){o=[!1];break}if(!1===Ap[c][f].latexParens){o=[!1];break}}if(null===c){o=[!1];break}if(c<=u){o=[!0];break}o=[!1];break;case 2:var p,m,h=Cp(i[0],r,n,e),d=Fp(e,i[0],r);p=null!==h&&(h===u&&"right"===s&&!d||h=2&&"OperatorNode:multiply"===e.getIdentifier()&&e.implicit&&"all"!==r&&"hide"===n)for(var w=1;w2&&("OperatorNode:add"===this.getIdentifier()||"OperatorNode:multiply"===this.getIdentifier())){var l=i.map((function(t,r){return t=t.toString(e),a[r]&&(t="("+t+")"),t}));return this.implicit&&"OperatorNode:multiply"===this.getIdentifier()&&"hide"===n?l.join(" "):l.join(" "+this.op+" ")}return this.fn+"("+this.args.join(", ")+")"}},{key:"toJSON",value:function(){return{mathjs:Qp,op:this.op,fn:this.fn,args:this.args,implicit:this.implicit,isPercentage:this.isPercentage}}},{key:"toHTML",value:function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",n=e&&e.implicit?e.implicit:"hide",i=this.args,a=r(this,t,n,i,!1);if(1===i.length){var o=Mp(this,t),u=i[0].toHTML(e);return a[0]&&(u='('+u+')'),"right"===o?''+Kr(this.op)+""+u:u+''+Kr(this.op)+""}if(2===i.length){var s=i[0].toHTML(e),c=i[1].toHTML(e);return a[0]&&(s='('+s+')'),a[1]&&(c='('+c+')'),this.implicit&&"OperatorNode:multiply"===this.getIdentifier()&&"hide"===n?s+''+c:s+''+Kr(this.op)+""+c}var f=i.map((function(t,r){return t=t.toHTML(e),a[r]&&(t='('+t+')'),t}));return i.length>2&&("OperatorNode:add"===this.getIdentifier()||"OperatorNode:multiply"===this.getIdentifier())?this.implicit&&"OperatorNode:multiply"===this.getIdentifier()&&"hide"===n?f.join(''):f.join(''+Kr(this.op)+""):''+Kr(this.fn)+'('+f.join(',')+')'}},{key:"_toTex",value:function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",n=e&&e.implicit?e.implicit:"hide",i=this.args,a=r(this,t,n,i,!0),o=qp[this.fn];if(o=void 0===o?this.op:o,1===i.length){var u=Mp(this,t),s=i[0].toTex(e);return a[0]&&(s="\\left(".concat(s,"\\right)")),"right"===u?o+s:s+o}if(2===i.length){var c=i[0],f=c.toTex(e);a[0]&&(f="\\left(".concat(f,"\\right)"));var l,p=i[1].toTex(e);switch(a[1]&&(p="\\left(".concat(p,"\\right)")),l="keep"===t?c.getIdentifier():c.getContent().getIdentifier(),this.getIdentifier()){case"OperatorNode:divide":return o+"{"+f+"}{"+p+"}";case"OperatorNode:pow":switch(f="{"+f+"}",p="{"+p+"}",l){case"ConditionalNode":case"OperatorNode:divide":f="\\left(".concat(f,"\\right)")}break;case"OperatorNode:multiply":if(this.implicit&&"hide"===n)return f+"~"+p}return f+o+p}if(i.length>2&&("OperatorNode:add"===this.getIdentifier()||"OperatorNode:multiply"===this.getIdentifier())){var m=i.map((function(t,r){return t=t.toTex(e),a[r]&&(t="\\left(".concat(t,"\\right)")),t}));return"OperatorNode:multiply"===this.getIdentifier()&&this.implicit&&"hide"===n?m.join("~"):m.join(o)}return"\\mathrm{"+this.fn+"}\\left("+i.map((function(t){return t.toTex(e)})).join(",")+"\\right)"}},{key:"getIdentifier",value:function(){return this.type+":"+this.fn}}],[{key:"fromJSON",value:function(e){return new a(e.op,e.fn,e.args,e.implicit,e.isPercentage)}}]),a}(e.Node);return Ua(n,"name",Qp),n}),{isClass:!0,isNode:!0});var em="ParenthesisNode",tm=Ee(em,["Node"],(function(e){var t=function(e){dp(i,e);var t,r,n=(t=i,r=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=yp(t);if(r){var i=yp(this).constructor;e=Reflect.construct(n,arguments,i)}else e=n.apply(this,arguments);return vp(this,e)});function i(e){var t;if(Ce(this,i),t=n.call(this),!R(e))throw new TypeError('Node expected for parameter "content"');return t.content=e,t}return Oe(i,[{key:"type",get:function(){return em}},{key:"isParenthesisNode",get:function(){return!0}},{key:"_compile",value:function(e,t){return this.content._compile(e,t)}},{key:"getContent",value:function(){return this.content.getContent()}},{key:"forEach",value:function(e){e(this.content,"content",this)}},{key:"map",value:function(e){return new i(e(this.content,"content",this))}},{key:"clone",value:function(){return new i(this.content)}},{key:"_toString",value:function(e){return!e||e&&!e.parenthesis||e&&"keep"===e.parenthesis?"("+this.content.toString(e)+")":this.content.toString(e)}},{key:"toJSON",value:function(){return{mathjs:em,content:this.content}}},{key:"toHTML",value:function(e){return!e||e&&!e.parenthesis||e&&"keep"===e.parenthesis?'('+this.content.toHTML(e)+')':this.content.toHTML(e)}},{key:"_toTex",value:function(e){return!e||e&&!e.parenthesis||e&&"keep"===e.parenthesis?"\\left(".concat(this.content.toTex(e),"\\right)"):this.content.toTex(e)}}],[{key:"fromJSON",value:function(e){return new i(e.content)}}]),i}(e.Node);return Ua(t,"name",em),t}),{isClass:!0,isNode:!0});var rm="RangeNode",nm=Ee(rm,["Node"],(function(e){function t(e,t,r){var n=Cp(e,t,r),i={},a=Cp(e.start,t,r);if(i.start=null!==a&&a<=n||"all"===t,e.step){var o=Cp(e.step,t,r);i.step=null!==o&&o<=n||"all"===t}var u=Cp(e.end,t,r);return i.end=null!==u&&u<=n||"all"===t,i}var r=function(e){dp(a,e);var r,n,i=(r=a,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,t=yp(r);if(n){var i=yp(this).constructor;e=Reflect.construct(t,arguments,i)}else e=t.apply(this,arguments);return vp(this,e)});function a(e,t,r){var n;if(Ce(this,a),n=i.call(this),!R(e))throw new TypeError("Node expected");if(!R(t))throw new TypeError("Node expected");if(r&&!R(r))throw new TypeError("Node expected");if(arguments.length>3)throw new Error("Too many arguments");return n.start=e,n.end=t,n.step=r||null,n}return Oe(a,[{key:"type",get:function(){return rm}},{key:"isRangeNode",get:function(){return!0}},{key:"needsEnd",value:function(){return this.filter((function(e){return U(e)&&"end"===e.name})).length>0}},{key:"_compile",value:function(e,t){var r=e.range,n=this.start._compile(e,t),i=this.end._compile(e,t);if(this.step){var a=this.step._compile(e,t);return function(e,t,o){return r(n(e,t,o),i(e,t,o),a(e,t,o))}}return function(e,t,a){return r(n(e,t,a),i(e,t,a))}}},{key:"forEach",value:function(e){e(this.start,"start",this),e(this.end,"end",this),this.step&&e(this.step,"step",this)}},{key:"map",value:function(e){return new a(this._ifNode(e(this.start,"start",this)),this._ifNode(e(this.end,"end",this)),this.step&&this._ifNode(e(this.step,"step",this)))}},{key:"clone",value:function(){return new a(this.start,this.end,this.step&&this.step)}},{key:"_toString",value:function(e){var r,n=t(this,e&&e.parenthesis?e.parenthesis:"keep",e&&e.implicit),i=this.start.toString(e);if(n.start&&(i="("+i+")"),r=i,this.step){var a=this.step.toString(e);n.step&&(a="("+a+")"),r+=":"+a}var o=this.end.toString(e);return n.end&&(o="("+o+")"),r+":"+o}},{key:"toJSON",value:function(){return{mathjs:rm,start:this.start,end:this.end,step:this.step}}},{key:"toHTML",value:function(e){var r,n=t(this,e&&e.parenthesis?e.parenthesis:"keep",e&&e.implicit),i=this.start.toHTML(e);if(n.start&&(i='('+i+')'),r=i,this.step){var a=this.step.toHTML(e);n.step&&(a='('+a+')'),r+=':'+a}var o=this.end.toHTML(e);return n.end&&(o='('+o+')'),r+':'+o}},{key:"_toTex",value:function(e){var r=t(this,e&&e.parenthesis?e.parenthesis:"keep",e&&e.implicit),n=this.start.toTex(e);if(r.start&&(n="\\left(".concat(n,"\\right)")),this.step){var i=this.step.toTex(e);r.step&&(i="\\left(".concat(i,"\\right)")),n+=":"+i}var a=this.end.toTex(e);return r.end&&(a="\\left(".concat(a,"\\right)")),n+":"+a}}],[{key:"fromJSON",value:function(e){return new a(e.start,e.end,e.step)}}]),a}(e.Node);return Ua(r,"name",rm),r}),{isClass:!0,isNode:!0});var im="RelationalNode",am=Ee(im,["Node"],(function(e){var t=e.Node,r={equal:"==",unequal:"!=",smaller:"<",larger:">",smallerEq:"<=",largerEq:">="},n=function(e){dp(a,e);var t,n,i=(t=a,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=yp(t);if(n){var i=yp(this).constructor;e=Reflect.construct(r,arguments,i)}else e=r.apply(this,arguments);return vp(this,e)});function a(e,t){var r;if(Ce(this,a),r=i.call(this),!Array.isArray(e))throw new TypeError("Parameter conditionals must be an array");if(!Array.isArray(t))throw new TypeError("Parameter params must be an array");if(e.length!==t.length-1)throw new TypeError("Parameter params must contain exactly one more element than parameter conditionals");return r.conditionals=e,r.params=t,r}return Oe(a,[{key:"type",get:function(){return im}},{key:"isRelationalNode",get:function(){return!0}},{key:"_compile",value:function(e,t){var r=this,n=this.params.map((function(r){return r._compile(e,t)}));return function(t,i,a){for(var o,u=n[0](t,i,a),s=0;s('+r.toHTML(e)+')':r.toHTML(e)})),a=i[0],o=0;o'+Kr(r[this.conditionals[o]])+""+i[o+1];return a}},{key:"_toTex",value:function(e){for(var t=e&&e.parenthesis?e.parenthesis:"keep",r=Cp(this,t,e&&e.implicit),n=this.params.map((function(n,i){var a=Cp(n,t,e&&e.implicit);return"all"===t||null!==a&&a<=r?"\\left("+n.toTex(e)+"\right)":n.toTex(e)})),i=n[0],a=0;a'+t+"":"i"===t?''+t+"":"Infinity"===t?''+t+"":"NaN"===t?''+t+"":"null"===t?''+t+"":"undefined"===t?''+t+"":''+t+""}},{key:"toJSON",value:function(){return{mathjs:"SymbolNode",name:this.name}}},{key:"_toTex",value:function(e){var r=!1;void 0===t[this.name]&&n(this.name)&&(r=!0);var i=Up(this.name,r);return"\\"===i[0]?i:" "+i}}],[{key:"onUndefinedSymbol",value:function(e){throw new Error("Undefined symbol "+e)}},{key:"fromJSON",value:function(e){return new u(e.name)}}]),u}(e.Node);return i}),{isClass:!0,isNode:!0});function um(){return um="undefined"!=typeof Reflect&&Reflect.get?Reflect.get.bind():function(e,t,r){var n=function(e,t){for(;!Object.prototype.hasOwnProperty.call(e,t)&&null!==(e=yp(e)););return e}(e,t);if(n){var i=Object.getOwnPropertyDescriptor(n,t);return i.get?i.get.call(arguments.length<3?e:r):i.value}},um.apply(this,arguments)}function sm(e){for(var t=arguments.length,r=new Array(t>1?t-1:0),n=1;n'+Kr(this.fn)+'('+t.join(',')+')'}},{key:"toTex",value:function(e){var r;return e&&"object"===t(e.handler)&&Ne(e.handler,this.name)&&(r=e.handler[this.name](this,e)),void 0!==r?r:um(yp(c.prototype),"toTex",this).call(this,e)}},{key:"_toTex",value:function(e){var r,i,a=this.args.map((function(t){return t.toTex(e)}));switch(jp[this.name]&&(r=jp[this.name]),!n[this.name]||"function"!=typeof n[this.name].toTex&&"object"!==t(n[this.name].toTex)&&"string"!=typeof n[this.name].toTex||(r=n[this.name].toTex),t(r)){case"function":i=r(this,e);break;case"string":i=u(r,this,e);break;case"object":switch(t(r[a.length])){case"function":i=r[a.length](this,e);break;case"string":i=u(r[a.length],this,e)}}return void 0!==i?i:u("\\mathrm{${name}}\\left(${args}\\right)",this,e)}},{key:"getIdentifier",value:function(){return this.type+":"+this.name}}]),c}(i);return r=s,Ua(s,"name",cm),Ua(s,"onUndefinedFunction",(function(e){throw new Error("Undefined function "+e)})),Ua(s,"fromJSON",(function(e){return new r(e.fn,e.args)})),s}),{isClass:!0,isNode:!0}),lm="parse",pm=Ee(lm,["typed","numeric","config","AccessorNode","ArrayNode","AssignmentNode","BlockNode","ConditionalNode","ConstantNode","FunctionAssignmentNode","FunctionNode","IndexNode","ObjectNode","OperatorNode","ParenthesisNode","RangeNode","RelationalNode","SymbolNode"],(function(e){var t=e.typed,r=e.numeric,n=e.config,i=e.AccessorNode,a=e.ArrayNode,o=e.AssignmentNode,u=e.BlockNode,s=e.ConditionalNode,c=e.ConstantNode,f=e.FunctionAssignmentNode,l=e.FunctionNode,p=e.IndexNode,m=e.ObjectNode,h=e.OperatorNode,d=e.ParenthesisNode,v=e.RangeNode,y=e.RelationalNode,g=e.SymbolNode,x=t(lm,{string:function(e){return L(e,{})},"Array | Matrix":function(e){return b(e,{})},"string, Object":function(e,t){return L(e,void 0!==t.nodes?t.nodes:{})},"Array | Matrix, Object":b});function b(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=void 0!==t.nodes?t.nodes:{};return Hn(e,(function(e){if("string"!=typeof e)throw new TypeError("String expected");return L(e,r)}))}var w={NULL:0,DELIMITER:1,NUMBER:2,SYMBOL:3,UNKNOWN:4},N={",":!0,"(":!0,")":!0,"[":!0,"]":!0,"{":!0,"}":!0,'"':!0,"'":!0,";":!0,"+":!0,"-":!0,"*":!0,".*":!0,"/":!0,"./":!0,"%":!0,"^":!0,".^":!0,"~":!0,"!":!0,"&":!0,"|":!0,"^|":!0,"=":!0,":":!0,"?":!0,"==":!0,"!=":!0,"<":!0,">":!0,"<=":!0,">=":!0,"<<":!0,">>":!0,">>>":!0},D={mod:!0,to:!0,in:!0,and:!0,xor:!0,or:!0,not:!0},E={true:!0,false:!1,null:null,undefined:void 0},A=["NaN","Infinity"],C={'"':'"',"'":"'","\\":"\\","/":"/",b:"\b",f:"\f",n:"\n",r:"\r",t:"\t"};function M(e,t){return e.expression.substr(e.index,t)}function F(e){return M(e,1)}function O(e){e.index++}function _(e){return e.expression.charAt(e.index-1)}function I(e){return e.expression.charAt(e.index+1)}function R(e){for(e.tokenType=w.NULL,e.token="",e.comment="";;){if("#"===F(e))for(;"\n"!==F(e)&&""!==F(e);)e.comment+=F(e),O(e);if(!x.isWhitespace(F(e),e.nestingLevel))break;O(e)}if(""!==F(e)){if("\n"===F(e)&&!e.nestingLevel)return e.tokenType=w.DELIMITER,e.token=F(e),void O(e);var t=F(e),r=M(e,2),n=M(e,3);if(3===n.length&&N[n])return e.tokenType=w.DELIMITER,e.token=n,O(e),O(e),void O(e);if(2===r.length&&N[r])return e.tokenType=w.DELIMITER,e.token=r,O(e),void O(e);if(N[t])return e.tokenType=w.DELIMITER,e.token=t,void O(e);if(x.isDigitDot(t)){e.tokenType=w.NUMBER;var i=M(e,2);if("0b"===i||"0o"===i||"0x"===i){for(e.token+=F(e),O(e),e.token+=F(e),O(e);x.isHexDigit(F(e));)e.token+=F(e),O(e);if("."===F(e))for(e.token+=".",O(e);x.isHexDigit(F(e));)e.token+=F(e),O(e);else if("i"===F(e))for(e.token+="i",O(e);x.isDigit(F(e));)e.token+=F(e),O(e);return}if("."===F(e)){if(e.token+=F(e),O(e),!x.isDigit(F(e)))return void(e.tokenType=w.DELIMITER)}else{for(;x.isDigit(F(e));)e.token+=F(e),O(e);x.isDecimalMark(F(e),I(e))&&(e.token+=F(e),O(e))}for(;x.isDigit(F(e));)e.token+=F(e),O(e);if("E"===F(e)||"e"===F(e))if(x.isDigit(I(e))||"-"===I(e)||"+"===I(e)){if(e.token+=F(e),O(e),"+"!==F(e)&&"-"!==F(e)||(e.token+=F(e),O(e)),!x.isDigit(F(e)))throw ce(e,'Digit expected, got "'+F(e)+'"');for(;x.isDigit(F(e));)e.token+=F(e),O(e);if(x.isDecimalMark(F(e),I(e)))throw ce(e,'Digit expected, got "'+F(e)+'"')}else if("."===I(e))throw O(e),ce(e,'Digit expected, got "'+F(e)+'"')}else{if(!x.isAlpha(F(e),_(e),I(e))){for(e.tokenType=w.UNKNOWN;""!==F(e);)e.token+=F(e),O(e);throw ce(e,'Syntax error in part "'+e.token+'"')}for(;x.isAlpha(F(e),_(e),I(e))||x.isDigit(F(e));)e.token+=F(e),O(e);Ne(D,e.token)?e.tokenType=w.DELIMITER:e.tokenType=w.SYMBOL}}else e.tokenType=w.DELIMITER}function z(e){do{R(e)}while("\n"===e.token)}function j(e){e.nestingLevel++}function P(e){e.nestingLevel--}function L(e,t){var r={extraNodes:{},expression:"",comment:"",index:0,token:"",tokenType:w.NULL,nestingLevel:0,conditionalLevel:null};$r(r,{expression:e,extraNodes:t}),R(r);var n=function(e){var t,r,n=[];for(""!==e.token&&"\n"!==e.token&&";"!==e.token&&(t=$(e),e.comment&&(t.comment=e.comment));"\n"===e.token||";"===e.token;)0===n.length&&t&&(r=";"!==e.token,n.push({node:t,visible:r})),R(e),"\n"!==e.token&&";"!==e.token&&""!==e.token&&(t=$(e),e.comment&&(t.comment=e.comment),r=";"!==e.token,n.push({node:t,visible:r}));return n.length>0?new u(n):(t||(t=new c(void 0),e.comment&&(t.comment=e.comment)),t)}(r);if(""!==r.token)throw r.tokenType===w.DELIMITER?fe(r,"Unexpected operator "+r.token):ce(r,'Unexpected part "'+r.token+'"');return n}function $(e){var t,r,n,i,a=function(e){for(var t=function(e){for(var t=H(e);"or"===e.token;)z(e),t=new h("or","or",[t,H(e)]);return t}(e);"?"===e.token;){var r=e.conditionalLevel;e.conditionalLevel=e.nestingLevel,z(e);var n=t,i=$(e);if(":"!==e.token)throw ce(e,"False part of conditional expression expected");e.conditionalLevel=null,z(e);var a=$(e);t=new s(n,i,a),e.conditionalLevel=r}return t}(e);if("="===e.token){if(U(a))return t=a.name,z(e),n=$(e),new o(new g(t),n);if(S(a))return z(e),n=$(e),new o(a.object,a.index,n);if(k(a)&&U(a.fn)&&(i=!0,r=[],t=a.name,a.args.forEach((function(e,t){U(e)?r[t]=e.name:i=!1})),i))return z(e),n=$(e),new f(t,r,n);throw ce(e,"Invalid left hand side of assignment operator =")}return a}function H(e){for(var t=G(e);"xor"===e.token;)z(e),t=new h("xor","xor",[t,G(e)]);return t}function G(e){for(var t=V(e);"and"===e.token;)z(e),t=new h("and","and",[t,V(e)]);return t}function V(e){for(var t=Z(e);"|"===e.token;)z(e),t=new h("|","bitOr",[t,Z(e)]);return t}function Z(e){for(var t=W(e);"^|"===e.token;)z(e),t=new h("^|","bitXor",[t,W(e)]);return t}function W(e){for(var t=Y(e);"&"===e.token;)z(e),t=new h("&","bitAnd",[t,Y(e)]);return t}function Y(e){for(var t=[J(e)],r=[],n={"==":"equal","!=":"unequal","<":"smaller",">":"larger","<=":"smallerEq",">=":"largerEq"};Ne(n,e.token);){var i={name:e.token,fn:n[e.token]};r.push(i),z(e),t.push(J(e))}return 1===t.length?t[0]:2===t.length?new h(r[0].name,r[0].fn,t):new y(r.map((function(e){return e.fn})),t)}function J(e){var t,r,n,i;t=X(e);for(var a={"<<":"leftShift",">>":"rightArithShift",">>>":"rightLogShift"};Ne(a,e.token);)n=a[r=e.token],z(e),i=[t,X(e)],t=new h(r,n,i);return t}function X(e){var t,r,n,i;t=Q(e);for(var a={to:"to",in:"to"};Ne(a,e.token);)n=a[r=e.token],z(e),"in"===r&&""===e.token?t=new h("*","multiply",[t,new g("in")],!0):(i=[t,Q(e)],t=new h(r,n,i));return t}function Q(e){var t,r=[];if(t=":"===e.token?new c(1):K(e),":"===e.token&&e.conditionalLevel!==e.nestingLevel){for(r.push(t);":"===e.token&&r.length<3;)z(e),")"===e.token||"]"===e.token||","===e.token||""===e.token?r.push(new g("end")):r.push(K(e));t=3===r.length?new v(r[0],r[2],r[1]):new v(r[0],r[1])}return t}function K(e){var t,r,n,i;t=ee(e);for(var a={"+":"add","-":"subtract"};Ne(a,e.token);){n=a[r=e.token],z(e);var o=ee(e);i=o.isPercentage?[t,new h("*","multiply",[t,o])]:[t,o],t=new h(r,n,i)}return t}function ee(e){var t,r,n,i;r=t=te(e);for(var a={"*":"multiply",".*":"dotMultiply","/":"divide","./":"dotDivide"};Ne(a,e.token);)i=a[n=e.token],z(e),r=te(e),t=new h(n,i,[t,r]);return t}function te(e){var t,r;for(r=t=re(e);e.tokenType===w.SYMBOL||"in"===e.token&&T(t)||!(e.tokenType!==w.NUMBER||T(r)||q(r)&&"!"!==r.op)||"("===e.token;)r=re(e),t=new h("*","multiply",[t,r],!0);return t}function re(e){for(var t=ne(e),r=t,n=[];"/"===e.token&&B(r);){if(n.push($r({},e)),z(e),e.tokenType!==w.NUMBER){$r(e,n.pop());break}if(n.push($r({},e)),z(e),e.tokenType!==w.SYMBOL&&"("!==e.token){n.pop(),$r(e,n.pop());break}$r(e,n.pop()),n.pop(),r=ne(e),t=new h("/","divide",[t,r])}return t}function ne(e){var t,r,n,i;t=ie(e);for(var a={"%":"mod",mod:"mod"};Ne(a,e.token);)n=a[r=e.token],z(e),"%"===r&&e.tokenType===w.DELIMITER&&"("!==e.token?t=new h("/","divide",[t,new c(100)],!1,!0):(i=[t,ie(e)],t=new h(r,n,i));return t}function ie(e){var t,i,o,u={"-":"unaryMinus","+":"unaryPlus","~":"bitNot",not:"not"};return Ne(u,e.token)?(o=u[e.token],t=e.token,z(e),i=[ie(e)],new h(t,o,i)):function(e){var t,i,o,u;return t=function(e){var t,i,o;t=function(e){var t=[];if(e.tokenType===w.SYMBOL&&Ne(e.extraNodes,e.token)){var i=e.extraNodes[e.token];if(R(e),"("===e.token){if(t=[],j(e),R(e),")"!==e.token)for(t.push($(e));","===e.token;)R(e),t.push($(e));if(")"!==e.token)throw ce(e,"Parenthesis ) expected");P(e),R(e)}return new i(t)}return function(e){var t;return e.tokenType===w.SYMBOL||e.tokenType===w.DELIMITER&&e.token in D?(t=e.token,R(e),ae(e,Ne(E,t)?new c(E[t]):-1!==A.indexOf(t)?new c(r(t,"number")):new g(t))):function(e){var t;return'"'===e.token||"'"===e.token?(t=oe(e,e.token),ae(e,new c(t))):function(e){var t,i,o,u;if("["===e.token){if(j(e),R(e),"]"!==e.token){var s=ue(e);if(";"===e.token){for(o=1,i=[s];";"===e.token;)R(e),i[o]=ue(e),o++;if("]"!==e.token)throw ce(e,"End of matrix ] expected");P(e),R(e),u=i[0].items.length;for(var f=1;f0},x.isDecimalMark=function(e,t){return"."===e&&"/"!==t&&"*"!==t&&"^"!==t},x.isDigitDot=function(e){return e>="0"&&e<="9"||"."===e},x.isDigit=function(e){return e>="0"&&e<="9"},x.isHexDigit=function(e){return e>="0"&&e<="9"||e>="a"&&e<="f"||e>="A"&&e<="F"},t.addConversion({from:"string",to:"Node",convert:x}),x})),mm="compile",hm=Ee(mm,["typed","parse"],(function(e){var t=e.typed,r=e.parse;return t(mm,{string:function(e){return r(e).compile()},"Array | Matrix":function(e){return Hn(e,(function(e){return r(e).compile()}))}})})),dm="evaluate",vm=Ee(dm,["typed","parse"],(function(e){var t=e.typed,r=e.parse;return t(dm,{string:function(e){var t=Le();return r(e).compile().evaluate(t)},"string, Map | Object":function(e,t){return r(e).compile().evaluate(t)},"Array | Matrix":function(e){var t=Le();return Hn(e,(function(e){return r(e).compile().evaluate(t)}))},"Array | Matrix, Map | Object":function(e,t){return Hn(e,(function(e){return r(e).compile().evaluate(t)}))}})})),ym=Ee("Parser",["evaluate"],(function(e){var t=e.evaluate;function r(){if(!(this instanceof r))throw new SyntaxError("Constructor must be called with the new operator");Object.defineProperty(this,"scope",{value:Le(),writable:!1})}return r.prototype.type="Parser",r.prototype.isParser=!0,r.prototype.evaluate=function(e){return t(e,this.scope)},r.prototype.get=function(e){if(this.scope.has(e))return this.scope.get(e)},r.prototype.getAll=function(){return function(e){if(e instanceof Pe)return e.wrappedObject;var t,r={},n=qe(e.keys());try{for(n.s();!(t=n.n()).done;){var i=t.value;Be(r,i,e.get(i))}}catch(e){n.e(e)}finally{n.f()}return r}(this.scope)},r.prototype.getAllAsMap=function(){return this.scope},r.prototype.set=function(e,t){return this.scope.set(e,t),t},r.prototype.remove=function(e){this.scope.delete(e)},r.prototype.clear=function(){this.scope.clear()},r}),{isClass:!0}),gm="parser",xm=Ee(gm,["typed","Parser"],(function(e){var t=e.typed,r=e.Parser;return t(gm,{"":function(){return new r}})})),bm=Ee("lup",["typed","matrix","abs","addScalar","divideScalar","multiplyScalar","subtractScalar","larger","equalScalar","unaryMinus","DenseMatrix","SparseMatrix","Spa"],(function(e){var t=e.typed,r=e.matrix,n=e.abs,i=e.addScalar,a=e.divideScalar,o=e.multiplyScalar,u=e.subtractScalar,s=e.larger,c=e.equalScalar,f=e.unaryMinus,l=e.DenseMatrix,p=e.SparseMatrix,m=e.Spa;return t("lup",{DenseMatrix:function(e){return h(e)},SparseMatrix:function(e){return function(e){var t,r,i,u=e._size[0],l=e._size[1],h=Math.min(u,l),d=e._values,v=e._index,y=e._ptr,g=[],x=[],b=[],w=[u,h],N=[],D=[],E=[],A=[h,l],S=[],C=[];for(t=0;t0&&e.forEach(0,r-1,(function(t,r){p._forEachRow(t,g,x,b,(function(n,i){n>t&&e.accumulate(n,f(o(i,r)))}))}));var M,F,O,T,B=r,_=e.get(r),k=n(_);e.forEach(r+1,u-1,(function(e,t){var r=n(t);s(r,k)&&(B=e,k=r,_=t)})),r!==B&&(p._swapRows(r,B,w[1],g,x,b),p._swapRows(r,B,A[1],N,D,E),e.swap(r,B),F=B,O=C[M=r],T=C[F],S[O]=F,S[T]=M,C[M]=T,C[F]=O),e.forEach(0,u-1,(function(e,t){e<=r?(N.push(t),D.push(e)):(t=a(t,_),c(t,0)||(g.push(t),x.push(e)))}))};for(r=0;r0)for(t=0;t0)for(var n="Complex"===r[0][0].type?d(0):0,i=0;i=0;){var s=r[o+u],c=r[n+s];-1===c?(u--,a[t++]=s):(r[n+s]=r[i+c],r[o+ ++u]=c)}return t}function Dm(e){return-e-2}var Em=Ee("csAmd",["add","multiply","transpose"],(function(e){var t=e.add,r=e.multiply,n=e.transpose;return function(e,o){if(!o||e<=0||e>3)return null;var u=o._size,s=u[0],c=u[1],f=0,l=Math.max(16,10*Math.sqrt(c)),p=function(e,i,a,o,u){var s=n(i);if(1===e&&o===a)return t(i,s);if(2===e){for(var c=s._index,f=s._ptr,l=0,p=0;pu))for(var h=f[p+1];mo)r[u+p]=0,r[i+p]=-1,l++,t[p]=Dm(e),r[u+e]++;else{var h=r[s+m];-1!==h&&(c[h]=p),r[f+p]=r[s+m],r[s+m]=p}}return l}(c,O,_,q,z,j,l,k,R,L,I),H=0;$G?(g=d,x=W,b=_[0+d]-G):(x=O[g=F[W++]],b=_[0+g]),y=1;y<=b;y++)(w=_[k+(m=F[x++])])<=0||(Z+=w,_[k+m]=-w,F[J++]=m,-1!==_[I+m]&&(L[_[I+m]]=L[m]),-1!==L[m]?_[I+L[m]]=_[I+m]:_[R+_[q+m]]=_[I+m]);g!==d&&(O[g]=Dm(d),_[j+g]=0)}for(0!==G&&(T=J),_[q+d]=Z,O[d]=Y,_[0+d]=J-Y,_[z+d]=-2,U=i(U,f,_,j,c),N=Y;N=U?_[j+g]-=w:0!==_[j+g]&&(_[j+g]=_[q+g]+X)}for(N=Y;N0?(M+=Q,F[S++]=g,C+=g):(O[g]=Dm(d),_[j+g]=0)}_[z+m]=S-E+1;var K=S,ee=E+_[0+m];for(W=A+1;W=0))for(m=_[P+(C=L[m])],_[P+C]=-1;-1!==m&&-1!==_[I+m];m=_[I+m],U++){for(b=_[0+m],D=_[z+m],W=O[m]+1;W<=O[m]+b-1;W++)_[j+F[W]]=U;var re=m;for(h=_[I+m];-1!==h;){var ne=_[0+h]===b&&_[z+h]===D;for(W=O[h]+1;ne&&W<=O[h]+b-1;W++)_[j+F[W]]!==U&&(ne=0);ne?(O[h]=Dm(m),_[k+m]+=_[k+h],_[k+h]=0,_[z+h]=-1,h=_[I+h],_[I+re]=h):(re=h,h=_[I+h])}}for(W=Y,N=Y;N=0;h--)_[k+h]>0||(_[I+h]=_[R+O[h]],_[R+O[h]]=h);for(g=c;g>=0;g--)_[k+g]<=0||-1!==O[g]&&(_[I+g]=_[R+O[g]],_[R+O[g]]=g);for(d=0,m=0;m<=c;m++)-1===O[m]&&(d=Nm(m,d,_,R,I,B,j));return B.splice(B.length-1,1),B};function i(e,t,r,n,i){if(e<2||e+t<0){for(var a=0;a=1&&N[o]++,2===S.jleaf&&N[S.q]--}-1!==r[o]&&(v[0+o]=r[o])}for(o=0;o=0;r--)-1!==e[r]&&(a[o+r]=a[0+e[r]],a[0+e[r]]=r);for(r=0;r=0;s--)for(f=r[s],l=r[s+1],c=f;c=0;u--)m[u]=-1,-1!==(s=h[u])&&(0==d[g+s]++&&(d[y+s]=u),d[0+u]=d[v+s],d[v+s]=u);for(t.lnz=0,t.m2=a,s=0;s=0;){e=n[l];var p=i?i[e]:e;Mm(c,e)||(Fm(c,e),n[f+l]=p<0?0:Om(c[p]));var m=1;for(o=n[f+l],u=p<0?0:Om(c[p+1]);o3)throw new Error("Symbolic Ordering and Analysis order must be an integer number in the interval [0, 3]");if(r<0||r>1)throw new Error("Partial pivoting threshold must be a number from 0 to 1");var n=l(t,e,!1),i=p(e,n,r);return{L:i.L,U:i.U,p:i.pinv,q:n.q,toString:function(){return"L: "+this.L.toString()+"\nU: "+this.U.toString()+"\np: "+this.p.toString()+(this.q?"\nq: "+this.q.toString():"")+"\n"}}}})}));function Im(e,t){var r,n=t.length,i=[];if(e)for(r=0;r0&&r(h[h.length-1]);)h.pop();if(h.length<2)throw new RangeError("Polynomial [".concat(e,", ").concat(t,"] must have a non-zero non-constant coefficient"));switch(h.length){case 2:return[c(u(h[0],h[1]))];case 3:var d=wa(h,3),v=d[0],y=d[1],g=d[2],x=o(2,g),b=o(y,y),w=o(4,g,v);if(n(b,w))return[u(c(y),x)];var N=s(a(b,w));return[u(a(N,y),x),u(a(c(N),y),x)];case 4:var D=wa(h,4),E=D[0],A=D[1],S=D[2],C=D[3],M=c(o(3,C)),F=o(S,S),O=o(3,C,A),T=i(o(2,S,S,S),o(27,C,C,E)),B=o(9,C,S,A);if(n(F,O)&&n(T,B))return[u(S,M)];var _,k=a(F,O),I=a(T,B),R=i(o(18,C,S,A,E),o(S,S,A,A)),z=i(o(4,S,S,S,E),o(4,C,A,A,A),o(27,C,C,E,E));return n(R,z)?[u(a(o(4,C,S,A),i(o(9,C,C,E),o(S,S,S))),o(C,k)),u(a(o(9,C,E),o(S,A)),o(2,k))]:(_=n(F,O)?I:u(i(I,s(a(o(I,I),o(4,k,k,k)))),2),f(_,!0).toArray().map((function(e){return u(i(S,e,u(k,e)),M)})).map((function(e){return"Complex"===l(e)&&n(m(e),m(e)+p(e))?m(e):e})));default:throw new RangeError("only implemented for cubic or lower-order polynomials, not ".concat(h))}}})})),Pm=Ee("Help",["parse"],(function(e){var t=e.parse;function r(e){if(!(this instanceof r))throw new SyntaxError("Constructor must be called with the new operator");if(!e)throw new Error('Argument "doc" missing');this.doc=e}return r.prototype.type="Help",r.prototype.isHelp=!0,r.prototype.toString=function(){var e=this.doc||{},r="\n";if(e.name&&(r+="Name: "+e.name+"\n\n"),e.category&&(r+="Category: "+e.category+"\n\n"),e.description&&(r+="Description:\n "+e.description+"\n\n"),e.syntax&&(r+="Syntax:\n "+e.syntax.join("\n ")+"\n\n"),e.examples){r+="Examples:\n";for(var n={},i=0;i1 and B<3]"],seealso:["bignumber","boolean","complex","matrix,","number","range","string","unit"]},matrix:{name:"matrix",category:"Construction",syntax:["[]","[a1, b1, ...; a2, b2, ...]","matrix()",'matrix("dense")',"matrix([...])"],description:"Create a matrix.",examples:["[]","[1, 2, 3]","[1, 2, 3; 4, 5, 6]","matrix()","matrix([3, 4])",'matrix([3, 4; 5, 6], "sparse")','matrix([3, 4; 5, 6], "sparse", "number")'],seealso:["bignumber","boolean","complex","index","number","string","unit","sparse"]},number:{name:"number",category:"Construction",syntax:["x","number(x)","number(unit, valuelessUnit)"],description:"Create a number or convert a string or boolean into a number.",examples:["2","2e3","4.05","number(2)",'number("7.2")',"number(true)","number([true, false, true, true])",'number(unit("52cm"), "m")'],seealso:["bignumber","boolean","complex","fraction","index","matrix","string","unit"]},sparse:{name:"sparse",category:"Construction",syntax:["sparse()","sparse([a1, b1, ...; a1, b2, ...])",'sparse([a1, b1, ...; a1, b2, ...], "number")'],description:"Create a sparse matrix.",examples:["sparse()","sparse([3, 4; 5, 6])",'sparse([3, 0; 5, 0], "number")'],seealso:["bignumber","boolean","complex","index","number","string","unit","matrix"]},splitUnit:{name:"splitUnit",category:"Construction",syntax:["splitUnit(unit: Unit, parts: Unit[])"],description:"Split a unit in an array of units whose sum is equal to the original unit.",examples:['splitUnit(1 m, ["feet", "inch"])'],seealso:["unit","createUnit"]},string:{name:"string",category:"Construction",syntax:['"text"',"string(x)"],description:"Create a string or convert a value to a string",examples:['"Hello World!"',"string(4.2)","string(3 + 2i)"],seealso:["bignumber","boolean","complex","index","matrix","number","unit"]},unit:{name:"unit",category:"Construction",syntax:["value unit","unit(value, unit)","unit(string)"],description:"Create a unit.",examples:["5.5 mm","3 inch",'unit(7.1, "kilogram")','unit("23 deg")'],seealso:["bignumber","boolean","complex","index","matrix","number","string"]},e:Um,E:Um,false:{name:"false",category:"Constants",syntax:["false"],description:"Boolean value false",examples:["false"],seealso:["true"]},i:{name:"i",category:"Constants",syntax:["i"],description:"Imaginary unit, defined as i*i=-1. A complex number is described as a + b*i, where a is the real part, and b is the imaginary part.",examples:["i","i * i","sqrt(-1)"],seealso:[]},Infinity:{name:"Infinity",category:"Constants",syntax:["Infinity"],description:"Infinity, a number which is larger than the maximum number that can be handled by a floating point number.",examples:["Infinity","1 / 0"],seealso:[]},LN2:{name:"LN2",category:"Constants",syntax:["LN2"],description:"Returns the natural logarithm of 2, approximately equal to 0.693",examples:["LN2","log(2)"],seealso:[]},LN10:{name:"LN10",category:"Constants",syntax:["LN10"],description:"Returns the natural logarithm of 10, approximately equal to 2.302",examples:["LN10","log(10)"],seealso:[]},LOG2E:{name:"LOG2E",category:"Constants",syntax:["LOG2E"],description:"Returns the base-2 logarithm of E, approximately equal to 1.442",examples:["LOG2E","log(e, 2)"],seealso:[]},LOG10E:{name:"LOG10E",category:"Constants",syntax:["LOG10E"],description:"Returns the base-10 logarithm of E, approximately equal to 0.434",examples:["LOG10E","log(e, 10)"],seealso:[]},NaN:{name:"NaN",category:"Constants",syntax:["NaN"],description:"Not a number",examples:["NaN","0 / 0"],seealso:[]},null:{name:"null",category:"Constants",syntax:["null"],description:"Value null",examples:["null"],seealso:["true","false"]},pi:$m,PI:$m,phi:{name:"phi",category:"Constants",syntax:["phi"],description:"Phi is the golden ratio. Two quantities are in the golden ratio if their ratio is the same as the ratio of their sum to the larger of the two quantities. Phi is defined as `(1 + sqrt(5)) / 2` and is approximately 1.618034...",examples:["phi"],seealso:[]},SQRT1_2:{name:"SQRT1_2",category:"Constants",syntax:["SQRT1_2"],description:"Returns the square root of 1/2, approximately equal to 0.707",examples:["SQRT1_2","sqrt(1/2)"],seealso:[]},SQRT2:{name:"SQRT2",category:"Constants",syntax:["SQRT2"],description:"Returns the square root of 2, approximately equal to 1.414",examples:["SQRT2","sqrt(2)"],seealso:[]},tau:{name:"tau",category:"Constants",syntax:["tau"],description:"Tau is the ratio constant of a circle's circumference to radius, equal to 2 * pi, approximately 6.2832.",examples:["tau","2 * pi"],seealso:["pi"]},true:{name:"true",category:"Constants",syntax:["true"],description:"Boolean value true",examples:["true"],seealso:["false"]},version:{name:"version",category:"Constants",syntax:["version"],description:"A string with the version number of math.js",examples:["version"],seealso:[]},speedOfLight:{description:"Speed of light in vacuum",examples:["speedOfLight"]},gravitationConstant:{description:"Newtonian constant of gravitation",examples:["gravitationConstant"]},planckConstant:{description:"Planck constant",examples:["planckConstant"]},reducedPlanckConstant:{description:"Reduced Planck constant",examples:["reducedPlanckConstant"]},magneticConstant:{description:"Magnetic constant (vacuum permeability)",examples:["magneticConstant"]},electricConstant:{description:"Electric constant (vacuum permeability)",examples:["electricConstant"]},vacuumImpedance:{description:"Characteristic impedance of vacuum",examples:["vacuumImpedance"]},coulomb:{description:"Coulomb's constant",examples:["coulomb"]},elementaryCharge:{description:"Elementary charge",examples:["elementaryCharge"]},bohrMagneton:{description:"Borh magneton",examples:["bohrMagneton"]},conductanceQuantum:{description:"Conductance quantum",examples:["conductanceQuantum"]},inverseConductanceQuantum:{description:"Inverse conductance quantum",examples:["inverseConductanceQuantum"]},magneticFluxQuantum:{description:"Magnetic flux quantum",examples:["magneticFluxQuantum"]},nuclearMagneton:{description:"Nuclear magneton",examples:["nuclearMagneton"]},klitzing:{description:"Von Klitzing constant",examples:["klitzing"]},bohrRadius:{description:"Borh radius",examples:["bohrRadius"]},classicalElectronRadius:{description:"Classical electron radius",examples:["classicalElectronRadius"]},electronMass:{description:"Electron mass",examples:["electronMass"]},fermiCoupling:{description:"Fermi coupling constant",examples:["fermiCoupling"]},fineStructure:{description:"Fine-structure constant",examples:["fineStructure"]},hartreeEnergy:{description:"Hartree energy",examples:["hartreeEnergy"]},protonMass:{description:"Proton mass",examples:["protonMass"]},deuteronMass:{description:"Deuteron Mass",examples:["deuteronMass"]},neutronMass:{description:"Neutron mass",examples:["neutronMass"]},quantumOfCirculation:{description:"Quantum of circulation",examples:["quantumOfCirculation"]},rydberg:{description:"Rydberg constant",examples:["rydberg"]},thomsonCrossSection:{description:"Thomson cross section",examples:["thomsonCrossSection"]},weakMixingAngle:{description:"Weak mixing angle",examples:["weakMixingAngle"]},efimovFactor:{description:"Efimov factor",examples:["efimovFactor"]},atomicMass:{description:"Atomic mass constant",examples:["atomicMass"]},avogadro:{description:"Avogadro's number",examples:["avogadro"]},boltzmann:{description:"Boltzmann constant",examples:["boltzmann"]},faraday:{description:"Faraday constant",examples:["faraday"]},firstRadiation:{description:"First radiation constant",examples:["firstRadiation"]},loschmidt:{description:"Loschmidt constant at T=273.15 K and p=101.325 kPa",examples:["loschmidt"]},gasConstant:{description:"Gas constant",examples:["gasConstant"]},molarPlanckConstant:{description:"Molar Planck constant",examples:["molarPlanckConstant"]},molarVolume:{description:"Molar volume of an ideal gas at T=273.15 K and p=101.325 kPa",examples:["molarVolume"]},sackurTetrode:{description:"Sackur-Tetrode constant at T=1 K and p=101.325 kPa",examples:["sackurTetrode"]},secondRadiation:{description:"Second radiation constant",examples:["secondRadiation"]},stefanBoltzmann:{description:"Stefan-Boltzmann constant",examples:["stefanBoltzmann"]},wienDisplacement:{description:"Wien displacement law constant",examples:["wienDisplacement"]},molarMass:{description:"Molar mass constant",examples:["molarMass"]},molarMassC12:{description:"Molar mass constant of carbon-12",examples:["molarMassC12"]},gravity:{description:"Standard acceleration of gravity (standard acceleration of free-fall on Earth)",examples:["gravity"]},planckLength:{description:"Planck length",examples:["planckLength"]},planckMass:{description:"Planck mass",examples:["planckMass"]},planckTime:{description:"Planck time",examples:["planckTime"]},planckCharge:{description:"Planck charge",examples:["planckCharge"]},planckTemperature:{description:"Planck temperature",examples:["planckTemperature"]},derivative:{name:"derivative",category:"Algebra",syntax:["derivative(expr, variable)","derivative(expr, variable, {simplify: boolean})"],description:"Takes the derivative of an expression expressed in parser Nodes. The derivative will be taken over the supplied variable in the second parameter. If there are multiple variables in the expression, it will return a partial derivative.",examples:['derivative("2x^3", "x")','derivative("2x^3", "x", {simplify: false})','derivative("2x^2 + 3x + 4", "x")','derivative("sin(2x)", "x")','f = parse("x^2 + x")','x = parse("x")',"df = derivative(f, x)","df.evaluate({x: 3})"],seealso:["simplify","parse","evaluate"]},lsolve:{name:"lsolve",category:"Algebra",syntax:["x=lsolve(L, b)"],description:"Finds one solution of the linear system L * x = b where L is an [n x n] lower triangular matrix and b is a [n] column vector.",examples:["a = [-2, 3; 2, 1]","b = [11, 9]","x = lsolve(a, b)"],seealso:["lsolveAll","lup","lusolve","usolve","matrix","sparse"]},lsolveAll:{name:"lsolveAll",category:"Algebra",syntax:["x=lsolveAll(L, b)"],description:"Finds all solutions of the linear system L * x = b where L is an [n x n] lower triangular matrix and b is a [n] column vector.",examples:["a = [-2, 3; 2, 1]","b = [11, 9]","x = lsolve(a, b)"],seealso:["lsolve","lup","lusolve","usolve","matrix","sparse"]},lup:{name:"lup",category:"Algebra",syntax:["lup(m)"],description:"Calculate the Matrix LU decomposition with partial pivoting. Matrix A is decomposed in three matrices (L, U, P) where P * A = L * U",examples:["lup([[2, 1], [1, 4]])","lup(matrix([[2, 1], [1, 4]]))","lup(sparse([[2, 1], [1, 4]]))"],seealso:["lusolve","lsolve","usolve","matrix","sparse","slu","qr"]},lusolve:{name:"lusolve",category:"Algebra",syntax:["x=lusolve(A, b)","x=lusolve(lu, b)"],description:"Solves the linear system A * x = b where A is an [n x n] matrix and b is a [n] column vector.",examples:["a = [-2, 3; 2, 1]","b = [11, 9]","x = lusolve(a, b)"],seealso:["lup","slu","lsolve","usolve","matrix","sparse"]},leafCount:{name:"leafCount",category:"Algebra",syntax:["leafCount(expr)"],description:"Computes the number of leaves in the parse tree of the given expression",examples:['leafCount("e^(i*pi)-1")','leafCount(parse("{a: 22/7, b: 10^(1/2)}"))'],seealso:["simplify"]},polynomialRoot:{name:"polynomialRoot",category:"Algebra",syntax:["x=polynomialRoot(-6, 3)","x=polynomialRoot(4, -4, 1)","x=polynomialRoot(-8, 12, -6, 1)"],description:"Finds the roots of a univariate polynomial given by its coefficients starting from constant, linear, and so on, increasing in degree.",examples:["a = polynomialRoot(-6, 11, -6, 1)"],seealso:["cbrt","sqrt"]},resolve:{name:"resolve",category:"Algebra",syntax:["resolve(node, scope)"],description:"Recursively substitute variables in an expression tree.",examples:['resolve(parse("1 + x"), { x: 7 })','resolve(parse("size(text)"), { text: "Hello World" })','resolve(parse("x + y"), { x: parse("3z") })','resolve(parse("3x"), { x: parse("y+z"), z: parse("w^y") })'],seealso:["simplify","evaluate"],mayThrow:["ReferenceError"]},simplify:{name:"simplify",category:"Algebra",syntax:["simplify(expr)","simplify(expr, rules)"],description:"Simplify an expression tree.",examples:['simplify("3 + 2 / 4")','simplify("2x + x")','f = parse("x * (x + 2 + x)")',"simplified = simplify(f)","simplified.evaluate({x: 2})"],seealso:["simplifyCore","derivative","evaluate","parse","rationalize","resolve"]},simplifyConstant:{name:"simplifyConstant",category:"Algebra",syntax:["simplifyConstant(expr)","simplifyConstant(expr, options)"],description:"Replace constant subexpressions of node with their values.",examples:['simplifyConstant("(3-3)*x")','simplifyConstant(parse("z-cos(tau/8)"))'],seealso:["simplify","simplifyCore","evaluate"]},simplifyCore:{name:"simplifyCore",category:"Algebra",syntax:["simplifyCore(node)"],description:"Perform simple one-pass simplifications on an expression tree.",examples:['simplifyCore(parse("0*x"))','simplifyCore(parse("(x+0)*2"))'],seealso:["simplify","simplifyConstant","evaluate"]},symbolicEqual:{name:"symbolicEqual",category:"Algebra",syntax:["symbolicEqual(expr1, expr2)","symbolicEqual(expr1, expr2, options)"],description:"Returns true if the difference of the expressions simplifies to 0",examples:['symbolicEqual("x*y","y*x")','symbolicEqual("abs(x^2)", "x^2")','symbolicEqual("abs(x)", "x", {context: {abs: {trivial: true}}})'],seealso:["simplify","evaluate"]},rationalize:{name:"rationalize",category:"Algebra",syntax:["rationalize(expr)","rationalize(expr, scope)","rationalize(expr, scope, detailed)"],description:"Transform a rationalizable expression in a rational fraction. If rational fraction is one variable polynomial then converts the numerator and denominator in canonical form, with decreasing exponents, returning the coefficients of numerator.",examples:['rationalize("2x/y - y/(x+1)")','rationalize("2x/y - y/(x+1)", true)'],seealso:["simplify"]},slu:{name:"slu",category:"Algebra",syntax:["slu(A, order, threshold)"],description:"Calculate the Matrix LU decomposition with full pivoting. Matrix A is decomposed in two matrices (L, U) and two permutation vectors (pinv, q) where P * A * Q = L * U",examples:["slu(sparse([4.5, 0, 3.2, 0; 3.1, 2.9, 0, 0.9; 0, 1.7, 3, 0; 3.5, 0.4, 0, 1]), 1, 0.001)"],seealso:["lusolve","lsolve","usolve","matrix","sparse","lup","qr"]},usolve:{name:"usolve",category:"Algebra",syntax:["x=usolve(U, b)"],description:"Finds one solution of the linear system U * x = b where U is an [n x n] upper triangular matrix and b is a [n] column vector.",examples:["x=usolve(sparse([1, 1, 1, 1; 0, 1, 1, 1; 0, 0, 1, 1; 0, 0, 0, 1]), [1; 2; 3; 4])"],seealso:["usolveAll","lup","lusolve","lsolve","matrix","sparse"]},usolveAll:{name:"usolveAll",category:"Algebra",syntax:["x=usolve(U, b)"],description:"Finds all solutions of the linear system U * x = b where U is an [n x n] upper triangular matrix and b is a [n] column vector.",examples:["x=usolve(sparse([1, 1, 1, 1; 0, 1, 1, 1; 0, 0, 1, 1; 0, 0, 0, 1]), [1; 2; 3; 4])"],seealso:["usolve","lup","lusolve","lsolve","matrix","sparse"]},qr:{name:"qr",category:"Algebra",syntax:["qr(A)"],description:"Calculates the Matrix QR decomposition. Matrix `A` is decomposed in two matrices (`Q`, `R`) where `Q` is an orthogonal matrix and `R` is an upper triangular matrix.",examples:["qr([[1, -1, 4], [1, 4, -2], [1, 4, 2], [1, -1, 0]])"],seealso:["lup","slu","matrix"]},abs:{name:"abs",category:"Arithmetic",syntax:["abs(x)"],description:"Compute the absolute value.",examples:["abs(3.5)","abs(-4.2)"],seealso:["sign"]},add:{name:"add",category:"Operators",syntax:["x + y","add(x, y)"],description:"Add two values.",examples:["a = 2.1 + 3.6","a - 3.6","3 + 2i","3 cm + 2 inch",'"2.3" + "4"'],seealso:["subtract"]},cbrt:{name:"cbrt",category:"Arithmetic",syntax:["cbrt(x)","cbrt(x, allRoots)"],description:"Compute the cubic root value. If x = y * y * y, then y is the cubic root of x. When `x` is a number or complex number, an optional second argument `allRoots` can be provided to return all three cubic roots. If not provided, the principal root is returned",examples:["cbrt(64)","cube(4)","cbrt(-8)","cbrt(2 + 3i)","cbrt(8i)","cbrt(8i, true)","cbrt(27 m^3)"],seealso:["square","sqrt","cube","multiply"]},ceil:{name:"ceil",category:"Arithmetic",syntax:["ceil(x)"],description:"Round a value towards plus infinity. If x is complex, both real and imaginary part are rounded towards plus infinity.",examples:["ceil(3.2)","ceil(3.8)","ceil(-4.2)"],seealso:["floor","fix","round"]},cube:{name:"cube",category:"Arithmetic",syntax:["cube(x)"],description:"Compute the cube of a value. The cube of x is x * x * x.",examples:["cube(2)","2^3","2 * 2 * 2"],seealso:["multiply","square","pow"]},divide:{name:"divide",category:"Operators",syntax:["x / y","divide(x, y)"],description:"Divide two values.",examples:["a = 2 / 3","a * 3","4.5 / 2","3 + 4 / 2","(3 + 4) / 2","18 km / 4.5"],seealso:["multiply"]},dotDivide:{name:"dotDivide",category:"Operators",syntax:["x ./ y","dotDivide(x, y)"],description:"Divide two values element wise.",examples:["a = [1, 2, 3; 4, 5, 6]","b = [2, 1, 1; 3, 2, 5]","a ./ b"],seealso:["multiply","dotMultiply","divide"]},dotMultiply:{name:"dotMultiply",category:"Operators",syntax:["x .* y","dotMultiply(x, y)"],description:"Multiply two values element wise.",examples:["a = [1, 2, 3; 4, 5, 6]","b = [2, 1, 1; 3, 2, 5]","a .* b"],seealso:["multiply","divide","dotDivide"]},dotPow:{name:"dotPow",category:"Operators",syntax:["x .^ y","dotPow(x, y)"],description:"Calculates the power of x to y element wise.",examples:["a = [1, 2, 3; 4, 5, 6]","a .^ 2"],seealso:["pow"]},exp:{name:"exp",category:"Arithmetic",syntax:["exp(x)"],description:"Calculate the exponent of a value.",examples:["exp(1.3)","e ^ 1.3","log(exp(1.3))","x = 2.4","(exp(i*x) == cos(x) + i*sin(x)) # Euler's formula"],seealso:["expm","expm1","pow","log"]},expm:{name:"expm",category:"Arithmetic",syntax:["exp(x)"],description:"Compute the matrix exponential, expm(A) = e^A. The matrix must be square. Not to be confused with exp(a), which performs element-wise exponentiation.",examples:["expm([[0,2],[0,0]])"],seealso:["exp"]},expm1:{name:"expm1",category:"Arithmetic",syntax:["expm1(x)"],description:"Calculate the value of subtracting 1 from the exponential value.",examples:["expm1(2)","pow(e, 2) - 1","log(expm1(2) + 1)"],seealso:["exp","pow","log"]},fix:{name:"fix",category:"Arithmetic",syntax:["fix(x)"],description:"Round a value towards zero. If x is complex, both real and imaginary part are rounded towards zero.",examples:["fix(3.2)","fix(3.8)","fix(-4.2)","fix(-4.8)"],seealso:["ceil","floor","round"]},floor:{name:"floor",category:"Arithmetic",syntax:["floor(x)"],description:"Round a value towards minus infinity.If x is complex, both real and imaginary part are rounded towards minus infinity.",examples:["floor(3.2)","floor(3.8)","floor(-4.2)"],seealso:["ceil","fix","round"]},gcd:{name:"gcd",category:"Arithmetic",syntax:["gcd(a, b)","gcd(a, b, c, ...)"],description:"Compute the greatest common divisor.",examples:["gcd(8, 12)","gcd(-4, 6)","gcd(25, 15, -10)"],seealso:["lcm","xgcd"]},hypot:{name:"hypot",category:"Arithmetic",syntax:["hypot(a, b, c, ...)","hypot([a, b, c, ...])"],description:"Calculate the hypotenusa of a list with values. ",examples:["hypot(3, 4)","sqrt(3^2 + 4^2)","hypot(-2)","hypot([3, 4, 5])"],seealso:["abs","norm"]},lcm:{name:"lcm",category:"Arithmetic",syntax:["lcm(x, y)"],description:"Compute the least common multiple.",examples:["lcm(4, 6)","lcm(6, 21)","lcm(6, 21, 5)"],seealso:["gcd"]},log:{name:"log",category:"Arithmetic",syntax:["log(x)","log(x, base)"],description:"Compute the logarithm of a value. If no base is provided, the natural logarithm of x is calculated. If base if provided, the logarithm is calculated for the specified base. log(x, base) is defined as log(x) / log(base).",examples:["log(3.5)","a = log(2.4)","exp(a)","10 ^ 4","log(10000, 10)","log(10000) / log(10)","b = log(1024, 2)","2 ^ b"],seealso:["exp","log1p","log2","log10"]},log2:{name:"log2",category:"Arithmetic",syntax:["log2(x)"],description:"Calculate the 2-base of a value. This is the same as calculating `log(x, 2)`.",examples:["log2(0.03125)","log2(16)","log2(16) / log2(2)","pow(2, 4)"],seealso:["exp","log1p","log","log10"]},log1p:{name:"log1p",category:"Arithmetic",syntax:["log1p(x)","log1p(x, base)"],description:"Calculate the logarithm of a `value+1`",examples:["log1p(2.5)","exp(log1p(1.4))","pow(10, 4)","log1p(9999, 10)","log1p(9999) / log(10)"],seealso:["exp","log","log2","log10"]},log10:{name:"log10",category:"Arithmetic",syntax:["log10(x)"],description:"Compute the 10-base logarithm of a value.",examples:["log10(0.00001)","log10(10000)","10 ^ 4","log(10000) / log(10)","log(10000, 10)"],seealso:["exp","log"]},mod:{name:"mod",category:"Operators",syntax:["x % y","x mod y","mod(x, y)"],description:"Calculates the modulus, the remainder of an integer division.",examples:["7 % 3","11 % 2","10 mod 4","isOdd(x) = x % 2","isOdd(2)","isOdd(3)"],seealso:["divide"]},multiply:{name:"multiply",category:"Operators",syntax:["x * y","multiply(x, y)"],description:"multiply two values.",examples:["a = 2.1 * 3.4","a / 3.4","2 * 3 + 4","2 * (3 + 4)","3 * 2.1 km"],seealso:["divide"]},norm:{name:"norm",category:"Arithmetic",syntax:["norm(x)","norm(x, p)"],description:"Calculate the norm of a number, vector or matrix.",examples:["abs(-3.5)","norm(-3.5)","norm(3 - 4i)","norm([1, 2, -3], Infinity)","norm([1, 2, -3], -Infinity)","norm([3, 4], 2)","norm([[1, 2], [3, 4]], 1)",'norm([[1, 2], [3, 4]], "inf")','norm([[1, 2], [3, 4]], "fro")']},nthRoot:{name:"nthRoot",category:"Arithmetic",syntax:["nthRoot(a)","nthRoot(a, root)"],description:'Calculate the nth root of a value. The principal nth root of a positive real number A, is the positive real solution of the equation "x^root = A".',examples:["4 ^ 3","nthRoot(64, 3)","nthRoot(9, 2)","sqrt(9)"],seealso:["nthRoots","pow","sqrt"]},nthRoots:{name:"nthRoots",category:"Arithmetic",syntax:["nthRoots(A)","nthRoots(A, root)"],description:'Calculate the nth roots of a value. An nth root of a positive real number A, is a positive real solution of the equation "x^root = A". This function returns an array of complex values.',examples:["nthRoots(1)","nthRoots(1, 3)"],seealso:["sqrt","pow","nthRoot"]},pow:{name:"pow",category:"Operators",syntax:["x ^ y","pow(x, y)"],description:"Calculates the power of x to y, x^y.",examples:["2^3","2*2*2","1 + e ^ (pi * i)","pow([[1, 2], [4, 3]], 2)","pow([[1, 2], [4, 3]], -1)"],seealso:["multiply","nthRoot","nthRoots","sqrt"]},round:{name:"round",category:"Arithmetic",syntax:["round(x)","round(x, n)"],description:"round a value towards the nearest integer.If x is complex, both real and imaginary part are rounded towards the nearest integer. When n is specified, the value is rounded to n decimals.",examples:["round(3.2)","round(3.8)","round(-4.2)","round(-4.8)","round(pi, 3)","round(123.45678, 2)"],seealso:["ceil","floor","fix"]},sign:{name:"sign",category:"Arithmetic",syntax:["sign(x)"],description:"Compute the sign of a value. The sign of a value x is 1 when x>1, -1 when x<0, and 0 when x=0.",examples:["sign(3.5)","sign(-4.2)","sign(0)"],seealso:["abs"]},sqrt:{name:"sqrt",category:"Arithmetic",syntax:["sqrt(x)"],description:"Compute the square root value. If x = y * y, then y is the square root of x.",examples:["sqrt(25)","5 * 5","sqrt(-1)"],seealso:["square","sqrtm","multiply","nthRoot","nthRoots","pow"]},sqrtm:{name:"sqrtm",category:"Arithmetic",syntax:["sqrtm(x)"],description:"Calculate the principal square root of a square matrix. The principal square root matrix `X` of another matrix `A` is such that `X * X = A`.",examples:["sqrtm([[33, 24], [48, 57]])"],seealso:["sqrt","abs","square","multiply"]},square:{name:"square",category:"Arithmetic",syntax:["square(x)"],description:"Compute the square of a value. The square of x is x * x.",examples:["square(3)","sqrt(9)","3^2","3 * 3"],seealso:["multiply","pow","sqrt","cube"]},subtract:{name:"subtract",category:"Operators",syntax:["x - y","subtract(x, y)"],description:"subtract two values.",examples:["a = 5.3 - 2","a + 2","2/3 - 1/6","2 * 3 - 3","2.1 km - 500m"],seealso:["add"]},unaryMinus:{name:"unaryMinus",category:"Operators",syntax:["-x","unaryMinus(x)"],description:"Inverse the sign of a value. Converts booleans and strings to numbers.",examples:["-4.5","-(-5.6)",'-"22"'],seealso:["add","subtract","unaryPlus"]},unaryPlus:{name:"unaryPlus",category:"Operators",syntax:["+x","unaryPlus(x)"],description:"Converts booleans and strings to numbers.",examples:["+true",'+"2"'],seealso:["add","subtract","unaryMinus"]},xgcd:{name:"xgcd",category:"Arithmetic",syntax:["xgcd(a, b)"],description:"Calculate the extended greatest common divisor for two values. The result is an array [d, x, y] with 3 entries, where d is the greatest common divisor, and d = x * a + y * b.",examples:["xgcd(8, 12)","gcd(8, 12)","xgcd(36163, 21199)"],seealso:["gcd","lcm"]},invmod:{name:"invmod",category:"Arithmetic",syntax:["invmod(a, b)"],description:"Calculate the (modular) multiplicative inverse of a modulo b. Solution to the equation ax ≣ 1 (mod b)",examples:["invmod(8, 12)","invmod(7, 13)","invmod(15151, 15122)"],seealso:["gcd","xgcd"]},bitAnd:{name:"bitAnd",category:"Bitwise",syntax:["x & y","bitAnd(x, y)"],description:"Bitwise AND operation. Performs the logical AND operation on each pair of the corresponding bits of the two given values by multiplying them. If both bits in the compared position are 1, the bit in the resulting binary representation is 1, otherwise, the result is 0",examples:["5 & 3","bitAnd(53, 131)","[1, 12, 31] & 42"],seealso:["bitNot","bitOr","bitXor","leftShift","rightArithShift","rightLogShift"]},bitNot:{name:"bitNot",category:"Bitwise",syntax:["~x","bitNot(x)"],description:"Bitwise NOT operation. Performs a logical negation on each bit of the given value. Bits that are 0 become 1, and those that are 1 become 0.",examples:["~1","~2","bitNot([2, -3, 4])"],seealso:["bitAnd","bitOr","bitXor","leftShift","rightArithShift","rightLogShift"]},bitOr:{name:"bitOr",category:"Bitwise",syntax:["x | y","bitOr(x, y)"],description:"Bitwise OR operation. Performs the logical inclusive OR operation on each pair of corresponding bits of the two given values. The result in each position is 1 if the first bit is 1 or the second bit is 1 or both bits are 1, otherwise, the result is 0.",examples:["5 | 3","bitOr([1, 2, 3], 4)"],seealso:["bitAnd","bitNot","bitXor","leftShift","rightArithShift","rightLogShift"]},bitXor:{name:"bitXor",category:"Bitwise",syntax:["bitXor(x, y)"],description:"Bitwise XOR operation, exclusive OR. Performs the logical exclusive OR operation on each pair of corresponding bits of the two given values. The result in each position is 1 if only the first bit is 1 or only the second bit is 1, but will be 0 if both are 0 or both are 1.",examples:["bitOr(1, 2)","bitXor([2, 3, 4], 4)"],seealso:["bitAnd","bitNot","bitOr","leftShift","rightArithShift","rightLogShift"]},leftShift:{name:"leftShift",category:"Bitwise",syntax:["x << y","leftShift(x, y)"],description:"Bitwise left logical shift of a value x by y number of bits.",examples:["4 << 1","8 >> 1"],seealso:["bitAnd","bitNot","bitOr","bitXor","rightArithShift","rightLogShift"]},rightArithShift:{name:"rightArithShift",category:"Bitwise",syntax:["x >> y","rightArithShift(x, y)"],description:"Bitwise right arithmetic shift of a value x by y number of bits.",examples:["8 >> 1","4 << 1","-12 >> 2"],seealso:["bitAnd","bitNot","bitOr","bitXor","leftShift","rightLogShift"]},rightLogShift:{name:"rightLogShift",category:"Bitwise",syntax:["x >>> y","rightLogShift(x, y)"],description:"Bitwise right logical shift of a value x by y number of bits.",examples:["8 >>> 1","4 << 1","-12 >>> 2"],seealso:["bitAnd","bitNot","bitOr","bitXor","leftShift","rightArithShift"]},bellNumbers:{name:"bellNumbers",category:"Combinatorics",syntax:["bellNumbers(n)"],description:"The Bell Numbers count the number of partitions of a set. A partition is a pairwise disjoint subset of S whose union is S. `bellNumbers` only takes integer arguments. The following condition must be enforced: n >= 0.",examples:["bellNumbers(3)","bellNumbers(8)"],seealso:["stirlingS2"]},catalan:{name:"catalan",category:"Combinatorics",syntax:["catalan(n)"],description:"The Catalan Numbers enumerate combinatorial structures of many different types. catalan only takes integer arguments. The following condition must be enforced: n >= 0.",examples:["catalan(3)","catalan(8)"],seealso:["bellNumbers"]},composition:{name:"composition",category:"Combinatorics",syntax:["composition(n, k)"],description:"The composition counts of n into k parts. composition only takes integer arguments. The following condition must be enforced: k <= n.",examples:["composition(5, 3)"],seealso:["combinations"]},stirlingS2:{name:"stirlingS2",category:"Combinatorics",syntax:["stirlingS2(n, k)"],description:"he Stirling numbers of the second kind, counts the number of ways to partition a set of n labelled objects into k nonempty unlabelled subsets. `stirlingS2` only takes integer arguments. The following condition must be enforced: k <= n. If n = k or k = 1, then s(n,k) = 1.",examples:["stirlingS2(5, 3)"],seealso:["bellNumbers"]},config:{name:"config",category:"Core",syntax:["config()","config(options)"],description:"Get configuration or change configuration.",examples:["config()","1/3 + 1/4",'config({number: "Fraction"})',"1/3 + 1/4"],seealso:[]},import:{name:"import",category:"Core",syntax:["import(functions)","import(functions, options)"],description:"Import functions or constants from an object.",examples:["import({myFn: f(x)=x^2, myConstant: 32 })","myFn(2)","myConstant"],seealso:[]},typed:{name:"typed",category:"Core",syntax:["typed(signatures)","typed(name, signatures)"],description:"Create a typed function.",examples:['double = typed({ "number": f(x)=x+x, "string": f(x)=concat(x,x) })',"double(2)",'double("hello")'],seealso:[]},arg:{name:"arg",category:"Complex",syntax:["arg(x)"],description:"Compute the argument of a complex value. If x = a+bi, the argument is computed as atan2(b, a).",examples:["arg(2 + 2i)","atan2(3, 2)","arg(2 + 3i)"],seealso:["re","im","conj","abs"]},conj:{name:"conj",category:"Complex",syntax:["conj(x)"],description:"Compute the complex conjugate of a complex value. If x = a+bi, the complex conjugate is a-bi.",examples:["conj(2 + 3i)","conj(2 - 3i)","conj(-5.2i)"],seealso:["re","im","abs","arg"]},re:{name:"re",category:"Complex",syntax:["re(x)"],description:"Get the real part of a complex number.",examples:["re(2 + 3i)","im(2 + 3i)","re(-5.2i)","re(2.4)"],seealso:["im","conj","abs","arg"]},im:{name:"im",category:"Complex",syntax:["im(x)"],description:"Get the imaginary part of a complex number.",examples:["im(2 + 3i)","re(2 + 3i)","im(-5.2i)","im(2.4)"],seealso:["re","conj","abs","arg"]},evaluate:{name:"evaluate",category:"Expression",syntax:["evaluate(expression)","evaluate(expression, scope)","evaluate([expr1, expr2, expr3, ...])","evaluate([expr1, expr2, expr3, ...], scope)"],description:"Evaluate an expression or an array with expressions.",examples:['evaluate("2 + 3")','evaluate("sqrt(16)")','evaluate("2 inch to cm")','evaluate("sin(x * pi)", { "x": 1/2 })','evaluate(["width=2", "height=4","width*height"])'],seealso:[]},help:{name:"help",category:"Expression",syntax:["help(object)","help(string)"],description:"Display documentation on a function or data type.",examples:["help(sqrt)",'help("complex")'],seealso:[]},distance:{name:"distance",category:"Geometry",syntax:["distance([x1, y1], [x2, y2])","distance([[x1, y1], [x2, y2]])"],description:"Calculates the Euclidean distance between two points.",examples:["distance([0,0], [4,4])","distance([[0,0], [4,4]])"],seealso:[]},intersect:{name:"intersect",category:"Geometry",syntax:["intersect(expr1, expr2, expr3, expr4)","intersect(expr1, expr2, expr3)"],description:"Computes the intersection point of lines and/or planes.",examples:["intersect([0, 0], [10, 10], [10, 0], [0, 10])","intersect([1, 0, 1], [4, -2, 2], [1, 1, 1, 6])"],seealso:[]},and:{name:"and",category:"Logical",syntax:["x and y","and(x, y)"],description:"Logical and. Test whether two values are both defined with a nonzero/nonempty value.",examples:["true and false","true and true","2 and 4"],seealso:["not","or","xor"]},not:{name:"not",category:"Logical",syntax:["not x","not(x)"],description:"Logical not. Flips the boolean value of given argument.",examples:["not true","not false","not 2","not 0"],seealso:["and","or","xor"]},or:{name:"or",category:"Logical",syntax:["x or y","or(x, y)"],description:"Logical or. Test if at least one value is defined with a nonzero/nonempty value.",examples:["true or false","false or false","0 or 4"],seealso:["not","and","xor"]},xor:{name:"xor",category:"Logical",syntax:["x xor y","xor(x, y)"],description:"Logical exclusive or, xor. Test whether one and only one value is defined with a nonzero/nonempty value.",examples:["true xor false","false xor false","true xor true","0 xor 4"],seealso:["not","and","or"]},concat:{name:"concat",category:"Matrix",syntax:["concat(A, B, C, ...)","concat(A, B, C, ..., dim)"],description:"Concatenate matrices. By default, the matrices are concatenated by the last dimension. The dimension on which to concatenate can be provided as last argument.",examples:["A = [1, 2; 5, 6]","B = [3, 4; 7, 8]","concat(A, B)","concat(A, B, 1)","concat(A, B, 2)"],seealso:["det","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},count:{name:"count",category:"Matrix",syntax:["count(x)"],description:"Count the number of elements of a matrix, array or string.",examples:["a = [1, 2; 3, 4; 5, 6]","count(a)","size(a)",'count("hello world")'],seealso:["size"]},cross:{name:"cross",category:"Matrix",syntax:["cross(A, B)"],description:"Calculate the cross product for two vectors in three dimensional space.",examples:["cross([1, 1, 0], [0, 1, 1])","cross([3, -3, 1], [4, 9, 2])","cross([2, 3, 4], [5, 6, 7])"],seealso:["multiply","dot"]},column:{name:"column",category:"Matrix",syntax:["column(x, index)"],description:"Return a column from a matrix or array.",examples:["A = [[1, 2], [3, 4]]","column(A, 1)","column(A, 2)"],seealso:["row","matrixFromColumns"]},ctranspose:{name:"ctranspose",category:"Matrix",syntax:["x'","ctranspose(x)"],description:"Complex Conjugate and Transpose a matrix",examples:["a = [1, 2, 3; 4, 5, 6]","a'","ctranspose(a)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","zeros"]},det:{name:"det",category:"Matrix",syntax:["det(x)"],description:"Calculate the determinant of a matrix",examples:["det([1, 2; 3, 4])","det([-2, 2, 3; -1, 1, 3; 2, 0, -1])"],seealso:["concat","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},diag:{name:"diag",category:"Matrix",syntax:["diag(x)","diag(x, k)"],description:"Create a diagonal matrix or retrieve the diagonal of a matrix. When x is a vector, a matrix with the vector values on the diagonal will be returned. When x is a matrix, a vector with the diagonal values of the matrix is returned. When k is provided, the k-th diagonal will be filled in or retrieved, if k is positive, the values are placed on the super diagonal. When k is negative, the values are placed on the sub diagonal.",examples:["diag(1:3)","diag(1:3, 1)","a = [1, 2, 3; 4, 5, 6; 7, 8, 9]","diag(a)"],seealso:["concat","det","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},diff:{name:"diff",category:"Matrix",syntax:["diff(arr)","diff(arr, dim)"],description:["Create a new matrix or array with the difference of the passed matrix or array.","Dim parameter is optional and used to indicant the dimension of the array/matrix to apply the difference","If no dimension parameter is passed it is assumed as dimension 0","Dimension is zero-based in javascript and one-based in the parser","Arrays must be 'rectangular' meaning arrays like [1, 2]","If something is passed as a matrix it will be returned as a matrix but other than that all matrices are converted to arrays"],examples:["A = [1, 2, 4, 7, 0]","diff(A)","diff(A, 1)","B = [[1, 2], [3, 4]]","diff(B)","diff(B, 1)","diff(B, 2)","diff(B, bignumber(2))","diff([[1, 2], matrix([3, 4])], 2)"],seealso:["subtract","partitionSelect"]},dot:{name:"dot",category:"Matrix",syntax:["dot(A, B)","A * B"],description:"Calculate the dot product of two vectors. The dot product of A = [a1, a2, a3, ..., an] and B = [b1, b2, b3, ..., bn] is defined as dot(A, B) = a1 * b1 + a2 * b2 + a3 * b3 + ... + an * bn",examples:["dot([2, 4, 1], [2, 2, 3])","[2, 4, 1] * [2, 2, 3]"],seealso:["multiply","cross"]},getMatrixDataType:{name:"getMatrixDataType",category:"Matrix",syntax:["getMatrixDataType(x)"],description:'Find the data type of all elements in a matrix or array, for example "number" if all items are a number and "Complex" if all values are complex numbers. If a matrix contains more than one data type, it will return "mixed".',examples:["getMatrixDataType([1, 2, 3])","getMatrixDataType([[5 cm], [2 inch]])",'getMatrixDataType([1, "text"])',"getMatrixDataType([1, bignumber(4)])"],seealso:["matrix","sparse","typeOf"]},identity:{name:"identity",category:"Matrix",syntax:["identity(n)","identity(m, n)","identity([m, n])"],description:"Returns the identity matrix with size m-by-n. The matrix has ones on the diagonal and zeros elsewhere.",examples:["identity(3)","identity(3, 5)","a = [1, 2, 3; 4, 5, 6]","identity(size(a))"],seealso:["concat","det","diag","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},filter:{name:"filter",category:"Matrix",syntax:["filter(x, test)"],description:"Filter items in a matrix.",examples:["isPositive(x) = x > 0","filter([6, -2, -1, 4, 3], isPositive)","filter([6, -2, 0, 1, 0], x != 0)"],seealso:["sort","map","forEach"]},flatten:{name:"flatten",category:"Matrix",syntax:["flatten(x)"],description:"Flatten a multi dimensional matrix into a single dimensional matrix.",examples:["a = [1, 2, 3; 4, 5, 6]","size(a)","b = flatten(a)","size(b)"],seealso:["concat","resize","size","squeeze"]},forEach:{name:"forEach",category:"Matrix",syntax:["forEach(x, callback)"],description:"Iterates over all elements of a matrix/array, and executes the given callback function.",examples:["numberOfPets = {}","addPet(n) = numberOfPets[n] = (numberOfPets[n] ? numberOfPets[n]:0 ) + 1;",'forEach(["Dog","Cat","Cat"], addPet)',"numberOfPets"],seealso:["map","sort","filter"]},inv:{name:"inv",category:"Matrix",syntax:["inv(x)"],description:"Calculate the inverse of a matrix",examples:["inv([1, 2; 3, 4])","inv(4)","1 / 4"],seealso:["concat","det","diag","identity","ones","range","size","squeeze","subset","trace","transpose","zeros"]},pinv:{name:"pinv",category:"Matrix",syntax:["pinv(x)"],description:"Calculate the Moore–Penrose inverse of a matrix",examples:["pinv([1, 2; 3, 4])","pinv([[1, 0], [0, 1], [0, 1]])","pinv(4)"],seealso:["inv"]},eigs:{name:"eigs",category:"Matrix",syntax:["eigs(x)"],description:"Calculate the eigenvalues and eigenvectors of a real symmetric matrix",examples:["eigs([[5, 2.3], [2.3, 1]])"],seealso:["inv"]},kron:{name:"kron",category:"Matrix",syntax:["kron(x, y)"],description:"Calculates the kronecker product of 2 matrices or vectors.",examples:["kron([[1, 0], [0, 1]], [[1, 2], [3, 4]])","kron([1,1], [2,3,4])"],seealso:["multiply","dot","cross"]},matrixFromFunction:{name:"matrixFromFunction",category:"Matrix",syntax:["matrixFromFunction(size, fn)","matrixFromFunction(size, fn, format)","matrixFromFunction(size, fn, format, datatype)","matrixFromFunction(size, format, fn)","matrixFromFunction(size, format, datatype, fn)"],description:"Create a matrix by evaluating a generating function at each index.",examples:["f(I) = I[1] - I[2]","matrixFromFunction([3,3], f)","g(I) = I[1] - I[2] == 1 ? 4 : 0",'matrixFromFunction([100, 100], "sparse", g)',"matrixFromFunction([5], random)"],seealso:["matrix","matrixFromRows","matrixFromColumns","zeros"]},matrixFromRows:{name:"matrixFromRows",category:"Matrix",syntax:["matrixFromRows(...arr)","matrixFromRows(row1, row2)","matrixFromRows(row1, row2, row3)"],description:"Create a dense matrix from vectors as individual rows.",examples:["matrixFromRows([1, 2, 3], [[4],[5],[6]])"],seealso:["matrix","matrixFromColumns","matrixFromFunction","zeros"]},matrixFromColumns:{name:"matrixFromColumns",category:"Matrix",syntax:["matrixFromColumns(...arr)","matrixFromColumns(row1, row2)","matrixFromColumns(row1, row2, row3)"],description:"Create a dense matrix from vectors as individual columns.",examples:["matrixFromColumns([1, 2, 3], [[4],[5],[6]])"],seealso:["matrix","matrixFromRows","matrixFromFunction","zeros"]},map:{name:"map",category:"Matrix",syntax:["map(x, callback)"],description:"Create a new matrix or array with the results of the callback function executed on each entry of the matrix/array.",examples:["map([1, 2, 3], square)"],seealso:["filter","forEach"]},ones:{name:"ones",category:"Matrix",syntax:["ones(m)","ones(m, n)","ones(m, n, p, ...)","ones([m])","ones([m, n])","ones([m, n, p, ...])"],description:"Create a matrix containing ones.",examples:["ones(3)","ones(3, 5)","ones([2,3]) * 4.5","a = [1, 2, 3; 4, 5, 6]","ones(size(a))"],seealso:["concat","det","diag","identity","inv","range","size","squeeze","subset","trace","transpose","zeros"]},partitionSelect:{name:"partitionSelect",category:"Matrix",syntax:["partitionSelect(x, k)","partitionSelect(x, k, compare)"],description:"Partition-based selection of an array or 1D matrix. Will find the kth smallest value, and mutates the input array. Uses Quickselect.",examples:["partitionSelect([5, 10, 1], 2)",'partitionSelect(["C", "B", "A", "D"], 1, compareText)',"arr = [5, 2, 1]","partitionSelect(arr, 0) # returns 1, arr is now: [1, 2, 5]","arr","partitionSelect(arr, 1, 'desc') # returns 2, arr is now: [5, 2, 1]","arr"],seealso:["sort"]},range:{name:"range",category:"Type",syntax:["start:end","start:step:end","range(start, end)","range(start, end, step)","range(string)"],description:"Create a range. Lower bound of the range is included, upper bound is excluded.",examples:["1:5","3:-1:-3","range(3, 7)","range(0, 12, 2)",'range("4:10")',"range(1m, 1m, 3m)","a = [1, 2, 3, 4; 5, 6, 7, 8]","a[1:2, 1:2]"],seealso:["concat","det","diag","identity","inv","ones","size","squeeze","subset","trace","transpose","zeros"]},resize:{name:"resize",category:"Matrix",syntax:["resize(x, size)","resize(x, size, defaultValue)"],description:"Resize a matrix.",examples:["resize([1,2,3,4,5], [3])","resize([1,2,3], [5])","resize([1,2,3], [5], -1)","resize(2, [2, 3])",'resize("hello", [8], "!")'],seealso:["size","subset","squeeze","reshape"]},reshape:{name:"reshape",category:"Matrix",syntax:["reshape(x, sizes)"],description:"Reshape a multi dimensional array to fit the specified dimensions.",examples:["reshape([1, 2, 3, 4, 5, 6], [2, 3])","reshape([[1, 2], [3, 4]], [1, 4])","reshape([[1, 2], [3, 4]], [4])","reshape([1, 2, 3, 4], [-1, 2])"],seealso:["size","squeeze","resize"]},rotate:{name:"rotate",category:"Matrix",syntax:["rotate(w, theta)","rotate(w, theta, v)"],description:"Returns a 2-D rotation matrix (2x2) for a given angle (in radians). Returns a 2-D rotation matrix (3x3) of a given angle (in radians) around given axis.",examples:["rotate([1, 0], pi / 2)",'rotate(matrix([1, 0]), unit("35deg"))','rotate([1, 0, 0], unit("90deg"), [0, 0, 1])','rotate(matrix([1, 0, 0]), unit("90deg"), matrix([0, 0, 1]))'],seealso:["matrix","rotationMatrix"]},rotationMatrix:{name:"rotationMatrix",category:"Matrix",syntax:["rotationMatrix(theta)","rotationMatrix(theta, v)","rotationMatrix(theta, v, format)"],description:"Returns a 2-D rotation matrix (2x2) for a given angle (in radians). Returns a 2-D rotation matrix (3x3) of a given angle (in radians) around given axis.",examples:["rotationMatrix(pi / 2)",'rotationMatrix(unit("45deg"), [0, 0, 1])','rotationMatrix(1, matrix([0, 0, 1]), "sparse")'],seealso:["cos","sin"]},row:{name:"row",category:"Matrix",syntax:["row(x, index)"],description:"Return a row from a matrix or array.",examples:["A = [[1, 2], [3, 4]]","row(A, 1)","row(A, 2)"],seealso:["column","matrixFromRows"]},size:{name:"size",category:"Matrix",syntax:["size(x)"],description:"Calculate the size of a matrix.",examples:["size(2.3)",'size("hello world")',"a = [1, 2; 3, 4; 5, 6]","size(a)","size(1:6)"],seealso:["concat","count","det","diag","identity","inv","ones","range","squeeze","subset","trace","transpose","zeros"]},sort:{name:"sort",category:"Matrix",syntax:["sort(x)","sort(x, compare)"],description:'Sort the items in a matrix. Compare can be a string "asc", "desc", "natural", or a custom sort function.',examples:["sort([5, 10, 1])",'sort(["C", "B", "A", "D"], "natural")',"sortByLength(a, b) = size(a)[1] - size(b)[1]",'sort(["Langdon", "Tom", "Sara"], sortByLength)','sort(["10", "1", "2"], "natural")'],seealso:["map","filter","forEach"]},squeeze:{name:"squeeze",category:"Matrix",syntax:["squeeze(x)"],description:"Remove inner and outer singleton dimensions from a matrix.",examples:["a = zeros(3,2,1)","size(squeeze(a))","b = zeros(1,1,3)","size(squeeze(b))"],seealso:["concat","det","diag","identity","inv","ones","range","size","subset","trace","transpose","zeros"]},subset:{name:"subset",category:"Matrix",syntax:["value(index)","value(index) = replacement","subset(value, [index])","subset(value, [index], replacement)"],description:"Get or set a subset of the entries of a matrix or characters of a string. Indexes are one-based. There should be one index specification for each dimension of the target. Each specification can be a single index, a list of indices, or a range in colon notation `l:u`. In a range, both the lower bound l and upper bound u are included; and if a bound is omitted it defaults to the most extreme valid value. The cartesian product of the indices specified in each dimension determines the target of the operation.",examples:["d = [1, 2; 3, 4]","e = []","e[1, 1:2] = [5, 6]","e[2, :] = [7, 8]","f = d * e","f[2, 1]","f[:, 1]","f[[1,2], [1,3]] = [9, 10; 11, 12]","f"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","trace","transpose","zeros"]},trace:{name:"trace",category:"Matrix",syntax:["trace(A)"],description:"Calculate the trace of a matrix: the sum of the elements on the main diagonal of a square matrix.",examples:["A = [1, 2, 3; -1, 2, 3; 2, 0, 3]","trace(A)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","transpose","zeros"]},transpose:{name:"transpose",category:"Matrix",syntax:["x'","transpose(x)"],description:"Transpose a matrix",examples:["a = [1, 2, 3; 4, 5, 6]","a'","transpose(a)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","zeros"]},zeros:{name:"zeros",category:"Matrix",syntax:["zeros(m)","zeros(m, n)","zeros(m, n, p, ...)","zeros([m])","zeros([m, n])","zeros([m, n, p, ...])"],description:"Create a matrix containing zeros.",examples:["zeros(3)","zeros(3, 5)","a = [1, 2, 3; 4, 5, 6]","zeros(size(a))"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose"]},fft:{name:"fft",category:"Matrix",syntax:["fft(x)"],description:"Calculate N-dimensional fourier transform",examples:["fft([[1, 0], [1, 0]])"],seealso:["ifft"]},ifft:{name:"ifft",category:"Matrix",syntax:["ifft(x)"],description:"Calculate N-dimensional inverse fourier transform",examples:["ifft([[2, 2], [0, 0]])"],seealso:["fft"]},sylvester:{name:"sylvester",category:"Algebra",syntax:["sylvester(A,B,C)"],description:"Solves the real-valued Sylvester equation AX+XB=C for X",examples:["sylvester([[-1, -2], [1, 1]], [[-2, 1], [-1, 2]], [[-3, 2], [3, 0]])","A = [[-1, -2], [1, 1]]; B = [[2, -1], [1, -2]]; C = [[-3, 2], [3, 0]]","sylvester(A, B, C)"],seealso:["schur","lyap"]},schur:{name:"schur",category:"Algebra",syntax:["schur(A)"],description:"Performs a real Schur decomposition of the real matrix A = UTU'",examples:["schur([[1, 0], [-4, 3]])","A = [[1, 0], [-4, 3]]","schur(A)"],seealso:["lyap","sylvester"]},lyap:{name:"lyap",category:"Algebra",syntax:["lyap(A,Q)"],description:"Solves the Continuous-time Lyapunov equation AP+PA'+Q=0 for P",examples:["lyap([[-2, 0], [1, -4]], [[3, 1], [1, 3]])","A = [[-2, 0], [1, -4]]","Q = [[3, 1], [1, 3]]","lyap(A,Q)"],seealso:["schur","sylvester"]},solveODE:{name:"solveODE",category:"Numeric",syntax:["solveODE(func, tspan, y0)","solveODE(func, tspan, y0, options)"],description:"Numerical Integration of Ordinary Differential Equations.",examples:["f(t,y) = y","tspan = [0, 4]","solveODE(f, tspan, 1)","solveODE(f, tspan, [1, 2])",'solveODE(f, tspan, 1, { method:"RK23", maxStep:0.1 })'],seealso:["derivative","simplifyCore"]},combinations:{name:"combinations",category:"Probability",syntax:["combinations(n, k)"],description:"Compute the number of combinations of n items taken k at a time",examples:["combinations(7, 5)"],seealso:["combinationsWithRep","permutations","factorial"]},combinationsWithRep:{name:"combinationsWithRep",category:"Probability",syntax:["combinationsWithRep(n, k)"],description:"Compute the number of combinations of n items taken k at a time with replacements.",examples:["combinationsWithRep(7, 5)"],seealso:["combinations","permutations","factorial"]},factorial:{name:"factorial",category:"Probability",syntax:["n!","factorial(n)"],description:"Compute the factorial of a value",examples:["5!","5 * 4 * 3 * 2 * 1","3!"],seealso:["combinations","combinationsWithRep","permutations","gamma"]},gamma:{name:"gamma",category:"Probability",syntax:["gamma(n)"],description:"Compute the gamma function. For small values, the Lanczos approximation is used, and for large values the extended Stirling approximation.",examples:["gamma(4)","3!","gamma(1/2)","sqrt(pi)"],seealso:["factorial"]},kldivergence:{name:"kldivergence",category:"Probability",syntax:["kldivergence(x, y)"],description:"Calculate the Kullback-Leibler (KL) divergence between two distributions.",examples:["kldivergence([0.7,0.5,0.4], [0.2,0.9,0.5])"],seealso:[]},lgamma:{name:"lgamma",category:"Probability",syntax:["lgamma(n)"],description:"Logarithm of the gamma function for real, positive numbers and complex numbers, using Lanczos approximation for numbers and Stirling series for complex numbers.",examples:["lgamma(4)","lgamma(1/2)","lgamma(i)","lgamma(complex(1.1, 2))"],seealso:["gamma"]},multinomial:{name:"multinomial",category:"Probability",syntax:["multinomial(A)"],description:"Multinomial Coefficients compute the number of ways of picking a1, a2, ..., ai unordered outcomes from `n` possibilities. multinomial takes one array of integers as an argument. The following condition must be enforced: every ai > 0.",examples:["multinomial([1, 2, 1])"],seealso:["combinations","factorial"]},permutations:{name:"permutations",category:"Probability",syntax:["permutations(n)","permutations(n, k)"],description:"Compute the number of permutations of n items taken k at a time",examples:["permutations(5)","permutations(5, 3)"],seealso:["combinations","combinationsWithRep","factorial"]},pickRandom:{name:"pickRandom",category:"Probability",syntax:["pickRandom(array)","pickRandom(array, number)","pickRandom(array, weights)","pickRandom(array, number, weights)","pickRandom(array, weights, number)"],description:"Pick a random entry from a given array.",examples:["pickRandom(0:10)","pickRandom([1, 3, 1, 6])","pickRandom([1, 3, 1, 6], 2)","pickRandom([1, 3, 1, 6], [2, 3, 2, 1])","pickRandom([1, 3, 1, 6], 2, [2, 3, 2, 1])","pickRandom([1, 3, 1, 6], [2, 3, 2, 1], 2)"],seealso:["random","randomInt"]},random:{name:"random",category:"Probability",syntax:["random()","random(max)","random(min, max)","random(size)","random(size, max)","random(size, min, max)"],description:"Return a random number.",examples:["random()","random(10, 20)","random([2, 3])"],seealso:["pickRandom","randomInt"]},randomInt:{name:"randomInt",category:"Probability",syntax:["randomInt(max)","randomInt(min, max)","randomInt(size)","randomInt(size, max)","randomInt(size, min, max)"],description:"Return a random integer number",examples:["randomInt(10, 20)","randomInt([2, 3], 10)"],seealso:["pickRandom","random"]},compare:{name:"compare",category:"Relational",syntax:["compare(x, y)"],description:"Compare two values. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:["compare(2, 3)","compare(3, 2)","compare(2, 2)","compare(5cm, 40mm)","compare(2, [1, 2, 3])"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compareNatural","compareText"]},compareNatural:{name:"compareNatural",category:"Relational",syntax:["compareNatural(x, y)"],description:"Compare two values of any type in a deterministic, natural way. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:["compareNatural(2, 3)","compareNatural(3, 2)","compareNatural(2, 2)","compareNatural(5cm, 40mm)",'compareNatural("2", "10")',"compareNatural(2 + 3i, 2 + 4i)","compareNatural([1, 2, 4], [1, 2, 3])","compareNatural([1, 5], [1, 2, 3])","compareNatural([1, 2], [1, 2])","compareNatural({a: 2}, {a: 4})"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compare","compareText"]},compareText:{name:"compareText",category:"Relational",syntax:["compareText(x, y)"],description:"Compare two strings lexically. Comparison is case sensitive. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:['compareText("B", "A")','compareText("A", "B")','compareText("A", "A")','compareText("2", "10")','compare("2", "10")',"compare(2, 10)",'compareNatural("2", "10")','compareText("B", ["A", "B", "C"])'],seealso:["compare","compareNatural"]},deepEqual:{name:"deepEqual",category:"Relational",syntax:["deepEqual(x, y)"],description:"Check equality of two matrices element wise. Returns true if the size of both matrices is equal and when and each of the elements are equal.",examples:["deepEqual([1,3,4], [1,3,4])","deepEqual([1,3,4], [1,3])"],seealso:["equal","unequal","smaller","larger","smallerEq","largerEq","compare"]},equal:{name:"equal",category:"Relational",syntax:["x == y","equal(x, y)"],description:"Check equality of two values. Returns true if the values are equal, and false if not.",examples:["2+2 == 3","2+2 == 4","a = 3.2","b = 6-2.8","a == b","50cm == 0.5m"],seealso:["unequal","smaller","larger","smallerEq","largerEq","compare","deepEqual","equalText"]},equalText:{name:"equalText",category:"Relational",syntax:["equalText(x, y)"],description:"Check equality of two strings. Comparison is case sensitive. Returns true if the values are equal, and false if not.",examples:['equalText("Hello", "Hello")','equalText("a", "A")','equal("2e3", "2000")','equalText("2e3", "2000")','equalText("B", ["A", "B", "C"])'],seealso:["compare","compareNatural","compareText","equal"]},larger:{name:"larger",category:"Relational",syntax:["x > y","larger(x, y)"],description:"Check if value x is larger than y. Returns true if x is larger than y, and false if not.",examples:["2 > 3","5 > 2*2","a = 3.3","b = 6-2.8","(a > b)","(b < a)","5 cm > 2 inch"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compare"]},largerEq:{name:"largerEq",category:"Relational",syntax:["x >= y","largerEq(x, y)"],description:"Check if value x is larger or equal to y. Returns true if x is larger or equal to y, and false if not.",examples:["2 >= 1+1","2 > 1+1","a = 3.2","b = 6-2.8","(a >= b)"],seealso:["equal","unequal","smallerEq","smaller","compare"]},smaller:{name:"smaller",category:"Relational",syntax:["x < y","smaller(x, y)"],description:"Check if value x is smaller than value y. Returns true if x is smaller than y, and false if not.",examples:["2 < 3","5 < 2*2","a = 3.3","b = 6-2.8","(a < b)","5 cm < 2 inch"],seealso:["equal","unequal","larger","smallerEq","largerEq","compare"]},smallerEq:{name:"smallerEq",category:"Relational",syntax:["x <= y","smallerEq(x, y)"],description:"Check if value x is smaller or equal to value y. Returns true if x is smaller than y, and false if not.",examples:["2 <= 1+1","2 < 1+1","a = 3.2","b = 6-2.8","(a <= b)"],seealso:["equal","unequal","larger","smaller","largerEq","compare"]},unequal:{name:"unequal",category:"Relational",syntax:["x != y","unequal(x, y)"],description:"Check unequality of two values. Returns true if the values are unequal, and false if they are equal.",examples:["2+2 != 3","2+2 != 4","a = 3.2","b = 6-2.8","a != b","50cm != 0.5m","5 cm != 2 inch"],seealso:["equal","smaller","larger","smallerEq","largerEq","compare","deepEqual"]},setCartesian:{name:"setCartesian",category:"Set",syntax:["setCartesian(set1, set2)"],description:"Create the cartesian product of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays and the values will be sorted in ascending order before the operation.",examples:["setCartesian([1, 2], [3, 4])"],seealso:["setUnion","setIntersect","setDifference","setPowerset"]},setDifference:{name:"setDifference",category:"Set",syntax:["setDifference(set1, set2)"],description:"Create the difference of two (multi)sets: every element of set1, that is not the element of set2. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setDifference([1, 2, 3, 4], [3, 4, 5, 6])","setDifference([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setIntersect","setSymDifference"]},setDistinct:{name:"setDistinct",category:"Set",syntax:["setDistinct(set)"],description:"Collect the distinct elements of a multiset. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setDistinct([1, 1, 1, 2, 2, 3])"],seealso:["setMultiplicity"]},setIntersect:{name:"setIntersect",category:"Set",syntax:["setIntersect(set1, set2)"],description:"Create the intersection of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setIntersect([1, 2, 3, 4], [3, 4, 5, 6])","setIntersect([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setDifference"]},setIsSubset:{name:"setIsSubset",category:"Set",syntax:["setIsSubset(set1, set2)"],description:"Check whether a (multi)set is a subset of another (multi)set: every element of set1 is the element of set2. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setIsSubset([1, 2], [3, 4, 5, 6])","setIsSubset([3, 4], [3, 4, 5, 6])"],seealso:["setUnion","setIntersect","setDifference"]},setMultiplicity:{name:"setMultiplicity",category:"Set",syntax:["setMultiplicity(element, set)"],description:"Count the multiplicity of an element in a multiset. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setMultiplicity(1, [1, 2, 2, 4])","setMultiplicity(2, [1, 2, 2, 4])"],seealso:["setDistinct","setSize"]},setPowerset:{name:"setPowerset",category:"Set",syntax:["setPowerset(set)"],description:"Create the powerset of a (multi)set: the powerset contains very possible subsets of a (multi)set. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setPowerset([1, 2, 3])"],seealso:["setCartesian"]},setSize:{name:"setSize",category:"Set",syntax:["setSize(set)","setSize(set, unique)"],description:'Count the number of elements of a (multi)set. When the second parameter "unique" is true, count only the unique values. A multi-dimension array will be converted to a single-dimension array before the operation.',examples:["setSize([1, 2, 2, 4])","setSize([1, 2, 2, 4], true)"],seealso:["setUnion","setIntersect","setDifference"]},setSymDifference:{name:"setSymDifference",category:"Set",syntax:["setSymDifference(set1, set2)"],description:"Create the symmetric difference of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setSymDifference([1, 2, 3, 4], [3, 4, 5, 6])","setSymDifference([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setIntersect","setDifference"]},setUnion:{name:"setUnion",category:"Set",syntax:["setUnion(set1, set2)"],description:"Create the union of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setUnion([1, 2, 3, 4], [3, 4, 5, 6])","setUnion([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setIntersect","setDifference"]},zpk2tf:{name:"zpk2tf",category:"Signal",syntax:["zpk2tf(z, p, k)"],description:"Compute the transfer function of a zero-pole-gain model.",examples:["zpk2tf([1, 2], [-1, -2], 1)","zpk2tf([1, 2], [-1, -2])","zpk2tf([1 - 3i, 2 + 2i], [-1, -2])"],seealso:[]},freqz:{name:"freqz",category:"Signal",syntax:["freqz(b, a)","freqz(b, a, w)"],description:"Calculates the frequency response of a filter given its numerator and denominator coefficients.",examples:["freqz([1, 2], [1, 2, 3])","freqz([1, 2], [1, 2, 3], [0, 1])","freqz([1, 2], [1, 2, 3], 512)"],seealso:[]},erf:{name:"erf",category:"Special",syntax:["erf(x)"],description:"Compute the erf function of a value using a rational Chebyshev approximations for different intervals of x",examples:["erf(0.2)","erf(-0.5)","erf(4)"],seealso:[]},zeta:{name:"zeta",category:"Special",syntax:["zeta(s)"],description:"Compute the Riemann Zeta Function using an infinite series and Riemanns Functional Equation for the entire complex plane",examples:["zeta(0.2)","zeta(-0.5)","zeta(4)"],seealso:[]},cumsum:{name:"cumsum",category:"Statistics",syntax:["cumsum(a, b, c, ...)","cumsum(A)"],description:"Compute the cumulative sum of all values.",examples:["cumsum(2, 3, 4, 1)","cumsum([2, 3, 4, 1])","cumsum([1, 2; 3, 4])","cumsum([1, 2; 3, 4], 1)","cumsum([1, 2; 3, 4], 2)"],seealso:["max","mean","median","min","prod","std","sum","variance"]},mad:{name:"mad",category:"Statistics",syntax:["mad(a, b, c, ...)","mad(A)"],description:"Compute the median absolute deviation of a matrix or a list with values. The median absolute deviation is defined as the median of the absolute deviations from the median.",examples:["mad(10, 20, 30)","mad([1, 2, 3])"],seealso:["mean","median","std","abs"]},max:{name:"max",category:"Statistics",syntax:["max(a, b, c, ...)","max(A)","max(A, dimension)"],description:"Compute the maximum value of a list of values.",examples:["max(2, 3, 4, 1)","max([2, 3, 4, 1])","max([2, 5; 4, 3])","max([2, 5; 4, 3], 1)","max([2, 5; 4, 3], 2)","max(2.7, 7.1, -4.5, 2.0, 4.1)","min(2.7, 7.1, -4.5, 2.0, 4.1)"],seealso:["mean","median","min","prod","std","sum","variance"]},mean:{name:"mean",category:"Statistics",syntax:["mean(a, b, c, ...)","mean(A)","mean(A, dimension)"],description:"Compute the arithmetic mean of a list of values.",examples:["mean(2, 3, 4, 1)","mean([2, 3, 4, 1])","mean([2, 5; 4, 3])","mean([2, 5; 4, 3], 1)","mean([2, 5; 4, 3], 2)","mean([1.0, 2.7, 3.2, 4.0])"],seealso:["max","median","min","prod","std","sum","variance"]},median:{name:"median",category:"Statistics",syntax:["median(a, b, c, ...)","median(A)"],description:"Compute the median of all values. The values are sorted and the middle value is returned. In case of an even number of values, the average of the two middle values is returned.",examples:["median(5, 2, 7)","median([3, -1, 5, 7])"],seealso:["max","mean","min","prod","std","sum","variance","quantileSeq"]},min:{name:"min",category:"Statistics",syntax:["min(a, b, c, ...)","min(A)","min(A, dimension)"],description:"Compute the minimum value of a list of values.",examples:["min(2, 3, 4, 1)","min([2, 3, 4, 1])","min([2, 5; 4, 3])","min([2, 5; 4, 3], 1)","min([2, 5; 4, 3], 2)","min(2.7, 7.1, -4.5, 2.0, 4.1)","max(2.7, 7.1, -4.5, 2.0, 4.1)"],seealso:["max","mean","median","prod","std","sum","variance"]},mode:{name:"mode",category:"Statistics",syntax:["mode(a, b, c, ...)","mode(A)","mode(A, a, b, B, c, ...)"],description:"Computes the mode of all values as an array. In case mode being more than one, multiple values are returned in an array.",examples:["mode(2, 1, 4, 3, 1)","mode([1, 2.7, 3.2, 4, 2.7])","mode(1, 4, 6, 1, 6)"],seealso:["max","mean","min","median","prod","std","sum","variance"]},prod:{name:"prod",category:"Statistics",syntax:["prod(a, b, c, ...)","prod(A)"],description:"Compute the product of all values.",examples:["prod(2, 3, 4)","prod([2, 3, 4])","prod([2, 5; 4, 3])"],seealso:["max","mean","min","median","min","std","sum","variance"]},quantileSeq:{name:"quantileSeq",category:"Statistics",syntax:["quantileSeq(A, prob[, sorted])","quantileSeq(A, [prob1, prob2, ...][, sorted])","quantileSeq(A, N[, sorted])"],description:"Compute the prob order quantile of a matrix or a list with values. The sequence is sorted and the middle value is returned. Supported types of sequence values are: Number, BigNumber, Unit Supported types of probablity are: Number, BigNumber. \n\nIn case of a (multi dimensional) array or matrix, the prob order quantile of all elements will be calculated.",examples:["quantileSeq([3, -1, 5, 7], 0.5)","quantileSeq([3, -1, 5, 7], [1/3, 2/3])","quantileSeq([3, -1, 5, 7], 2)","quantileSeq([-1, 3, 5, 7], 0.5, true)"],seealso:["mean","median","min","max","prod","std","sum","variance"]},std:{name:"std",category:"Statistics",syntax:["std(a, b, c, ...)","std(A)","std(A, dimension)","std(A, normalization)","std(A, dimension, normalization)"],description:'Compute the standard deviation of all values, defined as std(A) = sqrt(variance(A)). Optional parameter normalization can be "unbiased" (default), "uncorrected", or "biased".',examples:["std(2, 4, 6)","std([2, 4, 6, 8])",'std([2, 4, 6, 8], "uncorrected")','std([2, 4, 6, 8], "biased")',"std([1, 2, 3; 4, 5, 6])"],seealso:["max","mean","min","median","prod","sum","variance"]},sum:{name:"sum",category:"Statistics",syntax:["sum(a, b, c, ...)","sum(A)","sum(A, dimension)"],description:"Compute the sum of all values.",examples:["sum(2, 3, 4, 1)","sum([2, 3, 4, 1])","sum([2, 5; 4, 3])"],seealso:["max","mean","median","min","prod","std","sum","variance"]},variance:{name:"variance",category:"Statistics",syntax:["variance(a, b, c, ...)","variance(A)","variance(A, dimension)","variance(A, normalization)","variance(A, dimension, normalization)"],description:'Compute the variance of all values. Optional parameter normalization can be "unbiased" (default), "uncorrected", or "biased".',examples:["variance(2, 4, 6)","variance([2, 4, 6, 8])",'variance([2, 4, 6, 8], "uncorrected")','variance([2, 4, 6, 8], "biased")',"variance([1, 2, 3; 4, 5, 6])"],seealso:["max","mean","min","median","min","prod","std","sum"]},corr:{name:"corr",category:"Statistics",syntax:["corr(A,B)"],description:"Compute the correlation coefficient of a two list with values, For matrices, the matrix correlation coefficient is calculated.",examples:["corr([2, 4, 6, 8],[1, 2, 3, 6])","corr(matrix([[1, 2.2, 3, 4.8, 5], [1, 2, 3, 4, 5]]), matrix([[4, 5.3, 6.6, 7, 8], [1, 2, 3, 4, 5]]))"],seealso:["max","mean","min","median","min","prod","std","sum"]},acos:{name:"acos",category:"Trigonometry",syntax:["acos(x)"],description:"Compute the inverse cosine of a value in radians.",examples:["acos(0.5)","acos(cos(2.3))"],seealso:["cos","atan","asin"]},acosh:{name:"acosh",category:"Trigonometry",syntax:["acosh(x)"],description:"Calculate the hyperbolic arccos of a value, defined as `acosh(x) = ln(sqrt(x^2 - 1) + x)`.",examples:["acosh(1.5)"],seealso:["cosh","asinh","atanh"]},acot:{name:"acot",category:"Trigonometry",syntax:["acot(x)"],description:"Calculate the inverse cotangent of a value.",examples:["acot(0.5)","acot(cot(0.5))","acot(2)"],seealso:["cot","atan"]},acoth:{name:"acoth",category:"Trigonometry",syntax:["acoth(x)"],description:"Calculate the hyperbolic arccotangent of a value, defined as `acoth(x) = (ln((x+1)/x) + ln(x/(x-1))) / 2`.",examples:["acoth(2)","acoth(0.5)"],seealso:["acsch","asech"]},acsc:{name:"acsc",category:"Trigonometry",syntax:["acsc(x)"],description:"Calculate the inverse cotangent of a value.",examples:["acsc(2)","acsc(csc(0.5))","acsc(0.5)"],seealso:["csc","asin","asec"]},acsch:{name:"acsch",category:"Trigonometry",syntax:["acsch(x)"],description:"Calculate the hyperbolic arccosecant of a value, defined as `acsch(x) = ln(1/x + sqrt(1/x^2 + 1))`.",examples:["acsch(0.5)"],seealso:["asech","acoth"]},asec:{name:"asec",category:"Trigonometry",syntax:["asec(x)"],description:"Calculate the inverse secant of a value.",examples:["asec(0.5)","asec(sec(0.5))","asec(2)"],seealso:["acos","acot","acsc"]},asech:{name:"asech",category:"Trigonometry",syntax:["asech(x)"],description:"Calculate the inverse secant of a value.",examples:["asech(0.5)"],seealso:["acsch","acoth"]},asin:{name:"asin",category:"Trigonometry",syntax:["asin(x)"],description:"Compute the inverse sine of a value in radians.",examples:["asin(0.5)","asin(sin(0.5))"],seealso:["sin","acos","atan"]},asinh:{name:"asinh",category:"Trigonometry",syntax:["asinh(x)"],description:"Calculate the hyperbolic arcsine of a value, defined as `asinh(x) = ln(x + sqrt(x^2 + 1))`.",examples:["asinh(0.5)"],seealso:["acosh","atanh"]},atan:{name:"atan",category:"Trigonometry",syntax:["atan(x)"],description:"Compute the inverse tangent of a value in radians.",examples:["atan(0.5)","atan(tan(0.5))"],seealso:["tan","acos","asin"]},atanh:{name:"atanh",category:"Trigonometry",syntax:["atanh(x)"],description:"Calculate the hyperbolic arctangent of a value, defined as `atanh(x) = ln((1 + x)/(1 - x)) / 2`.",examples:["atanh(0.5)"],seealso:["acosh","asinh"]},atan2:{name:"atan2",category:"Trigonometry",syntax:["atan2(y, x)"],description:"Computes the principal value of the arc tangent of y/x in radians.",examples:["atan2(2, 2) / pi","angle = 60 deg in rad","x = cos(angle)","y = sin(angle)","atan2(y, x)"],seealso:["sin","cos","tan"]},cos:{name:"cos",category:"Trigonometry",syntax:["cos(x)"],description:"Compute the cosine of x in radians.",examples:["cos(2)","cos(pi / 4) ^ 2","cos(180 deg)","cos(60 deg)","sin(0.2)^2 + cos(0.2)^2"],seealso:["acos","sin","tan"]},cosh:{name:"cosh",category:"Trigonometry",syntax:["cosh(x)"],description:"Compute the hyperbolic cosine of x in radians.",examples:["cosh(0.5)"],seealso:["sinh","tanh","coth"]},cot:{name:"cot",category:"Trigonometry",syntax:["cot(x)"],description:"Compute the cotangent of x in radians. Defined as 1/tan(x)",examples:["cot(2)","1 / tan(2)"],seealso:["sec","csc","tan"]},coth:{name:"coth",category:"Trigonometry",syntax:["coth(x)"],description:"Compute the hyperbolic cotangent of x in radians.",examples:["coth(2)","1 / tanh(2)"],seealso:["sech","csch","tanh"]},csc:{name:"csc",category:"Trigonometry",syntax:["csc(x)"],description:"Compute the cosecant of x in radians. Defined as 1/sin(x)",examples:["csc(2)","1 / sin(2)"],seealso:["sec","cot","sin"]},csch:{name:"csch",category:"Trigonometry",syntax:["csch(x)"],description:"Compute the hyperbolic cosecant of x in radians. Defined as 1/sinh(x)",examples:["csch(2)","1 / sinh(2)"],seealso:["sech","coth","sinh"]},sec:{name:"sec",category:"Trigonometry",syntax:["sec(x)"],description:"Compute the secant of x in radians. Defined as 1/cos(x)",examples:["sec(2)","1 / cos(2)"],seealso:["cot","csc","cos"]},sech:{name:"sech",category:"Trigonometry",syntax:["sech(x)"],description:"Compute the hyperbolic secant of x in radians. Defined as 1/cosh(x)",examples:["sech(2)","1 / cosh(2)"],seealso:["coth","csch","cosh"]},sin:{name:"sin",category:"Trigonometry",syntax:["sin(x)"],description:"Compute the sine of x in radians.",examples:["sin(2)","sin(pi / 4) ^ 2","sin(90 deg)","sin(30 deg)","sin(0.2)^2 + cos(0.2)^2"],seealso:["asin","cos","tan"]},sinh:{name:"sinh",category:"Trigonometry",syntax:["sinh(x)"],description:"Compute the hyperbolic sine of x in radians.",examples:["sinh(0.5)"],seealso:["cosh","tanh"]},tan:{name:"tan",category:"Trigonometry",syntax:["tan(x)"],description:"Compute the tangent of x in radians.",examples:["tan(0.5)","sin(0.5) / cos(0.5)","tan(pi / 4)","tan(45 deg)"],seealso:["atan","sin","cos"]},tanh:{name:"tanh",category:"Trigonometry",syntax:["tanh(x)"],description:"Compute the hyperbolic tangent of x in radians.",examples:["tanh(0.5)","sinh(0.5) / cosh(0.5)"],seealso:["sinh","cosh"]},to:{name:"to",category:"Units",syntax:["x to unit","to(x, unit)"],description:"Change the unit of a value.",examples:["5 inch to cm","3.2kg to g","16 bytes in bits"],seealso:[]},clone:{name:"clone",category:"Utils",syntax:["clone(x)"],description:"Clone a variable. Creates a copy of primitive variables,and a deep copy of matrices",examples:["clone(3.5)","clone(2 - 4i)","clone(45 deg)","clone([1, 2; 3, 4])",'clone("hello world")'],seealso:[]},format:{name:"format",category:"Utils",syntax:["format(value)","format(value, precision)"],description:"Format a value of any type as string.",examples:["format(2.3)","format(3 - 4i)","format([])","format(pi, 3)"],seealso:["print"]},bin:{name:"bin",category:"Utils",syntax:["bin(value)"],description:"Format a number as binary",examples:["bin(2)"],seealso:["oct","hex"]},oct:{name:"oct",category:"Utils",syntax:["oct(value)"],description:"Format a number as octal",examples:["oct(56)"],seealso:["bin","hex"]},hex:{name:"hex",category:"Utils",syntax:["hex(value)"],description:"Format a number as hexadecimal",examples:["hex(240)"],seealso:["bin","oct"]},isNaN:{name:"isNaN",category:"Utils",syntax:["isNaN(x)"],description:"Test whether a value is NaN (not a number)",examples:["isNaN(2)","isNaN(0 / 0)","isNaN(NaN)","isNaN(Infinity)"],seealso:["isNegative","isNumeric","isPositive","isZero"]},isInteger:{name:"isInteger",category:"Utils",syntax:["isInteger(x)"],description:"Test whether a value is an integer number.",examples:["isInteger(2)","isInteger(3.5)","isInteger([3, 0.5, -2])"],seealso:["isNegative","isNumeric","isPositive","isZero"]},isNegative:{name:"isNegative",category:"Utils",syntax:["isNegative(x)"],description:"Test whether a value is negative: smaller than zero.",examples:["isNegative(2)","isNegative(0)","isNegative(-4)","isNegative([3, 0.5, -2])"],seealso:["isInteger","isNumeric","isPositive","isZero"]},isNumeric:{name:"isNumeric",category:"Utils",syntax:["isNumeric(x)"],description:"Test whether a value is a numeric value. Returns true when the input is a number, BigNumber, Fraction, or boolean.",examples:["isNumeric(2)",'isNumeric("2")','hasNumericValue("2")',"isNumeric(0)","isNumeric(bignumber(500))","isNumeric(fraction(0.125))","isNumeric(2 + 3i)",'isNumeric([2.3, "foo", false])'],seealso:["isInteger","isZero","isNegative","isPositive","isNaN","hasNumericValue"]},hasNumericValue:{name:"hasNumericValue",category:"Utils",syntax:["hasNumericValue(x)"],description:"Test whether a value is an numeric value. In case of a string, true is returned if the string contains a numeric value.",examples:["hasNumericValue(2)",'hasNumericValue("2")','isNumeric("2")',"hasNumericValue(0)","hasNumericValue(bignumber(500))","hasNumericValue(fraction(0.125))","hasNumericValue(2 + 3i)",'hasNumericValue([2.3, "foo", false])'],seealso:["isInteger","isZero","isNegative","isPositive","isNaN","isNumeric"]},isPositive:{name:"isPositive",category:"Utils",syntax:["isPositive(x)"],description:"Test whether a value is positive: larger than zero.",examples:["isPositive(2)","isPositive(0)","isPositive(-4)","isPositive([3, 0.5, -2])"],seealso:["isInteger","isNumeric","isNegative","isZero"]},isPrime:{name:"isPrime",category:"Utils",syntax:["isPrime(x)"],description:"Test whether a value is prime: has no divisors other than itself and one.",examples:["isPrime(3)","isPrime(-2)","isPrime([2, 17, 100])"],seealso:["isInteger","isNumeric","isNegative","isZero"]},isZero:{name:"isZero",category:"Utils",syntax:["isZero(x)"],description:"Test whether a value is zero.",examples:["isZero(2)","isZero(0)","isZero(-4)","isZero([3, 0, -2, 0])"],seealso:["isInteger","isNumeric","isNegative","isPositive"]},print:{name:"print",category:"Utils",syntax:["print(template, values)","print(template, values, precision)"],description:"Interpolate values into a string template.",examples:['print("Lucy is $age years old", {age: 5})','print("The value of pi is $pi", {pi: pi}, 3)','print("Hello, $user.name!", {user: {name: "John"}})','print("Values: $1, $2, $3", [6, 9, 4])'],seealso:["format"]},typeOf:{name:"typeOf",category:"Utils",syntax:["typeOf(x)"],description:"Get the type of a variable.",examples:["typeOf(3.5)","typeOf(2 - 4i)","typeOf(45 deg)",'typeOf("hello world")'],seealso:["getMatrixDataType"]},numeric:{name:"numeric",category:"Utils",syntax:["numeric(x)"],description:"Convert a numeric input to a specific numeric type: number, BigNumber, or Fraction.",examples:['numeric("4")','numeric("4", "number")','numeric("4", "BigNumber")','numeric("4", "Fraction")','numeric(4, "Fraction")','numeric(fraction(2, 5), "number")'],seealso:["number","fraction","bignumber","string","format"]}},Gm="help",Vm=Ee(Gm,["typed","mathWithTransform","Help"],(function(e){var t=e.typed,r=e.mathWithTransform,n=e.Help;return t(Gm,{any:function(e){var t,i=e;if("string"!=typeof e)for(t in r)if(Ne(r,t)&&e===r[t]){i=t;break}var a=Te(Hm,i);if(!a){var o="function"==typeof i?i.name:i;throw new Error('No documentation found on "'+o+'"')}return new n(a)}})})),Zm="chain",Wm=Ee(Zm,["typed","Chain"],(function(e){var t=e.typed,r=e.Chain;return t(Zm,{"":function(){return new r},any:function(e){return new r(e)}})})),Ym=Ee("det",["typed","matrix","subtractScalar","multiply","divideScalar","isZero","unaryMinus"],(function(e){var t=e.typed,r=e.matrix,n=e.subtractScalar,i=e.multiply,a=e.divideScalar,o=e.isZero,u=e.unaryMinus;return t("det",{any:function(e){return he(e)},"Array | Matrix":function(e){var t;switch((t=l(e)?e.size():Array.isArray(e)?(e=r(e)).size():[]).length){case 0:return he(e);case 1:if(1===t[0])return he(e.valueOf()[0]);if(0===t[0])return 1;throw new RangeError("Matrix must be square (size: "+Jr(t)+")");case 2:var s=t[0],c=t[1];if(s===c)return function(e,t,r){if(1===t)return he(e[0][0]);if(2===t)return n(i(e[0][0],e[1][1]),i(e[1][0],e[0][1]));for(var s=!1,c=new Array(t).fill(0).map((function(e,t){return t})),f=0;fx&&(x=c(v[f][g]),b=f),f++;if(0===x)throw Error("Cannot calculate inverse, determinant is zero");(f=b)!==g&&(h=v[g],v[g]=v[f],v[f]=h,h=y[g],y[g]=y[f],y[f]=h);var w=v[g],N=y[g];for(f=0;f=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==r.return||r.return()}finally{if(u)throw a}}}}function eh(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r2&&void 0!==arguments[2]?arguments[2]:t.epsilon,u=arguments.length>3?arguments[3]:void 0;if("number"===u)return function(e,r){for(var n,i=e.length,a=Math.abs(r/i),o=new Array(i),u=0;u=Math.abs(a);){var h=p[0][0],d=p[0][1];e=v(e,(s=e[h][h],c=e[d][d],f=e[h][d],l=void 0,l=c-s,n=Math.abs(l)<=t.epsilon?Math.PI/4:.5*Math.atan(2*f/(c-s))),h,d),o=m(o,n,h,d),p=y(e)}for(var g=b(i,0),w=0;w=i(p);){var A=E[0][0],S=E[0][1];e=d(e,(y=e[A][A],w=e[S][S],N=e[A][S],D=void 0,D=n(w,y),o=i(D)<=t.epsilon?f(-1).acos().div(4):s(.5,a(l(2,N,c(D))))),A,S),m=h(m,o,A,S),E=g(e)}for(var C=b(u,0),M=0;M=5)return null;for(u=0;;){var s=m(e,a);if(g(C(S(a,[s])),n))break;if(++u>=10)return null;a=M(s)}return a}function A(e,t,r){var n="BigNumber"===r,i="Complex"===r,a=Array(e).fill(0).map((function(e){return 2*Math.random()-1}));return n&&(a=a.map((function(e){return c(e)}))),i&&(a=a.map((function(e){return v(e)}))),M(a=S(a,t),r)}function S(e,t){var n,a=Km(t);try{for(a.s();!(n=a.n()).done;){var u=n.value;e=r(e,i(o(b(u,e),b(u,u)),u))}}catch(e){a.e(e)}finally{a.f()}return e}function C(e){return s(u(b(e,e)))}function M(e,t){var r="Complex"===t,n="BigNumber"===t?c(1):r?v(1):1;return i(o(n,C(e)),e)}return function(e,m,b,A,S){void 0===S&&(S=!0);var C=function(e,r,n,i,u){var l,p="BigNumber"===i,m="Complex"===i,h=p?c(0):0,x=p?c(1):m?v(1):1,b=p?c(1):1,w=p?c(10):2,N=a(w,w);u&&(l=Array(r).fill(x));for(var D=!1;!D;){D=!0;for(var E=0;E1&&(k=f(Array(T-1).fill(y)))),T-=1,F.pop();for(var L=0;L2&&(k=f(Array(T-2).fill(y)))),T-=2,F.pop(),F.pop();for(var $=0;$100){var H=Error("The eigenvalues failed to converge. Only found these eigenvalues: "+O.join(", "));throw H.values=O,H.vectors=[],H}var G=m?i(_,function(e,t){for(var r=[],n=0;n1&&(g=o(g,m),x=-x),d=n(d,o(y=y*(l-b+1)/((2*l-b+1)*b),g)),v=n(v,o(y*x,g));for(var w=o(a(v),d),N=0;Nm&&++a>1e3)throw new Error("computing square root of matrix: iterative method could not converge")}while(t>m);return o}return t(ih,{"Array | Matrix":function(e){var t=l(e)?e.size():an(e);switch(t.length){case 1:if(1===t[0])return a(e,o);throw new RangeError("Matrix must be square (size: "+Jr(t)+")");case 2:if(t[0]===t[1])return h(e);throw new RangeError("Matrix must be square (size: "+Jr(t)+")");default:throw new RangeError("Matrix must be at most two dimensional (size: "+Jr(t)+")")}}})})),oh="sylvester",uh=Ee(oh,["typed","schur","matrixFromColumns","matrix","multiply","range","concat","transpose","index","subset","add","subtract","identity","lusolve","abs"],(function(e){var t=e.typed,r=e.schur,n=e.matrixFromColumns,i=e.matrix,a=e.multiply,o=e.range,u=e.concat,s=e.transpose,c=e.index,f=e.subset,l=e.add,p=e.subtract,m=e.identity,h=e.lusolve,d=e.abs;return t(oh,{"Matrix, Matrix, Matrix":v,"Array, Matrix, Matrix":function(e,t,r){return v(i(e),t,r)},"Array, Array, Matrix":function(e,t,r){return v(i(e),i(t),r)},"Array, Matrix, Array":function(e,t,r){return v(i(e),t,i(r))},"Matrix, Array, Matrix":function(e,t,r){return v(e,i(t),r)},"Matrix, Array, Array":function(e,t,r){return v(e,i(t),i(r))},"Matrix, Matrix, Array":function(e,t,r){return v(e,t,i(r))},"Array, Array, Array":function(e,t,r){return v(i(e),i(t),i(r)).toArray()}});function v(e,t,v){for(var y=t.size()[0],g=e.size()[0],x=r(e),b=x.T,w=x.U,N=r(a(-1,t)),D=N.T,E=N.U,A=a(a(s(w),v),E),S=o(0,g),C=[],M=function(e,t){return u(e,t,1)},F=function(e,t){return u(e,t,0)},O=0;O1e-5){for(var T=F(f(A,c(S,O)),f(A,c(S,O+1))),B=0;B100)break}while(o(u(s,t))>1e-4);return{U:c,T:s}}})),fh="lyap",lh=Ee(fh,["typed","matrix","sylvester","multiply","transpose"],(function(e){var t=e.typed,r=e.matrix,n=e.sylvester,i=e.multiply,a=e.transpose;return t(fh,{"Matrix, Matrix":function(e,t){return n(e,a(e),i(-1,t))},"Array, Matrix":function(e,t){return n(r(e),a(r(e)),i(-1,t))},"Matrix, Array":function(e,t){return n(e,a(r(e)),r(i(-1,t)))},"Array, Array":function(e,t){return n(r(e),a(r(e)),r(i(-1,t))).toArray()}})})),ph=Ee("divide",["typed","matrix","multiply","equalScalar","divideScalar","inv"],(function(e){var t=e.typed,r=e.matrix,n=e.multiply,i=e.equalScalar,a=e.divideScalar,o=e.inv,u=Na({typed:t,equalScalar:i}),s=Ea({typed:t});return t("divide",ve({"Array | Matrix, Array | Matrix":function(e,t){return n(e,o(t))},"DenseMatrix, any":function(e,t){return s(e,t,a,!1)},"SparseMatrix, any":function(e,t){return u(e,t,a,!1)},"Array, any":function(e,t){return s(r(e),t,a,!1).valueOf()},"any, Array | Matrix":function(e,t){return n(e,o(t))}},a.signatures))})),mh="distance",hh=Ee(mh,["typed","addScalar","subtractScalar","divideScalar","multiplyScalar","deepEqual","sqrt","abs"],(function(e){var t=e.typed,r=e.addScalar,n=e.subtractScalar,i=e.multiplyScalar,o=e.divideScalar,u=e.deepEqual,s=e.sqrt,c=e.abs;return t(mh,{"Array, Array, Array":function(e,t,r){if(2===e.length&&2===t.length&&2===r.length){if(!l(e))throw new TypeError("Array with 2 numbers or BigNumbers expected for first argument");if(!l(t))throw new TypeError("Array with 2 numbers or BigNumbers expected for second argument");if(!l(r))throw new TypeError("Array with 2 numbers or BigNumbers expected for third argument");if(u(t,r))throw new TypeError("LinePoint1 should not be same with LinePoint2");var a=n(r[1],t[1]),o=n(t[0],r[0]),s=n(i(r[0],t[1]),i(t[0],r[1]));return v(e[0],e[1],a,o,s)}throw new TypeError("Invalid Arguments: Try again")},"Object, Object, Object":function(e,t,r){if(2===Object.keys(e).length&&2===Object.keys(t).length&&2===Object.keys(r).length){if(!l(e))throw new TypeError("Values of pointX and pointY should be numbers or BigNumbers");if(!l(t))throw new TypeError("Values of lineOnePtX and lineOnePtY should be numbers or BigNumbers");if(!l(r))throw new TypeError("Values of lineTwoPtX and lineTwoPtY should be numbers or BigNumbers");if(u(d(t),d(r)))throw new TypeError("LinePoint1 should not be same with LinePoint2");if("pointX"in e&&"pointY"in e&&"lineOnePtX"in t&&"lineOnePtY"in t&&"lineTwoPtX"in r&&"lineTwoPtY"in r){var a=n(r.lineTwoPtY,t.lineOnePtY),o=n(t.lineOnePtX,r.lineTwoPtX),s=n(i(r.lineTwoPtX,t.lineOnePtY),i(t.lineOnePtX,r.lineTwoPtY));return v(e.pointX,e.pointY,a,o,s)}throw new TypeError("Key names do not match")}throw new TypeError("Invalid Arguments: Try again")},"Array, Array":function(e,t){if(2===e.length&&3===t.length){if(!l(e))throw new TypeError("Array with 2 numbers or BigNumbers expected for first argument");if(!p(t))throw new TypeError("Array with 3 numbers or BigNumbers expected for second argument");return v(e[0],e[1],t[0],t[1],t[2])}if(3===e.length&&6===t.length){if(!p(e))throw new TypeError("Array with 3 numbers or BigNumbers expected for first argument");if(!h(t))throw new TypeError("Array with 6 numbers or BigNumbers expected for second argument");return y(e[0],e[1],e[2],t[0],t[1],t[2],t[3],t[4],t[5])}if(e.length===t.length&&e.length>0){if(!m(e))throw new TypeError("All values of an array should be numbers or BigNumbers");if(!m(t))throw new TypeError("All values of an array should be numbers or BigNumbers");return g(e,t)}throw new TypeError("Invalid Arguments: Try again")},"Object, Object":function(e,t){if(2===Object.keys(e).length&&3===Object.keys(t).length){if(!l(e))throw new TypeError("Values of pointX and pointY should be numbers or BigNumbers");if(!p(t))throw new TypeError("Values of xCoeffLine, yCoeffLine and constant should be numbers or BigNumbers");if("pointX"in e&&"pointY"in e&&"xCoeffLine"in t&&"yCoeffLine"in t&&"constant"in t)return v(e.pointX,e.pointY,t.xCoeffLine,t.yCoeffLine,t.constant);throw new TypeError("Key names do not match")}if(3===Object.keys(e).length&&6===Object.keys(t).length){if(!p(e))throw new TypeError("Values of pointX, pointY and pointZ should be numbers or BigNumbers");if(!h(t))throw new TypeError("Values of x0, y0, z0, a, b and c should be numbers or BigNumbers");if("pointX"in e&&"pointY"in e&&"x0"in t&&"y0"in t&&"z0"in t&&"a"in t&&"b"in t&&"c"in t)return y(e.pointX,e.pointY,e.pointZ,t.x0,t.y0,t.z0,t.a,t.b,t.c);throw new TypeError("Key names do not match")}if(2===Object.keys(e).length&&2===Object.keys(t).length){if(!l(e))throw new TypeError("Values of pointOneX and pointOneY should be numbers or BigNumbers");if(!l(t))throw new TypeError("Values of pointTwoX and pointTwoY should be numbers or BigNumbers");if("pointOneX"in e&&"pointOneY"in e&&"pointTwoX"in t&&"pointTwoY"in t)return g([e.pointOneX,e.pointOneY],[t.pointTwoX,t.pointTwoY]);throw new TypeError("Key names do not match")}if(3===Object.keys(e).length&&3===Object.keys(t).length){if(!p(e))throw new TypeError("Values of pointOneX, pointOneY and pointOneZ should be numbers or BigNumbers");if(!p(t))throw new TypeError("Values of pointTwoX, pointTwoY and pointTwoZ should be numbers or BigNumbers");if("pointOneX"in e&&"pointOneY"in e&&"pointOneZ"in e&&"pointTwoX"in t&&"pointTwoY"in t&&"pointTwoZ"in t)return g([e.pointOneX,e.pointOneY,e.pointOneZ],[t.pointTwoX,t.pointTwoY,t.pointTwoZ]);throw new TypeError("Key names do not match")}throw new TypeError("Invalid Arguments: Try again")},Array:function(e){if(!function(e){if(2===e[0].length&&f(e[0][0])&&f(e[0][1])){if(e.some((function(e){return 2!==e.length||!f(e[0])||!f(e[1])})))return!1}else{if(!(3===e[0].length&&f(e[0][0])&&f(e[0][1])&&f(e[0][2])))return!1;if(e.some((function(e){return 3!==e.length||!f(e[0])||!f(e[1])||!f(e[2])})))return!1}return!0}(e))throw new TypeError("Incorrect array format entered for pairwise distance calculation");return function(e){for(var t=[],r=[],n=[],i=0;i1&&Array.isArray(e[0])&&e.every((function(e){return Array.isArray(e)&&1===e.length}))?m(e):e}function x(e){return 2===e.length&&d(e[0])&&d(e[1])}function b(e){return 3===e.length&&d(e[0])&&d(e[1])&&d(e[2])}function w(e,t,r,n,i,o,u,c,l,p,m,h){var d=s(f(e,t),f(r,n)),v=s(f(i,o),f(u,c)),y=s(f(l,p),f(m,h));return a(a(d,v),y)}})),vh=Ee("sum",["typed","config","add","numeric"],(function(e){var t=e.typed,r=e.config,n=e.add,i=e.numeric;return t("sum",{"Array | Matrix":a,"Array | Matrix, number | BigNumber":function(e,t){try{return Gn(e,t,n)}catch(e){throw ks(e,"sum")}},"...":function(e){if(Un(e))throw new TypeError("Scalar values expected in function sum");return a(e)}});function a(e){var t;return $n(e,(function(e){try{t=void 0===t?e:n(t,e)}catch(t){throw ks(t,"sum",e)}})),void 0===t&&(t=i(0,r.number)),"string"==typeof t&&(t=i(t,r.number)),t}})),yh="cumsum",gh=Ee(yh,["typed","add","unaryPlus"],(function(e){var t=e.typed,r=e.add,n=e.unaryPlus;return t(yh,{Array:i,Matrix:function(e){return e.create(i(e.valueOf()))},"Array, number | BigNumber":o,"Matrix, number | BigNumber":function(e,t){return e.create(o(e.valueOf(),t))},"...":function(e){if(Un(e))throw new TypeError("All values expected to be scalar in function cumsum");return i(e)}});function i(e){try{return a(e)}catch(e){throw ks(e,yh)}}function a(e){if(0===e.length)return[];for(var t=[n(e[0])],i=1;i=r.length)throw new nn(t,r.length);try{return u(e,t)}catch(e){throw ks(e,yh)}}function u(e,t){var r,n,i;if(t<=0){var o=e[0][0];if(Array.isArray(o)){for(i=Ln(e),n=[],r=0;r0&&(o=e[c]);return s(o,n)}var f=a(e,(t-1)/2);return u(f)}catch(e){throw ks(e,"median")}}var u=t({"number | BigNumber | Complex | Unit":function(e){return e}}),s=t({"number | BigNumber | Complex | Unit, number | BigNumber | Complex | Unit":function(e,t){return n(r(e,t),2)}});return t(wh,{"Array | Matrix":o,"Array | Matrix, number | BigNumber":function(e,t){throw new Error("median(A, dim) is not yet supported")},"...":function(e){if(Un(e))throw new TypeError("Scalar values expected in function median");return o(e)}})})),Dh=Ee("mad",["typed","abs","map","median","subtract"],(function(e){var t=e.typed,r=e.abs,n=e.map,i=e.median,a=e.subtract;return t("mad",{"Array | Matrix":o,"...":function(e){return o(e)}});function o(e){if(0===(e=bn(e.valueOf())).length)throw new Error("Cannot calculate median absolute deviation (mad) of an empty array");try{var t=i(e);return i(n(e,(function(e){return r(a(e,t))})))}catch(e){throw e instanceof TypeError&&-1!==e.message.indexOf("median")?new TypeError(e.message.replace("median","mad")):ks(e,"mad")}}})),Eh="unbiased",Ah="variance",Sh=Ee(Ah,["typed","add","subtract","multiply","divide","apply","isNaN"],(function(e){var t=e.typed,r=e.add,n=e.subtract,i=e.multiply,o=e.divide,u=e.apply,s=e.isNaN;return t(Ah,{"Array | Matrix":function(e){return c(e,Eh)},"Array | Matrix, string":c,"Array | Matrix, number | BigNumber":function(e,t){return f(e,t,Eh)},"Array | Matrix, number | BigNumber, string":f,"...":function(e){return c(e,Eh)}});function c(e,t){var u,c=0;if(0===e.length)throw new SyntaxError("Function variance requires one or more parameters (0 provided)");if($n(e,(function(e){try{u=void 0===u?e:r(u,e),c++}catch(t){throw ks(t,"variance",e)}})),0===c)throw new Error("Cannot calculate variance of an empty array");var f=o(u,c);if(u=void 0,$n(e,(function(e){var t=n(e,f);u=void 0===u?i(t,t):r(u,i(t,t))})),s(u))return u;switch(t){case"uncorrected":return o(u,c);case"biased":return o(u,c+1);case"unbiased":var l=a(u)?u.mul(0):0;return 1===c?l:o(u,c-1);default:throw new Error('Unknown normalization "'+t+'". Choose "unbiased" (default), "uncorrected", or "biased".')}}function f(e,t,r){try{if(0===e.length)throw new SyntaxError("Function variance requires one or more parameters (0 provided)");return u(e,t,(function(e){return c(e,r)}))}catch(e){throw ks(e,"variance")}}})),Ch="quantileSeq",Mh=Ee(Ch,["typed","?bignumber","add","subtract","divide","multiply","partitionSelect","compare","isInteger","smaller","smallerEq","larger"],(function(e){var t=e.typed,r=e.bignumber,n=e.add,a=e.subtract,o=e.divide,u=e.multiply,s=e.partitionSelect,c=e.compare,f=e.isInteger,l=e.smaller,p=e.smallerEq,m=e.larger,h=ma({typed:t,isInteger:f});return t(Ch,{"Array | Matrix, number | BigNumber":function(e,t){return v(e,t,!1)},"Array | Matrix, number | BigNumber, number":function(e,t,r){return d(e,t,!1,r,v)},"Array | Matrix, number | BigNumber, boolean":v,"Array | Matrix, number | BigNumber, boolean, number":function(e,t,r,n){return d(e,t,r,n,v)},"Array | Matrix, Array | Matrix":function(e,t){return y(e,t,!1)},"Array | Matrix, Array | Matrix, number":function(e,t,r){return d(e,t,!1,r,y)},"Array | Matrix, Array | Matrix, boolean":y,"Array | Matrix, Array | Matrix, boolean, number":function(e,t,r,n){return d(e,t,r,n,y)}});function d(e,t,r,n,i){return h(e,n,(function(e){return i(e,t,r)}))}function v(e,t,a){var u,s=e.valueOf();if(l(t,0))throw new Error("N/prob must be non-negative");if(p(t,1))return i(t)?g(s,t,a):r(g(s,t,a));if(m(t,1)){if(!f(t))throw new Error("N must be a positive integer");if(m(t,4294967295))throw new Error("N must be less than or equal to 2^32-1, as that is the maximum length of an Array");var c=n(t,1);u=[];for(var h=0;l(h,t);h++){var d=o(h+1,c);u.push(g(s,d,a))}return i(t)?u:r(u)}}function y(e,t,r){for(var n=e.valueOf(),i=t.valueOf(),a=[],o=0;o0&&(p=o[y])}return n(u(p,a(1,v)),u(m,v))}})),Fh=Ee("std",["typed","map","sqrt","variance"],(function(e){var t=e.typed,r=e.map,n=e.sqrt,i=e.variance;return t("std",{"Array | Matrix":a,"Array | Matrix, string":a,"Array | Matrix, number | BigNumber":a,"Array | Matrix, number | BigNumber, string":a,"...":function(e){return a(e)}});function a(e,t){if(0===e.length)throw new SyntaxError("Function std requires one or more parameters (0 provided)");try{var a=i.apply(null,arguments);return p(a)?r(a,n):n(a)}catch(e){throw e instanceof TypeError&&-1!==e.message.indexOf(" variance")?new TypeError(e.message.replace(" variance"," std")):e}}})),Oh="corr",Th=Ee(Oh,["typed","matrix","mean","sqrt","sum","add","subtract","multiply","pow","divide"],(function(e){var t=e.typed,r=e.matrix,n=e.sqrt,i=e.sum,a=e.add,o=e.subtract,u=e.multiply,s=e.pow,c=e.divide;return t(Oh,{"Array, Array":function(e,t){return f(e,t)},"Matrix, Matrix":function(e,t){var n=f(e.toArray(),t.toArray());return Array.isArray(n)?r(n):n}});function f(e,t){var r=[];if(Array.isArray(e[0])&&Array.isArray(t[0])){if(e.length!==t.length)throw new SyntaxError("Dimension mismatch. Array A and B must have the same length.");for(var n=0;n>1;return Bh(e,r)*Bh(r+1,t)}function _h(e,t){if(!V(e)||e<0)throw new TypeError("Positive integer value expected in function combinations");if(!V(t)||t<0)throw new TypeError("Positive integer value expected in function combinations");if(t>e)throw new TypeError("k must be less than or equal to n");for(var r=e-t,n=1,i=2,a=t171?1/0:Bh(1,e-1);if(e<.5)return Math.PI/(Math.sin(Math.PI*e)*Ph(1-e));if(e>=171.35)return 1/0;if(e>85){var r=e*e,n=r*e,i=n*e,a=i*e;return Math.sqrt(2*Math.PI/e)*Math.pow(e/Math.E,e)*(1+1/(12*e)+1/(288*r)-139/(51840*n)-571/(2488320*i)+163879/(209018880*a)+5246819/(75246796800*a*e))}--e,t=Uh[0];for(var o=1;o=1;n--)r+=Hh[n]/(e+n);return $h+(e+.5)*Math.log(t)-t+Math.log(r)}Gh.signature="number";var Vh="gamma",Zh=Ee(Vh,["typed","config","multiplyScalar","pow","BigNumber","Complex"],(function(e){var t=e.typed,r=e.config,n=(e.multiplyScalar,e.pow,e.BigNumber),i=e.Complex;return t(Vh,{number:Ph,Complex:function e(t){if(0===t.im)return Ph(t.re);if(t.re<.5){var r=new i(1-t.re,-t.im),n=new i(Math.PI*t.re,Math.PI*t.im);return new i(Math.PI).div(n.sin()).div(e(r))}t=new i(t.re-1,t.im);for(var a=new i(Uh[0],0),o=1;o2;)s+=o-=2,u=u.times(s);return new n(u.toPrecision(n.precision))}})),Wh="lgamma",Yh=Ee(Wh,["Complex","typed"],(function(e){var t=e.Complex,r=e.typed,n=[-.029550653594771242,.00641025641025641,-.0019175269175269176,.0008417508417508417,-.0005952380952380953,.0007936507936507937,-.002777777777777778,.08333333333333333];return r(Wh,{number:Gh,Complex:function e(r){if(r.isNaN())return new t(NaN,NaN);if(0===r.im)return new t(Gh(r.re),0);if(r.re>=7||Math.abs(r.im)>=7)return i(r);if(r.re<=.1){var n=(s=6.283185307179586,(!0^((c=r.im)>0||!(c<0)&&1/c==1/0)?-s:s)*Math.floor(.5*r.re+.25)),o=r.mul(Math.PI).sin().log(),u=e(new t(1-r.re,-r.im));return new t(1.1447298858494002,n).sub(o).sub(u)}return r.im>=0?a(r):a(r.conjugate()).conjugate();var s,c},BigNumber:function(){throw new Error("mathjs doesn't yet provide an implementation of the algorithm lgamma for BigNumber")}});function i(e){for(var r=e.sub(.5).mul(e.log()).sub(e).add($h),i=new t(1,0).div(e),a=i.div(e),o=n[0],u=n[1],s=2*a.re,c=a.re*a.re+a.im*a.im,f=2;f<8;f++){var l=u;u=-c*o+n[f],o=s*o+l}var p=i.mul(a.mul(o).add(u));return r.add(p)}function a(e){var r=0,n=0,a=e;for(e=e.add(1);e.re<=7;){var o=(a=a.mul(e)).im<0?1:0;0!==o&&0===n&&r++,n=o,e=e.add(1)}return i(e).sub(a.log()).sub(new t(0,2*r*Math.PI*1))}})),Jh="factorial",Xh=Ee(Jh,["typed","gamma"],(function(e){var t=e.typed,r=e.gamma;return t(Jh,{number:function(e){if(e<0)throw new Error("Value must be non-negative");return r(e+1)},BigNumber:function(e){if(e.isNegative())throw new Error("Value must be non-negative");return r(e.plus(1))},"Array | Matrix":t.referToSelf((function(e){return function(t){return Hn(t,e)}}))})})),Qh="kldivergence",Kh=Ee(Qh,["typed","matrix","divide","sum","multiply","map","dotDivide","log","isNumeric"],(function(e){var t=e.typed,r=e.matrix,n=e.divide,i=e.sum,a=e.multiply,o=e.map,u=e.dotDivide,s=e.log,c=e.isNumeric;return t(Qh,{"Array, Array":function(e,t){return f(r(e),r(t))},"Matrix, Array":function(e,t){return f(e,r(t))},"Array, Matrix":function(e,t){return f(r(e),t)},"Matrix, Matrix":function(e,t){return f(e,t)}});function f(e,t){var r=t.size().length,f=e.size().length;if(r>1)throw new Error("first object must be one dimensional");if(f>1)throw new Error("second object must be one dimensional");if(r!==f)throw new Error("Length of two vectors must be equal");if(0===i(e))throw new Error("Sum of elements in first object must be non zero");if(0===i(t))throw new Error("Sum of elements in second object must be non zero");var l=n(e,i(e)),p=n(t,i(t)),m=i(a(l,o(u(l,p),(function(e){return s(e)}))));return c(m)?m:Number.NaN}})),ed="multinomial",td=Ee(ed,["typed","add","divide","multiply","factorial","isInteger","isPositive"],(function(e){var t=e.typed,r=e.add,n=e.divide,i=e.multiply,a=e.factorial,o=e.isInteger,u=e.isPositive;return t(ed,{"Array | Matrix":function(e){var t=0,s=1;return $n(e,(function(e){if(!o(e)||!u(e))throw new TypeError("Positive integer value expected in function multinomial");t=r(t,e),s=i(s,a(e))})),n(a(t),s)}})})),rd="permutations",nd=Ee(rd,["typed","factorial"],(function(e){var t=e.typed,r=e.factorial;return t(rd,{"number | BigNumber":r,"number, number":function(e,t){if(!V(e)||e<0)throw new TypeError("Positive integer value expected in function permutations");if(!V(t)||t<0)throw new TypeError("Positive integer value expected in function permutations");if(t>e)throw new TypeError("second argument k must be less than or equal to first argument n");return Bh(e-t+1,e)},"BigNumber, BigNumber":function(e,t){var r,n;if(!id(e)||!id(t))throw new TypeError("Positive integer value expected in function permutations");if(t.gt(e))throw new TypeError("second argument k must be less than or equal to first argument n");for(r=e.mul(0).add(1),n=e.minus(t).plus(1);n.lte(e);n=n.plus(1))r=r.times(n);return r}})}));function id(e){return e.isInteger()&&e.gte(0)}r(2227);var ad=r(6377),od=ad(Date.now());function ud(e){var t,r;return t=null===(r=e)?od:ad(String(r)),function(){return t()}}var sd="pickRandom",cd=Ee(sd,["typed","config","?on"],(function(e){var t=e.typed,r=e.config,n=e.on,a=ud(r.randomSeed);return n&&n("config",(function(e,t){e.randomSeed!==t.randomSeed&&(a=ud(e.randomSeed))})),t(sd,{"Array | Matrix":function(e){return o(e,{})},"Array | Matrix, Object":function(e,t){return o(e,t)},"Array | Matrix, number":function(e,t){return o(e,{number:t})},"Array | Matrix, Array | Matrix":function(e,t){return o(e,{weights:t})},"Array | Matrix, Array | Matrix, number":function(e,t,r){return o(e,{number:r,weights:t})},"Array | Matrix, number, Array | Matrix":function(e,t,r){return o(e,{number:t,weights:r})}});function o(e,t){var r=t.number,n=t.weights,o=t.elementWise,u=void 0===o||o,s=void 0===r;s&&(r=1);var c=l(e)?e.create:l(n)?n.create:null;e=e.valueOf(),n&&(n=n.valueOf()),!0===u&&(e=bn(e),n=bn(n));var f=0;if(void 0!==n){if(n.length!==e.length)throw new Error("Weights must have the same length as possibles");for(var p=0,m=n.length;p1)for(var n=0,i=e.shift();nv)return m[d][v];for(var y=0;y<=d;++y)if(m[y]||(m[y]=[h(0===y?1:0)]),0!==y)for(var g=m[y],x=m[y-1],b=g.length;b<=y&&b<=v;++b)g[b]=b===y?1:r(n(h(b),x[b]),x[b-1]);return m[d][v]}})})),yd="bellNumbers",gd=Ee(yd,["typed","addScalar","isNegative","isInteger","stirlingS2"],(function(e){var t=e.typed,r=e.addScalar,n=e.isNegative,i=e.isInteger,a=e.stirlingS2;return t(yd,{"number | BigNumber":function(e){if(!i(e)||n(e))throw new TypeError("Non-negative integer value expected in function bellNumbers");for(var t=0,o=0;o<=e;o++)t=r(t,a(e,o));return t}})})),xd="catalan",bd=Ee(xd,["typed","addScalar","divideScalar","multiplyScalar","combinations","isNegative","isInteger"],(function(e){var t=e.typed,r=e.addScalar,n=e.divideScalar,i=e.multiplyScalar,a=e.combinations,o=e.isNegative,u=e.isInteger;return t(xd,{"number | BigNumber":function(e){if(!u(e)||o(e))throw new TypeError("Non-negative integer value expected in function catalan");return n(a(i(e,2),e),r(e,1))}})})),wd="composition",Nd=Ee(wd,["typed","addScalar","combinations","isNegative","isPositive","isInteger","larger"],(function(e){var t=e.typed,r=e.addScalar,n=e.combinations,i=e.isPositive,a=(e.isNegative,e.isInteger),o=e.larger;return t(wd,{"number | BigNumber, number | BigNumber":function(e,t){if(!(a(e)&&i(e)&&a(t)&&i(t)))throw new TypeError("Positive integer value expected in function composition");if(o(t,e))throw new TypeError("k must be less than or equal to n in function composition");return n(r(e,-1),r(t,-1))}})})),Dd="leafCount",Ed=Ee(Dd,["parse","typed"],(function(e){function t(e){var r=0;return e.forEach((function(e){r+=t(e)})),r||1}return e.parse,(0,e.typed)(Dd,{Node:function(e){return t(e)}})}));function Ad(e){return T(e)||q(e)&&e.isUnary()&&T(e.args[0])}function Sd(e){return!!T(e)||!(!k(e)&&!q(e)||!e.args.every(Sd))||!(!j(e)||!Sd(e.content))}function Cd(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function Md(e){for(var t=1;t2&&void 0!==arguments[2]?arguments[2]:u,n=o;if("string"==typeof e?n=e:q(e)?n=e.fn.toString():k(e)?n=e.name:j(e)&&(n="paren"),Ne(r,n)){var i=r[n];if(Ne(i,t))return i[t];if(Ne(u,n))return u[n][t]}if(Ne(r,o)){var a=r[o];return Ne(a,t)?a[t]:u[o][t]}if(Ne(u,n)){var s=u[n];if(Ne(s,t))return s[t]}return u[o][t]}function c(e){return s(e,"associative",arguments.length>1&&void 0!==arguments[1]?arguments[1]:u)}function f(e,t){var r,n=[];return c(e,t)?(r=e.op,function e(t){for(var i=0;i1&&void 0!==arguments[1]?arguments[1]:u)},isAssociative:c,mergeContext:function(e,t){var r=Md({},e);for(var n in t)Ne(e,n)?r[n]=Md(Md({},t[n]),e[n]):r[n]=t[n];return r},flatten:function e(t,r){if(!t.args||0===t.args.length)return t;t.args=f(t,r);for(var n=0;n2&&c(t,r)){for(var o=t.args.pop();t.args.length>0;)o=n([t.args.pop(),o]);t.args=o.args}}},unflattenl:function e(t,r){if(t.args&&0!==t.args.length){for(var n=l(t),i=t.args.length,a=0;a2&&c(t,r)){for(var o=t.args.shift();t.args.length>0;)o=n([o,t.args.shift()]);t.args=o.args}}},defaultContext:u,realContext:{divide:{total:a},log:{total:a}},positiveContext:{subtract:{total:a},abs:{trivial:i},log:{total:i}}}})),Od=Ee("simplify",["config","typed","parse","add","subtract","multiply","divide","pow","isZero","equal","resolve","simplifyConstant","simplifyCore","?fraction","?bignumber","mathWithTransform","matrix","AccessorNode","ArrayNode","ConstantNode","FunctionNode","IndexNode","ObjectNode","OperatorNode","ParenthesisNode","SymbolNode"],(function(e){e.config;var r=e.typed,n=e.parse,i=(e.add,e.subtract,e.multiply,e.divide,e.pow,e.isZero,e.equal),a=e.resolve,o=e.simplifyConstant,u=e.simplifyCore,s=(e.fraction,e.bignumber,e.mathWithTransform,e.matrix,e.AccessorNode),c=e.ArrayNode,f=e.ConstantNode,l=e.FunctionNode,p=e.IndexNode,m=e.ObjectNode,h=e.OperatorNode,d=e.ParenthesisNode,v=e.SymbolNode,y=Fd({FunctionNode:l,OperatorNode:h,SymbolNode:v}),g=y.hasProperty,x=y.isCommutative,b=y.isAssociative,w=y.mergeContext,N=y.flatten,D=y.unflattenr,E=y.unflattenl,A=y.createMakeNodeFunction,S=y.defaultContext,C=y.realContext,M=y.positiveContext;r.addConversion({from:"Object",to:"Map",convert:Ue});var F=r("simplify",{Node:R,"Node, Map":function(e,t){return R(e,!1,t)},"Node, Map, Object":function(e,t,r){return R(e,!1,t,r)},"Node, Array":R,"Node, Array, Map":R,"Node, Array, Map, Object":R});function O(e){return e.transform((function(e,t,r){return j(e)?O(e.content):e}))}r.removeConversion({from:"Object",to:"Map",convert:Ue}),F.defaultContext=S,F.realContext=C,F.positiveContext=M;var B={true:!0,false:!0,e:!0,i:!0,Infinity:!0,LN2:!0,LN10:!0,LOG2E:!0,LOG10E:!0,NaN:!0,phi:!0,pi:!0,SQRT1_2:!0,SQRT2:!0,tau:!0};function _(e,t){var r={};if(e.s){var i=e.s.split("->");if(2!==i.length)throw SyntaxError("Could not parse rule: "+e.s);r.l=i[0],r.r=i[1]}else r.l=e.l,r.r=e.r;r.l=O(n(r.l)),r.r=O(n(r.r));for(var a=0,o=["imposeContext","repeat","assuming"];a n+-n1",assuming:{subtract:{total:!0}}},{s:"n-n -> 0",assuming:{subtract:{total:!1}}},{s:"-(cl*v) -> v * (-cl)",assuming:{multiply:{commutative:!0},subtract:{total:!0}}},{s:"-(cl*v) -> (-cl) * v",assuming:{multiply:{commutative:!1},subtract:{total:!0}}},{s:"-(v*cl) -> v * (-cl)",assuming:{multiply:{commutative:!1},subtract:{total:!0}}},{l:"-(n1/n2)",r:"-n1/n2"},{l:"-v",r:"v * (-1)"},{l:"(n1 + n2)*(-1)",r:"n1*(-1) + n2*(-1)",repeat:!0},{l:"n/n1^n2",r:"n*n1^-n2"},{l:"n/n1",r:"n*n1^-1"},{s:"(n1*n2)^n3 -> n1^n3 * n2^n3",assuming:{multiply:{commutative:!0}}},{s:"(n1*n2)^(-1) -> n2^(-1) * n1^(-1)",assuming:{multiply:{commutative:!1}}},{s:"(n ^ n1) ^ n2 -> n ^ (n1 * n2)",assuming:{divide:{total:!0}}},{l:" vd * ( vd * n1 + n2)",r:"vd^2 * n1 + vd * n2"},{s:" vd * (vd^n4 * n1 + n2) -> vd^(1+n4) * n1 + vd * n2",assuming:{divide:{total:!0}}},{s:"vd^n3 * ( vd * n1 + n2) -> vd^(n3+1) * n1 + vd^n3 * n2",assuming:{divide:{total:!0}}},{s:"vd^n3 * (vd^n4 * n1 + n2) -> vd^(n3+n4) * n1 + vd^n3 * n2",assuming:{divide:{total:!0}}},{l:"n*n",r:"n^2"},{s:"n * n^n1 -> n^(n1+1)",assuming:{divide:{total:!0}}},{s:"n^n1 * n^n2 -> n^(n1+n2)",assuming:{divide:{total:!0}}},o,{s:"n+n -> 2*n",assuming:{add:{total:!0}}},{l:"n+-n",r:"0"},{l:"vd*n + vd",r:"vd*(n+1)"},{l:"n3*n1 + n3*n2",r:"n3*(n1+n2)"},{l:"n3^(-n4)*n1 + n3 * n2",r:"n3^(-n4)*(n1 + n3^(n4+1) *n2)"},{l:"n3^(-n4)*n1 + n3^n5 * n2",r:"n3^(-n4)*(n1 + n3^(n4+n5)*n2)"},{s:"n*vd + vd -> (n+1)*vd",assuming:{multiply:{commutative:!1}}},{s:"vd + n*vd -> (1+n)*vd",assuming:{multiply:{commutative:!1}}},{s:"n1*n3 + n2*n3 -> (n1+n2)*n3",assuming:{multiply:{commutative:!1}}},{s:"n^n1 * n -> n^(n1+1)",assuming:{divide:{total:!0},multiply:{commutative:!1}}},{s:"n1*n3^(-n4) + n2 * n3 -> (n1 + n2*n3^(n4 + 1))*n3^(-n4)",assuming:{multiply:{commutative:!1}}},{s:"n1*n3^(-n4) + n2 * n3^n5 -> (n1 + n2*n3^(n4 + n5))*n3^(-n4)",assuming:{multiply:{commutative:!1}}},{l:"n*cd + cd",r:"(n+1)*cd"},{s:"cd*n + cd -> cd*(n+1)",assuming:{multiply:{commutative:!1}}},{s:"cd + cd*n -> cd*(1+n)",assuming:{multiply:{commutative:!1}}},o,{s:"(-n)*n1 -> -(n*n1)",assuming:{subtract:{total:!0}}},{s:"n1*(-n) -> -(n1*n)",assuming:{subtract:{total:!0},multiply:{commutative:!1}}},{s:"ce+ve -> ve+ce",assuming:{add:{commutative:!0}},imposeContext:{add:{commutative:!1}}},{s:"vd*cd -> cd*vd",assuming:{multiply:{commutative:!0}},imposeContext:{multiply:{commutative:!1}}},{l:"n+-n1",r:"n-n1"},{l:"n+-(n1)",r:"n-(n1)"},{s:"n*(n1^-1) -> n/n1",assuming:{multiply:{commutative:!0}}},{s:"n*n1^-n2 -> n/n1^n2",assuming:{multiply:{commutative:!0}}},{s:"n^-1 -> 1/n",assuming:{multiply:{commutative:!0}}},{l:"n^1",r:"n"},{s:"n*(n1/n2) -> (n*n1)/n2",assuming:{multiply:{associative:!0}}},{s:"n-(n1+n2) -> n-n1-n2",assuming:{addition:{associative:!0,commutative:!0}}},{l:"1*n",r:"n",imposeContext:{multiply:{commutative:!0}}},{s:"n1/(n2/n3) -> (n1*n3)/n2",assuming:{multiply:{associative:!0}}},{l:"n1/(-n2)",r:"-n1/n2"}];var k=0;function I(){return new v("_p"+k++)}function R(e,r){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:Le(),i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=i.consoleDebug;r=function(e,r){for(var n=[],i=0;i ").concat(r[l].r.toString()))),o){var m=u.toString({parenthesis:"all"});m!==f&&(console.log("Applying",p,"produced",m),f=m)}E(u,i.context)}c=u.toString({parenthesis:"all"})}return u}function z(e,t,r){var n=e;if(e)for(var i=0;i=2&&2===e.args.length){for(var o=function(e,t){var r,n,i=[],a=A(e);if(x(e,t))for(var o=0;o1&&(s=a(e.args.slice(0,u))),r=1===(n=e.args.slice(u)).length?n[0]:a(n),i.push(a([s,r]))}return i}(t,r),u=[],s=0;s2)throw Error("Unexpected non-binary associative function: "+e.toString());return[]}for(var p=[],m=0;m2)throw new Error("permuting >2 commutative non-associative rule arguments not yet implemented");var y=$(e.args[0],t.args[1],r);if(0===y.length)return[];var g=$(e.args[1],t.args[0],r);if(0===g.length)return[];p=[y,g]}a=function(e){if(0===e.length)return e;for(var t=e.reduce(L),r=[],n={},i=0;i="a"&&e.name[1]<="z"?e.name.substring(0,2):e.name[0]){case"n":case"_p":a[0].placeholders[e.name]=t;break;case"c":case"cl":if(!T(t))return[];a[0].placeholders[e.name]=t;break;case"v":if(T(t))return[];a[0].placeholders[e.name]=t;break;case"vl":if(!U(t))return[];a[0].placeholders[e.name]=t;break;case"cd":if(!Ad(t))return[];a[0].placeholders[e.name]=t;break;case"vd":if(Ad(t))return[];a[0].placeholders[e.name]=t;break;case"ce":if(!Sd(t))return[];a[0].placeholders[e.name]=t;break;case"ve":if(Sd(t))return[];a[0].placeholders[e.name]=t;break;default:throw new Error("Invalid symbol in rule: "+e.name)}}else{if(!(e instanceof f))return[];if(!i(e.value,t.value))return[]}return a}function H(e,t){if(e instanceof f&&t instanceof f){if(!i(e.value,t.value))return!1}else if(e instanceof v&&t instanceof v){if(e.name!==t.name)return!1}else{if(!(e instanceof h&&t instanceof h||e instanceof l&&t instanceof l))return!1;if(e instanceof h){if(e.op!==t.op||e.fn!==t.fn)return!1}else if(e instanceof l&&e.name!==t.name)return!1;if(e.args.length!==t.args.length)return!1;for(var r=0;r=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==r.return||r.return()}finally{if(u)throw a}}}}function Bd(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r1?(v=_(y,E,g,r),S.unshift(v),v=_(y,S,g,r)):v=_(y,s,g,r)}else v=_(y,s,g,r);else v=_(y,s=e.args.map((function(e){return k(e,r)})),g,r);return v;case"ParenthesisNode":return k(e.content,r);case"AccessorNode":return function(e,t,r){if(!I(t))return new c(M(e),M(t));if(C(e)||l(e)){for(var n=Array.from(t.dimensions);n.length>0;)if(T(n[0])&&"string"!=typeof n[0].value){var i=O(n.shift().value,r);C(e)?e=e.items[i-1]:(e=e.valueOf()[i-1])instanceof Array&&(e=a(e))}else{if(!(n.length>1&&T(n[1])&&"string"!=typeof n[1].value))break;var o,u=O(n[1].value,r),s=[],m=C(e)?e.items:e.valueOf(),d=Td(m);try{for(d.s();!(o=d.n()).done;){var v=o.value;if(C(v))s.push(v.items[u-1]);else{if(!l(e))break;s.push(v[u-1])}}}catch(e){d.e(e)}finally{d.f()}if(s.length!==m.length)break;e=C(e)?new f(s):a(s),n.splice(1,1)}return n.length===t.dimensions.length?new c(M(e),t):n.length>0?(t=new h(n),new c(M(e),t)):e}if(z(e)&&1===t.dimensions.length&&T(t.dimensions[0])){var y=t.dimensions[0].value;return y in e.properties?e.properties[y]:new p}return new c(M(e),t)}(k(e.object,r),k(e.index,r),r);case"ArrayNode":var B=e.items.map((function(e){return k(e,r)}));return B.some(R)?new f(B.map(M)):a(B);case"IndexNode":return new h(e.dimensions.map((function(e){return D(e,r)})));case"ObjectNode":var j={};for(var P in e.properties)j[P]=D(e.properties[P],r);return new d(j);default:throw new Error("Unimplemented node type in simplifyConstant: ".concat(e.type))}}return D})),kd="simplifyCore",Id=Ee(kd,["typed","parse","equal","isZero","add","subtract","multiply","divide","pow","AccessorNode","ArrayNode","ConstantNode","FunctionNode","IndexNode","ObjectNode","OperatorNode","ParenthesisNode","SymbolNode"],(function(e){var t=e.typed,r=(e.parse,e.equal),n=e.isZero,i=(e.add,e.subtract,e.multiply,e.divide,e.pow,e.AccessorNode),a=e.ArrayNode,o=e.ConstantNode,u=e.FunctionNode,s=e.IndexNode,c=e.ObjectNode,f=e.OperatorNode,l=(e.ParenthesisNode,e.SymbolNode),p=new o(0),m=new o(1),h=new o(!0),d=new o(!1);function v(e){return q(e)&&["and","not","or"].includes(e.op)}var y=Fd({FunctionNode:u,OperatorNode:f,SymbolNode:l}),g=y.hasProperty,x=y.isCommutative;function b(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=t?t.context:void 0;if(g(e,"trivial",o)){if(k(e)&&1===e.args.length)return b(e.args[0],t);var l=!1,y=0;if(e.forEach((function(e){1==++y&&(l=b(e,t))})),1===y)return l}var w=e;if(k(w)){var N=function(e){var t,r="OperatorNode:"+e,n=function(e,t){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,t){if(e){if("string"==typeof e)return Ep(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?Ep(e,t):void 0}}(e))||t&&e&&"number"==typeof e.length){r&&(e=r);var n=0,i=function(){};return{s:i,n:function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==r.return||r.return()}finally{if(u)throw a}}}}(Ap);try{for(n.s();!(t=n.n()).done;){var i=t.value;if(r in i)return i[r].op}}catch(e){n.e(e)}finally{n.f()}return null}(w.name);if(!N)return new u(b(w.fn),w.args.map((function(e){return b(e,t)})));if(w.args.length>2&&g(w,"associative",o))for(;w.args.length>2;){var D=w.args.pop(),E=w.args.pop();w.args.push(new f(N,w.name,[D,E]))}w=new f(N,w.name,w.args)}if(q(w)&&w.isUnary()){var A=b(w.args[0],t);if("~"===w.op&&q(A)&&A.isUnary()&&"~"===A.op)return A.args[0];if("not"===w.op&&q(A)&&A.isUnary()&&"not"===A.op&&v(A.args[0]))return A.args[0];var M=!0;if("-"===w.op&&q(A)&&(A.isBinary()&&"subtract"===A.fn&&(w=new f("-","subtract",[A.args[1],A.args[0]]),M=!1),A.isUnary()&&"-"===A.op))return A.args[0];if(M)return new f(w.op,w.fn,[A])}if(q(w)&&w.isBinary()){var F=b(w.args[0],t),O=b(w.args[1],t);if("+"===w.op){if(T(F)&&n(F.value))return O;if(T(O)&&n(O.value))return F;q(O)&&O.isUnary()&&"-"===O.op&&(O=O.args[0],w=new f("-","subtract",[F,O]))}if("-"===w.op)return q(O)&&O.isUnary()&&"-"===O.op?b(new f("+","add",[F,O.args[0]]),t):T(F)&&n(F.value)?b(new f("-","unaryMinus",[O])):T(O)&&n(O.value)?F:new f(w.op,w.fn,[F,O]);if("*"===w.op){if(T(F)){if(n(F.value))return p;if(r(F.value,1))return O}if(T(O)){if(n(O.value))return p;if(r(O.value,1))return F;if(x(w,o))return new f(w.op,w.fn,[O,F],w.implicit)}return new f(w.op,w.fn,[F,O],w.implicit)}if("/"===w.op)return T(F)&&n(F.value)?p:T(O)&&r(O.value,1)?F:new f(w.op,w.fn,[F,O]);if("^"===w.op&&T(O)){if(n(O.value))return m;if(r(O.value,1))return F}if("and"===w.op){if(T(F)){if(!F.value)return d;if(v(O))return O}if(T(O)){if(!O.value)return d;if(v(F))return F}}if("or"===w.op){if(T(F)){if(F.value)return h;if(v(O))return O}if(T(O)){if(O.value)return h;if(v(F))return F}}return new f(w.op,w.fn,[F,O])}if(q(w))return new f(w.op,w.fn,w.args.map((function(e){return b(e,t)})));if(C(w))return new a(w.items.map((function(e){return b(e,t)})));if(S(w))return new i(b(w.object,t),b(w.index,t));if(I(w))return new s(w.dimensions.map((function(e){return b(e,t)})));if(z(w)){var B={};for(var _ in w.properties)B[_]=b(w.properties[_],t);return new c(B)}return w}return t(kd,{Node:b,"Node,Object":b})})),Rd=Ee("resolve",["typed","parse","ConstantNode","FunctionNode","OperatorNode","ParenthesisNode"],(function(e){var t=e.typed,r=e.parse,n=e.ConstantNode,i=e.FunctionNode,a=e.OperatorNode,o=e.ParenthesisNode;function u(e,t){var s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:new Set;if(!t)return e;if(U(e)){if(s.has(e.name)){var c=Array.from(s).join(", ");throw new ReferenceError("recursive loop of variable definitions among {".concat(c,"}"))}var f=t.get(e.name);if(R(f)){var l=new Set(s);return l.add(e.name),u(f,t,l)}return"number"==typeof f?r(String(f)):void 0!==f?new n(f):e}if(q(e)){var p=e.args.map((function(e){return u(e,t,s)}));return new a(e.op,e.fn,p,e.implicit)}if(j(e))return new o(u(e.content,t,s));if(k(e)){var m=e.args.map((function(e){return u(e,t,s)}));return new i(e.name,m)}return e.map((function(e){return u(e,t,s)}))}return t("resolve",{Node:u,"Node, Map | null | undefined":u,"Node, Object":function(e,t){return u(e,Ue(t))},"Array | Matrix":t.referToSelf((function(e){return function(t){return t.map((function(t){return e(t)}))}})),"Array | Matrix, null | undefined":t.referToSelf((function(e){return function(t){return t.map((function(t){return e(t)}))}})),"Array, Object":t.referTo("Array,Map",(function(e){return function(t,r){return e(t,Ue(r))}})),"Matrix, Object":t.referTo("Matrix,Map",(function(e){return function(t,r){return e(t,Ue(r))}})),"Array | Matrix, Map":t.referToSelf((function(e){return function(t,r){return t.map((function(t){return e(t,r)}))}}))})})),zd="symbolicEqual",qd=Ee(zd,["parse","simplify","typed","OperatorNode"],(function(e){e.parse;var t=e.simplify,r=e.typed,n=e.OperatorNode;function i(e,r){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},a=new n("-","subtract",[e,r]),o=t(a,{},i);return T(o)&&!o.value}return r(zd,{"Node, Node":i,"Node, Node, Object":i})})),jd="derivative",Pd=Ee(jd,["typed","config","parse","simplify","equal","isZero","numeric","ConstantNode","FunctionNode","OperatorNode","ParenthesisNode","SymbolNode"],(function(e){var t=e.typed,r=e.config,n=e.parse,i=e.simplify,a=e.equal,o=e.isZero,u=e.numeric,s=e.ConstantNode,c=e.FunctionNode,f=e.OperatorNode,l=e.ParenthesisNode,p=e.SymbolNode;function m(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{simplify:!0},n={};v(n,e,t.name);var a=y(e,n);return r.simplify?i(a):a}t.addConversion({from:"identifier",to:"SymbolNode",convert:n});var h=t(jd,{"Node, SymbolNode":m,"Node, SymbolNode, Object":m});t.removeConversion({from:"identifier",to:"SymbolNode",convert:n}),h._simplify=!0,h.toTex=function(e){return d.apply(null,e.args)};var d=t("_derivTex",{"Node, SymbolNode":function(e,t){return T(e)&&"string"===H(e.value)?d(n(e.value).toString(),t.toString(),1):d(e.toTex(),t.toString(),1)},"Node, ConstantNode":function(e,t){if("string"===H(t.value))return d(e,n(t.value));throw new Error("The second parameter to 'derivative' is a non-string constant")},"Node, SymbolNode, ConstantNode":function(e,t,r){return d(e.toString(),t.name,r.value)},"string, string, number":function(e,t,r){return(1===r?"{d\\over d"+t+"}":"{d^{"+r+"}\\over d"+t+"^{"+r+"}}")+"\\left[".concat(e,"\\right]")}}),v=t("constTag",{"Object, ConstantNode, string":function(e,t){return e[t]=!0,!0},"Object, SymbolNode, string":function(e,t,r){return t.name!==r&&(e[t]=!0,!0)},"Object, ParenthesisNode, string":function(e,t,r){return v(e,t.content,r)},"Object, FunctionAssignmentNode, string":function(e,t,r){return-1===t.params.indexOf(r)?(e[t]=!0,!0):v(e,t.expr,r)},"Object, FunctionNode | OperatorNode, string":function(e,t,r){if(t.args.length>0){for(var n=v(e,t.args[0],r),i=1;i0){var n=e.args.filter((function(e){return void 0===t[e]})),i=1===n.length?n[0]:new f("*","multiply",n),u=r.concat(y(i,t));return new f("*","multiply",u)}return new f("+","add",e.args.map((function(r){return new f("*","multiply",e.args.map((function(e){return e===r?y(e,t):e.clone()})))})))}if("/"===e.op&&e.isBinary()){var s=e.args[0],l=e.args[1];return void 0!==t[l]?new f("/","divide",[y(s,t),l]):void 0!==t[s]?new f("*","multiply",[new f("-","unaryMinus",[s]),new f("/","divide",[y(l,t),new f("^","pow",[l.clone(),g(2)])])]):new f("/","divide",[new f("-","subtract",[new f("*","multiply",[y(s,t),l.clone()]),new f("*","multiply",[s.clone(),y(l,t)])]),new f("^","pow",[l.clone(),g(2)])])}if("^"===e.op&&e.isBinary()){var p=e.args[0],m=e.args[1];if(void 0!==t[p])return T(p)&&(o(p.value)||a(p.value,1))?g(0):new f("*","multiply",[e,new f("*","multiply",[new c("log",[p.clone()]),y(m.clone(),t)])]);if(void 0!==t[m]){if(T(m)){if(o(m.value))return g(0);if(a(m.value,1))return y(p,t)}var h=new f("^","pow",[p.clone(),new f("-","subtract",[m,g(1)])]);return new f("*","multiply",[m.clone(),new f("*","multiply",[y(p,t),h])])}return new f("*","multiply",[new f("^","pow",[p.clone(),m.clone()]),new f("+","add",[new f("*","multiply",[y(p,t),new f("/","divide",[m.clone(),p.clone()])]),new f("*","multiply",[y(m,t),new c("log",[p.clone()])])])])}throw new Error('Operator "'+e.op+'" is not supported by derivative, or a wrong number of arguments is passed')}});function g(e,t){return new s(u(e,t||r.number))}return h})),Ld="rationalize",Ud=Ee(Ld,["config","typed","equal","isZero","add","subtract","multiply","divide","pow","parse","simplifyConstant","simplifyCore","simplify","?bignumber","?fraction","mathWithTransform","matrix","AccessorNode","ArrayNode","ConstantNode","FunctionNode","IndexNode","ObjectNode","OperatorNode","SymbolNode","ParenthesisNode"],(function(e){e.config;var t=e.typed,r=(e.equal,e.isZero,e.add,e.subtract,e.multiply,e.divide,e.pow,e.parse,e.simplifyConstant),n=e.simplifyCore,i=e.simplify,a=(e.fraction,e.bignumber,e.mathWithTransform,e.matrix,e.AccessorNode,e.ArrayNode,e.ConstantNode),o=(e.FunctionNode,e.IndexNode,e.ObjectNode,e.OperatorNode),u=e.SymbolNode;function s(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},a=arguments.length>2&&void 0!==arguments[2]&&arguments[2],o=function(){var e=[n,{l:"n+n",r:"2*n"},{l:"n+-n",r:"0"},r,{l:"n*(n1^-1)",r:"n/n1"},{l:"n*n1^-n2",r:"n/n1^n2"},{l:"n1^-1",r:"1/n1"},{l:"n*(n1/n2)",r:"(n*n1)/n2"},{l:"1*n",r:"n"}],t=[{l:"(-n1)/(-n2)",r:"n1/n2"},{l:"(-n1)*(-n2)",r:"n1*n2"},{l:"n1--n2",r:"n1+n2"},{l:"n1-n2",r:"n1+(-n2)"},{l:"(n1+n2)*n3",r:"(n1*n3 + n2*n3)"},{l:"n1*(n2+n3)",r:"(n1*n2+n1*n3)"},{l:"c1*n + c2*n",r:"(c1+c2)*n"},{l:"c1*n + n",r:"(c1+1)*n"},{l:"c1*n - c2*n",r:"(c1-c2)*n"},{l:"c1*n - n",r:"(c1-1)*n"},{l:"v/c",r:"(1/c)*v"},{l:"v/-c",r:"-(1/c)*v"},{l:"-v*-c",r:"c*v"},{l:"-v*c",r:"-c*v"},{l:"v*-c",r:"-c*v"},{l:"v*c",r:"c*v"},{l:"-(-n1*n2)",r:"(n1*n2)"},{l:"-(n1*n2)",r:"(-n1*n2)"},{l:"-(-n1+n2)",r:"(n1-n2)"},{l:"-(n1+n2)",r:"(-n1-n2)"},{l:"(n1^n2)^n3",r:"(n1^(n2*n3))"},{l:"-(-n1/n2)",r:"(n1/n2)"},{l:"-(n1/n2)",r:"(-n1/n2)"}],i=[{l:"(n1/(n2/n3))",r:"((n1*n3)/n2)"},{l:"(n1/n2/n3)",r:"(n1/(n2*n3))"}],a={};return a.firstRules=e.concat(t,i),a.distrDivRules=[{l:"(n1/n2 + n3/n4)",r:"((n1*n4 + n3*n2)/(n2*n4))"},{l:"(n1/n2 + n3)",r:"((n1 + n3*n2)/n2)"},{l:"(n1 + n2/n3)",r:"((n1*n3 + n2)/n3)"}],a.sucDivRules=i,a.firstRulesAgain=e.concat(t),a.finalRules=[n,{l:"n*-n",r:"-n^2"},{l:"n*n",r:"n^2"},r,{l:"n*-n^n1",r:"-n^(n1+1)"},{l:"n*n^n1",r:"n^(n1+1)"},{l:"n^n1*-n^n2",r:"-n^(n1+n2)"},{l:"n^n1*n^n2",r:"n^(n1+n2)"},{l:"n^n1*-n",r:"-n^(n1+1)"},{l:"n^n1*n",r:"n^(n1+1)"},{l:"n^n1/-n",r:"-n^(n1-1)"},{l:"n^n1/n",r:"n^(n1-1)"},{l:"n/-n^n1",r:"-n^(1-n1)"},{l:"n/n^n1",r:"n^(1-n1)"},{l:"n^n1/-n^n2",r:"n^(n1-n2)"},{l:"n^n1/n^n2",r:"n^(n1-n2)"},{l:"n1+(-n2*n3)",r:"n1-n2*n3"},{l:"v*(-c)",r:"-c*v"},{l:"n1+-n2",r:"n1-n2"},{l:"v*c",r:"c*v"},{l:"(n1^n2)^n3",r:"(n1^(n2*n3))"}],a}(),u=function(e,t,r,n){var a=[],o=i(e,n,t,{exactFractions:!1}),u="+-*"+((r=!!r)?"/":"");!function e(t){var r=t.type;if("FunctionNode"===r)throw new Error("There is an unsolved function call");if("OperatorNode"===r)if("^"===t.op){if("ConstantNode"!==t.args[1].type||!V(parseFloat(t.args[1].value)))throw new Error("There is a non-integer exponent");e(t.args[0])}else{if(-1===u.indexOf(t.op))throw new Error("Operator "+t.op+" invalid in polynomial expression");for(var n=0;n=1){var m,h;e=c(e);var d,v=!0,y=!1;for(e=i(e,o.firstRules,{},l);h=v?o.distrDivRules:o.sucDivRules,v=!v,(d=(e=i(e,h,{},p)).toString())!==m;)y=!0,m=d;y&&(e=i(e,o.firstRulesAgain,{},l)),e=i(e,o.finalRules,{},l)}var g=[],x={};return"OperatorNode"===e.type&&e.isBinary()&&"/"===e.op?(1===s&&(e.args[0]=f(e.args[0],g),e.args[1]=f(e.args[1])),a&&(x.numerator=e.args[0],x.denominator=e.args[1])):(1===s&&(e=f(e,g)),a&&(x.numerator=e,x.denominator=null)),a?(x.coefficients=g,x.variables=u.variables,x.expression=e,x):e}return e.ParenthesisNode,t(Ld,{Node:s,"Node, boolean":function(e,t){return s(e,{},t)},"Node, Object":s,"Node, Object, boolean":s});function c(e,t,r){var n=e.type,i=arguments.length>1;if("OperatorNode"===n&&e.isBinary()){var u,s=!1;if("^"===e.op&&("ParenthesisNode"!==e.args[0].type&&"OperatorNode"!==e.args[0].type||"ConstantNode"!==e.args[1].type||(s=(u=parseFloat(e.args[1].value))>=2&&V(u))),s){if(u>2){var f=e.args[0],l=new o("^","pow",[e.args[0].cloneDeep(),new a(u-1)]);e=new o("*","multiply",[f,l])}else e=new o("*","multiply",[e.args[0],e.args[0].cloneDeep()]);i&&("content"===r?t.content=e:t.args[r]=e)}}if("ParenthesisNode"===n)c(e.content,e,"content");else if("ConstantNode"!==n&&"SymbolNode"!==n)for(var p=0;pr&&(t[c]=0),t[c]+=o.cte*("+"===o.oper?1:-1),void(r=Math.max(c,r))}o.cte=c,""===o.fire&&(t[0]+=o.cte*("+"===o.oper?1:-1))}}(e,null,{cte:1,oper:"+",fire:""});for(var i,s=!0,c=r=t.length-1;c>=0;c--)if(0!==t[c]){var f=new a(s?t[c]:Math.abs(t[c])),l=t[c]<0?"-":"+";if(c>0){var p=new u(n);if(c>1){var m=new a(c);p=new o("^","pow",[p,m])}f=-1===t[c]&&s?new o("-","unaryMinus",[p]):1===Math.abs(t[c])?p:new o("*","multiply",[f,p])}i=s?f:"+"===l?new o("+","add",[i,f]):new o("-","subtract",[i,f]),s=!1}return s?new a(0):i}})),$d="zpk2tf",Hd=Ee($d,["typed","add","multiply","Complex","number"],(function(e){var t=e.typed,r=e.add,n=e.multiply,i=e.Complex,a=e.number;return t($d,{"Array,Array,number":function(e,t,r){return o(e,t,r)},"Array,Array":function(e,t){return o(e,t,1)},"Matrix,Matrix,number":function(e,t,r){return o(e.valueOf(),t.valueOf(),r)},"Matrix,Matrix":function(e,t){return o(e.valueOf(),t.valueOf(),1)}});function o(e,t,r){e.some((function(e){return"BigNumber"===e.type}))&&(e=e.map((function(e){return a(e)}))),t.some((function(e){return"BigNumber"===e.type}))&&(t=t.map((function(e){return a(e)})));for(var o=[i(1,0)],s=[i(1,0)],c=0;c=0&&o-u0?0:2;else if(u&&!0===u.isSet)u=u.map((function(e){return e-1}));else if(f(u)||l(u))"boolean"!==r(u)&&(u=u.map((function(e){return e-1})));else if(i(u))u--;else if(a(u))u=u.toNumber()-1;else if("string"!=typeof u)throw new TypeError("Dimension must be an Array, Matrix, number, string, or Range");e[n]=u}var s=new t;return t.apply(s,e),s}}),{isTransformFunction:!0}),Dy=Ee("map",["typed"],(function(e){var t=e.typed;function r(e,t,r){var i,a;return e[0]&&(i=e[0].compile().evaluate(r)),e[1]&&(a=U(e[1])||_(e[1])?e[1].compile().evaluate(r):gy(e[1],t,r)),n(i,a)}r.rawArgs=!0;var n=t("map",{"Array, function":function(e,t){return Ey(e,t,e)},"Matrix, function":function(e,t){return e.create(Ey(e.valueOf(),t,e))}});return r}),{isTransformFunction:!0});function Ey(e,t,r){return function e(n,i){return Array.isArray(n)?wn(n,(function(t,r){return e(t,i.concat(r+1))})):Au(t,n,i,r,"map")}(e,[])}function Ay(e){if(2===e.length&&p(e[0])){var t=(e=e.slice())[1];i(t)?e[1]=t-1:a(t)&&(e[1]=t.minus(1))}return e}var Sy=Ee("max",["typed","config","numeric","larger"],(function(e){var t=e.typed,r=e.config,n=e.numeric,i=e.larger,a=lf({typed:t,config:r,numeric:n,larger:i});return t("max",{"...any":function(e){e=Ay(e);try{return a.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),Cy=Ee("mean",["typed","add","divide"],(function(e){var t=e.typed,r=e.add,n=e.divide,i=bh({typed:t,add:r,divide:n});return t("mean",{"...any":function(e){e=Ay(e);try{return i.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),My=Ee("min",["typed","config","numeric","smaller"],(function(e){var t=e.typed,r=e.config,n=e.numeric,i=e.smaller,a=pf({typed:t,config:r,numeric:n,smaller:i});return t("min",{"...any":function(e){e=Ay(e);try{return a.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),Fy=Ee("range",["typed","config","?matrix","?bignumber","smaller","smallerEq","larger","largerEq","add","isPositive"],(function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.bignumber,a=e.smaller,o=e.smallerEq,u=e.larger,s=e.largerEq,c=e.add,f=e.isPositive,l=Wu({typed:t,config:r,matrix:n,bignumber:i,smaller:a,smallerEq:o,larger:u,largerEq:s,add:c,isPositive:f});return t("range",{"...any":function(e){return"boolean"!=typeof e[e.length-1]&&e.push(!0),l.apply(null,e)}})}),{isTransformFunction:!0}),Oy=Ee("row",["typed","Index","matrix","range"],(function(e){var t=e.typed,r=e.Index,n=e.matrix,a=e.range,o=rs({typed:t,Index:r,matrix:n,range:a});return t("row",{"...any":function(e){var t=e.length-1,r=e[t];i(r)&&(e[t]=r-1);try{return o.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),Ty=Ee("subset",["typed","matrix","zeros","add"],(function(e){var t=e.typed,r=e.matrix,n=e.zeros,i=e.add,a=ss({typed:t,matrix:r,zeros:n,add:i});return t("subset",{"...any":function(e){try{return a.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),By=Ee("concat",["typed","matrix","isInteger"],(function(e){var t=e.typed,r=e.matrix,n=e.isInteger,o=vu({typed:t,matrix:r,isInteger:n});return t("concat",{"...any":function(e){var t=e.length-1,r=e[t];i(r)?e[t]=r-1:a(r)&&(e[t]=r.minus(1));try{return o.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),_y="diff",ky=Ee(_y,["typed","matrix","subtract","number","bignumber"],(function(e){var t=e.typed,r=e.matrix,n=e.subtract,i=e.number,a=e.bignumber,o=Uu({typed:t,matrix:r,subtract:n,number:i,bignumber:a});return t(_y,{"...any":function(e){e=Ay(e);try{return o.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),Iy=Ee("std",["typed","map","sqrt","variance"],(function(e){var t=e.typed,r=e.map,n=e.sqrt,i=e.variance,a=Fh({typed:t,map:r,sqrt:n,variance:i});return t("std",{"...any":function(e){e=Ay(e);try{return a.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),Ry=Ee("sum",["typed","config","add","numeric"],(function(e){var t=e.typed,r=e.config,n=e.add,i=e.numeric,a=vh({typed:t,config:r,add:n,numeric:i});return t("sum",{"...any":function(e){e=Ay(e);try{return a.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),zy=Ee("quantileSeq",["typed","bignumber","add","subtract","divide","multiply","partitionSelect","compare","isInteger","smaller","smallerEq","larger"],(function(e){var t=e.typed,r=e.bignumber,n=e.add,i=e.subtract,a=e.divide,o=e.multiply,u=e.partitionSelect,s=e.compare,c=e.isInteger,f=e.smaller,l=e.smallerEq,p=e.larger,m=Mh({typed:t,bignumber:r,add:n,subtract:i,divide:a,multiply:o,partitionSelect:u,compare:s,isInteger:c,smaller:f,smallerEq:l,larger:p});return t("quantileSeq",{"Array | Matrix, number | BigNumber":m,"Array | Matrix, number | BigNumber, number":function(e,t,r){return m(e,t,h(r))},"Array | Matrix, number | BigNumber, boolean":m,"Array | Matrix, number | BigNumber, boolean, number":function(e,t,r,n){return m(e,t,r,h(n))},"Array | Matrix, Array | Matrix":m,"Array | Matrix, Array | Matrix, number":function(e,t,r){return m(e,t,h(r))},"Array | Matrix, Array | Matrix, boolean":m,"Array | Matrix, Array | Matrix, boolean, number":function(e,t,r,n){return m(e,t,r,h(n))}});function h(e){return Ay([[],e])[1]}}),{isTransformFunction:!0}),qy="cumsum",jy=Ee(qy,["typed","add","unaryPlus"],(function(e){var t=e.typed,r=e.add,n=e.unaryPlus,o=gh({typed:t,add:r,unaryPlus:n});return t(qy,{"...any":function(e){if(2===e.length&&p(e[0])){var t=e[1];i(t)?e[1]=t-1:a(t)&&(e[1]=t.minus(1))}try{return o.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),Py="variance",Ly=Ee(Py,["typed","add","subtract","multiply","divide","apply","isNaN"],(function(e){var t=e.typed,r=e.add,n=e.subtract,i=e.multiply,a=e.divide,o=e.apply,u=e.isNaN,s=Sh({typed:t,add:r,subtract:n,multiply:i,divide:a,apply:o,isNaN:u});return t(Py,{"...any":function(e){e=Ay(e);try{return s.apply(null,e)}catch(e){throw gp(e)}}})}),{isTransformFunction:!0}),Uy="print",$y=Ee(Uy,["typed","matrix","zeros","add"],(function(e){var t=e.typed,r=e.matrix,n=e.zeros,i=e.add,a=Hs({typed:t,matrix:r,zeros:n,add:i});return t(Uy,{"string, Object | Array":function(e,t){return a(o(e),t)},"string, Object | Array, number | Object":function(e,t,r){return a(o(e),t,r)}});function o(e){return e.replace(Us,(function(e){return"$"+e.slice(1).split(".").map((function(e){return!isNaN(e)&&e.length>0?parseInt(e)-1:e})).join(".")}))}}),{isTransformFunction:!0}),Hy=(r(1517),r(4279));var Gy={epsilon:1e-12,matrix:"Matrix",number:"number",precision:64,predictable:!1,randomSeed:null},Vy=["Matrix","Array"],Zy=["number","BigNumber","Fraction"];function Wy(e,t){function r(r){if(r){var n=de(e,he);Yy(r,"matrix",Vy),Yy(r,"number",Zy),ye(e,r);var i=de(e,he),a=de(r,he);return t("config",i,n,a),i}return de(e,he)}return r.MATRIX_OPTIONS=Vy,r.NUMBER_OPTIONS=Zy,Object.keys(Gy).forEach((function(t){Object.defineProperty(r,t,{get:function(){return e[t]},enumerable:!0,configurable:!0})})),r}function Yy(e,t,r){var n,i;void 0!==e[t]&&(n=r,i=e[t],-1===n.indexOf(i))&&console.warn('Warning: Unknown value "'+e[t]+'" for configuration option "'+t+'". Available options: '+r.map((function(e){return JSON.stringify(e)})).join(", ")+".")}const Jy=function e(r,n){var B=$r({},Gy,n);if("function"!=typeof Object.create)throw new Error("ES5 not supported by this JavaScript engine. Please load the es5-shim and es5-sham library for compatibility.");var H,V,Z=(H={isNumber:i,isComplex:o,isBigNumber:a,isFraction:u,isUnit:s,isString:c,isArray:f,isMatrix:l,isCollection:p,isDenseMatrix:m,isSparseMatrix:h,isRange:d,isIndex:v,isBoolean:y,isResultSet:g,isHelp:x,isFunction:b,isDate:w,isRegExp:N,isObject:D,isNull:E,isUndefined:A,isAccessorNode:S,isArrayNode:C,isAssignmentNode:M,isBlockNode:F,isConditionalNode:O,isConstantNode:T,isFunctionAssignmentNode:_,isFunctionNode:k,isIndexNode:I,isNode:R,isObjectNode:z,isOperatorNode:q,isParenthesisNode:j,isRangeNode:P,isRelationalNode:L,isSymbolNode:U,isChain:$},V=new Hy,H.on=V.on.bind(V),H.off=V.off.bind(V),H.once=V.once.bind(V),H.emit=V.emit.bind(V),H);Z.config=Wy(B,Z.emit),Z.expression={transform:{},mathWithTransform:{config:Z.config}};var W={};function Y(){for(var e=arguments.length,t=new Array(e),r=0;r2&&void 0!==arguments[2]?arguments[2]:t.fn;if(Fn(a,"."))throw new Error("Factory name should not contain a nested path. Name: "+JSON.stringify(a));var o=v(t)?n.expression.transform:n,u=a in n.expression.transform,s=Ne(o,a)?o[a]:void 0,c=function(){var i={};t.dependencies.map(Se).forEach((function(e){if(Fn(e,"."))throw new Error("Factory dependency should not contain a nested path. Name: "+JSON.stringify(e));"math"===e?i.math=n:"mathWithTransform"===e?i.mathWithTransform=n.expression.mathWithTransform:"classes"===e?i.classes=n:i[e]=n[e]}));var o=t(i);if(o&&"function"==typeof o.transform)throw new Error('Transforms cannot be attached to factory functions. Please create a separate function for it with exports.path="expression.transform"');if(void 0===s||r.override)return o;if(e.isTypedFunction(s)&&e.isTypedFunction(o))return e(s,o);if(r.silent)return s;throw new Error('Cannot import "'+a+'": already exists')};t.meta&&!1===t.meta.lazy?(o[a]=c(),s&&u?p(a):(v(t)||d(t))&&we(n.expression.mathWithTransform,a,(function(){return o[a]}))):(we(o,a,c),s&&u?p(a):(v(t)||d(t))&&we(n.expression.mathWithTransform,a,(function(){return o[a]}))),i[a]=t,n.emit("import",a,c)}function h(e){return!Ne(y,e)}function d(e){return!(-1!==e.fn.indexOf(".")||Ne(y,e.fn)||e.meta&&e.meta.isClass)}function v(e){return void 0!==e&&void 0!==e.meta&&!0===e.meta.isTransformFunction||!1}var y={expression:!0,type:!0,docs:!0,error:!0,json:!0,chain:!0};return function(e,r){var n=arguments.length;if(1!==n&&2!==n)throw new Ka("import",n,1,2);r||(r={});var i,f={};for(var p in function e(n,i,a){if(Array.isArray(i))i.forEach((function(t){return e(n,t)}));else if("object"===t(i))for(var o in i)Ne(i,o)&&e(n,i[o],o);else if(Ae(i)||void 0!==a){var u=Ae(i)?v(i)?i.fn+".transform":i.fn:a;if(Ne(n,u)&&n[u]!==i&&!r.silent)throw new Error('Cannot import "'+u+'" twice');n[u]=i}else if(!r.silent)throw new TypeError("Factory, Object, or Array expected")}(f,e),f)if(Ne(f,p)){var h=f[p];if(Ae(h))m(h,r);else if("function"==typeof(i=h)||"number"==typeof i||"string"==typeof i||"boolean"==typeof i||null===i||s(i)||o(i)||a(i)||u(i)||l(i)||Array.isArray(i))c(p,h,r);else if(!r.silent)throw new TypeError("Factory, Object, or Array expected")}}}(Y,0,Z,W);return Z.import=J,Z.on("config",(function(){De(W).forEach((function(e){e&&e.meta&&e.meta.recreateOnConfigChange&&J(e,{override:!0})}))})),Z.create=e.bind(null,r),Z.factory=Ee,Z.import(De(xe(r))),Z.ArgumentsError=Ka,Z.DimensionError=rn,Z.IndexError=nn,Z}(e)})(),n.default})())); diff --git a/local-scratch-vm/src/extensions/pm_sensingExpansion/index.js b/local-scratch-vm/src/extensions/pm_sensingExpansion/index.js new file mode 100644 index 0000000000000000000000000000000000000000..51124ae744ab7e5867bdfafd34b07ad1d2c3767f --- /dev/null +++ b/local-scratch-vm/src/extensions/pm_sensingExpansion/index.js @@ -0,0 +1,723 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const formatMessage = require('format-message'); +const Cast = require('../../util/cast'); +const Color = require('../../util/color'); + +const blockSeparator = ''; // At default scale, about 28px + +const blocks = ` + + + + abc 10 def + + + + + + + A + + + + + + + foo bar + + + + + foo + + + + + g + + + +${blockSeparator} +%b16> +%b17> +%b20> +%b22> + + + + + +%b18> +%b19> +%b23> +%b24> +${blockSeparator} +%b14> + + + + my variable + + + + + 0 + + + +%b10> +${blockSeparator} +%b6> +%b9> +%b11> +%b15> +%b12> +%b13> +${blockSeparator} + + + +${blockSeparator} +%b7> +%b5> +%b8> +%b4> +${blockSeparator} +%b3> +${blockSeparator} +%b0> +%b1> +${blockSeparator} +%b2> +` + +/** + * Class of 2023 + * @constructor + */ +class pmSensingExpansion { + constructor(runtime) { + /** + * The runtime instantiating this block package. + * @type {runtime} + */ + this.runtime = runtime; + + this.canVibrate = true; + + this.lastUpdate = Date.now(); + + this.canGetLoudness = false; + this.loudnessArray = [0]; + + this.scrollDistance = 0; + + this.lastValues = {}; + } + + orderCategoryBlocks(extensionBlocks) { + let categoryBlocks = blocks; + + let idx = 0; + for (const block of extensionBlocks) { + categoryBlocks = categoryBlocks.replace('%b' + idx + ">", block); + idx++; + } + + return [categoryBlocks]; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo() { + return { + id: 'pmSensingExpansion', + name: 'Sensing Expansion', + color1: "#5CB1D6", + color2: "#47A8D1", + color3: "#2E8EB8", + isDynamic: true, + orderBlocks: this.orderCategoryBlocks, + blocks: [ + { + opcode: 'batteryPercentage', + text: 'battery percentage', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'batteryCharging', + text: 'is device charging?', + blockType: BlockType.BOOLEAN, + disableMonitor: true + }, + { + opcode: 'vibrateDevice', + text: 'vibrate', + blockType: BlockType.COMMAND + }, + { + opcode: 'browserLanguage', + text: 'preferred language', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'urlOptions', + text: 'url [OPTIONS]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + OPTIONS: { + type: ArgumentType.STRING, + menu: "urlSections" + } + } + }, + { + opcode: 'urlOptionsOf', + text: '[OPTIONS] of url [URL]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + OPTIONS: { + type: ArgumentType.STRING, + menu: "urlSections" + }, + URL: { + type: ArgumentType.STRING, + defaultValue: "https://home.penguinmod.com:3000/some/random/page?param=10#20" + } + } + }, + { + opcode: 'setUsername', + text: 'set username to [NAME]', + blockType: BlockType.COMMAND, + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "Penguin" + } + } + }, + { + opcode: 'setUrlEnd', + text: 'set url path to [PATH]', + blockType: BlockType.COMMAND, + arguments: { + PATH: { + type: ArgumentType.STRING, + defaultValue: "?parameter=10#you-can-change-these-without-refreshing" + } + } + }, + { + opcode: 'queryParamOfUrl', + text: 'query parameter [PARAM] of url [URL]', + blockType: BlockType.REPORTER, + disableMonitor: true, + arguments: { + PARAM: { + type: ArgumentType.STRING, + defaultValue: "param" + }, + URL: { + type: ArgumentType.STRING, + defaultValue: "https://penguinmod.com/?param=10" + } + } + }, + { + opcode: 'packaged', + text: 'project packaged?', + blockType: BlockType.BOOLEAN, + disableMonitor: true + }, + { + opcode: 'spriteName', + text: 'sprite name', + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'framed', + text: 'project in iframe?', + blockType: BlockType.BOOLEAN, + disableMonitor: true + }, + { + opcode: 'currentMillisecond', + text: 'current millisecond', + blockType: BlockType.REPORTER, + disableMonitor: false + }, + { + opcode: 'deltaTime', + text: 'delta time', + blockType: BlockType.REPORTER, + disableMonitor: false + }, + { + opcode: 'pickColor', + text: 'grab color at x: [X] y: [Y]', + blockType: BlockType.REPORTER, + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'maxSpriteLayers', + text: 'max sprite layers', + blockType: BlockType.REPORTER + }, + { + opcode: 'averageLoudness', + text: 'average loudness', + blockType: BlockType.REPORTER + }, + { + opcode: 'scrollingDistance', + text: 'scrolling distance', + blockType: BlockType.REPORTER + }, + { + opcode: 'setScrollingDistance', + text: 'set scrolling distance to [AMOUNT]', + blockType: BlockType.COMMAND, + arguments: { + AMOUNT: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'changeScrollingDistanceBy', + text: 'change scrolling distance by [AMOUNT]', + blockType: BlockType.COMMAND, + arguments: { + AMOUNT: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'currentKeyPressed', + text: 'current key pressed', + blockType: BlockType.REPORTER + }, + { + opcode: 'amountOfTimeKeyHasBeenHeld', + text: 'seconds since holding [KEY]', + blockType: BlockType.REPORTER, + arguments: { + KEY: { + // this is replaced later + type: ArgumentType.STRING, + defaultValue: 'a' + } + } + }, + { + opcode: 'getLastKeyPressed', + text: formatMessage({ + id: 'tw.blocks.lastKeyPressed', + default: 'last key pressed', + description: 'Block that returns the last key that was pressed' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getButtonIsDown', + text: formatMessage({ + id: 'tw.blocks.buttonIsDown', + default: '[MOUSE_BUTTON] mouse button down?', + description: 'Block that returns whether a specific mouse button is down' + }), + blockType: BlockType.BOOLEAN, + arguments: { + MOUSE_BUTTON: { + type: ArgumentType.NUMBER, + menu: 'mouseButton', + defaultValue: '0' + } + } + }, + { + opcode: 'changed', + blockType: BlockType.BOOLEAN, + text: '[ONE] changed?', + arguments: { + ONE: { + type: null, + }, + }, + } + ], + menus: { + mouseButton: { + items: [ + { + text: formatMessage({ + id: 'tw.blocks.mouseButton.primary', + default: '(0) primary', + description: 'Dropdown item to select primary (usually left) mouse button' + }), + value: '0' + }, + { + text: formatMessage({ + id: 'tw.blocks.mouseButton.middle', + default: '(1) middle', + description: 'Dropdown item to select middle mouse button' + }), + value: '1' + }, + { + text: formatMessage({ + id: 'tw.blocks.mouseButton.secondary', + default: '(2) secondary', + description: 'Dropdown item to select secondary (usually right) mouse button' + }), + value: '2' + } + ], + acceptReporters: true + }, + urlSections: { + acceptReporters: true, + items: [ + "protocol", + "host", + "hostname", + "port", + "pathname", + "search", + "hash", + "origin", + "subdomain", + "path" + ].map(item => ({ text: item, value: item })) + } + } + }; + } + + getLastKeyPressed (_, util) { + return util.ioQuery('keyboard', 'getLastKeyPressed'); + } + + getButtonIsDown (args, util) { + const button = Cast.toNumber(args.MOUSE_BUTTON); + return util.ioQuery('mouse', 'getButtonIsDown', [button]); + } + + changed(args, util) { + const id = util.thread.peekStack() + if (!this.lastValues[id]) + this.lastValues[id] = Cast.toString(args.ONE); + if (Cast.toString(args.ONE) !== this.lastValues[id]) { + this.lastValues[id] = Cast.toString(args.ONE); + return true; + } + return false; + } + + pickColor(args) { + const renderer = this.runtime.renderer; + const scratchX = Cast.toNumber(args.X); + const scratchY = Cast.toNumber(args.Y); + const clientX = Math.round((((this.runtime.stageWidth / 2) + scratchX) / this.runtime.stageWidth) * renderer._gl.canvas.clientWidth); + const clientY = Math.round((((this.runtime.stageHeight / 2) - scratchY) / this.runtime.stageHeight) * renderer._gl.canvas.clientHeight); + const colorInfo = renderer.extractColor(clientX, clientY, 20); + return Color.rgbToHex(colorInfo.color); + } + + // util + urlOptionFromObject(option, urlObject) { + const validOptions = [ + "protocol", + "host", + "hostname", + "port", + "pathname", + "search", + "hash", + "origin", + "subdomain", + "path" + ]; + if (!validOptions.includes(option)) return ''; + + switch (option) { + case 'subdomain': { + const origin = urlObject.origin; + if (origin.split('.').length <= 2) return ''; + const splitSubdomain = origin.split('.')[0]; + const subdomain = splitSubdomain.split('//')[1]; + if (!subdomain) return ''; + return subdomain.replace(/\./gmi, ''); + } + case 'path': { + const origin = urlObject.origin; + if (origin.endsWith('/')) { + return urlObject.href.replace(origin, ''); + } + return urlObject.href.replace(origin + '/', ''); + } + } + + return Cast.toString(urlObject[option]); + } + validateUrl(url) { + let valid = true; + try { + new URL(url); + } catch { + valid = false; + } + return valid; + } + + // blocks + batteryPercentage() { + if ('getBattery' in navigator) { + return new Promise((resolve) => { + navigator.getBattery().then(batteryManager => { + resolve(batteryManager.level * 100); + }).catch(() => { + return 100; + }); + }); + } else { + return 100; + } + } + batteryCharging() { + if ('getBattery' in navigator) { + return new Promise((resolve) => { + navigator.getBattery().then(batteryManager => { + resolve(batteryManager.charging); + }).catch(() => { + return true; + }); + }); + } else { + return true; + } + } + + maxSpriteLayers() { + return this.runtime.renderer._drawList.length - 1; + } + averageLoudness() { + if (!this.canGetLoudness) { + // set interval here because why create an interval + // on extension register if we never use the block + console.log('created average loudness loop'); + setInterval(() => { + if (!this.canGetLoudness) return; + const loudness = this.runtime.audioEngine.getLoudness(); + if (typeof loudness !== 'number') return; + if (this.loudnessArray.length > 20) { + this.loudnessArray.shift(); + } + if (loudness < 0) { + this.loudnessArray.push(0); + return; + } + this.loudnessArray.push(loudness); + }, 50); + } + // get average + this.canGetLoudness = true; + let addedTogether = 0; + let max = this.loudnessArray.length; + for (const loudness of this.loudnessArray) { + addedTogether += loudness; + } + return addedTogether / max; + } + + scrollingDistance() { + return this.scrollDistance; + } + setScrollingDistance(args) { + const amount = Cast.toNumber(args.AMOUNT); + this.scrollDistance = amount; + } + changeScrollingDistanceBy(args) { + const amount = Cast.toNumber(args.AMOUNT); + this.scrollDistance += amount; + } + + currentKeyPressed(_, util) { + const keys = util.ioQuery('keyboard', 'getAllKeysPressed'); + const key = keys[keys.length - 1]; + if (!key) return ''; + return Cast.toString(key).toLowerCase(); + } + amountOfTimeKeyHasBeenHeld(args, util) { + const key = Cast.toString(args.KEY); + const keyTimestamp = util.ioQuery('keyboard', 'getKeyTimestamp', [key]); + if (keyTimestamp === 0) return 0; + const currentTime = Date.now(); + const timestamp = currentTime - keyTimestamp; + return timestamp / 1000; + } + + vibrateDevice() { + // avoid vibration spam + // only vibrate every 1s + if (!this.canVibrate) return; + + if ('vibrate' in navigator) { + this.canVibrate = false; + navigator.vibrate(250); + setTimeout(() => { + this.canVibrate = true; + }, 1000); + } + } + + browserLanguage() { + if (!('language' in navigator)) return 'Unknown'; + const lang = Cast.toString(navigator.language); + const check = lang.split("-")[0].toLowerCase(); + + switch (check) { + case 'en': + return 'English'; + case 'es': + return 'Spanish'; + case 'fr': + return 'French'; + case 'it': + return 'Italian'; + case 'pt': + return 'Portuguese'; + case 'de': + return 'German'; + case 'ru': + return 'Russian'; + case 'ar': + return 'Arabic'; + case 'zh': + return 'Chinese (Mandarin)'; + case 'he': + return 'Hebrew'; + case 'ja': + return 'Japanese'; + case 'ko': + return 'Korean'; + case 'sw': + return 'Swahili'; + case 'sq': + return 'Albanian'; + case 'hy': + return 'Armenian'; + case 'eu': + return 'Basque'; + case 'nl': + return 'Dutch'; + case 'ka': + return 'Georgian'; + case 'gd': + return 'Scottish Gaelic'; + case 'ga': + return 'Modern Irish'; + case 'fa': + return 'Persian (Farsi)'; + case 'bo': + return 'Tibetan'; + case 'cy': + return 'Welsh'; + case 'el': + return 'Modern Greek'; + case 'grc': + return 'Ancient Greek'; + case 'la': + return 'Latin'; + case 'ang': + return 'Anglo-Saxon'; + case 'enm': + return 'Middle English'; + default: + return 'Unknown'; + } + } + + urlOptions(args) { + if (!('location' in window)) return ''; // idk how this would fail but funny + const option = Cast.toString(args.OPTIONS).toLowerCase(); + return this.urlOptionFromObject(option, location); + } + urlOptionsOf(args) { + if (!('location' in window)) return ''; // idk how this would fail but funny + const option = Cast.toString(args.OPTIONS).toLowerCase(); + const url = Cast.toString(args.URL); + if (!this.validateUrl(url)) return ''; + return this.urlOptionFromObject(option, new URL(url)); + } + + setUsername(args) { + const username = Cast.toString(args.NAME); + vm.postIOData('userData', { + username: username, + loggedIn: false, + }); + } + + setUrlEnd(args) { + if (!('history' in window)) return; + const path = Cast.toString(args.PATH); + const target = location.origin.endsWith('/') ? location.origin + path : location.origin + '/' + path; + history.replaceState('', '', target); + } + queryParamOfUrl(args) { + if (!('URLSearchParams' in window)) return ''; + const url = Cast.toString(args.URL); + if (!this.validateUrl(url)) return ''; + const urlObject = new URL(url); + const queryParams = new URLSearchParams(urlObject.search); + return queryParams.get(Cast.toString(args.PARAM)); + } + + packaged() { + return this.runtime.isPackaged; + } + + spriteName(_, util) { + return util.target.getName(); + } + + framed() { + if (!window.parent) return false; + return window.parent !== window; + } + + currentMillisecond() { + return Date.now() % 1000; + } + + deltaTime() { + let now = Date.now(); + let dt = now - this.lastUpdate; + this.lastUpdate = now; + return dt; + } + +} + +module.exports = pmSensingExpansion; diff --git a/local-scratch-vm/src/extensions/scratch3_boost/index.js b/local-scratch-vm/src/extensions/scratch3_boost/index.js new file mode 100644 index 0000000000000000000000000000000000000000..21564befb9629fdfe16dfba06f57d0bcdb75b3c0 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_boost/index.js @@ -0,0 +1,2113 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); +const formatMessage = require('format-message'); +const color = require('../../util/color'); +const BLE = require('../../io/ble'); +const Base64Util = require('../../util/base64-util'); +const MathUtil = require('../../util/math-util'); +const RateLimiter = require('../../util/rateLimiter.js'); +const log = require('../../util/log'); + +/** + * The LEGO Wireless Protocol documentation used to create this extension can be found at: + * https://lego.github.io/lego-ble-wireless-protocol-docs/index.html + */ + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const iconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAMAAAC5zwKfAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACpQTFRF////fIel5ufolZ62/2YavsPS+YZOkJmy9/j53+Hk6+zs6N/b6dfO////tDhMHAAAAA50Uk5T/////////////////wBFwNzIAAAA6ElEQVR42uzX2w6DIBAEUGDVtlr//3dLaLwgiwUd2z7MJPJg5EQWiGhGcAxBggQJEiT436CIfqXJPTn3MKNYYMSDFpoAmp24OaYgvwKnFgL2zvVTCwHrMoMi+nUQLFthaNCCa0iwclLkDgYVsQp0mzxuqXgK1MRzoCLWgkPXNN2wI/q6Kvt7u/cX0HtejN8x2sXpnpb8J8D3b0Keuhh3X975M+i0xNVbg3s1TIasgK21bQyGO+s2PykaGMYbge8KrNrssvkOWDXkErB8UuBHETjoYLkKBA8ZfuDkbwVBggQJEiR4MC8BBgDTtMZLx2nFCQAAAABJRU5ErkJggg=='; + +/** + * Boost BLE UUIDs. + * @enum {string} + */ +const BoostBLE = { + service: '00001623-1212-efde-1623-785feabcd123', + characteristic: '00001624-1212-efde-1623-785feabcd123', + sendInterval: 100, + sendRateMax: 20 +}; + +/** + * Boost Motor Max Power Add. Defines how much more power than the target speed + * the motors may supply to reach the target speed faster. + * Lower number == softer, slower reached target speed. + * Higher number == harder, faster reached target speed. + * @constant {number} + */ +const BoostMotorMaxPowerAdd = 10; + +/** + * A time interval to wait (in milliseconds) in between battery check calls. + * @type {number} + */ +const BoostPingInterval = 5000; + +/** + * The number of continuous samples the color-sensor will evaluate color from. + * @type {number} + */ +const BoostColorSampleSize = 5; + +/** + * Enum for Boost sensor and actuator types. + * @readonly + * @enum {number} + */ +const BoostIO = { + MOTOR_WEDO: 0x01, + MOTOR_SYSTEM: 0x02, + BUTTON: 0x05, + LIGHT: 0x08, + VOLTAGE: 0x14, + CURRENT: 0x15, + PIEZO: 0x16, + LED: 0x17, + TILT_EXTERNAL: 0x22, + MOTION_SENSOR: 0x23, + COLOR: 0x25, + MOTOREXT: 0x26, + MOTORINT: 0x27, + TILT: 0x28 +}; + +/** + * Enum for ids for various output command feedback types on the Boost. + * @readonly + * @enum {number} + */ +const BoostPortFeedback = { + IN_PROGRESS: 0x01, + COMPLETED: 0x02, + DISCARDED: 0x04, + IDLE: 0x08, + BUSY_OR_FULL: 0x10 +}; + +/** + * Enum for physical Boost Ports + * @readonly + * @enum {number} + */ + +const BoostPort10000223OrOlder = { + A: 55, + B: 56, + C: 1, + D: 2 +}; + +const BoostPort10000224OrNewer = { + A: 0, + B: 1, + C: 2, + D: 3 +}; + +// Set default port mapping to support the newer firmware +let BoostPort = BoostPort10000224OrNewer; + +/** + * Ids for each color sensor value used by the extension. + * @readonly + * @enum {string} + */ +const BoostColor = { + ANY: 'any', + NONE: 'none', + RED: 'red', + BLUE: 'blue', + GREEN: 'green', + YELLOW: 'yellow', + WHITE: 'white', + BLACK: 'black' +}; + +/** + * Enum for indices for each color sensed by the Boost vision sensor. + * @readonly + * @enum {number} + */ +const BoostColorIndex = { + [BoostColor.NONE]: 255, + [BoostColor.RED]: 9, + [BoostColor.BLUE]: 3, + [BoostColor.GREEN]: 5, + [BoostColor.YELLOW]: 7, + [BoostColor.WHITE]: 10, + [BoostColor.BLACK]: 0 +}; + +/** + * Enum for Message Types + * @readonly + * @enum {number} + */ +const BoostMessage = { + HUB_PROPERTIES: 0x01, + HUB_ACTIONS: 0x02, + HUB_ALERTS: 0x03, + HUB_ATTACHED_IO: 0x04, + ERROR: 0x05, + PORT_INPUT_FORMAT_SETUP_SINGLE: 0x41, + PORT_INPUT_FORMAT_SETUP_COMBINED: 0x42, + PORT_INFORMATION: 0x43, + PORT_MODEINFORMATION: 0x44, + PORT_VALUE: 0x45, + PORT_VALUE_COMBINED: 0x46, + PORT_INPUT_FORMAT: 0x47, + PORT_INPUT_FORMAT_COMBINED: 0x48, + OUTPUT: 0x81, + PORT_FEEDBACK: 0x82 +}; + +/** + * Enum for Hub Property Types + * @readonly + * @enum {number} + */ + +const BoostHubProperty = { + ADVERTISEMENT_NAME: 0x01, + BUTTON: 0x02, + FW_VERSION: 0x03, + HW_VERSION: 0x04, + RSSI: 0x05, + BATTERY_VOLTAGE: 0x06, + BATTERY_TYPE: 0x07, + MANUFACTURER_NAME: 0x08, + RADIO_FW_VERSION: 0x09, + LEGO_WP_VERSION: 0x0A, + SYSTEM_TYPE_ID: 0x0B, + HW_NETWORK_ID: 0x0C, + PRIMARY_MAC: 0x0D, + SECONDARY_MAC: 0x0E, + HW_NETWORK_FAMILY: 0x0F +}; + +/** + * Enum for Hub Property Operations + * @readonly + * @enum {number} + */ + +const BoostHubPropertyOperation = { + SET: 0x01, + ENABLE_UPDATES: 0x02, + DISABLE_UPDATES: 0x03, + RESET: 0x04, + REQUEST_UPDATE: 0x05, + UPDATE: 0x06 +}; + +/** + * Enum for Motor Subcommands (for 0x81) + * @readonly + * @enum {number} + */ +const BoostOutputSubCommand = { + START_POWER: 0x01, + START_POWER_PAIR: 0x02, + SET_ACC_TIME: 0x05, + SET_DEC_TIME: 0x06, + START_SPEED: 0x07, + START_SPEED_PAIR: 0x08, + START_SPEED_FOR_TIME: 0x09, + START_SPEED_FOR_TIME_PAIR: 0x0A, + START_SPEED_FOR_DEGREES: 0x0B, + START_SPEED_FOR_DEGREES_PAIR: 0x0C, + GO_TO_ABS_POSITION: 0x0D, + GO_TO_ABS_POSITION_PAIR: 0x0E, + PRESET_ENCODER: 0x14, + WRITE_DIRECT_MODE_DATA: 0x51 +}; + +/** + * Enum for Startup/Completion information for an output command. + * Startup and completion bytes must be OR'ed to be combined to a single byte. + * @readonly + * @enum {number} + */ +const BoostOutputExecution = { + // Startup information + BUFFER_IF_NECESSARY: 0x00, + EXECUTE_IMMEDIATELY: 0x10, + // Completion information + NO_ACTION: 0x00, + COMMAND_FEEDBACK: 0x01 +}; + +/** + * Enum for Boost Motor end states + * @readonly + * @enum {number} + */ +const BoostMotorEndState = { + FLOAT: 0, + HOLD: 126, + BRAKE: 127 +}; + +/** + * Enum for Boost Motor acceleration/deceleration profiles + * @readyonly + * @enum {number} + */ +const BoostMotorProfile = { + DO_NOT_USE: 0x00, + ACCELERATION: 0x01, + DECELERATION: 0x02 +}; + +/** + * Enum for when Boost IO's are attached/detached + * @readonly + * @enum {number} + */ +const BoostIOEvent = { + ATTACHED: 0x01, + DETACHED: 0x00, + ATTACHED_VIRTUAL: 0x02 +}; + +/** + * Enum for selected sensor modes. + * @enum {number} + */ +const BoostMode = { + TILT: 0, // angle (pitch/yaw) + LED: 1, // Set LED to accept RGB values + COLOR: 0, // Read indexed colors from Vision Sensor + MOTOR_SENSOR: 2, // Set motors to report their position + UNKNOWN: 0 // Anything else will use the default mode (mode 0) +}; + +/** + * Enum for Boost motor states. + * @param {number} + */ +const BoostMotorState = { + OFF: 0, + ON_FOREVER: 1, + ON_FOR_TIME: 2, + ON_FOR_ROTATION: 3 +}; + +/** + * Helper function for converting a JavaScript number to an INT32-number + * @param {number} number - a number + * @return {array} - a 4-byte array of Int8-values representing an INT32-number + */ +const numberToInt32Array = function (number) { + const buffer = new ArrayBuffer(4); + const dataview = new DataView(buffer); + dataview.setInt32(0, number); + return [ + dataview.getInt8(3), + dataview.getInt8(2), + dataview.getInt8(1), + dataview.getInt8(0) + ]; +}; + +/** + * Helper function for converting a regular array to a Little Endian INT32-value + * @param {Array} array - an array containing UInt8-values + * @return {number} - a number + */ +const int32ArrayToNumber = function (array) { + const i = Uint8Array.from(array); + const d = new DataView(i.buffer); + return d.getInt32(0, true); +}; + +/** + * Manage power, direction, position, and timers for one Boost motor. + */ +class BoostMotor { + /** + * Construct a Boost Motor instance. + * @param {Boost} parent - the Boost peripheral which owns this motor. + * @param {int} index - the zero-based index of this motor on its parent peripheral. + */ + constructor (parent, index) { + /** + * The Boost peripheral which owns this motor. + * @type {Boost} + * @private + */ + this._parent = parent; + + /** + * The zero-based index of this motor on its parent peripheral. + * @type {int} + * @private + */ + this._index = index; + + /** + * This motor's current direction: 1 for "this way" or -1 for "that way" + * @type {number} + * @private + */ + this._direction = 1; + + /** + * This motor's current power level, in the range [0,100]. + * @type {number} + * @private + */ + this._power = 50; + + /** + * This motor's current relative position + * @type {number} + * @private + */ + this._position = 0; + + /** + * Is this motor currently moving? + * @type {boolean} + * @private + */ + this._status = BoostMotorState.OFF; + + /** + * If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for + * the end-of-action handler. Cancel this when changing plans. + * @type {Object} + * @private + */ + this._pendingDurationTimeoutId = null; + + /** + * The starting time for the pending duration timeout. + * @type {number} + * @private + */ + this._pendingDurationTimeoutStartTime = null; + + /** + * The delay/duration of the pending duration timeout. + * @type {number} + * @private + */ + this._pendingDurationTimeoutDelay = null; + + /** + * The target position of a turn-based command. + * @type {number} + * @private + */ + this._pendingRotationDestination = null; + + /** + * If the motor has been turned on run for a specific rotation, this is the function + * that will be called once Scratch VM gets a notification from the Move Hub. + * @type {Object} + * @private + */ + this._pendingRotationPromise = null; + + this.turnOff = this.turnOff.bind(this); + } + + /** + * @return {int} - this motor's current direction: 1 for "this way" or -1 for "that way" + */ + get direction () { + return this._direction; + } + + /** + * @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way" + */ + set direction (value) { + if (value < 0) { + this._direction = -1; + } else { + this._direction = 1; + } + } + + /** + * @return {int} - this motor's current power level, in the range [0,100]. + */ + get power () { + return this._power; + } + + /** + * @param {int} value - this motor's new power level, in the range [10,100]. + */ + set power (value) { + /** + * Scale the motor power to a range between 10 and 100, + * to make sure the motors will run with something built onto them. + */ + if (value === 0) { + this._power = 0; + } else { + this._power = MathUtil.scale(value, 1, 100, 10, 100); + } + } + + /** + * @return {int} - this motor's current position, in the range of [-MIN_INT32,MAX_INT32] + */ + get position () { + return this._position; + } + + /** + * @param {int} value - set this motor's current position. + */ + set position (value) { + this._position = value; + } + + /** + * @return {BoostMotorState} - the motor's current state. + */ + get status () { + return this._status; + } + + /** + * @param {BoostMotorState} value - set this motor's state. + */ + set status (value) { + this._clearRotationState(); + this._clearDurationTimeout(); + this._status = value; + } + + /** + * @return {number} - time, in milliseconds, of when the pending duration timeout began. + */ + get pendingDurationTimeoutStartTime () { + return this._pendingDurationTimeoutStartTime; + } + + /** + * @return {number} - delay, in milliseconds, of the pending duration timeout. + */ + get pendingDurationTimeoutDelay () { + return this._pendingDurationTimeoutDelay; + } + + /** + * @return {number} - target position, in degrees, of the pending rotation. + */ + get pendingRotationDestination () { + return this._pendingRotationDestination; + } + + /** + * @return {Promise} - the Promise function for the pending rotation. + */ + get pendingRotationPromise () { + return this._pendingRotationPromise; + } + + /** + * @param {function} func - function to resolve pending rotation Promise + */ + set pendingRotationPromise (func) { + this._pendingRotationPromise = func; + } + + /** + * Turn this motor on indefinitely + * @private + */ + _turnOn () { + const cmd = this._parent.generateOutputCommand( + this._index, + BoostOutputExecution.EXECUTE_IMMEDIATELY, + BoostOutputSubCommand.START_SPEED, + [ + this.power * this.direction, + MathUtil.clamp(this.power + BoostMotorMaxPowerAdd, 0, 100), + BoostMotorProfile.DO_NOT_USE + ]); + + this._parent.send(BoostBLE.characteristic, cmd); + } + + /** + * Turn this motor on indefinitely + */ + turnOnForever () { + this.status = BoostMotorState.ON_FOREVER; + this._turnOn(); + } + + /** + * Turn this motor on for a specific duration. + * @param {number} milliseconds - run the motor for this long. + */ + turnOnFor (milliseconds) { + milliseconds = Math.max(0, milliseconds); + this.status = BoostMotorState.ON_FOR_TIME; + this._turnOn(); + this._setNewDurationTimeout(this.turnOff, milliseconds); + } + + /** + * Turn this motor on for a specific rotation in degrees. + * @param {number} degrees - run the motor for this amount of degrees. + * @param {number} direction - rotate in this direction + */ + turnOnForDegrees (degrees, direction) { + degrees = Math.max(0, degrees); + + const cmd = this._parent.generateOutputCommand( + this._index, + (BoostOutputExecution.EXECUTE_IMMEDIATELY ^ BoostOutputExecution.COMMAND_FEEDBACK), + BoostOutputSubCommand.START_SPEED_FOR_DEGREES, + [ + ...numberToInt32Array(degrees), + this.power * this.direction * direction, + MathUtil.clamp(this.power + BoostMotorMaxPowerAdd, 0, 100), + BoostMotorEndState.BRAKE, + BoostMotorProfile.DO_NOT_USE + ] + ); + + this.status = BoostMotorState.ON_FOR_ROTATION; + this._pendingRotationDestination = this.position + (degrees * this.direction * direction); + this._parent.send(BoostBLE.characteristic, cmd); + } + + /** + * Turn this motor off. + * @param {boolean} [useLimiter=true] - if true, use the rate limiter + */ + turnOff (useLimiter = true) { + const cmd = this._parent.generateOutputCommand( + this._index, + BoostOutputExecution.EXECUTE_IMMEDIATELY, + BoostOutputSubCommand.START_POWER, + [ + BoostMotorEndState.FLOAT + ] + ); + + this.status = BoostMotorState.OFF; + this._parent.send(BoostBLE.characteristic, cmd, useLimiter); + } + + /** + * Clear the motor action timeout, if any. Safe to call even when there is no pending timeout. + * @private + */ + _clearDurationTimeout () { + if (this._pendingDurationTimeoutId !== null) { + clearTimeout(this._pendingDurationTimeoutId); + this._pendingDurationTimeoutId = null; + this._pendingDurationTimeoutStartTime = null; + this._pendingDurationTimeoutDelay = null; + } + } + + /** + * Set a new motor action timeout, after clearing an existing one if necessary. + * @param {Function} callback - to be called at the end of the timeout. + * @param {int} delay - wait this many milliseconds before calling the callback. + * @private + */ + _setNewDurationTimeout (callback, delay) { + this._clearDurationTimeout(); + const timeoutID = setTimeout(() => { + if (this._pendingDurationTimeoutId === timeoutID) { + this._pendingDurationTimeoutId = null; + this._pendingDurationTimeoutStartTime = null; + this._pendingDurationTimeoutDelay = null; + } + callback(); + }, delay); + this._pendingDurationTimeoutId = timeoutID; + this._pendingDurationTimeoutStartTime = Date.now(); + this._pendingDurationTimeoutDelay = delay; + } + + /** + * Clear the motor states related to rotation-based commands, if any. + * Safe to call even when there is no pending promise function. + * @private + */ + _clearRotationState () { + if (this._pendingRotationPromise !== null) { + this._pendingRotationPromise(); + this._pendingRotationPromise = null; + } + this._pendingRotationDestination = null; + } +} + +/** + * Manage communication with a Boost peripheral over a Bluetooth Low Energy client socket. + */ +class Boost { + + constructor (runtime, extensionId) { + + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} + * @private + */ + this._runtime = runtime; + this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this)); + + /** + * The id of the extension this peripheral belongs to. + */ + this._extensionId = extensionId; + + /** + * A list of the ids of the physical or virtual sensors. + * @type {string[]} + * @private + */ + this._ports = []; + + /** + * A list of motors registered by the Boost hardware. + * @type {BoostMotor[]} + * @private + */ + this._motors = []; + + /** + * The most recently received value for each sensor. + * @type {Object.} + * @private + */ + this._sensors = { + tiltX: 0, + tiltY: 0, + color: BoostColor.NONE, + previousColor: BoostColor.NONE + }; + + /** + * An array of values from the Boost Vision Sensor. + * @type {Array} + * @private + */ + this._colorSamples = []; + + /** + * The Bluetooth connection socket for reading/writing peripheral data. + * @type {BLE} + * @private + */ + this._ble = null; + this._runtime.registerPeripheralExtension(extensionId, this); + + /** + * A rate limiter utility, to help limit the rate at which we send BLE messages + * over the socket to Scratch Link to a maximum number of sends per second. + * @type {RateLimiter} + * @private + */ + this._rateLimiter = new RateLimiter(BoostBLE.sendRateMax); + + /** + * An interval id for the battery check interval. + * @type {number} + * @private + */ + this._pingDeviceId = null; + + this.reset = this.reset.bind(this); + this._onConnect = this._onConnect.bind(this); + this._onMessage = this._onMessage.bind(this); + this._pingDevice = this._pingDevice.bind(this); + } + + /** + * @return {number} - the latest value received for the tilt sensor's tilt about the X axis. + */ + get tiltX () { + return this._sensors.tiltX; + } + + /** + * @return {number} - the latest value received for the tilt sensor's tilt about the Y axis. + */ + get tiltY () { + return this._sensors.tiltY; + } + + /** + * @return {number} - the latest color value received from the vision sensor. + */ + get color () { + return this._sensors.color; + } + + /** + * @return {number} - the previous color value received from the vision sensor. + */ + get previousColor () { + return this._sensors.previousColor; + } + + /** + * Look up the color id for an index received from the vision sensor. + * @param {number} index - the color index to look up. + * @return {BoostColor} the color id for this index. + */ + boostColorForIndex (index) { + const colorForIndex = Object.keys(BoostColorIndex).find(key => BoostColorIndex[key] === index); + return colorForIndex || BoostColor.NONE; + } + + /** + * Access a particular motor on this peripheral. + * @param {int} index - the index of the desired motor. + * @return {BoostMotor} - the BoostMotor instance, if any, at that index. + */ + motor (index) { + return this._motors[index]; + } + + /** + * Stop all the motors that are currently running. + */ + stopAllMotors () { + this._motors.forEach(motor => { + if (motor) { + // Send the motor off command without using the rate limiter. + // This allows the stop button to stop motors even if we are + // otherwise flooded with commands. + motor.turnOff(false); + } + }); + } + + /** + * Set the Boost peripheral's LED to a specific color. + * @param {int} inputRGB - a 24-bit RGB color in 0xRRGGBB format. + * @return {Promise} - a promise of the completion of the set led send operation. + */ + setLED (inputRGB) { + const rgb = [ + (inputRGB >> 16) & 0x000000FF, + (inputRGB >> 8) & 0x000000FF, + (inputRGB) & 0x000000FF + ]; + + const cmd = this.generateOutputCommand( + this._ports.indexOf(BoostIO.LED), + BoostOutputExecution.EXECUTE_IMMEDIATELY ^ BoostOutputExecution.COMMAND_FEEDBACK, + BoostOutputSubCommand.WRITE_DIRECT_MODE_DATA, + [BoostMode.LED, + ...rgb] + ); + + return this.send(BoostBLE.characteristic, cmd); + } + + /** + * Sets the input mode of the LED to RGB. + * @return {Promise} - a promise returned by the send operation. + */ + setLEDMode () { + const cmd = this.generateInputCommand( + this._ports.indexOf(BoostIO.LED), + BoostMode.LED, + 0, + false + ); + + return this.send(BoostBLE.characteristic, cmd); + } + + /** + * Stop the motors on the Boost peripheral. + */ + stopAll () { + if (!this.isConnected()) return; + this.stopAllMotors(); + } + + /** + * Called by the runtime when user wants to scan for a Boost peripheral. + */ + scan () { + if (this._ble) { + this._ble.disconnect(); + } + this._ble = new BLE(this._runtime, this._extensionId, { + filters: [{ + services: [BoostBLE.service], + manufacturerData: { + 0x0397: { + dataPrefix: [0x00, 0x40], + mask: [0x00, 0xFF] + } + } + }], + optionalServices: [] + }, this._onConnect, this.reset); + } + + /** + * Called by the runtime when user wants to connect to a certain Boost peripheral. + * @param {number} id - the id of the peripheral to connect to. + */ + connect (id) { + if (this._ble) { + this._ble.connectPeripheral(id); + } + } + + /** + * Disconnects from the current BLE socket and resets state. + */ + disconnect () { + if (this._ble) { + this._ble.disconnect(); + } + + this.reset(); + } + + /** + * Reset all the state and timeout/interval ids. + */ + reset () { + this._ports = []; + this._motors = []; + this._sensors = { + tiltX: 0, + tiltY: 0, + color: BoostColor.NONE, + previousColor: BoostColor.NONE + }; + + if (this._pingDeviceId) { + window.clearInterval(this._pingDeviceId); + this._pingDeviceId = null; + } + } + + /** + * Called by the runtime to detect whether the Boost peripheral is connected. + * @return {boolean} - the connected state. + */ + isConnected () { + let connected = false; + if (this._ble) { + connected = this._ble.isConnected(); + } + return connected; + } + + /** + * Write a message to the Boost peripheral BLE socket. + * @param {number} uuid - the UUID of the characteristic to write to + * @param {Array} message - the message to write. + * @param {boolean} [useLimiter=true] - if true, use the rate limiter + * @return {Promise} - a promise result of the write operation + */ + send (uuid, message, useLimiter = true) { + if (!this.isConnected()) return Promise.resolve(); + + if (useLimiter) { + if (!this._rateLimiter.okayToSend()) return Promise.resolve(); + } + + return this._ble.write( + BoostBLE.service, + uuid, + Base64Util.uint8ArrayToBase64(message), + 'base64' + ); + } + + /** + * Generate a Boost 'Output Command' in the byte array format + * (COMMON HEADER, PORT ID, EXECUTION BYTE, SUBCOMMAND ID, PAYLOAD). + * + * Payload is accepted as an array since these vary across different subcommands. + * + * @param {number} portID - the port (Connect ID) to send a command to. + * @param {number} execution - Byte containing startup/completion information + * @param {number} subCommand - the id of the subcommand byte. + * @param {array} payload - the list of bytes to send as subcommand payload + * @return {array} - a generated output command. + */ + generateOutputCommand (portID, execution, subCommand, payload) { + const hubID = 0x00; + const command = [hubID, BoostMessage.OUTPUT, portID, execution, subCommand, ...payload]; + command.unshift(command.length + 1); // Prepend payload with length byte; + + return command; + } + + /** + * Generate a Boost 'Input Command' in the byte array format + * (COMMAND ID, COMMAND TYPE, CONNECT ID, TYPE ID, MODE, DELTA INTERVAL (4 BYTES), + * UNIT, NOTIFICATIONS ENABLED). + * + * This sends a command to the Boost that sets that input format + * of the specified inputs and sets value change notifications. + * + * @param {number} portID - the port (Connect ID) to send a command to. + * @param {number} mode - the mode of the input sensor. + * @param {number} delta - the delta change needed to trigger notification. + * @param {boolean} enableNotifications - whether to enable notifications. + * @return {array} - a generated input command. + */ + generateInputCommand (portID, mode, delta, enableNotifications) { + const command = [ + 0x00, // Hub ID + BoostMessage.PORT_INPUT_FORMAT_SETUP_SINGLE, + portID, + mode + ].concat(numberToInt32Array(delta)).concat([ + enableNotifications + ]); + command.unshift(command.length + 1); // Prepend payload with length byte; + + return command; + } + + /** + * Starts reading data from peripheral after BLE has connected. + * @private + */ + _onConnect () { + this._ble.startNotifications( + BoostBLE.service, + BoostBLE.characteristic, + this._onMessage + ); + this._pingDeviceId = window.setInterval(this._pingDevice, BoostPingInterval); + + // Send a request for firmware version. + setTimeout(() => { + const command = [ + 0x00, // Hub ID + BoostMessage.HUB_PROPERTIES, + BoostHubProperty.FW_VERSION, + BoostHubPropertyOperation.REQUEST_UPDATE + ]; + command.unshift(command.length + 1); + this.send(BoostBLE.characteristic, command, false); + }, 500); + + } + + /** + * Process the sensor data from the incoming BLE characteristic. + * @param {object} base64 - the incoming BLE data. + * @private + */ + _onMessage (base64) { + const data = Base64Util.base64ToUint8Array(base64); + + /** + * First three bytes are the common header: + * 0: Length of message + * 1: Hub ID (always 0x00 at the moment, unused) + * 2: Message Type + * 3: Port ID + * We base our switch-case on Message Type + */ + + const messageType = data[2]; + const portID = data[3]; + + switch (messageType) { + + case BoostMessage.HUB_PROPERTIES: { + const property = data[3]; + switch (property) { + case BoostHubProperty.FW_VERSION: { + // Establish firmware version 1.0.00.0224 as a 32-bit signed integer (little endian) + const fwVersion10000224 = int32ArrayToNumber([0x24, 0x02, 0x00, 0x10]); + const fwHub = int32ArrayToNumber(data.slice(5, data.length)); + if (fwHub < fwVersion10000224) { + BoostPort = BoostPort10000223OrOlder; + log.info('Move Hub firmware older than version 1.0.00.0224 detected. Using old port mapping.'); + } else { + BoostPort = BoostPort10000224OrNewer; + } + break; + } + } + break; + } + case BoostMessage.HUB_ATTACHED_IO: { // IO Attach/Detach events + const event = data[4]; + const typeId = data[5]; + + switch (event) { + case BoostIOEvent.ATTACHED: + this._registerSensorOrMotor(portID, typeId); + break; + case BoostIOEvent.DETACHED: + this._clearPort(portID); + break; + case BoostIOEvent.ATTACHED_VIRTUAL: + default: + } + break; + } + case BoostMessage.PORT_VALUE: { + const type = this._ports[portID]; + + switch (type) { + case BoostIO.TILT: + this._sensors.tiltX = data[4]; + this._sensors.tiltY = data[5]; + break; + case BoostIO.COLOR: + this._colorSamples.unshift(data[4]); + if (this._colorSamples.length > BoostColorSampleSize) { + this._colorSamples.pop(); + if (this._colorSamples.every((v, i, arr) => v === arr[0])) { + this._sensors.previousColor = this._sensors.color; + this._sensors.color = this.boostColorForIndex(this._colorSamples[0]); + } else { + this._sensors.color = BoostColor.NONE; + } + } else { + this._sensors.color = BoostColor.NONE; + } + break; + case BoostIO.MOTOREXT: + case BoostIO.MOTORINT: + this.motor(portID).position = int32ArrayToNumber(data.slice(4, 8)); + break; + case BoostIO.CURRENT: + case BoostIO.VOLTAGE: + case BoostIO.LED: + break; + default: + log.warn(`Unknown sensor value! Type: ${type}`); + } + break; + } + case BoostMessage.PORT_FEEDBACK: { + const feedback = data[4]; + const motor = this.motor(portID); + if (motor) { + // Makes sure that commands resolve both when they actually complete and when they fail + const isBusy = feedback & BoostPortFeedback.IN_PROGRESS; + const commandCompleted = feedback & (BoostPortFeedback.COMPLETED ^ BoostPortFeedback.DISCARDED); + if (!isBusy && commandCompleted) { + if (motor.status === BoostMotorState.ON_FOR_ROTATION) { + motor.status = BoostMotorState.OFF; + } + } + } + break; + } + case BoostMessage.ERROR: + log.warn(`Error reported by hub: ${data}`); + break; + } + } + + /** + * Ping the Boost hub. If the Boost hub has disconnected + * for some reason, the BLE socket will get an error back and automatically + * close the socket. + * @private + */ + _pingDevice () { + this._ble.read( + BoostBLE.service, + BoostBLE.characteristic, + false + ); + } + + /** + * Register a new sensor or motor connected at a port. Store the type of + * sensor or motor internally, and then register for notifications on input + * values if it is a sensor. + * @param {number} portID - the port to register a sensor or motor on. + * @param {number} type - the type ID of the sensor or motor + * @private + */ + _registerSensorOrMotor (portID, type) { + // Record which port is connected to what type of device + this._ports[portID] = type; + + // Record motor port + if (type === BoostIO.MOTORINT || type === BoostIO.MOTOREXT) { + this._motors[portID] = new BoostMotor(this, portID); + } + + // Set input format for tilt or distance sensor + let mode = null; + let delta = 1; + + switch (type) { + case BoostIO.MOTORINT: + case BoostIO.MOTOREXT: + mode = BoostMode.MOTOR_SENSOR; + break; + case BoostIO.COLOR: + mode = BoostMode.COLOR; + delta = 0; + break; + case BoostIO.LED: + mode = BoostMode.LED; + /** + * Sets the LED to blue to give an indication on the hub + * that it has connected successfully. + */ + this.setLEDMode(); + this.setLED(0x0000FF); + break; + case BoostIO.TILT: + mode = BoostMode.TILT; + break; + default: + mode = BoostMode.UNKNOWN; + } + + const cmd = this.generateInputCommand( + portID, + mode, + delta, + true // Receive feedback + ); + + this.send(BoostBLE.characteristic, cmd); + } + + /** + * Clear the sensors or motors present on the ports. + * @param {number} portID - the port to clear. + * @private + */ + _clearPort (portID) { + const type = this._ports[portID]; + if (type === BoostIO.TILT) { + this._sensors.tiltX = this._sensors.tiltY = 0; + } + if (type === BoostIO.COLOR) { + this._sensors.color = BoostColor.NONE; + } + this._ports[portID] = 'none'; + this._motors[portID] = null; + } +} + +/** + * Enum for motor specification. + * @readonly + * @enum {string} + */ +const BoostMotorLabel = { + A: 'A', + B: 'B', + C: 'C', + D: 'D', + AB: 'AB', + ALL: 'ABCD' +}; + +/** + * Enum for motor direction specification. + * @readonly + * @enum {string} + */ +const BoostMotorDirection = { + FORWARD: 'this way', + BACKWARD: 'that way', + REVERSE: 'reverse' +}; + +/** + * Enum for tilt sensor direction. + * @readonly + * @enum {string} + */ +const BoostTiltDirection = { + UP: 'up', + DOWN: 'down', + LEFT: 'left', + RIGHT: 'right', + ANY: 'any' +}; + +/** + * Scratch 3.0 blocks to interact with a LEGO Boost peripheral. + */ +class Scratch3BoostBlocks { + + /** + * @return {string} - the ID of this extension. + */ + static get EXTENSION_ID () { + return 'boost'; + } + + /** + * @return {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold. + */ + static get TILT_THRESHOLD () { + return 15; + } + + /** + * Construct a set of Boost blocks. + * @param {Runtime} runtime - the Scratch 3.0 runtime. + */ + constructor (runtime) { + /** + * The Scratch 3.0 runtime. + * @type {Runtime} + */ + this.runtime = runtime; + + // Create a new Boost peripheral instance + this._peripheral = new Boost(this.runtime, Scratch3BoostBlocks.EXTENSION_ID); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: Scratch3BoostBlocks.EXTENSION_ID, + name: 'BOOST', + blockIconURI: iconURI, + showStatusButton: true, + blocks: [ + { + opcode: 'motorOnFor', + text: formatMessage({ + id: 'boost.motorOnFor', + default: 'turn motor [MOTOR_ID] for [DURATION] seconds', + description: 'turn a motor on for some time' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.A + }, + DURATION: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'motorOnForRotation', + text: formatMessage({ + id: 'boost.motorOnForRotation', + default: 'turn motor [MOTOR_ID] for [ROTATION] rotations', + description: 'turn a motor on for rotation' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.A + }, + ROTATION: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'motorOn', + text: formatMessage({ + id: 'boost.motorOn', + default: 'turn motor [MOTOR_ID] on', + description: 'turn a motor on indefinitely' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.A + } + } + }, + { + opcode: 'motorOff', + text: formatMessage({ + id: 'boost.motorOff', + default: 'turn motor [MOTOR_ID] off', + description: 'turn a motor off' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.A + } + } + }, + { + opcode: 'setMotorPower', + text: formatMessage({ + id: 'boost.setMotorPower', + default: 'set motor [MOTOR_ID] speed to [POWER] %', + description: 'set the motor\'s speed without turning it on' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.ALL + }, + POWER: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'setMotorDirection', + text: formatMessage({ + id: 'boost.setMotorDirection', + default: 'set motor [MOTOR_ID] direction [MOTOR_DIRECTION]', + description: 'set the motor\'s turn direction without turning it on' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.A + }, + MOTOR_DIRECTION: { + type: ArgumentType.STRING, + menu: 'MOTOR_DIRECTION', + defaultValue: BoostMotorDirection.FORWARD + } + } + }, + { + opcode: 'getMotorPosition', + text: formatMessage({ + id: 'boost.getMotorPosition', + default: 'motor [MOTOR_REPORTER_ID] position', + description: 'the position returned by the motor' + }), + blockType: BlockType.REPORTER, + arguments: { + MOTOR_REPORTER_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_REPORTER_ID', + defaultValue: BoostMotorLabel.A + } + } + }, + { + opcode: 'whenColor', + text: formatMessage({ + id: 'boost.whenColor', + default: 'when [COLOR] brick seen', + description: 'check for when color' + }), + blockType: BlockType.HAT, + arguments: { + COLOR: { + type: ArgumentType.STRING, + menu: 'COLOR', + defaultValue: BoostColor.ANY + } + } + }, + { + opcode: 'seeingColor', + text: formatMessage({ + id: 'boost.seeingColor', + default: 'seeing [COLOR] brick?', + description: 'is the color sensor seeing a certain color?' + }), + blockType: BlockType.BOOLEAN, + arguments: { + COLOR: { + type: ArgumentType.STRING, + menu: 'COLOR', + defaultValue: BoostColor.ANY + } + } + }, + { + opcode: 'whenTilted', + text: formatMessage({ + id: 'boost.whenTilted', + default: 'when tilted [TILT_DIRECTION_ANY]', + description: 'check when tilted in a certain direction' + }), + func: 'isTilted', + blockType: BlockType.HAT, + arguments: { + TILT_DIRECTION_ANY: { + type: ArgumentType.STRING, + menu: 'TILT_DIRECTION_ANY', + defaultValue: BoostTiltDirection.ANY + } + } + }, + { + opcode: 'getTiltAngle', + text: formatMessage({ + id: 'boost.getTiltAngle', + default: 'tilt angle [TILT_DIRECTION]', + description: 'the angle returned by the tilt sensor' + }), + blockType: BlockType.REPORTER, + arguments: { + TILT_DIRECTION: { + type: ArgumentType.STRING, + menu: 'TILT_DIRECTION', + defaultValue: BoostTiltDirection.UP + } + } + }, + { + opcode: 'setLightHue', + text: formatMessage({ + id: 'boost.setLightHue', + default: 'set light color to [HUE]', + description: 'set the LED color' + }), + blockType: BlockType.COMMAND, + arguments: { + HUE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + } + ], + menus: { + MOTOR_ID: { + acceptReporters: true, + items: [ + { + text: 'A', + value: BoostMotorLabel.A + }, + { + text: 'B', + value: BoostMotorLabel.B + }, + { + text: 'C', + value: BoostMotorLabel.C + }, + { + text: 'D', + value: BoostMotorLabel.D + }, + { + text: 'AB', + value: BoostMotorLabel.AB + }, + { + text: 'ABCD', + value: BoostMotorLabel.ALL + } + ] + }, + MOTOR_REPORTER_ID: { + acceptReporters: true, + items: [ + { + text: 'A', + value: BoostMotorLabel.A + }, + { + text: 'B', + value: BoostMotorLabel.B + }, + { + text: 'C', + value: BoostMotorLabel.C + }, + { + text: 'D', + value: BoostMotorLabel.D + } + ] + }, + MOTOR_DIRECTION: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'boost.motorDirection.forward', + default: 'this way', + description: + 'label for forward element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.FORWARD + }, + { + text: formatMessage({ + id: 'boost.motorDirection.backward', + default: 'that way', + description: + 'label for backward element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.BACKWARD + }, + { + text: formatMessage({ + id: 'boost.motorDirection.reverse', + default: 'reverse', + description: + 'label for reverse element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.REVERSE + } + ] + }, + TILT_DIRECTION: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'boost.tiltDirection.up', + default: 'up', + description: 'label for up element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.UP + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.down', + default: 'down', + description: 'label for down element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.left', + default: 'left', + description: 'label for left element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.right', + default: 'right', + description: 'label for right element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.RIGHT + } + ] + }, + TILT_DIRECTION_ANY: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'boost.tiltDirection.up', + default: 'up' + }), + value: BoostTiltDirection.UP + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.down', + default: 'down' + }), + value: BoostTiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.left', + default: 'left' + }), + value: BoostTiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.right', + default: 'right' + }), + value: BoostTiltDirection.RIGHT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.any', + default: 'any', + description: 'label for any element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.ANY + } + ] + }, + COLOR: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'boost.color.red', + default: 'red', + description: 'the color red' + }), + value: BoostColor.RED + }, + { + text: formatMessage({ + id: 'boost.color.blue', + default: 'blue', + description: 'the color blue' + }), + value: BoostColor.BLUE + }, + { + text: formatMessage({ + id: 'boost.color.green', + default: 'green', + description: 'the color green' + }), + value: BoostColor.GREEN + }, + { + text: formatMessage({ + id: 'boost.color.yellow', + default: 'yellow', + description: 'the color yellow' + }), + value: BoostColor.YELLOW + }, + { + text: formatMessage({ + id: 'boost.color.white', + default: 'white', + desription: 'the color white' + }), + value: BoostColor.WHITE + }, + { + text: formatMessage({ + id: 'boost.color.black', + default: 'black', + description: 'the color black' + }), + value: BoostColor.BLACK + }, + { + text: formatMessage({ + id: 'boost.color.any', + default: 'any color', + description: 'any color' + }), + value: BoostColor.ANY + } + ] + } + } + }; + } + + /** + * Turn specified motor(s) on for a specified duration. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to activate. + * @property {int} DURATION - the amount of time to run the motors. + * @return {Promise} - a promise which will resolve at the end of the duration. + */ + motorOnFor (args) { + // TODO: cast args.MOTOR_ID? + let durationMS = Cast.toNumber(args.DURATION) * 1000; + durationMS = MathUtil.clamp(durationMS, 0, 15000); + return new Promise(resolve => { + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) motor.turnOnFor(durationMS); + }); + + // Run for some time even when no motor is connected + setTimeout(resolve, durationMS); + }); + } + + /** + * Turn specified motor(s) on for a specified rotation in full rotations. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to activate. + * @property {int} ROTATION - the amount of full rotations to turn the motors. + * @return {Promise} - a promise which will resolve at the end of the duration. + */ + motorOnForRotation (args) { + // TODO: cast args.MOTOR_ID? + let degrees = Cast.toNumber(args.ROTATION) * 360; + // TODO: Clamps to 100 rotations. Consider changing. + const sign = Math.sign(degrees); + degrees = Math.abs(MathUtil.clamp(degrees, -360000, 360000)); + + const motors = []; + this._forEachMotor(args.MOTOR_ID, motorIndex => { + motors.push(motorIndex); + }); + + /** + * Checks that the motors given in args.MOTOR_ID exist, + * and maps a promise for each of the motor-commands to an array. + */ + const promises = motors.map(portID => { + const motor = this._peripheral.motor(portID); + if (motor) { + // to avoid a hanging block if power is 0, return an immediately resolving promise. + if (motor.power === 0) return Promise.resolve(); + return new Promise(resolve => { + motor.turnOnForDegrees(degrees, sign); + motor.pendingRotationPromise = resolve; + }); + } + return null; + }); + /** + * Make sure all promises are resolved, i.e. all motor-commands have completed. + * To prevent the block from returning a value, an empty function is added to the .then + */ + return Promise.all(promises).then(() => {}); + } + + /** + * Turn specified motor(s) on indefinitely. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to activate. + * @return {Promise} - a Promise that resolves after some delay. + */ + motorOn (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) motor.turnOnForever(); + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BoostBLE.sendInterval); + }); + } + + /** + * Turn specified motor(s) off. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to deactivate. + * @return {Promise} - a Promise that resolves after some delay. + */ + motorOff (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) motor.turnOff(); + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BoostBLE.sendInterval); + }); + } + + /** + * Set the power level of the specified motor(s). + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to be affected. + * @property {int} POWER - the new power level for the motor(s). + * @return {Promise} - returns a promise to make sure the block yields. + */ + setMotorPower (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100); + switch (motor.status) { + case BoostMotorState.ON_FOREVER: + motor.turnOnForever(); + break; + case BoostMotorState.ON_FOR_TIME: + motor.turnOnFor(motor.pendingDurationTimeoutStartTime + + motor.pendingDurationTimeoutDelay - Date.now()); + break; + } + } + }); + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BoostBLE.sendInterval); + }); + } + + /** + * Set the direction of rotation for specified motor(s). + * If the direction is 'reverse' the motor(s) will be reversed individually. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to be affected. + * @property {MotorDirection} MOTOR_DIRECTION - the new direction for the motor(s). + * @return {Promise} - returns a promise to make sure the block yields. + */ + setMotorDirection (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + switch (args.MOTOR_DIRECTION) { + case BoostMotorDirection.FORWARD: + motor.direction = 1; + break; + case BoostMotorDirection.BACKWARD: + motor.direction = -1; + break; + case BoostMotorDirection.REVERSE: + motor.direction = -motor.direction; + break; + default: + log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`); + break; + } + // keep the motor on if it's running, and update the pending timeout if needed + if (motor) { + switch (motor.status) { + case BoostMotorState.ON_FOREVER: + motor.turnOnForever(); + break; + case BoostMotorState.ON_FOR_TIME: + motor.turnOnFor(motor.pendingDurationTimeoutStartTime + + motor.pendingDurationTimeoutDelay - Date.now()); + break; + } + } + } + }); + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BoostBLE.sendInterval); + }); + } + + /** + * @param {object} args - the block's arguments. + * @return {number} - returns the motor's position. + */ + getMotorPosition (args) { + let portID = null; + switch (args.MOTOR_REPORTER_ID) { + + case BoostMotorLabel.A: + portID = BoostPort.A; + break; + case BoostMotorLabel.B: + portID = BoostPort.B; + break; + case BoostMotorLabel.C: + portID = BoostPort.C; + break; + case BoostMotorLabel.D: + portID = BoostPort.D; + break; + default: + log.warn('Asked for a motor position that doesnt exist!'); + return false; + } + if (portID !== null && this._peripheral.motor(portID)) { + let val = this._peripheral.motor(portID).position; + // Boost motor A position direction is reversed by design + // so we have to reverse the position here + if (portID === BoostPort.A) { + val *= -1; + } + return MathUtil.wrapClamp(val, 0, 360); + } + return 0; + } + + /** + * Call a callback for each motor indexed by the provided motor ID. + * @param {MotorID} motorID - the ID specifier. + * @param {Function} callback - the function to call with the numeric motor index for each motor. + * @private + */ + _forEachMotor (motorID, callback) { + let motors; + switch (motorID) { + case BoostMotorLabel.A: + motors = [BoostPort.A]; + break; + case BoostMotorLabel.B: + motors = [BoostPort.B]; + break; + case BoostMotorLabel.C: + motors = [BoostPort.C]; + break; + case BoostMotorLabel.D: + motors = [BoostPort.D]; + break; + case BoostMotorLabel.AB: + motors = [BoostPort.A, BoostPort.B]; + break; + case BoostMotorLabel.ALL: + motors = [BoostPort.A, BoostPort.B, BoostPort.C, BoostPort.D]; + break; + default: + log.warn(`Invalid motor ID: ${motorID}`); + motors = []; + break; + } + for (const index of motors) { + callback(index); + } + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + whenTilted (args) { + return this._isTilted(args.TILT_DIRECTION_ANY); + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + isTilted (args) { + return this._isTilted(args.TILT_DIRECTION_ANY); + } + + /** + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION - the direction (up, down, left, right) to check. + * @return {number} - the tilt sensor's angle in the specified direction. + * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right). + */ + getTiltAngle (args) { + return this._getTiltAngle(args.TILT_DIRECTION); + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {TiltDirection} direction - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + * @private + */ + _isTilted (direction) { + switch (direction) { + case BoostTiltDirection.ANY: + return (Math.abs(this._peripheral.tiltX) >= Scratch3BoostBlocks.TILT_THRESHOLD) || + (Math.abs(this._peripheral.tiltY) >= Scratch3BoostBlocks.TILT_THRESHOLD); + default: + return this._getTiltAngle(direction) >= Scratch3BoostBlocks.TILT_THRESHOLD; + } + } + + /** + * @param {TiltDirection} direction - the direction (up, down, left, right) to check. + * @return {number} - the tilt sensor's angle in the specified direction. + * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right). + * @private + */ + _getTiltAngle (direction) { + switch (direction) { + case BoostTiltDirection.UP: + return this._peripheral.tiltY > 90 ? 256 - this._peripheral.tiltY : -this._peripheral.tiltY; + case BoostTiltDirection.DOWN: + return this._peripheral.tiltY > 90 ? this._peripheral.tiltY - 256 : this._peripheral.tiltY; + case BoostTiltDirection.LEFT: + return this._peripheral.tiltX > 90 ? this._peripheral.tiltX - 256 : this._peripheral.tiltX; + case BoostTiltDirection.RIGHT: + return this._peripheral.tiltX > 90 ? 256 - this._peripheral.tiltX : -this._peripheral.tiltX; + default: + log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`); + } + } + + /** + * Edge-triggering hat function, for when the vision sensor is detecting + * a certain color. + * @param {object} args - the block's arguments. + * @return {boolean} - true when the color sensor senses the specified color. + */ + whenColor (args) { + if (args.COLOR === BoostColor.ANY) { + // For "any" color, return true if the color is not "none", and + // the color is different from the previous color detected. This + // allows the hat to trigger when the color changes from one color + // to another. + return this._peripheral.color !== BoostColor.NONE && + this._peripheral.color !== this._peripheral.previousColor; + } + + return args.COLOR === this._peripheral.color; + } + + /** + * A boolean reporter function, for whether the vision sensor is detecting + * a certain color. + * @param {object} args - the block's arguments. + * @return {boolean} - true when the color sensor senses the specified color. + */ + seeingColor (args) { + if (args.COLOR === BoostColor.ANY) { + return this._peripheral.color !== BoostColor.NONE; + } + + return args.COLOR === this._peripheral.color; + } + + /** + * Set the LED's hue. + * @param {object} args - the block's arguments. + * @property {number} HUE - the hue to set, in the range [0,100]. + * @return {Promise} - a Promise that resolves after some delay. + */ + setLightHue (args) { + // Convert from [0,100] to [0,360] + let inputHue = Cast.toNumber(args.HUE); + inputHue = MathUtil.wrapClamp(inputHue, 0, 100); + const hue = inputHue * 360 / 100; + + const rgbObject = color.hsvToRgb({h: hue, s: 1, v: 1}); + + const rgbDecimal = color.rgbToDecimal(rgbObject); + + this._peripheral._led = inputHue; + this._peripheral.setLED(rgbDecimal); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BoostBLE.sendInterval); + }); + } +} + +module.exports = Scratch3BoostBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_ev3/index.js b/local-scratch-vm/src/extensions/scratch3_ev3/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7803f2394e390a2dc6fc5662f39170cd81e1257d --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_ev3/index.js @@ -0,0 +1,1355 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); +const formatMessage = require('format-message'); +const uid = require('../../util/uid'); +const BT = require('../../io/bt'); +const Base64Util = require('../../util/base64-util'); +const MathUtil = require('../../util/math-util'); +const RateLimiter = require('../../util/rateLimiter.js'); +const log = require('../../util/log'); + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIwIDAgNDAgNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUwLjIgKDU1MDQ3KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5ldjMtYmxvY2staWNvbjwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJldjMtYmxvY2staWNvbiIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9ImV2MyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNS41MDAwMDAsIDMuNTAwMDAwKSIgZmlsbC1ydWxlPSJub256ZXJvIj4KICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZS1wYXRoIiBzdHJva2U9IiM3Qzg3QTUiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgeD0iMC41IiB5PSIzLjU5IiB3aWR0aD0iMjgiIGhlaWdodD0iMjUuODEiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRTZFN0U4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHg9IjIuNSIgeT0iMC41IiB3aWR0aD0iMjQiIGhlaWdodD0iMzIiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRkZGRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHg9IjIuNSIgeT0iMTQuNSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjEzIj48L3JlY3Q+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNC41LDEwLjUgTDE0LjUsMTQuNSIgaWQ9IlNoYXBlIiBzdHJva2U9IiM3Qzg3QTUiIGZpbGw9IiNFNkU3RTgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPgogICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlLXBhdGgiIGZpbGw9IiM0MTQ3NTciIHg9IjQuNSIgeT0iMi41IiB3aWR0aD0iMjAiIGhlaWdodD0iMTAiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgZmlsbD0iIzdDODdBNSIgb3BhY2l0eT0iMC41IiB4PSIxMy41IiB5PSIyMC4xMyIgd2lkdGg9IjIiIGhlaWdodD0iMiIgcng9IjAuNSI+PC9yZWN0PgogICAgICAgICAgICA8cGF0aCBkPSJNOS4wNiwyMC4xMyBMMTAuNTYsMjAuMTMgQzEwLjgzNjE0MjQsMjAuMTMgMTEuMDYsMjAuMzUzODU3NiAxMS4wNiwyMC42MyBMMTEuMDYsMjEuNjMgQzExLjA2LDIxLjkwNjE0MjQgMTAuODM2MTQyNCwyMi4xMyAxMC41NiwyMi4xMyBMOS4wNiwyMi4xMyBDOC41MDc3MTUyNSwyMi4xMyA4LjA2LDIxLjY4MjI4NDcgOC4wNiwyMS4xMyBDOC4wNiwyMC41Nzc3MTUzIDguNTA3NzE1MjUsMjAuMTMgOS4wNiwyMC4xMyBaIiBpZD0iU2hhcGUiIGZpbGw9IiM3Qzg3QTUiIG9wYWNpdHk9IjAuNSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMTguOTEsMjAuMTMgTDIwLjQyLDIwLjEzIEMyMC42OTYxNDI0LDIwLjEzIDIwLjkyLDIwLjM1Mzg1NzYgMjAuOTIsMjAuNjMgTDIwLjkyLDIxLjYzIEMyMC45MiwyMS45MDYxNDI0IDIwLjY5NjE0MjQsMjIuMTMgMjAuNDIsMjIuMTMgTDE4LjkyLDIyLjEzIEMxOC4zNjc3MTUzLDIyLjEzIDE3LjkyLDIxLjY4MjI4NDcgMTcuOTIsMjEuMTMgQzE3LjkxOTk3MjYsMjAuNTgxNTk3IDE4LjM2MTYyNDUsMjAuMTM1NDg0IDE4LjkxLDIwLjEzIFoiIGlkPSJTaGFwZSIgZmlsbD0iIzdDODdBNSIgb3BhY2l0eT0iMC41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOS40MjAwMDAsIDIxLjEzMDAwMCkgcm90YXRlKC0xODAuMDAwMDAwKSB0cmFuc2xhdGUoLTE5LjQyMDAwMCwgLTIxLjEzMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik04LjIzLDE3LjUgTDUsMTcuNSBDNC43MjM4NTc2MywxNy41IDQuNSwxNy4yNzYxNDI0IDQuNSwxNyBMNC41LDE0LjUgTDEwLjUsMTQuNSBMOC42NSwxNy4yOCBDOC41NTQ2Njk2MSwxNy40MTc5MDgyIDguMzk3NjUwMDYsMTcuNTAwMTU2NiA4LjIzLDE3LjUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjN0M4N0E1IiBvcGFjaXR5PSIwLjUiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTE4LjE1LDE4Ljg1IEwxNy42NSwxOS4zNSBDMTcuNTUyMzQxNiwxOS40NDQwNzU2IDE3LjQ5ODAzMzksMTkuNTc0NDE0MiAxNy41LDE5LjcxIEwxNy41LDIwIEMxNy41LDIwLjI3NjE0MjQgMTcuMjc2MTQyNCwyMC41IDE3LDIwLjUgTDE2LjUsMjAuNSBDMTYuMjIzODU3NiwyMC41IDE2LDIwLjI3NjE0MjQgMTYsMjAgQzE2LDE5LjcyMzg1NzYgMTUuNzc2MTQyNCwxOS41IDE1LjUsMTkuNSBMMTMuNSwxOS41IEMxMy4yMjM4NTc2LDE5LjUgMTMsMTkuNzIzODU3NiAxMywyMCBDMTMsMjAuMjc2MTQyNCAxMi43NzYxNDI0LDIwLjUgMTIuNSwyMC41IEwxMiwyMC41IEMxMS43MjM4NTc2LDIwLjUgMTEuNSwyMC4yNzYxNDI0IDExLjUsMjAgTDExLjUsMTkuNzEgQzExLjUwMTk2NjEsMTkuNTc0NDE0MiAxMS40NDc2NTg0LDE5LjQ0NDA3NTYgMTEuMzUsMTkuMzUgTDEwLjg1LDE4Ljg1IEMxMC42NTgyMTY3LDE4LjY1MjE4NjMgMTAuNjU4MjE2NywxOC4zMzc4MTM3IDEwLjg1LDE4LjE0IEwxMi4zNiwxNi42NSBDMTIuNDUwMjgwMywxNi41NTI4NjE3IDEyLjU3NzM5NjEsMTYuNDk4MzgzNSAxMi43MSwxNi41IEwxNi4yOSwxNi41IEMxNi40MjI2MDM5LDE2LjQ5ODM4MzUgMTYuNTQ5NzE5NywxNi41NTI4NjE3IDE2LjY0LDE2LjY1IEwxOC4xNSwxOC4xNCBDMTguMzQxNzgzMywxOC4zMzc4MTM3IDE4LjM0MTc4MzMsMTguNjUyMTg2MyAxOC4xNSwxOC44NSBaIiBpZD0iU2hhcGUiIGZpbGw9IiM3Qzg3QTUiIG9wYWNpdHk9IjAuNSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMTAuODUsMjMuNDUgTDExLjM1LDIyLjk1IEMxMS40NDc2NTg0LDIyLjg1NTkyNDQgMTEuNTAxOTY2MSwyMi43MjU1ODU4IDExLjUsMjIuNTkgTDExLjUsMjIuMyBDMTEuNSwyMi4wMjM4NTc2IDExLjcyMzg1NzYsMjEuOCAxMiwyMS44IEwxMi41LDIxLjggQzEyLjc3NjE0MjQsMjEuOCAxMywyMi4wMjM4NTc2IDEzLDIyLjMgQzEzLDIyLjU3NjE0MjQgMTMuMjIzODU3NiwyMi44IDEzLjUsMjIuOCBMMTUuNSwyMi44IEMxNS43NzYxNDI0LDIyLjggMTYsMjIuNTc2MTQyNCAxNiwyMi4zIEMxNiwyMi4wMjM4NTc2IDE2LjIyMzg1NzYsMjEuOCAxNi41LDIxLjggTDE3LDIxLjggQzE3LjI3NjE0MjQsMjEuOCAxNy41LDIyLjAyMzg1NzYgMTcuNSwyMi4zIEwxNy41LDIyLjU5IEMxNy40OTgwMzM5LDIyLjcyNTU4NTggMTcuNTUyMzQxNiwyMi44NTU5MjQ0IDE3LjY1LDIyLjk1IEwxOC4xNSwyMy40NSBDMTguMzQwNTcxNCwyMy42NDQ0MjE4IDE4LjM0MDU3MTQsMjMuOTU1NTc4MiAxOC4xNSwyNC4xNSBMMTYuNjQsMjUuNjUgQzE2LjU0OTcxOTcsMjUuNzQ3MTM4MyAxNi40MjI2MDM5LDI1LjgwMTYxNjUgMTYuMjksMjUuOCBMMTIuNzEsMjUuOCBDMTIuNTc3Mzk2MSwyNS44MDE2MTY1IDEyLjQ1MDI4MDMsMjUuNzQ3MTM4MyAxMi4zNiwyNS42NSBMMTAuODUsMjQuMTUgQzEwLjY1OTQyODYsMjMuOTU1NTc4MiAxMC42NTk0Mjg2LDIzLjY0NDQyMTggMTAuODUsMjMuNDUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjN0M4N0E1IiBvcGFjaXR5PSIwLjUiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTIxLjUsMjcuNSBMMjYuNSwyNy41IEwyNi41LDMxLjUgQzI2LjUsMzIuMDUyMjg0NyAyNi4wNTIyODQ3LDMyLjUgMjUuNSwzMi41IEwyMS41LDMyLjUgTDIxLjUsMjcuNSBaIiBpZD0iU2hhcGUiIHN0cm9rZT0iI0NDNEMyMyIgZmlsbD0iI0YxNUEyOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4='; + +/** + * String with Ev3 expected pairing pin. + * @readonly + */ +const Ev3PairingPin = '1234'; + +/** + * A maximum number of BT message sends per second, to be enforced by the rate limiter. + * @type {number} + */ +const BTSendRateMax = 40; + +/** + * Enum for Ev3 parameter encodings of various argument and return values. + * Found in the 'EV3 Firmware Developer Kit', section4, page 9, at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits. + * + * The format for these values is: + * 0xxxxxxx for Short Format + * 1ttt-bbb for Long Format + * + * @readonly + * @enum {number} + */ +const Ev3Encoding = { + ONE_BYTE: 0x81, // = 0b1000-001, "1 byte to follow" + TWO_BYTES: 0x82, // = 0b1000-010, "2 bytes to follow" + FOUR_BYTES: 0x83, // = 0b1000-011, "4 bytes to follow" + GLOBAL_VARIABLE_ONE_BYTE: 0xE1, // = 0b1110-001, "1 byte to follow" + GLOBAL_CONSTANT_INDEX_0: 0x20, // = 0b00100000 + GLOBAL_VARIABLE_INDEX_0: 0x60 // = 0b01100000 +}; + +/** + * Enum for Ev3 direct command types. + * Found in the 'EV3 Communication Developer Kit', section 4, page 24, at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits. + * @readonly + * @enum {number} + */ +const Ev3Command = { + DIRECT_COMMAND_REPLY: 0x00, + DIRECT_COMMAND_NO_REPLY: 0x80, + DIRECT_REPLY: 0x02 +}; + +/** + * Enum for Ev3 commands opcodes. + * Found in the 'EV3 Firmware Developer Kit', section 4, page 10, at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits. + * @readonly + * @enum {number} + */ +const Ev3Opcode = { + OPOUTPUT_STEP_SPEED: 0xAE, + OPOUTPUT_TIME_SPEED: 0xAF, + OPOUTPUT_STOP: 0xA3, + OPOUTPUT_RESET: 0xA2, + OPOUTPUT_STEP_SYNC: 0xB0, + OPOUTPUT_TIME_SYNC: 0xB1, + OPOUTPUT_GET_COUNT: 0xB3, + OPSOUND: 0x94, + OPSOUND_CMD_TONE: 1, + OPSOUND_CMD_STOP: 0, + OPINPUT_DEVICE_LIST: 0x98, + OPINPUT_READSI: 0x9D +}; + +/** + * Enum for Ev3 values used as arguments to various opcodes. + * Found in the 'EV3 Firmware Developer Kit', section4, page 10-onwards, at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits. + * @readonly + * @enum {number} + */ +const Ev3Args = { + LAYER: 0, // always 0, chained EV3s not supported + COAST: 0, + BRAKE: 1, + RAMP: 50, // time in milliseconds + DO_NOT_CHANGE_TYPE: 0, + MAX_DEVICES: 32 // 'Normally 32' from pg. 46 +}; + +/** + * Enum for Ev3 device type numbers. + * Found in the 'EV3 Firmware Developer Kit', section 5, page 100, at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits. + * @readonly + * @enum {string} + */ +const Ev3Device = { + 29: 'color', + 30: 'ultrasonic', + 32: 'gyro', + 16: 'touch', + 8: 'mediumMotor', + 7: 'largeMotor', + 126: 'none', + 125: 'none' +}; + +/** + * Enum for Ev3 device modes. + * Found in the 'EV3 Firmware Developer Kit', section 5, page 100, at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits. + * @readonly + * @enum {number} + */ +const Ev3Mode = { + touch: 0, // touch + color: 1, // ambient + ultrasonic: 1, // inch + none: 0 +}; + +/** + * Enum for Ev3 device labels used in the Scratch blocks/UI. + * @readonly + * @enum {string} + */ +const Ev3Label = { + touch: 'button', + color: 'brightness', + ultrasonic: 'distance' +}; + +/** + * Manage power, direction, and timers for one EV3 motor. + */ +class EV3Motor { + + /** + * Construct a EV3 Motor instance, which could be of type 'largeMotor' or + * 'mediumMotor'. + * + * @param {EV3} parent - the EV3 peripheral which owns this motor. + * @param {int} index - the zero-based index of this motor on its parent peripheral. + * @param {string} type - the type of motor (i.e. 'largeMotor' or 'mediumMotor'). + */ + constructor (parent, index, type) { + /** + * The EV3 peripheral which owns this motor. + * @type {EV3} + * @private + */ + this._parent = parent; + + /** + * The zero-based index of this motor on its parent peripheral. + * @type {int} + * @private + */ + this._index = index; + + /** + * The type of EV3 motor this could be: 'largeMotor' or 'mediumMotor'. + * @type {string} + * @private + */ + this._type = type; + + /** + * This motor's current direction: 1 for "clockwise" or -1 for "counterclockwise" + * @type {number} + * @private + */ + this._direction = 1; + + /** + * This motor's current power level, in the range [0,100]. + * @type {number} + * @private + */ + this._power = 50; + + /** + * This motor's current position, in the range [0,360]. + * @type {number} + * @private + */ + this._position = 0; + + /** + * An ID for the current coast command, to help override multiple coast + * commands sent in succession. + * @type {number} + * @private + */ + this._commandID = null; + + /** + * A delay, in milliseconds, to add to coasting, to make sure that a brake + * first takes effect if one was sent. + * @type {number} + * @private + */ + this._coastDelay = 1000; + } + + /** + * @return {string} - this motor's type: 'largeMotor' or 'mediumMotor' + */ + get type () { + return this._type; + } + + /** + * @param {string} value - this motor's new type: 'largeMotor' or 'mediumMotor' + */ + set type (value) { + this._type = value; + } + + /** + * @return {int} - this motor's current direction: 1 for "clockwise" or -1 for "counterclockwise" + */ + get direction () { + return this._direction; + } + + /** + * @param {int} value - this motor's new direction: 1 for "clockwise" or -1 for "counterclockwise" + */ + set direction (value) { + if (value < 0) { + this._direction = -1; + } else { + this._direction = 1; + } + } + + /** + * @return {int} - this motor's current power level, in the range [0,100]. + */ + get power () { + return this._power; + } + + /** + * @param {int} value - this motor's new power level, in the range [0,100]. + */ + set power (value) { + this._power = value; + } + + /** + * @return {int} - this motor's current position, in the range [-inf,inf]. + */ + get position () { + return this._position; + } + + /** + * @param {int} array - this motor's new position, in the range [0,360]. + */ + set position (array) { + // tachoValue from Paula + let value = array[0] + (array[1] * 256) + (array[2] * 256 * 256) + (array[3] * 256 * 256 * 256); + if (value > 0x7fffffff) { + value = value - 0x100000000; + } + this._position = value; + } + + /** + * Turn this motor on for a specific duration. + * Found in the 'EV3 Firmware Developer Kit', page 56, at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits. + * + * Opcode arguments: + * (Data8) LAYER – Specify chain layer number [0 - 3] + * (Data8) NOS – Output bit field [0x00 – 0x0F] + * (Data8) SPEED – Power level, [-100 – 100] + * (Data32) STEP1 – Time in milliseconds for ramp up + * (Data32) STEP2 – Time in milliseconds for continues run + * (Data32) STEP3 – Time in milliseconds for ramp down + * (Data8) BRAKE - Specify break level [0: Float, 1: Break] + * + * @param {number} milliseconds - run the motor for this long. + */ + turnOnFor (milliseconds) { + if (this._power === 0) return; + + const port = this._portMask(this._index); + let n = milliseconds; + let speed = this._power * this._direction; + const ramp = Ev3Args.RAMP; + + let byteCommand = []; + byteCommand[0] = Ev3Opcode.OPOUTPUT_TIME_SPEED; + + // If speed is less than zero, make it positive and multiply the input + // value by -1 + if (speed < 0) { + speed = -1 * speed; + n = -1 * n; + } + // If the input value is less than 0 + const dir = (n < 0) ? 0x100 - speed : speed; // step negative or positive + n = Math.abs(n); + // Setup motor run duration and ramping behavior + let rampup = ramp; + let rampdown = ramp; + let run = n - (ramp * 2); + if (run < 0) { + rampup = Math.floor(n / 2); + run = 0; + rampdown = n - rampup; + } + // Generate motor command values + const runcmd = this._runValues(run); + byteCommand = byteCommand.concat([ + Ev3Args.LAYER, + port, + Ev3Encoding.ONE_BYTE, + dir & 0xff, + Ev3Encoding.ONE_BYTE, + rampup + ]).concat(runcmd.concat([ + Ev3Encoding.ONE_BYTE, + rampdown, + Ev3Args.BRAKE + ])); + + const cmd = this._parent.generateCommand( + Ev3Command.DIRECT_COMMAND_NO_REPLY, + byteCommand + ); + + this._parent.send(cmd); + + this.coastAfter(milliseconds); + } + + /** + * Set the motor to coast after a specified amount of time. + * @param {number} time - the time in milliseconds. + */ + coastAfter (time) { + if (this._power === 0) return; + + // Set the motor command id to check before starting coast + const commandId = uid(); + this._commandID = commandId; + + // Send coast message + setTimeout(() => { + // Do not send coast if another motor command changed the command id. + if (this._commandID === commandId) { + this.coast(); + this._commandID = null; + } + }, time + this._coastDelay); // add a delay so the brake takes effect + } + + /** + * Set the motor to coast. + */ + coast () { + if (this._power === 0) return; + + const cmd = this._parent.generateCommand( + Ev3Command.DIRECT_COMMAND_NO_REPLY, + [ + Ev3Opcode.OPOUTPUT_STOP, + Ev3Args.LAYER, + this._portMask(this._index), // port output bit field + Ev3Args.COAST + ] + ); + + this._parent.send(cmd, false); // don't use rate limiter to ensure motor stops + } + + /** + * Generate motor run values for a given input. + * @param {number} run - run input. + * @return {array} - run values as a byte array. + */ + _runValues (run) { + // If run duration is less than max 16-bit integer + if (run < 0x7fff) { + return [ + Ev3Encoding.TWO_BYTES, + run & 0xff, + (run >> 8) & 0xff + ]; + } + + // Run forever + return [ + Ev3Encoding.FOUR_BYTES, + run & 0xff, + (run >> 8) & 0xff, + (run >> 16) & 0xff, + (run >> 24) & 0xff + ]; + } + + /** + * Return a port value for the EV3 that is in the format for 'output bit field' + * as 1/2/4/8, generally needed for motor ports, instead of the typical 0/1/2/3. + * The documentation in the 'EV3 Firmware Developer Kit' for motor port arguments + * is sometimes mistaken, but we believe motor ports are mostly addressed this way. + * @param {number} port - the port number to convert to an 'output bit field'. + * @return {number} - the converted port number. + */ + _portMask (port) { + return Math.pow(2, port); + } +} + +class EV3 { + + constructor (runtime, extensionId) { + + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} + * @private + */ + this._runtime = runtime; + this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this)); + + /** + * The id of the extension this peripheral belongs to. + */ + this._extensionId = extensionId; + + /** + * A list of the names of the sensors connected in ports 1,2,3,4. + * @type {string[]} + * @private + */ + this._sensorPorts = []; + + /** + * A list of the names of the motors connected in ports A,B,C,D. + * @type {string[]} + * @private + */ + this._motorPorts = []; + + /** + * The state of all sensor values. + * @type {string[]} + * @private + */ + this._sensors = { + distance: 0, + brightness: 0, + buttons: [0, 0, 0, 0] + }; + + /** + * The motors which this EV3 could possibly have connected. + * @type {string[]} + * @private + */ + this._motors = [null, null, null, null]; + + /** + * The polling interval, in milliseconds. + * @type {number} + * @private + */ + this._pollingInterval = 150; + + /** + * The polling interval ID. + * @type {number} + * @private + */ + this._pollingIntervalID = null; + + /** + * The counter keeping track of polling cycles. + * @type {string[]} + * @private + */ + this._pollingCounter = 0; + + /** + * The Bluetooth socket connection for reading/writing peripheral data. + * @type {BT} + * @private + */ + this._bt = null; + this._runtime.registerPeripheralExtension(extensionId, this); + + /** + * A rate limiter utility, to help limit the rate at which we send BT messages + * over the socket to Scratch Link to a maximum number of sends per second. + * @type {RateLimiter} + * @private + */ + this._rateLimiter = new RateLimiter(BTSendRateMax); + + this.reset = this.reset.bind(this); + this._onConnect = this._onConnect.bind(this); + this._onMessage = this._onMessage.bind(this); + this._pollValues = this._pollValues.bind(this); + } + + get distance () { + let value = this._sensors.distance > 100 ? 100 : this._sensors.distance; + value = value < 0 ? 0 : value; + value = Math.round(100 * value) / 100; + + return value; + } + + get brightness () { + return this._sensors.brightness; + } + + /** + * Access a particular motor on this peripheral. + * @param {int} index - the zero-based index of the desired motor. + * @return {EV3Motor} - the EV3Motor instance, if any, at that index. + */ + motor (index) { + return this._motors[index]; + } + + isButtonPressed (port) { + return this._sensors.buttons[port] === 1; + } + + beep (freq, time) { + const cmd = this.generateCommand( + Ev3Command.DIRECT_COMMAND_NO_REPLY, + [ + Ev3Opcode.OPSOUND, + Ev3Opcode.OPSOUND_CMD_TONE, + Ev3Encoding.ONE_BYTE, + 2, + Ev3Encoding.TWO_BYTES, + freq, + freq >> 8, + Ev3Encoding.TWO_BYTES, + time, + time >> 8 + ] + ); + + this.send(cmd); + } + + stopAll () { + this.stopAllMotors(); + this.stopSound(); + } + + stopSound () { + const cmd = this.generateCommand( + Ev3Command.DIRECT_COMMAND_NO_REPLY, + [ + Ev3Opcode.OPSOUND, + Ev3Opcode.OPSOUND_CMD_STOP + ] + ); + + this.send(cmd, false); // don't use rate limiter to ensure sound stops + } + + stopAllMotors () { + this._motors.forEach(motor => { + if (motor) { + motor.coast(); + } + }); + } + + /** + * Called by the runtime when user wants to scan for an EV3 peripheral. + */ + scan () { + if (this._bt) { + this._bt.disconnect(); + } + this._bt = new BT(this._runtime, this._extensionId, { + majorDeviceClass: 8, + minorDeviceClass: 1 + }, this._onConnect, this.reset, this._onMessage); + } + + /** + * Called by the runtime when user wants to connect to a certain EV3 peripheral. + * @param {number} id - the id of the peripheral to connect to. + */ + connect (id) { + if (this._bt) { + this._bt.connectPeripheral(id, Ev3PairingPin); + } + } + + /** + * Called by the runtime when user wants to disconnect from the EV3 peripheral. + */ + disconnect () { + if (this._bt) { + this._bt.disconnect(); + } + + this.reset(); + } + + /** + * Reset all the state and timeout/interval ids. + */ + reset () { + this._sensorPorts = []; + this._motorPorts = []; + this._sensors = { + distance: 0, + brightness: 0, + buttons: [0, 0, 0, 0] + }; + this._motors = [null, null, null, null]; + + if (this._pollingIntervalID) { + window.clearInterval(this._pollingIntervalID); + this._pollingIntervalID = null; + } + } + + /** + * Called by the runtime to detect whether the EV3 peripheral is connected. + * @return {boolean} - the connected state. + */ + isConnected () { + let connected = false; + if (this._bt) { + connected = this._bt.isConnected(); + } + return connected; + } + + /** + * Send a message to the peripheral BT socket. + * @param {Uint8Array} message - the message to send. + * @param {boolean} [useLimiter=true] - if true, use the rate limiter + * @return {Promise} - a promise result of the send operation. + */ + send (message, useLimiter = true) { + if (!this.isConnected()) return Promise.resolve(); + + if (useLimiter) { + if (!this._rateLimiter.okayToSend()) return Promise.resolve(); + } + + return this._bt.sendMessage({ + message: Base64Util.uint8ArrayToBase64(message), + encoding: 'base64' + }); + } + + /** + * Genrates direct commands that are sent to the EV3 as a single or compounded byte arrays. + * See 'EV3 Communication Developer Kit', section 4, page 24 at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits. + * + * Direct commands are one of two types: + * DIRECT_COMMAND_NO_REPLY = a direct command where no reply is expected + * DIRECT_COMMAND_REPLY = a direct command where a reply is expected, and the + * number and length of returned values needs to be specified. + * + * The direct command byte array sent takes the following format: + * Byte 0 - 1: Command size, Little Endian. Command size not including these 2 bytes + * Byte 2 - 3: Message counter, Little Endian. Forth running counter + * Byte 4: Command type. Either DIRECT_COMMAND_REPLY or DIRECT_COMMAND_NO_REPLY + * Byte 5 - 6: Reservation (allocation) of global and local variables using a compressed format + * (globals reserved in byte 5 and the 2 lsb of byte 6, locals reserved in the upper + * 6 bits of byte 6) – see documentation for more details. + * Byte 7 - n: Byte codes as a single command or compound commands (I.e. more commands composed + * as a small program) + * + * @param {number} type - the direct command type. + * @param {string} byteCommands - a compound array of EV3 Opcode + arguments. + * @param {number} allocation - the allocation of global and local vars needed for replies. + * @return {array} - generated complete command byte array, with header and compounded commands. + */ + generateCommand (type, byteCommands, allocation = 0) { + + // Header (Bytes 0 - 6) + let command = []; + command[2] = 0; // Message counter unused for now + command[3] = 0; // Message counter unused for now + command[4] = type; + command[5] = allocation & 0xFF; + command[6] = allocation >> 8 && 0xFF; + + // Bytecodes (Bytes 7 - n) + command = command.concat(byteCommands); + + // Calculate command length minus first two header bytes + const len = command.length - 2; + command[0] = len & 0xFF; + command[1] = len >> 8 && 0xFF; + + return command; + } + + /** + * When the EV3 peripheral connects, start polling for sensor and motor values. + * @private + */ + _onConnect () { + this._pollingIntervalID = window.setInterval(this._pollValues, this._pollingInterval); + } + + /** + * Poll the EV3 for sensor and motor input values, based on the list of + * known connected sensors and motors. This is sent as many compound commands + * in a direct command, with a reply expected. + * + * See 'EV3 Firmware Developer Kit', section 4.8, page 46, at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits + * for a list of polling/input device commands and their arguments. + * + * @private + */ + _pollValues () { + if (!this.isConnected()) { + window.clearInterval(this._pollingIntervalID); + return; + } + + const cmds = []; // compound command + let allocation = 0; + let sensorCount = 0; + + // Reset the list of devices every 20 counts + if (this._pollingCounter % 20 === 0) { + // GET DEVICE LIST + cmds[0] = Ev3Opcode.OPINPUT_DEVICE_LIST; + cmds[1] = Ev3Encoding.ONE_BYTE; + cmds[2] = Ev3Args.MAX_DEVICES; + cmds[3] = Ev3Encoding.GLOBAL_VARIABLE_INDEX_0; + cmds[4] = Ev3Encoding.GLOBAL_VARIABLE_ONE_BYTE; + cmds[5] = Ev3Encoding.GLOBAL_CONSTANT_INDEX_0; + + // Command and payload lengths + allocation = 33; + + this._updateDevices = true; + } else { + // GET SENSOR VALUES FOR CONNECTED SENSORS + let index = 0; + for (let i = 0; i < 4; i++) { + if (this._sensorPorts[i] !== 'none') { + cmds[index + 0] = Ev3Opcode.OPINPUT_READSI; + cmds[index + 1] = Ev3Args.LAYER; + cmds[index + 2] = i; // PORT + cmds[index + 3] = Ev3Args.DO_NOT_CHANGE_TYPE; + cmds[index + 4] = Ev3Mode[this._sensorPorts[i]]; + cmds[index + 5] = Ev3Encoding.GLOBAL_VARIABLE_ONE_BYTE; + cmds[index + 6] = sensorCount * 4; // GLOBAL INDEX + index += 7; + } + sensorCount++; + } + + // GET MOTOR POSITION VALUES, EVEN IF NO MOTOR PRESENT + for (let i = 0; i < 4; i++) { + cmds[index + 0] = Ev3Opcode.OPOUTPUT_GET_COUNT; + cmds[index + 1] = Ev3Args.LAYER; + cmds[index + 2] = i; // PORT (incorrectly specified as 'Output bit field' in LEGO docs) + cmds[index + 3] = Ev3Encoding.GLOBAL_VARIABLE_ONE_BYTE; + cmds[index + 4] = sensorCount * 4; // GLOBAL INDEX + index += 5; + sensorCount++; + } + + // Command and payload lengths + allocation = sensorCount * 4; + } + + const cmd = this.generateCommand( + Ev3Command.DIRECT_COMMAND_REPLY, + cmds, + allocation + ); + + this.send(cmd); + + this._pollingCounter++; + } + + /** + * Message handler for incoming EV3 reply messages, either a list of connected + * devices (sensors and motors) or the values of the connected sensors and motors. + * + * See 'EV3 Communication Developer Kit', section 4.1, page 24 at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits + * for more details on direct reply formats. + * + * The direct reply byte array sent takes the following format: + * Byte 0 – 1: Reply size, Little Endian. Reply size not including these 2 bytes + * Byte 2 – 3: Message counter, Little Endian. Equals the Direct Command + * Byte 4: Reply type. Either DIRECT_REPLY or DIRECT_REPLY_ERROR + * Byte 5 - n: Resonse buffer. I.e. the content of the by the Command reserved global variables. + * I.e. if the command reserved 64 bytes, these bytes will be placed in the reply + * packet as the bytes 5 to 68. + * + * See 'EV3 Firmware Developer Kit', section 4.8, page 56 at + * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits + * for direct response buffer formats for various commands. + * + * @param {object} params - incoming message parameters + * @private + */ + _onMessage (params) { + const message = params.message; + const data = Base64Util.base64ToUint8Array(message); + + if (data[4] !== Ev3Command.DIRECT_REPLY) { + return; + } + + if (this._updateDevices) { + + // PARSE DEVICE LIST + for (let i = 0; i < 4; i++) { + const deviceType = Ev3Device[data[i + 5]]; + // if returned device type is null, use 'none' + this._sensorPorts[i] = deviceType ? deviceType : 'none'; + } + for (let i = 0; i < 4; i++) { + const deviceType = Ev3Device[data[i + 21]]; + // if returned device type is null, use 'none' + this._motorPorts[i] = deviceType ? deviceType : 'none'; + } + for (let m = 0; m < 4; m++) { + const type = this._motorPorts[m]; + if (type !== 'none' && !this._motors[m]) { + // add new motor if don't already have one + this._motors[m] = new EV3Motor(this, m, type); + } + if (type === 'none' && this._motors[m]) { + // clear old motor + this._motors[m] = null; + } + } + this._updateDevices = false; + + // eslint-disable-next-line no-undefined + } else if (!this._sensorPorts.includes(undefined) && !this._motorPorts.includes(undefined)) { + + // PARSE SENSOR VALUES + let offset = 5; // start reading sensor values at byte 5 + for (let i = 0; i < 4; i++) { + // array 2 float + const buffer = new Uint8Array([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3] + ]).buffer; + const view = new DataView(buffer); + const value = view.getFloat32(0, true); + + if (Ev3Label[this._sensorPorts[i]] === 'button') { + // Read a button value per port + this._sensors.buttons[i] = value ? value : 0; + } else if (Ev3Label[this._sensorPorts[i]]) { // if valid + // Read brightness / distance values and set to 0 if null + this._sensors[Ev3Label[this._sensorPorts[i]]] = value ? value : 0; + } + offset += 4; + } + + // PARSE MOTOR POSITION VALUES, EVEN IF NO MOTOR PRESENT + for (let i = 0; i < 4; i++) { + const positionArray = [ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3] + ]; + if (this._motors[i]) { + this._motors[i].position = positionArray; + } + offset += 4; + } + + } + } +} + +/** + * Enum for motor port names. + * Note: if changed, will break compatibility with previously saved projects. + * @readonly + * @enum {string} + */ +const Ev3MotorMenu = ['A', 'B', 'C', 'D']; + +/** + * Enum for sensor port names. + * Note: if changed, will break compatibility with previously saved projects. + * @readonly + * @enum {string} + */ +const Ev3SensorMenu = ['1', '2', '3', '4']; + +class Scratch3Ev3Blocks { + + /** + * The ID of the extension. + * @return {string} the id + */ + static get EXTENSION_ID () { + return 'ev3'; + } + + /** + * Creates a new instance of the EV3 extension. + * @param {object} runtime VM runtime + * @constructor + */ + constructor (runtime) { + /** + * The Scratch 3.0 runtime. + * @type {Runtime} + */ + this.runtime = runtime; + + // Create a new EV3 peripheral instance + this._peripheral = new EV3(this.runtime, Scratch3Ev3Blocks.EXTENSION_ID); + + this._playNoteForPicker = this._playNoteForPicker.bind(this); + this.runtime.on('PLAY_NOTE', this._playNoteForPicker); + } + + /** + * Define the EV3 extension. + * @return {object} Extension description. + */ + getInfo () { + return { + id: Scratch3Ev3Blocks.EXTENSION_ID, + name: 'LEGO EV3', + blockIconURI: blockIconURI, + showStatusButton: true, + blocks: [ + { + opcode: 'motorTurnClockwise', + text: formatMessage({ + id: 'ev3.motorTurnClockwise', + default: 'motor [PORT] turn this way for [TIME] seconds', + description: 'turn a motor clockwise for some time' + }), + blockType: BlockType.COMMAND, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: 0 + }, + TIME: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'motorTurnCounterClockwise', + text: formatMessage({ + id: 'ev3.motorTurnCounterClockwise', + default: 'motor [PORT] turn that way for [TIME] seconds', + description: 'turn a motor counter-clockwise for some time' + }), + blockType: BlockType.COMMAND, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: 0 + }, + TIME: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'motorSetPower', + text: formatMessage({ + id: 'ev3.motorSetPower', + default: 'motor [PORT] set power [POWER] %', + description: 'set a motor\'s power to some value' + }), + blockType: BlockType.COMMAND, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: 0 + }, + POWER: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'getMotorPosition', + text: formatMessage({ + id: 'ev3.getMotorPosition', + default: 'motor [PORT] position', + description: 'get the measured degrees a motor has turned' + }), + blockType: BlockType.REPORTER, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: 0 + } + } + }, + { + opcode: 'whenButtonPressed', + text: formatMessage({ + id: 'ev3.whenButtonPressed', + default: 'when button [PORT] pressed', + description: 'when a button connected to a port is pressed' + }), + blockType: BlockType.HAT, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'sensorPorts', + defaultValue: 0 + } + } + }, + { + opcode: 'whenDistanceLessThan', + text: formatMessage({ + id: 'ev3.whenDistanceLessThan', + default: 'when distance < [DISTANCE]', + description: 'when the value measured by the distance sensor is less than some value' + }), + blockType: BlockType.HAT, + arguments: { + DISTANCE: { + type: ArgumentType.NUMBER, + defaultValue: 5 + } + } + }, + { + opcode: 'whenBrightnessLessThan', + text: formatMessage({ + id: 'ev3.whenBrightnessLessThan', + default: 'when brightness < [DISTANCE]', + description: 'when value measured by brightness sensor is less than some value' + }), + blockType: BlockType.HAT, + arguments: { + DISTANCE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'buttonPressed', + text: formatMessage({ + id: 'ev3.buttonPressed', + default: 'button [PORT] pressed?', + description: 'is a button on some port pressed?' + }), + blockType: BlockType.BOOLEAN, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'sensorPorts', + defaultValue: 0 + } + } + }, + { + opcode: 'getDistance', + text: formatMessage({ + id: 'ev3.getDistance', + default: 'distance', + description: 'gets measured distance' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getBrightness', + text: formatMessage({ + id: 'ev3.getBrightness', + default: 'brightness', + description: 'gets measured brightness' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'beep', + text: formatMessage({ + id: 'ev3.beepNote', + default: 'beep note [NOTE] for [TIME] secs', + description: 'play some note on EV3 for some time' + }), + blockType: BlockType.COMMAND, + arguments: { + NOTE: { + type: ArgumentType.NOTE, + defaultValue: 60 + }, + TIME: { + type: ArgumentType.NUMBER, + defaultValue: 0.5 + } + } + } + ], + menus: { + motorPorts: { + acceptReporters: true, + items: this._formatMenu(Ev3MotorMenu) + }, + sensorPorts: { + acceptReporters: true, + items: this._formatMenu(Ev3SensorMenu) + } + } + }; + } + + motorTurnClockwise (args) { + const port = Cast.toNumber(args.PORT); + let time = Cast.toNumber(args.TIME) * 1000; + time = MathUtil.clamp(time, 0, 15000); + + return new Promise(resolve => { + this._forEachMotor(port, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.direction = 1; + motor.turnOnFor(time); + } + }); + + // Run for some time even when no motor is connected + setTimeout(resolve, time); + }); + } + + motorTurnCounterClockwise (args) { + const port = Cast.toNumber(args.PORT); + let time = Cast.toNumber(args.TIME) * 1000; + time = MathUtil.clamp(time, 0, 15000); + + return new Promise(resolve => { + this._forEachMotor(port, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.direction = -1; + motor.turnOnFor(time); + } + }); + + // Run for some time even when no motor is connected + setTimeout(resolve, time); + }); + } + + motorSetPower (args) { + const port = Cast.toNumber(args.PORT); + const power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100); + + this._forEachMotor(port, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.power = power; + } + }); + } + + getMotorPosition (args) { + const port = Cast.toNumber(args.PORT); + + if (![0, 1, 2, 3].includes(port)) { + return; + } + + const motor = this._peripheral.motor(port); + let position = 0; + if (motor) { + position = MathUtil.wrapClamp(motor.position, 0, 360); + } + + return position; + } + + whenButtonPressed (args) { + const port = Cast.toNumber(args.PORT); + + if (![0, 1, 2, 3].includes(port)) { + return; + } + + return this._peripheral.isButtonPressed(port); + } + + whenDistanceLessThan (args) { + const distance = MathUtil.clamp(Cast.toNumber(args.DISTANCE), 0, 100); + + return this._peripheral.distance < distance; + } + + whenBrightnessLessThan (args) { + const brightness = MathUtil.clamp(Cast.toNumber(args.DISTANCE), 0, 100); + + return this._peripheral.brightness < brightness; + } + + buttonPressed (args) { + const port = Cast.toNumber(args.PORT); + + if (![0, 1, 2, 3].includes(port)) { + return; + } + + return this._peripheral.isButtonPressed(port); + } + + getDistance () { + return this._peripheral.distance; + } + + getBrightness () { + return this._peripheral.brightness; + } + + _playNoteForPicker (note, category) { + if (category !== this.getInfo().name) return; + this.beep({ + NOTE: note, + TIME: 0.25 + }); + } + + beep (args) { + const note = MathUtil.clamp(Cast.toNumber(args.NOTE), 47, 99); // valid EV3 sounds + let time = Cast.toNumber(args.TIME) * 1000; + time = MathUtil.clamp(time, 0, 3000); + + if (time === 0) { + return; // don't send a beep time of 0 + } + + return new Promise(resolve => { + // https://en.wikipedia.org/wiki/MIDI_tuning_standard#Frequency_values + const freq = Math.pow(2, ((note - 69 + 12) / 12)) * 440; + this._peripheral.beep(freq, time); + + // Run for some time even when no piezo is connected. + setTimeout(resolve, time); + }); + } + + /** + * Call a callback for each motor indexed by the provided motor ID. + * + * Note: This way of looping through motors is currently unnecessary, but could be + * useful if an 'all motors' option is added in the future (see WeDo2 extension). + * + * @param {MotorID} motorID - the ID specifier. + * @param {Function} callback - the function to call with the numeric motor index for each motor. + * @private + */ + _forEachMotor (motorID, callback) { + let motors; + switch (motorID) { + case 0: + motors = [0]; + break; + case 1: + motors = [1]; + break; + case 2: + motors = [2]; + break; + case 3: + motors = [3]; + break; + default: + log.warn(`Invalid motor ID: ${motorID}`); + motors = []; + break; + } + for (const index of motors) { + callback(index); + } + } + + /** + * Formats menus into a format suitable for block menus, and loading previously + * saved projects: + * [ + * { + * text: label, + * value: index + * }, + * { + * text: label, + * value: index + * }, + * etc... + * ] + * + * @param {array} menu - a menu to format. + * @return {object} - a formatted menu as an object. + * @private + */ + _formatMenu (menu) { + const m = []; + for (let i = 0; i < menu.length; i++) { + const obj = {}; + obj.text = menu[i]; + obj.value = i.toString(); + m.push(obj); + } + return m; + } +} + +module.exports = Scratch3Ev3Blocks; diff --git a/local-scratch-vm/src/extensions/scratch3_gdx_for/index.js b/local-scratch-vm/src/extensions/scratch3_gdx_for/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e88b142e6393911e0f4a05a0703561349c14dab8 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_gdx_for/index.js @@ -0,0 +1,981 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const log = require('../../util/log'); +const formatMessage = require('format-message'); +const MathUtil = require('../../util/math-util'); +const BLE = require('../../io/ble'); +const godirect = require('@vernier/godirect/dist/godirect.min.umd.js'); +const ScratchLinkDeviceAdapter = require('./scratch-link-device-adapter'); + +/** + * Icon png to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAABGdBTUEAALGPC/xhBQAACCNJREFUeAHtnGtsFFUUgM+dfXbbbbcWaKHSFgrlkWgkJCb6A4kmJfiHIBYBpcFfRg1GEkmEVAvhFYw/TExMxGoICAECiZEIIUQCiiT4gh+KILRQCi2ENIV2t/ue6zl3u2Upu4XuzO4csCe587iPmXO/OWfunTszV4ABWfflQU+0p+9bTcLzEmS5gUPlvagAcVMXcMpnK1u+evW8QLYKaNkWpHKxnt6dQsqFjxo80p10Jt1vx7t30n62Ys+2IJUTUpDlqUNomgYutwsjhZFD5r6slBAOhUHX9YTe6D1GTmrIAhFeBZ2c4JFCpBiggmwlBR7pTGLUewxZYBIUWV7yqgb7g8lotuukt5ihqyELHCSEbusk931ExMxbjSkWSNxEyr3vysxZLFHWnDuT0CtFV6OKmmOBRrV4hMubZoGmMZA6lHTfgsLeHnBEIiCxUY86XRDw+sBfOgZ0m820U5lxIFYAncF+GNvVDo5QaLBu1ClyYTyF4tvd8lZltQgXFA6mW73BxoVt0ShUXG2VCp4QQdDEFqez4Bm7p7gaO0of422r3x4Ji/KrbdIexu4SE2FjgWO6OkCLx6gt6gxOiNV92tiY+ni1Ye1nu7dpQfk35ikru9EBN6unsEDIwgLJPQv8dwCfT3WPt+iFIfAUqM3vL7vpjmuz0KX1gkAfOMN33dxKkjwA9vsTDIS8uubdBZcyAWlqWtohQbRSuru/L1O2vMazAGiLxRKVFqDgDEdAaHCN0kU8Ply2vKWxABhzJZ5ipC6qHlRzfJxVz99S49GdYQEw7PYkuAmokZJ6fumlQUqiNpVSQ56i9JnyHMsCYMRdADGHk0ZyHM1b976XicH0rXtWYR57FPNSGQ7CAiCBCJQ8oXhI0FdmBiPfVnl9ZZmz5DmFDcA+HwIUOEYMcjL2+e57PbBp04HxONI4ifIEKC8TYQMwhs+7IU+hwBFOYQvB5qF8grbwJnRfQXnIhbkIG4AExF+ScE00w0X3AZLwisrDyH1JH1YAA8UlIG029FRZsu6TPfVJiIltWYIjMTLgLUlGs1izeRYmGtS383t9wnu7G2J6fH/Tln2LNUdExGLxvZSOQ1qCS/+P9CFhBZAUuj12PHgCvRJHZ7w4EnhYjya6hXGHQ2Jaxj4ilbVC2AFEUNBVXSdKb3WC29+rmISKiqFn7ARBadyEHUACFHM64VZlDTdWafVh1Yik1ZB5JEsLJGaVtosw37ld4TscWQHX4+oRWO1zWrAEWCR6oMnTCEXijmI1234MVvsPgV+WcmKndGHpwlNtZwbhkZYEkuI4CkuAXfpk0HGAPym0TXEchaUL39Br4JvQeljk+lwxOxBeCRQ3UrFHI+AMBsEV6gcnhlwIS4BU0RORV1V42EqnwnLgSyo3AsM3eA9bPOt8bAEOV6NUWGRZ9FYvHSx6R0pfYgkMmk2DCH1+Z7KwB5gKazjLGgpLgUOAuRZWALnDSncxLAOYCmskbqjhe02h5d6y0sFKF5cXgI8LrLwB9PTeGew6POwNnptlpYOVLi4nFjjuWts957rnBk8tomoZ+bjhPcqOcCcnAG34EaTqOjxmsNKxzQnAkX5wronsOry6zIn66ThljLNcg+W1a2Gi55+MCg6XcKl3NuxrbxouS87TLAcY1V0QV5+8jLyuEekeeSGTS1gOcM/lZpOrlN/DsRzOyi8CY2fLuwUum/wR1BT+ZUzrDKUv9D4LB9rXZEjNTfRjZYFS5r86ebfA3W0bcmMKFh01/5fMoorm6rSjAA2SNc2F8dvmQVWCgdy8fxg8gcEN0pWez80QUyyQFAqn/N9mhmK5PAYN7adecCPnMsUCCZ7U8ari4IGb87wJeKFDA/MlmHXBDVkgTR1CV4/gaThKzBoeKYpuSzqSrqSzEiFuJDayWxqyQJp3RUhYSKfWUSEz5iDIrhrZl8I5b37JvrTBT3wdpd43cOqT/WiJhq6ikQpkW5a8BxuS/X219uXZHoPKmdMUGdEgpWzTll3Kr95Z8VJK7N3NL7b/qHY2rnmdjd6G7oF3q/b/3RoFaPDajwIcBWiQgMHioxZoEKChfqDBc2csnmxtM2ZglMDKArFvduhBbLDv9sOD8oymA0xBCHVtl6+c7ey6Ibdt+3ox7WOoxMCmD4i68PrZkBQaEDUe1tnVqSyyfl79+vr6evz1C2jKogkYWEEc0JnViiZRqKuoqJiZtEJcn0GIsykewzhW2jJVZjzBamxsfK79ase/5MoXL106TnEDwfq36qgIF6HGjKyqFsNkDGMwUNxEDEmIHQTxyNGjH1AchvumBcC4vAuXVpiA+TDYMFDXiiZFoN+SrmMI7tixo/v3337diNtQUzNpPq1RChIra5ccAFKDUEwYLra2fnXu3PmtA0gojqbaVUNl23ft+pPiPW73U7RGYdGH5QCQYCg93C73075S34I5c+ZQa0s/B1Njou51tVVVatJAXcrED3Q4EI5plgsHgAQiSiRCoRD9ECeam9fPo32UJzFQYwJLlix9mdZ9fb1naY2iyiQ2rVtyAEi199Pi5M8/tdB62vRpzceOH3+toaHBh61w2clTp96sqq5ehUnxw0eO7KA8KKpMYtO6JZcOKTUeNRhsp0+ffmtilYI1VLf4+Qvn1784d+5ezEfW144hMR05blglpDgHSbqxt6Wl5Y8ZM6afKq8oL7LZHd54PH7H7w+cOPj9dx8uXbLk+ICynbhm4cJDr7LVMKmhoP5dphaWoFGrHMTAQrgBJCjkFdQHpPntqCUmiWCge14PBsvdFnUYlP8AMAKfKIKmYukAAAAASUVORK5CYII='; + +/** + * Icon png to be displayed in the blocks category menu, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const menuIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAA9dJREFUWAnNmE2IFEcUgF/9dE/v7LoaM9kkK4JBRA0EFBIPRm85hBAvEXHXwyo5eFE87GFcReMkObgJiQnkkJzEg9n8HIJixKNe1IMKihgiCbviwV11V3d0d3pmuqsqr5ppcEnb3TNVggVFVVe9eu+r97qqq4tASqp8/fsboQgmU0TMugi571K29bPy9ovPU8Sf16HbpQj3EkYFBcJcr5Am2nZfs94AIWVfqMQeHNwhICUBZ4ypUIA/X2sbIm2AW8AJK0lkEP6TJpfqwXgg4QxmF/fB7Gtvxk1G5ZKHU1CqTgPJoSUXYJYeohSUJu+qrqdVUGh2/pVX4VFffx77WaqBZkrkEFj271+qWH0sXcU3FBzyQe/Mg7B//LbKMTRTxNiDbsMHHjTJlyM7HEJIBHXs2KXFj+oTNSdoQOCYLS5jD9IwBMm5H8NplwwPb/QV4yEIcycaAza9IuA76B38fuz1OF5RXUkmHCdu6rg0BpSMgV/sAe7DdzGFrvvdi0D3mSZjQA0wt7REQsY+iWF0XbfFzyal8SLRxuteD+Du4h4Z/flbqaBHibAQtZmQtcZaAZSMwtTylaR/4vaw1ju5YhWG10pwwAqghmp2FeHO2+t11WqyM80W0m7vAOhsM1kD7CGz8L57Jsq6bitZC/GcWgLf1H6KuHT92cTDAFy/BgXMXm0OCpgV50Bo9kK3BqiBboabQMMU/WoL5im4jToeq/AIgXsiRx5KKCjcwPEsiAv/BQMu9EwyDHXd/3kqCOSzDk6t5/YglQKKeJwq+PNRmJI8kwSTaj1HZy5AhSHqnXkIvU9mMUwEw4Q5wTM57LUtkg8QPw/cdcBJ+PhvKJ0Gj80nGq6JXrg6/XFiX97GXIBpyqTieKpKViOl+WEhWXMaUavvvdIZ8Giy5+Lh3bwKm/t+Be3JazMfxc1tldY26rastiHcsQevTG9pw0znovkAcRWHzSDKnZtaOJLSfMFLB5RqtRBS4LbCurqLCy0YPkU3C0IIPEimMqR2ei7ZX2+KQdRi/WahNT/GmfOD4Vyzhx/66pcjp85dUvcmp6J8+txldXh07PPskdkS+V6EbD0vTOKlB0x9B/O6BS8ULly9PgE6x4kDPR/XX5pyYKj8xcCucsUmkNUQE0JvKKm2VioVK5HRE7UKOHbi6B94RzP+93jtpC0vWgXUF0hr3ipuw8uadwd3jXxoA9IK4Pah8t6BneV9GgjD28Svw1mlxFobgFbeFTz13cKbth93fDryp2CEq0a4hTA+aAPQ/ESJFDdvXLzzzrqNjlTqOP6uDeFf0uhvJ0ZP2QD8D6ZzU6u8YIbBAAAAAElFTkSuQmCC'; + +/** + * Enum for Vernier godirect protocol. + * @readonly + * @enum {string} + */ +const BLEUUID = { + service: 'd91714ef-28b9-4f91-ba16-f0d9a604f112', + commandChar: 'f4bf14a6-c7d5-4b6d-8aa8-df1a7c83adcb', + responseChar: 'b41e6675-a329-40e0-aa01-44d2f444babe' +}; + +/** + * A time interval to wait (in milliseconds) before reporting to the BLE socket + * that data has stopped coming from the peripheral. + */ +const BLETimeout = 4500; + +/** + * A string to report to the BLE socket when the GdxFor has stopped receiving data. + * @type {string} + */ +const BLEDataStoppedError = 'Force and Acceleration extension stopped receiving data'; + +/** + * Sensor ID numbers for the GDX-FOR. + */ +const GDXFOR_SENSOR = { + FORCE: 1, + ACCELERATION_X: 2, + ACCELERATION_Y: 3, + ACCELERATION_Z: 4, + SPIN_SPEED_X: 5, + SPIN_SPEED_Y: 6, + SPIN_SPEED_Z: 7 +}; + +/** + * The update rate, in milliseconds, for sensor data input from the peripheral. + */ +const GDXFOR_UPDATE_RATE = 80; + +/** + * Threshold for pushing and pulling force, for the whenForcePushedOrPulled hat block. + * @type {number} + */ +const FORCE_THRESHOLD = 5; + +/** + * Threshold for acceleration magnitude, for the "shaken" gesture. + * @type {number} + */ +const SHAKEN_THRESHOLD = 30; + +/** + * Threshold for acceleration magnitude, to check if we are facing up. + * @type {number} + */ +const FACING_THRESHOLD = 9; + +/** + * An offset for the facing threshold, used to check that we are no longer facing up. + * @type {number} + */ +const FACING_THRESHOLD_OFFSET = 5; + +/** + * Threshold for acceleration magnitude, below which we are in freefall. + * @type {number} + */ +const FREEFALL_THRESHOLD = 0.5; + +/** + * Factor used to account for influence of rotation during freefall. + * @type {number} + */ +const FREEFALL_ROTATION_FACTOR = 0.3; + +/** + * Threshold in degrees for reporting that the sensor is tilted. + * @type {number} + */ +const TILT_THRESHOLD = 15; + +/** + * Acceleration due to gravity, in m/s^2. + * @type {number} + */ +const GRAVITY = 9.8; + +/** + * Manage communication with a GDX-FOR peripheral over a Scratch Link client socket. + */ +class GdxFor { + + /** + * Construct a GDX-FOR communication object. + * @param {Runtime} runtime - the Scratch 3.0 runtime + * @param {string} extensionId - the id of the extension + */ + constructor (runtime, extensionId) { + + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} + * @private + */ + this._runtime = runtime; + + /** + * The BluetoothLowEnergy connection socket for reading/writing peripheral data. + * @type {BLE} + * @private + */ + this._ble = null; + + /** + * An @vernier/godirect Device + * @type {Device} + * @private + */ + this._device = null; + + this._runtime.registerPeripheralExtension(extensionId, this); + + /** + * The id of the extension this peripheral belongs to. + */ + this._extensionId = extensionId; + + /** + * The most recently received value for each sensor. + * @type {Object.} + * @private + */ + this._sensors = { + force: 0, + accelerationX: 0, + accelerationY: 0, + accelerationZ: 0, + spinSpeedX: 0, + spinSpeedY: 0, + spinSpeedZ: 0 + }; + + /** + * Interval ID for data reading timeout. + * @type {number} + * @private + */ + this._timeoutID = null; + + this.reset = this.reset.bind(this); + this._onConnect = this._onConnect.bind(this); + } + + + /** + * Called by the runtime when user wants to scan for a peripheral. + */ + scan () { + if (this._ble) { + this._ble.disconnect(); + } + + this._ble = new BLE(this._runtime, this._extensionId, { + filters: [ + {namePrefix: 'GDX-FOR'} + ], + optionalServices: [ + BLEUUID.service + ] + }, this._onConnect, this.reset); + } + + /** + * Called by the runtime when user wants to connect to a certain peripheral. + * @param {number} id - the id of the peripheral to connect to. + */ + connect (id) { + if (this._ble) { + this._ble.connectPeripheral(id); + } + } + + /** + * Called by the runtime when a user exits the connection popup. + * Disconnect from the GDX FOR. + */ + disconnect () { + if (this._ble) { + this._ble.disconnect(); + } + + this.reset(); + } + + /** + * Reset all the state and timeout/interval ids. + */ + reset () { + this._sensors = { + force: 0, + accelerationX: 0, + accelerationY: 0, + accelerationZ: 0, + spinSpeedX: 0, + spinSpeedY: 0, + spinSpeedZ: 0 + }; + + if (this._timeoutID) { + window.clearInterval(this._timeoutID); + this._timeoutID = null; + } + } + + /** + * Return true if connected to the goforce device. + * @return {boolean} - whether the goforce is connected. + */ + isConnected () { + let connected = false; + if (this._ble) { + connected = this._ble.isConnected(); + } + return connected; + } + + /** + * Starts reading data from peripheral after BLE has connected to it. + * @private + */ + _onConnect () { + const adapter = new ScratchLinkDeviceAdapter(this._ble, BLEUUID); + godirect.createDevice(adapter, {open: true, startMeasurements: false}).then(device => { + // Setup device + this._device = device; + this._device.keepValues = false; // todo: possibly remove after updating Vernier godirect module + + // Enable sensors + this._device.sensors.forEach(sensor => { + sensor.setEnabled(true); + }); + + // Set sensor value-update behavior + this._device.on('measurements-started', () => { + const enabledSensors = this._device.sensors.filter(s => s.enabled); + enabledSensors.forEach(sensor => { + sensor.on('value-changed', s => { + this._onSensorValueChanged(s); + }); + }); + this._timeoutID = window.setInterval( + () => this._ble.handleDisconnectError(BLEDataStoppedError), + BLETimeout + ); + }); + + // Start device + this._device.start(GDXFOR_UPDATE_RATE); + }); + } + + /** + * Handler for sensor value changes from the goforce device. + * @param {object} sensor - goforce device sensor whose value has changed + * @private + */ + _onSensorValueChanged (sensor) { + switch (sensor.number) { + case GDXFOR_SENSOR.FORCE: + // Normalize the force, which can be measured between -50 and 50 N, + // to be a value between -100 and 100. + this._sensors.force = MathUtil.clamp(sensor.value * 2, -100, 100); + break; + case GDXFOR_SENSOR.ACCELERATION_X: + this._sensors.accelerationX = sensor.value; + break; + case GDXFOR_SENSOR.ACCELERATION_Y: + this._sensors.accelerationY = sensor.value; + break; + case GDXFOR_SENSOR.ACCELERATION_Z: + this._sensors.accelerationZ = sensor.value; + break; + case GDXFOR_SENSOR.SPIN_SPEED_X: + this._sensors.spinSpeedX = this._spinSpeedFromGyro(sensor.value); + break; + case GDXFOR_SENSOR.SPIN_SPEED_Y: + this._sensors.spinSpeedY = this._spinSpeedFromGyro(sensor.value); + break; + case GDXFOR_SENSOR.SPIN_SPEED_Z: + this._sensors.spinSpeedZ = this._spinSpeedFromGyro(sensor.value); + break; + } + // cancel disconnect timeout and start a new one + window.clearInterval(this._timeoutID); + this._timeoutID = window.setInterval( + () => this._ble.handleDisconnectError(BLEDataStoppedError), + BLETimeout + ); + } + + _spinSpeedFromGyro (val) { + const framesPerSec = 1000 / this._runtime.currentStepTime; + val = MathUtil.radToDeg(val); + val = val / framesPerSec; // convert to from degrees per sec to degrees per frame + val = val * -1; + return val; + } + + getForce () { + return this._sensors.force; + } + + getTiltFrontBack (back = false) { + const x = this.getAccelerationX(); + const y = this.getAccelerationY(); + const z = this.getAccelerationZ(); + + // Compute the yz unit vector + const y2 = y * y; + const z2 = z * z; + let value = y2 + z2; + value = Math.sqrt(value); + + // For sufficiently small zy vector values we are essentially at 90 degrees. + // The following snaps to 90 and avoids divide-by-zero errors. + // The snap factor was derived through observation -- just enough to + // still allow single degree steps up to 90 (..., 87, 88, 89, 90). + if (value < 0.35) { + value = (x < 0) ? 90 : -90; + } else { + value = x / value; + value = Math.atan(value); + value = MathUtil.radToDeg(value) * -1; + } + + // Back is the inverse of front + if (back) value *= -1; + + return value; + } + + getTiltLeftRight (right = false) { + const x = this.getAccelerationX(); + const y = this.getAccelerationY(); + const z = this.getAccelerationZ(); + + // Compute the yz unit vector + const x2 = x * x; + const z2 = z * z; + let value = x2 + z2; + value = Math.sqrt(value); + + // For sufficiently small zy vector values we are essentially at 90 degrees. + // The following snaps to 90 and avoids divide-by-zero errors. + // The snap factor was derived through observation -- just enough to + // still allow single degree steps up to 90 (..., 87, 88, 89, 90). + if (value < 0.35) { + value = (y < 0) ? 90 : -90; + } else { + value = y / value; + value = Math.atan(value); + value = MathUtil.radToDeg(value) * -1; + } + + // Right is the inverse of left + if (right) value *= -1; + + return value; + } + + getAccelerationX () { + return this._sensors.accelerationX; + } + + getAccelerationY () { + return this._sensors.accelerationY; + } + + getAccelerationZ () { + return this._sensors.accelerationZ; + } + + getSpinSpeedX () { + return this._sensors.spinSpeedX; + } + + getSpinSpeedY () { + return this._sensors.spinSpeedY; + } + + getSpinSpeedZ () { + return this._sensors.spinSpeedZ; + } +} + +/** + * Enum for pushed and pulled menu options. + * @readonly + * @enum {string} + */ +const PushPullValues = { + PUSHED: 'pushed', + PULLED: 'pulled' +}; + +/** + * Enum for motion gesture menu options. + * @readonly + * @enum {string} + */ +const GestureValues = { + SHAKEN: 'shaken', + STARTED_FALLING: 'started falling', + TURNED_FACE_UP: 'turned face up', + TURNED_FACE_DOWN: 'turned face down' +}; + +/** + * Enum for tilt axis menu options. + * @readonly + * @enum {string} + */ +const TiltAxisValues = { + FRONT: 'front', + BACK: 'back', + LEFT: 'left', + RIGHT: 'right', + ANY: 'any' +}; + +/** + * Enum for axis menu options. + * @readonly + * @enum {string} + */ +const AxisValues = { + X: 'x', + Y: 'y', + Z: 'z' +}; + +/** + * Scratch 3.0 blocks to interact with a GDX-FOR peripheral. + */ +class Scratch3GdxForBlocks { + + /** + * @return {string} - the name of this extension. + */ + static get EXTENSION_NAME () { + return 'Force and Acceleration'; + } + + /** + * @return {string} - the ID of this extension. + */ + static get EXTENSION_ID () { + return 'gdxfor'; + } + + get AXIS_MENU () { + return [ + { + text: 'x', + value: AxisValues.X + }, + { + text: 'y', + value: AxisValues.Y + }, + { + text: 'z', + value: AxisValues.Z + } + ]; + } + + get TILT_MENU () { + return [ + { + text: formatMessage({ + id: 'gdxfor.tiltDirectionMenu.front', + default: 'front', + description: 'label for front element in tilt direction picker for gdxfor extension' + }), + value: TiltAxisValues.FRONT + }, + { + text: formatMessage({ + id: 'gdxfor.tiltDirectionMenu.back', + default: 'back', + description: 'label for back element in tilt direction picker for gdxfor extension' + }), + value: TiltAxisValues.BACK + }, + { + text: formatMessage({ + id: 'gdxfor.tiltDirectionMenu.left', + default: 'left', + description: 'label for left element in tilt direction picker for gdxfor extension' + }), + value: TiltAxisValues.LEFT + }, + { + text: formatMessage({ + id: 'gdxfor.tiltDirectionMenu.right', + default: 'right', + description: 'label for right element in tilt direction picker for gdxfor extension' + }), + value: TiltAxisValues.RIGHT + } + ]; + } + + get TILT_MENU_ANY () { + return [ + ...this.TILT_MENU, + { + text: formatMessage({ + id: 'gdxfor.tiltDirectionMenu.any', + default: 'any', + description: 'label for any direction element in tilt direction picker for gdxfor extension' + }), + value: TiltAxisValues.ANY + } + ]; + } + + get PUSH_PULL_MENU () { + return [ + { + text: formatMessage({ + id: 'gdxfor.pushed', + default: 'pushed', + description: 'the force sensor was pushed inward' + }), + value: PushPullValues.PUSHED + }, + { + text: formatMessage({ + id: 'gdxfor.pulled', + default: 'pulled', + description: 'the force sensor was pulled outward' + }), + value: PushPullValues.PULLED + } + ]; + } + + get GESTURE_MENU () { + return [ + { + text: formatMessage({ + id: 'gdxfor.shaken', + default: 'shaken', + description: 'the sensor was shaken' + }), + value: GestureValues.SHAKEN + }, + { + text: formatMessage({ + id: 'gdxfor.startedFalling', + default: 'started falling', + description: 'the sensor started free falling' + }), + value: GestureValues.STARTED_FALLING + }, + { + text: formatMessage({ + id: 'gdxfor.turnedFaceUp', + default: 'turned face up', + description: 'the sensor was turned to face up' + }), + value: GestureValues.TURNED_FACE_UP + }, + { + text: formatMessage({ + id: 'gdxfor.turnedFaceDown', + default: 'turned face down', + description: 'the sensor was turned to face down' + }), + value: GestureValues.TURNED_FACE_DOWN + } + ]; + } + + /** + * Construct a set of GDX-FOR blocks. + * @param {Runtime} runtime - the Scratch 3.0 runtime. + */ + constructor (runtime) { + /** + * The Scratch 3.0 runtime. + * @type {Runtime} + */ + this.runtime = runtime; + + // Create a new GdxFor peripheral instance + this._peripheral = new GdxFor(this.runtime, Scratch3GdxForBlocks.EXTENSION_ID); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: Scratch3GdxForBlocks.EXTENSION_ID, + name: Scratch3GdxForBlocks.EXTENSION_NAME, + blockIconURI: blockIconURI, + menuIconURI: menuIconURI, + showStatusButton: true, + blocks: [ + { + opcode: 'whenGesture', + text: formatMessage({ + id: 'gdxfor.whenGesture', + default: 'when [GESTURE]', + description: 'when the sensor detects a gesture' + }), + blockType: BlockType.HAT, + arguments: { + GESTURE: { + type: ArgumentType.STRING, + menu: 'gestureOptions', + defaultValue: GestureValues.SHAKEN + } + } + }, + { + opcode: 'whenForcePushedOrPulled', + text: formatMessage({ + id: 'gdxfor.whenForcePushedOrPulled', + default: 'when force sensor [PUSH_PULL]', + description: 'when the force sensor is pushed or pulled' + }), + blockType: BlockType.HAT, + arguments: { + PUSH_PULL: { + type: ArgumentType.STRING, + menu: 'pushPullOptions', + defaultValue: PushPullValues.PUSHED + } + } + }, + { + opcode: 'getForce', + text: formatMessage({ + id: 'gdxfor.getForce', + default: 'force', + description: 'gets force' + }), + blockType: BlockType.REPORTER + }, + '---', + { + opcode: 'whenTilted', + text: formatMessage({ + id: 'gdxfor.whenTilted', + default: 'when tilted [TILT]', + description: 'when the sensor detects tilt' + }), + blockType: BlockType.HAT, + arguments: { + TILT: { + type: ArgumentType.STRING, + menu: 'tiltAnyOptions', + defaultValue: TiltAxisValues.ANY + } + } + }, + { + opcode: 'isTilted', + text: formatMessage({ + id: 'gdxfor.isTilted', + default: 'tilted [TILT]?', + description: 'is the device tilted?' + }), + blockType: BlockType.BOOLEAN, + arguments: { + TILT: { + type: ArgumentType.STRING, + menu: 'tiltAnyOptions', + defaultValue: TiltAxisValues.ANY + } + } + }, + { + opcode: 'getTilt', + text: formatMessage({ + id: 'gdxfor.getTilt', + default: 'tilt angle [TILT]', + description: 'gets tilt' + }), + blockType: BlockType.REPORTER, + arguments: { + TILT: { + type: ArgumentType.STRING, + menu: 'tiltOptions', + defaultValue: TiltAxisValues.FRONT + } + } + }, + '---', + { + opcode: 'isFreeFalling', + text: formatMessage({ + id: 'gdxfor.isFreeFalling', + default: 'falling?', + description: 'is the device in free fall?' + }), + blockType: BlockType.BOOLEAN + }, + { + opcode: 'getSpinSpeed', + text: formatMessage({ + id: 'gdxfor.getSpin', + default: 'spin speed [DIRECTION]', + description: 'gets spin speed' + }), + blockType: BlockType.REPORTER, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'axisOptions', + defaultValue: AxisValues.Z + } + } + }, + { + opcode: 'getAcceleration', + text: formatMessage({ + id: 'gdxfor.getAcceleration', + default: 'acceleration [DIRECTION]', + description: 'gets acceleration' + }), + blockType: BlockType.REPORTER, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'axisOptions', + defaultValue: AxisValues.X + } + } + } + ], + menus: { + pushPullOptions: { + acceptReporters: true, + items: this.PUSH_PULL_MENU + }, + gestureOptions: { + acceptReporters: true, + items: this.GESTURE_MENU + }, + axisOptions: { + acceptReporters: true, + items: this.AXIS_MENU + }, + tiltOptions: { + acceptReporters: true, + items: this.TILT_MENU + }, + tiltAnyOptions: { + acceptReporters: true, + items: this.TILT_MENU_ANY + } + } + }; + } + + whenForcePushedOrPulled (args) { + switch (args.PUSH_PULL) { + case PushPullValues.PUSHED: + return this._peripheral.getForce() < FORCE_THRESHOLD * -1; + case PushPullValues.PULLED: + return this._peripheral.getForce() > FORCE_THRESHOLD; + default: + log.warn(`unknown push/pull value in whenForcePushedOrPulled: ${args.PUSH_PULL}`); + return false; + } + } + + getForce () { + return Math.round(this._peripheral.getForce()); + } + + whenGesture (args) { + switch (args.GESTURE) { + case GestureValues.SHAKEN: + return this.gestureMagnitude() > SHAKEN_THRESHOLD; + case GestureValues.STARTED_FALLING: + return this.isFreeFalling(); + case GestureValues.TURNED_FACE_UP: + return this._isFacing(GestureValues.TURNED_FACE_UP); + case GestureValues.TURNED_FACE_DOWN: + return this._isFacing(GestureValues.TURNED_FACE_DOWN); + default: + log.warn(`unknown gesture value in whenGesture: ${args.GESTURE}`); + return false; + } + } + + _isFacing (direction) { + if (typeof this._facingUp === 'undefined') { + this._facingUp = false; + } + if (typeof this._facingDown === 'undefined') { + this._facingDown = false; + } + + // If the sensor is already facing up or down, reduce the threshold. + // This prevents small fluctations in acceleration while it is being + // turned from causing the hat block to trigger multiple times. + let threshold = FACING_THRESHOLD; + if (this._facingUp || this._facingDown) { + threshold -= FACING_THRESHOLD_OFFSET; + } + + this._facingUp = this._peripheral.getAccelerationZ() > threshold; + this._facingDown = this._peripheral.getAccelerationZ() < threshold * -1; + + switch (direction) { + case GestureValues.TURNED_FACE_UP: + return this._facingUp; + case GestureValues.TURNED_FACE_DOWN: + return this._facingDown; + default: + return false; + } + } + + whenTilted (args) { + return this._isTilted(args.TILT); + } + + isTilted (args) { + return this._isTilted(args.TILT); + } + + getTilt (args) { + return this._getTiltAngle(args.TILT); + } + + _isTilted (direction) { + switch (direction) { + case TiltAxisValues.ANY: + return this._getTiltAngle(TiltAxisValues.FRONT) > TILT_THRESHOLD || + this._getTiltAngle(TiltAxisValues.BACK) > TILT_THRESHOLD || + this._getTiltAngle(TiltAxisValues.LEFT) > TILT_THRESHOLD || + this._getTiltAngle(TiltAxisValues.RIGHT) > TILT_THRESHOLD; + default: + return this._getTiltAngle(direction) > TILT_THRESHOLD; + } + } + + _getTiltAngle (direction) { + // Tilt values are calculated using acceleration due to gravity, + // so we need to return 0 when the peripheral is not connected. + if (!this._peripheral.isConnected()) { + return 0; + } + + switch (direction) { + case TiltAxisValues.FRONT: + return Math.round(this._peripheral.getTiltFrontBack(true)); + case TiltAxisValues.BACK: + return Math.round(this._peripheral.getTiltFrontBack(false)); + case TiltAxisValues.LEFT: + return Math.round(this._peripheral.getTiltLeftRight(true)); + case TiltAxisValues.RIGHT: + return Math.round(this._peripheral.getTiltLeftRight(false)); + default: + log.warn(`Unknown direction in getTilt: ${direction}`); + } + } + + getSpinSpeed (args) { + switch (args.DIRECTION) { + case AxisValues.X: + return Math.round(this._peripheral.getSpinSpeedX()); + case AxisValues.Y: + return Math.round(this._peripheral.getSpinSpeedY()); + case AxisValues.Z: + return Math.round(this._peripheral.getSpinSpeedZ()); + default: + log.warn(`Unknown direction in getSpinSpeed: ${args.DIRECTION}`); + } + } + + getAcceleration (args) { + switch (args.DIRECTION) { + case AxisValues.X: + return Math.round(this._peripheral.getAccelerationX()); + case AxisValues.Y: + return Math.round(this._peripheral.getAccelerationY()); + case AxisValues.Z: + return Math.round(this._peripheral.getAccelerationZ()); + default: + log.warn(`Unknown direction in getAcceleration: ${args.DIRECTION}`); + } + } + + /** + * @param {number} x - x axis vector + * @param {number} y - y axis vector + * @param {number} z - z axis vector + * @return {number} - the magnitude of a three dimension vector. + */ + magnitude (x, y, z) { + return Math.sqrt((x * x) + (y * y) + (z * z)); + } + + accelMagnitude () { + return this.magnitude( + this._peripheral.getAccelerationX(), + this._peripheral.getAccelerationY(), + this._peripheral.getAccelerationZ() + ); + } + + gestureMagnitude () { + return this.accelMagnitude() - GRAVITY; + } + + spinMagnitude () { + return this.magnitude( + this._peripheral.getSpinSpeedX(), + this._peripheral.getSpinSpeedY(), + this._peripheral.getSpinSpeedZ() + ); + } + + isFreeFalling () { + // When the peripheral is not connected, the acceleration magnitude + // is 0 instead of ~9.8, which ends up calculating as a positive + // free fall; so we need to return 'false' here to prevent returning 'true'. + if (!this._peripheral.isConnected()) { + return false; + } + + const accelMag = this.accelMagnitude(); + const spinMag = this.spinMagnitude(); + + // We want to account for rotation during freefall, + // so we tack on a an estimated "rotational effect" + // The FREEFALL_ROTATION_FACTOR const is used to both scale the + // gyro measurements and convert them to radians/second. + // So, we compare our accel magnitude against: + // FREEFALL_THRESHOLD + (some_scaled_magnitude_of_rotation). + const ffThresh = FREEFALL_THRESHOLD + (FREEFALL_ROTATION_FACTOR * spinMag); + + return accelMag < ffThresh; + } +} + +module.exports = Scratch3GdxForBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js b/local-scratch-vm/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js new file mode 100644 index 0000000000000000000000000000000000000000..bad68d8b22a1bbfb93ac523330cab9219e6f5a7e --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js @@ -0,0 +1,44 @@ +const Base64Util = require('../../util/base64-util'); + +/** + * Adapter class + */ +class ScratchLinkDeviceAdapter { + constructor (socket, {service, commandChar, responseChar}) { + this.socket = socket; + + this._service = service; + this._commandChar = commandChar; + this._responseChar = responseChar; + this._onResponse = this._onResponse.bind(this); + this._deviceOnResponse = null; + } + + get godirectAdapter () { + return true; + } + + writeCommand (commandBuffer) { + const data = Base64Util.uint8ArrayToBase64(commandBuffer); + + return this.socket + .write(this._service, this._commandChar, data, 'base64'); + } + + setup ({onResponse}) { + this._deviceOnResponse = onResponse; + return this.socket + .startNotifications(this._service, this._responseChar, this._onResponse); + + // TODO: + // How do we find out from scratch link if communication closes? + } + + _onResponse (base64) { + const array = Base64Util.base64ToUint8Array(base64); + const response = new DataView(array.buffer); + return this._deviceOnResponse(response); + } +} + +module.exports = ScratchLinkDeviceAdapter; diff --git a/local-scratch-vm/src/extensions/scratch3_makeymakey/index.js b/local-scratch-vm/src/extensions/scratch3_makeymakey/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ca02853c05f7da86074b8b62ced50250682ad433 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_makeymakey/index.js @@ -0,0 +1,423 @@ +const formatMessage = require('format-message'); +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHN0eWxlPi5zdDJ7ZmlsbDpyZWR9LnN0M3tmaWxsOiNlMGUwZTB9LnN0NHtmaWxsOm5vbmU7c3Ryb2tlOiM2NjY7c3Ryb2tlLXdpZHRoOi41O3N0cm9rZS1taXRlcmxpbWl0OjEwfTwvc3R5bGU+PHBhdGggZD0iTTM1IDI4SDVhMSAxIDAgMCAxLTEtMVYxMmMwLS42LjQtMSAxLTFoMzBjLjUgMCAxIC40IDEgMXYxNWMwIC41LS41IDEtMSAxeiIgZmlsbD0iI2ZmZiIgaWQ9IkxheWVyXzYiLz48ZyBpZD0iTGF5ZXJfNCI+PHBhdGggY2xhc3M9InN0MiIgZD0iTTQgMjVoMzJ2Mi43SDR6TTEzIDI0aC0yLjJhMSAxIDAgMCAxLTEtMXYtOS43YzAtLjYuNC0xIDEtMUgxM2MuNiAwIDEgLjQgMSAxVjIzYzAgLjYtLjUgMS0xIDF6Ii8+PHBhdGggY2xhc3M9InN0MiIgZD0iTTYuMSAxOS4zdi0yLjJjMC0uNS40LTEgMS0xaDkuN2MuNSAwIDEgLjUgMSAxdjIuMmMwIC41LS41IDEtMSAxSDcuMWExIDEgMCAwIDEtMS0xeiIvPjxjaXJjbGUgY2xhc3M9InN0MiIgY3g9IjIyLjgiIGN5PSIxOC4yIiByPSIzLjQiLz48Y2lyY2xlIGNsYXNzPSJzdDIiIGN4PSIzMC42IiBjeT0iMTguMiIgcj0iMy40Ii8+PHBhdGggY2xhc3M9InN0MiIgZD0iTTQuMiAyN2gzMS45di43SDQuMnoiLz48L2c+PGcgaWQ9IkxheWVyXzUiPjxjaXJjbGUgY2xhc3M9InN0MyIgY3g9IjIyLjgiIGN5PSIxOC4yIiByPSIyLjMiLz48Y2lyY2xlIGNsYXNzPSJzdDMiIGN4PSIzMC42IiBjeT0iMTguMiIgcj0iMi4zIi8+PHBhdGggY2xhc3M9InN0MyIgZD0iTTEyLjUgMjIuOWgtMS4yYy0uMyAwLS41LS4yLS41LS41VjE0YzAtLjMuMi0uNS41LS41aDEuMmMuMyAwIC41LjIuNS41djguNGMwIC4zLS4yLjUtLjUuNXoiLz48cGF0aCBjbGFzcz0ic3QzIiBkPSJNNy4yIDE4Ljd2LTEuMmMwLS4zLjItLjUuNS0uNWg4LjRjLjMgMCAuNS4yLjUuNXYxLjJjMCAuMy0uMi41LS41LjVINy43Yy0uMyAwLS41LS4yLS41LS41ek00IDI2aDMydjJINHoiLz48L2c+PGcgaWQ9IkxheWVyXzMiPjxwYXRoIGNsYXNzPSJzdDQiIGQ9Ik0zNS4yIDI3LjlINC44YTEgMSAwIDAgMS0xLTFWMTIuMWMwLS42LjUtMSAxLTFoMzAuNWMuNSAwIDEgLjQgMSAxVjI3YTEgMSAwIDAgMS0xLjEuOXoiLz48cGF0aCBjbGFzcz0ic3Q0IiBkPSJNMzUuMiAyNy45SDQuOGExIDEgMCAwIDEtMS0xVjEyLjFjMC0uNi41LTEgMS0xaDMwLjVjLjUgMCAxIC40IDEgMVYyN2ExIDEgMCAwIDEtMS4xLjl6Ii8+PC9nPjwvc3ZnPg=='; + +/** + * Length of the buffer to store key presses for the "when keys pressed in order" hat + * @type {number} + */ +const KEY_BUFFER_LENGTH = 100; + +/** + * Timeout in milliseconds to reset the completed flag for a sequence. + * @type {number} + */ +const SEQUENCE_HAT_TIMEOUT = 100; + +/** + * An id for the space key on a keyboard. + */ +const KEY_ID_SPACE = 'SPACE'; + +/** + * An id for the left arrow key on a keyboard. + */ +const KEY_ID_LEFT = 'LEFT'; + +/** + * An id for the right arrow key on a keyboard. + */ +const KEY_ID_RIGHT = 'RIGHT'; + +/** + * An id for the up arrow key on a keyboard. + */ +const KEY_ID_UP = 'UP'; + +/** + * An id for the down arrow key on a keyboard. + */ +const KEY_ID_DOWN = 'DOWN'; + +/** + * Names used by keyboard io for keys used in scratch. + * @enum {string} + */ +const SCRATCH_KEY_NAME = { + [KEY_ID_SPACE]: 'space', + [KEY_ID_LEFT]: 'left arrow', + [KEY_ID_UP]: 'up arrow', + [KEY_ID_RIGHT]: 'right arrow', + [KEY_ID_DOWN]: 'down arrow' +}; + +/** + * Class for the makey makey blocks in Scratch 3.0 + * @constructor + */ +class Scratch3MakeyMakeyBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * A toggle that alternates true and false each frame, so that an + * edge-triggered hat can trigger on every other frame. + * @type {boolean} + */ + this.frameToggle = false; + + // Set an interval that toggles the frameToggle every frame. + setInterval(() => { + this.frameToggle = !this.frameToggle; + }, this.runtime.currentStepTime); + + this.keyPressed = this.keyPressed.bind(this); + this.runtime.on('KEY_PRESSED', this.keyPressed); + + this._clearkeyPressBuffer = this._clearkeyPressBuffer.bind(this); + this.runtime.on('PROJECT_STOP_ALL', this._clearkeyPressBuffer); + + /* + * An object containing a set of sequence objects. + * These are the key sequences currently being detected by the "when + * keys pressed in order" hat block. Each sequence is keyed by its + * string representation (the sequence's value in the menu, which is a + * string of KEY_IDs separated by spaces). Each sequence object + * has an array property (an array of KEY_IDs) and a boolean + * completed property that is true when the sequence has just been + * pressed. + * @type {object} + */ + this.sequences = {}; + + /* + * An array of the key codes of recently pressed keys. + * @type {array} + */ + this.keyPressBuffer = []; + } + + /* + * Localized short-form names of the space bar and arrow keys, for use in the + * displayed menu items of the "when keys pressed in order" block. + * @type {object} + */ + get KEY_TEXT_SHORT () { + return { + [KEY_ID_SPACE]: formatMessage({ + id: 'makeymakey.spaceKey', + default: 'space', + description: 'The space key on a computer keyboard.' + }), + [KEY_ID_LEFT]: formatMessage({ + id: 'makeymakey.leftArrowShort', + default: 'left', + description: 'Short name for the left arrow key on a computer keyboard.' + }), + [KEY_ID_UP]: formatMessage({ + id: 'makeymakey.upArrowShort', + default: 'up', + description: 'Short name for the up arrow key on a computer keyboard.' + }), + [KEY_ID_RIGHT]: formatMessage({ + id: 'makeymakey.rightArrowShort', + default: 'right', + description: 'Short name for the right arrow key on a computer keyboard.' + }), + [KEY_ID_DOWN]: formatMessage({ + id: 'makeymakey.downArrowShort', + default: 'down', + description: 'Short name for the down arrow key on a computer keyboard.' + }) + }; + } + + /* + * An array of strings of KEY_IDs representing the default set of + * key sequences for use by the "when keys pressed in order" block. + * @type {array} + */ + get DEFAULT_SEQUENCES () { + return [ + `${KEY_ID_LEFT} ${KEY_ID_UP} ${KEY_ID_RIGHT}`, + `${KEY_ID_RIGHT} ${KEY_ID_UP} ${KEY_ID_LEFT}`, + `${KEY_ID_LEFT} ${KEY_ID_RIGHT}`, + `${KEY_ID_RIGHT} ${KEY_ID_LEFT}`, + `${KEY_ID_UP} ${KEY_ID_DOWN}`, + `${KEY_ID_DOWN} ${KEY_ID_UP}`, + `${KEY_ID_UP} ${KEY_ID_RIGHT} ${KEY_ID_DOWN} ${KEY_ID_LEFT}`, + `${KEY_ID_UP} ${KEY_ID_LEFT} ${KEY_ID_DOWN} ${KEY_ID_RIGHT}`, + `${KEY_ID_UP} ${KEY_ID_UP} ${KEY_ID_DOWN} ${KEY_ID_DOWN} ` + + `${KEY_ID_LEFT} ${KEY_ID_RIGHT} ${KEY_ID_LEFT} ${KEY_ID_RIGHT}` + ]; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'makeymakey', + name: 'Makey Makey', + blockIconURI: blockIconURI, + blocks: [ + { + opcode: 'whenMakeyKeyPressed', + text: formatMessage({ + id: 'makeymakey.whenKeyPressed', + default: 'when [KEY] key pressed', + description: 'when a keyboard key is pressed' + }), + blockType: BlockType.HAT, + arguments: { + KEY: { + type: ArgumentType.STRING, + menu: 'KEY', + defaultValue: KEY_ID_SPACE + } + } + }, + { + opcode: 'whenCodePressed', + text: formatMessage({ + id: 'makeymakey.whenKeysPressedInOrder', + default: 'when [SEQUENCE] pressed in order', + description: 'when a sequence of keyboard keys is pressed in a specific order' + }), + blockType: BlockType.HAT, + arguments: { + SEQUENCE: { + type: ArgumentType.STRING, + menu: 'SEQUENCE', + defaultValue: this.DEFAULT_SEQUENCES[0] + } + } + }, + "---", + { + opcode: 'isMakeyKeyPressed', + text: formatMessage({ + id: 'makeymakey.isKeyPressed', + default: 'is [KEY] key pressed', + description: 'is a keyboard key is pressed' + }), + blockType: BlockType.BOOLEAN, + arguments: { + KEY: { + type: ArgumentType.STRING, + menu: 'KEY', + defaultValue: KEY_ID_SPACE + } + } + } + ], + menus: { + KEY: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'makeymakey.spaceKey', + default: 'space', + description: 'The space key on a computer keyboard.' + }), + value: KEY_ID_SPACE + }, + { + text: formatMessage({ + id: 'makeymakey.upArrow', + default: 'up arrow', + description: 'The up arrow key on a computer keyboard.' + }), + value: KEY_ID_UP + }, + { + text: formatMessage({ + id: 'makeymakey.downArrow', + default: 'down arrow', + description: 'The down arrow key on a computer keyboard.' + }), + value: KEY_ID_DOWN + }, + { + text: formatMessage({ + id: 'makeymakey.rightArrow', + default: 'right arrow', + description: 'The right arrow key on a computer keyboard.' + }), + value: KEY_ID_RIGHT + }, + { + text: formatMessage({ + id: 'makeymakey.leftArrow', + default: 'left arrow', + description: 'The left arrow key on a computer keyboard.' + }), + value: KEY_ID_LEFT + }, + {text: 'w', value: 'w'}, + {text: 'a', value: 'a'}, + {text: 's', value: 's'}, + {text: 'd', value: 'd'}, + {text: 'f', value: 'f'}, + {text: 'g', value: 'g'} + ] + }, + SEQUENCE: { + acceptReporters: true, + items: this.buildSequenceMenu(this.DEFAULT_SEQUENCES) + } + } + }; + } + + /* + * Build the menu of key sequences. + * @param {array} sequencesArray an array of strings of KEY_IDs. + * @returns {array} an array of objects with text and value properties. + */ + buildSequenceMenu (sequencesArray) { + return sequencesArray.map( + str => this.getMenuItemForSequenceString(str) + ); + } + + /* + * Create a menu item for a sequence string. + * @param {string} sequenceString a string of KEY_IDs. + * @return {object} an object with text and value properties. + */ + getMenuItemForSequenceString (sequenceString) { + let sequenceArray = sequenceString.split(' '); + sequenceArray = sequenceArray.map(str => this.KEY_TEXT_SHORT[str]); + return { + text: sequenceArray.join(' '), + value: sequenceString + }; + } + + /* + * Check whether a keyboard key is currently pressed. + * Also, toggle the results of the test on alternate frames, so that the + * hat block fires repeatedly. + * @param {object} args - the block arguments. + * @property {number} KEY - a key code. + * @param {object} util - utility object provided by the runtime. + */ + whenMakeyKeyPressed (args, util) { + let key = args.KEY; + // Convert the key arg, if it is a KEY_ID, to the key name used by + // the Keyboard io module. + if (SCRATCH_KEY_NAME[args.KEY]) { + key = SCRATCH_KEY_NAME[args.KEY]; + } + const isDown = util.ioQuery('keyboard', 'getKeyIsDown', [key]); + return (isDown && this.frameToggle); + } + + isMakeyKeyPressed (args, util) { + let key = args.KEY; + // Convert the key arg, if it is a KEY_ID, to the key name used by + // the Keyboard io module. + if (SCRATCH_KEY_NAME[args.KEY]) { + key = SCRATCH_KEY_NAME[args.KEY]; + } + return util.ioQuery('keyboard', 'getKeyIsDown', [key]); + } + + /* + * A function called on the KEY_PRESSED event, to update the key press + * buffer and check if any of the key sequences have been completed. + * @param {string} key A scratch key name. + */ + keyPressed (key) { + // Store only the first word of the Scratch key name, so that e.g. when + // "left arrow" is pressed, we store "LEFT", which matches KEY_ID_LEFT + key = key.split(' ')[0]; + key = key.toUpperCase(); + this.keyPressBuffer.push(key); + // Keep the buffer under the length limit + if (this.keyPressBuffer.length > KEY_BUFFER_LENGTH) { + this.keyPressBuffer.shift(); + } + // Check the buffer for each sequence in use + for (const str in this.sequences) { + const arr = this.sequences[str].array; + // Bail out if we don't have enough presses for this sequence + if (this.keyPressBuffer.length < arr.length) { + continue; + } + let missFlag = false; + // Slice the buffer to the length of the sequence we're checking + const bufferSegment = this.keyPressBuffer.slice(-1 * arr.length); + for (let i = 0; i < arr.length; i++) { + if (arr[i] !== bufferSegment[i]) { + missFlag = true; + } + } + // If the miss flag is false, the sequence matched the buffer + if (!missFlag) { + this.sequences[str].completed = true; + // Clear the completed flag after a timeout. This is necessary because + // the hat is edge-triggered (not event triggered). Multiple hats + // may be checking the same sequence, so this timeout gives them enough + // time to all trigger before resetting the flag. + setTimeout(() => { + this.sequences[str].completed = false; + }, SEQUENCE_HAT_TIMEOUT); + } + } + } + + /** + * Clear the key press buffer. + */ + _clearkeyPressBuffer () { + this.keyPressBuffer = []; + } + + /* + * Add a key sequence to the set currently being checked on each key press. + * @param {string} sequenceString a string of space-separated KEY_IDs. + * @param {array} sequenceArray an array of KEY_IDs. + */ + addSequence (sequenceString, sequenceArray) { + // If we already have this sequence string, return. + if (this.sequences.hasOwnProperty(sequenceString)) { + return; + } + this.sequences[sequenceString] = { + array: sequenceArray, + completed: false + }; + } + + /* + * Check whether a key sequence was recently completed. + * @param {object} args The block arguments. + * @property {number} SEQUENCE A string of KEY_IDs. + */ + whenCodePressed (args) { + const sequenceString = Cast.toString(args.SEQUENCE).toUpperCase(); + const sequenceArray = sequenceString.split(' '); + if (sequenceArray.length < 2) { + return; + } + this.addSequence(sequenceString, sequenceArray); + + return this.sequences[sequenceString].completed; + } +} +module.exports = Scratch3MakeyMakeyBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_microbit/index.js b/local-scratch-vm/src/extensions/scratch3_microbit/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6a7b9b6c83c3d7869a1c1ed67cb3ce39fde64b82 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_microbit/index.js @@ -0,0 +1,984 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const log = require('../../util/log'); +const cast = require('../../util/cast'); +const formatMessage = require('format-message'); +const BLE = require('../../io/ble'); +const Base64Util = require('../../util/base64-util'); + +/** + * Icon png to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAACXBIWXMAABYlAAAWJQFJUiTwAAAKcElEQVR42u2cfXAU9RnHv7u3L3d7l9yR5PIGXO7MkQKaYiCUWqJhFGvRMk4JZXSc8aXVaSmiYlthVHQEW99FxiIdrVY6teiMdoa+ICqhIqgQAsjwMgYDOQKXl7uY17u9293b3f5x5JKYe8+FJGSfvzbP/n77e/azz+95nt9v90KoqgpN0hdSQ6AB1ABqADWAmmgANYAaQA2gJhpADeBEE2q8GPLaWzu/CslyiY4k9dOn5uijtXGd7+jWkaReVpT3Hrhv6d0awEFC07rgD+ZeYYnXprhwigUAvjj0zbjxQCLebozT7iDzK1ZUWCru2K7L//6MVC8ue45Blz8n6rlQ815QtuohOlXiEdy/AUqPa6y59Mkh6Q1345GNja6m7pHEQKNl3t0704EXat4L6fSOmOeEI1vHKzwAyNJR9MPFpRUPOu0ONm2A0xatWaTLm5WfDrzvAppA8AbiG03fC8CQNkDKZK2YrPAuRrhpifJERsuYywveJc7CqcIDMAyeLm82dEXzw39I/qjXkpr3QuW9lxfAdOABGAKPslWDnbsy7Jl8BxTeM3SqmO0gaA5U6c3jymup0YSn9JyLee67wpTfBQAQjmyF3HFqiJcRtDECjy5dAmbmcgQPvjjxl3Lx4IVjnD/5cE1zkWtyP34VBGcdKLJnLgc9cznk1kMXFdzEn8KJ4KUqqsSHvcxWDf7j1UM8UPr6/YgHhhX8xAaYaXgAIB7fBnbuSrBzV8aNgarEQ/z6/YkLcDTg9V9XlXjQtuqoU1TpcUHlvZDOfDiuyh5qPMCLrJ1bDw3EuUtx81N/BH3pjQBJQ2HMF5V6iKfeRchVm9kkMtrwxmSdobeA9daBde8GwVlBcFYofS1Jw0vaAy9HeJHQwBUPzIBvGxDc92Rmp/BowJs10wkAONfsBs8HAAAltqngOAO8HZ3o6OiMqcvLy4E1Lwc8H8C5ZndMXdLJa/qNacNLCDBw/O8nFUNWxp/64+tWAwBefe1tHKg7CgC4/9d3ori4EHv3HcDrb26PqVt2602ovvaHaGlpw+8ffSamLqXYmya8jG8mpFy6iGLkWLh4HAwG4+r6j4VBfaPpLgU8IMGO9MLqW2pYQ9aQokuR5dgXIwCC1CUcNMj3hpdvLAdSF54EYpCHooRA0Swomo2pC0kCQpIAkqTA6LmYupgxL0X7m78+aG10NXVkpIwxsAwWXncDCESHLkohfPbpbiT6ZFPPZQ9fC0e58Wi6wTDj6UbT/rQAyiERS2pW4Kc3LQDLRO8miCEAKj7d83FcTxyLJJJJ+9MCqKoq9HomMrgkSThxsgEcZ8AMpwMkSYJlKDA0DVUFiHGWRDJp/4jXwqIo4uFHnkZXdw8AYGbZFXhs3WqQJDkhkkim7E8KoMlkxKbnn8DBunrwUli3e8/+yOAA0HjmHDq7upGXm5PUoDUr7hmWRB5Zt3FYwoime+vtd/H6G9uGJIxouniSyP6H7v8FystnY80jGzIA0MihsMAKu20aTp3JzFb6WCWRuDUvHwByw8cOhw2FBVaYjNzIAba1e3Hfb9aiq7MTNStuBwAsvr4KO3d9GnmKztIS5EyxTJiVSDT7p04tipx/9MnnYc7ORlu7NzMxsK3di5AkDHgGw2DTC+uHBeGJshJJZL/fxyMQEDKbRAiCQDAoQhBDYBkKNE2j4uqrhpUBoiSBIMZfEhkN+1NeiWSqEB2rlUg69md0JRIQRHy86z8jXsqNVRLJlP0jqgNJXXgAgjbCcONmCHUvQ+44NWG2s/rtH5Mt/ciToo0wLH4JBGO6LLazRiJk2vBYy4gHHw/bWSN+LZBKEhkMjzn/CaSiKgQOvJDyFB7L7axUJWNJZDA8IhQA1boPin7KZbMSGfUYyFx9b3hXg/cCsoBA2Z0AoYOaxlcC4+mdyCUDKBzanLFBJ3USyaRMuiSSKZmUSSSTMimTCABUlblRU9kAZ0E39p+eii21c+EL0jHbOwu6sfaWgyjND//U4oP6MmzZnfi79XT7mfQSNi7bh0JzOLG19XBY/89r49pYVebGqhuOosDsh1+gsWV3BXYdd2Q+BlaVuXFv9bHgkSbzk+vfcVRyjHhi47J9cftsXLYf7T36Ix8cLHlo6ydlv6qpPI2qssRZcuOy/Wjp4k5s+2zG+offKqtcUt6kJtNv7S0H0RtkvEufXTB/6bML5je2Wy7UVDbEbF9o9mPDsv2oP5v75vbPS26rP5u3fdXiozDppcwDrKlswOlWy9E//DX09Mt/azh8zzNM1RybF86C7pheVGD240CDeX3NWtfml94Rt+0+Mf3Lm8qbEnpfgdmPs+3G9+564vTT//pM/GrHYduWRP0AYOEMN/5S61xT92Vtfd2XtfWb/vu91fHALyxzw9tnkB/cTD5w+2Ou9375HHtfa7exM5mxRpKFaafdQQKgAcDERs98/foLHrXdaXfoABi8vczhWO2/28/TRR5z2h00gKymNl1ton79oigq6bQ7dE67Q+ew9mb1h4FYYwVESgLAXLSRa+3mWpIdK+UYuPiq89f8+XfT/+ftZQ4vLm9ZmUyfdcsv1M2fWfRaUCK8i8vdK1u6ktuAWPWTsztm24o/cnnYHUsrWzd1+fVJ9XtqxbG3XzFdNcPTawjcueibpxK1t+X26f/9R8a953jub4typOvm2b1XnvUmv8JKWMZcaZffX3XDERRP8cGaFRjWxtPLoZvXY4oxgPBNEsgxBhCUKEzL6Ru+JydS8Ak0giKFgESDJFQoKmCgQzAwIfQEWETzmoBIwd2VNaStu8uEHGO4Buz06zHHFv0dRkefAZ1+PQx0KNK2eIoPLCUj2zDc275qzgcBFWv+cf3IyxgTK2KOzQufEM5kfpGF12eGPSf8DXN+No/87HDWiwYYALw+M6ym8AscAxO++X7xCTRM7EDQzht0Da8v/NWo1dQDAxNCocUXs+303IGHdaptOmYXnh/SLlZbV+fwnwJm6UXEm/ojqgM/PFmJQ81OPHfrtqT7bN23BE8seTflYLvz5DwYGQHLKz5Puo/XZ8aLtT+D1dSDuxbsGQIymmz48DbwIguOESJOcce8XaO3oVpZ8k3Em5KVVAAMFnuOB9as1MbimCBunn04vBmR40ls29Wfgxf1KMn1gBdY+MXUCvK4ANvPndpLzrLzALjBN2VPwrDBksgLYkn1jBMp90nVY2++8vAw3RlPeLNYVZSPAEgjKWP6ZCn4lF+gMdnE08spQb73RQB9aXtgo6tJcNodf8rWz3L//Br340UW3sExEkXrFFKSSUVHqkRfkJZ8QSZk5gS6hw9H+GyDQAclSs41BVmSUIn+toAKIUTJskKoQUknCxKlkISKb/sM0NMyyVAhXW+AlYosfgOgQlUJVadTSUWBKoQoudvPioPbenq5oIUTaRUqenhWKi3oyVIUqKpKREoLggDhF6hQb4CV9LRM9rctMPN6glChp2SdTqeSskwoAECSKnG61fzFR/XsGu+FhmONriYl7TImsjoYKJyZSeB8CoBQo6spqU8TCO1fgE7gDVUNoCYaQA2gBlADqAHURAOoAdQAagA10QCOgfwfNp/hXbfBMCAAAAAASUVORK5CYII='; + +/** + * Enum for micro:bit BLE command protocol. + * https://github.com/LLK/scratch-microbit-firmware/blob/master/protocol.md + * @readonly + * @enum {number} + */ +const BLECommand = { + CMD_PIN_CONFIG: 0x80, + CMD_DISPLAY_TEXT: 0x81, + CMD_DISPLAY_LED: 0x82 +}; + + +/** + * A time interval to wait (in milliseconds) before reporting to the BLE socket + * that data has stopped coming from the peripheral. + */ +const BLETimeout = 4500; + +/** + * A time interval to wait (in milliseconds) while a block that sends a BLE message is running. + * @type {number} + */ +const BLESendInterval = 100; + +/** + * A string to report to the BLE socket when the micro:bit has stopped receiving data. + * @type {string} + */ +const BLEDataStoppedError = 'micro:bit extension stopped receiving data'; + +/** + * Enum for micro:bit protocol. + * https://github.com/LLK/scratch-microbit-firmware/blob/master/protocol.md + * @readonly + * @enum {string} + */ +const BLEUUID = { + service: 0xf005, + rxChar: '5261da01-fa7e-42ab-850b-7c80220097cc', + txChar: '5261da02-fa7e-42ab-850b-7c80220097cc' +}; + +/** + * Manage communication with a MicroBit peripheral over a Scrath Link client socket. + */ +class MicroBit { + + /** + * Construct a MicroBit communication object. + * @param {Runtime} runtime - the Scratch 3.0 runtime + * @param {string} extensionId - the id of the extension + */ + constructor (runtime, extensionId) { + + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} + * @private + */ + this._runtime = runtime; + + /** + * The BluetoothLowEnergy connection socket for reading/writing peripheral data. + * @type {BLE} + * @private + */ + this._ble = null; + this._runtime.registerPeripheralExtension(extensionId, this); + + /** + * The id of the extension this peripheral belongs to. + */ + this._extensionId = extensionId; + + /** + * The most recently received value for each sensor. + * @type {Object.} + * @private + */ + this._sensors = { + tiltX: 0, + tiltY: 0, + buttonA: 0, + buttonB: 0, + touchPins: [0, 0, 0], + gestureState: 0, + ledMatrixState: new Uint8Array(5) + }; + + /** + * The most recently received value for each gesture. + * @type {Object.} + * @private + */ + this._gestures = { + moving: false, + move: { + active: false, + timeout: false + }, + shake: { + active: false, + timeout: false + }, + jump: { + active: false, + timeout: false + } + }; + + /** + * Interval ID for data reading timeout. + * @type {number} + * @private + */ + this._timeoutID = null; + + /** + * A flag that is true while we are busy sending data to the BLE socket. + * @type {boolean} + * @private + */ + this._busy = false; + + /** + * ID for a timeout which is used to clear the busy flag if it has been + * true for a long time. + */ + this._busyTimeoutID = null; + + this.reset = this.reset.bind(this); + this._onConnect = this._onConnect.bind(this); + this._onMessage = this._onMessage.bind(this); + } + + /** + * @param {string} text - the text to display. + * @return {Promise} - a Promise that resolves when writing to peripheral. + */ + displayText (text) { + const output = new Uint8Array(text.length); + for (let i = 0; i < text.length; i++) { + output[i] = text.charCodeAt(i); + } + return this.send(BLECommand.CMD_DISPLAY_TEXT, output); + } + + /** + * @param {Uint8Array} matrix - the matrix to display. + * @return {Promise} - a Promise that resolves when writing to peripheral. + */ + displayMatrix (matrix) { + return this.send(BLECommand.CMD_DISPLAY_LED, matrix); + } + + /** + * @return {number} - the latest value received for the tilt sensor's tilt about the X axis. + */ + get tiltX () { + return this._sensors.tiltX; + } + + /** + * @return {number} - the latest value received for the tilt sensor's tilt about the Y axis. + */ + get tiltY () { + return this._sensors.tiltY; + } + + /** + * @return {boolean} - the latest value received for the A button. + */ + get buttonA () { + return this._sensors.buttonA; + } + + /** + * @return {boolean} - the latest value received for the B button. + */ + get buttonB () { + return this._sensors.buttonB; + } + + /** + * @return {number} - the latest value received for the motion gesture states. + */ + get gestureState () { + return this._sensors.gestureState; + } + + /** + * @return {Uint8Array} - the current state of the 5x5 LED matrix. + */ + get ledMatrixState () { + return this._sensors.ledMatrixState; + } + + /** + * Called by the runtime when user wants to scan for a peripheral. + */ + scan () { + if (this._ble) { + this._ble.disconnect(); + } + this._ble = new BLE(this._runtime, this._extensionId, { + filters: [ + {services: [BLEUUID.service]} + ] + }, this._onConnect, this.reset); + } + + /** + * Called by the runtime when user wants to connect to a certain peripheral. + * @param {number} id - the id of the peripheral to connect to. + */ + connect (id) { + if (this._ble) { + this._ble.connectPeripheral(id); + } + } + + /** + * Disconnect from the micro:bit. + */ + disconnect () { + if (this._ble) { + this._ble.disconnect(); + } + + this.reset(); + } + + /** + * Reset all the state and timeout/interval ids. + */ + reset () { + if (this._timeoutID) { + window.clearTimeout(this._timeoutID); + this._timeoutID = null; + } + } + + /** + * Return true if connected to the micro:bit. + * @return {boolean} - whether the micro:bit is connected. + */ + isConnected () { + let connected = false; + if (this._ble) { + connected = this._ble.isConnected(); + } + return connected; + } + + /** + * Send a message to the peripheral BLE socket. + * @param {number} command - the BLE command hex. + * @param {Uint8Array} message - the message to write + */ + send (command, message) { + if (!this.isConnected()) return; + if (this._busy) return; + + // Set a busy flag so that while we are sending a message and waiting for + // the response, additional messages are ignored. + this._busy = true; + + // Set a timeout after which to reset the busy flag. This is used in case + // a BLE message was sent for which we never received a response, because + // e.g. the peripheral was turned off after the message was sent. We reset + // the busy flag after a while so that it is possible to try again later. + this._busyTimeoutID = window.setTimeout(() => { + this._busy = false; + }, 5000); + + const output = new Uint8Array(message.length + 1); + output[0] = command; // attach command to beginning of message + for (let i = 0; i < message.length; i++) { + output[i + 1] = message[i]; + } + const data = Base64Util.uint8ArrayToBase64(output); + + this._ble.write(BLEUUID.service, BLEUUID.txChar, data, 'base64', true).then( + () => { + this._busy = false; + window.clearTimeout(this._busyTimeoutID); + } + ); + } + + /** + * Starts reading data from peripheral after BLE has connected to it. + * @private + */ + _onConnect () { + this._ble.read(BLEUUID.service, BLEUUID.rxChar, true, this._onMessage); + this._timeoutID = window.setTimeout( + () => this._ble.handleDisconnectError(BLEDataStoppedError), + BLETimeout + ); + } + + /** + * Process the sensor data from the incoming BLE characteristic. + * @param {object} base64 - the incoming BLE data. + * @private + */ + _onMessage (base64) { + // parse data + const data = Base64Util.base64ToUint8Array(base64); + + this._sensors.tiltX = data[1] | (data[0] << 8); + if (this._sensors.tiltX > (1 << 15)) this._sensors.tiltX -= (1 << 16); + this._sensors.tiltY = data[3] | (data[2] << 8); + if (this._sensors.tiltY > (1 << 15)) this._sensors.tiltY -= (1 << 16); + + this._sensors.buttonA = data[4]; + this._sensors.buttonB = data[5]; + + this._sensors.touchPins[0] = data[6]; + this._sensors.touchPins[1] = data[7]; + this._sensors.touchPins[2] = data[8]; + + this._sensors.gestureState = data[9]; + + // cancel disconnect timeout and start a new one + window.clearTimeout(this._timeoutID); + this._timeoutID = window.setTimeout( + () => this._ble.handleDisconnectError(BLEDataStoppedError), + BLETimeout + ); + } + + /** + * @param {number} pin - the pin to check touch state. + * @return {number} - the latest value received for the touch pin states. + * @private + */ + _checkPinState (pin) { + return this._sensors.touchPins[pin]; + } +} + +/** + * Enum for tilt sensor direction. + * @readonly + * @enum {string} + */ +const MicroBitTiltDirection = { + FRONT: 'front', + BACK: 'back', + LEFT: 'left', + RIGHT: 'right', + ANY: 'any' +}; + +/** + * Enum for micro:bit gestures. + * @readonly + * @enum {string} + */ +const MicroBitGestures = { + MOVED: 'moved', + SHAKEN: 'shaken', + JUMPED: 'jumped' +}; + +/** + * Enum for micro:bit buttons. + * @readonly + * @enum {string} + */ +const MicroBitButtons = { + A: 'A', + B: 'B', + ANY: 'any' +}; + +/** + * Enum for micro:bit pin states. + * @readonly + * @enum {string} + */ +const MicroBitPinState = { + ON: 'on', + OFF: 'off' +}; + +/** + * Scratch 3.0 blocks to interact with a MicroBit peripheral. + */ +class Scratch3MicroBitBlocks { + + /** + * @return {string} - the name of this extension. + */ + static get EXTENSION_NAME () { + return 'micro:bit'; + } + + /** + * @return {string} - the ID of this extension. + */ + static get EXTENSION_ID () { + return 'microbit'; + } + + /** + * @return {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold. + */ + static get TILT_THRESHOLD () { + return 15; + } + + /** + * @return {array} - text and values for each buttons menu element + */ + get BUTTONS_MENU () { + return [ + { + text: 'A', + value: MicroBitButtons.A + }, + { + text: 'B', + value: MicroBitButtons.B + }, + { + text: formatMessage({ + id: 'microbit.buttonsMenu.any', + default: 'any', + description: 'label for "any" element in button picker for micro:bit extension' + }), + value: MicroBitButtons.ANY + } + ]; + } + + /** + * @return {array} - text and values for each gestures menu element + */ + get GESTURES_MENU () { + return [ + { + text: formatMessage({ + id: 'microbit.gesturesMenu.moved', + default: 'moved', + description: 'label for moved gesture in gesture picker for micro:bit extension' + }), + value: MicroBitGestures.MOVED + }, + { + text: formatMessage({ + id: 'microbit.gesturesMenu.shaken', + default: 'shaken', + description: 'label for shaken gesture in gesture picker for micro:bit extension' + }), + value: MicroBitGestures.SHAKEN + }, + { + text: formatMessage({ + id: 'microbit.gesturesMenu.jumped', + default: 'jumped', + description: 'label for jumped gesture in gesture picker for micro:bit extension' + }), + value: MicroBitGestures.JUMPED + } + ]; + } + + /** + * @return {array} - text and values for each pin state menu element + */ + get PIN_STATE_MENU () { + return [ + { + text: formatMessage({ + id: 'microbit.pinStateMenu.on', + default: 'on', + description: 'label for on element in pin state picker for micro:bit extension' + }), + value: MicroBitPinState.ON + }, + { + text: formatMessage({ + id: 'microbit.pinStateMenu.off', + default: 'off', + description: 'label for off element in pin state picker for micro:bit extension' + }), + value: MicroBitPinState.OFF + } + ]; + } + + /** + * @return {array} - text and values for each tilt direction menu element + */ + get TILT_DIRECTION_MENU () { + return [ + { + text: formatMessage({ + id: 'microbit.tiltDirectionMenu.front', + default: 'front', + description: 'label for front element in tilt direction picker for micro:bit extension' + }), + value: MicroBitTiltDirection.FRONT + }, + { + text: formatMessage({ + id: 'microbit.tiltDirectionMenu.back', + default: 'back', + description: 'label for back element in tilt direction picker for micro:bit extension' + }), + value: MicroBitTiltDirection.BACK + }, + { + text: formatMessage({ + id: 'microbit.tiltDirectionMenu.left', + default: 'left', + description: 'label for left element in tilt direction picker for micro:bit extension' + }), + value: MicroBitTiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'microbit.tiltDirectionMenu.right', + default: 'right', + description: 'label for right element in tilt direction picker for micro:bit extension' + }), + value: MicroBitTiltDirection.RIGHT + } + ]; + } + + /** + * @return {array} - text and values for each tilt direction (plus "any") menu element + */ + get TILT_DIRECTION_ANY_MENU () { + return [ + ...this.TILT_DIRECTION_MENU, + { + text: formatMessage({ + id: 'microbit.tiltDirectionMenu.any', + default: 'any', + description: 'label for any direction element in tilt direction picker for micro:bit extension' + }), + value: MicroBitTiltDirection.ANY + } + ]; + } + + /** + * Construct a set of MicroBit blocks. + * @param {Runtime} runtime - the Scratch 3.0 runtime. + */ + constructor (runtime) { + /** + * The Scratch 3.0 runtime. + * @type {Runtime} + */ + this.runtime = runtime; + + // Create a new MicroBit peripheral instance + this._peripheral = new MicroBit(this.runtime, Scratch3MicroBitBlocks.EXTENSION_ID); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: Scratch3MicroBitBlocks.EXTENSION_ID, + name: Scratch3MicroBitBlocks.EXTENSION_NAME, + blockIconURI: blockIconURI, + showStatusButton: true, + blocks: [ + { + opcode: 'whenButtonPressed', + text: formatMessage({ + id: 'microbit.whenButtonPressed', + default: 'when [BTN] button pressed', + description: 'when the selected button on the micro:bit is pressed' + }), + blockType: BlockType.HAT, + arguments: { + BTN: { + type: ArgumentType.STRING, + menu: 'buttons', + defaultValue: MicroBitButtons.A + } + } + }, + { + opcode: 'isButtonPressed', + text: formatMessage({ + id: 'microbit.isButtonPressed', + default: '[BTN] button pressed?', + description: 'is the selected button on the micro:bit pressed?' + }), + blockType: BlockType.BOOLEAN, + arguments: { + BTN: { + type: ArgumentType.STRING, + menu: 'buttons', + defaultValue: MicroBitButtons.A + } + } + }, + '---', + { + opcode: 'whenGesture', + text: formatMessage({ + id: 'microbit.whenGesture', + default: 'when [GESTURE]', + description: 'when the selected gesture is detected by the micro:bit' + }), + blockType: BlockType.HAT, + arguments: { + GESTURE: { + type: ArgumentType.STRING, + menu: 'gestures', + defaultValue: MicroBitGestures.MOVED + } + } + }, + '---', + { + opcode: 'displaySymbol', + text: formatMessage({ + id: 'microbit.displaySymbol', + default: 'display [MATRIX]', + description: 'display a pattern on the micro:bit display' + }), + blockType: BlockType.COMMAND, + arguments: { + MATRIX: { + type: ArgumentType.MATRIX, + defaultValue: '0101010101100010101000100' + } + } + }, + { + opcode: 'displayText', + text: formatMessage({ + id: 'microbit.displayText', + default: 'display text [TEXT]', + description: 'display text on the micro:bit display' + }), + blockType: BlockType.COMMAND, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'microbit.defaultTextToDisplay', + default: 'Hello!', + description: `default text to display. + IMPORTANT - the micro:bit only supports letters a-z, A-Z. + Please substitute a default word in your language + that can be written with those characters, + substitute non-accented characters or leave it as "Hello!". + Check the micro:bit site documentation for details` + }) + } + } + }, + { + opcode: 'displayClear', + text: formatMessage({ + id: 'microbit.clearDisplay', + default: 'clear display', + description: 'display nothing on the micro:bit display' + }), + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'whenTilted', + text: formatMessage({ + id: 'microbit.whenTilted', + default: 'when tilted [DIRECTION]', + description: 'when the micro:bit is tilted in a direction' + }), + blockType: BlockType.HAT, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'tiltDirectionAny', + defaultValue: MicroBitTiltDirection.ANY + } + } + }, + { + opcode: 'isTilted', + text: formatMessage({ + id: 'microbit.isTilted', + default: 'tilted [DIRECTION]?', + description: 'is the micro:bit is tilted in a direction?' + }), + blockType: BlockType.BOOLEAN, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'tiltDirectionAny', + defaultValue: MicroBitTiltDirection.ANY + } + } + }, + { + opcode: 'getTiltAngle', + text: formatMessage({ + id: 'microbit.tiltAngle', + default: 'tilt angle [DIRECTION]', + description: 'how much the micro:bit is tilted in a direction' + }), + blockType: BlockType.REPORTER, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'tiltDirection', + defaultValue: MicroBitTiltDirection.FRONT + } + } + }, + '---', + { + opcode: 'whenPinConnected', + text: formatMessage({ + id: 'microbit.whenPinConnected', + default: 'when pin [PIN] connected', + description: 'when the pin detects a connection to Earth/Ground' + + }), + blockType: BlockType.HAT, + arguments: { + PIN: { + type: ArgumentType.STRING, + menu: 'touchPins', + defaultValue: '0' + } + } + } + ], + menus: { + buttons: { + acceptReporters: true, + items: this.BUTTONS_MENU + }, + gestures: { + acceptReporters: true, + items: this.GESTURES_MENU + }, + pinState: { + acceptReporters: true, + items: this.PIN_STATE_MENU + }, + tiltDirection: { + acceptReporters: true, + items: this.TILT_DIRECTION_MENU + }, + tiltDirectionAny: { + acceptReporters: true, + items: this.TILT_DIRECTION_ANY_MENU + }, + touchPins: { + acceptReporters: true, + items: ['0', '1', '2'] + } + } + }; + } + + /** + * Test whether the A or B button is pressed + * @param {object} args - the block's arguments. + * @return {boolean} - true if the button is pressed. + */ + whenButtonPressed (args) { + if (args.BTN === 'any') { + return this._peripheral.buttonA | this._peripheral.buttonB; + } else if (args.BTN === 'A') { + return this._peripheral.buttonA; + } else if (args.BTN === 'B') { + return this._peripheral.buttonB; + } + return false; + } + + /** + * Test whether the A or B button is pressed + * @param {object} args - the block's arguments. + * @return {boolean} - true if the button is pressed. + */ + isButtonPressed (args) { + if (args.BTN === 'any') { + return (this._peripheral.buttonA | this._peripheral.buttonB) !== 0; + } else if (args.BTN === 'A') { + return this._peripheral.buttonA !== 0; + } else if (args.BTN === 'B') { + return this._peripheral.buttonB !== 0; + } + return false; + } + + /** + * Test whether the micro:bit is moving + * @param {object} args - the block's arguments. + * @return {boolean} - true if the micro:bit is moving. + */ + whenGesture (args) { + const gesture = cast.toString(args.GESTURE); + if (gesture === 'moved') { + return (this._peripheral.gestureState >> 2) & 1; + } else if (gesture === 'shaken') { + return this._peripheral.gestureState & 1; + } else if (gesture === 'jumped') { + return (this._peripheral.gestureState >> 1) & 1; + } + return false; + } + + /** + * Display a predefined symbol on the 5x5 LED matrix. + * @param {object} args - the block's arguments. + * @return {Promise} - a Promise that resolves after a tick. + */ + displaySymbol (args) { + const symbol = cast.toString(args.MATRIX).replace(/\s/g, ''); + const reducer = (accumulator, c, index) => { + const value = (c === '0') ? accumulator : accumulator + Math.pow(2, index); + return value; + }; + const hex = symbol.split('').reduce(reducer, 0); + if (hex !== null) { + this._peripheral.ledMatrixState[0] = hex & 0x1F; + this._peripheral.ledMatrixState[1] = (hex >> 5) & 0x1F; + this._peripheral.ledMatrixState[2] = (hex >> 10) & 0x1F; + this._peripheral.ledMatrixState[3] = (hex >> 15) & 0x1F; + this._peripheral.ledMatrixState[4] = (hex >> 20) & 0x1F; + this._peripheral.displayMatrix(this._peripheral.ledMatrixState); + } + + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Display text on the 5x5 LED matrix. + * @param {object} args - the block's arguments. + * @return {Promise} - a Promise that resolves after the text is done printing. + * Note the limit is 19 characters + * The print time is calculated by multiplying the number of horizontal pixels + * by the default scroll delay of 120ms. + * The number of horizontal pixels = 6px for each character in the string, + * 1px before the string, and 5px after the string. + */ + displayText (args) { + const text = String(args.TEXT).substring(0, 19); + if (text.length > 0) this._peripheral.displayText(text); + const yieldDelay = 120 * ((6 * text.length) + 6); + + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, yieldDelay); + }); + } + + /** + * Turn all 5x5 matrix LEDs off. + * @return {Promise} - a Promise that resolves after a tick. + */ + displayClear () { + for (let i = 0; i < 5; i++) { + this._peripheral.ledMatrixState[i] = 0; + } + this._peripheral.displayMatrix(this._peripheral.ledMatrixState); + + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {TiltDirection} DIRECTION - the tilt direction to test (front, back, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + whenTilted (args) { + return this._isTilted(args.DIRECTION); + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {TiltDirection} DIRECTION - the tilt direction to test (front, back, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + isTilted (args) { + return this._isTilted(args.DIRECTION); + } + + /** + * @param {object} args - the block's arguments. + * @property {TiltDirection} DIRECTION - the direction (front, back, left, right) to check. + * @return {number} - the tilt sensor's angle in the specified direction. + * Note that getTiltAngle(front) = -getTiltAngle(back) and getTiltAngle(left) = -getTiltAngle(right). + */ + getTiltAngle (args) { + return this._getTiltAngle(args.DIRECTION); + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {TiltDirection} direction - the tilt direction to test (front, back, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + * @private + */ + _isTilted (direction) { + switch (direction) { + case MicroBitTiltDirection.ANY: + return (Math.abs(this._peripheral.tiltX / 10) >= Scratch3MicroBitBlocks.TILT_THRESHOLD) || + (Math.abs(this._peripheral.tiltY / 10) >= Scratch3MicroBitBlocks.TILT_THRESHOLD); + default: + return this._getTiltAngle(direction) >= Scratch3MicroBitBlocks.TILT_THRESHOLD; + } + } + + /** + * @param {TiltDirection} direction - the direction (front, back, left, right) to check. + * @return {number} - the tilt sensor's angle in the specified direction. + * Note that getTiltAngle(front) = -getTiltAngle(back) and getTiltAngle(left) = -getTiltAngle(right). + * @private + */ + _getTiltAngle (direction) { + switch (direction) { + case MicroBitTiltDirection.FRONT: + return Math.round(this._peripheral.tiltY / -10); + case MicroBitTiltDirection.BACK: + return Math.round(this._peripheral.tiltY / 10); + case MicroBitTiltDirection.LEFT: + return Math.round(this._peripheral.tiltX / -10); + case MicroBitTiltDirection.RIGHT: + return Math.round(this._peripheral.tiltX / 10); + default: + log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`); + } + } + + /** + * @param {object} args - the block's arguments. + * @return {boolean} - the touch pin state. + * @private + */ + whenPinConnected (args) { + const pin = parseInt(args.PIN, 10); + if (isNaN(pin)) return; + if (pin < 0 || pin > 2) return false; + return this._peripheral._checkPinState(pin); + } +} + +module.exports = Scratch3MicroBitBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/1-snare.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/1-snare.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6cbd0c8c1a961347794a5ca3410b3b4d7a2f4afd Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/1-snare.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..9c551b3661873ca96cc76af6df131f648c0e8e4b Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4f623cd120b464012c57cd8579877bcf7db001c0 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0987759cae7299681033cb17d65b48169c5029eb Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5faf07936131df26ee82ad31e6da9558060c278e Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/14-conga.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/14-conga.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..585feca6a17a5bff3dcf5f085270ed371c87c333 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/14-conga.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4fce061764374f78508d50cbc6e8e34c33a864b2 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..882feaaafc039b527572a7d627076e5357dea5d8 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..faf7315ae3db40ed431b40120a9feaff4ff3b6bd Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..1970b60cbff5ad8106e16bf69d814f1c683b43c0 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..bc25332748272a81fdaa8f751d4bf6a53a50f09b Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c7757c932b951d636278aac249344ac5d4b609d8 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..bf9c664793615bcb5f5422b770b02d70e3793802 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..9e9e62b5599986b2005b0595bb8c56d0c2748994 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..842228e334b1231bc476ecd7f6885b3e255b8ed3 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f0ea66a585ec62f281219a156336a9fb193fd8e0 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a5b55f522f628c09360473ca0429a1a41b3331c4 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/drums/9-claves.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/9-claves.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..75b76533dec423cece275edae9d2887073ed78ae Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/drums/9-claves.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a7c0507397443cc7ac9f94457f83917042def883 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3ee19ed18f6d99ca4feb5e3938c21bb84639f9ce Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..b680da226e2798e7f98b2d558773095e1bdee7eb Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..cf099153036ab4eb5abb3db299d8bc97f5495adc Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..adb7052b715745b0f68533236af0d804b127ed74 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..27451fc6a1914cb91939644c065d0c098a54e4af Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f32a1da73c948532fa102b80ff65ffb4dee668be Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4efc40bf0c536e7dee1ffd4ba085ead6a3c2664d Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c7d46dbd66ac141f3ce3c6f5e204c67ae9eb5433 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..12460dfca0bf3deb48bd90fef953d3e277f20378 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..472afa8f99cec1ea3835c92c391bca6010ec6e7a Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..92da76ce0b37ab6a00fdcf53b8aa1185640c685b Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7a961d0dfe2c257ca55ee7f1ca63a869a10e5c14 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0189b3effe290f8e5e4ff5a068796407859e66c2 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7b57139bf7fff48e0396e3853d73b6451d624357 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c898b844e6e3383ade3ab42b5ec33e298a03a4fb Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6a7a9e99e9826955cdd38633defd14888cd2b479 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d8ff9f5ce0544eb931146252567ae17b7911a390 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5592bc64979f40a098c71e19c7be4acdf0e16ea3 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..640f3de262fcb50e2c4ffe4b4a301c654de39052 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0b87ef6e0bbd2a031aeca227ed974f66270560a2 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..489247955c62bb7a57bc82f3f47269cdb9e3c3b4 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0e3074f4328c3144c5868aa8946cf4ceb720e823 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..988f8231875907b06f7e5d0e8896d779675fdca1 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ebdd5e0e6730e38209519074f3d9decdc05b70cf Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..169df684f790953e2d7e7cd80c508f72793efbc5 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e7b8585bb63251141ca3784131ed5a5e594762fd Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..54b7df80b9c086bd58b919b98001cb1ca22d1485 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d8d1abc1d61f58bd70d3690b7c92b795cdef1c66 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0c90c3e32e8ff2bbbe59c150b0ab055c2f2e841c Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..93ff1d94eb4cfd6a8f4d39b93be281f0524235f1 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a329b732cec12fdf74629d6562b2a398a8f50c93 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..064672bb625a5c030d7f572f028128a4a5fc8a18 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f8558dee44d1dee57e26275ff68b8524d610b91a Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d967d830ef56e0a9450a3032a47e1d9970f2f0e1 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5c8ce5664ef7da7d3eb1c002d4496edb1e6c5df5 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..589c65786785ba934243cb6d3008597ac8f88fa6 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c337dedf2cda849ed2ab020ed1d17db09789411b Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c4b556da2f46e54069966a7737bb23b9c9c1a664 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..28aeded5178f46ed8b2e7883ab172ae9902ace24 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..95c289ba3e421e368ec8ec03551b282771eb52c5 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..2eee89d3b83ffed8bf2728599e867c69bc0e9fc5 Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4f00cf119d12e454bf113d17d30f8a6e354eea1c Binary files /dev/null and b/local-scratch-vm/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 differ diff --git a/local-scratch-vm/src/extensions/scratch3_music/index.js b/local-scratch-vm/src/extensions/scratch3_music/index.js new file mode 100644 index 0000000000000000000000000000000000000000..982bd7a247a0e92e5cfe08b2ef32540e7e127f2e --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_music/index.js @@ -0,0 +1,1335 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Clone = require('../../util/clone'); +const Cast = require('../../util/cast'); +const formatMessage = require('format-message'); +const MathUtil = require('../../util/math-util'); +const Timer = require('../../util/timer'); + +/** + * The instrument and drum sounds, loaded as static assets. + * @type {object} + */ +let assetData = {}; +try { + assetData = require('./manifest'); +} catch (e) { + // Non-webpack environment, don't worry about assets. +} + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHRpdGxlPm11c2ljLWJsb2NrLWljb248L3RpdGxlPjxkZWZzPjxwYXRoIGQ9Ik0zMi4xOCAyNS44NzRDMzIuNjM2IDI4LjE1NyAzMC41MTIgMzAgMjcuNDMzIDMwYy0zLjA3IDAtNS45MjMtMS44NDMtNi4zNzItNC4xMjYtLjQ1OC0yLjI4NSAxLjY2NS00LjEzNiA0Ljc0My00LjEzNi42NDcgMCAxLjI4My4wODQgMS44OS4yMzQuMzM4LjA4Ni42MzcuMTguOTM4LjMwMi44Ny0uMDItLjEwNC0yLjI5NC0xLjgzNS0xMi4yMy0yLjEzNC0xMi4zMDIgMy4wNi0xLjg3IDguNzY4LTIuNzUyIDUuNzA4LS44ODUuMDc2IDQuODItMy42NSAzLjg0NC0zLjcyNC0uOTg3LTQuNjUtNy4xNTMuMjYzIDE0LjczOHptLTE2Ljk5OCA1Ljk5QzE1LjYzIDM0LjE0OCAxMy41MDcgMzYgMTAuNDQgMzZjLTMuMDcgMC01LjkyMi0xLjg1Mi02LjM4LTQuMTM2LS40NDgtMi4yODQgMS42NzQtNC4xMzUgNC43NS00LjEzNSAxLjAwMyAwIDEuOTc1LjE5NiAyLjg1NS41NDMuODIyLS4wNTUtLjE1LTIuMzc3LTEuODYyLTEyLjIyOC0yLjEzMy0xMi4zMDMgMy4wNi0xLjg3IDguNzY0LTIuNzUzIDUuNzA2LS44OTQuMDc2IDQuODItMy42NDggMy44MzQtMy43MjQtLjk4Ny00LjY1LTcuMTUyLjI2MiAxNC43Mzh6IiBpZD0iYSIvPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjx1c2UgZmlsbD0iI0ZGRiIgeGxpbms6aHJlZj0iI2EiLz48cGF0aCBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZT0iIzAwMCIgZD0iTTI4LjQ1NiAyMS42NzVjLS4wMS0uMzEyLS4wODctLjgyNS0uMjU2LTEuNzAyLS4wOTYtLjQ5NS0uNjEyLTMuMDIyLS43NTMtMy43My0uMzk1LTEuOTgtLjc2LTMuOTItMS4xNDItNi4xMTMtLjczMi00LjIyMy0uNjkzLTYuMDUuMzQ0LTYuNTI3LjUtLjIzIDEuMDYtLjA4IDEuODQuMzUuNDE0LjIyNyAyLjE4MiAxLjM2NSAyLjA3IDEuMjk2IDEuOTk0IDEuMjQyIDMuNDY0IDEuNzc0IDQuOTMgMS41NDggMS41MjYtLjIzNyAyLjUwNC0uMDYgMi44NzYuNjE4LjM0OC42MzUuMDE1IDEuNDE2LS43MyAyLjE4LTEuNDcyIDEuNTE2LTMuOTc1IDIuNTE0LTUuODQ4IDIuMDIzLS44MjItLjIyLTEuMjM4LS40NjUtMi4zOC0xLjI2N2wtLjA5NS0uMDY2Yy4wNDcuNTkzLjI2NCAxLjc0LjcxNyAzLjgwMy4yOTQgMS4zMzYgMi4wOCA5LjE4NyAyLjYzNyAxMS42NzRsLjAwMi4wMTJjLjUyOCAyLjYzNy0xLjg3MyA0LjcyNC01LjIzNiA0LjcyNC0zLjI5IDAtNi4zNjMtMS45ODgtNi44NjItNC41MjgtLjUzLTIuNjQgMS44NzMtNC43MzQgNS4yMzMtNC43MzQuNjcyIDAgMS4zNDcuMDg1IDIuMDE0LjI1LjIyNy4wNTcuNDM2LjExOC42MzYuMTg3em0tMTYuOTk2IDUuOTljLS4wMS0uMzE4LS4wOS0uODM4LS4yNjYtMS43MzctLjA5LS40Ni0uNTk1LTIuOTM3LS43NTMtMy43MjctLjM5LTEuOTYtLjc1LTMuODktMS4xMy02LjA3LS43MzItNC4yMjMtLjY5Mi02LjA1LjM0NC02LjUyNi41MDItLjIzIDEuMDYtLjA4MiAxLjg0LjM1LjQxNS4yMjcgMi4xODIgMS4zNjQgMi4wNyAxLjI5NSAxLjk5MyAxLjI0MiAzLjQ2MiAxLjc3NCA0LjkyNiAxLjU0OCAxLjUyNS0uMjQgMi41MDQtLjA2NCAyLjg3Ni42MTQuMzQ4LjYzNS4wMTUgMS40MTUtLjcyOCAyLjE4LTEuNDc0IDEuNTE3LTMuOTc3IDIuNTEzLTUuODQ3IDIuMDE3LS44Mi0uMjItMS4yMzYtLjQ2NC0yLjM3OC0xLjI2N2wtLjA5NS0uMDY1Yy4wNDcuNTkzLjI2NCAxLjc0LjcxNyAzLjgwMi4yOTQgMS4zMzcgMi4wNzggOS4xOSAyLjYzNiAxMS42NzVsLjAwMy4wMTNjLjUxNyAyLjYzOC0xLjg4NCA0LjczMi01LjIzNCA0LjczMi0zLjI4NyAwLTYuMzYtMS45OTMtNi44Ny00LjU0LS41Mi0yLjY0IDEuODg0LTQuNzMgNS4yNC00LjczLjkwNSAwIDEuODAzLjE1IDIuNjUuNDM2eiIvPjwvZz48L3N2Zz4='; + +/** + * Icon svg to be displayed in the category menu, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE2LjA5IDEyLjkzN2MuMjI4IDEuMTQxLS44MzMgMi4wNjMtMi4zNzMgMi4wNjMtMS41MzUgMC0yLjk2Mi0uOTIyLTMuMTg2LTIuMDYzLS4yMy0xLjE0Mi44MzMtMi4wNjggMi4zNzItMi4wNjguMzIzIDAgLjY0MS4wNDIuOTQ1LjExN2EzLjUgMy41IDAgMCAxIC40NjguMTUxYy40MzUtLjAxLS4wNTItMS4xNDctLjkxNy02LjExNC0xLjA2Ny02LjE1MiAxLjUzLS45MzUgNC4zODQtMS4zNzcgMi44NTQtLjQ0Mi4wMzggMi40MS0xLjgyNSAxLjkyMi0xLjg2Mi0uNDkzLTIuMzI1LTMuNTc3LjEzMiA3LjM3ek03LjQ2IDguNTYzYy0xLjg2Mi0uNDkzLTIuMzI1LTMuNTc2LjEzIDcuMzdDNy44MTYgMTcuMDczIDYuNzU0IDE4IDUuMjIgMThjLTEuNTM1IDAtMi45NjEtLjkyNi0zLjE5LTIuMDY4LS4yMjQtMS4xNDIuODM3LTIuMDY3IDIuMzc1LTIuMDY3LjUwMSAwIC45ODcuMDk4IDEuNDI3LjI3Mi40MTItLjAyOC0uMDc0LTEuMTg5LS45My02LjExNEMzLjgzNCAxLjg3IDYuNDMgNy4wODcgOS4yODIgNi42NDZjMi44NTQtLjQ0Ny4wMzggMi40MS0xLjgyMyAxLjkxN3oiIGZpbGw9IiM1NzVFNzUiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg=='; + +/** + * Class for the music-related blocks in Scratch 3.0 + * @param {Runtime} runtime - the runtime instantiating this block package. + * @constructor + */ +class Scratch3MusicBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * The number of drum and instrument sounds currently being played simultaneously. + * @type {number} + * @private + */ + this._concurrencyCounter = 0; + + /** + * An array of sound players, one for each drum sound. + * @type {Array} + * @private + */ + this._drumPlayers = []; + + /** + * An array of arrays of sound players. Each instrument has one or more audio players. + * @type {Array[]} + * @private + */ + this._instrumentPlayerArrays = []; + + /** + * An array of arrays of sound players. Each instrument mya have an audio player for each playable note. + * @type {Array[]} + * @private + */ + this._instrumentPlayerNoteArrays = []; + + /** + * An array of audio bufferSourceNodes. Each time you play an instrument or drum sound, + * a bufferSourceNode is created. We keep references to them to make sure their onended + * events can fire. + * @type {Array} + * @private + */ + this._bufferSources = []; + + this._loadAllSounds(); + + this._onTargetCreated = this._onTargetCreated.bind(this); + this.runtime.on('targetWasCreated', this._onTargetCreated); + + this._playNoteForPicker = this._playNoteForPicker.bind(this); + this.runtime.on('PLAY_NOTE', this._playNoteForPicker); + } + + /** + * Decode the full set of drum and instrument sounds, and store the audio buffers in arrays. + */ + _loadAllSounds () { + const loadingPromises = []; + this.DRUM_INFO.forEach((drumInfo, index) => { + const filePath = `drums/${drumInfo.fileName}`; + const promise = this._storeSound(filePath, index, this._drumPlayers); + loadingPromises.push(promise); + }); + this.INSTRUMENT_INFO.forEach((instrumentInfo, instrumentIndex) => { + this._instrumentPlayerArrays[instrumentIndex] = []; + this._instrumentPlayerNoteArrays[instrumentIndex] = []; + instrumentInfo.samples.forEach((sample, noteIndex) => { + const filePath = `instruments/${instrumentInfo.dirName}/${sample}`; + const promise = this._storeSound(filePath, noteIndex, this._instrumentPlayerArrays[instrumentIndex]); + loadingPromises.push(promise); + }); + }); + Promise.all(loadingPromises).then(() => { + // @TODO: Update the extension status indicator. + }); + } + + /** + * Decode a sound and store the player in an array. + * @param {string} filePath - the audio file name. + * @param {number} index - the index at which to store the audio player. + * @param {array} playerArray - the array of players in which to store it. + * @return {Promise} - a promise which will resolve once the sound has been stored. + */ + _storeSound (filePath, index, playerArray) { + const fullPath = `${filePath}.mp3`; + + if (!assetData[fullPath]) return; + + const soundFile = assetData[fullPath]; + + return fetch(soundFile) + .then(r => r.arrayBuffer()) + .then(soundBuffer => this._decodeSound(soundBuffer)) + .then(player => { + playerArray[index] = player; + }); + } + + /** + * Decode a sound and return a promise with the audio buffer. + * @param {ArrayBuffer} soundBuffer - a buffer containing the encoded audio. + * @return {Promise} - a promise which will resolve once the sound has decoded. + */ + _decodeSound (soundBuffer) { + const engine = this.runtime.audioEngine; + + if (!engine) { + return Promise.reject(new Error('No Audio Context Detected')); + } + + // Check for newer promise-based API + return engine.decodeSoundPlayer({data: {buffer: soundBuffer}}); + } + + /** + * Create data for a menu in scratch-blocks format, consisting of an array of objects with text and + * value properties. The text is a translated string, and the value is one-indexed. + * @param {object[]} info - An array of info objects each having a name property. + * @return {array} - An array of objects with text and value properties. + * @private + */ + _buildMenu (info) { + return info.map((entry, index) => { + const obj = {}; + obj.text = entry.name; + obj.value = String(index + 1); + return obj; + }); + } + + /** + * An array of info about each drum. + * @type {object[]} + * @param {string} name - the translatable name to display in the drums menu. + * @param {string} fileName - the name of the audio file containing the drum sound. + */ + get DRUM_INFO () { + return [ + { + name: formatMessage({ + id: 'music.drumSnare', + default: '(1) Snare Drum', + description: 'Sound of snare drum as used in a standard drum kit' + }), + fileName: '1-snare' + }, + { + name: formatMessage({ + id: 'music.drumBass', + default: '(2) Bass Drum', + description: 'Sound of bass drum as used in a standard drum kit' + }), + fileName: '2-bass-drum' + }, + { + name: formatMessage({ + id: 'music.drumSideStick', + default: '(3) Side Stick', + description: 'Sound of a drum stick hitting the side of a drum (usually the snare)' + }), + fileName: '3-side-stick' + }, + { + name: formatMessage({ + id: 'music.drumCrashCymbal', + default: '(4) Crash Cymbal', + description: 'Sound of a drum stick hitting a crash cymbal' + }), + fileName: '4-crash-cymbal' + }, + { + name: formatMessage({ + id: 'music.drumOpenHiHat', + default: '(5) Open Hi-Hat', + description: 'Sound of a drum stick hitting a hi-hat while open' + }), + fileName: '5-open-hi-hat' + }, + { + name: formatMessage({ + id: 'music.drumClosedHiHat', + default: '(6) Closed Hi-Hat', + description: 'Sound of a drum stick hitting a hi-hat while closed' + }), + fileName: '6-closed-hi-hat' + }, + { + name: formatMessage({ + id: 'music.drumTambourine', + default: '(7) Tambourine', + description: 'Sound of a tambourine being struck' + }), + fileName: '7-tambourine' + }, + { + name: formatMessage({ + id: 'music.drumHandClap', + default: '(8) Hand Clap', + description: 'Sound of two hands clapping together' + }), + fileName: '8-hand-clap' + }, + { + name: formatMessage({ + id: 'music.drumClaves', + default: '(9) Claves', + description: 'Sound of claves being struck together' + }), + fileName: '9-claves' + }, + { + name: formatMessage({ + id: 'music.drumWoodBlock', + default: '(10) Wood Block', + description: 'Sound of a wood block being struck' + }), + fileName: '10-wood-block' + }, + { + name: formatMessage({ + id: 'music.drumCowbell', + default: '(11) Cowbell', + description: 'Sound of a cowbell being struck' + }), + fileName: '11-cowbell' + }, + { + name: formatMessage({ + id: 'music.drumTriangle', + default: '(12) Triangle', + description: 'Sound of a triangle (instrument) being struck' + }), + fileName: '12-triangle' + }, + { + name: formatMessage({ + id: 'music.drumBongo', + default: '(13) Bongo', + description: 'Sound of a bongo being struck' + }), + fileName: '13-bongo' + }, + { + name: formatMessage({ + id: 'music.drumConga', + default: '(14) Conga', + description: 'Sound of a conga being struck' + }), + fileName: '14-conga' + }, + { + name: formatMessage({ + id: 'music.drumCabasa', + default: '(15) Cabasa', + description: 'Sound of a cabasa being shaken' + }), + fileName: '15-cabasa' + }, + { + name: formatMessage({ + id: 'music.drumGuiro', + default: '(16) Guiro', + description: 'Sound of a guiro being played' + }), + fileName: '16-guiro' + }, + { + name: formatMessage({ + id: 'music.drumVibraslap', + default: '(17) Vibraslap', + description: 'Sound of a Vibraslap being played' + }), + fileName: '17-vibraslap' + }, + { + name: formatMessage({ + id: 'music.drumCuica', + default: '(18) Cuica', + description: 'Sound of a cuica being played' + }), + fileName: '18-cuica' + } + ]; + } + + /** + * An array of info about each instrument. + * @type {object[]} + * @param {string} name - the translatable name to display in the instruments menu. + * @param {string} dirName - the name of the directory containing audio samples for this instrument. + * @param {number} [releaseTime] - an optional duration for the release portion of each note. + * @param {number[]} samples - an array of numbers representing the MIDI note number for each + * sampled sound used to play this instrument. + */ + get INSTRUMENT_INFO () { + return [ + { + name: formatMessage({ + id: 'music.instrumentPiano', + default: '(1) Piano', + description: 'Sound of a piano' + }), + dirName: '1-piano', + releaseTime: 0.5, + samples: [24, 36, 48, 60, 72, 84, 96, 108] + }, + { + name: formatMessage({ + id: 'music.instrumentElectricPiano', + default: '(2) Electric Piano', + description: 'Sound of an electric piano' + }), + dirName: '2-electric-piano', + releaseTime: 0.5, + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentOrgan', + default: '(3) Organ', + description: 'Sound of an organ' + }), + dirName: '3-organ', + releaseTime: 0.5, + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentGuitar', + default: '(4) Guitar', + description: 'Sound of an accoustic guitar' + }), + dirName: '4-guitar', + releaseTime: 0.5, + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentElectricGuitar', + default: '(5) Electric Guitar', + description: 'Sound of an electric guitar' + }), + dirName: '5-electric-guitar', + releaseTime: 0.5, + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentBass', + default: '(6) Bass', + description: 'Sound of an accoustic upright bass' + }), + dirName: '6-bass', + releaseTime: 0.25, + samples: [36, 48] + }, + { + name: formatMessage({ + id: 'music.instrumentPizzicato', + default: '(7) Pizzicato', + description: 'Sound of a string instrument (e.g. violin) being plucked' + }), + dirName: '7-pizzicato', + releaseTime: 0.25, + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentCello', + default: '(8) Cello', + description: 'Sound of a cello being played with a bow' + }), + dirName: '8-cello', + releaseTime: 0.1, + samples: [36, 48, 60] + }, + { + name: formatMessage({ + id: 'music.instrumentTrombone', + default: '(9) Trombone', + description: 'Sound of a trombone being played' + }), + dirName: '9-trombone', + samples: [36, 48, 60] + }, + { + name: formatMessage({ + id: 'music.instrumentClarinet', + default: '(10) Clarinet', + description: 'Sound of a clarinet being played' + }), + dirName: '10-clarinet', + samples: [48, 60] + }, + { + name: formatMessage({ + id: 'music.instrumentSaxophone', + default: '(11) Saxophone', + description: 'Sound of a saxophone being played' + }), + dirName: '11-saxophone', + samples: [36, 60, 84] + }, + { + name: formatMessage({ + id: 'music.instrumentFlute', + default: '(12) Flute', + description: 'Sound of a flute being played' + }), + dirName: '12-flute', + samples: [60, 72] + }, + { + name: formatMessage({ + id: 'music.instrumentWoodenFlute', + default: '(13) Wooden Flute', + description: 'Sound of a wooden flute being played' + }), + dirName: '13-wooden-flute', + samples: [60, 72] + }, + { + name: formatMessage({ + id: 'music.instrumentBassoon', + default: '(14) Bassoon', + description: 'Sound of a bassoon being played' + }), + dirName: '14-bassoon', + samples: [36, 48, 60] + }, + { + name: formatMessage({ + id: 'music.instrumentChoir', + default: '(15) Choir', + description: 'Sound of a choir singing' + }), + dirName: '15-choir', + releaseTime: 0.25, + samples: [48, 60, 72] + }, + { + name: formatMessage({ + id: 'music.instrumentVibraphone', + default: '(16) Vibraphone', + description: 'Sound of a vibraphone being struck' + }), + dirName: '16-vibraphone', + releaseTime: 0.5, + samples: [60, 72] + }, + { + name: formatMessage({ + id: 'music.instrumentMusicBox', + default: '(17) Music Box', + description: 'Sound of a music box playing' + }), + dirName: '17-music-box', + releaseTime: 0.25, + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentSteelDrum', + default: '(18) Steel Drum', + description: 'Sound of a steel drum being struck' + }), + dirName: '18-steel-drum', + releaseTime: 0.5, + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentMarimba', + default: '(19) Marimba', + description: 'Sound of a marimba being struck' + }), + dirName: '19-marimba', + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentSynthLead', + default: '(20) Synth Lead', + description: 'Sound of a "lead" synthesizer being played' + }), + dirName: '20-synth-lead', + releaseTime: 0.1, + samples: [60] + }, + { + name: formatMessage({ + id: 'music.instrumentSynthPad', + default: '(21) Synth Pad', + description: 'Sound of a "pad" synthesizer being played' + }), + dirName: '21-synth-pad', + releaseTime: 0.25, + samples: [60] + } + ]; + } + + /** + * An array that is a mapping from MIDI instrument numbers to Scratch instrument numbers. + * @type {number[]} + */ + get MIDI_INSTRUMENTS () { + return [ + // Acoustic Grand, Bright Acoustic, Electric Grand, Honky-Tonk + 1, 1, 1, 1, + // Electric Piano 1, Electric Piano 2, Harpsichord, Clavinet + 2, 2, 4, 4, + // Celesta, Glockenspiel, Music Box, Vibraphone + 17, 17, 17, 16, + // Marimba, Xylophone, Tubular Bells, Dulcimer + 19, 16, 17, 17, + // Drawbar Organ, Percussive Organ, Rock Organ, Church Organ + 3, 3, 3, 3, + // Reed Organ, Accordion, Harmonica, Tango Accordion + 3, 3, 3, 3, + // Nylon String Guitar, Steel String Guitar, Electric Jazz Guitar, Electric Clean Guitar + 4, 4, 5, 5, + // Electric Muted Guitar, Overdriven Guitar,Distortion Guitar, Guitar Harmonics + 5, 5, 5, 5, + // Acoustic Bass, Electric Bass (finger), Electric Bass (pick), Fretless Bass + 6, 6, 6, 6, + // Slap Bass 1, Slap Bass 2, Synth Bass 1, Synth Bass 2 + 6, 6, 6, 6, + // Violin, Viola, Cello, Contrabass + 8, 8, 8, 8, + // Tremolo Strings, Pizzicato Strings, Orchestral Strings, Timpani + 8, 7, 8, 19, + // String Ensemble 1, String Ensemble 2, SynthStrings 1, SynthStrings 2 + 8, 8, 8, 8, + // Choir Aahs, Voice Oohs, Synth Voice, Orchestra Hit + 15, 15, 15, 19, + // Trumpet, Trombone, Tuba, Muted Trumpet + 9, 9, 9, 9, + // French Horn, Brass Section, SynthBrass 1, SynthBrass 2 + 9, 9, 9, 9, + // Soprano Sax, Alto Sax, Tenor Sax, Baritone Sax + 11, 11, 11, 11, + // Oboe, English Horn, Bassoon, Clarinet + 14, 14, 14, 10, + // Piccolo, Flute, Recorder, Pan Flute + 12, 12, 13, 13, + // Blown Bottle, Shakuhachi, Whistle, Ocarina + 13, 13, 12, 12, + // Lead 1 (square), Lead 2 (sawtooth), Lead 3 (calliope), Lead 4 (chiff) + 20, 20, 20, 20, + // Lead 5 (charang), Lead 6 (voice), Lead 7 (fifths), Lead 8 (bass+lead) + 20, 20, 20, 20, + // Pad 1 (new age), Pad 2 (warm), Pad 3 (polysynth), Pad 4 (choir) + 21, 21, 21, 21, + // Pad 5 (bowed), Pad 6 (metallic), Pad 7 (halo), Pad 8 (sweep) + 21, 21, 21, 21, + // FX 1 (rain), FX 2 (soundtrack), FX 3 (crystal), FX 4 (atmosphere) + 21, 21, 21, 21, + // FX 5 (brightness), FX 6 (goblins), FX 7 (echoes), FX 8 (sci-fi) + 21, 21, 21, 21, + // Sitar, Banjo, Shamisen, Koto + 4, 4, 4, 4, + // Kalimba, Bagpipe, Fiddle, Shanai + 17, 14, 8, 10, + // Tinkle Bell, Agogo, Steel Drums, Woodblock + 17, 17, 18, 19, + // Taiko Drum, Melodic Tom, Synth Drum, Reverse Cymbal + 1, 1, 1, 1, + // Guitar Fret Noise, Breath Noise, Seashore, Bird Tweet + 21, 21, 21, 21, + // Telephone Ring, Helicopter, Applause, Gunshot + 21, 21, 21, 21 + ]; + } + + /** + * An array that is a mapping from MIDI drum numbers in range (35..81) to Scratch drum numbers. + * It's in the format [drumNum, pitch, decay]. + * The pitch and decay properties are not currently being used. + * @type {Array[]} + */ + get MIDI_DRUMS () { + return [ + [1, -4], // "BassDrum" in 2.0, "Bass Drum" in 3.0 (which was "Tom" in 2.0) + [1, 0], // Same as just above + [2, 0], + [0, 0], + [7, 0], + [0, 2], + [1, -6, 4], + [5, 0], + [1, -3, 3.2], + [5, 0], // "HiHatPedal" in 2.0, "Closed Hi-Hat" in 3.0 + [1, 0, 3], + [4, -8], + [1, 4, 3], + [1, 7, 2.7], + [3, -8], + [1, 10, 2.7], + [4, -2], + [3, -11], + [4, 2], + [6, 0], + [3, 0, 3.5], + [10, 0], + [3, -8, 3.5], + [16, -6], + [4, 2], + [12, 2], + [12, 0], + [13, 0, 0.2], + [13, 0, 2], + [13, -5, 2], + [12, 12], + [12, 5], + [10, 19], + [10, 12], + [14, 0], + [14, 0], // "Maracas" in 2.0, "Cabasa" in 3.0 (TODO: pitch up?) + [17, 12], + [17, 5], + [15, 0], // "GuiroShort" in 2.0, "Guiro" in 3.0 (which was "GuiroLong" in 2.0) (TODO: decay?) + [15, 0], + [8, 0], + [9, 0], + [9, -4], + [17, -5], + [17, 0], + [11, -6, 1], + [11, -6, 3] + ]; + } + + /** + * The key to load & store a target's music-related state. + * @type {string} + */ + static get STATE_KEY () { + return 'Scratch.music'; + } + + /** + * The default music-related state, to be used when a target has no existing music state. + * @type {MusicState} + */ + static get DEFAULT_MUSIC_STATE () { + return { + currentInstrument: 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: 0, max: 130}; + } + + /** + * 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 maximum number of sounds to allow to play simultaneously. + * @type {number} + */ + static get CONCURRENCY_LIMIT () { + return 30; + } + + /** + * @param {Target} target - collect music state for this target. + * @returns {MusicState} the mutable music state associated with that target. This will be created if necessary. + * @private + */ + _getMusicState (target) { + let musicState = target.getCustomState(Scratch3MusicBlocks.STATE_KEY); + if (!musicState) { + musicState = Clone.simple(Scratch3MusicBlocks.DEFAULT_MUSIC_STATE); + target.setCustomState(Scratch3MusicBlocks.STATE_KEY, musicState); + } + return musicState; + } + + /** + * When a music-playing Target is cloned, clone the music 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 musicState = sourceTarget.getCustomState(Scratch3MusicBlocks.STATE_KEY); + if (musicState) { + newTarget.setCustomState(Scratch3MusicBlocks.STATE_KEY, Clone.simple(musicState)); + } + } + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'music', + name: formatMessage({ + id: 'music.categoryName', + default: 'Music', + description: 'Label for the Music extension category' + }), + menuIconURI: menuIconURI, + blockIconURI: blockIconURI, + blocks: [ + { + opcode: 'playDrumForBeats', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'music.playDrumForBeats', + default: 'play drum [DRUM] for [BEATS] beats', + description: 'play drum sample for a number of beats' + }), + arguments: { + DRUM: { + type: ArgumentType.NUMBER, + menu: 'DRUM', + defaultValue: 1 + }, + BEATS: { + type: ArgumentType.NUMBER, + defaultValue: 0.25 + } + } + }, + { + opcode: 'midiPlayDrumForBeats', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'music.midiPlayDrumForBeats', + default: 'play drum [DRUM] for [BEATS] beats', + description: 'play drum sample for a number of beats according to a mapping of MIDI codes' + }), + arguments: { + DRUM: { + type: ArgumentType.NUMBER, + menu: 'DRUM', + defaultValue: 1 + }, + BEATS: { + type: ArgumentType.NUMBER, + defaultValue: 0.25 + } + }, + hideFromPalette: true + }, + { + opcode: 'restForBeats', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'music.restForBeats', + default: 'rest for [BEATS] beats', + description: 'rest (play no sound) for a number of beats' + }), + arguments: { + BEATS: { + type: ArgumentType.NUMBER, + defaultValue: 0.25 + } + } + }, + { + opcode: 'playNoteForBeats', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'music.playNoteForBeats', + default: 'play note [NOTE] for [BEATS] beats', + description: 'play a note for a number of beats' + }), + arguments: { + NOTE: { + type: ArgumentType.NOTE, + defaultValue: 60 + }, + BEATS: { + type: ArgumentType.NUMBER, + defaultValue: 0.25 + } + } + }, + { + opcode: 'setInstrument', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'music.setInstrument', + default: 'set instrument to [INSTRUMENT]', + description: 'set the instrument (e.g. piano, guitar, trombone) for notes played' + }), + arguments: { + INSTRUMENT: { + type: ArgumentType.NUMBER, + menu: 'INSTRUMENT', + defaultValue: 1 + } + } + }, + { + opcode: 'midiSetInstrument', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'music.midiSetInstrument', + default: 'set instrument to [INSTRUMENT]', + description: 'set the instrument for notes played according to a mapping of MIDI codes' + }), + arguments: { + INSTRUMENT: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + hideFromPalette: true + }, + { + opcode: 'setTempo', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'music.setTempo', + default: 'set tempo to [TEMPO]', + description: 'set tempo (speed) for notes, drums, and rests played' + }), + arguments: { + TEMPO: { + type: ArgumentType.NUMBER, + defaultValue: 60 + } + } + }, + { + opcode: 'changeTempo', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'music.changeTempo', + default: 'change tempo by [TEMPO]', + description: 'change tempo (speed) for notes, drums, and rests played' + }), + arguments: { + TEMPO: { + type: ArgumentType.NUMBER, + defaultValue: 20 + } + } + }, + { + opcode: 'getTempo', + text: formatMessage({ + id: 'music.getTempo', + default: 'tempo', + description: 'get the current tempo (speed) for notes, drums, and rests played' + }), + blockType: BlockType.REPORTER + } + ], + menus: { + DRUM: { + acceptReporters: true, + items: this._buildMenu(this.DRUM_INFO) + }, + INSTRUMENT: { + acceptReporters: true, + items: this._buildMenu(this.INSTRUMENT_INFO) + } + } + }; + } + + /** + * Play a drum sound for some number of beats. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {int} DRUM - the number of the drum to play. + * @property {number} BEATS - the duration in beats of the drum sound. + */ + playDrumForBeats (args, util) { + this._playDrumForBeats(args.DRUM, args.BEATS, util); + } + + /** + * Play a drum sound for some number of beats according to the range of "MIDI" drum codes supported. + * This block is implemented for compatibility with old Scratch projects that use the + * 'drum:duration:elapsed:from:' block. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + */ + midiPlayDrumForBeats (args, util) { + let drumNum = Cast.toNumber(args.DRUM); + drumNum = Math.round(drumNum); + const midiDescription = this.MIDI_DRUMS[drumNum - 35]; + if (midiDescription) { + drumNum = midiDescription[0]; + } else { + drumNum = 2; // Default instrument used in Scratch 2.0 + } + drumNum += 1; // drumNum input to _playDrumForBeats is one-indexed + this._playDrumForBeats(drumNum, args.BEATS, util); + } + + /** + * Internal code to play a drum sound for some number of beats. + * @param {number} drumNum - the drum number. + * @param {beats} beats - the duration in beats to pause after playing the sound. + * @param {object} util - utility object provided by the runtime. + */ + _playDrumForBeats (drumNum, beats, util) { + if (this._stackTimerNeedsInit(util)) { + drumNum = Cast.toNumber(drumNum); + drumNum = Math.round(drumNum); + drumNum -= 1; // drums are one-indexed + drumNum = MathUtil.wrapClamp(drumNum, 0, this.DRUM_INFO.length - 1); + beats = Cast.toNumber(beats); + beats = this._clampBeats(beats); + this._playDrumNum(util, drumNum); + this._startStackTimer(util, this._beatsToSec(beats)); + } else { + this._checkStackTimer(util); + } + } + + /** + * Play a drum sound using its 0-indexed number. + * @param {object} util - utility object provided by the runtime. + * @param {number} drumNum - the number of the drum to play. + * @private + */ + _playDrumNum (util, drumNum) { + if (util.runtime.audioEngine === null) return; + if (util.target.sprite.soundBank === null) return; + // If we're playing too many sounds, do not play the drum sound. + if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) { + return; + } + + const player = this._drumPlayers[drumNum]; + + if (typeof player === 'undefined') return; + + if (player.isPlaying && !player.isStarting) { + // Take the internal player state and create a new player with it. + // `.play` does this internally but then instructs the sound to + // stop. + player.take(); + } + + const engine = util.runtime.audioEngine; + const context = engine.audioContext; + const volumeGain = context.createGain(); + volumeGain.gain.setValueAtTime(util.target.volume / 100, engine.currentTime); + volumeGain.connect(engine.getInputNode()); + + this._concurrencyCounter++; + player.once('stop', () => { + this._concurrencyCounter--; + }); + + player.play(); + // Connect the player to the gain node. + player.connect({getInputNode () { + return volumeGain; + }}); + } + + /** + * Rest for some number of beats. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {number} BEATS - the duration in beats of the rest. + */ + restForBeats (args, util) { + if (this._stackTimerNeedsInit(util)) { + let beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); + this._startStackTimer(util, this._beatsToSec(beats)); + } else { + this._checkStackTimer(util); + } + } + + /** + * Play a note using the current musical instrument for some number of beats. + * This function processes the arguments, and handles the timing of the block's execution. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {number} NOTE - the pitch of the note to play, interpreted as a MIDI note number. + * @property {number} BEATS - the duration in beats of the note. + */ + playNoteForBeats (args, util) { + if (this._stackTimerNeedsInit(util)) { + let note = Cast.toNumber(args.NOTE); + note = MathUtil.clamp(note, + Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max); + let beats = Cast.toNumber(args.BEATS); + beats = this._clampBeats(beats); + // If the duration is 0, do not play the note. In Scratch 2.0, "play drum for 0 beats" plays the drum, + // but "play note for 0 beats" is silent. + if (beats === 0) return; + + const durationSec = this._beatsToSec(beats); + + this._playNote(util, note, durationSec); + + this._startStackTimer(util, durationSec); + } else { + this._checkStackTimer(util); + } + } + + _playNoteForPicker (noteNum, category) { + if (category !== this.getInfo().name) return; + const util = { + runtime: this.runtime, + target: this.runtime.getEditingTarget() + }; + this._playNote(util, noteNum, 0.25); + } + + /** + * Play a note using the current instrument for a duration in seconds. + * This function actually plays the sound, and handles the timing of the sound, including the + * "release" portion of the sound, which continues briefly after the block execution has finished. + * @param {object} util - utility object provided by the runtime. + * @param {number} note - the pitch of the note to play, interpreted as a MIDI note number. + * @param {number} durationSec - the duration in seconds to play the note. + * @private + */ + _playNote (util, note, durationSec) { + if (util.runtime.audioEngine === null) return; + if (util.target.sprite.soundBank === null) return; + + // If we're playing too many sounds, do not play the note. + if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) { + return; + } + + // Determine which of the audio samples for this instrument to play + const musicState = this._getMusicState(util.target); + const inst = musicState.currentInstrument; + const instrumentInfo = this.INSTRUMENT_INFO[inst]; + const sampleArray = instrumentInfo.samples; + const sampleIndex = this._selectSampleIndexForNote(note, sampleArray); + + // If the audio sample has not loaded yet, bail out + if (typeof this._instrumentPlayerArrays[inst] === 'undefined') return; + if (typeof this._instrumentPlayerArrays[inst][sampleIndex] === 'undefined') return; + + // Fetch the sound player to play the note. + const engine = util.runtime.audioEngine; + + if (!this._instrumentPlayerNoteArrays[inst][note]) { + this._instrumentPlayerNoteArrays[inst][note] = this._instrumentPlayerArrays[inst][sampleIndex].take(); + } + + const player = this._instrumentPlayerNoteArrays[inst][note]; + + if (player.isPlaying && !player.isStarting) { + // Take the internal player state and create a new player with it. + // `.play` does this internally but then instructs the sound to + // stop. + player.take(); + } + + // Set its pitch. + const sampleNote = sampleArray[sampleIndex]; + const notePitchInterval = this._ratioForPitchInterval(note - sampleNote); + + // Create gain nodes for this note's volume and release, and chain them + // to the output. + const context = engine.audioContext; + const volumeGain = context.createGain(); + volumeGain.gain.setValueAtTime(util.target.volume / 100, engine.currentTime); + const releaseGain = context.createGain(); + volumeGain.connect(releaseGain); + releaseGain.connect(engine.getInputNode()); + + // Schedule the release of the note, ramping its gain down to zero, + // and then stopping the sound. + let releaseDuration = this.INSTRUMENT_INFO[inst].releaseTime; + if (typeof releaseDuration === 'undefined') { + releaseDuration = 0.01; + } + const releaseStart = context.currentTime + durationSec; + const releaseEnd = releaseStart + releaseDuration; + releaseGain.gain.setValueAtTime(1, releaseStart); + releaseGain.gain.linearRampToValueAtTime(0.0001, releaseEnd); + + this._concurrencyCounter++; + player.once('stop', () => { + this._concurrencyCounter--; + }); + + // Start playing the note + player.play(); + // Connect the player to the gain node. + player.connect({getInputNode () { + return volumeGain; + }}); + // Set playback now after play creates the outputNode. + player.outputNode.playbackRate.value = notePitchInterval; + // Schedule playback to stop. + player.outputNode.stop(releaseEnd); + } + + /** + * The samples array for each instrument is the set of pitches of the available audio samples. + * This function selects the best one to use to play a given input note, and returns its index + * in the samples array. + * @param {number} note - the input note to select a sample for. + * @param {number[]} samples - an array of the pitches of the available samples. + * @return {index} the index of the selected sample in the samples array. + * @private + */ + _selectSampleIndexForNote (note, samples) { + // Step backwards through the array of samples, i.e. in descending pitch, in order to find + // the sample that is the closest one below (or matching) the pitch of the input note. + for (let i = samples.length - 1; i >= 0; i--) { + if (note >= samples[i]) { + return i; + } + } + return 0; + } + + /** + * Calcuate the frequency ratio for a given musical interval. + * @param {number} interval - the pitch interval to convert. + * @return {number} a ratio corresponding to the input interval. + * @private + */ + _ratioForPitchInterval (interval) { + return Math.pow(2, (interval / 12)); + } + + /** + * Clamp a duration in beats to the allowed min and max duration. + * @param {number} beats - a duration in beats. + * @return {number} - the clamped duration. + * @private + */ + _clampBeats (beats) { + return MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max); + } + + /** + * Convert a number of beats to a number of seconds, using the current tempo. + * @param {number} beats - number of beats to convert to secs. + * @return {number} seconds - number of seconds `beats` will last. + * @private + */ + _beatsToSec (beats) { + return (60 / this.getTempo()) * beats; + } + + /** + * Check if the stack timer needs initialization. + * @param {object} util - utility object provided by the runtime. + * @return {boolean} - true if the stack timer needs to be initialized. + * @private + */ + _stackTimerNeedsInit (util) { + return !util.stackFrame.timer; + } + + /** + * Start the stack timer and the yield the thread if necessary. + * @param {object} util - utility object provided by the runtime. + * @param {number} duration - a duration in seconds to set the timer for. + * @private + */ + _startStackTimer (util, duration) { + util.stackFrame.timer = new Timer(); + util.stackFrame.timer.start(); + util.stackFrame.duration = duration; + util.yield(); + } + + /** + * Check the stack timer, and if its time is not up yet, yield the thread. + * @param {object} util - utility object provided by the runtime. + * @private + */ + _checkStackTimer (util) { + const timeElapsed = util.stackFrame.timer.timeElapsed(); + if (timeElapsed < util.stackFrame.duration * 1000) { + util.yield(); + } + } + + /** + * Select an instrument for playing notes. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {int} INSTRUMENT - the number of the instrument to select. + */ + setInstrument (args, util) { + this._setInstrument(args.INSTRUMENT, util, false); + } + + /** + * Select an instrument for playing notes according to a mapping of MIDI codes to Scratch instrument numbers. + * This block is implemented for compatibility with old Scratch projects that use the 'midiInstrument:' block. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + * @property {int} INSTRUMENT - the MIDI number of the instrument to select. + */ + midiSetInstrument (args, util) { + this._setInstrument(args.INSTRUMENT, util, true); + } + + /** + * Internal code to select an instrument for playing notes. If mapMidi is true, set the instrument according to + * the MIDI to Scratch instrument mapping. + * @param {number} instNum - the instrument number. + * @param {object} util - utility object provided by the runtime. + * @param {boolean} mapMidi - whether or not instNum is a MIDI instrument number. + */ + _setInstrument (instNum, util, mapMidi) { + const musicState = this._getMusicState(util.target); + instNum = Cast.toNumber(instNum); + instNum = Math.round(instNum); + instNum -= 1; // instruments are one-indexed + if (mapMidi) { + instNum = (this.MIDI_INSTRUMENTS[instNum] || 0) - 1; + } + instNum = MathUtil.wrapClamp(instNum, 0, this.INSTRUMENT_INFO.length - 1); + musicState.currentInstrument = instNum; + } + + /** + * Set the current tempo to a new value. + * @param {object} args - the block arguments. + * @property {number} TEMPO - the tempo, in beats per minute. + */ + setTempo (args) { + const tempo = Cast.toNumber(args.TEMPO); + this._updateTempo(tempo); + } + + /** + * Change the current tempo by some amount. + * @param {object} args - the block arguments. + * @property {number} TEMPO - the amount to change the tempo, in beats per minute. + */ + changeTempo (args) { + const change = Cast.toNumber(args.TEMPO); + const tempo = change + this.getTempo(); + this._updateTempo(tempo); + } + + /** + * Update the current tempo, clamping it to the min and max allowable range. + * @param {number} tempo - the tempo to set, in beats per minute. + * @private + */ + _updateTempo (tempo) { + tempo = MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max); + const stage = this.runtime.getTargetForStage(); + if (stage) { + stage.tempo = tempo; + } + } + + /** + * Get the current tempo. + * @return {number} - the current tempo, in beats per minute. + */ + getTempo () { + const stage = this.runtime.getTargetForStage(); + if (stage) { + return stage.tempo; + } + return 60; + } +} + +module.exports = Scratch3MusicBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_music/manifest.js b/local-scratch-vm/src/extensions/scratch3_music/manifest.js new file mode 100644 index 0000000000000000000000000000000000000000..e2c6261d740ead80c3169c608e1de09e8eb8349c --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_music/manifest.js @@ -0,0 +1,63 @@ +module.exports = { + 'drums/1-snare.mp3': require('./assets/drums/1-snare.mp3'), + 'drums/2-bass-drum.mp3': require('./assets/drums/2-bass-drum.mp3'), + 'drums/3-side-stick.mp3': require('./assets/drums/3-side-stick.mp3'), + 'drums/4-crash-cymbal.mp3': require('./assets/drums/4-crash-cymbal.mp3'), + 'drums/5-open-hi-hat.mp3': require('./assets/drums/5-open-hi-hat.mp3'), + 'drums/6-closed-hi-hat.mp3': require('./assets/drums/6-closed-hi-hat.mp3'), + 'drums/7-tambourine.mp3': require('./assets/drums/7-tambourine.mp3'), + 'drums/8-hand-clap.mp3': require('./assets/drums/8-hand-clap.mp3'), + 'drums/9-claves.mp3': require('./assets/drums/9-claves.mp3'), + 'drums/10-wood-block.mp3': require('./assets/drums/10-wood-block.mp3'), + 'drums/11-cowbell.mp3': require('./assets/drums/11-cowbell.mp3'), + 'drums/12-triangle.mp3': require('./assets/drums/12-triangle.mp3'), + 'drums/13-bongo.mp3': require('./assets/drums/13-bongo.mp3'), + 'drums/14-conga.mp3': require('./assets/drums/14-conga.mp3'), + 'drums/15-cabasa.mp3': require('./assets/drums/15-cabasa.mp3'), + 'drums/16-guiro.mp3': require('./assets/drums/16-guiro.mp3'), + 'drums/17-vibraslap.mp3': require('./assets/drums/17-vibraslap.mp3'), + 'drums/18-cuica.mp3': require('./assets/drums/18-cuica.mp3'), + 'instruments/1-piano/24.mp3': require('./assets/instruments/1-piano/24.mp3'), + 'instruments/1-piano/36.mp3': require('./assets/instruments/1-piano/36.mp3'), + 'instruments/1-piano/48.mp3': require('./assets/instruments/1-piano/48.mp3'), + 'instruments/1-piano/60.mp3': require('./assets/instruments/1-piano/60.mp3'), + 'instruments/1-piano/72.mp3': require('./assets/instruments/1-piano/72.mp3'), + 'instruments/1-piano/84.mp3': require('./assets/instruments/1-piano/84.mp3'), + 'instruments/1-piano/96.mp3': require('./assets/instruments/1-piano/96.mp3'), + 'instruments/1-piano/108.mp3': require('./assets/instruments/1-piano/108.mp3'), + 'instruments/2-electric-piano/60.mp3': require('./assets/instruments/2-electric-piano/60.mp3'), + 'instruments/3-organ/60.mp3': require('./assets/instruments/3-organ/60.mp3'), + 'instruments/4-guitar/60.mp3': require('./assets/instruments/4-guitar/60.mp3'), + 'instruments/5-electric-guitar/60.mp3': require('./assets/instruments/5-electric-guitar/60.mp3'), + 'instruments/6-bass/36.mp3': require('./assets/instruments/6-bass/36.mp3'), + 'instruments/6-bass/48.mp3': require('./assets/instruments/6-bass/48.mp3'), + 'instruments/7-pizzicato/60.mp3': require('./assets/instruments/7-pizzicato/60.mp3'), + 'instruments/8-cello/36.mp3': require('./assets/instruments/8-cello/36.mp3'), + 'instruments/8-cello/48.mp3': require('./assets/instruments/8-cello/48.mp3'), + 'instruments/8-cello/60.mp3': require('./assets/instruments/8-cello/60.mp3'), + 'instruments/9-trombone/36.mp3': require('./assets/instruments/9-trombone/36.mp3'), + 'instruments/9-trombone/48.mp3': require('./assets/instruments/9-trombone/48.mp3'), + 'instruments/9-trombone/60.mp3': require('./assets/instruments/9-trombone/60.mp3'), + 'instruments/10-clarinet/48.mp3': require('./assets/instruments/10-clarinet/48.mp3'), + 'instruments/10-clarinet/60.mp3': require('./assets/instruments/10-clarinet/60.mp3'), + 'instruments/11-saxophone/36.mp3': require('./assets/instruments/11-saxophone/36.mp3'), + 'instruments/11-saxophone/60.mp3': require('./assets/instruments/11-saxophone/60.mp3'), + 'instruments/11-saxophone/84.mp3': require('./assets/instruments/11-saxophone/84.mp3'), + 'instruments/12-flute/60.mp3': require('./assets/instruments/12-flute/60.mp3'), + 'instruments/12-flute/72.mp3': require('./assets/instruments/12-flute/72.mp3'), + 'instruments/13-wooden-flute/60.mp3': require('./assets/instruments/13-wooden-flute/60.mp3'), + 'instruments/13-wooden-flute/72.mp3': require('./assets/instruments/13-wooden-flute/72.mp3'), + 'instruments/14-bassoon/36.mp3': require('./assets/instruments/14-bassoon/36.mp3'), + 'instruments/14-bassoon/48.mp3': require('./assets/instruments/14-bassoon/48.mp3'), + 'instruments/14-bassoon/60.mp3': require('./assets/instruments/14-bassoon/60.mp3'), + 'instruments/15-choir/48.mp3': require('./assets/instruments/15-choir/48.mp3'), + 'instruments/15-choir/60.mp3': require('./assets/instruments/15-choir/60.mp3'), + 'instruments/15-choir/72.mp3': require('./assets/instruments/15-choir/72.mp3'), + 'instruments/16-vibraphone/60.mp3': require('./assets/instruments/16-vibraphone/60.mp3'), + 'instruments/16-vibraphone/72.mp3': require('./assets/instruments/16-vibraphone/72.mp3'), + 'instruments/17-music-box/60.mp3': require('./assets/instruments/17-music-box/60.mp3'), + 'instruments/18-steel-drum/60.mp3': require('./assets/instruments/18-steel-drum/60.mp3'), + 'instruments/19-marimba/60.mp3': require('./assets/instruments/19-marimba/60.mp3'), + 'instruments/20-synth-lead/60.mp3': require('./assets/instruments/20-synth-lead/60.mp3'), + 'instruments/21-synth-pad/60.mp3': require('./assets/instruments/21-synth-pad/60.mp3') +}; diff --git a/local-scratch-vm/src/extensions/scratch3_pen/index.js b/local-scratch-vm/src/extensions/scratch3_pen/index.js new file mode 100644 index 0000000000000000000000000000000000000000..06824146ced704f445b9729f27649eb7047d7e50 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_pen/index.js @@ -0,0 +1,1644 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const TargetType = require('../../extension-support/target-type'); +const Cast = require('../../util/cast'); +const Clone = require('../../util/clone'); +const Color = require('../../util/color'); +const { translateForCamera } = require('../../util/pos-math'); +const formatMessage = require('format-message'); +const MathUtil = require('../../util/math-util'); +const log = require('../../util/log'); +const StageLayering = require('../../engine/stage-layering'); + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+cGVuLWljb248L3RpdGxlPjxnIHN0cm9rZT0iIzU3NUU3NSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik04Ljc1MyAzNC42MDJsLTQuMjUgMS43OCAxLjc4My00LjIzN2MxLjIxOC0yLjg5MiAyLjkwNy01LjQyMyA1LjAzLTcuNTM4TDMxLjA2NiA0LjkzYy44NDYtLjg0MiAyLjY1LS40MSA0LjAzMi45NjcgMS4zOCAxLjM3NSAxLjgxNiAzLjE3My45NyA0LjAxNUwxNi4zMTggMjkuNTljLTIuMTIzIDIuMTE2LTQuNjY0IDMuOC03LjU2NSA1LjAxMiIgZmlsbD0iI0ZGRiIvPjxwYXRoIGQ9Ik0yOS40MSA2LjExcy00LjQ1LTIuMzc4LTguMjAyIDUuNzcyYy0xLjczNCAzLjc2Ni00LjM1IDEuNTQ2LTQuMzUgMS41NDYiLz48cGF0aCBkPSJNMzYuNDIgOC44MjVjMCAuNDYzLS4xNC44NzMtLjQzMiAxLjE2NGwtOS4zMzUgOS4zYy4yODItLjI5LjQxLS42NjguNDEtMS4xMiAwLS44NzQtLjUwNy0xLjk2My0xLjQwNi0yLjg2OC0xLjM2Mi0xLjM1OC0zLjE0Ny0xLjgtNC4wMDItLjk5TDMwLjk5IDUuMDFjLjg0NC0uODQgMi42NS0uNDEgNC4wMzUuOTYuODk4LjkwNCAxLjM5NiAxLjk4MiAxLjM5NiAyLjg1NU0xMC41MTUgMzMuNzc0Yy0uNTczLjMwMi0xLjE1Ny41Ny0xLjc2NC44M0w0LjUgMzYuMzgybDEuNzg2LTQuMjM1Yy4yNTgtLjYwNC41My0xLjE4Ni44MzMtMS43NTcuNjkuMTgzIDEuNDQ4LjYyNSAyLjEwOCAxLjI4Mi42Ni42NTggMS4xMDIgMS40MTIgMS4yODcgMi4xMDIiIGZpbGw9IiM0Qzk3RkYiLz48cGF0aCBkPSJNMzYuNDk4IDguNzQ4YzAgLjQ2NC0uMTQuODc0LS40MzMgMS4xNjVsLTE5Ljc0MiAxOS42OGMtMi4xMyAyLjExLTQuNjczIDMuNzkzLTcuNTcyIDUuMDFMNC41IDM2LjM4bC45NzQtMi4zMTYgMS45MjUtLjgwOGMyLjg5OC0xLjIxOCA1LjQ0LTIuOSA3LjU3LTUuMDFsMTkuNzQzLTE5LjY4Yy4yOTItLjI5Mi40MzItLjcwMi40MzItMS4xNjUgMC0uNjQ2LS4yNy0xLjQtLjc4LTIuMTIyLjI1LjE3Mi41LjM3Ny43MzcuNjE0Ljg5OC45MDUgMS4zOTYgMS45ODMgMS4zOTYgMi44NTYiIGZpbGw9IiM1NzVFNzUiIG9wYWNpdHk9Ii4xNSIvPjxwYXRoIGQ9Ik0xOC40NSAxMi44M2MwIC41LS40MDQuOTA1LS45MDQuOTA1cy0uOTA1LS40MDUtLjkwNS0uOTA0YzAtLjUuNDA3LS45MDMuOTA2LS45MDMuNSAwIC45MDQuNDA0LjkwNC45MDR6IiBmaWxsPSIjNTc1RTc1Ii8+PC9nPjwvc3ZnPg=='; + +// aka nothing because every image is way too big just like your mother +const DefaultDrawImage = 'data:image/png;base64,'; + +const SANS_SERIF_ID = 'Sans Serif'; +const SERIF_ID = 'Serif'; +const HANDWRITING_ID = 'Handwriting'; +const MARKER_ID = 'Marker'; +const CURLY_ID = 'Curly'; +const PIXEL_ID = 'Pixel'; + +/* PenguinMod Fonts */ +const PLAYFUL_ID = 'Playful'; +const BUBBLY_ID = 'Bubbly'; +const BITSANDBYTES_ID = 'Bits and Bytes'; +const TECHNOLOGICAL_ID = 'Technological'; +const ARCADE_ID = 'Arcade'; +const ARCHIVO_ID = 'Archivo'; +const ARCHIVOBLACK_ID = 'Archivo Black'; +const SCRATCH_ID = 'Scratch'; + +const RANDOM_ID = 'Random'; + +/** + * Enum for pen color parameter values. + * @readonly + * @enum {string} + */ +const ColorParam = { + COLOR: 'color', + SATURATION: 'saturation', + BRIGHTNESS: 'brightness', + TRANSPARENCY: 'transparency' +}; + +/** + * Enum for layer parameter values. + * @readonly + * @enum {string} + */ +const LayerParam = { + FRONT: 'front', + BACK: 'back' +}; + +const ItalicsParam = { + ON: 'on', + OFF: 'off' +}; + +/** + * Enum for layer parameter values. + * @readonly + * @enum {string} + */ +const LayerNames = { + 'front': StageLayering.PEN_LAYER, + 'back': StageLayering.SPRITE_LAYER +}; + +const parseArray = (string) => { + let array; + try { + array = JSON.parse(string); + } catch { + array = []; + } + if (!Array.isArray(array)) return []; + return array; +}; + +/** + * @typedef {object} PenState - the pen state associated with a particular target. + * @property {Boolean} penDown - tracks whether the pen should draw for this target. + * @property {number} color - the current color (hue) of the pen. + * @property {PenAttributes} penAttributes - cached pen attributes for the renderer. This is the authoritative value for + * diameter but not for pen color. + */ + +/** + * Host for the Pen-related blocks in Scratch 3.0 + * @param {Runtime} runtime - the runtime instantiating this block package. + * @constructor + */ +class Scratch3PenBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * The ID of the renderer Drawable corresponding to the pen layer. + * @type {int} + * @private + */ + this._penDrawableId = -1; + + /** + * The ID of the renderer Skin corresponding to the pen layer. + * @type {int} + * @private + */ + this._penSkinId = -1; + + /** + * The attribute of print text. + * @type {object} + */ + this.printTextAttribute = { + weight: '400', + italic: false, + size: '28', + font: 'Arial', + color: '#000000', + strokeColor: '#000000', + strokeWidth: 0 + }; + + this._onTargetCreated = this._onTargetCreated.bind(this); + this._onTargetMoved = this._onTargetMoved.bind(this); + this._onCameraMoved = this._onCameraMoved.bind(this); + + runtime.on('targetWasCreated', this._onTargetCreated); + runtime.on('RUNTIME_DISPOSED', this.clear.bind(this)); + // runtime.on('CAMERA_CHANGED', this._onCameraMoved); + + this.preloadedImages = {}; + + this.cameraBound = -1; + } + + /** + * The default pen state, to be used when a target has no existing pen state. + * @type {PenState} + */ + static get DEFAULT_PEN_STATE () { + return { + penDown: false, + color: 66.66, + saturation: 100, + brightness: 100, + transparency: 0, + _shade: 50, // Used only for legacy `change shade by` blocks + penAttributes: { + color4f: [0, 0, 1, 1], + diameter: 1 + } + }; + } + + + /** + * The minimum and maximum allowed pen size. + * The maximum is twice the diagonal of the stage, so that even an + * off-stage sprite can fill it. + * @type {{min: number, max: number}} + */ + static get PEN_SIZE_RANGE () { + return { min: 1, max: 1e308 }; + } + + /** + * The key to load & store a target's pen-related state. + * @type {string} + */ + static get STATE_KEY () { + // tw: We've hardcoded this value in various places for slight performance gains + // Make sure to update those if this changes. + return 'Scratch.pen'; + } + + /** + * Clamp a pen size value to the range allowed by the pen. + * @param {number} requestedSize - the requested pen size. + * @returns {number} the clamped size. + * @private + */ + _clampPenSize (requestedSize) { + if ( + (this.runtime.renderer && this.runtime.renderer.useHighQualityRender) || + !this.runtime.runtimeOptions.miscLimits + ) { + return Math.max(0, requestedSize); + } + return MathUtil.clamp( + requestedSize, + Scratch3PenBlocks.PEN_SIZE_RANGE.min, + Scratch3PenBlocks.PEN_SIZE_RANGE.max + ); + } + + /** + * Retrieve the ID of the renderer "Skin" corresponding to the pen layer. If + * the pen Skin doesn't yet exist, create it. + * @returns {int} the Skin ID of the pen layer, or -1 on failure. + * @private + */ + _getPenLayerID () { + if (this._penSkinId < 0 && this.runtime.renderer) { + this._penSkinId = this.runtime.renderer.createPenSkin(); + this._penDrawableId = this.runtime.renderer.createDrawable(StageLayering.PEN_LAYER); + this.runtime.renderer.updateDrawableSkinId(this._penDrawableId, this._penSkinId); + + this.bitmapCanvas = document.createElement('canvas'); + this.bitmapCanvas.width = this.runtime.stageWidth; + this.bitmapCanvas.height = this.runtime.stageHeight; + this.bitmapSkinID = this.runtime.renderer.createBitmapSkin(this.bitmapCanvas, 1); + this.bitmapDrawableID = this.runtime.renderer.createDrawable(StageLayering.PEN_LAYER); + this.runtime.renderer.updateDrawableSkinId(this.bitmapDrawableID, this.bitmapSkinID); + this.runtime.renderer.updateDrawableVisible(this.bitmapDrawableID, false); + } + return this._penSkinId; + } + + /** + * @param {Target} target - collect pen state for this target. Probably, but not necessarily, a RenderedTarget. + * @returns {PenState} the mutable pen state associated with that target. This will be created if necessary. + * @private + */ + _getPenState (target) { + let penState = target._customState['Scratch.pen']; + if (!penState) { + penState = Clone.simple(Scratch3PenBlocks.DEFAULT_PEN_STATE); + target.setCustomState(Scratch3PenBlocks.STATE_KEY, penState); + } + return penState; + } + + /** + * When a pen-using Target is cloned, clone the pen 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 penState = sourceTarget.getCustomState(Scratch3PenBlocks.STATE_KEY); + if (penState) { + newTarget.setCustomState(Scratch3PenBlocks.STATE_KEY, Clone.simple(penState)); + if (penState.penDown) { + newTarget.onTargetMoved = this._onTargetMoved; + } + } + } + } + + /** + * Handle a target which has moved. This only fires when the pen is down. + * @param {RenderedTarget} target - the target which has moved. + * @param {number} oldX - the previous X position. + * @param {number} oldY - the previous Y position. + * @param {boolean} isForce - whether the movement was forced. + * @private + */ + _onTargetMoved (target, oldX, oldY, isForce) { + // Only move the pen if the movement isn't forced (ie. dragged). + if (!isForce) { + const penSkinId = this._getPenLayerID(); + if (penSkinId >= 0) { + const penState = this._getPenState(target); + // find the rendered possition of the sprite rather then the true possition of the sprite + const [newX, newY] = target._translatePossitionToCamera(); + if (target.cameraBound >= 0) { + [oldX, oldY] = translateForCamera(this.runtime, target.cameraBound, oldX, oldY); + } + this.runtime.renderer.penLine(penSkinId, penState.penAttributes, oldX, oldY, newX, newY); + this.runtime.requestRedraw(); + } + } + } + + _onCameraMoved(screen) { + if (screen !== this.cameraBound) return; + const cameraState = this.runtime.cameraStates[screen]; + const penSkinId = this._getPenLayerID(); + if (penSkinId >= 0) { + this.runtime.renderer.penTranslate(penSkinId, ...cameraState.pos, cameraState.scale, cameraState.dir); + } + this.runtime.requestRedraw(); + } + + bindToCamera(screen) { + this.cameraBound = screen; + this._onCameraMoved(); + } + + removeCameraBinding() { + this.cameraBound = -1; + const penSkinId = this._getPenLayerID(); + if (penSkinId >= 0) { + this.runtime.renderer.penTranslate(penSkinId, 0, 0, 1, 0); + } + } + + /** + * Wrap a color input into the range (0,100). + * @param {number} value - the value to be wrapped. + * @returns {number} the wrapped value. + * @private + */ + _wrapColor (value) { + return MathUtil.wrapClamp(value, 0, 100); + } + + /** + * Initialize color parameters menu with localized strings + * @returns {array} of the localized text and values for each menu element + * @private + */ + _initColorParam () { + return [ + { + text: formatMessage({ + id: 'pen.colorMenu.color', + default: 'color', + description: 'label for color element in color picker for pen extension' + }), + value: ColorParam.COLOR + }, + { + text: formatMessage({ + id: 'pen.colorMenu.saturation', + default: 'saturation', + description: 'label for saturation element in color picker for pen extension' + }), + value: ColorParam.SATURATION + }, + { + text: formatMessage({ + id: 'pen.colorMenu.brightness', + default: 'brightness', + description: 'label for brightness element in color picker for pen extension' + }), + value: ColorParam.BRIGHTNESS + }, + { + text: formatMessage({ + id: 'pen.colorMenu.transparency', + default: 'transparency', + description: 'label for transparency element in color picker for pen extension' + }), + value: ColorParam.TRANSPARENCY + + } + ]; + } + + getLayerParam () { + return [ + { + text: formatMessage({ + id: 'pen.layerMenu.front', + default: 'front', + description: 'label for front' + }), + value: LayerParam.FRONT + }, + { + text: formatMessage({ + id: 'pen.layerMenu.back', + default: 'back', + description: 'label for back' + }), + value: LayerParam.BACK + } + ]; + } + + getItalicsToggleParam () { + return [ + { + text: formatMessage({ + id: 'pen.italicsToggle.on', + default: 'on', + description: 'label for on' + }), + value: ItalicsParam.ON + }, + { + text: formatMessage({ + id: 'pen.italicsToggle.off', + default: 'off', + description: 'label for off' + }), + value: ItalicsParam.OFF + } + ]; + } + + /** + * Clamp a pen color parameter to the range (0,100). + * @param {number} value - the value to be clamped. + * @returns {number} the clamped value. + * @private + */ + _clampColorParam (value) { + return MathUtil.clamp(value, 0, 100); + } + + /** + * Convert an alpha value to a pen transparency value. + * Alpha ranges from 0 to 1, where 0 is transparent and 1 is opaque. + * Transparency ranges from 0 to 100, where 0 is opaque and 100 is transparent. + * @param {number} alpha - the input alpha value. + * @returns {number} the transparency value. + * @private + */ + _alphaToTransparency (alpha) { + return (1.0 - alpha) * 100.0; + } + + /** + * Convert a pen transparency value to an alpha value. + * Alpha ranges from 0 to 1, where 0 is transparent and 1 is opaque. + * Transparency ranges from 0 to 100, where 0 is opaque and 100 is transparent. + * @param {number} transparency - the input transparency value. + * @returns {number} the alpha value. + * @private + */ + _transparencyToAlpha (transparency) { + return 1.0 - (transparency / 100.0); + } + + _getFonts() { + return [{ + text: 'Sans Serif', + value: SANS_SERIF_ID + }, { + text: 'Serif', + value: SERIF_ID + }, { + text: 'Handwriting', + value: HANDWRITING_ID + }, { + text: 'Marker', + value: MARKER_ID + }, { + text: 'Curly', + value: CURLY_ID + }, { + text: 'Pixel', + value: PIXEL_ID + }, { + text: 'Playful', + value: PLAYFUL_ID + }, { + text: 'Bubbly', + value: BUBBLY_ID + }, { + text: 'Arcade', + value: ARCADE_ID + }, { + text: 'Bits and Bytes', + value: BITSANDBYTES_ID + }, { + text: 'Technological', + value: TECHNOLOGICAL_ID + }, { + text: 'Scratch', + value: SCRATCH_ID + }, { + text: 'Archivo', + value: ARCHIVO_ID + }, { + text: 'Archivo Black', + value: ARCHIVOBLACK_ID + }, + ...this.runtime.fontManager.getFonts().map(i => ({ + text: i.name, + value: i.family + })), + { + text: 'random font', + value: RANDOM_ID + }]; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'pen', + name: formatMessage({ + id: 'pen.categoryName', + default: 'Pen', + description: 'Label for the pen extension category' + }), + blockIconURI: blockIconURI, + blocks: [ + { + blockType: BlockType.LABEL, + text: formatMessage({ + id: 'pm.pen.stageSelected', + default: 'Stage selected: less pen blocks', + description: 'Label that appears in the Pen category when the stage is selected' + }), + filter: [TargetType.STAGE] + }, + { + opcode: 'clear', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.clear', + default: 'erase all', + description: 'erase all pen trails and stamps' + }) + }, + { + opcode: 'stamp', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.stamp', + default: 'stamp', + description: 'render current costume on the background' + }), + filter: [TargetType.SPRITE] + }, + { + opcode: 'penDown', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.penDown', + default: 'pen down', + description: 'start leaving a trail when the sprite moves' + }), + filter: [TargetType.SPRITE] + }, + { + opcode: 'penUp', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.penUp', + default: 'pen up', + description: 'stop leaving a trail behind the sprite' + }), + filter: [TargetType.SPRITE] + }, + { + opcode: 'setPenColorToColor', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setColor', + default: 'set pen color to [COLOR]', + description: 'set the pen color to a particular (RGB) value' + }), + arguments: { + COLOR: { + type: ArgumentType.COLOR + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'changePenColorParamBy', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.changeColorParam', + default: 'change pen [COLOR_PARAM] by [VALUE]', + description: 'change the state of a pen color parameter' + }), + arguments: { + COLOR_PARAM: { + type: ArgumentType.STRING, + menu: 'colorParam', + defaultValue: ColorParam.COLOR + }, + VALUE: { + type: ArgumentType.NUMBER, + defaultValue: 10 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'setPenColorParamTo', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setColorParam', + default: 'set pen [COLOR_PARAM] to [VALUE]', + description: 'set the state for a pen color parameter e.g. saturation' + }), + arguments: { + COLOR_PARAM: { + type: ArgumentType.STRING, + menu: 'colorParam', + defaultValue: ColorParam.COLOR + }, + VALUE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'changePenSizeBy', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.changeSize', + default: 'change pen size by [SIZE]', + description: 'change the diameter of the trail left by a sprite' + }), + arguments: { + SIZE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'setPenSizeTo', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setSize', + default: 'set pen size to [SIZE]', + description: 'set the diameter of a trail left by a sprite' + }), + arguments: { + SIZE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + filter: [TargetType.SPRITE] + }, + "---", + { + opcode: 'drawRect', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.drawRect', + default: 'use [COLOR] to draw a square on x:[X] y:[Y] width:[WIDTH] height:[HEIGHT]', + description: 'draw a square' + }), + arguments: { + COLOR: { + type: ArgumentType.COLOR + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 10 + }, + HEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: 'drawArrayComplexShape', + blockType: BlockType.COMMAND, + text: 'draw polygon from points [SHAPE] with fill [COLOR]', + arguments: { + SHAPE: { + type: ArgumentType.STRING, + defaultValue: '[-20, 20, 20, 20, 0, -20]' + }, + COLOR: { + type: ArgumentType.COLOR + } + }, + hideFromPalette: false + }, + "---", + { + opcode: 'preloadUriImage', + blockType: BlockType.COMMAND, + text: 'preload image [URI] as [NAME]', + arguments: { + URI: { + type: ArgumentType.STRING, + defaultValue: DefaultDrawImage + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "preloaded image" + } + } + }, + { + opcode: 'unloadUriImage', + blockType: BlockType.COMMAND, + text: 'unload image [NAME]', + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "preloaded image" + } + } + }, + { + opcode: 'drawUriImage', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.drawUriImage', + default: 'draw image [URI] at x:[X] y:[Y]', + description: 'draw image' + }), + arguments: { + URI: { + type: ArgumentType.STRING, + defaultValue: DefaultDrawImage + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'drawUriImageWHR', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.drawUriImageWHR', + default: 'draw image [URI] at x:[X] y:[Y] width:[WIDTH] height:[HEIGHT] pointed at: [ROTATE]', + description: 'draw image width height rotation' + }), + arguments: { + URI: { + type: ArgumentType.STRING, + defaultValue: DefaultDrawImage + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 64 + }, + HEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 64 + }, + ROTATE: { + type: ArgumentType.ANGLE, + defaultValue: 90 + } + } + }, + { + opcode: 'drawUriImageWHCX1Y1X2Y2R', + blockType: BlockType.COMMAND, + text: 'draw image [URI] at x:[X] y:[Y] width:[WIDTH] height:[HEIGHT] cropping from x:[CROPX] y:[CROPY] width:[CROPW] height:[CROPH] pointed at: [ROTATE]', + arguments: { + URI: { + type: ArgumentType.STRING, + defaultValue: DefaultDrawImage + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 64 + }, + HEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 64 + }, + CROPX: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + CROPY: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + CROPW: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + CROPH: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + ROTATE: { + type: ArgumentType.ANGLE, + defaultValue: 90 + } + } + }, + "---", + { + opcode: 'printText', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.printText', + default: 'print [TEXT] on x:[X] y:[Y]', + description: 'print text' + }), + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: 'Foobars are yummy' + }, + X: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'setPrintFont', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setPrintFont', + default: 'set print font to [FONT]', + description: 'set print font' + }), + arguments: { + FONT: { + type: ArgumentType.STRING, + defaultValue: 'Arial', + menu: 'FONT' + } + } + }, + { + opcode: 'setPrintFontSize', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setPrintFontSize', + default: 'set print font size to [SIZE]', + description: 'set print font size' + }), + arguments: { + SIZE: { + type: ArgumentType.NUMBER, + defaultValue: 24 + } + } + }, + { + opcode: 'setPrintFontColor', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setPrintFontColor', + default: 'set print font color to [COLOR]', + description: 'set print font color' + }), + arguments: { + COLOR: { + type: ArgumentType.COLOR + } + } + }, + { + opcode: 'setPrintFontStrokeColor', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setPrintFontStrokeColor', + default: 'set print stroke color to [COLOR]', + description: 'set print stroke color' + }), + arguments: { + COLOR: { + type: ArgumentType.COLOR + } + } + }, + { + opcode: 'setPrintFontStrokeWidth', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setPrintFontStrokeWidth', + default: 'set print stroke width to [WIDTH]', + description: 'set print stroke width' + }), + arguments: { + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'setPrintFontWeight', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setPrintFontWeight', + default: 'set print font weight to [WEIGHT]', + description: 'set print font weight' + }), + arguments: { + WEIGHT: { + type: ArgumentType.NUMBER, + defaultValue: 700 + } + } + }, + { + opcode: 'setPrintFontItalics', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setPrintFontItalics', + default: 'turn print font italics [OPTION]', + description: 'toggle print font italics' + }), + arguments: { + OPTION: { + type: ArgumentType.STRING, + menu: 'italicsToggleParam', + defaultValue: ItalicsParam.ON + } + } + }, + /* Legacy blocks, should not be shown in flyout */ + { + opcode: 'drawComplexShape', + blockType: BlockType.COMMAND, + text: 'draw triangle [SHAPE] with fill [COLOR]', + arguments: { + SHAPE: { + type: ArgumentType.POLYGON, + nodes: 3 + }, + COLOR: { + type: ArgumentType.COLOR + } + }, + hideFromPalette: true + }, + { + opcode: 'draw4SidedComplexShape', + blockType: BlockType.COMMAND, + text: 'draw quadrilateral [SHAPE] with fill [COLOR]', + arguments: { + SHAPE: { + type: ArgumentType.POLYGON, + nodes: 4 + }, + COLOR: { + type: ArgumentType.COLOR + } + }, + hideFromPalette: true + }, + { + opcode: 'setPenShadeToNumber', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setShade', + default: 'LEGACY - set pen shade to [SHADE]', + description: 'legacy pen blocks - set pen shade' + }), + arguments: { + SHADE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + hideFromPalette: true + }, + { + opcode: 'changePenShadeBy', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.changeShade', + default: 'LEGACY - change pen shade by [SHADE]', + description: 'legacy pen blocks - change pen shade' + }), + arguments: { + SHADE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + hideFromPalette: true + }, + { + opcode: 'setPenHueToNumber', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.setHue', + default: 'LEGACY - set pen color to [HUE]', + description: 'legacy pen blocks - set pen color to number' + }), + arguments: { + HUE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + hideFromPalette: true + }, + { + opcode: 'changePenHueBy', + blockType: BlockType.COMMAND, + text: formatMessage({ + id: 'pen.changeHue', + default: 'LEGACY - change pen color by [HUE]', + description: 'legacy pen blocks - change pen color' + }), + arguments: { + HUE: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + }, + hideFromPalette: true + }, + { + opcode: 'goPenLayer', + blockType: BlockType.COMMAND, + hideFromPalette: true, + text: formatMessage({ + id: 'pen.GoPenLayer', + default: 'go to [OPTION] layer', + description: 'go to front layer(pen)' + }), + arguments: { + OPTION: { + type: ArgumentType.STRING, + menu: 'layerParam', + defaultValue: LayerParam.FRONT + } + } + } + ], + menus: { + colorParam: { + acceptReporters: true, + items: this._initColorParam() + }, + layerParam: { + acceptReporters: false, + items: this.getLayerParam() + }, + italicsToggleParam: { + acceptReporters: false, + items: this.getItalicsToggleParam() + }, + FONT: { + items: '_getFonts', + isTypeable: true + } + } + }; + } + + /** + * The pen "clear" block clears the pen layer's contents. + */ + clear () { // used by compiler + const penSkinId = this._getPenLayerID(); + if (penSkinId >= 0) { + this.runtime.renderer.penClear(penSkinId); + this.runtime.requestRedraw(); + } + } + + setPrintFont (args) { + this.printTextAttribute.font = args.FONT; + } + setPrintFontSize (args) { + this.printTextAttribute.size = args.SIZE; + } + setPrintFontColor (args) { + const rgb = Cast.toRgbColorObject(args.COLOR); + const hex = Color.rgbToHex(rgb); + this.printTextAttribute.color = hex; + } + setPrintFontStrokeColor (args) { + const rgb = Cast.toRgbColorObject(args.COLOR); + const hex = Color.rgbToHex(rgb); + this.printTextAttribute.strokeColor = hex; + } + setPrintFontStrokeWidth (args) { + this.printTextAttribute.strokeWidth = args.WIDTH; + } + setPrintFontWeight (args) { + this.printTextAttribute.weight = args.WEIGHT; + } + setPrintFontItalics (args) { + this.printTextAttribute.italic = args.OPTION === ItalicsParam.ON; + } + printText (args) { + const ctx = this._getBitmapCanvas(); + + let resultFont = ''; + resultFont += `${this.printTextAttribute.italic ? 'italic ' : ''}`; + resultFont += `${this.printTextAttribute.weight} `; + resultFont += `${this.printTextAttribute.size}px `; + resultFont += this.printTextAttribute.font; + ctx.font = resultFont; + + ctx.strokeStyle = this.printTextAttribute.strokeWidth > 0 ? this.printTextAttribute.strokeColor : this.printTextAttribute.color; + ctx.lineWidth = this.printTextAttribute.strokeWidth; + ctx.fillStyle = this.printTextAttribute.color; + + if (this.printTextAttribute.strokeWidth > 0) ctx.strokeText(args.TEXT, args.X, -args.Y); + ctx.fillText(args.TEXT, args.X, -args.Y); + + this._drawContextToPen(ctx); + } + + async _drawUriImage({URI, X, Y, WIDTH, HEIGHT, ROTATE, CROPX, CROPY, CROPW, CROPH}) { + const image = this.preloadedImages[URI] ?? await new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => resolve(image); + image.onerror = (err) => { + console.error('failed to load', URI, err); + reject('Image failed to load'); + }; + image.src = URI; + }); + + // protect the user from uninteligable errors that may be thrown but probably never will + if (!image.complete) throw new Error('the provided image never loaded') + if (image.width <= 0) throw new Error(`the image has an invalid width of ${image.width}`) + if (image.height <= 0) throw new Error(`the image has an invalid height of ${image.height}`) + + const ctx = this._getBitmapCanvas(); + // an error that really should never happen, but also shouldnt ever get to the user through here + if (ctx.canvas.width <= 0 && ctx.canvas.height <= 0) return; + + ctx.rotate(MathUtil.degToRad(ROTATE - 90)); + + // use sizes from the image if none specified + const width = WIDTH ?? image.width; + const height = HEIGHT ?? image.height; + const realX = X - (width / 2); + const realY = -Y - (height / 2); + const drawArgs = [CROPX, CROPY, CROPW, CROPH, realX, realY, width, height]; + + // ensure that all of the drop values exist, just in case :Trollhans + if (!(typeof CROPX === "number" && typeof CROPY === "number" && CROPH && CROPH)) { + drawArgs.splice(0, 4); + } + + ctx.drawImage(image, ...drawArgs); + this._drawContextToPen(ctx); + } + + // todo: should these be merged into their own function? they all have the same code... + drawUriImage (args) { + const preloaded = this.preloadedImages[args.URI]; + const possiblePromise = this._drawUriImage(args); + if (!preloaded) { + return possiblePromise; + } + } + drawUriImageWHR (args) { + const preloaded = this.preloadedImages[args.URI]; + const possiblePromise = this._drawUriImage(args); + if (!preloaded) { + return possiblePromise; + } + } + drawUriImageWHCX1Y1X2Y2R (args) { + const preloaded = this.preloadedImages[args.URI]; + const possiblePromise = this._drawUriImage(args); + if (!preloaded) { + return possiblePromise; + } + } + + preloadUriImage ({ URI, NAME }) { + return new Promise(resolve => { + const image = new Image(); + image.crossOrigin = "anonymous"; + image.onload = () => { + this.preloadedImages[Cast.toString(NAME)] = image; + resolve(); + }; + image.onerror = resolve; // ignore loading errors lol! + image.src = Cast.toString(URI); + }); + } + unloadUriImage ({ NAME }) { + const name = Cast.toString(NAME); + if (this.preloadedImages.hasOwnProperty(name)) { + this.preloadedImages[name].remove(); + delete this.preloadedImages[name]; + } + } + + drawRect (args) { + const ctx = this._getBitmapCanvas(); + + const rgb = Cast.toRgbColorObject(args.COLOR); + const hex = Color.rgbToHex(rgb); + ctx.fillStyle = hex; + ctx.strokeStyle = ctx.fillStyle; + ctx.fillRect(args.X, -args.Y, args.WIDTH, args.HEIGHT); + + this._drawContextToPen(ctx); + } + + _drawContextToPen (ctx) { + const penSkinId = this._getPenLayerID(); + const width = this.bitmapCanvas.width; + const height = this.bitmapCanvas.height; + ctx.restore(); + + const printSkin = this.runtime.renderer._allSkins[this.bitmapSkinID]; + const imageData = ctx.getImageData(0, 0, width, height); + printSkin._setTexture(imageData); + this.runtime.renderer.penStamp(penSkinId, this.bitmapDrawableID); + + this.runtime.requestRedraw(); + } + + _getBitmapCanvas () { + const penSkinId = this._getPenLayerID(); + const penSkin = this.runtime.renderer._allSkins[penSkinId]; + const width = penSkin._size[0]; + const height = penSkin._size[1]; + this.bitmapCanvas.width = width; + this.bitmapCanvas.height = height; + + const ctx = this.bitmapCanvas.getContext('2d'); + + ctx.clearRect(0, 0, width, height); + ctx.translate(width / 2, height / 2); + // console.log(penSkin.renderQuality, this.bitmapCanvas.width, this.bitmapCanvas.height); + ctx.scale(penSkin.renderQuality, penSkin.renderQuality); + return ctx; + } + + /** + * The pen "stamp" block stamps the current drawable's image onto the pen layer. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + */ + stamp (args, util) { + this._stamp(util.target); + } + _stamp (target) { // used by compiler + const penSkinId = this._getPenLayerID(); + if (penSkinId >= 0) { + this.runtime.renderer.penStamp(penSkinId, target.drawableID); + this.runtime.requestRedraw(); + } + } + + /** + * The pen "pen down" block causes the target to leave pen trails on future motion. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + */ + penDown (args, util) { + this._penDown(util.target); + } + _penDown (target) { // used by compiler + const penState = this._getPenState(target); + + if (!penState.penDown) { + penState.penDown = true; + target.onTargetMoved = this._onTargetMoved; + } + + const penSkinId = this._getPenLayerID(); + if (penSkinId >= 0) { + this.runtime.renderer.penPoint(penSkinId, penState.penAttributes, target.x, target.y); + this.runtime.requestRedraw(); + } + } + + /** + * The pen "pen up" block stops the target from leaving pen trails. + * @param {object} args - the block arguments. + * @param {object} util - utility object provided by the runtime. + */ + penUp (args, util) { + this._penUp(util.target); + } + _penUp (target) { // used by compiler + const penState = this._getPenState(target); + + if (penState.penDown) { + penState.penDown = false; + target.onTargetMoved = null; + } + } + + /** + * The pen "set pen color to {color}" block sets the pen to a particular RGB color. + * The transparency is reset to 0. + * @param {object} args - the block arguments. + * @property {int} COLOR - the color to set, expressed as a 24-bit RGB value (0xRRGGBB). + * @param {object} util - utility object provided by the runtime. + */ + setPenColorToColor (args, util) { + this._setPenColorToColor(args.COLOR, util.target); + } + _setPenColorToColor (color, target) { // used by compiler + const penState = this._getPenState(target); + const rgb = Cast.toRgbColorObject(color); + const hsv = Color.rgbToHsv(rgb); + penState.color = (hsv.h / 360) * 100; + penState.saturation = hsv.s * 100; + penState.brightness = hsv.v * 100; + if (rgb.hasOwnProperty('a')) { + penState.transparency = 100 * (1 - (rgb.a / 255.0)); + } else { + penState.transparency = 0; + } + + // Set the legacy "shade" value the same way scratch 2 did. + penState._shade = penState.brightness / 2; + + this._updatePenColor(penState); + } + + /** + * Update the cached color from the color, saturation, brightness and transparency values + * in the provided PenState object. + * @param {PenState} penState - the pen state to update. + * @private + */ + _updatePenColor (penState) { + const rgb = Color.hsvToRgb({ + h: penState.color * 360 / 100, + s: penState.saturation / 100, + v: penState.brightness / 100 + }); + penState.penAttributes.color4f[0] = rgb.r / 255.0; + penState.penAttributes.color4f[1] = rgb.g / 255.0; + penState.penAttributes.color4f[2] = rgb.b / 255.0; + penState.penAttributes.color4f[3] = this._transparencyToAlpha(penState.transparency); + } + + /** + * Set or change a single color parameter on the pen state, and update the pen color. + * @param {ColorParam} param - the name of the color parameter to set or change. + * @param {number} value - the value to set or change the param by. + * @param {PenState} penState - the pen state to update. + * @param {boolean} change - if true change param by value, if false set param to value. + * @private + */ + _setOrChangeColorParam (param, value, penState, change) { // used by compiler + switch (param) { + case ColorParam.COLOR: + penState.color = this._wrapColor(value + (change ? penState.color : 0)); + break; + case ColorParam.SATURATION: + penState.saturation = this._clampColorParam(value + (change ? penState.saturation : 0)); + break; + case ColorParam.BRIGHTNESS: + penState.brightness = this._clampColorParam(value + (change ? penState.brightness : 0)); + break; + case ColorParam.TRANSPARENCY: + penState.transparency = this._clampColorParam(value + (change ? penState.transparency : 0)); + break; + default: + log.warn(`Tried to set or change unknown color parameter: ${param}`); + } + this._updatePenColor(penState); + } + + /** + * The "change pen {ColorParam} by {number}" block changes one of the pen's color parameters + * by a given amound. + * @param {object} args - the block arguments. + * @property {ColorParam} COLOR_PARAM - the name of the selected color parameter. + * @property {number} VALUE - the amount to change the selected parameter by. + * @param {object} util - utility object provided by the runtime. + */ + changePenColorParamBy (args, util) { + const penState = this._getPenState(util.target); + this._setOrChangeColorParam(args.COLOR_PARAM, Cast.toNumber(args.VALUE), penState, true); + } + + /** + * The "set pen {ColorParam} to {number}" block sets one of the pen's color parameters + * to a given amound. + * @param {object} args - the block arguments. + * @property {ColorParam} COLOR_PARAM - the name of the selected color parameter. + * @property {number} VALUE - the amount to set the selected parameter to. + * @param {object} util - utility object provided by the runtime. + */ + setPenColorParamTo (args, util) { + const penState = this._getPenState(util.target); + this._setOrChangeColorParam(args.COLOR_PARAM, Cast.toNumber(args.VALUE), penState, false); + } + + /** + * The pen "change pen size by {number}" block changes the pen size by the given amount. + * @param {object} args - the block arguments. + * @property {number} SIZE - the amount of desired size change. + * @param {object} util - utility object provided by the runtime. + */ + changePenSizeBy (args, util) { + this._changePenSizeBy(Cast.toNumber(args.SIZE), util.target); + } + _changePenSizeBy (size, target) { // used by compiler + const penAttributes = this._getPenState(target).penAttributes; + penAttributes.diameter = this._clampPenSize(penAttributes.diameter + size); + } + + /** + * The pen "set pen size to {number}" block sets the pen size to the given amount. + * @param {object} args - the block arguments. + * @property {number} SIZE - the amount of desired size change. + * @param {object} util - utility object provided by the runtime. + */ + setPenSizeTo (args, util) { + this._setPenSizeTo(Cast.toNumber(args.SIZE), util.target); + } + _setPenSizeTo (size, target) { // used by compiler + const penAttributes = this._getPenState(target).penAttributes; + penAttributes.diameter = this._clampPenSize(size); + } + + /* LEGACY OPCODES */ + /** + * Scratch 2 "hue" param is equivelant to twice the new "color" param. + * @param {object} args - the block arguments. + * @property {number} HUE - the amount to set the hue to. + * @param {object} util - utility object provided by the runtime. + */ + setPenHueToNumber (args, util) { + this._setPenHueToNumber(Cast.toNumber(args.HUE), util.target); + } + _setPenHueToNumber (hueValue, target) { + const penState = this._getPenState(target); + const colorValue = hueValue / 2; + this._setOrChangeColorParam(ColorParam.COLOR, colorValue, penState, false); + this._setOrChangeColorParam(ColorParam.TRANSPARENCY, 0, penState, false); + this._legacyUpdatePenColor(penState); + } + + /** + * Scratch 2 "hue" param is equivelant to twice the new "color" param. + * @param {object} args - the block arguments. + * @property {number} HUE - the amount of desired hue change. + * @param {object} util - utility object provided by the runtime. + */ + changePenHueBy (args, util) { + this._changePenHueBy(Cast.toNumber(args.HUE), util.target); + } + _changePenHueBy (hueChange, target) { // used by compiler + const penState = this._getPenState(target); + const colorChange = hueChange / 2; + this._setOrChangeColorParam(ColorParam.COLOR, colorChange, penState, true); + + this._legacyUpdatePenColor(penState); + } + + /** + * Use legacy "set shade" code to calculate RGB value for shade, + * then convert back to HSV and store those components. + * It is important to also track the given shade in penState._shade + * because it cannot be accurately backed out of the new HSV later. + * @param {object} args - the block arguments. + * @property {number} SHADE - the amount to set the shade to. + * @param {object} util - utility object provided by the runtime. + */ + setPenShadeToNumber (args, util) { + this._setPenShadeToNumber(Cast.toNumber(args.SHADE), util.target); + } + _setPenShadeToNumber (shade, target) { + const penState = this._getPenState(target); + let newShade = Cast.toNumber(shade); + + // Wrap clamp the new shade value the way scratch 2 did. + newShade = newShade % 200; + if (newShade < 0) newShade += 200; + + // And store the shade that was used to compute this new color for later use. + penState._shade = newShade; + + this._legacyUpdatePenColor(penState); + } + + /** + * Because "shade" cannot be backed out of hsv consistently, use the previously + * stored penState._shade to make the shade change. + * @param {object} args - the block arguments. + * @property {number} SHADE - the amount of desired shade change. + * @param {object} util - utility object provided by the runtime. + */ + changePenShadeBy (args, util) { + this._changePenShadeBy(args.SHADE, util.target); + } + _changePenShadeBy (shade, target) { + const penState = this._getPenState(target); + const shadeChange = Cast.toNumber(shade); + this._setPenShadeToNumber(penState._shade + shadeChange, target); + } + + /** + * Update the pen state's color from its hue & shade values, Scratch 2.0 style. + * @param {object} penState - update the HSV & RGB values in this pen state from its hue & shade values. + * @private + */ + _legacyUpdatePenColor (penState) { + // Create the new color in RGB using the scratch 2 "shade" model + let rgb = Color.hsvToRgb({ h: penState.color * 360 / 100, s: 1, v: 1 }); + const shade = (penState._shade > 100) ? 200 - penState._shade : penState._shade; + if (shade < 50) { + rgb = Color.mixRgb(Color.RGB_BLACK, rgb, (10 + shade) / 60); + } else { + rgb = Color.mixRgb(rgb, Color.RGB_WHITE, (shade - 50) / 60); + } + + // Update the pen state according to new color + const hsv = Color.rgbToHsv(rgb); + penState.color = 100 * hsv.h / 360; + penState.saturation = 100 * hsv.s; + penState.brightness = 100 * hsv.v; + + this._updatePenColor(penState); + } + + goPenLayer (args) { + this._getPenLayerID(); + if (!this._penDrawableId) return; + // layer order is already set correctly, dont do anything + if (this.runtime.renderer._groupOrdering.at(-1) === LayerNames[args.OPTION]) return; + if (args.OPTION === LayerParam.FRONT) { + console.log('setting the layer order to', StageLayering.LAYER_GROUPS_PEN); + this.runtime.renderer.setLayerGroupOrdering(StageLayering.LAYER_GROUPS_PEN); + this._penDrawableId = this.runtime.renderer.setDrawableOrder(this._penDrawableId, + Infinity, StageLayering.PEN_LAYER); + } else { + console.log('setting the layer order to', StageLayering.LAYER_GROUPS); + this.runtime.renderer.setLayerGroupOrdering(StageLayering.LAYER_GROUPS); + this._penDrawableId = this.runtime.renderer.setDrawableOrder(this._penDrawableId, + -Infinity, StageLayering.PEN_LAYER); + } + } + + _getPenColor (target) { + const rgba = {}; + const penState = this._getPenState(target); + rgba.r = penState.penAttributes.color4f[0] * 255; + rgba.g = penState.penAttributes.color4f[1] * 255; + rgba.b = penState.penAttributes.color4f[2] * 255; + rgba.a = this._alphaToTransparency(penState.penAttributes.color4f[3]); + return Color.rgbToHex(rgba); + } + + drawComplexShape (args, util) { + const target = util.target; + const penState = this._getPenState(target); + const penAttributes = penState.penAttributes; + const penColor = this._getPenColor(util.target); + const points = args.SHAPE; + const firstPos = points.at(-1); + + const ctx = this._getBitmapCanvas(); + + const rgb = Cast.toRgbColorObject(args.COLOR); + const hex = Color.rgbToHex(rgb); + ctx.fillStyle = hex; + ctx.strokeStyle = penColor; + ctx.lineWidth = penAttributes.diameter; + + ctx.beginPath(); + ctx.moveTo(firstPos.x, -firstPos.y); + for (const pos of points) { + ctx.lineTo(pos.x, -pos.y); + } + ctx.closePath(); + if (penState.penDown) ctx.stroke(); + ctx.fill(); + + this._drawContextToPen(ctx); + } + + draw4SidedComplexShape (args, util) { + this.drawComplexShape(args, util); + } + + drawArrayComplexShape (args, util) { + const providedData = Cast.toString(args.SHAPE); + const providedPoints = parseArray(providedData); // ignores objects + // we can save processing by just ignoring empty arrays + if (providedPoints.length <= 0) return; + // the last point is missing a Y value, Y will be 0 for that point + if (providedPoints.length % 2 !== 0) providedPoints.push(0); + const points = []; + let currentPoint = {}; + let isXCoord = true; + for (const num of providedPoints) { + if (isXCoord) { + currentPoint.x = Cast.toNumber(num); + isXCoord = false; + continue; + } + currentPoint.y = Cast.toNumber(num); + points.push(currentPoint); + currentPoint = {}; // make a new object so we dont override the others inside the array + isXCoord = true; + } + this.drawComplexShape({ + ...args, + SHAPE: points + }, util); + } +} + +module.exports = Scratch3PenBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_speech2text/index.js b/local-scratch-vm/src/extensions/scratch3_speech2text/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b42e7f8c1fa48a5dd467e0edc330c58f3f12584b --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_speech2text/index.js @@ -0,0 +1,700 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const BlockType = require('../../extension-support/block-type'); +const formatMessage = require('format-message'); +const log = require('../../util/log'); +const DiffMatchPatch = require('diff-match-patch'); + + +/** + * Url of icon to be displayed at the left edge of each extension block. + * @type {string} + */ +// eslint-disable-next-line max-len +const iconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTIgMTRjMS42NiAwIDIuOTktMS4zNCAyLjk5LTNMMTUgNWMwLTEuNjYtMS4zNC0zLTMtM1M5IDMuMzQgOSA1djZjMCAxLjY2IDEuMzQgMyAzIDN6bTUuMy0zYzAgMy0yLjU0IDUuMS01LjMgNS4xUzYuNyAxNCA2LjcgMTFINWMwIDMuNDEgMi43MiA2LjIzIDYgNi43MlYyMWgydi0zLjI4YzMuMjgtLjQ4IDYtMy4zIDYtNi43MmgtMS43eiIvPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4K'; + + +/** + * Url of icon to be displayed in the toolbox menu for the extension category. + * @type {string} + */ +// eslint-disable-next-line max-len +const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzc1NzU3NSI+CiAgICA8cGF0aCBkPSJNMTIgMTRjMS42NiAwIDIuOTktMS4zNCAyLjk5LTNMMTUgNWMwLTEuNjYtMS4zNC0zLTMtM1M5IDMuMzQgOSA1djZjMCAxLjY2IDEuMzQgMyAzIDN6bTUuMy0zYzAgMy0yLjU0IDUuMS01LjMgNS4xUzYuNyAxNCA2LjcgMTFINWMwIDMuNDEgMi43MiA2LjIzIDYgNi43MlYyMWgydi0zLjI4YzMuMjgtLjQ4IDYtMy4zIDYtNi43MmgtMS43eiIvPgogICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPgo8L3N2Zz4K'; + + +/** + * The url of the speech server. + * @type {string} + */ +const serverURL = 'wss://speech.scratch.mit.edu'; + +/** + * The amount of time to wait between when we stop sending speech data to the server and when + * we expect the transcription result marked with isFinal: true to come back from the server. + * @type {int} + */ +const finalResponseTimeoutDurationMs = 3000; + +/** + * The max amount of time the Listen And Wait block will listen for. It may listen for less time + * if we get back results that are good and think the user is done talking. + * Currently set to 10sec. This should not exceed the speech api limit (60sec) without redoing how + * we stream the microphone data data. + * @type {int} + */ +const listenAndWaitBlockTimeoutMs = 10000; + + +class Scratch3Speech2TextBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * An array of phrases from the [when I hear] hat blocks. + * The list of phrases in the when I hear hat blocks. This list is sent + * to the speech api to seed the recognition engine and for deciding + * whether the transcription results match. + * @type {Array} + * @private + */ + this._phraseList = []; + + /** + * The most recent transcription result received from the speech API that we decided to keep. + * This is the value returned by the reporter block. + * @type {String} + * @private + */ + this._currentUtterance = ''; + + /** + * Similar to _currentUtterance, but set back to '' at the beginning of listening block + * and on green flag. + * Used to get the hat blocks to edge trigger. In order to detect someone saying + * the same thing twice in two subsequent listen and wait blocks + * and still trigger the hat, we need this to go from + * '' at the beginning of the listen block to '' at the end. + * @type {string} + * @private + */ + this._utteranceForEdgeTrigger = null; + + /** + * The list of queued `resolve` callbacks for 'Listen and Wait' blocks. + * We only listen to for one utterance at a time. We may encounter multiple + * 'Listen and wait' blocks that tell us to start listening. If one starts + * and hasn't receieved results back yet, when we encounter more, any further ones + * will all resolve when we get the next acceptable transcription result back. + * @type {!Array} + * @private + */ + this._speechPromises = []; + + /** + * The id of the timeout that will run if we start listening and don't get any + * transcription results back. e.g. because we didn't hear anything. + * @type {number} + * @private + */ + this._speechTimeoutId = null; + + /** + * The id of the timeout that will run to wait for after we're done listening but + * are still waiting for a potential isFinal:true transcription result to come back. + * @type {number} + * @private + */ + this._speechFinalResponseTimeout = null; + + /** + * The ScriptProcessorNode hooked up to the audio context. + * @type {ScriptProcessorNode} + * @private + */ + this._scriptNode = null; + + /** + * The socket used to communicate with the speech server to send microphone data + * and recieve transcription results. + * @type {WebSocket} + * @private + */ + this._socket = null; + + /** + * The AudioContext used to manage the microphone. + * @type {AudioContext} + * @private + */ + this._context = null; + + /** + * MediaStreamAudioSourceNode to handle microphone data. + * @type {MediaStreamAudioSourceNode} + * @private + */ + this._sourceNode = null; + + /** + * A Promise whose fulfillment handler receives a MediaStream object when the microphone has been obtained. + * @type {Promise} + * @private + */ + this._audioPromise = null; + + + /** + * Diff Match Patch is used to do some fuzzy matching of the transcription results + * with what is in the hat blocks. + */ + this._dmp = new DiffMatchPatch(); + // Threshold for diff match patch to use: (0.0 = perfection, 1.0 = very loose). + this._dmp.Match_Threshold = 0.3; + + this._newSocketCallback = this._newSocketCallback.bind(this); + this._setupSocketCallback = this._setupSocketCallback.bind(this); + this._socketMessageCallback = this._socketMessageCallback.bind(this); + this._processAudioCallback = this._processAudioCallback.bind(this); + this._onTranscriptionFromServer = this._onTranscriptionFromServer.bind(this); + this._resetListening = this._resetListening.bind(this); + this._stopTranscription = this._stopTranscription.bind(this); + + + this.runtime.on('PROJECT_STOP_ALL', this._resetListening.bind(this)); + this.runtime.on('PROJECT_START', this._resetEdgeTriggerUtterance.bind(this)); + + } + + /** + * Scans all the 'When I hear' hat blocks for each sprite and pulls out the text. The list + * is sent off to the speech recognition server as hints. This *only* reads the value out of + * the hat block shadow. If a block is dropped on top of the shadow, it is skipped. + * @returns {Array} list of strings from the hat blocks in the project. + * @private + */ + _scanBlocksForPhraseList () { + const words = []; + // For each each target, walk through the top level blocks and check whether + // they are speech hat/when I hear blocks. + this.runtime.targets.forEach(target => { + target.blocks._scripts.forEach(id => { + const b = target.blocks.getBlock(id); + if (b.opcode === 'speech_whenIHearHat') { + // Grab the text from the hat block's shadow. + const inputId = b.inputs.PHRASE.block; + const inputBlock = target.blocks.getBlock(inputId); + // Only grab the value from text blocks. This means we'll + // miss some. e.g. values in variables or other reporters. + if (inputBlock.opcode === 'text') { + const word = target.blocks.getBlock(inputId).fields.TEXT.value; + words.push(word); + } + } + }); + }); + return words; + } + + /** + * Get the viewer's language code. + * @return {string} the language code. + */ + _getViewerLanguageCode () { + return formatMessage.setup().locale || navigator.language || navigator.userLanguage || 'en-US'; + } + + /** + * Resets all things related to listening. Called on Red Stop sign button. + * - suspends audio processing + * - closes socket with speech socket server + * - clears out any remaining speech blocks that are waiting. + * @private. + */ + _resetListening () { + this.runtime.emitMicListening(false); + this._stopListening(); + this._closeWebsocket(); + this._resolveSpeechPromises(); + } + + /** + * Reset the utterance we look for in the when I hear hat block back to + * the empty string. + * @private + */ + _resetEdgeTriggerUtterance () { + this._utteranceForEdgeTrigger = ''; + } + + /** + * Close the connection to the socket server if it is open. + * @private + */ + _closeWebsocket () { + if (this._socket && this._socket.readyState === this._socket.OPEN) { + this._socket.close(); + } + } + + /** + * Call to suspend getting data from the microphone. + * @private + */ + _stopListening () { + // Note that this can be called before any Listen And Wait block did setup, + // so check that things exist before disconnecting them. + if (this._context) { + this._context.suspend.bind(this._context); + } + // This is called on green flag to reset things that may never have existed + // in the first place. Do a bunch of checks. + if (this._scriptNode) { + this._scriptNode.removeEventListener('audioprocess', this._processAudioCallback); + this._scriptNode.disconnect(); + } + if (this._sourceNode) { + this._sourceNode.disconnect(); + } + } + + /** + * Resolves all the speech promises we've accumulated so far and empties out the list. + * @private + */ + _resolveSpeechPromises () { + for (let i = 0; i < this._speechPromises.length; i++) { + const resFn = this._speechPromises[i]; + resFn(); + } + this._speechPromises = []; + } + + /** + * Called when we want to stop listening (e.g. when a listen block times out) + * but we still want to wait a little to see if we get any transcription results + * back before yielding the block execution. + * @private + */ + _stopTranscription () { + this._stopListening(); + if (this._socket && this._socket.readyState === this._socket.OPEN) { + this._socket.send('stopTranscription'); + } + // Give it a couple seconds to response before giving up and assuming nothing else will come back. + this._speechFinalResponseTimeout = setTimeout(this._resetListening, finalResponseTimeoutDurationMs); + } + + /** + * Decides whether to keep a given transcirption result. + * @param {number} fuzzyMatchIndex Index of the fuzzy match or -1 if there is no match. + * @param {object} result The json object representing the transcription result. + * @param {string} normalizedTranscript The transcription text used for matching (i.e. lowercased, no punctuation). + * @returns {boolean} true If a result is good enough to be kept. + * @private + */ + _shouldKeepResult (fuzzyMatchIndex, result, normalizedTranscript) { + // The threshold above which we decide transcription results are unlikely to change again. + // See https://cloud.google.com/speech-to-text/docs/basics#streaming_responses. + const stabilityThreshold = .85; + + // For responsiveness of the When I Hear hat blocks, sometimes we want to keep results that are not + // yet marked 'isFinal' by the speech api. Here are some signals we use. + + // If the result from the speech api isn't very stable and we only had a fuzzy match, we don't want to use it. + const shouldKeepFuzzyMatch = fuzzyMatchIndex !== -1 && result.stability > stabilityThreshold; + + // TODO: This is for debugging. Remove when this function is finalized. + if (shouldKeepFuzzyMatch) { + log.info(`Fuzzy match with high stability.`); + log.info(`match index is ${fuzzyMatchIndex}`); + const phrases = this._phraseList.join(' '); + const matchPhrase = phrases.substring(fuzzyMatchIndex, fuzzyMatchIndex + normalizedTranscript.length); + log.info(`fuzzy match: ${matchPhrase} in ${normalizedTranscript}`); + } + + // If the result is in the phraseList (i.e. it matches one of the 'When I Hear' blocks), we keep it. + // This might be aggressive... but so far seems to be a good thing. + const shouldKeepPhraseListMatch = this._phraseList.includes(normalizedTranscript); + // TODO: This is just for debugging. Remove when this function is finalized. + if (shouldKeepPhraseListMatch) { + log.info(`phrase list ${this._phraseList} includes ${normalizedTranscript}`); + } + // TODO: This is for debugging. Remove when this function is finalized. + if (result.isFinal) { + log.info(`result is final`); + } + + if (!result.isFinal && !shouldKeepPhraseListMatch && !shouldKeepFuzzyMatch) { + return false; + } + return true; + } + + /** + * Normalizes text a bit to facilitate matching. Lowercases, removes some punctuation and whitespace. + * @param {string} text The text to normalzie + * @returns {string} The normalized text. + * @private + */ + _normalizeText (text) { + text = Cast.toString(text).toLowerCase(); + text = text.replace(/[.?!]/g, ''); + text = text.trim(); + return text; + } + + /** + * Call into diff match patch library to compute whether there is a fuzzy match. + * @param {string} text The text to search in. + * @param {string} pattern The pattern to look for in text. + * @returns {number} The index of the match or -1 if there isn't one. + */ + _computeFuzzyMatch (text, pattern) { + // Don't bother matching if any are null. + if (!pattern || !text) { + return -1; + } + let match = -1; + try { + // Look for the text in the pattern starting at position 0. + match = this._dmp.match_main(text, pattern, 0); + } catch (e) { + // This can happen inf the text or pattern gets too long. If so just substring match. + return pattern.indexOf(text); + } + return match; + } + + /** + * Processes the results we get back from the speech server. Decides whether the results + * are good enough to keep. If they are, resolves the 'Listen and Wait' blocks promise and cleans up. + * @param {object} result The transcription result. + * @private + */ + _processTranscriptionResult (result) { + log.info(`Got result: ${JSON.stringify(result)}`); + const transcriptionResult = this._normalizeText(result.alternatives[0].transcript); + + // Waiting for an exact match is not satisfying. It makes it hard to catch + // things like homonyms or things that sound similar "let us" vs "lettuce". Using the fuzzy matching helps + // more aggressively match the phrases that are in the "When I hear" hat blocks. + const phrases = this._phraseList.join(' '); + const fuzzyMatchIndex = this._computeFuzzyMatch(phrases, transcriptionResult); + + // If the result isn't good enough yet, return without saving and resolving the promises. + if (!this._shouldKeepResult(fuzzyMatchIndex, result, transcriptionResult)) { + return; + } + + this._currentUtterance = transcriptionResult; + log.info(`Keeing result: ${this._currentUtterance}`); + this._utteranceForEdgeTrigger = transcriptionResult; + + // We're done listening so resolove all the promises and reset everying so we're ready for next time. + this._resetListening(); + + // We got results so clear out the timeouts. + if (this._speechTimeoutId) { + clearTimeout(this._speechTimeoutId); + this._speechTimeoutId = null; + } + if (this._speechFinalResponseTimeout) { + clearTimeout(this._speechFinalResponseTimeout); + this._speechFinalResponseTimeout = null; + } + } + + /** + * Handle a message from the socket. It contains transcription results. + * @param {MessageEvent} e The message event containing data from speech server. + * @private + */ + _onTranscriptionFromServer (e) { + let result = null; + try { + result = JSON.parse(e.data); + } catch (ex) { + log.error(`Problem parsing json. continuing: ${ex}`); + // TODO: Question - Should we kill listening and continue? + return; + } + this._processTranscriptionResult(result); + } + + + /** + * Decide whether the pattern given matches the text. Uses fuzzy matching + * @param {string} pattern The pattern to look for. Usually this is the transcription result + * @param {string} text The text to look in. Usually this is the set of phrases from the when I hear blocks + * @returns {boolean} true if there is a fuzzy match. + * @private + */ + _speechMatches (pattern, text) { + pattern = this._normalizeText(pattern); + text = this._normalizeText(text); + const match = this._computeFuzzyMatch(text, pattern); + return match !== -1; + } + + /** + * Kick off the listening process. + * @private + */ + _startListening () { + this.runtime.emitMicListening(true); + this._initListening(); + // Force the block to timeout if we don't get any results back/the user didn't say anything. + this._speechTimeoutId = setTimeout(this._stopTranscription, listenAndWaitBlockTimeoutMs); + } + + /** + * Resume listening for audio and re-open the socket to send data. + * @private + */ + _resumeListening () { + this._context.resume.bind(this._context); + this._newWebsocket(); + } + + /** + * Does all setup to get microphone data and initializes the web socket. + * that data to the speech server. + * @private + */ + _initListening () { + this._initializeMicrophone(); + this._initScriptNode(); + this._newWebsocket(); + } + + /** + * Initialize the audio context and connect the microphone. + * @private + */ + _initializeMicrophone () { + // Don't make a new context if we already made one. + if (!this._context) { + // Safari still needs a webkit prefix for audio context + this._context = new (window.AudioContext || window.webkitAudioContext)(); + } + // In safari we have to call getUserMedia every time we want to listen. Other browsers allow + // you to reuse the mediaStream. See #1202 for more context. + this._audioPromise = navigator.mediaDevices.getUserMedia({ + audio: true + }); + + this._audioPromise.then().catch(e => { + log.error(`Problem connecting to microphone: ${e}`); + }); + } + + /** + * Sets up the script processor and the web socket. + * @private + * + */ + _initScriptNode () { + // Create a node that sends raw bytes across the websocket + this._scriptNode = this._context.createScriptProcessor(4096, 1, 1); + } + + /** + * Callback called when it is time to setup the new web socket. + * @param {Function} resolve - function to call when the web socket opens succesfully. + * @param {Function} reject - function to call if opening the web socket fails. + */ + _newSocketCallback (resolve, reject) { + this._socket = new WebSocket(serverURL); + this._socket.addEventListener('open', resolve); + this._socket.addEventListener('error', reject); + } + + /** + * Callback called once we've initially established the web socket is open and working. + * Sets up the callback for subsequent messages (i.e. transcription results) and + * connects to the script node to get data. + * @private + */ + _socketMessageCallback () { + this._socket.addEventListener('message', this._onTranscriptionFromServer); + this._startByteStream(); + } + + /** + * Sets up callback for when socket and audio are initialized. + * @private + */ + _newWebsocket () { + const websocketPromise = new Promise(this._newSocketCallback); + Promise.all([this._audioPromise, websocketPromise]).then( + this._setupSocketCallback) + .catch(e => { + log.error(`Problem with setup: ${e}`); + }); + } + + /** + * Callback to handle initial setting up of a socket. + * Currently we send a setup message (only contains sample rate) but might + * be useful to send more data so we can do quota stuff. + * @param {Array} values The + */ + _setupSocketCallback (values) { + this._micStream = values[0]; + this._socket = values[1].target; + + this._socket.addEventListener('error', e => { + log.error(`Error from web socket: ${e}`); + }); + + // Send the initial configuration message. When the server acknowledges + // it, start streaming the audio bytes to the server and listening for + // transcriptions. + this._socket.addEventListener('message', this._socketMessageCallback, {once: true}); + const langCode = this._getViewerLanguageCode(); + this._socket.send(JSON.stringify( + { + sampleRate: this._context.sampleRate, + phrases: this._phraseList, + locale: langCode + } + )); + } + + /** + * Do setup so we can start streaming mic data. + * @private + */ + _startByteStream () { + // Hook up the scriptNode to the mic + this._sourceNode = this._context.createMediaStreamSource(this._micStream); + this._sourceNode.connect(this._scriptNode); + this._scriptNode.addEventListener('audioprocess', this._processAudioCallback); + this._scriptNode.connect(this._context.destination); + } + + /** + * Called when we have data from the microphone. Takes that data and ships + * it off to the speech server for transcription. + * @param {audioProcessingEvent} e The event with audio data in it. + * @private + */ + _processAudioCallback (e) { + if (this._socket.readyState === WebSocket.CLOSED || + this._socket.readyState === WebSocket.CLOSING) { + log.error(`Not sending data because not in ready state. State: ${this._socket.readyState}`); + // TODO: should we stop trying and reset state so it might work next time? + return; + } + const MAX_INT = Math.pow(2, 16 - 1) - 1; + const floatSamples = e.inputBuffer.getChannelData(0); + // The samples are floats in range [-1, 1]. Convert to 16-bit signed + // integer. + this._socket.send(Int16Array.from(floatSamples.map(n => n * MAX_INT))); + } + + /** + * The key to load & store a target's speech-related state. + * @type {string} + */ + static get STATE_KEY () { + return 'Scratch.speech'; + } + + /** + * @returns {object} Metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'speech2text', + name: formatMessage({ + id: 'speech.extensionName', + default: 'Speech to Text', + description: 'Name of extension that adds speech recognition blocks.' + }), + menuIconURI: menuIconURI, + blockIconURI: iconURI, + blocks: [ + { + opcode: 'listenAndWait', + text: formatMessage({ + id: 'speech.listenAndWait', + default: 'listen and wait', + // eslint-disable-next-line max-len + description: 'Start listening to the microphone and wait for a result from the speech recognition system.' + }), + blockType: BlockType.COMMAND + }, + { + opcode: 'whenIHearHat', + text: formatMessage({ + id: 'speech.whenIHear', + default: 'when I hear [PHRASE]', + // eslint-disable-next-line max-len + description: 'Event that triggers when the text entered on the block is recognized by the speech recognition system.' + }), + blockType: BlockType.HAT, + arguments: { + PHRASE: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'speech.defaultWhenIHearValue', + default: 'let\'s go', + description: 'The default phrase/word that, when heard, triggers the event.' + }) + } + } + }, + { + opcode: 'getSpeech', + text: formatMessage({ + id: 'speech.speechReporter', + default: 'speech', + description: 'Get the text of spoken words transcribed by the speech recognition system.' + }), + blockType: BlockType.REPORTER + } + ] + }; + } + + /** + * Start the listening process if it isn't already in progress. + * @return {Promise} A promise that will resolve when listening is complete. + */ + listenAndWait () { + this._phraseList = this._scanBlocksForPhraseList(); + this._resetEdgeTriggerUtterance(); + + const speechPromise = new Promise(resolve => { + const listeningInProgress = this._speechPromises.length > 0; + this._speechPromises.push(resolve); + if (!listeningInProgress) { + this._startListening(); + } + }); + return speechPromise; + } + + /** + * An edge triggered hat block to listen for a specific phrase. + * @param {object} args - the block arguments. + * @return {boolean} true if the phrase matches what was transcribed. + */ + whenIHearHat (args) { + return this._speechMatches(args.PHRASE, this._utteranceForEdgeTrigger); + } + + /** + * Reporter for the last heard phrase/utterance. + * @return {string} The lastest thing we heard from a listen and wait block. + */ + getSpeech () { + return this._currentUtterance; + } +} +module.exports = Scratch3Speech2TextBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_text2speech/index.js b/local-scratch-vm/src/extensions/scratch3_text2speech/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f4cff935e80664e72955de56ea862f35355e3644 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_text2speech/index.js @@ -0,0 +1,910 @@ +const formatMessage = require('format-message'); +const languageNames = require('scratch-translate-extension-languages'); + +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); +const MathUtil = require('../../util/math-util'); +const Clone = require('../../util/clone'); +const log = require('../../util/log'); +const fetchWithTimeout = require('../../util/fetch-with-timeout'); + +/** + * Icon svg to be displayed in the blocks category menu, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const menuIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjIgKDY3MTQ1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5FeHRlbnNpb25zL1NvZnR3YXJlL1RleHQtdG8tU3BlZWNoLU1lbnU8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZyBpZD0iRXh0ZW5zaW9ucy9Tb2Z0d2FyZS9UZXh0LXRvLVNwZWVjaC1NZW51IiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyBpZD0idGV4dDJzcGVlY2giIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIuMDAwMDAwLCAyLjAwMDAwMCkiIGZpbGwtcnVsZT0ibm9uemVybyI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik01Ljc1LDguODM0NjcxNzMgQzUuNzUsOC4zMjY5NjM0NCA1LjAwMzAwNzI3LDguMDQyMjEzNzEgNC41NTYyODAxMiw4LjQ0NDE0OTk5IEwzLjIwNjI4MDEyLDkuNTI1MzU3MDIgQzIuNjk2NzMzNzgsOS45MzM0NDk2OCAyLjAzNzQ4Njc1LDEwLjE2NTg3ODggMS4zNSwxMC4xNjU4Nzg4IEwxLjE1LDEwLjE2NTg3ODggQzAuNjMyNTk2MTY1LDEwLjE2NTg3ODggMC4yNSwxMC41MTA2MDAyIDAuMjUsMTAuOTUyMDM1NSBMMC4yNSwxMy4wNjkzOTkzIEMwLjI1LDEzLjUxMDgzNDYgMC42MzI1OTYxNjUsMTMuODU1NTU2IDEuMTUsMTMuODU1NTU2IEwxLjM1LDEzLjg1NTU1NiBDMi4wNzg3Nzg0MSwxMy44NTU1NTYgMi43MjY4NjE2MSwxNC4wNjY3NjM2IDMuMjU5ODYwNDksMTQuNDk5IEw0LjU1OTIwMTQ3LDE1LjU3OTY2MDggQzUuMDEzMDkyNzYsMTUuOTU0NTM5NiA1Ljc1LDE1LjY3MzYzNDQgNS43NSwxNS4xNDE3MTI4IEw1Ljc1LDguODM0NjcxNzMgWiIgaWQ9InNwZWFrZXIiIHN0cm9rZS1vcGFjaXR5PSIwLjE1IiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMC41IiBmaWxsPSIjNEQ0RDREIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMC43MDQ4MzEzLDggQzkuNzkwNjc0NjgsOS4xMzExNDg0NyA4LjMwNjYxODQsOS43MTQyODU3MSA3LjgzMzMzMzMzLDkuNzE0Mjg1NzEgQzcuODMzMzMzMzMsOS43MTQyODU3MSA3LjUsOS43MTQyODU3MSA3LjUsOS4zODA5NTIzOCBDNy41LDkuMDg1MjI2ODQgOC4wNjIyMDE2OCw4LjkwMTk0MTY0IDguMTg5MDYwNjcsNy41Njc1NDA1OCBDNi44ODk5Njk5MSw2LjkwNjc5MDA1IDYsNS41NTczMjY4MyA2LDQgQzYsMS43OTA4NjEgNy43OTA4NjEsNC4wNTgxMjI1MWUtMTYgMTAsMCBMMTIsMCBDMTQuMjA5MTM5LC00LjA1ODEyMjUxZS0xNiAxNiwxLjc5MDg2MSAxNiw0IEMxNiw2LjIwOTEzOSAxNC4yMDkxMzksOCAxMiw4IEwxMC43MDQ4MzEzLDggWiIgaWQ9InNwZWVjaCIgZmlsbD0iIzBFQkQ4QyI+PC9wYXRoPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+'; + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIwIDAgNDAgNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjIgKDY3MTQ1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5FeHRlbnNpb25zL1NvZnR3YXJlL1RleHQtdG8tU3BlZWNoLUJsb2NrPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGcgaWQ9IkV4dGVuc2lvbnMvU29mdHdhcmUvVGV4dC10by1TcGVlY2gtQmxvY2siIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1vcGFjaXR5PSIwLjE1Ij4KICAgICAgICA8ZyBpZD0idGV4dDJzcGVlY2giIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuMDAwMDAwLCA0LjAwMDAwMCkiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjMDAwMDAwIj4KICAgICAgICAgICAgPHBhdGggZD0iTTExLjUsMTcuNjY5MzQzNSBDMTEuNSwxNi42NTM5MjY5IDEwLjAwNjAxNDUsMTYuMDg0NDI3NCA5LjExMjU2MDI0LDE2Ljg4ODMgTDYuNDEyNTYwMjQsMTkuMDUwNzE0IEM1LjM5MzQ2NzU1LDE5Ljg2Njg5OTQgNC4wNzQ5NzM1MSwyMC4zMzE3NTc1IDIuNywyMC4zMzE3NTc1IEwyLjMsMjAuMzMxNzU3NSBDMS4yNjUxOTIzMywyMC4zMzE3NTc1IDAuNSwyMS4wMjEyMDAzIDAuNSwyMS45MDQwNzEgTDAuNSwyNi4xMzg3OTg2IEMwLjUsMjcuMDIxNjY5MyAxLjI2NTE5MjMzLDI3LjcxMTExMiAyLjMsMjcuNzExMTEyIEwyLjcsMjcuNzExMTEyIEM0LjE1NzU1NjgyLDI3LjcxMTExMiA1LjQ1MzcyMzIyLDI4LjEzMzUyNzEgNi41MTk3MjA5OCwyOC45OTggTDkuMTE4NDAyOTMsMzEuMTU5MzIxNiBDMTAuMDI2MTg1NSwzMS45MDkwNzkzIDExLjUsMzEuMzQ3MjY4OSAxMS41LDMwLjI4MzQyNTUgTDExLjUsMTcuNjY5MzQzNSBaIiBpZD0ic3BlYWtlciIgZmlsbD0iIzRENEQ0RCI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMjEuNjQzNjA2NiwxNi41IEMxOS45NzcwMDk5LDE4LjQzNzAyMzQgMTcuMTA1MDI3NSwxOS45Mjg1NzE0IDE1LjY2NjY2NjcsMTkuOTI4NTcxNCBDMTUuNTEyNjM5NywxOS45Mjg1NzE0IDE1LjMxNjYyOTIsMTkuODk1OTAzIDE1LjEwOTcyNjUsMTkuNzkyNDUxNyBDMTQuNzM3NjAzOSwxOS42MDYzOTA0IDE0LjUsMTkuMjQ5OTg0NiAxNC41LDE4Ljc2MTkwNDggQzE0LjUsMTguNjU2ODA0MSAxNC41MTcwNTU1LDE4LjU1NDUwNzYgMTQuNTQ5NDQ2NywxOC40NTQwODQ0IEMxNC42MjU3NTQ1LDE4LjIxNzUwNjMgMTUuMTczNTcyMSwxNy40Njc1MzEgMTUuMjc3MjA3MSwxNy4yODA5ODgxIEMxNS41NDYzNTI2LDE2Ljc5NjUyNjEgMTUuNzM5MDI1LDE2LjIwNjM1NjEgMTUuODQzMjg5MSwxNS40MTYwMDM0IEMxMy4xODk3MDA1LDEzLjkyNjgzNjkgMTEuNSwxMS4xMTM5NjY4IDExLjUsOCBDMTEuNSwzLjMwNTU3OTYzIDE1LjMwNTU3OTYsLTAuNSAyMCwtMC41IEwyNCwtMC41IEMyOC42OTQ0MjA0LC0wLjUgMzIuNSwzLjMwNTU3OTYzIDMyLjUsOCBDMzIuNSwxMi42OTQ0MjA0IDI4LjY5NDQyMDQsMTYuNSAyNCwxNi41IEwyMS42NDM2MDY2LDE2LjUgWiIgaWQ9InNwZWVjaCIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+'; + +/** + * The url of the synthesis server. + * @type {string} + */ +const SERVER_HOST = 'https://synthesis-service.scratch.mit.edu'; + +/** + * pm: The url of the extra TTS server. + * @type {string} + */ +const PM_SERVER_HOST = 'https://gextapi.derpygamer2142.com'; + +/** + * How long to wait in ms before timing out requests to synthesis server. + * @type {int} + */ +const SERVER_TIMEOUT = 10000; // 10 seconds + +/** + * Volume for playback of speech sounds, as a percentage. + * @type {number} + */ +const SPEECH_VOLUME = 250; + +/** + * An id for one of the voices. + */ +const ALTO_ID = 'ALTO'; + +/** + * An id for one of the voices. + */ +const TENOR_ID = 'TENOR'; + +/** + * An id for one of the voices. + */ +const SQUEAK_ID = 'SQUEAK'; + +/** + * An id for one of the voices. + */ +const GIANT_ID = 'GIANT'; + +/** + * An id for one of the voices. + */ +const KITTEN_ID = 'KITTEN'; + +/** + * An id for one of the voices. + */ +const GOOGLE_ID = 'GOOGLE'; + +/** + * Playback rate for the tenor voice, for cases where we have only a female gender voice. + */ +const FEMALE_TENOR_RATE = 0.89; // -2 semitones + +/** + * Playback rate for the giant voice, for cases where we have only a female gender voice. + */ +const FEMALE_GIANT_RATE = 0.79; // -4 semitones + +/** + * Language ids. The value for each language id is a valid Scratch locale. + */ +const ARABIC_ID = 'ar'; +const CHINESE_ID = 'zh-cn'; +const DANISH_ID = 'da'; +const DUTCH_ID = 'nl'; +const ENGLISH_ID = 'en'; +const FRENCH_ID = 'fr'; +const GERMAN_ID = 'de'; +const HINDI_ID = 'hi'; +const ICELANDIC_ID = 'is'; +const ITALIAN_ID = 'it'; +const JAPANESE_ID = 'ja'; +const KOREAN_ID = 'ko'; +const NORWEGIAN_ID = 'nb'; +const POLISH_ID = 'pl'; +const PORTUGUESE_BR_ID = 'pt-br'; +const PORTUGUESE_ID = 'pt'; +const ROMANIAN_ID = 'ro'; +const RUSSIAN_ID = 'ru'; +const SPANISH_ID = 'es'; +const SPANISH_419_ID = 'es-419'; +const SWEDISH_ID = 'sv'; +const TURKISH_ID = 'tr'; +const WELSH_ID = 'cy'; + +const clampToAudioLimits = (num) => { + // these limits are based on the chromium & firefox audio element limits + return Math.min(Math.max(num, 0.0625), 16); +}; + +/** + * Class for the text2speech blocks. + * @constructor + */ +class Scratch3Text2SpeechBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * Map of soundPlayers by sound id. + * @type {Map} + */ + this._soundPlayers = new Map(); + + this._stopAllSpeech = this._stopAllSpeech.bind(this); + if (this.runtime) { + this.runtime.on('PROJECT_STOP_ALL', this._stopAllSpeech); + } + + this._onTargetCreated = this._onTargetCreated.bind(this); + if (this.runtime) { + runtime.on('targetWasCreated', this._onTargetCreated); + } + + /** + * A list of all Scratch locales that are supported by the extension. + * @type {Array} + */ + this._supportedLocales = this._getSupportedLocales(); + } + + /** + * An object with info for each voice. + */ + get VOICE_INFO () { + return { + [ALTO_ID]: { + name: formatMessage({ + id: 'text2speech.alto', + default: 'alto', + description: 'Name for a voice with ambiguous gender.' + }), + gender: 'female', + playbackRate: 1 + }, + [TENOR_ID]: { + name: formatMessage({ + id: 'text2speech.tenor', + default: 'tenor', + description: 'Name for a voice with ambiguous gender.' + }), + gender: 'male', + playbackRate: 1 + }, + [SQUEAK_ID]: { + name: formatMessage({ + id: 'text2speech.squeak', + default: 'squeak', + description: 'Name for a funny voice with a high pitch.' + }), + gender: 'female', + playbackRate: 1.19 // +3 semitones + }, + [GIANT_ID]: { + name: formatMessage({ + id: 'text2speech.giant', + default: 'giant', + description: 'Name for a funny voice with a low pitch.' + }), + gender: 'male', + playbackRate: 0.84 // -3 semitones + }, + [KITTEN_ID]: { + name: formatMessage({ + id: 'text2speech.kitten', + default: 'kitten', + description: 'A baby cat.' + }), + gender: 'female', + playbackRate: 1.41 // +6 semitones + }, + [GOOGLE_ID]: { + name: formatMessage({ + id: 'text2speech.google', + default: 'google', + description: 'Name for a voice with ambiguous gender.' + }), + special: 'google', + gender: 'mixed', + playbackRate: 1 + }, + }; + } + + /** + * An object with information for each language. + * + * A note on the different sets of locales referred to in this extension: + * + * SCRATCH LOCALE + * Set by the editor, and used to store the language state in the project. + * Listed in l10n: https://github.com/LLK/scratch-l10n/blob/master/src/supported-locales.js + * SUPPORTED LOCALE + * A Scratch locale that has a corresponding extension locale. + * EXTENSION LOCALE + * A locale corresponding to one of the available spoken languages + * in the extension. There can be multiple supported locales for a single + * extension locale. For example, for both written versions of chinese, + * zh-cn and zh-tw, we use a single spoken language (Mandarin). So there + * are two supported locales, with a single extension locale. + * SPEECH SYNTH LOCALE + * A different locale code system, used by our speech synthesis service. + * Each extension locale has a speech synth locale. + * PENGUINMOD SYNTH LOCALE + * A different locale code system, used by PenguinMod's speech synthesis service. + * Each extension locale has a PenguinMod synth locale, and some may be the same as another locale. + */ + get LANGUAGE_INFO () { + return { + [ARABIC_ID]: { + name: 'Arabic', + locales: ['ar'], + speechSynthLocale: 'arb', + penguinmodSynthLocale: 'ar', + singleGender: true + }, + [CHINESE_ID]: { + name: 'Chinese (Mandarin)', + locales: ['zh-cn', 'zh-tw'], + speechSynthLocale: 'cmn-CN', + penguinmodSynthLocale: 'zh-cn', + singleGender: true + }, + [DANISH_ID]: { + name: 'Danish', + locales: ['da'], + speechSynthLocale: 'da-DK', + penguinmodSynthLocale: 'da', + }, + [DUTCH_ID]: { + name: 'Dutch', + locales: ['nl'], + speechSynthLocale: 'nl-NL', + penguinmodSynthLocale: 'nl', + }, + [ENGLISH_ID]: { + name: 'English', + locales: ['en'], + speechSynthLocale: 'en-US', + penguinmodSynthLocale: 'en', + }, + [FRENCH_ID]: { + name: 'French', + locales: ['fr'], + speechSynthLocale: 'fr-FR', + penguinmodSynthLocale: 'fr', + }, + [GERMAN_ID]: { + name: 'German', + locales: ['de'], + speechSynthLocale: 'de-DE', + penguinmodSynthLocale: 'de', + }, + [HINDI_ID]: { + name: 'Hindi', + locales: ['hi'], + speechSynthLocale: 'hi-IN', + penguinmodSynthLocale: 'hi', + singleGender: true + }, + [ICELANDIC_ID]: { + name: 'Icelandic', + locales: ['is'], + speechSynthLocale: 'is-IS', + penguinmodSynthLocale: 'is', + }, + [ITALIAN_ID]: { + name: 'Italian', + locales: ['it'], + speechSynthLocale: 'it-IT', + penguinmodSynthLocale: 'it', + }, + [JAPANESE_ID]: { + name: 'Japanese', + locales: ['ja', 'ja-hira'], + speechSynthLocale: 'ja-JP', + penguinmodSynthLocale: 'ja', + }, + [KOREAN_ID]: { + name: 'Korean', + locales: ['ko'], + speechSynthLocale: 'ko-KR', + penguinmodSynthLocale: 'ko', + singleGender: true + }, + [NORWEGIAN_ID]: { + name: 'Norwegian', + locales: ['nb', 'nn'], + speechSynthLocale: 'nb-NO', + penguinmodSynthLocale: 'no', + singleGender: true + }, + [POLISH_ID]: { + name: 'Polish', + locales: ['pl'], + speechSynthLocale: 'pl-PL', + penguinmodSynthLocale: 'pl', + }, + [PORTUGUESE_BR_ID]: { + name: 'Portuguese (Brazilian)', + locales: ['pt-br'], + speechSynthLocale: 'pt-BR', + penguinmodSynthLocale: 'pt-br', + }, + [PORTUGUESE_ID]: { + name: 'Portuguese (European)', + locales: ['pt'], + speechSynthLocale: 'pt-PT', + penguinmodSynthLocale: 'pt', + }, + [ROMANIAN_ID]: { + name: 'Romanian', + locales: ['ro'], + speechSynthLocale: 'ro-RO', + penguinmodSynthLocale: 'ro', + singleGender: true + }, + [RUSSIAN_ID]: { + name: 'Russian', + locales: ['ru'], + speechSynthLocale: 'ru-RU', + penguinmodSynthLocale: 'ru', + }, + [SPANISH_ID]: { + name: 'Spanish (European)', + locales: ['es'], + speechSynthLocale: 'es-ES', + penguinmodSynthLocale: 'es-es', + }, + [SPANISH_419_ID]: { + name: 'Spanish (Latin American)', + locales: ['es-419'], + speechSynthLocale: 'es-US', + penguinmodSynthLocale: 'es-us', + }, + [SWEDISH_ID]: { + name: 'Swedish', + locales: ['sv'], + speechSynthLocale: 'sv-SE', + penguinmodSynthLocale: 'sv', + singleGender: true + }, + [TURKISH_ID]: { + name: 'Turkish', + locales: ['tr'], + speechSynthLocale: 'tr-TR', + penguinmodSynthLocale: 'tr', + singleGender: true + }, + [WELSH_ID]: { + name: 'Welsh', + locales: ['cy'], + speechSynthLocale: 'cy-GB', + penguinmodSynthLocale: 'cy', + singleGender: true + } + }; + } + + /** + * An array of IDs that are the voices that will only work on PenguinMod's API. + */ + get PENGUINMOD_VOICES () { + return [ + GOOGLE_ID + ]; + } + /** + * Key-value pairs for turning a voice ID into the parameter for the PenguinMod API. + */ + get PENGUINMOD_VOICE_MAP () { + return { + [GOOGLE_ID]: 'google' + }; + } + /** + * Key-value pairs for getting a nice volume setting for a specific PenguinMod voice. + * The volumes are a percentage number like 100 for 100% volume. + */ + get PENGUINMOD_VOICE_VOLUMES () { + return { + [GOOGLE_ID]: 100 + }; + } + + /** + * The key to load & store a target's text2speech state. + * @return {string} The key. + */ + static get STATE_KEY () { + return 'Scratch.text2speech'; + } + + /** + * The default state, to be used when a target has no existing state. + * @type {Text2SpeechState} + */ + static get DEFAULT_TEXT2SPEECH_STATE () { + return { + voiceId: ALTO_ID + }; + } + + /** + * A default language to use for speech synthesis. + * @type {string} + */ + get DEFAULT_LANGUAGE () { + return ENGLISH_ID; + } + + /** + * @param {Target} target - collect state for this target. + * @returns {Text2SpeechState} the mutable state associated with that target. This will be created if necessary. + * @private + */ + _getState (target) { + let state = target.getCustomState(Scratch3Text2SpeechBlocks.STATE_KEY); + if (!state) { + state = Clone.simple(Scratch3Text2SpeechBlocks.DEFAULT_TEXT2SPEECH_STATE); + target.setCustomState(Scratch3Text2SpeechBlocks.STATE_KEY, state); + } + return state; + } + + /** + * When a Target is cloned, clone the 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 state = sourceTarget.getCustomState(Scratch3Text2SpeechBlocks.STATE_KEY); + if (state) { + newTarget.setCustomState(Scratch3Text2SpeechBlocks.STATE_KEY, Clone.simple(state)); + } + } + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + // Only localize the default input to the "speak" block if we are in a + // supported language. + let defaultTextToSpeak = 'hello'; + if (this.isSupportedLanguage(this.getEditorLanguage())) { + defaultTextToSpeak = formatMessage({ + id: 'text2speech.defaultTextToSpeak', + default: 'hello', + description: 'hello: the default text to speak' + }); + } + + return { + id: 'text2speech', + name: formatMessage({ + id: 'text2speech.categoryName', + default: 'Text to Speech', + description: 'Name of the Text to Speech extension.' + }), + blockIconURI: blockIconURI, + menuIconURI: menuIconURI, + blocks: [ + { + opcode: 'speakAndWait', + text: formatMessage({ + id: 'text2speech.speakAndWaitBlock', + default: 'speak [WORDS]', + description: 'Speak some words.' + }), + blockType: BlockType.COMMAND, + arguments: { + WORDS: { + type: ArgumentType.STRING, + defaultValue: defaultTextToSpeak + } + } + }, + { + opcode: 'setVoice', + text: formatMessage({ + id: 'text2speech.setVoiceBlock', + default: 'set voice to [VOICE]', + description: 'Set the voice for speech synthesis.' + }), + blockType: BlockType.COMMAND, + arguments: { + VOICE: { + type: ArgumentType.STRING, + menu: 'voices', + defaultValue: ALTO_ID + } + } + }, + { + opcode: 'setLanguage', + text: formatMessage({ + id: 'text2speech.setLanguageBlock', + default: 'set language to [LANGUAGE]', + description: 'Set the language for speech synthesis.' + }), + blockType: BlockType.COMMAND, + arguments: { + LANGUAGE: { + type: ArgumentType.STRING, + menu: 'languages', + defaultValue: this.getCurrentLanguage() + } + } + }, + { + opcode: 'setSpeed', + text: formatMessage({ + id: 'text2speech.setSpeedBlock', + default: 'set reading speed to [SPEED]%', + description: 'Set the reading speed and pitch for speech synthesis.' + }), + blockType: BlockType.COMMAND, + arguments: { + SPEED: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + } + ], + menus: { + voices: { + acceptReporters: true, + items: this.getVoiceMenu() + }, + languages: { + acceptReporters: true, + items: this.getLanguageMenu() + } + } + }; + } + + /** + * Get the language code currently set in the editor, or fall back to the + * browser locale. + * @return {string} a Scratch locale code. + */ + getEditorLanguage () { + const locale = formatMessage.setup().locale || + navigator.language || navigator.userLanguage || this.DEFAULT_LANGUAGE; + return locale.toLowerCase(); + } + + /** + * Get the language code currently set for the extension. + * @returns {string} a Scratch locale code. + */ + getCurrentLanguage () { + const stage = this.runtime.getTargetForStage(); + if (!stage) return this.DEFAULT_LANGUAGE; + // If no language has been set, set it to the editor locale (or default). + if (!stage.textToSpeechLanguage) { + this.setCurrentLanguage(this.getEditorLanguage()); + } + return stage.textToSpeechLanguage; + } + + /** + * Set the language code for the extension. + * It is stored in the stage so it can be saved and loaded with the project. + * @param {string} locale a locale code. + */ + setCurrentLanguage (locale) { + const stage = this.runtime.getTargetForStage(); + if (!stage) return; + + if (this.isSupportedLanguage(locale)) { + stage.textToSpeechLanguage = this._getExtensionLocaleForSupportedLocale(locale); + } + + // Support language names dropped onto the menu via reporter block + // such as a variable containing a language name (in any language), + // or the translate extension's language reporter. + const localeForDroppedName = languageNames.nameMap[locale.toLowerCase()]; + if (localeForDroppedName && this.isSupportedLanguage(localeForDroppedName)) { + stage.textToSpeechLanguage = + this._getExtensionLocaleForSupportedLocale(localeForDroppedName); + } + + // If the language is null, set it to the default language. + // This can occur e.g. if the extension was loaded with the editor + // set to a language that is not in the list. + if (!stage.textToSpeechLanguage) { + stage.textToSpeechLanguage = this.DEFAULT_LANGUAGE; + } + } + + /** + * Get the extension locale for a supported locale, or null. + * @param {string} locale a locale code. + * @returns {?string} a locale supported by the extension. + */ + _getExtensionLocaleForSupportedLocale (locale) { + for (const lang in this.LANGUAGE_INFO) { + if (this.LANGUAGE_INFO[lang].locales.includes(locale)) { + return lang; + } + } + log.error(`cannot find extension locale for locale ${locale}`); + } + + /** + * Get the locale code used by the speech synthesis server corresponding to + * the current language code set for the extension. + * @returns {string} a speech synthesis locale. + */ + _getSpeechSynthLocale () { + let speechSynthLocale = this.LANGUAGE_INFO[this.DEFAULT_LANGUAGE].speechSynthLocale; + if (this.LANGUAGE_INFO[this.getCurrentLanguage()]) { + speechSynthLocale = this.LANGUAGE_INFO[this.getCurrentLanguage()].speechSynthLocale; + } + return speechSynthLocale; + } + + /** + * Get the locale code used by the PenguinMod TTS server corresponding to + * the current language code set for the extension. + * @returns {string} a PenguinMod TTS locale. + */ + _getPenguinModSynthLocale () { + let speechSynthLocale = this.LANGUAGE_INFO[this.DEFAULT_LANGUAGE].penguinmodSynthLocale; + if (this.LANGUAGE_INFO[this.getCurrentLanguage()]) { + speechSynthLocale = this.LANGUAGE_INFO[this.getCurrentLanguage()].penguinmodSynthLocale; + } + return speechSynthLocale; + } + + /** + * Get an array of the locales supported by this extension. + * @returns {Array} An array of locale strings. + */ + _getSupportedLocales () { + return Object.keys(this.LANGUAGE_INFO).reduce((acc, lang) => + acc.concat(this.LANGUAGE_INFO[lang].locales), []); + } + + /** + * Check if a Scratch language code is in the list of supported languages for the + * speech synthesis service. + * @param {string} languageCode the language code to check. + * @returns {boolean} true if the language code is supported. + */ + isSupportedLanguage (languageCode) { + return this._supportedLocales.includes(languageCode); + } + + /** + * Get the menu of voices for the "set voice" block. + * @return {array} the text and value for each menu item. + */ + getVoiceMenu () { + return Object.keys(this.VOICE_INFO).map(voiceId => ({ + text: this.VOICE_INFO[voiceId].name, + value: voiceId + })); + } + + /** + * Get the localized menu of languages for the "set language" block. + * For each language: + * if there is a custom translated spoken language name, use that; + * otherwise use the translation in the languageNames menuMap; + * otherwise fall back to the untranslated name in LANGUAGE_INFO. + * @return {array} the text and value for each menu item. + */ + getLanguageMenu () { + const editorLanguage = this.getEditorLanguage(); + // Get the array of localized language names + const localizedNameMap = {}; + let nameArray = languageNames.menuMap[editorLanguage]; + if (nameArray) { + // Also get any localized names of spoken languages + let spokenNameArray = []; + if (languageNames.spokenLanguages) { + spokenNameArray = languageNames.spokenLanguages[editorLanguage]; + nameArray = nameArray.concat(spokenNameArray); + } + // Create a map of language code to localized name + // The localized spoken language names have been concatenated onto + // the end of the name array, so the result of the forEach below is + // when there is both a written language name (e.g. 'Chinese + // (simplified)') and a spoken language name (e.g. 'Chinese + // (Mandarin)', we always use the spoken version. + nameArray.forEach(lang => { + localizedNameMap[lang.code] = lang.name; + }); + } + + return Object.keys(this.LANGUAGE_INFO).map(key => { + let name = this.LANGUAGE_INFO[key].name; + const localizedName = localizedNameMap[key]; + if (localizedName) { + name = localizedName; + } + // Uppercase the first character of the name + name = name.charAt(0).toUpperCase() + name.slice(1); + return { + text: name, + value: key + }; + }); + } + + /** + * Set the voice for speech synthesis for this sprite. + * @param {object} args Block arguments + * @param {object} util Utility object provided by the runtime. + */ + setVoice (args, util) { + const state = this._getState(util.target); + + let voice = args.VOICE; + + // If the arg is a dropped number, treat it as a voice index + let voiceNum = parseInt(voice, 10); + if (!isNaN(voiceNum)) { + voiceNum -= 1; // Treat dropped args as one-indexed + voiceNum = MathUtil.wrapClamp(voiceNum, 0, Object.keys(this.VOICE_INFO).length - 1); + voice = Object.keys(this.VOICE_INFO)[voiceNum]; + } + + // Only set the voice if the arg is a valid voice id. + if (Object.keys(this.VOICE_INFO).includes(voice)) { + state.voiceId = voice; + } + } + + /** + * Set the language for speech synthesis. + * @param {object} args Block arguments + */ + setLanguage (args) { + this.setCurrentLanguage(args.LANGUAGE); + } + + setSpeed (args, util) { + const state = this._getState(util.target); + const speed = Cast.toNumber(args.SPEED) / 100; + // ideally no core blocks should cause errors + state.speed = clampToAudioLimits(speed); + } + + /** + * Stop all currently playing speech sounds. + */ + _stopAllSpeech () { + this._soundPlayers.forEach(player => { + player.stop(); + }); + } + + /** + * Convert the provided text into a sound file and then play the file. + * @param {object} args Block arguments + * @param {object} util Utility object provided by the runtime. + * @return {Promise} A promise that resolves after playing the sound + */ + speakAndWait (args, util) { + // Cast input to string + let words = Cast.toString(args.WORDS); + let locale = this._getSpeechSynthLocale(); + + const state = this._getState(util.target); + + let gender = this.VOICE_INFO[state.voiceId].gender; + let playbackRate = this.VOICE_INFO[state.voiceId].playbackRate; + + // Special case for voices where the synthesis service only provides a + // single gender voice. In that case, always request the female voice, + // and set special playback rates for the tenor and giant voices. + if (this.LANGUAGE_INFO[this.getCurrentLanguage()].singleGender) { + gender = 'female'; + if (state.voiceId === TENOR_ID) { + playbackRate = FEMALE_TENOR_RATE; + } + if (state.voiceId === GIANT_ID) { + playbackRate = FEMALE_GIANT_RATE; + } + } + + if (state.voiceId === KITTEN_ID) { + words = words.replace(/\S+/g, 'meow'); + locale = this.LANGUAGE_INFO[this.DEFAULT_LANGUAGE].speechSynthLocale; + } + + let isPenguinMod = false; + let penguinModVoice = ''; + let speechVolume = SPEECH_VOLUME; + if (this.PENGUINMOD_VOICES.includes(state.voiceId)) { + // This is a PenguinMod voice and has to be handled differently. + isPenguinMod = true; + locale = this._getPenguinModSynthLocale(); + penguinModVoice = this.PENGUINMOD_VOICE_MAP[state.voiceId]; + speechVolume = this.PENGUINMOD_VOICE_VOLUMES[state.voiceId]; + } + + // Build up URL + let path = ''; + if (isPenguinMod) { + path = `${PM_SERVER_HOST}/tts`; + } else { + path = `${SERVER_HOST}/synth`; + } + if (isPenguinMod) { + path += `?lang=${locale}`; + path += `&voice=${penguinModVoice}`; + } else { + path += `?locale=${locale}`; + } + path += `&gender=${gender}`; + // this textLimit is enforced on the API, no point in increasing it here + let textLimit = 128; + if (isPenguinMod) { + textLimit = 512; + } + path += `&text=${encodeURIComponent(words.substring(0, textLimit))}`; + + if (typeof state.speed === 'number') { + playbackRate *= state.speed; + playbackRate = clampToAudioLimits(playbackRate); + } + + // Perform HTTP request to get audio file + return fetchWithTimeout(path, {}, SERVER_TIMEOUT) + .then(res => { + if (res.status !== 200) { + throw new Error(`HTTP ${res.status} error reaching translation service`); + } + + return res.arrayBuffer(); + }) + .then(buffer => { + // Play the sound + const sound = { + data: { + buffer + } + }; + return this.runtime.audioEngine.decodeSoundPlayer(sound); + }) + .then(soundPlayer => { + this._soundPlayers.set(soundPlayer.id, soundPlayer); + + soundPlayer.setPlaybackRate(playbackRate); + + // Increase the volume + const engine = this.runtime.audioEngine; + const chain = engine.createEffectChain(); + chain.set('volume', speechVolume); + soundPlayer.connect(chain); + + soundPlayer.play(); + return new Promise(resolve => { + soundPlayer.on('stop', () => { + this._soundPlayers.delete(soundPlayer.id); + resolve(); + }); + }); + }) + .catch(err => { + log.warn(err); + }); + } +} +module.exports = Scratch3Text2SpeechBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_translate/index.js b/local-scratch-vm/src/extensions/scratch3_translate/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e072710efe3ecca210c5811aa5a69a599a6e4f78 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_translate/index.js @@ -0,0 +1,286 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); +const log = require('../../util/log'); +const fetchWithTimeout = require('../../util/fetch-with-timeout'); +const languageNames = require('scratch-translate-extension-languages'); +const formatMessage = require('format-message'); + +/** + * Icon svg to be displayed in the blocks category menu, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const menuIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAACXBIWXMAABYlAAAWJQFJUiTwAAAGAklEQVRYhe1YbUxTVxh+rh02o0KtkOEgKA4U4yeRWCdgxDoxCnH6h22iqSz76aasZlnijzkTBlvS4TJ/LGaJsmiyESe4hAVJvMJGxwQhLKECcRWkpWNZERs6Ctb2Lm97C/fe3n6Jyfzhk5y09z3nPPe57znnPe85DMdxeJ6x6LlW90LgM8BLchR1dXUZeXl5b3Ect+ppXsEwzHBfX98PVVVVY0GbmjW2AdgpaFYP4JxTZ+iLyCVdJFeuXNmdn59fn56enrFkyRIsWhSfk30+H1wuF+x2+1hPT4++oqLiJi/wEoA8AJslXSqdOsOlmARWV1dnlpeXd2ZnZ2fEK0xOqMViGWtoaNh++vRpa9CuZo1ZAJokQlc5dYYROR6RCq1WW56WlhZV3H0H8O9sZIHEQVzEKbTzQooBPBCYz4TlET4oFIosGtZoOHUN+Ph61GYgLuIU2tSscSmAYwAeCcx6NWs8o2aNxVKOkEUi9R55qv428Ng7b3viA/6eAs7dmrctVgD6bYBKGZ6LB4mrk7F/whcmokApfh8BWu6G2mc8ADsktuWmAbtzozGiLUJdu9QQVSC98JUkYNgBfPsboH4Z+GhPoK62FZiaAU7sCrTZmB5VHM3BPjVrrARwUVL1B4CD0vYxLVV68YFNQIICcLrn7SROtTjwEbGIE4iksFIpEVfs1BkeSdvGFUsObAz8Gm8CNTcC/49q42EIEbkLwKfhxCGWIRZC/zrQ/ifgcAWMK5YB+zc8nUBeZFuUORmfQIp/PsHGM/04YMta5oPT6cTs7Cw8Ho+oj9vtzmloaCgPZQtApVI96ejo6K2trR3lOM4nrRftJCzLfq3T6Y7LCfvuNtDL7wepfKgkTz6ZdeHdzePYlq30xz2lUintHhH0UbQ12my2+oKCguMcx7mE7aOHmWHgsxvzzzQP3ysMxMfzt2bxKmNHyZblSE5OjktYEImJidBoNFCr1frOzs5khmHe4Thubp8SCVQoFBwNUUJCwpyNwsfyZGBDOvB2fuCZQAH56KYJKJUpTy1OCOJYvXr1ocbGxjIAPwarRKvYZrNdn5iYEHV8LRW4cBj4oHheXBDT09PPRFwQxKXRaIQpmVjgkSNHfrFardcmJydjIqSMRehtOfzjmMTZmm/8hf5HAnF5vV7RVicSyHGcR6vVHh4YGPjKYrFMkTelq5JAH0B1MzMzUT+iu6cfdwfv+wv9jxchgZomaEFBwcmcnJxVY2NjXQqFQlQ/Pj6O/v7+s2az+U2Hw9Ec7X3tHXfm/v/c2hG3wLCruLm5+VBGRoY2mJGQJ0nc4ODgqZKSkjqKWSzL7olEPjJqx4PRv5CaqvE/OxyTflvWitj3xbBbnUql2kRxjYTRcA4MDHR1d3frguJiIW//NeC9/SVF2LplvcgWK8J6sKWl5UuVSrXO4/HYHj58+FNZWVkLx3HT8Rz0u3vN/t8Ho3aRaH3FgYULrKmpodT8jeBzvDcQ3T1m/5D6RXX0zNmn3TP+uq356xcmkE/NTwLoc+oMTXGpA3CnN7Bi99Hw5s8PL4mlulgFys5BXlwbn4I3qlnjsXgFBr22f+8OrFub7S/79u4Q1cWCEA8KxAmPhRfVrBFy51cK1nJnj+/rvwix0eqVswu5pJDzoPTMKhSZJzQolUoLZSLPCsRFnEI6OYE7I7xPdGYoKiq6YLVaByllWiiIg7iIM5rAYBouBB2yq5w6g+iATWGnqampZGhoqItiJSUP4YrcR9CQUh31JQ7iIk5hm7AXmPxdip5/dNIUCnduYBgm8fLly9tzc3NLwzlSqVTuW7NmzVphQkubwL179+xdXV3HKisrTVJxiJKwnuGHVM2XNjVrPCh3h8IT3+SLLKqrq+tKS0uvrly5UksJKvjsJSkpKd3r9TrkxCGSBxHwIoWXc7zAIOiIOOLUGULOsNHAMIzSZDJ9npmZeSIlJcWfTdPQm0ym8zqd7n257hGPnXxYyePv8py8mVb40ji1+UGZUmFh4Yetra1bzGbzteHh4SlKQNxu961wff7XS3Sau/w0c4VLQF7c8i8IAP4DcHKth/4Ur7MAAAAASUVORK5CYII='; + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAACXBIWXMAABYlAAAWJQFJUiTwAAAN+UlEQVR4Ae1ce2xT1xn/Tkhq4hqHJKRLDAlQGI+GUfFc14HaLmxuGd0ab93GgK6Vmm01y9BUsaU0RfyRFTakaRHq3So6jVapWEUxa9dRuU8x6IAGCoO6wa1KXiSQOE9jkjivO/2u7yWOuff6XvvekFb5SUdx7ON7v/vz9zrnO+cwnudpAokjZYK75DBBYJKYIDBJTBCYJCYITBJfOgIZYzbGWA5jLJ8xNm/z5s334a/4P1omYyzNsPt9WdIYxhiUId/j8azPz89fY7VaF6ampjqi+4TD4Qvd3d0f7t+/fx/HcTVE1M7z/EBS99VDoCikTWzWMdTgYSLqIaIQGs/zwzKyOaqrq1+aOnVqUUZGBqWnp5PFYqG0tDQaHh6mvr4+GhwcpFAoJLSenp4P9+7dW8Zx3Fme5zsTFSxVa0eovtPpnL1ly5YfzJgxw2WxWBYketNEAO25dOmSp7Ky8iBjrFbmoW12u70oOzubMjMzR32QkpJCVqtVeG2322lgYIACgcDK0tLS1+6+++4yxpiH5/krCQkGDYzXiCi3qqrKXVNTE7x48SLf0dHB9/X18WMF3Av3xL0hA2SBTNFyE9E8v9/PX758WbNUuCa+4/F4ymKvp7XF1UDGWK7H43m0sLBwZ05Ozg2/7lgApoiGe3d2dk5ZsWLFcx6Px84Y2xelOcHW1taXiOiRcDgsaJ2gljab0GDKscD1Jk2ahHd3ejwePOtenufb9TySKoEw26qqKhfIy83NFdT/ZiP6oauqqoKMsf2iObeuXr26vKKiwpOVlTXFbrfb8/LyFubk5KyBu1H68aVnGh4e3uZ2u08yxo7pCixKqgrX4XQ6l8JkoOrjDZAJskFGyBolt0UMcGhgJ19yP2rm3dDQwJ85c+akXlNWI9B++PDh38PvjFdANsgIWVUfksheUVHxfZDY3d0t+zT9/f2CP3S73feJWYZFC4FqJmxDtJ06daphRnuiluhrDqJbLcZcD7JBRiJ6Dj5Qrg9jDHfLhlm3tbUdslgsj8i5IvhI+MpNmzaVLlq0yCN+9wMiCvA8H1KSQY1AK3yHFP6ThfcTouePEaVNItrzI6LbpiR/TcgmplOyQmLE4XQ6C3fv3v0y+iEQIbggjZELKiB2+vTpxXl5ecUIRD6fr/nQoUNuxtjbPM/3yN1DLREWPsNNjUBNC9HgMFEKI2q5asglo2VTeo7sioqKP6anpwtBZObMmZSfny9LHokRG5+jn8PhoFtuucVRXFzMQdmVZPiyTyakIblGBqE3/QKZIBLDQafTmavUT/NIRA3XwkSlB4jaFD3FCPqHiJ5+Xb3PNBvRnoeN85UYyiUCmDHg9XoVn8wQDTxRR9RxjQjxLl4DgfH64Fq4pgEY6OjoONTe3q6bRPTH9zBmFsfgsjBEA4vmE/kuE30eiPzfN0B0WYyJGelEWQpx6FJXhNC0FKIcG9Fk0TUVZEWuaQBad+zYUbF79+6FjY2NC+DfpBGKGkBeY2MjBYPBC2VlZU9g1sZUAoFf3zvyGia9/u8RbRrmiSofvrG/1IeEaEn0/E+NkmQEmLVhjPmIaAMisRYSo8nbunXrBq/X6+N5PqzU35QgAt8177bI655+ovPNN/Z50xeJyIyIlswwQ4oI8PAgAWSAFJCjZM56ySMzo/DP7iJKTSEaGibi/jP6M2jfwbNEQzzRpBSin68yS4oItJCYCHlkJoEYcWSKvu9KkOjwxyOf7T9F1DsQ0b6CTGOS6niQIxEJNSVBHhnpA+Xwm28RPfOvSAL9t+NEy2cShcJEr5+P+EcQ+PT9ZkowGiBF8olIsMPhcBFmrnt7e4Voi4Chhzwym0BoYWFexAcODBGVvUbU3RshbxIjWrdobLQvGhKJXq+3xO12z1q3bt2aN9544x2O45A4Neshj8wmENjmJHr8ZaJQ/0iizcRk+fFvmn13eYgk1TLGWjiOwxQWKY1148H0oRwiMohiUYky8Oz3zL5zfIA0qSV6DdM18NWPiF4+FXnNogh84h9EW9cQ3TV7pC+ceVTVTNPooa5OGLLMYkgmE0fcqp8STCPwYhvRn94jauqKBBHJbOED4Q8xAtn1FtH8rxA9WUSUNthJiQy5Zs2aRX6/35usvBqqfrJQrAujmu/3+/3z5s3TJQgCxsEzRP9riuSAUsBYXhCJysCWVyP+EHkgiA0Hr9D9Xw3S/QuJJlvShIlSca4vASr0A5MG0Piuri68vnr69OmyjRs3aip1GqaByPNePRuZCMDwTSIuPY1oy32jTfWFDUQvniB67RzRta4rtDwvSN8tJBrnVT9ZGEIgpupf+C/RgGh90CpMEKwtJFq/XH5aCiOVb0zvpH+fipD3Baj6yUKNQIEOqLcWU4IPx9ANGue6k+iBQvX5PPi6lHD7dc0bD+RJgCxDQ0O0bNmyXU6n8wRjrFsxsKhUshznzp2r0VrSbAny/OcB7eU7VMdQBfuiV/3U8sAQohIcqxZgRHH7NO2/MlIVEitr4xVRVT+bkoiqBCKkIyp1dia8eEkRiHokVtbGK+JV/VQJhM17vd5ahPRAIICZCkMfU8r3xipVSQQaqn7qURjRB0u/ENIRleBYb0aaEQ+Btk4qfXLnqF7bn/oF3bFgjun3jjsWRh7kcrn2VVdXb25pablaW1uLXOl6xWo84LD36A1SHDl6akwk05QHgkTkQwjpsQsstSa/V65cGeUGMIbFMMwIyJFVfdpHj2zopVut6YbcQwmaZ2Ngzl6v9+zatWv/sHjx4u9UVlYKAzMUoOMB5HV1dV09cODAQ/NFOJ1OpxEPAKJ6evtueB/vnTrtM+IWqtA1nYXAwvM81GgAi3CUFi5KQKBobm4WyDt48OCm8vLy93me/xQNSmjEAxw5NqJ91vTJQpNQ/dE4I5AikwxWjuPWZGVlFWM9shLgI+vr6zHDcoHjuAdF8gwN5dd6eulUFEkrlhUKTQI+Q4AxE4lMqOasWrXqafg9uRREquiDvKamppe2bdv2kz179pwwmjyS8X3Lly4SWjSqT38s/2WDkMhkggUBJHbsCuIQndH6+/ubjxw58ju32/0eXKCeCUo9iDbfadMyr2sfzFjyi4ffOkZrnatNoi8xAoX1JjabrRhDnehZZBDn9/v3uFyuV+ItTEwWdQ3NVN9w+fpVViwdMV0QeeTYaeF1W1un0HdWgcNoEQQkYsJd27dvfxa+7dKlS9TQ0NBcV1d36OTJk5sLCwtXuVwujuf5WjPJIxnzvWf18uuvY834TZk80Sjo1kBxdPLp4sWLizBnKr4NewnqLQkmg1gCy575s+LVkOo8UWKOHAlV5cSAgGmadnG/WWCsyZPL/ZSAvmaNTHRroLho2xFdlGaMXRSDxZiQGJvf3bHgdtl+n1y4OOo70WZuFHQRCPKwaHvXrl1/sVqtK7EsYs6cOdsee+yxd8vLy3+Lir/ZJCKvi879Zhbk0fanfinb91dP7hSCCEXlhDnTjJ0M0WzCEnlYZ5eZmbly7ty5woLs2bNnYzxchPfxuaihpiE2r7tnlbJWRUdmMikn1ERgNHl2u33UIkUM5fA/3h8LEpHXRWPFskWKfWNNNva7RkDLZkNF8iTgf7yPFaDoh9VPZpnzvVEaZ7VOVjVJ5H4/fOjbRoswGnG2SIG8pSgu1dXV8UNDQ6pFGHyOfugv7mFT3C5FRHNRVBrLbbN6AdkgI2RNZK+cLvIkyJCYonB9XVW/mwHIBhkhayJVuWxEWyWzVYJkzjabbcGOHTvKUbBT6Kqr6nczANkgo9o2BzVWbEhVMGWllbzrF01JIXwPU16IMwrdTK36JQuxbHEVMiZEoNPpFKaaE62axSPd7KpfMoAskAmyQUa12STFKOz1egODg4PN9fX1Dqxb0TJ1LwG/HuYEg8Hgu5i9Ueo3Hqt+kB3k+Xy+p8QVWqrmoZbGtGOrJ3YrNjc3O6StonhAOTKx4h21D0xvYTYa6+0wOlHb5UMjBat9WMiDtShdXV1TvkjL21TPjcEpQJiB5jhOWM28ZMkSV0FBQTG2gsoBJU/MQp8/f/5tt9v9gZ5F20Yeq4LJXlhNPEjaRjELLEWz1eaYNR57AlWwYTs88iJsj5cDFgxhWz2218fbhq90ToN4zgF+obk4ykRvk7b2azn+JGabf754b9m0S3ceqPCAuTiYAQc0KAGCR53tIgklHQKh6RyCZFr0+QhNTU1x81f0OX78uMe0c2Ni0I7jknDiTzAYxNEiN3SA6VgsFmGV57lz50oDgcA7fr9f2ErQ0dFxlTH2ERE1mVUnwVwlY+z9lpaWB0tKSv4aDocXwOUo+VOkW6FQqJjjOI94gpG+FfsJmFk2TvqB6iudgCGZBzJ5aCsaRif4ztGjR19M9NfWKadl3bp1d+J+uG9bW5uiNra2tkojjpm675OgcLkSiXqGYjBvcWw5z2wC+RGf6uA4bqPP52v67LPPZImMGvPqliuhNdKxqUdvb+8UrJFRW6VAN2Epm+gmmhlj/ySiDzwez4/nz59f2tnZ6ZCOhEJqhvQFlUa1nFUJCS8ylxYcud3umpKSkl2hUGilJFRqaipNnjz5+hEjyLOwoa+7u1tKrk2t2MnIKmygYYzhBI5XqqqqHpgzZ84au93+dRwqgRQGlUaxzqMLSR/AKJ4Gme12uxeuX7/+0YyMjJWxORxGND09PTWNjY3vuFyu/UTUaFYQ0SizRcwOpIU0Q2JVUf8Pa6C/AZGZYuqSL+VkYh6H/3OQS46F7xvLNnGSeZKYOMU3SUwQmCQmCEwSEwQmiQkCkwER/R+aET3lwEIlXgAAAABJRU5ErkJggg=='; + +/** + * The url of the translate server. + * @type {string} + */ +const serverURL = 'https://trampoline.turbowarp.org/translate/'; + +/** + * How long to wait in ms before timing out requests to translate server. + * @type {int} + */ +const serverTimeoutMs = 10000; // 10 seconds (chosen arbitrarily). + +/** + * Class for the translate block in Scratch 3.0. + * @constructor + */ +class Scratch3TranslateBlocks { + constructor () { + /** + * Language code of the viewer, based on their locale. + * @type {string} + * @private + */ + this._viewerLanguageCode = this.getViewerLanguageCode(); + + /** + * List of supported language name and language code pairs, for use in the block menu. + * Filled in by getInfo so it is updated when the interface language changes. + * @type {Array.>} + * @private + */ + this._supportedLanguages = []; + + /** + * A randomly selected language code, for use as the default value in the language menu. + * Properly filled in getInfo so it is updated when the interface languages changes. + * @type {string} + * @private + */ + this._randomLanguageCode = 'en'; + + + /** + * The result from the most recent translation. + * @type {string} + * @private + */ + this._translateResult = ''; + + /** + * The language of the text most recently translated. + * @type {string} + * @private + */ + this._lastLangTranslated = ''; + + /** + * The text most recently translated. + * @type {string} + * @private + */ + this._lastTextTranslated = ''; + } + + /** + * The key to load & store a target's translate state. + * @return {string} The key. + */ + static get STATE_KEY () { + return 'Scratch.translate'; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + this._supportedLanguages = this._getSupportedLanguages(this.getViewerLanguageCode()); + this._randomLanguageCode = this._supportedLanguages[ + Math.floor(Math.random() * this._supportedLanguages.length)].value; + + return { + id: 'translate', + name: formatMessage({ + id: 'translate.categoryName', + default: 'Translate', + description: 'Name of extension that adds translate blocks' + }), + blockIconURI: blockIconURI, + menuIconURI: menuIconURI, + blocks: [ + { + opcode: 'getTranslate', + text: formatMessage({ + id: 'translate.translateBlock', + default: 'translate [WORDS] to [LANGUAGE]', + description: 'translate some text to a different language' + }), + blockType: BlockType.REPORTER, + arguments: { + WORDS: { + type: ArgumentType.STRING, + defaultValue: formatMessage({ + id: 'translate.defaultTextToTranslate', + default: 'hello', + description: 'hello: the default text to translate' + }) + }, + LANGUAGE: { + type: ArgumentType.STRING, + menu: 'languages', + defaultValue: this._randomLanguageCode + } + } + }, + { + opcode: 'getViewerLanguage', + text: formatMessage({ + id: 'translate.viewerLanguage', + default: 'language', + description: 'the languge of the project viewer' + }), + blockType: BlockType.REPORTER, + arguments: {} + } + ], + menus: { + languages: { + acceptReporters: true, + items: this._supportedLanguages + } + } + }; + } + + /** + * Computes a list of language code and name pairs for the given language. + * @param {string} code The language code to get the list of language pairs + * @return {Array.>} An array of languge name and + * language code pairs. + * @private + */ + _getSupportedLanguages (code) { + return languageNames.menuMap[code].map(entry => { + const obj = {text: entry.name, value: entry.code}; + return obj; + }); + } + /** + * Get the human readable language value for the reporter block. + * @return {string} the language name of the project viewer. + */ + getViewerLanguage () { + this._viewerLanguageCode = this.getViewerLanguageCode(); + const names = languageNames.menuMap[this._viewerLanguageCode]; + let langNameObj = names.find(obj => obj.code === this._viewerLanguageCode); + + // If we don't have a name entry yet, try looking it up via the Google langauge + // code instead of Scratch's (e.g. for es-419 we look up es to get espanol) + if (!langNameObj && languageNames.scratchToGoogleMap[this._viewerLanguageCode]) { + const lookupCode = languageNames.scratchToGoogleMap[this._viewerLanguageCode]; + langNameObj = names.find(obj => obj.code === lookupCode); + } + + let langName = this._viewerLanguageCode; + if (langNameObj) { + langName = langNameObj.name; + } + return langName; + } + + /** + * Get the viewer's language code. + * @return {string} the language code. + */ + getViewerLanguageCode () { + const locale = formatMessage.setup().locale; + const viewerLanguages = [locale].concat(navigator.languages); + const languageKeys = Object.keys(languageNames.menuMap); + // Return the first entry in viewerLanguages that matches + // one of the available language keys. + const languageCode = viewerLanguages.reduce((acc, lang) => { + if (acc) { + return acc; + } + if (languageKeys.indexOf(lang.toLowerCase()) > -1) { + return lang; + } + return acc; + }, '') || 'en'; + + return languageCode.toLowerCase(); + } + + /** + * Get a language code from a block argument. The arg can be a language code + * or a language name, written in any language. + * @param {object} arg A block argument. + * @return {string} A language code. + */ + getLanguageCodeFromArg (arg) { + const languageArg = Cast.toString(arg).toLowerCase(); + // Check if the arg matches a language code in the menu. + if (languageNames.menuMap.hasOwnProperty(languageArg)) { + return languageArg; + } + // Check for a dropped-in language name, and convert to a language code. + if (languageNames.nameMap.hasOwnProperty(languageArg)) { + return languageNames.nameMap[languageArg]; + } + + // There are some languages we launched in the language menu that Scratch did not + // end up launching in. In order to keep projects that may have had that menu item + // working, check for those language codes and let them through. + // Examples: 'ab', 'hi'. + if (languageNames.previouslySupported.indexOf(languageArg) !== -1) { + return languageArg; + } + // Default to English. + return 'en'; + } + + /** + * Translates the text in the translate block to the language specified in the menu. + * @param {object} args - the block arguments. + * @return {Promise} - a promise that resolves after the response from the translate server. + */ + getTranslate (args) { + // If the text contains only digits 0-9 and nothing else, return it without + // making a request. + if (/^\d+$/.test(args.WORDS)) return Promise.resolve(args.WORDS); + + // Don't remake the request if we already have the value. + if (this._lastTextTranslated === args.WORDS && + this._lastLangTranslated === args.LANGUAGE) { + return this._translateResult; + } + + const lang = this.getLanguageCodeFromArg(args.LANGUAGE); + + let urlBase = `${serverURL}translate?language=`; + urlBase += lang; + urlBase += '&text='; + urlBase += encodeURIComponent(args.WORDS); + + const tempThis = this; + const translatePromise = fetchWithTimeout(urlBase, {}, serverTimeoutMs) + .then(response => response.text()) + .then(responseText => { + const translated = JSON.parse(responseText).result; + tempThis._translateResult = translated; + // Cache what we just translated so we don't keep making the + // same call over and over. + tempThis._lastTextTranslated = args.WORDS; + tempThis._lastLangTranslated = args.LANGUAGE; + return translated; + }) + .catch(err => { + log.warn(`error fetching translate result! ${err}`); + return ''; + }); + return translatePromise; + } +} +module.exports = Scratch3TranslateBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_video_sensing/debug.js b/local-scratch-vm/src/extensions/scratch3_video_sensing/debug.js new file mode 100644 index 0000000000000000000000000000000000000000..0dd3917ef9850c1ea1bfc4ef428f6e09580ee969 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_video_sensing/debug.js @@ -0,0 +1,13 @@ +/** + * A debug "index" module exporting VideoMotion and VideoMotionView to debug + * VideoMotion directly. + * @file debug.js + */ + +const VideoMotion = require('./library'); +const VideoMotionView = require('./view'); + +module.exports = { + VideoMotion, + VideoMotionView +}; diff --git a/local-scratch-vm/src/extensions/scratch3_video_sensing/index.js b/local-scratch-vm/src/extensions/scratch3_video_sensing/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8b5de418f7852843135dd52d4fa370e80b082ba0 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_video_sensing/index.js @@ -0,0 +1,588 @@ +const Runtime = require('../../engine/runtime'); + +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Clone = require('../../util/clone'); +const Cast = require('../../util/cast'); +const formatMessage = require('format-message'); +const Video = require('../../io/video'); + +const VideoMotion = require('./library'); + +/** + * Icon svg to be displayed in the blocks category menu, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const menuIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjIgKDY3MTQ1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5FeHRlbnNpb25zL1NvZnR3YXJlL1ZpZGVvLVNlbnNpbmctTWVudTwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxnIGlkPSJFeHRlbnNpb25zL1NvZnR3YXJlL1ZpZGVvLVNlbnNpbmctTWVudSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9InZpZGVvLW1vdGlvbiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC4wMDAwMDAsIDUuMDAwMDAwKSIgZmlsbC1ydWxlPSJub256ZXJvIj4KICAgICAgICAgICAgPGNpcmNsZSBpZD0iT3ZhbC1Db3B5IiBmaWxsPSIjMEVCRDhDIiBvcGFjaXR5PSIwLjI1IiBjeD0iMTYiIGN5PSI4IiByPSIyIj48L2NpcmNsZT4KICAgICAgICAgICAgPGNpcmNsZSBpZD0iT3ZhbC1Db3B5IiBmaWxsPSIjMEVCRDhDIiBvcGFjaXR5PSIwLjUiIGN4PSIxNiIgY3k9IjYiIHI9IjIiPjwvY2lyY2xlPgogICAgICAgICAgICA8Y2lyY2xlIGlkPSJPdmFsLUNvcHkiIGZpbGw9IiMwRUJEOEMiIG9wYWNpdHk9IjAuNzUiIGN4PSIxNiIgY3k9IjQiIHI9IjIiPjwvY2lyY2xlPgogICAgICAgICAgICA8Y2lyY2xlIGlkPSJPdmFsIiBmaWxsPSIjMEVCRDhDIiBjeD0iMTYiIGN5PSIyIiByPSIyIj48L2NpcmNsZT4KICAgICAgICAgICAgPHBhdGggZD0iTTExLjMzNTk3MzksMi4yMDk3ODgyNSBMOC4yNSw0LjIwOTk1NjQ5IEw4LjI1LDMuMDUgQzguMjUsMi4wNDQ4ODIyNyA3LjQ2ODU5MDMxLDEuMjUgNi41LDEuMjUgTDIuMDUsMS4yNSBDMS4wMzgwNzExOSwxLjI1IDAuMjUsMi4wMzgwNzExOSAwLjI1LDMuMDUgTDAuMjUsNyBDMC4yNSw3Ljk2MzY5OTM3IDEuMDQyMjQ5MTksOC43NTU5NDg1NiAyLjA1LDguOCBMNi41LDguOCBDNy40NTA4MzAwOSw4LjggOC4yNSw3Ljk3MzI3MjUgOC4yNSw3IEw4LjI1LDUuODU4NDUyNDEgTDguNjI4NjIzOTQsNi4wODU2MjY3NyBMMTEuNDI2Nzc2Nyw3Ljc3MzIyMzMgQzExLjQzNjg5NDMsNy43ODMzNDA5MSAxMS40NzU3NjU1LDcuOCAxMS41LDcuOCBDMTEuNjMzNDkzMiw3LjggMTEuNzUsNy42OTEyNjAzNCAxMS43NSw3LjU1IEwxMS43NSwyLjQgQzExLjc1LDIuNDE4MzgyNjkgMTEuNzIxOTAyOSwyLjM1MjgyMjgyIDExLjY4NTYyNjgsMi4yNzg2MjM5NCBDMTEuNjEyOTUyOCwyLjE1NzUwMDY5IDExLjQ3MDc5NjgsMi4xMjkwNjk1IDExLjMzNTk3MzksMi4yMDk3ODgyNSBaIiBpZD0idmlkZW9fMzdfIiBzdHJva2Utb3BhY2l0eT0iMC4xNSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjAuNSIgZmlsbD0iIzRENEQ0RCI+PC9wYXRoPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+'; + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIwIDAgNDAgNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjIgKDY3MTQ1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5FeHRlbnNpb25zL1NvZnR3YXJlL1ZpZGVvLVNlbnNpbmctQmxvY2s8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZyBpZD0iRXh0ZW5zaW9ucy9Tb2Z0d2FyZS9WaWRlby1TZW5zaW5nLUJsb2NrIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2Utb3BhY2l0eT0iMC4xNSI+CiAgICAgICAgPGcgaWQ9InZpZGVvLW1vdGlvbiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC4wMDAwMDAsIDEwLjAwMDAwMCkiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjMDAwMDAwIj4KICAgICAgICAgICAgPGNpcmNsZSBpZD0iT3ZhbC1Db3B5IiBmaWxsPSIjRkZGRkZGIiBvcGFjaXR5PSIwLjI1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGN4PSIzMiIgY3k9IjE2IiByPSI0LjUiPjwvY2lyY2xlPgogICAgICAgICAgICA8Y2lyY2xlIGlkPSJPdmFsLUNvcHkiIGZpbGw9IiNGRkZGRkYiIG9wYWNpdHk9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjeD0iMzIiIGN5PSIxMiIgcj0iNC41Ij48L2NpcmNsZT4KICAgICAgICAgICAgPGNpcmNsZSBpZD0iT3ZhbC1Db3B5IiBmaWxsPSIjRkZGRkZGIiBvcGFjaXR5PSIwLjc1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGN4PSIzMiIgY3k9IjgiIHI9IjQuNSI+PC9jaXJjbGU+CiAgICAgICAgICAgIDxjaXJjbGUgaWQ9Ik92YWwiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY3g9IjMyIiBjeT0iNCIgcj0iNC41Ij48L2NpcmNsZT4KICAgICAgICAgICAgPHBhdGggZD0iTTIyLjY3MTk0NzcsNC40MTk1NzY0OSBMMTYuNSw4LjQxOTkxMjk4IEwxNi41LDYuMSBDMTYuNSw0LjA4OTc2NDU0IDE0LjkzNzE4MDYsMi41IDEzLDIuNSBMNC4xLDIuNSBDMi4wNzYxNDIzNywyLjUgMC41LDQuMDc2MTQyMzcgMC41LDYuMSBMMC41LDE0IEMwLjUsMTUuOTI3Mzk4NyAyLjA4NDQ5ODM5LDE3LjUxMTg5NzEgNC4xLDE3LjYgTDEzLDE3LjYgQzE0LjkwMTY2MDIsMTcuNiAxNi41LDE1Ljk0NjU0NSAxNi41LDE0IEwxNi41LDExLjcxNjkwNDggTDIyLjc1NzI0NzksMTUuNDcxMjUzNSBMMjIuODUzNTUzNCwxNS41NDY0NDY2IEMyMi44NzM3ODg2LDE1LjU2NjY4MTggMjIuOTUxNTMxLDE1LjYgMjMsMTUuNiBDMjMuMjY2OTg2NSwxNS42IDIzLjUsMTUuMzgyNTIwNyAyMy41LDE1LjEgTDIzLjUsNC44IEMyMy41LDQuODM2NzY1MzggMjMuNDQzODA1OCw0LjcwNTY0NTYzIDIzLjM3MTI1MzUsNC41NTcyNDc4OCBDMjMuMjI1OTA1Niw0LjMxNTAwMTM5IDIyLjk0MTU5MzcsNC4yNTgxMzg5OSAyMi42NzE5NDc3LDQuNDE5NTc2NDkgWiIgaWQ9InZpZGVvXzM3XyIgZmlsbD0iIzRENEQ0RCI+PC9wYXRoPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+'; + +/** + * Sensor attribute video sensor block should report. + * @readonly + * @enum {string} + */ +const SensingAttribute = { + /** The amount of motion. */ + MOTION: 'motion', + + /** The direction of the motion. */ + DIRECTION: 'direction' +}; + +/** + * Subject video sensor block should report for. + * @readonly + * @enum {string} + */ +const SensingSubject = { + /** The sensor traits of the whole stage. */ + STAGE: 'Stage', + + /** The senosr traits of the area overlapped by this sprite. */ + SPRITE: 'this sprite' +}; + +/** + * States the video sensing activity can be set to. + * @readonly + * @enum {string} + */ +const VideoState = { + /** Video turned off. */ + OFF: 'off', + + /** Video turned on with default y axis mirroring. */ + ON: 'on', + + /** Video turned on without default y axis mirroring. */ + ON_FLIPPED: 'on-flipped' +}; + +/** + * Class for the motion-related blocks in Scratch 3.0 + * @param {Runtime} runtime - the runtime instantiating this block package. + * @constructor + */ +class Scratch3VideoSensingBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + /** + * The motion detection algoritm used to power the motion amount and + * direction values. + * @type {VideoMotion} + */ + this.detect = new VideoMotion(); + + /** + * The last millisecond epoch timestamp that the video stream was + * analyzed. + * @type {number} + */ + this._lastUpdate = null; + + /** + * A flag to determine if this extension has been installed in a project. + * It is set to false the first time getInfo is run. + * @type {boolean} + */ + this.firstInstall = true; + + if (this.runtime.ioDevices) { + // Configure the video device with values from globally stored locations. + this.runtime.on(Runtime.PROJECT_LOADED, this.updateVideoDisplay.bind(this)); + + // Clear target motion state values when the project starts. + this.runtime.on(Runtime.PROJECT_RUN_START, this.reset.bind(this)); + + // Kick off looping the analysis logic. + this._loop(); + } + } + + /** + * After analyzing a frame the amount of milliseconds until another frame + * is analyzed. + * @type {number} + */ + static get INTERVAL () { + return 33; + } + + /** + * Dimensions the video stream is analyzed at after its rendered to the + * sample canvas. + * @type {Array.} + */ + static get DIMENSIONS () { + return [480, 360]; + } + + /** + * The key to load & store a target's motion-related state. + * @type {string} + */ + static get STATE_KEY () { + return 'Scratch.videoSensing'; + } + + /** + * The default motion-related state, to be used when a target has no existing motion state. + * @type {MotionState} + */ + static get DEFAULT_MOTION_STATE () { + return { + motionFrameNumber: 0, + motionAmount: 0, + motionDirection: 0 + }; + } + + /** + * The transparency setting of the video preview stored in a value + * accessible by any object connected to the virtual machine. + * @type {number} + */ + get globalVideoTransparency () { + const stage = this.runtime.getTargetForStage(); + if (stage) { + return stage.videoTransparency; + } + return 50; + } + + set globalVideoTransparency (transparency) { + const stage = this.runtime.getTargetForStage(); + if (stage) { + stage.videoTransparency = transparency; + } + return transparency; + } + + /** + * The video state of the video preview stored in a value accessible by any + * object connected to the virtual machine. + * @type {number} + */ + get globalVideoState () { + const stage = this.runtime.getTargetForStage(); + if (stage) { + return stage.videoState; + } + // Though the default value for the stage is normally 'on', we need to default + // to 'off' here to prevent the video device from briefly activating + // while waiting for stage targets to be installed that say it should be off + return VideoState.OFF; + } + + set globalVideoState (state) { + const stage = this.runtime.getTargetForStage(); + if (stage) { + stage.videoState = state; + } + return state; + } + + /** + * Get the latest values for video transparency and state, + * and set the video device to use them. + */ + updateVideoDisplay () { + this.setVideoTransparency({ + TRANSPARENCY: this.globalVideoTransparency + }); + this.videoToggle({ + VIDEO_STATE: this.globalVideoState + }); + } + + /** + * Reset the extension's data motion detection data. This will clear out + * for example old frames, so the first analyzed frame will not be compared + * against a frame from before reset was called. + */ + reset () { + this.detect.reset(); + + const targets = this.runtime.targets; + for (let i = 0; i < targets.length; i++) { + const state = targets[i].getCustomState(Scratch3VideoSensingBlocks.STATE_KEY); + if (state) { + state.motionAmount = 0; + state.motionDirection = 0; + } + } + } + + /** + * Occasionally step a loop to sample the video, stamp it to the preview + * skin, and add a TypedArray copy of the canvas's pixel data. + * @private + */ + _loop () { + setTimeout(this._loop.bind(this), Math.max(this.runtime.currentStepTime, Scratch3VideoSensingBlocks.INTERVAL)); + + // Add frame to detector + const time = Date.now(); + if (this._lastUpdate === null) { + this._lastUpdate = time; + } + const offset = time - this._lastUpdate; + if (offset > Scratch3VideoSensingBlocks.INTERVAL) { + const frame = this.runtime.ioDevices.video.getFrame({ + format: Video.FORMAT_IMAGE_DATA, + dimensions: Scratch3VideoSensingBlocks.DIMENSIONS + }); + if (frame) { + this._lastUpdate = time; + this.detect.addFrame(frame.data); + } + } + } + + /** + * Create data for a menu in scratch-blocks format, consisting of an array + * of objects with text and value properties. The text is a translated + * string, and the value is one-indexed. + * @param {object[]} info - An array of info objects each having a name + * property. + * @return {array} - An array of objects with text and value properties. + * @private + */ + _buildMenu (info) { + return info.map((entry, index) => { + const obj = {}; + obj.text = entry.name; + obj.value = entry.value || String(index + 1); + return obj; + }); + } + + /** + * @param {Target} target - collect motion state for this target. + * @returns {MotionState} the mutable motion state associated with that + * target. This will be created if necessary. + * @private + */ + _getMotionState (target) { + let motionState = target.getCustomState(Scratch3VideoSensingBlocks.STATE_KEY); + if (!motionState) { + motionState = Clone.simple(Scratch3VideoSensingBlocks.DEFAULT_MOTION_STATE); + target.setCustomState(Scratch3VideoSensingBlocks.STATE_KEY, motionState); + } + return motionState; + } + + static get SensingAttribute () { + return SensingAttribute; + } + + /** + * An array of choices of whether a reporter should return the frame's + * motion amount or direction. + * @type {object[]} + * @param {string} name - the translatable name to display in sensor + * attribute menu + * @param {string} value - the serializable value of the attribute + */ + get ATTRIBUTE_INFO () { + return [ + { + name: formatMessage({ + id: 'videoSensing.motion', + default: 'motion', + description: 'Attribute for the "video [ATTRIBUTE] on [SUBJECT]" block' + }), + value: SensingAttribute.MOTION + }, + { + name: formatMessage({ + id: 'videoSensing.direction', + default: 'direction', + description: 'Attribute for the "video [ATTRIBUTE] on [SUBJECT]" block' + }), + value: SensingAttribute.DIRECTION + } + ]; + } + + static get SensingSubject () { + return SensingSubject; + } + + /** + * An array of info about the subject choices. + * @type {object[]} + * @param {string} name - the translatable name to display in the subject menu + * @param {string} value - the serializable value of the subject + */ + get SUBJECT_INFO () { + return [ + { + name: formatMessage({ + id: 'videoSensing.sprite', + default: 'sprite', + description: 'Subject for the "video [ATTRIBUTE] on [SUBJECT]" block' + }), + value: SensingSubject.SPRITE + }, + { + name: formatMessage({ + id: 'videoSensing.stage', + default: 'stage', + description: 'Subject for the "video [ATTRIBUTE] on [SUBJECT]" block' + }), + value: SensingSubject.STAGE + } + ]; + } + + /** + * States the video sensing activity can be set to. + * @readonly + * @enum {string} + */ + static get VideoState () { + return VideoState; + } + + /** + * An array of info on video state options for the "turn video [STATE]" block. + * @type {object[]} + * @param {string} name - the translatable name to display in the video state menu + * @param {string} value - the serializable value stored in the block + */ + get VIDEO_STATE_INFO () { + return [ + { + name: formatMessage({ + id: 'videoSensing.off', + default: 'off', + description: 'Option for the "turn video [STATE]" block' + }), + value: VideoState.OFF + }, + { + name: formatMessage({ + id: 'videoSensing.on', + default: 'on', + description: 'Option for the "turn video [STATE]" block' + }), + value: VideoState.ON + }, + { + name: formatMessage({ + id: 'videoSensing.onFlipped', + default: 'on flipped', + description: 'Option for the "turn video [STATE]" block that causes the video to be flipped' + + ' horizontally (reversed as in a mirror)' + }), + value: VideoState.ON_FLIPPED + } + ]; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + // Set the video display properties to defaults the first time + // getInfo is run. This turns on the video device when it is + // first added to a project, and is overwritten by a PROJECT_LOADED + // event listener that later calls updateVideoDisplay + if (this.firstInstall) { + this.globalVideoState = VideoState.ON; + this.globalVideoTransparency = 50; + this.updateVideoDisplay(); + this.firstInstall = false; + } + + // Return extension definition + return { + id: 'videoSensing', + name: formatMessage({ + id: 'videoSensing.categoryName', + default: 'Video Sensing', + description: 'Label for the video sensing extension category' + }), + blockIconURI: blockIconURI, + menuIconURI: menuIconURI, + blocks: [ + { + // @todo this hat needs to be set itself to restart existing + // threads like Scratch 2's behaviour. + opcode: 'whenMotionGreaterThan', + text: formatMessage({ + id: 'videoSensing.whenMotionGreaterThan', + default: 'when video motion > [REFERENCE]', + description: 'Event that triggers when the amount of motion is greater than [REFERENCE]' + }), + blockType: BlockType.HAT, + arguments: { + REFERENCE: { + type: ArgumentType.NUMBER, + defaultValue: 10 + } + } + }, + { + opcode: 'videoOn', + blockType: BlockType.REPORTER, + text: formatMessage({ + id: 'videoSensing.videoOn', + default: 'video [ATTRIBUTE] on [SUBJECT]', + description: 'Reporter that returns the amount of [ATTRIBUTE] for the selected [SUBJECT]' + }), + arguments: { + ATTRIBUTE: { + type: ArgumentType.STRING, + menu: 'ATTRIBUTE', + defaultValue: SensingAttribute.MOTION + }, + SUBJECT: { + type: ArgumentType.STRING, + menu: 'SUBJECT', + defaultValue: SensingSubject.SPRITE + } + } + }, + { + opcode: 'videoToggle', + text: formatMessage({ + id: 'videoSensing.videoToggle', + default: 'turn video [VIDEO_STATE]', + description: 'Controls display of the video preview layer' + }), + arguments: { + VIDEO_STATE: { + type: ArgumentType.STRING, + menu: 'VIDEO_STATE', + defaultValue: VideoState.ON + } + } + }, + { + opcode: 'setVideoTransparency', + text: formatMessage({ + id: 'videoSensing.setVideoTransparency', + default: 'set video transparency to [TRANSPARENCY]', + description: 'Controls transparency of the video preview layer' + }), + arguments: { + TRANSPARENCY: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + } + ], + menus: { + ATTRIBUTE: { + acceptReporters: true, + items: this._buildMenu(this.ATTRIBUTE_INFO) + }, + SUBJECT: { + acceptReporters: true, + items: this._buildMenu(this.SUBJECT_INFO) + }, + VIDEO_STATE: { + acceptReporters: true, + items: this._buildMenu(this.VIDEO_STATE_INFO) + } + } + }; + } + + /** + * Analyze a part of the frame that a target overlaps. + * @param {Target} target - a target to determine where to analyze + * @returns {MotionState} the motion state for the given target + */ + _analyzeLocalMotion (target) { + const drawable = this.runtime.renderer._allDrawables[target.drawableID]; + const state = this._getMotionState(target); + this.detect.getLocalMotion(drawable, state); + return state; + } + + /** + * A scratch reporter block handle that analyzes the last two frames and + * depending on the arguments, returns the motion or direction for the + * whole stage or just the target sprite. + * @param {object} args - the block arguments + * @param {BlockUtility} util - the block utility + * @returns {number} the motion amount or direction of the stage or sprite + */ + videoOn (args, util) { + this.detect.analyzeFrame(); + + let state = this.detect; + if (args.SUBJECT === SensingSubject.SPRITE) { + state = this._analyzeLocalMotion(util.target); + } + + if (args.ATTRIBUTE === SensingAttribute.MOTION) { + return state.motionAmount; + } + return state.motionDirection; + } + + /** + * A scratch hat block edge handle that analyzes the last two frames where + * the target sprite overlaps and if it has more motion than the given + * reference value. + * @param {object} args - the block arguments + * @param {BlockUtility} util - the block utility + * @returns {boolean} true if the sprite overlaps more motion than the + * reference + */ + whenMotionGreaterThan (args, util) { + this.detect.analyzeFrame(); + const state = this._analyzeLocalMotion(util.target); + return state.motionAmount > Number(args.REFERENCE); + } + + /** + * A scratch command block handle that configures the video state from + * passed arguments. + * @param {object} args - the block arguments + * @param {VideoState} args.VIDEO_STATE - the video state to set the device to + */ + videoToggle (args) { + const state = args.VIDEO_STATE; + this.globalVideoState = state; + if (state === VideoState.OFF) { + this.runtime.ioDevices.video.disableVideo(); + } else { + this.runtime.ioDevices.video.enableVideo(); + // Mirror if state is ON. Do not mirror if state is ON_FLIPPED. + this.runtime.ioDevices.video.mirror = state === VideoState.ON; + } + } + + /** + * A scratch command block handle that configures the video preview's + * transparency from passed arguments. + * @param {object} args - the block arguments + * @param {number} args.TRANSPARENCY - the transparency to set the video + * preview to + */ + setVideoTransparency (args) { + const transparency = Cast.toNumber(args.TRANSPARENCY); + this.globalVideoTransparency = transparency; + this.runtime.ioDevices.video.setPreviewGhost(transparency); + } +} + +module.exports = Scratch3VideoSensingBlocks; diff --git a/local-scratch-vm/src/extensions/scratch3_video_sensing/library.js b/local-scratch-vm/src/extensions/scratch3_video_sensing/library.js new file mode 100644 index 0000000000000000000000000000000000000000..6de242142121f8772f5d4f2597d78a4412225273 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_video_sensing/library.js @@ -0,0 +1,384 @@ +/** + * @file library.js + * + * Tony Hwang and John Maloney, January 2011 + * Michael "Z" Goddard, March 2018 + * + * Video motion sensing primitives. + */ + +const {motionVector, scratchAtan2} = require('./math'); + +/** + * The width of the intended resolution to analyze for motion. + * @type {number} + */ +const WIDTH = 480; + +/** + * The height of the intended resolution to analyze for motion. + * @type {number} + */ +const HEIGHT = 360; + +/** + * A constant value to scale the magnitude of the x and y components called u + * and v. This creates the motionAmount value. + * + * Old note: chosen empirically to give a range of roughly 0-100 + * + * @type {number} + */ +const AMOUNT_SCALE = 100; + +/** + * A constant value to scale the magnitude of the x and y components called u + * and v in the local motion derivative. This creates the motionAmount value on + * a target's motion state. + * + * Old note: note 2e-4 * activePixelNum is an experimentally tuned threshold + * for my logitech Pro 9000 webcam - TTH + * + * @type {number} + */ +const LOCAL_AMOUNT_SCALE = AMOUNT_SCALE * 2e-4; + +/** + * The motion amount must be higher than the THRESHOLD to calculate a new + * direction value. + * @type {number} + */ +const THRESHOLD = 10; + +/** + * The size of the radius of the window of summarized values when considering + * the motion inside the full resolution of the sample. + * @type {number} + */ +const WINSIZE = 8; + +/** + * A ceiling for the motionAmount stored to a local target's motion state. The + * motionAmount is not allowed to be larger than LOCAL_MAX_AMOUNT. + * @type {number} + */ +const LOCAL_MAX_AMOUNT = 100; + +/** + * The motion amount for a target's local motion must be higher than the + * LOCAL_THRESHOLD to calculate a new direction value. + * @type {number} + */ +const LOCAL_THRESHOLD = THRESHOLD / 3; + +/** + * Store the necessary image pixel data to compares frames of a video and + * detect an amount and direction of motion in the full sample or in a + * specified area. + * @constructor + */ +class VideoMotion { + constructor () { + /** + * The number of frames that have been added from a source. + * @type {number} + */ + this.frameNumber = 0; + + /** + * The frameNumber last analyzed. + * @type {number} + */ + this.lastAnalyzedFrame = 0; + + /** + * The amount of motion detected in the current frame. + * @type {number} + */ + this.motionAmount = 0; + + /** + * The direction the motion detected in the frame is general moving in. + * @type {number} + */ + this.motionDirection = 0; + + /** + * A copy of the current frame's pixel values. A index of the array is + * represented in RGBA. The lowest byte is red. The next is green. The + * next is blue. And the last is the alpha value of that pixel. + * @type {Uint32Array} + */ + this.curr = null; + + /** + * A copy of the last frame's pixel values. + * @type {Uint32Array} + */ + this.prev = null; + + /** + * A buffer for holding one component of a pixel's full value twice. + * One for the current value. And one for the last value. + * @type {number} + */ + this._arrays = new ArrayBuffer(WIDTH * HEIGHT * 2 * 1); + + /** + * A clamped uint8 view of _arrays. One component of each index of the + * curr member is copied into this array. + * @type {number} + */ + this._curr = new Uint8ClampedArray(this._arrays, WIDTH * HEIGHT * 0 * 1, WIDTH * HEIGHT); + + /** + * A clamped uint8 view of _arrays. One component of each index of the + * prev member is copied into this array. + * @type {number} + */ + this._prev = new Uint8ClampedArray(this._arrays, WIDTH * HEIGHT * 1 * 1, WIDTH * HEIGHT); + } + + /** + * Reset internal state so future frame analysis does not consider values + * from before this method was called. + */ + reset () { + this.frameNumber = 0; + this.lastAnalyzedFrame = 0; + this.motionAmount = this.motionDirection = 0; + this.prev = this.curr = null; + } + + /** + * Add a frame to be next analyzed. The passed array represent a pixel with + * each index in the RGBA format. + * @param {Uint32Array} source - a source frame of pixels to copy + */ + addFrame (source) { + this.frameNumber++; + + // Swap curr to prev. + this.prev = this.curr; + // Create a clone of the array so any modifications made to the source + // array do not affect the work done in here. + this.curr = new Uint32Array(source.buffer.slice(0)); + + // Swap _prev and _curr. Copy one of the color components of the new + // array into _curr overwriting what was the old _prev data. + const _tmp = this._prev; + this._prev = this._curr; + this._curr = _tmp; + for (let i = 0; i < this.curr.length; i++) { + this._curr[i] = this.curr[i] & 0xff; + } + } + + /** + * Analyze the current frame against the previous frame determining the + * amount of motion and direction of the motion. + */ + analyzeFrame () { + if (!this.curr || !this.prev) { + this.motionAmount = this.motionDirection = -1; + // Don't have two frames to analyze yet + return; + } + + // Return early if new data has not been received. + if (this.lastAnalyzedFrame === this.frameNumber) { + return; + } + this.lastAnalyzedFrame = this.frameNumber; + + const { + _curr: curr, + _prev: prev + } = this; + + const winStep = (WINSIZE * 2) + 1; + const wmax = WIDTH - WINSIZE - 1; + const hmax = HEIGHT - WINSIZE - 1; + + // Accumulate 2d motion vectors from groups of pixels and average it + // later. + let uu = 0; + let vv = 0; + let n = 0; + + // Iterate over groups of cells building up the components to determine + // a motion vector for each cell instead of the whole frame to avoid + // integer overflows. + for (let i = WINSIZE + 1; i < hmax; i += winStep) { + for (let j = WINSIZE + 1; j < wmax; j += winStep) { + let A2 = 0; + let A1B2 = 0; + let B1 = 0; + let C1 = 0; + let C2 = 0; + + // This is a performance critical math region. + let address = ((i - WINSIZE) * WIDTH) + j - WINSIZE; + let nextAddress = address + winStep; + const maxAddress = ((i + WINSIZE) * WIDTH) + j + WINSIZE; + for (; address <= maxAddress; address += WIDTH - winStep, nextAddress += WIDTH) { + for (; address <= nextAddress; address += 1) { + // The difference in color between the last frame and + // the current frame. + const gradT = ((prev[address]) - (curr[address])); + // The difference between the pixel to the left and the + // pixel to the right. + const gradX = ((curr[address - 1]) - (curr[address + 1])); + // The difference between the pixel above and the pixel + // below. + const gradY = ((curr[address - WIDTH]) - (curr[address + WIDTH])); + + // Add the combined values of this pixel to previously + // considered pixels. + A2 += gradX * gradX; + A1B2 += gradX * gradY; + B1 += gradY * gradY; + C2 += gradX * gradT; + C1 += gradY * gradT; + } + } + + // Use the accumalated values from the for loop to determine a + // motion direction. + const {u, v} = motionVector(A2, A1B2, B1, C2, C1); + + // If u and v are within negative winStep to positive winStep, + // add them to a sum that will later be averaged. + if (-winStep < u && u < winStep && -winStep < v && v < winStep) { + uu += u; + vv += v; + n++; + } + } + } + + // Average the summed vector values of all of the motion groups. + uu /= n; + vv /= n; + + // Scale the magnitude of the averaged UV vector. + this.motionAmount = Math.round(AMOUNT_SCALE * Math.hypot(uu, vv)); + if (this.motionAmount > THRESHOLD) { + // Scratch direction + this.motionDirection = scratchAtan2(vv, uu); + } + } + + /** + * Build motion amount and direction values based on stored current and + * previous frame that overlaps a given drawable. + * @param {Drawable} drawable - touchable and bounded drawable to build motion for + * @param {MotionState} state - state to store built values to + */ + getLocalMotion (drawable, state) { + if (!this.curr || !this.prev) { + state.motionAmount = state.motionDirection = -1; + // Don't have two frames to analyze yet + return; + } + + // Skip if the current frame has already been considered for this state. + if (state.motionFrameNumber !== this.frameNumber) { + const { + _prev: prev, + _curr: curr + } = this; + + // The public APIs for Renderer#isTouching manage keeping the matrix and + // silhouette up-to-date, which is needed for drawable#isTouching to work (used below) + drawable.updateCPURenderAttributes(); + + // Restrict the region the amount and direction are built from to + // the area of the current frame overlapped by the given drawable's + // bounding box. + const boundingRect = drawable.getFastBounds(); + // Transform the bounding box from scratch space to a space from 0, + // 0 to WIDTH, HEIGHT. + const xmin = Math.max(Math.floor(boundingRect.left + (WIDTH / 2)), 1); + const xmax = Math.min(Math.floor(boundingRect.right + (WIDTH / 2)), WIDTH - 1); + const ymin = Math.max(Math.floor((HEIGHT / 2) - boundingRect.top), 1); + const ymax = Math.min(Math.floor((HEIGHT / 2) - boundingRect.bottom), HEIGHT - 1); + + let A2 = 0; + let A1B2 = 0; + let B1 = 0; + let C1 = 0; + let C2 = 0; + let scaleFactor = 0; + + const position = [0, 0, 0]; + + // This is a performance critical math region. + for (let i = ymin; i < ymax; i++) { + for (let j = xmin; j < xmax; j++) { + // i and j are in a coordinate planning ranging from 0 to + // HEIGHT and 0 to WIDTH. Transform that into Scratch's + // range of HEIGHT / 2 to -HEIGHT / 2 and -WIDTH / 2 to + // WIDTH / 2; + position[0] = j - (WIDTH / 2); + position[1] = (HEIGHT / 2) - i; + // Consider only pixels in the drawable that can touch the + // edge or other drawables. Empty space in the current skin + // is skipped. + if (drawable.isTouching(position)) { + const address = (i * WIDTH) + j; + // The difference in color between the last frame and + // the current frame. + const gradT = ((prev[address]) - (curr[address])); + // The difference between the pixel to the left and the + // pixel to the right. + const gradX = ((curr[address - 1]) - (curr[address + 1])); + // The difference between the pixel above and the pixel + // below. + const gradY = ((curr[address - WIDTH]) - (curr[address + WIDTH])); + + // Add the combined values of this pixel to previously + // considered pixels. + A2 += gradX * gradX; + A1B2 += gradX * gradY; + B1 += gradY * gradY; + C2 += gradX * gradT; + C1 += gradY * gradT; + scaleFactor++; + } + } + } + + // Use the accumalated values from the for loop to determine a + // motion direction. + let {u, v} = motionVector(A2, A1B2, B1, C2, C1); + + let activePixelNum = 0; + if (scaleFactor) { + // Store the area of the sprite in pixels + activePixelNum = scaleFactor; + + scaleFactor /= (2 * WINSIZE * 2 * WINSIZE); + u = u / scaleFactor; + v = v / scaleFactor; + } + + // Scale the magnitude of the averaged UV vector and the number of + // overlapping drawable pixels. + state.motionAmount = Math.round(LOCAL_AMOUNT_SCALE * activePixelNum * Math.hypot(u, v)); + if (state.motionAmount > LOCAL_MAX_AMOUNT) { + // Clip all magnitudes greater than 100. + state.motionAmount = LOCAL_MAX_AMOUNT; + } + if (state.motionAmount > LOCAL_THRESHOLD) { + // Scratch direction. + state.motionDirection = scratchAtan2(v, u); + } + + // Skip future calls on this state until a new frame is added. + state.motionFrameNumber = this.frameNumber; + } + } +} + +module.exports = VideoMotion; diff --git a/local-scratch-vm/src/extensions/scratch3_video_sensing/math.js b/local-scratch-vm/src/extensions/scratch3_video_sensing/math.js new file mode 100644 index 0000000000000000000000000000000000000000..b6357b1f1e5c356a62be2b2b8f8a4d270079e7b7 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_video_sensing/math.js @@ -0,0 +1,76 @@ +/** + * A constant value helping to transform a value in radians to degrees. + * @type {number} + */ +const TO_DEGREE = 180 / Math.PI; + +/** + * A object reused to save on memory allocation returning u and v vector from + * motionVector. + * @type {UV} + */ +const _motionVectorOut = {u: 0, v: 0}; + +/** + * Determine a motion vector combinations of the color component difference on + * the x axis, y axis, and temporal axis. + * @param {number} A2 - a sum of x axis squared + * @param {number} A1B2 - a sum of x axis times y axis + * @param {number} B1 - a sum of y axis squared + * @param {number} C2 - a sum of x axis times temporal axis + * @param {number} C1 - a sum of y axis times temporal axis + * @param {UV} out - optional object to store return UV info in + * @returns {UV} a uv vector representing the motion for the given input + */ +const motionVector = function (A2, A1B2, B1, C2, C1, out = _motionVectorOut) { + // Compare sums of X * Y and sums of X squared and Y squared. + const delta = ((A1B2 * A1B2) - (A2 * B1)); + if (delta) { + // System is not singular - solving by Kramer method. + const deltaX = -((C1 * A1B2) - (C2 * B1)); + const deltaY = -((A1B2 * C2) - (A2 * C1)); + const Idelta = 8 / delta; + out.u = deltaX * Idelta; + out.v = deltaY * Idelta; + } else { + // Singular system - find optical flow in gradient direction. + const Norm = ((A1B2 + A2) * (A1B2 + A2)) + ((B1 + A1B2) * (B1 + A1B2)); + if (Norm) { + const IGradNorm = 8 / Norm; + const temp = -(C1 + C2) * IGradNorm; + out.u = (A1B2 + A2) * temp; + out.v = (B1 + A1B2) * temp; + } else { + out.u = 0; + out.v = 0; + } + } + return out; +}; + +/** + * Translate an angle in degrees with the range -180 to 180 rotated to + * Scratch's reference angle. + * @param {number} degrees - angle in range -180 to 180 + * @returns {number} angle from Scratch's reference angle + */ +const scratchDegrees = function (degrees) { + return ((degrees + 270) % 360) - 180; +}; + +/** + * Get the angle of the y and x component of a 2d vector in degrees in + * Scratch's coordinate plane. + * @param {number} y - the y component of a 2d vector + * @param {number} x - the x component of a 2d vector + * @returns {number} angle in degrees in Scratch's coordinate plane + */ +const scratchAtan2 = function (y, x) { + return scratchDegrees(Math.atan2(y, x) * TO_DEGREE); +}; + +module.exports = { + motionVector, + scratchDegrees, + scratchAtan2 +}; diff --git a/local-scratch-vm/src/extensions/scratch3_video_sensing/view.js b/local-scratch-vm/src/extensions/scratch3_video_sensing/view.js new file mode 100644 index 0000000000000000000000000000000000000000..35623a651c6899cc255e2224ee440e9b74888649 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_video_sensing/view.js @@ -0,0 +1,509 @@ +const {motionVector} = require('./math'); + +const WIDTH = 480; +const HEIGHT = 360; +const WINSIZE = 8; +const AMOUNT_SCALE = 100; +const THRESHOLD = 10; + +/** + * Modes of debug output that can be rendered. + * @type {object} + */ +const OUTPUT = { + /** + * Render the original input. + * @type {number} + */ + INPUT: -1, + + /** + * Render the difference of neighboring pixels for each pixel. The + * horizontal difference, or x value, renders in the red output component. + * The vertical difference, or y value, renders in the green output + * component. Pixels with equal neighbors with a kind of lime green or + * #008080 in a RGB hex value. Colors with more red have a lower value to + * the right than the value to the left. Colors with less red have a higher + * value to the right than the value to the left. Similarly colors with + * more green have lower values below than above and colors with less green + * have higher values below than above. + * @type {number} + */ + XY: 0, + + /** + * Render the XY output with groups of pixels averaged together. The group + * shape and size matches the full frame's analysis window size. + * @type {number} + */ + XY_CELL: 1, + + /** + * Render three color components matching the detection algorith's values + * that multiple the horizontal difference, or x value, and the vertical + * difference, or y value together. The red component is the x value + * squared. The green component is the y value squared. The blue component + * is the x value times the y value. The detection code refers to these + * values as A2, B1, and A1B2. + * @type {number} + */ + AB: 2, + + /** + * Render the AB output of groups of pixels summarized by their combined + * square root. The group shape and size matches the full frame's analysis + * window size. + * @type {number} + */ + AB_CELL: 3, + + /** + * Render a single color component matching the temporal difference or the + * difference in color for the same pixel coordinate in the current frame + * and the last frame. The difference is rendered in the blue color + * component since x and y axis differences tend to use red and green. + * @type {number} + */ + T: 4, + + /** + * Render the T output of groups of pixels averaged. The group shape and + * size matches the full frame's analysis window. + * @type {number} + */ + T_CELL: 5, + + /** + * Render the XY and T outputs together. The x and y axis values use the + * red and green color components as they do in the XY output. The t values + * use the blue color component as the T output does. + * @type {number} + */ + XYT: 6, + + /** + * Render the XYT output of groups of pixels averaged. The group shape and + * size matches the full frame's analysis window. + * @type {number} + */ + XYT_CELL: 7, + + /** + * Render the horizontal pixel difference times the temporal difference as + * red and the vertical and temporal difference as green. Multiplcation of + * these values ends up with sharp differences in the output showing edge + * details where motion is happening. + * @type {number} + */ + C: 8, + + /** + * Render the C output of groups of pixels averaged. The group shape and + * size matches the full frame's analysis window. + * @type {number} + */ + C_CELL: 9, + + /** + * Render a per pixel version of UV_CELL. UV_CELL is a close to final step + * of the motion code that builds a motion amount and direction from those + * values. UV_CELL renders grouped summarized values, UV does the per pixel + * version but its can only represent one motion vector code path out of + * two choices. Determining the motion vector compares some of the built + * values but building the values with one pixel ensures this first + * comparison says the values are equal. Even though only one code path is + * used to build the values, its output is close to approximating the + * better solution building vectors from groups of pixels to help + * illustrate when the code determines the motion amount and direction to + * be. + * @type {number} + */ + UV: 10, + + /** + * Render cells of mulitple pixels at a step in the motion code that has + * the same cell values and turns them into motion vectors showing the + * amount of motion in the x axis and y axis separately. Those values are a + * step away from becoming a motion amount and direction through standard + * vector to magnitude and angle values. + * @type {number} + */ + UV_CELL: 11 +}; + +/** + * Temporary storage structure for returning values in + * VideoMotionView._components. + * @type {object} + */ +const _videoMotionViewComponentsTmp = { + A2: 0, + A1B2: 0, + B1: 0, + C2: 0, + C1: 0 +}; + +/** + * Manage a debug canvas with VideoMotion input frames running parts of what + * VideoMotion does to visualize what it does. + * @param {VideoMotion} motion - VideoMotion with inputs to visualize + * @param {OUTPUT} output - visualization output mode + * @constructor + */ +class VideoMotionView { + constructor (motion, output = OUTPUT.XYT) { + /** + * VideoMotion instance to visualize. + * @type {VideoMotion} + */ + this.motion = motion; + + /** + * Debug canvas to render to. + * @type {HTMLCanvasElement} + */ + const canvas = this.canvas = document.createElement('canvas'); + canvas.width = WIDTH; + canvas.height = HEIGHT; + + /** + * 2D context to draw to debug canvas. + * @type {CanvasRendering2DContext} + */ + this.context = canvas.getContext('2d'); + + /** + * Visualization output mode. + * @type {OUTPUT} + */ + this.output = output; + + /** + * Pixel buffer to store output values into before they replace the last frames info in the debug canvas. + * @type {Uint32Array} + */ + this.buffer = new Uint32Array(WIDTH * HEIGHT); + } + + /** + * Modes of debug output that can be rendered. + * @type {object} + */ + static get OUTPUT () { + return OUTPUT; + } + + /** + * Iterate each pixel address location and call a function with that address. + * @param {number} xStart - start location on the x axis of the output pixel buffer + * @param {number} yStart - start location on the y axis of the output pixel buffer + * @param {nubmer} xStop - location to stop at on the x axis + * @param {number} yStop - location to stop at on the y axis + * @param {function} fn - handle to call with each iterated address + */ + _eachAddress (xStart, yStart, xStop, yStop, fn) { + for (let i = yStart; i < yStop; i++) { + for (let j = xStart; j < xStop; j++) { + const address = (i * WIDTH) + j; + fn(address, j, i); + } + } + } + + /** + * Iterate over cells of pixels and call a function with a function to + * iterate over pixel addresses. + * @param {number} xStart - start location on the x axis + * @param {number} yStart - start lcoation on the y axis + * @param {number} xStop - location to stop at on the x axis + * @param {number} yStop - location to stop at on the y axis + * @param {number} xStep - width of the cells + * @param {number} yStep - height of the cells + * @param {function} fn - function to call with a bound handle to _eachAddress + */ + _eachCell (xStart, yStart, xStop, yStop, xStep, yStep, fn) { + const xStep2 = (xStep / 2) | 0; + const yStep2 = (yStep / 2) | 0; + for (let i = yStart; i < yStop; i += yStep) { + for (let j = xStart; j < xStop; j += xStep) { + fn( + _fn => this._eachAddress(j - xStep2 - 1, i - yStep2 - 1, j + xStep2, i + yStep2, _fn), + j - xStep2 - 1, + i - yStep2 - 1, + j + xStep2, + i + yStep2 + ); + } + } + } + + /** + * Build horizontal, vertical, and temporal difference of a pixel address. + * @param {number} address - address to build values for + * @returns {object} a object with a gradX, grady, and gradT value + */ + _grads (address) { + const {curr, prev} = this.motion; + const gradX = (curr[address - 1] & 0xff) - (curr[address + 1] & 0xff); + const gradY = (curr[address - WIDTH] & 0xff) - (curr[address + WIDTH] & 0xff); + const gradT = (prev[address] & 0xff) - (curr[address] & 0xff); + return {gradX, gradY, gradT}; + } + + /** + * Build component values used in determining a motion vector for a pixel + * address. + * @param {function} eachAddress - a bound handle to _eachAddress to build + * component values for + * @returns {object} a object with a A2, A1B2, B1, C2, C1 value + */ + _components (eachAddress) { + let A2 = 0; + let A1B2 = 0; + let B1 = 0; + let C2 = 0; + let C1 = 0; + + eachAddress(address => { + const {gradX, gradY, gradT} = this._grads(address); + A2 += gradX * gradX; + A1B2 += gradX * gradY; + B1 += gradY * gradY; + C2 += gradX * gradT; + C1 += gradY * gradT; + }); + + _videoMotionViewComponentsTmp.A2 = A2; + _videoMotionViewComponentsTmp.A1B2 = A1B2; + _videoMotionViewComponentsTmp.B1 = B1; + _videoMotionViewComponentsTmp.C2 = C2; + _videoMotionViewComponentsTmp.C1 = C1; + return _videoMotionViewComponentsTmp; + } + + /** + * Visualize the motion code output mode selected for this view to the + * debug canvas. + */ + draw () { + if (!(this.motion.prev && this.motion.curr)) { + return; + } + + const {buffer} = this; + + if (this.output === OUTPUT.INPUT) { + const {curr} = this.motion; + this._eachAddress(1, 1, WIDTH - 1, HEIGHT - 1, address => { + buffer[address] = curr[address]; + }); + } + if (this.output === OUTPUT.XYT) { + this._eachAddress(1, 1, WIDTH - 1, HEIGHT - 1, address => { + const {gradX, gradY, gradT} = this._grads(address); + const over1 = gradT / 0xcf; + buffer[address] = + (0xff << 24) + + (Math.floor((((gradY * over1) & 0xff) + 0xff) / 2) << 8) + + Math.floor((((gradX * over1) & 0xff) + 0xff) / 2); + }); + } + if (this.output === OUTPUT.XYT_CELL) { + const winStep = (WINSIZE * 2) + 1; + const wmax = WIDTH - WINSIZE - 1; + const hmax = HEIGHT - WINSIZE - 1; + + this._eachCell(WINSIZE + 1, WINSIZE + 1, wmax, hmax, winStep, winStep, eachAddress => { + let C1 = 0; + let C2 = 0; + let n = 0; + + eachAddress(address => { + const {gradX, gradY, gradT} = this._grads(address); + C2 += (Math.max(Math.min(gradX / 0x0f, 1), -1)) * (gradT / 0xff); + C1 += (Math.max(Math.min(gradY / 0x0f, 1), -1)) * (gradT / 0xff); + n += 1; + }); + + C1 /= n; + C2 /= n; + C1 = Math.log(C1 + (1 * Math.sign(C1))) / Math.log(2); + C2 = Math.log(C2 + (1 * Math.sign(C2))) / Math.log(2); + + eachAddress(address => { + buffer[address] = (0xff << 24) + + (((((C1 * 0x7f) | 0) + 0x80) << 8) & 0xff00) + + (((((C2 * 0x7f) | 0) + 0x80) << 0) & 0xff); + }); + }); + } + if (this.output === OUTPUT.XY) { + this._eachAddress(1, 1, WIDTH - 1, HEIGHT - 1, address => { + const {gradX, gradY} = this._grads(address); + buffer[address] = (0xff << 24) + (((gradY + 0xff) / 2) << 8) + ((gradX + 0xff) / 2); + }); + } + if (this.output === OUTPUT.XY_CELL) { + const winStep = (WINSIZE * 2) + 1; + const wmax = WIDTH - WINSIZE - 1; + const hmax = HEIGHT - WINSIZE - 1; + + this._eachCell(WINSIZE + 1, WINSIZE + 1, wmax, hmax, winStep, winStep, eachAddress => { + let C1 = 0; + let C2 = 0; + let n = 0; + + eachAddress(address => { + const {gradX, gradY} = this._grads(address); + C2 += Math.max(Math.min(gradX / 0x1f, 1), -1); + C1 += Math.max(Math.min(gradY / 0x1f, 1), -1); + n += 1; + }); + + C1 /= n; + C2 /= n; + C1 = Math.log(C1 + (1 * Math.sign(C1))) / Math.log(2); + C2 = Math.log(C2 + (1 * Math.sign(C2))) / Math.log(2); + + eachAddress(address => { + buffer[address] = (0xff << 24) + + (((((C1 * 0x7f) | 0) + 0x80) << 8) & 0xff00) + + (((((C2 * 0x7f) | 0) + 0x80) << 0) & 0xff); + }); + }); + } else if (this.output === OUTPUT.T) { + this._eachAddress(1, 1, WIDTH - 1, HEIGHT - 1, address => { + const {gradT} = this._grads(address); + buffer[address] = (0xff << 24) + ((gradT + 0xff) / 2 << 16); + }); + } + if (this.output === OUTPUT.T_CELL) { + const winStep = (WINSIZE * 2) + 1; + const wmax = WIDTH - WINSIZE - 1; + const hmax = HEIGHT - WINSIZE - 1; + + this._eachCell(WINSIZE + 1, WINSIZE + 1, wmax, hmax, winStep, winStep, eachAddress => { + let T = 0; + let n = 0; + + eachAddress(address => { + const {gradT} = this._grads(address); + T += gradT / 0xff; + n += 1; + }); + + T /= n; + + eachAddress(address => { + buffer[address] = (0xff << 24) + + (((((T * 0x7f) | 0) + 0x80) << 16) & 0xff0000); + }); + }); + } else if (this.output === OUTPUT.C) { + this._eachAddress(1, 1, WIDTH - 1, HEIGHT - 1, address => { + const {gradX, gradY, gradT} = this._grads(address); + buffer[address] = + (0xff << 24) + + (((Math.sqrt(gradY * gradT) * 0x0f) & 0xff) << 8) + + ((Math.sqrt(gradX * gradT) * 0x0f) & 0xff); + }); + } + if (this.output === OUTPUT.C_CELL) { + const winStep = (WINSIZE * 2) + 1; + const wmax = WIDTH - WINSIZE - 1; + const hmax = HEIGHT - WINSIZE - 1; + + this._eachCell(WINSIZE + 1, WINSIZE + 1, wmax, hmax, winStep, winStep, eachAddress => { + let {C2, C1} = this._components(eachAddress); + + C2 = Math.sqrt(C2); + C1 = Math.sqrt(C1); + + eachAddress(address => { + buffer[address] = + (0xff << 24) + + ((C1 & 0xff) << 8) + + ((C2 & 0xff) << 0); + }); + }); + } else if (this.output === OUTPUT.AB) { + this._eachAddress(1, 1, WIDTH - 1, HEIGHT - 1, address => { + const {gradX, gradY} = this._grads(address); + buffer[address] = + (0xff << 24) + + (((gradX * gradY) & 0xff) << 16) + + (((gradY * gradY) & 0xff) << 8) + + ((gradX * gradX) & 0xff); + }); + } + if (this.output === OUTPUT.AB_CELL) { + const winStep = (WINSIZE * 2) + 1; + const wmax = WIDTH - WINSIZE - 1; + const hmax = HEIGHT - WINSIZE - 1; + + this._eachCell(WINSIZE + 1, WINSIZE + 1, wmax, hmax, winStep, winStep, eachAddress => { + let {A2, A1B2, B1} = this._components(eachAddress); + + A2 = Math.sqrt(A2); + A1B2 = Math.sqrt(A1B2); + B1 = Math.sqrt(B1); + + eachAddress(address => { + buffer[address] = + (0xff << 24) + + ((A1B2 & 0xff) << 16) + + ((B1 & 0xff) << 8) + + (A2 & 0xff); + }); + }); + } else if (this.output === OUTPUT.UV) { + const winStep = (WINSIZE * 2) + 1; + + this._eachAddress(1, 1, WIDTH - 1, HEIGHT - 1, address => { + const {A2, A1B2, B1, C2, C1} = this._components(fn => fn(address)); + const {u, v} = motionVector(A2, A1B2, B1, C2, C1); + + const inRange = (-winStep < u && u < winStep && -winStep < v && v < winStep); + const hypot = Math.hypot(u, v); + const amount = AMOUNT_SCALE * hypot; + + buffer[address] = + (0xff << 24) + + (inRange && amount > THRESHOLD ? + (((((v / winStep) + 1) / 2 * 0xff) << 8) & 0xff00) + + (((((u / winStep) + 1) / 2 * 0xff) << 0) & 0xff) : + 0x8080 + ); + }); + } else if (this.output === OUTPUT.UV_CELL) { + const winStep = (WINSIZE * 2) + 1; + const wmax = WIDTH - WINSIZE - 1; + const hmax = HEIGHT - WINSIZE - 1; + + this._eachCell(WINSIZE + 1, WINSIZE + 1, wmax, hmax, winStep, winStep, eachAddress => { + const {A2, A1B2, B1, C2, C1} = this._components(eachAddress); + const {u, v} = motionVector(A2, A1B2, B1, C2, C1); + + const inRange = (-winStep < u && u < winStep && -winStep < v && v < winStep); + const hypot = Math.hypot(u, v); + const amount = AMOUNT_SCALE * hypot; + + eachAddress(address => { + buffer[address] = + (0xff << 24) + + (inRange && amount > THRESHOLD ? + (((((v / winStep) + 1) / 2 * 0xff) << 8) & 0xff00) + + (((((u / winStep) + 1) / 2 * 0xff) << 0) & 0xff) : + 0x8080 + ); + }); + }); + } + + const data = new ImageData(new Uint8ClampedArray(this.buffer.buffer), WIDTH, HEIGHT); + this.context.putImageData(data, 0, 0); + } +} + +module.exports = VideoMotionView; diff --git a/local-scratch-vm/src/extensions/scratch3_wedo2/index.js b/local-scratch-vm/src/extensions/scratch3_wedo2/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6806e4f3cac2d4f3be561401db61f7eb5348771e --- /dev/null +++ b/local-scratch-vm/src/extensions/scratch3_wedo2/index.js @@ -0,0 +1,1616 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); +const formatMessage = require('format-message'); +const color = require('../../util/color'); +const BLE = require('../../io/ble'); +const Base64Util = require('../../util/base64-util'); +const MathUtil = require('../../util/math-util'); +const RateLimiter = require('../../util/rateLimiter.js'); +const log = require('../../util/log'); + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const iconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAACXBIWXMAABYlAAAWJQFJUiTwAAAF8klEQVR4Ae2cbWxTVRjH/7ctbVc2tyEMNpWBk0VIkLcEjSAQgglTE5HEaKqJi1E/mbCP/dJA0kQbvzgTQ0Ki2T7V6AeYGoEPLJmGKPiyzZDwEpYJCHSbQIcbdLvres1zOa13Xbvdu2eTDp9fst329Lnn5XfPPfece7tphmFAmDkuccdDBDIRgUxEIBMRyEQEMhGBTEQgExHIRAQyEYFMRCATEchEBDIRgUxEIBMRyEQEMhGBTEQgExHIxMPNIByNVQBoBUDb7kgo2KTS9wBoUmFNkVCwW6U3A1gP4JJKHwxHY/S+WcW2RkLBVhV7AMAOAIMAGlWstbyOSCh4QMU2Uoy1PBVL+a7IqZu1vOZIKNg20/azBarGvKxebw9HY22RULADwBFLTBcATQnZl4lVEimN4ssteXQrQfstebQpmW1q30xshyqvxRLbofYnYW9ZYgeV8C5LLOWlzbTxM3ouHI7GPgSwWx3Z0syBSBku6IYnlTbM+uQenJQaMnKHDaqAFnDrcCFbl3G1defEjas0a4N/Vz10OybyvapfrSX1sjpo+WIz0ME7QL3djgtHPTAcjb2mepw/b2ZaGh5NL5RnofR8R99dIC5fHusK5JsrCUpm7TSx21XvbcwTNwnbAsPR2GcA3qaG+H0LsHlDPZ7fca/ujZ+cRW9/Em5vCXzlNVhQUjFpf/3OTSRvXkKJz43Xt1bh1S1LUeq/5+njQ9/iVmLIfL1ieRU2b1iFtavztXNu6TrTi8PfnYI67WdPoOp5przV9Y8iuHdb9rOW9uumPI+vDIElddBckztPOqVn5X36Xj1WVQeynx1sOWbK83jc2PviM/dFXIYNax9H55leXLoyYHsfWwI14JCRRx7x5ckBU1oheYQ+1G9u39lVM0Hej7+cR7w/Yb7e9+5LqChfaLvixcK088BwNNZkAOV02ubK6+odwt3RcfOULSSPGEveG48bNj08If3kqXPmdtO6unkpDzYn0u/TLxrzcumJJ80Ut79sygzoFF6/siw75mUYupOEpmnY0/A0pw33FTsCa+hX5oJhZXgkZb5zub2O20CnL7EwkPeCPm+wI7CEBvi5wuOZ36tJW7X3uGXJXAgxk8P4eNpRPEvgskqfuR0Z/BNGejxvDM3/5gs0pboWv+motqybCc+tqUCzz43kaBJ/X+2eMjZ3ClNsjIzo5ioknXZ2b4AlkKYltLJoaY9jOJm/B0KJbtg4c4F/XOmH3+dF9dLKbBo1OD6QQGV56YQ55ODtO0jcHkZ1VSX8/n9nB9S7RkZ1rFy+NG8ZR9s70TeQQKDEh7vJUdt1Y9/OopXFB2/WcbMpyOexE9mlFS21aLlHMmKHfzBl0QT/hV2bzM9oLXv0xG8YGR0zpdLEn6RT2k+/XjDzoLX2G3u3TZBLUyral/Z5qCyAK1f/sl2/or+IWNel1Eji3MWrpjyCZHWqdNrSe6ieSHFERl4mP+q5GehgHGvvRGal5XI5uzU47f3A/R99YTgdF2wXrmkolr9ToZ5NvTjT4yOhoC2T057CJM/r9WDxoqmXa07R9THcuDVcMO8bt4ag6ynULKvkFjWBTLl0ugZKvNlyqLeSQKfYGgOpgXt2b5zVhlzrS+Dr451YvKg0b95txztxvS8xZ+VuXFuLJ5+oNgV+9c3PuHDxGs6cu+w4v//9RJo6x5bN9UgbBo4cPY1U6j+cSD8orFvzGFYuX4KxsRQGbth6FCICc9m5dY05HtN46AQRqPB5PWjY+ZT5RnMwkxGBFh5ZVmle9Z3MrGbjwfqccrC1vajrV7QCaVCfS6qrJj96nQlFK5CujPRT7MgYyEQEMhGBTGwJpAW4kJ9pBbo0zbx70X7y7AOv8HxP3LyB4YTpb2cZBt2iqL3QEwf9zDbX+waLca439QMeC7a+YBmOxugLiM/OTt2yaOoMoO+H6LOcNwf6xusrthsh/7mIh1yFmYhAJiKQiQhkIgKZiEAmIpCJCGQiApmIQCYikIkIZCICmYhAJiKQiQhkIgKZiEAmIpCJCGQiAjkA+AeOwQKMcWZqHgAAAABJRU5ErkJggg=='; + +/** + * A list of WeDo 2.0 BLE service UUIDs. + * @enum + */ +const BLEService = { + DEVICE_SERVICE: '00001523-1212-efde-1523-785feabcd123', + IO_SERVICE: '00004f0e-1212-efde-1523-785feabcd123' +}; + +/** + * A list of WeDo 2.0 BLE characteristic UUIDs. + * + * Characteristics on DEVICE_SERVICE: + * - ATTACHED_IO + * + * Characteristics on IO_SERVICE: + * - INPUT_VALUES + * - INPUT_COMMAND + * - OUTPUT_COMMAND + * + * @enum + */ +const BLECharacteristic = { + ATTACHED_IO: '00001527-1212-efde-1523-785feabcd123', + LOW_VOLTAGE_ALERT: '00001528-1212-efde-1523-785feabcd123', + INPUT_VALUES: '00001560-1212-efde-1523-785feabcd123', + INPUT_COMMAND: '00001563-1212-efde-1523-785feabcd123', + OUTPUT_COMMAND: '00001565-1212-efde-1523-785feabcd123' +}; + +/** + * A time interval to wait (in milliseconds) in between battery check calls. + * @type {number} + */ +const BLEBatteryCheckInterval = 5000; + +/** + * A time interval to wait (in milliseconds) while a block that sends a BLE message is running. + * @type {number} + */ +const BLESendInterval = 100; + +/** + * A maximum number of BLE message sends per second, to be enforced by the rate limiter. + * @type {number} + */ +const BLESendRateMax = 20; + +/** + * Enum for WeDo 2.0 sensor and output types. + * @readonly + * @enum {number} + */ +const WeDo2Device = { + MOTOR: 1, + PIEZO: 22, + LED: 23, + TILT: 34, + DISTANCE: 35 +}; + +/** + * Enum for connection/port ids assigned to internal WeDo 2.0 output devices. + * @readonly + * @enum {number} + */ +// TODO: Check for these more accurately at startup? +const WeDo2ConnectID = { + LED: 6, + PIEZO: 5 +}; + +/** + * Enum for ids for various output commands on the WeDo 2.0. + * @readonly + * @enum {number} + */ +const WeDo2Command = { + MOTOR_POWER: 1, + PLAY_TONE: 2, + STOP_TONE: 3, + WRITE_RGB: 4, + SET_VOLUME: 255 +}; + +/** + * Enum for modes for input sensors on the WeDo 2.0. + * @enum {number} + */ +const WeDo2Mode = { + TILT: 0, // angle + DISTANCE: 0, // detect + LED: 1 // RGB +}; + +/** + * Enum for units for input sensors on the WeDo 2.0. + * + * 0 = raw + * 1 = percent + * + * @enum {number} + */ +const WeDo2Unit = { + TILT: 0, + DISTANCE: 1, + LED: 0 +}; + +/** + * Manage power, direction, and timers for one WeDo 2.0 motor. + */ +class WeDo2Motor { + /** + * Construct a WeDo 2.0 Motor instance. + * @param {WeDo2} parent - the WeDo 2.0 peripheral which owns this motor. + * @param {int} index - the zero-based index of this motor on its parent peripheral. + */ + constructor (parent, index) { + /** + * The WeDo 2.0 peripheral which owns this motor. + * @type {WeDo2} + * @private + */ + this._parent = parent; + + /** + * The zero-based index of this motor on its parent peripheral. + * @type {int} + * @private + */ + this._index = index; + + /** + * This motor's current direction: 1 for "this way" or -1 for "that way" + * @type {number} + * @private + */ + this._direction = 1; + + /** + * This motor's current power level, in the range [0,100]. + * @type {number} + * @private + */ + this._power = 100; + + /** + * Is this motor currently moving? + * @type {boolean} + * @private + */ + this._isOn = false; + + /** + * If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for + * the end-of-action handler. Cancel this when changing plans. + * @type {Object} + * @private + */ + this._pendingTimeoutId = null; + + /** + * The starting time for the pending timeout. + * @type {Object} + * @private + */ + this._pendingTimeoutStartTime = null; + + /** + * The delay/duration of the pending timeout. + * @type {Object} + * @private + */ + this._pendingTimeoutDelay = null; + + this.startBraking = this.startBraking.bind(this); + this.turnOff = this.turnOff.bind(this); + } + + /** + * @return {number} - the duration of active braking after a call to startBraking(). Afterward, turn the motor off. + * @constructor + */ + static get BRAKE_TIME_MS () { + return 1000; + } + + /** + * @return {int} - this motor's current direction: 1 for "this way" or -1 for "that way" + */ + get direction () { + return this._direction; + } + + /** + * @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way" + */ + set direction (value) { + if (value < 0) { + this._direction = -1; + } else { + this._direction = 1; + } + } + + /** + * @return {int} - this motor's current power level, in the range [0,100]. + */ + get power () { + return this._power; + } + + /** + * @param {int} value - this motor's new power level, in the range [0,100]. + */ + set power (value) { + const p = Math.max(0, Math.min(value, 100)); + // Lego Wedo 2.0 hub only turns motors at power range [30 - 100], so + // map value from [0 - 100] to [30 - 100]. + if (p === 0) { + this._power = 0; + } else { + const delta = 100 / p; + this._power = 30 + (70 / delta); + } + } + + /** + * @return {boolean} - true if this motor is currently moving, false if this motor is off or braking. + */ + get isOn () { + return this._isOn; + } + + /** + * @return {boolean} - time, in milliseconds, of when the pending timeout began. + */ + get pendingTimeoutStartTime () { + return this._pendingTimeoutStartTime; + } + + /** + * @return {boolean} - delay, in milliseconds, of the pending timeout. + */ + get pendingTimeoutDelay () { + return this._pendingTimeoutDelay; + } + + /** + * Turn this motor on indefinitely. + */ + turnOn () { + if (this._power === 0) return; + + const cmd = this._parent.generateOutputCommand( + this._index + 1, + WeDo2Command.MOTOR_POWER, + [this._power * this._direction] // power in range 0-100 + ); + + this._parent.send(BLECharacteristic.OUTPUT_COMMAND, cmd); + + this._isOn = true; + this._clearTimeout(); + } + + /** + * Turn this motor on for a specific duration. + * @param {number} milliseconds - run the motor for this long. + */ + turnOnFor (milliseconds) { + if (this._power === 0) return; + + milliseconds = Math.max(0, milliseconds); + this.turnOn(); + this._setNewTimeout(this.startBraking, milliseconds); + } + + /** + * Start active braking on this motor. After a short time, the motor will turn off. + */ + startBraking () { + if (this._power === 0) return; + + const cmd = this._parent.generateOutputCommand( + this._index + 1, + WeDo2Command.MOTOR_POWER, + [127] // 127 = break + ); + + this._parent.send(BLECharacteristic.OUTPUT_COMMAND, cmd); + + this._isOn = false; + this._setNewTimeout(this.turnOff, WeDo2Motor.BRAKE_TIME_MS); + } + + /** + * Turn this motor off. + * @param {boolean} [useLimiter=true] - if true, use the rate limiter + */ + turnOff (useLimiter = true) { + if (this._power === 0) return; + + const cmd = this._parent.generateOutputCommand( + this._index + 1, + WeDo2Command.MOTOR_POWER, + [0] // 0 = stop + ); + + this._parent.send(BLECharacteristic.OUTPUT_COMMAND, cmd, useLimiter); + + this._isOn = false; + } + + /** + * Clear the motor action timeout, if any. Safe to call even when there is no pending timeout. + * @private + */ + _clearTimeout () { + if (this._pendingTimeoutId !== null) { + clearTimeout(this._pendingTimeoutId); + this._pendingTimeoutId = null; + this._pendingTimeoutStartTime = null; + this._pendingTimeoutDelay = null; + } + } + + /** + * Set a new motor action timeout, after clearing an existing one if necessary. + * @param {Function} callback - to be called at the end of the timeout. + * @param {int} delay - wait this many milliseconds before calling the callback. + * @private + */ + _setNewTimeout (callback, delay) { + this._clearTimeout(); + const timeoutID = setTimeout(() => { + if (this._pendingTimeoutId === timeoutID) { + this._pendingTimeoutId = null; + this._pendingTimeoutStartTime = null; + this._pendingTimeoutDelay = null; + } + callback(); + }, delay); + this._pendingTimeoutId = timeoutID; + this._pendingTimeoutStartTime = Date.now(); + this._pendingTimeoutDelay = delay; + } +} + +/** + * Manage communication with a WeDo 2.0 peripheral over a Bluetooth Low Energy client socket. + */ +class WeDo2 { + + constructor (runtime, extensionId) { + + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} + * @private + */ + this._runtime = runtime; + this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this)); + + /** + * The id of the extension this peripheral belongs to. + */ + this._extensionId = extensionId; + + /** + * A list of the ids of the motors or sensors in ports 1 and 2. + * @type {string[]} + * @private + */ + this._ports = ['none', 'none']; + + /** + * The motors which this WeDo 2.0 could possibly have. + * @type {WeDo2Motor[]} + * @private + */ + this._motors = [null, null]; + + /** + * The most recently received value for each sensor. + * @type {Object.} + * @private + */ + this._sensors = { + tiltX: 0, + tiltY: 0, + distance: 0 + }; + + /** + * The Bluetooth connection socket for reading/writing peripheral data. + * @type {BLE} + * @private + */ + this._ble = null; + this._runtime.registerPeripheralExtension(extensionId, this); + + /** + * A rate limiter utility, to help limit the rate at which we send BLE messages + * over the socket to Scratch Link to a maximum number of sends per second. + * @type {RateLimiter} + * @private + */ + this._rateLimiter = new RateLimiter(BLESendRateMax); + + /** + * An interval id for the battery check interval. + * @type {number} + * @private + */ + this._batteryLevelIntervalId = null; + + this.reset = this.reset.bind(this); + this._onConnect = this._onConnect.bind(this); + this._onMessage = this._onMessage.bind(this); + this._checkBatteryLevel = this._checkBatteryLevel.bind(this); + } + + /** + * @return {number} - the latest value received for the tilt sensor's tilt about the X axis. + */ + get tiltX () { + return this._sensors.tiltX; + } + + /** + * @return {number} - the latest value received for the tilt sensor's tilt about the Y axis. + */ + get tiltY () { + return this._sensors.tiltY; + } + + /** + * @return {number} - the latest value received from the distance sensor. + */ + get distance () { + return this._sensors.distance; + } + + /** + * Access a particular motor on this peripheral. + * @param {int} index - the zero-based index of the desired motor. + * @return {WeDo2Motor} - the WeDo2Motor instance, if any, at that index. + */ + motor (index) { + return this._motors[index]; + } + + /** + * Stop all the motors that are currently running. + */ + stopAllMotors () { + this._motors.forEach(motor => { + if (motor) { + // Send the motor off command without using the rate limiter. + // This allows the stop button to stop motors even if we are + // otherwise flooded with commands. + motor.turnOff(false); + } + }); + } + + /** + * Set the WeDo 2.0 peripheral's LED to a specific color. + * @param {int} inputRGB - a 24-bit RGB color in 0xRRGGBB format. + * @return {Promise} - a promise of the completion of the set led send operation. + */ + setLED (inputRGB) { + const rgb = [ + (inputRGB >> 16) & 0x000000FF, + (inputRGB >> 8) & 0x000000FF, + (inputRGB) & 0x000000FF + ]; + + const cmd = this.generateOutputCommand( + WeDo2ConnectID.LED, + WeDo2Command.WRITE_RGB, + rgb + ); + + return this.send(BLECharacteristic.OUTPUT_COMMAND, cmd); + } + + /** + * Sets the input mode of the LED to RGB. + * @return {Promise} - a promise returned by the send operation. + */ + setLEDMode () { + const cmd = this.generateInputCommand( + WeDo2ConnectID.LED, + WeDo2Device.LED, + WeDo2Mode.LED, + 0, + WeDo2Unit.LED, + false + ); + + return this.send(BLECharacteristic.INPUT_COMMAND, cmd); + } + + /** + * Switch off the LED on the WeDo 2.0. + * @return {Promise} - a promise of the completion of the stop led send operation. + */ + stopLED () { + const cmd = this.generateOutputCommand( + WeDo2ConnectID.LED, + WeDo2Command.WRITE_RGB, + [0, 0, 0] + ); + + return this.send(BLECharacteristic.OUTPUT_COMMAND, cmd); + } + + /** + * Play a tone from the WeDo 2.0 peripheral for a specific amount of time. + * @param {int} tone - the pitch of the tone, in Hz. + * @param {int} milliseconds - the duration of the note, in milliseconds. + * @return {Promise} - a promise of the completion of the play tone send operation. + */ + playTone (tone, milliseconds) { + const cmd = this.generateOutputCommand( + WeDo2ConnectID.PIEZO, + WeDo2Command.PLAY_TONE, + [ + tone, + tone >> 8, + milliseconds, + milliseconds >> 8 + ] + ); + + return this.send(BLECharacteristic.OUTPUT_COMMAND, cmd); + } + + /** + * Stop the tone playing from the WeDo 2.0 peripheral, if any. + * @return {Promise} - a promise that the command sent. + */ + stopTone () { + const cmd = this.generateOutputCommand( + WeDo2ConnectID.PIEZO, + WeDo2Command.STOP_TONE + ); + + // Send this command without using the rate limiter, because it is + // only triggered by the stop button. + return this.send(BLECharacteristic.OUTPUT_COMMAND, cmd, false); + } + + /** + * Stop the tone playing and motors on the WeDo 2.0 peripheral. + */ + stopAll () { + if (!this.isConnected()) return; + this.stopTone(); + this.stopAllMotors(); + } + + /** + * Called by the runtime when user wants to scan for a WeDo 2.0 peripheral. + */ + scan () { + if (this._ble) { + this._ble.disconnect(); + } + this._ble = new BLE(this._runtime, this._extensionId, { + filters: [{ + services: [BLEService.DEVICE_SERVICE] + }], + optionalServices: [BLEService.IO_SERVICE] + }, this._onConnect, this.reset); + } + + /** + * Called by the runtime when user wants to connect to a certain WeDo 2.0 peripheral. + * @param {number} id - the id of the peripheral to connect to. + */ + connect (id) { + if (this._ble) { + this._ble.connectPeripheral(id); + } + } + + /** + * Disconnects from the current BLE socket. + */ + disconnect () { + if (this._ble) { + this._ble.disconnect(); + } + + this.reset(); + } + + /** + * Reset all the state and timeout/interval ids. + */ + reset () { + this._ports = ['none', 'none']; + this._motors = [null, null]; + this._sensors = { + tiltX: 0, + tiltY: 0, + distance: 0 + }; + + if (this._batteryLevelIntervalId) { + window.clearInterval(this._batteryLevelIntervalId); + this._batteryLevelIntervalId = null; + } + } + + /** + * Called by the runtime to detect whether the WeDo 2.0 peripheral is connected. + * @return {boolean} - the connected state. + */ + isConnected () { + let connected = false; + if (this._ble) { + connected = this._ble.isConnected(); + } + return connected; + } + + /** + * Write a message to the WeDo 2.0 peripheral BLE socket. + * @param {number} uuid - the UUID of the characteristic to write to + * @param {Array} message - the message to write. + * @param {boolean} [useLimiter=true] - if true, use the rate limiter + * @return {Promise} - a promise result of the write operation + */ + send (uuid, message, useLimiter = true) { + if (!this.isConnected()) return Promise.resolve(); + + if (useLimiter) { + if (!this._rateLimiter.okayToSend()) return Promise.resolve(); + } + + return this._ble.write( + BLEService.IO_SERVICE, + uuid, + Base64Util.uint8ArrayToBase64(message), + 'base64' + ); + } + + /** + * Generate a WeDo 2.0 'Output Command' in the byte array format + * (CONNECT ID, COMMAND ID, NUMBER OF BYTES, VALUES ...). + * + * This sends a command to the WeDo 2.0 to actuate the specified outputs. + * + * @param {number} connectID - the port (Connect ID) to send a command to. + * @param {number} commandID - the id of the byte command. + * @param {array} values - the list of values to write to the command. + * @return {array} - a generated output command. + */ + generateOutputCommand (connectID, commandID, values = null) { + let command = [connectID, commandID]; + if (values) { + command = command.concat( + values.length + ).concat( + values + ); + } + return command; + } + + /** + * Generate a WeDo 2.0 'Input Command' in the byte array format + * (COMMAND ID, COMMAND TYPE, CONNECT ID, TYPE ID, MODE, DELTA INTERVAL (4 BYTES), + * UNIT, NOTIFICATIONS ENABLED). + * + * This sends a command to the WeDo 2.0 that sets that input format + * of the specified inputs and sets value change notifications. + * + * @param {number} connectID - the port (Connect ID) to send a command to. + * @param {number} type - the type of input sensor. + * @param {number} mode - the mode of the input sensor. + * @param {number} delta - the delta change needed to trigger notification. + * @param {array} units - the unit of the input sensor value. + * @param {boolean} enableNotifications - whether to enable notifications. + * @return {array} - a generated input command. + */ + generateInputCommand (connectID, type, mode, delta, units, enableNotifications) { + const command = [ + 1, // Command ID = 1 = "Sensor Format" + 2, // Command Type = 2 = "Write" + connectID, + type, + mode, + delta, + 0, // Delta Interval Byte 2 + 0, // Delta Interval Byte 3 + 0, // Delta Interval Byte 4 + units, + enableNotifications ? 1 : 0 + ]; + + return command; + } + + /** + * Sets LED mode and initial color and starts reading data from peripheral after BLE has connected. + * @private + */ + _onConnect () { + this.setLEDMode(); + this.setLED(0x0000FF); + this._ble.startNotifications( + BLEService.DEVICE_SERVICE, + BLECharacteristic.ATTACHED_IO, + this._onMessage + ); + this._batteryLevelIntervalId = window.setInterval(this._checkBatteryLevel, BLEBatteryCheckInterval); + } + + /** + * Process the sensor data from the incoming BLE characteristic. + * @param {object} base64 - the incoming BLE data. + * @private + */ + _onMessage (base64) { + const data = Base64Util.base64ToUint8Array(base64); + // log.info(data); + + /** + * If first byte of data is '1' or '2', then either clear the + * sensor present in ports 1 or 2 or set their format. + * + * If first byte of data is anything else, read incoming sensor value. + */ + switch (data[0]) { + case 1: + case 2: { + const connectID = data[0]; + if (data[1] === 0) { + // clear sensor or motor + this._clearPort(connectID); + } else { + // register sensor or motor + this._registerSensorOrMotor(connectID, data[3]); + } + break; + } + default: { + // read incoming sensor value + const connectID = data[1]; + const type = this._ports[connectID - 1]; + if (type === WeDo2Device.DISTANCE) { + this._sensors.distance = data[2]; + } + if (type === WeDo2Device.TILT) { + this._sensors.tiltX = data[2]; + this._sensors.tiltY = data[3]; + } + break; + } + } + } + + /** + * Check the battery level on the WeDo 2.0. If the WeDo 2.0 has disconnected + * for some reason, the BLE socket will get an error back and automatically + * close the socket. + */ + _checkBatteryLevel () { + this._ble.read( + BLEService.DEVICE_SERVICE, + BLECharacteristic.LOW_VOLTAGE_ALERT, + false + ); + } + + /** + * Register a new sensor or motor connected at a port. Store the type of + * sensor or motor internally, and then register for notifications on input + * values if it is a sensor. + * @param {number} connectID - the port to register a sensor or motor on. + * @param {number} type - the type ID of the sensor or motor + * @private + */ + _registerSensorOrMotor (connectID, type) { + // Record which port is connected to what type of device + this._ports[connectID - 1] = type; + + // Record motor port + if (type === WeDo2Device.MOTOR) { + this._motors[connectID - 1] = new WeDo2Motor(this, connectID - 1); + } else { + // Set input format for tilt or distance sensor + const typeString = type === WeDo2Device.DISTANCE ? 'DISTANCE' : 'TILT'; + const cmd = this.generateInputCommand( + connectID, + type, + WeDo2Mode[typeString], + 1, + WeDo2Unit[typeString], + true + ); + + this.send(BLECharacteristic.INPUT_COMMAND, cmd); + this._ble.startNotifications( + BLEService.IO_SERVICE, + BLECharacteristic.INPUT_VALUES, + this._onMessage + ); + } + } + + /** + * Clear the sensor or motor present at port 1 or 2. + * @param {number} connectID - the port to clear. + * @private + */ + _clearPort (connectID) { + const type = this._ports[connectID - 1]; + if (type === WeDo2Device.TILT) { + this._sensors.tiltX = this._sensors.tiltY = 0; + } + if (type === WeDo2Device.DISTANCE) { + this._sensors.distance = 0; + } + this._ports[connectID - 1] = 'none'; + this._motors[connectID - 1] = null; + } +} + +/** + * Enum for motor specification. + * @readonly + * @enum {string} + */ +const WeDo2MotorLabel = { + DEFAULT: 'motor', + A: 'motor A', + B: 'motor B', + ALL: 'all motors' +}; + +/** + * Enum for motor direction specification. + * @readonly + * @enum {string} + */ +const WeDo2MotorDirection = { + FORWARD: 'this way', + BACKWARD: 'that way', + REVERSE: 'reverse' +}; + +/** + * Enum for tilt sensor direction. + * @readonly + * @enum {string} + */ +const WeDo2TiltDirection = { + UP: 'up', + DOWN: 'down', + LEFT: 'left', + RIGHT: 'right', + ANY: 'any' +}; + +/** + * Scratch 3.0 blocks to interact with a LEGO WeDo 2.0 peripheral. + */ +class Scratch3WeDo2Blocks { + + /** + * @return {string} - the ID of this extension. + */ + static get EXTENSION_ID () { + return 'wedo2'; + } + + /** + * @return {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold. + */ + static get TILT_THRESHOLD () { + return 15; + } + + /** + * Construct a set of WeDo 2.0 blocks. + * @param {Runtime} runtime - the Scratch 3.0 runtime. + */ + constructor (runtime) { + /** + * The Scratch 3.0 runtime. + * @type {Runtime} + */ + this.runtime = runtime; + + // Create a new WeDo 2.0 peripheral instance + this._peripheral = new WeDo2(this.runtime, Scratch3WeDo2Blocks.EXTENSION_ID); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: Scratch3WeDo2Blocks.EXTENSION_ID, + name: 'WeDo 2.0', + blockIconURI: iconURI, + showStatusButton: true, + blocks: [ + { + opcode: 'motorOnFor', + text: formatMessage({ + id: 'wedo2.motorOnFor', + default: 'turn [MOTOR_ID] on for [DURATION] seconds', + description: 'turn a motor on for some time' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: WeDo2MotorLabel.DEFAULT + }, + DURATION: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'motorOn', + text: formatMessage({ + id: 'wedo2.motorOn', + default: 'turn [MOTOR_ID] on', + description: 'turn a motor on indefinitely' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: WeDo2MotorLabel.DEFAULT + } + } + }, + { + opcode: 'motorOff', + text: formatMessage({ + id: 'wedo2.motorOff', + default: 'turn [MOTOR_ID] off', + description: 'turn a motor off' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: WeDo2MotorLabel.DEFAULT + } + } + }, + { + opcode: 'startMotorPower', + text: formatMessage({ + id: 'wedo2.startMotorPower', + default: 'set [MOTOR_ID] power to [POWER]', + description: 'set the motor\'s power and turn it on' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: WeDo2MotorLabel.DEFAULT + }, + POWER: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'setMotorDirection', + text: formatMessage({ + id: 'wedo2.setMotorDirection', + default: 'set [MOTOR_ID] direction to [MOTOR_DIRECTION]', + description: 'set the motor\'s turn direction' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: WeDo2MotorLabel.DEFAULT + }, + MOTOR_DIRECTION: { + type: ArgumentType.STRING, + menu: 'MOTOR_DIRECTION', + defaultValue: WeDo2MotorDirection.FORWARD + } + } + }, + { + opcode: 'setLightHue', + text: formatMessage({ + id: 'wedo2.setLightHue', + default: 'set light color to [HUE]', + description: 'set the LED color' + }), + blockType: BlockType.COMMAND, + arguments: { + HUE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'playNoteFor', + text: formatMessage({ + id: 'wedo2.playNoteFor', + default: 'play note [NOTE] for [DURATION] seconds', + description: 'play a certain note for some time' + }), + blockType: BlockType.COMMAND, + arguments: { + NOTE: { + type: ArgumentType.NUMBER, // TODO: ArgumentType.MIDI_NOTE? + defaultValue: 60 + }, + DURATION: { + type: ArgumentType.NUMBER, + defaultValue: 0.5 + } + }, + hideFromPalette: true + }, + { + opcode: 'whenDistance', + text: formatMessage({ + id: 'wedo2.whenDistance', + default: 'when distance [OP] [REFERENCE]', + description: 'check for when distance is < or > than reference' + }), + blockType: BlockType.HAT, + arguments: { + OP: { + type: ArgumentType.STRING, + menu: 'OP', + defaultValue: '<' + }, + REFERENCE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'whenTilted', + text: formatMessage({ + id: 'wedo2.whenTilted', + default: 'when tilted [TILT_DIRECTION_ANY]', + description: 'check when tilted in a certain direction' + }), + func: 'isTilted', + blockType: BlockType.HAT, + arguments: { + TILT_DIRECTION_ANY: { + type: ArgumentType.STRING, + menu: 'TILT_DIRECTION_ANY', + defaultValue: WeDo2TiltDirection.ANY + } + } + }, + { + opcode: 'getDistance', + text: formatMessage({ + id: 'wedo2.getDistance', + default: 'distance', + description: 'the value returned by the distance sensor' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'isTilted', + text: formatMessage({ + id: 'wedo2.isTilted', + default: 'tilted [TILT_DIRECTION_ANY]?', + description: 'whether the tilt sensor is tilted' + }), + blockType: BlockType.BOOLEAN, + arguments: { + TILT_DIRECTION_ANY: { + type: ArgumentType.STRING, + menu: 'TILT_DIRECTION_ANY', + defaultValue: WeDo2TiltDirection.ANY + } + } + }, + { + opcode: 'getTiltAngle', + text: formatMessage({ + id: 'wedo2.getTiltAngle', + default: 'tilt angle [TILT_DIRECTION]', + description: 'the angle returned by the tilt sensor' + }), + blockType: BlockType.REPORTER, + arguments: { + TILT_DIRECTION: { + type: ArgumentType.STRING, + menu: 'TILT_DIRECTION', + defaultValue: WeDo2TiltDirection.UP + } + } + } + ], + menus: { + MOTOR_ID: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'wedo2.motorId.default', + default: 'motor', + description: 'label for motor element in motor menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorLabel.DEFAULT + }, + { + text: formatMessage({ + id: 'wedo2.motorId.a', + default: 'motor A', + description: 'label for motor A element in motor menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorLabel.A + }, + { + text: formatMessage({ + id: 'wedo2.motorId.b', + default: 'motor B', + description: 'label for motor B element in motor menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorLabel.B + }, + { + text: formatMessage({ + id: 'wedo2.motorId.all', + default: 'all motors', + description: 'label for all motors element in motor menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorLabel.ALL + } + ] + }, + MOTOR_DIRECTION: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'wedo2.motorDirection.forward', + default: 'this way', + description: + 'label for forward element in motor direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorDirection.FORWARD + }, + { + text: formatMessage({ + id: 'wedo2.motorDirection.backward', + default: 'that way', + description: + 'label for backward element in motor direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorDirection.BACKWARD + }, + { + text: formatMessage({ + id: 'wedo2.motorDirection.reverse', + default: 'reverse', + description: + 'label for reverse element in motor direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorDirection.REVERSE + } + ] + }, + TILT_DIRECTION: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'wedo2.tiltDirection.up', + default: 'up', + description: 'label for up element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.UP + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.down', + default: 'down', + description: 'label for down element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.left', + default: 'left', + description: 'label for left element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.right', + default: 'right', + description: 'label for right element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.RIGHT + } + ] + }, + TILT_DIRECTION_ANY: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'wedo2.tiltDirection.up', + default: 'up' + }), + value: WeDo2TiltDirection.UP + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.down', + default: 'down' + }), + value: WeDo2TiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.left', + default: 'left' + }), + value: WeDo2TiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.right', + default: 'right' + }), + value: WeDo2TiltDirection.RIGHT + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.any', + default: 'any', + description: 'label for any element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.ANY + } + ] + }, + OP: { + acceptReporters: true, + items: ['<', '>'] + } + } + }; + } + + /** + * Turn specified motor(s) on for a specified duration. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to activate. + * @property {int} DURATION - the amount of time to run the motors. + * @return {Promise} - a promise which will resolve at the end of the duration. + */ + motorOnFor (args) { + // TODO: cast args.MOTOR_ID? + let durationMS = Cast.toNumber(args.DURATION) * 1000; + durationMS = MathUtil.clamp(durationMS, 0, 15000); + return new Promise(resolve => { + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.turnOnFor(durationMS); + } + }); + + // Run for some time even when no motor is connected + setTimeout(resolve, durationMS); + }); + } + + /** + * Turn specified motor(s) on indefinitely. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to activate. + * @return {Promise} - a Promise that resolves after some delay. + */ + motorOn (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.turnOn(); + } + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Turn specified motor(s) off. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to deactivate. + * @return {Promise} - a Promise that resolves after some delay. + */ + motorOff (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.turnOff(); + } + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Turn specified motor(s) off. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to be affected. + * @property {int} POWER - the new power level for the motor(s). + * @return {Promise} - a Promise that resolves after some delay. + */ + startMotorPower (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100); + motor.turnOn(); + } + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Set the direction of rotation for specified motor(s). + * If the direction is 'reverse' the motor(s) will be reversed individually. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to be affected. + * @property {MotorDirection} MOTOR_DIRECTION - the new direction for the motor(s). + * @return {Promise} - a Promise that resolves after some delay. + */ + setMotorDirection (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + switch (args.MOTOR_DIRECTION) { + case WeDo2MotorDirection.FORWARD: + motor.direction = 1; + break; + case WeDo2MotorDirection.BACKWARD: + motor.direction = -1; + break; + case WeDo2MotorDirection.REVERSE: + motor.direction = -motor.direction; + break; + default: + log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`); + break; + } + // keep the motor on if it's running, and update the pending timeout if needed + if (motor.isOn) { + if (motor.pendingTimeoutDelay) { + motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now()); + } else { + motor.turnOn(); + } + } + } + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Set the LED's hue. + * @param {object} args - the block's arguments. + * @property {number} HUE - the hue to set, in the range [0,100]. + * @return {Promise} - a Promise that resolves after some delay. + */ + setLightHue (args) { + // Convert from [0,100] to [0,360] + let inputHue = Cast.toNumber(args.HUE); + inputHue = MathUtil.wrapClamp(inputHue, 0, 100); + const hue = inputHue * 360 / 100; + + const rgbObject = color.hsvToRgb({h: hue, s: 1, v: 1}); + + const rgbDecimal = color.rgbToDecimal(rgbObject); + + this._peripheral.setLED(rgbDecimal); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Make the WeDo 2.0 peripheral play a MIDI note for the specified duration. + * @param {object} args - the block's arguments. + * @property {number} NOTE - the MIDI note to play. + * @property {number} DURATION - the duration of the note, in seconds. + * @return {Promise} - a promise which will resolve at the end of the duration. + */ + playNoteFor (args) { + let durationMS = Cast.toNumber(args.DURATION) * 1000; + durationMS = MathUtil.clamp(durationMS, 0, 3000); + const note = MathUtil.clamp(Cast.toNumber(args.NOTE), 25, 125); // valid WeDo 2.0 sounds + if (durationMS === 0) return; // WeDo 2.0 plays duration '0' forever + return new Promise(resolve => { + const tone = this._noteToTone(note); + this._peripheral.playTone(tone, durationMS); + + // Run for some time even when no piezo is connected + setTimeout(resolve, durationMS); + }); + } + + /** + * Compare the distance sensor's value to a reference. + * @param {object} args - the block's arguments. + * @property {string} OP - the comparison operation: '<' or '>'. + * @property {number} REFERENCE - the value to compare against. + * @return {boolean} - the result of the comparison, or false on error. + */ + whenDistance (args) { + switch (args.OP) { + case '<': + return this._peripheral.distance < Cast.toNumber(args.REFERENCE); + case '>': + return this._peripheral.distance > Cast.toNumber(args.REFERENCE); + default: + log.warn(`Unknown comparison operator in whenDistance: ${args.OP}`); + return false; + } + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + whenTilted (args) { + return this._isTilted(args.TILT_DIRECTION_ANY); + } + + /** + * @return {number} - the distance sensor's value, scaled to the [0,100] range. + */ + getDistance () { + return this._peripheral.distance; + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + isTilted (args) { + return this._isTilted(args.TILT_DIRECTION_ANY); + } + + /** + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION - the direction (up, down, left, right) to check. + * @return {number} - the tilt sensor's angle in the specified direction. + * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right). + */ + getTiltAngle (args) { + return this._getTiltAngle(args.TILT_DIRECTION); + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {TiltDirection} direction - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + * @private + */ + _isTilted (direction) { + switch (direction) { + case WeDo2TiltDirection.ANY: + return this._getTiltAngle(WeDo2TiltDirection.UP) >= Scratch3WeDo2Blocks.TILT_THRESHOLD || + this._getTiltAngle(WeDo2TiltDirection.DOWN) >= Scratch3WeDo2Blocks.TILT_THRESHOLD || + this._getTiltAngle(WeDo2TiltDirection.LEFT) >= Scratch3WeDo2Blocks.TILT_THRESHOLD || + this._getTiltAngle(WeDo2TiltDirection.RIGHT) >= Scratch3WeDo2Blocks.TILT_THRESHOLD; + default: + return this._getTiltAngle(direction) >= Scratch3WeDo2Blocks.TILT_THRESHOLD; + } + } + + /** + * @param {TiltDirection} direction - the direction (up, down, left, right) to check. + * @return {number} - the tilt sensor's angle in the specified direction. + * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right). + * @private + */ + _getTiltAngle (direction) { + switch (direction) { + case WeDo2TiltDirection.UP: + return this._peripheral.tiltY > 45 ? 256 - this._peripheral.tiltY : -this._peripheral.tiltY; + case WeDo2TiltDirection.DOWN: + return this._peripheral.tiltY > 45 ? this._peripheral.tiltY - 256 : this._peripheral.tiltY; + case WeDo2TiltDirection.LEFT: + return this._peripheral.tiltX > 45 ? 256 - this._peripheral.tiltX : -this._peripheral.tiltX; + case WeDo2TiltDirection.RIGHT: + return this._peripheral.tiltX > 45 ? this._peripheral.tiltX - 256 : this._peripheral.tiltX; + default: + log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`); + } + } + + /** + * Call a callback for each motor indexed by the provided motor ID. + * @param {MotorID} motorID - the ID specifier. + * @param {Function} callback - the function to call with the numeric motor index for each motor. + * @private + */ + _forEachMotor (motorID, callback) { + let motors; + switch (motorID) { + case WeDo2MotorLabel.A: + motors = [0]; + break; + case WeDo2MotorLabel.B: + motors = [1]; + break; + case WeDo2MotorLabel.ALL: + case WeDo2MotorLabel.DEFAULT: + motors = [0, 1]; + break; + default: + log.warn(`Invalid motor ID: ${motorID}`); + motors = []; + break; + } + for (const index of motors) { + callback(index); + } + } + + /** + * @param {number} midiNote - the MIDI note value to convert. + * @return {number} - the frequency, in Hz, corresponding to that MIDI note value. + * @private + */ + _noteToTone (midiNote) { + // Note that MIDI note 69 is A4, 440 Hz + return 440 * Math.pow(2, (midiNote - 69) / 12); + } +} + +module.exports = Scratch3WeDo2Blocks; diff --git a/local-scratch-vm/src/extensions/scratchLab_animatedText/index.js b/local-scratch-vm/src/extensions/scratchLab_animatedText/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e9584e03f4af36b23d34188e9ea5b8e3e7956650 --- /dev/null +++ b/local-scratch-vm/src/extensions/scratchLab_animatedText/index.js @@ -0,0 +1,953 @@ +const formatMessage = require('format-message'); +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); +const Color = require('../../util/color'); +const Clone = require('../../util/clone'); +const Timer = require('../../util/timer'); + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMjcuODM0IDlhMyAzIDAgMDEyLjU0NiAxLjQxMmwuMDk3LjE2Ny4wNTQuMTEuMDUyLjExMi4wNDguMTEyIDYuMjIyIDE2YTMuMDAxIDMuMDAxIDAgMDEtMi4yNyA0LjA0MWwtLjE4LjAyNS0uMTE1LjAxMS0uMTE2LjAwNy0uMTE1LjAwM2gtMS44NTVhMyAzIDAgMDEtMi41NDUtMS40MTJsLS4wOTYtLjE2Ny0uMTA3LS4yMjItLjA0OC0uMTExTDI4Ljk4MyAyOGgtNC45M2wtLjQyMiAxLjA4N2EzLjAwMyAzLjAwMyAwIDAxLTIuNDEgMS44ODlsLS4xOTMuMDE4LS4xOTQuMDA2LTEuOTQtLjAwMi0uMDk2LjAwMkg3YTMgMyAwIDAxLTIuODctMy44NzJsLjA3Mi0uMjA5IDYuMTgzLTE2YTMuMDAxIDMuMDAxIDAgMDEyLjYwNC0xLjkxM0wxMy4xODQgOWwzLjkuMDAxLjA5OS0uMDAxIDMuOTI0LjAwMi4wOTUtLjAwMiAzLjkwNS4wMDIuMDk1LS4wMDJoMi42MzJ6IiBmaWxsLW9wYWNpdHk9Ii4xNSIgZmlsbD0iIzAwMCIvPjxwYXRoIGQ9Ik0yNS42NjMgMjFsLjgxNi0yLjA5OS44MTYgMi4wOTloLTEuNjMyem0xMC4yNTggNi4yNzVsLTYuMjIzLTE2LS4wNzUtLjE2OC0uMDg1LS4xNDVjLS4zODctLjYxMS0xLjAxOS0uOTYyLTEuNzAzLS45NjJoLTIuNjMzbC0uMDk2LjAwMi0uMDYyLS4wMDFMMjEuMjAyIDEwbC0uMDk2LjAwMi0uMDYyLS4wMDFMMTcuMTgzIDEwbC0uMDg2LjAwMkwxMy4xODQgMTBsLS4xNjUuMDA3YTIuMDAzIDIuMDAzIDAgMDAtMS43MDIgMS4yNzJsLTYuMTgyIDE2LS4wNTkuMTc1QTIgMiAwIDAwNyAzMGgxMS43OThsLjA4OC0uMDAyIDEuOTQ5LjAwMi4xNjMtLjAwNy4xNjEtLjAxOWEyIDIgMCAwMDEuNTM5LTEuMjQ5bC42Ny0xLjcyNWg2LjI5OWwuNjcyIDEuNzI2LjA3NC4xNjcuMDg2LjE0NWMuMzg3LjYxMSAxLjAxOC45NjIgMS43MDMuOTYyaDEuODU1bC4xNzQtLjAwOS4xNjQtLjAyNGMuOTc2LS4xODcgMS42NjItMS4wMDMgMS42NjItMS45NjcgMC0uMjQ4LS4wNDYtLjQ5NC0uMTM2LS43MjV6IiBmaWxsLW9wYWNpdHk9Ii4yNSIgZmlsbD0iIzAwMCIvPjxwYXRoIGQ9Ik0xMy4xODMgMTFoMy44MThhMSAxIDAgMDEuOTQxIDEuMzM4bC01Ljc0MiAxNmExIDEgMCAwMS0uOTQuNjYySDdhMSAxIDAgMDEtLjkzMy0xLjM2bDYuMTgzLTE2YTEgMSAwIDAxLjkzMy0uNjR6IiBmaWxsPSIjNEM5N0ZGIi8+PHBhdGggZD0iTTE3LjE4MyAxMUgyMWExIDEgMCAwMS45NDIgMS4zMzhsLTUuNzQyIDE2YTEgMSAwIDAxLS45NDEuNjYyaC00LjI2YTEgMSAwIDAxLS45MzItMS4zNmw2LjE4My0xNmExIDEgMCAwMS45MzMtLjY0eiIgZmlsbD0iI0NGNjNDRiIvPjxwYXRoIGQ9Ik0yMS4yMDIgMTFIMjVhMSAxIDAgMDEuOTMzIDEuMzYxbC02LjIwMyAxNmExIDEgMCAwMS0uOTMyLjYzOUgxNWExIDEgMCAwMS0uOTMzLTEuMzYxbDYuMjAzLTE2YTEgMSAwIDAxLjkzMi0uNjM5eiIgZmlsbD0iI0ZGQkYwMCIvPjxwYXRoIGQ9Ik0yNy44MzQgMTFhMSAxIDAgMDEuOTMyLjYzOGw2LjIyMiAxNkExIDEgMCAwMTM0LjA1NiAyOWgtMS44NTRhMSAxIDAgMDEtLjkzMi0uNjM4TDMwLjM1MSAyNmgtNy42NjZsLS45MTkgMi4zNjJhMSAxIDAgMDEtLjkzMi42MzhIMTguOThhMSAxIDAgMDEtLjkzMi0xLjM2Mmw2LjIyMi0xNmExIDEgMCAwMS45MzItLjYzOHptLTEuMzE2IDUuMTQzTDI0LjI0IDIyaDQuNTU2bC0yLjI3OC01Ljg1N3oiIGZpbGw9IiNGRkYiLz48L2c+PC9zdmc+'; +const menuIconURI = blockIconURI; +const DefaultText = 'Welcome to my project!'; +const DefaultAnimateText = 'Here we go!'; +const SANS_SERIF_ID = 'Sans Serif'; +const SERIF_ID = 'Serif'; +const HANDWRITING_ID = 'Handwriting'; +const MARKER_ID = 'Marker'; +const CURLY_ID = 'Curly'; +const PIXEL_ID = 'Pixel'; + +/* PenguinMod Fonts */ +const PLAYFUL_ID = 'Playful'; +const BUBBLY_ID = 'Bubbly'; +const BITSANDBYTES_ID = 'Bits and Bytes'; +const TECHNOLOGICAL_ID = 'Technological'; +const ARCADE_ID = 'Arcade'; +const ARCHIVO_ID = 'Archivo'; +const ARCHIVOBLACK_ID = 'Archivo Black'; +const SCRATCH_ID = 'Scratch'; + +const RANDOM_ID = 'Random'; + +class Scratch3TextBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + this._onTargetWillExit = this._onTargetWillExit.bind(this); + this.runtime.on('targetWasRemoved', this._onTargetWillExit); + this._onTargetCreated = this._onTargetCreated.bind(this); + this.runtime.on('targetWasCreated', this._onTargetCreated); + this.runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this)); + } + + get FONT_IDS () { + return [SANS_SERIF_ID, SERIF_ID, HANDWRITING_ID, MARKER_ID, CURLY_ID, PIXEL_ID, PLAYFUL_ID, BUBBLY_ID, ARCADE_ID, BITSANDBYTES_ID, TECHNOLOGICAL_ID, SCRATCH_ID, ARCHIVO_ID, ARCHIVOBLACK_ID]; + } + get DEFAULT_TEXT_STATE () { + return { + skinId: null, + text: DefaultText, + font: 'Handwriting', + color: 'hsla(225, 15%, 40%, 1)', + // GUI's text-primary color + size: 24, + maxWidth: 480, + align: 'center', + strokeWidth: 0, + strokeColor: 'black', + rainbow: false, + visible: false, + targetSize: null, + fullText: null + }; + } + + /** + * The key to load & store a target's text-related state. + * @type {string} + */ + get STATE_KEY () { + return 'Scratch.text'; + } + + _getFonts() { + return [{ + text: 'Sans Serif', + value: SANS_SERIF_ID + }, { + text: 'Serif', + value: SERIF_ID + }, { + text: 'Handwriting', + value: HANDWRITING_ID + }, { + text: 'Marker', + value: MARKER_ID + }, { + text: 'Curly', + value: CURLY_ID + }, { + text: 'Pixel', + value: PIXEL_ID + }, { + text: 'Playful', + value: PLAYFUL_ID + }, { + text: 'Bubbly', + value: BUBBLY_ID + }, { + text: 'Arcade', + value: ARCADE_ID + }, { + text: 'Bits and Bytes', + value: BITSANDBYTES_ID + }, { + text: 'Technological', + value: TECHNOLOGICAL_ID + }, { + text: 'Scratch', + value: SCRATCH_ID + }, { + text: 'Archivo', + value: ARCHIVO_ID + }, { + text: 'Archivo Black', + value: ARCHIVOBLACK_ID + }, + ...this.runtime.fontManager.getFonts().map(i => ({ + text: i.name, + value: i.family + })), + { + text: 'random font', + value: RANDOM_ID + }]; + } + + getInfo () { + return { + id: 'text', + name: 'Animated Text', + blockIconURI: blockIconURI, + menuIconURI: menuIconURI, + blocks: [{ + opcode: 'setText', + text: formatMessage({ + id: 'text.setText', + "default": 'show text [TEXT]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: DefaultText + } + } + }, { + opcode: 'animateText', + text: formatMessage({ + id: 'text.animateText', + "default": '[ANIMATE] text [TEXT]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + ANIMATE: { + type: ArgumentType.STRING, + menu: 'ANIMATE', + defaultValue: 'rainbow' + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: DefaultAnimateText + } + } + }, { + opcode: 'clearText', + text: formatMessage({ + id: 'text.clearText', + "default": 'show sprite', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: {} + }, '---', { + opcode: 'setFont', + text: formatMessage({ + id: 'text.setFont', + "default": 'set font to [FONT]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + FONT: { + type: ArgumentType.STRING, + menu: 'FONT', + defaultValue: 'Pixel' + } + } + }, { + opcode: 'setColor', + text: formatMessage({ + id: 'text.setColor', + "default": 'set text color to [COLOR]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + COLOR: { + type: ArgumentType.COLOR + } + } + }, { + opcode: 'setWidth', + text: formatMessage({ + id: 'text.setWidth', + "default": 'set width to [WIDTH] aligned [ALIGN]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 200 + }, + ALIGN: { + type: ArgumentType.STRING, + defaultValue: 'left', + menu: 'ALIGN' + } + } + }, { + opcode: 'rainbow', + text: formatMessage({ + id: 'text.rainbow', + default: 'rainbow for [SECS] seconds', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + SECS: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + '---', + { + opcode: 'addLine', + text: formatMessage({ + id: 'text.addLine', + default: 'add line [TEXT]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: 'more lines!' + } + } + }, + '---', + { + opcode: 'setOutlineWidth', + text: formatMessage({ + id: 'text.setOutlineWidth', + default: 'set outline width to [WIDTH]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, { + opcode: 'setOutlineColor', + text: formatMessage({ + id: 'text.setOutlineColor', + default: 'set outline color to [COLOR]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + COLOR: { + type: ArgumentType.COLOR + } + } + }, + '---', + { + opcode: 'getVisible', + text: 'is text visible?', + blockType: BlockType.BOOLEAN + }, { + opcode: 'getWidth', + text: 'get width of the text', + blockType: BlockType.REPORTER + }, { + opcode: 'getHeight', + text: 'get height of the text', + blockType: BlockType.REPORTER + }, + { + opcode: "getDisplayedText", + blockType: BlockType.REPORTER, + text: ("displayed text") + }, + { + opcode: "getRender", + blockType: BlockType.REPORTER, + text: ("get data uri of last rendered text") + }, + + // TODO: Give these blocks actual functionality. + // Most of them can be done easily. + + // TURBOWARP BLOCKS (added for compatibility reasons) + // TURBOWARP BLOCKS (added for compatibility reasons) + // TURBOWARP BLOCKS (added for compatibility reasons) + // TURBOWARP BLOCKS (added for compatibility reasons) + // TURBOWARP BLOCKS (added for compatibility reasons) + // TURBOWARP BLOCKS (added for compatibility reasons) + + // TODO: Give these blocks actual functionality. + // Most of them can be done easily. + + { + opcode: "setAlignment", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) align text to [ALIGN]"), + hideFromPalette: true, + arguments: { + ALIGN: { + type: ArgumentType.STRING, + menu: "twAlign" + } + } + }, + { + // why is the other block called "setWidth" :( + opcode: "setWidthValue", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) set width to [WIDTH]"), + hideFromPalette: true, + arguments: { + WIDTH: { + type: ArgumentType.NUMBER, + defaultValue: 200 + } + } + }, + { + opcode: "resetWidth", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) reset text width"), + hideFromPalette: true + }, + "---", + { + opcode: "getLines", + blockType: BlockType.REPORTER, + text: ("(NOT USABLE YET) # of lines"), + hideFromPalette: true, + disableMonitor: true + }, + "---", + { + opcode: "startAnimate", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) start [ANIMATE] animation"), + hideFromPalette: true, + arguments: { + ANIMATE: { + type: ArgumentType.STRING, + menu: "twAnimate", + defaultValue: "rainbow" + } + } + }, + { + opcode: "animateUntilDone", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) animate [ANIMATE] until done"), + hideFromPalette: true, + arguments: { + ANIMATE: { + type: ArgumentType.STRING, + menu: "twAnimate", + defaultValue: "rainbow" + } + } + }, + { + opcode: "isAnimating", + blockType: BlockType.BOOLEAN, + text: ("(NOT USABLE YET) is animating?"), + hideFromPalette: true, + disableMonitor: true + }, + "---", + { + opcode: "setAnimateDuration", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) set [ANIMATE] duration to [NUM] seconds"), + hideFromPalette: true, + arguments: { + ANIMATE: { + type: ArgumentType.STRING, + menu: "twAnimateDuration", + defaultValue: "rainbow" + }, + NUM: { + type: ArgumentType.NUMBER, + defaultValue: 3 + } + } + }, + { + opcode: "resetAnimateDuration", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) reset [ANIMATE] duration"), + hideFromPalette: true, + arguments: { + ANIMATE: { + type: ArgumentType.STRING, + menu: "twAnimateDuration", + defaultValue: "rainbow" + } + } + }, + { + opcode: "getAnimateDuration", + blockType: BlockType.REPORTER, + text: ("(NOT USABLE YET) [ANIMATE] duration"), + hideFromPalette: true, + arguments: { + ANIMATE: { + type: ArgumentType.STRING, + menu: "twAnimateDuration", + defaultValue: "rainbow" + } + } + }, + "---", + { + opcode: "setTypeDelay", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) set typing delay to [NUM] seconds"), + hideFromPalette: true, + arguments: { + NUM: { + type: ArgumentType.NUMBER, + defaultValue: 0.1 + } + } + }, + { + opcode: "resetTypeDelay", + blockType: BlockType.COMMAND, + text: ("(NOT USABLE YET) reset typing delay"), + hideFromPalette: true + }, + { + opcode: "getTypeDelay", + blockType: BlockType.REPORTER, + text: ("(NOT USABLE YET) typing delay"), + hideFromPalette: true, + disableMonitor: true + }, + "---", + { + opcode: "textActive", + blockType: BlockType.BOOLEAN, + text: ("(TURBOWARP BLOCK) is showing text?"), + hideFromPalette: true, + disableMonitor: true + }, + { + opcode: "getTextAttribute", + blockType: BlockType.REPORTER, + text: "(NOT USABLE YET) text [ATTRIBUTE]", + arguments: { + ATTRIBUTE: { + type: ArgumentType.STRING, + menu: "twAnimate" + } + }, + disableMonitor: true, + hideFromPalette: true + } + + ], + menus: { + FONT: { + items: '_getFonts', + isTypeable: true + }, + ALIGN: { + items: [{ + text: 'left', + value: 'left' + }, { + text: 'center', + value: 'center' + }, { + text: 'right', + value: 'right' + }] + }, + ANIMATE: { + items: [{ + text: 'type', + value: 'type' + }, { + text: 'rainbow', + value: 'rainbow' + }, { + text: 'zoom', + value: 'zoom' + }] + }, + // TurboWarp menus (acceptReporters: true) + twAnimate: { + acceptReporters: true, + items: [ + { + text: ("type"), + value: "type" + }, + { + text: ("rainbow"), + value: "rainbow" + }, + { + text: ("zoom"), + value: "zoom" + } + ] + }, + twAnimateDuration: { + acceptReporters: true, + items: [ + { + text: ("rainbow"), + value: "rainbow" + }, + { + text: ("zoom"), + value: "zoom" + } + ] + }, + twAlign: { + acceptReporters: true, + items: [ + { + text: ("left"), + value: "left" + }, + { + text: ("center"), + value: "center" + }, + { + text: ("right"), + value: "right" + } + ] + } + } + }; + } + setText (args, util) { + const textState = this._getTextState(util.target); + + textState.text = this._formatText(args.TEXT); + textState.visible = true; + textState.animating = false; + + this._renderText(util.target); // Yield until the next tick. + } + clearText (args, util) { + const target = util.target; + + const textState = this._getTextState(target); + + textState.visible = false; // Set state so that clones can know not to render text + + textState.animating = false; + const costume = target.getCostumes()[target.currentCostume]; + this.runtime.renderer.updateDrawableSkinId(target.drawableID, costume.skinId); // Yield until the next tick. + } + stopAll () { + this.runtime.targets.forEach(target => { + this.clearText({}, { + target: target + }); + }); + } + addLine (args, util) { + const textState = this._getTextState(util.target); + + textState.text += `\n${this._formatText(args.TEXT)}`; + textState.visible = true; + textState.animating = false; + + this._renderText(util.target); // Yield until the next tick. + } + setFont (args, util) { + const textState = this._getTextState(util.target); + + if (args.FONT === RANDOM_ID) { + textState.font = this._randomFontOtherThan(textState.font); + } else { + textState.font = args.FONT; + } + + this._renderText(util.target); + } + _randomFontOtherThan (currentFont) { + const otherFonts = this.FONT_IDS.filter(id => id !== currentFont); + return otherFonts[Math.floor(Math.random() * otherFonts.length)]; + } + setColor (args, util) { + const textState = this._getTextState(util.target); + + textState.color = Cast.toString(args.COLOR); + + this._renderText(util.target); + } + setWidth (args, util) { + const textState = this._getTextState(util.target); + + textState.maxWidth = Cast.toNumber(args.WIDTH); + textState.align = args.ALIGN; + + this._renderText(util.target); + } + setSize (args, util) { + const textState = this._getTextState(util.target); + + textState.size = Cast.toNumber(args.SIZE); + + this._renderText(util.target); + } + setAlign (args, util) { + const textState = this._getTextState(util.target); + + textState.maxWidth = Cast.toNumber(args.WIDTH); + textState.align = args.ALIGN; + + this._renderText(util.target); + } + setOutlineWidth (args, util) { + const textState = this._getTextState(util.target); + + textState.strokeWidth = Cast.toNumber(args.WIDTH); + + this._renderText(util.target); + } + setOutlineColor (args, util) { + const textState = this._getTextState(util.target); + + textState.strokeColor = Cast.toString(args.COLOR); + textState.visible = true; + + this._renderText(util.target); + } + + textActive (args, util) { + return this.getVisible(args, util); + } + + getVisible (args, util) { + const textState = this._getTextState(util.target); + + return textState.visible; + } + + getDisplayedText(args, util) { + const textState = this._getTextState(util.target); + + return textState.text; + } + + getRender(args, util) { + const textSkin = this._getTextSkin(util.target); + if (!textSkin) return; + + return textSkin._canvas.toDataURL(); + } + + getWidth (args, util) { + const textSkin = this._getTextSkin(util.target); + if (!textSkin) return 0; + return textSkin.width; + } + + getHeight (args, util) { + const textSkin = this._getTextSkin(util.target); + if (!textSkin) return 0; + return textSkin.height; + } + + _getTextSkin (target) { + const textState = this._getTextState(target); + if (!textState) return; + if (!textState.skinId) return; + const textSkin = this.runtime.renderer._allSkins[textState.skinId]; + + return textSkin; + } + + /* + * The animations (type, zoom and rainbow) all follow the same pattern. + * 1. The inital state of the animation is set and rendered + * 2. constiables to indicate the final state are stored on the textState + * 3. A promise is returned that starts a tick interval for some frame rate + * 4. The tick function checks for animation-specific end condition (like time) + * and global end condition (like being cancelled by stopAll or setText) + * 5. If the end conditions are met, the tick function does the following: + * (a) Sets the final state + * (b) Clears the animation state constiables + * (c) Clears the interval to stop tick from running + * (d) Resolves the promise to indicate the block is done + * + * We do not use the stack timer/stack counter functionality the VM provides + * because those would leave the animation hanging in the middle if the stack is cancelled. + * + * TODO abstract this shared functionality for all animations. + */ + _animateText (args, util) { + const target = util.target; + + const textState = this._getTextState(target); + + if (textState.fullText !== null) return; // Let the running animation finish, do nothing + // On "first tick", set the text and force animation flags on and render + + textState.fullText = this._formatText(args.TEXT); + textState.text = textState.fullText[0]; // Start with first char visible + + textState.visible = true; + textState.animating = true; + + this._renderText(target); + + this.runtime.requestRedraw(); + return new Promise((resolve => { + const interval = setInterval(() => { + if (textState.animating && textState.visible && textState.text !== textState.fullText) { + textState.text = textState.fullText.substring(0, textState.text.length + 1); + } else { + // NB there is no need to update the .text state here, since it is at the end of the + // animation (when text == fullText), is being cancelled by force setting text, + // or is being cancelled by hitting the stop button which hides the text anyway. + textState.fullText = null; + clearInterval(interval); + resolve(); + } + + this._renderText(target); + + this.runtime.requestRedraw(); + }, 60 + /* ms, about 1 char every 2 frames */ + ); + })); + } + _zoomText (args, util) { + const target = util.target; + + const textState = this._getTextState(target); + + if (textState.targetSize !== null) return; // Let the running animation finish, do nothing + + const timer = new Timer(); + // On "first tick", set the text and force animation flags on and render + const durationMs = Cast.toNumber(args.SECS || 0.5) * 1000; + + textState.text = this._formatText(args.TEXT); + textState.visible = true; + textState.animating = true; + textState.targetSize = target.size; + target.setSize(0); + + this._renderText(target); + + this.runtime.requestRedraw(); + timer.start(); + return new Promise((resolve => { + const interval = setInterval(() => { + const timeElapsed = timer.timeElapsed(); + + if (textState.animating && textState.visible && timeElapsed < durationMs) { + target.setSize(textState.targetSize * timeElapsed / durationMs); + } else { + target.setSize(textState.targetSize); + textState.targetSize = null; + clearInterval(interval); + resolve(); + } + + this._renderText(target); + + this.runtime.requestRedraw(); + }, this.runtime.currentStepTime); + })); + } + animateText (args, util) { + switch (args.ANIMATE) { + case 'rainbow': + return this.rainbow(args, util); + + case 'type': + return this._animateText(args, util); + + case 'zoom': + return this._zoomText(args, util); + } + } + rainbow (args, util) { + const target = util.target; + + const textState = this._getTextState(target); + + if (textState.rainbow) return; // Let the running animation finish, do nothing + + const timer = new Timer(); + // On "first tick", set the text and force animation flags on and render + const durationMs = Cast.toNumber(args.SECS || 2) * 1000; + if (!args.TEXT) { + args.TEXT = textState.text; + if (!textState.visible) return; + } + + textState.text = this._formatText(args.TEXT); + textState.visible = true; + textState.animating = true; + textState.rainbow = true; + + this._renderText(target); + + timer.start(); + return new Promise((resolve => { + const interval = setInterval(() => { + const timeElapsed = timer.timeElapsed(); + + if (textState.animating && textState.visible && timeElapsed < durationMs) { + textState.rainbow = true; + target.setEffect('color', timeElapsed / -5); + } else { + textState.rainbow = false; + target.setEffect('color', 0); + clearInterval(interval); + resolve(); + } + + this._renderText(target); + }, this.runtime.currentStepTime); + })); + } + _getTextState (target) { + let textState = target.getCustomState(this.STATE_KEY); + + if (!textState) { + textState = Clone.simple(this.DEFAULT_TEXT_STATE); + target.setCustomState(this.STATE_KEY, textState); + } + + return textState; + } + _formatText (text) { + // Non-integers should be rounded to 2 decimal places (no more, no less), unless they're small enough that + if (text === '') return text; + // rounding would display them as 0.00. This matches 2.0's behavior: + // https://github.com/LLK/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/scratch/ScratchSprite.as#L579-L585 + + if (typeof text === 'number' && Math.abs(text) >= 0.01 && text % 1 !== 0) { + text = text.toFixed(2); + } + + text = Cast.toString(text); + return text; + } + _renderText (target) { + if (!this.runtime.renderer) return; + + const textState = this._getTextState(target); + + if (!textState.visible) return; // Resetting to costume is done in clear block, early return here is for clones + + textState.skinId = this.runtime.renderer.updateTextCostumeSkin(textState); + this.runtime.renderer.updateDrawableSkinId(target.drawableID, textState.skinId); + } + + /** + * When a Target is cloned, clone the text 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 sourceTextState = sourceTarget.getCustomState(this.STATE_KEY); + + if (sourceTextState) { + newTarget.setCustomState(this.STATE_KEY, Clone.simple(sourceTextState)); + // Note here that clones do not share skins with their original target. This is a subtle but important + const newTargetState = newTarget.getCustomState(this.STATE_KEY); + // departure from the rest of Scratch, where clones always stay in sync with the originals costume. + // The "rule" is anything that can be done with the blocks is clone-specific, + // since that is where you make clones, + // but anything outside of the blocks (costume/sounds) are shared. + // For example, graphic effects are clone-specific, + // but changing the costume in the paint editor is shared. + // Since you can change the text on the skin from the blocks, each clone needs its own skin. + + newTargetState.skinId = null; // Unset all of the animation flags + + newTargetState.rainbow = false; + newTargetState.targetSize = null; + newTargetState.fullText = null; + // Must wait until the drawable has been initialized, but before render. We can + // wait for the first EVENT_TARGET_VISUAL_CHANGE for this. + newTargetState.animating = false; + + const onDrawableReady = () => { + this._renderText(newTarget); + + newTarget.off('EVENT_TARGET_VISUAL_CHANGE', onDrawableReady); + }; + + newTarget.on('EVENT_TARGET_VISUAL_CHANGE', onDrawableReady); + } + } + } + _onTargetWillExit (target) { + const textState = this._getTextState(target); + + if (textState.skinId) { + // The drawable will get cleaned up by RenderedTarget#dispose, but that doesn't + // automatically destroy attached skins (because they are usually shared between clones). + // For text skins, however, all clones get their own, so we need to manually destroy them. + this.runtime.renderer.destroySkin(textState.skinId); + textState.skinId = null; + } + } +} + +module.exports = Scratch3TextBlocks; diff --git a/local-scratch-vm/src/extensions/sharkpool_printing/index.js b/local-scratch-vm/src/extensions/sharkpool_printing/index.js new file mode 100644 index 0000000000000000000000000000000000000000..99e1fe86c08b36ef0e5e0b745e7dc8d74e10f7bd --- /dev/null +++ b/local-scratch-vm/src/extensions/sharkpool_printing/index.js @@ -0,0 +1,563 @@ +const ArgumentType = require("../../extension-support/argument-type"); +const BlockType = require("../../extension-support/block-type"); +const ProjectPermissionManager = require('../../util/project-permissions'); +const DOMPurify = require("dompurify"); +const Cast = require("../../util/cast"); +const Color = require("../../util/color"); + +const xmlEscape = function (unsafe) { + unsafe = Cast.toString(unsafe) + return unsafe.replace(/[<>&'"]/g, c => { + switch (c) { + case "<": return "<"; + case ">": return ">"; + case "&": return "&"; + case "'": return "'"; + case "\"": return """; + } + }); +}; +const delay = (ms) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); + }); +}; + +class sharkpoolPrinting { + constructor(runtime) { + this.runtime = runtime; + + // Text Tools + this.letterInfo = { + color: "#000000", font: "Arial", size: "12", + align: "left", letterSpacing: "0", linHeight: "1.2" + }; + // Image Tools + this.imgInfo = { + width: "100", + height: "100", + x: 0, + y: 0, + rot: 0, + }; + + this.isCameraScreenshotEnabled = false; + + this.lastHTMLtxt = ""; + this.elementsToPrint = []; + this.printBackground = null; + } + getInfo() { + return { + id: "sharkpoolPrinting", + name: "Printing", + blocks: [ + { + opcode: "isPrintingSupported", + blockType: BlockType.BOOLEAN, + text: "is printing supported?", + // actually seems like browsers havent deprecated this even though it causes crashes in certain browsers + hideFromPalette: true, + disableMonitor: true, + }, + { + opcode: "printElements", + blockType: BlockType.COMMAND, + text: "print elements and wait", + }, + '---', + { + opcode: "addElementText", + blockType: BlockType.COMMAND, + text: "add text [TXT]", + arguments: { + TXT: { + type: ArgumentType.STRING, + defaultValue: "Hello world!" + } + }, + }, + { + opcode: "addElementScreenshot", + blockType: BlockType.COMMAND, + text: "add stage screenshot", + }, + { + opcode: "addElementImg", + blockType: BlockType.COMMAND, + text: "add image [IMG]", + arguments: { + IMG: { + type: ArgumentType.STRING, + defaultValue: "https://penguinmod.com/favicon.png" + } + }, + }, + { + opcode: "addElementHtml", + blockType: BlockType.COMMAND, + text: "add html [HTML]", + arguments: { + HTML: { + type: ArgumentType.STRING, + defaultValue: "

Header text

Paragraph here

" + } + }, + }, + { + opcode: "removeElements", + blockType: BlockType.COMMAND, + text: "remove all elements", + }, + { blockType: BlockType.LABEL, text: "Formatting" }, + { + opcode: "txtFont", + blockType: BlockType.COMMAND, + text: "set font to [FONT] size [SZ]", + arguments: { + FONT: { + type: ArgumentType.STRING, + defaultValue: "Arial" + }, + SZ: { + type: ArgumentType.NUMBER, + defaultValue: 12 + }, + }, + }, + { + opcode: "txtColor", + blockType: BlockType.COMMAND, + text: "set text color [COLOR]", + arguments: { + COLOR: { + type: ArgumentType.COLOR + }, + }, + }, + { + opcode: "txtAlign", + blockType: BlockType.COMMAND, + text: "align text [ALIGN]", + arguments: { + ALIGN: { + type: ArgumentType.STRING, + menu: "ALIGNMENTS" + }, + }, + }, + { + opcode: "txtSpacing", + blockType: BlockType.COMMAND, + text: "set text spacing letter [LET] line [LIN]", + arguments: { + LET: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + LIN: { + type: ArgumentType.NUMBER, + defaultValue: 1.2 + }, + }, + }, + "---", + { + opcode: "imgSize", + blockType: BlockType.COMMAND, + text: "set image width [W] height [H]", + arguments: { + W: { + type: ArgumentType.NUMBER, + defaultValue: 200 + }, + H: { + type: ArgumentType.NUMBER, + defaultValue: 200 + }, + }, + }, + { + opcode: "imgPos", + blockType: BlockType.COMMAND, + text: "set image position to x [x] y [y]", + arguments: { + x: { + type: ArgumentType.NUMBER, + defaultValue: 100 + }, + y: { + type: ArgumentType.NUMBER, + defaultValue: 0 + }, + }, + }, + { + opcode: "imgRot", + blockType: BlockType.COMMAND, + text: "set image rotation to [rot]", + arguments: { + rot: { + type: ArgumentType.ANGLE, + defaultValue: 90 + }, + }, + }, + { blockType: BlockType.LABEL, text: "Background" }, + { + opcode: "setBGColor", + blockType: BlockType.COMMAND, + text: "set background color [COLOR]", + arguments: { + COLOR: { + type: ArgumentType.COLOR + }, + }, + }, + { + opcode: "setBGImage", + blockType: BlockType.COMMAND, + text: "set background image [IMG]", + arguments: { + IMG: { + type: ArgumentType.STRING, + defaultValue: "https://penguinmod.com/test.png" + } + }, + }, + { + opcode: "setBGRepeat", + blockType: BlockType.COMMAND, + text: "set background to [BGMODE]", + arguments: { + BGMODE: { + type: ArgumentType.STRING, + menu: "BGMODE" + }, + }, + }, + { + opcode: "resetBackground", + blockType: BlockType.COMMAND, + text: "remove background", + }, + { blockType: BlockType.LABEL, text: "Miscellaneous" }, + { + opcode: "elementCount", + blockType: BlockType.REPORTER, + text: "elements in print" + }, + { + opcode: "lastHTML", + blockType: BlockType.REPORTER, + text: "last printed html" + }, + ], + menus: { + ALIGNMENTS: { + acceptReporters: true, + items: ["left", "right", "center"] + }, + BGMODE: { + acceptReporters: true, + items: ["repeat", "not repeat", "fill", "stretch"] + }, + } + }; + } + + // util + _getStageScreenshot() { + // should we look for an external canvas + if (this.runtime.prism_screenshot_checkForExternalCanvas) { + // if so, does one exist (this will check for more than 1 in the future) + if (this.runtime.prism_screenshot_externalCanvas) { + // we dont need to check camera permissions since external canvases + // will never have the ability to get camera data + return this.runtime.prism_screenshot_externalCanvas.toDataURL(); + } + } + // DO NOT REMOVE, USER HAS NOT GIVEN PERMISSION TO SAVE CAMERA IMAGES. + if (this.runtime.ext_videoSensing || this.runtime.ioDevices.video.provider.enabled) { + // user's camera is on, ask for permission to take a picture of them + if (!(this.isCameraScreenshotEnabled)) { + this.isCameraScreenshotEnabled = ProjectPermissionManager.RequestPermission("cameraPictures"); + // 1 pixel of white + if (!this.isCameraScreenshotEnabled) return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/j//z8ABf4C/qc1gYQAAAAASUVORK5CYII="; + } + } + return new Promise(resolve => { + this.runtime.renderer.requestSnapshot(uri => { + resolve(uri); + }); + }); + } + _convertToDataURL(stringg, optMimeType) { + return new Promise((resolve, reject) => { + const config = {}; + if (optMimeType) { + config.type = optMimeType; + } + const blob = new Blob([stringg], config); + + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = reader.result; + resolve(dataUrl); + }; + reader.onerror = (er) => { + reject(er); + }; + reader.readAsDataURL(blob); + }); + } + waitForLoad(element) { + return new Promise((resolve, reject) => { + element.onload = resolve; + element.onerror = reject; + }); + } + applyStylings(objectt, element) { + for (const key in objectt) { + element.style[key] = objectt[key]; + } + } + + // Main + isPrintingSupported() { + return 'print' in window; + } + async printElements() { + await this.beginPrint(); + } + + // Elements + addElementText(args) { + this.prepare(xmlEscape(args.TXT), "txt"); + } + async addElementScreenshot() { + const screenshotUrl = await this._getStageScreenshot(); + this.prepare(xmlEscape(screenshotUrl), "img"); + } + async addElementImg(args) { + let url = args.IMG; + const canFetch = await this.runtime.vm.securityManager.canFetch(url); + if (!canFetch) { + url = 'https://penguinmod.com/notallowed.png'; + } + this.prepare(xmlEscape(url), "img"); + } + async addElementHtml(args) { + const html = args.HTML; + const dataUrl = await this._convertToDataURL(html, 'text/html'); + const canEmbed = await this.runtime.vm.securityManager.canEmbed(dataUrl); + if (!canEmbed) return; + this.prepare(html, "html"); + } + removeElements() { + this.elementsToPrint = []; + } + + // Formatting + txtFont(args) { + this.letterInfo.font = xmlEscape(args.FONT); + this.letterInfo.size = Cast.toNumber(args.SZ); + } + txtColor(args) { + const rgb = Cast.toRgbColorObject(args.COLOR); + const hex = Color.rgbToHex(rgb); + this.letterInfo.color = xmlEscape(hex); + } + txtAlign(args) { + this.letterInfo.align = xmlEscape(args.ALIGN); + } + txtSpacing(args) { + this.letterInfo.letterSpacing = Cast.toNumber(args.LET); + this.letterInfo.linHeight = Cast.toNumber(args.LIN); + } + + imgSize(args) { + this.imgInfo.width = Cast.toNumber(args.W); + this.imgInfo.height = Cast.toNumber(args.H); + } + imgPos(args) { + this.imgInfo.x = Cast.toNumber(args.x); + this.imgInfo.y = Cast.toNumber(args.y); + } + imgRot(args) { + this.imgInfo.rot = Cast.toNumber(args.rot) - 90; + } + + // Background + setBGColor(args) { + if (!this.printBackground) this.printBackground = {}; + const rgb = Cast.toRgbColorObject(args.COLOR); + const hex = Color.rgbToHex(rgb); + this.printBackground.color = hex; + } + async setBGImage(args) { + let url = args.IMG; + const canFetch = await this.runtime.vm.securityManager.canFetch(url); + if (!canFetch) return; + + if (!this.printBackground) this.printBackground = {}; + this.printBackground.image = url; + } + setBGRepeat(args) { + if (!this.printBackground) this.printBackground = {}; + this.printBackground.bgmode = args.BGMODE; + } + resetBackground() { + this.printBackground = null; + } + + // Miscellaneous + elementCount() { + return this.elementsToPrint.length; + } + lastHTML() { + return this.lastHTMLtxt; + } + + prepare(content, type) { + const element = {}; + element.type = type; + + switch (type) { + case 'txt': { + element.style = {}; + element.style.fontFamily = this.letterInfo.font; + element.style.fontSize = `${this.letterInfo.size}px`; + element.style.color = this.letterInfo.color; + element.style.textAlign = this.letterInfo.align; + element.style.letterSpacing = `${this.letterInfo.letterSpacing}px`; + element.style.lineHeight = `${this.letterInfo.linHeight}px`; + + element.textContent = content; + break; + } + case 'img': { + element.style = {}; + element.width = this.imgInfo.width; + element.height = this.imgInfo.height; + element.style.display = "block"; + element.style.transform = `translate(${Cast.toNumber(this.imgInfo.x)}px, ${Cast.toNumber(this.imgInfo.y)}px) rotate(${Cast.toNumber(this.imgInfo.rot)}deg)`; + + element.src = content; + break; + } + case 'html': { + element.html = DOMPurify.sanitize(content); + break; + } + } + + this.elementsToPrint.push(element); + } + async beginPrint() { + const modal = document.createElement("div"); + + const imageElements = []; + for (const element of this.elementsToPrint) { + switch (element.type) { + case 'txt': { + const txtDoc = document.createElement("div"); + this.applyStylings(element.style, txtDoc); + txtDoc.textContent = element.textContent; + modal.appendChild(txtDoc); + break; + } + case 'img': { + const imgDoc = document.createElement("img"); + imgDoc.width = element.width; + imgDoc.height = element.height; + this.applyStylings(element.style, imgDoc); + imgDoc.src = element.src; + modal.appendChild(imgDoc); + imageElements.push(imgDoc); + break; + } + case 'html': { + // sanitization happens on element preperation + modal.innerHTML += element.html; + break; + } + } + } + + this.lastHTMLtxt = modal.innerHTML; + const printWindow = window.open( + "", + "Document", + `scrollbars=yes,resizable=yes,status=no,location=no,toolbar=no,menubar=no,width=720,height=720,left=10,top=10` + ); + if (printWindow) { + printWindow.document.body.appendChild(modal); + printWindow.document.title = 'Document'; + if (this.printBackground) { + const bg = this.printBackground; + const style = document.createElement('style'); + let innerHTML = 'body {\nbackground-attachment: fixed;\n'; + if (bg.color) { + innerHTML += `background: ${xmlEscape(bg.color)};`; + } + if (bg.image) { + innerHTML += `background-image: url(${JSON.stringify(xmlEscape(bg.image))});`; + } + if (bg.bgmode) { + let repeat = 'no-repeat'; + switch (bg.bgmode) { + case 'repeat': + repeat = 'repeat'; + case 'not repeat': + innerHTML += `background-repeat: ${repeat};`; + break; + case 'stretch': + innerHTML += `background-size: 100% 100%;`; + break; + case 'fill': + innerHTML += `background-size: 100%;`; + break; + } + } + innerHTML += '\n}'; + style.innerHTML = innerHTML; + printWindow.document.head.appendChild(style); + // wait for bg image load + if (bg.image) { + const imageEl = new Image(); + imageEl.style = 'position: absolute; left: 0px; top: 0px;' + try { + const promise = this.waitForLoad(imageEl); + imageEl.src = bg.image; + printWindow.document.head.appendChild(imageEl); + await promise; + } finally { + imageEl.remove(); + } + } + } + // wait for images to load if they exist + if (imageElements.length > 0) { + for (const imageEl of imageElements) { + try { + await this.waitForLoad(imageEl); + } catch (e) { + console.warn('Failed to load', imageEl, e); + } + } + } + await delay(50); // browser tends to need just a little bit even after we waited for all the assets + // packaged applications tend to require await on window prompts, print is a prompting api + await printWindow.print(); + printWindow.close(); + modal.remove(); + } else { + console.error("Unable to open print window"); + } + } +} + +module.exports = sharkpoolPrinting; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/silvxrcat_oddmessages/index.js b/local-scratch-vm/src/extensions/silvxrcat_oddmessages/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ad3e953ec862635b10cd39f2083d8a10f9d15660 --- /dev/null +++ b/local-scratch-vm/src/extensions/silvxrcat_oddmessages/index.js @@ -0,0 +1,224 @@ +// Created by silvxrcat +// https://github.com/silvxrcat/ +// +// Most if not all of the code here was not created by a PenguinMod developer! +// Look above for proper crediting :) + +const icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAWYAAAGACAYAAACeO8iJAAAgAElEQVR4Xu2cPZIcSXJGGzrvsDRqcwGarchTkKfheWjGQ6y+PAA0GqnwBKODSADVqK6urAz38N+It8La2iLSw+N9Hm8S2Rh8eXP+z9//+PbNeQvKQwACEAgj8M9fv3zx3sx0AyTsHRf1IQCBqgQshT0tZmRcdUzoCwIQyCIwK2m1mBFyVuTsCwEIdCGgFbRYzAi5y0jQJwQgUIWAVNAiMSPlKjHTBwQg0I2ARM7DYkbK3caAfiEAgWoERuU8JGakXC1e+oEABLoSGJHzpZiRctf46RsCEKhK4ErOL8WMlKvGSl8QgEB3Aq/kfCpmpNw9dvqHAASqEziT81MxI+XqcdIfBCCwCoFncv4kZksp//l1FXScAwIQgMBHAv/whx2RRzmbixkZ24VFJQhAoAeBWUm/FPPM2zJC7jFAdAkBCPgRmBH0vZw/vDFrxYyU/YKmMgQg0IuAVs5PxYyUe4VPtxCAQF0Cs3J+f2PWiJk35bqDQWcQgEAuAY2cb2/NajEj5dzQ2R0CEKhPQCrnD2Lmbbl+wHQIAQj0IyAV83HCQ84/3pilYuZtud+A0DEEIJBDQCpnxJyTE7tCAAIbEQgRM2/LG00UR4UABEwISOT8442Zzxgm3CkCAQhA4JSARMxHEcTMMEEAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1kMAAhBwJoCYnQFTHgIQgICUAGKWEmM9BCAAAWcCiNkZMOUhAAEISAkgZikx1r8T+JdvcTD+9iVuL3aCQDYBxJydQOH9I8VrgQF5W1CkRgUCiLlCCgV66CbhEWSIeoQSayoSQMwVU3HqaUX5SlAh6re3ZzMAF8kUxaxFzDGcU3bZXcSvoK8kI++cV2KVchEVmyJmBbSqj3hf0Krnnu2rk3iyM+7EanYuMp9HzJn0jfbOvqxGx0gvU006HXKtxix9iIwaQMxGIKPLdLi00Uys9ouWzQpZRjOzyrpqHcRcNZmTvla4xB2Qe4lm9fy8uHWYGcseEbMlTcdaq19oR3RTpWdEs3NmM9ymAlvkYcTcIMidL3iFeK4kQz7nKV2xq5BvxR4Qc8VUfvXEha8VziEZMpFngpzlzBCznFnIEwggBDObBBJA0OOwEfM4q5CVCDkEM5skEUDOY+AR8xinkFVIOQQzmyQTQM7XASDma0YhK5ByCGY2KUQAQZ+HgZgLDCpSLhACLaQQQM7PsSPmlHH8vWlnKUdeqs6ckkes/PaRc1Qexq8GEXNiUl1kU/3idOGYOGqlt64+XxnwEHMG9e97VpTJKhekItukMWuz7SqzZwUcMVuRFNTJFsdOlyCbtWAsXJeeZV6Jz05zeRU2Yr4iZPzrGReBgf8ZYgZ74/F5Wc465wxe1meI5G+5F2K2pHlRK3LQGfDzMCJz8BivjGwjmWWczyOnmZqIeYae4NmowWaox0KJymOsm/NV1fKM4FbtzLMZap5HzBpqime8B5phVoRS7PNGpwyZZ928jT6FmEdJTaxjiCfgBTzqnc+zI3SS8KsIvNitwkc7vohZS27wOa/BPbbffXgHIxha5pXTDhnBbmjERIsQswiXfDFDK2eW9cRsVjtI+CybWXZndXdlipgdLcCwOsJ1Kj2a2a7CuMI+yu+qzu3Xd+WMmEcnRLjOekB3H1Qh/qnlz7LbVRBSkB5zvyN7xCydvMH1DOggKJYtR8B69hHz9Yh8+fsf375dL/u94s+vktXrrGU418mSk8gJMP9yZvdP8MY8x+/p0wylA1RKtiNgeQ92e2tGzA7jzkA6QKVkOwLcA31kiFnPzv1tebe3BOMoKFeAAHLWhYCYddxOn2IQjYFSrj0Bqzux04sKYjYce6sBPFraaQgNI6BUQQJW92KnO4GYDQeZATSESamlCHA3ZHEiZhmvl6sZPkOYlFqKAHdDFidilvHi27IRL8rsRQAxy/JGzDJe7mLe6TuaEXrKNCFgIedd7gdiNhpqi6Hjh35GYVCmJAHuyHgsiHmclfv35V3eBoyQU6YZAcQ8HhhiHmfFZwwDVpTYm4CFnHd4gUHMBvfEYtj4jGEQBCXKE7C4K4j5c8z87XJPRp9hK+8DGixCgLsyFgRvzGOc+L5swIkSEEDMYzOAmMc4IWYDTpSAAGIemwHEPMYJMQs5WVzAsy13+MYoxN1mucVc7JA/YjYY6d2HzeL8MzHscFFn+FR61mJWdsgbMRtM7Q7DZnFGA9SXJXa4tJcQCi+wmKMdMkbMBkO86rBZnMsAr7jEDhf3BmUko0o8Rvq9CrzSea561f46YtaSu3tutWGzOI8BVnWJFS+uRSYVuKxyDvVwDj6ImAdBvVq20rBZnMUA6XSJChLSHsIzg2wuFmfLPoM2V8lziFlC62TtKsNmcQ4DnCYlulzeDOZZbKzOmtW/yWAOFkHMg6BWf2O2ujQGOKdLVL24VRhn8bE6f1b/04MpKICYBbDOlnYfOKv+DVCalMi+uNV5ZvGx4JLVu8lgCoogZgGsVd+aLS6MEUaTMtGXtxu/aD63UC04ZfVuMpiCIohZAAsxG8FyLBN1cS0k44jhZekoRvdNWPHK6D0jJ8RsRL3z4Fn1boRSXSbi0q7AKoLTY4hW3DJ6Vw/kxIOIeQLeKm8EVpfGCKW4TNRl7c7pABvFapW7IR5GowcQsxHIo4zVxe18eQxxLvvb8ShGj/tkzFX3e5GVFWI2JN9ZzJYXaAZpljyuerbK9mofr1/P4mrJLesMXpm8qouYDamvMISWZxhF2+HCZXAZ5fdqXTZbK27Z57DIQlIDMUtoXay1GsKsb4GPx7M8zxm6ThcugsfMOFZjacmr2tlmchp5FjGPUBKs2W0YZ8/b6cLNnlUwRkNLK7OzZlX5rENhCRchZiGwq+U7DuTMmbtduJmzvvpdg6ZuZXaa86zwu6orP4z+OmIeJSVYt9tQzpy3slxeRS4989U5pfWqfO56xkhzlsrfyQVX32wpYjZD+bvQjoOpPfOVsBziMS357NyaM2n4afYxPfyTYppzIOXPBBCz06TuNqDa81aUi9NIvCyr4VeNneYMV6yrnfGqX6tfR8xWJB/q7Dak2vPuevEex07DrxI7Tf9XV6/S+a56tf51xGxN9K7eTsOqPevOl+9+9KT8KnGT9j565SqdcbRnq3WI2YpkwPe22xYVB1Z7OSuexXEkzD5nVOCmzXyEb4XzjfTptQYxe5H9VXeX4dWec/cLqH1rzuamzXvkumWfbaRH7zWI2Zmw5wAfrVcZYu05q/TvPAZD5SUMs7hJehw69JNFWWfT9uvxHGL2oPpQc4dh1p6RS/h7WCQMM7hJ+tNeq4xzaXv1fA4xe9K9qx0x1Jlv0NrzcRERc+WfnQTp4dM2iDmIvFZc2vaihac9X3SfWp4Rz0kYZnCT9KfhlXEmTZ8RzyDmCMq/9vAe7MejRA+65nzRPQbGLd5Kwi+Dm6Q/6eEzziPtMXI9Yo6k/X0vz+FGzMFhGm8nmY0MkUn6k6DJOIukv4y1iDmButeAI+aEMA23lMxFhswk/Y1iyTjHaG+Z6xBzEn2PIUfMSWEabSuZiQyhSfobQZJxhpG+KqxBzIkpWA86Yk4M02BryTxkSE3S3xWOjP6veqr064g5OQ3LYUfMyWFObi+ZhQyxSfp7hSKj98lowh9HzOHIP29oNfCIuUCYEy1I5iBDbpL+zjBk9D0RSdqjiDkN/ceNLYYeMRcJU9mGZAYyBCfpDzErh+DXY4h5jp/50xbDf2sq+vJqe4/u0zw0o4ISfhnMJP0h5rmhQMxz/FyetrgAR2PRl1fbd3SfLqEZFJXwy2Am6Q8xzw0EYp7j5/Z0x0ug7TlDMm7BTRSW8MtgJukPMU8MwvdHEfMcP7enO14Cbc8ZknELbqKwhF8GM0l/iHliEBDzHDzPpzteAm3PGZLxzE5bW8Ivg5mkP8SsnYKfz/HGPMfP7emOl0Dbc4Zk3IKbKCzhl8FM0h9inhgExDwHz/Npi0tw9Bd5gbU9R/bomdlsbQm/DGaS/hDz3DTwxjzHz/XpbhdB22+GZFyDUxaX8MtgJukPMSuH4NdjiHmOn+vT3S6Ctt8MybgGpyguZZfBTNrjMwwZfSviSH8EMadHcN5Ax4ug6ZnLKv97ujOYabJ9nO6Mvgtf8dPWEHPh1DpeBE3PXFbEXPgaprSGmFOwj22qkVz2G4qmZ8SMmMduxD6rEHPhrDWSQ8yFA33RmjTrjH+YSXvkG7N+FhGznp37kx0vgqbnDMm4hyfcQMotg5m0R8QsHIK75YhZz879yY4XQdNzhmTcwxNuIOUWzUzaH1IWDsDDcsQ8x8/16Y6XQdNztGRcQ1MWl3KLZibtDzErB+HXY4h5jp/r0x0vg6bnaMm4hqYsLuUWzUzaH2JWDgJingMX8XTHy6DpOVoyEdlJ95Byi2Ym7Q8xSyfg43remOf4uT7d8TJoeo6WjGtoyuJSbtHMpP0hZuUg8MY8By7i6Y6XQdNztGQispPuIeUWzUzaH2KWTgBvzHPEAp/ueBk0PUdLJjDC4a2k3KKZSftDzMPRP13Ip4w5fq5Pd7wMmp6jJeMamrK4hFsGL0l/Zwgy+lbGkf4YYk6P4LyBjpdB0zMXVvavZGfw0uT6ONkZfRe+3i9bQ8yFk+t4GTQ9c2ERc+FrmNIaYk7BPrapRnLZbymanncXs5RZBi9pj3xjHrvjZ6sQ8xw/96e7XQhNvxmicQ9OsIGUWTQvaX98YxaEf7IUMc8zdK1gcSkiL7Km38j+XMNSFpcyi+Yl7Y+3ZeUg3D2GmOcZulbodik0/UaLxjUwRXEps2he0v4Qs2IIHh5BzPMMXSt0vBSanqNl4xqasLiUVzQraX+IWTgAT5Yj5nmGrhU6XgpNz9GycQ1NWFzKK5qVtD/ELBwAxDwPLLpCx0uh6TlaNtE5vtpPyiualbQ/xDw/XbwxzzN0rdDxUmh6jpaNa2iC4lJWGZykPSJmwQCcLEXM8wxdK3S8FJqeM4TjGtxgcSmrDE7SHhHzYPgvliHmeYauFTpeCk3PGcJxDW6wuJRVBidpj4h5MHzEPA8qq0LHS6HpOUM4WZne7ytllcFJ2iNinp8s3pjnGbpW6HgpND1nCMc1uMHiUlbRnKT9nR07uu9B/GWXIeay0fxszOJiRF8KTc/RPVaJXcoqmpO0P96WbSYLMdtwdKvS8WJoeo4WjltgwsJSVtGcpP0hZuEAnCxHzDYc3ap0vBianqOF4xaYoHAHTpoeHxHsmK1gDJ4uRcyzBJ2f73gxND3veHk7cNL0iJjnpYCY5xm6Vuh4MTQ9I+brMcpgpMkSMV9nebUCMV8RSv71jhdD03OGdJKjFf9gN4ORJkvEPD9ZiHmeoWuFjhdD03OGdFyDGygu5ZTBSNojP/wbCH5gCWIegJS5pOPF0PScIZ3MXI+9pZyiGUn7O+MZ3Xd2rhb7I2YLio41LC5H9MXQ9Bzdo2NkQ6U7MNL0yGeMofgvFyHmS0T5C7pdEE2/iPl6zqIZaXJEzNc5jqxAzCOUktd0vCDSnqOlkxyp+DPG0W80I2mGfF+2myrEbMfSrVLHCyLtOVo6bmENFpbyQcyDYBdZhpgbBKm5xNm/pZT2jJhfD2IGH2mGvDHbyQQx27F0q9Txgmh6zpCPW2gXhaV8MthIe0TMdtOEmO1YulXqeEE0PWfIxy20F4W7sNH0mf07tYw8PfZEzB5UjWt2vCCanhHz+eBksNFkiJhtLj9ituHoWqXjBdH0nCEf1+BOindgo+mRTxl204SY7Vi6VbK4JNHS0/Yc3adbaM0/ZWjzuz/2Dll6zQ9i9iJrWLfjJdH2vMNllrLJYCLtkbdlwwv/vRRituXpUq3rJdH0nSEhl9Aaf8Y4Wtdkx/dlu0lCzHYs3Sp1vSSavhHz5zHKYKLJDjHbKQAx27F0q2RxSY7moi+4pu/oHt1C4405fN6is/TcDzF70jWsrZFc9huMtudV5azhkcVC02v2vBlet/RSiDk9grEGOl4Ubc9ZMhpLQr9KwyODhabPZ1QyetenU+tJxFwrj9NuLC5LxkXR9p3Rq+codOKg7ZU3ZrsJQsx2LF0rWVyWDNlp+87o1TPAThy0vd7zWy0/z9l4VhsxRxNX7tf1ssz0vdLl1nDIOr+mV96WlRf75DHEbMvTrVrny6LtPUtM1iF2O7+2X96Y7SYHMduxdK1kcVmOBjNkN9N7Rr/WQWrPn3F2ba+8MdtODWK25elazeLSdLzsGT1bBqnJLevMml6RsuW0/KyFmO2ZulXsfGlme88S1WyY2nNnnVfbL58xZifl4/OI2Zana7Xul2a2/yxZaUPVnjfznNqeEbN2Sp4/h5htebpW635pZvvPFJYmWO15s86p7ZdPGZrpeP0MYrZn6lZxhYsze4YsaUlD1Z4z83zanhGzdDqu1yPma0ZlVqxwcSzOkCmvkWGYOWPm2Wb6vnHJ7H8kmy5rEHOXpH71ucLlsTjDgaOiBGbOln2emd4Rs61IELMtT/dqq1wei3NUk/PsmTLFPNs7Yra9+ojZlqd7tZUuEGf5PS6ZUj66WCkL90sYsAFiDoBsucVKF8jqLNlvaxbnWEHM2WewvGfZtRBzdgKK/VcQwe3YFme5RxgpB6veI3t+Nm6rnENxlco+gpjLRnPe2GoXyeo8EYK27jVbynzGqCkAxFwzl5ddWcmhghS83pwtJG3F+SzMKvwtzlnlLA2v89OWEXPDJC0u0nHsapfJ6lwjkT6ePXLvSuytzl1tlkZmoPIaxFw5nRe9rXqhrM5VOdZKErPiXelMlbMf7Q0xj5Iqtm7lC2V1tmKR/WinksCsOFc6U8XMNT0hZg21As9YXapqsrhHa3nGApGVkvLBw4ovYrafLsRszzSs4g4Xy+qMYaE82aiquKzYVj1fZuazeyPmWYKJz1tdrMpvzZZvdtFRVRbWLrMTnbnVfojZimRSHasLVlkiN7RWZ42IqjpPK5bVzxmRtcceiNmDamBNqwtW/a25w7fnLpLacWYCr6TJVojZBGNeEctL1knOlT5xdBGy9e88up0775bKd0bMcmblnrCUc8fLZnn+kXA7MrKWcrd/iI/kWmkNYq6UhrIXazEhno9BdObxOFJWs7ISE+W1c30MMbvijStudeFuHa908UbYrHTes6kb4TA6sTvwGmXhsQ4xe1BNqGl56fhtakKAzlsyH86AjcsjZmOgmeW4fJn0a+9tORu8LftnjZj9GYftYHn5VvykERZEsY2s5wIx+weMmP0Zh+5gfQn5rBEan8tmljOBlF0i+lQUMcdwDt3F8iIi5tDozDdjFsyRhhREzCGYYzexvozIOTY/q92s54C3Zatkrusg5mtGLVdYX0rk3G8MrGcAMcfNAGKOYx26k/Wl5IeBofFNb2adP1KejkRUADGLcPVabH05kXOP/K1zR8rxuSPmeOahO1pfUuQcGp94M4+8EbM4hukHEPM0wvoFPC4r35zr5e6RM1LOyRkx53AP3dXjwvLmHBrh5WZeGSPmS/QuCxCzC9Z6Rb0uLm/O+Vl7ZYuU87JFzHnsw3f2usDIOTzK9w29MkXKeZkeOyPmXP7hu3tdZOQcHuUbWcYzj9oRMUeRLrKP52VGzjEhk2EM58xdEHMm/aS9vS82gvYL1js7PmH4ZSepjJgltBZa633BkbP9sHhnhpTtM9NWRMxacgs8533RkbPNkETkRFY2WVlVQcxWJJvW4dLXDo58aufj1R1i9iLbqC6Xv15YUZnwplwv+6MjxFwzl/CuEEE48qcbRuaAlGtk/qwLxFw3m/DOkEI48g8bwj+Xf6XdEXOlNAr0Ei0H3treXP9FkbOR4k9gFLhsL1pAzLXzSekOOftjz2B8OxVS9s93dgfEPEtw0ecRh32wmUyRsn2enhURsyfd5rWzRbLCm102w/sRXIFn8ys13D5iHka158IqYukilSq8Hqe1C789b9nnUyNmJuGSQDXZVJJMNTYI+XKcWyxAzC1iqtFkVQlFibrq+fmTFzXuh2UXiNmS5ga1uslp9IdeXc+FlNe8dIh5zVxdT7WaxFxhBReP+t1D8LG22w4xbxe53YERtB1Li0pI2YJijRqIuUYObbtAzvnRIeT8DKw7QMzWRDeth6BzgkfKOdy9d0XM3oQ3qo+c48JGyHGsM3ZCzBnUF98TQfsFjJD92FaqjJgrpbFYLwjaNlCkbMuzcjXEXDmdRXpD0HNBIuQ5fh2fRswdU2vaM4IeDw4Zj7NacSViXjHVwmdCzq/DQciFhzewNcQcCJutPhJA0j95IGNuxiMBxMxMlCCwm6SRcYmxK9sEYi4bzZ6NrSpoRLznPGtPjZi15HjOnUBnSSNi9/FYegPEvHS8ax2uqqiR8FpzVuE0iLlCCvSgJhAtaySsjooHBQQQswAWS/sSuBI4wu2b7YqdI+YVU+VMEIBAawKIuXV8NA8BCKxIADGvmCpnggAEWhNAzK3jo3kIQGBFAoh5xVQ5EwQg0JoAYm4dH81DAAIrEkDMK6bKmSAAgdYEEHPr+GgeAhBYkQBiXjFVzgQBCLQmIBbzf7z9z7d/+uMvw4f+8+vwUhZCAAIQgMB3AhIx//fX/337IhXzQRk5M2sQgAAExghIpHxURMxjXFkFAQhAQE0gTMy8Nasz4kEIQGAjAlIpT70xI+aNJoujQgACagJTYj52lfwA8NYl35rVefEgBCCwOAGtlA8sP374pxUzb86LTxbHgwAEVAQ0Ur59xvggZuSs4s9DEIAABD4QmJWymZh5c2YyIQABCMj+vPIjr+OPyd3+8/4p4/Z/aL4132/Ad2fGEwIQ2I2A9i35xuleyp/emGc+ZzwGgaB3G03OC4H9CMwKeVjMlnJePSb+4bN6wpzPioCVwKz6qVTn8W356Ruz1SeNSgeP6gVRR5Fmn8oEkPB4Os+k/FLMvDmPw322EknP8ePpXgSQsTyvMykjZjlL1RNIWoWNhxoQQMj6kNRi5q1ZD523aFt2VKtFACHP5fFKypdvzPdbz/4xurljrPU0b9Br5bnTaRDyXNpXQr5V//TnmF9ti5znQrl/GjnbsaSSPwGEPM94VMqiN+ZbW8h5PiAEbcuQar4EkPI8X4mUVWJG0PMhPVbg7dmeKRVtCCDlOY5SIas+ZTxrkTfoueBuTyNnG45UsSGAkOc4aoX8Lubjf9z+6s+5VnR/p/Psnis9j5xXSrPvWZCyLrtZGd92/be3f/zyxVLMj8fhbVoeMHKWM+MJOwJIeZyllYgfd3QX8/gRe6781//869t//fv/mTePnM2RUnCAgJeUvQQ2cKSWSxCzYWzWkkbOhuFQ6pKAtZSR8SXy0wXvYvb8nKFvr++TVp9wkHPfGejWuZWYEfJc8oeUjwo//gsxz8F89rTVGzRyts+Gih8JWEgZIdtMFWK24XhZxeLtGTlfYmaBksCslBGyEvzJY5/EzFuzLeDHajOCRsy+2exaHSnXSv4m5Q+fMhCzf0jI2Z8xO4wRQMpjnCJXnYoZOfvHgJz9GbPDawJIud6E3Ev50xszYo4JDDnHcGaXzwSQcs2puBQzco4JTitnvjfH5LPqLjNi5gd9PlPxKOWnb8y3ra3+/gyfo/SvqhXzcXLk3D//jBMg5Qzqr/d8JmXEnJwTck4OYKPtkXLNsMVi5pNGTJDIOYbzzrsg5Zrpn0n55RsznzTiwkTOcax32wkp10z8lZSHxMybc0ywWjnzvTkmn467zEj5OC8/7PNJ/UrKw2Lm7dknoPuqWjEfNZCzfz7ddkDK9RIbEfKt6/e/xGj0GPxpjVFS8nXIWc6MJ54TmBEzb8r2UyWRsviN+b5dBG0f3lEROftw3akqUq6TtlTI6jdm5OwfOnL2Z7zqDki5TrJaKU+9MT8enzdo24HQypnvzbY5dKqGlPPTmpHxfffib8yjR0fUo6Ser9OK+aiGnOfYd3waKeekZiXix+7dxJyDKW7XiH/wIOe4PDvvNCPl49xRP+zzkljn7M56R8wGqXpKGjkbBLRwicpSRsT6wUPMenZPn/SQtFbOfNIwDrdYuYpSRsY2Q4KYbTh+qmIpaK2Y+d7sFG6RsjNi9vh8gZTtBgMx27F0fYNGzs5BNStfScoI2X54ELM9U7e3Z+QcEFaDLapIGSH7DQti9mP7obLVpw3kHBRY0W2QctFgjNtCzMZAX5WrIGe+OwcGbrjVjJCPNiy/KfOmbBjsSSnE7M/Y/M155q351gx/YiM4eOV2s0K+bWslZqSsDFL4GGIWArNYbvHmbCHn+7MgaotkbWpYyRgp2+SRUQUxZ1D/vmdFOSehYFtHArwpO8J1LI2YHeFelUbOV4T49RkCSHmGXu6ziDmXP2/OyfxX3d5KygcfvivHTwlijmf+aUfenAuEsFALSLl/mIi5QIYWYj6OYf0DwQJoaEFIACkLgRVdjpiLBIOciwTRuA2k3Di8h9YRc6EskXOhMJq1gpSbBXbRLmIulmILRdUAAAHaSURBVKeVnPm0USxYp3YshcwP+pxCUpRFzApo3o8gZ2/C/etbCxkp15oJxFwrjx/dWIr5/nj8cLBg2MKWPIR8a4E/FicMw3E5YnaEO1PaS863npD0TDqxz3rKGCnHZjm6G2IeJZWwzlvOCUdiy4IEeFOuFwpirpfJh46Qc/GAmreHlGsGiJhr5vLeFWIuHlDj9pBy3fAQc91skHODbLq2iJRrJ4eYa+eDnJvk06lNpFw/LcRcPyPk3Cij6q0i5eoJ/ewPMffICTk3y6lqu4i5ajIf+0LMPXJCzM1yqtguUq6YyvOeEHOfrJBzw6yqtIyUqyQx1gdiHuNUbhV/jK5cJGUbQsplozltDDH3y4w358aZRbeOlKOJ2+yHmG04plXhzTkNffmNkXL5iHhj7hvRdefI+ZrRbiuQcu/EeWPund+P7hHzAiEaHgEpG8JMKoWYk8Bbb4ucrYn2rIeUe+b22DViXiNH3pwXylF7FKSsJVfvOcRcL5Ppjnh7nkbYrgBSbhfZy4YR81p5vp8GOS8a7MOxEPKaOSPmNXNF0IvnehwPKa8bMmJeN9sPJ+MNep2gEfI6WZ6dBDGvn/GnEyLpfqEj436ZzXSMmGfoLfgs0s4LFfnmsa+28/8Dsno9STJozBkAAAAASUVORK5CYII='; + +class OddMessage { + constructor(runtime) { + this.runtime = runtime; + this.messageQueue = []; + this.recording = []; + this.logs = []; + this.recordingDelay = 1000; + } + getInfo() { + return { + id: 'oddMessage', + name: 'Odd Messages', + menuIconURI: icon, + blockIconURI: icon, + color1: '#BE00FF', + blocks: [ + { + opcode: 'emit', + blockType: 'command', + text: 'emit [a] [b]', + arguments: { + a: { + type: 'string', + defaultValue: 'message', + }, + b: { + type: 'string', + defaultValue: 'data', + }, + }, + }, + { + opcode: 'on', + blockType: 'hat', + text: 'on [a] set [b]', + arguments: { + a: { + type: 'string', + defaultValue: 'message', + }, + b: { + type: 'string', + menu: 'variables', + }, + }, + }, + '---', + { + opcode: 'whenVarChange', + blockType: 'hat', + text: 'when [a] changes', + arguments: { + a: { + type: 'string', + menu: 'variables', + }, + }, + }, + { + opcode: 'recordVar', + blockType: 'command', + text: 'record [a] in list [b]', + arguments: { + a: { + type: 'string', + menu: 'variables', + }, + b: { + type: 'string', + menu: 'lists', + }, + }, + }, + { + opcode: 'stopRecording', + blockType: 'command', + text: 'stop recording [a]', + arguments: { + a: { + type: 'string', + menu: 'variables', + }, + }, + }, + { + opcode: 'setRecordingDelay', + blockType: 'command', + text: 'set recording delay to [a]', + arguments: { + a: { + type: 'number', + defaultValue: 1000, + }, + }, + }, + '---', + { + opcode: 'logToJSON', + blockType: 'reporter', + text: 'list logs', + }, + { + opcode: 'log', + blockType: 'command', + text: 'log [a] as [b]', + arguments: { + a: { + type: 'string', + defaultValue: 'message', + }, + b: { + type: 'string', + defaultValue: 'warn', + }, + }, + }, + { + opcode: 'logClear', + blockType: 'command', + text: 'clear logs', + }, + { + opcode: 'logToArray', + blockType: 'reporter', + text: '#[n] log as array', + arguments: { + n: { + type: 'number', + defaultValue: 0, + }, + }, + }, + ], + menus: { + variables: { + acceptReporters: true, + items: '_getVariableMenu', + }, + lists: { + acceptReporters: true, + items: '_getListMenu', + }, + }, + } + } + _getVariableMenu() { + const vars = this.runtime.getAllVarNamesOfType('') + return vars.length == 0 ? [" "] : vars + } + _getListMenu() { + const lists = this.runtime.getAllVarNamesOfType('list') + return lists.length == 0 ? [" "] : lists + } + log({ a, b }) { + this.logs.push({ log: a, type: b }); + } + logClear() { + this.logs = []; + } + logToArray({ n }) { + let a = this.logs[n]; + if (a) { + return JSON.stringify([a.log, a.type]); + } else { + return '[]'; + } + } + logToJSON() { + return JSON.stringify(this.logs); + } + emit({ a, b }) { + this.messageQueue.push([a, b]); + } + on({ a, b }) { + if (this.messageQueue.length == 0) return false; + if (this.messageQueue[0][0] == a) { + const stage = this.runtime.getTargetForStage(); + if (!stage) return true; + const variable = stage.lookupVariableByNameAndType(b); + if (!variable) return true; + variable.value = this.messageQueue[0][1]; + this.messageQueue.shift(); + return true + } else { + return false + } + } + async whenVarChange({ a }) { + if (!this.runtime.getTargetForStage().lookupVariableByNameAndType(a)) return false; + let b = this.runtime.getTargetForStage().lookupVariableByNameAndType(a).value; + await new Promise(resolve => { setTimeout(resolve, 100) }); + let c = this.runtime.getTargetForStage().lookupVariableByNameAndType(a).value; + return b != c; + } + async recordVar({ a, b }) { + if (this.recording.includes(a)) return; + const delay = async (ms) => { await new Promise(r => setTimeout(r, ms)) }; + this.recording.push(a); + while (this.recording.includes(a)) { + await delay(this.recordingDelay); + let c = this.runtime.getTargetForStage().lookupVariableByNameAndType(a).value; + let d = this.runtime.getTargetForStage().lookupVariableByNameAndType(b, 'list'); + d.value.push(c); + } + } + stopRecording({ a }) { + if (this.recording.includes(a)) { + this.recording.splice(this.recording.indexOf(a), 1); + } + } + setRecordingDelay({ a }) { + this.recordingDelay = a; + } +} + +module.exports = OddMessage; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/theshovel_canvasEffects/index.js b/local-scratch-vm/src/extensions/theshovel_canvasEffects/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6eb35e37c4a1560e85cff8d89346f2acc9485863 --- /dev/null +++ b/local-scratch-vm/src/extensions/theshovel_canvasEffects/index.js @@ -0,0 +1,425 @@ +// Created by TheShovel +// https://github.com/TheShovel +// +// Extra modifications: (their names will be listed nearby their changes for convenience) +// SharkPool +// https://github.com/SharkPool-SP +// +// 99% of the code here was not created by a PenguinMod developer! +// Look above for proper crediting :) + +const ArgumentType = require("../../extension-support/argument-type"); +const BlockType = require("../../extension-support/block-type"); +const Cast = require("../../util/cast"); + +let borderRadius = 0; +let rotation = 0; +let offsetY = 0; +let offsetX = 0; +let skewY = 0; +let skewX = 0; +let scale = 100; + +// Thanks SharkPool for telling me about these +let transparency = 0; +let sepia = 0; +let blur = 0; +let contrast = 100; +let saturation = 100; +let color = 0; +let brightness = 100; +let invert = 0; +let resizeMode = "default"; + +// SharkPool +let imageC = ["", 100]; +let borderC = [0, "none", "#ff0000", "transparent"]; + +let canvas; +const updateStyle = () => { + // Gotta keep the translation to % because of the stage size, window size and so on + const transform = `rotate(${rotation}deg) scale(${scale}%) skew(${skewX}deg, ${skewY}deg) translate(${offsetX}%, ${ + 0 - offsetY + }%)`; + if (canvas.style.transform !== transform) { + canvas.style.transform = transform; + } + const filter = `blur(${blur}px) contrast(${ + contrast / 100 + }) saturate(${saturation}%) hue-rotate(${color}deg) brightness(${brightness}%) invert(${invert}%) sepia(${sepia}%) opacity(${ + 100 - transparency + }%)`; + if (canvas.style.filter !== filter) { + canvas.style.filter = filter; + } + const cssBorderRadius = borderRadius === 0 ? "" : `${borderRadius}%`; + if (canvas.style.borderRadius !== cssBorderRadius) { + canvas.style.borderRadius = cssBorderRadius; + } + const imageRendering = resizeMode === "pixelated" ? "pixelated" : ""; + if (canvas.style.imageRendering !== imageRendering) { + canvas.style.imageRendering = imageRendering; + } + // SharkPool + canvas.style.border = Cast.toString( + `${borderC[0]}px ${borderC[1]} ${borderC[2]}` + ); + canvas.style.backgroundColor = Cast.toString(borderC[3]); + if (imageC[0].length > 3) { + canvas.style.backgroundImage = imageC[0]; + canvas.style.backgroundSize = `${Cast.toNumber(imageC[1])}%`; + } +}; + +class CanvasEffects { + constructor(runtime) { + this.runtime = runtime; + this.canvas = runtime.renderer.canvas; + canvas = this.canvas; + // scratch-gui may reset canvas styles when resizing the window or going in/out of fullscreen + new MutationObserver(updateStyle).observe(this.canvas, { + attributeFilter: ["style"], + attributes: true, + }); + this.runtime.on("RUNTIME_DISPOSED", this.cleareffects); + } + + getInfo() { + return { + id: "theshovelcanvaseffects", + name: "Canvas Effects", + blocks: [ + { + opcode: "seteffect", + blockType: BlockType.COMMAND, + text: "set canvas [EFFECT] to [NUMBER]", + arguments: { + EFFECT: { + type: ArgumentType.STRING, + menu: "EFFECTMENU", + }, + NUMBER: { + type: ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "changeEffect", + blockType: BlockType.COMMAND, + text: "change canvas [EFFECT] by [NUMBER]", + arguments: { + EFFECT: { + type: ArgumentType.STRING, + menu: "EFFECTMENU", + }, + NUMBER: { + type: ArgumentType.NUMBER, + defaultValue: 5, + }, + }, + }, + { + opcode: "geteffect", + blockType: BlockType.REPORTER, + text: "get canvas [EFFECT]", + arguments: { + EFFECT: { + type: ArgumentType.STRING, + menu: "EFFECTGETMENU", + }, + }, + }, + { + opcode: "setBorder", + blockType: BlockType.COMMAND, + text: "add [BORDER] border to canvas with color [COLOR1] and backup [COLOR2] and thickness [THICK]", + arguments: { + BORDER: { + type: ArgumentType.STRING, + menu: "BORDERTYPES", + }, + THICK: { + type: ArgumentType.NUMBER, + defaultValue: 5, + }, + COLOR1: { + type: ArgumentType.COLOR, + defaultValue: "#ff0000", + }, + COLOR2: { + type: ArgumentType.COLOR, + defaultValue: "#0000ff", + }, + }, + }, + { + opcode: "setImage", + blockType: BlockType.COMMAND, + text: "set canvas image to [IMAGE] scaled [AMT]%", + hideFromPalette: true, // only appears when stage BG is transparent + arguments: { + IMAGE: { + type: ArgumentType.STRING, + defaultValue: "https://extensions.turbowarp.org/dango.png", + }, + AMT: { + type: ArgumentType.NUMBER, + defaultValue: 100, + }, + }, + }, + { + opcode: "cleareffects", + blockType: BlockType.COMMAND, + text: "clear canvas effects", + }, + { + opcode: "renderscale", + blockType: BlockType.COMMAND, + text: "set canvas render size to width:[X] height:[Y]", + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 100, + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 100, + }, + }, + }, + { + opcode: "setrendermode", + blockType: BlockType.COMMAND, + text: "set canvas resize rendering mode [EFFECT]", + arguments: { + EFFECT: { + type: ArgumentType.STRING, + menu: "RENDERMODE", + }, + }, + }, + ], + menus: { + EFFECTMENU: { + acceptReporters: true, + items: [ + "blur", + "contrast", + "saturation", + "color shift", + "brightness", + "invert", + "sepia", + "transparency", + "scale", + "skew X", + "skew Y", + "offset X", + "offset Y", + "rotation", + "border radius", + ], + }, + RENDERMODE: { + acceptReporters: true, + items: ["pixelated", "default"], + }, + EFFECTGETMENU: { + acceptReporters: true, + // this contains 'resize rendering mode', EFFECTMENU does not + items: [ + "blur", + "contrast", + "saturation", + "color shift", + "brightness", + "invert", + "resize rendering mode", + "sepia", + "transparency", + "scale", + "skew X", + "skew Y", + "offset X", + "offset Y", + "rotation", + "border radius", + ], + }, + BORDERTYPES: { + acceptReporters: true, + items: [ + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset", + "none", + ], + }, + }, + }; + } + geteffect({ EFFECT }) { + if (EFFECT === "blur") { + return blur; + } else if (EFFECT === "contrast") { + return contrast; + } else if (EFFECT === "saturation") { + return saturation; + } else if (EFFECT === "color shift") { + return color; + } else if (EFFECT === "brightness") { + return brightness; + } else if (EFFECT === "invert") { + return invert; + } else if (EFFECT === "resize rendering mode") { + return resizeMode; + } else if (EFFECT === "sepia") { + return sepia; + } else if (EFFECT === "transparency") { + return transparency; + } else if (EFFECT === "scale") { + return scale; + } else if (EFFECT === "skew X") { + return skewX; + } else if (EFFECT === "skew Y") { + return skewY; + } else if (EFFECT === "offset X") { + return offsetX; + } else if (EFFECT === "offset Y") { + return offsetY; + } else if (EFFECT === "rotation") { + return rotation; + } else if (EFFECT === "border radius") { + return borderRadius; + } + return ""; + } + seteffect({ EFFECT, NUMBER }) { + NUMBER = Cast.toNumber(NUMBER); + if (EFFECT === "blur") { + blur = NUMBER; + } else if (EFFECT === "contrast") { + contrast = NUMBER; + } else if (EFFECT === "saturation") { + saturation = NUMBER; + } else if (EFFECT === "color shift") { + color = NUMBER; + } else if (EFFECT === "brightness") { + brightness = NUMBER; + } else if (EFFECT === "invert") { + invert = NUMBER; + } else if (EFFECT === "sepia") { + sepia = NUMBER; + } else if (EFFECT === "transparency") { + transparency = NUMBER; + } else if (EFFECT === "scale") { + scale = NUMBER; + } else if (EFFECT === "skew X") { + skewX = NUMBER; + } else if (EFFECT === "skew Y") { + skewY = NUMBER; + } else if (EFFECT === "offset X") { + offsetX = NUMBER; + } else if (EFFECT === "offset Y") { + offsetY = NUMBER; + } else if (EFFECT === "rotation") { + rotation = NUMBER; + } else if (EFFECT === "border radius") { + borderRadius = NUMBER; + } + updateStyle(); + } + changeEffect(args) { + const EFFECT = args.EFFECT; + const currentEffect = this.geteffect(args); + const NUMBER = Cast.toNumber(args.NUMBER) + currentEffect; + if (EFFECT === "blur") { + blur = NUMBER; + } else if (EFFECT === "contrast") { + contrast = NUMBER; + } else if (EFFECT === "saturation") { + saturation = NUMBER; + } else if (EFFECT === "color shift") { + color = NUMBER; + } else if (EFFECT === "brightness") { + brightness = NUMBER; + } else if (EFFECT === "invert") { + invert = NUMBER; + } else if (EFFECT === "sepia") { + sepia = NUMBER; + } else if (EFFECT === "transparency") { + transparency = NUMBER; + } else if (EFFECT === "scale") { + scale = NUMBER; + } else if (EFFECT === "skew X") { + skewX = NUMBER; + } else if (EFFECT === "skew Y") { + skewY = NUMBER; + } else if (EFFECT === "offset X") { + offsetX = NUMBER; + } else if (EFFECT === "offset Y") { + offsetY = NUMBER; + } else if (EFFECT === "rotation") { + rotation = NUMBER; + } else if (EFFECT === "border radius") { + borderRadius = NUMBER; + } + updateStyle(); + } + cleareffects() { + borderRadius = 0; + rotation = 0; + offsetY = 0; + offsetX = 0; + skewY = 0; + skewX = 0; + scale = 100; + transparency = 0; + sepia = 0; + blur = 0; + contrast = 100; + saturation = 100; + color = 0; + brightness = 100; + invert = 0; + resizeMode = "default"; + imageC = ["", 100]; + borderC = [0, "none", "#ff0000", "transparent"]; + updateStyle(); + } + setrendermode({ EFFECT }) { + resizeMode = EFFECT; + updateStyle(); + } + renderscale({ X, Y }) { + this.runtime.renderer.resize(X, Y); + } + setImage(args) { + this.runtime.vm.securityManager + .canFetch(encodeURI(args.IMAGE)) + .then((canFetch) => { + if (canFetch) { + imageC = [`url(${encodeURI(args.IMAGE)})`, args.AMT]; + } else { + console.log("Cannot fetch content from the URL."); + imageC = []; + } + updateStyle(); + }); + } + setBorder(args) { + borderC = [args.THICK, args.BORDER, args.COLOR1, args.COLOR2]; + if (args.BORDER === 'none') { + borderC[3] = 'transparent'; + } + updateStyle(); + } +} + +module.exports = CanvasEffects; diff --git a/local-scratch-vm/src/extensions/theshovel_colorPicker/index.js b/local-scratch-vm/src/extensions/theshovel_colorPicker/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5da85160d22bc91b3b5e7b37e60499dc5713dd08 --- /dev/null +++ b/local-scratch-vm/src/extensions/theshovel_colorPicker/index.js @@ -0,0 +1,163 @@ +// Created by TheShovel +// https://github.com/TheShovel +// +// 99% of the code here was not created by a PenguinMod developer! +// Look above for proper crediting :) + +const ArgumentType = require("../../extension-support/argument-type"); +const BlockType = require("../../extension-support/block-type"); +const Cast = require("../../util/cast"); + +let input; + +let x = 0; +let y = 0; +const updatePosition = () => { + input.style.transform = `translate(${x}px, ${-y}px)`; +}; + +class ColorPicker { + constructor(runtime) { + this.runtime = runtime; + + input = document.createElement("input"); + input.type = "color"; + input.value = "#9966ff"; // default scratch-paint color + input.style.pointerEvents = "none"; + input.style.width = "1px"; + input.style.height = "1px"; + input.style.visibility = "hidden"; + this.runtime.renderer.addOverlay(input, "scale-centered"); + + input.addEventListener("input", () => { + this.runtime.startHats("shovelColorPicker_whenChanged"); + }); + + updatePosition(); + } + + getInfo() { + return { + id: "shovelColorPicker", + name: "ColorPicker", + color1: "#ff7db5", + color2: "#e0649a", + color3: "#c14d7f", + blocks: [ + { + opcode: "showPicker", + blockType: BlockType.COMMAND, + text: "show color picker", + }, + { + opcode: "setPos", + blockType: BlockType.COMMAND, + text: "set picker position to x: [X] y: [Y]", + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: 0, + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "setColor", + blockType: BlockType.COMMAND, + text: "set picker color to [COLOR]", + arguments: { + COLOR: { + type: ArgumentType.COLOR, + defaultValue: "#855CD6", + }, + }, + }, + { + opcode: "getColor", + blockType: BlockType.REPORTER, + text: "color [TYPE] value", + arguments: { + TYPE: { + type: ArgumentType.STRING, + menu: "RGBMenu", + }, + }, + }, + { + opcode: "getPos", + blockType: BlockType.REPORTER, + text: "picker [COORD] position", + arguments: { + COORD: { + type: ArgumentType.STRING, + menu: "POSMenu", + }, + }, + }, + { + opcode: "whenChanged", + blockType: BlockType.EVENT, + isEdgeActivated: false, + text: "when color changed", + }, + ], + menus: { + RGBMenu: { + acceptReporters: true, + items: ["hex", "red", "green", "blue"], + }, + POSMenu: { + acceptReporters: true, + items: ["X", "Y"], + }, + }, + }; + } + + setColor(args) { + input.value = args.COLOR; + } + + getColorHEX() { + return input.value; + } + + showPicker() { + input.click(); + } + + getColor(args) { + if (args.TYPE === "hex") { + return input.value; + } else if (args.TYPE == "red") { + return Cast.toRgbColorObject(input.value).r; + } else if (args.TYPE == "green") { + return Cast.toRgbColorObject(input.value).g; + } else if (args.TYPE == "blue") { + return Cast.toRgbColorObject(input.value).b; + } else { + return ""; + } + } + + setPos(args) { + x = Cast.toNumber(args.X); + y = Cast.toNumber(args.Y); + updatePosition(); + } + + getPos(args) { + if (args.COORD == "X") { + return x; + } else if (args.COORD == "Y") { + return y; + } else { + return ""; + } + } +} + +module.exports = ColorPicker; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/theshovel_customStyles/index.js b/local-scratch-vm/src/extensions/theshovel_customStyles/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4f72706afd6c38316cc1ab36731a1bd179736795 --- /dev/null +++ b/local-scratch-vm/src/extensions/theshovel_customStyles/index.js @@ -0,0 +1,732 @@ +// Created by TheShovel +// https://github.com/TheShovel + +// Thanks LilyMakesThings for the awesome banner! + +const ArgumentType = require("../../extension-support/argument-type"); +const BlockType = require("../../extension-support/block-type"); +const Cast = require("../../util/cast"); + +// Styles +let monitorText = "", monitorBorder = "", monitorBackgroundColor = ""; +let variableValueBackground = "", variableValueTextColor = ""; +let listFooterBackground = "", listHeaderBackground = ""; +let listValueText = "", listValueBackground = ""; +let variableValueRoundness = -1, listValueRoundness = -1; +let monitorBackgroundRoundness = -1, monitorBackgroundBorderWidth = -1; +let allowScrolling = "", askBackground = ""; +let askBackgroundRoundness = -1, askBackgroundBorderWidth = -1; +let askButtonBackground = "", askButtonRoundness = -1; +let askInputBackground = "", askInputRoundness = -1, askInputBorderWidth = -1; +let askBoxIcon = "", askInputText = "", askQuestionText = "", askButtonImage = "", askInputBorder = ""; + +// CSS selectors +let monitorRoot, monitorValue, monitorListHeader, monitorListFooter, monitorRowValueOuter, monitorRowsInner, monitorRowsScroller, monitorRowIndex, monitorValueLarge; +let askBoxBG, askBoxButton, askBoxInner, askBoxText, askBoxBorderMain, askBoxBorderOuter; +if (typeof scaffolding !== "undefined") { + monitorRoot = ".sc-monitor-root"; + monitorValue = ".sc-monitor-value"; + monitorListHeader = ".sc-monitor-list-label"; + monitorListFooter = ".sc-monitor-list-footer"; + monitorRowValueOuter = ".sc-monitor-row-value-outer"; + monitorRowsInner = ".sc-monitor-rows-inner"; + monitorRowsScroller = monitorRowsInner; + monitorRowIndex = ".sc-monitor-row-index"; + monitorValueLarge = ".sc-monitor-large-value"; + askBoxBG = ".sc-question-inner"; + askBoxButton = ".sc-question-submit-button"; + askBoxInner = ".sc-question-input"; + askBoxBorderMain = ".sc-question-input:hover"; + askBoxBorderOuter = ".sc-question-input:focus"; + askBoxText = ".sc-question-text"; +} else { + monitorRoot = 'div[class^="monitor_monitor-container_"]'; + monitorValue = 'div[class^="monitor_value_"]'; + monitorListHeader = 'div[class^="monitor_list-header_"]'; + monitorListFooter = 'div[class^="monitor_list-footer_"]'; + monitorRowValueOuter = 'div[class^="monitor_list-value_"]'; + monitorRowsInner = 'div[class^="monitor_list-body_"]'; + monitorRowsScroller = 'div[class^="monitor_list-body_"] > .ReactVirtualized__List'; + monitorRowIndex = 'div[class^="monitor_list-index_"]'; + monitorValueLarge = 'div[class^="monitor_large-value_"]'; + askBoxBG = 'div[class^="question_question-container_"]'; + askBoxButton = 'button[class^="question_question-submit-button_"]'; + askBoxInner = '[class^="question_question-container_"] input[class^="input_input-form_"]'; + askBoxIcon = 'img[class^="question_question-submit-button-icon_"]'; + askBoxBorderMain = '[class^="question_question-input_"] input:focus, [class^="question_question-input_"] input:hover'; + askBoxBorderOuter = '[class^="question_question-input_"] > input:focus'; + askBoxText = '[class^="question_question-container_"] div[class^="question_question-label_"]'; +} + +const ColorIcon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMTYuNDE5NjQiIGhlaWdodD0iMTE4LjM0OTk0IiB2aWV3Qm94PSIwLDAsMTE2LjQxOTY0LDExOC4zNDk5NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOC45MjgwOSw3LjY1MTk0KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTMxLjc3NzE4LDEwLjU1MDMzYy0xMC4yOTkxNywxLjEwODUyIC0xNi4zNDU2NSw0LjY2NTg3IC0xOS4xNDcxOSw5LjE4MDU3Yy0xLjg5MjIsMy4yNDc5NyAtMi4zNDE1Niw3LjEzODcyIC0xLjIzOTUzLDEwLjczMjVsMC4wOTA3LDAuMzQyNjRsMjAuMjY1NzksLTIwLjI1NTcxek03OC4yODQ2OSwyMS43NTY0OGMtNy4zNDY0NywtNy4zNTY1NSAtMTQuNzczNTcsLTEzLjEwMDcgLTIwLjg5MDU5LC0xNi4yOTUyN2MtNC4zNDMzOSwtMi4yMzcyIC03LjcyOTQyLC0zLjEzNDA5IC05LjM5MjIsLTIuMTg2ODFsLTAuODI2MzYsMC44MjYzNmMtMS4yMjk0NSwyLjAxNTUgLTAuNTEzOTYsNi4zMDg0OSAxLjc5Mzc5LDExLjY2OTdjMy41NjMzNSw3LjczMjE5IDguNTIwMTUsMTQuNzQyMDggMTQuNjIyNCwyMC42Nzg5N2M2Ljk5Mzc3LDYuOTkzNzcgMTQuNTAxNDgsMTIuMjM0MDQgMjAuODkwNTksMTUuMTE2MmM0LjU0NDk0LDIuMDE1NSA4LjI3MzYsMi45MjI0NyAxMC40MzAxOCwyLjMzNzk3bDEuOTI0OCwtMS45MzQ4OGMwLjQ1MzQ4LC0xLjkyNDggLTAuNTAzODgsLTUuMTc5ODIgLTIuNTA5MjksLTkuMjAwNzNjLTMuMTEzOTQsLTYuMjU4MTEgLTguNzg3NTUsLTEzLjc2NTgyIC0xNi4wNDMzMywtMjEuMDExNTJ6TTYwLjI4NjMzLC0wLjE1MTk0YzYuNzExNTksMy40NTY1NyAxNC42OTI5NSw5LjY0NDE0IDIyLjQ4MjgzLDE3LjQzNDAyYzcuNzg5ODksNy43ODk4OSAxMy44NTY1MiwxNS44MjE2MyAxNy4yMjIzOSwyMi43MjQ2OWMzLjc3OTA1LDcuNTY4MTggNC4zODM3LDE0LjAyNzg0IDAuNjk1MzQsMTcuNzE2MTljLTAuNjA1OCwwLjU3NzQ5IC0xLjI5NTUyLDEuMDU5OTYgLTIuMDQ1NzIsMS40MzFsLTM5LjQyMzA2LDM5LjQwMjljLTIuMzY4MiwyLjM4ODM2IC0zLjM1NTgsMy4zOTYxMSAtNy40MzcxNyw0LjMxMzE1Yy0yLjAzMTMyLDAuNDQwMzcgLTQuMTE4NywwLjU2Mjc2IC02LjE4NzU3LDAuMzYyNzhjLTIuMTQxODMsLTAuMjA3OCAtNC4yNTIxNSwtMC42NjQzNyAtNi4yODgzNCwtMS4zNjA0NmMtOC4zOTQ1MywtMi44MjE2OSAtMTcuMTMxNjksLTguNzI3MDkgLTI0LjQwNzYyLC0xNS45ODI4NmMtNy4yNzU5MywtNy4yNTU3NyAtMTMuMzcyOCwtMTYuMDUzNDEgLTE2LjMyNTUsLTI0LjQ2ODFjLTAuNTUwNDYsLTEuNTI1NTIgLTAuOTc4NDMsLTMuMDkyNDggLTEuMjc5ODQsLTQuNjg2MDJjLTAuMjg2MDMsLTEuNDc3NzUgLTAuNDMxMTIsLTIuOTc5MyAtMC40MzMzNCwtNC40ODQ0N2MtMC4xMTgxNywtMi4zNTIzNCAwLjMwNTkyLC00LjcwMDM3IDEuMjM5NTMsLTYuODYyNzVjMC45Njc0LC0xLjg2MTczIDIuMjUzMiwtMy41Mzk3NiAzLjc5OTIxLC00Ljk1ODExbDAuMTYxMjQsLTAuMTcxMzJsNC43NDY0OSwtNC43MzY0MWMtMC40OTc0NywtMS4xMjYwMiAtMC45MDg0OCwtMi4yODgyOCAtMS4yMjk0NSwtMy40NzY3M2MtMS41NTYxLC01LjE5NTg5IC0wLjg1MzgyLC0xMC44MDY3NSAxLjkzNDg4LC0xNS40NTg4NGM0LjMwMzA4LC03LjAzNDA3IDEzLjk1NzMsLTEyLjI2NDI4IDMwLjY2NTczLC0xMi40MzU2bDQuMjAyMzEsLTQuMTkyMjNjMC40NDM2OCwtMC42MDgzMiAwLjk4MTExLC0xLjE0MjM1IDEuNTkyMjQsLTEuNTgyMTZjMC4xOTg1NywtMC4xODczMSAwLjQyNjcsLTAuMzQwNTMgMC42NzUxOSwtMC40NTM0OGMzLjczODc0LC0yLjI1NzM1IDkuMjcxMjcsLTEuMzYwNDYgMTUuNjQwMjMsMS45MjQ4ek04OS4wNDc0Myw1OS43Njg2OWMtMi40NjcyLC0wLjU1OTY2IC00Ljg2ODc0LC0xLjM3NzA3IC03LjE2NTA4LC0yLjQzODc1Yy03LjA1NDIzLC0zLjE4NDQ4IC0xNS4yMjcwNiwtOC44NzgyNSAtMjIuNzY1LC0xNi40MDYxMmMtNi42NjcyNywtNi41MDY3NiAtMTIuMDc4NTEsLTE0LjE4NjM2IC0xNS45NjI3MSwtMjIuNjU0MTVjLTEuMDk4MjYsLTIuNDc4NDUgLTEuOTA2NDYsLTUuMDc1NTEgLTIuNDA4NTIsLTcuNzM5NDlsLTI2LjQxODk4LDI2LjMwMjJjMTMuODgzNTcsMTUuMDM3NTkgNTUuMzcyMzcsMjAuMzU1OTQgNjguMjQxMywxNC44NDM1NmMxLjUyOTk3LC0wLjYyNjEyIDMuMjc5MTUsMC4wOTI3NiAzLjkyNjY5LDEuNjEzOGMwLjY0NzU0LDEuNTIxMDMgLTAuMDQ2NzEsMy4yODAxNCAtMS41NTg0OSwzLjk0ODk3Yy0xNS4zMjc4Myw2LjU2MDQzIC02Mi41NTg3NiwtMS41MjU3NiAtNzQuODY2MzQsLTE2LjA4MzA5bC0zLjY5ODQzLDMuNzM4NzRjLTEuMDE3OSwwLjkxNDA1IC0xLjg4MjE3LDEuOTg1ODcgLTIuNTU5NjgsMy4xNzQ0Yy0wLjUxNTM2LDEuMzI3MzcgLTAuNzM1NDUsMi43NTEwNSAtMC42NDQ5Niw0LjE3MjA3YzAuMDA3NywxLjExNjQyIDAuMTE5MDQsMi4yMjk3MiAwLjMzMjU2LDMuMzI1NTZjMC4yMzkxMywxLjI4MDIzIDAuNTc2MDMsMi41NDAyNCAxLjAwNzc0LDMuNzY4OTdjMi42MzAyMiw3LjQ4NzU2IDguMDYxOTcsMTUuMzg4MjkgMTQuODIzOTYsMjIuMDU5NThjNi43NjE5OSw2LjY3MTI5IDE0LjUyMTYzLDEyLjAyMjQyIDIxLjk3ODk2LDE0LjQ5MTRjMS41NjYyMSwwLjU0IDMuMTg5MjMsMC44OTg0MSA0LjgzNzE5LDEuMDY4MjF2MGMxLjQxNDg2LDAuMTM5NCAyLjg0Mjc3LDAuMDU3ODEgNC4yMzI1MywtMC4yNDE4NmMxLjcwOTMzLC0wLjI5ODk0IDMuMjQ4NTEsLTEuMjE3NDEgNC4zMjMyMywtMi41Nzk4M2MwLjA2MDQ2LC0wLjA3MDU0IDAuNjQ0OTYsLTAuNjQ0OTYgMC42MzQ4OCwtMC42NTUwNHoiIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlLW9wYWNpdHk9IjAuMTI5NDEiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxNSIvPjxwYXRoIGQ9Ik0zLjI5NzAyLDQ5LjQyMTZjMi40NDg2OSwwLjQ4ODgxIDE0LjYwMzczLDI0LjI5Nzc5IDMxLjc5OSwyMS40MjkyM2MxNS42MzU4MywtMi42MDg0MSA5LjA4MDY3LDE0LjMzMjQ1IDQ4LjU0ODY1LC01LjUwNjgybC0yOC41MDkxNiwyOC40Nzg5M2MwLDAgLTAuNTc0NDIsMC41ODQ1IC0wLjYzNDg4LDAuNjU1MDRjLTEuMDc0NzIsMS4zNjI0MSAtMi42MTM5LDIuMjgwOSAtNC4zMjMyMywyLjU3OTgzYy0xLjM4OTc2LDAuMjk5NjYgLTIuODE3NjgsMC4zODEyNiAtNC4yMzI1MywwLjI0MTg2djBjLTEuNjQ3OTYsLTAuMTY5NzkgLTMuMjcwOTcsLTAuNTI4MjEgLTQuODM3MTksLTEuMDY4MjFjLTcuNDU3MzMsLTIuNDk5MjEgLTE1LjI4NzUyLC03Ljg2MDQzIC0yMS45NTg4LC0xNC40ODEzMmMtNi42NzEyOSwtNi42MjA4OSAtMTIuMTYzNSwtMTQuNTcyMDIgLTE0Ljc5MzcyLC0yMi4wNTk1OGMtMC40MzE3MiwtMS4yMjg3NCAtMC43Njg2MiwtMi40ODg3NSAtMS4wMDc3NCwtMy43Njg5N2MtMC4yMTM1MiwtMS4wOTU4NSAtMC4zMjQ4NiwtMi4yMDkxNCAtMC4zMzI1NiwtMy4zMjU1NmMtMC4wMzY5LC0xLjA2NTcxIDAuMDU3ODcsLTIuMTMxOSAwLjI4MjE2LC0zLjE3NDR6IiBmaWxsPSIjZjU0MjQyIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIi8+PHBhdGggZD0iTTMxLjc3NzE4LDEwLjU1MDMzYy0xMC4yOTkxNywxLjEwODUyIC0xNi4zNDU2NSw0LjY2NTg3IC0xOS4xNDcxOSw5LjE4MDU3Yy0xLjg5MjIsMy4yNDc5NyAtMi4zNDE1Niw3LjEzODcyIC0xLjIzOTUzLDEwLjczMjVsMC4wOTA3LDAuMzQyNjRsMjAuMjY1NzksLTIwLjI1NTcxek03OC4yODQ2OSwyMS43NTY0OGMtNy4zNDY0NywtNy4zNTY1NSAtMTQuNzczNTcsLTEzLjEwMDcgLTIwLjg5MDU5LC0xNi4yOTUyN2MtNC4zNDMzOSwtMi4yMzcyIC03LjcyOTQyLC0zLjEzNDA5IC05LjM5MjIsLTIuMTg2ODFsLTAuODI2MzYsMC44MjYzNmMtMS4yMjk0NSwyLjAxNTUgLTAuNTEzOTYsNi4zMDg0OSAxLjc5Mzc5LDExLjY2OTdjMy41NjMzNSw3LjczMjE5IDguNTIwMTUsMTQuNzQyMDggMTQuNjIyNCwyMC42Nzg5N2M2Ljk5Mzc3LDYuOTkzNzcgMTQuNTAxNDgsMTIuMjM0MDQgMjAuODkwNTksMTUuMTE2MmM0LjU0NDk0LDIuMDE1NSA4LjI3MzYsMi45MjI0NyAxMC40MzAxOCwyLjMzNzk3bDEuOTI0OCwtMS45MzQ4OGMwLjQ1MzQ4LC0xLjkyNDggLTAuNTAzODgsLTUuMTc5ODIgLTIuNTA5MjksLTkuMjAwNzNjLTMuMTEzOTQsLTYuMjU4MTEgLTguNzg3NTUsLTEzLjc2NTgyIC0xNi4wNDMzMywtMjEuMDExNTJ6TTYwLjI4NjMzLC0wLjE1MTk0YzYuNzExNTksMy40NTY1NyAxNC42OTI5NSw5LjY0NDE0IDIyLjQ4MjgzLDE3LjQzNDAyYzcuNzg5ODksNy43ODk4OSAxMy44NTY1MiwxNS44MjE2MyAxNy4yMjIzOSwyMi43MjQ2OWMzLjc3OTA1LDcuNTY4MTggNC4zODM3LDE0LjAyNzg0IDAuNjk1MzQsMTcuNzE2MTljLTAuNjA1OCwwLjU3NzQ5IC0xLjI5NTUyLDEuMDU5OTYgLTIuMDQ1NzIsMS40MzFsLTM5LjQyMzA2LDM5LjQwMjljLTIuMzY4MiwyLjM4ODM2IC0zLjM1NTgsMy4zOTYxMSAtNy40MzcxNyw0LjMxMzE1Yy0yLjAzMTMyLDAuNDQwMzcgLTQuMTE4NywwLjU2Mjc2IC02LjE4NzU3LDAuMzYyNzhjLTIuMTQxODMsLTAuMjA3OCAtNC4yNTIxNSwtMC42NjQzNyAtNi4yODgzNCwtMS4zNjA0NmMtOC4zOTQ1MywtMi44MjE2OSAtMTcuMTMxNjksLTguNzI3MDkgLTI0LjQwNzYyLC0xNS45ODI4NmMtNy4yNzU5MywtNy4yNTU3NyAtMTMuMzcyOCwtMTYuMDUzNDEgLTE2LjMyNTUsLTI0LjQ2ODFjLTAuNTUwNDYsLTEuNTI1NTIgLTAuOTc4NDMsLTMuMDkyNDggLTEuMjc5ODQsLTQuNjg2MDJjLTAuMjg2MDMsLTEuNDc3NzUgLTAuNDMxMTIsLTIuOTc5MyAtMC40MzMzNCwtNC40ODQ0N2MtMC4xMTgxNywtMi4zNTIzNCAwLjMwNTkyLC00LjcwMDM3IDEuMjM5NTMsLTYuODYyNzVjMC45Njc0LC0xLjg2MTczIDIuMjUzMiwtMy41Mzk3NiAzLjc5OTIxLC00Ljk1ODExbDAuMTYxMjQsLTAuMTcxMzJsNC43NDY0OSwtNC43MzY0MWMtMC40OTc0NywtMS4xMjYwMiAtMC45MDg0OCwtMi4yODgyOCAtMS4yMjk0NSwtMy40NzY3M2MtMS41NTYxLC01LjE5NTg5IC0wLjg1MzgyLC0xMC44MDY3NSAxLjkzNDg4LC0xNS40NTg4NGM0LjMwMzA4LC03LjAzNDA3IDEzLjk1NzMsLTEyLjI2NDI4IDMwLjY2NTczLC0xMi40MzU2bDQuMjAyMzEsLTQuMTkyMjNjMC40NDM2OCwtMC42MDgzMiAwLjk4MTExLC0xLjE0MjM1IDEuNTkyMjQsLTEuNTgyMTZjMC4xOTg1NywtMC4xODczMSAwLjQyNjcsLTAuMzQwNTMgMC42NzUxOSwtMC40NTM0OGMzLjczODc0LC0yLjI1NzM1IDkuMjcxMjcsLTEuMzYwNDYgMTUuNjQwMjMsMS45MjQ4ek04OS4wNDc0Myw1OS43Njg2OWMtMi40NjcyLC0wLjU1OTY2IC00Ljg2ODc0LC0xLjM3NzA3IC03LjE2NTA4LC0yLjQzODc1Yy03LjA1NDIzLC0zLjE4NDQ4IC0xNS4yMjcwNiwtOC44NzgyNSAtMjIuNzY1LC0xNi40MDYxMmMtNi42NjcyNywtNi41MDY3NiAtMTIuMDc4NTEsLTE0LjE4NjM2IC0xNS45NjI3MSwtMjIuNjU0MTVjLTEuMDk4MjYsLTIuNDc4NDUgLTEuOTA2NDYsLTUuMDc1NTEgLTIuNDA4NTIsLTcuNzM5NDlsLTI2LjQxODk4LDI2LjMwMjJjMTMuODgzNTcsMTUuMDM3NTkgNTUuMzcyMzcsMjAuMzU1OTQgNjguMjQxMywxNC44NDM1NmMxLjUyOTk3LC0wLjYyNjEyIDMuMjc5MTUsMC4wOTI3NiAzLjkyNjY5LDEuNjEzOGMwLjY0NzU0LDEuNTIxMDMgLTAuMDQ2NzEsMy4yODAxNCAtMS41NTg0OSwzLjk0ODk3Yy0xNS4zMjc4Myw2LjU2MDQzIC02Mi41NTg3NiwtMS41MjU3NiAtNzQuODY2MzQsLTE2LjA4MzA5bC0zLjY5ODQzLDMuNzM4NzRjLTEuMDE3OSwwLjkxNDA1IC0xLjg4MjE3LDEuOTg1ODcgLTIuNTU5NjgsMy4xNzQ0Yy0wLjUxNTM2LDEuMzI3MzcgLTAuNzM1NDUsMi43NTEwNSAtMC42NDQ5Niw0LjE3MjA3YzAuMDA3NywxLjExNjQyIDAuMTE5MDQsMi4yMjk3MiAwLjMzMjU2LDMuMzI1NTZjMC4yMzkxMywxLjI4MDIzIDAuNTc2MDMsMi41NDAyNCAxLjAwNzc0LDMuNzY4OTdjMi42MzAyMiw3LjQ4NzU2IDguMDYxOTcsMTUuMzg4MjkgMTQuODIzOTYsMjIuMDU5NThjNi43NjE5OSw2LjY3MTI5IDE0LjUyMTYzLDEyLjAyMjQyIDIxLjk3ODk2LDE0LjQ5MTRjMS41NjYyMSwwLjU0IDMuMTg5MjMsMC44OTg0MSA0LjgzNzE5LDEuMDY4MjF2MGMxLjQxNDg2LDAuMTM5NCAyLjg0Mjc3LDAuMDU3ODEgNC4yMzI1MywtMC4yNDE4NmMxLjcwOTMzLC0wLjI5ODk0IDMuMjQ4NTEsLTEuMjE3NDEgNC4zMjMyMywtMi41Nzk4M2MwLjA2MDQ2LC0wLjA3MDU0IDAuNjQ0OTYsLTAuNjQ0OTYgMC42MzQ4OCwtMC42NTUwNHoiIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEwIi8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6NTguOTI4MDk6NTcuNjUxOTM5OTk5OTk5OTk2LS0+"; +const BorderIcon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMjkuMDA4NDIiIGhlaWdodD0iMTI5LjAwODQzIiB2aWV3Qm94PSIwLDAsMTI5LjAwODQyLDEyOS4wMDg0MyI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQuNTA0MjEsMTQuNTA0MjIpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0tMi4wMDQyMSw4Ny40NTM4OGMwLC0yMi44MTE2IDAsLTY1LjYwODczIDAsLTc3LjM5MjMxYzAsLTUuOTA0NzkgNy4wNTUxNiwtMTIuMDY1NzkgMTMuMTA3OTYsLTEyLjA2NTc5YzExLjkwNTQsMCA1NC4yNjQ2NSwwIDc2LjcwNzQyLDBjOC41Mzc2NywwIDE0LjE5MzA0LDcuMTQ4NzcgMTQuMTkzMDQsMTcuNTQ0ODljMCwyMy44MjMyNSAwLDY0LjY5OTA0IDAsNzUuNjgwMDljMCw1LjM2NDQ4IC02LjcyOTAyLDEwLjc4MzQ1IC0xNi41OTAxNSwxMC43ODM0NWMtMjIuODg5MjQsMCAtNjIuNjUzNTUsMCAtNzQuMzEwMzEsMGMtNi4zMzM1NSwwIC0xMy4xMDc5NiwtNS44MDcyNCAtMTMuMTA3OTYsLTE0LjU1MDMzeiIgc3Ryb2tlLW9wYWNpdHk9IjAuMTI5NDEiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyNSIvPjxwYXRoIGQ9Ik0tMi4wMDQyMSw4Ny40NTM4OGMwLC0yMi44MTE2IDAsLTY1LjYwODczIDAsLTc3LjM5MjMxYzAsLTUuOTA0NzkgNy4wNTUxNiwtMTIuMDY1NzkgMTMuMTA3OTYsLTEyLjA2NTc5YzExLjkwNTQsMCA1NC4yNjQ2NSwwIDc2LjcwNzQyLDBjOC41Mzc2NywwIDE0LjE5MzA0LDcuMTQ4NzcgMTQuMTkzMDQsMTcuNTQ0ODljMCwyMy44MjMyNSAwLDY0LjY5OTA0IDAsNzUuNjgwMDljMCw1LjM2NDQ4IC02LjcyOTAyLDEwLjc4MzQ1IC0xNi41OTAxNSwxMC43ODM0NWMtMjIuODg5MjQsMCAtNjIuNjUzNTUsMCAtNzQuMzEwMzEsMGMtNi4zMzM1NSwwIC0xMy4xMDc5NiwtNS44MDcyNCAtMTMuMTA3OTYsLTE0LjU1MDMzeiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIwIi8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6NjQuNTA0MjE6NjQuNTA0MjItLT4="; +const extensionIcon = + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgMjk2Mjk3IDMzMzMzMyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHNoYXBlLXJlbmRlcmluZz0iZ2VvbWV0cmljUHJlY2lzaW9uIiB0ZXh0LXJlbmRlcmluZz0iZ2VvbWV0cmljUHJlY2lzaW9uIiBpbWFnZS1yZW5kZXJpbmc9Im9wdGltaXplUXVhbGl0eSIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iaWQ0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjU0MTI4LjciIHkxPSI3OTM1NS41IiB4Mj0iMjQwMzE4IiB5Mj0iNzkzNTUuNSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZThlN2U1Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImlkNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSI2MjAxOS4zIiB5MT0iMjAyODY4IiB4Mj0iMjMzNTE1IiB5Mj0iMjAyODY4Ij48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlOGU3ZTUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iaWQ2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjEwNDk2MyIgeTE9Ijk5NjE2LjkiIHgyPSIxMDQ5NjMiIHkyPSIxNzEwMjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2QxZDNkNCIvPjxzdG9wIG9mZnNldD0iLjM4OCIgc3RvcC1jb2xvcj0iI2QxZDNkNCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2QxZDNkNCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJpZDciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4bGluazpocmVmPSIjaWQ2IiB4MT0iMTk0MTc5IiB5MT0iNjExODUuOCIgeDI9IjE5NDE3OSIgeTI9IjEzNTQwNyIvPjxtYXNrIGlkPSJpZDAiPjxsaW5lYXJHcmFkaWVudCBpZD0iaWQxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjEwNDk2MyIgeTE9Ijk5NjE2LjkiIHgyPSIxMDQ5NjMiIHkyPSIxNzEwMjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1vcGFjaXR5PSIwIiBzdG9wLWNvbG9yPSIjZmZmIi8+PHN0b3Agb2Zmc2V0PSIuMzg4IiBzdG9wLWNvbG9yPSIjZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii44MzEiIHN0b3AtY29sb3I9IiNmZmYiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoIGZpbGw9InVybCgjaWQxKSIgZD0iTTYxNzM3IDk5NDY3aDg2NDUzdjcxNzA0SDYxNzM3eiIvPjwvbWFzaz48bWFzayBpZD0iaWQyIj48bGluZWFyR3JhZGllbnQgaWQ9ImlkMyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIxOTQxNzkiIHkxPSI2MTE4NS44IiB4Mj0iMTk0MTc5IiB5Mj0iMTM1NDA3Ij48c3RvcCBvZmZzZXQ9IjAiIHN0b3Atb3BhY2l0eT0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIvPjxzdG9wIG9mZnNldD0iLjM4OCIgc3RvcC1jb2xvcj0iI2ZmZiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1vcGFjaXR5PSIuODMxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBmaWxsPSJ1cmwoI2lkMykiIGQ9Ik0xNDc4OTAgNjEwMzZoOTI1Nzh2NzQ1MjFoLTkyNTc4eiIvPjwvbWFzaz48c3R5bGU+LmZpbDZ7ZmlsbDojMDAwO2ZpbGwtb3BhY2l0eTouMDUwOTh9PC9zdHlsZT48L2RlZnM+PGcgaWQ9IkxheWVyX3gwMDIwXzEiPjxnIGlkPSJfNTEzMDg1MzA0Ij48cGF0aCBmaWxsPSIjMjA2MmFmIiBkPSJNMjY4NTE3IDMwMDkyMmwtMTIwMzY5IDMyNDExLTEyMDM3MS0zMjQxMUwwIDBoMjk2Mjk3eiIvPjxwYXRoIGZpbGw9IiMzYzljZDciIGQ9Ik0xNDgxNDYgMjQzNzR2MjgzMTA5bDI3MyA3NCA5NzQwOS0yNjIyOSAyMjQ4NS0yNTY5NTR6Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0ODA0MCA5OTYxN2wtODYxNTMgMzU4ODAgMjg1NyAzNTUyNCA4MzI5Ni0zNTYxNCA4ODYwNC0zNzg4MyAzNjc0LTM2MzM5LTkyMjc4IDM4NDMyeiIvPjxwYXRoIG1hc2s9InVybCgjaWQwKSIgZmlsbD0idXJsKCNpZDYpIiBkPSJNNjE4ODcgMTM1NDk3bDI4NTcgMzU1MjQgODMyOTUtMzU2MTRWOTk2MTd6Ii8+PHBhdGggbWFzaz0idXJsKCNpZDIpIiBmaWxsPSJ1cmwoI2lkNykiIGQ9Ik0yNDAzMTggNjExODZsLTkyMjc4IDM4NDMxdjM1NzkwbDg4NjA0LTM3ODgzeiIvPjxwYXRoIGZpbGw9InVybCgjaWQ1KSIgZD0iTTYyMDE5IDEzNTQ5N2wyODU4IDM1NTI0IDEyNzgwNiA0MDctMjg1OSA0NzM2NS00MjA1NSAxMTg0MC00MDQyOC0xMDIwOC0yNDUwLTI5Mzk5SDY3MzI3bDQ5MDAgNTY3NTYgNzU5NTAgMjI0NTcgNzU1MzgtMjIwNTAgOTgwMC0xMTI2OTJ6Ii8+PHBhdGggY2xhc3M9ImZpbDYiIGQ9Ik0xNDgwNDAgMTM1NDk3SDYxODg4bDI4NTcgMzU1MjQgODMyOTUgMjY2di0zNTc5MHptMCA5NTAyMmwtNDA4IDExNC00MDQyMi0xMDIwOC0yNDUwLTI5Mzk5SDY3MTk3bDQ4OTkgNTY3NTYgNzU5NDQgMjI0NTd2LTM5NzIweiIvPjxwYXRoIGZpbGw9InVybCgjaWQ0KSIgZD0iTTU0MTI5IDYxMTg2aDE4NjE4OWwtMzY3NCAzNjMzOUg1ODYyMGwtNDQ5MS0zNjMzOXoiLz48cGF0aCBjbGFzcz0iZmlsNiIgZD0iTTE0ODA0MCA2MTE4Nkg1NDEyOWw0NDkxIDM2MzM5aDg5NDIweiIvPjwvZz48L2c+PC9zdmc+"; +const miscIcon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuMzc5IiBoZWlnaHQ9IjEzNS4zNzciIHZpZXdCb3g9IjAsMCwxMzUuMzc5LDEzNS4zNzciPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzIuMzEsLTgyLjMxMSkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgZmlsbD0iIzAwMDAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTM0OC45NjcsMTEyLjA4YzIuMzIxLDIuMzIxIDIuMzIxLDYuMTE4IDAsOC40MzlsLTcuMTAxLDcuMTAxYzEuOTU5LDMuNjU4IDMuNDU0LDcuNjAxIDQuNDA1LDExLjc1Mmg5LjE5OWMzLjI4MywwIDUuOTY5LDIuNjg2IDUuOTY5LDUuOTY4djEyLjQ3MWMwLDMuMjgzIC0yLjY4Niw1Ljk2OSAtNS45NjksNS45NjloLTEwLjAzOWMtMS4yMzEsNC4wNjMgLTIuOTkyLDcuODk2IC01LjIwNCwxMS40MThsNi41MTIsNi41MWMyLjMyMSwyLjMyMyAyLjMyMSw2LjEyIDAsOC40NGwtOC44MTgsOC44MTljLTIuMzIxLDIuMzIgLTYuMTE5LDIuMzIgLTguNDM5LDBsLTcuMTAyLC03LjEwMmMtMy42NTcsMS45NiAtNy42MDEsMy40NTYgLTExLjc1Myw0LjQwNnY5LjE5OWMwLDMuMjgyIC0yLjY4NSw1Ljk2OCAtNS45NjgsNS45NjhoLTEyLjQ3Yy0zLjI4MywwIC01Ljk2OSwtMi42ODYgLTUuOTY5LC01Ljk2OHYtMTAuMDM5Yy00LjA2MywtMS4yMzIgLTcuODk2LC0yLjk5MyAtMTEuNDE3LC01LjIwNWwtNi41MTEsNi41MTJjLTIuMzIzLDIuMzIxIC02LjEyLDIuMzIxIC04LjQ0MSwwbC04LjgxOCwtOC44MThjLTIuMzIxLC0yLjMyMSAtMi4zMjEsLTYuMTE4IDAsLTguNDM5bDcuMTAyLC03LjEwMmMtMS45NiwtMy42NTcgLTMuNDU2LC03LjYgLTQuNDA1LC0xMS43NTFoLTkuMjAyYy0zLjI4MiwwIC01Ljk2OCwtMi42ODUgLTUuOTY4LC01Ljk2OHYtMTIuNDcxYzAsLTMuMjgzIDIuNjg2LC01Ljk2OCA1Ljk2OCwtNS45NjhoMTAuMDM5YzEuMjMyLC00LjA2MyAyLjk5MywtNy44OTYgNS4yMDQsLTExLjQxOGwtNi41MTEsLTYuNTFjLTIuMzIxLC0yLjMyMiAtMi4zMjEsLTYuMTIgMCwtOC40NGw4LjgxOSwtOC44MTljMi4zMjEsLTIuMzIxIDYuMTE4LC0yLjMyMSA4LjQzOSwwbDcuMTAxLDcuMTAxYzMuNjU4LC0xLjk2IDcuNjAxLC0zLjQ1NiAxMS43NTMsLTQuNDA2di05LjE5OWMwLC0zLjI4MyAyLjY4NiwtNS45NjkgNS45NjgsLTUuOTY5aDEyLjQ3MWMzLjI4MiwwIDUuOTY4LDIuNjg2IDUuOTY4LDUuOTY5djEwLjAzNmM0LjA2NCwxLjIzMSA3Ljg5OCwyLjk5MiAxMS40MjIsNS4yMDRsNi41MDcsLTYuNTA5YzIuMzIzLC0yLjMyMSA2LjEyLC0yLjMyMSA4LjQ0MSwwek0zMjQuNTE5LDE1MGMwLDEzLjUzOCAtMTAuOTc5LDI0LjUxOSAtMjQuNTE5LDI0LjUxOWMtMTMuNTM5LDAgLTI0LjUxOSwtMTAuOTggLTI0LjUxOSwtMjQuNTE5YzAsLTEzLjUzOSAxMC45OCwtMjQuNTE5IDI0LjUxOSwtMjQuNTE5YzEzLjU0LDAgMjQuNTE5LDEwLjk4IDI0LjUxOSwyNC41MTl6IiBzdHJva2Utb3BhY2l0eT0iMC4xMjk0MSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEyLjUiLz48cGF0aCBkPSJNMzQ4Ljk2NywxMTIuMDhjMi4zMjEsMi4zMjEgMi4zMjEsNi4xMTggMCw4LjQzOWwtNy4xMDEsNy4xMDFjMS45NTksMy42NTggMy40NTQsNy42MDEgNC40MDUsMTEuNzUyaDkuMTk5YzMuMjgzLDAgNS45NjksMi42ODYgNS45NjksNS45Njh2MTIuNDcxYzAsMy4yODMgLTIuNjg2LDUuOTY5IC01Ljk2OSw1Ljk2OWgtMTAuMDM5Yy0xLjIzMSw0LjA2MyAtMi45OTIsNy44OTYgLTUuMjA0LDExLjQxOGw2LjUxMiw2LjUxYzIuMzIxLDIuMzIzIDIuMzIxLDYuMTIgMCw4LjQ0bC04LjgxOCw4LjgxOWMtMi4zMjEsMi4zMiAtNi4xMTksMi4zMiAtOC40MzksMGwtNy4xMDIsLTcuMTAyYy0zLjY1NywxLjk2IC03LjYwMSwzLjQ1NiAtMTEuNzUzLDQuNDA2djkuMTk5YzAsMy4yODIgLTIuNjg1LDUuOTY4IC01Ljk2OCw1Ljk2OGgtMTIuNDdjLTMuMjgzLDAgLTUuOTY5LC0yLjY4NiAtNS45NjksLTUuOTY4di0xMC4wMzljLTQuMDYzLC0xLjIzMiAtNy44OTYsLTIuOTkzIC0xMS40MTcsLTUuMjA1bC02LjUxMSw2LjUxMmMtMi4zMjMsMi4zMjEgLTYuMTIsMi4zMjEgLTguNDQxLDBsLTguODE4LC04LjgxOGMtMi4zMjEsLTIuMzIxIC0yLjMyMSwtNi4xMTggMCwtOC40MzlsNy4xMDIsLTcuMTAyYy0xLjk2LC0zLjY1NyAtMy40NTYsLTcuNiAtNC40MDUsLTExLjc1MWgtOS4yMDJjLTMuMjgyLDAgLTUuOTY4LC0yLjY4NSAtNS45NjgsLTUuOTY4di0xMi40NzFjMCwtMy4yODMgMi42ODYsLTUuOTY4IDUuOTY4LC01Ljk2OGgxMC4wMzljMS4yMzIsLTQuMDYzIDIuOTkzLC03Ljg5NiA1LjIwNCwtMTEuNDE4bC02LjUxMSwtNi41MWMtMi4zMjEsLTIuMzIyIC0yLjMyMSwtNi4xMiAwLC04LjQ0bDguODE5LC04LjgxOWMyLjMyMSwtMi4zMjEgNi4xMTgsLTIuMzIxIDguNDM5LDBsNy4xMDEsNy4xMDFjMy42NTgsLTEuOTYgNy42MDEsLTMuNDU2IDExLjc1MywtNC40MDZ2LTkuMTk5YzAsLTMuMjgzIDIuNjg2LC01Ljk2OSA1Ljk2OCwtNS45NjloMTIuNDcxYzMuMjgyLDAgNS45NjgsMi42ODYgNS45NjgsNS45Njl2MTAuMDM2YzQuMDY0LDEuMjMxIDcuODk4LDIuOTkyIDExLjQyMiw1LjIwNGw2LjUwNywtNi41MDljMi4zMjMsLTIuMzIxIDYuMTIsLTIuMzIxIDguNDQxLDB6TTMyNC41MTksMTUwYzAsMTMuNTM4IC0xMC45NzksMjQuNTE5IC0yNC41MTksMjQuNTE5Yy0xMy41MzksMCAtMjQuNTE5LC0xMC45OCAtMjQuNTE5LC0yNC41MTljMCwtMTMuNTM5IDEwLjk4LC0yNC41MTkgMjQuNTE5LC0yNC41MTljMTMuNTQsMCAyNC41MTksMTAuOTggMjQuNTE5LDI0LjUxOXoiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI3LjUiLz48L2c+PC9nPjwvc3ZnPjwhLS1yb3RhdGlvbkNlbnRlcjo2Ny42OTo2Ny42ODktLT4="; +const TransparentIcon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMTYuNDE5NjQiIGhlaWdodD0iMTE4LjM0OTk0IiB2aWV3Qm94PSIwLDAsMTE2LjQxOTY0LDExOC4zNDk5NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOC45MjgwOSw3LjY1MTk0KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTQuNDk0OTYsNDYuMzAwNDRjMi40Mjk4NiwwLjQ4NTA1IDE0LjQ5MTQ3LDI0LjExMSAzMS41NTQ1NSwyMS4yNjQ1YzE1LjUxNTYzLC0yLjU4ODM2IDEuNjU1NjMsMjMuNDU1NDUgNDAuODIwMjEsMy43Njg2OWwtMjAuOTM0NzYsMTkuMDI2ODJjMCwwIC0wLjU3LDAuNTggLTAuNjMsMC42NWMtMS4wNjY0NiwxLjM1MTk0IC0yLjU5MzgxLDIuMjYzMzYgLTQuMjksMi41NmMtMS4zNzkwOCwwLjI5NzM2IC0yLjc5NjAyLDAuMzc4MzMgLTQuMiwwLjI0djBjLTEuNjM1MjksLTAuMTY4NDkgLTMuMjQ1ODMsLTAuNTI0MTUgLTQuOCwtMS4wNmMtNy40LC0yLjQ4IC0xNS4xNywtNy44IC0yMS43OSwtMTQuMzdjLTYuNjIsLTYuNTcgLTEyLjA3LC0xNC40NiAtMTQuNjgsLTIxLjg5Yy0wLjQyODQsLTEuMjE5MjkgLTAuNzYyNzEsLTIuNDY5NjIgLTEsLTMuNzRjLTAuMjExODgsLTEuMDg3NDIgLTAuMzIyMzYsLTIuMTkyMTYgLTAuMzMsLTMuM2MtMC4wMzY2MiwtMS4wNTc1MiAwLjA1NzQyLC0yLjExNTUyIDAuMjgsLTMuMTV6IiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxMCIvPjxwYXRoIGQ9Ik0zMS43NzcxOCwxMC41NTAzM2MtMTAuMjk5MTcsMS4xMDg1MiAtMTYuMzQ1NjUsNC42NjU4NyAtMTkuMTQ3MTksOS4xODA1N2MtMS44OTIyLDMuMjQ3OTcgLTIuMzQxNTYsNy4xMzg3MiAtMS4yMzk1MywxMC43MzI1bDAuMDkwNywwLjM0MjY0bDIwLjI2NTc5LC0yMC4yNTU3MXpNNzguMjg0NjksMjEuNzU2NDhjLTcuMzQ2NDcsLTcuMzU2NTUgLTE0Ljc3MzU3LC0xMy4xMDA3IC0yMC44OTA1OSwtMTYuMjk1MjdjLTQuMzQzMzksLTIuMjM3MiAtNy43Mjk0MiwtMy4xMzQwOSAtOS4zOTIyLC0yLjE4NjgxbC0wLjgyNjM2LDAuODI2MzZjLTEuMjI5NDUsMi4wMTU1IC0wLjUxMzk2LDYuMzA4NDkgMS43OTM3OSwxMS42Njk3YzMuNTYzMzUsNy43MzIxOSA4LjUyMDE1LDE0Ljc0MjA4IDE0LjYyMjQsMjAuNjc4OTdjNi45OTM3Nyw2Ljk5Mzc3IDE0LjUwMTQ4LDEyLjIzNDA0IDIwLjg5MDU5LDE1LjExNjJjNC41NDQ5NCwyLjAxNTUgOC4yNzM2LDIuOTIyNDcgMTAuNDMwMTgsMi4zMzc5N2wxLjkyNDgsLTEuOTM0ODhjMC40NTM0OCwtMS45MjQ4IC0wLjUwMzg4LC01LjE3OTgyIC0yLjUwOTI5LC05LjIwMDczYy0zLjExMzk0LC02LjI1ODExIC04Ljc4NzU1LC0xMy43NjU4MiAtMTYuMDQzMzMsLTIxLjAxMTUyek02MC4yODYzMywtMC4xNTE5NGM2LjcxMTU5LDMuNDU2NTcgMTQuNjkyOTUsOS42NDQxNCAyMi40ODI4MywxNy40MzQwMmM3Ljc4OTg5LDcuNzg5ODkgMTMuODU2NTIsMTUuODIxNjMgMTcuMjIyMzksMjIuNzI0NjljMy43NzkwNSw3LjU2ODE4IDQuMzgzNywxNC4wMjc4NCAwLjY5NTM0LDE3LjcxNjE5Yy0wLjYwNTgsMC41Nzc0OSAtMS4yOTU1MiwxLjA1OTk2IC0yLjA0NTcyLDEuNDMxbC0zOS40MjMwNiwzOS40MDI5Yy0yLjM2ODIsMi4zODgzNiAtMy4zNTU4LDMuMzk2MTEgLTcuNDM3MTcsNC4zMTMxNWMtMi4wMzEzMiwwLjQ0MDM3IC00LjExODcsMC41NjI3NiAtNi4xODc1NywwLjM2Mjc4Yy0yLjE0MTgzLC0wLjIwNzggLTQuMjUyMTUsLTAuNjY0MzcgLTYuMjg4MzQsLTEuMzYwNDZjLTguMzk0NTMsLTIuODIxNjkgLTE3LjEzMTY5LC04LjcyNzA5IC0yNC40MDc2MiwtMTUuOTgyODZjLTcuMjc1OTMsLTcuMjU1NzcgLTEzLjM3MjgsLTE2LjA1MzQxIC0xNi4zMjU1LC0yNC40NjgxYy0wLjU1MDQ2LC0xLjUyNTUyIC0wLjk3ODQzLC0zLjA5MjQ4IC0xLjI3OTg0LC00LjY4NjAyYy0wLjI4NjAzLC0xLjQ3Nzc1IC0wLjQzMTEyLC0yLjk3OTMgLTAuNDMzMzQsLTQuNDg0NDdjLTAuMTE4MTcsLTIuMzUyMzQgMC4zMDU5MiwtNC43MDAzNyAxLjIzOTUzLC02Ljg2Mjc1YzAuOTY3NCwtMS44NjE3MyAyLjI1MzIsLTMuNTM5NzYgMy43OTkyMSwtNC45NTgxMWwwLjE2MTI0LC0wLjE3MTMybDQuNzQ2NDksLTQuNzM2NDFjLTAuNDk3NDcsLTEuMTI2MDIgLTAuOTA4NDgsLTIuMjg4MjggLTEuMjI5NDUsLTMuNDc2NzNjLTEuNTU2MSwtNS4xOTU4OSAtMC44NTM4MiwtMTAuODA2NzUgMS45MzQ4OCwtMTUuNDU4ODRjNC4zMDMwOCwtNy4wMzQwNyAxMy45NTczLC0xMi4yNjQyOCAzMC42NjU3MywtMTIuNDM1Nmw0LjIwMjMxLC00LjE5MjIzYzAuNDQzNjgsLTAuNjA4MzIgMC45ODExMSwtMS4xNDIzNSAxLjU5MjI0LC0xLjU4MjE2YzAuMTk4NTcsLTAuMTg3MzEgMC40MjY3LC0wLjM0MDUzIDAuNjc1MTksLTAuNDUzNDhjMy43Mzg3NCwtMi4yNTczNSA5LjI3MTI3LC0xLjM2MDQ2IDE1LjY0MDIzLDEuOTI0OHpNODkuMDQ3NDMsNTkuNzY4NjljLTIuNDY3MiwtMC41NTk2NiAtNC44Njg3NCwtMS4zNzcwNyAtNy4xNjUwOCwtMi40Mzg3NWMtNy4wNTQyMywtMy4xODQ0OCAtMTUuMjI3MDYsLTguODc4MjUgLTIyLjc2NSwtMTYuNDA2MTJjLTYuNjY3MjcsLTYuNTA2NzYgLTEyLjA3ODUxLC0xNC4xODYzNiAtMTUuOTYyNzEsLTIyLjY1NDE1Yy0xLjA5ODI2LC0yLjQ3ODQ1IC0xLjkwNjQ2LC01LjA3NTUxIC0yLjQwODUyLC03LjczOTQ5bC0yNi40MTg5OCwyNi4zMDIyYzEzLjg4MzU3LDE1LjAzNzU5IDU1LjM3MjM3LDIwLjM1NTk0IDY4LjI0MTMsMTQuODQzNTZjMS41Mjk5NywtMC42MjYxMiAzLjI3OTE1LDAuMDkyNzYgMy45MjY2OSwxLjYxMzhjMC42NDc1NCwxLjUyMTAzIC0wLjA0NjcxLDMuMjgwMTQgLTEuNTU4NDksMy45NDg5N2MtMTUuMzI3ODMsNi41NjA0MyAtNjIuNTU4NzYsLTEuNTI1NzYgLTc0Ljg2NjM0LC0xNi4wODMwOWwtMy42OTg0MywzLjczODc0Yy0xLjAxNzksMC45MTQwNSAtMS44ODIxNywxLjk4NTg3IC0yLjU1OTY4LDMuMTc0NGMtMC41MTUzNiwxLjMyNzM3IC0wLjczNTQ1LDIuNzUxMDUgLTAuNjQ0OTYsNC4xNzIwN2MwLjAwNzcsMS4xMTY0MiAwLjExOTA0LDIuMjI5NzIgMC4zMzI1NiwzLjMyNTU2YzAuMjM5MTMsMS4yODAyMyAwLjU3NjAzLDIuNTQwMjQgMS4wMDc3NCwzLjc2ODk3YzIuNjMwMjIsNy40ODc1NiA4LjA2MTk3LDE1LjM4ODI5IDE0LjgyMzk2LDIyLjA1OTU4YzYuNzYxOTksNi42NzEyOSAxNC41MjE2MywxMi4wMjI0MiAyMS45Nzg5NiwxNC40OTE0YzEuNTY2MjEsMC41NCAzLjE4OTIzLDAuODk4NDEgNC44MzcxOSwxLjA2ODIxdjBjMS40MTQ4NiwwLjEzOTQgMi44NDI3NywwLjA1NzgxIDQuMjMyNTMsLTAuMjQxODZjMS43MDkzMywtMC4yOTg5NCAzLjI0ODUxLC0xLjIxNzQxIDQuMzIzMjMsLTIuNTc5ODNjMC4wNjA0NiwtMC4wNzA1NCAwLjY0NDk2LC0wLjY0NDk2IDAuNjM0ODgsLTAuNjU1MDR6IiBmaWxsPSIjMDAwMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1vcGFjaXR5PSIwLjEyOTQxIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMTUiLz48cGF0aCBkPSJNMzEuNzc3MTgsMTAuNTUwMzNjLTEwLjI5OTE3LDEuMTA4NTIgLTE2LjM0NTY1LDQuNjY1ODcgLTE5LjE0NzE5LDkuMTgwNTdjLTEuODkyMiwzLjI0Nzk3IC0yLjM0MTU2LDcuMTM4NzIgLTEuMjM5NTMsMTAuNzMyNWwwLjA5MDcsMC4zNDI2NGwyMC4yNjU3OSwtMjAuMjU1NzF6TTc4LjI4NDY5LDIxLjc1NjQ4Yy03LjM0NjQ3LC03LjM1NjU1IC0xNC43NzM1NywtMTMuMTAwNyAtMjAuODkwNTksLTE2LjI5NTI3Yy00LjM0MzM5LC0yLjIzNzIgLTcuNzI5NDIsLTMuMTM0MDkgLTkuMzkyMiwtMi4xODY4MWwtMC44MjYzNiwwLjgyNjM2Yy0xLjIyOTQ1LDIuMDE1NSAtMC41MTM5Niw2LjMwODQ5IDEuNzkzNzksMTEuNjY5N2MzLjU2MzM1LDcuNzMyMTkgOC41MjAxNSwxNC43NDIwOCAxNC42MjI0LDIwLjY3ODk3YzYuOTkzNzcsNi45OTM3NyAxNC41MDE0OCwxMi4yMzQwNCAyMC44OTA1OSwxNS4xMTYyYzQuNTQ0OTQsMi4wMTU1IDguMjczNiwyLjkyMjQ3IDEwLjQzMDE4LDIuMzM3OTdsMS45MjQ4LC0xLjkzNDg4YzAuNDUzNDgsLTEuOTI0OCAtMC41MDM4OCwtNS4xNzk4MiAtMi41MDkyOSwtOS4yMDA3M2MtMy4xMTM5NCwtNi4yNTgxMSAtOC43ODc1NSwtMTMuNzY1ODIgLTE2LjA0MzMzLC0yMS4wMTE1MnpNNjAuMjg2MzMsLTAuMTUxOTRjNi43MTE1OSwzLjQ1NjU3IDE0LjY5Mjk1LDkuNjQ0MTQgMjIuNDgyODMsMTcuNDM0MDJjNy43ODk4OSw3Ljc4OTg5IDEzLjg1NjUyLDE1LjgyMTYzIDE3LjIyMjM5LDIyLjcyNDY5YzMuNzc5MDUsNy41NjgxOCA0LjM4MzcsMTQuMDI3ODQgMC42OTUzNCwxNy43MTYxOWMtMC42MDU4LDAuNTc3NDkgLTEuMjk1NTIsMS4wNTk5NiAtMi4wNDU3MiwxLjQzMWwtMzkuNDIzMDYsMzkuNDAyOWMtMi4zNjgyLDIuMzg4MzYgLTMuMzU1OCwzLjM5NjExIC03LjQzNzE3LDQuMzEzMTVjLTIuMDMxMzIsMC40NDAzNyAtNC4xMTg3LDAuNTYyNzYgLTYuMTg3NTcsMC4zNjI3OGMtMi4xNDE4MywtMC4yMDc4IC00LjI1MjE1LC0wLjY2NDM3IC02LjI4ODM0LC0xLjM2MDQ2Yy04LjM5NDUzLC0yLjgyMTY5IC0xNy4xMzE2OSwtOC43MjcwOSAtMjQuNDA3NjIsLTE1Ljk4Mjg2Yy03LjI3NTkzLC03LjI1NTc3IC0xMy4zNzI4LC0xNi4wNTM0MSAtMTYuMzI1NSwtMjQuNDY4MWMtMC41NTA0NiwtMS41MjU1MiAtMC45Nzg0MywtMy4wOTI0OCAtMS4yNzk4NCwtNC42ODYwMmMtMC4yODYwMywtMS40Nzc3NSAtMC40MzExMiwtMi45NzkzIC0wLjQzMzM0LC00LjQ4NDQ3Yy0wLjExODE3LC0yLjM1MjM0IDAuMzA1OTIsLTQuNzAwMzcgMS4yMzk1MywtNi44NjI3NWMwLjk2NzQsLTEuODYxNzMgMi4yNTMyLC0zLjUzOTc2IDMuNzk5MjEsLTQuOTU4MTFsMC4xNjEyNCwtMC4xNzEzMmw0Ljc0NjQ5LC00LjczNjQxYy0wLjQ5NzQ3LC0xLjEyNjAyIC0wLjkwODQ4LC0yLjI4ODI4IC0xLjIyOTQ1LC0zLjQ3NjczYy0xLjU1NjEsLTUuMTk1ODkgLTAuODUzODIsLTEwLjgwNjc1IDEuOTM0ODgsLTE1LjQ1ODg0YzQuMzAzMDgsLTcuMDM0MDcgMTMuOTU3MywtMTIuMjY0MjggMzAuNjY1NzMsLTEyLjQzNTZsNC4yMDIzMSwtNC4xOTIyM2MwLjQ0MzY4LC0wLjYwODMyIDAuOTgxMTEsLTEuMTQyMzUgMS41OTIyNCwtMS41ODIxNmMwLjE5ODU3LC0wLjE4NzMxIDAuNDI2NywtMC4zNDA1MyAwLjY3NTE5LC0wLjQ1MzQ4YzMuNzM4NzQsLTIuMjU3MzUgOS4yNzEyNywtMS4zNjA0NiAxNS42NDAyMywxLjkyNDh6TTg5LjA0NzQzLDU5Ljc2ODY5Yy0yLjQ2NzIsLTAuNTU5NjYgLTQuODY4NzQsLTEuMzc3MDcgLTcuMTY1MDgsLTIuNDM4NzVjLTcuMDU0MjMsLTMuMTg0NDggLTE1LjIyNzA2LC04Ljg3ODI1IC0yMi43NjUsLTE2LjQwNjEyYy02LjY2NzI3LC02LjUwNjc2IC0xMi4wNzg1MSwtMTQuMTg2MzYgLTE1Ljk2MjcxLC0yMi42NTQxNWMtMS4wOTgyNiwtMi40Nzg0NSAtMS45MDY0NiwtNS4wNzU1MSAtMi40MDg1MiwtNy43Mzk0OWwtMjYuNDE4OTgsMjYuMzAyMmMxMy44ODM1NywxNS4wMzc1OSA1NS4zNzIzNywyMC4zNTU5NCA2OC4yNDEzLDE0Ljg0MzU2YzEuNTI5OTcsLTAuNjI2MTIgMy4yNzkxNSwwLjA5Mjc2IDMuOTI2NjksMS42MTM4YzAuNjQ3NTQsMS41MjEwMyAtMC4wNDY3MSwzLjI4MDE0IC0xLjU1ODQ5LDMuOTQ4OTdjLTE1LjMyNzgzLDYuNTYwNDMgLTYyLjU1ODc2LC0xLjUyNTc2IC03NC44NjYzNCwtMTYuMDgzMDlsLTMuNjk4NDMsMy43Mzg3NGMtMS4wMTc5LDAuOTE0MDUgLTEuODgyMTcsMS45ODU4NyAtMi41NTk2OCwzLjE3NDRjLTAuNTE1MzYsMS4zMjczNyAtMC43MzU0NSwyLjc1MTA1IC0wLjY0NDk2LDQuMTcyMDdjMC4wMDc3LDEuMTE2NDIgMC4xMTkwNCwyLjIyOTcyIDAuMzMyNTYsMy4zMjU1NmMwLjIzOTEzLDEuMjgwMjMgMC41NzYwMywyLjU0MDI0IDEuMDA3NzQsMy43Njg5N2MyLjYzMDIyLDcuNDg3NTYgOC4wNjE5NywxNS4zODgyOSAxNC44MjM5NiwyMi4wNTk1OGM2Ljc2MTk5LDYuNjcxMjkgMTQuNTIxNjMsMTIuMDIyNDIgMjEuOTc4OTYsMTQuNDkxNGMxLjU2NjIxLDAuNTQgMy4xODkyMywwLjg5ODQxIDQuODM3MTksMS4wNjgyMXYwYzEuNDE0ODYsMC4xMzk0IDIuODQyNzcsMC4wNTc4MSA0LjIzMjUzLC0wLjI0MTg2YzEuNzA5MzMsLTAuMjk4OTQgMy4yNDg1MSwtMS4yMTc0MSA0LjMyMzIzLC0yLjU3OTgzYzAuMDYwNDYsLTAuMDcwNTQgMC42NDQ5NiwtMC42NDQ5NiAwLjYzNDg4LC0wLjY1NTA0eiIgZmlsbD0iIzAwMDAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMTAiLz48L2c+PC9nPjwvc3ZnPjwhLS1yb3RhdGlvbkNlbnRlcjo1OC45MjgwOTo1Ny42NTE5Mzk5OTk5OTk5OTYtLT4="; +const GradientIcon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMTYuNDE5NjQiIGhlaWdodD0iMTE4LjM0OTk0IiB2aWV3Qm94PSIwLDAsMTE2LjQxOTY0LDExOC4zNDk5NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IHgxPSIzLjM2ODMzIiB5MT0iNzMuMjEzOCIgeDI9IjgzLjM4NjAzIiB5Mj0iNzMuMjEzOCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJjb2xvci0xIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmNTQyNDIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM0Mjk3ZjUiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4LjkyODA5LDcuNjUxOTQpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMy42NTYwMyw0OS40MjYwM2MyLjQyOTg2LDAuNDg1MDUgMTQuNDkxNDcsMjQuMTExIDMxLjU1NDU1LDIxLjI2NDVjMTUuNTE1NjMsLTIuNTg4MzYgOS4wMTA4NywxNC4yMjIyNyA0OC4xNzU0NSwtNS40NjQ0OWwtMjguMjksMjguMjZjMCwwIC0wLjU3LDAuNTggLTAuNjMsMC42NWMtMS4wNjY0NiwxLjM1MTk0IC0yLjU5MzgxLDIuMjYzMzYgLTQuMjksMi41NmMtMS4zNzkwOCwwLjI5NzM2IC0yLjc5NjAyLDAuMzc4MzMgLTQuMiwwLjI0djBjLTEuNjM1MjksLTAuMTY4NDkgLTMuMjQ1ODMsLTAuNTI0MTUgLTQuOCwtMS4wNmMtNy40LC0yLjQ4IC0xNS4xNywtNy44IC0yMS43OSwtMTQuMzdjLTYuNjIsLTYuNTcgLTEyLjA3LC0xNC40NiAtMTQuNjgsLTIxLjg5Yy0wLjQyODQsLTEuMjE5MjkgLTAuNzYyNzEsLTIuNDY5NjIgLTEsLTMuNzRjLTAuMjExODgsLTEuMDg3NDIgLTAuMzIyMzYsLTIuMTkyMTYgLTAuMzMsLTMuM2MtMC4wMzY2MiwtMS4wNTc1MiAwLjA1NzQyLC0yLjExNTUyIDAuMjgsLTMuMTV6IiBmaWxsPSJ1cmwoI2NvbG9yLTEpIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIi8+PHBhdGggZD0iTTMxLjc3NzE4LDEwLjU1MDMzYy0xMC4yOTkxNywxLjEwODUyIC0xNi4zNDU2NSw0LjY2NTg3IC0xOS4xNDcxOSw5LjE4MDU3Yy0xLjg5MjIsMy4yNDc5NyAtMi4zNDE1Niw3LjEzODcyIC0xLjIzOTUzLDEwLjczMjVsMC4wOTA3LDAuMzQyNjRsMjAuMjY1NzksLTIwLjI1NTcxek03OC4yODQ2OSwyMS43NTY0OGMtNy4zNDY0NywtNy4zNTY1NSAtMTQuNzczNTcsLTEzLjEwMDcgLTIwLjg5MDU5LC0xNi4yOTUyN2MtNC4zNDMzOSwtMi4yMzcyIC03LjcyOTQyLC0zLjEzNDA5IC05LjM5MjIsLTIuMTg2ODFsLTAuODI2MzYsMC44MjYzNmMtMS4yMjk0NSwyLjAxNTUgLTAuNTEzOTYsNi4zMDg0OSAxLjc5Mzc5LDExLjY2OTdjMy41NjMzNSw3LjczMjE5IDguNTIwMTUsMTQuNzQyMDggMTQuNjIyNCwyMC42Nzg5N2M2Ljk5Mzc3LDYuOTkzNzcgMTQuNTAxNDgsMTIuMjM0MDQgMjAuODkwNTksMTUuMTE2MmM0LjU0NDk0LDIuMDE1NSA4LjI3MzYsMi45MjI0NyAxMC40MzAxOCwyLjMzNzk3bDEuOTI0OCwtMS45MzQ4OGMwLjQ1MzQ4LC0xLjkyNDggLTAuNTAzODgsLTUuMTc5ODIgLTIuNTA5MjksLTkuMjAwNzNjLTMuMTEzOTQsLTYuMjU4MTEgLTguNzg3NTUsLTEzLjc2NTgyIC0xNi4wNDMzMywtMjEuMDExNTJ6TTYwLjI4NjMzLC0wLjE1MTk0YzYuNzExNTksMy40NTY1NyAxNC42OTI5NSw5LjY0NDE0IDIyLjQ4MjgzLDE3LjQzNDAyYzcuNzg5ODksNy43ODk4OSAxMy44NTY1MiwxNS44MjE2MyAxNy4yMjIzOSwyMi43MjQ2OWMzLjc3OTA1LDcuNTY4MTggNC4zODM3LDE0LjAyNzg0IDAuNjk1MzQsMTcuNzE2MTljLTAuNjA1OCwwLjU3NzQ5IC0xLjI5NTUyLDEuMDU5OTYgLTIuMDQ1NzIsMS40MzFsLTM5LjQyMzA2LDM5LjQwMjljLTIuMzY4MiwyLjM4ODM2IC0zLjM1NTgsMy4zOTYxMSAtNy40MzcxNyw0LjMxMzE1Yy0yLjAzMTMyLDAuNDQwMzcgLTQuMTE4NywwLjU2Mjc2IC02LjE4NzU3LDAuMzYyNzhjLTIuMTQxODMsLTAuMjA3OCAtNC4yNTIxNSwtMC42NjQzNyAtNi4yODgzNCwtMS4zNjA0NmMtOC4zOTQ1MywtMi44MjE2OSAtMTcuMTMxNjksLTguNzI3MDkgLTI0LjQwNzYyLC0xNS45ODI4NmMtNy4yNzU5MywtNy4yNTU3NyAtMTMuMzcyOCwtMTYuMDUzNDEgLTE2LjMyNTUsLTI0LjQ2ODFjLTAuNTUwNDYsLTEuNTI1NTIgLTAuOTc4NDMsLTMuMDkyNDggLTEuMjc5ODQsLTQuNjg2MDJjLTAuMjg2MDMsLTEuNDc3NzUgLTAuNDMxMTIsLTIuOTc5MyAtMC40MzMzNCwtNC40ODQ0N2MtMC4xMTgxNywtMi4zNTIzNCAwLjMwNTkyLC00LjcwMDM3IDEuMjM5NTMsLTYuODYyNzVjMC45Njc0LC0xLjg2MTczIDIuMjUzMiwtMy41Mzk3NiAzLjc5OTIxLC00Ljk1ODExbDAuMTYxMjQsLTAuMTcxMzJsNC43NDY0OSwtNC43MzY0MWMtMC40OTc0NywtMS4xMjYwMiAtMC45MDg0OCwtMi4yODgyOCAtMS4yMjk0NSwtMy40NzY3M2MtMS41NTYxLC01LjE5NTg5IC0wLjg1MzgyLC0xMC44MDY3NSAxLjkzNDg4LC0xNS40NTg4NGM0LjMwMzA4LC03LjAzNDA3IDEzLjk1NzMsLTEyLjI2NDI4IDMwLjY2NTczLC0xMi40MzU2bDQuMjAyMzEsLTQuMTkyMjNjMC40NDM2OCwtMC42MDgzMiAwLjk4MTExLC0xLjE0MjM1IDEuNTkyMjQsLTEuNTgyMTZjMC4xOTg1NywtMC4xODczMSAwLjQyNjcsLTAuMzQwNTMgMC42NzUxOSwtMC40NTM0OGMzLjczODc0LC0yLjI1NzM1IDkuMjcxMjcsLTEuMzYwNDYgMTUuNjQwMjMsMS45MjQ4ek04OS4wNDc0Myw1OS43Njg2OWMtMi40NjcyLC0wLjU1OTY2IC00Ljg2ODc0LC0xLjM3NzA3IC03LjE2NTA4LC0yLjQzODc1Yy03LjA1NDIzLC0zLjE4NDQ4IC0xNS4yMjcwNiwtOC44NzgyNSAtMjIuNzY1LC0xNi40MDYxMmMtNi42NjcyNywtNi41MDY3NiAtMTIuMDc4NTEsLTE0LjE4NjM2IC0xNS45NjI3MSwtMjIuNjU0MTVjLTEuMDk4MjYsLTIuNDc4NDUgLTEuOTA2NDYsLTUuMDc1NTEgLTIuNDA4NTIsLTcuNzM5NDlsLTI2LjQxODk4LDI2LjMwMjJjMTMuODgzNTcsMTUuMDM3NTkgNTUuMzcyMzcsMjAuMzU1OTQgNjguMjQxMywxNC44NDM1NmMxLjUyOTk3LC0wLjYyNjEyIDMuMjc5MTUsMC4wOTI3NiAzLjkyNjY5LDEuNjEzOGMwLjY0NzU0LDEuNTIxMDMgLTAuMDQ2NzEsMy4yODAxNCAtMS41NTg0OSwzLjk0ODk3Yy0xNS4zMjc4Myw2LjU2MDQzIC02Mi41NTg3NiwtMS41MjU3NiAtNzQuODY2MzQsLTE2LjA4MzA5bC0zLjY5ODQzLDMuNzM4NzRjLTEuMDE3OSwwLjkxNDA1IC0xLjg4MjE3LDEuOTg1ODcgLTIuNTU5NjgsMy4xNzQ0Yy0wLjUxNTM2LDEuMzI3MzcgLTAuNzM1NDUsMi43NTEwNSAtMC42NDQ5Niw0LjE3MjA3YzAuMDA3NywxLjExNjQyIDAuMTE5MDQsMi4yMjk3MiAwLjMzMjU2LDMuMzI1NTZjMC4yMzkxMywxLjI4MDIzIDAuNTc2MDMsMi41NDAyNCAxLjAwNzc0LDMuNzY4OTdjMi42MzAyMiw3LjQ4NzU2IDguMDYxOTcsMTUuMzg4MjkgMTQuODIzOTYsMjIuMDU5NThjNi43NjE5OSw2LjY3MTI5IDE0LjUyMTYzLDEyLjAyMjQyIDIxLjk3ODk2LDE0LjQ5MTRjMS41NjYyMSwwLjU0IDMuMTg5MjMsMC44OTg0MSA0LjgzNzE5LDEuMDY4MjF2MGMxLjQxNDg2LDAuMTM5NCAyLjg0Mjc3LDAuMDU3ODEgNC4yMzI1MywtMC4yNDE4NmMxLjcwOTMzLC0wLjI5ODk0IDMuMjQ4NTEsLTEuMjE3NDEgNC4zMjMyMywtMi41Nzk4M2MwLjA2MDQ2LC0wLjA3MDU0IDAuNjQ0OTYsLTAuNjQ0OTYgMC42MzQ4OCwtMC42NTUwNHoiIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlLW9wYWNpdHk9IjAuMTI5NDEiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxNSIvPjxwYXRoIGQ9Ik0zMS43NzcxOCwxMC41NTAzM2MtMTAuMjk5MTcsMS4xMDg1MiAtMTYuMzQ1NjUsNC42NjU4NyAtMTkuMTQ3MTksOS4xODA1N2MtMS44OTIyLDMuMjQ3OTcgLTIuMzQxNTYsNy4xMzg3MiAtMS4yMzk1MywxMC43MzI1bDAuMDkwNywwLjM0MjY0bDIwLjI2NTc5LC0yMC4yNTU3MXpNNzguMjg0NjksMjEuNzU2NDhjLTcuMzQ2NDcsLTcuMzU2NTUgLTE0Ljc3MzU3LC0xMy4xMDA3IC0yMC44OTA1OSwtMTYuMjk1MjdjLTQuMzQzMzksLTIuMjM3MiAtNy43Mjk0MiwtMy4xMzQwOSAtOS4zOTIyLC0yLjE4NjgxbC0wLjgyNjM2LDAuODI2MzZjLTEuMjI5NDUsMi4wMTU1IC0wLjUxMzk2LDYuMzA4NDkgMS43OTM3OSwxMS42Njk3YzMuNTYzMzUsNy43MzIxOSA4LjUyMDE1LDE0Ljc0MjA4IDE0LjYyMjQsMjAuNjc4OTdjNi45OTM3Nyw2Ljk5Mzc3IDE0LjUwMTQ4LDEyLjIzNDA0IDIwLjg5MDU5LDE1LjExNjJjNC41NDQ5NCwyLjAxNTUgOC4yNzM2LDIuOTIyNDcgMTAuNDMwMTgsMi4zMzc5N2wxLjkyNDgsLTEuOTM0ODhjMC40NTM0OCwtMS45MjQ4IC0wLjUwMzg4LC01LjE3OTgyIC0yLjUwOTI5LC05LjIwMDczYy0zLjExMzk0LC02LjI1ODExIC04Ljc4NzU1LC0xMy43NjU4MiAtMTYuMDQzMzMsLTIxLjAxMTUyek02MC4yODYzMywtMC4xNTE5NGM2LjcxMTU5LDMuNDU2NTcgMTQuNjkyOTUsOS42NDQxNCAyMi40ODI4MywxNy40MzQwMmM3Ljc4OTg5LDcuNzg5ODkgMTMuODU2NTIsMTUuODIxNjMgMTcuMjIyMzksMjIuNzI0NjljMy43NzkwNSw3LjU2ODE4IDQuMzgzNywxNC4wMjc4NCAwLjY5NTM0LDE3LjcxNjE5Yy0wLjYwNTgsMC41Nzc0OSAtMS4yOTU1MiwxLjA1OTk2IC0yLjA0NTcyLDEuNDMxbC0zOS40MjMwNiwzOS40MDI5Yy0yLjM2ODIsMi4zODgzNiAtMy4zNTU4LDMuMzk2MTEgLTcuNDM3MTcsNC4zMTMxNWMtMi4wMzEzMiwwLjQ0MDM3IC00LjExODcsMC41NjI3NiAtNi4xODc1NywwLjM2Mjc4Yy0yLjE0MTgzLC0wLjIwNzggLTQuMjUyMTUsLTAuNjY0MzcgLTYuMjg4MzQsLTEuMzYwNDZjLTguMzk0NTMsLTIuODIxNjkgLTE3LjEzMTY5LC04LjcyNzA5IC0yNC40MDc2MiwtMTUuOTgyODZjLTcuMjc1OTMsLTcuMjU1NzcgLTEzLjM3MjgsLTE2LjA1MzQxIC0xNi4zMjU1LC0yNC40NjgxYy0wLjU1MDQ2LC0xLjUyNTUyIC0wLjk3ODQzLC0zLjA5MjQ4IC0xLjI3OTg0LC00LjY4NjAyYy0wLjI4NjAzLC0xLjQ3Nzc1IC0wLjQzMTEyLC0yLjk3OTMgLTAuNDMzMzQsLTQuNDg0NDdjLTAuMTE4MTcsLTIuMzUyMzQgMC4zMDU5MiwtNC43MDAzNyAxLjIzOTUzLC02Ljg2Mjc1YzAuOTY3NCwtMS44NjE3MyAyLjI1MzIsLTMuNTM5NzYgMy43OTkyMSwtNC45NTgxMWwwLjE2MTI0LC0wLjE3MTMybDQuNzQ2NDksLTQuNzM2NDFjLTAuNDk3NDcsLTEuMTI2MDIgLTAuOTA4NDgsLTIuMjg4MjggLTEuMjI5NDUsLTMuNDc2NzNjLTEuNTU2MSwtNS4xOTU4OSAtMC44NTM4MiwtMTAuODA2NzUgMS45MzQ4OCwtMTUuNDU4ODRjNC4zMDMwOCwtNy4wMzQwNyAxMy45NTczLC0xMi4yNjQyOCAzMC42NjU3MywtMTIuNDM1Nmw0LjIwMjMxLC00LjE5MjIzYzAuNDQzNjgsLTAuNjA4MzIgMC45ODExMSwtMS4xNDIzNSAxLjU5MjI0LC0xLjU4MjE2YzAuMTk4NTcsLTAuMTg3MzEgMC40MjY3LC0wLjM0MDUzIDAuNjc1MTksLTAuNDUzNDhjMy43Mzg3NCwtMi4yNTczNSA5LjI3MTI3LC0xLjM2MDQ2IDE1LjY0MDIzLDEuOTI0OHpNODkuMDQ3NDMsNTkuNzY4NjljLTIuNDY3MiwtMC41NTk2NiAtNC44Njg3NCwtMS4zNzcwNyAtNy4xNjUwOCwtMi40Mzg3NWMtNy4wNTQyMywtMy4xODQ0OCAtMTUuMjI3MDYsLTguODc4MjUgLTIyLjc2NSwtMTYuNDA2MTJjLTYuNjY3MjcsLTYuNTA2NzYgLTEyLjA3ODUxLC0xNC4xODYzNiAtMTUuOTYyNzEsLTIyLjY1NDE1Yy0xLjA5ODI2LC0yLjQ3ODQ1IC0xLjkwNjQ2LC01LjA3NTUxIC0yLjQwODUyLC03LjczOTQ5bC0yNi40MTg5OCwyNi4zMDIyYzEzLjg4MzU3LDE1LjAzNzU5IDU1LjM3MjM3LDIwLjM1NTk0IDY4LjI0MTMsMTQuODQzNTZjMS41Mjk5NywtMC42MjYxMiAzLjI3OTE1LDAuMDkyNzYgMy45MjY2OSwxLjYxMzhjMC42NDc1NCwxLjUyMTAzIC0wLjA0NjcxLDMuMjgwMTQgLTEuNTU4NDksMy45NDg5N2MtMTUuMzI3ODMsNi41NjA0MyAtNjIuNTU4NzYsLTEuNTI1NzYgLTc0Ljg2NjM0LC0xNi4wODMwOWwtMy42OTg0MywzLjczODc0Yy0xLjAxNzksMC45MTQwNSAtMS44ODIxNywxLjk4NTg3IC0yLjU1OTY4LDMuMTc0NGMtMC41MTUzNiwxLjMyNzM3IC0wLjczNTQ1LDIuNzUxMDUgLTAuNjQ0OTYsNC4xNzIwN2MwLjAwNzcsMS4xMTY0MiAwLjExOTA0LDIuMjI5NzIgMC4zMzI1NiwzLjMyNTU2YzAuMjM5MTMsMS4yODAyMyAwLjU3NjAzLDIuNTQwMjQgMS4wMDc3NCwzLjc2ODk3YzIuNjMwMjIsNy40ODc1NiA4LjA2MTk3LDE1LjM4ODI5IDE0LjgyMzk2LDIyLjA1OTU4YzYuNzYxOTksNi42NzEyOSAxNC41MjE2MywxMi4wMjI0MiAyMS45Nzg5NiwxNC40OTE0YzEuNTY2MjEsMC41NCAzLjE4OTIzLDAuODk4NDEgNC44MzcxOSwxLjA2ODIxdjBjMS40MTQ4NiwwLjEzOTQgMi44NDI3NywwLjA1NzgxIDQuMjMyNTMsLTAuMjQxODZjMS43MDkzMywtMC4yOTg5NCAzLjI0ODUxLC0xLjIxNzQxIDQuMzIzMjMsLTIuNTc5ODNjMC4wNjA0NiwtMC4wNzA1NCAwLjY0NDk2LC0wLjY0NDk2IDAuNjM0ODgsLTAuNjU1MDR6IiBmaWxsPSIjMDAwMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxMCIvPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjU4LjkyODA4ODk1NTAxODk4OjU3LjY1MTk0MTAyMjI0MTgzLS0+"; +const PictureIcon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzguNjA3MDUiIGhlaWdodD0iMTE0LjIxMjI3IiB2aWV3Qm94PSIwLDAsMTM4LjYwNzA1LDExNC4yMTIyNyI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTkuMzAzNTIsNy4xMDYxNCkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgZmlsbD0iIzAwMDAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2Utb3BhY2l0eT0iMC4xMjk0MSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjcuNSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0xMTUuNTUzNTIsNS43OTA3NnY4OC40MTg0OGMwLDUuMDUxMzcgLTQuMDk1NTIsOS4xNDY5IC05LjE0NjksOS4xNDY5aC0xMTIuODEzMjVjLTUuMDUxMzcsMCAtOS4xNDY5LC00LjA5NTUyIC05LjE0NjksLTkuMTQ2OXYtODguNDE4NDhjMCwtNS4wNTEzNyA0LjA5NTUyLC05LjE0NjkgOS4xNDY5LC05LjE0NjloMTEyLjgxMzI1YzUuMDUxMzcsMCA5LjE0NjksNC4wOTU1MiA5LjE0NjksOS4xNDY5ek0yLjc0MDI3LDU3LjU4OTY0bDE1LjE2MDk4LC0xMS4xNDc3OGMzLjgxODgzLC0yLjgxMjY3IDkuMTYwNjIsLTIuMjM0MTMgMTIuMjkzNDMsMS4zMzU0NWwxMC4yMjM5NCwxMS42NDg1N2wyNC41NDU3LC0xMi4yNzA1NmMzLjAxNjE5LC0xLjUxMTUyIDYuNjI0NjQsLTEuMjM3MTIgOS4zODAxNCwwLjcxNTc0bDIyLjkxNTI2LDE2LjIzMzQ2di00OS4xNjY4NmgtOTQuNTE5NDZ6TTM4LjU3NzgxLDg1LjA2MjM0bC0xNi41OTcwNCwtMTguOTA4OTJsLTE5LjE1NTg5LDE0LjA4MTY1Yy0wLjAyNzQ0LDAuMDIwNTggLTAuMDU3MTcsMC4wMjk3MyAtMC4wODQ2MSwwLjA1MDMxdjQuNzc2OTd6TTk1LjE5NzEsODUuMDYyMzRsLTI2Ljk1NTkxLC0xOS4wOTE4NmwtMTUuMzQ2MjEsNy42NzE5NmwxMC4wMjI3MSwxMS40MTk5ek0zMC4xODA5NiwyOS44MDEzNmMwLC01LjQ3MjEzIC00LjQzNjI1LC05LjkwODM4IC05LjkwODM4LC05LjkwODM4Yy01LjQ3MjEzLDAgLTkuOTA4MzgsNC40MzYyNSAtOS45MDgzOCw5LjkwODM4YzAsNS40NzIxMyA0LjQzNjI1LDkuOTA4MzggOS45MDgzOCw5LjkwODM4YzUuNDcyMTMsMCA5LjkwODM4LC00LjQzNjI1IDkuOTA4MzgsLTkuOTA4Mzh6Ii8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6NjkuMzAzNTIzOTQ2MzExODo1Ny4xMDYxMzY4MjA3MDk5Ni0tPg=="; +const ResetIcon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIzNDkuMzIzMjQiIGhlaWdodD0iMzI3LjM0OTEyIiB2aWV3Qm94PSIwLDAsMzQ5LjMyMzI0LDMyNy4zNDkxMiI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTY1LjMzODM4LC0xNi4zMjU0NCkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTM2LjQxNTc0LDk3LjkwMTk0Yy0zOC4yNDgwNiw4Mi43NzIyNSAtMzAuNTkzMDYsMjMyLjc5NzI4IDEzOS43MzA3NywyMjAuMDA0MTZjMTQyLjQzOSwtMTAuNjk4NjggMTQzLjc0MTk3LC0xOTIuNTE4ODQgNDMuMzc5NjksLTI0OS41MzgxIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iNTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxwYXRoIGQ9Ik05NS42NDc3MSw0OS43ODA2N2w5OS44NTIyMiwtMC40MDI2MyIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTkxLjQ1MzI4LDY1LjUxNDIxbDEwOS42ODM0NywtMjQuMTg4NzciIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI1MCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTIwNC4wMjE1Miw0My4yOTQzNWwtOS44MjI0LDEwNi4wMTExMyIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjUwIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNOTAuMzM4MzgsNjcuNDI2MDhsMTAzLjUyMzY2LDgyLjIwODIiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI1MCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTE1My45MTUxOCwxMDMuODQ5ODV2LTMyLjA5NTloMTQuNjk0NTF2MzIuMDk1OXoiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI1MCIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNMTk1LjQ5OTkyLDQ5LjM3ODA0IiBzdHJva2U9IiNmZjAwMDAiIHN0cm9rZS13aWR0aD0iNTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjE3NC42NjE2MjoxNjMuNjc0NTU5OTk5OTk5OTktLT4="; + +const stylesheet = document.createElement("style"); +stylesheet.className = "shovelcss-style"; +// end of for higher precedence than other sheets +document.body.appendChild(stylesheet); + +const applyCSS = () => { + let css = ""; + + // We assume all values are sanitized when they are set, so then we can just use them as-is here. + + if (monitorText) { + css += `${monitorRoot}, ${monitorListFooter}, ${monitorListHeader}, ${monitorRowIndex} { color: ${monitorText}; }`; + } + if (monitorBackgroundColor) { + css += `${monitorRoot}, ${monitorRowsInner} { background: ${monitorBackgroundColor}; }`; + } + if (monitorBorder) { + css += `${monitorRoot} { border-color: ${monitorBorder}; }`; + } + if (monitorBackgroundRoundness >= 0) { + css += `${monitorRoot} { border-radius: ${monitorBackgroundRoundness}px; }`; + } + if (monitorBackgroundBorderWidth >= 0) { + css += `${monitorRoot} { border-width: ${monitorBackgroundBorderWidth}px; }`; + } + if (variableValueBackground) { + css += `${monitorValue}, ${monitorValueLarge} { background: ${variableValueBackground} !important; }`; + } + if (variableValueTextColor) { + css += `${monitorValue}, ${monitorValueLarge} { color: ${variableValueTextColor}; }`; + } + if (variableValueRoundness >= 0) { + css += `${monitorValue} { border-radius: ${variableValueRoundness}px; }`; + } + if (listHeaderBackground) { + css += `${monitorListHeader} { background: ${listHeaderBackground}; }`; + } + if (listFooterBackground) { + css += `${monitorListFooter} { background: ${listHeaderBackground}; }`; + } + if (listValueBackground) { + css += `${monitorRowValueOuter} { background: ${listValueBackground} !important; }`; + } + if (listValueText) { + css += `${monitorRowValueOuter} { color: ${listValueText}; }`; + } + if (listValueRoundness >= 0) { + css += `${monitorRowValueOuter} { border-radius: ${listValueRoundness}px; }`; + } + if (allowScrolling) { + css += `${monitorRowsScroller} { overflow: ${allowScrolling} !important; }`; + } + if (askBackground) { + css += `${askBoxBG} { background: ${askBackground} !important; border: none !important; }`; + } + if (askBackgroundRoundness >= 0) { + css += `${askBoxBG} { border-radius: ${askBackgroundRoundness}px !important; }`; + } + if (askBackgroundBorderWidth >= 0) { + css += `${askBoxBG} { border-width: ${askBackgroundBorderWidth}px !important; }`; + } + if (askButtonBackground) { + css += `${askBoxButton} { background-color: ${askButtonBackground}; }`; + } + if (askButtonRoundness >= 0) { + css += `${askBoxButton} { border-radius: ${askButtonRoundness}px !important; }`; + } + if (askInputBackground) { + css += `${askBoxInner} { background: ${askInputBackground} !important; }`; + css += `${askBoxInner} { border: none !important; }`; + } + if (askInputText) { + css += `${askBoxInner} { color: ${askInputText} !important; }`; + } + if (askInputRoundness >= 0) { + css += `${askBoxInner} { border-radius: ${askInputRoundness}px !important; }`; + } + if (askInputBorderWidth >= 0) { + css += `${askBoxInner} { border-width: ${askInputBorderWidth}px !important; }`; + } + if (askButtonImage) { + css += `${askBoxButton} { background-image: url("${encodeURI( + askButtonImage + )}") !important; background-repeat: no-repeat; background-size: contain; }`; + css += `${askBoxIcon} { visibility: hidden; }`; + } + if (askInputBorder) { + css += `${askBoxBorderMain}, ${askBoxBorderOuter} { border-color: ${askInputBorder} !important; }`; + css += `${askBoxBorderOuter} { box-shadow: none !important; }`; + } + if (askQuestionText) { + css += `${askBoxText} { color: ${askQuestionText} !important; }`; + } + + stylesheet.textContent = css; +}; + + const resetStyles = () => { + monitorText = "", monitorBorder = "", monitorBackgroundColor = ""; + variableValueBackground = "", variableValueTextColor = ""; + listFooterBackground = "", listHeaderBackground = ""; + listValueText = "", listValueBackground = ""; + variableValueRoundness = -1, listValueRoundness = -1; + monitorBackgroundRoundness = -1, monitorBackgroundBorderWidth = -1; + allowScrolling = "", askBackground = ""; + askBackgroundRoundness = -1, askBackgroundBorderWidth = -1; + askButtonBackground = "", askButtonRoundness = -1; + askInputBackground = "", askInputRoundness = -1, askInputBorderWidth = -1; + askBoxIcon = "", askInputText = "", askQuestionText = "", askButtonImage = "", askInputBorder = ""; + applyCSS(); + }; + +const getMonitorRoot = (id) => { + const allMonitors = document.querySelectorAll(monitorRoot); + for (const monitor of allMonitors) { + if (monitor.dataset.id === id) { + return monitor; + } + } + return null; +}; + +/** + * @param {string} id + * @param {number} x + * @param {number} y + */ +const setMonitorPosition = (id, x, y) => { + const root = getMonitorRoot(id); + if (root) { + root.style.transform = `translate(${x}px, ${y}px)`; + root.style.left = "0px"; + root.style.top = "0px"; + } +}; + +/** + * @param {VM.Target} target + * @param {string} name + * @param {VM.VariableType} type + * @param {number} x + * @param {number} y + */ +const setVariableMonitorPosition = (target, name, type, x, y) => { + // @ts-expect-error + const variable = target.lookupVariableByNameAndType(name, type); + if (variable) { + // @ts-expect-error + setMonitorPosition(variable.id, x, y); + } +}; + +const parseColor = (color, callback) => { + color = Cast.toString(color); + + // These might have some exponential backtracking/ReDoS, but that's not really a concern here. + // If a project wanted to get stuck in an infinite loop, there are so many other ways to do that. + + // Simple color code or name + if (/^#?[a-z0-9]+$/.test(color)) { + callback(color); + return; + } + + // General gradient pattern + if (/^[a-z-]+-gradient\([a-z0-9,#%. ]+\)$/i.test(color)) { + callback(color); + return; + } + + // URL + // see list of non-escaped characters: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI#description + const match = color.match(/^url\("([A-Za-z0-9\-_.!~*'();/?:@&=+$,#]+)"\)$/); + if (match) { + const url = match[1]; + return vm.securityManager.canFetch(url).then((allowed) => { + if (allowed) { + callback(color); + } + }); + } + + console.error("Invalid color", color); +}; + +class MonitorStyles { + constructor(runtime) { + this.runtime = runtime; + this.runtime.on("RUNTIME_DISPOSED", resetStyles); + } + getInfo() { + return { + id: "shovelcss", + name: "Custom Styles", + menuIconURI: extensionIcon, + color1: "#0072d6", + color2: "#0064bc", + color3: "#01539b", + blocks: [ + { + blockIconURI: ColorIcon, + opcode: "changecss", + blockType: BlockType.COMMAND, + text: "set [COLORABLE] to [COLOR]", + arguments: { + COLORABLE: { + type: ArgumentType.STRING, + menu: "COLORABLE_MENU", + }, + COLOR: { + type: ArgumentType.COLOR, + defaultValue: "#ff0000", + }, + }, + }, + { + blockIconURI: GradientIcon, + opcode: "gradientAngle", + blockType: BlockType.REPORTER, + text: "make a gradient with [COLOR1] and [COLOR2] at angle [ANGLE]", + arguments: { + COLOR1: { + type: ArgumentType.COLOR, + defaultValue: "#ff0000", + }, + COLOR2: { + type: ArgumentType.COLOR, + defaultValue: "#6ed02d", + }, + ANGLE: { + type: ArgumentType.ANGLE, + defaultValue: "90", + }, + }, + }, + { + blockIconURI: TransparentIcon, + disableMonitor: true, + opcode: "transparentinput", + blockType: BlockType.REPORTER, + text: "transparent", + }, + { + blockIconURI: PictureIcon, + disableMonitor: true, + opcode: "pictureinput", + blockType: BlockType.REPORTER, + text: "image [URL]", + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: "https://extensions.turbowarp.org/dango.png", + }, + }, + }, + "---", + { + blockIconURI: PictureIcon, + disableMonitor: true, + opcode: "setAskURI", + blockType: BlockType.COMMAND, + text: "set ask prompt button image to [URL]", + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: "https://extensions.turbowarp.org/dango.png", + }, + }, + }, + "---", + { + blockIconURI: BorderIcon, + opcode: "setbordersize", + blockType: BlockType.COMMAND, + text: "set border width of [BORDER] to [SIZE]", + arguments: { + BORDER: { + type: ArgumentType.STRING, + menu: "BORDER_WIDTH_MENU", + }, + SIZE: { + type: ArgumentType.NUMBER, + defaultValue: "2", + }, + }, + }, + { + blockIconURI: BorderIcon, + opcode: "setborderradius", + blockType: BlockType.COMMAND, + text: "set roundness of [CORNER] to [SIZE]", + arguments: { + SIZE: { + type: ArgumentType.NUMBER, + defaultValue: "4", + }, + CORNER: { + type: ArgumentType.STRING, + menu: "BORDER_ROUNDNESS_MENU", + }, + }, + }, + "---", + { + blockIconURI: ResetIcon, + opcode: "clearCSS", + blockType: BlockType.COMMAND, + text: "reset styles", + }, + "---", + { + blockIconURI: miscIcon, + opcode: "allowscrollrule", + blockType: BlockType.COMMAND, + text: "set list scrolling to [SCROLLRULE]", + arguments: { + SCROLLRULE: { + type: ArgumentType.STRING, + menu: "SCROLL_MENU", + }, + }, + }, + { + blockIconURI: miscIcon, + opcode: "getValue", + blockType: BlockType.REPORTER, + text: "get [ITEM]", + arguments: { + ITEM: { + type: ArgumentType.STRING, + menu: "VALUEGET_LIST", + }, + }, + }, + "---", + { + blockIconURI: miscIcon, + opcode: "setvarpos", + blockType: BlockType.COMMAND, + text: "set position of variable [NAME] to x: [X] y: [Y]", + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "my variable", + }, + }, + }, + { + blockIconURI: miscIcon, + opcode: "setlistpos", + blockType: BlockType.COMMAND, + text: "set position of list [NAME] to x: [X] y: [Y]", + arguments: { + X: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + Y: { + type: ArgumentType.NUMBER, + defaultValue: "0", + }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "my variable", + }, + }, + }, + ], + // Accepting reporters because there can't be errors in case the value is not correct + menus: { + COLORABLE_MENU: { + acceptReporters: true, + items: [ + "monitor text", + "monitor background", + "monitor border", + "variable value background", + "variable value text", + "list header background", + "list footer background", + "list value background", + "list value text", + "ask prompt background", + "ask prompt button background", + "ask prompt input background", + "ask prompt question text", + "ask prompt input text", + "ask prompt input border", + ], + }, + BORDER_WIDTH_MENU: { + acceptReporters: true, + items: [ + "monitor background", + "ask prompt background", + "ask prompt input", + ], + }, + BORDER_ROUNDNESS_MENU: { + acceptReporters: true, + items: [ + "monitor background", + "variable value", + "list value", + "ask prompt background", + "ask prompt button", + "ask prompt input", + ], + }, + SCROLL_MENU: { + acceptReporters: true, + items: ["enabled", "disabled"], + }, + VALUEGET_LIST: { + acceptReporters: true, + items: [ + "monitor text", + "monitor background", + "monitor border color", + "variable value background", + "variable value text", + "list header background", + "list footer background", + "list value background", + "list value text", + "ask prompt background", + "ask prompt button background", + "ask prompt input background", + "ask prompt input text", + "ask prompt input border", + "monitor background border width", + "ask prompt background border width", + "ask prompt input border width", + "monitor background roundness", + "variable value roundness", + "list value roundness", + "ask prompt background roundness", + "ask prompt button roundness", + "ask prompt input roundness", + "ask prompt button image", + "list scroll rule", + ], + }, + }, + }; + } + + changecss(args) { + return parseColor(args.COLOR, (color) => { + if (args.COLORABLE === "monitor text") { + monitorText = color; + } else if (args.COLORABLE === "monitor background") { + monitorBackgroundColor = color; + } else if (args.COLORABLE === "monitor border") { + monitorBorder = color; + } else if (args.COLORABLE === "variable value background") { + variableValueBackground = color; + } else if (args.COLORABLE === "variable value text") { + variableValueTextColor = color; + } else if (args.COLORABLE === "list header background") { + listHeaderBackground = color; + } else if (args.COLORABLE === "list footer background") { + listFooterBackground = color; + } else if (args.COLORABLE === "list value background") { + listValueBackground = color; + } else if (args.COLORABLE === "list value text") { + listValueText = color; + } else if (args.COLORABLE === "ask prompt background") { + askBackground = color; + } else if (args.COLORABLE === "ask prompt button background") { + askButtonBackground = color; + } else if (args.COLORABLE === "ask prompt input background") { + askInputBackground = color; + } else if (args.COLORABLE === "ask prompt input text") { + askInputText = color; + } else if (args.COLORABLE === "ask prompt question text") { + askQuestionText = color; + } else if (args.COLORABLE === "ask prompt input border") { + askInputBorder = color; + } + + applyCSS(); + }); + } + + gradientAngle(args) { + return ( + "linear-gradient(" + + args.ANGLE + + "deg," + + args.COLOR1 + + "," + + args.COLOR2 + + ")" + ); + } + + setbordersize(args) { + const size = Cast.toNumber(args.SIZE); + if (args.BORDER === "monitor background") { + monitorBackgroundBorderWidth = size; + } else if (args.BORDER === "ask prompt background") { + askBackgroundBorderWidth = size; + } else if (args.BORDER === "ask prompt input") { + askInputBorderWidth = size; + } + applyCSS(); + } + + setborderradius(args) { + const size = Cast.toNumber(args.SIZE); + if (args.CORNER === "monitor background") { + monitorBackgroundRoundness = size; + } else if (args.CORNER === "variable value") { + variableValueRoundness = size; + } else if (args.CORNER === "list value") { + listValueRoundness = size; + } else if (args.CORNER === "ask prompt background") { + askBackgroundRoundness = size; + } else if (args.CORNER === "ask prompt button") { + askButtonRoundness = size; + } else if (args.CORNER === "ask prompt input") { + askInputRoundness = size; + } + applyCSS(); + } + + allowscrollrule(args) { + if (args.SCROLLRULE === "enabled") { + allowScrolling = "auto"; + } else { + allowScrolling = "hidden"; + } + applyCSS(); + } + + setvarpos(args, util) { + setVariableMonitorPosition( + util.target, + args.NAME, + "", + Cast.toNumber(args.X) + this.runtime.stageWidth / 2, + this.runtime.stageHeight / 2 - Cast.toNumber(args.Y) + ); + } + + setlistpos(args, util) { + setVariableMonitorPosition( + util.target, + args.NAME, + "list", + Cast.toNumber(args.X) + this.runtime.stageWidth / 2, + this.runtime.stageHeight / 2 - Cast.toNumber(args.Y) + ); + } + + help() { + alert( + "\nThis is a short introduction to how to use the Monitor Styles extension!\n\n𝗟𝗼𝗼𝗸𝘀 𝗯𝗹𝗼𝗰𝗸𝘀\nThese blocks change the appearance of the variable and list didsplays. You can use the drop-down menu to select what component you want to modify. 𝙏𝙝𝙚 𝙘𝙤𝙡𝙤𝙧 𝙗𝙡𝙤𝙘𝙠 modifieas the color of a component. You can use the 𝙜𝙧𝙖𝙙𝙞𝙚𝙣𝙩 block inside the color input, to create gradients or the 𝙄𝙢𝙖𝙜𝙚 block to use a image instead of solid colors. 𝙏𝙝𝙚𝙨𝙚 𝙩𝙬𝙤 𝙤𝙣𝙡𝙮 𝙬𝙤𝙧𝙠 𝙤𝙣 𝙘𝙚𝙧𝙩𝙖𝙞𝙣 𝙘𝙤𝙢𝙥𝙤𝙣𝙚𝙣𝙩𝙨! You can also use the 𝙩𝙧𝙖𝙣𝙨𝙥𝙖𝙧𝙚𝙣𝙩 𝙗𝙡𝙤𝙘𝙠 as a color input, to make components invisible. The 𝙗𝙤𝙧𝙙𝙚𝙧 𝙗𝙡𝙤𝙘𝙠𝙨 modify the borders of components.\n\n𝗦𝗲𝗻𝘀𝗶𝗻𝗴 𝗯𝗹𝗼𝗰𝗸𝘀\nThese blocks can change the behaviour of certain components. The 𝙨𝙘𝙧𝙤𝙡𝙡 𝙧𝙪𝙡𝙚 block change the behaviour for lists. On 'auto' they will show the scroll bar, and allow you to school, but on 'hidden', they won't let you do that, and the scroll bar will be hidden.\n\n𝗠𝗼𝘁𝗶𝗼𝗻 𝗯𝗹𝗼𝗰𝗸𝘀\nThese blocks allow you to move variable and list displays around. You need to use their 𝙡𝙖𝙗𝙚𝙡 𝙣𝙖𝙢𝙚. The label name is the text that displays on the monitor. For example, a 'for this sprite only' variable will be like 'Sprite1: my variable'." + ); + } + + transparentinput() { + return "transparent"; + } + + pictureinput(args) { + return `url("${encodeURI(args.URL)}")`; + } + + clearCSS() { + resetStyles(); + } + + getValue(args) { + if (args.ITEM === "monitor text") { + return monitorText; + } else if (args.ITEM === "monitor background") { + return monitorBackgroundColor; + } else if (args.ITEM === "monitor border color") { + return monitorBorder; + } else if (args.ITEM === "variable value background") { + return variableValueBackground; + } else if (args.ITEM === "variable value text") { + return variableValueTextColor; + } else if (args.ITEM === "list header background") { + return listHeaderBackground; + } else if (args.ITEM === "list footer background") { + return listFooterBackground; + } else if (args.ITEM === "list value background") { + return listValueBackground; + } else if (args.ITEM === "list value text") { + return listValueText; + } else if (args.ITEM === "ask prompt background") { + return askBackground; + } else if (args.ITEM === "ask prompt button background") { + return askButtonBackground; + } else if (args.ITEM === "ask prompt input background") { + return askInputBackground; + } else if (args.ITEM === "ask prompt input text") { + return askInputText; + } else if (args.ITEM === "ask prompt question text") { + return askQuestionText; + } else if (args.ITEM === "ask prompt input border") { + return askInputBorder; + } else if (args.ITEM === "monitor background border width") { + return monitorBackgroundBorderWidth; + } else if (args.ITEM === "ask prompt background border width") { + return askBackgroundBorderWidth; + } else if (args.ITEM === "ask prompt input border width") { + return askInputBorderWidth; + } else if (args.ITEM === "monitor background roundness") { + return monitorBackgroundRoundness; + } else if (args.ITEM === "variable value roundness") { + return variableValueRoundness; + } else if (args.ITEM === "list value roundness") { + return listValueRoundness; + } else if (args.ITEM === "ask prompt background roundness") { + return askBackgroundRoundness; + } else if (args.ITEM === "ask prompt button roundness") { + return askButtonRoundness; + } else if (args.ITEM === "ask prompt input roundness") { + return askInputRoundness; + } else if (args.ITEM === "ask prompt button image") { + return askButtonImage; + } else if (args.ITEM === "list scrolling") { + if (allowScrolling === "auto") { + return "enabled"; + } else { + return "disabled"; + } + } + return ""; + } + + setAskURI(args) { + return this.runtime.vm.securityManager.canFetch(args.URL).then((allowed) => { + if (allowed) { + askButtonImage = args.URL; + applyCSS(); + } + }); + } +} + +module.exports = MonitorStyles; diff --git a/local-scratch-vm/src/extensions/theshovel_lzString/index.js b/local-scratch-vm/src/extensions/theshovel_lzString/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c8370a2f1aa165e462a6e8f8f81306e68ea6b995 --- /dev/null +++ b/local-scratch-vm/src/extensions/theshovel_lzString/index.js @@ -0,0 +1,110 @@ +// Created by TheShovel +// https://github.com/TheShovel +// +// 99% of the code here was not created by a PenguinMod developer! +// Look above for proper crediting :) + +const ArgumentType = require("../../extension-support/argument-type"); +const BlockType = require("../../extension-support/block-type"); +const { validateArray } = require('../../util/json-block-utilities'); +const ArrayBufferUtil = require('../../util/array buffer'); +const BufferParser = new ArrayBufferUtil(); +const Cast = require("../../util/cast"); +const LZString = require('lz-string'); + +class lzcompress { + getInfo() { + return { + id: "shovellzcompresss", + name: "LZ Compress", + blocks: [ + { + opcode: "compress", + blockType: BlockType.REPORTER, + text: "compress [TEXT] to [TYPE]", + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "Hello world!", + }, + TYPE: { + type: ArgumentType.STRING, + menu: "COMPRESSIONTYPES", + }, + }, + }, + { + opcode: "decompress", + blockType: BlockType.REPORTER, + text: "decompress [TEXT] from [TYPE]", + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "҅〶惶@✰Ӏ葀", + }, + TYPE: { + type: ArgumentType.STRING, + menu: "COMPRESSIONTYPES", + }, + }, + }, + ], + menus: { + COMPRESSIONTYPES: { + acceptReporters: true, + items: [ + "Raw", + "Base64", + "EncodedURIComponent", + "ArrayBuffer", + "UTF16", + ], + }, + }, + }; + } + compress(args) { + const text = Cast.toString(args.TEXT); + if (args.TYPE == "Raw") { + return LZString.compress(text); + } else if (args.TYPE == "Base64") { + return LZString.compressToBase64(text); + } else if (args.TYPE == "EncodedURIComponent") { + return LZString.compressToEncodedURIComponent(text); + } else if (args.TYPE == "ArrayBuffer") { + const uint8Array = LZString.compressToUint8Array(text); + const buffer = BufferParser.uint8ArrayToBuffer(uint8Array); + const array = BufferParser.bufferToArray(buffer); + return JSON.stringify(array); + } else if (args.TYPE == "UTF16") { + return LZString.compressToUTF16(text); + } + return ""; + } + + decompress(args) { + try { + const text = Cast.toString(args.TEXT); + if (args.TYPE == "Raw") { + return LZString.decompress(text) || ""; + } else if (args.TYPE == "Base64") { + return LZString.decompressFromBase64(text) || ""; + } else if (args.TYPE == "EncodedURIComponent") { + return LZString.decompressFromEncodedURIComponent(text) || ""; + } else if (args.TYPE == "ArrayBuffer") { + const array = validateArray(text); + if (!array.isValid) return ""; + const buffer = BufferParser.arrayToBuffer(array.array); + const uint8Array = BufferParser.bufferToUint8Array(buffer); + return LZString.decompressFromUint8Array(uint8Array) || ""; + } else if (args.TYPE == "UTF16") { + return LZString.decompressFromUTF16(text) || ""; + } + } catch (e) { + console.error("decompress error", e); + } + return ""; + } +} + +module.exports = lzcompress; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/theshovel_profanity/index.js b/local-scratch-vm/src/extensions/theshovel_profanity/index.js new file mode 100644 index 0000000000000000000000000000000000000000..be87c04e28ad1f05492c09f16e55744102c33e94 --- /dev/null +++ b/local-scratch-vm/src/extensions/theshovel_profanity/index.js @@ -0,0 +1,41 @@ +// Created by TheShovel +// https://github.com/TheShovel +// +// 99% of the code here was not created by a PenguinMod developer! +// Look above for proper crediting :) + +const ArgumentType = require("../../extension-support/argument-type"); +const BlockType = require("../../extension-support/block-type"); +const Cast = require("../../util/cast"); + +class profanityAPI { + getInfo() { + return { + id: "profanityAPI", + name: "Censorship", + blocks: [ + { + opcode: "checkProfanity", + blockType: BlockType.REPORTER, + disableMonitor: false, + text: "remove profanity from [TEXT]", + arguments: { + TEXT: { + type: ArgumentType.STRING, + defaultValue: "Hello, I love pizza!", + }, + }, + }, + ], + }; + } + + checkProfanity({ TEXT }) { + const text = encodeURIComponent(Cast.toString(TEXT)); + return fetch(`https://www.purgomalum.com/service/plain?text=${text}`) + .then((r) => r.text()) + .catch(() => ""); + } +} + +module.exports = profanityAPI; \ No newline at end of file diff --git a/local-scratch-vm/src/extensions/tw/index.js b/local-scratch-vm/src/extensions/tw/index.js new file mode 100644 index 0000000000000000000000000000000000000000..60f2174575ea273690871d5ea45392bdf44b760a --- /dev/null +++ b/local-scratch-vm/src/extensions/tw/index.js @@ -0,0 +1,107 @@ +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); + +// eslint-disable-next-line max-len +const iconURI = `data:image/svg+xml;base64,${btoa('')}`; + +/** + * Class for TurboWarp blocks + * @deprecated Blocks have been moved to Sensing Expansion + * @constructor + */ +class TurboWarpBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'tw', + name: 'TurboWarp', + color1: '#ff4c4c', + color2: '#e64444', + color3: '#c73a3a', + docsURI: 'https://docs.turbowarp.org/blocks', + menuIconURI: iconURI, + blockIconURI: iconURI, + blocks: [ + { + opcode: 'getLastKeyPressed', + text: formatMessage({ + id: 'tw.blocks.lastKeyPressed', + default: 'last key pressed', + description: 'Block that returns the last key that was pressed' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getButtonIsDown', + text: formatMessage({ + id: 'tw.blocks.buttonIsDown', + default: '[MOUSE_BUTTON] mouse button down?', + description: 'Block that returns whether a specific mouse button is down' + }), + blockType: BlockType.BOOLEAN, + arguments: { + MOUSE_BUTTON: { + type: ArgumentType.NUMBER, + menu: 'mouseButton', + defaultValue: '0' + } + } + } + ], + menus: { + mouseButton: { + items: [ + { + text: formatMessage({ + id: 'tw.blocks.mouseButton.primary', + default: '(0) primary', + description: 'Dropdown item to select primary (usually left) mouse button' + }), + value: '0' + }, + { + text: formatMessage({ + id: 'tw.blocks.mouseButton.middle', + default: '(1) middle', + description: 'Dropdown item to select middle mouse button' + }), + value: '1' + }, + { + text: formatMessage({ + id: 'tw.blocks.mouseButton.secondary', + default: '(2) secondary', + description: 'Dropdown item to select secondary (usually right) mouse button' + }), + value: '2' + } + ], + acceptReporters: true + } + } + }; + } + + getLastKeyPressed (args, util) { + return util.ioQuery('keyboard', 'getLastKeyPressed'); + } + + getButtonIsDown (args, util) { + const button = Cast.toNumber(args.MOUSE_BUTTON); + return util.ioQuery('mouse', 'getButtonIsDown', [button]); + } +} + +module.exports = TurboWarpBlocks; diff --git a/local-scratch-vm/src/extensions/tw_files/index.js b/local-scratch-vm/src/extensions/tw_files/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fa908ba28fbf03b9643a55b3f949a0d34920c466 --- /dev/null +++ b/local-scratch-vm/src/extensions/tw_files/index.js @@ -0,0 +1,380 @@ +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const { validateArray } = require('../../util/json-block-utilities'); +const AHHHHHHHHHHHHHH = require('../../util/array buffer'); +const BufferStuff = new AHHHHHHHHHHHHHH(); + +const MODE_MODAL = 'modal'; +const MODE_IMMEDIATELY_SHOW_SELECTOR = 'selector'; +const MODE_ONLY_SELECTOR = 'only-selector'; +const ALL_MODES = [MODE_MODAL, MODE_IMMEDIATELY_SHOW_SELECTOR, MODE_ONLY_SELECTOR]; +let openFileSelectorMode = MODE_MODAL; +let lastOpenedFileName = ''; + +const AS_TEXT = 'text'; +const AS_BUFFER = 'buffer'; +const AS_DATA_URL = 'url'; + +/** + * @param {string} accept See MODE_ constants above + * @param {string} as See AS_ constants above + * @returns {Promise} format given by as parameter + */ +const showFilePrompt = (accept, as) => new Promise((_resolve) => { + // We can't reliably show an picker without "user interaction" in all environments, + // so we have to show our own UI anyways. We may as well use this to implement some nice features + // that native file pickers don't have: + // - Easy drag+drop + // - Reliable cancel button (input cancel event is still basically nonexistent) + // This is important so we can make this just a reporter instead of a command+hat block. + // Without an interface, the script would be stalled if the prompt was just cancelled. + + /** @param {string} text */ + const callback = (text) => { + _resolve(text); + outer.remove(); + document.body.removeEventListener('keydown', handleKeyDown); + }; + + let isReadingFile = false; + let isReadingAsBuffer = false; + + /** @param {File} file */ + const readFile = (file) => { + if (isReadingFile) { + return; + } + isReadingFile = true; + + const reader = new FileReader(); + reader.onload = () => { + if (isReadingAsBuffer) { + callback(/** @type {string} */(JSON.stringify(BufferStuff.bufferToArray(reader.result)))); + return; + } + callback(/** @type {string} */(reader.result)); + }; + reader.onerror = () => { + console.error('Failed to read file as text', reader.error); + callback(''); + }; + if (as === AS_DATA_URL) { + reader.readAsDataURL(file); + } else if (as === AS_BUFFER) { + isReadingAsBuffer = true; + reader.readAsArrayBuffer(file); + } else { + reader.readAsText(file); + } + }; + + /** @param {KeyboardEvent} e */ + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + callback(''); + } + }; + document.body.addEventListener('keydown', handleKeyDown, { + capture: true + }); + + const INITIAL_BORDER_COLOR = '#888'; + const DROPPING_BORDER_COLOR = '#03a9fc'; + + const outer = document.createElement('div'); + outer.className = 'extension-content'; + outer.style.position = 'fixed'; + outer.style.top = '0'; + outer.style.left = '0'; + outer.style.width = '100%'; + outer.style.height = '100%'; + outer.style.display = 'flex'; + outer.style.alignItems = 'center'; + outer.style.justifyContent = 'center'; + outer.style.background = 'rgba(0, 0, 0, 0.5)'; + outer.style.zIndex = '20000'; + outer.style.color = 'black'; + outer.style.colorScheme = 'light'; + outer.addEventListener('dragover', (e) => { + if (e.dataTransfer.types.includes('Files')) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + modal.style.borderColor = DROPPING_BORDER_COLOR; + } + }); + outer.addEventListener('dragleave', () => { + modal.style.borderColor = INITIAL_BORDER_COLOR; + }); + outer.addEventListener('drop', (e) => { + const file = e.dataTransfer.files[0]; + if (file) { + lastOpenedFileName = file.name; + e.preventDefault(); + readFile(file); + } + }); + outer.addEventListener('click', (e) => { + if (e.target === outer) { + callback(''); + } + }); + + const modal = document.createElement('button'); + modal.style.boxShadow = '0 0 10px -5px currentColor'; + modal.style.cursor = 'pointer'; + modal.style.font = 'inherit'; + modal.style.background = 'white'; + modal.style.padding = '16px'; + modal.style.borderRadius = '16px'; + modal.style.border = `8px dashed ${INITIAL_BORDER_COLOR}`; + modal.style.position = 'relative'; + modal.style.textAlign = 'center'; + modal.addEventListener('click', () => { + input.click(); + }); + modal.focus(); + outer.appendChild(modal); + + const input = document.createElement('input'); + input.type = 'file'; + input.accept = accept; + input.addEventListener('change', (e) => { + // @ts-expect-error + const file = e.target.files[0]; + if (file) { + lastOpenedFileName = file.name; + readFile(file); + } + }); + + const title = document.createElement('div'); + title.textContent = 'Select or drop file'; + title.style.fontSize = '1.5em'; + title.style.marginBottom = '8px'; + modal.appendChild(title); + + const subtitle = document.createElement('div'); + const formattedAccept = accept || 'any'; + subtitle.textContent = `Accepted formats: ${formattedAccept}`; + modal.appendChild(subtitle); + + document.body.appendChild(outer); + + if (openFileSelectorMode === MODE_IMMEDIATELY_SHOW_SELECTOR || openFileSelectorMode === MODE_ONLY_SELECTOR) { + input.click(); + } + + if (openFileSelectorMode === MODE_ONLY_SELECTOR) { + // Note that browser support for cancel is currently quite bad + input.addEventListener('cancel', () => { + callback(''); + }); + outer.remove(); + } +}); + +const dataURLtoBlob = (url) => { + var arr = url.split(','), mime = arr[0].match(/:(.*?);/)[1], + bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + return new Blob([u8arr], { type: mime }); +} + +/** + * @param {string} text Text to download + * @param {string} file Name of the file + */ +const download = (text, file, encoding) => { + let _blob; + if (encoding === AS_DATA_URL) { + _blob = dataURLtoBlob(text); + } else if (encoding === AS_BUFFER) { + const array = validateArray(text); + if (array.isValid) { + text = BufferStuff.arrayToBuffer(array.array); + } + _blob = new Blob([text]); + } else { + _blob = new Blob([text]); + } + const blob = _blob; + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = file; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); +}; + +class Files { + getInfo() { + return { + id: 'twFiles', + name: 'Files', + color1: '#fcb103', + color2: '#db9a37', + color3: '#db8937', + blocks: [ + { + opcode: 'showPicker', + blockType: BlockType.REPORTER, + text: 'open a file', + disableMonitor: true, + hideFromPalette: true + }, + { + opcode: 'showPickerExtensions', + blockType: BlockType.REPORTER, + text: 'open a [extension] file', + arguments: { + extension: { + type: ArgumentType.STRING, + defaultValue: '.txt' + } + }, + hideFromPalette: true + }, + + { + opcode: 'showPickerAs', + blockType: BlockType.REPORTER, + text: 'open a file as [as]', + arguments: { + as: { + type: ArgumentType.STRING, + menu: 'encoding' + } + } + }, + { + opcode: 'showPickerExtensionsAs', + blockType: BlockType.REPORTER, + text: 'open a [extension] file as [as]', + arguments: { + extension: { + type: ArgumentType.STRING, + defaultValue: '.txt' + }, + as: { + type: ArgumentType.STRING, + menu: 'encoding' + } + } + }, + + '---', + + { + opcode: 'download', + blockType: BlockType.COMMAND, + text: 'download [encoding] [text] as [file]', + arguments: { + encoding: { + type: ArgumentType.STRING, + menu: 'encoding' + }, + text: { + type: ArgumentType.STRING, + defaultValue: 'Hello, world!' + }, + file: { + type: ArgumentType.STRING, + defaultValue: 'save.txt' + } + } + }, + { + opcode: 'setOpenMode', + blockType: BlockType.COMMAND, + text: 'set open file selector mode to [mode]', + arguments: { + mode: { + type: ArgumentType.STRING, + defaultValue: MODE_MODAL, + menu: 'automaticallyOpen' + } + } + }, + '---', + { + opcode: 'getFileName', + blockType: BlockType.REPORTER, + text: 'last opened file name', + disableMonitor: true + } + ], + menus: { + encoding: { + acceptReporters: true, + items: [ + { + text: 'text', + value: AS_TEXT + }, + { + text: 'data: URL', + value: AS_DATA_URL + }, + { + text: 'array buffer', + value: AS_BUFFER + } + ] + }, + automaticallyOpen: { + acceptReporters: true, + items: [ + { + text: 'show modal', + value: MODE_MODAL + }, + { + text: 'open selector immediately', + value: MODE_IMMEDIATELY_SHOW_SELECTOR + } + ] + } + } + }; + } + + showPicker() { + return showFilePrompt('', AS_TEXT); + } + + showPickerExtensions(args) { + return showFilePrompt(args.extension, AS_TEXT); + } + + showPickerAs(args) { + return showFilePrompt('', args.as); + } + + showPickerExtensionsAs(args) { + return showFilePrompt(args.extension, args.as); + } + + download(args) { + download(args.text, args.file, args.encoding); + } + + setOpenMode(args) { + if (ALL_MODES.includes(args.mode)) { + openFileSelectorMode = args.mode; + } else { + console.warn(`unknown mode`, args.mode); + } + } + + getFileName() { + return lastOpenedFileName || ''; + } +} + +module.exports = Files; diff --git a/local-scratch-vm/src/extensions/xeltalliv_clippingblending/index.js b/local-scratch-vm/src/extensions/xeltalliv_clippingblending/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2404aa086bb00995cd1f0e6c03cd68cb25cde952 --- /dev/null +++ b/local-scratch-vm/src/extensions/xeltalliv_clippingblending/index.js @@ -0,0 +1,449 @@ +const ExtensionApi = require("../../util/custom-ext-api-to-core.js"); +const Scratch = new ExtensionApi(true); + +// Simplified remake of an icon by True-Fantom +const icon = 'data:image/svg+xml,' + encodeURIComponent(` + + + + + + + + + `); + + +let toCorrectThing = null; +let active = false; +let flipY = false; +const vm = Scratch.vm; +const runtime = vm.runtime; +const renderer = vm.renderer; +const _drawThese = renderer._drawThese; +const gl = renderer._gl; +const canvas = renderer.canvas; +let width = 0; +let height = 0; +let scratchUnitWidth = 480; +let scratchUnitHeight = 360; +let penDirty = false; + + +renderer._drawThese = function (drawables, drawMode, projection, opts) { + active = true; + [scratchUnitWidth, scratchUnitHeight] = renderer.getNativeSize(); + _drawThese.call(this, drawables, drawMode, projection, opts); + gl.disable(gl.SCISSOR_TEST); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + active = false; +}; + +const bfb = gl.bindFramebuffer; +gl.bindFramebuffer = function (target, framebuffer) { + if (target == gl.FRAMEBUFFER) { + if (framebuffer == null) { + toCorrectThing = true; + flipY = false; + width = canvas.width; + height = canvas.height; + } else if (renderer._penSkinId) { + const fbInfo = renderer._allSkins[renderer._penSkinId]._framebuffer; + if (framebuffer == fbInfo.framebuffer) { + toCorrectThing = true; + flipY = true; + width = fbInfo.width; + height = fbInfo.height; + } else { + toCorrectThing = false; + } + } else { + toCorrectThing = false; + } + } + bfb.call(this, target, framebuffer); +}; + +// Getting Drawable +const dr = renderer.createDrawable('background'); +const DrawableProto = renderer._allDrawables[dr].__proto__; +renderer.destroyDrawable(dr, 'background'); + +function setupModes(clipbox, blendMode, flipY) { + if (clipbox) { + gl.enable(gl.SCISSOR_TEST); + let x = (clipbox.x_min / scratchUnitWidth + 0.5) * width | 0; + let y = (clipbox.y_min / scratchUnitHeight + 0.5) * height | 0; + let x2 = (clipbox.x_max / scratchUnitWidth + 0.5) * width | 0; + let y2 = (clipbox.y_max / scratchUnitHeight + 0.5) * height | 0; + let w = x2 - x; + let h = y2 - y; + if (flipY) { + y = (-(clipbox.y_max) / scratchUnitHeight + 0.5) * height | 0; + } + gl.scissor(x, y, w, h); + } else { + gl.disable(gl.SCISSOR_TEST); + } + switch (blendMode) { + case 'additive': + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ONE); + break; + case 'subtract': + gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT); + gl.blendFunc(gl.ONE, gl.ONE); + break; + case 'multiply': + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA); + break; + case 'invert': + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE_MINUS_DST_COLOR, gl.ONE_MINUS_SRC_COLOR); + break; + default: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + } +} + +// Modifying and expanding Drawable +const gu = DrawableProto.getUniforms; +DrawableProto.getUniforms = function () { + if (active && toCorrectThing) { + setupModes(this.clipbox, this.blendMode, flipY); + } + return gu.call(this); +}; +DrawableProto.updateClipBox = function (clipbox) { + this.clipbox = clipbox; +}; +DrawableProto.updateBlendMode = function (blendMode) { + this.blendMode = blendMode; +}; + + +// Expanding renderer +renderer.updateDrawableClipBox = function (drawableID, clipbox) { + const drawable = this._allDrawables[drawableID]; + if (!drawable) return; + drawable.updateClipBox(clipbox); +}; +renderer.updateDrawableBlendMode = function (drawableID, blendMode) { + const drawable = this._allDrawables[drawableID]; + if (!drawable) return; + drawable.updateBlendMode(blendMode); +}; + + +// Reset on stop & clones inherit effects +const regTargetStuff = function (args) { + if (args.editingTarget) { + vm.removeListener('targetsUpdate', regTargetStuff); + const proto = vm.runtime.targets[0].__proto__; + const osa = proto.onStopAll; + proto.onStopAll = function () { + this.renderer.updateDrawableClipBox.call(renderer, this.drawableID, null); + this.renderer.updateDrawableBlendMode.call(renderer, this.drawableID, null); + osa.call(this); + }; + const mc = proto.makeClone; + proto.makeClone = function () { + const newTarget = mc.call(this); + if (this.clipbox || this.blendMode) { + newTarget.clipbox = Object.assign({}, this.clipbox); + newTarget.blendMode = this.blendMode; + renderer.updateDrawableClipBox.call(renderer, newTarget.drawableID, this.clipbox); + renderer.updateDrawableBlendMode.call(renderer, newTarget.drawableID, this.blendMode); + } + return newTarget; + }; + } +}; +vm.on('targetsUpdate', regTargetStuff); + + +// Pen lines support +let emptyObject = {}; +let lastTarget = emptyObject; +let lastClipbox = {}; +let lastBlendMode = "default"; +function patchPen(skin) { + const ext_pen = runtime.ext_pen; + skin._lineOnBufferDrawRegionId.exit = () => { + skin._exitDrawLineOnBuffer(); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.disable(gl.SCISSOR_TEST); + lastTarget = emptyObject; + lastClipbox = null; + lastBlendMode = "default"; + }; + const willDrawPenWithTarget = function (target) { + if (!penDirty && target == lastTarget) return; + penDirty = false; + + const clipbox = target.clipbox; + if ((!lastClipbox ^ !clipbox) || + (lastBlendMode != target.blendMode) || + (clipbox && (clipbox.x_min != lastClipbox.x_min || clipbox.y_min != lastClipbox.y_min || clipbox.x_max != lastClipbox.x_max || clipbox.y_max != lastClipbox.y_max))) { + if (skin.a_lineColorIndex) { + skin._flushLines(); + } + lastTarget = target; + if (clipbox) { + lastClipbox = { + x_min: clipbox.x_min, + y_min: clipbox.y_min, + x_max: clipbox.x_max, + y_max: clipbox.y_max + }; + } else { + lastClipbox = null; + } + lastBlendMode = target.blendMode; + } + }; + // onTargetMoved function of pen draws a line. + // When drawing a line it is important to know the target. + // This saves target. + const onTargetMoved = ext_pen._onTargetMoved; + ext_pen._onTargetMoved = function (target, oldX, oldY, isForce) { + willDrawPenWithTarget(target); + onTargetMoved.call(this, target, oldX, oldY, isForce); + }; + // Existing tragets may still have old onTargetMoved + for (let target in runtime.tragets) { + if (target.onTargetMoved == onTargetMoved) { + target.onTargetMoved = ext_pen._onTargetMoved; + } + } + // When drawing a dot it is important to know the target. + // This saves target. + const penDown = ext_pen._penDown; + ext_pen._penDown = function (target) { + willDrawPenWithTarget(target); + penDown.call(this, target); + }; + // Set up correct clipping/blending before drawing + const flushLines = skin.__proto__._flushLines; + skin.__proto__._flushLines = function () { + setupModes(lastClipbox, lastBlendMode, true); + flushLines.call(this); + }; +} +if (renderer._allSkins[renderer._penSkinId]) { + // If pen skin already exists, things can be patched + patchPen(renderer._allSkins[renderer._penSkinId]); +} else { + // If pen skin does not exist, wait until it will, + // trigger code once, and return everything as it was + const createPenSkin = renderer.createPenSkin; + renderer.createPenSkin = function () { + let skinId = createPenSkin.call(this); + patchPen(renderer._allSkins[skinId]); + renderer.createPenSkin = createPenSkin; + return skinId; + }; +} + +class Extension { + getInfo() { + return { + id: 'xeltallivclipblend', + name: 'Clipping and Blending', + color1: '#9966FF', + color2: '#855CD6', + color3: '#774DCB', + menuIconURI: icon, + blocks: [ + { + opcode: 'setClipbox', + blockType: Scratch.BlockType.COMMAND, + text: 'set clipping box x1:[X1] y1:[Y1] x2:[X2] y2:[Y2]', + arguments: { + X1: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0' + }, + Y1: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0' + }, + X2: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '100' + }, + Y2: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '100' + } + }, + filter: [Scratch.TargetType.SPRITE] + }, + { + opcode: 'clearClipbox', + blockType: Scratch.BlockType.COMMAND, + text: 'clear clipping box', + filter: [Scratch.TargetType.SPRITE] + }, + { + opcode: 'getClipbox', + blockType: Scratch.BlockType.REPORTER, + text: 'clipping box [PROP]', + arguments: { + PROP: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'width', + menu: 'props' + } + }, + filter: [Scratch.TargetType.SPRITE] + }, + '---', + { + opcode: 'setBlend', + blockType: Scratch.BlockType.COMMAND, + text: 'use [BLENDMODE] blending ', + arguments: { + BLENDMODE: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'default', + menu: 'blends' + } + }, + filter: [Scratch.TargetType.SPRITE] + }, + { + opcode: 'getBlend', + blockType: Scratch.BlockType.REPORTER, + text: 'blending', + filter: [Scratch.TargetType.SPRITE], + disableMonitor: true + }, + '---', + { + opcode: 'setAdditiveBlend', + blockType: Scratch.BlockType.COMMAND, + text: 'turn additive blending [STATE]', + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'on', + menu: 'states' + } + }, + filter: [Scratch.TargetType.SPRITE], + hideFromPalette: true + }, + { + opcode: 'getAdditiveBlend', + blockType: Scratch.BlockType.BOOLEAN, + text: 'is additive blending on?', + filter: [Scratch.TargetType.SPRITE], + hideFromPalette: true + }, + ], + menus: { + states: { + acceptReporters: true, + items: ['on', 'off'] + }, + blends: { + acceptReporters: true, + items: ['default', 'additive', 'subtract', 'multiply', 'invert'] + }, + props: { + acceptReporters: true, + items: ['width', 'height', 'min x', 'min y', 'max x', 'max y'] + }, + } + }; + } + + setClipbox({ X1, Y1, X2, Y2 }, { target }) { + if (target.isStage) return; + const newClipbox = { + x_min: Math.min(X1, X2), + y_min: Math.min(Y1, Y2), + x_max: Math.max(X1, X2), + y_max: Math.max(Y1, Y2) + }; + penDirty = true; + target.clipbox = newClipbox; + renderer.updateDrawableClipBox.call(renderer, target.drawableID, newClipbox); + if (target.visible) { + renderer.dirty = true; + target.emitVisualChange(); + target.runtime.requestRedraw(); + target.runtime.requestTargetsUpdate(target); + } + } + + clearClipbox(args, { target }) { + if (target.isStage) return; + target.clipbox = null; + penDirty = true; + renderer.updateDrawableClipBox.call(renderer, target.drawableID, null); + if (target.visible) { + renderer.dirty = true; + target.emitVisualChange(); + target.runtime.requestRedraw(); + target.runtime.requestTargetsUpdate(target); + } + } + + getClipbox({ PROP }, { target }) { + const clipbox = target.clipbox; + if (!clipbox) return ''; + switch (PROP) { + case 'width': return clipbox.x_max - clipbox.x_min; + case 'height': return clipbox.y_max - clipbox.y_min; + case 'min x': return clipbox.x_min; + case 'min y': return clipbox.y_min; + case 'max x': return clipbox.x_max; + case 'max y': return clipbox.y_max; + default: return ''; + } + } + + setBlend({ BLENDMODE }, { target }) { + let newValue = null; + switch (BLENDMODE) { + case 'default': + case 'additive': + case 'subtract': + case 'multiply': + case 'invert': + newValue = BLENDMODE; + break; + default: + return; + } + if (target.isStage) return; + penDirty = true; + target.blendMode = newValue; + renderer.updateDrawableBlendMode.call(renderer, target.drawableID, newValue); + if (target.visible) { + renderer.dirty = true; + target.emitVisualChange(); + target.runtime.requestRedraw(); + target.runtime.requestTargetsUpdate(target); + } + } + + getBlend(args, { target }) { + return target.blendMode ?? 'default'; + } + + setAdditiveBlend({ STATE }, util) { + if (STATE === 'on') this.setBlend({ BLENDMODE: 'additive' }, util); + if (STATE === 'off') this.setBlend({ BLENDMODE: 'default' }, util); + } + + getAdditiveBlend(args, { target }) { + return target.blendMode === 'additive'; + } +} + +module.exports = Extension \ No newline at end of file diff --git a/local-scratch-vm/src/import/load-costume.js b/local-scratch-vm/src/import/load-costume.js new file mode 100644 index 0000000000000000000000000000000000000000..bb7119b65989604c3e01d36fd32f50354d8880ac --- /dev/null +++ b/local-scratch-vm/src/import/load-costume.js @@ -0,0 +1,495 @@ +const StringUtil = require('../util/string-util'); +const log = require('../util/log'); +const AsyncLimiter = require('../util/async-limiter'); +const {loadSvgString, serializeSvgToString} = require('scratch-svg-renderer'); +const {parseVectorMetadata} = require('../serialization/tw-costume-import-export'); + +const loadVector_ = function (costume, runtime, rotationCenter, optVersion) { + return new Promise(resolve => { + let svgString = costume.asset.decodeText(); + + // TW: We allow SVGs to specify their rotation center using a special comment. + if (typeof rotationCenter === 'undefined') { + const parsedRotationCenter = parseVectorMetadata(svgString); + if (parsedRotationCenter) { + rotationCenter = parsedRotationCenter; + costume.rotationCenterX = rotationCenter[0]; + costume.rotationCenterY = rotationCenter[1]; + } + } + + // SVG Renderer load fixes "quirks" associated with Scratch 2 projects + if (optVersion && optVersion === 2) { + // scratch-svg-renderer fixes syntax that causes loading issues, + // and if optVersion is 2, fixes "quirks" associated with Scratch 2 SVGs, + const fixedSvgString = serializeSvgToString(loadSvgString(svgString, true /* fromVersion2 */)); + + // If the string changed, put back into storage + if (svgString !== fixedSvgString) { + svgString = fixedSvgString; + const storage = runtime.storage; + costume.asset.encodeTextData(fixedSvgString, storage.DataFormat.SVG, true); + costume.assetId = costume.asset.assetId; + costume.md5 = `${costume.assetId}.${costume.dataFormat}`; + } + } + + // createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's + // undefined here + costume.skinId = runtime.renderer.createSVGSkin(svgString, rotationCenter); + costume.size = runtime.renderer.getSkinSize(costume.skinId); + // Now we should have a rotationCenter even if we didn't before + if (!rotationCenter) { + rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId); + costume.rotationCenterX = rotationCenter[0]; + costume.rotationCenterY = rotationCenter[1]; + costume.bitmapResolution = 1; + } + + if (runtime.isPackaged) { + costume.asset = null; + } + + resolve(costume); + }); +}; + +const canvasPool = (function () { + /** + * A pool of canvas objects that can be reused to reduce memory + * allocations. And time spent in those allocations and the later garbage + * collection. + */ + class CanvasPool { + constructor () { + this.pool = []; + this.clearSoon = null; + } + + /** + * After a short wait period clear the pool to let the VM collect + * garbage. + */ + clear () { + if (!this.clearSoon) { + this.clearSoon = new Promise(resolve => setTimeout(resolve, 1000)) + .then(() => { + this.pool.length = 0; + this.clearSoon = null; + }); + } + } + + /** + * Return a canvas. Create the canvas if the pool is empty. + * @returns {HTMLCanvasElement} A canvas element. + */ + create () { + return this.pool.pop() || document.createElement('canvas'); + } + + /** + * Release the canvas to be reused. + * @param {HTMLCanvasElement} canvas A canvas element. + */ + release (canvas) { + this.clear(); + this.pool.push(canvas); + } + } + + return new CanvasPool(); +}()); + +/** + * @param {string} src URL of image + * @returns {Promise} + */ +const readAsImageElement = src => new Promise((resolve, reject) => { + const image = new Image(); + image.onload = function () { + resolve(image); + image.onload = null; + image.onerror = null; + }; + image.onerror = function () { + reject(new Error('Costume load failed. Asset could not be read.')); + image.onload = null; + image.onerror = null; + }; + image.src = src; +}); + +/** + * @param {Asset} asset scratch-storage asset + * @returns {Promise} + */ +const _persistentReadImage = async asset => { + // Sometimes, when a lot of images are loaded at once, especially in Chrome, reading an image + // can throw an error even on valid images. To mitigate this, we'll retry image reading a few + // time with delays. + let firstError; + for (let i = 0; i < 3; i++) { + try { + if (typeof createImageBitmap === 'function') { + const imageBitmap = await createImageBitmap( + new Blob([asset.data.buffer], {type: asset.assetType.contentType}) + ); + // If we do too many createImageBitmap at the same time, some browsers (Chrome) will + // sometimes resolve with undefined. We limit concurrency so this shouldn't ever + // happen, but if it somehow does, throw an error so it can be retried or so that it + // falls back to scratch's broken costume handling. + if (!imageBitmap) { + throw new Error(`createImageBitmap resolved with ${imageBitmap}`); + } + return imageBitmap; + } + return await readAsImageElement(asset.encodeDataURI()); + } catch (e) { + if (!firstError) { + firstError = e; + } + log.warn(e); + await new Promise(resolve => setTimeout(resolve, Math.random() * 2000)); + } + } + throw firstError; +}; + +// Browsers break when we do too many createImageBitmap at the same time. +const readImage = new AsyncLimiter(_persistentReadImage, 25); + +/** + * Return a promise to fetch a bitmap from storage and return it as a canvas + * If the costume has bitmapResolution 1, it will be converted to bitmapResolution 2 here (the standard for Scratch 3) + * If the costume has a text layer asset, which is a text part from Scratch 1.4, then this function + * will merge the two image assets. See the issue LLK/scratch-vm#672 for more information. + * @param {!object} costume - the Scratch costume object. + * @param {!Runtime} runtime - Scratch runtime, used to access the v2BitmapAdapter + * @param {?object} rotationCenter - optionally passed in coordinates for the center of rotation for the image. If + * none is given, the rotation center of the costume will be set to the middle of the costume later on. + * @property {number} costume.bitmapResolution - the resolution scale for a bitmap costume. + * @returns {?Promise} - a promise which will resolve to an object {canvas, rotationCenter, assetMatchesBase}, + * or reject on error. + * assetMatchesBase is true if the asset matches the base layer; false if it required adjustment + */ +const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) { + if (!costume || !costume.asset) { // TODO: We can probably remove this check... + return Promise.reject('Costume load failed. Assets were missing.'); + } + if (!runtime.v2BitmapAdapter) { + return Promise.reject('No V2 Bitmap adapter present.'); + } + + return Promise.all([costume.asset, costume.textLayerAsset].map(asset => { + if (!asset) { + return null; + } + + return readImage.do(asset); + })) + .then(([baseImageElement, textImageElement]) => { + if (!baseImageElement) { + throw new Error('Loading bitmap costume base failed.'); + } + + const scale = costume.bitmapResolution === 1 ? 2 : 1; + + let imageOrCanvas; + let canvas; + if (textImageElement) { + canvas = canvasPool.create(); + canvas.width = baseImageElement.width; + canvas.height = baseImageElement.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(baseImageElement, 0, 0); + ctx.drawImage(textImageElement, 0, 0); + imageOrCanvas = canvas; + } else { + imageOrCanvas = baseImageElement; + } + if (scale !== 1) { + // resize() returns a new canvas. + imageOrCanvas = runtime.v2BitmapAdapter.resize( + imageOrCanvas, + imageOrCanvas.width * scale, + imageOrCanvas.height * scale + ); + // Old canvas is no longer used. + if (canvas) { + canvasPool.release(canvas); + } + } + + // This informs TurboWarp/scratch-render that this canvas won't be reused by the canvas pool, + // which helps it optimize memory use. + imageOrCanvas.reusable = false; + + // By scaling, we've converted it to bitmap resolution 2 + if (rotationCenter) { + rotationCenter[0] = rotationCenter[0] * scale; + rotationCenter[1] = rotationCenter[1] * scale; + costume.rotationCenterX = rotationCenter[0]; + costume.rotationCenterY = rotationCenter[1]; + } + costume.bitmapResolution = 2; + + // Clean up the costume object + delete costume.textLayerMD5; + delete costume.textLayerAsset; + + return { + image: imageOrCanvas, + rotationCenter, + // True if the asset matches the base layer; false if it required adjustment + assetMatchesBase: scale === 1 && !textImageElement + }; + }) + .finally(() => { + // Clean up the text layer properties if it fails to load + delete costume.textLayerMD5; + delete costume.textLayerAsset; + }); +}; + +const toDataURL = imageOrCanvas => { + if (imageOrCanvas instanceof HTMLCanvasElement) { + return imageOrCanvas.toDataURL(); + } + const canvas = canvasPool.create(); + canvas.width = imageOrCanvas.width; + canvas.height = imageOrCanvas.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(imageOrCanvas, 0, 0); + const url = canvas.toDataURL(); + canvasPool.release(canvas); + return url; +}; + +const loadBitmap_ = function (costume, runtime, _rotationCenter) { + return fetchBitmapCanvas_(costume, runtime, _rotationCenter) + .then(fetched => { + const updateCostumeAsset = function (dataURI) { + if (!runtime.v2BitmapAdapter) { + // TODO: This might be a bad practice since the returned + // promise isn't acted on. If this is something we should be + // creating a rejected promise for we should also catch it + // somewhere and act on that error (like logging). + // + // Return a rejection to stop executing updateCostumeAsset. + return Promise.reject('No V2 Bitmap adapter present.'); + } + + const storage = runtime.storage; + costume.asset = storage.createAsset( + storage.AssetType.ImageBitmap, + storage.DataFormat.PNG, + runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI), + null, + true // generate md5 + ); + costume.dataFormat = storage.DataFormat.PNG; + costume.assetId = costume.asset.assetId; + costume.md5 = `${costume.assetId}.${costume.dataFormat}`; + }; + + if (!fetched.assetMatchesBase) { + updateCostumeAsset(toDataURL(fetched.image)); + } + + return fetched; + }) + .then(({image, rotationCenter}) => { + // createBitmapSkin does the right thing if costume.rotationCenter is undefined. + // That will be the case if you upload a bitmap asset or create one by taking a photo. + let center; + if (rotationCenter) { + // fetchBitmapCanvas will ensure that the costume's bitmap resolution is 2 and its rotation center is + // scaled to match, so it's okay to always divide by 2. + center = [ + rotationCenter[0] / 2, + rotationCenter[1] / 2 + ]; + } + + // TODO: costume.bitmapResolution will always be 2 at this point because of fetchBitmapCanvas_, so we don't + // need to pass it in here. + costume.skinId = runtime.renderer.createBitmapSkin(image, costume.bitmapResolution, center); + const renderSize = runtime.renderer.getSkinSize(costume.skinId); + costume.size = [renderSize[0] * 2, renderSize[1] * 2]; // Actual size, since all bitmaps are resolution 2 + + if (!rotationCenter) { + rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId); + // Actual rotation center, since all bitmaps are resolution 2 + costume.rotationCenterX = rotationCenter[0] * 2; + costume.rotationCenterY = rotationCenter[1] * 2; + costume.bitmapResolution = 2; + } + + if (runtime.isPackaged) { + costume.asset = null; + } + + return costume; + }); +}; + +// Handle all manner of costume errors with a Gray Question Mark (default costume) +// and preserve as much of the original costume data as possible +// Returns a promise of a costume +const handleCostumeLoadError = function (costume, runtime) { + // Keep track of the old asset information until we're done loading the default costume + const oldAsset = costume.asset; // could be null + const oldAssetId = costume.assetId; + const oldRotationX = costume.rotationCenterX; + const oldRotationY = costume.rotationCenterY; + const oldBitmapResolution = costume.bitmapResolution; + const oldDataFormat = costume.dataFormat; + + const AssetType = runtime.storage.AssetType; + const isVector = costume.dataFormat === AssetType.ImageVector.runtimeFormat; + + // Use default asset if original fails to load + costume.assetId = isVector ? + runtime.storage.defaultAssetId.ImageVector : + runtime.storage.defaultAssetId.ImageBitmap; + costume.asset = runtime.storage.get(costume.assetId); + costume.md5 = `${costume.assetId}.${costume.asset.dataFormat}`; + + const defaultCostumePromise = (isVector) ? + loadVector_(costume, runtime) : loadBitmap_(costume, runtime); + + return defaultCostumePromise.then(loadedCostume => { + loadedCostume.broken = {}; + loadedCostume.broken.assetId = oldAssetId; + loadedCostume.broken.md5 = `${oldAssetId}.${oldDataFormat}`; + + // Should be null if we got here because the costume was missing + loadedCostume.broken.asset = oldAsset; + loadedCostume.broken.dataFormat = oldDataFormat; + + loadedCostume.broken.rotationCenterX = oldRotationX; + loadedCostume.broken.rotationCenterY = oldRotationY; + loadedCostume.broken.bitmapResolution = oldBitmapResolution; + return loadedCostume; + }); +}; + +/** + * Initialize a costume from an asset asynchronously. + * Do not call this unless there is a renderer attached. + * @param {!object} costume - the Scratch costume object. + * @property {int} skinId - the ID of the costume's render skin, once installed. + * @property {number} rotationCenterX - the X component of the costume's origin. + * @property {number} rotationCenterY - the Y component of the costume's origin. + * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume. + * @property {!Asset} costume.asset - the asset of the costume loaded from storage. + * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. + * @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set + * to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0. + * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error. + */ +const loadCostumeFromAsset = function (costume, runtime, optVersion) { + costume.assetId = costume.asset.assetId; + const renderer = runtime.renderer; + if (!renderer) { + log.warn('No rendering module present; cannot load costume: ', costume.name); + return Promise.resolve(costume); + } + const AssetType = runtime.storage.AssetType; + let rotationCenter; + // Use provided rotation center and resolution if they are defined. Bitmap resolution + // should only ever be 1 or 2. + if (typeof costume.rotationCenterX === 'number' && !isNaN(costume.rotationCenterX) && + typeof costume.rotationCenterY === 'number' && !isNaN(costume.rotationCenterY)) { + rotationCenter = [costume.rotationCenterX, costume.rotationCenterY]; + } + if (costume.asset.assetType.runtimeFormat === AssetType.ImageVector.runtimeFormat) { + return loadVector_(costume, runtime, rotationCenter, optVersion) + .catch(error => { + log.warn(`Error loading vector image: ${error}`); + return handleCostumeLoadError(costume, runtime); + + }); + } + return loadBitmap_(costume, runtime, rotationCenter, optVersion) + .catch(error => { + log.warn(`Error loading bitmap image: ${error}`); + return handleCostumeLoadError(costume, runtime); + }); +}; + + +/** + * Load a costume's asset into memory asynchronously. + * Do not call this unless there is a renderer attached. + * @param {!string} md5ext - the MD5 and extension of the costume to be loaded. + * @param {!object} costume - the Scratch costume object. + * @property {int} skinId - the ID of the costume's render skin, once installed. + * @property {number} rotationCenterX - the X component of the costume's origin. + * @property {number} rotationCenterY - the Y component of the costume's origin. + * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume. + * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. + * @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set + * to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0. + * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error. + */ +const loadCostume = function (md5ext, costume, runtime, optVersion) { + const idParts = StringUtil.splitFirst(md5ext, '.'); + const md5 = idParts[0]; + const ext = idParts[1].toLowerCase(); + costume.dataFormat = ext; + + if (costume.asset) { + // Costume comes with asset. It could be coming from image upload, drag and drop, or file + return loadCostumeFromAsset(costume, runtime, optVersion); + } + + // Need to load the costume from storage. The server should have a reference to this md5. + if (!runtime.storage) { + log.warn('No storage module present; cannot load costume asset: ', md5ext); + return Promise.resolve(costume); + } + + if (!runtime.storage.defaultAssetId) { + log.warn(`No default assets found`); + return Promise.resolve(costume); + } + + const AssetType = runtime.storage.AssetType; + const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap; + + const costumePromise = runtime.storage.load(assetType, md5, ext); + + let textLayerPromise; + if (costume.textLayerMD5) { + textLayerPromise = runtime.storage.load(AssetType.ImageBitmap, costume.textLayerMD5, 'png'); + } else { + textLayerPromise = Promise.resolve(null); + } + + return Promise.all([costumePromise, textLayerPromise]) + .then(assetArray => { + if (assetArray[0]) { + costume.asset = assetArray[0]; + } else { + return handleCostumeLoadError(costume, runtime); + } + + if (assetArray[1]) { + costume.textLayerAsset = assetArray[1]; + } + return loadCostumeFromAsset(costume, runtime, optVersion); + }) + .catch(error => { + // Handle case where storage.load rejects with errors + // instead of resolving null + log.warn('Error loading costume: ', error); + return handleCostumeLoadError(costume, runtime); + }); +}; + +module.exports = { + loadCostume, + loadCostumeFromAsset +}; diff --git a/local-scratch-vm/src/import/load-sound.js b/local-scratch-vm/src/import/load-sound.js new file mode 100644 index 0000000000000000000000000000000000000000..1c9cb495304b39812ba2779c721b27a66be07ce1 --- /dev/null +++ b/local-scratch-vm/src/import/load-sound.js @@ -0,0 +1,120 @@ +const StringUtil = require('../util/string-util'); +const log = require('../util/log'); + +/** + * Initialize a sound from an asset asynchronously. + * @param {!object} sound - the Scratch sound object. + * @property {string} md5 - the MD5 and extension of the sound to be loaded. + * @property {Buffer} data - sound data will be written here once loaded. + * @param {!Asset} soundAsset - the asset loaded from storage. + * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. + * @param {SoundBank} soundBank - Scratch Audio SoundBank to add sounds to. + * @returns {!Promise} - a promise which will resolve to the sound when ready. + */ +const loadSoundFromAsset = function (sound, soundAsset, runtime, soundBank) { + sound.assetId = soundAsset.assetId; + if (!runtime.audioEngine) { + log.warn('No audio engine present; cannot load sound asset: ', sound.md5); + return Promise.resolve(sound); + } + return runtime.audioEngine.decodeSoundPlayer(Object.assign( + {}, + sound, + {data: soundAsset.data} + )).then(soundPlayer => { + sound.soundId = soundPlayer.id; + // Set the sound sample rate and sample count based on the + // the audio buffer from the audio engine since the sound + // gets resampled by the audio engine + const soundBuffer = soundPlayer.buffer; + sound.rate = soundBuffer.sampleRate; + sound.sampleCount = soundBuffer.length; + + if (soundBank !== null) { + soundBank.addSoundPlayer(soundPlayer); + } + + if (runtime.isPackaged) { + sound.asset = null; + } + + return sound; + }); +}; + +// Handle sound loading errors by replacing the runtime sound with the +// default sound from storage, but keeping track of the original sound metadata +// in a `broken` field +const handleSoundLoadError = function (sound, runtime, soundBank) { + // Keep track of the old asset information until we're done loading the default sound + const oldAsset = sound.asset; // could be null + const oldAssetId = sound.assetId; + const oldSample = sound.sampleCount; + const oldRate = sound.rate; + const oldFormat = sound.format; + const oldDataFormat = sound.dataFormat; + + // Use default asset if original fails to load + sound.assetId = runtime.storage.defaultAssetId.Sound; + sound.asset = runtime.storage.get(sound.assetId); + sound.md5 = `${sound.assetId}.${sound.asset.dataFormat}`; + + return loadSoundFromAsset(sound, sound.asset, runtime, soundBank).then(loadedSound => { + loadedSound.broken = {}; + loadedSound.broken.assetId = oldAssetId; + loadedSound.broken.md5 = `${oldAssetId}.${oldDataFormat}`; + + // Should be null if we got here because the sound was missing + loadedSound.broken.asset = oldAsset; + + loadedSound.broken.sampleCount = oldSample; + loadedSound.broken.rate = oldRate; + loadedSound.broken.format = oldFormat; + loadedSound.broken.dataFormat = oldDataFormat; + + return loadedSound; + }); +}; + +/** + * Load a sound's asset into memory asynchronously. + * @param {!object} sound - the Scratch sound object. + * @property {string} md5 - the MD5 and extension of the sound to be loaded. + * @property {Buffer} data - sound data will be written here once loaded. + * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. + * @param {SoundBank} soundBank - Scratch Audio SoundBank to add sounds to. + * @returns {!Promise} - a promise which will resolve to the sound when ready. + */ +const loadSound = function (sound, runtime, soundBank) { + if (!runtime.storage) { + log.warn('No storage module present; cannot load sound asset: ', sound.md5); + return Promise.resolve(sound); + } + const idParts = StringUtil.splitFirst(sound.md5, '.'); + const md5 = idParts[0]; + const ext = idParts[1].toLowerCase(); + sound.dataFormat = ext; + return ( + (sound.asset && Promise.resolve(sound.asset)) || + runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext) + ) + .then(soundAsset => { + sound.asset = soundAsset; + + if (!soundAsset) { + log.warn('Failed to find sound data: ', sound.md5); + return handleSoundLoadError(sound, runtime, soundBank); + } + + return loadSoundFromAsset(sound, soundAsset, runtime, soundBank); + }) + .catch(e => { + log.warn(`Failed to load sound: ${sound.md5} with error: ${e}`); + return handleSoundLoadError(sound, runtime, soundBank); + }); +}; + +module.exports = { + loadSound, + loadSoundFromAsset +}; diff --git a/local-scratch-vm/src/index.js b/local-scratch-vm/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dfb566329981df226ba1984d896ccec078815119 --- /dev/null +++ b/local-scratch-vm/src/index.js @@ -0,0 +1,3 @@ +const VirtualMachine = require('./virtual-machine'); + +module.exports = VirtualMachine; diff --git a/local-scratch-vm/src/io/ble.js b/local-scratch-vm/src/io/ble.js new file mode 100644 index 0000000000000000000000000000000000000000..3d800ef069153b4926464a4950c7a5055d5eafd6 --- /dev/null +++ b/local-scratch-vm/src/io/ble.js @@ -0,0 +1,256 @@ +const JSONRPC = require('../util/jsonrpc'); + +class BLE extends JSONRPC { + + /** + * A BLE peripheral socket object. It handles connecting, over web sockets, to + * BLE peripherals, and reading and writing data to them. + * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. + * @param {string} extensionId - the id of the extension using this socket. + * @param {object} peripheralOptions - the list of options for peripheral discovery. + * @param {object} connectCallback - a callback for connection. + * @param {object} resetCallback - a callback for resetting extension state. + */ + constructor (runtime, extensionId, peripheralOptions, connectCallback, resetCallback = null) { + super(); + + this._socket = runtime.getScratchLinkSocket('BLE'); + this._socket.setOnOpen(this.requestPeripheral.bind(this)); + this._socket.setOnClose(this.handleDisconnectError.bind(this)); + this._socket.setOnError(this._handleRequestError.bind(this)); + this._socket.setHandleMessage(this._handleMessage.bind(this)); + + this._sendMessage = this._socket.sendMessage.bind(this._socket); + + this._availablePeripherals = {}; + this._connectCallback = connectCallback; + this._connected = false; + this._characteristicDidChangeCallback = null; + this._resetCallback = resetCallback; + this._discoverTimeoutID = null; + this._extensionId = extensionId; + this._peripheralOptions = peripheralOptions; + this._runtime = runtime; + + this._socket.open(); + } + + /** + * Request connection to the peripheral. + * If the web socket is not yet open, request when the socket promise resolves. + */ + requestPeripheral () { + this._availablePeripherals = {}; + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000); + this.sendRemoteRequest('discover', this._peripheralOptions) + .catch(e => { + this._handleRequestError(e); + }); + } + + /** + * Try connecting to the input peripheral id, and then call the connect + * callback if connection is successful. + * @param {number} id - the id of the peripheral to connect to + */ + connectPeripheral (id) { + this.sendRemoteRequest('connect', {peripheralId: id}) + .then(() => { + this._connected = true; + this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); + this._connectCallback(); + }) + .catch(e => { + this._handleRequestError(e); + }); + } + + /** + * Close the websocket. + */ + disconnect () { + if (this._connected) { + this._connected = false; + } + + if (this._socket.isOpen()) { + this._socket.close(); + } + + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + + // Sets connection status icon to orange + this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED); + } + + /** + * @return {bool} whether the peripheral is connected. + */ + isConnected () { + return this._connected; + } + + /** + * Start receiving notifications from the specified ble service. + * @param {number} serviceId - the ble service to read. + * @param {number} characteristicId - the ble characteristic to get notifications from. + * @param {object} onCharacteristicChanged - callback for characteristic change notifications. + * @return {Promise} - a promise from the remote startNotifications request. + */ + startNotifications (serviceId, characteristicId, onCharacteristicChanged = null) { + const params = { + serviceId, + characteristicId + }; + this._characteristicDidChangeCallback = onCharacteristicChanged; + return this.sendRemoteRequest('startNotifications', params) + .catch(e => { + this.handleDisconnectError(e); + }); + } + + /** + * Read from the specified ble service. + * @param {number} serviceId - the ble service to read. + * @param {number} characteristicId - the ble characteristic to read. + * @param {boolean} optStartNotifications - whether to start receiving characteristic change notifications. + * @param {object} onCharacteristicChanged - callback for characteristic change notifications. + * @return {Promise} - a promise from the remote read request. + */ + read (serviceId, characteristicId, optStartNotifications = false, onCharacteristicChanged = null) { + const params = { + serviceId, + characteristicId + }; + if (optStartNotifications) { + params.startNotifications = true; + } + if (onCharacteristicChanged) { + this._characteristicDidChangeCallback = onCharacteristicChanged; + } + return this.sendRemoteRequest('read', params) + .catch(e => { + this.handleDisconnectError(e); + }); + } + + /** + * Write data to the specified ble service. + * @param {number} serviceId - the ble service to write. + * @param {number} characteristicId - the ble characteristic to write. + * @param {string} message - the message to send. + * @param {string} encoding - the message encoding type. + * @param {boolean} withResponse - if true, resolve after peripheral's response. + * @return {Promise} - a promise from the remote send request. + */ + write (serviceId, characteristicId, message, encoding = null, withResponse = null) { + const params = {serviceId, characteristicId, message}; + if (encoding) { + params.encoding = encoding; + } + if (withResponse !== null) { + params.withResponse = withResponse; + } + return this.sendRemoteRequest('write', params) + .catch(e => { + this.handleDisconnectError(e); + }); + } + + /** + * Handle a received call from the socket. + * @param {string} method - a received method label. + * @param {object} params - a received list of parameters. + * @return {object} - optional return value. + */ + didReceiveCall (method, params) { + switch (method) { + case 'didDiscoverPeripheral': + this._availablePeripherals[params.peripheralId] = params; + this._runtime.emit( + this._runtime.constructor.PERIPHERAL_LIST_UPDATE, + this._availablePeripherals + ); + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + break; + case 'userDidPickPeripheral': + this._availablePeripherals[params.peripheralId] = params; + this._runtime.emit( + this._runtime.constructor.USER_PICKED_PERIPHERAL, + this._availablePeripherals + ); + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + break; + case 'userDidNotPickPeripheral': + this._runtime.emit( + this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT + ); + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + break; + case 'characteristicDidChange': + if (this._characteristicDidChangeCallback) { + this._characteristicDidChangeCallback(params.message); + } + break; + case 'ping': + return 42; + } + } + + /** + * Handle an error resulting from losing connection to a peripheral. + * + * This could be due to: + * - battery depletion + * - going out of bluetooth range + * - being powered down + * + * Disconnect the socket, and if the extension using this socket has a + * reset callback, call it. Finally, emit an error to the runtime. + */ + handleDisconnectError (/* e */) { + // log.error(`BLE error: ${JSON.stringify(e)}`); + + if (!this._connected) return; + + this.disconnect(); + + if (this._resetCallback) { + this._resetCallback(); + } + + this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR, { + message: `Scratch lost connection to`, + extensionId: this._extensionId + }); + } + + _handleRequestError (/* e */) { + // log.error(`BLE error: ${JSON.stringify(e)}`); + + this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, { + message: `Scratch lost connection to`, + extensionId: this._extensionId + }); + } + + _handleDiscoverTimeout () { + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + this._runtime.emit(this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT); + } +} + +module.exports = BLE; diff --git a/local-scratch-vm/src/io/bt.js b/local-scratch-vm/src/io/bt.js new file mode 100644 index 0000000000000000000000000000000000000000..be1ce5db3a1b22420ef0d2e24a47f9bf9b9d52ef --- /dev/null +++ b/local-scratch-vm/src/io/bt.js @@ -0,0 +1,202 @@ +const JSONRPC = require('../util/jsonrpc'); + +class BT extends JSONRPC { + + /** + * A BT peripheral socket object. It handles connecting, over web sockets, to + * BT peripherals, and reading and writing data to them. + * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. + * @param {string} extensionId - the id of the extension using this socket. + * @param {object} peripheralOptions - the list of options for peripheral discovery. + * @param {object} connectCallback - a callback for connection. + * @param {object} resetCallback - a callback for resetting extension state. + * @param {object} messageCallback - a callback for message sending. + */ + constructor (runtime, extensionId, peripheralOptions, connectCallback, resetCallback = null, messageCallback) { + super(); + + this._socket = runtime.getScratchLinkSocket('BT'); + this._socket.setOnOpen(this.requestPeripheral.bind(this)); + this._socket.setOnError(this._handleRequestError.bind(this)); + this._socket.setOnClose(this.handleDisconnectError.bind(this)); + this._socket.setHandleMessage(this._handleMessage.bind(this)); + + this._sendMessage = this._socket.sendMessage.bind(this._socket); + + this._availablePeripherals = {}; + this._connectCallback = connectCallback; + this._connected = false; + this._characteristicDidChangeCallback = null; + this._resetCallback = resetCallback; + this._discoverTimeoutID = null; + this._extensionId = extensionId; + this._peripheralOptions = peripheralOptions; + this._messageCallback = messageCallback; + this._runtime = runtime; + + this._socket.open(); + } + + /** + * Request connection to the peripheral. + * If the web socket is not yet open, request when the socket promise resolves. + */ + requestPeripheral () { + this._availablePeripherals = {}; + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000); + this.sendRemoteRequest('discover', this._peripheralOptions) + .catch( + e => this._handleRequestError(e) + ); + } + + /** + * Try connecting to the input peripheral id, and then call the connect + * callback if connection is successful. + * @param {number} id - the id of the peripheral to connect to + * @param {string} pin - an optional pin for pairing + */ + connectPeripheral (id, pin = null) { + const params = {peripheralId: id}; + if (pin) { + params.pin = pin; + } + this.sendRemoteRequest('connect', params) + .then(() => { + this._connected = true; + this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); + this._connectCallback(); + }) + .catch(e => { + this._handleRequestError(e); + }); + } + + /** + * Close the websocket. + */ + disconnect () { + if (this._connected) { + this._connected = false; + } + + if (this._socket.isOpen()) { + this._socket.close(); + } + + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + + // Sets connection status icon to orange + this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED); + } + + /** + * @return {bool} whether the peripheral is connected. + */ + isConnected () { + return this._connected; + } + + sendMessage (options) { + return this.sendRemoteRequest('send', options) + .catch(e => { + this.handleDisconnectError(e); + }); + } + + /** + * Handle a received call from the socket. + * @param {string} method - a received method label. + * @param {object} params - a received list of parameters. + * @return {object} - optional return value. + */ + didReceiveCall (method, params) { + // TODO: Add peripheral 'undiscover' handling + switch (method) { + case 'didDiscoverPeripheral': + this._availablePeripherals[params.peripheralId] = params; + this._runtime.emit( + this._runtime.constructor.PERIPHERAL_LIST_UPDATE, + this._availablePeripherals + ); + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + break; + case 'userDidPickPeripheral': + this._availablePeripherals[params.peripheralId] = params; + this._runtime.emit( + this._runtime.constructor.USER_PICKED_PERIPHERAL, + this._availablePeripherals + ); + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + break; + case 'userDidNotPickPeripheral': + this._runtime.emit( + this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT + ); + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + break; + case 'didReceiveMessage': + this._messageCallback(params); // TODO: refine? + break; + default: + return 'nah'; + } + } + + /** + * Handle an error resulting from losing connection to a peripheral. + * + * This could be due to: + * - battery depletion + * - going out of bluetooth range + * - being powered down + * + * Disconnect the socket, and if the extension using this socket has a + * reset callback, call it. Finally, emit an error to the runtime. + */ + handleDisconnectError (/* e */) { + // log.error(`BT error: ${JSON.stringify(e)}`); + + if (!this._connected) return; + + this.disconnect(); + + if (this._resetCallback) { + this._resetCallback(); + } + + this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR, { + message: `Scratch lost connection to`, + extensionId: this._extensionId + }); + } + + _handleRequestError (/* e */) { + // log.error(`BT error: ${JSON.stringify(e)}`); + + this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, { + message: `Scratch lost connection to`, + extensionId: this._extensionId + }); + } + + _handleDiscoverTimeout () { + if (this._discoverTimeoutID) { + window.clearTimeout(this._discoverTimeoutID); + } + this._runtime.emit(this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT); + } +} + +module.exports = BT; diff --git a/local-scratch-vm/src/io/clock.js b/local-scratch-vm/src/io/clock.js new file mode 100644 index 0000000000000000000000000000000000000000..bde13052a0a8f336c39efd214463243778e82579 --- /dev/null +++ b/local-scratch-vm/src/io/clock.js @@ -0,0 +1,34 @@ +const Timer = require('../util/timer'); + +class Clock { + constructor (runtime) { + this._projectTimer = new Timer({now: () => runtime.currentMSecs}); + this._projectTimer.start(); + this._paused = false; + /** + * Reference to the owning Runtime. + * @type{!Runtime} + */ + this.runtime = runtime; + } + + projectTimer () { + return this._projectTimer.timeElapsed() / 1000; + } + + pause () { + this._paused = true; + this._projectTimer.pause(); + } + + resume () { + this._paused = false; + this._projectTimer.play(); + } + + resetProjectTimer () { + this._projectTimer.start(); + } +} + +module.exports = Clock; diff --git a/local-scratch-vm/src/io/cloud.js b/local-scratch-vm/src/io/cloud.js new file mode 100644 index 0000000000000000000000000000000000000000..86e64ed79ee334319693e05dcc1d1056fe72c921 --- /dev/null +++ b/local-scratch-vm/src/io/cloud.js @@ -0,0 +1,169 @@ +const Variable = require('../engine/variable'); +const log = require('../util/log'); + +class Cloud { + /** + * @typedef updateVariable + * @param {string} name The name of the cloud variable to update on the server + * @param {(string | number)} value The value to update the cloud variable with. + */ + + /** + * A cloud data provider, responsible for managing the connection to the + * cloud data server and for posting data about cloud data activity to + * this IO device. + * @typedef {object} CloudProvider + * @property {updateVariable} updateVariable A function which sends a cloud variable + * update to the cloud data server. + * @property {Function} requestCloseConnection A function which closes + * the connection to the cloud data server. + */ + + /** + * Part of a cloud io data post indicating a cloud variable update. + * @typedef {object} VarUpdateData + * @property {string} name The name of the variable to update + * @property {(number | string)} value The scalar value to update the variable with + */ + + /** + * A cloud io data post message. + * @typedef {object} CloudIOData + * @property {VarUpdateData} varUpdate A {@link VarUpdateData} message indicating + * a cloud variable update + */ + + /** + * Cloud IO Device responsible for sending and receiving messages from + * cloud provider (mananging the cloud server connection) and interacting + * with cloud variables in the current project. + * @param {Runtime} runtime The runtime context for this cloud io device. + */ + constructor (runtime) { + /** + * Reference to the cloud data provider, responsible for mananging + * the web socket connection to the cloud data server. + * @type {?CloudProvider} + */ + this.provider = null; + + /** + * Reference to the runtime that owns this cloud io device. + * @type {!Runtime} + */ + this.runtime = runtime; + + /** + * Reference to the stage target which owns the cloud variables + * in the project. + * @type {?Target} + */ + this.stage = null; + } + + /** + * Set a reference to the cloud data provider. + * @param {CloudProvider} provider The cloud data provider + */ + setProvider (provider) { + this.provider = provider; + } + + /** + * Set a reference to the stage target which owns the + * cloud variables in the project. + * @param {Target} stage The stage target + */ + setStage (stage) { + this.stage = stage; + } + + /** + * Handle incoming data to this io device. + * @param {CloudIOData} data The {@link CloudIOData} object to process + */ + postData (data) { + if (data.varUpdate) { + this.updateCloudVariable(data.varUpdate); + } + } + + requestCreateVariable (variable) { + if (this.runtime.canAddCloudVariable()) { + if (this.provider) { + this.provider.createVariable(variable.name, variable.value); + // We'll set the cloud flag and update the + // cloud variable limit when we actually + // get a confirmation from the cloud data server + } + } // TODO else track creation for later + } + + /** + * Request the cloud data provider to update the given variable with + * the given value. Does nothing if this io device does not have a provider set. + * @param {string} name The name of the variable to update + * @param {string | number} value The value to update the variable with + */ + requestUpdateVariable (name, value) { + if (this.provider) { + this.provider.updateVariable(name, value); + } + } + + /** + * Request the cloud data provider to rename the variable with the given name + * to the given new name. Does nothing if this io device does not have a provider set. + * @param {string} oldName The name of the variable to rename + * @param {string | number} newName The new name for the variable + */ + requestRenameVariable (oldName, newName) { + if (this.provider) { + this.provider.renameVariable(oldName, newName); + } + } + + /** + * Request the cloud data provider to delete the variable with the given name + * Does nothing if this io device does not have a provider set. + * @param {string} name The name of the variable to delete + */ + requestDeleteVariable (name) { + if (this.provider) { + this.provider.deleteVariable(name); + } + } + + /** + * Update a cloud variable in the runtime based on the message received + * from the cloud provider. + * @param {VarData} varUpdate A {@link VarData} object describing + * a cloud variable update received from the cloud data provider. + */ + updateCloudVariable (varUpdate) { + const varName = varUpdate.name; + + const variable = this.stage.lookupVariableByNameAndType(varName, Variable.SCALAR_TYPE); + if (!variable || !variable.isCloud) { + log.warn(`Received an update for a cloud variable that does not exist: ${varName}`); + return; + } + + variable.value = varUpdate.value; + } + + /** + * Request the cloud data provider to close the web socket connection and + * clear this io device of references to the cloud data provider and the + * stage. + */ + clear () { + if (!this.provider) return; + + this.provider.requestCloseConnection(); + this.provider = null; + this.stage = null; + } +} + +module.exports = Cloud; diff --git a/local-scratch-vm/src/io/keyboard.js b/local-scratch-vm/src/io/keyboard.js new file mode 100644 index 0000000000000000000000000000000000000000..655fb4ab0ab1d12e9a5bc9a2e3e875a2489b1c29 --- /dev/null +++ b/local-scratch-vm/src/io/keyboard.js @@ -0,0 +1,283 @@ +const Cast = require('../util/cast'); + +/** + * Names used internally for keys used in scratch, also known as "scratch keys". + * @enum {string} + */ +const KEY_NAME = { + SPACE: 'space', + LEFT: 'left arrow', + UP: 'up arrow', + RIGHT: 'right arrow', + DOWN: 'down arrow', + ENTER: 'enter', + // tw: extra keys + BACKSPACE: 'backspace', + DELETE: 'delete', + SHIFT: 'shift', + CAPS_LOCK: 'caps lock', + SCROLL_LOCK: 'scroll lock', + CONTROL: 'control', + ESCAPE: 'escape', + INSERT: 'insert', + HOME: 'home', + END: 'end', + PAGE_UP: 'page up', + PAGE_DOWN: 'page down' +}; + +/** + * An set of the names of scratch keys. + * @type {Set} + */ +const KEY_NAME_SET = new Set(Object.values(KEY_NAME)); + +class Keyboard { + constructor (runtime) { + /** + * List of currently pressed scratch keys. + * A scratch key is: + * A key you can press on a keyboard, excluding modifier keys. + * An uppercase string of length one; + * except for special key names for arrow keys and space (e.g. 'left arrow'). + * Can be a non-english unicode letter like: æ ø ש נ 手 廿. + * @type{Array.} + */ + this._keysPressed = []; + // pm: keep track of hit keys + this._keysHit = []; + this._keysHitOnStep = {}; // key: the key pressed, value: the step they were pressed on + // pm: keep track of how long keys have been pressed for + this._keyTimestamps = {}; + /** + * Reference to the owning Runtime. + * Can be used, for example, to activate hats. + * @type{!Runtime} + */ + this.runtime = runtime; + // tw: track last pressed key + this.lastKeyPressed = ''; + this._numeralKeyCodesToStringKey = new Map(); + + // after processing all blocks, we can check if this step is after any keys we pressed + this.runtime.on("RUNTIME_STEP_END", () => { + const newHitKeys = []; + for (const key of this._keysHit) { + const stepKeyPressedOn = this._keysHitOnStep[key] || -1; + if (this.runtime.frameLoop._stepCounter <= stepKeyPressedOn) { + newHitKeys.push(key); + } + } + + // replace with the keys that are now pressed + this._keysHit = newHitKeys; + }); + } + + /** + * Convert from a keyboard event key name to a Scratch key name. + * @param {string} keyString the input key string. + * @return {string} the corresponding Scratch key, or an empty string. + */ + _keyStringToScratchKey (keyString) { + keyString = Cast.toString(keyString); + // Convert space and arrow keys to their Scratch key names. + switch (keyString) { + case ' ': return KEY_NAME.SPACE; + case 'ArrowLeft': + case 'Left': return KEY_NAME.LEFT; + case 'ArrowUp': + case 'Up': return KEY_NAME.UP; + case 'Right': + case 'ArrowRight': return KEY_NAME.RIGHT; + case 'Down': + case 'ArrowDown': return KEY_NAME.DOWN; + case 'Enter': return KEY_NAME.ENTER; + // tw: extra keys + case 'Backspace': return KEY_NAME.BACKSPACE; + case 'Delete': return KEY_NAME.DELETE; + case 'Shift': return KEY_NAME.SHIFT; + case 'CapsLock': return KEY_NAME.CAPS_LOCK; + case 'ScrollLock': return KEY_NAME.SCROLL_LOCK; + case 'Control': return KEY_NAME.CONTROL; + case 'Escape': return KEY_NAME.ESCAPE; + case 'Insert': return KEY_NAME.INSERT; + case 'Home': return KEY_NAME.HOME; + case 'End': return KEY_NAME.END; + case 'PageUp': return KEY_NAME.PAGE_UP; + case 'PageDown': return KEY_NAME.PAGE_DOWN; + } + // Ignore modifier keys + if (keyString.length > 1) { + return ''; + } + // tw: toUpperCase() happens later. We need to track key case. + return keyString; + } + + /** + * Convert from a block argument to a Scratch key name. + * @param {string} keyArg the input arg. + * @return {string} the corresponding Scratch key. + */ + _keyArgToScratchKey (keyArg) { + // If a number was dropped in, try to convert from ASCII to Scratch key. + if (typeof keyArg === 'number') { + // Check for the ASCII range containing numbers, some punctuation, + // and uppercase letters. + if (keyArg >= 48 && keyArg <= 90) { + return String.fromCharCode(keyArg); + } + switch (keyArg) { + case 32: return KEY_NAME.SPACE; + case 37: return KEY_NAME.LEFT; + case 38: return KEY_NAME.UP; + case 39: return KEY_NAME.RIGHT; + case 40: return KEY_NAME.DOWN; + } + } + + keyArg = Cast.toString(keyArg); + + // If the arg matches a special key name, return it. + // No special keys have a name that is only 1 character long, so we can avoid the lookup + // entirely in the most common case. + if (keyArg.length > 1 && KEY_NAME_SET.has(keyArg)) { + return keyArg; + } + + // Use only the first character. + if (keyArg.length > 1) { + keyArg = keyArg[0]; + } + + // Check for the space character. + if (keyArg === ' ') { + return KEY_NAME.SPACE; + } + // tw: support Scratch 2 hacked blocks + // There are more hacked blocks but most of them get mangled by Scratch 2 -> Scratch 3 conversion + if (keyArg === '\r') { + // this probably belongs upstream + return KEY_NAME.ENTER; + } + if (keyArg === '\u001b') { + return KEY_NAME.ESCAPE; + } + + return keyArg.toUpperCase(); + } + + /** + * Keyboard DOM event handler. + * @param {object} data Data from DOM event. + */ + postData (data) { + if (!data.key) return; + // tw: convert single letter keys to uppercase because of changes in _keyStringToScratchKey + const scratchKeyCased = this._keyStringToScratchKey(data.key); + const scratchKey = scratchKeyCased.length === 1 ? scratchKeyCased.toUpperCase() : scratchKeyCased; + if (scratchKey === '') return; + const index = this._keysPressed.indexOf(scratchKey); + if (data.isDown) { + // tw: track last pressed key + this.lastKeyPressed = scratchKeyCased; + this.runtime.emit('KEY_PRESSED', scratchKey); + // If not already present, add to the list. + if (index < 0) { + // pm: key isnt present? we hit it for the first time + this.runtime.emit('KEY_HIT', scratchKey); + this._keysPressed.push(scratchKey); + this._keyTimestamps[scratchKey] = Date.now(); + // pm: keep track of hit keys + this._keysHit.push(scratchKey); + this._keysHitOnStep[scratchKey] = this.runtime.frameLoop._stepCounter; + } + } else if (index > -1) { + // If already present, remove from the list. + this._keysPressed.splice(index, 1); + if (scratchKey in this._keyTimestamps) { + delete this._keyTimestamps[scratchKey]; + } + } + // Fix for https://github.com/LLK/scratch-vm/issues/2271 + if (data.hasOwnProperty('keyCode')) { + const keyCode = data.keyCode; + if (this._numeralKeyCodesToStringKey.has(keyCode)) { + const lastKeyOfSameCode = this._numeralKeyCodesToStringKey.get(keyCode); + if (lastKeyOfSameCode !== scratchKey) { + const indexToUnpress = this._keysPressed.indexOf(lastKeyOfSameCode); + if (indexToUnpress !== -1) { + this._keysPressed.splice(indexToUnpress, 1); + if (scratchKey in this._keyTimestamps) { + delete this._keyTimestamps[lastKeyOfSameCode]; + } + } + } + } + this._numeralKeyCodesToStringKey.set(keyCode, scratchKey); + } + } + + /** + * Get key down state for a specified key. + * @param {Any} keyArg key argument. + * @return {boolean} Is the specified key down? + */ + getKeyIsDown (keyArg) { + if (keyArg === 'any') { + return this._keysPressed.length > 0; + } + const scratchKey = this._keyArgToScratchKey(keyArg); + return this._keysPressed.indexOf(scratchKey) > -1; + } + + /** + * pm: Get if key was hit this tick for a specified key. + * @param {Any} keyArg key argument. + * @return {boolean} Is the specified key hit? + */ + getKeyIsHit (keyArg) { + if (keyArg === 'any') { + return this._keysHit.length > 0; + } + const scratchKey = this._keyArgToScratchKey(keyArg); + return this._keysHit.indexOf(scratchKey) > -1; + } + + // tw: expose last pressed key + getLastKeyPressed () { + return this.lastKeyPressed; + } + // pm: why dont we expose all keys? + getAllKeysPressed () { + return this._keysPressed; + } + getKeyTimestamp (keyArg) { + if (keyArg === 'any') { + // loop through all keys and see which one we have held the longest + let oldestTimestamp = Infinity; + let found = false; + for (const keyName in this._keyTimestamps) { + const timestamp = this._keyTimestamps[keyName]; + if (timestamp < oldestTimestamp) { + oldestTimestamp = timestamp; + found = true; + } + } + if (!found) return 0; + return oldestTimestamp; + } + // everything else + const scratchKey = this._keyArgToScratchKey(keyArg); + if (!(scratchKey in this._keyTimestamps)) { + return 0; + } + return this._keyTimestamps[scratchKey]; + } + getKeyTimestamps () { + return this._keyTimestamps; + } +} + +module.exports = Keyboard; diff --git a/local-scratch-vm/src/io/mouse.js b/local-scratch-vm/src/io/mouse.js new file mode 100644 index 0000000000000000000000000000000000000000..1cd608c0ada618a13a26b90d00f4e31b1ee7b92e --- /dev/null +++ b/local-scratch-vm/src/io/mouse.js @@ -0,0 +1,225 @@ +const MathUtil = require('../util/math-util'); +const { translateScreenPos } = require('../util/pos-math'); + +const roundToThreeDecimals = number => Math.round(number * 1000) / 1000; + +class Mouse { + constructor (runtime) { + this._clientX = 0; + this._clientY = 0; + this._scratchX = 0; + this._scratchY = 0; + + this._buttons = new Set(); + this._isDown = false; + + this.usesRightClickDown = false; + + // pm: keep track of clicks + this._isClicked = false; + this._clickOnStep = -1; + + /** + * Reference to the owning Runtime. + * Can be used, for example, to activate hats. + * @type{!Runtime} + */ + this.runtime = runtime; + this.cameraBound = null; + + // after processing all blocks, we can check if this step is after the one we clicked on + this.runtime.on("RUNTIME_STEP_END", () => { + if (this.runtime.frameLoop._stepCounter > this._clickOnStep) { + this._isClicked = false; + } + }); + } + + bindToCamera(screen) { + this.cameraBound = screen; + } + + removeCameraBinding() { + this.cameraBound = null; + } + + /** + * Activate "event_whenthisspriteclicked" hats. + * @param {Target} target to trigger hats on. + * @private + */ + _activateClickHats (target) { + // Activate both "this sprite clicked" and "stage clicked" + // They were separated into two opcodes for labeling, + // but should act the same way. + // Intentionally not checking isStage to make it work when sharing blocks. + this.runtime.startHats('event_whenthisspriteclicked', null, target); + this.runtime.startHats('event_whenstageclicked', null, target); + if (target.isStage) { + this.runtime.startHats('pmEventsExpansion_whenSpriteClicked', { SPRITE: '_stage_' }); + return; + } + if (target.sprite) { + this.runtime.startHats('pmEventsExpansion_whenSpriteClicked', { SPRITE: target.sprite.name }); + } + } + + /** + * Find a target by XY location + * @param {number} x X position to be sent to the renderer. + * @param {number} y Y position to be sent to the renderer. + * @return {Target} the target at that location + * @private + */ + _pickTarget (x, y) { + if (this.runtime.renderer) { + const drawableID = this.runtime.renderer.pick(x, y); + for (let i = 0; i < this.runtime.targets.length; i++) { + const target = this.runtime.targets[i]; + if (target.hasOwnProperty('drawableID') && + target.drawableID === drawableID) { + return target; + } + } + } + // Return the stage if no target was found + return this.runtime.getTargetForStage(); + } + + /** + * Mouse DOM event handler. + * @param {object} data Data from DOM event. + */ + postData (data) { + if (typeof data.x === 'number') { + this._clientX = data.x; + this._scratchX = MathUtil.clamp( + this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5), + -(this.runtime.stageWidth / 2), + (this.runtime.stageWidth / 2) + ); + } + if (typeof data.y === 'number') { + this._clientY = data.y; + this._scratchY = MathUtil.clamp( + -this.runtime.stageHeight * ((data.y / data.canvasHeight) - 0.5), + -(this.runtime.stageHeight / 2), + (this.runtime.stageHeight / 2) + ); + } + if (typeof data.isDown !== 'undefined') { + // If no button specified, default to left button for compatibility + const button = typeof data.button === 'undefined' ? 0 : data.button; + if (data.isDown) { + this._buttons.add(button); + } else { + this._buttons.delete(button); + } + + const previousDownState = this._isDown; + this._isDown = data.isDown; + if (data.isDown) { + this._isClicked = true; + this._clickOnStep = this.runtime.frameLoop._stepCounter; + } + + // Do not trigger if down state has not changed + if (previousDownState === this._isDown) return; + + // Never trigger click hats at the end of a drag + if (data.wasDragged) return; + + // Do not activate click hats for clicks outside canvas bounds + if (!(data.x > 0 && data.x < data.canvasWidth && + data.y > 0 && data.y < data.canvasHeight)) return; + + const target = this._pickTarget(data.x, data.y); + const isNewMouseDown = !previousDownState && this._isDown; + const isNewMouseUp = previousDownState && !this._isDown; + + // Draggable targets start click hats on mouse up. + // Non-draggable targets start click hats on mouse down. + if (target.draggable && isNewMouseUp) { + this._activateClickHats(target); + } else if (!target.draggable && isNewMouseDown) { + this._activateClickHats(target); + } + } + } + + /** + * Get the X position of the mouse in client coordinates. + * @return {number} Non-clamped X position of the mouse cursor. + */ + getClientX () { + return this._clientX; + } + + /** + * Get the Y position of the mouse in client coordinates. + * @return {number} Non-clamped Y position of the mouse cursor. + */ + getClientY () { + return this._clientY; + } + + /** + * Get the X position of the mouse in scratch coordinates. + * @return {number} Clamped and integer rounded X position of the mouse cursor. + */ + getScratchX () { + const mouseX = this.cameraBound + ? translateScreenPos(this.runtime, this.cameraBound, this._scratchX, this._scratchY)[0] + // ? (this._scratchX * cameraState.scale) - cameraState.pos[0] + : this._scratchX; + if (this.runtime.runtimeOptions.miscLimits) { + return Math.round(mouseX); + } + return roundToThreeDecimals(mouseX); + } + + /** + * Get the Y position of the mouse in scratch coordinates. + * @return {number} Clamped and integer rounded Y position of the mouse cursor. + */ + getScratchY () { + const mouseY = this.cameraBound + ? translateScreenPos(this.runtime, this.cameraBound, this._scratchX, this._scratchY)[1] + // ? (this._scratchY * cameraState.scale) - cameraState.pos[1] + : this._scratchY; + if (this.runtime.runtimeOptions.miscLimits) { + return Math.round(mouseY); + } + return roundToThreeDecimals(mouseY); + } + + /** + * Get the down state of the mouse. + * @return {boolean} Is the mouse down? + */ + getIsDown () { + return this._isDown; + } + + /** + * pm: Get if the mouse was pressed down on this tick. + * @return {boolean} Is the mouse clicked? + */ + getIsClicked () { + return this._isClicked; + } + + /** + * tw: Get the down state of a specific button of the mouse. + * @param {number} button The ID of the button. 0 = left, 1 = middle, 2 = right + * @return {boolean} Is the mouse button down? + */ + getButtonIsDown (button) { + if (button === 2) { + this.usesRightClickDown = true; + } + return this._buttons.has(button); + } +} + +module.exports = Mouse; diff --git a/local-scratch-vm/src/io/mouseWheel.js b/local-scratch-vm/src/io/mouseWheel.js new file mode 100644 index 0000000000000000000000000000000000000000..8665b4f3d8eb8f8bf4b4d528adc5fc2f6af1eee7 --- /dev/null +++ b/local-scratch-vm/src/io/mouseWheel.js @@ -0,0 +1,54 @@ +class MouseWheel { + constructor (runtime) { + /** + * Reference to the owning Runtime. + * @type{!Runtime} + */ + this.runtime = runtime; + + // pm: track scroll deltaY + this.scrollDelta = 0; + this.runtime.on("RUNTIME_STEP_END", () => { + this.scrollDelta = 0; + }); + } + + _addToScrollingDistanceBlock (amount) { + if ('ext_pmSensingExpansion' in this.runtime) { + this.runtime.ext_pmSensingExpansion.scrollDistance += amount; + } + } + + /** + * Mouse wheel DOM event handler. + * @param {object} data Data from DOM event. + */ + postData (data) { + // pm: store scroll delta + this.scrollDelta = data.deltaY; + // add to scrolling distance + this._addToScrollingDistanceBlock(0 - data.deltaY); + + const matchFields = {}; + const scrollFields = {}; + if (data.deltaY < 0) { + matchFields.KEY_OPTION = 'up arrow'; + scrollFields.KEY_OPTION = 'up'; + } else if (data.deltaY > 0) { + matchFields.KEY_OPTION = 'down arrow'; + scrollFields.KEY_OPTION = 'down'; + } else { + return; + } + + this.runtime.startHats('event_whenkeypressed', matchFields); + this.runtime.startHats('event_whenmousescrolled', scrollFields); + } + + // pm: expose scroll delta for sensing block + getScrollDelta () { + return this.scrollDelta; + } +} + +module.exports = MouseWheel; diff --git a/local-scratch-vm/src/io/touch.js b/local-scratch-vm/src/io/touch.js new file mode 100644 index 0000000000000000000000000000000000000000..56812943b3f025738b3b8ce8746b9fabda659fba --- /dev/null +++ b/local-scratch-vm/src/io/touch.js @@ -0,0 +1,139 @@ +const MathUtil = require('../util/math-util'); + +const roundToThreeDecimals = number => Math.round(number * 1000) / 1000; + +class Touch { + constructor (runtime) { + this.fingers = Array.from(Array(5).keys()).map(() => { + return { + _clientX: 0, + _clientY: 0, + _scratchX: 0, + _scratchY: 0, + _isDown: false, + + // pm: keep track of taps + _isTapped: false, + _tapOnStep: -1 + } + }); + + /** + * Reference to the owning Runtime. + * Can be used, for example, to activate hats. + * @type{!Runtime} + */ + this.runtime = runtime; + + // after processing all blocks, we can check if this step is after the one we tapped on + this.runtime.on("RUNTIME_STEP_END", () => { + for (const finger of this.fingers) { + if (this.runtime.frameLoop._stepCounter > finger._tapOnStep) { + finger._isTapped = false; + } + } + }); + } + + /** + * Touch DOM event handler. + * @param {object} data Data from DOM event. + */ + postData (data) { + data.changedTouches.forEach(touch => { + const finger = this.fingers[touch.identifier]; + if (!finger) return; + if (typeof touch.x === 'number') { + finger._clientX = touch.x; + finger._scratchX = MathUtil.clamp( + this.runtime.stageWidth * ((touch.x / data.canvasWidth) - 0.5), + -(this.runtime.stageWidth / 2), + (this.runtime.stageWidth / 2) + ); + } + if (typeof touch.y === 'number') { + finger._clientY = touch.y; + finger._scratchY = MathUtil.clamp( + -this.runtime.stageHeight * ((touch.y / data.canvasHeight) - 0.5), + -(this.runtime.stageHeight / 2), + (this.runtime.stageHeight / 2) + ); + } + if (typeof data.isDown !== "undefined") { + finger._isDown = data.isDown; + } + if (data.isDown === true) { + finger._isTapped = true; + finger._tapOnStep = this.runtime.frameLoop._stepCounter; + } + }) + } + + /** + * Get the X position of the finger in client coordinates. + * @return {number} Non-clamped X position of the finger cursor. + */ + getClientX (finger) { + const f = this.fingers[finger]; + if (!f) return 0; + return f._clientX; + } + + /** + * Get the Y position of the finger in client coordinates. + * @return {number} Non-clamped Y position of the finger cursor. + */ + getClientY (finger) { + const f = this.fingers[finger]; + if (!f) return 0; + return f._clientY; + } + + /** + * Get the X position of the finger in scratch coordinates. + * @return {number} Clamped and integer rounded X position of the finger cursor. + */ + getScratchX (finger) { + const f = this.fingers[finger]; + if (!f) return 0; + if (this.runtime.runtimeOptions.miscLimits) { + return Math.round(f._scratchX); + } + return roundToThreeDecimals(f._scratchX); + } + + /** + * Get the Y position of the finger in scratch coordinates. + * @return {number} Clamped and integer rounded Y position of the finger cursor. + */ + getScratchY (finger) { + const f = this.fingers[finger]; + if (!f) return 0; + if (this.runtime.runtimeOptions.miscLimits) { + return Math.round(f._scratchY); + } + return roundToThreeDecimals(f._scratchY); + } + + /** + * Get the down state of the finger. + * @return {boolean} Is the finger down? + */ + getIsDown (finger) { + const f = this.fingers[finger]; + if (!f) return false; + return f._isDown; + } + + /** + * pm: Get if the finger was pressed down on this tick. + * @return {boolean} Is the finger tapping? + */ + getIsTapped (finger) { + const f = this.fingers[finger]; + if (!f) return false; + return f._isTapped; + } +} + +module.exports = Touch; diff --git a/local-scratch-vm/src/io/userData.js b/local-scratch-vm/src/io/userData.js new file mode 100644 index 0000000000000000000000000000000000000000..fbc22a40f0d72eaf3510e524c2e17aa62e68a827 --- /dev/null +++ b/local-scratch-vm/src/io/userData.js @@ -0,0 +1,37 @@ +class UserData { + constructor () { + this._username = ''; + this._loggedIn = false; + } + + /** + * Handler for updating the username + * @param {object} data Data posted to this ioDevice. + * @property {!string} username The new username. + */ + postData (data) { + this._username = data.username; + this._loggedIn = false; + if (data.loggedIn === true) { + this._loggedIn = true; + } + } + + /** + * Getter for username. Initially empty string, until set via postData. + * @returns {!string} The current username + */ + getUsername () { + return this._username; + } + + /** + * Getter for loggedIn. Will be false, until set via postData. + * @returns {boolean} The current loggedIn state + */ + getLoggedIn() { + return this._loggedIn; + } +} + +module.exports = UserData; diff --git a/local-scratch-vm/src/io/video.js b/local-scratch-vm/src/io/video.js new file mode 100644 index 0000000000000000000000000000000000000000..35eeadf7cf4b43f1ee081a6eb09438bfdc654f1b --- /dev/null +++ b/local-scratch-vm/src/io/video.js @@ -0,0 +1,215 @@ +const StageLayering = require('../engine/stage-layering'); + +class Video { + constructor (runtime) { + this.runtime = runtime; + + /** + * @typedef VideoProvider + * @property {Function} enableVideo - Requests camera access from the user, and upon success, + * enables the video feed + * @property {Function} disableVideo - Turns off the video feed + * @property {Function} getFrame - Return frame data from the video feed in + * specified dimensions, format, and mirroring. + */ + this.provider = null; + + /** + * Id representing a Scratch Renderer skin the video is rendered to for + * previewing. + * @type {number} + */ + this._skinId = -1; + + /** + * Id for a drawable using the video's skin that will render as a video + * preview. + * @type {Drawable} + */ + this._drawable = -1; + + /** + * Store the last state of the video transparency ghost effect + * @type {number} + */ + this._ghost = 0; + + /** + * Store a flag that allows the preview to be forced transparent. + * @type {number} + */ + this._forceTransparentPreview = false; + } + + static get FORMAT_IMAGE_DATA () { + return 'image-data'; + } + + static get FORMAT_CANVAS () { + return 'canvas'; + } + + /** + * Dimensions the video stream is analyzed at after its rendered to the + * sample canvas. + * @type {Array.} + */ + static get DIMENSIONS () { + return [480, 360]; + } + + /** + * Order preview drawable is inserted at in the renderer. + * @type {number} + */ + static get ORDER () { + return 1; + } + + /** + * Set a video provider for this device. A default implementation of + * a video provider can be found in scratch-gui/src/lib/video/video-provider + * @param {VideoProvider} provider - Video provider to use + */ + setProvider (provider) { + this.provider = provider; + } + + /** + * Request video be enabled. Sets up video, creates video skin and enables preview. + * + * ioDevices.video.requestVideo() + * + * @return {Promise.
+ +
+ + + + + + + +
FrameSelf TimeTotal TimeExecutions
+ + + + + + + + + + +
opcodeSelf TimeTotal TimeExecutions
+
+ +
+ + + + + diff --git a/local-scratch-vm/src/playground/suite.css b/local-scratch-vm/src/playground/suite.css new file mode 100644 index 0000000000000000000000000000000000000000..06df9a3fc75bf835db9d173b424c73e95cfc7b95 --- /dev/null +++ b/local-scratch-vm/src/playground/suite.css @@ -0,0 +1,78 @@ +html, body { + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; +} + +iframe { + border: none; +} + +.runner-controls { + position: absolute; + width: 30em; + height: 100%; + left: 0; + right: 30em; + top: 0; + bottom: 0; + overflow: scroll; + padding: 1em; + padding-top: 0; + box-sizing: border-box; +} + +.bench-frame-owner { + position: absolute; + width: calc(100% - 30em); + height: 100%; + left: 30em; + right: 100%; + top: 0; + bottom: 0; +} + +.controls { + margin-bottom: 1em; +} + +.legend { + margin: 1em 0; +} + +.result-view { + border-bottom: 1px solid #ccc; + border-spacing: 0; + border-collapse: collapse; + padding: 5px; +} + +.fixture-project { + display: inline-block; + clear: both; +} + +.fixture-warm-up { + display: inline-block; +} + +.fixture-recording { + display: inline-block; +} + +.result-view.resume { + cursor: pointer; +} + +.result-status { + float: right; + text-align: right; + margin-left: 0.3em; +} + +.compare-file { + cursor: pointer; + visibility: hidden; + width: 0; +} diff --git a/local-scratch-vm/src/playground/suite.html b/local-scratch-vm/src/playground/suite.html new file mode 100644 index 0000000000000000000000000000000000000000..4100ddd82e30e38ff1999b81674737c884aa4514 --- /dev/null +++ b/local-scratch-vm/src/playground/suite.html @@ -0,0 +1,25 @@ + + + + + + Scratch VM Benchmark Suite + + + +
+

Scratch VM Benchmark Suite

+
+ +
+
+ +
+ +
+ + + + + diff --git a/local-scratch-vm/src/playground/suite.js b/local-scratch-vm/src/playground/suite.js new file mode 100644 index 0000000000000000000000000000000000000000..7b324f1eac68e868f8693336e137e688f3963deb --- /dev/null +++ b/local-scratch-vm/src/playground/suite.js @@ -0,0 +1,544 @@ +const soon = (() => { + let _soon; + return () => { + if (!_soon) { + _soon = Promise.resolve() + .then(() => { + _soon = null; + }); + } + return _soon; + }; +})(); + +class Emitter { + constructor () { + Object.defineProperty(this, '_listeners', { + value: {}, + enumerable: false + }); + } + on (name, listener, context) { + if (!this._listeners[name]) { + this._listeners[name] = []; + } + + this._listeners[name].push(listener, context); + } + off (name, listener, context) { + if (this._listeners[name]) { + if (listener) { + for (let i = 0; i < this._listeners[name].length; i += 2) { + if ( + this._listeners[name][i] === listener && + this._listeners[name][i + 1] === context) { + this._listeners[name].splice(i, 2); + i -= 2; + } + } + } else { + for (let i = 0; i < this._listeners[name].length; i += 2) { + if (this._listeners[name][i + 1] === context) { + this._listeners[name].splice(i, 2); + i -= 2; + } + } + } + } + } + emit (name, ...args) { + if (this._listeners[name]) { + for (let i = 0; i < this._listeners[name].length; i += 2) { + this._listeners[name][i].call(this._listeners[name][i + 1] || this, ...args); + } + } + } +} + +class BenchFrameStream extends Emitter { + constructor (frame) { + super(); + + this.frame = frame; + window.addEventListener('message', message => { + this.emit('message', message.data); + }); + } + + send (message) { + this.frame.send(message); + } +} + +const benchmarkUrlArgs = args => ( + [ + args.projectId, + args.warmUpTime, + args.recordingTime + ].join(',') +); + +const BENCH_MESSAGE_TYPE = { + INACTIVE: 'BENCH_MESSAGE_INACTIVE', + LOAD: 'BENCH_MESSAGE_LOAD', + LOADING: 'BENCH_MESSAGE_LOADING', + WARMING_UP: 'BENCH_MESSAGE_WARMING_UP', + ACTIVE: 'BENCH_MESSAGE_ACTIVE', + COMPLETE: 'BENCH_MESSAGE_COMPLETE' +}; + +class BenchUtil { + constructor (frame) { + this.frame = frame; + this.benchStream = new BenchFrameStream(frame); + } + + setFrameLocation (url) { + this.frame.contentWindow.location.assign(url); + } + + startBench (args) { + this.benchArgs = args; + this.setFrameLocation(`index.html#${benchmarkUrlArgs(args)}`); + } + + pauseBench () { + new Promise(resolve => setTimeout(resolve, 1000)) + .then(() => { + this.benchStream.emit('message', { + type: BENCH_MESSAGE_TYPE.INACTIVE + }); + }); + } + + resumeBench () { + this.startBench(this.benchArgs); + } + + renderResults (results) { + this.setFrameLocation( + `index.html#view/${btoa(JSON.stringify(results))}` + ); + } +} + +const BENCH_STATUS = { + INACTIVE: 'BENCH_STATUS_INACTIVE', + RESUME: 'BENCH_STATUS_RESUME', + STARTING: 'BENCH_STATUS_STARTING', + LOADING: 'BENCH_STATUS_LOADING', + WARMING_UP: 'BENCH_STATUS_WARMING_UP', + ACTIVE: 'BENCH_STATUS_ACTIVE', + COMPLETE: 'BENCH_STATUS_COMPLETE' +}; + +class BenchResult { + constructor ({fixture, status = BENCH_STATUS.INACTIVE, frames = null, opcodes = null}) { + this.fixture = fixture; + this.status = status; + this.frames = frames; + this.opcodes = opcodes; + } +} + +class BenchFixture extends Emitter { + constructor ({ + projectId, + warmUpTime = 4000, + recordingTime = 6000 + }) { + super(); + + this.projectId = projectId; + this.warmUpTime = warmUpTime; + this.recordingTime = recordingTime; + } + + get id () { + return `${this.projectId}-${this.warmUpTime}-${this.recordingTime}`; + } + + run (util) { + return new Promise(resolve => { + util.benchStream.on('message', message => { + const result = { + fixture: this, + status: BENCH_STATUS.STARTING, + frames: null, + opcodes: null + }; + if (message.type === BENCH_MESSAGE_TYPE.INACTIVE) { + result.status = BENCH_STATUS.RESUME; + } else if (message.type === BENCH_MESSAGE_TYPE.LOADING) { + result.status = BENCH_STATUS.LOADING; + } else if (message.type === BENCH_MESSAGE_TYPE.WARMING_UP) { + result.status = BENCH_STATUS.WARMING_UP; + } else if (message.type === BENCH_MESSAGE_TYPE.ACTIVE) { + result.status = BENCH_STATUS.ACTIVE; + } else if (message.type === BENCH_MESSAGE_TYPE.COMPLETE) { + result.status = BENCH_STATUS.COMPLETE; + result.frames = message.frames; + result.opcodes = message.opcodes; + resolve(new BenchResult(result)); + util.benchStream.off('message', null, this); + } + this.emit('result', new BenchResult(result)); + }, this); + util.startBench(this); + }); + } +} + +class BenchSuiteResult extends Emitter { + constructor ({suite, results = []}) { + super(); + + this.suite = suite; + this.results = results; + + if (suite) { + suite.on('result', result => { + if (result.status === BENCH_STATUS.COMPLETE) { + this.results.push(results); + this.emit('add', this); + } + }); + } + } +} + +class BenchSuite extends Emitter { + constructor (fixtures = []) { + super(); + + this.fixtures = fixtures; + } + + add (fixture) { + this.fixtures.push(fixture); + } + + run (util) { + return new Promise(resolve => { + const fixtures = this.fixtures.slice(); + const results = []; + const push = result => { + result.fixture.off('result', null, this); + results.push(result); + }; + const emitResult = this.emit.bind(this, 'result'); + const pop = () => { + const fixture = fixtures.shift(); + if (fixture) { + fixture.on('result', emitResult, this); + fixture.run(util) + .then(push) + .then(pop); + } else { + resolve(new BenchSuiteResult({suite: this, results})); + } + }; + pop(); + }); + } +} + +class BenchRunner extends Emitter { + constructor ({frame, suite}) { + super(); + + this.frame = frame; + this.suite = suite; + this.util = new BenchUtil(frame); + } + + run () { + return this.suite.run(this.util); + } +} + +const viewNames = { + [BENCH_STATUS.INACTIVE]: 'Inactive', + [BENCH_STATUS.RESUME]: 'Resume', + [BENCH_STATUS.STARTING]: 'Starting', + [BENCH_STATUS.LOADING]: 'Loading', + [BENCH_STATUS.WARMING_UP]: 'Warming Up', + [BENCH_STATUS.ACTIVE]: 'Active', + [BENCH_STATUS.COMPLETE]: 'Complete' +}; + +class BenchResultView { + constructor ({result, benchUtil}) { + this.result = result; + this.compare = null; + this.benchUtil = benchUtil; + this.dom = document.createElement('div'); + } + + update (result) { + soon().then(() => this.render(result)); + } + + resume () { + this.benchUtil.resumeBench(); + } + + setFrameLocation (loc) { + this.benchUtil.pauseBench(); + this.benchUtil.setFrameLocation(loc); + } + + act (ev) { + if ( + ev.type === 'click' && + ev.button === 0 && + !(ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) + ) { + let target = ev.target; + while (target && target.tagName.toLowerCase() !== 'a') { + target = target.parentElement; + } + if (target && target.tagName.toLowerCase() === 'a') { + if (target.href) { + this.setFrameLocation(target.href); + ev.preventDefault(); + } + } else if (ev.currentTarget.classList.contains('resume')) { + this.resume(); + } + } + } + + render (newResult = this.result, compareResult = this.compare) { + const newResultFrames = (newResult.frames ? newResult.frames : []).filter(i => i); + const blockFunctionFrame = newResultFrames + .find(frame => frame.name === 'blockFunction'); + const stepThreadsInnerFrame = newResultFrames + .find(frame => frame.name === 'Sequencer.stepThreads#inner'); + + const blocksPerSecond = blockFunctionFrame ? + (blockFunctionFrame.executions / + (stepThreadsInnerFrame.totalTime / 1000)) | 0 : + 0; + const stepsPerSecond = stepThreadsInnerFrame ? + (stepThreadsInnerFrame.executions / + (stepThreadsInnerFrame.totalTime / 1000)) | 0 : + 0; + + const compareResultFrames = ( + compareResult && compareResult.frames ? + compareResult.frames : + [] + ); + const blockFunctionCompareFrame = compareResultFrames + .find(frame => frame.name === 'blockFunction'); + const stepThreadsInnerCompareFrame = compareResultFrames + .find(frame => frame.name === 'Sequencer.stepThreads#inner'); + + const compareBlocksPerSecond = blockFunctionCompareFrame ? + (blockFunctionCompareFrame.executions / + (stepThreadsInnerCompareFrame.totalTime / 1000)) | 0 : + 0; + const compareStepsPerSecond = stepThreadsInnerCompareFrame ? + (stepThreadsInnerCompareFrame.executions / + (stepThreadsInnerCompareFrame.totalTime / 1000)) | 0 : + 0; + + const statusName = viewNames[newResult.status]; + + this.dom.className = `result-view ${ + viewNames[newResult.status].toLowerCase() + }`; + this.dom.onclick = this.act.bind(this); + let url = `index.html#${benchmarkUrlArgs(newResult.fixture)}`; + if (newResult.status === BENCH_STATUS.COMPLETE) { + url = `index.html#view/${btoa(JSON.stringify(newResult))}`; + } + let compareUrl = url; + if (compareResult && compareResult) { + compareUrl = + `index.html#view/${btoa(JSON.stringify(compareResult))}`; + } + let compareHTML = ''; + if (stepThreadsInnerFrame && stepThreadsInnerCompareFrame) { + compareHTML = ` +
+
${compareStepsPerSecond}
+
${compareBlocksPerSecond}
+
+
`; + } + this.dom.innerHTML = ` + +
+
${stepThreadsInnerFrame ? `steps/s` : ''}
+
${blockFunctionFrame ? `blocks/s` : statusName}
+
+ +
+
${stepThreadsInnerFrame ? `${stepsPerSecond}` : ''}
+
${blockFunctionFrame ? `${blocksPerSecond}` : ''}
+
+
+ ${compareHTML} +
+ Run for ${newResult.fixture.recordingTime / 1000} seconds after + ${newResult.fixture.warmUpTime / 1000} seconds +
+ `; + + this.result = newResult; + return this; + } +} + +class BenchSuiteResultView { + constructor ({runner}) { + const {suite, util} = runner; + + this.runner = runner; + this.suite = suite; + this.views = {}; + this.dom = document.createElement('div'); + + for (const fixture of suite.fixtures) { + this.views[fixture.id] = new BenchResultView({ + result: new BenchResult({fixture}), + benchUtil: util + }); + } + + suite.on('result', result => { + this.views[result.fixture.id].update(result); + }); + } + + render () { + this.dom.innerHTML = `
+ Project ID +
+
steps per second
+
blocks per second
+
+
Description
+
+ + `; + + for (const fixture of this.suite.fixtures) { + this.dom.appendChild(this.views[fixture.id].render().dom); + } + + return this; + } +} + +let suite; +let suiteView; + +window.upload = function (_this) { + if (!_this.files.length) { + return; + } + const reader = new FileReader(); + reader.onload = function () { + const report = JSON.parse(reader.result); + Object.values(suiteView.views) + .forEach(view => { + const sameFixture = report.results.find(result => ( + result.fixture.projectId === + view.result.fixture.projectId && + result.fixture.warmUpTime === + view.result.fixture.warmUpTime && + result.fixture.recordingTime === + view.result.fixture.recordingTime + )); + + if (sameFixture) { + if ( + view.result && view.result.frames && + view.result.frames.length > 0 + ) { + view.render(view.result, sameFixture); + } else { + view.compare = sameFixture; + } + } + }); + }; + reader.readAsText(_this.files[0]); +}; + +window.download = function (_this) { + const blob = new Blob([JSON.stringify({ + meta: { + source: 'Scratch VM Benchmark Suite', + version: 1 + }, + results: Object.values(suiteView.views) + .map(view => view.result) + .filter(view => view.status === BENCH_STATUS.COMPLETE) + })], {type: 'application/json'}); + + _this.download = 'scratch-vm-benchmark.json'; + _this.href = URL.createObjectURL(blob); +}; + +window.onload = function () { + suite = new BenchSuite(); + + const add = (projectId, warmUp = 0, recording = 5000) => { + suite.add(new BenchFixture({ + projectId, + warmUpTime: warmUp, + recordingTime: recording + })); + }; + + const standard = projectId => { + add(projectId, 0, 5000); + add(projectId, 5000, 5000); + }; + + add(130041250, 0, 2000); // floating blocks + add(130041250, 4000, 6000); + + add(14844969, 0, 2000); // scratch cats + add(14844969, 1000, 6000); + + standard(173918262); // bouncy heros + standard(155128646); // stacky build + standard(89811578); // solar system + standard(139193539); // pixel art maker + standard(187694931); // spiralgraph + standard(219313833); // sensing_touching benchmark + standard(236115215); // touching color benchmark + standard(238750909); // bob ross painting (heavy pen stamp) + + const frame = document.getElementsByTagName('iframe')[0]; + const runner = new BenchRunner({frame, suite}); + const resultsView = suiteView = new BenchSuiteResultView({runner}).render(); + + document.getElementsByClassName('suite-results')[0] + .appendChild(resultsView.dom); + + runner.run(); +}; diff --git a/local-scratch-vm/src/playground/video-sensing.html b/local-scratch-vm/src/playground/video-sensing.html new file mode 100644 index 0000000000000000000000000000000000000000..d8db0601beb527eea183bcb80fbb7669c23f6ee9 --- /dev/null +++ b/local-scratch-vm/src/playground/video-sensing.html @@ -0,0 +1,18 @@ + + + + Video Motion Test Playground + + + + + + + + + + + + + + diff --git a/local-scratch-vm/src/playground/video-sensing.js b/local-scratch-vm/src/playground/video-sensing.js new file mode 100644 index 0000000000000000000000000000000000000000..9493518a5aabfe3497c2c064acdb7e722a6da875 --- /dev/null +++ b/local-scratch-vm/src/playground/video-sensing.js @@ -0,0 +1,133 @@ +(function () { + const BENCHMARK_THROTTLE = 250; + const INTERVAL = 33; + + const video = document.createElement('video'); + navigator.getUserMedia({ + audio: false, + video: { + width: {min: 480, ideal: 640}, + height: {min: 360, ideal: 480} + } + }, stream => { + video.autoplay = true; + video.src = window.URL.createObjectURL(stream); + // Get the track to hint to the browser the stream needs to be running + // even though we don't add the video tag to the DOM. + stream.getTracks(); + video.addEventListener('play', () => { + video.width = video.videoWidth; + video.height = video.videoHeight; + }); + }, err => { + // eslint-disable-next-line no-console + console.log(err); + }); + + const VideoMotion = window.Scratch3VideoSensingDebug.VideoMotion; + const VideoMotionView = window.Scratch3VideoSensingDebug.VideoMotionView; + + // Create motion detector + const motion = new VideoMotion(); + + // Create debug views that will render different slices of how the detector + // uses a frame of input. + const OUTPUT = VideoMotionView.OUTPUT; + const outputKeys = Object.keys(OUTPUT); + const outputValues = Object.values(OUTPUT); + const views = outputValues + .map(output => new VideoMotionView(motion, output)); + const view = views[0]; + + const defaultViews = [OUTPUT.INPUT, OUTPUT.XY_CELL, OUTPUT.T_CELL, OUTPUT.UV_CELL]; + + // Add activation toggles for each debug view. + const activators = document.createElement('div'); + activators.style.userSelect = 'none'; + outputValues.forEach((output, index) => { + const checkboxLabel = document.createElement('label'); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = defaultViews.indexOf(output) !== -1; + const checkboxSpan = document.createElement('span'); + checkboxSpan.innerText = outputKeys[index]; + checkboxLabel.appendChild(checkbox); + checkboxLabel.appendChild(checkboxSpan); + + const _view = views[index]; + _view.canvas.style.display = checkbox.checked ? '' : 'none'; + _view.active = checkbox.checked; + checkbox.onchange = event => { + _view.canvas.style.display = checkbox.checked ? '' : 'none'; + _view.active = checkbox.checked; + event.preventDefault(); + return false; + }; + + activators.appendChild(checkboxLabel); + }); + document.body.appendChild(activators); + + // Add a text line to display milliseconds per frame, motion value, and + // motion direction + const textContainer = document.createElement('div'); + const textHeader = document.createElement('div'); + textHeader.innerText = 'duration (us) :: motion amount :: motion direction'; + textContainer.appendChild(textHeader); + const textEl = document.createElement('div'); + textEl.innerText = `0 :: 0 :: 0`; + textContainer.appendChild(textEl); + document.body.appendChild(textContainer); + let textTimer = Date.now(); + + // Add the motion debug views to the dom after the text line, so the text + // appears first. + views.forEach(_view => document.body.appendChild(_view.canvas)); + + // Create a temporary canvas the video will be drawn to so the video's + // bitmap data can be transformed into a TypeArray. + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = view.canvas.width; + tempCanvas.height = view.canvas.height; + const ctx = tempCanvas.getContext('2d'); + + const loop = function () { + const timeoutId = setTimeout(loop, INTERVAL); + + try { + // Get the bitmap data for the video frame + ctx.scale(-1, 1); + ctx.drawImage( + video, + 0, 0, video.width || video.clientWidth, video.height || video.clientHeight, + -tempCanvas.width, 0, tempCanvas.width, tempCanvas.height + ); + ctx.resetTransform(); + const data = ctx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); + + // Analyze the latest frame. + const b = performance.now(); + motion.addFrame(data.data); + motion.analyzeFrame(); + + // Every so often update the visible debug numbers with duration in + // microseconds, the amount of motion and the direction of the + // motion. + if (Date.now() - textTimer > BENCHMARK_THROTTLE) { + const e = performance.now(); + const analyzeDuration = ((e - b) * 1000).toFixed(0); + const motionAmount = motion.motionAmount.toFixed(1); + const motionDirection = motion.motionDirection.toFixed(1); + textEl.innerText = `${analyzeDuration} :: ${motionAmount} :: ${motionDirection}`; + textTimer = Date.now(); + } + views.forEach(_view => _view.active && _view.draw()); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error.stack || error); + clearTimeout(timeoutId); + } + }; + + loop(); +}()); diff --git a/local-scratch-vm/src/serialization/deserialize-assets.js b/local-scratch-vm/src/serialization/deserialize-assets.js new file mode 100644 index 0000000000000000000000000000000000000000..e587ebd11d13c4ead1b6559217396e135f1ba7bf --- /dev/null +++ b/local-scratch-vm/src/serialization/deserialize-assets.js @@ -0,0 +1,188 @@ +const JSZip = require('jszip'); +const log = require('../util/log'); + +/** + * Deserializes sound from file into storage cache so that it can + * be loaded into the runtime. + * @param {object} sound Descriptor for sound from sb3 file + * @param {Runtime} runtime The runtime containing the storage to cache the sounds in + * @param {JSZip} zip The zip containing the sound file being described by `sound` + * @param {string} assetFileName Optional file name for the given asset + * (sb2 files have filenames of the form [int].[ext], + * sb3 files have filenames of the form [md5].[ext]) + * @return {Promise} Promise that resolves after the described sound has been stored + * into the runtime storage cache, the sound was already stored, or an error has + * occurred. + */ +const deserializeSound = function (sound, runtime, zip, assetFileName) { + const fileName = assetFileName ? assetFileName : sound.md5; + const storage = runtime.storage; + if (!storage) { + log.warn('No storage module present; cannot load sound asset: ', fileName); + return Promise.resolve(null); + } + + if (!zip) { // Zip will not be provided if loading project json from server + return Promise.resolve(null); + } + + let soundFile = zip.file(fileName); + if (!soundFile) { + // look for assetfile in a flat list of files, or in a folder + const fileMatch = new RegExp(`^([^/]*/)?${fileName}$`); + soundFile = zip.file(fileMatch)[0]; // use first matching file + } + + if (!soundFile) { + log.error(`Could not find sound file associated with the ${sound.name} sound.`); + return Promise.resolve(null); + } + + if (!JSZip.support.uint8array) { + log.error('JSZip uint8array is not supported in this browser.'); + return Promise.resolve(null); + } + + let dataFormat = storage.DataFormat.WAV; + switch (sound.dataFormat.toLowerCase()) { + case "mp3": + dataFormat = storage.DataFormat.MP3; + break; + case "ogg": + dataFormat = storage.DataFormat.OGG; + break; + case "flac": + dataFormat = storage.DataFormat.FLAC; + break; + } + return soundFile.async('uint8array').then(data => storage.createAsset( + storage.AssetType.Sound, + dataFormat, + data, + null, + true + )) + .then(asset => { + sound.asset = asset; + sound.assetId = asset.assetId; + sound.md5 = `${asset.assetId}.${asset.dataFormat}`; + }); +}; + +/** + * Deserializes costume from file into storage cache so that it can + * be loaded into the runtime. + * @param {object} costume Descriptor for costume from sb3 file + * @param {Runtime} runtime The runtime containing the storage to cache the costumes in + * @param {JSZip} zip The zip containing the costume file being described by `costume` + * @param {string} assetFileName Optional file name for the given asset + * (sb2 files have filenames of the form [int].[ext], + * sb3 files have filenames of the form [md5].[ext]) + * @param {string} textLayerFileName Optional file name for the given asset's text layer + * (sb2 only; files have filenames of the form [int].png) + * @return {Promise} Promise that resolves after the described costume has been stored + * into the runtime storage cache, the costume was already stored, or an error has + * occurred. + */ +const deserializeCostume = function (costume, runtime, zip, assetFileName, textLayerFileName) { + const storage = runtime.storage; + const assetId = costume.assetId; + const fileName = assetFileName ? assetFileName : + `${assetId}.${costume.dataFormat}`; + + if (!storage) { + log.warn('No storage module present; cannot load costume asset: ', fileName); + return Promise.resolve(null); + } + + if (costume.asset) { + // When uploading a sprite from an image file, the asset data will be provided + // @todo Cache the asset data somewhere and pull it out here + return Promise.resolve(storage.createAsset( + costume.asset.assetType, + costume.asset.dataFormat, + new Uint8Array(Object.keys(costume.asset.data).map(key => costume.asset.data[key])), + null, + true + )).then(asset => { + costume.asset = asset; + costume.assetId = asset.assetId; + costume.md5 = `${asset.assetId}.${asset.dataFormat}`; + }); + } + + if (!zip) { + // Zip will not be provided if loading project json from server + return Promise.resolve(null); + } + + let costumeFile = zip.file(fileName); + if (!costumeFile) { + // look for assetfile in a flat list of files, or in a folder + const fileMatch = new RegExp(`^([^/]*/)?${fileName}$`); + costumeFile = zip.file(fileMatch)[0]; // use the first matched file + } + + if (!costumeFile) { + log.error(`Could not find costume file associated with the ${costume.name} costume.`); + return Promise.resolve(null); + } + let assetType = null; + const costumeFormat = costume.dataFormat.toLowerCase(); + if (costumeFormat === 'svg') { + assetType = storage.AssetType.ImageVector; + } else if (['png', 'bmp', 'jpeg', 'jpg', 'gif'].indexOf(costumeFormat) >= 0) { + assetType = storage.AssetType.ImageBitmap; + } else { + log.error(`Unexpected file format for costume: ${costumeFormat}`); + } + if (!JSZip.support.uint8array) { + log.error('JSZip uint8array is not supported in this browser.'); + return Promise.resolve(null); + } + + // textLayerMD5 exists if there is a text layer, which is a png of text from Scratch 1.4 + // that was opened in Scratch 2.0. In this case, set costume.textLayerAsset. + let textLayerFilePromise; + if (costume.textLayerMD5) { + const textLayerFile = zip.file(textLayerFileName); + if (!textLayerFile) { + log.error(`Could not find text layer file associated with the ${costume.name} costume.`); + return Promise.resolve(null); + } + textLayerFilePromise = textLayerFile.async('uint8array') + .then(data => storage.createAsset( + storage.AssetType.ImageBitmap, + 'png', + data, + costume.textLayerMD5 + )) + .then(asset => { + costume.textLayerAsset = asset; + }); + } else { + textLayerFilePromise = Promise.resolve(null); + } + + return Promise.all([textLayerFilePromise, + costumeFile.async('uint8array') + .then(data => storage.createAsset( + assetType, + // TODO eventually we want to map non-png's to their actual file types? + costumeFormat, + data, + null, + true + )) + .then(asset => { + costume.asset = asset; + costume.assetId = asset.assetId; + costume.md5 = `${asset.assetId}.${asset.dataFormat}`; + }) + ]); +}; + +module.exports = { + deserializeSound, + deserializeCostume +}; diff --git a/local-scratch-vm/src/serialization/extension patcher.js b/local-scratch-vm/src/serialization/extension patcher.js new file mode 100644 index 0000000000000000000000000000000000000000..842bc4e9274b1ef250f20f3efdd2008a2b4a8271 --- /dev/null +++ b/local-scratch-vm/src/serialization/extension patcher.js @@ -0,0 +1,60 @@ +class extensionsPatch { + /** + * constructor + * @param {Runtime} runtime runtime + */ + constructor (runtime) { + this.runtime = runtime; + this.extensions = {}; + this.loaded = []; + } + + /** + * replaces a core extension with a external extension for the loader + * @param {String} id extension id + * @param {String} url new extension url + * @param {Object} extensions sb3 loader extension object + */ + basicPatch (id, url, extensions) { + extensions.extensionURLs.set(id, url); + } + + /** + * runs the patch for an extension + * @param {String} id the extension to patch + * @param {Object} extensions the extensions object + * @param {Blocks} blocks all of the blocks + */ + runExtensionPatch (id, extensions, object) { + const patch = this.extensions[id]; + if (typeof patch === 'object') { + if (!this.loaded.includes(id)) { + // patch to fix added urls not loading + this.runtime.extensionManager.loadExtensionURL(patch.url); + this.loaded.push(id); + } + this.basicPatch(patch.id, patch.url, extensions); + return; + } + patch(extensions, object, this.runtime); + } + + /** + * registers extension patches to the patcher + * @param {Object} list a list of patches to register + */ + registerExtensions (list) { + this.extensions = Object.assign(this.extensions, list); + } + + /** + * gets if a patch exists for an extension + * @param {String} id the extension id to check + * @returns {Boolean} if the given extension exists + */ + patchExists (id) { + return !!this.extensions[id]; + } +} + +module.exports = extensionsPatch; diff --git a/local-scratch-vm/src/serialization/replacers patch.json b/local-scratch-vm/src/serialization/replacers patch.json new file mode 100644 index 0000000000000000000000000000000000000000..060080d4bd6c8d0abf136e2c1533f81954f32bc8 --- /dev/null +++ b/local-scratch-vm/src/serialization/replacers patch.json @@ -0,0 +1,623 @@ +{ + "variables": { + "_text": [ + "_text", + "" + ], + "_replacers": [ + "_replacers", + "[]" + ], + "_replacer": [ + "_replacer", + 1 + ], + "_rep": [ + "_rep", + "" + ] + }, + "blocks": { + "setReplacerToDefinition": { + "opcode": "procedures_definition", + "next": "setReplacersVarToReplacerJson", + "parent": null, + "inputs": { + "custom_block": [ + 1, + "setReplacerToDisplay" + ] + }, + "fields": {}, + "shadow": false, + "topLevel": true, + "x": 48, + "y": 64 + }, + "setReplacerToDisplay": { + "opcode": "procedures_prototype", + "next": null, + "parent": "setReplacerToDefinition", + "inputs": { + "REPLACER": [ + 1, + "setReplacerArgumentREPLACER" + ], + "VALUE": [ + 1, + "setReplacerArgumentVALUE" + ] + }, + "fields": {}, + "shadow": true, + "topLevel": false, + "mutation": { + "tagName": "mutation", + "children": [], + "proccode": "replacer %s to %s", + "argumentids": "[\"REPLACER\",\"VALUE\"]", + "argumentnames": "[\"REPLACER\",\"VALUE\"]", + "argumentdefaults": "[\"\",\"\",\"\"]", + "warp": "true", + "returns": "false", + "edited": "true" + } + }, + "setReplacerArgumentREPLACER": { + "opcode": "argument_reporter_string_number", + "next": null, + "parent": "REPLACER", + "inputs": {}, + "fields": { + "VALUE": [ + "REPLACER", + "REPLACER" + ] + }, + "shadow": true, + "topLevel": false + }, + "setReplacerArgumentVALUE": { + "opcode": "argument_reporter_string_number", + "next": null, + "parent": "VALUE", + "inputs": {}, + "fields": { + "VALUE": [ + "VALUE", + "VALUE" + ] + }, + "shadow": true, + "topLevel": false + }, + "setReplacersVarToReplacerJson": { + "opcode": "data_setvariableto", + "next": null, + "parent": "setReplacerToDefinition", + "inputs": { + "VALUE": [ + 3, + "setReplacerValueTo", + [ + 10, + "0" + ] + ] + }, + "fields": { + "VARIABLE": [ + "replacers", + "replacers" + ] + }, + "shadow": false, + "topLevel": false + }, + "setReplacerValueTo": { + "opcode": "jgJSON_setValueToKeyInJSON", + "next": null, + "parent": "setReplacersVarToReplacerJson", + "inputs": { + "KEY": [ + 3, + "leftAndMidleHalf", + [ + 10, + "key" + ] + ], + "VALUE": [ + 3, + "VALUE", + [ + 10, + "value" + ] + ], + "JSON": [ + 3, + [ + 12, + "replacers", + "replacers" + ], + [ + 10, + "{}" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "REPLACER": { + "opcode": "argument_reporter_string_number", + "next": null, + "parent": "rightHalf", + "inputs": {}, + "fields": { + "VALUE": [ + "REPLACER", + "REPLACER" + ] + }, + "shadow": false, + "topLevel": false + }, + "VALUE": { + "opcode": "argument_reporter_string_number", + "next": null, + "parent": "setReplacerValueTo", + "inputs": {}, + "fields": { + "VALUE": [ + "VALUE", + "VALUE" + ] + }, + "shadow": false, + "topLevel": false + }, + "replaceWithReplacersDefinition": { + "opcode": "procedures_definition_return", + "next": "defineTempReplacersList", + "parent": null, + "inputs": { + "custom_block": [ + 1, + "replaceWithReplacersDisplay" + ] + }, + "fields": {}, + "shadow": false, + "topLevel": true, + "x": 48, + "y": 280 + }, + "replaceWithReplacersDisplay": { + "opcode": "procedures_prototype", + "next": null, + "parent": "replaceWithReplacersDefinition", + "inputs": { + "STRING": [ + 1, + "STRING" + ] + }, + "fields": {}, + "shadow": true, + "topLevel": false, + "mutation": { + "tagName": "mutation", + "children": [], + "proccode": "replace %s with replacers", + "argumentids": "[\"STRING\"]", + "argumentnames": "[\"STRING\"]", + "argumentdefaults": "[\"\"]", + "warp": "false", + "returns": "true", + "edited": "true" + } + }, + "STRING": { + "opcode": "argument_reporter_string_number", + "next": null, + "parent": "replaceWithReplacersDisplay", + "inputs": {}, + "fields": { + "VALUE": [ + "STRING", + "STRING" + ] + }, + "shadow": true, + "topLevel": false + }, + "runThroughEachReplacer": { + "opcode": "control_for_each", + "next": "returnTheFinnalText", + "parent": "defineTempText", + "inputs": { + "VALUE": [ + 3, + "theNumberOfReplacers", + [ + 6, + "10" + ] + ], + "SUBSTACK": [ + 2, + "defineTempReplacerName" + ] + }, + "fields": { + "VARIABLE": [ + "_replacer", + "_replacer" + ] + }, + "shadow": false, + "topLevel": false + }, + "defineTempReplacersList": { + "opcode": "data_setvariableto", + "next": "defineTempText", + "parent": "replaceWithReplacersDefinition", + "inputs": { + "VALUE": [ + 3, + "getAllReplacers", + [ + 10, + "0" + ] + ] + }, + "fields": { + "VARIABLE": [ + "_replacers", + "_replacers" + ] + }, + "shadow": false, + "topLevel": false + }, + "getAllReplacers": { + "opcode": "jgJSON_json_keys", + "next": null, + "parent": "defineTempReplacersList", + "inputs": { + "json": [ + 3, + [ + 12, + "replacers", + "replacers" + ], + [ + 10, + "{}" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "rightHalf": { + "opcode": "operator_join", + "next": null, + "parent": "leftAndMidleHalf", + "inputs": { + "STRING1": [ + 1, + [ + 10, + "{" + ] + ], + "STRING2": [ + 3, + "REPLACER", + [ + 10, + "banana" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "leftAndMidleHalf": { + "opcode": "operator_join", + "next": null, + "parent": "setReplacerValueTo", + "inputs": { + "STRING1": [ + 3, + "rightHalf", + [ + 10, + "foo" + ] + ], + "STRING2": [ + 1, + [ + 10, + "}" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "theNumberOfReplacers": { + "opcode": "jgJSON_json_array_length", + "next": null, + "parent": "runThroughEachReplacer", + "inputs": { + "array": [ + 3, + [ + 12, + "_replacers", + "_replacers" + ], + [ + 10, + "[]" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "getCurrentReplacer": { + "opcode": "jgJSON_json_array_get", + "next": null, + "parent": "defineTempReplacerName", + "inputs": { + "array": [ + 3, + [ + 12, + "_replacers", + "_replacers" + ], + [ + 10, + "[\"A\", \"B\", \"C\"]" + ] + ], + "index": [ + 3, + "offsetIndexBy", + [ + 4, + "2" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "replaceAllReplacersWithValue": { + "opcode": "operator_replaceAll", + "next": null, + "parent": "replaceTextWithThisReplacer", + "inputs": { + "text": [ + 3, + [ + 12, + "_text", + "_text" + ], + [ + 10, + "foo bar" + ] + ], + "term": [ + 3, + [ + 12, + "_rep", + "_rep" + ], + [ + 10, + "foo" + ] + ], + "res": [ + 3, + "getReplacerValueFromReplacerList", + [ + 10, + "bar" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "defineTempReplacerName": { + "opcode": "data_setvariableto", + "next": "replaceTextWithThisReplacer", + "parent": "runThroughEachReplacer", + "inputs": { + "VALUE": [ + 3, + "getCurrentReplacer", + [ + 10, + "0" + ] + ] + }, + "fields": { + "VARIABLE": [ + "_rep", + "_rep" + ] + }, + "shadow": false, + "topLevel": false + }, + "getReplacerValueFromReplacerList": { + "opcode": "jgJSON_getValueFromJSON", + "next": null, + "parent": "replaceAllReplacersWithValue", + "inputs": { + "VALUE": [ + 3, + [ + 12, + "_rep", + "_rep" + ], + [ + 10, + "key" + ] + ], + "JSON": [ + 3, + [ + 12, + "replacers", + "replacers" + ], + [ + 10, + "{\"key\": \"value\"}" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "defineTempText": { + "opcode": "data_setvariableto", + "next": "runThroughEachReplacer", + "parent": "defineTempReplacersList", + "inputs": { + "VALUE": [ + 3, + "STRINGargument", + [ + 10, + "0" + ] + ] + }, + "fields": { + "VARIABLE": [ + "_text", + "_text" + ] + }, + "shadow": false, + "topLevel": false + }, + "STRINGargument": { + "opcode": "argument_reporter_string_number", + "next": null, + "parent": "defineTempText", + "inputs": {}, + "fields": { + "VALUE": [ + "STRING", + "STRING" + ] + }, + "shadow": false, + "topLevel": false + }, + "replaceTextWithThisReplacer": { + "opcode": "data_setvariableto", + "next": null, + "parent": "defineTempReplacerName", + "inputs": { + "VALUE": [ + 3, + "replaceAllReplacersWithValue", + [ + 10, + "0" + ] + ] + }, + "fields": { + "VARIABLE": [ + "_text", + "_text" + ] + }, + "shadow": false, + "topLevel": false + }, + "returnTheFinnalText": { + "opcode": "procedures_return", + "next": null, + "parent": "runThroughEachReplacer", + "inputs": { + "return": [ + 3, + [ + 12, + "_text", + "_text" + ], + [ + 10, + "1" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + }, + "offsetIndexBy": { + "opcode": "operator_subtract", + "next": null, + "parent": "getCurrentReplacer", + "inputs": { + "NUM1": [ + 3, + [ + 12, + "_replacer", + "_replacer" + ], + [ + 4, + "" + ] + ], + "NUM2": [ + 1, + [ + 4, + "1" + ] + ] + }, + "fields": {}, + "shadow": false, + "topLevel": false + } + } +} \ No newline at end of file diff --git a/local-scratch-vm/src/serialization/sb2.js b/local-scratch-vm/src/serialization/sb2.js new file mode 100644 index 0000000000000000000000000000000000000000..3c5abdd30e40dbf55230b388443a5168c387b0a5 --- /dev/null +++ b/local-scratch-vm/src/serialization/sb2.js @@ -0,0 +1,1355 @@ +/** + * @fileoverview + * Partial implementation of an SB2 JSON importer. + * Parses provided JSON and then generates all needed + * scratch-vm runtime structures. + */ + +const Blocks = require('../engine/blocks'); +const RenderedTarget = require('../sprites/rendered-target'); +const Sprite = require('../sprites/sprite'); +const Color = require('../util/color'); +const log = require('../util/log'); +const uid = require('../util/uid'); +const StringUtil = require('../util/string-util'); +const MathUtil = require('../util/math-util'); +const specMap = require('./sb2_specmap'); +const Comment = require('../engine/comment'); +const Variable = require('../engine/variable'); +const MonitorRecord = require('../engine/monitor-record'); +const StageLayering = require('../engine/stage-layering'); +const ScratchXUtilities = require('../extension-support/tw-scratchx-utilities'); + +const {loadCostume} = require('../import/load-costume.js'); +const {loadSound} = require('../import/load-sound.js'); +const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js'); + +// Constants used during deserialization of an SB2 file +const CORE_EXTENSIONS = [ + 'argument', + 'control', + 'data', + 'event', + 'looks', + 'math', + 'motion', + 'operator', + 'procedures', + 'sensing', + 'sound' +]; + +// Adjust script coordinates to account for +// larger block size in scratch-blocks. +// @todo: Determine more precisely the right formulas here. +const WORKSPACE_X_SCALE = 1.5; +const WORKSPACE_Y_SCALE = 2.2; + +// By examining ScratchX projects, we've found that ScratchX can use either "\u001f" or "." +// to separate the extension name from the extension method opcode eg. "Text To Speech.say" +// eslint-disable-next-line no-control-regex +const SCRATCHX_OPCODE_SEPARATOR = /\u001f|\./; + +/** + * @param {string} opcode + * @returns {boolean} + */ +const isPossiblyScratchXBlock = opcode => SCRATCHX_OPCODE_SEPARATOR.test(opcode); + +/** + * @param {string} opcode + * @returns {string} + */ +const mapScratchXOpcode = opcode => { + const [extensionName, extensionMethod] = opcode.split(SCRATCHX_OPCODE_SEPARATOR); + const newOpcodeBase = ScratchXUtilities.generateExtensionId(extensionName); + return `${newOpcodeBase}_${extensionMethod}`; +}; + +/** + * @param {object} block + * @returns {object} + */ +const mapScratchXBlock = block => { + const opcode = block[0]; + const argumentCount = block.length - 1; + const args = []; + for (let i = 0; i < argumentCount; i++) { + args.push({ + type: 'input', + inputOp: 'text', + inputName: ScratchXUtilities.argumentIndexToId(i) + }); + } + return { + opcode: mapScratchXOpcode(opcode), + argMap: args + }; +}; + +/** + * Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n") + * into an argument map. This allows us to provide the expected inputs + * to a mutated procedure call. + * @param {string} procCode Scratch 2.0 procedure string. + * @return {object} Argument map compatible with those in sb2specmap. + */ +const parseProcedureArgMap = function (procCode) { + const argMap = [ + {} // First item in list is op string. + ]; + const INPUT_PREFIX = 'input'; + let inputCount = 0; + // Split by %n, %b, %s. + const parts = procCode.split(/(?=[^\\]%[nbs])/); + for (let i = 0; i < parts.length; i++) { + const part = parts[i].trim(); + if (part.substring(0, 1) === '%') { + const argType = part.substring(1, 2); + const arg = { + type: 'input', + inputName: INPUT_PREFIX + (inputCount++) + }; + if (argType === 'n') { + arg.inputOp = 'math_number'; + } else if (argType === 's') { + arg.inputOp = 'text'; + } else if (argType === 'b') { + arg.inputOp = 'boolean'; + } + argMap.push(arg); + } + } + return argMap; +}; + +/** + * Generate a list of "argument IDs" for procdefs and caller mutations. + * IDs just end up being `input0`, `input1`, ... which is good enough. + * @param {string} procCode Scratch 2.0 procedure string. + * @return {Array.} Array of argument id strings. + */ +const parseProcedureArgIds = function (procCode) { + return parseProcedureArgMap(procCode) + .map(arg => arg.inputName) + .filter(name => name); // Filter out unnamed inputs which are labels +}; + +/** + * Flatten a block tree into a block list. + * Children are temporarily stored on the `block.children` property. + * @param {Array.} blocks list generated by `parseBlockList`. + * @return {Array.} Flattened list to be passed to `blocks.createBlock`. + */ +const flatten = function (blocks) { + let finalBlocks = []; + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + finalBlocks.push(block); + if (block.children) { + finalBlocks = finalBlocks.concat(flatten(block.children)); + } + delete block.children; + } + return finalBlocks; +}; + +/** + * Parse any list of blocks from SB2 JSON into a list of VM-format blocks. + * Could be used to parse a top-level script, + * a list of blocks in a branch (e.g., in forever), + * or a list of blocks in an argument (e.g., move [pick random...]). + * @param {Array.} blockList SB2 JSON-format block list. + * @param {Function} addBroadcastMsg function to update broadcast message name map + * @param {Function} getVariableId function to retreive a variable's ID based on name + * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. + * @param {ParseState} parseState - info on the state of parsing beyond the current block. + * @param {object} comments - Comments from sb2 project that need to be attached to blocks. + * They are indexed in this object by the sb2 flattened block list index indicating + * which block they should attach to. + * @param {int} commentIndex The current index of the top block in this list if it were in a flattened + * list of all blocks for the target + * @return {Array|int>} Tuple where first item is the Scratch VM-format block list, and + * second item is the updated comment index + */ +const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions, parseState, comments, + commentIndex) { + const resultingList = []; + let previousBlock = null; // For setting next. + for (let i = 0; i < blockList.length; i++) { + const block = blockList[i]; + // eslint-disable-next-line no-use-before-define + const parsedBlockAndComments = parseBlock(block, addBroadcastMsg, getVariableId, + extensions, parseState, comments, commentIndex); + const parsedBlock = parsedBlockAndComments[0]; + // Update commentIndex + commentIndex = parsedBlockAndComments[1]; + + if (!parsedBlock) continue; + if (previousBlock) { + parsedBlock.parent = previousBlock.id; + previousBlock.next = parsedBlock.id; + } + previousBlock = parsedBlock; + resultingList.push(parsedBlock); + } + return [resultingList, commentIndex]; +}; + +/** + * Parse a Scratch object's scripts into VM blocks. + * This should only handle top-level scripts that include X, Y coordinates. + * @param {!object} scripts Scripts object from SB2 JSON. + * @param {!Blocks} blocks Blocks object to load parsed blocks into. + * @param {Function} addBroadcastMsg function to update broadcast message name map + * @param {Function} getVariableId function to retreive a variable's ID based on name + * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. + * @param {object} comments Comments that need to be attached to the blocks that need to be parsed + */ +const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions, comments) { + // Keep track of the index of the current script being + // parsed in order to attach block comments correctly + let scriptIndexForComment = 0; + + for (let i = 0; i < scripts.length; i++) { + const script = scripts[i]; + const scriptX = script[0]; + const scriptY = script[1]; + const blockList = script[2]; + const parseState = {}; + const [parsedBlockList, newCommentIndex] = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions, + parseState, comments, scriptIndexForComment); + scriptIndexForComment = newCommentIndex; + if (parsedBlockList[0]) { + parsedBlockList[0].x = scriptX * WORKSPACE_X_SCALE; + parsedBlockList[0].y = scriptY * WORKSPACE_Y_SCALE; + parsedBlockList[0].topLevel = true; + parsedBlockList[0].parent = null; + } + // Flatten children and create add the blocks. + const convertedBlocks = flatten(parsedBlockList); + for (let j = 0; j < convertedBlocks.length; j++) { + blocks.createBlock(convertedBlocks[j]); + } + } +}; + +/** + * Create a callback for assigning fixed IDs to imported variables + * Generator stores the global variable mapping in a closure + * @param {!string} targetId the id of the target to scope the variable to + * @return {string} variable ID + */ +const generateVariableIdGetter = (function () { + let globalVariableNameMap = {}; + const namer = (targetId, name, type) => `${targetId}-${StringUtil.replaceUnsafeChars(name)}-${type}`; + return function (targetId, topLevel) { + // Reset the global variable map if topLevel + if (topLevel) globalVariableNameMap = {}; + return function (name, type) { + if (topLevel) { // Store the name/id pair in the globalVariableNameMap + globalVariableNameMap[`${name}-${type}`] = namer(targetId, name, type); + return globalVariableNameMap[`${name}-${type}`]; + } + // Not top-level, so first check the global name map + if (globalVariableNameMap[`${name}-${type}`]) return globalVariableNameMap[`${name}-${type}`]; + return namer(targetId, name, type); + }; + }; +}()); + +const globalBroadcastMsgStateGenerator = (function () { + let broadcastMsgNameMap = {}; + const allBroadcastFields = []; + const emptyStringName = uid(); + return function (topLevel) { + if (topLevel) broadcastMsgNameMap = {}; + return { + broadcastMsgMapUpdater: function (name, field) { + name = name.toLowerCase(); + if (name === '') { + name = emptyStringName; + } + broadcastMsgNameMap[name] = `broadcastMsgId-${StringUtil.replaceUnsafeChars(name)}`; + allBroadcastFields.push(field); + return broadcastMsgNameMap[name]; + }, + globalBroadcastMsgs: broadcastMsgNameMap, + allBroadcastFields: allBroadcastFields, + emptyMsgName: emptyStringName + }; + }; +}()); + +/** + * Parse a single monitor object and create all its in-memory VM objects. + * + * It is important that monitors are parsed last, + * - after all sprite targets have finished parsing, and + * - after the rest of the stage has finished parsing. + * + * It is specifically important that all the scripts in the project + * have been parsed and all the relevant targets exist, have uids, + * and have their variables initialized. + * Calling this function before these things are true, will result in + * undefined behavior. + * @param {!object} object - From-JSON "Monitor object" + * @param {!Runtime} runtime - (in/out) Runtime object to load monitor info into. + * @param {!Array.} targets - Targets have already been parsed. + * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. + */ + +const parseMonitorObject = (object, runtime, targets, extensions) => { + // If we can't find the block in the spec map, ignore it. + // This happens for things like Lego Wedo 1.0 monitors. + const mapped = specMap[object.cmd]; + if (!mapped) { + log.warn(`Could not find monitor block with opcode: ${object.cmd}`); + return; + } + // In scratch 2.0, there are two monitors that now correspond to extension + // blocks (tempo and video motion/direction). In the case of the + // video motion/direction block, this reporter is not monitorable in Scratch 3.0. + // In the case of the tempo block, we should import it and load the music extension + // only when the monitor is actually visible. + + const opcode = specMap[object.cmd].opcode; + const extIndex = opcode.indexOf('_'); + const extID = opcode.substring(0, extIndex); + + if (extID === 'videoSensing') { + return; + } else if (CORE_EXTENSIONS.indexOf(extID) === -1 && extID !== '' && + !extensions.extensionIDs.has(extID) && !object.visible) { + // Don't import this monitor if it refers to a non-core extension that + // doesn't exist anywhere else in the project and it isn't visible. + // This should only apply to the tempo block at this point since + // there are no other sb2 blocks that are now extension monitors. + return; + } + + let target = null; + // List blocks don't come in with their target name set. + // Find the target by searching for a target with matching variable name/type. + if (!object.hasOwnProperty('target')) { + for (let i = 0; i < targets.length; i++) { + const currTarget = targets[i]; + const listVariables = Object.keys(currTarget.variables).filter(key => { + const variable = currTarget.variables[key]; + return variable.type === Variable.LIST_TYPE && variable.name === object.listName; + }); + if (listVariables.length > 0) { + target = currTarget; // Keep this target for later use + object.target = currTarget.getName(); // Set target name to normalize with other monitors + } + } + } + + // Get the target for this monitor, if not gotten above. + target = target || targets.filter(t => t.getName() === object.target)[0]; + if (!target) throw new Error('Cannot create monitor for target that cannot be found by name'); + + // Create var id getter to make block naming/parsing easier, variables already created. + const getVariableId = generateVariableIdGetter(target.id, false); + // eslint-disable-next-line no-use-before-define + const [block, _] = parseBlock( + [object.cmd, object.param], // Scratch 2 monitor blocks only have one param. + null, // `addBroadcastMsg`, not needed for monitor blocks. + getVariableId, + extensions, + {}, + null, // `comments`, not needed for monitor blocks + null // `commentIndex`, not needed for monitor blocks + ); + + // Monitor blocks have special IDs to match the toolbox obtained from the getId + // function in the runtime.monitorBlocksInfo. Variable monitors, however, + // get their IDs from the variable id they reference. + if (object.cmd === 'getVar:') { + block.id = getVariableId(object.param, Variable.SCALAR_TYPE); + } else if (object.cmd === 'contentsOfList:') { + block.id = getVariableId(object.param, Variable.LIST_TYPE); + } else if (runtime.monitorBlockInfo.hasOwnProperty(block.opcode)) { + block.id = runtime.monitorBlockInfo[block.opcode].getId(target.id, block.fields); + } else { + // If the opcode can't be found in the runtime monitorBlockInfo, + // then default to using the block opcode as the id instead. + // This is for extension monitors, and assumes that extension monitors + // cannot be sprite specific. + block.id = block.opcode; + } + + // Block needs a targetId if it is targetting something other than the stage + block.targetId = target.isStage ? null : target.id; + + // Property required for running monitored blocks. + block.isMonitored = object.visible; + + const existingMonitorBlock = runtime.monitorBlocks._blocks[block.id]; + if (existingMonitorBlock) { + // A monitor block already exists if the toolbox has been loaded and + // the monitor block is not target specific (because the block gets recycled). + // Update the existing block with the relevant monitor information. + existingMonitorBlock.isMonitored = object.visible; + existingMonitorBlock.targetId = block.targetId; + } else { + // Blocks can be created with children, flatten and add to monitorBlocks. + const newBlocks = flatten([block]); + for (let i = 0; i < newBlocks.length; i++) { + runtime.monitorBlocks.createBlock(newBlocks[i]); + } + } + + // Convert numbered mode into strings for better understandability. + switch (object.mode) { + case 1: + object.mode = 'default'; + break; + case 2: + object.mode = 'large'; + break; + case 3: + object.mode = 'slider'; + break; + } + + // Create a monitor record for the runtime's monitorState + runtime.requestAddMonitor(MonitorRecord({ + id: block.id, + targetId: block.targetId, + spriteName: block.targetId ? object.target : null, + opcode: block.opcode, + params: runtime.monitorBlocks._getBlockParams(block), + value: '', + mode: object.mode, + sliderMin: object.sliderMin, + sliderMax: object.sliderMax, + isDiscrete: object.isDiscrete, + x: object.x, + y: object.y, + width: object.width, + height: object.height, + visible: object.visible + })); +}; + +/** + * Parse the assets of a single "Scratch object" and load them. This + * preprocesses objects to support loading the data for those assets over a + * network while the objects are further processed into Blocks, Sprites, and a + * list of needed Extensions. + * @param {!object} object - From-JSON "Scratch object:" sprite, stage, watcher. + * @param {!Runtime} runtime - Runtime object to load all structures into. + * @param {boolean} topLevel - Whether this is the top-level object (stage). + * @param {?object} zip - Optional zipped assets for local file import + * @return {?{costumePromises:Array.,soundPromises:Array.,soundBank:SoundBank,children:object}} + * Object of arrays of promises and child objects for asset objects used in + * Sprites. As well as a SoundBank for the sound assets. null for unsupported + * objects. + */ +const parseScratchAssets = function (object, runtime, topLevel, zip) { + if (!object.hasOwnProperty('objName')) { + // Skip parsing monitors. Or any other objects missing objName. + return null; + } + + const assets = { + costumePromises: [], + soundPromises: [], + soundBank: runtime.audioEngine && runtime.audioEngine.createBank(), + children: [] + }; + + // Costumes from JSON. + const costumePromises = assets.costumePromises; + if (object.hasOwnProperty('costumes')) { + for (let i = 0; i < object.costumes.length; i++) { + const costumeSource = object.costumes[i]; + const bitmapResolution = costumeSource.bitmapResolution || 1; + const costume = { + name: costumeSource.costumeName, + bitmapResolution: bitmapResolution, + rotationCenterX: topLevel ? 240 * bitmapResolution : costumeSource.rotationCenterX, + rotationCenterY: topLevel ? 180 * bitmapResolution : costumeSource.rotationCenterY, + // TODO we eventually want this next property to be called + // md5ext to reflect what it actually contains, however this + // will be a very extensive change across many repositories + // and should be done carefully and altogether + md5: costumeSource.baseLayerMD5, + skinId: null + }; + const md5ext = costumeSource.baseLayerMD5; + const idParts = StringUtil.splitFirst(md5ext, '.'); + const md5 = idParts[0]; + let ext; + if (idParts.length === 2 && idParts[1]) { + ext = idParts[1]; + } else { + // Default to 'png' if baseLayerMD5 is not formatted correctly + ext = 'png'; + // Fix costume md5 for later + costume.md5 = `${costume.md5}.${ext}`; + } + costume.dataFormat = ext; + costume.assetId = md5; + if (costumeSource.textLayerMD5) { + costume.textLayerMD5 = StringUtil.splitFirst(costumeSource.textLayerMD5, '.')[0]; + } + // If there is no internet connection, or if the asset is not in storage + // for some reason, and we are doing a local .sb2 import, (e.g. zip is provided) + // the file name of the costume should be the baseLayerID followed by the file ext + const assetFileName = `${costumeSource.baseLayerID}.${ext}`; + const textLayerFileName = costumeSource.textLayerID ? `${costumeSource.textLayerID}.png` : null; + costumePromises.push(deserializeCostume(costume, runtime, zip, assetFileName, textLayerFileName) + .then(() => loadCostume(costume.md5, costume, runtime, 2 /* optVersion */)) + ); + } + } + // Sounds from JSON + const {soundBank, soundPromises} = assets; + if (object.hasOwnProperty('sounds')) { + for (let s = 0; s < object.sounds.length; s++) { + const soundSource = object.sounds[s]; + const sound = { + name: soundSource.soundName, + format: soundSource.format, + rate: soundSource.rate, + sampleCount: soundSource.sampleCount, + // TODO we eventually want this next property to be called + // md5ext to reflect what it actually contains, however this + // will be a very extensive change across many repositories + // and should be done carefully and altogether + // (for example, the audio engine currently relies on this + // property to be named 'md5') + md5: soundSource.md5, + data: null + }; + const md5ext = soundSource.md5; + const idParts = StringUtil.splitFirst(md5ext, '.'); + const md5 = idParts[0]; + const ext = idParts[1].toLowerCase(); + sound.dataFormat = ext; + sound.assetId = md5; + // If there is no internet connection, or if the asset is not in storage + // for some reason, and we are doing a local .sb2 import, (e.g. zip is provided) + // the file name of the sound should be the soundID (provided from the project.json) + // followed by the file ext + const assetFileName = `${soundSource.soundID}.${ext}`; + soundPromises.push( + deserializeSound(sound, runtime, zip, assetFileName) + .then(() => loadSound(sound, runtime, soundBank)) + ); + } + } + + // The stage will have child objects; recursively process them. + const childrenAssets = assets.children; + if (object.children) { + for (let m = 0; m < object.children.length; m++) { + childrenAssets.push(parseScratchAssets(object.children[m], runtime, false, zip)); + } + } + + return assets; +}; + +/** + * Parse a single "Scratch object" and create all its in-memory VM objects. + * TODO: parse the "info" section, especially "savedExtensions" + * @param {!object} object - From-JSON "Scratch object:" sprite, stage, watcher. + * @param {!Runtime} runtime - Runtime object to load all structures into. + * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. + * @param {boolean} topLevel - Whether this is the top-level object (stage). + * @param {?object} zip - Optional zipped assets for local file import + * @param {object} assets - Promises for assets of this scratch object grouped + * into costumes and sounds + * @return {!Promise.>} Promise for the loaded targets when ready, or null for unsupported objects. + */ +const parseScratchObject = function (object, runtime, extensions, topLevel, zip, assets) { + if (!object.hasOwnProperty('objName')) { + if (object.hasOwnProperty('listName')) { + // Shim these objects so they can be processed as monitors + object.cmd = 'contentsOfList:'; + object.param = object.listName; + object.mode = 'list'; + } + // Defer parsing monitors until targets are all parsed + object.deferredMonitor = true; + return Promise.resolve(object); + } + + // Blocks container for this object. + const blocks = new Blocks(runtime); + // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite. + const sprite = new Sprite(blocks, runtime); + // Sprite/stage name from JSON. + if (object.hasOwnProperty('objName')) { + if (topLevel && object.objName !== 'Stage') { + for (const child of object.children) { + if (!child.hasOwnProperty('objName') && child.target === object.objName) { + child.target = 'Stage'; + } + } + object.objName = 'Stage'; + } + + sprite.name = object.objName; + } + // Costumes from JSON. + const costumePromises = assets.costumePromises; + // Sounds from JSON + const {soundBank, soundPromises} = assets; + + // Create the first clone, and load its run-state from JSON. + const target = sprite.createClone(topLevel ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER); + + const getVariableId = generateVariableIdGetter(target.id, topLevel); + + const globalBroadcastMsgObj = globalBroadcastMsgStateGenerator(topLevel); + const addBroadcastMsg = globalBroadcastMsgObj.broadcastMsgMapUpdater; + + // Load target properties from JSON. + if (object.hasOwnProperty('variables')) { + for (let j = 0; j < object.variables.length; j++) { + const variable = object.variables[j]; + // A variable is a cloud variable if: + // - the project says it's a cloud variable, and + // - it's a stage variable, and + // - the runtime can support another cloud variable + const isCloud = variable.isPersistent && topLevel && runtime.canAddCloudVariable(); + const newVariable = new Variable( + getVariableId(variable.name, Variable.SCALAR_TYPE), + variable.name, + Variable.SCALAR_TYPE, + isCloud + ); + if (isCloud) runtime.addCloudVariable(); + newVariable.value = variable.value; + target.variables[newVariable.id] = newVariable; + } + } + + // If included, parse any and all comments on the object (this includes top-level + // workspace comments as well as comments attached to specific blocks) + const blockComments = {}; + if (object.hasOwnProperty('scriptComments')) { + const comments = object.scriptComments.map(commentDesc => { + const [ + commentX, + commentY, + commentWidth, + commentHeight, + commentFullSize, + flattenedBlockIndex, + commentText + ] = commentDesc; + const isBlockComment = commentDesc[5] >= 0; + const newComment = new Comment( + null, // generate a new id for this comment + commentText, // text content of sb2 comment + // Only serialize x & y position of comment if it's a workspace comment + // If it's a block comment, we'll let scratch-blocks handle positioning + isBlockComment ? null : commentX * WORKSPACE_X_SCALE, + isBlockComment ? null : commentY * WORKSPACE_Y_SCALE, + commentWidth * WORKSPACE_X_SCALE, + commentHeight * WORKSPACE_Y_SCALE, + !commentFullSize + ); + if (isBlockComment) { + // commentDesc[5] refers to the index of the block that this + // comment is attached to -- in a flattened version of the + // scripts array. + // If commentDesc[5] is -1, this is a workspace comment (we don't need to do anything + // extra at this point), otherwise temporarily save the flattened script array + // index as the blockId property of the new comment. We will + // change this to refer to the actual block id of the corresponding + // block when that block gets created + newComment.blockId = flattenedBlockIndex; + // Add this comment to the block comments object with its script index + // as the key + if (blockComments.hasOwnProperty(flattenedBlockIndex)) { + blockComments[flattenedBlockIndex].push(newComment); + } else { + blockComments[flattenedBlockIndex] = [newComment]; + } + } + return newComment; + }); + + // Add all the comments that were just created to the target.comments, + // referenced by id + comments.forEach(comment => { + target.comments[comment.id] = comment; + }); + } + + // If included, parse any and all scripts/blocks on the object. + if (object.hasOwnProperty('scripts')) { + parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions, blockComments); + } + + // If there are any comments referring to a numerical block ID, make them + // workspace comments. These are comments that were originally created as + // block comments, detached from the block, and then had the associated + // block deleted. + // These comments should be imported as workspace comments + // by making their blockIDs (which currently refer to non-existing blocks) + // null (See #1452). + for (const commentIndex in blockComments) { + const currBlockComments = blockComments[commentIndex]; + currBlockComments.forEach(c => { + if (typeof c.blockId === 'number') { + c.blockId = null; + } + }); + } + + // Update stage specific blocks (e.g. sprite clicked <=> stage clicked) + blocks.updateTargetSpecificBlocks(topLevel); // topLevel = isStage + + if (object.hasOwnProperty('lists')) { + for (let k = 0; k < object.lists.length; k++) { + const list = object.lists[k]; + const newVariable = new Variable( + getVariableId(list.listName, Variable.LIST_TYPE), + list.listName, + Variable.LIST_TYPE, + false + ); + newVariable.value = list.contents; + target.variables[newVariable.id] = newVariable; + } + } + if (object.hasOwnProperty('scratchX')) { + target.x = object.scratchX; + } + if (object.hasOwnProperty('scratchY')) { + target.y = object.scratchY; + } + if (object.hasOwnProperty('direction')) { + target.direction = object.direction; + } + if (object.hasOwnProperty('isDraggable')) { + target.draggable = object.isDraggable; + } + if (object.hasOwnProperty('scale')) { + // SB2 stores as 1.0 = 100%; we use % in the VM. + target.size = object.scale * 100; + } + if (object.hasOwnProperty('visible')) { + target.visible = object.visible; + } + if (object.hasOwnProperty('currentCostumeIndex')) { + // Current costume index can sometimes be a floating + // point number, use Math.floor to come up with an appropriate index + // and clamp it to the actual number of costumes the object has for good measure. + target.currentCostume = MathUtil.clamp(Math.floor(object.currentCostumeIndex), 0, object.costumes.length - 1); + } + if (object.hasOwnProperty('rotationStyle')) { + if (object.rotationStyle === 'none') { + target.rotationStyle = RenderedTarget.ROTATION_STYLE_NONE; + } else if (object.rotationStyle === 'leftRight') { + target.rotationStyle = RenderedTarget.ROTATION_STYLE_LEFT_RIGHT; + } else if (object.rotationStyle === 'upDown') { + target.rotationStyle = RenderedTarget.ROTATION_STYLE_UP_DOWN; + } else if (object.rotationStyle === 'lookAt') { + target.rotationStyle = RenderedTarget.ROTATION_STYLE_LOOK_AT; + } else if (object.rotationStyle === 'normal') { + target.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND; + } + } + if (object.hasOwnProperty('tempoBPM')) { + target.tempo = object.tempoBPM; + } + if (object.hasOwnProperty('videoAlpha')) { + // SB2 stores alpha as opacity, where 1.0 is opaque. + // We convert to a percentage, and invert it so 100% is full transparency. + target.videoTransparency = 100 - (100 * object.videoAlpha); + } + if (object.hasOwnProperty('info')) { + if (object.info.hasOwnProperty('videoOn')) { + if (object.info.videoOn) { + target.videoState = RenderedTarget.VIDEO_STATE.ON; + } else { + target.videoState = RenderedTarget.VIDEO_STATE.OFF; + } + } + } + if (object.hasOwnProperty('indexInLibrary')) { + // Temporarily store the 'indexInLibrary' property from the sb2 file + // so that we can correctly order sprites in the target pane. + // This will be deleted after we are done parsing and ordering the targets list. + target.targetPaneOrder = object.indexInLibrary; + } + + target.isStage = topLevel; + + Promise.all(costumePromises).then(costumes => { + sprite.costumes = costumes; + }); + + Promise.all(soundPromises).then(sounds => { + sprite.sounds = sounds; + // Make sure if soundBank is undefined, sprite.soundBank is then null. + sprite.soundBank = soundBank || null; + }); + + // The stage will have child objects; recursively process them. + const childrenPromises = []; + if (object.children) { + for (let m = 0; m < object.children.length; m++) { + childrenPromises.push( + parseScratchObject(object.children[m], runtime, extensions, false, zip, assets.children[m]) + ); + } + } + + // Parse extension list from ScratchX projects. + if (topLevel) { + const savedExtensions = object.info && object.info.savedExtensions; + if (Array.isArray(savedExtensions)) { + for (const extension of savedExtensions) { + const id = ScratchXUtilities.generateExtensionId(extension.extensionName); + const url = extension.javascriptURL; + extensions.extensionURLs.set(id, url); + } + } + } + + return Promise.all( + costumePromises.concat(soundPromises) + ).then(() => + Promise.all( + childrenPromises + ).then(children => { + // Need create broadcast msgs as variables after + // all other targets have finished processing. + if (target.isStage) { + const allBroadcastMsgs = globalBroadcastMsgObj.globalBroadcastMsgs; + const allBroadcastMsgFields = globalBroadcastMsgObj.allBroadcastFields; + const oldEmptyMsgName = globalBroadcastMsgObj.emptyMsgName; + if (allBroadcastMsgs[oldEmptyMsgName]) { + // Find a fresh 'messageN' + let currIndex = 1; + while (allBroadcastMsgs[`message${currIndex}`]) { + currIndex += 1; + } + const newEmptyMsgName = `message${currIndex}`; + // Add the new empty message name to the broadcast message + // name map, and assign it the old id. + // Then, delete the old entry in map. + allBroadcastMsgs[newEmptyMsgName] = allBroadcastMsgs[oldEmptyMsgName]; + delete allBroadcastMsgs[oldEmptyMsgName]; + // Now update all the broadcast message fields with + // the new empty message name. + for (let i = 0; i < allBroadcastMsgFields.length; i++) { + if (allBroadcastMsgFields[i].value === '') { + allBroadcastMsgFields[i].value = newEmptyMsgName; + } + } + } + // Traverse the broadcast message name map and create + // broadcast messages as variables on the stage (which is this + // target). + for (const msgName in allBroadcastMsgs) { + const msgId = allBroadcastMsgs[msgName]; + const newMsg = new Variable( + msgId, + msgName, + Variable.BROADCAST_MESSAGE_TYPE, + false + ); + target.variables[newMsg.id] = newMsg; + } + } + let targets = [target]; + const deferredMonitors = []; + for (let n = 0; n < children.length; n++) { + if (children[n]) { + if (children[n].deferredMonitor) { + deferredMonitors.push(children[n]); + } else { + targets = targets.concat(children[n]); + } + } + } + // It is important that monitors are parsed last + // - after all sprite targets have finished parsing + // - and this is the last thing that happens in the stage parsing + // It is specifically important that all the scripts in the project + // have been parsed and all the relevant targets exist, have uids, + // and have their variables initialized. + for (let n = 0; n < deferredMonitors.length; n++) { + parseMonitorObject(deferredMonitors[n], runtime, targets, extensions); + } + return targets; + }) + ); +}; + +const reorderParsedTargets = function (targets) { + // Reorder parsed targets based on the temporary targetPaneOrder property + // and then delete it. + + const reordered = targets.map((t, index) => { + t.layerOrder = index; + return t; + }).sort((a, b) => a.targetPaneOrder - b.targetPaneOrder); + + // Delete the temporary target pane ordering since we shouldn't need it anymore. + reordered.forEach(t => { + delete t.targetPaneOrder; + }); + + return reordered; +}; + + +/** + * Top-level handler. Parse provided JSON, + * and process the top-level object (the stage object). + * @param {!object} json SB2-format JSON to load. + * @param {!Runtime} runtime Runtime object to load all structures into. + * @param {boolean=} optForceSprite If set, treat as sprite (Sprite2). + * @param {?object} zip Optional zipped assets for local file import + * @return {Promise.} Promise that resolves to the loaded targets when ready. + */ +const sb2import = function (json, runtime, optForceSprite, zip) { + const extensions = { + extensionIDs: new Set(), + extensionURLs: new Map() + }; + return Promise.resolve(parseScratchAssets(json, runtime, !optForceSprite, zip)) + // Force this promise to wait for the next loop in the js tick. Let + // storage have some time to send off asset requests. + .then(assets => Promise.resolve(assets)) + .then(assets => ( + parseScratchObject(json, runtime, extensions, !optForceSprite, zip, assets) + )) + .then(reorderParsedTargets) + .then(targets => ({ + targets, + extensions + })); +}; + +/** + * Given the sb2 block, inspect the specmap for a translation method or object. + * @param {!object} block a sb2 formatted block + * @return {object} specmap block to parse this opcode + */ +const specMapBlock = function (block) { + const opcode = block[0]; + const mapped = opcode && specMap[opcode]; + if (!mapped) { + if (opcode && isPossiblyScratchXBlock(opcode)) { + return mapScratchXBlock(block); + } + log.warn(`Couldn't find SB2 block: ${opcode}`); + return null; + } + if (typeof mapped === 'function') { + return mapped(block); + } + return mapped; +}; + +/** + * Parse a single SB2 JSON-formatted block and its children. + * @param {!object} sb2block SB2 JSON-formatted block. + * @param {Function} addBroadcastMsg function to update broadcast message name map + * @param {Function} getVariableId function to retrieve a variable's ID based on name + * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. + * @param {ParseState} parseState - info on the state of parsing beyond the current block. + * @param {object} comments - Comments from sb2 project that need to be attached to blocks. + * They are indexed in this object by the sb2 flattened block list index indicating + * which block they should attach to. + * @param {int} commentIndex The comment index for the block to be parsed if it were in a flattened + * list of all blocks for the target + * @return {Array.} Tuple where first item is the Scratch VM-format block (or null if unsupported object), + * and second item is the updated comment index (after this block and its children are parsed) + */ +const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions, parseState, comments, commentIndex) { + const commentsForParsedBlock = (comments && typeof commentIndex === 'number' && !isNaN(commentIndex)) ? + comments[commentIndex] : null; + const blockMetadata = specMapBlock(sb2block); + if (!blockMetadata) { + // No block opcode found, exclude this block, increment the commentIndex, + // make all block comments into workspace comments and send them to zero/zero + // to prevent serialization issues. + if (commentsForParsedBlock) { + commentsForParsedBlock.forEach(comment => { + comment.blockId = null; + comment.x = comment.y = 0; + }); + } + return [null, commentIndex + 1]; + } + const oldOpcode = sb2block[0]; + + // If the block is from an extension, record it. + const index = blockMetadata.opcode.indexOf('_'); + const prefix = blockMetadata.opcode.substring(0, index); + if (CORE_EXTENSIONS.indexOf(prefix) === -1) { + if (prefix !== '') extensions.extensionIDs.add(prefix); + } + + // Block skeleton. + const activeBlock = { + id: uid(), // Generate a new block unique ID. + opcode: blockMetadata.opcode, // Converted, e.g. "motion_movesteps". + inputs: {}, // Inputs to this block and the blocks they point to. + fields: {}, // Fields on this block and their values. + next: null, // Next block. + shadow: false, // No shadow blocks in an SB2 by default. + children: [] // Store any generated children, flattened in `flatten`. + }; + + // Attach any comments to this block.. + if (commentsForParsedBlock) { + // Attach only the last comment to the block, make all others workspace comments + activeBlock.comment = commentsForParsedBlock[commentsForParsedBlock.length - 1].id; + commentsForParsedBlock.forEach(comment => { + if (comment.id === activeBlock.comment) { + comment.blockId = activeBlock.id; + } else { + // All other comments don't get a block ID and are sent back to zero. + // This is important, because if they have `null` x/y, serialization breaks. + comment.blockId = null; + comment.x = comment.y = 0; + } + }); + } + commentIndex++; + + const parentExpectedArg = parseState.expectedArg; + + // For a procedure call, generate argument map from proc string. + if (oldOpcode === 'call') { + blockMetadata.argMap = parseProcedureArgMap(sb2block[1]); + } + // Look at the expected arguments in `blockMetadata.argMap.` + // The basic problem here is to turn positional SB2 arguments into + // non-positional named Scratch VM arguments. + for (let i = 0; i < blockMetadata.argMap.length; i++) { + const expectedArg = blockMetadata.argMap[i]; + const providedArg = sb2block[i + 1]; // (i = 0 is opcode) + // Whether the input is obscuring a shadow. + let shadowObscured = false; + // Positional argument is an input. + if (expectedArg.type === 'input') { + // Create a new block and input metadata. + const inputUid = uid(); + activeBlock.inputs[expectedArg.inputName] = { + name: expectedArg.inputName, + block: null, + shadow: null + }; + if (typeof providedArg === 'object' && providedArg) { + // Block or block list occupies the input. + let innerBlocks; + parseState.expectedArg = expectedArg; + if (typeof providedArg[0] === 'object' && providedArg[0]) { + // Block list occupies the input. + [innerBlocks, commentIndex] = parseBlockList(providedArg, addBroadcastMsg, getVariableId, + extensions, parseState, comments, commentIndex); + } else { + // Single block occupies the input. + const parsedBlockDesc = parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions, + parseState, comments, commentIndex); + innerBlocks = parsedBlockDesc[0] ? [parsedBlockDesc[0]] : []; + // Update commentIndex + commentIndex = parsedBlockDesc[1]; + } + parseState.expectedArg = parentExpectedArg; + + // Check if innerBlocks is not an empty list. + // An empty list indicates that all the inner blocks from the sb2 have + // unknown opcodes and have been skipped. + if (innerBlocks.length > 0) { + let previousBlock = null; + for (let j = 0; j < innerBlocks.length; j++) { + if (j === 0) { + innerBlocks[j].parent = activeBlock.id; + } else { + innerBlocks[j].parent = previousBlock; + } + previousBlock = innerBlocks[j].id; + } + activeBlock.inputs[expectedArg.inputName].block = ( + innerBlocks[0].id + ); + activeBlock.children = ( + activeBlock.children.concat(innerBlocks) + ); + } + + // Obscures any shadow. + shadowObscured = true; + } + // Generate a shadow block to occupy the input. + if (!expectedArg.inputOp) { + // Undefined inputOp. inputOp should always be defined for inputs. + log.warn(`Unknown input operation for input ${expectedArg.inputName} of opcode ${activeBlock.opcode}.`); + continue; + } + if (expectedArg.inputOp === 'boolean' || expectedArg.inputOp === 'substack') { + // No editable shadow input; e.g., for a boolean. + continue; + } + // Each shadow has a field generated for it automatically. + // Value to be filled in the field. + let fieldValue = providedArg; + // Shadows' field names match the input name, except for these: + let fieldName = expectedArg.inputName; + if (expectedArg.inputOp === 'math_number' || + expectedArg.inputOp === 'math_whole_number' || + expectedArg.inputOp === 'math_positive_number' || + expectedArg.inputOp === 'math_integer' || + expectedArg.inputOp === 'math_angle') { + fieldName = 'NUM'; + // Fields are given Scratch 2.0 default values if obscured. + if (shadowObscured) { + fieldValue = 10; + } + } else if (expectedArg.inputOp === 'text') { + fieldName = 'TEXT'; + if (shadowObscured) { + fieldValue = ''; + } + } else if (expectedArg.inputOp === 'colour_picker') { + // Convert SB2 color to hex. + fieldValue = Color.decimalToHex(providedArg); + fieldName = 'COLOUR'; + if (shadowObscured) { + fieldValue = '#990000'; + } + } else if (expectedArg.inputOp === 'event_broadcast_menu') { + fieldName = 'BROADCAST_OPTION'; + if (shadowObscured) { + fieldValue = ''; + } + } else if (expectedArg.inputOp === 'sensing_of_object_menu') { + if (shadowObscured) { + fieldValue = '_stage_'; + } else if (fieldValue === 'Stage') { + fieldValue = '_stage_'; + } + } else if (expectedArg.inputOp === 'note') { + if (shadowObscured) { + fieldValue = 60; + } + } else if (expectedArg.inputOp === 'music.menu.DRUM') { + if (shadowObscured) { + fieldValue = 1; + } + } else if (expectedArg.inputOp === 'music.menu.INSTRUMENT') { + if (shadowObscured) { + fieldValue = 1; + } + } else if (expectedArg.inputOp === 'videoSensing.menu.ATTRIBUTE') { + if (shadowObscured) { + fieldValue = 'motion'; + } + } else if (expectedArg.inputOp === 'videoSensing.menu.SUBJECT') { + if (shadowObscured) { + fieldValue = 'this sprite'; + } + } else if (expectedArg.inputOp === 'videoSensing.menu.VIDEO_STATE') { + if (shadowObscured) { + fieldValue = 'on'; + } + } else if (shadowObscured) { + // Filled drop-down menu. + fieldValue = ''; + } + const fields = {}; + fields[fieldName] = { + name: fieldName, + value: fieldValue + }; + // event_broadcast_menus have some extra properties to add to the + // field and a different value than the rest + if (expectedArg.inputOp === 'event_broadcast_menu') { + // Need to update the broadcast message name map with + // the value of this field. + // Also need to provide the fields[fieldName] object, + // so that we can later update its value property, e.g. + // if sb2 message name is empty string, we will later + // replace this field's value with messageN + // once we can traverse through all the existing message names + // and come up with a fresh messageN. + const broadcastId = addBroadcastMsg(fieldValue, fields[fieldName]); + fields[fieldName].id = broadcastId; + fields[fieldName].variableType = expectedArg.variableType; + } + activeBlock.children.push({ + id: inputUid, + opcode: expectedArg.inputOp, + inputs: {}, + fields: fields, + next: null, + topLevel: false, + parent: activeBlock.id, + shadow: true + }); + activeBlock.inputs[expectedArg.inputName].shadow = inputUid; + // If no block occupying the input, alias to the shadow. + if (!activeBlock.inputs[expectedArg.inputName].block) { + activeBlock.inputs[expectedArg.inputName].block = inputUid; + } + } else if (expectedArg.type === 'field') { + // Add as a field on this block. + activeBlock.fields[expectedArg.fieldName] = { + name: expectedArg.fieldName, + value: providedArg + }; + + if (expectedArg.fieldName === 'CURRENTMENU') { + // In 3.0, the field value of the `sensing_current` block + // is in all caps. + activeBlock.fields[expectedArg.fieldName].value = providedArg.toUpperCase(); + if (providedArg === 'day of week') { + activeBlock.fields[expectedArg.fieldName].value = 'DAYOFWEEK'; + } + } + + if (expectedArg.fieldName === 'VARIABLE') { + // Add `id` property to variable fields + activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg, Variable.SCALAR_TYPE); + } else if (expectedArg.fieldName === 'LIST') { + // Add `id` property to variable fields + activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg, Variable.LIST_TYPE); + } else if (expectedArg.fieldName === 'BROADCAST_OPTION') { + // Add the name in this field to the broadcast msg name map. + // Also need to provide the fields[fieldName] object, + // so that we can later update its value property, e.g. + // if sb2 message name is empty string, we will later + // replace this field's value with messageN + // once we can traverse through all the existing message names + // and come up with a fresh messageN. + const broadcastId = addBroadcastMsg(providedArg, activeBlock.fields[expectedArg.fieldName]); + activeBlock.fields[expectedArg.fieldName].id = broadcastId; + } + const varType = expectedArg.variableType; + if (typeof varType === 'string') { + activeBlock.fields[expectedArg.fieldName].variableType = varType; + } + } + } + + // Updates for blocks that have new menus (e.g. in Looks) + switch (oldOpcode) { + case 'comeToFront': + activeBlock.fields.FRONT_BACK = { + name: 'FRONT_BACK', + value: 'front' + }; + break; + case 'goBackByLayers:': + activeBlock.fields.FORWARD_BACKWARD = { + name: 'FORWARD_BACKWARD', + value: 'backward' + }; + break; + case 'backgroundIndex': + activeBlock.fields.NUMBER_NAME = { + name: 'NUMBER_NAME', + value: 'number' + }; + break; + case 'sceneName': + activeBlock.fields.NUMBER_NAME = { + name: 'NUMBER_NAME', + value: 'name' + }; + break; + case 'costumeIndex': + activeBlock.fields.NUMBER_NAME = { + name: 'NUMBER_NAME', + value: 'number' + }; + break; + case 'costumeName': + activeBlock.fields.NUMBER_NAME = { + name: 'NUMBER_NAME', + value: 'name' + }; + break; + } + + // Special cases to generate mutations. + if (oldOpcode === 'stopScripts') { + // Mutation for stop block: if the argument is 'other scripts', + // the block needs a next connection. + if (sb2block[1] === 'other scripts in sprite' || + sb2block[1] === 'other scripts in stage') { + activeBlock.mutation = { + tagName: 'mutation', + hasnext: 'true', + children: [] + }; + } + } else if (oldOpcode === 'procDef') { + // Mutation for procedure definition: + // store all 2.0 proc data. + const procData = sb2block.slice(1); + // Create a new block and input metadata. + const inputUid = uid(); + const inputName = 'custom_block'; + activeBlock.inputs[inputName] = { + name: inputName, + block: inputUid, + shadow: inputUid + }; + activeBlock.children = [{ + id: inputUid, + opcode: 'procedures_prototype', + inputs: {}, + fields: {}, + next: null, + shadow: true, + children: [], + mutation: { + tagName: 'mutation', + proccode: procData[0], // e.g., "abc %n %b %s" + argumentnames: JSON.stringify(procData[1]), // e.g. ['arg1', 'arg2'] + argumentids: JSON.stringify(parseProcedureArgIds(procData[0])), + argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc'] + warp: procData[3], // Warp mode, e.g., true/false. + children: [] + } + }]; + } else if (oldOpcode === 'call') { + // Mutation for procedure call: + // string for proc code (e.g., "abc %n %b %s"). + activeBlock.mutation = { + tagName: 'mutation', + children: [], + proccode: sb2block[1], + argumentids: JSON.stringify(parseProcedureArgIds(sb2block[1])) + }; + } else if (oldOpcode === 'getParam') { + let returnCode = sb2block[2]; + + // Ensure the returnCode is "b" if used in a boolean input. + if (parentExpectedArg && parentExpectedArg.inputOp === 'boolean' && returnCode !== 'b') { + returnCode = 'b'; + } + + // Assign correct opcode based on the block shape. + switch (returnCode) { + case 'r': + activeBlock.opcode = 'argument_reporter_string_number'; + break; + case 'b': + activeBlock.opcode = 'argument_reporter_boolean'; + break; + } + } + return [activeBlock, commentIndex]; +}; + +module.exports = { + deserialize: sb2import +}; diff --git a/local-scratch-vm/src/serialization/sb2_specmap.js b/local-scratch-vm/src/serialization/sb2_specmap.js new file mode 100644 index 0000000000000000000000000000000000000000..5f0e14f4b47bbd9a98b46f8464060eebfc56bad4 --- /dev/null +++ b/local-scratch-vm/src/serialization/sb2_specmap.js @@ -0,0 +1,1818 @@ +/** + * @fileoverview + * The specMap below handles a few pieces of "translation" work between + * the SB2 JSON format and the data we need to run a project + * in the Scratch 3.0 VM. + * Notably: + * - Map 2.0 and 1.4 opcodes (forward:) into 3.0-format (motion_movesteps). + * - Map ordered, unnamed args to unordered, named inputs and fields. + * Keep this up-to-date as 3.0 blocks are renamed, changed, etc. + * Originally this was generated largely by a hand-guided scripting process. + * The relevant data lives here: + * https://github.com/LLK/scratch-flash/blob/master/src/Specs.as + * (for the old opcode and argument order). + * and here: + * https://github.com/LLK/scratch-blocks/tree/develop/blocks_vertical + * (for the new opcodes and argument names). + * and here: + * https://github.com/LLK/scratch-blocks/blob/develop/tests/ + * (for the shadow blocks created for each block). + * I started with the `commands` array in Specs.as, and discarded irrelevant + * properties. By hand, I matched the opcode name to the 3.0 opcode. + * Finally, I filled in the expected arguments as below. + */ + +const Variable = require('../engine/variable'); + +/** + * @typedef {object} SB2SpecMap_blockInfo + * @property {string} opcode - the Scratch 3.0 block opcode. Use 'extensionID.opcode' for extension opcodes. + * @property {Array.} argMap - metadata for this block's arguments. + */ + +/** + * @typedef {object} SB2SpecMap_argInfo + * @property {string} type - the type of this arg (such as 'input' or 'field') + * @property {string} inputOp - the scratch-blocks shadow type for this arg + * @property {string} inputName - the name this argument will take when provided to the block implementation + */ + +/** + * Mapping of Scratch 2.0 opcode to Scratch 3.0 block metadata. + * @type {object.} + */ +const specMap = { + 'forward:': { + opcode: 'motion_movesteps', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'STEPS' + } + ] + }, + 'turnRight:': { + opcode: 'motion_turnright', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'DEGREES' + } + ] + }, + 'turnLeft:': { + opcode: 'motion_turnleft', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'DEGREES' + } + ] + }, + 'heading:': { + opcode: 'motion_pointindirection', + argMap: [ + { + type: 'input', + inputOp: 'math_angle', + inputName: 'DIRECTION' + } + ] + }, + 'pointTowards:': { + opcode: 'motion_pointtowards', + argMap: [ + { + type: 'input', + inputOp: 'motion_pointtowards_menu', + inputName: 'TOWARDS' + } + ] + }, + 'gotoX:y:': { + opcode: 'motion_gotoxy', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'X' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'Y' + } + ] + }, + 'gotoSpriteOrMouse:': { + opcode: 'motion_goto', + argMap: [ + { + type: 'input', + inputOp: 'motion_goto_menu', + inputName: 'TO' + } + ] + }, + 'glideSecs:toX:y:elapsed:from:': { + opcode: 'motion_glidesecstoxy', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'SECS' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'X' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'Y' + } + ] + }, + 'changeXposBy:': { + opcode: 'motion_changexby', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'DX' + } + ] + }, + 'xpos:': { + opcode: 'motion_setx', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'X' + } + ] + }, + 'changeYposBy:': { + opcode: 'motion_changeyby', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'DY' + } + ] + }, + 'ypos:': { + opcode: 'motion_sety', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'Y' + } + ] + }, + 'bounceOffEdge': { + opcode: 'motion_ifonedgebounce', + argMap: [ + ] + }, + 'setRotationStyle': { + opcode: 'motion_setrotationstyle', + argMap: [ + { + type: 'field', + fieldName: 'STYLE' + } + ] + }, + 'xpos': { + opcode: 'motion_xposition', + argMap: [ + ] + }, + 'ypos': { + opcode: 'motion_yposition', + argMap: [ + ] + }, + 'heading': { + opcode: 'motion_direction', + argMap: [ + ] + }, + 'scrollRight': { + opcode: 'motion_scroll_right', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'DISTANCE' + } + ] + }, + 'scrollUp': { + opcode: 'motion_scroll_up', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'DISTANCE' + } + ] + }, + 'scrollAlign': { + opcode: 'motion_align_scene', + argMap: [ + { + type: 'field', + fieldName: 'ALIGNMENT' + } + ] + }, + 'xScroll': { + opcode: 'motion_xscroll', + argMap: [ + ] + }, + 'yScroll': { + opcode: 'motion_yscroll', + argMap: [ + ] + }, + 'say:duration:elapsed:from:': { + opcode: 'looks_sayforsecs', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'MESSAGE' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'SECS' + } + ] + }, + 'say:': { + opcode: 'looks_say', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'MESSAGE' + } + ] + }, + 'think:duration:elapsed:from:': { + opcode: 'looks_thinkforsecs', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'MESSAGE' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'SECS' + } + ] + }, + 'think:': { + opcode: 'looks_think', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'MESSAGE' + } + ] + }, + 'show': { + opcode: 'looks_show', + argMap: [ + ] + }, + 'hide': { + opcode: 'looks_hide', + argMap: [ + ] + }, + 'hideAll': { + opcode: 'looks_hideallsprites', + argMap: [ + ] + }, + 'lookLike:': { + opcode: 'looks_switchcostumeto', + argMap: [ + { + type: 'input', + inputOp: 'looks_costume', + inputName: 'COSTUME' + } + ] + }, + 'nextCostume': { + opcode: 'looks_nextcostume', + argMap: [ + ] + }, + 'startScene': { + opcode: 'looks_switchbackdropto', + argMap: [ + { + type: 'input', + inputOp: 'looks_backdrops', + inputName: 'BACKDROP' + } + ] + }, + 'changeGraphicEffect:by:': { + opcode: 'looks_changeeffectby', + argMap: [ + { + type: 'field', + fieldName: 'EFFECT' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'CHANGE' + } + ] + }, + 'setGraphicEffect:to:': { + opcode: 'looks_seteffectto', + argMap: [ + { + type: 'field', + fieldName: 'EFFECT' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'VALUE' + } + ] + }, + 'filterReset': { + opcode: 'looks_cleargraphiceffects', + argMap: [ + ] + }, + 'changeSizeBy:': { + opcode: 'looks_changesizeby', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'CHANGE' + } + ] + }, + 'setSizeTo:': { + opcode: 'looks_setsizeto', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'SIZE' + } + ] + }, + 'changeStretchBy:': { + opcode: 'looks_changestretchby', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'CHANGE' + } + ] + }, + 'setStretchTo:': { + opcode: 'looks_setstretchto', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'STRETCH' + } + ] + }, + 'comeToFront': { + opcode: 'looks_gotofrontback', + argMap: [ + ] + }, + 'goBackByLayers:': { + opcode: 'looks_goforwardbackwardlayers', + argMap: [ + { + type: 'input', + inputOp: 'math_integer', + inputName: 'NUM' + } + ] + }, + 'costumeIndex': { + opcode: 'looks_costumenumbername', + argMap: [ + ] + }, + 'costumeName': { + opcode: 'looks_costumenumbername', + argMap: [ + ] + }, + 'sceneName': { + opcode: 'looks_backdropnumbername', + argMap: [ + ] + }, + 'scale': { + opcode: 'looks_size', + argMap: [ + ] + }, + 'startSceneAndWait': { + opcode: 'looks_switchbackdroptoandwait', + argMap: [ + { + type: 'input', + inputOp: 'looks_backdrops', + inputName: 'BACKDROP' + } + ] + }, + 'nextScene': { + opcode: 'looks_nextbackdrop', + argMap: [ + ] + }, + 'backgroundIndex': { + opcode: 'looks_backdropnumbername', + argMap: [ + ] + }, + 'playSound:': { + opcode: 'sound_play', + argMap: [ + { + type: 'input', + inputOp: 'sound_sounds_menu', + inputName: 'SOUND_MENU' + } + ] + }, + 'doPlaySoundAndWait': { + opcode: 'sound_playuntildone', + argMap: [ + { + type: 'input', + inputOp: 'sound_sounds_menu', + inputName: 'SOUND_MENU' + } + ] + }, + 'stopAllSounds': { + opcode: 'sound_stopallsounds', + argMap: [ + ] + }, + 'playDrum': { + opcode: 'music_playDrumForBeats', + argMap: [ + { + type: 'input', + inputOp: 'music_menu_DRUM', + inputName: 'DRUM' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'BEATS' + } + ] + }, + 'drum:duration:elapsed:from:': { + opcode: 'music_midiPlayDrumForBeats', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'DRUM' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'BEATS' + } + ] + }, + 'rest:elapsed:from:': { + opcode: 'music_restForBeats', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'BEATS' + } + ] + }, + 'noteOn:duration:elapsed:from:': { + opcode: 'music_playNoteForBeats', + argMap: [ + { + type: 'input', + inputOp: 'note', + inputName: 'NOTE' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'BEATS' + } + ] + }, + 'instrument:': { + opcode: 'music_setInstrument', + argMap: [ + { + type: 'input', + inputOp: 'music_menu_INSTRUMENT', + inputName: 'INSTRUMENT' + } + ] + }, + 'midiInstrument:': { + opcode: 'music_midiSetInstrument', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'INSTRUMENT' + } + ] + }, + 'changeVolumeBy:': { + opcode: 'sound_changevolumeby', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'VOLUME' + } + ] + }, + 'setVolumeTo:': { + opcode: 'sound_setvolumeto', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'VOLUME' + } + ] + }, + 'volume': { + opcode: 'sound_volume', + argMap: [ + ] + }, + 'changeTempoBy:': { + opcode: 'music_changeTempo', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'TEMPO' + } + ] + }, + 'setTempoTo:': { + opcode: 'music_setTempo', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'TEMPO' + } + ] + }, + 'tempo': { + opcode: 'music_getTempo', + argMap: [ + ] + }, + 'clearPenTrails': { + opcode: 'pen_clear', + argMap: [ + ] + }, + 'stampCostume': { + opcode: 'pen_stamp', + argMap: [ + ] + }, + 'putPenDown': { + opcode: 'pen_penDown', + argMap: [ + ] + }, + 'putPenUp': { + opcode: 'pen_penUp', + argMap: [ + ] + }, + 'penColor:': { + opcode: 'pen_setPenColorToColor', + argMap: [ + { + type: 'input', + inputOp: 'colour_picker', + inputName: 'COLOR' + } + ] + }, + 'changePenHueBy:': { + opcode: 'pen_changePenHueBy', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'HUE' + } + ] + }, + 'setPenHueTo:': { + opcode: 'pen_setPenHueToNumber', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'HUE' + } + ] + }, + 'changePenShadeBy:': { + opcode: 'pen_changePenShadeBy', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'SHADE' + } + ] + }, + 'setPenShadeTo:': { + opcode: 'pen_setPenShadeToNumber', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'SHADE' + } + ] + }, + 'changePenSizeBy:': { + opcode: 'pen_changePenSizeBy', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'SIZE' + } + ] + }, + 'penSize:': { + opcode: 'pen_setPenSizeTo', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'SIZE' + } + ] + }, + 'senseVideoMotion': { + opcode: 'videoSensing_videoOn', + argMap: [ + { + type: 'input', + inputOp: 'videoSensing_menu_ATTRIBUTE', + inputName: 'ATTRIBUTE' + }, + { + type: 'input', + inputOp: 'videoSensing_menu_SUBJECT', + inputName: 'SUBJECT' + } + ] + }, + 'whenGreenFlag': { + opcode: 'event_whenflagclicked', + argMap: [ + ] + }, + 'whenKeyPressed': { + opcode: 'event_whenkeypressed', + argMap: [ + { + type: 'field', + fieldName: 'KEY_OPTION' + } + ] + }, + 'whenClicked': { + opcode: 'event_whenthisspriteclicked', + argMap: [ + ] + }, + 'whenSceneStarts': { + opcode: 'event_whenbackdropswitchesto', + argMap: [ + { + type: 'field', + fieldName: 'BACKDROP' + } + ] + }, + 'whenSensorGreaterThan': ([, sensor]) => { + if (sensor === 'video motion') { + return { + opcode: 'videoSensing_whenMotionGreaterThan', + argMap: [ + // skip the first arg, since we converted to a video specific sensing block + {}, + { + type: 'input', + inputOp: 'math_number', + inputName: 'REFERENCE' + } + ] + }; + } + return { + opcode: 'event_whengreaterthan', + argMap: [ + { + type: 'field', + fieldName: 'WHENGREATERTHANMENU' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'VALUE' + } + ] + }; + }, + 'whenIReceive': { + opcode: 'event_whenbroadcastreceived', + argMap: [ + { + type: 'field', + fieldName: 'BROADCAST_OPTION', + variableType: Variable.BROADCAST_MESSAGE_TYPE + } + ] + }, + 'broadcast:': { + opcode: 'event_broadcast', + argMap: [ + { + type: 'input', + inputOp: 'event_broadcast_menu', + inputName: 'BROADCAST_INPUT', + variableType: Variable.BROADCAST_MESSAGE_TYPE + } + ] + }, + 'doBroadcastAndWait': { + opcode: 'event_broadcastandwait', + argMap: [ + { + type: 'input', + inputOp: 'event_broadcast_menu', + inputName: 'BROADCAST_INPUT', + variableType: Variable.BROADCAST_MESSAGE_TYPE + } + ] + }, + 'wait:elapsed:from:': { + opcode: 'control_wait', + argMap: [ + { + type: 'input', + inputOp: 'math_positive_number', + inputName: 'DURATION' + } + ] + }, + 'doRepeat': { + opcode: 'control_repeat', + argMap: [ + { + type: 'input', + inputOp: 'math_whole_number', + inputName: 'TIMES' + }, + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK' + } + ] + }, + 'doForever': { + opcode: 'control_forever', + argMap: [ + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK' + } + ] + }, + 'doIf': { + opcode: 'control_if', + argMap: [ + { + type: 'input', + inputOp: 'boolean', + inputName: 'CONDITION' + }, + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK' + } + ] + }, + 'doIfElse': { + opcode: 'control_if_else', + argMap: [ + { + type: 'input', + inputOp: 'boolean', + inputName: 'CONDITION' + }, + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK' + }, + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK2' + } + ] + }, + 'doWaitUntil': { + opcode: 'control_wait_until', + argMap: [ + { + type: 'input', + inputOp: 'boolean', + inputName: 'CONDITION' + } + ] + }, + 'doUntil': { + opcode: 'control_repeat_until', + argMap: [ + { + type: 'input', + inputOp: 'boolean', + inputName: 'CONDITION' + }, + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK' + } + ] + }, + 'doWhile': { + opcode: 'control_while', + argMap: [ + { + type: 'input', + inputOp: 'boolean', + inputName: 'CONDITION' + }, + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK' + } + ] + }, + 'doForLoop': { + opcode: 'control_for_each', + argMap: [ + { + type: 'field', + fieldName: 'VARIABLE' + }, + { + type: 'input', + inputOp: 'text', + inputName: 'VALUE' + }, + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK' + } + ] + }, + 'stopScripts': { + opcode: 'control_stop', + argMap: [ + { + type: 'field', + fieldName: 'STOP_OPTION' + } + ] + }, + 'whenCloned': { + opcode: 'control_start_as_clone', + argMap: [ + ] + }, + 'createCloneOf': { + opcode: 'control_create_clone_of', + argMap: [ + { + type: 'input', + inputOp: 'control_create_clone_of_menu', + inputName: 'CLONE_OPTION' + } + ] + }, + 'deleteClone': { + opcode: 'control_delete_this_clone', + argMap: [ + ] + }, + 'COUNT': { + opcode: 'control_get_counter', + argMap: [ + ] + }, + 'INCR_COUNT': { + opcode: 'control_incr_counter', + argMap: [ + ] + }, + 'CLR_COUNT': { + opcode: 'control_clear_counter', + argMap: [ + ] + }, + 'warpSpeed': { + opcode: 'control_all_at_once', + argMap: [ + { + type: 'input', + inputOp: 'substack', + inputName: 'SUBSTACK' + } + ] + }, + 'touching:': { + opcode: 'sensing_touchingobject', + argMap: [ + { + type: 'input', + inputOp: 'sensing_touchingobjectmenu', + inputName: 'TOUCHINGOBJECTMENU' + } + ] + }, + 'touchingColor:': { + opcode: 'sensing_touchingcolor', + argMap: [ + { + type: 'input', + inputOp: 'colour_picker', + inputName: 'COLOR' + } + ] + }, + 'color:sees:': { + opcode: 'sensing_coloristouchingcolor', + argMap: [ + { + type: 'input', + inputOp: 'colour_picker', + inputName: 'COLOR' + }, + { + type: 'input', + inputOp: 'colour_picker', + inputName: 'COLOR2' + } + ] + }, + 'distanceTo:': { + opcode: 'sensing_distanceto', + argMap: [ + { + type: 'input', + inputOp: 'sensing_distancetomenu', + inputName: 'DISTANCETOMENU' + } + ] + }, + 'doAsk': { + opcode: 'sensing_askandwait', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'QUESTION' + } + ] + }, + 'answer': { + opcode: 'sensing_answer', + argMap: [ + ] + }, + 'keyPressed:': { + opcode: 'sensing_keypressed', + argMap: [ + { + type: 'input', + inputOp: 'sensing_keyoptions', + inputName: 'KEY_OPTION' + } + ] + }, + 'mousePressed': { + opcode: 'sensing_mousedown', + argMap: [ + ] + }, + 'mouseX': { + opcode: 'sensing_mousex', + argMap: [ + ] + }, + 'mouseY': { + opcode: 'sensing_mousey', + argMap: [ + ] + }, + 'soundLevel': { + opcode: 'sensing_loudness', + argMap: [ + ] + }, + 'isLoud': { + opcode: 'sensing_loud', + argMap: [ + ] + }, + // 'senseVideoMotion': { + // opcode: 'sensing_videoon', + // argMap: [ + // { + // type: 'input', + // inputOp: 'sensing_videoonmenuone', + // inputName: 'VIDEOONMENU1' + // }, + // { + // type: 'input', + // inputOp: 'sensing_videoonmenutwo', + // inputName: 'VIDEOONMENU2' + // } + // ] + // }, + 'setVideoState': { + opcode: 'videoSensing_videoToggle', + argMap: [ + { + type: 'input', + inputOp: 'videoSensing_menu_VIDEO_STATE', + inputName: 'VIDEO_STATE' + } + ] + }, + 'setVideoTransparency': { + opcode: 'videoSensing_setVideoTransparency', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'TRANSPARENCY' + } + ] + }, + 'timer': { + opcode: 'sensing_timer', + argMap: [ + ] + }, + 'timerReset': { + opcode: 'sensing_resettimer', + argMap: [ + ] + }, + 'getAttribute:of:': { + opcode: 'sensing_of', + argMap: [ + { + type: 'field', + fieldName: 'PROPERTY' + }, + { + type: 'input', + inputOp: 'sensing_of_object_menu', + inputName: 'OBJECT' + } + ] + }, + 'timeAndDate': { + opcode: 'sensing_current', + argMap: [ + { + type: 'field', + fieldName: 'CURRENTMENU' + } + ] + }, + 'timestamp': { + opcode: 'sensing_dayssince2000', + argMap: [ + ] + }, + 'getUserName': { + opcode: 'sensing_username', + argMap: [ + ] + }, + 'getUserId': { + opcode: 'sensing_userid', + argMap: [ + ] + }, + '+': { + opcode: 'operator_add', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM1' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM2' + } + ] + }, + '-': { + opcode: 'operator_subtract', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM1' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM2' + } + ] + }, + '*': { + opcode: 'operator_multiply', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM1' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM2' + } + ] + }, + '/': { + opcode: 'operator_divide', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM1' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM2' + } + ] + }, + 'randomFrom:to:': { + opcode: 'operator_random', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'FROM' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'TO' + } + ] + }, + '<': { + opcode: 'operator_lt', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'OPERAND1' + }, + { + type: 'input', + inputOp: 'text', + inputName: 'OPERAND2' + } + ] + }, + '=': { + opcode: 'operator_equals', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'OPERAND1' + }, + { + type: 'input', + inputOp: 'text', + inputName: 'OPERAND2' + } + ] + }, + '>': { + opcode: 'operator_gt', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'OPERAND1' + }, + { + type: 'input', + inputOp: 'text', + inputName: 'OPERAND2' + } + ] + }, + '&': { + opcode: 'operator_and', + argMap: [ + { + type: 'input', + inputOp: 'boolean', + inputName: 'OPERAND1' + }, + { + type: 'input', + inputOp: 'boolean', + inputName: 'OPERAND2' + } + ] + }, + '|': { + opcode: 'operator_or', + argMap: [ + { + type: 'input', + inputOp: 'boolean', + inputName: 'OPERAND1' + }, + { + type: 'input', + inputOp: 'boolean', + inputName: 'OPERAND2' + } + ] + }, + 'not': { + opcode: 'operator_not', + argMap: [ + { + type: 'input', + inputOp: 'boolean', + inputName: 'OPERAND' + } + ] + }, + 'concatenate:with:': { + opcode: 'operator_join', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'STRING1' + }, + { + type: 'input', + inputOp: 'text', + inputName: 'STRING2' + } + ] + }, + 'letter:of:': { + opcode: 'operator_letter_of', + argMap: [ + { + type: 'input', + inputOp: 'math_whole_number', + inputName: 'LETTER' + }, + { + type: 'input', + inputOp: 'text', + inputName: 'STRING' + } + ] + }, + 'stringLength:': { + opcode: 'operator_length', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'STRING' + } + ] + }, + '%': { + opcode: 'operator_mod', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM1' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM2' + } + ] + }, + 'rounded': { + opcode: 'operator_round', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM' + } + ] + }, + 'computeFunction:of:': { + opcode: 'operator_mathop', + argMap: [ + { + type: 'field', + fieldName: 'OPERATOR' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'NUM' + } + ] + }, + 'readVariable': { + opcode: 'data_variable', + argMap: [ + { + type: 'field', + fieldName: 'VARIABLE', + variableType: Variable.SCALAR_TYPE + } + ] + }, + // Scratch 2 uses this alternative variable getter opcode only in monitors, + // blocks use the `readVariable` opcode above. + 'getVar:': { + opcode: 'data_variable', + argMap: [ + { + type: 'field', + fieldName: 'VARIABLE', + variableType: Variable.SCALAR_TYPE + } + ] + }, + 'setVar:to:': { + opcode: 'data_setvariableto', + argMap: [ + { + type: 'field', + fieldName: 'VARIABLE', + variableType: Variable.SCALAR_TYPE + }, + { + type: 'input', + inputOp: 'text', + inputName: 'VALUE' + } + ] + }, + 'changeVar:by:': { + opcode: 'data_changevariableby', + argMap: [ + { + type: 'field', + fieldName: 'VARIABLE', + variableType: Variable.SCALAR_TYPE + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'VALUE' + } + ] + }, + 'showVariable:': { + opcode: 'data_showvariable', + argMap: [ + { + type: 'field', + fieldName: 'VARIABLE', + variableType: Variable.SCALAR_TYPE + } + ] + }, + 'hideVariable:': { + opcode: 'data_hidevariable', + argMap: [ + { + type: 'field', + fieldName: 'VARIABLE', + variableType: Variable.SCALAR_TYPE + } + ] + }, + 'contentsOfList:': { + opcode: 'data_listcontents', + argMap: [ + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + } + ] + }, + 'append:toList:': { + opcode: 'data_addtolist', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'ITEM' + }, + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + } + ] + }, + 'deleteLine:ofList:': { + opcode: 'data_deleteoflist', + argMap: [ + { + type: 'input', + inputOp: 'math_integer', + inputName: 'INDEX' + }, + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + } + ] + }, + 'insert:at:ofList:': { + opcode: 'data_insertatlist', + argMap: [ + { + type: 'input', + inputOp: 'text', + inputName: 'ITEM' + }, + { + type: 'input', + inputOp: 'math_integer', + inputName: 'INDEX' + }, + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + } + ] + }, + 'setLine:ofList:to:': { + opcode: 'data_replaceitemoflist', + argMap: [ + { + type: 'input', + inputOp: 'math_integer', + inputName: 'INDEX' + }, + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + }, + { + type: 'input', + inputOp: 'text', + inputName: 'ITEM' + } + ] + }, + 'getLine:ofList:': { + opcode: 'data_itemoflist', + argMap: [ + { + type: 'input', + inputOp: 'math_integer', + inputName: 'INDEX' + }, + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + } + ] + }, + 'lineCountOfList:': { + opcode: 'data_lengthoflist', + argMap: [ + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + } + ] + }, + 'list:contains:': { + opcode: 'data_listcontainsitem', + argMap: [ + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + }, + { + type: 'input', + inputOp: 'text', + inputName: 'ITEM' + } + ] + }, + 'showList:': { + opcode: 'data_showlist', + argMap: [ + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + } + ] + }, + 'hideList:': { + opcode: 'data_hidelist', + argMap: [ + { + type: 'field', + fieldName: 'LIST', + variableType: Variable.LIST_TYPE + } + ] + }, + 'procDef': { + opcode: 'procedures_definition', + argMap: [] + }, + 'getParam': { + // Doesn't map to single opcode. Import step assigns final correct opcode. + opcode: 'argument_reporter_string_number', + argMap: [ + { + type: 'field', + fieldName: 'VALUE' + } + ] + }, + 'call': { + opcode: 'procedures_call', + argMap: [] + } +}; + +/** + * Add to the specMap entries for an opcode from a Scratch 2.0 extension. Two entries will be made with the same + * metadata; this is done to support projects saved by both older and newer versions of the Scratch 2.0 editor. + * @param {string} sb2Extension - the Scratch 2.0 name of the extension + * @param {string} sb2Opcode - the Scratch 2.0 opcode + * @param {SB2SpecMap_blockInfo} blockInfo - the Scratch 3.0 block info + */ +const addExtensionOp = function (sb2Extension, sb2Opcode, blockInfo) { + /** + * This string separates the name of an extension and the name of an opcode in more recent Scratch 2.0 projects. + * Earlier projects used '.' as a separator, up until we added the 'LEGO WeDo 2.0' extension... + * @type {string} + */ + const sep = '\u001F'; // Unicode Unit Separator + + // make one entry for projects saved by recent versions of the Scratch 2.0 editor + specMap[`${sb2Extension}${sep}${sb2Opcode}`] = blockInfo; + + // make a second for projects saved by older versions of the Scratch 2.0 editor + specMap[`${sb2Extension}.${sb2Opcode}`] = blockInfo; +}; + +const weDo2 = 'LEGO WeDo 2.0'; + +addExtensionOp(weDo2, 'motorOnFor', { + opcode: 'wedo2_motorOnFor', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_MOTOR_ID', + inputName: 'MOTOR_ID' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'DURATION' + } + ] +}); + +addExtensionOp(weDo2, 'motorOn', { + opcode: 'wedo2_motorOn', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_MOTOR_ID', + inputName: 'MOTOR_ID' + } + ] +}); + +addExtensionOp(weDo2, 'motorOff', { + opcode: 'wedo2_motorOff', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_MOTOR_ID', + inputName: 'MOTOR_ID' + } + ] +}); + +addExtensionOp(weDo2, 'startMotorPower', { + opcode: 'wedo2_startMotorPower', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_MOTOR_ID', + inputName: 'MOTOR_ID' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'POWER' + } + ] +}); + +addExtensionOp(weDo2, 'setMotorDirection', { + opcode: 'wedo2_setMotorDirection', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_MOTOR_ID', + inputName: 'MOTOR_ID' + }, + { + type: 'input', + inputOp: 'wedo2_menu_MOTOR_DIRECTION', + inputName: 'MOTOR_DIRECTION' + } + ] +}); + +addExtensionOp(weDo2, 'setLED', { + opcode: 'wedo2_setLightHue', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'HUE' + } + ] +}); + +addExtensionOp(weDo2, 'playNote', { + opcode: 'wedo2_playNoteFor', + argMap: [ + { + type: 'input', + inputOp: 'math_number', + inputName: 'NOTE' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'DURATION' + } + ] +}); + +addExtensionOp(weDo2, 'whenDistance', { + opcode: 'wedo2_whenDistance', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_OP', + inputName: 'OP' + }, + { + type: 'input', + inputOp: 'math_number', + inputName: 'REFERENCE' + } + ] +}); + +addExtensionOp(weDo2, 'whenTilted', { + opcode: 'wedo2_whenTilted', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_TILT_DIRECTION_ANY', + inputName: 'TILT_DIRECTION_ANY' + } + ] +}); + +addExtensionOp(weDo2, 'getDistance', { + opcode: 'wedo2_getDistance', + argMap: [] +}); + +addExtensionOp(weDo2, 'isTilted', { + opcode: 'wedo2_isTilted', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_TILT_DIRECTION_ANY', + inputName: 'TILT_DIRECTION_ANY' + } + ] +}); + +addExtensionOp(weDo2, 'getTilt', { + opcode: 'wedo2_getTiltAngle', + argMap: [ + { + type: 'input', + inputOp: 'wedo2_menu_TILT_DIRECTION', + inputName: 'TILT_DIRECTION' + } + ] +}); + +module.exports = specMap; diff --git a/local-scratch-vm/src/serialization/sb3.js b/local-scratch-vm/src/serialization/sb3.js new file mode 100644 index 0000000000000000000000000000000000000000..c7ce0d72025467011eea9be9f88ed0849b296469 --- /dev/null +++ b/local-scratch-vm/src/serialization/sb3.js @@ -0,0 +1,1670 @@ +/* eslint-disable no-invalid-this */ +/** + * @fileoverview + * An SB3 serializer and deserializer. Parses provided + * JSON and then generates all needed scratch-vm runtime structures. + */ + +const Blocks = require('../engine/blocks'); +const Sprite = require('../sprites/sprite'); +const Variable = require('../engine/variable'); +const Comment = require('../engine/comment'); +const MonitorRecord = require('../engine/monitor-record'); +const StageLayering = require('../engine/stage-layering'); +const log = require('../util/log'); +const uid = require('../util/uid'); +const MathUtil = require('../util/math-util'); +const StringUtil = require('../util/string-util'); +const VariableUtil = require('../util/variable-util'); +const Clone = require('../util/clone'); +const compress = require('./tw-compress-sb3'); +const OldExtensions = require('./extension patcher'); + +const {loadCostume} = require('../import/load-costume.js'); +const {loadSound} = require('../import/load-sound.js'); +const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js'); +const replacersPatch = require('./replacers patch.json'); + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +/** + * @typedef {object} ImportedProject + * @property {Array.} targets - the imported Scratch 3.0 target objects. + * @property {ImportedExtensionsInfo} extensionsInfo - the ID of each extension actually used by this project. + */ + +/** + * @typedef {object} ImportedExtensionsInfo + * @property {Set.} extensionIDs - the ID of each extension actually in use by blocks in this project. + * @property {Map.} extensionURLs - map of ID => URL from project metadata. May not match extensionIDs. + */ + +// Constants used during serialization and deserialization +const INPUT_SAME_BLOCK_SHADOW = 1; // unobscured shadow +const INPUT_BLOCK_NO_SHADOW = 2; // no shadow +const INPUT_DIFF_BLOCK_SHADOW = 3; // obscured shadow +// There shouldn't be a case where block is null, but shadow is present... + +// Constants used during deserialization of an SB3 file +const CORE_EXTENSIONS = [ + 'argument', + 'colour', + 'control', + 'data', + 'event', + 'looks', + 'math', + 'motion', + 'operator', + 'procedures', + 'sensing', + 'sound' +]; + +// Constants referring to 'primitive' blocks that are usually shadows, +// or in the case of variables and lists, appear quite often in projects +// math_number +const MATH_NUM_PRIMITIVE = 4; // there's no reason these constants can't collide +// math_positive_number +const POSITIVE_NUM_PRIMITIVE = 5; // with the above, but removing duplication for clarity +// math_whole_number +const WHOLE_NUM_PRIMITIVE = 6; +// math_integer +const INTEGER_NUM_PRIMITIVE = 7; +// math_angle +const ANGLE_NUM_PRIMITIVE = 8; +// colour_picker +const COLOR_PICKER_PRIMITIVE = 9; +// text +const TEXT_PRIMITIVE = 10; +// event_broadcast_menu +const BROADCAST_PRIMITIVE = 11; +// data_variable +const VAR_PRIMITIVE = 12; +// data_listcontents +const LIST_PRIMITIVE = 13; +// any single-fielded item not covered above +const LONE_FIELD = 14; + +// Map block opcodes to the above primitives and the name of the field we can use +// to find the value of the field +const primitiveOpcodeInfoMap = { + math_number: [MATH_NUM_PRIMITIVE, 'NUM'], + math_positive_number: [POSITIVE_NUM_PRIMITIVE, 'NUM'], + math_whole_number: [WHOLE_NUM_PRIMITIVE, 'NUM'], + math_integer: [INTEGER_NUM_PRIMITIVE, 'NUM'], + math_angle: [ANGLE_NUM_PRIMITIVE, 'NUM'], + colour_picker: [COLOR_PICKER_PRIMITIVE, 'COLOUR'], + text: [TEXT_PRIMITIVE, 'TEXT'], + event_broadcast_menu: [BROADCAST_PRIMITIVE, 'BROADCAST_OPTION'], + data_variable: [VAR_PRIMITIVE, 'VARIABLE'], + data_listcontents: [LIST_PRIMITIVE, 'LIST'] +}; + +// the list of blocks and there replacements for jwUnite +const uniteReplacments = { + 'jwUnite_always': 'event_always', + 'jwUnite_whenanything': 'event_whenanything', + 'jwUnite_getspritewithattrib': 'sensing_getspritewithattrib', + 'jwUnite_backToGreenFlag': 'control_backToGreenFlag', + 'jwUnite_trueBoolean': 'operator_trueBoolean', + 'jwUnite_falseBoolean': 'operator_falseBoolean', + 'jwUnite_randomBoolean': 'operator_randomBoolean', + 'jwUnite_mobile': 'sensing_mobile', + 'jwUnite_thing_is_text': 'sensing_thing_is_text', + 'jwUnite_thing_is_number': 'sensing_thing_is_number', + 'jwUnite_if_return_else_return': 'control_if_return_else_return', + 'jwUnite_indexOfTextInText': 'operator_indexOfTextInText', + 'jwUnite_regextest': 'sensing_regextest', + 'jwUnite_regexmatch': 'operator_regexmatch', + 'jwUnite_replaceAll': 'operator_replaceAll', + 'jwUnite_getLettersFromIndexToIndexInText': 'operator_getLettersFromIndexToIndexInText', + 'jwUnite_readLineInMultilineText': 'operator_readLineInMultilineText', + 'jwUnite_newLine': 'operator_newLine', + 'jwUnite_stringify': 'operator_stringify', + 'jwUnite_lerpFunc': 'operator_lerpFunc', + 'jwUnite_advMath': 'operator_advMath', + 'jwUnite_constrainnumber': 'operator_constrainnumber' +}; + +// extensions to be patched by the extension patcher +const ExtensionPatches = { + "griffpatch": {id: 'griffpatch', url: 'https://extensions.turbowarp.org/box2d.js'}, + // "cloudlink": {id: 'cloudlink', url: 'https://extensions.turbowarp.org/cloudlink.js'}, + "jwUnite": (extensions, object, runtime) => { + extensions.extensionIDs.delete("jwUnite"); + let blocks = object.blocks; + const blockIDs = Object.keys(blocks); + const patcher = extensions.patcher; + + for (let block, idx = 0; idx < blockIDs.length; idx++) { + block = blocks[blockIDs[idx]]; + if (typeof block !== 'object' || Array.isArray(block)) continue; + // handle all 1:1 blocks + if (uniteReplacments[block.opcode]) { + block.opcode = uniteReplacments[block.opcode]; + if (block.opcode === 'sensing_regextest' || block.opcode === 'operator_regexmatch') { + block.inputs.regrule = [ + INPUT_SAME_BLOCK_SHADOW, + [TEXT_PRIMITIVE, "g"] + ]; + } + } + // handle replacer blocks + if (block.opcode === 'jwUnite_setReplacer' || block.opcode === 'jwUnite_replaceWithReplacers') { + if (!patcher.loaded.includes('jgJSON')) { + runtime.extensionManager.loadExtensionURL('jgJSON'); + patcher.loaded.push('jgJSON'); + } + blocks = Object.assign(blocks, Clone.simple(replacersPatch.blocks)); + object.variables = Object.assign(object.variables, Clone.simple(replacersPatch.variables)); + const repBlock = block.opcode === 'jwUnite_setReplacer' + ? "setReplacerToDisplay" + : "replaceWithReplacersDisplay"; + const replacment = Clone.simple(replacersPatch.blocks[repBlock]); + block.opcode = 'procedures_call'; + block.mutation = replacment.mutation; + } + blocks[blockIDs[idx]] = block; + } + object.blocks = blocks; + }, + // eslint-disable-next-line no-unused-vars + // 'text': (extensions, object, runtime) => { + // const blocks = object.blocks; + // const patcher = extensions.patcher; + // if (!patcher.loaded.includes('text')) { + // runtime.extensionManager.loadExtensionURL('text'); + // patcher.loaded.push('text'); + // } + // for (const id in blocks) { + // const block = blocks[id]; + // const oldFont = block.fields?.FONT ?? block.fields?.font; + // if (!oldFont) continue; + // block.inputs.FONT = [ + // INPUT_SAME_BLOCK_SHADOW, + // [ + // LONE_FIELD, + // 'text_menu_FONT', + // 'FONT', + // { + // name: 'FONT', + // value: oldFont[0], + // id: oldFont[1] + // } + // ] + // ]; + // } + // } +}; + +/** + * Serializes primitives described above into a more compact format + * @param {object} block the block to serialize + * @return {array} An array representing the information in the block, + * or null if the given block is not one of the primitives described above. + */ +const serializePrimitiveBlock = function (block) { + // Returns an array represeting a primitive block or null if not one of + // the primitive types above + // if (Object.keys(block.inputs).length === 0 && Object.keys(block.fields).length === 1) { + // const opcode = block.opcode; + // const fieldName = Object.keys(block.fields)[0]; + // const fieldValue = block.fields[fieldName]; + // const primitiveDesc = [LONE_FIELD, opcode, fieldName, fieldValue]; + // if (block.topLevel) { + // primitiveDesc.push(block.x ? Math.round(block.x) : 0); + // primitiveDesc.push(block.y ? Math.round(block.y) : 0); + // } + // return primitiveDesc; + // } + if (hasOwnProperty.call(primitiveOpcodeInfoMap, block.opcode)) { + const primitiveInfo = primitiveOpcodeInfoMap[block.opcode]; + const primitiveConstant = primitiveInfo[0]; + const fieldName = primitiveInfo[1]; + const field = block.fields[fieldName]; + const primitiveDesc = [primitiveConstant, field.value]; + if (block.opcode === 'event_broadcast_menu') { + primitiveDesc.push(field.id); + } else if (block.opcode === 'data_variable' || block.opcode === 'data_listcontents') { + primitiveDesc.push(field.id); + if (block.topLevel) { + primitiveDesc.push(block.x ? Math.round(block.x) : 0); + primitiveDesc.push(block.y ? Math.round(block.y) : 0); + } + } + return primitiveDesc; + } + return null; +}; + +/** + * Serializes the inputs field of a block in a compact form using + * constants described above to represent the relationship between the + * inputs of this block (e.g. if there is an unobscured shadow, an obscured shadow + * -- a block plugged into a droppable input -- or, if there is just a block). + * Based on this relationship, serializes the ids of the block and shadow (if present) + * + * @param {object} inputs The inputs to serialize + * @return {object} An object representing the serialized inputs + */ +const serializeInputs = function (inputs) { + const obj = Object.create(null); + for (const inputName in inputs) { + if (!hasOwnProperty.call(inputs, inputName)) continue; + // if block and shadow refer to the same block, only serialize one + if (inputs[inputName].block === inputs[inputName].shadow) { + // has block and shadow, and they are the same + obj[inputName] = [ + INPUT_SAME_BLOCK_SHADOW, + inputs[inputName].block + ]; + } else if (inputs[inputName].shadow === null) { + // does not have shadow + obj[inputName] = [ + INPUT_BLOCK_NO_SHADOW, + inputs[inputName].block + ]; + } else { + // block and shadow are both present and are different + obj[inputName] = [ + INPUT_DIFF_BLOCK_SHADOW, + inputs[inputName].block, + inputs[inputName].shadow + ]; + } + } + return obj; +}; + +/** + * Serialize the fields of a block in a more compact form. + * @param {object} fields The fields object to serialize + * @return {object} An object representing the serialized fields + */ +const serializeFields = function (fields) { + const obj = Object.create(null); + for (const fieldName in fields) { + if (!hasOwnProperty.call(fields, fieldName)) continue; + obj[fieldName] = [fields[fieldName].value]; + if (fields[fieldName].hasOwnProperty('id')) { + obj[fieldName].push(fields[fieldName].id); + } + if (fields[fieldName].hasOwnProperty('variableType')) { + obj[fieldName].push(fields[fieldName].variableType); + } + } + return obj; +}; + +/** + * Serialize the given block in the SB3 format with some compression of inputs, + * fields, and primitives. + * @param {object} block The block to serialize + * @return {object | array} A serialized representation of the block. This is an + * array if the block is one of the primitive types described above or an object, + * if not. + */ +const serializeBlock = function (block) { + const serializedPrimitive = serializePrimitiveBlock(block); + if (serializedPrimitive) return serializedPrimitive; + // If serializedPrimitive is null, proceed with serializing a non-primitive block + const obj = Object.create(null); + obj.opcode = block.opcode; + // NOTE: this is extremely important to serialize even if null; + // not serializing `next: null` results in strange behavior with block + // execution + obj.next = block.next; + obj.parent = block.parent; + obj.inputs = serializeInputs(block.inputs); + obj.fields = serializeFields(block.fields); + obj.shadow = block.shadow; + if (block.topLevel) { + obj.topLevel = true; + obj.x = block.x ? Math.round(block.x) : 0; + obj.y = block.y ? Math.round(block.y) : 0; + } else { + obj.topLevel = false; + } + if (block.mutation) { + obj.mutation = block.mutation; + } + if (block.comment) { + obj.comment = block.comment; + } + return obj; +}; + +/** + * Compresses the serialized inputs replacing block/shadow ids that refer to + * one of the primitives with the primitive itself. E.g. + * + * blocks: { + * aUidForMyBlock: { + * inputs: { + * MYINPUT: [1, 'aUidForAnUnobscuredShadowPrimitive'] + * } + * }, + * aUidForAnUnobscuredShadowPrimitive: [4, 10] + * // the above is a primitive representing a 'math_number' with value 10 + * } + * + * becomes: + * + * blocks: { + * aUidForMyBlock: { + * inputs: { + * MYINPUT: [1, [4, 10]] + * } + * } + * } + * Note: this function modifies the given blocks object in place + * @param {object} block The block with inputs to compress + * @param {objec} blocks The object containing all the blocks currently getting serialized + * @return {object} The serialized block with compressed inputs + */ +const compressInputTree = function (block, blocks) { + // This is the second pass on the block + // so the inputs field should be an object of key - array pairs + const serializedInputs = block.inputs; + for (const inputName in serializedInputs) { + // don't need to check for hasOwnProperty because of how we constructed + // inputs + const currInput = serializedInputs[inputName]; + // traverse currInput skipping the first element, which describes whether the block + // and shadow are the same + for (let i = 1; i < currInput.length; i++) { + if (!currInput[i]) continue; // need this check b/c block/shadow can be null + const blockOrShadowID = currInput[i]; + // replace element of currInput directly + // (modifying input block directly) + const blockOrShadow = blocks[blockOrShadowID]; + if (Array.isArray(blockOrShadow)) { + currInput[i] = blockOrShadow; + // Modifying blocks in place! + delete blocks[blockOrShadowID]; + } + } + } + return block; +}; + +/** + * Get sanitized non-core extension ID for a given sb3 opcode. + * Note that this should never return a URL. If in the future the SB3 loader supports loading extensions by URL, this + * ID should be used to (for example) look up the extension's full URL from a table in the SB3's JSON. + * @param {!string} opcode The opcode to examine for extension. + * @return {?string} The extension ID, if it exists and is not a core extension. + */ +const getExtensionIdForOpcode = function (opcode) { + // Allowed ID characters are those matching the regular expression [\w-]: A-Z, a-z, 0-9, and hyphen ("-"). + if (!(typeof opcode === 'string')) { + console.error('invalid opcode ' + opcode); + return ''; + } + const index = opcode.indexOf('_'); + const forbiddenSymbols = /[^\w-]/g; + const prefix = opcode.substring(0, index).replace(forbiddenSymbols, '-'); + if (CORE_EXTENSIONS.indexOf(prefix) === -1) { + if (prefix !== '') return prefix; + } +}; + +/** + * @param {Runtime} runtime + * @returns {Array} runtime -> extensionIDs + */ +const getExtensionIDs = runtime => runtime._blockInfo + .map(ext => ext.id) + .filter(ext => runtime.extensionManager.isExtensionLoaded(ext)); + +/** + * @param {Set|string[]} extensionIDs Project extension IDs + * @param {Runtime} runtime + * @returns {Record|null} extension ID -> URL map, or null if no custom extensions. + */ +const getExtensionURLsToSave = (extensionIDs, runtime) => { + // Extension manager only exists when runtime is wrapped by VirtualMachine + if (!runtime.extensionManager) { + return null; + } + + // We'll save the extensions in the format: + // { + // "extensionid": "https://...", + // "otherid": "https://..." + // } + // Which lets the VM know which URLs correspond to which IDs, which is useful when the project + // is being loaded. For example, if the extension is eventually converted to a builtin extension + // or if it is already loaded, then it doesn't need to fetch the script again. + const extensionURLs = runtime.extensionManager.getExtensionURLs(); + const toSave = {}; + for (const extension of extensionIDs) { + const url = extensionURLs[extension]; + if (typeof url === 'string') { + toSave[extension] = url; + } + } + if (Object.keys(toSave).length === 0) { + return null; + } + return toSave; +}; + +/** + * Serialize the given blocks object (representing all the blocks for the target + * currently being serialized.) + * @param {object} blocks The blocks to be serialized + * @return {Array} An array of the serialized blocks with compressed inputs and + * compressed primitives and the list of all extension IDs present + * in the serialized blocks. + */ +const serializeBlocks = function (blocks) { + const obj = Object.create(null); + for (const blockID in blocks) { + if (!blocks.hasOwnProperty(blockID)) continue; + obj[blockID] = serializeBlock(blocks[blockID], blocks); + } + // once we have completed a first pass, do a second pass on block inputs + for (const blockID in obj) { + // don't need to do the hasOwnProperty check here since we + // created an object that doesn't get extra properties/functions + const serializedBlock = obj[blockID]; + // caution, this function deletes parts of this object in place as + // it's traversing it + obj[blockID] = compressInputTree(serializedBlock, obj); + // second pass on connecting primitives to serialized inputs directly + } + // Do one last pass and remove any top level shadows (these are caused by + // a bug: LLK/scratch-vm#1011, and this pass should be removed once that is + // completely fixed) + for (const blockID in obj) { + const serializedBlock = obj[blockID]; + // If the current block is serialized as a primitive (e.g. it's an array + // instead of an object), AND it is not one of the top level primitives + // e.g. variable getter or list getter, then it should be deleted as it's + // a shadow block, and there are no blocks that reference it, otherwise + // they would have been compressed in the last pass) + if (Array.isArray(serializedBlock) && + [VAR_PRIMITIVE, LIST_PRIMITIVE].indexOf(serializedBlock[0]) < 0) { + log.warn(`Found an unexpected top level primitive with block ID: ${ + blockID}; deleting it from serialized blocks.`); + delete obj[blockID]; + } + } + return obj; +}; + +/** + * @param {unknown} blocks Output of serializeStandaloneBlocks + * @returns {{blocks: Block[], extensionURLs: Map}} + */ +const deserializeStandaloneBlocks = blocks => { + // deep clone to ensure it's safe to modify later + blocks = JSON.parse(JSON.stringify(blocks)); + + if (blocks.extensionURLs) { + const extensionURLs = new Map(); + for (const [id, url] of Object.entries(blocks.extensionURLs)) { + extensionURLs.set(id, url); + } + return { + blocks: blocks.blocks, + extensionURLs + }; + } + + // Vanilla Scratch format is just a list of block objects + return { + blocks, + extensionURLs: new Map() + }; +}; + +/** + * @param {Block[]} blocks List of block objects. + * @param {Runtime} runtime Runtime + * @returns {object} Something that can be understood by deserializeStandaloneBlocks + */ +const serializeStandaloneBlocks = (blocks, runtime) => { + const extensionIDs = new Set(getExtensionIDs(runtime)); + const extensionURLs = getExtensionURLsToSave(extensionIDs, runtime); + if (extensionURLs) { + return { + blocks, + // same format as project.json + extensionURLs: extensionURLs + }; + } + // Vanilla Scratch always just uses the block array as-is. To reduce compatibility concerns + // we too will use that when possible. + return blocks; +}; + +/** + * Serialize the given costume. + * @param {object} costume The costume to be serialized. + * @return {object} A serialized representation of the costume. + */ +const serializeCostume = function (costume) { + const obj = Object.create(null); + obj.name = costume.name; + + const costumeToSerialize = costume.broken || costume; + + obj.bitmapResolution = costumeToSerialize.bitmapResolution; + obj.dataFormat = costumeToSerialize.dataFormat.toLowerCase(); + + obj.assetId = costumeToSerialize.assetId; + + // serialize this property with the name 'md5ext' because that's + // what it's actually referring to. TODO runtime objects need to be + // updated to actually refer to this as 'md5ext' instead of 'md5' + // but that change should be made carefully since it is very + // pervasive + obj.md5ext = costumeToSerialize.md5; + + obj.rotationCenterX = costumeToSerialize.rotationCenterX; + obj.rotationCenterY = costumeToSerialize.rotationCenterY; + + return obj; +}; + +/** + * Serialize the given sound. + * @param {object} sound The sound to be serialized. + * @return {object} A serialized representation of the sound. + */ +const serializeSound = function (sound) { + const obj = Object.create(null); + obj.name = sound.name; + + const soundToSerialize = sound.broken || sound; + + obj.assetId = soundToSerialize.assetId; + obj.dataFormat = soundToSerialize.dataFormat.toLowerCase(); + obj.format = soundToSerialize.format; + obj.rate = soundToSerialize.rate; + obj.sampleCount = soundToSerialize.sampleCount; + // serialize this property with the name 'md5ext' because that's + // what it's actually referring to. TODO runtime objects need to be + // updated to actually refer to this as 'md5ext' instead of 'md5' + // but that change should be made carefully since it is very + // pervasive + obj.md5ext = soundToSerialize.md5; + return obj; +}; + +// Using some bugs, it can be possible to get values like undefined, null, or complex objects into +// variables or lists. This will cause make the project unusable after exporting without JSON editing +// as it will fail validation in scratch-parser. +// To avoid this, we'll convert those objects to strings before saving them. +const isVariableValueSafeForJSON = value => ( + typeof value === 'number' || + typeof value === 'string' || + typeof value === 'boolean' +); +const makeSafeForJSON = (runtime, value) => { + if (Array.isArray(value)) { + let copy = null; + for (let i = 0; i < value.length; i++) { + if (value[i].customId) { + const {serialize} = runtime.serializers[value[i].customId]; + value[i] = serialize(value[i]); + } + if (!isVariableValueSafeForJSON(value[i])) { + if (!copy) { + // Only copy the list when needed + copy = value.slice(); + } + copy[i] = `${copy[i]}`; + } + } + if (copy) { + return copy; + } + return value; + } + if (value.customId) { + const {serialize} = runtime.serializers[value.customId]; + return { + customType: true, + typeId: value.customId, + serialized: serialize(value) + }; + } + if (isVariableValueSafeForJSON(value)) { + return value; + } + return `${value}`; +}; + +/** + * Serialize the given variables object. + * @param {object} variables The variables to be serialized. + * @return {object} A serialized representation of the variables. They get + * separated by type to compress the representation of each given variable and + * reduce duplicate information. + */ +const serializeVariables = function (obj, runtime, variables) { + // separate out variables into types at the top level so we don't have + // keep track of a type for each + obj.variables = Object.create(null); + obj.lists = Object.create(null); + obj.broadcasts = Object.create(null); + obj.customVars = []; + for (const varId in variables) { + const v = variables[varId]; + if (v.type === Variable.BROADCAST_MESSAGE_TYPE) { + obj.broadcasts[varId] = v.value; // name and value is the same for broadcast msgs + continue; + } + if (v.type === Variable.LIST_TYPE) { + obj.lists[varId] = [v.name, makeSafeForJSON(runtime, v.value)]; + continue; + } + if (v.type === Variable.SCALAR_TYPE) { + obj.variables[varId] = [v.name, makeSafeForJSON(runtime, v.value)]; + if (v.isCloud) obj.variables[varId].push(true); + continue; + } + // else custom variable type + const varInfo = v.serialize(); + varInfo.unshift(v.type); + obj.customVars.push(varInfo); + } +}; + +const serializeComments = function (comments) { + const obj = Object.create(null); + for (const commentId in comments) { + if (!comments.hasOwnProperty(commentId)) continue; + const comment = comments[commentId]; + + const serializedComment = Object.create(null); + serializedComment.blockId = comment.blockId; + serializedComment.x = comment.x; + serializedComment.y = comment.y; + serializedComment.width = comment.width; + serializedComment.height = comment.height; + serializedComment.minimized = comment.minimized; + serializedComment.text = comment.text; + + obj[commentId] = serializedComment; + } + return obj; +}; + +/** + * Serialize the given target. Only serialize properties that are necessary + * for saving and loading this target. + * @param {object} target The target to be serialized. + * @param {Set} extensions A set of extensions to add extension IDs to + * @return {object} A serialized representation of the given target. + */ +const serializeTarget = function (runtime, target) { + const obj = Object.create(null); + obj.isStage = target.isStage; + obj.name = obj.isStage ? 'Stage' : target.name; + serializeVariables(obj, runtime, target.variables); + obj.blocks = serializeBlocks(target.blocks); + obj.comments = serializeComments(target.comments); + + // TODO remove this check/patch when (#1901) is fixed + if (target.currentCostume < 0 || target.currentCostume >= target.costumes.length) { + log.warn(`currentCostume property for target ${target.name} is out of range`); + target.currentCostume = MathUtil.clamp(target.currentCostume, 0, target.costumes.length - 1); + } + + obj.currentCostume = target.currentCostume; + obj.costumes = target.costumes.map(serializeCostume); + obj.sounds = target.sounds.map(serializeSound); + obj.id = target.id; + if (target.hasOwnProperty('volume')) obj.volume = target.volume; + if (target.hasOwnProperty('layerOrder')) obj.layerOrder = target.layerOrder; + if (obj.isStage) { // Only the stage should have these properties + if (target.hasOwnProperty('tempo')) obj.tempo = target.tempo; + if (target.hasOwnProperty('videoTransparency')) obj.videoTransparency = target.videoTransparency; + if (target.hasOwnProperty('videoState')) obj.videoState = target.videoState; + if (target.hasOwnProperty('textToSpeechLanguage')) obj.textToSpeechLanguage = target.textToSpeechLanguage; + } else { // The stage does not need the following properties, but sprites should + obj.visible = target.visible; + obj.x = target.x; + obj.y = target.y; + obj.size = target.size; + obj.direction = target.direction; + obj.draggable = target.draggable; + obj.rotationStyle = target.rotationStyle; + } + + return obj; +}; + +const getSimplifiedLayerOrdering = function (targets) { + const layerOrders = targets.map(t => t.getLayerOrder()); + return MathUtil.reducedSortOrdering(layerOrders); +}; + +const serializeMonitors = function (monitors, runtime) { + // Monitors position is always stored as position from top-left corner in 480x360 stage. + const xOffset = (runtime.stageWidth - 480) / 2; + const yOffset = (runtime.stageHeight - 360) / 2; + return monitors.valueSeq() + // Don't include hidden monitors from extensions + // https://github.com/LLK/scratch-vm/issues/2331 + .filter(monitorData => { + const extensionID = getExtensionIdForOpcode(monitorData.opcode); + return !extensionID || monitorData.visible; + }) + .map(monitorData => { + const serializedMonitor = { + id: monitorData.id, + mode: monitorData.mode, + opcode: monitorData.opcode, + params: monitorData.params, + spriteName: monitorData.spriteName, + value: Array.isArray(monitorData.value) ? [] : 0, + width: monitorData.width, + height: monitorData.height, + x: monitorData.x - xOffset, + y: monitorData.y - yOffset, + visible: monitorData.visible + }; + if (monitorData.mode !== 'list') { + serializedMonitor.sliderMin = monitorData.sliderMin; + serializedMonitor.sliderMax = monitorData.sliderMax; + serializedMonitor.isDiscrete = monitorData.isDiscrete; + } + return serializedMonitor; + }); +}; + +/** + * Serializes the specified VM runtime. + * @param {!Runtime} runtime VM runtime instance to be serialized. + * @param {string=} targetId Optional target id if serializing only a single target + * @return {object} Serialized runtime instance. + */ +const serialize = function (runtime, targetId, {allowOptimization = true} = {}) { + // Fetch targets + const obj = Object.create(null); + // Create extension set to hold extension ids found while serializing targets + const extensions = getExtensionIDs(runtime); + + const originalTargetsToSerialize = targetId ? + [runtime.getTargetById(targetId)] : + runtime.targets.filter(target => target.isOriginal); + + const layerOrdering = getSimplifiedLayerOrdering(originalTargetsToSerialize); + + const flattenedOriginalTargets = originalTargetsToSerialize.map(t => t.toJSON()); + + // If the renderer is attached, and we're serializing a whole project (not a sprite) + // add a temporary layerOrder property to each target. + if (runtime.renderer && !targetId) { + flattenedOriginalTargets.forEach((t, index) => { + t.layerOrder = layerOrdering[index]; + }); + } + + const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(runtime, t, extensions)); + const fonts = runtime.fontManager.serializeJSON(); + + if (targetId) { + const target = serializedTargets[0]; + const extensionURLs = getExtensionURLsToSave(extensions, runtime); + target.extensions = extensions; + if (extensionURLs) { + target.extensionURLs = extensionURLs; + } + + // add extension datas + target.extensionData = {}; + for (const extension of extensions) { + if (`ext_${extension}` in runtime) { + if (typeof runtime[`ext_${extension}`].serialize === 'function') { + target.extensionData[extension] = runtime[`ext_${extension}`].serialize(); + } + } + } + + if (fonts) { + target.customFonts = fonts; + } + return target; + } + + obj.targets = serializedTargets; + + obj.monitors = serializeMonitors(runtime.getMonitorState(), runtime); + + // add extension datas + obj.extensionData = {}; + for (const extension of extensions) { + if (`ext_${extension}` in runtime) { + if (typeof runtime[`ext_${extension}`].serialize === 'function') { + obj.extensionData[extension] = runtime[`ext_${extension}`].serialize(); + } + } + } + + // Assemble extension list + obj.extensions = extensions; + const extensionURLs = getExtensionURLsToSave(extensions, runtime); + if (extensionURLs) { + obj.extensionURLs = extensionURLs; + } + + if (fonts) { + obj.customFonts = fonts; + } + + // Assemble metadata + const meta = Object.create(null); + meta.semver = '3.0.0'; + // TW: There isn't a good reason to put the full version number in the json, so we don't. + meta.vm = '0.2.0'; + if (runtime.origin) { + meta.origin = runtime.origin; + } + + // Attach full user agent string to metadata if available + meta.agent = ''; + // TW: Never include full user agent to slightly improve user privacy + // if (typeof navigator !== 'undefined') meta.agent = navigator.userAgent; + + // Attach platform information so TurboWarp and other mods can detect where the file comes from + const platform = Object.create(null); + platform.name = "PenguinMod"; + platform.url = "https://penguinmod.com/"; + platform.version = "stable"; + meta.platform = platform; + + // Assemble payload and return + obj.meta = meta; + + if (allowOptimization) { + compress(obj); + } + + return obj; +}; + +/** + * Deserialize a block input descriptors. This is either a + * block id or a serialized primitive, e.g. an array + * (see serializePrimitiveBlock function). + * @param {string | array} inputDescOrId The block input descriptor to be serialized. + * @param {string} parentId The id of the parent block for this input block. + * @param {boolean} isShadow Whether or not this input block is a shadow. + * @param {object} blocks The entire blocks object currently in the process of getting serialized. + * @return {object} The deserialized input descriptor. + */ +const deserializeInputDesc = function (inputDescOrId, parentId, isShadow, blocks) { + if (!Array.isArray(inputDescOrId)) return inputDescOrId; + const primitiveObj = Object.create(null); + const newId = uid(); + primitiveObj.id = newId; + primitiveObj.next = null; + primitiveObj.parent = parentId; + primitiveObj.shadow = isShadow; + primitiveObj.inputs = Object.create(null); + // need a reference to parent id + switch (inputDescOrId[0]) { + case MATH_NUM_PRIMITIVE: { + primitiveObj.opcode = 'math_number'; + primitiveObj.fields = { + NUM: { + name: 'NUM', + value: inputDescOrId[1] + } + }; + primitiveObj.topLevel = false; + break; + } + case POSITIVE_NUM_PRIMITIVE: { + primitiveObj.opcode = 'math_positive_number'; + primitiveObj.fields = { + NUM: { + name: 'NUM', + value: inputDescOrId[1] + } + }; + primitiveObj.topLevel = false; + break; + } + case WHOLE_NUM_PRIMITIVE: { + primitiveObj.opcode = 'math_whole_number'; + primitiveObj.fields = { + NUM: { + name: 'NUM', + value: inputDescOrId[1] + } + }; + primitiveObj.topLevel = false; + break; + } + case INTEGER_NUM_PRIMITIVE: { + primitiveObj.opcode = 'math_integer'; + primitiveObj.fields = { + NUM: { + name: 'NUM', + value: inputDescOrId[1] + } + }; + primitiveObj.topLevel = false; + break; + } + case ANGLE_NUM_PRIMITIVE: { + primitiveObj.opcode = 'math_angle'; + primitiveObj.fields = { + NUM: { + name: 'NUM', + value: inputDescOrId[1] + } + }; + primitiveObj.topLevel = false; + break; + } + case COLOR_PICKER_PRIMITIVE: { + primitiveObj.opcode = 'colour_picker'; + primitiveObj.fields = { + COLOUR: { + name: 'COLOUR', + value: inputDescOrId[1] + } + }; + primitiveObj.topLevel = false; + break; + } + case TEXT_PRIMITIVE: { + primitiveObj.opcode = 'text'; + primitiveObj.fields = { + TEXT: { + name: 'TEXT', + value: inputDescOrId[1] + } + }; + primitiveObj.topLevel = false; + break; + } + case BROADCAST_PRIMITIVE: { + primitiveObj.opcode = 'event_broadcast_menu'; + primitiveObj.fields = { + BROADCAST_OPTION: { + name: 'BROADCAST_OPTION', + value: inputDescOrId[1], + id: inputDescOrId[2], + variableType: Variable.BROADCAST_MESSAGE_TYPE + } + }; + primitiveObj.topLevel = false; + break; + } + case VAR_PRIMITIVE: { + primitiveObj.opcode = 'data_variable'; + primitiveObj.fields = { + VARIABLE: { + name: 'VARIABLE', + value: inputDescOrId[1], + id: inputDescOrId[2], + variableType: Variable.SCALAR_TYPE + } + }; + if (inputDescOrId.length > 3) { + primitiveObj.topLevel = true; + primitiveObj.x = inputDescOrId[3]; + primitiveObj.y = inputDescOrId[4]; + } + break; + } + case LIST_PRIMITIVE: { + primitiveObj.opcode = 'data_listcontents'; + primitiveObj.fields = { + LIST: { + name: 'LIST', + value: inputDescOrId[1], + id: inputDescOrId[2], + variableType: Variable.LIST_TYPE + } + }; + if (inputDescOrId.length > 3) { + primitiveObj.topLevel = true; + primitiveObj.x = inputDescOrId[3]; + primitiveObj.y = inputDescOrId[4]; + } + break; + } + case LONE_FIELD: { + primitiveObj.opcode = inputDescOrId[1]; + primitiveObj.fields = { + [inputDescOrId[2]]: inputDescOrId[3] + }; + if (inputDescOrId.length > 4) { + primitiveObj.topLevel = true; + primitiveObj.x = inputDescOrId[4]; + primitiveObj.y = inputDescOrId[5]; + } + break; + } + default: { + log.error(`Found unknown primitive type during deserialization: ${JSON.stringify(inputDescOrId)}`); + return null; + } + } + blocks[newId] = primitiveObj; + return newId; +}; + +/** + * Deserialize the given block inputs. + * @param {object} inputs The inputs to deserialize. + * @param {string} parentId The block id of the parent block + * @param {object} blocks The object representing the entire set of blocks currently + * in the process of getting deserialized. + * @return {object} The deserialized and uncompressed inputs. + */ +const deserializeInputs = function (inputs, parentId, blocks) { + // Explicitly not using Object.create(null) here + // because we call prototype functions later in the vm + const obj = {}; + for (const inputName in inputs) { + if (!hasOwnProperty.call(inputs, inputName)) continue; + const inputDescArr = inputs[inputName]; + // If this block has already been deserialized (it's not an array) skip it + if (!Array.isArray(inputDescArr)) continue; + let block = null; + let shadow = null; + const blockShadowInfo = inputDescArr[0]; + if (blockShadowInfo === INPUT_SAME_BLOCK_SHADOW) { + // block and shadow are the same id, and only one is provided + block = shadow = deserializeInputDesc(inputDescArr[1], parentId, true, blocks); + } else if (blockShadowInfo === INPUT_BLOCK_NO_SHADOW) { + block = deserializeInputDesc(inputDescArr[1], parentId, false, blocks); + } else { // assume INPUT_DIFF_BLOCK_SHADOW + block = deserializeInputDesc(inputDescArr[1], parentId, false, blocks); + shadow = deserializeInputDesc(inputDescArr[2], parentId, true, blocks); + } + obj[inputName] = { + name: inputName, + block: block, + shadow: shadow + }; + } + return obj; +}; + +/** + * Deserialize the given block fields. + * @param {object} fields The fields to be deserialized + * @return {object} The deserialized and uncompressed block fields. + */ +const deserializeFields = function (fields) { + // Explicitly not using Object.create(null) here + // because we call prototype functions later in the vm + const obj = {}; + for (const fieldName in fields) { + if (!hasOwnProperty.call(fields, fieldName)) continue; + const fieldDescArr = fields[fieldName]; + // If this block has already been deserialized (it's not an array) skip it + if (!Array.isArray(fieldDescArr)) continue; + obj[fieldName] = { + name: fieldName, + value: fieldDescArr[0] + }; + if (fieldDescArr.length > 1) { + obj[fieldName].id = fieldDescArr[1]; + } + if (fieldDescArr.length > 2) { + obj[fieldName].variableType = fieldDescArr[2]; + } + // "old" compat code :bleh: + if (fieldName === 'BROADCAST_OPTION') { + obj[fieldName].variableType = Variable.BROADCAST_MESSAGE_TYPE; + } else if (fieldName === 'VARIABLE') { + obj[fieldName].variableType = Variable.SCALAR_TYPE; + } else if (fieldName === 'LIST') { + obj[fieldName].variableType = Variable.LIST_TYPE; + } + } + return obj; +}; + +/** + * Covnert serialized INPUT and FIELD primitives back to hydrated block templates. + * Should be able to deserialize a format that has already been deserialized. The only + * "east" path to adding new targets/code requires going through deserialize, so it should + * work with pre-parsed deserialized blocks. + * + * @param {object} blocks Serialized SB3 "blocks" property of a target. Will be mutated. + * @return {object} input is modified and returned + */ +const deserializeBlocks = function (blocks) { + for (const blockId in blocks) { + if (!Object.prototype.hasOwnProperty.call(blocks, blockId)) { + continue; + } + const block = blocks[blockId]; + if (Array.isArray(block)) { + // this is one of the primitives + // delete the old entry in object.blocks and replace it with the + // deserialized object + delete blocks[blockId]; + deserializeInputDesc(block, null, false, blocks); + continue; + } + block.id = blockId; // add id back to block since it wasn't serialized + block.inputs = deserializeInputs(block.inputs, blockId, blocks); + block.fields = deserializeFields(block.fields); + } + return blocks; +}; + +/** + * Parse the assets of a single "Scratch object" and load them. This + * preprocesses objects to support loading the data for those assets over a + * network while the objects are further processed into Blocks, Sprites, and a + * list of needed Extensions. + * @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher. + * @param {!Runtime} runtime Runtime object to load all structures into. + * @param {JSZip} zip Sb3 file describing this project (to load assets from) + * @return {?{costumePromises:Array.,soundPromises:Array.,soundBank:SoundBank}} + * Object of arrays of promises for asset objects used in Sprites. As well as a + * SoundBank for the sound assets. null for unsupported objects. + */ +const parseScratchAssets = function (object, runtime, zip) { + if (!object.hasOwnProperty('name')) { + // Watcher/monitor - skip this object until those are implemented in VM. + // @todo + return Promise.resolve(null); + } + + const assets = { + costumePromises: null, + soundPromises: null, + soundBank: runtime.audioEngine && runtime.audioEngine.createBank() + }; + + // Costumes from JSON. + assets.costumePromises = (object.costumes || []).map(costumeSource => { + // @todo: Make sure all the relevant metadata is being pulled out. + const costume = { + // costumeSource only has an asset if an image is being uploaded as + // a sprite + asset: costumeSource.asset, + assetId: costumeSource.assetId, + skinId: null, + name: costumeSource.name, + bitmapResolution: costumeSource.bitmapResolution, + rotationCenterX: costumeSource.rotationCenterX, + rotationCenterY: costumeSource.rotationCenterY + }; + const dataFormat = + costumeSource.dataFormat || + (costumeSource.assetType && costumeSource.assetType.runtimeFormat) || // older format + 'png'; // if all else fails, guess that it might be a PNG + const costumeMd5Ext = costumeSource.hasOwnProperty('md5ext') ? + costumeSource.md5ext : `${costumeSource.assetId}.${dataFormat}`; + costume.md5 = costumeMd5Ext; + costume.dataFormat = dataFormat; + // deserializeCostume should be called on the costume object we're + // creating above instead of the source costume object, because this way + // we're always loading the 'sb3' representation of the costume + // any translation that needs to happen will happen in the process + // of building up the costume object into an sb3 format + return deserializeCostume(costume, runtime, zip) + .then(() => loadCostume(costumeMd5Ext, costume, runtime)); + // Only attempt to load the costume after the deserialization + // process has been completed + }); + // Sounds from JSON + assets.soundPromises = (object.sounds || []).map(soundSource => { + const sound = { + assetId: soundSource.assetId, + format: soundSource.format, + rate: soundSource.rate, + sampleCount: soundSource.sampleCount, + name: soundSource.name, + // TODO we eventually want this property to be called md5ext, + // but there are many things relying on this particular name at the + // moment, so this translation is very important + md5: soundSource.md5ext, + dataFormat: soundSource.dataFormat, + data: null + }; + // deserializeSound should be called on the sound object we're + // creating above instead of the source sound object, because this way + // we're always loading the 'sb3' representation of the costume + // any translation that needs to happen will happen in the process + // of building up the costume object into an sb3 format + return deserializeSound(sound, runtime, zip) + .then(() => loadSound(sound, runtime, assets.soundBank)); + // Only attempt to load the sound after the deserialization + // process has been completed. + }); + + return assets; +}; + +/** + * Parse a single "Scratch object" and create all its in-memory VM objects. + * @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher. + * @param {!Runtime} runtime Runtime object to load all structures into. + * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. + * @param {JSZip} zip Sb3 file describing this project (to load assets from) + * @param {object} assets - Promises for assets of this scratch object grouped + * into costumes and sounds + * @return {!Promise.} Promise for the target created (stage or sprite), or null for unsupported objects. + */ +const parseScratchObject = function (object, runtime, extensions, zip, assets) { + if (!object.hasOwnProperty('name')) { + // Watcher/monitor - skip this object until those are implemented in VM. + // @todo + return Promise.resolve(null); + } + // Blocks container for this object. + const blocks = new Blocks(runtime); + + // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite. + const sprite = new Sprite(blocks, runtime); + + // Sprite/stage name from JSON. + if (object.hasOwnProperty('name')) { + sprite.name = object.name; + } + if (object.hasOwnProperty('blocks')) { + // register and patch extensions + for (const blockId in object.blocks) { + if (!object.blocks.hasOwnProperty(blockId)) continue; + const blockJSON = object.blocks[blockId]; + // this is a internal constant and cant be patched + if (typeof blockJSON !== 'object' || Array.isArray(blockJSON)) continue; + const extensionID = getExtensionIdForOpcode(blockJSON.opcode); + const isPatched = extensions.patcher.patchExists(extensionID); + if (isPatched) { + extensions.patcher.runExtensionPatch(extensionID, extensions, object); + } + } + + deserializeBlocks(object.blocks); + // Take a second pass to create objects and add extensions + for (const blockId in object.blocks) { + if (!object.blocks.hasOwnProperty(blockId)) continue; + const blockJSON = object.blocks[blockId]; + blocks.createBlock(blockJSON); + } + } + // Costumes from JSON. + const {costumePromises} = assets; + // Sounds from JSON + const {soundBank, soundPromises} = assets; + // Create the first clone, and load its run-state from JSON. + const target = sprite.createClone(object.isStage ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER); + // Load target properties from JSON. + if (object.hasOwnProperty('tempo')) { + target.tempo = object.tempo; + } + if (object.hasOwnProperty('volume')) { + target.volume = object.volume; + } + if (object.hasOwnProperty('videoTransparency')) { + target.videoTransparency = object.videoTransparency; + } + if (object.hasOwnProperty('videoState')) { + target.videoState = object.videoState; + } + if (object.hasOwnProperty('textToSpeechLanguage')) { + target.textToSpeechLanguage = object.textToSpeechLanguage; + } + if (object.hasOwnProperty('variables')) { + for (const varId in object.variables) { + const variable = object.variables[varId]; + // A variable is a cloud variable if: + // - the project says it's a cloud variable, and + // - it's a stage variable, and + // - the runtime can support another cloud variable + const isCloud = (variable.length === 3) && variable[2] && + object.isStage && runtime.canAddCloudVariable(); + const newVariable = new Variable( + varId, // var id is the index of the variable desc array in the variables obj + variable[0], // name of the variable + Variable.SCALAR_TYPE, // type of the variable + isCloud + ); + if (isCloud) runtime.addCloudVariable(); + newVariable.value = variable[1]; + target.variables[newVariable.id] = newVariable; + } + } + if (object.hasOwnProperty('lists')) { + for (const listId in object.lists) { + const list = object.lists[listId]; + const newList = new Variable( + listId, + list[0], + Variable.LIST_TYPE, + false + ); + newList.value = list[1]; + target.variables[newList.id] = newList; + } + } + if (object.hasOwnProperty('broadcasts')) { + for (const broadcastId in object.broadcasts) { + const broadcast = object.broadcasts[broadcastId]; + const newBroadcast = new Variable( + broadcastId, + broadcast, + Variable.BROADCAST_MESSAGE_TYPE, + false + ); + // no need to explicitly set the value, variable constructor + // sets the value to the same as the name for broadcast msgs + target.variables[newBroadcast.id] = newBroadcast; + } + } + if (object.hasOwnProperty('customVars')) { + for (const info of object.customVars) { + // im lay z so customVars is just a list of arg lists to be passed into the variable creator + const newVar = runtime.newVariableInstance(...info); + target.variables[newVar.id] = newVar; + } + } + if (object.hasOwnProperty('comments')) { + for (const commentId in object.comments) { + const comment = object.comments[commentId]; + const newComment = new Comment( + commentId, + comment.text, + comment.x, + comment.y, + comment.width, + comment.height, + comment.minimized + ); + if (comment.blockId) { + newComment.blockId = comment.blockId; + } + target.comments[newComment.id] = newComment; + } + } + if (object.hasOwnProperty('x')) { + target.x = object.x; + } + if (object.hasOwnProperty('y')) { + target.y = object.y; + } + if (object.hasOwnProperty('direction')) { + target.direction = object.direction; + } + if (object.hasOwnProperty('size')) { + target.size = object.size; + } + if (object.hasOwnProperty('visible')) { + target.visible = object.visible; + } + if (object.hasOwnProperty('currentCostume')) { + target.currentCostume = MathUtil.clamp(object.currentCostume, 0, object.costumes.length - 1); + } + if (object.hasOwnProperty('rotationStyle')) { + target.rotationStyle = object.rotationStyle; + } + if (object.hasOwnProperty('isStage')) { + target.isStage = object.isStage; + } + if (object.hasOwnProperty('targetPaneOrder')) { + // Temporarily store the 'targetPaneOrder' property + // so that we can correctly order sprites in the target pane. + // This will be deleted after we are done parsing and ordering the targets list. + target.targetPaneOrder = object.targetPaneOrder; + } + if (object.hasOwnProperty('draggable')) { + target.draggable = object.draggable; + } + const existingTargetIds = runtime.targets.map(target => target.id); + if (object.hasOwnProperty('id') && !existingTargetIds.includes(object.id)) { + target.id = object.id; + } + Promise.all(costumePromises).then(costumes => { + sprite.costumes = costumes; + }); + Promise.all(soundPromises).then(sounds => { + sprite.sounds = sounds; + // Make sure if soundBank is undefined, sprite.soundBank is then null. + sprite.soundBank = soundBank || null; + }); + return Promise.all(costumePromises.concat(soundPromises)).then(() => target); +}; + +const deserializeMonitor = function (monitorData, runtime, targets, extensions) { + // Monitors position is always stored as position from top-left corner in 480x360 stage. + const xOffset = (runtime.stageWidth - 480) / 2; + const yOffset = (runtime.stageHeight - 360) / 2; + monitorData.x += xOffset; + monitorData.y += yOffset; + monitorData.x = MathUtil.clamp(monitorData.x, 0, runtime.stageWidth); + monitorData.y = MathUtil.clamp(monitorData.y, 0, runtime.stageHeight); + + // If the serialized monitor has spriteName defined, look up the sprite + // by name in the given list of targets and update the monitor's targetId + // to match the sprite's id. + if (monitorData.spriteName) { + const filteredTargets = targets.filter(t => t.sprite.name === monitorData.spriteName); + if (filteredTargets && filteredTargets.length > 0) { + monitorData.targetId = filteredTargets[0].id; + } else { + log.warn(`Tried to deserialize sprite specific monitor ${ + monitorData.opcode} but could not find sprite ${monitorData.spriteName}.`); + } + } + + // Get information about this monitor, if it exists, given the monitor's opcode. + // This will be undefined for extension blocks + const monitorBlockInfo = runtime.monitorBlockInfo[monitorData.opcode]; + + // Due to a bug (see https://github.com/LLK/scratch-vm/pull/2322), renamed list monitors may have been serialized + // with an outdated/incorrect LIST parameter. Fix it up to use the current name of the actual corresponding list. + if (monitorData.opcode === 'data_listcontents') { + const listTarget = monitorData.targetId ? + targets.find(t => t.id === monitorData.targetId) : + targets.find(t => t.isStage); + if ( + listTarget && + Object.prototype.hasOwnProperty.call(listTarget.variables, monitorData.id) + ) { + monitorData.params.LIST = listTarget.variables[monitorData.id].name; + } + } + + // Convert the serialized monitorData params into the block fields structure + const fields = {}; + for (const paramKey in monitorData.params) { + const field = { + name: paramKey, + value: monitorData.params[paramKey] + }; + fields[paramKey] = field; + } + + // Variables, lists, and non-sprite-specific monitors, including any extension + // monitors should already have the correct monitor ID serialized in the monitorData, + // find the correct id for all other monitors. + if (monitorData.opcode !== 'data_variable' && monitorData.opcode !== 'data_listcontents' && + monitorBlockInfo && monitorBlockInfo.isSpriteSpecific) { + monitorData.id = monitorBlockInfo.getId( + monitorData.targetId, fields); + } else { + // Replace unsafe characters in monitor ID, if there are any. + // These would have come from projects that were originally 2.0 projects + // that had unsafe characters in the variable name (and then the name was + // used as part of the variable ID when importing the project). + monitorData.id = StringUtil.replaceUnsafeChars(monitorData.id); + } + + // If the runtime already has a monitor block for this monitor's id, + // update the existing block with the relevant monitor information. + const existingMonitorBlock = runtime.monitorBlocks._blocks[monitorData.id]; + if (existingMonitorBlock) { + // A monitor block already exists if the toolbox has been loaded and + // the monitor block is not target specific (because the block gets recycled). + existingMonitorBlock.isMonitored = monitorData.visible; + existingMonitorBlock.targetId = monitorData.targetId; + } else { + // If a monitor block doesn't already exist for this monitor, + // construct a monitor block to add to the monitor blocks container + const monitorBlock = { + id: monitorData.id, + opcode: monitorData.opcode, + inputs: {}, // Assuming that monitor blocks don't have droppable fields + fields: fields, + topLevel: true, + next: null, + parent: null, + shadow: false, + x: 0, + y: 0, + isMonitored: monitorData.visible, + targetId: monitorData.targetId + }; + + // Variables and lists have additional properties + // stored in their fields, update this info in the + // monitor block fields + if (monitorData.opcode === 'data_variable') { + const field = monitorBlock.fields.VARIABLE; + field.id = monitorData.id; + field.variableType = Variable.SCALAR_TYPE; + } else if (monitorData.opcode === 'data_listcontents') { + const field = monitorBlock.fields.LIST; + field.id = monitorData.id; + field.variableType = Variable.LIST_TYPE; + } + + runtime.monitorBlocks.createBlock(monitorBlock); + } + + runtime.requestAddMonitor(MonitorRecord(monitorData)); +}; + +// Replace variable IDs throughout the project with +// xml-safe versions. +// This is to fix up projects imported from 2.0 where xml-unsafe names +// were getting added to the variable ids. +const replaceUnsafeCharsInVariableIds = function (targets) { + const allVarRefs = VariableUtil.getAllVarRefsForTargets(targets, true); + // Re-id the variables in the actual targets + targets.forEach(t => { + Object.keys(t.variables).forEach(id => { + const newId = StringUtil.replaceUnsafeChars(id); + if (newId === id) return; + t.variables[id].id = newId; + t.variables[newId] = t.variables[id]; + delete t.variables[id]; + }); + }); + + // Replace the IDs in the blocks refrencing variables or lists + for (const id in allVarRefs) { + const newId = StringUtil.replaceUnsafeChars(id); + if (id === newId) continue; // ID was already safe, skip + // We're calling this on the stage target because we need a + // target to call on but this shouldn't matter because we're passing + // in all the varRefs we want to operate on + VariableUtil.updateVariableIdentifiers(allVarRefs[id], newId); + } + return targets; +}; + +/** + * Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance. + * @param {object} json - JSON representation of a VM runtime. + * @param {Runtime} runtime - Runtime instance + * @param {JSZip} zip - Sb3 file describing this project (to load assets from) + * @param {boolean} isSingleSprite - If true treat as single sprite, else treat as whole project + * @returns {Promise.} Promise that resolves to the list of targets after the project is deserialized + */ +const deserialize = function (json, runtime, zip, isSingleSprite) { + const extensionPatcher = new OldExtensions(runtime); + extensionPatcher.registerExtensions(ExtensionPatches); + const extensions = { + extensionIDs: new Set(json.extensions), + extensionURLs: new Map(), + extensionData: {}, + patcher: extensionPatcher + }; + + // Store the origin field (e.g. project originated at CSFirst) so that we can save it again. + if (json.meta && json.meta.origin) { + runtime.origin = json.meta.origin; + } else { + runtime.origin = null; + } + + // Extract custom extension IDs, if they exist. + if (json.extensionURLs) { + extensions.extensionURLs = new Map(Object.entries(json.extensionURLs)); + } + if (json.extensionData) { + extensions.extensionData = json.extensionData; + } + + // Extract any custom fonts before loading costumes. + let fontPromise; + if (json.customFonts) { + fontPromise = runtime.fontManager.deserialize(json.customFonts, zip, isSingleSprite); + } else { + fontPromise = Promise.resolve(); + } + + // First keep track of the current target order in the json, + // then sort by the layer order property before parsing the targets + // so that their corresponding render drawables can be created in + // their layer order (e.g. back to front) + const targetObjects = ((isSingleSprite ? [json] : json.targets) || []) + .map((t, i) => Object.assign(t, {targetPaneOrder: i})) + .sort((a, b) => a.layerOrder - b.layerOrder); + + const monitorObjects = json.monitors || []; + + return fontPromise.then(() => targetObjects.map(target => parseScratchAssets(target, runtime, zip))) + // Force this promise to wait for the next loop in the js tick. Let + // storage have some time to send off asset requests. + .then(assets => Promise.resolve(assets)) + .then(assets => Promise.all(targetObjects + .map((target, index) => + parseScratchObject(target, runtime, extensions, zip, assets[index])))) + .then(targets => targets // Re-sort targets back into original sprite-pane ordering + .map((t, i) => { + // Add layer order property to deserialized targets. + // This property is used to initialize executable targets in + // the correct order and is deleted in VM's installTargets function + t.layerOrder = i; + return t; + }) + .sort((a, b) => a.targetPaneOrder - b.targetPaneOrder) + .map(t => { + // Delete the temporary properties used for + // sprite pane ordering and stage layer ordering + delete t.targetPaneOrder; + return t; + })) + .then(targets => replaceUnsafeCharsInVariableIds(targets)) + .then(targets => { + monitorObjects.map(monitorDesc => deserializeMonitor(monitorDesc, runtime, targets, extensions)); + return targets; + }) + .then(targets => ({ + targets, + extensions + })); +}; + +module.exports = { + serialize: serialize, + deserialize: deserialize, + deserializeBlocks: deserializeBlocks, + serializeBlocks: serializeBlocks, + deserializeStandaloneBlocks: deserializeStandaloneBlocks, + serializeStandaloneBlocks: serializeStandaloneBlocks, + getExtensionIdForOpcode: getExtensionIdForOpcode +}; diff --git a/local-scratch-vm/src/serialization/serialize-assets.js b/local-scratch-vm/src/serialization/serialize-assets.js new file mode 100644 index 0000000000000000000000000000000000000000..62b28b12382873f180e07acf9237efcf7773e32f --- /dev/null +++ b/local-scratch-vm/src/serialization/serialize-assets.js @@ -0,0 +1,60 @@ +/** + * Serialize all the assets of the given type ('sounds' or 'costumes') + * in the provided runtime into an array of file descriptors. + * A file descriptor is an object containing the name of the file + * to be written and the contents of the file, the serialized asset. + * @param {Runtime} runtime The runtime with the assets to be serialized + * @param {string} assetType The type of assets to be serialized: 'sounds' | 'costumes' + * @param {string=} optTargetId Optional target id to serialize assets for + * @returns {Array} An array of file descriptors for each asset + */ +const serializeAssets = function (runtime, assetType, optTargetId) { + const targets = optTargetId ? [runtime.getTargetById(optTargetId)] : runtime.targets; + const assetDescs = []; + for (let i = 0; i < targets.length; i++) { + const currTarget = targets[i]; + const currAssets = currTarget.sprite[assetType]; + for (let j = 0; j < currAssets.length; j++) { + const currAsset = currAssets[j]; + const asset = currAsset.broken ? currAsset.broken.asset : currAsset.asset; + if (asset) { + // Serialize asset if it exists, otherwise skip + assetDescs.push({ + fileName: `${asset.assetId}.${asset.dataFormat}`, + fileContent: asset.data + }); + } + } + } + return assetDescs; +}; + +/** + * Serialize all the sounds in the provided runtime or, if a target id is provided, + * in the specified target into an array of file descriptors. + * A file descriptor is an object containing the name of the file + * to be written and the contents of the file, the serialized sound. + * @param {Runtime} runtime The runtime with the sounds to be serialized + * @param {string=} optTargetId Optional targetid for serializing sounds of a single target + * @returns {Array} An array of file descriptors for each sound + */ +const serializeSounds = function (runtime, optTargetId) { + return serializeAssets(runtime, 'sounds', optTargetId); +}; + +/** + * Serialize all the costumes in the provided runtime into an array of file + * descriptors. A file descriptor is an object containing the name of the file + * to be written and the contents of the file, the serialized costume. + * @param {Runtime} runtime The runtime with the costumes to be serialized + * @param {string} optTargetId Optional targetid for serializing costumes of a single target + * @returns {Array} An array of file descriptors for each costume + */ +const serializeCostumes = function (runtime, optTargetId) { + return serializeAssets(runtime, 'costumes', optTargetId); +}; + +module.exports = { + serializeSounds, + serializeCostumes +}; diff --git a/local-scratch-vm/src/serialization/tw-compress-sb3.js b/local-scratch-vm/src/serialization/tw-compress-sb3.js new file mode 100644 index 0000000000000000000000000000000000000000..22dfedfd42666e96960887ec606b32d40849701b --- /dev/null +++ b/local-scratch-vm/src/serialization/tw-compress-sb3.js @@ -0,0 +1,160 @@ +// We don't generate new IDs using numbers at this time because their enumeration +// order can affect script execution order as they always come first. +// https://tc39.es/ecma262/#sec-ordinaryownpropertykeys +const SOUP = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#%()*+,-./:;=?@[]^_`{|}~'; +const generateId = i => { + let str = ''; + while (i >= 0) { + str = SOUP[i % SOUP.length] + str; + i = Math.floor(i / SOUP.length) - 1; + } + return str; +}; + +class Pool { + constructor () { + this.generatedIds = new Map(); + this.references = new Map(); + this.skippedIds = new Set(); + // IDs in Object.keys(vm.runtime.monitorBlocks._blocks) already have meaning, so make sure to skip those + // We don't bother listing many here because most would take more than ten million items to be used + this.skippedIds.add('of'); + } + skip (id) { + this.skippedIds.add(id); + } + addReference (id) { + const currentCount = this.references.get(id) || 0; + this.references.set(id, currentCount + 1); + } + generateNewIds () { + const entries = Array.from(this.references.entries()); + // The most used original IDs should get the shortest new IDs. + entries.sort((a, b) => b[1] - a[1]); + + let i = 0; + for (const entry of entries) { + const oldId = entry[0]; + + let newId = generateId(i); + while (this.skippedIds.has(newId)) { + i++; + newId = generateId(i); + } + + this.generatedIds.set(oldId, newId); + i++; + } + } + getNewId (originalId) { + if (this.generatedIds.has(originalId)) { + return this.generatedIds.get(originalId); + } + return originalId; + } +} + +const compress = projectData => { + // projectData is modified in-place + + // The optimization here is not optimal. This is intentional. + // We only compress block and comment IDs because we want to maintain 100% (not 99.99%; 100%) compatibility and be + // truly lossless. Optimizing things like variable IDs will cause things such as the editor's backpack feature + // to misbehave. + + // We use the same variable pool for all objects to avoid any possible issues if IDs are ever treated as unique + // within a given project. + const pool = new Pool(); + + for (const target of projectData.targets) { + // While we don't compress these IDs, we need to make sure that our compressed IDs + // do not intersect, which could happen if the project was compressed with a + // different tool. + for (const variableId of Object.keys(target.variables)) { + pool.skip(variableId); + } + for (const listId of Object.keys(target.lists)) { + pool.skip(listId); + } + for (const broadcastId of Object.keys(target.broadcasts)) { + pool.skip(broadcastId); + } + for (const blockId of Object.keys(target.blocks)) { + const block = target.blocks[blockId]; + pool.addReference(blockId); + if (Array.isArray(block)) { + // Compressed native + continue; + } + if (block.parent) { + pool.addReference(block.parent); + } + if (block.next) { + pool.addReference(block.next); + } + if (block.comment) { + pool.addReference(block.comment); + } + for (const input of Object.values(block.inputs)) { + for (let i = 1; i < input.length; i++) { + const inputValue = input[i]; + if (typeof inputValue === 'string') { + pool.addReference(inputValue); + } + } + } + } + + for (const commentId of Object.keys(target.comments)) { + const comment = target.comments[commentId]; + pool.addReference(commentId); + if (comment.blockId) { + pool.addReference(comment.blockId); + } + } + } + + pool.generateNewIds(); + for (const target of projectData.targets) { + const newBlocks = {}; + const newComments = {}; + for (const blockId of Object.keys(target.blocks)) { + const block = target.blocks[blockId]; + newBlocks[pool.getNewId(blockId)] = block; + if (Array.isArray(block)) { + // Compressed native + continue; + } + if (block.parent) { + block.parent = pool.getNewId(block.parent); + } + if (block.next) { + block.next = pool.getNewId(block.next); + } + if (block.comment) { + block.comment = pool.getNewId(block.comment); + } + for (const input of Object.values(block.inputs)) { + for (let i = 1; i < input.length; i++) { + const inputValue = input[i]; + if (typeof inputValue === 'string') { + input[i] = pool.getNewId(inputValue); + } + } + } + } + + for (const commentId of Object.keys(target.comments)) { + const comment = target.comments[commentId]; + newComments[pool.getNewId(commentId)] = comment; + if (comment.blockId) { + comment.blockId = pool.getNewId(comment.blockId); + } + } + + target.blocks = newBlocks; + target.comments = newComments; + } +}; + +module.exports = compress; diff --git a/local-scratch-vm/src/serialization/tw-costume-import-export.js b/local-scratch-vm/src/serialization/tw-costume-import-export.js new file mode 100644 index 0000000000000000000000000000000000000000..5725510dfbdabe9a4a2b9c9e67a57c3404dab103 --- /dev/null +++ b/local-scratch-vm/src/serialization/tw-costume-import-export.js @@ -0,0 +1,78 @@ +// We want to preserve the rotation center of exported SVGs when they are later imported. +// Unfortunately, the SVG itself does not have sufficient information to accomplish this. +// Instead we must add a small amount of extra information to the end of exported SVGs +// that can be read on import. + +// Adding this comment in scratch-paint is not a viable approach because the user can +// open projects not made with TurboWarp and we want costumes exported from there to +// have their center saved even if they haven't been edited. + +let _TextEncoder; +let _TextDecoder; +if (typeof TextEncoder === 'undefined') { + _TextEncoder = require('text-encoding').TextEncoder; + _TextDecoder = require('text-encoding').TextDecoder; +} else { + _TextEncoder = TextEncoder; + _TextDecoder = TextDecoder; +} + +// Using literal HTML comments tokens will cause this script to be very hard to inline in +// a ', + '', + '' + ].join("\n"); + + const blob = new Blob([html], { type: 'text/html;charset=UTF-8' }); + const url = URL.createObjectURL(blob); + + return url; +}; + +class SandboxRunner { + static execute(code) { + return new Promise(resolve => { + const frame = createFrame(); + /** + * please vscode show me the autofill + * @param {MessageEvent} e + */ + const trueHandler = e => { + // this code is weird but we need to remove + // event handler ladter + // console.log(e); // debug + messageHandler(e, frame, trueHandler).then(payload => { + // console.log(payload) + resolve({ + success: payload.success, + value: payload.value + }); + }); + }; + window.addEventListener('message', trueHandler); + frame.src = generateEvaluateSrc(code, frame); + }); + } +} + +module.exports = SandboxRunner; diff --git a/local-scratch-vm/src/util/scratch-link-websocket.js b/local-scratch-vm/src/util/scratch-link-websocket.js new file mode 100644 index 0000000000000000000000000000000000000000..24f74e970cd49dfb3eae7585eb8c279e488d0308 --- /dev/null +++ b/local-scratch-vm/src/util/scratch-link-websocket.js @@ -0,0 +1,135 @@ +/** + * This class provides a ScratchLinkSocket implementation using WebSockets, + * attempting to connect with the locally installed Scratch-Link. + * + * To connect with ScratchLink without WebSockets, you must implement all of the + * public methods in this class. + * - open() + * - close() + * - setOn[Open|Close|Error] + * - setHandleMessage + * - sendMessage(msgObj) + * - isOpen() + */ +class ScratchLinkWebSocket { + constructor (type) { + this._type = type; + this._onOpen = null; + this._onClose = null; + this._onError = null; + this._handleMessage = null; + + this._ws = null; + } + + open () { + if (!(this._onOpen && this._onClose && this._onError && this._handleMessage)) { + throw new Error('Must set open, close, message and error handlers before calling open on the socket'); + } + + let pathname; + switch (this._type) { + case 'BLE': + pathname = 'scratch/ble'; + break; + case 'BT': + pathname = 'scratch/bt'; + break; + default: + throw new Error(`Unknown ScratchLink socket Type: ${this._type}`); + } + + // Try ws:// (the new way) and wss:// (the old way) simultaneously. If either connects, close the other. If we + // were to try one and fall back to the other on failure, that could mean a delay of 30 seconds or more for + // those who need the fallback. + // If both connections fail we should report only one error. + + const setSocket = (socketToUse, socketToClose) => { + socketToClose.onopen = socketToClose.onerror = null; + socketToClose.close(); + + this._ws = socketToUse; + this._ws.onopen = this._onOpen; + this._ws.onclose = this._onClose; + this._ws.onerror = this._onError; + this._ws.onmessage = this._onMessage.bind(this); + }; + + const ws = new WebSocket(`ws://127.0.0.1:20111/${pathname}`); + const wss = new WebSocket(`wss://device-manager.scratch.mit.edu:20110/${pathname}`); + + const connectTimeout = setTimeout(() => { + // neither socket succeeded before the timeout + setSocket(ws, wss); + this._ws.onerror(new Event('timeout')); + }, 15 * 1000); + ws.onopen = openEvent => { + clearTimeout(connectTimeout); + setSocket(ws, wss); + this._ws.onopen(openEvent); + }; + wss.onopen = openEvent => { + clearTimeout(connectTimeout); + setSocket(wss, ws); + this._ws.onopen(openEvent); + }; + + let wsError; + let wssError; + const errorHandler = () => { + // if only one has received an error, we haven't overall failed yet + if (wsError && wssError) { + clearTimeout(connectTimeout); + setSocket(ws, wss); + this._ws.onerror(wsError); + } + }; + ws.onerror = errorEvent => { + wsError = errorEvent; + errorHandler(); + }; + wss.onerror = errorEvent => { + wssError = errorEvent; + errorHandler(); + }; + } + + close () { + if (this.isOpen()) { + this._ws.close(); + this._ws = null; + } + } + + sendMessage (message) { + const messageText = JSON.stringify(message); + this._ws.send(messageText); + } + + setOnOpen (fn) { + this._onOpen = fn; + } + + setOnClose (fn) { + this._onClose = fn; + } + + setOnError (fn) { + this._onError = fn; + } + + setHandleMessage (fn) { + this._handleMessage = fn; + } + + isOpen () { + return this._ws && this._ws.readyState === this._ws.OPEN; + } + + _onMessage (e) { + const json = JSON.parse(e.data); + this._handleMessage(json); + } +} + +module.exports = ScratchLinkWebSocket; diff --git a/local-scratch-vm/src/util/string-util.js b/local-scratch-vm/src/util/string-util.js new file mode 100644 index 0000000000000000000000000000000000000000..6fc99530f963732e51f44fa0bd2ea870a4d534f0 --- /dev/null +++ b/local-scratch-vm/src/util/string-util.js @@ -0,0 +1,94 @@ +const log = require('./log'); + +class StringUtil { + static withoutTrailingDigits (s) { + let i = s.length - 1; + while ((i >= 0) && ('0123456789'.indexOf(s.charAt(i)) > -1)) i--; + return s.slice(0, i + 1); + } + + static unusedName (name, existingNames) { + if (existingNames.indexOf(name) < 0) return name; + name = StringUtil.withoutTrailingDigits(name); + let i = 2; + while (existingNames.indexOf(name + i) >= 0) i++; + return name + i; + } + + /** + * Split a string on the first occurrence of a split character. + * @param {string} text - the string to split. + * @param {string} separator - split the text on this character. + * @returns {string[]} - the two parts of the split string, or [text, null] if no split character found. + * @example + * // returns ['foo', 'tar.gz'] + * splitFirst('foo.tar.gz', '.'); + * @example + * // returns ['foo', null] + * splitFirst('foo', '.'); + * @example + * // returns ['foo', ''] + * splitFirst('foo.', '.'); + */ + static splitFirst (text, separator) { + const index = text.indexOf(separator); + if (index >= 0) { + return [text.substring(0, index), text.substring(index + 1)]; + } + return [text, null]; + + } + + /** + * A customized version of JSON.stringify that sets Infinity/NaN to 0, + * instead of the default (null). + * Needed because null is not of type number, but Infinity/NaN are, which + * can lead to serialization producing JSON that isn't valid based on the parser schema. + * It is also consistent with the behavior of saving 2.0 projects. + * This is only needed when stringifying an object for saving. + * + * @param {!object} obj - The object to serialize + * @return {!string} The JSON.stringified string with Infinity/NaN replaced with 0 + */ + static stringify (obj) { + return JSON.stringify(obj, (_key, value) => { + if (typeof value === 'number' && + (value === Infinity || value === -Infinity || isNaN(value))){ + return 0; + } + return value; + }); + } + /** + * A function to replace unsafe characters (not allowed in XML) with safe ones. This is used + * in cases where we're replacing non-user facing strings (e.g. variable IDs). + * When replacing user facing strings, the xmlEscape utility function should be used + * instead so that the user facing string does not change how it displays. + * @param {!string | !Array.} unsafe Unsafe string possibly containing unicode control characters. + * In some cases this argument may be an array (e.g. hacked inputs from 2.0) + * @return {string} String with control characters replaced. + */ + static replaceUnsafeChars (unsafe) { + if (typeof unsafe !== 'string') { + if (Array.isArray(unsafe)) { + // This happens when we have hacked blocks from 2.0 + // See #1030 + unsafe = String(unsafe); + } else { + log.error('Unexpected input recieved in replaceUnsafeChars'); + return unsafe; + } + } + return unsafe.replace(/[<>&'"]/g, c => { + switch (c) { + case '<': return 'lt'; + case '>': return 'gt'; + case '&': return 'amp'; + case '\'': return 'apos'; + case '"': return 'quot'; + } + }); + } +} + +module.exports = StringUtil; diff --git a/local-scratch-vm/src/util/task-queue.js b/local-scratch-vm/src/util/task-queue.js new file mode 100644 index 0000000000000000000000000000000000000000..0b452ffb9ce46b5a628be4795ab5c9dc542738a0 --- /dev/null +++ b/local-scratch-vm/src/util/task-queue.js @@ -0,0 +1,201 @@ +const Timer = require('../util/timer'); + +/** + * This class uses the token bucket algorithm to control a queue of tasks. + */ +class TaskQueue { + /** + * Creates an instance of TaskQueue. + * To allow bursts, set `maxTokens` to several times the average task cost. + * To prevent bursts, set `maxTokens` to the cost of the largest tasks. + * Note that tasks with a cost greater than `maxTokens` will be rejected. + * + * @param {number} maxTokens - the maximum number of tokens in the bucket (burst size). + * @param {number} refillRate - the number of tokens to be added per second (sustain rate). + * @param {object} options - optional settings for the new task queue instance. + * @property {number} startingTokens - the number of tokens the bucket starts with (default: `maxTokens`). + * @property {number} maxTotalCost - reject a task if total queue cost would pass this limit (default: no limit). + * @memberof TaskQueue + */ + constructor (maxTokens, refillRate, options = {}) { + this._maxTokens = maxTokens; + this._refillRate = refillRate; + this._pendingTaskRecords = []; + this._tokenCount = options.hasOwnProperty('startingTokens') ? options.startingTokens : maxTokens; + this._maxTotalCost = options.hasOwnProperty('maxTotalCost') ? options.maxTotalCost : Infinity; + this._timer = new Timer(); + this._timer.start(); + this._timeout = null; + this._lastUpdateTime = this._timer.timeElapsed(); + + this._runTasks = this._runTasks.bind(this); + } + + /** + * Get the number of queued tasks which have not yet started. + * + * @readonly + * @memberof TaskQueue + */ + get length () { + return this._pendingTaskRecords.length; + } + + /** + * Wait until the token bucket is full enough, then run the provided task. + * + * @param {Function} task - the task to run. + * @param {number} [cost=1] - the number of tokens this task consumes from the bucket. + * @returns {Promise} - a promise for the task's return value. + * @memberof TaskQueue + */ + do (task, cost = 1) { + if (this._maxTotalCost < Infinity) { + const currentTotalCost = this._pendingTaskRecords.reduce((t, r) => t + r.cost, 0); + if (currentTotalCost + cost > this._maxTotalCost) { + return Promise.reject('Maximum total cost exceeded'); + } + } + const newRecord = { + cost + }; + newRecord.promise = new Promise((resolve, reject) => { + newRecord.cancel = () => { + reject(new Error('Task canceled')); + }; + + // The caller, `_runTasks()`, is responsible for cost-checking and spending tokens. + newRecord.wrappedTask = () => { + try { + resolve(task()); + } catch (e) { + reject(e); + } + }; + }); + this._pendingTaskRecords.push(newRecord); + + // If the queue has been idle we need to prime the pump + if (this._pendingTaskRecords.length === 1) { + this._runTasks(); + } + + return newRecord.promise; + } + + /** + * Cancel one pending task, rejecting its promise. + * + * @param {Promise} taskPromise - the promise returned by `do()`. + * @returns {boolean} - true if the task was found, or false otherwise. + * @memberof TaskQueue + */ + cancel (taskPromise) { + const taskIndex = this._pendingTaskRecords.findIndex(r => r.promise === taskPromise); + if (taskIndex !== -1) { + const [taskRecord] = this._pendingTaskRecords.splice(taskIndex, 1); + taskRecord.cancel(); + if (taskIndex === 0 && this._pendingTaskRecords.length > 0) { + this._runTasks(); + } + return true; + } + return false; + } + + /** + * Cancel all pending tasks, rejecting all their promises. + * + * @memberof TaskQueue + */ + cancelAll () { + if (this._timeout !== null) { + this._timer.clearTimeout(this._timeout); + this._timeout = null; + } + const oldTasks = this._pendingTaskRecords; + this._pendingTaskRecords = []; + oldTasks.forEach(r => r.cancel()); + } + + /** + * Shorthand for calling _refill() then _spend(cost). + * + * @see {@link TaskQueue#_refill} + * @see {@link TaskQueue#_spend} + * @param {number} cost - the number of tokens to try to spend. + * @returns {boolean} true if we had enough tokens; false otherwise. + * @memberof TaskQueue + */ + _refillAndSpend (cost) { + this._refill(); + return this._spend(cost); + } + + /** + * Refill the token bucket based on the amount of time since the last refill. + * + * @memberof TaskQueue + */ + _refill () { + const now = this._timer.timeElapsed(); + const timeSinceRefill = now - this._lastUpdateTime; + if (timeSinceRefill <= 0) return; + + this._lastUpdateTime = now; + this._tokenCount += timeSinceRefill * this._refillRate / 1000; + this._tokenCount = Math.min(this._tokenCount, this._maxTokens); + } + + /** + * If we can "afford" the given cost, subtract that many tokens and return true. + * Otherwise, return false. + * + * @param {number} cost - the number of tokens to try to spend. + * @returns {boolean} true if we had enough tokens; false otherwise. + * @memberof TaskQueue + */ + _spend (cost) { + if (cost <= this._tokenCount) { + this._tokenCount -= cost; + return true; + } + return false; + } + + /** + * Loop until the task queue is empty, running each task and spending tokens to do so. + * Any time the bucket can't afford the next task, delay asynchronously until it can. + * + * @memberof TaskQueue + */ + _runTasks () { + if (this._timeout) { + this._timer.clearTimeout(this._timeout); + this._timeout = null; + } + for (;;) { + const nextRecord = this._pendingTaskRecords.shift(); + if (!nextRecord) { + // We ran out of work. Go idle until someone adds another task to the queue. + return; + } + if (nextRecord.cost > this._maxTokens) { + throw new Error(`Task cost ${nextRecord.cost} is greater than bucket limit ${this._maxTokens}`); + } + // Refill before each task in case the time it took for the last task to run was enough to afford the next. + if (this._refillAndSpend(nextRecord.cost)) { + nextRecord.wrappedTask(); + } else { + // We can't currently afford this task. Put it back and wait until we can and try again. + this._pendingTaskRecords.unshift(nextRecord); + const tokensNeeded = Math.max(nextRecord.cost - this._tokenCount, 0); + const estimatedWait = Math.ceil(1000 * tokensNeeded / this._refillRate); + this._timeout = this._timer.setTimeout(this._runTasks, estimatedWait); + return; + } + } + } +} + +module.exports = TaskQueue; diff --git a/local-scratch-vm/src/util/text leveler.js b/local-scratch-vm/src/util/text leveler.js new file mode 100644 index 0000000000000000000000000000000000000000..cb36db1cc87097134d0940ad8e91fe7b45f08d0a --- /dev/null +++ b/local-scratch-vm/src/util/text leveler.js @@ -0,0 +1,27 @@ +/** + * creates a string of a given length from a given content + * @param {Number} length the goal length + * @param {String} contents what to make the string from + * @returns {String} a string with contents repeated length times + */ +const makeString = (length, contents) => { + let array; + for (array = []; array.length < length; array.push(contents)); + return array.join(); +}; + +/** + * levels text so its always the same length + * @param {String} text the text to level + * @param {Number} length the length to level to + * @param {String} sus the filler character + * @returns {String} the leveled text + */ +const levelText = (text, length, sus) => { + if (text.length === length) return text; + if (text.length > length) return text.slice(0, length + 1); + const full = makeString(length, sus); + return `${full.slice(0, length - text.length)}${text}`; +}; + +module.exports = levelText; diff --git a/local-scratch-vm/src/util/timer.js b/local-scratch-vm/src/util/timer.js new file mode 100644 index 0000000000000000000000000000000000000000..a2114351cfa6c434dd11e0f161efb00a2c2347e6 --- /dev/null +++ b/local-scratch-vm/src/util/timer.js @@ -0,0 +1,138 @@ +/** + * @fileoverview + * A utility for accurately measuring time. + * To use: + * --- + * var timer = new Timer(); + * timer.start(); + * ... pass some time ... + * var timeDifference = timer.timeElapsed(); + * --- + * Or, you can use the `time` and `relativeTime` + * to do some measurement yourself. + */ + +class Timer { + constructor (nowObj = Timer.nowObj) { + /** + * Used to store the start time of a timer action. + * Updated when calling `timer.start`. + */ + this.startTime = 0; + + /** + * Used to pass custom logic for determining the value for "now", + * which is sometimes useful for compatibility with Scratch 2 + */ + this.nowObj = nowObj; + + /** + * Detirmins if this timer is paused or not + */ + this._pausedTime = null; + } + + /** + * Disable use of self.performance for now as it results in lower performance + * However, instancing it like below (caching the self.performance to a local variable) negates most of the issues. + * @type {boolean} + */ + static get USE_PERFORMANCE () { + return false; + } + + /** + * Legacy object to allow for us to call now to get the old style date time (for backwards compatibility) + * @deprecated This is only called via the nowObj.now() if no other means is possible... + */ + static get legacyDateCode () { + return { + now: function () { + return new Date().getTime(); + } + }; + } + + /** + * Use this object to route all time functions through single access points. + */ + static get nowObj () { + if (Timer.USE_PERFORMANCE && typeof self !== 'undefined' && self.performance && 'now' in self.performance) { + return self.performance; + } else if (Date.now) { + return Date; + } + return Timer.legacyDateCode; + } + + /** + * Return the currently known absolute time, in ms precision. + * @returns {number} ms elapsed since 1 January 1970 00:00:00 UTC. + */ + time () { + return this.nowObj.now(); + } + + /** + * Returns a time accurate relative to other times produced by this function. + * If possible, will use sub-millisecond precision. + * If not, will use millisecond precision. + * Not guaranteed to produce the same absolute values per-system. + * @returns {number} ms-scale accurate time relative to other relative times. + */ + relativeTime () { + return this.nowObj.now(); + } + + /** + * Start a timer for measuring elapsed time, + * at the most accurate precision possible. + */ + start () { + this.startTime = this.nowObj.now(); + } + + /** + * pause the timer + */ + pause() { + if (this._pausedTime) return; + this._pausedTime = this.timeElapsed(); + } + + /** + * unpause the timer + */ + play() { + if (!this._pausedTime) return; + this.startTime = this.nowObj.now() - this._pausedTime; + this._pausedTime = null; + } + + timeElapsed () { + if (this._pausedTime) return this._pausedTime; + const now = this.nowObj.now(); + return now - this.startTime; + } + + /** + * Call a handler function after a specified amount of time has elapsed. + * @param {function} handler - function to call after the timeout + * @param {number} timeout - number of milliseconds to delay before calling the handler + * @returns {number} - the ID of the new timeout + */ + setTimeout (handler, timeout) { + return global.setTimeout(handler, timeout); + } + + /** + * Clear a timeout from the pending timeout pool. + * @param {number} timeoutId - the ID returned by `setTimeout()` + * @memberof Timer + */ + clearTimeout (timeoutId) { + global.clearTimeout(timeoutId); + } +} + +module.exports = Timer; diff --git a/local-scratch-vm/src/util/tw-asset-util.js b/local-scratch-vm/src/util/tw-asset-util.js new file mode 100644 index 0000000000000000000000000000000000000000..0774b91b4a13ffe2e6b1b09a456626b3b39a66ec --- /dev/null +++ b/local-scratch-vm/src/util/tw-asset-util.js @@ -0,0 +1,43 @@ +const StringUtil = require('./string-util'); + +class AssetUtil { + /** + * @param {Runtime} runtime runtime with storage attached + * @param {JSZip} zip optional JSZip to search for asset in + * @param {Storage.assetType} assetType scratch-storage asset type + * @param {string} md5ext full md5 with file extension + * @returns {Promise} scratch-storage asset object + */ + static getByMd5ext(runtime, zip, assetType, md5ext) { + const storage = runtime.storage; + const idParts = StringUtil.splitFirst(md5ext, '.'); + const md5 = idParts[0]; + const ext = idParts[1].toLowerCase(); + + if (zip) { + // Search the root of the zip + let file = zip.file(md5ext); + + // Search subfolders of the zip + // This matches behavior of deserialize-assets.js + if (!file) { + const fileMatch = new RegExp(`^([^/]*/)?${md5ext}$`); + file = zip.file(fileMatch)[0]; + } + + if (file) { + return file.async('uint8array').then(data => runtime.storage.createAsset( + assetType, + ext, + data, + md5, + false + )); + } + } + + return storage.load(assetType, md5, ext); + } +} + +module.exports = AssetUtil; \ No newline at end of file diff --git a/local-scratch-vm/src/util/uid.js b/local-scratch-vm/src/util/uid.js new file mode 100644 index 0000000000000000000000000000000000000000..fd4c41e8710ca068e1871f392d4fa608e01f914d --- /dev/null +++ b/local-scratch-vm/src/util/uid.js @@ -0,0 +1,29 @@ +/** + * @fileoverview UID generator, from Blockly. + */ + +/** + * Legal characters for the unique ID. + * Should be all on a US keyboard. No XML special characters or control codes. + * Removed $ due to issue 251. + * @private + */ +const soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' + + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + +/** + * Generate a unique ID, from Blockly. This should be globally unique. + * 87 characters ^ 20 length > 128 bits (better than a UUID). + * @return {string} A globally unique ID string. + */ +const uid = function () { + const length = 20; + const soupLength = soup_.length; + const id = []; + for (let i = 0; i < length; i++) { + id[i] = soup_.charAt(Math.random() * soupLength); + } + return id.join(''); +}; + +module.exports = uid; diff --git a/local-scratch-vm/src/util/variable-util.js b/local-scratch-vm/src/util/variable-util.js new file mode 100644 index 0000000000000000000000000000000000000000..3e34a7327757956fddefb466093159de0a91bb31 --- /dev/null +++ b/local-scratch-vm/src/util/variable-util.js @@ -0,0 +1,48 @@ +class VariableUtil { + static _mergeVarRefObjects (accum, obj2) { + for (const id in obj2) { + if (accum[id]) { + accum[id] = accum[id].concat(obj2[id]); + } else { + accum[id] = obj2[id]; + } + } + return accum; + } + + /** + * Get all variable/list references in the given list of targets + * in the project. + * @param {Array.} targets The list of targets to get the variable + * and list references from. + * @param {boolean} shouldIncludeBroadcast Whether to include broadcast message fields. + * @return {object} An object with variable ids as the keys and a list of block fields referencing + * the variable. + */ + static getAllVarRefsForTargets (targets, shouldIncludeBroadcast) { + return targets + .map(t => t.blocks.getAllVariableAndListReferences(null, shouldIncludeBroadcast)) + .reduce(VariableUtil._mergeVarRefObjects, {}); + } + + /** + * Give all variable references provided a new id and possibly new name. + * @param {Array} referencesToUpdate Context of the change, the object containing variable + * references to update. + * @param {string} newId ID of the variable that the old references should be replaced with + * @param {?string} optNewName New variable name to merge with. The old + * variable name in the references being updated should be replaced with this new name. + * If this parameter is not provided or is '', no name change occurs. + */ + static updateVariableIdentifiers (referencesToUpdate, newId, optNewName) { + referencesToUpdate.map(ref => { + ref.referencingField.id = newId; + if (optNewName) { + ref.referencingField.value = optNewName; + } + return ref; + }); + } +} + +module.exports = VariableUtil; diff --git a/local-scratch-vm/src/util/xml-escape.js b/local-scratch-vm/src/util/xml-escape.js new file mode 100644 index 0000000000000000000000000000000000000000..953c7b40d64f121d6f92dc01b0857360d462e677 --- /dev/null +++ b/local-scratch-vm/src/util/xml-escape.js @@ -0,0 +1,44 @@ +const log = require('./log'); + +/** + * Escape a string to be safe to use in XML content. + * CC-BY-SA: hgoebl + * https://stackoverflow.com/questions/7918868/ + * how-to-escape-xml-entities-in-javascript + * @param {!string | !Array.} unsafe Unsafe string. + * @return {string} XML-escaped string, for use within an XML tag. + */ +const xmlEscape = function (unsafe) { + if (typeof unsafe !== 'string') { + if (Array.isArray(unsafe)) { + // This happens when we have hacked blocks from 2.0 + // See #1030 + unsafe = String(unsafe); + } else { + log.error(`Unexptected type ${typeof unsafe} in xmlEscape at: ${new Error().stack}`); + return unsafe; + } + } + return unsafe.replace(/[<>&'"]/g, c => { + switch (c) { + case '<': return '<'; + case '>': return '>'; + case '&': return '&'; + case '\'': return '''; + case '"': return '"'; + } + }); +}; + +/** + * creates escaped text suitible for attributes + * @param {string} unsafe the contents to escape + * @returns {string} escaped contents + */ +const escapeAttribute = unsafe => { + const escaped = xmlEscape(unsafe); + return JSON.stringify(escaped).slice(1, -1); +}; + +module.exports = xmlEscape; +module.exports.escapeAttribute = escapeAttribute; diff --git a/local-scratch-vm/src/virtual-machine.js b/local-scratch-vm/src/virtual-machine.js new file mode 100644 index 0000000000000000000000000000000000000000..a131680c1ab6f1144e21fd7c8c929592713186d0 --- /dev/null +++ b/local-scratch-vm/src/virtual-machine.js @@ -0,0 +1,2010 @@ +let _TextEncoder; +if (typeof TextEncoder === 'undefined') { + _TextEncoder = require('text-encoding').TextEncoder; +} else { + /* global TextEncoder */ + _TextEncoder = TextEncoder; +} +const EventEmitter = require('events'); +const JSZip = require('jszip'); + +const Buffer = require('buffer').Buffer; +const centralDispatch = require('./dispatch/central-dispatch'); +const ExtensionManager = require('./extension-support/extension-manager'); +const log = require('./util/log'); +const MathUtil = require('./util/math-util'); +const Runtime = require('./engine/runtime'); +const StringUtil = require('./util/string-util'); +const RenderedTarget = require('./sprites/rendered-target'); +const StageLayering = require('./engine/stage-layering'); +const Sprite = require('./sprites/sprite'); +const Blocks = require('./engine/blocks'); +const formatMessage = require('format-message'); + +const Variable = require('./engine/variable'); +const newBlockIds = require('./util/new-block-ids'); + +const {loadCostume} = require('./import/load-costume.js'); +const {loadSound} = require('./import/load-sound.js'); +const {serializeSounds, serializeCostumes} = require('./serialization/serialize-assets'); +require('canvas-toBlob'); +const {exportCostume} = require('./serialization/tw-costume-import-export'); +const Base64Util = require('./util/base64-util'); + +const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; +const PM_LIBRARY_API = "https://library.penguinmod.com/"; + +const IRGenerator = require('./compiler/irgen'); +const JSGenerator = require('./compiler/jsgen'); +const jsexecute = require('./compiler/jsexecute'); +const { SyntheticModule } = require('vm'); + +const CORE_EXTENSIONS = [ + // 'motion', + // 'looks', + // 'sound', + // 'events', + // 'control', + // 'sensing', + // 'operators', + // 'variables', + // 'myBlocks' +]; + +// Disable missing translation warnings in console +formatMessage.setup({ + missingTranslation: 'ignore' +}); + +const createRuntimeService = runtime => { + const service = {}; + service._refreshExtensionPrimitives = runtime._refreshExtensionPrimitives.bind(runtime); + service._registerExtensionPrimitives = runtime._registerExtensionPrimitives.bind(runtime); + service._removeExtensionPrimitive = runtime._removeExtensionPrimitive.bind(runtime); + return service; +}; + +/** + * Handles connections between blocks, stage, and extensions. + * @constructor + */ +class VirtualMachine extends EventEmitter { + constructor () { + super(); + + /** + * VM runtime, to store blocks, I/O devices, sprites/targets, etc. + * @type {!Runtime} + */ + this.runtime = new Runtime(); + centralDispatch.setService('runtime', createRuntimeService(this.runtime)).catch(e => { + log.error(`Failed to register runtime service: ${JSON.stringify(e)}`); + }); + + /** + * The "currently editing"/selected target ID for the VM. + * Block events from any Blockly workspace are routed to this target. + * @type {Target} + */ + this.editingTarget = null; + + /** + * The currently dragging target, for redirecting IO data. + * @type {Target} + */ + this._dragTarget = null; + + // Runtime emits are passed along as VM emits. + this.runtime.on(Runtime.SCRIPT_GLOW_ON, glowData => { + this.emit(Runtime.SCRIPT_GLOW_ON, glowData); + }); + this.runtime.on(Runtime.SCRIPT_GLOW_OFF, glowData => { + this.emit(Runtime.SCRIPT_GLOW_OFF, glowData); + }); + this.runtime.on(Runtime.BLOCK_GLOW_ON, glowData => { + this.emit(Runtime.BLOCK_GLOW_ON, glowData); + }); + this.runtime.on(Runtime.BLOCK_GLOW_OFF, glowData => { + this.emit(Runtime.BLOCK_GLOW_OFF, glowData); + }); + this.runtime.on(Runtime.PROJECT_START, () => { + this.emit(Runtime.PROJECT_START); + }); + this.runtime.on(Runtime.PROJECT_RUN_START, () => { + this.emit(Runtime.PROJECT_RUN_START); + }); + this.runtime.on(Runtime.PROJECT_RUN_STOP, () => { + this.emit(Runtime.PROJECT_RUN_STOP); + }); + this.runtime.on(Runtime.PROJECT_CHANGED, () => { + this.emit(Runtime.PROJECT_CHANGED); + }); + this.runtime.on(Runtime.VISUAL_REPORT, visualReport => { + this.emit(Runtime.VISUAL_REPORT, visualReport); + }); + this.runtime.on(Runtime.BLOCK_STACK_ERROR, visualReport => { + this.emit(Runtime.BLOCK_STACK_ERROR, visualReport); + }); + this.runtime.on(Runtime.TARGETS_UPDATE, emitProjectChanged => { + this.emitTargetsUpdate(emitProjectChanged); + }); + this.runtime.on(Runtime.MONITORS_UPDATE, monitorList => { + this.emit(Runtime.MONITORS_UPDATE, monitorList); + }); + this.runtime.on(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui => { + this.emit(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui); + }); + this.runtime.on(Runtime.BLOCK_DRAG_END, (blocks, topBlockId) => { + this.emit(Runtime.BLOCK_DRAG_END, blocks, topBlockId); + }); + this.runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => { + this.emit(Runtime.EXTENSION_ADDED, categoryInfo); + }); + this.runtime.on(Runtime.EXTENSION_REMOVED, () => { + this.emit(Runtime.EXTENSION_REMOVED); + }); + this.runtime.on(Runtime.EXTENSION_FIELD_ADDED, (fieldName, fieldImplementation) => { + this.emit(Runtime.EXTENSION_FIELD_ADDED, fieldName, fieldImplementation); + }); + this.runtime.on(Runtime.BLOCKSINFO_UPDATE, categoryInfo => { + this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo); + }); + this.runtime.on(Runtime.BLOCKS_NEED_UPDATE, () => { + this.emitWorkspaceUpdate(); + }); + this.runtime.on(Runtime.TOOLBOX_EXTENSIONS_NEED_UPDATE, () => { + this.extensionManager.refreshBlocks(); + }); + this.runtime.on(Runtime.PERIPHERAL_LIST_UPDATE, info => { + this.emit(Runtime.PERIPHERAL_LIST_UPDATE, info); + }); + this.runtime.on(Runtime.USER_PICKED_PERIPHERAL, info => { + this.emit(Runtime.USER_PICKED_PERIPHERAL, info); + }); + this.runtime.on(Runtime.PERIPHERAL_CONNECTED, () => + this.emit(Runtime.PERIPHERAL_CONNECTED) + ); + this.runtime.on(Runtime.PERIPHERAL_REQUEST_ERROR, () => + this.emit(Runtime.PERIPHERAL_REQUEST_ERROR) + ); + this.runtime.on(Runtime.PERIPHERAL_DISCONNECTED, () => + this.emit(Runtime.PERIPHERAL_DISCONNECTED) + ); + this.runtime.on(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, data => + this.emit(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, data) + ); + this.runtime.on(Runtime.PERIPHERAL_SCAN_TIMEOUT, () => + this.emit(Runtime.PERIPHERAL_SCAN_TIMEOUT) + ); + this.runtime.on(Runtime.MIC_LISTENING, listening => { + this.emit(Runtime.MIC_LISTENING, listening); + }); + this.runtime.on(Runtime.RUNTIME_STARTED, () => { + this.emit(Runtime.RUNTIME_STARTED); + }); + this.runtime.on(Runtime.RUNTIME_PAUSED, () => { + this.emit(Runtime.RUNTIME_PAUSED); + }); + this.runtime.on(Runtime.RUNTIME_UNPAUSED, () => { + this.emit(Runtime.RUNTIME_UNPAUSED); + }); + this.runtime.on(Runtime.RUNTIME_STOPPED, () => { + this.emit(Runtime.RUNTIME_STOPPED); + }); + this.runtime.on(Runtime.HAS_CLOUD_DATA_UPDATE, hasCloudData => { + this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, hasCloudData); + }); + this.runtime.on(Runtime.RUNTIME_OPTIONS_CHANGED, runtimeOptions => { + this.emit(Runtime.RUNTIME_OPTIONS_CHANGED, runtimeOptions); + }); + this.runtime.on(Runtime.COMPILER_OPTIONS_CHANGED, compilerOptions => { + this.emit(Runtime.COMPILER_OPTIONS_CHANGED, compilerOptions); + }); + this.runtime.on(Runtime.FRAMERATE_CHANGED, framerate => { + this.emit(Runtime.FRAMERATE_CHANGED, framerate); + }); + this.runtime.on(Runtime.INTERPOLATION_CHANGED, framerate => { + this.emit(Runtime.INTERPOLATION_CHANGED, framerate); + }); + this.runtime.on(Runtime.BEFORE_INTERPOLATE, target => { + this.emit(Runtime.BEFORE_INTERPOLATE, target); + }); + this.runtime.on(Runtime.AFTER_INTERPOLATE, target => { + this.emit(Runtime.AFTER_INTERPOLATE, target); + }); + this.runtime.on(Runtime.STAGE_SIZE_CHANGED, (width, height) => { + this.emit(Runtime.STAGE_SIZE_CHANGED, width, height); + }); + this.runtime.on(Runtime.COMPILE_ERROR, (target, error) => { + this.emit(Runtime.COMPILE_ERROR, target, error); + }); + this.runtime.on(Runtime.TURBO_MODE_OFF, () => { + this.emit(Runtime.TURBO_MODE_OFF); + }); + this.runtime.on(Runtime.TURBO_MODE_ON, () => { + this.emit(Runtime.TURBO_MODE_ON); + }); + + this.extensionManager = new ExtensionManager(this); + this.securityManager = this.extensionManager.securityManager; + this.runtime.extensionManager = this.extensionManager; + this.runtime.vm = this; + + // Load core extensions + for (const id of CORE_EXTENSIONS) { + this.extensionManager.loadExtensionIdSync(id); + } + + this.blockListener = this.blockListener.bind(this); + this.flyoutBlockListener = this.flyoutBlockListener.bind(this); + this.monitorBlockListener = this.monitorBlockListener.bind(this); + this.variableListener = this.variableListener.bind(this); + this.addListener('workspaceUpdate', () => { + this.extensionManager.refreshDynamicCategorys(); + }); + + /** + * Export some internal classes for extensions. + */ + this.exports = { + Sprite, + RenderedTarget, + JSZip, + JSGenerator, + IRGenerator, + jsexecute, + loadCostume, + loadSound, + Blocks, + StageLayering, + Variable, + Thread: require('./engine/thread.js'), + execute: require('./engine/execute.js') + }; + } + + /** + * Start running the VM - do this before anything else. + */ + start () { + this.runtime.start(); + } + + /** + * tw: Stop running the VM + * Note: This only stops the loop. It will not stop any threads the next time the VM starts + */ + stop () { + this.runtime.stop(); + } + + /** + * "Green flag" handler - start all threads starting with a green flag. + */ + greenFlag () { + this.runtime.greenFlag(); + } + + /** + * Set whether the VM is in "turbo mode." + * When true, loops don't yield to redraw. + * @param {boolean} turboModeOn Whether turbo mode should be set. + */ + setTurboMode (turboModeOn) { + this.runtime.turboMode = !!turboModeOn; + if (this.runtime.turboMode) { + this.emit(Runtime.TURBO_MODE_ON); + } else { + this.emit(Runtime.TURBO_MODE_OFF); + } + } + + /** + * Set whether the VM is in 2.0 "compatibility mode." + * When true, ticks go at 2.0 speed (30 TPS). + * @param {boolean} compatibilityModeOn Whether compatibility mode is set. + */ + setCompatibilityMode (compatibilityModeOn) { + this.runtime.setCompatibilityMode(!!compatibilityModeOn); + } + + setFramerate (framerate) { + this.runtime.setFramerate(framerate); + } + + setInterpolation (interpolationEnabled) { + this.runtime.setInterpolation(interpolationEnabled); + } + + setRuntimeOptions (runtimeOptions) { + this.runtime.setRuntimeOptions(runtimeOptions); + } + + setCompilerOptions (compilerOptions) { + this.runtime.setCompilerOptions(compilerOptions); + } + + setStageSize (width, height) { + this.runtime.setStageSize(width, height); + } + + setInEditor (inEditor) { + this.runtime.setInEditor(inEditor); + } + + convertToPackagedRuntime () { + this.runtime.convertToPackagedRuntime(); + } + + addAddonBlock (options) { + this.runtime.addAddonBlock(options); + } + + getAddonBlock (procedureCode) { + return this.runtime.getAddonBlock(procedureCode); + } + + storeProjectOptions () { + this.runtime.storeProjectOptions(); + if (this.editingTarget.isStage) { + this.emitWorkspaceUpdate(); + } + } + + enableDebug () { + this.runtime.enableDebug(); + return 'enabled debug mode'; + } + + /** + * Stop all threads and running activities. + */ + stopAll () { + this.runtime.stopAll(); + } + + /** + * Clear out current running project data. + */ + clear () { + this.runtime.dispose(); + this.editingTarget = null; + this.emitTargetsUpdate(false /* Don't emit project change */); + } + + /** + * Get data for playground. Data comes back in an emitted event. + */ + getPlaygroundData () { + const instance = this; + // Only send back thread data for the current editingTarget. + const threadData = this.runtime.threads.filter(thread => thread.target === instance.editingTarget); + // Remove the target key, since it's a circular reference. + const filteredThreadData = JSON.stringify(threadData, (key, value) => { + if (key === 'target' || key === 'blockContainer') return; + return value; + }, 2); + this.emit('playgroundData', { + blocks: this.editingTarget.blocks, + threads: filteredThreadData + }); + } + + /** + * Post I/O data to the virtual devices. + * @param {?string} device Name of virtual I/O device. + * @param {object} data Any data object to post to the I/O device. + */ + postIOData (device, data) { + if (this.runtime.ioDevices[device]) { + this.runtime.ioDevices[device].postData(data); + } + } + + setVideoProvider (videoProvider) { + this.runtime.ioDevices.video.setProvider(videoProvider); + } + + setCloudProvider (cloudProvider) { + this.runtime.ioDevices.cloud.setProvider(cloudProvider); + } + + /** + * Tell the specified extension to scan for a peripheral. + * @param {string} extensionId - the id of the extension. + */ + scanForPeripheral (extensionId) { + this.runtime.scanForPeripheral(extensionId); + } + + /** + * Connect to the extension's specified peripheral. + * @param {string} extensionId - the id of the extension. + * @param {number} peripheralId - the id of the peripheral. + */ + connectPeripheral (extensionId, peripheralId) { + this.runtime.connectPeripheral(extensionId, peripheralId); + } + + /** + * Disconnect from the extension's connected peripheral. + * @param {string} extensionId - the id of the extension. + */ + disconnectPeripheral (extensionId) { + this.runtime.disconnectPeripheral(extensionId); + } + + /** + * Returns whether the extension has a currently connected peripheral. + * @param {string} extensionId - the id of the extension. + * @return {boolean} - whether the extension has a connected peripheral. + */ + getPeripheralIsConnected (extensionId) { + return this.runtime.getPeripheralIsConnected(extensionId); + } + + isSB2(json) { + return Array.isArray(json.children) && !Array.isArray(json.targets); + } + /** + * Load a Scratch project from a .sb, .sb2, .sb3 or json string. + * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load. + * @return {!Promise} Promise that resolves after targets are installed. + */ + loadProject (input) { + return new Promise(async (resolve, reject) => { + try { + const arr = new Uint8Array(input); + const tag = [...arr.slice(0, 7)] + .map(char => String.fromCharCode(char)) + .join(''); + if (tag === 'Scratch') { + const { SB1File } = require('scratch-sb1-converter'); + const sb1 = new SB1File(input); + const json = sb1.json; + json.projectVersion = 2; + return resolve([json, sb1.zip]); + } + + // if it isnt a zip, maby its the project.json in ArrayBuffer form + if (tag.slice(0, 2) !== 'PK') { + const decoder = new TextDecoder('UTF-8'); + input = decoder.decode(input); + } + if (typeof input === 'string') + input = JSON.parse(input); + // generic objects return [object Object] on stringify + if (input.toString() === '[object Object]') { + input.projectVersion = this.isSB2(input) ? 2 : 3; + return resolve([input, null]); + } + const zip = await JSZip.loadAsync(input); + const proj = zip.file('project.json'); + if (!proj) return reject('No project.json file inside the given project'); + const json = JSON.parse(await proj.async('string')); + delete json.meta; + json.projectVersion = this.isSB2(json) ? 2 : 3; + return resolve([json, zip]); + } catch (err) { + reject(err.toString()); + } + }) + .then(validatedInput => this.deserializeProject(validatedInput[0], validatedInput[1])) + .then(() => this.runtime.emitProjectLoaded()) + .catch(error => { + console.error(error); + // Intentionally rejecting here (want errors to be handled by caller) + if (error.hasOwnProperty('validationError')) { + return Promise.reject(JSON.stringify(error, null, 4)); + } + return Promise.reject(error); + }); + } + + /** + * Load a project from the Scratch web site, by ID. + * @param {string} id - the ID of the project to download, as a string. + */ + downloadProjectId (id) { + const storage = this.runtime.storage; + if (!storage) { + log.error('No storage module present; cannot load project: ', id); + return; + } + const vm = this; + const promise = storage.load(storage.AssetType.Project, id); + promise.then(projectAsset => { + if (!projectAsset) { + log.error(`Failed to fetch project with id: ${id}`); + return null; + } + return vm.loadProject(projectAsset.data); + }); + } + + /** + * @returns {JSZip} JSZip zip object representing the sb3. + */ + _saveProjectZip () { + const projectJson = this.toJSON(); + + // TODO want to eventually move zip creation out of here, and perhaps + // into scratch-storage + const zip = new JSZip(); + + // Put everything in a zip file + zip.file('project.json', projectJson); + this._addFileDescsToZip(this.serializeAssets(), zip); + + // Use a fixed modification date for the files in the zip instead of letting JSZip use the + // current time to avoid a very small metadata leak and make zipping deterministic. The magic + // number is from the first TurboWarp/scratch-vm commit after forking + const date = new Date(1591657163000); + for (const file of Object.values(zip.files)) { + file.date = date; + } + return zip; + } + + /** + * @param {JSZip.OutputType} [type] JSZip output type. Defaults to 'blob' for Scratch compatibility. + * @returns {Promise} Compressed sb3 file in a type determined by the type argument. + */ + saveProjectSb3 (type) { + return this._saveProjectZip().generateAsync({ + type: type || 'blob', + mimeType: 'application/x.scratch.sb3', + compression: 'DEFLATE' + }); + } + + /** + * @param {JSZip.OutputType} [type] JSZip output type. Defaults to 'arraybuffer'. + * @returns {StreamHelper} JSZip StreamHelper object generating the compressed sb3. + * See: https://stuk.github.io/jszip/documentation/api_streamhelper.html + */ + saveProjectSb3Stream (type) { + return this._saveProjectZip().generateInternalStream({ + type: type || 'arraybuffer', + mimeType: 'application/x.scratch.sb3', + compression: 'DEFLATE' + }); + } + + /** + * tw: Serialize the project into a map of files without actually zipping the project. + * The buffers returned are the exact same ones used internally, not copies. Avoid directly + * manipulating them (except project.json, which is created by this function). + * @returns {Record} Map of file name to the raw data for that file. + */ + saveProjectSb3DontZip () { + const projectJson = this.toJSON(); + + const files = { + 'project.json': new _TextEncoder().encode(projectJson) + }; + for (const fileDesc of this.serializeAssets()) { + files[fileDesc.fileName] = fileDesc.fileContent; + } + + return files; + } + + /** + * @type {Array} Array of all assets currently in the runtime + */ + get assets () { + const costumesAndSounds = this.runtime.targets.reduce((acc, target) => ( + acc + .concat(target.sprite.sounds.map(sound => sound.asset)) + .concat(target.sprite.costumes.map(costume => costume.asset)) + ), []); + const fonts = this.runtime.fontManager.serializeAssets(); + return [ + ...costumesAndSounds, + ...fonts + ]; + } + + /** + * @param {string} targetId Optional ID of target to export + * @returns {Array<{fileName: string; fileContent: Uint8Array;}} list of file descs + */ + serializeAssets(targetId) { + const costumeDescs = serializeCostumes(this.runtime, targetId); + const soundDescs = serializeSounds(this.runtime, targetId); + const fontDescs = this.runtime.fontManager.serializeAssets().map(asset => ({ + fileName: `${asset.assetId}.${asset.dataFormat}`, + fileContent: asset.data + })); + return [ + ...costumeDescs, + ...soundDescs, + ...fontDescs + ]; + } + + _addFileDescsToZip (fileDescs, zip) { + for (let i = 0; i < fileDescs.length; i++) { + const currFileDesc = fileDescs[i]; + zip.file(currFileDesc.fileName, currFileDesc.fileContent); + } + } + + /** + * Exports a sprite in the sprite3 format. + * @param {string} targetId ID of the target to export + * @param {string=} optZipType Optional type that the resulting + * zip should be outputted in. Options are: base64, binarystring, + * array, uint8array, arraybuffer, blob, or nodebuffer. Defaults to + * blob if argument not provided. + * See https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html#type-option + * for more information about these options. + * @return {object} A generated zip of the sprite and its assets in the format + * specified by optZipType or blob by default. + */ + exportSprite (targetId, optZipType) { + const spriteJson = this.toJSON(targetId); + + const zip = new JSZip(); + zip.file('sprite.json', spriteJson); + this._addFileDescsToZip(this.serializeAssets(targetId), zip); + + return zip.generateAsync({ + type: typeof optZipType === 'string' ? optZipType : 'blob', + mimeType: 'application/x.scratch.sprite3', + compression: 'DEFLATE', + compressionOptions: { + level: 6 + } + }); + } + + /** + * Export project or sprite as a Scratch 3.0 JSON representation. + * @param {string=} optTargetId - Optional id of a sprite to serialize + * @param {*} serializationOptions Options to pass to the serializer + * @return {string} Serialized state of the runtime. + */ + toJSON (optTargetId, serializationOptions) { + const sb3 = require('./serialization/sb3'); + return StringUtil.stringify(sb3.serialize(this.runtime, optTargetId, serializationOptions)); + } + + // TODO do we still need this function? Keeping it here so as not to introduce + // a breaking change. + /** + * Load a project from a Scratch JSON representation. + * @param {string} json JSON string representing a project. + * @returns {Promise} Promise that resolves after the project has loaded + */ + fromJSON (json) { + log.warning('fromJSON is now just a wrapper around loadProject, please use that function instead.'); + return this.loadProject(json); + } + + /** + * Load a project from a Scratch JSON representation. + * @param {string} projectJSON JSON string representing a project. + * @param {?JSZip} zip Optional zipped project containing assets to be loaded. + * @returns {Promise} Promise that resolves after the project has loaded + */ + deserializeProject (projectJSON, zip) { + // Clear the current runtime + this.clear(); + + if (typeof performance !== 'undefined') { + performance.mark('scratch-vm-deserialize-start'); + } + const runtime = this.runtime; + const deserializePromise = function () { + const projectVersion = projectJSON.projectVersion; + if (projectVersion === 2) { + const sb2 = require('./serialization/sb2'); + return sb2.deserialize(projectJSON, runtime, false, zip); + } + if (projectVersion === 3) { + const sb3 = require('./serialization/sb3'); + // eslint-disable-next-line no-invalid-this + return sb3.deserialize(projectJSON, runtime, zip, false, this); + } + return Promise.reject('Unable to verify Scratch Project version.'); + }; + return deserializePromise() + .then(({targets, extensions}) => { + if (typeof performance !== 'undefined') { + performance.mark('scratch-vm-deserialize-end'); + try { + performance.measure('scratch-vm-deserialize', + 'scratch-vm-deserialize-start', 'scratch-vm-deserialize-end'); + } catch (e) { + // performance.measure() will throw an error if the start deserialize + // marker was removed from memory before we finished deserializing + // the project. We've seen this happen a couple times when loading + // very large projects. + log.error(e); + } + } + return this.installTargets(targets, extensions, true); + }); + } + + /** + * @param {string[]} extensionIDs The IDs of the extensions + * @param {Map} extensionURLs A map of extension ID to URL + */ + async _loadExtensions (extensionIDs, extensionURLs = new Map()) { + const extensionPromises = []; + for (const extensionID of extensionIDs) { + const url = extensionURLs.get(extensionID); + if (this.extensionManager.isExtensionLoaded(extensionID)) { + // Already loaded + } else if (url) { + // extension url + if (await this.securityManager.canLoadExtensionFromProject(url)) { + extensionPromises.push(this.extensionManager.loadExtensionURL(url)); + } else { + throw new Error(`Permission to load extension denied: ${extensionID}`); + } + } else if (this.extensionManager.isBuiltinExtension(extensionID)) { + // Builtin extension + this.extensionManager.loadExtensionIdSync(extensionID); + } else { + throw new Error(`Unknown extension: ${extensionID}`); + } + } + return Promise.all(extensionPromises); + } + + /** + * Install `deserialize` results: zero or more targets after the extensions (if any) used by those targets. + * @param {Array.} targets - the targets to be installed + * @param {ImportedExtensionsInfo} extensions - metadata about extensions used by these targets + * @param {boolean} wholeProject - set to true if installing a whole project, as opposed to a single sprite. + * @returns {Promise} resolved once targets have been installed + */ + async installTargets (targets, extensions, wholeProject) { + await this.extensionManager.allAsyncExtensionsLoaded(); + + targets = targets.filter(target => !!target); + + return this._loadExtensions(extensions.extensionIDs, extensions.extensionURLs).then(() => { + for (const extension of extensions.extensionIDs) { + if (`ext_${extension}` in this.runtime) { + if ((typeof this.runtime[`ext_${extension}`].deserialize === 'function') && + extensions.extensionData[extension]) { + this.runtime[`ext_${extension}`].deserialize(extensions.extensionData[extension]); + } + } + } + targets.forEach(target => { + this.runtime.addTarget(target); + (/** @type RenderedTarget */ target).updateAllDrawableProperties(); + // Ensure unique sprite name + if (target.isSprite()) this.renameSprite(target.id, target.getName()); + }); + // Sort the executable targets by layerOrder. + // Remove layerOrder property after use. + this.runtime.executableTargets.sort((a, b) => a.layerOrder - b.layerOrder); + targets.forEach(target => { + delete target.layerOrder; + }); + + // Select the first target for editing, e.g., the first sprite. + if (wholeProject && (targets.length > 1)) { + this.editingTarget = targets[1]; + } else { + this.editingTarget = targets[0]; + } + + if (!wholeProject) { + this.editingTarget.fixUpVariableReferences(); + } + + if (wholeProject) { + this.runtime.parseProjectOptions(); + } + + // Update the VM user's knowledge of targets and blocks on the workspace. + this.emitTargetsUpdate(false /* Don't emit project change */); + this.emitWorkspaceUpdate(); + this.runtime.setEditingTarget(this.editingTarget); + this.runtime.ioDevices.cloud.setStage(this.runtime.getTargetForStage()); + }); + } + + /** + * Add a sprite, this could be .sprite2 or .sprite3. Unpack and validate + * such a file first. + * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load. + * @return {!Promise} Promise that resolves after targets are installed. + */ + addSprite (input) { + const errorPrefix = 'Sprite Upload Error:'; + if (typeof input === 'object' && !(input instanceof ArrayBuffer) && + !ArrayBuffer.isView(input)) { + // If the input is an object and not any ArrayBuffer + // or an ArrayBuffer view (this includes all typed arrays and DataViews) + // turn the object into a JSON string, because we suspect + // this is a project.json as an object + // validate expects a string or buffer as input + // TODO not sure if we need to check that it also isn't a data view + input = JSON.stringify(input); + } + + const validationPromise = new Promise((resolve, reject) => { + const validate = require('scratch-parser'); + // The second argument of true below indicates to the parser/validator + // the given input should be treated as a single sprite and not + // an entire project + validate(input, true, (error, res) => { + if (error) return reject(error); + resolve(res); + }); + }); + + return validationPromise + .then(validatedInput => { + const projectVersion = validatedInput[0].projectVersion; + if (projectVersion === 2) { + return this._addSprite2(validatedInput[0], validatedInput[1]); + } + if (projectVersion === 3) { + return this._addSprite3(validatedInput[0], validatedInput[1]); + } + return Promise.reject(`${errorPrefix} Unable to verify sprite version.`); + }) + .then(() => this.runtime.emitProjectChanged()) + .catch(error => { + // Intentionally rejecting here (want errors to be handled by caller) + if (error.hasOwnProperty('validationError')) { + return Promise.reject(JSON.stringify(error)); + } + return Promise.reject(`${errorPrefix} ${error}`); + }); + } + + /** + * Add a single sprite from the "Sprite2" (i.e., SB2 sprite) format. + * @param {object} sprite Object representing 2.0 sprite to be added. + * @param {?ArrayBuffer} zip Optional zip of assets being referenced by json + * @returns {Promise} Promise that resolves after the sprite is added + */ + _addSprite2 (sprite, zip) { + // Validate & parse + + const sb2 = require('./serialization/sb2'); + return sb2.deserialize(sprite, this.runtime, true, zip) + .then(({targets, extensions}) => + this.installTargets(targets, extensions, false)); + } + + /** + * Add a single sb3 sprite. + * @param {object} sprite Object rperesenting 3.0 sprite to be added. + * @param {?ArrayBuffer} zip Optional zip of assets being referenced by target json + * @returns {Promise} Promise that resolves after the sprite is added + */ + _addSprite3 (sprite, zip) { + // Validate & parse + const sb3 = require('./serialization/sb3'); + return sb3 + .deserialize(sprite, this.runtime, zip, true) + .then(({targets, extensions}) => this.installTargets(targets, extensions, false)); + } + + /** + * Add a costume to the current editing target. + * @param {string} md5ext - the MD5 and extension of the costume to be loaded. + * @param {!object} costumeObject Object representing the costume. + * @property {int} skinId - the ID of the costume's render skin, once installed. + * @property {number} rotationCenterX - the X component of the costume's origin. + * @property {number} rotationCenterY - the Y component of the costume's origin. + * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume. + * @param {string} optTargetId - the id of the target to add to, if not the editing target. + * @param {number} optVersion - if this is 2, load costume as sb2, otherwise load costume as sb3. + * @returns {?Promise} - a promise that resolves when the costume has been added + */ + addCostume (md5ext, costumeObject, optTargetId, optVersion) { + const target = optTargetId ? this.runtime.getTargetById(optTargetId) : + this.editingTarget; + if (target) { + if (costumeObject.fromPenguinModLibrary === true) { + return new Promise((resolve, reject) => { + fetch(`${PM_LIBRARY_API}files/${costumeObject.libraryId}`) + .then((r) => r.arrayBuffer()) + .then((arrayBuffer) => { + const dataFormat = costumeObject.dataFormat; + const storage = this.runtime.storage; + const asset = new storage.Asset( + storage.AssetType[dataFormat === 'svg' ? "ImageVector" : "ImageBitmap"], + null, + storage.DataFormat[dataFormat.toUpperCase()], + new Uint8Array(arrayBuffer), + true + ); + const newCostumeObject = { + md5: asset.assetId + '.' + asset.dataFormat, + asset: asset, + name: costumeObject.name + } + loadCostume(newCostumeObject.md5, newCostumeObject, this.runtime, optVersion).then(costumeAsset => { + target.addCostume(newCostumeObject); + target.setCostume( + target.getCostumes().length - 1 + ); + this.runtime.emitProjectChanged(); + resolve(costumeAsset, newCostumeObject); + }) + }).catch(reject); + }); + } + return loadCostume(md5ext, costumeObject, this.runtime, optVersion).then(costumeObject => { + target.addCostume(costumeObject); + target.setCostume( + target.getCostumes().length - 1 + ); + this.runtime.emitProjectChanged(); + }); + } + // If the target cannot be found by id, return a rejected promise + return Promise.reject(); + } + + /** + * Add a costume loaded from the library to the current editing target. + * @param {string} md5ext - the MD5 and extension of the costume to be loaded. + * @param {!object} costumeObject Object representing the costume. + * @property {int} skinId - the ID of the costume's render skin, once installed. + * @property {number} rotationCenterX - the X component of the costume's origin. + * @property {number} rotationCenterY - the Y component of the costume's origin. + * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume. + * @returns {?Promise} - a promise that resolves when the costume has been added + */ + addCostumeFromLibrary (md5ext, costumeObject) { + if (!this.editingTarget) return Promise.reject(); + return this.addCostume(md5ext, costumeObject, this.editingTarget.id, 2 /* optVersion */); + } + + /** + * Duplicate the costume at the given index. Add it at that index + 1. + * @param {!int} costumeIndex Index of costume to duplicate + * @returns {?Promise} - a promise that resolves when the costume has been decoded and added + */ + duplicateCostume (costumeIndex) { + const originalCostume = this.editingTarget.getCostumes()[costumeIndex]; + const clone = Object.assign({}, originalCostume); + const md5ext = `${clone.assetId}.${clone.dataFormat}`; + return loadCostume(md5ext, clone, this.runtime).then(() => { + this.editingTarget.addCostume(clone, costumeIndex + 1); + this.editingTarget.setCostume(costumeIndex + 1); + this.emitTargetsUpdate(); + }); + } + + /** + * Duplicate the sound at the given index. Add it at that index + 1. + * @param {!int} soundIndex Index of sound to duplicate + * @returns {?Promise} - a promise that resolves when the sound has been decoded and added + */ + duplicateSound (soundIndex) { + const originalSound = this.editingTarget.getSounds()[soundIndex]; + const clone = Object.assign({}, originalSound); + return loadSound(clone, this.runtime, this.editingTarget.sprite.soundBank).then(() => { + this.editingTarget.addSound(clone, soundIndex + 1); + this.emitTargetsUpdate(); + }); + } + + /** + * Rename a costume on the current editing target. + * @param {int} costumeIndex - the index of the costume to be renamed. + * @param {string} newName - the desired new name of the costume (will be modified if already in use). + */ + renameCostume (costumeIndex, newName) { + this.editingTarget.renameCostume(costumeIndex, newName); + this.emitTargetsUpdate(); + } + + /** + * Delete a costume from the current editing target. + * @param {int} costumeIndex - the index of the costume to be removed. + * @return {?function} A function to restore the deleted costume, or null, + * if no costume was deleted. + */ + deleteCostume (costumeIndex) { + const deletedCostume = this.editingTarget.deleteCostume(costumeIndex); + if (deletedCostume) { + const target = this.editingTarget; + this.runtime.emitProjectChanged(); + return () => { + target.addCostume(deletedCostume); + this.emitTargetsUpdate(); + }; + } + return null; + } + + /** + * Pause running scripts + */ + pause() { + this.runtime.pause(); + } + + /** + * Unpause running scripts + */ + play() { + this.runtime.play(); + } + + /** + * Add a sound to the current editing target. + * @param {!object} soundObject Object representing the costume. + * @param {string} optTargetId - the id of the target to add to, if not the editing target. + * @returns {?Promise} - a promise that resolves when the sound has been decoded and added + */ + addSound (soundObject, optTargetId) { + const target = optTargetId ? this.runtime.getTargetById(optTargetId) : + this.editingTarget; + if (target) { + if (soundObject.fromPenguinModLibrary === true) { + return new Promise((resolve, reject) => { + fetch(`${PM_LIBRARY_API}files/${soundObject.libraryId}`) + .then((r) => r.arrayBuffer()) + .then((arrayBuffer) => { + const storage = this.runtime.storage; + const asset = new storage.Asset( + storage.AssetType.Sound, + null, + storage.DataFormat.MP3, + new Uint8Array(arrayBuffer), + true + ); + const newSoundObject = { + md5: asset.assetId + '.' + asset.dataFormat, + asset: asset, + name: soundObject.name + } + loadSound(newSoundObject, this.runtime, target.sprite.soundBank).then(soundAsset => { + target.addSound(newSoundObject); + this.emitTargetsUpdate(); + resolve(soundAsset, newSoundObject); + }); + }).catch(reject); + }); + } + return loadSound(soundObject, this.runtime, target.sprite.soundBank).then(() => { + target.addSound(soundObject); + this.emitTargetsUpdate(); + }); + } + // If the target cannot be found by id, return a rejected promise + return Promise.reject(new Error(`No target with ID: ${optTargetId}`)); + } + + /** + * Rename a sound on the current editing target. + * @param {int} soundIndex - the index of the sound to be renamed. + * @param {string} newName - the desired new name of the sound (will be modified if already in use). + */ + renameSound (soundIndex, newName) { + this.editingTarget.renameSound(soundIndex, newName); + this.emitTargetsUpdate(); + } + + /** + * Get a sound buffer from the audio engine. + * @param {int} soundIndex - the index of the sound to be got. + * @return {AudioBuffer} the sound's audio buffer. + */ + getSoundBuffer (soundIndex) { + const id = this.editingTarget.sprite.sounds[soundIndex].soundId; + if (id && this.runtime && this.runtime.audioEngine) { + return this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer; + } + return null; + } + + /** + * Update a sound buffer. + * @param {int} soundIndex - the index of the sound to be updated. + * @param {AudioBuffer} newBuffer - new audio buffer for the audio engine. + * @param {ArrayBuffer} soundEncoding - the new (wav) encoded sound to be stored + */ + updateSoundBuffer (soundIndex, newBuffer, soundEncoding) { + const sound = this.editingTarget.sprite.sounds[soundIndex]; + if (sound && sound.broken) delete sound.broken; + const id = sound ? sound.soundId : null; + if (id && this.runtime && this.runtime.audioEngine) { + this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer = newBuffer; + } + // Update sound in runtime + if (soundEncoding) { + // Now that we updated the sound, the format should also be updated + // so that the sound can eventually be decoded the right way. + // Sounds that were formerly 'adpcm', but were updated in sound editor + // will not get decoded by the audio engine correctly unless the format + // is updated as below. + sound.format = ''; + const storage = this.runtime.storage; + sound.asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.WAV, + soundEncoding, + null, + true // generate md5 + ); + sound.assetId = sound.asset.assetId; + sound.dataFormat = storage.DataFormat.WAV; + sound.md5 = `${sound.assetId}.${sound.dataFormat}`; + sound.sampleCount = newBuffer.length; + sound.rate = newBuffer.sampleRate; + } + // If soundEncoding is null, it's because gui had a problem + // encoding the updated sound. We don't want to store anything in this + // case, and gui should have logged an error. + + this.emitTargetsUpdate(); + } + + /** + * Delete a sound from the current editing target. + * @param {int} soundIndex - the index of the sound to be removed. + * @return {?Function} A function to restore the sound that was deleted, + * or null, if no sound was deleted. + */ + deleteSound (soundIndex) { + const target = this.editingTarget; + const deletedSound = this.editingTarget.deleteSound(soundIndex); + if (deletedSound) { + this.runtime.emitProjectChanged(); + const restoreFun = () => { + target.addSound(deletedSound); + this.emitTargetsUpdate(); + }; + return restoreFun; + } + return null; + } + + /** + * Get a string representation of the image from storage. + * @param {int} costumeIndex - the index of the costume to be got. + * @return {string} the costume's SVG string if it's SVG, + * a dataURI if it's a PNG or JPG, or null if it couldn't be found or decoded. + */ + getCostume (costumeIndex) { + const asset = this.editingTarget.getCostumes()[costumeIndex].asset; + if (!asset || !this.runtime || !this.runtime.storage) return null; + const format = asset.dataFormat; + if (format === this.runtime.storage.DataFormat.SVG) { + return asset.decodeText(); + } else if (format === this.runtime.storage.DataFormat.PNG || + format === this.runtime.storage.DataFormat.JPG) { + return asset.encodeDataURI(); + } + log.error(`Unhandled format: ${asset.dataFormat}`); + return null; + } + + /** + * TW: Get the raw binary data to use when exporting a costume to the user's local file system. + * @param {Costume} costumeObject scratch-vm costume object + * @returns {Uint8Array} + */ + getExportedCostume (costumeObject) { + return exportCostume(costumeObject); + } + + /** + * TW: Get a base64 string to use when exporting a costume to the user's local file system. + * @param {Costume} costumeObject scratch-vm costume object + * @returns {string} base64 string. Not a data: URI. + */ + getExportedCostumeBase64 (costumeObject) { + const binaryData = this.getExportedCostume(costumeObject); + return Base64Util.uint8ArrayToBase64(binaryData); + } + + /** + * Update a costume with the given bitmap + * @param {!int} costumeIndex - the index of the costume to be updated. + * @param {!ImageData} bitmap - new bitmap for the renderer. + * @param {!number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner + * @param {!number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner + * @param {!number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage, + * 2 for double-resolution bitmaps + */ + updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) { + return this._updateBitmap( + this.editingTarget.getCostumes()[costumeIndex], + bitmap, + rotationCenterX, + rotationCenterY, + bitmapResolution + ); + } + + _updateBitmap (costume, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) { + if (!(costume && this.runtime && this.runtime.renderer)) return; + if (costume && costume.broken) delete costume.broken; + + costume.rotationCenterX = rotationCenterX; + costume.rotationCenterY = rotationCenterY; + + // If the bitmap originally had a zero width or height, use that value + const bitmapWidth = bitmap.sourceWidth === 0 ? 0 : bitmap.width; + const bitmapHeight = bitmap.sourceHeight === 0 ? 0 : bitmap.height; + // @todo: updateBitmapSkin does not take ImageData + const canvas = document.createElement('canvas'); + canvas.width = bitmapWidth; + canvas.height = bitmapHeight; + const context = canvas.getContext('2d'); + context.putImageData(bitmap, 0, 0); + + // Divide by resolution because the renderer's definition of the rotation center + // is the rotation center divided by the bitmap resolution + this.runtime.renderer.updateBitmapSkin( + costume.skinId, + canvas, + bitmapResolution, + [rotationCenterX / bitmapResolution, rotationCenterY / bitmapResolution] + ); + + // @todo there should be a better way to get from ImageData to a decodable storage format + canvas.toBlob(blob => { + const reader = new FileReader(); + reader.addEventListener('loadend', () => { + const storage = this.runtime.storage; + costume.dataFormat = storage.DataFormat.PNG; + costume.bitmapResolution = bitmapResolution; + costume.size = [bitmapWidth, bitmapHeight]; + costume.asset = storage.createAsset( + storage.AssetType.ImageBitmap, + costume.dataFormat, + Buffer.from(reader.result), + null, // id + true // generate md5 + ); + costume.assetId = costume.asset.assetId; + costume.md5 = `${costume.assetId}.${costume.dataFormat}`; + this.emitTargetsUpdate(); + }); + // Bitmaps with a zero width or height return null for their blob + if (blob){ + reader.readAsArrayBuffer(blob); + } + }); + } + + /** + * Update a costume with the given SVG + * @param {int} costumeIndex - the index of the costume to be updated. + * @param {string} svg - new SVG for the renderer. + * @param {number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner + * @param {number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner + */ + updateSvg (costumeIndex, svg, rotationCenterX, rotationCenterY) { + return this._updateSvg( + this.editingTarget.getCostumes()[costumeIndex], + svg, + rotationCenterX, + rotationCenterY + ); + } + + _updateSvg (costume, svg, rotationCenterX, rotationCenterY) { + if (costume && costume.broken) delete costume.broken; + if (costume && this.runtime && this.runtime.renderer) { + costume.rotationCenterX = rotationCenterX; + costume.rotationCenterY = rotationCenterY; + this.runtime.renderer.updateSVGSkin(costume.skinId, svg, [rotationCenterX, rotationCenterY]); + costume.size = this.runtime.renderer.getSkinSize(costume.skinId); + } + const storage = this.runtime.storage; + // If we're in here, we've edited an svg in the vector editor, + // so the dataFormat should be 'svg' + costume.dataFormat = storage.DataFormat.SVG; + costume.bitmapResolution = 1; + costume.asset = storage.createAsset( + storage.AssetType.ImageVector, + costume.dataFormat, + (new _TextEncoder()).encode(svg), + null, + true // generate md5 + ); + costume.assetId = costume.asset.assetId; + costume.md5 = `${costume.assetId}.${costume.dataFormat}`; + this.emitTargetsUpdate(); + } + + /** + * Add a backdrop to the stage. + * @param {string} md5ext - the MD5 and extension of the backdrop to be loaded. + * @param {!object} backdropObject Object representing the backdrop. + * @property {int} skinId - the ID of the backdrop's render skin, once installed. + * @property {number} rotationCenterX - the X component of the backdrop's origin. + * @property {number} rotationCenterY - the Y component of the backdrop's origin. + * @property {number} [bitmapResolution] - the resolution scale for a bitmap backdrop. + * @returns {?Promise} - a promise that resolves when the backdrop has been added + */ + addBackdrop(md5ext, backdropObject) { + if (backdropObject.fromPenguinModLibrary === true) { + return new Promise((resolve, reject) => { + fetch(`${PM_LIBRARY_API}files/${backdropObject.libraryId}`) + .then((r) => r.arrayBuffer()) + .then((arrayBuffer) => { + const dataFormat = backdropObject.dataFormat; + const storage = this.runtime.storage; + const asset = new storage.Asset( + storage.AssetType[dataFormat === 'svg' ? "ImageVector" : "ImageBitmap"], + null, + storage.DataFormat[dataFormat.toUpperCase()], + new Uint8Array(arrayBuffer), + true + ); + const newCostumeObject = { + md5: asset.assetId + '.' + asset.dataFormat, + asset: asset, + name: backdropObject.name + } + loadCostume(newCostumeObject.md5, newCostumeObject, this.runtime).then(costumeAsset => { + const stage = this.runtime.getTargetForStage(); + stage.addCostume(newCostumeObject); + stage.setCostume(stage.getCostumes().length - 1); + this.runtime.emitProjectChanged(); + resolve(costumeAsset, newCostumeObject); + }) + }).catch(reject); + }); + } + return loadCostume(md5ext, backdropObject, this.runtime).then(() => { + const stage = this.runtime.getTargetForStage(); + stage.addCostume(backdropObject); + stage.setCostume(stage.getCostumes().length - 1); + this.runtime.emitProjectChanged(); + }); + } + + /** + * Rename a sprite. + * @param {string} targetId ID of a target whose sprite to rename. + * @param {string} newName New name of the sprite. + */ + renameSprite (targetId, newName) { + const target = this.runtime.getTargetById(targetId); + if (target) { + if (!target.isSprite()) { + throw new Error('Cannot rename non-sprite targets.'); + } + const sprite = target.sprite; + if (!sprite) { + throw new Error('No sprite associated with this target.'); + } + if (newName && RESERVED_NAMES.indexOf(newName) === -1) { + const names = this.runtime.targets + .filter(runtimeTarget => runtimeTarget.isSprite() && runtimeTarget.id !== target.id) + .map(runtimeTarget => runtimeTarget.sprite.name); + const oldName = sprite.name; + const newUnusedName = StringUtil.unusedName(newName, names); + sprite.name = newUnusedName; + if (oldName === newUnusedName) { + return; + } + const allTargets = this.runtime.targets; + for (let i = 0; i < allTargets.length; i++) { + const currTarget = allTargets[i]; + currTarget.blocks.updateAssetName(oldName, newName, 'sprite'); + } + + if (newUnusedName !== oldName) this.emitTargetsUpdate(); + } + } else { + throw new Error('No target with the provided id.'); + } + } + + /** + * Delete a sprite and all its clones. + * @param {string} targetId ID of a target whose sprite to delete. + * @return {Function} Returns a function to restore the sprite that was deleted + */ + deleteSprite (targetId) { + const target = this.runtime.getTargetById(targetId); + + if (target) { + const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id); + if (!target.isSprite()) { + throw new Error('Cannot delete non-sprite targets.'); + } + const sprite = target.sprite; + if (!sprite) { + throw new Error('No sprite associated with this target.'); + } + const spritePromise = this.exportSprite(targetId, 'uint8array'); + const restoreSprite = () => spritePromise.then(spriteBuffer => this.addSprite(spriteBuffer)); + // Remove monitors from the runtime state and remove the + // target-specific monitored blocks (e.g. local variables) + target.deleteMonitors(); + const currentEditingTarget = this.editingTarget; + for (let i = 0; i < sprite.clones.length; i++) { + const clone = sprite.clones[i]; + this.runtime.stopForTarget(sprite.clones[i]); + this.runtime.disposeTarget(sprite.clones[i]); + // Ensure editing target is switched if we are deleting it. + if (clone === currentEditingTarget) { + const nextTargetIndex = Math.min(this.runtime.targets.length - 1, targetIndexBeforeDelete); + if (this.runtime.targets.length > 0){ + this.setEditingTarget(this.runtime.targets[nextTargetIndex].id); + } else { + this.editingTarget = null; + } + } + } + // Sprite object should be deleted by GC. + this.emitTargetsUpdate(); + return restoreSprite; + } + + throw new Error('No target with the provided id.'); + } + + /** + * pm: Clone of deleteSprite, used if an addon or script replaces the original deleteSprite. + * @param {string} targetId ID of a target whose sprite to delete. + * @return {Function} Returns a function to restore the sprite that was deleted + */ + deleteSpriteInternal(targetId) { + const target = this.runtime.getTargetById(targetId); + + if (target) { + const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id); + if (!target.isSprite()) { + throw new Error('Cannot delete non-sprite targets.'); + } + const sprite = target.sprite; + if (!sprite) { + throw new Error('No sprite associated with this target.'); + } + const spritePromise = this.exportSprite(targetId, 'uint8array'); + const restoreSprite = () => spritePromise.then(spriteBuffer => this.addSprite(spriteBuffer)); + // Remove monitors from the runtime state and remove the + // target-specific monitored blocks (e.g. local variables) + target.deleteMonitors(); + const currentEditingTarget = this.editingTarget; + for (let i = 0; i < sprite.clones.length; i++) { + const clone = sprite.clones[i]; + this.runtime.stopForTarget(sprite.clones[i]); + this.runtime.disposeTarget(sprite.clones[i]); + // Ensure editing target is switched if we are deleting it. + if (clone === currentEditingTarget) { + const nextTargetIndex = Math.min(this.runtime.targets.length - 1, targetIndexBeforeDelete); + if (this.runtime.targets.length > 0) { + this.setEditingTarget(this.runtime.targets[nextTargetIndex].id); + } else { + this.editingTarget = null; + } + } + } + // Sprite object should be deleted by GC. + this.emitTargetsUpdate(); + return restoreSprite; + } + + throw new Error('No target with the provided id.'); + } + + /** + * Duplicate a sprite. + * @param {string} targetId ID of a target whose sprite to duplicate. + * @returns {Promise} Promise that resolves when duplicated target has + * been added to the runtime. + */ + duplicateSprite (targetId) { + const target = this.runtime.getTargetById(targetId); + if (!target) { + throw new Error('No target with the provided id.'); + } else if (!target.isSprite()) { + throw new Error('Cannot duplicate non-sprite targets.'); + } else if (!target.sprite) { + throw new Error('No sprite associated with this target.'); + } + return target.duplicate().then(newTarget => { + this.runtime.addTarget(newTarget); + newTarget.goBehindOther(target); + this.setEditingTarget(newTarget.id); + }); + } + + /** + * Set the audio engine for the VM/runtime + * @param {!AudioEngine} audioEngine The audio engine to attach + */ + attachAudioEngine (audioEngine) { + this.runtime.attachAudioEngine(audioEngine); + } + + /** + * Set the renderer for the VM/runtime + * @param {!RenderWebGL} renderer The renderer to attach + */ + attachRenderer (renderer) { + this.runtime.attachRenderer(renderer); + } + + /** + * @returns {RenderWebGL} The renderer attached to the vm + */ + get renderer () { + return this.runtime && this.runtime.renderer; + } + + // @deprecated + attachV2SVGAdapter () { + } + + /** + * Set the bitmap adapter for the VM/runtime, which converts scratch 2 + * bitmaps to scratch 3 bitmaps. (Scratch 3 bitmaps are all bitmap resolution 2) + * @param {!function} bitmapAdapter The adapter to attach + */ + attachV2BitmapAdapter (bitmapAdapter) { + this.runtime.attachV2BitmapAdapter(bitmapAdapter); + } + + /** + * Set the storage module for the VM/runtime + * @param {!ScratchStorage} storage The storage module to attach + */ + attachStorage (storage) { + this.runtime.attachStorage(storage); + } + + /** + * set the current locale and builtin messages for the VM + * @param {!string} locale current locale + * @param {!object} messages builtin messages map for current locale + * @returns {Promise} Promise that resolves when all the blocks have been + * updated for a new locale (or empty if locale hasn't changed.) + */ + setLocale (locale, messages) { + if (locale !== formatMessage.setup().locale) { + formatMessage.setup({locale: locale, translations: {[locale]: messages}}); + } + this.emit('LOCALE_CHANGED', locale); + return this.extensionManager.refreshBlocks(); + } + + /** + * get the current locale for the VM + * @returns {string} the current locale in the VM + */ + getLocale () { + return formatMessage.setup().locale; + } + + /** + * Handle a Blockly event for the current editing target. + * @param {!Blockly.Event} e Any Blockly event. + */ + blockListener (e) { + if (this.editingTarget) { + this.editingTarget.blocks.blocklyListen(e); + } + } + + /** + * Handle a Blockly event for the flyout. + * @param {!Blockly.Event} e Any Blockly event. + */ + flyoutBlockListener (e) { + this.runtime.flyoutBlocks.blocklyListen(e); + } + + /** + * Handle a Blockly event for the flyout to be passed to the monitor container. + * @param {!Blockly.Event} e Any Blockly event. + */ + monitorBlockListener (e) { + // Filter events by type, since monitor blocks only need to listen to these events. + // Monitor blocks shouldn't be destroyed when flyout blocks are deleted. + if (['create', 'change'].indexOf(e.type) !== -1) { + this.runtime.monitorBlocks.blocklyListen(e); + } + } + + /** + * Handle a Blockly event for the variable map. + * @param {!Blockly.Event} e Any Blockly event. + */ + variableListener (e) { + // Filter events by type, since blocks only needs to listen to these + // var events. + if (['var_create', 'var_rename', 'var_delete'].indexOf(e.type) !== -1) { + this.runtime.getTargetForStage().blocks.blocklyListen(e); + } + } + + /** + * Set an editing target. An editor UI can use this function to switch + * between editing different targets, sprites, etc. + * After switching the editing target, the VM may emit updates + * to the list of targets and any attached workspace blocks + * (see `emitTargetsUpdate` and `emitWorkspaceUpdate`). + * @param {string} targetId Id of target to set as editing. + */ + setEditingTarget (targetId) { + // Has the target id changed? If not, exit. + if (this.editingTarget && targetId === this.editingTarget.id) { + return; + } + const target = this.runtime.getTargetById(targetId); + if (target) { + this.editingTarget = target; + // Emit appropriate UI updates. + this.emitTargetsUpdate(false /* Don't emit project change */); + this.emitWorkspaceUpdate(); + this.runtime.setEditingTarget(target); + } + } + + /** + * @param {Block[]} blockObjects + * @returns {object} + */ + exportStandaloneBlocks (blockObjects) { + const sb3 = require('./serialization/sb3'); + const serialized = sb3.serializeStandaloneBlocks(blockObjects, this.runtime); + return serialized; + } + + /** + * Called when blocks are dragged from one sprite to another. Adds the blocks to the + * workspace of the given target. + * @param {!Array} blocks Blocks to add. + * @param {!string} targetId Id of target to add blocks to. + * @param {?string} optFromTargetId Optional target id indicating that blocks are being + * shared from that target. This is needed for resolving any potential variable conflicts. + * @return {!Promise} Promise that resolves when the extensions and blocks have been added. + */ + shareBlocksToTarget (blocks, targetId, optFromTargetId) { + const sb3 = require('./serialization/sb3'); + + const {blocks: copiedBlocks, extensionURLs} = sb3.deserializeStandaloneBlocks(blocks); + newBlockIds(copiedBlocks); + const target = this.runtime.getTargetById(targetId); + + if (optFromTargetId) { + // If the blocks are being shared from another target, + // resolve any possible variable conflicts that may arise. + const fromTarget = this.runtime.getTargetById(optFromTargetId); + fromTarget.resolveVariableSharingConflictsWithTarget(copiedBlocks, target); + } + + // Create a unique set of extensionIds that are not yet loaded + const extensionIDs = new Set(copiedBlocks + .map(b => sb3.getExtensionIdForOpcode(b.opcode)) + .filter(id => !!id) // Remove ids that do not exist + .filter(id => !this.extensionManager.isExtensionLoaded(id)) // and remove loaded extensions + ); + + return this._loadExtensions(extensionIDs, extensionURLs).then(() => { + copiedBlocks.forEach(block => { + target.blocks.createBlock(block); + }); + target.blocks.updateTargetSpecificBlocks(target.isStage); + }); + } + + /** + * Called when costumes are dragged from editing target to another target. + * Sets the newly added costume as the current costume. + * @param {!number} costumeIndex Index of the costume of the editing target to share. + * @param {!string} targetId Id of target to add the costume. + * @return {Promise} Promise that resolves when the new costume has been loaded. + */ + shareCostumeToTarget (costumeIndex, targetId) { + const originalCostume = this.editingTarget.getCostumes()[costumeIndex]; + const clone = Object.assign({}, originalCostume); + const md5ext = `${clone.assetId}.${clone.dataFormat}`; + return loadCostume(md5ext, clone, this.runtime).then(() => { + const target = this.runtime.getTargetById(targetId); + if (target) { + target.addCostume(clone); + target.setCostume( + target.getCostumes().length - 1 + ); + } + }); + } + + /** + * Called when sounds are dragged from editing target to another target. + * @param {!number} soundIndex Index of the sound of the editing target to share. + * @param {!string} targetId Id of target to add the sound. + * @return {Promise} Promise that resolves when the new sound has been loaded. + */ + shareSoundToTarget (soundIndex, targetId) { + const originalSound = this.editingTarget.getSounds()[soundIndex]; + const clone = Object.assign({}, originalSound); + const target = this.runtime.getTargetById(targetId); + return loadSound(clone, this.runtime, target.sprite.soundBank).then(() => { + if (target) { + target.addSound(clone); + this.emitTargetsUpdate(); + } + }); + } + + /** + * Repopulate the workspace with the blocks of the current editingTarget. This + * allows us to get around bugs like gui#413. + */ + refreshWorkspace () { + if (this.editingTarget) { + this.emitWorkspaceUpdate(); + this.runtime.setEditingTarget(this.editingTarget); + this.emitTargetsUpdate(false /* Don't emit project change */); + } + } + + /** + * Emit metadata about available targets. + * An editor UI could use this to display a list of targets and show + * the currently editing one. + * @param {bool} triggerProjectChange If true, also emit a project changed event. + * Disabled selectively by updates that don't affect project serialization. + * Defaults to true. + */ + emitTargetsUpdate (triggerProjectChange) { + if (typeof triggerProjectChange === 'undefined') triggerProjectChange = true; + let lazyTargetList; + const getTargetListLazily = () => { + if (!lazyTargetList) { + lazyTargetList = this.runtime.targets + .filter( + // Don't report clones. + target => !target.hasOwnProperty('isOriginal') || target.isOriginal + ).map( + target => target.toJSON() + ); + } + return lazyTargetList; + }; + this.emit('targetsUpdate', { + // [[target id, human readable target name], ...]. + get targetList () { + return getTargetListLazily(); + }, + // Currently editing target id. + editingTarget: this.editingTarget ? this.editingTarget.id : null + }); + if (triggerProjectChange) { + this.runtime.emitProjectChanged(); + } + } + + /** + * Emit an Blockly/scratch-blocks compatible XML representation + * of the current editing target's blocks. + */ + emitWorkspaceUpdate () { + // Create a list of broadcast message Ids according to the stage variables + const stageVariables = this.runtime.getTargetForStage().variables; + let messageIds = []; + for (const varId in stageVariables) { + if (stageVariables[varId].type === Variable.BROADCAST_MESSAGE_TYPE) { + messageIds.push(varId); + } + } + // Go through all blocks on all targets, removing referenced + // broadcast ids from the list. + for (let i = 0; i < this.runtime.targets.length; i++) { + const currTarget = this.runtime.targets[i]; + const currBlocks = currTarget.blocks._blocks; + for (const blockId in currBlocks) { + if (currBlocks[blockId].fields.BROADCAST_OPTION) { + const id = currBlocks[blockId].fields.BROADCAST_OPTION.id; + const index = messageIds.indexOf(id); + if (index !== -1) { + messageIds = messageIds.slice(0, index) + .concat(messageIds.slice(index + 1)); + } + } + } + } + // Anything left in messageIds is not referenced by a block, so delete it. + for (let i = 0; i < messageIds.length; i++) { + const id = messageIds[i]; + delete this.runtime.getTargetForStage().variables[id]; + } + const globalVarMap = Object.assign({}, this.runtime.getTargetForStage().variables); + const localVarMap = this.editingTarget.isStage ? + Object.create(null) : + Object.assign({}, this.editingTarget.variables); + + const globalVariables = Object.keys(globalVarMap).map(k => globalVarMap[k]); + const localVariables = Object.keys(localVarMap).map(k => localVarMap[k]); + const workspaceComments = Object.keys(this.editingTarget.comments) + .map(k => this.editingTarget.comments[k]) + .filter(c => c.blockId === null); + + const xmlString = ` + + ${globalVariables.map(v => v.toXML()).join()} + ${localVariables.map(v => v.toXML(true)).join()} + + ${workspaceComments.map(c => c.toXML()).join()} + ${this.editingTarget.blocks.toXML(this.editingTarget.comments)} + `; + + this.emit('workspaceUpdate', {xml: xmlString}); + } + + /** + * Get a target id for a drawable id. Useful for interacting with the renderer + * @param {int} drawableId The drawable id to request the target id for + * @returns {?string} The target id, if found. Will also be null if the target found is the stage. + */ + getTargetIdForDrawableId (drawableId) { + const target = this.runtime.getTargetByDrawableId(drawableId); + if (target && target.hasOwnProperty('id') && target.hasOwnProperty('isStage') && !target.isStage) { + return target.id; + } + return null; + } + + /** + * Reorder target by index. Return whether a change was made. + * @param {!string} targetIndex Index of the target. + * @param {!number} newIndex index that the target should be moved to. + * @returns {boolean} Whether a target was reordered. + */ + reorderTarget (targetIndex, newIndex) { + let targets = this.runtime.targets; + targetIndex = MathUtil.clamp(targetIndex, 0, targets.length - 1); + newIndex = MathUtil.clamp(newIndex, 0, targets.length - 1); + if (targetIndex === newIndex) return false; + const target = targets[targetIndex]; + targets = targets.slice(0, targetIndex).concat(targets.slice(targetIndex + 1)); + targets.splice(newIndex, 0, target); + this.runtime.targets = targets; + this.emitTargetsUpdate(); + return true; + } + + /** + * Reorder the costumes of a target if it exists. Return whether it succeeded. + * @param {!string} targetId ID of the target which owns the costumes. + * @param {!number} costumeIndex index of the costume to move. + * @param {!number} newIndex index that the costume should be moved to. + * @returns {boolean} Whether a costume was reordered. + */ + reorderCostume (targetId, costumeIndex, newIndex) { + const target = this.runtime.getTargetById(targetId); + if (target) { + const reorderSuccessful = target.reorderCostume(costumeIndex, newIndex); + if (reorderSuccessful) { + this.runtime.emitProjectChanged(); + } + return reorderSuccessful; + } + return false; + } + + /** + * Reorder the sounds of a target if it exists. Return whether it occured. + * @param {!string} targetId ID of the target which owns the sounds. + * @param {!number} soundIndex index of the sound to move. + * @param {!number} newIndex index that the sound should be moved to. + * @returns {boolean} Whether a sound was reordered. + */ + reorderSound (targetId, soundIndex, newIndex) { + const target = this.runtime.getTargetById(targetId); + if (target) { + const reorderSuccessful = target.reorderSound(soundIndex, newIndex); + if (reorderSuccessful) { + this.runtime.emitProjectChanged(); + } + return reorderSuccessful; + } + return false; + } + + /** + * Put a target into a "drag" state, during which its X/Y positions will be unaffected + * by blocks. + * @param {string} targetId The id for the target to put into a drag state + */ + startDrag (targetId) { + const target = this.runtime.getTargetById(targetId); + if (target) { + this._dragTarget = target; + target.startDrag(); + } + } + + /** + * Remove a target from a drag state, so blocks may begin affecting X/Y position again + * @param {string} targetId The id for the target to remove from the drag state + */ + stopDrag (targetId) { + const target = this.runtime.getTargetById(targetId); + if (target) { + this._dragTarget = null; + target.stopDrag(); + this.setEditingTarget(target.sprite && target.sprite.clones[0] ? + target.sprite.clones[0].id : target.id); + } + } + + /** + * Post/edit sprite info for the current editing target or the drag target. + * @param {object} data An object with sprite info data to set. + */ + postSpriteInfo (data) { + if (this._dragTarget) { + this._dragTarget.postSpriteInfo(data); + } else { + this.editingTarget.postSpriteInfo(data); + } + // Post sprite info means the gui has changed something about a sprite, + // either through the sprite info pane fields (e.g. direction, size) or + // through dragging a sprite on the stage + // Emit a project changed event. + this.runtime.emitProjectChanged(); + } + + /** + * Set a target's variable's value. Return whether it succeeded. + * @param {!string} targetId ID of the target which owns the variable. + * @param {!string} variableId ID of the variable to set. + * @param {!*} value The new value of that variable. + * @returns {boolean} whether the target and variable were found and updated. + */ + setVariableValue (targetId, variableId, value) { + const target = this.runtime.getTargetById(targetId); + if (target) { + const variable = target.lookupVariableById(variableId); + if (variable) { + variable.value = value; + + if (variable.isCloud) { + this.runtime.ioDevices.cloud.requestUpdateVariable(variable.name, variable.value); + } + + return true; + } + } + return false; + } + + /** + * Get a target's variable's value. Return null if the target or variable does not exist. + * @param {!string} targetId ID of the target which owns the variable. + * @param {!string} variableId ID of the variable to set. + * @returns {?*} The value of the variable, or null if it could not be looked up. + */ + getVariableValue (targetId, variableId) { + const target = this.runtime.getTargetById(targetId); + if (target) { + const variable = target.lookupVariableById(variableId); + if (variable) { + return variable.value; + } + } + return null; + } + + /** + * Allow VM consumer to configure the ScratchLink socket creator. + * @param {Function} factory The custom ScratchLink socket factory. + */ + configureScratchLinkSocketFactory (factory) { + this.runtime.configureScratchLinkSocketFactory(factory); + } +} + +module.exports = VirtualMachine; diff --git a/local-scratch-vm/test/.eslintrc.js b/local-scratch-vm/test/.eslintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..666194097be14b7894f3bc965c4fe2adb7635459 --- /dev/null +++ b/local-scratch-vm/test/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'no-undefined': [0] + } +}; diff --git a/local-scratch-vm/test/fixtures/block-to-workspace-comments-without-scripts.sb2 b/local-scratch-vm/test/fixtures/block-to-workspace-comments-without-scripts.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..70d29c1b03a7a4ffb0db6947154c34d7b21d25ea Binary files /dev/null and b/local-scratch-vm/test/fixtures/block-to-workspace-comments-without-scripts.sb2 differ diff --git a/local-scratch-vm/test/fixtures/block-to-workspace-comments.sb2 b/local-scratch-vm/test/fixtures/block-to-workspace-comments.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..5d94f4044093d7759c3bb02613ff6b176f0b67f2 Binary files /dev/null and b/local-scratch-vm/test/fixtures/block-to-workspace-comments.sb2 differ diff --git a/local-scratch-vm/test/fixtures/broadcast_special_chars.sb2 b/local-scratch-vm/test/fixtures/broadcast_special_chars.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..afaf4ba25b0197665599cca3699026fa3fc37aa5 Binary files /dev/null and b/local-scratch-vm/test/fixtures/broadcast_special_chars.sb2 differ diff --git a/local-scratch-vm/test/fixtures/broadcast_special_chars.sb3 b/local-scratch-vm/test/fixtures/broadcast_special_chars.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..9a840b0ef3dacae8acdb47ca0d6b6e24a3900543 Binary files /dev/null and b/local-scratch-vm/test/fixtures/broadcast_special_chars.sb3 differ diff --git a/local-scratch-vm/test/fixtures/cat.sprite2 b/local-scratch-vm/test/fixtures/cat.sprite2 new file mode 100644 index 0000000000000000000000000000000000000000..9339b8e6f50ccf52fb8c156f8f8eceb4dbba2b1e Binary files /dev/null and b/local-scratch-vm/test/fixtures/cat.sprite2 differ diff --git a/local-scratch-vm/test/fixtures/cat.sprite3 b/local-scratch-vm/test/fixtures/cat.sprite3 new file mode 100644 index 0000000000000000000000000000000000000000..8a5890c1db9574a0ac49e1e24c08329bc30fe097 Binary files /dev/null and b/local-scratch-vm/test/fixtures/cat.sprite3 differ diff --git a/local-scratch-vm/test/fixtures/clone-cleanup.sb2 b/local-scratch-vm/test/fixtures/clone-cleanup.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..61baeb7f2def6182f3372a4d89edcee7bf23db88 Binary files /dev/null and b/local-scratch-vm/test/fixtures/clone-cleanup.sb2 differ diff --git a/local-scratch-vm/test/fixtures/cloud_variables_exceeded_limit.sb2 b/local-scratch-vm/test/fixtures/cloud_variables_exceeded_limit.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..cf1cc0850aae9f6bc42e58263a7a58a4810432c3 Binary files /dev/null and b/local-scratch-vm/test/fixtures/cloud_variables_exceeded_limit.sb2 differ diff --git a/local-scratch-vm/test/fixtures/cloud_variables_exceeded_limit.sb3 b/local-scratch-vm/test/fixtures/cloud_variables_exceeded_limit.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..ce1b78ac76fd290301bdb8597fa3e7ad53306dbe Binary files /dev/null and b/local-scratch-vm/test/fixtures/cloud_variables_exceeded_limit.sb3 differ diff --git a/local-scratch-vm/test/fixtures/cloud_variables_limit.sb2 b/local-scratch-vm/test/fixtures/cloud_variables_limit.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..954e97d69b8d31d667896d7c1382876e2f4cc3fc Binary files /dev/null and b/local-scratch-vm/test/fixtures/cloud_variables_limit.sb2 differ diff --git a/local-scratch-vm/test/fixtures/cloud_variables_limit.sb3 b/local-scratch-vm/test/fixtures/cloud_variables_limit.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..373f73a963ee015dec7ba96fb93977b95e104998 Binary files /dev/null and b/local-scratch-vm/test/fixtures/cloud_variables_limit.sb3 differ diff --git a/local-scratch-vm/test/fixtures/cloud_variables_local.sb2 b/local-scratch-vm/test/fixtures/cloud_variables_local.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..61e4556e37595dd23f55779a9c43d4000556a553 Binary files /dev/null and b/local-scratch-vm/test/fixtures/cloud_variables_local.sb2 differ diff --git a/local-scratch-vm/test/fixtures/cloud_variables_local.sb3 b/local-scratch-vm/test/fixtures/cloud_variables_local.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..35df5de92249238808ac7ff4fa4b8a151de0041e Binary files /dev/null and b/local-scratch-vm/test/fixtures/cloud_variables_local.sb3 differ diff --git a/local-scratch-vm/test/fixtures/cloud_variables_simple.sb2 b/local-scratch-vm/test/fixtures/cloud_variables_simple.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..ddbf782d785926db040e38f14908b03194f9f450 Binary files /dev/null and b/local-scratch-vm/test/fixtures/cloud_variables_simple.sb2 differ diff --git a/local-scratch-vm/test/fixtures/cloud_variables_simple.sb3 b/local-scratch-vm/test/fixtures/cloud_variables_simple.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..a25bd6252d52c50f1032fb99e86d18b6df7efd2e Binary files /dev/null and b/local-scratch-vm/test/fixtures/cloud_variables_simple.sb3 differ diff --git a/local-scratch-vm/test/fixtures/comments.sb2 b/local-scratch-vm/test/fixtures/comments.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..081fbe68a5cd5f2b411125fa623bf68544807a95 Binary files /dev/null and b/local-scratch-vm/test/fixtures/comments.sb2 differ diff --git a/local-scratch-vm/test/fixtures/comments.sb3 b/local-scratch-vm/test/fixtures/comments.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..ce937c45474cef94891edced09c38bf88852493f Binary files /dev/null and b/local-scratch-vm/test/fixtures/comments.sb3 differ diff --git a/local-scratch-vm/test/fixtures/comments_no_duplicate_id_serialization.sb3 b/local-scratch-vm/test/fixtures/comments_no_duplicate_id_serialization.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..64b4117b9ae85dd2fbb21ca5dc74acb66f212a59 Binary files /dev/null and b/local-scratch-vm/test/fixtures/comments_no_duplicate_id_serialization.sb3 differ diff --git a/local-scratch-vm/test/fixtures/complex.sb2 b/local-scratch-vm/test/fixtures/complex.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..acf0270c4ca428550b23dc75bdc8da4beb0d45eb --- /dev/null +++ b/local-scratch-vm/test/fixtures/complex.sb2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a1b738daf079f8334c51bc3a4cd53430d1dfc1a654c3a68293ee19133febbf +size 1898460 diff --git a/local-scratch-vm/test/fixtures/control.sb2 b/local-scratch-vm/test/fixtures/control.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..ea758168ba462e6259b6abcf411422689cc1b9fc Binary files /dev/null and b/local-scratch-vm/test/fixtures/control.sb2 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_png.sb2 b/local-scratch-vm/test/fixtures/corrupt_png.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..9e19272bd000c24a77520dd539b84c4b6a973b60 Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_png.sb2 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_png.sb3 b/local-scratch-vm/test/fixtures/corrupt_png.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..acc4822166ce8a3762caaebf28f203aed480aa51 Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_png.sb3 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_png.sprite2 b/local-scratch-vm/test/fixtures/corrupt_png.sprite2 new file mode 100644 index 0000000000000000000000000000000000000000..b292e438a58e17d888614def9aed642b29a6ff0c Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_png.sprite2 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_png.sprite3 b/local-scratch-vm/test/fixtures/corrupt_png.sprite3 new file mode 100644 index 0000000000000000000000000000000000000000..fc1d61356273c436ab53b5e7a020a97861bbd633 Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_png.sprite3 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_sound.sb3 b/local-scratch-vm/test/fixtures/corrupt_sound.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..d2611babcb293a365e7de6e2d9ba9753bfaf336e Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_sound.sb3 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_svg.sb2 b/local-scratch-vm/test/fixtures/corrupt_svg.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..4290c1bfcda03b2e1d1b598c288e18b9494cdfe8 Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_svg.sb2 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_svg.sb3 b/local-scratch-vm/test/fixtures/corrupt_svg.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..52fb93096f082385fc0aec5c32df6fff96e588b7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_svg.sb3 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_svg.sprite2 b/local-scratch-vm/test/fixtures/corrupt_svg.sprite2 new file mode 100644 index 0000000000000000000000000000000000000000..0db0dd70b95050ef688cc07821845b01650baad5 Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_svg.sprite2 differ diff --git a/local-scratch-vm/test/fixtures/corrupt_svg.sprite3 b/local-scratch-vm/test/fixtures/corrupt_svg.sprite3 new file mode 100644 index 0000000000000000000000000000000000000000..1d9dd0b3dd08da0ca2ebdfd1c435b295e44d179e Binary files /dev/null and b/local-scratch-vm/test/fixtures/corrupt_svg.sprite3 differ diff --git a/local-scratch-vm/test/fixtures/data.sb2 b/local-scratch-vm/test/fixtures/data.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..e4b8c0b1c8658629cb8b77eb4afc10ac3bf0de7d Binary files /dev/null and b/local-scratch-vm/test/fixtures/data.sb2 differ diff --git a/local-scratch-vm/test/fixtures/default.sb2 b/local-scratch-vm/test/fixtures/default.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..1736d2ddafaa4a7cc2dac021d0ddedec78f149a1 Binary files /dev/null and b/local-scratch-vm/test/fixtures/default.sb2 differ diff --git a/local-scratch-vm/test/fixtures/default.sb3 b/local-scratch-vm/test/fixtures/default.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..61b71838914e562a69f235781217de928d921b00 Binary files /dev/null and b/local-scratch-vm/test/fixtures/default.sb3 differ diff --git a/local-scratch-vm/test/fixtures/default_nested.sb2 b/local-scratch-vm/test/fixtures/default_nested.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..958abad5d4113c3e04e15f4ea727dfa1b33669b8 Binary files /dev/null and b/local-scratch-vm/test/fixtures/default_nested.sb2 differ diff --git a/local-scratch-vm/test/fixtures/demo.json b/local-scratch-vm/test/fixtures/demo.json new file mode 100644 index 0000000000000000000000000000000000000000..1467a463db25d13a41cfa8bfd80e55f9047b86fd --- /dev/null +++ b/local-scratch-vm/test/fixtures/demo.json @@ -0,0 +1 @@ +{"targets":[{"id":"m#69^p_G_]:NI,ZR#?iy","name":"Stage","isStage":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"currentCostume":10,"costume":{"skinId":10,"name":"backdrop2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"dataFormat":"svg","assetId":"7da4181ee167de7b3f5d1a91880277ff"},"costumeCount":11,"visible":true,"rotationStyle":"all around","blocks":{"O7D.%E~TH^ULpAuHM8)@":{"id":"O7D.%E~TH^ULpAuHM8)@","opcode":"event_whenflagclicked","inputs":{},"fields":{},"next":"78+=E[P7b8!mBXMwQF`@","shadow":false,"x":138,"y":356.40000000000003,"topLevel":true,"parent":null},"78+=E[P7b8!mBXMwQF`@":{"id":"78+=E[P7b8!mBXMwQF`@","opcode":"control_forever","inputs":{"SUBSTACK":{"name":"SUBSTACK","block":"nlnk{2XqtVI*Qg,admbW","shadow":null}},"fields":{},"next":null,"shadow":false,"parent":"O7D.%E~TH^ULpAuHM8)@"},"nlnk{2XqtVI*Qg,admbW":{"id":"nlnk{2XqtVI*Qg,admbW","opcode":"looks_changeeffectby","inputs":{"EFFECT":{"name":"EFFECT","block":"*vh;qV87Q}5IP@sW=)wD","shadow":"*vh;qV87Q}5IP@sW=)wD"},"CHANGE":{"name":"CHANGE","block":"woYo[[v=PD(`R;qW{PZ%","shadow":"woYo[[v=PD(`R;qW{PZ%"}},"fields":{},"next":null,"shadow":false,"parent":"78+=E[P7b8!mBXMwQF`@"},"*vh;qV87Q}5IP@sW=)wD":{"id":"*vh;qV87Q}5IP@sW=)wD","opcode":"looks_effectmenu","inputs":{},"fields":{"EFFECT":{"name":"EFFECT","value":"color"}},"next":null,"topLevel":false,"parent":"nlnk{2XqtVI*Qg,admbW","shadow":true},"woYo[[v=PD(`R;qW{PZ%":{"id":"woYo[[v=PD(`R;qW{PZ%","opcode":"math_number","inputs":{},"fields":{"NUM":{"name":"NUM","value":1}},"next":null,"topLevel":false,"parent":"nlnk{2XqtVI*Qg,admbW","shadow":true}},"variables":{"6C~ysz%{q=IY/@VyR,}u":{"id":"6C~ysz%{q=IY/@VyR,}u","name":"vy","value":-3.5165000000000006,"type":""},"@M%UU5XBzcjofm,PEY-3":{"id":"@M%UU5XBzcjofm,PEY-3","name":"g","value":"-.5","type":""},"rNxv=Ia#{]Jvhmo{G6cf":{"id":"rNxv=Ia#{]Jvhmo{G6cf","name":"vx","value":1.4580000000000002,"type":""},"fc2rL3s1GcD4UyOOCaN(":{"id":"fc2rL3s1GcD4UyOOCaN(","name":"i","value":3, "type":""},"0A~.tga@}|%UW31FAhz5":{"id":"0A~.tga@}|%UW31FAhz5","name":"d1","value":0, "type":""},"^T|b;%@AzqwgHp},y98=":{"id":"^T|b;%@AzqwgHp},y98=","name":"x","value":233.9173278872492,"type":""},"6m`MGC(+0!,Mwj8xE2OB":{"id":"6m`MGC(+0!,Mwj8xE2OB","name":"acceleration","value":"-3","type":""}},"lists":{},"costumes":[{"skinId":9,"name":"blue sky2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"dataFormat":"svg","assetId":"7623e679b2baa2e7d48808614820844f"},{"skinId":13,"name":"woods","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"dataFormat":"svg","assetId":"1e0f7a4c932423b13250b5cb44928109"},{"skinId":5,"name":"party","bitmapResolution":1,"rotationCenterX":251,"rotationCenterY":189,"dataFormat":"svg","assetId":"108160d0e44d1c340182e31c9dc0758a"},{"skinId":3,"name":"boardwalk","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"de0e54cd11551566f044e7e6bc588b2c"},{"skinId":6,"name":"blue sky3","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":179,"dataFormat":"svg","assetId":"2024d59c1980e667e8f656134796e2c1"},{"skinId":14,"name":"underwater1","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"f339c6f31b11ea71d0fb8d607cec392e"},{"skinId":7,"name":"underwater2","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"1517c21786d2d0edc2f3037408d850bd"},{"skinId":8,"name":"stars","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"e87fed9c2a968dbeae8c94617e600e8c"},{"skinId":11,"name":"parking-ramp","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"a7832479977c166ca0057f2a99a73305"},{"skinId":12,"name":"backdrop1","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"f67dc7de38bac6fbb0ab68e46352521d"},{"skinId":10,"name":"backdrop2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"dataFormat":"svg","assetId":"7da4181ee167de7b3f5d1a91880277ff"}],"sounds":[{"format":"","rate":11025,"sampleCount":258,"soundID":-1,"name":"pop","md5":"83a9787d4cb6f3b7632b4ddfebf74367.wav","data":null,"dataFormat":"wav","assetId":"83a9787d4cb6f3b7632b4ddfebf74367","soundId":"yGa~fKV|[-9r)^[i+xMU"}]},{"id":"./Q-Mm#v_Ifl9@[}Ye^8","name":"Earth","isStage":false,"x":-10,"y":10,"size":100,"direction":90,"draggable":false,"currentCostume":0,"costume":{"skinId":4,"name":"earth","bitmapResolution":1,"rotationCenterX":72,"rotationCenterY":72,"dataFormat":"svg","assetId":"814197522984a302972998b1a7f92d91"},"costumeCount":1,"visible":true,"rotationStyle":"all around","blocks":{},"variables":{},"lists":{},"costumes":[{"skinId":4,"name":"earth","bitmapResolution":1,"rotationCenterX":72,"rotationCenterY":72,"dataFormat":"svg","assetId":"814197522984a302972998b1a7f92d91"}],"sounds":[{"format":"","rate":11025,"sampleCount":258,"soundID":-1,"name":"pop","md5":"83a9787d4cb6f3b7632b4ddfebf74367.wav","data":null,"dataFormat":"wav","assetId":"83a9787d4cb6f3b7632b4ddfebf74367","soundId":"MRZ+h9~E:8O(Fk,.zDVC"}]}],"meta":{"semver":"3.0.0","vm":"0.1.0-prerelease.1510327661-prerelease.1510327675","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36"}} diff --git a/local-scratch-vm/test/fixtures/dispatch-test-service.js b/local-scratch-vm/test/fixtures/dispatch-test-service.js new file mode 100644 index 0000000000000000000000000000000000000000..3e01e68f525e106981c94ab5ddb9053521e1cf1e --- /dev/null +++ b/local-scratch-vm/test/fixtures/dispatch-test-service.js @@ -0,0 +1,15 @@ +class DispatchTestService { + returnFortyTwo () { + return 42; + } + + doubleArgument (x) { + return 2 * x; + } + + throwException () { + throw new Error('This is a test exception thrown by DispatchTest'); + } +} + +module.exports = DispatchTestService; diff --git a/local-scratch-vm/test/fixtures/dispatch-test-worker-shim.js b/local-scratch-vm/test/fixtures/dispatch-test-worker-shim.js new file mode 100644 index 0000000000000000000000000000000000000000..ae11a4950e53ec4f151156870dbdaf2bffbeb216 --- /dev/null +++ b/local-scratch-vm/test/fixtures/dispatch-test-worker-shim.js @@ -0,0 +1,19 @@ +const Module = require('module'); + +const callsite = require('callsite'); +const path = require('path'); + +const oldRequire = Module.prototype.require; +Module.prototype.require = function (target) { + if (target.indexOf('/') === -1) { + return oldRequire.apply(this, arguments); + } + + const stack = callsite(); + const callerFile = stack[2].getFileName(); + const callerDir = path.dirname(callerFile); + target = path.resolve(callerDir, target); + return oldRequire.call(this, target); +}; + +oldRequire(path.resolve(__dirname, 'dispatch-test-worker')); diff --git a/local-scratch-vm/test/fixtures/dispatch-test-worker.js b/local-scratch-vm/test/fixtures/dispatch-test-worker.js new file mode 100644 index 0000000000000000000000000000000000000000..2c57fbb3edd69d379f03c91e946e38e64176535b --- /dev/null +++ b/local-scratch-vm/test/fixtures/dispatch-test-worker.js @@ -0,0 +1,11 @@ +const dispatch = require('../../src/dispatch/worker-dispatch'); +const DispatchTestService = require('./dispatch-test-service'); +const log = require('../../src/util/log'); + +dispatch.setService('RemoteDispatchTest', new DispatchTestService()); + +dispatch.waitForConnection.then(() => { + dispatch.call('test', 'onWorkerReady').catch(e => { + log(`Test worker failed to call onWorkerReady: ${JSON.stringify(e)}`); + }); +}); diff --git a/local-scratch-vm/test/fixtures/draggable.sb3 b/local-scratch-vm/test/fixtures/draggable.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..06cbdda54d9721ce12fdef22e8eca16000a1a7bf Binary files /dev/null and b/local-scratch-vm/test/fixtures/draggable.sb3 differ diff --git a/local-scratch-vm/test/fixtures/edge-triggered-hat.sb3 b/local-scratch-vm/test/fixtures/edge-triggered-hat.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..cd9464378bdf6570a43e7da2f1420fbf435a70fa Binary files /dev/null and b/local-scratch-vm/test/fixtures/edge-triggered-hat.sb3 differ diff --git a/local-scratch-vm/test/fixtures/event.sb2 b/local-scratch-vm/test/fixtures/event.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..ee231f1ee61136a631ffb4f6fbad3c773de2caa0 --- /dev/null +++ b/local-scratch-vm/test/fixtures/event.sb2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f60f983ed899fd0cd98083f8f1f6bb0b3833840847ade2af597fb964a916dfe3 +size 464027 diff --git a/local-scratch-vm/test/fixtures/events.json b/local-scratch-vm/test/fixtures/events.json new file mode 100644 index 0000000000000000000000000000000000000000..a6b42cf8186833b7892dd1a855c4f8112e93ee5f --- /dev/null +++ b/local-scratch-vm/test/fixtures/events.json @@ -0,0 +1,112 @@ +{ + "create": { + "workspaceId": "7Luws3lyb*Z98~Kk+IG|", + "group": ";OswyM#@%`%,xOrhOXC=", + "recordUndo": true, + "name": "block", + "xml": { + "outerHTML": "10" + }, + "ids": [ + "z!+#Nqr,_(V=xz0y7a@d", + "!6Ahqg4f}Ljl}X5Hws?Z" + ] + }, + "createComment": { + "workspaceId": "7Luws3lyb*Z98~Kk+IG|", + "group": ";OswyM#@%`%,xOrhOXC=", + "recordUndo": true, + "name": "block", + "xml": { + "outerHTML": "Some comment text10" + }, + "ids": [ + "z!+#Nqr,_(V=xz0y7a@d", + "!6Ahqg4f}Ljl}X5Hws?Z" + ] + }, + "createbranch": { + "name": "block", + "xml": { + "outerHTML": "1" + } + }, + "createtwobranches": { + "name": "block", + "xml": { + "outerHTML": "" + } + }, + "createtoplevelshadow": { + "name": "shadow", + "xml": { + "outerHTML": "4" + } + }, + "createwithnext": { + "name": "block", + "xml": { + "outerHTML": "" + } + }, + "createinvalid": { + "name": "whatever", + "xml": { + "outerHTML": "" + } + }, + "createinvalidgrandchild": { + "name": "block", + "xml": { + "outerHTML": "xxx" + } + }, + "createbadxml": { + "name": "whatever", + "xml": { + "outerHTML": ">" + } + }, + "createemptyfield": { + "name": "block", + "xml": { + "outerHTML": "" + } + }, + "createvariablewithentity": { + "name": "block", + "xml": { + "outerHTML": "this & that" + } + }, + "createobscuredshadow": { + "name": "block", + "xml": { + "outerHTML": "" + } + }, + "createcommentUpdatePosition": { + "name": "comment", + "type": "comment_create", + "commentId": "a comment", + "xy": {"x": 10, "y": 20} + }, + "mockVariableBlock": { + "name": "block", + "xml": { + "outerHTML": "a mock variable" + } + }, + "mockBroadcastBlock": { + "name": "block", + "xml": { + "outerHTML": "my message" + } + }, + "mockListBlock": { + "name": "block", + "xml": { + "outerHTML": "a mock list" + } + } +} diff --git a/local-scratch-vm/test/fixtures/example_sprite.sprite2 b/local-scratch-vm/test/fixtures/example_sprite.sprite2 new file mode 100644 index 0000000000000000000000000000000000000000..5336124e67a608ed658847a08d34826c5fc9fdbf Binary files /dev/null and b/local-scratch-vm/test/fixtures/example_sprite.sprite2 differ diff --git a/local-scratch-vm/test/fixtures/execute/README.md b/local-scratch-vm/test/fixtures/execute/README.md new file mode 100644 index 0000000000000000000000000000000000000000..140d6a95f6e016b7821d38686cbd3e558bd768e8 --- /dev/null +++ b/local-scratch-vm/test/fixtures/execute/README.md @@ -0,0 +1,3 @@ +Tests in this folder are run in scratch by integration/execute.js. The tests can SAY test messages that map to tap methods. Read integration/execute.js for more. + +Tests whose names start with `tw-` are added by TurboWarp. Some of these tests are based on real projects from the scratch.mit.edu website, in which case their filename contains the original project ID and additional credits are within the project's scripts. We believe our use of these projects is fair use as the projects have been sufficiently modified and stripped down such that the project found here bears not even a slight resemblance to the original project. diff --git a/local-scratch-vm/test/fixtures/execute/broadcast-wait-arg-change.sb2 b/local-scratch-vm/test/fixtures/execute/broadcast-wait-arg-change.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..73e8d8f931894e7a09dd6a1bbe3fa257b3c23dde Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/broadcast-wait-arg-change.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/control-if-false-then-else.sb2 b/local-scratch-vm/test/fixtures/execute/control-if-false-then-else.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..798d30b3d722f3e6c8e871cd0a5f81ca57427157 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/control-if-false-then-else.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/control-if-false-then.sb2 b/local-scratch-vm/test/fixtures/execute/control-if-false-then.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..03aed37eaa8316fdae7ded03bccae0b24122a6a5 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/control-if-false-then.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/control-if-true-then-else.sb2 b/local-scratch-vm/test/fixtures/execute/control-if-true-then-else.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..b375985d1e9afff92c7207ea2527884f51530801 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/control-if-true-then-else.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/control-if-true-then.sb2 b/local-scratch-vm/test/fixtures/execute/control-if-true-then.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..ed80d8248fa92cd5b987370da8a9f7820475a5a7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/control-if-true-then.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/control-stop-all-leaks.sb2 b/local-scratch-vm/test/fixtures/execute/control-stop-all-leaks.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..fb0ea115a82a4ce5ad9412559f2b4a002d977af8 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/control-stop-all-leaks.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/data-operators-global.sb2 b/local-scratch-vm/test/fixtures/execute/data-operators-global.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..037399bc2bf06b84850c1e7159f92c9ec7ac1d30 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/data-operators-global.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/data-operators-local.sb2 b/local-scratch-vm/test/fixtures/execute/data-operators-local.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..b1c4660e4fb8d1e7e7b52675098e61f1090984e7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/data-operators-local.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/data-reporter-contents-global.sb2 b/local-scratch-vm/test/fixtures/execute/data-reporter-contents-global.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..e5b89e838abb9c2555bccbe0734c85ee6b000698 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/data-reporter-contents-global.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/data-reporter-contents-local.sb2 b/local-scratch-vm/test/fixtures/execute/data-reporter-contents-local.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..8280dea04a1311aaa42c06f419a252da868eb54a Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/data-reporter-contents-local.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/event-broadcast-and-wait-can-continue-same-tick.sb2 b/local-scratch-vm/test/fixtures/execute/event-broadcast-and-wait-can-continue-same-tick.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..3e3e3c85e757712acfb71849a0a76c8520d3011a Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/event-broadcast-and-wait-can-continue-same-tick.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/event-when-green-flag.sb2 b/local-scratch-vm/test/fixtures/execute/event-when-green-flag.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..396c17089b592f0cdbac5f7ead6bdc1f5ab3f7d0 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/event-when-green-flag.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/events-broadcast-and-wait-yields-a-tick.sb2 b/local-scratch-vm/test/fixtures/execute/events-broadcast-and-wait-yields-a-tick.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..9decd2cc943806471aaaafaac86f302eecda5a3e Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/events-broadcast-and-wait-yields-a-tick.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/hat-thread-execution.sb2 b/local-scratch-vm/test/fixtures/execute/hat-thread-execution.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..6338f33a51120c126006405ae6ba925ec3e9632a Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/hat-thread-execution.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/monitors-stage-name.sb2 b/local-scratch-vm/test/fixtures/execute/monitors-stage-name.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..6c1af6c05c1cc2962d0610ca3e99be4c566bbc41 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/monitors-stage-name.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/operators-not-blank.sb2 b/local-scratch-vm/test/fixtures/execute/operators-not-blank.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..ed80d8248fa92cd5b987370da8a9f7820475a5a7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/operators-not-blank.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-back-2-broadcast-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-back-2-broadcast-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..295519656a6be734809820d55362e9aa740dea46 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-back-2-broadcast-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-and-wait-repeat-message.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-and-wait-repeat-message.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..95decac2965b792a41445f88679d84adc96e22cf Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-and-wait-repeat-message.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-and-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-and-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..18e86331cc1dc8777eedfed89014bf8b7b9cb7ce Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-and-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-no-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-no-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..07d3c46634eeb52c4a45c30e143e9509daae65bd Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-no-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..aa51b024654529a48a209e3cbebb1b287628e36a Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-broadcast-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-continuous.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-continuous.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..7b55e54f8de48a743ce004db1d92036b87b040de Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-continuous.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-threads-broadcast-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-threads-broadcast-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..0eefe5a6eaf40e27769c723924a3aa031b8fd465 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-backwards-2-threads-broadcast-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-forewards-2-broadcast-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-forewards-2-broadcast-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..fcdcdcea486d871b1ccc6e7f0d58dd753db4f6c9 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-forewards-2-broadcast-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-changes-front-2-broadcast-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-changes-front-2-broadcast-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..2471815bc25a0459675b4061a8374a1b0bfc2ac0 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-changes-front-2-broadcast-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-clones-backwards-2-broadcast-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-clones-backwards-2-broadcast-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..5fe5c44caf0889486a0c9963028d5bce0d842a9a Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-clones-backwards-2-broadcast-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-clones-backwards-broadcast-wait.sb2 b/local-scratch-vm/test/fixtures/execute/order-clones-backwards-broadcast-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..fcd84e0df7326546058241ed464f59e849292aed Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-clones-backwards-broadcast-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-clones-static-2.sb2 b/local-scratch-vm/test/fixtures/execute/order-clones-static-2.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..2c0d9f05c54e4048cb719ce8124ad130a58c12e5 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-clones-static-2.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-immobile-stage.sb2 b/local-scratch-vm/test/fixtures/execute/order-immobile-stage.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..caa67e10e9496257e0c7305426c1dc1b3faca098 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-immobile-stage.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-library-reverse.sb2 b/local-scratch-vm/test/fixtures/execute/order-library-reverse.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..a48b8606b8ca710fe3fcee793e7695deff4c5d3e Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-library-reverse.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-library-reverse.sb3 b/local-scratch-vm/test/fixtures/execute/order-library-reverse.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..f818a47fead84a81e6aef85bb27288800297edf2 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-library-reverse.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-library.sb2 b/local-scratch-vm/test/fixtures/execute/order-library.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..b8d340800e92fb7fe65ea2d9623e2460dcad2d62 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-library.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/order-library.sb3 b/local-scratch-vm/test/fixtures/execute/order-library.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..f3c6dbc46b81d5c4cb0b46c7d687df5c0c4906ea Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/order-library.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-boolean-reporter-bug.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-boolean-reporter-bug.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..fd8ef4af46071baed7a7f1801e2d9addaa428327 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-boolean-reporter-bug.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-boolean-param.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-boolean-param.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..710193168728126de8670ad7e2466595651dc4fe Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-boolean-param.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-no-param.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-no-param.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..af6916e52edaebb3e82acb9590544282a766afad Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-no-param.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-number-param.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-number-param.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..c306518fd7e829fe834feb18b3bb9714bf88886e Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-number-param.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-string-param.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-string-param.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..04363c5bc862e84b52b3133e1e789fdd7bf85eaa Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-nested-missing-string-param.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-number-number-boolean.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-number-number-boolean.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..18664d00fa933ad7b288c9809535cf512ba1faab Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-number-number-boolean.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-param-outside-boolean.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-param-outside-boolean.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..c96ce324740c904db57961b709d340bd6addd95e Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-param-outside-boolean.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-param-outside-number.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-param-outside-number.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..ee2f9e21758daf82ebd4a837dc1d4993658062f7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-param-outside-number.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-param-outside-string.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-param-outside-string.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..102c92e2f054f1c3c7106e4fa4d664f0be947e37 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-param-outside-string.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-boolean.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-boolean.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..b2873d2e15978555905411ad369f16f4eda02dc4 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-boolean.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-number.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-number.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..cb72634880c1e0b183ce7909aaadb92397a6c3a7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-number.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-string.sb2 b/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-string.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..bc1b4a99fa9db714ab1a7f8d75d41c1fbfc39a23 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/procedures-recursive-default-string.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/sensing-get-attribute-of-stage-alt-name.sb2 b/local-scratch-vm/test/fixtures/execute/sensing-get-attribute-of-stage-alt-name.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..c4bddcbdca0d8428e6453425cb7482a10219b4e1 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/sensing-get-attribute-of-stage-alt-name.sb2 differ diff --git a/local-scratch-vm/test/fixtures/execute/sprite-number-name.sb2 b/local-scratch-vm/test/fixtures/execute/sprite-number-name.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..7ed65391135769b8ee9ee29acdf5d849370619fc --- /dev/null +++ b/local-scratch-vm/test/fixtures/execute/sprite-number-name.sb2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ec555df0ef3b72e9afadbd57bcbfe3ca20b29bb23e9db68b1ae9d835fe5179f +size 126033 diff --git a/local-scratch-vm/test/fixtures/execute/tw-NaN.sb3 b/local-scratch-vm/test/fixtures/execute/tw-NaN.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..17ddc26bdb14c47bb00600accae4b19495d903eb Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-NaN.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-add-can-return-nan.sb3 b/local-scratch-vm/test/fixtures/execute/tw-add-can-return-nan.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..04b089e7847c99c7cca527d1e6af9965f07231b7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-add-can-return-nan.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-all-at-once.sb3 b/local-scratch-vm/test/fixtures/execute/tw-all-at-once.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..741e8605635f37965fe31f5913e0952184b947df Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-all-at-once.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-broadcast-id-and-name-desync.sb3 b/local-scratch-vm/test/fixtures/execute/tw-broadcast-id-and-name-desync.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..7af93aedd900e696a768fb102af1e6b5de65ab66 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-broadcast-id-and-name-desync.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-change-size-does-not-use-rounded-size.sb3 b/local-scratch-vm/test/fixtures/execute/tw-change-size-does-not-use-rounded-size.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..dfffe493da5164baf9796440f88e11b161230c51 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-change-size-does-not-use-rounded-size.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-color-input-returns-hex.sb3 b/local-scratch-vm/test/fixtures/execute/tw-color-input-returns-hex.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..a57a98370a935bd85a835cf7a1f75b1f1be2eea7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-color-input-returns-hex.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-comparison-matrix-inline.sb3 b/local-scratch-vm/test/fixtures/execute/tw-comparison-matrix-inline.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..6be9e53270b79892c318a8056139247b69c6e9de Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-comparison-matrix-inline.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-comparison-matrix-runtime.sb3 b/local-scratch-vm/test/fixtures/execute/tw-comparison-matrix-runtime.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..5d77c0eec57895ebe93236d8615ea7b2d75c6aec Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-comparison-matrix-runtime.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-coordinate-precision.sb3 b/local-scratch-vm/test/fixtures/execute/tw-coordinate-precision.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..832c327c11df1ddc3197dd2815ce23c3af2fe3f2 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-coordinate-precision.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-boolean-number-comparison.sb3 b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-boolean-number-comparison.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..bce51b73b0c066f14f424061f4cf844858c2959e Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-boolean-number-comparison.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-non-finite-direction.sb3 b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-non-finite-direction.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..32885353f99bf8f111a49fda42602e52f60ccdf1 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-non-finite-direction.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-random-with-invalid-number-with-period.sb3 b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-random-with-invalid-number-with-period.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..8112222c3815eeef5b273e4f47fea64e7a5d6b4a Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-random-with-invalid-number-with-period.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-variable-id-name-desync-name-fallback.sb3 b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-variable-id-name-desync-name-fallback.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..eafa49843e0e6d7e26cc0ad91a6a4651e13b16ab Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-variable-id-name-desync-name-fallback.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-wait-zero-seconds-in-warp-mode.sb3 b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-wait-zero-seconds-in-warp-mode.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..9280c85fd2880016e9cf133137ebd9254b0616dd Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-forkphorus-515-wait-zero-seconds-in-warp-mode.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-generate-comparison-matrix-inline.js b/local-scratch-vm/test/fixtures/execute/tw-generate-comparison-matrix-inline.js new file mode 100644 index 0000000000000000000000000000000000000000..2b1b25ed7e8407eced288784bde33a2ffd6f44be --- /dev/null +++ b/local-scratch-vm/test/fixtures/execute/tw-generate-comparison-matrix-inline.js @@ -0,0 +1,140 @@ +const fs = require('fs'); +const Cast = require('../../../src/util/cast'); + +/* +This is a command-line tool to generate the tw-comparison-matrix-inline test project. + +To use output: +Blockly.Xml.domToWorkspace( + new DOMParser().parseFromString(XML_GOES_HERE, 'text/xml').documentElement, + Blockly.getMainWorkspace() +); +*/ + +/* eslint-disable no-console */ + +const VALUES = [ + '0', + '0.0', + '1.23', + '.23', + '0.123', + '-0', + '-1', + 'true', + 'false', + 'NaN', + 'Infinity', + 'banana', + '🎉', + '' +]; + +const OPERATORS = [ + { + opcode: 'operator_lt', + symbol: '<', + execute: (a, b) => Cast.compare(a, b) < 0 + }, + { + opcode: 'operator_equals', + symbol: '=', + execute: (a, b) => Cast.compare(a, b) === 0 + }, + { + opcode: 'operator_gt', + symbol: '>', + execute: (a, b) => Cast.compare(a, b) > 0 + } +]; + +const NEXT = '{{NEXT}}'; + +let result = ` + + + + + + + plan 0 + + + ${NEXT} + + + + +`; + +let n = 0; +for (const i of VALUES) { + for (const j of VALUES) { + for (const operator of OPERATORS) { + n++; + result = result.replace(NEXT, ` + + + + + + + + + + + + + + ${i} + + + + + ${j} + + + + + + + ${operator.execute(i, j)} + + + + + + + + + + + fail ${n}: ${i} should be ${operator.symbol} ${j} + + + + + ${NEXT} + + + `.replace(/ {4}/g, ' ')); + } + } +} + +result = result.replace(NEXT, ` + + + + + end + + + + +`); + +result = result.replace(NEXT, ''); + +console.log(`Expecting ${n}`); +fs.writeFileSync('matrix-inline-output-do-not-commit.xml', result); diff --git a/local-scratch-vm/test/fixtures/execute/tw-list-any.sb3 b/local-scratch-vm/test/fixtures/execute/tw-list-any.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..37be9b1844bb6ab80475da1dc64be8a11c7972ed Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-list-any.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-obsolete-blocks.sb3 b/local-scratch-vm/test/fixtures/execute/tw-obsolete-blocks.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..e5eefd8bb674f1c5b3b5878ca90889b2afc9ba91 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-obsolete-blocks.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-one-divide-negative-zero.sb3 b/local-scratch-vm/test/fixtures/execute/tw-one-divide-negative-zero.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..7bddcd8616af49a7e512c4abcd2f56eb8221c5c5 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-one-divide-negative-zero.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-preciseProjectTimer-drift-453118719.sb3 b/local-scratch-vm/test/fixtures/execute/tw-preciseProjectTimer-drift-453118719.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..c886ef4cf32e97d5ffc6b96b88783b3d90cfb4a0 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-preciseProjectTimer-drift-453118719.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-prefers-first-occurence-of-procedure-387608267.sb3 b/local-scratch-vm/test/fixtures/execute/tw-prefers-first-occurence-of-procedure-387608267.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..6d653a27e9a319cf37e37244ec3264e304cf14ed Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-prefers-first-occurence-of-procedure-387608267.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-procedure-arguments-with-same-name.sb3 b/local-scratch-vm/test/fixtures/execute/tw-procedure-arguments-with-same-name.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..069a58cc78cffa5964aa452ceb823ac31efd8325 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-procedure-arguments-with-same-name.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-procedure-call-resets-variable-input-types-430811055.sb3 b/local-scratch-vm/test/fixtures/execute/tw-procedure-call-resets-variable-input-types-430811055.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..756033614c2578bea5d3f175614eaf73305ec382 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-procedure-call-resets-variable-input-types-430811055.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-procedure-prototype-exists-but-not-definition-549160843.sb3 b/local-scratch-vm/test/fixtures/execute/tw-procedure-prototype-exists-but-not-definition-549160843.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..0513a480e6f7bfe331e269d700fe6feb16862371 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-procedure-prototype-exists-but-not-definition-549160843.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-promise-loop-double-yield-kouzeru.sb3 b/local-scratch-vm/test/fixtures/execute/tw-promise-loop-double-yield-kouzeru.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..f42e7705474e9b420a16117b0ddae92ff73e2eab Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-promise-loop-double-yield-kouzeru.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-restart-broadcast-threads.sb3 b/local-scratch-vm/test/fixtures/execute/tw-restart-broadcast-threads.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..ceae92ddd9e5e5e12ecd7aaef0a3890334c8c974 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-restart-broadcast-threads.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-safe-procedure-argument-casting.sb3 b/local-scratch-vm/test/fixtures/execute/tw-safe-procedure-argument-casting.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..16139265a1431117e77a4adc30e12324b2f358a2 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-safe-procedure-argument-casting.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-sensing-of.sb3 b/local-scratch-vm/test/fixtures/execute/tw-sensing-of.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..8e15af26c1b71441e21a8c817a907ceb334677cb Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-sensing-of.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-stage-cannot-move-layers.sb3 b/local-scratch-vm/test/fixtures/execute/tw-stage-cannot-move-layers.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..67c2552a83d8e9aab8ff5201c91821aee3df93fb Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-stage-cannot-move-layers.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-subtract-can-return-nan.sb3 b/local-scratch-vm/test/fixtures/execute/tw-subtract-can-return-nan.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..161f27bed0c5b4d06edbcfd3eec97ee53dfcaf66 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-subtract-can-return-nan.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-tab-equals-zero.sb3 b/local-scratch-vm/test/fixtures/execute/tw-tab-equals-zero.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..0a63760305666dab23b3df2c8181b699f8705a46 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-tab-equals-zero.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-tangent.sb3 b/local-scratch-vm/test/fixtures/execute/tw-tangent.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..3936fabe7015c3403dc7b246477c97824a457aca Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-tangent.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-unsafe-equals.sb3 b/local-scratch-vm/test/fixtures/execute/tw-unsafe-equals.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..62d6794c2ebb227e0073fbbe54289b684e6aed75 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-unsafe-equals.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-warp-repeat-until-timer-greater-than.sb3 b/local-scratch-vm/test/fixtures/execute/tw-warp-repeat-until-timer-greater-than.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..77ef3f288ba13469e10b61fe24c9a8ab91c3457a Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-warp-repeat-until-timer-greater-than.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-when-backdrop-switches-to-next-backdrop.sb3 b/local-scratch-vm/test/fixtures/execute/tw-when-backdrop-switches-to-next-backdrop.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..71640df8fad11db19a5fd53dc5687d2927cebe51 Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-when-backdrop-switches-to-next-backdrop.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-when-backdrop-switches-to-switch-backdrop-to.sb3 b/local-scratch-vm/test/fixtures/execute/tw-when-backdrop-switches-to-switch-backdrop-to.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..e28ab9d51ed3fdebd02d4646f0a35af34f5e055d Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-when-backdrop-switches-to-switch-backdrop-to.sb3 differ diff --git a/local-scratch-vm/test/fixtures/execute/tw-zombie-cube-escape-284516654.sb3 b/local-scratch-vm/test/fixtures/execute/tw-zombie-cube-escape-284516654.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..ad3334848d5cbc309e6941f4c75ffbc013c0f07b Binary files /dev/null and b/local-scratch-vm/test/fixtures/execute/tw-zombie-cube-escape-284516654.sb3 differ diff --git a/local-scratch-vm/test/fixtures/fake-bitmap-adapter.js b/local-scratch-vm/test/fixtures/fake-bitmap-adapter.js new file mode 100644 index 0000000000000000000000000000000000000000..a7e5c8aa78368e99e8aa430f33f917e37f85b8ef --- /dev/null +++ b/local-scratch-vm/test/fixtures/fake-bitmap-adapter.js @@ -0,0 +1,7 @@ +const FakeBitmapAdapter = require('scratch-svg-renderer').BitmapAdapter; + +FakeBitmapAdapter.prototype.resize = function (canvas) { + return canvas; +}; + +module.exports = FakeBitmapAdapter; diff --git a/local-scratch-vm/test/fixtures/fake-renderer.js b/local-scratch-vm/test/fixtures/fake-renderer.js new file mode 100644 index 0000000000000000000000000000000000000000..2a4fcfb1348a1ddf2ecc3f088827da92a53da68b --- /dev/null +++ b/local-scratch-vm/test/fixtures/fake-renderer.js @@ -0,0 +1,98 @@ +const FakeRenderer = function () { + this.unused = ''; + this.x = 0; + this.y = 0; + this.order = 0; + this.spriteCount = 5; + this._nextSkinId = -1; +}; + +FakeRenderer.prototype.createSVGSkin = function () { + return this._nextSkinId++; +}; + +FakeRenderer.prototype.createBitmapSkin = function () { + return this._nextSkinId++; +}; + +FakeRenderer.prototype.getSkinSize = function (d) { // eslint-disable-line no-unused-vars + return [0, 0]; +}; + +FakeRenderer.prototype.getSkinRotationCenter = function (d) { // eslint-disable-line no-unused-vars + return [0, 0]; +}; + +FakeRenderer.prototype.createDrawable = function () { + return true; +}; + +FakeRenderer.prototype.getFencedPositionOfDrawable = function (d, p) { // eslint-disable-line no-unused-vars + return [p[0], p[1]]; +}; + +FakeRenderer.prototype.updateDrawableSkinId = function (d, skinId) { // eslint-disable-line no-unused-vars +}; + +FakeRenderer.prototype.updateDrawablePosition = function (d, position) { // eslint-disable-line no-unused-vars + this.x = position[0]; + this.y = position[1]; +}; + +FakeRenderer.prototype.updateDrawableDirectionScale = + function (d, direction, scale) {}; // eslint-disable-line no-unused-vars + +FakeRenderer.prototype.updateDrawableVisible = function (d, visible) { // eslint-disable-line no-unused-vars +}; + +FakeRenderer.prototype.updateDrawableEffect = function (d, effectName, value) { // eslint-disable-line no-unused-vars +}; + +FakeRenderer.prototype.getCurrentSkinSize = function (d) { // eslint-disable-line no-unused-vars + return [0, 0]; +}; + +FakeRenderer.prototype.pick = function (x, y, a, b, d) { // eslint-disable-line no-unused-vars + return true; +}; + +FakeRenderer.prototype.drawableTouching = function (d, x, y, w, h) { // eslint-disable-line no-unused-vars + return true; +}; + +FakeRenderer.prototype.isTouchingColor = function (d, c) { // eslint-disable-line no-unused-vars + return true; +}; + +FakeRenderer.prototype.getBounds = function (d) { // eslint-disable-line no-unused-vars + return {left: this.x, right: this.x, top: this.y, bottom: this.y}; +}; + +FakeRenderer.prototype.setDrawableOrder = function (d, a, optG, optA, optB) { // eslint-disable-line no-unused-vars + if (d === 999) return 1; // fake for test case + if (optA) { + a += this.order; + } + if (optB) { + a = Math.max(a, optB); + } + a = Math.max(a, 0); + this.order = Math.min(a, this.spriteCount); + return this.order; +}; + +FakeRenderer.prototype.getDrawableOrder = function (d) { // eslint-disable-line no-unused-vars + return 'stub'; +}; + +FakeRenderer.prototype.pick = function (x, y, a, b, c) { // eslint-disable-line no-unused-vars + return c[0]; +}; + +FakeRenderer.prototype.isTouchingColor = function (a, b) { // eslint-disable-line no-unused-vars + return false; +}; + +FakeRenderer.prototype.setLayerGroupOrdering = function (a) {}; // eslint-disable-line no-unused-vars + +module.exports = FakeRenderer; diff --git a/local-scratch-vm/test/fixtures/hat-execution-order.sb2 b/local-scratch-vm/test/fixtures/hat-execution-order.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..d88057eb100a88d0635955a8174fe27827988ccd Binary files /dev/null and b/local-scratch-vm/test/fixtures/hat-execution-order.sb2 differ diff --git a/local-scratch-vm/test/fixtures/invisible-tempo-monitor-no-other-music-blocks.sb2 b/local-scratch-vm/test/fixtures/invisible-tempo-monitor-no-other-music-blocks.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..885a36e47e71af57cfe551e9cdbb7f2a4134730d Binary files /dev/null and b/local-scratch-vm/test/fixtures/invisible-tempo-monitor-no-other-music-blocks.sb2 differ diff --git a/local-scratch-vm/test/fixtures/invisible-video-monitor.sb2 b/local-scratch-vm/test/fixtures/invisible-video-monitor.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..802e0a3b692336f00a9156cc931c358ea616b248 Binary files /dev/null and b/local-scratch-vm/test/fixtures/invisible-video-monitor.sb2 differ diff --git a/local-scratch-vm/test/fixtures/list-monitor-rename.sb3 b/local-scratch-vm/test/fixtures/list-monitor-rename.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..e3d20ea799515a9edd94f25042c87e515cec80ae Binary files /dev/null and b/local-scratch-vm/test/fixtures/list-monitor-rename.sb3 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/README.md b/local-scratch-vm/test/fixtures/load-extensions/README.md new file mode 100644 index 0000000000000000000000000000000000000000..20c63c25e0f1913da82ba074f35ec8ef9a6f94ec --- /dev/null +++ b/local-scratch-vm/test/fixtures/load-extensions/README.md @@ -0,0 +1,18 @@ +Tests in this folder are run in scratch by integration/load-extensions.js to determine whether an extension can load properly. The test projects in this folder are examples of non-core extensions usage. Read `integration/load-extensions.js` for more. + +### Adding new extensions + +When extending Scratch with non-core extensions, save an example project to this the appropiate subdirectory based on which test in `load-extensions.js` will be using that test file. The file should use the following naming convention: + +`[extensionID]-rest-of-file-name.[file type sb3 or sb2]` + +The load-extensions.js test will automatically test this new project file since it gets a list of all files in its repsective subdirectories for testing and extracts the extension id from the first section of the file same separated by a dash. + +Each of the `[extensionID]-simple-project` test files have been made as the simplest possible cases for loading the extension. This means that only one block has been added to the project and that block is from the relevant extension. + +### Adding more example projects + +Sometimes we need to test more complex projects to catch cases and contexts where an extension should load and doesn't. We can save those project files using the convention [extensionID]-project-name. For example, the Dolphins 3D project (#115870836) had a pen extension that wouldn't load, whereas `pen-simple-project.sb2` and `pen-simple-project.sb3` did pass these tests. For this reason, `pen-dolphin-3d.sb2` and `pen-dolphin-3d.sb3` are now part of the test examples. + +### // TO DO +The translation extension doesn't have test projects added for them yet since they need a little more infrastructure stubbed out in the test. diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/ev3-simple-project.sb3 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/ev3-simple-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..3d72a98f3a759b4cbfdfa05152a0e22088a89761 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/ev3-simple-project.sb3 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/microbit-simple-project.sb3 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/microbit-simple-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..3651b1c9185a45843ae4ada2e53597f3a52726c6 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/microbit-simple-project.sb3 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/music-simple-project.sb2 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/music-simple-project.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..e4ed857a186a1a900ff994c8fe3f96f2bc371893 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/music-simple-project.sb2 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/music-simple-project.sb3 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/music-simple-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..9610b62b3880c76146096b5d60add978f47e1fcf Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/music-simple-project.sb3 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-dolphin-3d.sb2 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-dolphin-3d.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..fd789a79569fd464244f37ee85c06a8e7d43b526 --- /dev/null +++ b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-dolphin-3d.sb2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:236a485675408db3ab4dba27ddb13ef850aeea7ab4d5173c054ad7df1ec7c4e4 +size 1799360 diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-dolphin-3d.sb3 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-dolphin-3d.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..ba4a0812d4125170e228f87d2666f2d59b53bad3 --- /dev/null +++ b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-dolphin-3d.sb3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:975052cbd3946ea1d60eb3c2c8c0d99fc1d9bf9aa39d4e0c30a96112b4297a83 +size 1527227 diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-simple-project.sb2 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-simple-project.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..3c52dbb3eedce94547ff3572fc517095c30fcf9c Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-simple-project.sb2 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-simple-project.sb3 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-simple-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..411ae8c9eb2149ec9f67cebc14229d5c8ab3daf7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/pen-simple-project.sb3 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/text2speech-simple-project.sb3 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/text2speech-simple-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..b0651ccae348a77f4447df7f2e24ceb00d5b56ca Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/text2speech-simple-project.sb3 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/videoSensing-simple-project.sb2 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/videoSensing-simple-project.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..36b5f500e1bf6fa47f4626febb15d63da2eb256e Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/videoSensing-simple-project.sb2 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/videoSensing-simple-project.sb3 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/videoSensing-simple-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..435864e0a117347b7e246db6c6116c84d430aca7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/videoSensing-simple-project.sb3 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/wedo2-simple-project.sb2 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/wedo2-simple-project.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..d93a96f99a1a67d4fbed07e120dca20480acc4b8 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/wedo2-simple-project.sb2 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/confirm-load/wedo2-simple-project.sb3 b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/wedo2-simple-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..7eabedc00c2064f24574915467e76334b6f3ccc2 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/confirm-load/wedo2-simple-project.sb3 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/music-visible-monitor-no-blocks.sb2 b/local-scratch-vm/test/fixtures/load-extensions/music-visible-monitor-no-blocks.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..ead127b34af8487f0347fb123bd7225e80b265a7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/music-visible-monitor-no-blocks.sb2 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/video-state/videoState-off.sb2 b/local-scratch-vm/test/fixtures/load-extensions/video-state/videoState-off.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..2d9af41d2829d02f5ea35da6c37e15e6e8065a1f Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/video-state/videoState-off.sb2 differ diff --git a/local-scratch-vm/test/fixtures/load-extensions/video-state/videoState-on-transparency-0.sb2 b/local-scratch-vm/test/fixtures/load-extensions/video-state/videoState-on-transparency-0.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..f9c308dd92c0a3b8043db14176210fffe0263799 Binary files /dev/null and b/local-scratch-vm/test/fixtures/load-extensions/video-state/videoState-on-transparency-0.sb2 differ diff --git a/local-scratch-vm/test/fixtures/looks.sb2 b/local-scratch-vm/test/fixtures/looks.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..24a91c521927f3bec0a50cb3bd0c50083a4f5ca8 --- /dev/null +++ b/local-scratch-vm/test/fixtures/looks.sb2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:deeff3322abfb6cfdd6395eb3c7ae83dc7821c79345a377589b13d6907d62490 +size 486036 diff --git a/local-scratch-vm/test/fixtures/make-test-storage.js b/local-scratch-vm/test/fixtures/make-test-storage.js new file mode 100644 index 0000000000000000000000000000000000000000..1401a2c1edbadbb236ab55f9ed87e80897d6d531 --- /dev/null +++ b/local-scratch-vm/test/fixtures/make-test-storage.js @@ -0,0 +1,47 @@ +const ScratchStorage = require('scratch-storage'); + +const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu/'; +const PROJECT_SERVER = 'https://cdn.projects.scratch.mit.edu/'; + +/** + * @param {Asset} asset - calculate a URL for this asset. + * @returns {string} a URL to download a project file. + */ +const getProjectUrl = function (asset) { + const assetIdParts = asset.assetId.split('.'); + const assetUrlParts = [PROJECT_SERVER, 'internalapi/project/', assetIdParts[0], '/get/']; + if (assetIdParts[1]) { + assetUrlParts.push(assetIdParts[1]); + } + return assetUrlParts.join(''); +}; + +/** + * @param {Asset} asset - calculate a URL for this asset. + * @returns {string} a URL to download a project asset (PNG, WAV, etc.) + */ +const getAssetUrl = function (asset) { + const assetUrlParts = [ + ASSET_SERVER, + 'internalapi/asset/', + asset.assetId, + '.', + asset.dataFormat, + '/get/' + ]; + return assetUrlParts.join(''); +}; + +/** + * Construct a new instance of ScratchStorage and provide it with default web sources. + * @returns {ScratchStorage} - an instance of ScratchStorage, ready to be used for tests. + */ +const makeTestStorage = function () { + const storage = new ScratchStorage(); + const AssetType = storage.AssetType; + storage.addWebStore([AssetType.Project], getProjectUrl); + storage.addWebStore([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl); + return storage; +}; + +module.exports = makeTestStorage; diff --git a/local-scratch-vm/test/fixtures/missing_png.sb2 b/local-scratch-vm/test/fixtures/missing_png.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..1539fed21a93400a0d418ec854c7a71db6ffadc1 Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_png.sb2 differ diff --git a/local-scratch-vm/test/fixtures/missing_png.sb3 b/local-scratch-vm/test/fixtures/missing_png.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..40260a33bf4740eb43a426bd162919cb3185e3bb Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_png.sb3 differ diff --git a/local-scratch-vm/test/fixtures/missing_png.sprite2 b/local-scratch-vm/test/fixtures/missing_png.sprite2 new file mode 100644 index 0000000000000000000000000000000000000000..514119934f3469b824c392ba12c1ff78a140c0e2 Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_png.sprite2 differ diff --git a/local-scratch-vm/test/fixtures/missing_png.sprite3 b/local-scratch-vm/test/fixtures/missing_png.sprite3 new file mode 100644 index 0000000000000000000000000000000000000000..0702cdcd35c63e0b8d34e9bbe345d2a123711ae6 Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_png.sprite3 differ diff --git a/local-scratch-vm/test/fixtures/missing_sound.sb3 b/local-scratch-vm/test/fixtures/missing_sound.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..14dea75830d86de68144b77fe81a02588092fd9f Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_sound.sb3 differ diff --git a/local-scratch-vm/test/fixtures/missing_svg.sb2 b/local-scratch-vm/test/fixtures/missing_svg.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..45daf2cc806da2720bedd0f6460b57014b7f398d Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_svg.sb2 differ diff --git a/local-scratch-vm/test/fixtures/missing_svg.sb3 b/local-scratch-vm/test/fixtures/missing_svg.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..e0956c36f06c05e56a9de97f38882654b9158ee7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_svg.sb3 differ diff --git a/local-scratch-vm/test/fixtures/missing_svg.sprite2 b/local-scratch-vm/test/fixtures/missing_svg.sprite2 new file mode 100644 index 0000000000000000000000000000000000000000..c8e3732de77bdb45a542cad5dd89cdf623b58100 Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_svg.sprite2 differ diff --git a/local-scratch-vm/test/fixtures/missing_svg.sprite3 b/local-scratch-vm/test/fixtures/missing_svg.sprite3 new file mode 100644 index 0000000000000000000000000000000000000000..5cc0986898c684baa0d736794cc4f0606e887c51 Binary files /dev/null and b/local-scratch-vm/test/fixtures/missing_svg.sprite3 differ diff --git a/local-scratch-vm/test/fixtures/mock-timer.js b/local-scratch-vm/test/fixtures/mock-timer.js new file mode 100644 index 0000000000000000000000000000000000000000..c0e3ae4958e208884e1108e46929aed8f639d6c4 --- /dev/null +++ b/local-scratch-vm/test/fixtures/mock-timer.js @@ -0,0 +1,167 @@ +/** + * Mimic the Timer class with external control of the "time" value, allowing tests to run more quickly and + * reliably. Multiple instances of this class operate independently: they may report different time values, and + * advancing one timer will not trigger timeouts set on another. + */ +class MockTimer { + /** + * Creates an instance of MockTimer. + * @param {*} [nowObj=null] - alert the caller that this parameter, supported by Timer, is not supported here. + * @memberof MockTimer + */ + constructor (nowObj = null) { + if (nowObj) { + throw new Error('nowObj is not implemented in MockTimer'); + } + + /** + * The fake "current time" value, in epoch milliseconds. + * @type {number} + */ + this._mockTime = 0; + + /** + * Used to store the start time of a timer action. + * Updated when calling `timer.start`. + * @type {number} + */ + this.startTime = 0; + + /** + * The ID to use the next time `setTimeout` is called. + * @type {number} + */ + this._nextTimeoutId = 1; + + /** + * Map of timeout ID to pending timeout callback info. + * @type {Map.} + * @property {number} time - the time at/after which this handler should run + * @property {Function} handler - the handler to call when the time comes + */ + this._timeouts = new Map(); + } + + /** + * Advance this MockTimer's idea of "current time", running timeout handlers if appropriate. + * + * @param {number} milliseconds - the amount of time to add to the current mock time value, in milliseconds. + * @memberof MockTimer + */ + advanceMockTime (milliseconds) { + if (milliseconds < 0) { + throw new Error('Time may not move backward'); + } + this._mockTime += milliseconds; + this._runTimeouts(); + } + + /** + * Advance this MockTimer's idea of "current time", running timeout handlers if appropriate. + * + * @param {number} milliseconds - the amount of time to add to the current mock time value, in milliseconds. + * @returns {Promise} - promise which resolves after timeout handlers have had an opportunity to run. + * @memberof MockTimer + */ + advanceMockTimeAsync (milliseconds) { + return new Promise(resolve => { + this.advanceMockTime(milliseconds); + global.setTimeout(resolve, 0); + }); + } + + /** + * @returns {number} - current mock time elapsed since 1 January 1970 00:00:00 UTC. + * @memberof MockTimer + */ + time () { + return this._mockTime; + } + + /** + * Returns a time accurate relative to other times produced by this function. + * @returns {number} ms-scale accurate time relative to other relative times. + * @memberof MockTimer + */ + relativeTime () { + return this._mockTime; + } + + /** + * Start a timer for measuring elapsed time. + * @memberof MockTimer + */ + start () { + this.startTime = this._mockTime; + } + + /** + * @returns {number} - the time elapsed since `start()` was called. + * @memberof MockTimer + */ + timeElapsed () { + return this._mockTime - this.startTime; + } + + /** + * Call a handler function after a specified amount of time has elapsed. + * Guaranteed to happen in between "ticks" of JavaScript. + * @param {function} handler - function to call after the timeout + * @param {number} timeout - number of milliseconds to delay before calling the handler + * @returns {number} - the ID of the new timeout. + * @memberof MockTimer + */ + setTimeout (handler, timeout) { + const timeoutId = this._nextTimeoutId++; + this._timeouts.set(timeoutId, { + time: this._mockTime + timeout, + handler + }); + this._runTimeouts(); + return timeoutId; + } + + /** + * Clear a particular timeout from the pending timeout pool. + * @param {number} timeoutId - the value returned from `setTimeout()` + * @memberof MockTimer + */ + clearTimeout (timeoutId) { + this._timeouts.delete(timeoutId); + } + + /** + * WARNING: this method has no equivalent in `Timer`. Do not use this method outside of tests! + * @returns {boolean} - true if there are any pending timeouts, false otherwise. + * @memberof MockTimer + */ + hasTimeouts () { + return this._timeouts.size > 0; + } + + /** + * Run any timeout handlers whose timeouts have expired. + * @memberof MockTimer + */ + _runTimeouts () { + const ready = []; + + this._timeouts.forEach((timeoutRecord, timeoutId) => { + const isReady = timeoutRecord.time <= this._mockTime; + if (isReady) { + ready.push(timeoutRecord); + this._timeouts.delete(timeoutId); + } + }); + + // sort so that earlier timeouts run before later timeouts + ready.sort((a, b) => a.time < b.time); + + // next tick, call everything that's ready + global.setTimeout(() => { + ready.forEach(o => o.handler()); + }, 0); + } +} + +module.exports = MockTimer; diff --git a/local-scratch-vm/test/fixtures/monitored_variables.sb3 b/local-scratch-vm/test/fixtures/monitored_variables.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..cae7b47d51b0d971b7dfb0d44dbce1330620b01a Binary files /dev/null and b/local-scratch-vm/test/fixtures/monitored_variables.sb3 differ diff --git a/local-scratch-vm/test/fixtures/monitors.sb2 b/local-scratch-vm/test/fixtures/monitors.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..5874889056c47a0e3c7f558f8ced5651a47b8b0e Binary files /dev/null and b/local-scratch-vm/test/fixtures/monitors.sb2 differ diff --git a/local-scratch-vm/test/fixtures/monitors.sb3 b/local-scratch-vm/test/fixtures/monitors.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..1343243ddd84bdacfcc94c6424440998d2454b1b Binary files /dev/null and b/local-scratch-vm/test/fixtures/monitors.sb3 differ diff --git a/local-scratch-vm/test/fixtures/motion.sb2 b/local-scratch-vm/test/fixtures/motion.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..08c5635444876d44e361b3a0a7a45ee37298f625 Binary files /dev/null and b/local-scratch-vm/test/fixtures/motion.sb2 differ diff --git a/local-scratch-vm/test/fixtures/offline-custom-assets.sb2 b/local-scratch-vm/test/fixtures/offline-custom-assets.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..b375d9b5d0be8f8a2b87cc6b81ffcc359b6b71f1 Binary files /dev/null and b/local-scratch-vm/test/fixtures/offline-custom-assets.sb2 differ diff --git a/local-scratch-vm/test/fixtures/ordering.sb2 b/local-scratch-vm/test/fixtures/ordering.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..d55d69dca130c8198a13bff8a8b7d90b4295ede2 --- /dev/null +++ b/local-scratch-vm/test/fixtures/ordering.sb2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b8cb2ba51303a5974258c7177cd530ad6bdc40c69ecda0ebd2eaf9d05a4a276 +size 139568 diff --git a/local-scratch-vm/test/fixtures/origin-absent.sb3 b/local-scratch-vm/test/fixtures/origin-absent.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..c6c7755d895e6afc4a880bb14c83ac0ac6bb60ae Binary files /dev/null and b/local-scratch-vm/test/fixtures/origin-absent.sb3 differ diff --git a/local-scratch-vm/test/fixtures/origin.sb3 b/local-scratch-vm/test/fixtures/origin.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..b8356a1f0daa58e3885eea65427e95b9dd993acc Binary files /dev/null and b/local-scratch-vm/test/fixtures/origin.sb3 differ diff --git a/local-scratch-vm/test/fixtures/pen.sb2 b/local-scratch-vm/test/fixtures/pen.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..369ebce8c471e174a123746b5d561f2c0ae575aa --- /dev/null +++ b/local-scratch-vm/test/fixtures/pen.sb2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6131733c10649f69bfd4688feb054e2bf819e6a215d42bb96f5d8ac3e09bda8 +size 145466 diff --git a/local-scratch-vm/test/fixtures/procedure.sb2 b/local-scratch-vm/test/fixtures/procedure.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..7c6c2d60485878e56c6528eec57ce40c9dcc8650 Binary files /dev/null and b/local-scratch-vm/test/fixtures/procedure.sb2 differ diff --git a/local-scratch-vm/test/fixtures/readProjectFile.js b/local-scratch-vm/test/fixtures/readProjectFile.js new file mode 100644 index 0000000000000000000000000000000000000000..36f42797cff5745c919022f6b95cf34d82927111 --- /dev/null +++ b/local-scratch-vm/test/fixtures/readProjectFile.js @@ -0,0 +1,21 @@ +const AdmZip = require('adm-zip'); +const fs = require('fs'); + +module.exports = { + readFileToBuffer: function (path) { + return Buffer.from(fs.readFileSync(path)); + }, + extractProjectJson: function (path) { + const zip = new AdmZip(path); + const projectEntry = zip.getEntries().find(item => item.entryName.match(/project\.json/)); + if (projectEntry) { + return JSON.parse(zip.readAsText(projectEntry.entryName, 'utf8')); + } + return null; + }, + extractAsset: function (path, assetFileName) { + const zip = new AdmZip(path); + const assetEntry = zip.getEntries().find(item => item.entryName.match(assetFileName)); + return assetEntry.getData(); + } +}; diff --git a/local-scratch-vm/test/fixtures/saythink-and-wait.sb2 b/local-scratch-vm/test/fixtures/saythink-and-wait.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..d9be202c4281dd3afa75c4c0d5c62b55056cc41d Binary files /dev/null and b/local-scratch-vm/test/fixtures/saythink-and-wait.sb2 differ diff --git a/local-scratch-vm/test/fixtures/sb2-from-sb1-missing-backdrop-image.sb2 b/local-scratch-vm/test/fixtures/sb2-from-sb1-missing-backdrop-image.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..6c36c68b2ed0dc51e6e2852c0f7c830108006b04 Binary files /dev/null and b/local-scratch-vm/test/fixtures/sb2-from-sb1-missing-backdrop-image.sb2 differ diff --git a/local-scratch-vm/test/fixtures/sensing.sb2 b/local-scratch-vm/test/fixtures/sensing.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..c7fa5d640a1870515a3820a013767026681eb22c Binary files /dev/null and b/local-scratch-vm/test/fixtures/sensing.sb2 differ diff --git a/local-scratch-vm/test/fixtures/simple-stack.js b/local-scratch-vm/test/fixtures/simple-stack.js new file mode 100644 index 0000000000000000000000000000000000000000..acd5f5d3a25f1deef8efeac29522169e6110b85e --- /dev/null +++ b/local-scratch-vm/test/fixtures/simple-stack.js @@ -0,0 +1,65 @@ +module.exports = [ + { + id: '1ZGd(W8DvU?vI1RN)e0E', + opcode: 'motion_goto', + inputs: { + TO: { + name: 'TO', + block: 'Ht(rOKMe0@sB3n4(b3;=', + shadow: '-C=c-_TI_7d(l3ii2[wh' + } + }, + fields: { + + }, + next: 'l.JBk`WcXE+A@i9y1tCU', + topLevel: true, + parent: null, + shadow: false + }, + { + id: 'Ht(rOKMe0@sB3n4(b3;=', + opcode: 'looks_size', + inputs: { + + }, + fields: { + + }, + next: null, + topLevel: false, + parent: '1ZGd(W8DvU?vI1RN)e0E', + shadow: false + }, + { + id: '-C=c-_TI_7d(l3ii2[wh', + opcode: 'motion_goto_menu', + inputs: { + + }, + fields: { + TO: { + name: 'TO', + value: '_random_' + } + }, + next: null, + topLevel: false, + parent: '1ZGd(W8DvU?vI1RN)e0E', + shadow: true + }, + { + id: 'l.JBk`WcXE+A@i9y1tCU', + opcode: 'sound_stopallsounds', + inputs: { + + }, + fields: { + + }, + next: null, + topLevel: false, + parent: '1ZGd(W8DvU?vI1RN)e0E', + shadow: false + } +]; diff --git a/local-scratch-vm/test/fixtures/single_sound.sb b/local-scratch-vm/test/fixtures/single_sound.sb new file mode 100644 index 0000000000000000000000000000000000000000..dfa87ce1ae32fadd201da40ee748c1bb54869f3c Binary files /dev/null and b/local-scratch-vm/test/fixtures/single_sound.sb differ diff --git a/local-scratch-vm/test/fixtures/sound.sb2 b/local-scratch-vm/test/fixtures/sound.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..dce594b371b6ddb4f47ecdb5d24edc363a120999 Binary files /dev/null and b/local-scratch-vm/test/fixtures/sound.sb2 differ diff --git a/local-scratch-vm/test/fixtures/sprite.json b/local-scratch-vm/test/fixtures/sprite.json new file mode 100644 index 0000000000000000000000000000000000000000..c2f15acd3cc5d24ed7d151357a55ed2dd32c9991 --- /dev/null +++ b/local-scratch-vm/test/fixtures/sprite.json @@ -0,0 +1,38 @@ +{ + "objName": "Sprite1", + "sounds": [{ + "soundName": "meow", + "soundID": 0, + "md5": "83c36d806dc92327b9e7049a565c6bff.wav", + "sampleCount": 18688, + "rate": 22050, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": 0, + "baseLayerMD5": "f9a1c175dbe2e5dee472858dd30d16bb.svg", + "bitmapResolution": 1, + "rotationCenterX": 47, + "rotationCenterY": 55 + }, + { + "costumeName": "costume2", + "baseLayerID": 1, + "baseLayerMD5": "6e8bd9ae68fdb02b7e1e3df656a75635.svg", + "bitmapResolution": 1, + "rotationCenterX": 47, + "rotationCenterY": 55 + }], + "currentCostumeIndex": 0, + "scratchX": 0, + "scratchY": 0, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 100000, + "visible": true, + "spriteInfo": { + } +} \ No newline at end of file diff --git a/local-scratch-vm/test/fixtures/stack-click.sb2 b/local-scratch-vm/test/fixtures/stack-click.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..8b735b73b2f3098d23ff829bb2ea574e61402eb7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/stack-click.sb2 differ diff --git a/local-scratch-vm/test/fixtures/test-compare.js b/local-scratch-vm/test/fixtures/test-compare.js new file mode 100644 index 0000000000000000000000000000000000000000..4aa625ea72e943ea3dc9b6185968edc8c4ede445 --- /dev/null +++ b/local-scratch-vm/test/fixtures/test-compare.js @@ -0,0 +1,15 @@ +const testCompare = (t, lhs, op, rhs, message) => { + const details = `Expected: ${lhs} ${op} ${rhs}`; + const extra = {details}; + switch (op) { + case '<': return t.ok(lhs < rhs, message, extra); + case '<=': return t.ok(lhs <= rhs, message, extra); + case '===': return t.ok(lhs === rhs, message, extra); + case '!==': return t.ok(lhs !== rhs, message, extra); + case '>=': return t.ok(lhs >= rhs, message, extra); + case '>': return t.ok(lhs > rhs, message, extra); + default: return t.fail(`Unrecognized op: ${op}`); + } +}; + +module.exports = testCompare; diff --git a/local-scratch-vm/test/fixtures/timer-greater-than-hat.sb2 b/local-scratch-vm/test/fixtures/timer-greater-than-hat.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..b3f8c0f019e957135ef477ede436ae0a3b48779d Binary files /dev/null and b/local-scratch-vm/test/fixtures/timer-greater-than-hat.sb2 differ diff --git a/local-scratch-vm/test/fixtures/timer-monitor.sb3 b/local-scratch-vm/test/fixtures/timer-monitor.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..b5f9d567c370529ca378f8d979d18f55d34c7ba1 Binary files /dev/null and b/local-scratch-vm/test/fixtures/timer-monitor.sb3 differ diff --git a/local-scratch-vm/test/fixtures/top-level-reporters.sb3 b/local-scratch-vm/test/fixtures/top-level-reporters.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..7e66deef10343d84721f4aab17c960ba726c9ef1 Binary files /dev/null and b/local-scratch-vm/test/fixtures/top-level-reporters.sb3 differ diff --git a/local-scratch-vm/test/fixtures/top-level-variable-reporter.sb2 b/local-scratch-vm/test/fixtures/top-level-variable-reporter.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..5c50402173e68021a92223b2fd5a3ef8d3a922db Binary files /dev/null and b/local-scratch-vm/test/fixtures/top-level-variable-reporter.sb2 differ diff --git a/local-scratch-vm/test/fixtures/tw-addon-blocks.sb3 b/local-scratch-vm/test/fixtures/tw-addon-blocks.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..245a7a30efcff479f97ab43bf84490d770a3b4ce Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-addon-blocks.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-edge-activated-hat-returns-promise.sb3 b/local-scratch-vm/test/fixtures/tw-edge-activated-hat-returns-promise.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..e9762b249242a797e505b8696dfd5cb5931ce1f8 Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-edge-activated-hat-returns-promise.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-empty-project.sb3 b/local-scratch-vm/test/fixtures/tw-empty-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..337e356904b6c51c1a294244578112cac46e1990 Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-empty-project.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-project-using-xml-extension.sb3 b/local-scratch-vm/test/fixtures/tw-project-using-xml-extension.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..8e06614f271e79614889b2d7e68f6326f1730e1b Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-project-using-xml-extension.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-project-with-extensions.sb3 b/local-scratch-vm/test/fixtures/tw-project-with-extensions.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..9599c8c0e29275a58a3fcfdcf274be3407d9e035 Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-project-with-extensions.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-save-project-sb3.sb3 b/local-scratch-vm/test/fixtures/tw-save-project-sb3.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..c11e3c713159f87e13d41f15e740f6547f48efe7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-save-project-sb3.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-stored-settings/empty-comment.sb3 b/local-scratch-vm/test/fixtures/tw-stored-settings/empty-comment.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..b66b6c937d01d799582639e7de7f6f53a92c5cd6 Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-stored-settings/empty-comment.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-stored-settings/no-comment.sb3 b/local-scratch-vm/test/fixtures/tw-stored-settings/no-comment.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..31d98224db555c68cefdd1c7abf2b64703f3dbe6 Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-stored-settings/no-comment.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-stored-settings/sprite.sb3 b/local-scratch-vm/test/fixtures/tw-stored-settings/sprite.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..a3df9703615d83be9c2b7bca7e7aa4ceaa30030c Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-stored-settings/sprite.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw-stored-settings/turbo-mode.sb3 b/local-scratch-vm/test/fixtures/tw-stored-settings/turbo-mode.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..060dff8c8888086792eaa123558a6d2208bcaa98 Binary files /dev/null and b/local-scratch-vm/test/fixtures/tw-stored-settings/turbo-mode.sb3 differ diff --git a/local-scratch-vm/test/fixtures/tw_mock_blob.js b/local-scratch-vm/test/fixtures/tw_mock_blob.js new file mode 100644 index 0000000000000000000000000000000000000000..094b8646b1ed1585381e93bb37e72f7e7e191451 --- /dev/null +++ b/local-scratch-vm/test/fixtures/tw_mock_blob.js @@ -0,0 +1,21 @@ +class MockBlob { + constructor (objects = [], options = {}) { + this.size = objects.reduce((a, i) => a + i.byteLength, 0); + this.type = options || options.type; + + this._objects = objects; + } + + _readAsBuffer () { + const result = Buffer.alloc(this.size); + let i = 0; + for (const object of this._objects) { + const view = new Uint8Array(object); + result.set(view, i); + i += object.byteLength; + } + return result; + } +} + +global.Blob = MockBlob; diff --git a/local-scratch-vm/test/fixtures/unknown-opcode-as-reporter-block.sb2 b/local-scratch-vm/test/fixtures/unknown-opcode-as-reporter-block.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..41443127eba772f11eeb8a1daf57b46520d972c5 Binary files /dev/null and b/local-scratch-vm/test/fixtures/unknown-opcode-as-reporter-block.sb2 differ diff --git a/local-scratch-vm/test/fixtures/unknown-opcode-in-c-block.sb2 b/local-scratch-vm/test/fixtures/unknown-opcode-in-c-block.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..0a11138add19a106b90c4c2e2c2eb5d3dd20db88 Binary files /dev/null and b/local-scratch-vm/test/fixtures/unknown-opcode-in-c-block.sb2 differ diff --git a/local-scratch-vm/test/fixtures/unknown-opcode.sb2 b/local-scratch-vm/test/fixtures/unknown-opcode.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..cc3341aa54349145058f98fa14dd91332eaca590 Binary files /dev/null and b/local-scratch-vm/test/fixtures/unknown-opcode.sb2 differ diff --git a/local-scratch-vm/test/fixtures/variable_characters.sb2 b/local-scratch-vm/test/fixtures/variable_characters.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..301860488be31dc2f0d89aefed8d157783aebb12 Binary files /dev/null and b/local-scratch-vm/test/fixtures/variable_characters.sb2 differ diff --git a/local-scratch-vm/test/fixtures/variable_characters.sb3 b/local-scratch-vm/test/fixtures/variable_characters.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..10720e5eee055793f25b797a35c9ca07ea74dcb7 Binary files /dev/null and b/local-scratch-vm/test/fixtures/variable_characters.sb3 differ diff --git a/local-scratch-vm/test/fixtures/visible-tempo-monitor-no-other-music-blocks.sb2 b/local-scratch-vm/test/fixtures/visible-tempo-monitor-no-other-music-blocks.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..d132a367e20e11a5a120f60283ccc12a7ea31058 Binary files /dev/null and b/local-scratch-vm/test/fixtures/visible-tempo-monitor-no-other-music-blocks.sb2 differ diff --git a/local-scratch-vm/test/fixtures/visible-video-monitor-and-video-blocks.sb2 b/local-scratch-vm/test/fixtures/visible-video-monitor-and-video-blocks.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..73c74d385137809c5b7406b5d36457fcfb1df0f4 Binary files /dev/null and b/local-scratch-vm/test/fixtures/visible-video-monitor-and-video-blocks.sb2 differ diff --git a/local-scratch-vm/test/fixtures/visible-video-monitor-no-other-video-blocks.sb2 b/local-scratch-vm/test/fixtures/visible-video-monitor-no-other-video-blocks.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..3b286f56498501b90582d01ff47f7fbfe4af1116 Binary files /dev/null and b/local-scratch-vm/test/fixtures/visible-video-monitor-no-other-video-blocks.sb2 differ diff --git a/local-scratch-vm/test/fixtures/when-clicked.sb2 b/local-scratch-vm/test/fixtures/when-clicked.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..e5d789d8d00e2831ea8c4fc6d79ee0ba24eaf8a0 Binary files /dev/null and b/local-scratch-vm/test/fixtures/when-clicked.sb2 differ diff --git a/local-scratch-vm/test/integration/addSprite.js b/local-scratch-vm/test/integration/addSprite.js new file mode 100644 index 0000000000000000000000000000000000000000..4e61522b6c5c55da7e142e94142a9a42acd2fbd4 --- /dev/null +++ b/local-scratch-vm/test/integration/addSprite.js @@ -0,0 +1,79 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; + +const VirtualMachine = require('../../src/virtual-machine'); +const RenderedTarget = require('../../src/sprites/rendered-target'); + +const projectUri = path.resolve(__dirname, '../fixtures/default.sb2'); +const project = readFileToBuffer(projectUri); + +const vm = new VirtualMachine(); + +test('spec', t => { + t.type(vm.addSprite, 'function'); + t.end(); +}); + +test('default cat', t => { + // Get default cat from .sprite2 + const uri = path.resolve(__dirname, '../fixtures/example_sprite.sprite2'); + const sprite = readFileToBuffer(uri); + + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + t.doesNotThrow(() => { + vm.loadProject(project).then(() => { + + t.equal(vm.runtime.targets.length, 2); // stage and default sprite + + // Add another sprite + vm.addSprite(sprite).then(() => { + const targets = vm.runtime.targets; + + // Test + t.type(targets, 'object'); + t.equal(targets.length, 3); + + const newTarget = targets[2]; + + t.ok(newTarget instanceof RenderedTarget); + t.type(newTarget.id, 'string'); + t.type(newTarget.blocks, 'object'); + t.type(newTarget.variables, 'object'); + const varIds = Object.keys(newTarget.variables); + t.type(varIds.length, 1); + const variable = newTarget.variables[varIds[0]]; + t.equal(variable.name, 'foo'); + t.equal(variable.value, 0); + + t.equal(newTarget.isOriginal, true); + t.equal(newTarget.currentCostume, 0); + t.equal(newTarget.isOriginal, true); + t.equal(newTarget.isStage, false); + t.equal(newTarget.sprite.name, 'Apple'); + + vm.greenFlag(); + + setTimeout(() => { + t.equal(variable.value, 10); + vm.getPlaygroundData(); + vm.stopAll(); + }, 1000); + }); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/block_to_workspace_comment_import.js b/local-scratch-vm/test/integration/block_to_workspace_comment_import.js new file mode 100644 index 0000000000000000000000000000000000000000..3eeb30aee8e36c87c201571c62f6214aee16dd1a --- /dev/null +++ b/local-scratch-vm/test/integration/block_to_workspace_comment_import.js @@ -0,0 +1,54 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/block-to-workspace-comments.sb2'); +const project = readFileToBuffer(projectUri); + +test('importing sb2 project where block comment is converted to workspace comment and block is deleted', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.equal(threads.length, 0); + + const target = vm.runtime.targets[1]; + + // Sprite 1 has 3 Comments, 1 block comment and 2 workspace comments (which were + // originally created via a block comment to workspace comment conversion in Scratch 2.0). + const targetComments = Object.values(target.comments); + t.equal(targetComments.length, 3); + const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null); + t.equal(spriteWorkspaceComments.length, 2); + + // Test the sprite block comments + const blockComments = targetComments.filter(comment => !!comment.blockId); + t.equal(blockComments.length, 1); + + // There should not be any comments where blockId is a number + const invalidComments = targetComments.filter(comment => typeof comment.blockId === 'number'); + t.equal(invalidComments.length, 0); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/block_to_workspace_comment_import_no_scripts.js b/local-scratch-vm/test/integration/block_to_workspace_comment_import_no_scripts.js new file mode 100644 index 0000000000000000000000000000000000000000..95ac376725051ce080661b9ae5439d931128e6b4 --- /dev/null +++ b/local-scratch-vm/test/integration/block_to_workspace_comment_import_no_scripts.js @@ -0,0 +1,59 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/block-to-workspace-comments-without-scripts.sb2'); +const project = readFileToBuffer(projectUri); + +/* eslint-disable-next-line max-len */ +test('importing sb2 project where block comment is converted to workspace comment and block is deleted, and there are no scripts on the workspace', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.equal(threads.length, 0); + + const target = vm.runtime.targets[1]; + + // Sprite 1 has 1 comments, a workspace comment which was + // originally created via a block comment to workspace comment conversion in Scratch 2.0. + // What differentiates this test from above is that there are no scripts in this project. + const targetComments = Object.values(target.comments); + t.equal(targetComments.length, 1); + const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null); + t.equal(spriteWorkspaceComments.length, 1); + + // Test the sprite block comments + const blockComments = targetComments.filter(comment => !!comment.blockId); + t.equal(blockComments.length, 0); + + // There should not be any comments where blockId is a number + const invalidComments = targetComments.filter(comment => typeof comment.blockId === 'number'); + t.equal(invalidComments.length, 0); + + const targetBlocks = Object.values(target.blocks._blocks); + t.equal(targetBlocks.length, 0); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/broadcast_special_chars_sb2.js b/local-scratch-vm/test/integration/broadcast_special_chars_sb2.js new file mode 100644 index 0000000000000000000000000000000000000000..eec5d6b5e08ba817b743c223e9111f51b4c4f09f --- /dev/null +++ b/local-scratch-vm/test/integration/broadcast_special_chars_sb2.js @@ -0,0 +1,83 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const Variable = require('../../src/engine/variable'); +const StringUtil = require('../../src/util/string-util'); +const VariableUtil = require('../../src/util/variable-util'); + +const projectUri = path.resolve(__dirname, '../fixtures/broadcast_special_chars.sb2'); +const project = readFileToBuffer(projectUri); + +test('importing sb2 project with special chars in message names', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.equal(threads.length, 0); + + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + const cat = vm.runtime.targets[1]; + + const allBroadcastFields = VariableUtil.getAllVarRefsForTargets(vm.runtime.targets, true); + + const abMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'a&b')[0]; + const abMessage = stage.variables[abMessageId]; + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the message ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(abMessageId), abMessageId); + + // Check that the message still has the correct info + t.equal(StringUtil.replaceUnsafeChars(abMessage.id), abMessage.id); + t.equal(abMessage.id, abMessageId); + t.equal(abMessage.type, Variable.BROADCAST_MESSAGE_TYPE); + t.equal(abMessage.value, 'a&b'); + + + const ltPerfectMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === '< perfect')[0]; + const ltPerfectMessage = stage.variables[ltPerfectMessageId]; + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the message ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(ltPerfectMessageId), ltPerfectMessageId); + + // Check that the message still has the correct info + t.equal(StringUtil.replaceUnsafeChars(ltPerfectMessage.id), ltPerfectMessage.id); + t.equal(ltPerfectMessage.id, ltPerfectMessageId); + t.equal(ltPerfectMessage.type, Variable.BROADCAST_MESSAGE_TYPE); + t.equal(ltPerfectMessage.value, '< perfect'); + + // Find all the references for these messages, and verify they have the correct ID + t.equal(allBroadcastFields[ltPerfectMessageId].length, 1); + t.equal(allBroadcastFields[abMessageId].length, 1); + const catBlocks = Object.keys(cat.blocks._blocks).map(blockId => cat.blocks._blocks[blockId]); + const catMessageBlocks = catBlocks.filter(block => block.fields.hasOwnProperty('BROADCAST_OPTION')); + t.equal(catMessageBlocks.length, 2); + t.equal(catMessageBlocks[0].fields.BROADCAST_OPTION.id, ltPerfectMessageId); + t.equal(catMessageBlocks[1].fields.BROADCAST_OPTION.id, abMessageId); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/broadcast_special_chars_sb3.js b/local-scratch-vm/test/integration/broadcast_special_chars_sb3.js new file mode 100644 index 0000000000000000000000000000000000000000..62e0770c731d9738d730d949ff468892e5461efa --- /dev/null +++ b/local-scratch-vm/test/integration/broadcast_special_chars_sb3.js @@ -0,0 +1,83 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const Variable = require('../../src/engine/variable'); +const StringUtil = require('../../src/util/string-util'); +const VariableUtil = require('../../src/util/variable-util'); + +const projectUri = path.resolve(__dirname, '../fixtures/broadcast_special_chars.sb3'); +const project = readFileToBuffer(projectUri); + +test('importing sb3 project with special chars in message names', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.equal(threads.length, 0); + + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + const cat = vm.runtime.targets[1]; + + const allBroadcastFields = VariableUtil.getAllVarRefsForTargets(vm.runtime.targets, true); + + const abMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'a&b')[0]; + const abMessage = stage.variables[abMessageId]; + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the message ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(abMessageId), abMessageId); + + // Check that the message still has the correct info + t.equal(StringUtil.replaceUnsafeChars(abMessage.id), abMessage.id); + t.equal(abMessage.id, abMessageId); + t.equal(abMessage.type, Variable.BROADCAST_MESSAGE_TYPE); + t.equal(abMessage.value, 'a&b'); + + + const ltPerfectMessageId = Object.keys(stage.variables).filter(k => stage.variables[k].name === '< perfect')[0]; + const ltPerfectMessage = stage.variables[ltPerfectMessageId]; + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the message ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(ltPerfectMessageId), ltPerfectMessageId); + + // Check that the message still has the correct info + t.equal(StringUtil.replaceUnsafeChars(ltPerfectMessage.id), ltPerfectMessage.id); + t.equal(ltPerfectMessage.id, ltPerfectMessageId); + t.equal(ltPerfectMessage.type, Variable.BROADCAST_MESSAGE_TYPE); + t.equal(ltPerfectMessage.value, '< perfect'); + + // Find all the references for these messages, and verify they have the correct ID + t.equal(allBroadcastFields[ltPerfectMessageId].length, 1); + t.equal(allBroadcastFields[abMessageId].length, 1); + const catBlocks = Object.keys(cat.blocks._blocks).map(blockId => cat.blocks._blocks[blockId]); + const catMessageBlocks = catBlocks.filter(block => block.fields.hasOwnProperty('BROADCAST_OPTION')); + t.equal(catMessageBlocks.length, 2); + t.equal(catMessageBlocks[0].fields.BROADCAST_OPTION.id, ltPerfectMessageId); + t.equal(catMessageBlocks[1].fields.BROADCAST_OPTION.id, abMessageId); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/clone-cleanup.js b/local-scratch-vm/test/integration/clone-cleanup.js new file mode 100644 index 0000000000000000000000000000000000000000..ea5f8483f8ace02618b39fa58f6c20ef2144b65f --- /dev/null +++ b/local-scratch-vm/test/integration/clone-cleanup.js @@ -0,0 +1,96 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2'); +const project = readFileToBuffer(projectUri); + +test('clone-cleanup', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + /** + * Track which step of the project is currently under test. + * @type {number} + */ + let testStep = -1; + + /** + * We test using setInterval; track the interval ID here so we can cancel it. + * @type {object} + */ + let testInterval = null; + + const verifyCounts = (expectedClones, extraThreads) => { + // stage plus one sprite, plus clones + t.strictEqual(vm.runtime.targets.length, 2 + expectedClones, + `target count at step ${testStep}`); + + // the stage should never have any clones + t.strictEqual(vm.runtime.targets[0].sprite.clones.length, 1, + `stage clone count at step ${testStep}`); + + // check sprite clone count (+1 for original) + t.strictEqual(vm.runtime.targets[1].sprite.clones.length, 1 + expectedClones, + `sprite clone count at step ${testStep}`); + + // thread count isn't directly tied to clone count since threads can end + t.strictEqual(vm.runtime.threads.length, extraThreads + (2 * expectedClones), + `thread count at step ${testStep}`); + }; + + const testNextStep = () => { + ++testStep; + switch (testStep) { + case 0: + // Project has started, main thread running, no clones yet + verifyCounts(0, 1); + break; + + case 1: + // 10 clones have been created, main thread still running + verifyCounts(10, 1); + break; + + case 2: + // The first batch of clones has deleted themselves; main thread still running + verifyCounts(0, 1); + break; + + case 3: + // The second batch of clones has been created and the main thread has ended + verifyCounts(10, 0); + break; + + case 4: + // The second batch of clones has deleted themselves; everything is finished + verifyCounts(0, 0); + + clearInterval(testInterval); + t.end(); + process.nextTick(process.exit); + break; + } + }; + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + + // Verify initial state: no clones, nothing running ("step -1") + verifyCounts(0, 0); + + vm.greenFlag(); + + // Every second, advance the testing step + testInterval = setInterval(testNextStep, 1000); + }); + }); + +}); diff --git a/local-scratch-vm/test/integration/cloud_variables_sb2.js b/local-scratch-vm/test/integration/cloud_variables_sb2.js new file mode 100644 index 0000000000000000000000000000000000000000..5da62a23aba8c72b1f388021663e2377dc88e601 --- /dev/null +++ b/local-scratch-vm/test/integration/cloud_variables_sb2.js @@ -0,0 +1,152 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const cloudVarSimpleUri = path.resolve(__dirname, '../fixtures/cloud_variables_simple.sb2'); +const cloudVarLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_limit.sb2'); +const cloudVarExceededLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_exceeded_limit.sb2'); +const cloudVarLocalUri = path.resolve(__dirname, '../fixtures/cloud_variables_local.sb2'); + +const cloudVarSimple = readFileToBuffer(cloudVarSimpleUri); +const cloudVarLimit = readFileToBuffer(cloudVarLimitUri); +const cloudVarExceededLimit = readFileToBuffer(cloudVarExceededLimitUri); +const cloudVarLocal = readFileToBuffer(cloudVarLocalUri); + +test('importing an sb2 project with cloud variables', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarSimple).then(() => { + t.equal(vm.runtime.hasCloudData(), true); + + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + t.equal(stageVars.length, 1); + + const variable = stageVars[0]; + t.equal(variable.name, '☁ firstCloud'); + t.equal(Number(variable.value), 100); // Though scratch 2 requires + // cloud variables to be numbers, this is something that happens + // when the message is being sent to the server rather than on the client + t.equal(variable.isCloud, true); + + t.end(); + }); +}); + +test('importing an sb2 project with cloud variables at the limit for a project', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarLimit).then(() => { + t.equal(vm.runtime.hasCloudData(), true); + + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + + t.equal(stageVars.length, 10); + // All of the 8 stage variables should be cloud variables + t.equal(stageVars.filter(v => v.isCloud).length, 10); + + t.end(); + }); +}); + +test('importing an sb2 project with cloud variables exceeding the limit for a project', t => { + // This tests a hacked project where additional cloud variables exceeding + // the project limit have been added. + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarExceededLimit).then(() => { + t.equal(vm.runtime.hasCloudData(), true); + + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + + t.equal(stageVars.length, 15); + // Only 8 of the variables should have the isCloud flag set to true + t.equal(stageVars.filter(v => v.isCloud).length, 10); + + t.end(); + }); +}); + +test('importing one project after the other resets cloud variable limit', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarExceededLimit).then(() => { + t.equal(vm.runtime.canAddCloudVariable(), false); + + vm.loadProject(cloudVarSimple).then(() => { + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + t.equal(stageVars.length, 1); + + const variable = stageVars[0]; + t.equal(variable.name, '☁ firstCloud'); + t.equal(Number(variable.value), 100); // Though scratch 2 requires + // cloud variables to be numbers, this is something that happens + // when the message is being sent to the server rather than on the client + t.equal(variable.isCloud, true); + + t.equal(vm.runtime.canAddCloudVariable(), true); + + t.end(); + }); + }); +}); + +test('local cloud variables get imported as regular variables', t => { + // This tests a hacked project where a sprite-local variable is + // has the cloud variable flag set. + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarLocal).then(() => { + t.equal(vm.runtime.hasCloudData(), false); + + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + + t.equal(stageVars.length, 0); + + const sprite = vm.runtime.targets[1]; + const spriteVars = Object.values(sprite.variables); + + t.equal(spriteVars.length, 1); + t.equal(spriteVars[0].isCloud, false); + + t.end(); + + process.nextTick(process.exit); // This is needed because this is the end of the last test in this file!!! + }); +}); diff --git a/local-scratch-vm/test/integration/cloud_variables_sb3.js b/local-scratch-vm/test/integration/cloud_variables_sb3.js new file mode 100644 index 0000000000000000000000000000000000000000..ea177b23aedf965b07e2687d058399b2c6cc689b --- /dev/null +++ b/local-scratch-vm/test/integration/cloud_variables_sb3.js @@ -0,0 +1,148 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const cloudVarSimpleUri = path.resolve(__dirname, '../fixtures/cloud_variables_simple.sb3'); +const cloudVarLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_limit.sb3'); +const cloudVarExceededLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_exceeded_limit.sb3'); +const cloudVarLocalUri = path.resolve(__dirname, '../fixtures/cloud_variables_local.sb3'); + +const cloudVarSimple = readFileToBuffer(cloudVarSimpleUri); +const cloudVarLimit = readFileToBuffer(cloudVarLimitUri); +const cloudVarExceededLimit = readFileToBuffer(cloudVarExceededLimitUri); +const cloudVarLocal = readFileToBuffer(cloudVarLocalUri); + +test('importing an sb3 project with cloud variables', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarSimple).then(() => { + t.equal(vm.runtime.hasCloudData(), true); + + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + t.equal(stageVars.length, 1); + + const variable = stageVars[0]; + t.equal(variable.name, '☁ firstCloud'); + t.equal(Number(variable.value), 100); + t.equal(variable.isCloud, true); + + t.end(); + }); +}); + +test('importing an sb3 project with cloud variables at the limit for a project', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarLimit).then(() => { + t.equal(vm.runtime.hasCloudData(), true); + + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + + t.equal(stageVars.length, 10); + // All of the 10 stage variables should be cloud variables + t.equal(stageVars.filter(v => v.isCloud).length, 10); + + t.end(); + }); +}); + +test('importing an sb3 project with cloud variables exceeding the limit for a project', t => { + // This tests a hacked project where additional cloud variables exceeding + // the project limit have been added. + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarExceededLimit).then(() => { + t.equal(vm.runtime.hasCloudData(), true); + + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + + t.equal(stageVars.length, 15); + // Only 8 of the variables should have the isCloud flag set to true + t.equal(stageVars.filter(v => v.isCloud).length, 10); + + t.end(); + }); +}); + +test('importing one project after the other resets cloud variable limit', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarExceededLimit).then(() => { + t.equal(vm.runtime.canAddCloudVariable(), false); + + vm.loadProject(cloudVarSimple).then(() => { + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + t.equal(stageVars.length, 1); + + const variable = stageVars[0]; + t.equal(variable.name, '☁ firstCloud'); + t.equal(Number(variable.value), 100); + t.equal(variable.isCloud, true); + + t.equal(vm.runtime.canAddCloudVariable(), true); + + t.end(); + }); + }); +}); + +test('local cloud variables get imported as regular variables', t => { + // This tests a hacked project where a sprite-local variable is + // has the cloud variable flag set. + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(cloudVarLocal).then(() => { + t.equal(vm.runtime.hasCloudData(), false); + + const stage = vm.runtime.targets[0]; + const stageVars = Object.values(stage.variables); + + t.equal(stageVars.length, 0); + + const sprite = vm.runtime.targets[1]; + const spriteVars = Object.values(sprite.variables); + + t.equal(spriteVars.length, 1); + t.equal(spriteVars[0].isCloud, false); + + t.end(); + + process.nextTick(process.exit); // This is needed because this is the end of the last test in this file!!! + }); +}); diff --git a/local-scratch-vm/test/integration/comments.js b/local-scratch-vm/test/integration/comments.js new file mode 100644 index 0000000000000000000000000000000000000000..5962b5eb6507954c0cff377503ffc9e413b11c19 --- /dev/null +++ b/local-scratch-vm/test/integration/comments.js @@ -0,0 +1,91 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/comments.sb2'); +const project = readFileToBuffer(projectUri); + +test('importing sb2 project with comments', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.equal(threads.length, 0); + + const stage = vm.runtime.targets[0]; + const target = vm.runtime.targets[1]; + + const stageComments = Object.values(stage.comments); + + // Stage has 1 comment, and it is minimized. + t.equal(stageComments.length, 1); + t.equal(stageComments[0].minimized, true); + t.equal(stageComments[0].text, 'A minimized stage comment.'); + // The stage comment is a workspace comment + t.equal(stageComments[0].blockId, null); + + // Sprite 1 has 6 Comments, 1 workspace comment, and 5 block comments + const targetComments = Object.values(target.comments); + t.equal(targetComments.length, 6); + const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null); + t.equal(spriteWorkspaceComments.length, 1); + t.equal(spriteWorkspaceComments[0].minimized, false); + t.equal(spriteWorkspaceComments[0].text, 'This is a workspace comment.'); + + // Test the sprite block comments + const blockComments = targetComments.filter(comment => !!comment.blockId); + t.equal(blockComments.length, 5); + + t.equal(blockComments[0].minimized, true); + t.equal(blockComments[0].text, '1. Green Flag Comment.'); + const greenFlagBlock = target.blocks.getBlock(blockComments[0].blockId); + t.equal(greenFlagBlock.comment, blockComments[0].id); + t.equal(greenFlagBlock.opcode, 'event_whenflagclicked'); + + t.equal(blockComments[1].minimized, true); + t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.'); + const turnRightBlock = target.blocks.getBlock(blockComments[1].blockId); + t.equal(turnRightBlock.comment, blockComments[1].id); + t.equal(turnRightBlock.opcode, 'motion_turnright'); + + t.equal(blockComments[2].minimized, false); + t.equal(blockComments[2].text, '3. Comment for a loop.'); + const repeatBlock = target.blocks.getBlock(blockComments[2].blockId); + t.equal(repeatBlock.comment, blockComments[2].id); + t.equal(repeatBlock.opcode, 'control_repeat'); + + t.equal(blockComments[3].minimized, false); + t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.'); + const changeColorBlock = target.blocks.getBlock(blockComments[3].blockId); + t.equal(changeColorBlock.comment, blockComments[3].id); + t.equal(changeColorBlock.opcode, 'looks_changeeffectby'); + + t.equal(blockComments[4].minimized, false); + t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.'); + const stopAllBlock = target.blocks.getBlock(blockComments[4].blockId); + t.equal(stopAllBlock.comment, blockComments[4].id); + t.equal(stopAllBlock.opcode, 'control_stop'); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/comments_sb3.js b/local-scratch-vm/test/integration/comments_sb3.js new file mode 100644 index 0000000000000000000000000000000000000000..fd3766622e6597eae33289478a58664f46b05b30 --- /dev/null +++ b/local-scratch-vm/test/integration/comments_sb3.js @@ -0,0 +1,91 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/comments.sb3'); +const project = readFileToBuffer(projectUri); + +test('load an sb3 project with comments', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.equal(threads.length, 0); + + const stage = vm.runtime.targets[0]; + const target = vm.runtime.targets[1]; + + const stageComments = Object.values(stage.comments); + + // Stage has 1 comment, and it is minimized. + t.equal(stageComments.length, 1); + t.equal(stageComments[0].minimized, true); + t.equal(stageComments[0].text, 'A minimized stage comment.'); + // The stage comment is a workspace comment + t.equal(stageComments[0].blockId, null); + + // Sprite 1 has 6 Comments, 1 workspace comment, and 5 block comments + const targetComments = Object.values(target.comments); + t.equal(targetComments.length, 6); + const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null); + t.equal(spriteWorkspaceComments.length, 1); + t.equal(spriteWorkspaceComments[0].minimized, false); + t.equal(spriteWorkspaceComments[0].text, 'This is a workspace comment.'); + + // Test the sprite block comments + const blockComments = targetComments.filter(comment => !!comment.blockId); + t.equal(blockComments.length, 5); + + t.equal(blockComments[0].minimized, true); + t.equal(blockComments[0].text, '1. Green Flag Comment.'); + const greenFlagBlock = target.blocks.getBlock(blockComments[0].blockId); + t.equal(greenFlagBlock.comment, blockComments[0].id); + t.equal(greenFlagBlock.opcode, 'event_whenflagclicked'); + + t.equal(blockComments[1].minimized, true); + t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.'); + const turnRightBlock = target.blocks.getBlock(blockComments[1].blockId); + t.equal(turnRightBlock.comment, blockComments[1].id); + t.equal(turnRightBlock.opcode, 'motion_turnright'); + + t.equal(blockComments[2].minimized, false); + t.equal(blockComments[2].text, '3. Comment for a loop.'); + const repeatBlock = target.blocks.getBlock(blockComments[2].blockId); + t.equal(repeatBlock.comment, blockComments[2].id); + t.equal(repeatBlock.opcode, 'control_repeat'); + + t.equal(blockComments[3].minimized, false); + t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.'); + const changeColorBlock = target.blocks.getBlock(blockComments[3].blockId); + t.equal(changeColorBlock.comment, blockComments[3].id); + t.equal(changeColorBlock.opcode, 'looks_changeeffectby'); + + t.equal(blockComments[4].minimized, false); + t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.'); + const stopAllBlock = target.blocks.getBlock(blockComments[4].blockId); + t.equal(stopAllBlock.comment, blockComments[4].id); + t.equal(stopAllBlock.opcode, 'control_stop'); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/complex.js b/local-scratch-vm/test/integration/complex.js new file mode 100644 index 0000000000000000000000000000000000000000..bce19ca675c2be328f9262491c29c2acdaa1c093 --- /dev/null +++ b/local-scratch-vm/test/integration/complex.js @@ -0,0 +1,99 @@ +const fs = require('fs'); +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/complex.sb2'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/sprite.json'); +const sprite = fs.readFileSync(spriteUri, 'utf8'); + +test('complex', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + // Manipulate each target + vm.on('targetsUpdate', data => { + const targets = data.targetList; + for (const i in targets) { + if (targets[i].isStage === true) continue; + if (targets[i].name.match(/test/)) continue; + + vm.setEditingTarget(targets[i].id); + vm.renameSprite(targets[i].id, 'test'); + vm.postSpriteInfo({ + x: 0, + y: 10, + direction: 90, + draggable: true, + rotationStyle: 'all around', + visible: true + }); + vm.addCostume( + 'f9a1c175dbe2e5dee472858dd30d16bb.svg', + { + name: 'costume1', + baseLayerID: 0, + baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg', + bitmapResolution: 1, + rotationCenterX: 47, + rotationCenterY: 55 + } + ); + } + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // Post IO data + vm.postIOData('mouse', { + isDown: true, + x: 0, + y: 10, + canvasWidth: 100, + canvasHeight: 100 + }); + + // Add sprite + vm.addSprite(sprite); + + // Add backdrop + vm.addBackdrop( + '6b3d87ba2a7f89be703163b6c1d4c964.png', + { + name: 'baseball-field', + baseLayerID: 26, + baseLayerMD5: '6b3d87ba2a7f89be703163b6c1d4c964.png', + bitmapResolution: 2, + rotationCenterX: 480, + rotationCenterY: 360 + } + ); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); + +}); diff --git a/local-scratch-vm/test/integration/control.js b/local-scratch-vm/test/integration/control.js new file mode 100644 index 0000000000000000000000000000000000000000..eb3108365a0119e16e3caf7534fbec7b2d814eed --- /dev/null +++ b/local-scratch-vm/test/integration/control.js @@ -0,0 +1,38 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/control.sb2'); +const project = readFileToBuffer(uri); + +test('control', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length > 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + }); + }); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); +}); diff --git a/local-scratch-vm/test/integration/data.js b/local-scratch-vm/test/integration/data.js new file mode 100644 index 0000000000000000000000000000000000000000..297ea5de0a4b522044746bb4894b63d822346f0f --- /dev/null +++ b/local-scratch-vm/test/integration/data.js @@ -0,0 +1,37 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/data.sb2'); +const project = readFileToBuffer(uri); + +test('data', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', () => { + // @todo Additional tests + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/delete-and-restore-sprite.js b/local-scratch-vm/test/integration/delete-and-restore-sprite.js new file mode 100644 index 0000000000000000000000000000000000000000..9ed1c781c26c7e19f08100d250fbba3f36f6cc2a --- /dev/null +++ b/local-scratch-vm/test/integration/delete-and-restore-sprite.js @@ -0,0 +1,65 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; + +const VirtualMachine = require('../../src/virtual-machine'); +// const RenderedTarget = require('../../src/sprites/rendered-target'); + +const projectUri = path.resolve(__dirname, '../fixtures/default.sb2'); +const project = readFileToBuffer(projectUri); + +const vm = new VirtualMachine(); + +test('spec', t => { + t.type(vm.deleteSprite, 'function'); + t.end(); +}); + +test('default cat', t => { + // Get default cat from .sprite2 + // const uri = path.resolve(__dirname, '../fixtures/example_sprite.sprite2'); + // const sprite = readFileToBuffer(uri); + + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + t.doesNotThrow(() => { + vm.loadProject(project).then(() => { + + t.equal(vm.runtime.targets.length, 2); // stage and default sprite + + const defaultSprite = vm.runtime.targets[1]; + + // Delete the sprite + const addSpriteBack = vm.deleteSprite(vm.runtime.targets[1].id); + + t.equal(vm.runtime.targets.length, 1); + + t.type(addSpriteBack, 'function'); + + addSpriteBack().then(() => { + t.equal(vm.runtime.targets.length, 2); + t.equal(vm.runtime.targets[1].getName(), defaultSprite.getName()); + + vm.greenFlag(); + + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 1000); + }); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/event.js b/local-scratch-vm/test/integration/event.js new file mode 100644 index 0000000000000000000000000000000000000000..d64e30003a8e16ba7df944df089b190bbb6d1d1c --- /dev/null +++ b/local-scratch-vm/test/integration/event.js @@ -0,0 +1,38 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/event.sb2'); +const project = readFileToBuffer(uri); + +test('event', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length > 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/execute.js b/local-scratch-vm/test/integration/execute.js new file mode 100644 index 0000000000000000000000000000000000000000..d35aa1f399fef5fbbed0dadc74c192190e9cc5d6 --- /dev/null +++ b/local-scratch-vm/test/integration/execute.js @@ -0,0 +1,158 @@ +const fs = require('fs'); +const path = require('path'); + +const test = require('tap').test; + +const log = require('../../src/util/log'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +/** + * @fileoverview Transform each sb2 in fixtures/execute into a test. + * + * Test execution of a group of scratch blocks by SAYing if a test did "pass", + * or did "fail". Four keywords can be set at the beginning of a SAY messaage + * to indicate a test primitive. + * + * - "pass MESSAGE" will t.pass(MESSAGE). + * - "fail MESSAGE" will t.fail(MESSAGE). + * - "plan NUMBER_OF_TESTS" will t.plan(Number(NUMBER_OF_TESTS)). + * - "end" will t.end(). + * + * A good strategy to follow is to SAY "plan NUMBER_OF_TESTS" first. Then + * "pass" and "fail" depending on expected scratch results in conditions, event + * scripts, or what is best for testing the target block or group of blocks. + * When its done you must SAY "end" so the test and tap know that the end has + * been reached. + */ + +const whenThreadsComplete = (t, vm, timeLimit = 2000) => ( + // When the number of threads reaches 0 the test is expected to be complete. + new Promise((resolve, reject) => { + const intervalId = setInterval(() => { + let active = 0; + const threads = vm.runtime.threads; + for (let i = 0; i < threads.length; i++) { + if (!threads[i].updateMonitor) { + active += 1; + } + } + if (active === 0) { + resolve(); + } + }, 50); + + const timeoutId = setTimeout(() => { + reject(new Error('time limit reached')); + }, timeLimit); + + // Clear the interval to allow the process to exit + // naturally. + t.tearDown(() => { + clearInterval(intervalId); + clearTimeout(timeoutId); + }); + }) +); + +const executeDir = path.resolve(__dirname, '../fixtures/execute'); + +fs.readdirSync(executeDir) + .filter(uri => uri.endsWith('.sb2') || uri.endsWith('.sb3')) + .forEach(uri => { + const run = (t, enableCompiler) => { + // Disable logging during this test. + log.suggest.deny('vm', 'error'); + t.tearDown(() => log.suggest.clear()); + + // Map string messages to tap reporting methods. This will be used + // with events from scratch's runtime emitted on block instructions. + let didPlan; + let didEnd; + const reporters = { + comment (message) { + t.comment(message); + }, + pass (reason) { + t.pass(reason); + }, + fail (reason) { + t.fail(reason); + }, + plan (count) { + didPlan = true; + t.plan(Number(count)); + }, + end () { + didEnd = true; + t.end(); + } + }; + const reportVmResult = text => { + const command = text.split(/\s+/, 1)[0].toLowerCase(); + if (reporters[command]) { + return reporters[command](text.substring(command.length).trim()); + } + + // Default to a comment with the full text if we didn't match + // any command prefix + return reporters.comment(text); + }; + + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start the VM and initialize some vm properties. + // complete. + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.setCompilerOptions({enabled: enableCompiler}); + + // TW: Script compilation errors should fail. + if (enableCompiler) { + vm.on('COMPILE_ERROR', (target, error) => { + // Edge-activated hats are a known error. + if (!`${error}`.includes('edge-activated hat')) { + throw new Error(`Could not compile script in ${target.getName()}: ${error}`); + } + }); + } + + // Stop the runtime interval once the test is complete so the test + // process may naturally exit. + t.tearDown(() => { + vm.stop(); + }); + + // Report the text of SAY events as testing instructions. + vm.runtime.on('SAY', (target, type, text) => reportVmResult(text)); + + const project = readFileToBuffer(path.resolve(executeDir, uri)); + + // Load the project and once all threads are complete ensure that + // the scratch project sent us a "end" message. + return vm.loadProject(project) + .then(() => vm.greenFlag()) + .then(() => whenThreadsComplete(t, vm)) + .then(() => { + // Setting a plan is not required but is a good idea. + if (!didPlan) { + t.comment('did not say "plan NUMBER_OF_TESTS"'); + } + + // End must be called so that tap knows the test is done. If + // the test has an SAY "end" block but that block did not + // execute, this explicit failure will raise that issue so + // it can be resolved. + if (!didEnd) { + t.fail('did not say "end"'); + t.end(); + } + }); + }; + test(`${uri} (interpreted)`, t => run(t, false)); + test(`${uri} (compiled)`, t => run(t, true)); + }); diff --git a/local-scratch-vm/test/integration/hat-execution-order.js b/local-scratch-vm/test/integration/hat-execution-order.js new file mode 100644 index 0000000000000000000000000000000000000000..78f2bef27a46b0a0bca179de94da986839c61806 --- /dev/null +++ b/local-scratch-vm/test/integration/hat-execution-order.js @@ -0,0 +1,43 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/hat-execution-order.sb2'); +const project = readFileToBuffer(projectUri); + +test('complex', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + + const resultKey = Object.keys(vm.runtime.targets[0].variables)[0]; + const results = vm.runtime.targets[0].variables[resultKey].value; + t.deepEqual(results, ['3', '2', '1', 'stage']); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/hat-threads-run-every-frame.js b/local-scratch-vm/test/integration/hat-threads-run-every-frame.js new file mode 100644 index 0000000000000000000000000000000000000000..9eaf9771f8a1b9b6f78e263a0627c0005da9c5c5 --- /dev/null +++ b/local-scratch-vm/test/integration/hat-threads-run-every-frame.js @@ -0,0 +1,356 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const Thread = require('../../src/engine/thread'); +const Runtime = require('../../src/engine/runtime'); +const execute = require('../../src/engine/execute.js'); + +const projectUri = path.resolve(__dirname, '../fixtures/timer-greater-than-hat.sb2'); +const project = readFileToBuffer(projectUri); + +const checkIsHatThread = (t, vm, hatThread) => { + t.equal(hatThread.stackClick, false); + t.equal(hatThread.updateMonitor, false); + const blockContainer = hatThread.target.blocks; + const opcode = blockContainer.getOpcode(blockContainer.getBlock(hatThread.topBlock)); + t.assert(vm.runtime.getIsEdgeActivatedHat(opcode)); +}; + +const checkIsStackClickThread = (t, vm, stackClickThread) => { + t.equal(stackClickThread.stackClick, true); + t.equal(stackClickThread.updateMonitor, false); +}; + +/** + * timer-greater-than-hat.sb2 contains a single stack + * when timer > -1 + * change color effect by 25 + * The intention is to make sure that the hat block condition is evaluated + * on each frame. + */ +test('edge activated hat thread runs once every frame', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(project).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + let threads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(threads.length, 1); + checkIsHatThread(t, vm, threads[0]); + t.assert(threads[0].status === Thread.STATUS_DONE); + + // Check that the hat thread is added again when another step is taken + vm.runtime._step(); + threads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(threads.length, 1); + checkIsHatThread(t, vm, threads[0]); + t.assert(threads[0].status === Thread.STATUS_DONE); + t.end(); + }); + }); +}); + +/** + * When a hat is added it should run in the next frame. Any block related + * caching should be reset. + */ +test('edge activated hat thread runs after being added to previously executed target', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(project).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + let threads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(threads.length, 1); + checkIsHatThread(t, vm, threads[0]); + t.assert(threads[0].status === Thread.STATUS_DONE); + + // Add a second hat that should create a second thread + const hatBlock = threads[0].target.blocks.getBlock(threads[0].topBlock); + threads[0].target.blocks.createBlock(Object.assign( + {}, hatBlock, {id: 'hatblock2', next: null} + )); + + // Check that the hat thread is added again when another step is taken + vm.runtime._step(); + threads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(threads.length, 2); + checkIsHatThread(t, vm, threads[0]); + checkIsHatThread(t, vm, threads[1]); + t.assert(threads[0].status === Thread.STATUS_DONE); + t.assert(threads[1].status === Thread.STATUS_DONE); + t.end(); + }); + }); +}); + +/** + * If the hat doesn't finish evaluating within one frame, it shouldn't be added again + * on the next frame. (We skip execution by setting the step time to 0) + */ +test('edge activated hat thread not added twice', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = 0; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(project).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + let doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 1); + t.equal(doneThreads.length, 0); + const prevThread = vm.runtime.threads[0]; + checkIsHatThread(t, vm, vm.runtime.threads[0]); + t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING); + + // Check that no new threads are added when another step is taken + vm.runtime._step(); + doneThreads = vm.runtime._lastStepDoneThreads; + // There should now be one done hat thread and one new hat thread to run + t.equal(vm.runtime.threads.length, 1); + t.equal(doneThreads.length, 0); + checkIsHatThread(t, vm, vm.runtime.threads[0]); + t.assert(vm.runtime.threads[0] === prevThread); + t.end(); + }); + }); +}); + + +/** + * Duplicating a sprite should also track duplicated edge activated hat in + * runtime's _edgeActivatedHatValues map. + */ +test('edge activated hat should trigger for both sprites when sprite is duplicated', t => { + + // Project that is similar to timer-greater-than-hat.sb2, but has code on the sprite so that + // the sprite can be duplicated + const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3'); + const projectWithSprite = readFileToBuffer(projectWithSpriteUri); + + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = 0; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(projectWithSprite).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + t.equal(vm.runtime.threads.length, 1); + checkIsHatThread(t, vm, vm.runtime.threads[0]); + t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING); + let numTargetEdgeHats = vm.runtime.targets.reduce((val, target) => + val + Object.keys(target._edgeActivatedHatValues).length, 0); + t.equal(numTargetEdgeHats, 1); + + vm.duplicateSprite(vm.runtime.targets[1].id).then(() => { + vm.runtime._step(); + // Check that the runtime's _edgeActivatedHatValues object has two separate keys + // after execute is run on each thread + numTargetEdgeHats = vm.runtime.targets.reduce((val, target) => + val + Object.keys(target._edgeActivatedHatValues).length, 0); + t.equal(numTargetEdgeHats, 2); + t.end(); + }); + + }); + }); +}); + +/** + * Cloning a sprite should also track cloned edge activated hat separately + * runtime's _edgeActivatedHatValues map. + */ +test('edge activated hat should trigger for both sprites when sprite is cloned', t => { + + // Project that is similar to loudness-hat-block.sb2, but has code on the sprite so that + // the sprite can be duplicated + const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3'); + const projectWithSprite = readFileToBuffer(projectWithSpriteUri); + + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = 0; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(projectWithSprite).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + t.equal(vm.runtime.threads.length, 1); + checkIsHatThread(t, vm, vm.runtime.threads[0]); + t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING); + // Run execute on the thread to populate the runtime's + // _edgeActivatedHatValues object + execute(vm.runtime.sequencer, vm.runtime.threads[0]); + let numTargetEdgeHats = vm.runtime.targets.reduce((val, target) => + val + Object.keys(target._edgeActivatedHatValues).length, 0); + t.equal(numTargetEdgeHats, 1); + + const cloneTarget = vm.runtime.targets[1].makeClone(); + vm.runtime.addTarget(cloneTarget); + + vm.runtime._step(); + // Check that the runtime's _edgeActivatedHatValues object has two separate keys + // after execute is run on each thread + vm.runtime.threads.forEach(thread => execute(vm.runtime.sequencer, thread)); + numTargetEdgeHats = vm.runtime.targets.reduce((val, target) => + val + Object.keys(target._edgeActivatedHatValues).length, 0); + t.equal(numTargetEdgeHats, 2); + t.end(); + }); + }); +}); + +/** + * When adding a stack click thread first, make sure that the edge activated hat thread and + * the stack click thread are both pushed and run (despite having the same top block) + */ +test('edge activated hat thread does not interrupt stack click thread', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(project).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + let doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(doneThreads.length, 1); + checkIsHatThread(t, vm, doneThreads[0]); + t.assert(doneThreads[0].status === Thread.STATUS_DONE); + + // Add stack click thread on this hat + vm.runtime.toggleScript(doneThreads[0].topBlock, {stackClick: true}); + + // Check that the hat thread is added again when another step is taken + vm.runtime._step(); + doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(doneThreads.length, 2); + let hatThread; + let stackClickThread; + if (doneThreads[0].stackClick) { + stackClickThread = doneThreads[0]; + hatThread = doneThreads[1]; + } else { + stackClickThread = doneThreads[1]; + hatThread = doneThreads[0]; + } + checkIsHatThread(t, vm, hatThread); + checkIsStackClickThread(t, vm, stackClickThread); + t.assert(doneThreads[0].status === Thread.STATUS_DONE); + t.assert(doneThreads[1].status === Thread.STATUS_DONE); + t.end(); + }); + }); +}); + +/** + * When adding the hat thread first, make sure that the edge activated hat thread and + * the stack click thread are both pushed and run (despite having the same top block) + */ +test('edge activated hat thread does not interrupt stack click thread', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = 0; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(project).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + let doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 1); + t.equal(doneThreads.length, 0); + checkIsHatThread(t, vm, vm.runtime.threads[0]); + t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING); + + vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL; + + // Add stack click thread on this hat + vm.runtime.toggleScript(vm.runtime.threads[0].topBlock, {stackClick: true}); + + // Check that the hat thread is added again when another step is taken + vm.runtime._step(); + doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(doneThreads.length, 2); + let hatThread; + let stackClickThread; + if (doneThreads[0].stackClick) { + stackClickThread = doneThreads[0]; + hatThread = doneThreads[1]; + } else { + stackClickThread = doneThreads[1]; + hatThread = doneThreads[0]; + } + checkIsHatThread(t, vm, hatThread); + checkIsStackClickThread(t, vm, stackClickThread); + t.assert(doneThreads[0].status === Thread.STATUS_DONE); + t.assert(doneThreads[1].status === Thread.STATUS_DONE); + t.end(); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/import-sb.js b/local-scratch-vm/test/integration/import-sb.js new file mode 100644 index 0000000000000000000000000000000000000000..3e96051ce8a480086a32e437eb5747a678405896 --- /dev/null +++ b/local-scratch-vm/test/integration/import-sb.js @@ -0,0 +1,45 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/single_sound.sb'); +const project = readFileToBuffer(uri); + +test('default', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + const stageSounds = vm.runtime.targets[0].sprite.sounds; + const firstSound = stageSounds[0]; + + // Check that the sound has the correct md5 + // This md5 was obtained from the asset server + t.equal(firstSound.md5, 'edb9713dedbe9a2e05c09e0540182ef1.wav'); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/import-sb2-from-object.js b/local-scratch-vm/test/integration/import-sb2-from-object.js new file mode 100644 index 0000000000000000000000000000000000000000..43397d09233ed22e326855206fcd4cf2e2c84562 --- /dev/null +++ b/local-scratch-vm/test/integration/import-sb2-from-object.js @@ -0,0 +1,38 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/default.sb2'); +const project = extractProjectJson(uri); + +test('default', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/import_nested_sb2.js b/local-scratch-vm/test/integration/import_nested_sb2.js new file mode 100644 index 0000000000000000000000000000000000000000..38104669f9bfca3b672da5237e4b271ea704086c --- /dev/null +++ b/local-scratch-vm/test/integration/import_nested_sb2.js @@ -0,0 +1,52 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson; + +const renderedTarget = require('../../src/sprites/rendered-target'); +const runtime = require('../../src/engine/runtime'); +const sb2 = require('../../src/serialization/sb2'); + +test('spec', t => { + t.type(sb2.deserialize, 'function'); + t.end(); +}); + +test('nested default/*', t => { + // Get SB2 JSON (string) + const uri = path.resolve(__dirname, '../fixtures/default_nested.sb2'); + const json = extractProjectJson(uri, 'default'); + + // Create runtime instance & load SB2 into it + const rt = new runtime(); + rt.attachStorage(makeTestStorage()); + sb2.deserialize(json, rt).then(({targets}) => { + // Test + t.type(json, 'object'); + t.type(rt, 'object'); + t.type(targets, 'object'); + + t.ok(targets[0] instanceof renderedTarget); + t.type(targets[0].id, 'string'); + t.type(targets[0].blocks, 'object'); + t.type(targets[0].variables, 'object'); + t.type(targets[0].comments, 'object'); + + t.equal(targets[0].isOriginal, true); + t.equal(targets[0].currentCostume, 0); + t.equal(targets[0].isOriginal, true); + t.equal(targets[0].isStage, true); + + t.ok(targets[1] instanceof renderedTarget); + t.type(targets[1].id, 'string'); + t.type(targets[1].blocks, 'object'); + t.type(targets[1].variables, 'object'); + t.type(targets[1].comments, 'object'); + + t.equal(targets[1].isOriginal, true); + t.equal(targets[1].currentCostume, 0); + t.equal(targets[1].isOriginal, true); + t.equal(targets[1].isStage, false); + t.end(); + }); +}); diff --git a/local-scratch-vm/test/integration/import_sb2.js b/local-scratch-vm/test/integration/import_sb2.js new file mode 100644 index 0000000000000000000000000000000000000000..fb6fe8508f6b849678073a6da49f5d9267bff7c5 --- /dev/null +++ b/local-scratch-vm/test/integration/import_sb2.js @@ -0,0 +1,52 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson; + +const renderedTarget = require('../../src/sprites/rendered-target'); +const runtime = require('../../src/engine/runtime'); +const sb2 = require('../../src/serialization/sb2'); + +test('spec', t => { + t.type(sb2.deserialize, 'function'); + t.end(); +}); + +test('default', t => { + // Get SB2 JSON (string) + const uri = path.resolve(__dirname, '../fixtures/default.sb2'); + const json = extractProjectJson(uri); + + // Create runtime instance & load SB2 into it + const rt = new runtime(); + rt.attachStorage(makeTestStorage()); + sb2.deserialize(json, rt).then(({targets}) => { + // Test + t.type(json, 'object'); + t.type(rt, 'object'); + t.type(targets, 'object'); + + t.ok(targets[0] instanceof renderedTarget); + t.type(targets[0].id, 'string'); + t.type(targets[0].blocks, 'object'); + t.type(targets[0].variables, 'object'); + t.type(targets[0].comments, 'object'); + + t.equal(targets[0].isOriginal, true); + t.equal(targets[0].currentCostume, 0); + t.equal(targets[0].isOriginal, true); + t.equal(targets[0].isStage, true); + + t.ok(targets[1] instanceof renderedTarget); + t.type(targets[1].id, 'string'); + t.type(targets[1].blocks, 'object'); + t.type(targets[1].variables, 'object'); + t.type(targets[1].comments, 'object'); + + t.equal(targets[1].isOriginal, true); + t.equal(targets[1].currentCostume, 0); + t.equal(targets[1].isOriginal, true); + t.equal(targets[1].isStage, false); + t.end(); + }); +}); diff --git a/local-scratch-vm/test/integration/internal-extension.js b/local-scratch-vm/test/integration/internal-extension.js new file mode 100644 index 0000000000000000000000000000000000000000..51540e180aec385823a11182b67d4ef7ea476b4e --- /dev/null +++ b/local-scratch-vm/test/integration/internal-extension.js @@ -0,0 +1,127 @@ +const test = require('tap').test; +const Worker = require('tiny-worker'); + +const BlockType = require('../../src/extension-support/block-type'); + +const dispatch = require('../../src/dispatch/central-dispatch'); +const VirtualMachine = require('../../src/virtual-machine'); + +const Sprite = require('../../src/sprites/sprite'); +const RenderedTarget = require('../../src/sprites/rendered-target'); + +// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead. +dispatch.workerClass = Worker; + +class TestInternalExtension { + constructor () { + this.status = {}; + this.status.constructorCalled = true; + } + + getInfo () { + this.status.getInfoCalled = true; + return { + id: 'testInternalExtension', + name: 'Test Internal Extension', + blocks: [ + { + opcode: 'go' + } + ], + menus: { + simpleMenu: this._buildAMenu(), + dynamicMenu: '_buildDynamicMenu' + } + }; + } + + go (args, util, blockInfo) { + this.status.goCalled = true; + return blockInfo; + } + + _buildAMenu () { + this.status.buildMenuCalled = true; + return ['abcd', 'efgh', 'ijkl']; + } + + _buildDynamicMenu () { + this.status.buildDynamicMenuCalled = true; + return [1, 2, 3, 4, 6]; + } +} + +test('internal extension', t => { + const vm = new VirtualMachine(); + + const extension = new TestInternalExtension(); + t.ok(extension.status.constructorCalled); + + t.notOk(extension.status.getInfoCalled); + vm.extensionManager._registerInternalExtension(extension); + t.ok(extension.status.getInfoCalled); + + const func = vm.runtime.getOpcodeFunction('testInternalExtension_go'); + t.type(func, 'function'); + + t.notOk(extension.status.goCalled); + const goBlockInfo = func(); + t.ok(extension.status.goCalled); + + // The 'go' block returns its own blockInfo. Make sure it matches the expected info. + // Note that the extension parser fills in missing fields so there are more fields here than in `getInfo`. + const expectedBlockInfo = { + arguments: {}, + blockAllThreads: false, + blockType: BlockType.COMMAND, + func: goBlockInfo.func, // Cheat since we don't have a good way to ensure we generate the same function + opcode: 'go', + terminal: false, + text: 'go' + }; + t.deepEqual(goBlockInfo, expectedBlockInfo); + + // There should be 2 menus - one is an array, one is the function to call. + t.equal(vm.runtime._blockInfo[0].menus.length, 2); + // First menu has 3 items. + t.equal( + vm.runtime._blockInfo[0].menus[0].json.args0[0].options.length, 3); + // Second menu is a dynamic menu and therefore should be a function. + t.type( + vm.runtime._blockInfo[0].menus[1].json.args0[0].options, 'function'); + + t.end(); +}); + +test('load sync', t => { + const vm = new VirtualMachine(); + vm.extensionManager.loadExtensionIdSync('coreExample'); + t.ok(vm.extensionManager.isExtensionLoaded('coreExample')); + + t.equal(vm.runtime._blockInfo.length, 1); + + // blocks should be an array of two items: a button pseudo-block and a reporter block. + t.equal(vm.runtime._blockInfo[0].blocks.length, 3); + t.type(vm.runtime._blockInfo[0].blocks[0].info, 'object'); + t.type(vm.runtime._blockInfo[0].blocks[0].info.func, 'MAKE_A_VARIABLE'); + t.equal(vm.runtime._blockInfo[0].blocks[0].info.blockType, 'button'); + t.type(vm.runtime._blockInfo[0].blocks[1].info, 'object'); + t.equal(vm.runtime._blockInfo[0].blocks[1].info.opcode, 'exampleOpcode'); + t.equal(vm.runtime._blockInfo[0].blocks[1].info.blockType, 'reporter'); + t.type(vm.runtime._blockInfo[0].blocks[2].info, 'object'); + t.equal(vm.runtime._blockInfo[0].blocks[2].info.opcode, 'exampleWithInlineImage'); + t.equal(vm.runtime._blockInfo[0].blocks[2].info.blockType, 'command'); + + // Test the opcode function + t.equal(vm.runtime._blockInfo[0].blocks[1].info.func(), 'no stage yet'); + + const sprite = new Sprite(null, vm.runtime); + sprite.name = 'Stage'; + const stage = new RenderedTarget(sprite, vm.runtime); + stage.isStage = true; + vm.runtime.targets = [stage]; + + t.equal(vm.runtime._blockInfo[0].blocks[1].info.func(), 'Stage'); + + t.end(); +}); diff --git a/local-scratch-vm/test/integration/list-monitor-rename.js b/local-scratch-vm/test/integration/list-monitor-rename.js new file mode 100644 index 0000000000000000000000000000000000000000..bb57daf163e900aae4cf993bc7711f4d05690cae --- /dev/null +++ b/local-scratch-vm/test/integration/list-monitor-rename.js @@ -0,0 +1,52 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/list-monitor-rename.sb3'); +const project = readFileToBuffer(projectUri); + +test('importing sb3 project with incorrect list monitor name', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', () => { + const stage = vm.runtime.targets[0]; + const cat = vm.runtime.targets[1]; + + for (const {target, renamedListName} of [ + {target: stage, renamedListName: 'renamed global'}, + {target: cat, renamedListName: 'renamed local'} + ]) { + const listId = Object.keys(target.variables).find(k => target.variables[k].name === renamedListName); + + const monitorRecord = vm.runtime._monitorState.get(listId); + const monitorBlock = vm.runtime.monitorBlocks.getBlock(listId); + t.equal(monitorRecord.opcode, 'data_listcontents'); + + // The list name should be properly renamed + t.equal(monitorRecord.params.LIST, renamedListName); + t.equal(monitorBlock.fields.LIST.value, renamedListName); + } + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/load-extensions.js b/local-scratch-vm/test/integration/load-extensions.js new file mode 100644 index 0000000000000000000000000000000000000000..bdeafb001faa37ec99bede3a09159cffdb53da4c --- /dev/null +++ b/local-scratch-vm/test/integration/load-extensions.js @@ -0,0 +1,68 @@ +const path = require('path'); +const tap = require('tap'); +const {test} = tap; +const fs = require('fs'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +tap.tearDown(() => process.nextTick(process.exit)); + +test('Load external extensions', async t => { + const vm = new VirtualMachine(); + const testFiles = fs.readdirSync('./test/fixtures/load-extensions/confirm-load/'); + + // Test each example extension file + for (const file of testFiles) { + const ext = file.split('-')[0]; + const uri = path.resolve(__dirname, `../fixtures/load-extensions/confirm-load/${file}`); + const project = readFileToBuffer(uri); + + await t.test('Confirm expected extension is installed in example sb2 and sb3 projects', extTest => { + vm.loadProject(project) + .then(() => { + extTest.ok(vm.extensionManager.isExtensionLoaded(ext)); + extTest.end(); + }); + }); + } + t.end(); +}); + +test('Load video sensing extension and video properties', async t => { + const vm = new VirtualMachine(); + // An array of test projects and their expected video state values + const testProjects = [ + { + file: 'videoState-off.sb2', + videoState: 'off', + videoTransparency: 50, + mirror: undefined + }, + { + file: 'videoState-on-transparency-0.sb2', + videoState: 'on', + videoTransparency: 0, + mirror: true + }]; + + for (const project of testProjects) { + const uri = path.resolve(__dirname, `../fixtures/load-extensions/video-state/${project.file}`); + const projectData = readFileToBuffer(uri); + + await vm.loadProject(projectData); + + const stage = vm.runtime.getTargetForStage(); + + t.ok(vm.extensionManager.isExtensionLoaded('videoSensing')); + + // Check that the stage target has the video state values we expect + // based on the test project files, then check that the video io device + // has the expected state as well + t.equal(stage.videoState, project.videoState); + t.equal(vm.runtime.ioDevices.video.mirror, project.mirror); + t.equal(stage.videoTransparency, project.videoTransparency); + t.equal(vm.runtime.ioDevices.video._ghost, project.videoTransparency); + } + + t.end(); +}); diff --git a/local-scratch-vm/test/integration/load-sb2-originally-sb1-without-backdrop-image.js b/local-scratch-vm/test/integration/load-sb2-originally-sb1-without-backdrop-image.js new file mode 100644 index 0000000000000000000000000000000000000000..8a79cd223954b26680d2389227bed124068cd1c8 --- /dev/null +++ b/local-scratch-vm/test/integration/load-sb2-originally-sb1-without-backdrop-image.js @@ -0,0 +1,41 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; + +const VirtualMachine = require('../../src/virtual-machine'); + +const projectUri = path.resolve(__dirname, '../fixtures/sb2-from-sb1-missing-backdrop-image.sb2'); +const project = readFileToBuffer(projectUri); + +const vm = new VirtualMachine(); + +test('sb2 project (originally from Scratch 1.4) with missing backdrop image should load', t => { + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + t.doesNotThrow(() => { + vm.loadProject(project).then(() => { + + t.equal(vm.runtime.targets.length, 2); // stage and default sprite + + vm.greenFlag(); + + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 1000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/looks.js b/local-scratch-vm/test/integration/looks.js new file mode 100644 index 0000000000000000000000000000000000000000..63999d3798332dce665157386d9bbf336e2728c9 --- /dev/null +++ b/local-scratch-vm/test/integration/looks.js @@ -0,0 +1,38 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/looks.sb2'); +const project = readFileToBuffer(uri); + +test('looks', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/monitor-threads-run-every-frame.js b/local-scratch-vm/test/integration/monitor-threads-run-every-frame.js new file mode 100644 index 0000000000000000000000000000000000000000..089beafd205002fc86aa9a7d3a4681cf7bc98e5c --- /dev/null +++ b/local-scratch-vm/test/integration/monitor-threads-run-every-frame.js @@ -0,0 +1,95 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const Thread = require('../../src/engine/thread'); +const Runtime = require('../../src/engine/runtime'); + +const projectUri = path.resolve(__dirname, '../fixtures/timer-monitor.sb3'); +const project = readFileToBuffer(projectUri); + +const checkMonitorThreadPresent = (t, threads) => { + t.equal(threads.length, 1); + const monitorThread = threads[0]; + t.equal(monitorThread.stackClick, false); + t.equal(monitorThread.updateMonitor, true); + t.equal(monitorThread.topBlock.toString(), 'timer'); +}; + +/** + * Creates a monitor and then checks if it gets run every frame. + */ +test('monitor thread runs every frame', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(project).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + let doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(doneThreads.length, 1); + checkMonitorThreadPresent(t, doneThreads); + t.assert(doneThreads[0].status === Thread.STATUS_DONE); + + // Check that both are added again when another step is taken + vm.runtime._step(); + doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 0); + t.equal(doneThreads.length, 1); + checkMonitorThreadPresent(t, doneThreads); + t.assert(doneThreads[0].status === Thread.STATUS_DONE); + t.end(); + }); + }); +}); + +/** + * If the monitor doesn't finish evaluating within one frame, it shouldn't be added again + * on the next frame. (We skip execution by setting the step time to 0) + */ +test('monitor thread not added twice', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = 0; + + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(project).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + let doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 1); + t.equal(doneThreads.length, 0); + checkMonitorThreadPresent(t, vm.runtime.threads); + t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING); + const prevThread = vm.runtime.threads[0]; + + // Check that both are added again when another step is taken + vm.runtime._step(); + doneThreads = vm.runtime._lastStepDoneThreads; + t.equal(vm.runtime.threads.length, 1); + t.equal(doneThreads.length, 0); + checkMonitorThreadPresent(t, vm.runtime.threads); + t.equal(vm.runtime.threads[0], prevThread); + t.end(); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/monitors_sb2.js b/local-scratch-vm/test/integration/monitors_sb2.js new file mode 100644 index 0000000000000000000000000000000000000000..12ab92a836da367735878dbb617a9e19e144e8c7 --- /dev/null +++ b/local-scratch-vm/test/integration/monitors_sb2.js @@ -0,0 +1,143 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb2'); +const project = readFileToBuffer(projectUri); + +test('importing sb2 project with monitors', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + // All monitors should create threads that finish during the step and + // are revoved from runtime.threads. + t.equal(threads.length, 0); + + // we care that the last step updated the right number of monitors + // we don't care whether the last step ran other threads or not + const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor); + t.equal(lastStepUpdatedMonitorThreads.length, 8); + + // There should be one additional hidden monitor that is in the monitorState but + // does not start a thread. + t.equal(vm.runtime._monitorState.size, 9); + + const stage = vm.runtime.targets[0]; + const target = vm.runtime.targets[1]; + + // Global variable named "global" is a slider + let variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'global')[0]; + let monitorRecord = vm.runtime._monitorState.get(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'slider'); + t.equal(monitorRecord.sliderMin, -200); // Make sure these are imported for sliders. + t.equal(monitorRecord.sliderMax, 30); + t.equal(monitorRecord.isDiscrete, false); + t.equal(monitorRecord.x, 5); // These are imported for all monitors, just check once. + t.equal(monitorRecord.y, 59); + t.equal(monitorRecord.visible, true); + + // Global variable named "global list" is a list + variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'global list')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + t.equal(monitorRecord.opcode, 'data_listcontents'); + t.equal(monitorRecord.mode, 'list'); + t.equal(monitorRecord.visible, true); + + // Local variable named "local" is hidden + variableId = Object.keys(target.variables).filter(k => target.variables[k].name === 'local')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, false); + + // Local list named "local list" is visible + variableId = Object.keys(target.variables).filter(k => target.variables[k].name === 'local list')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + t.equal(monitorRecord.opcode, 'data_listcontents'); + t.equal(monitorRecord.mode, 'list'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.width, 106); // Make sure these are imported from lists. + t.equal(monitorRecord.height, 206); + + // Backdrop name monitor is visible, not sprite specific + // should get imported with id that references the name parameter + // via '_name' at the end since the 3.0 block has a dropdown. + monitorRecord = vm.runtime._monitorState.get('backdropnumbername_name'); + t.equal(monitorRecord.opcode, 'looks_backdropnumbername'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + // x position monitor is in large mode, specific to sprite 1 + monitorRecord = vm.runtime._monitorState.get(`${target.id}_xposition`); + t.equal(monitorRecord.opcode, 'motion_xposition'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Sprite1'); + t.equal(monitorRecord.targetId, target.id); + + + let monitorId; + let monitorBlock; + + // The monitor IDs for the sensing_current block should be unique + // to the parameter that is selected on the block being monitored. + // The paramater portion of the id should be lowercase even + // though the field value on the block is uppercase. + + monitorId = 'current_date'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'DATE'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + monitorId = 'current_minute'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'MINUTE'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + monitorId = 'current_dayofweek'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'DAYOFWEEK'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/monitors_sb2_to_sb3.js b/local-scratch-vm/test/integration/monitors_sb2_to_sb3.js new file mode 100644 index 0000000000000000000000000000000000000000..ec881c2d321e2fc216f399d1936aee781deb119c --- /dev/null +++ b/local-scratch-vm/test/integration/monitors_sb2_to_sb3.js @@ -0,0 +1,156 @@ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +let vm; + +tap.beforeEach(() => { + const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb2'); + const project = readFileToBuffer(projectUri); + + vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // TODO figure out why running threads doesn't work in this test + // vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + return vm.loadProject(project); +}); +const test = tap.test; + +test('saving and loading sb2 project with monitors preserves sliderMin and sliderMax', t => { + + vm.on('playgroundData', e /* eslint-disable-line no-unused-vars */ => { + // TODO related to above TODO, comment these back in when we figure out + // why running threads doesn't work with this test + + // const threads = JSON.parse(e.threads); + // All monitors should create threads that finish during the step and + // are revoved from runtime.threads. + // t.equal(threads.length, 0); + + // we care that the last step updated the right number of monitors + // we don't care whether the last step ran other threads or not + // const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor); + // t.equal(lastStepUpdatedMonitorThreads.length, 8); + + // There should be one additional hidden monitor that is in the monitorState but + // does not start a thread. + t.equal(vm.runtime._monitorState.size, 9); + + const stage = vm.runtime.targets[0]; + const target = vm.runtime.targets[1]; + + // Global variable named "global" is a slider + let variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'global')[0]; + // Used later when checking save and load of slider min/max + let monitorRecord = vm.runtime._monitorState.get(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'slider'); + t.equal(monitorRecord.sliderMin, -200); // Make sure these are imported for sliders. + t.equal(monitorRecord.sliderMax, 30); + t.equal(monitorRecord.isDiscrete, false); + t.equal(monitorRecord.x, 5); // These are imported for all monitors, just check once. + t.equal(monitorRecord.y, 59); + t.equal(monitorRecord.visible, true); + + // Global variable named "global list" is a list + variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'global list')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + t.equal(monitorRecord.opcode, 'data_listcontents'); + t.equal(monitorRecord.mode, 'list'); + t.equal(monitorRecord.visible, true); + + // Local variable named "local" is hidden + variableId = Object.keys(target.variables).filter(k => target.variables[k].name === 'local')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, false); + + // Local list named "local list" is visible + variableId = Object.keys(target.variables).filter(k => target.variables[k].name === 'local list')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + t.equal(monitorRecord.opcode, 'data_listcontents'); + t.equal(monitorRecord.mode, 'list'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.width, 106); // Make sure these are imported from lists. + t.equal(monitorRecord.height, 206); + + // Backdrop name monitor is visible, not sprite specific + // should get imported with id that references the name parameter + // via '_name' at the end since the 3.0 block has a dropdown. + monitorRecord = vm.runtime._monitorState.get('backdropnumbername_name'); + t.equal(monitorRecord.opcode, 'looks_backdropnumbername'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + // x position monitor is in large mode, specific to sprite 1 + monitorRecord = vm.runtime._monitorState.get(`${target.id}_xposition`); + t.equal(monitorRecord.opcode, 'motion_xposition'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Sprite1'); + t.equal(monitorRecord.targetId, target.id); + + + let monitorId; + let monitorBlock; + + // The monitor IDs for the sensing_current block should be unique + // to the parameter that is selected on the block being monitored. + // The paramater portion of the id should be lowercase even + // though the field value on the block is uppercase. + + monitorId = 'current_date'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'DATE'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + monitorId = 'current_minute'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'MINUTE'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + monitorId = 'current_dayofweek'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'DAYOFWEEK'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + const sb3ProjectJson = vm.toJSON(); + return vm.loadProject(sb3ProjectJson).then(() => { + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/monitors_sb3.js b/local-scratch-vm/test/integration/monitors_sb3.js new file mode 100644 index 0000000000000000000000000000000000000000..f55edb864344eadd119a91bdc56c26e3f258d0a7 --- /dev/null +++ b/local-scratch-vm/test/integration/monitors_sb3.js @@ -0,0 +1,268 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const Variable = require('../../src/engine/variable'); + +const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb3'); +const project = readFileToBuffer(projectUri); + +test('importing sb3 project with monitors', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + // All monitors should create threads that finish during the step and + // are revoved from runtime.threads. + t.equal(threads.length, 0); + + // we care that the last step updated the right number of monitors + // we don't care whether the last step ran other threads or not + const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor); + t.equal(lastStepUpdatedMonitorThreads.length, 17); + + // There should be one additional hidden monitor that is in the monitorState but + // does not start a thread. + t.equal(vm.runtime._monitorState.size, 18); + + const stage = vm.runtime.targets[0]; + const shirtSprite = vm.runtime.targets[1]; + const heartSprite = vm.runtime.targets[2]; + + // Global variable named "my variable" exists + let variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'my variable')[0]; + let monitorRecord = vm.runtime._monitorState.get(variableId); + let monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'default'); + // The following few properties are imported for all monitors, just check once. + t.equal(monitorRecord.sliderMin, 0); + t.equal(monitorRecord.sliderMax, 100); + t.equal(monitorRecord.isDiscrete, true); // The default if not present + t.equal(monitorRecord.x, 10); + t.equal(monitorRecord.y, 62); + // Height and width are only used for list monitors and should default to 0 + // for all other monitors + t.equal(monitorRecord.width, 0); + t.equal(monitorRecord.height, 0); + t.equal(monitorRecord.visible, true); + t.type(monitorRecord.params, 'object'); + // The variable name should be stored in the monitor params + t.equal(monitorRecord.params.VARIABLE, 'my variable'); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.VARIABLE.value, 'my variable'); + t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE'); + t.equal(monitorBlock.fields.VARIABLE.id, variableId); + t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE); + + // There is a global variable named 'secret_slide' which has a hidden monitor + variableId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'secret_slide')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'slider'); + t.equal(monitorRecord.visible, false); + t.equal(monitorRecord.sliderMin, 0); + t.equal(monitorRecord.sliderMax, 100); + t.type(monitorRecord.params, 'object'); + t.equal(monitorRecord.params.VARIABLE, 'secret_slide'); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.VARIABLE.value, 'secret_slide'); + t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE'); + t.equal(monitorBlock.fields.VARIABLE.id, variableId); + t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE); + + + // Shirt sprite has a local list named "fashion" + variableId = Object.keys(shirtSprite.variables).filter(k => shirtSprite.variables[k].name === 'fashion')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_listcontents'); + t.equal(monitorRecord.mode, 'list'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.height, 122); + t.equal(monitorRecord.width, 104); + t.type(monitorRecord.params, 'object'); + t.equal(monitorRecord.params.LIST, 'fashion'); // The list name should be stored in the monitor params + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.LIST.value, 'fashion'); + t.equal(monitorBlock.fields.LIST.name, 'LIST'); + t.equal(monitorBlock.fields.LIST.id, variableId); + t.equal(monitorBlock.fields.LIST.variableType, Variable.LIST_TYPE); + + // Shirt sprite has a local variable named "tee" + variableId = Object.keys(shirtSprite.variables).filter(k => shirtSprite.variables[k].name === 'tee')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'slider'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.sliderMin, 0); + t.equal(monitorRecord.sliderMax, 100); + t.type(monitorRecord.params, 'object'); + t.equal(monitorRecord.params.VARIABLE, 'tee'); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.VARIABLE.value, 'tee'); + t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE'); + t.equal(monitorBlock.fields.VARIABLE.id, variableId); + t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE); + + // Heart sprite has a local list named "hearty" + variableId = Object.keys(heartSprite.variables).filter(k => heartSprite.variables[k].name === 'hearty')[0]; + monitorRecord = vm.runtime._monitorState.get(variableId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(variableId); + t.equal(monitorRecord.opcode, 'data_variable'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.type(monitorRecord.params, 'object'); + t.equal(monitorRecord.params.VARIABLE, 'hearty'); // The variable name should be stored in the monitor params + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.VARIABLE.value, 'hearty'); + t.equal(monitorBlock.fields.VARIABLE.name, 'VARIABLE'); + t.equal(monitorBlock.fields.VARIABLE.id, variableId); + t.equal(monitorBlock.fields.VARIABLE.variableType, Variable.SCALAR_TYPE); + + // Backdrop name monitor is visible, not sprite specific + // should get imported with id that references the name parameter + // via '_name' at the end since the 3.0 block has a dropdown. + let monitorId = 'backdropnumbername_name'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'looks_backdropnumbername'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.NUMBER_NAME.value, 'name'); + + // Backdrop name monitor is visible, not sprite specific + // should get imported with id that references the name parameter + // via '_number' at the end since the 3.0 block has a dropdown. + monitorId = 'backdropnumbername_number'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'looks_backdropnumbername'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + // Test that the monitor block and its fields were constructed correctly + t.equal(monitorBlock.fields.NUMBER_NAME.value, 'number'); + + // x position monitor is in large mode, specific to shirt sprite + monitorId = `${shirtSprite.id}_xposition`; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'motion_xposition'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Shirt-T'); + t.equal(monitorRecord.targetId, shirtSprite.id); + + // y position monitor is in large mode, specific to shirt sprite + monitorId = `${shirtSprite.id}_yposition`; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'motion_yposition'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Shirt-T'); + t.equal(monitorRecord.targetId, shirtSprite.id); + + // direction monitor is in large mode, specific to shirt sprite + monitorId = `${shirtSprite.id}_direction`; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'motion_direction'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Shirt-T'); + t.equal(monitorRecord.targetId, shirtSprite.id); + + monitorId = `${shirtSprite.id}_size`; + monitorRecord = vm.runtime._monitorState.get(monitorId); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.opcode, 'looks_size'); + t.equal(monitorRecord.mode, 'large'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, 'Shirt-T'); + t.equal(monitorRecord.targetId, shirtSprite.id); + + // The monitor IDs for the sensing_current block should be unique + // to the parameter that is selected on the block being monitored. + // The paramater portion of the id should be lowercase even + // though the field value on the block is uppercase. + monitorId = 'current_date'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'DATE'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + monitorId = 'current_year'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'YEAR'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + monitorId = 'current_month'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'sensing_current'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorBlock.fields.CURRENTMENU.value, 'MONTH'); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + + // Extension Monitors + monitorId = 'music_getTempo'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'music_getTempo'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + t.equal(vm.extensionManager.isExtensionLoaded('music'), true); + + monitorId = 'ev3_getDistance'; + monitorRecord = vm.runtime._monitorState.get(monitorId); + t.equal(monitorRecord.opcode, 'ev3_getDistance'); + monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId); + t.equal(monitorRecord.mode, 'default'); + t.equal(monitorRecord.visible, true); + t.equal(monitorRecord.spriteName, null); + t.equal(monitorRecord.targetId, null); + t.equal(vm.extensionManager.isExtensionLoaded('ev3'), true); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/motion.js b/local-scratch-vm/test/integration/motion.js new file mode 100644 index 0000000000000000000000000000000000000000..67839385df462d441470f63e277b9000f1dcea80 --- /dev/null +++ b/local-scratch-vm/test/integration/motion.js @@ -0,0 +1,38 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/motion.sb2'); +const project = readFileToBuffer(uri); + +test('motion', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length > 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/offline-custom-assets.js b/local-scratch-vm/test/integration/offline-custom-assets.js new file mode 100644 index 0000000000000000000000000000000000000000..8e8ed59e251d6dd8a1196f4819d12c750c391667 --- /dev/null +++ b/local-scratch-vm/test/integration/offline-custom-assets.js @@ -0,0 +1,77 @@ +/** + * @fileoverview + * This integration test ensures that a local upload of a sb2 project pulls + * in assets correctly (from the provided .sb2 file) even if the assets + * are not present on our servers. + */ +const path = require('path'); +const fs = require('fs'); +const test = require('tap').test; +const AdmZip = require('adm-zip'); +const ScratchStorage = require('scratch-storage'); +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/offline-custom-assets.sb2'); +const projectZip = AdmZip(projectUri); +const project = Buffer.from(fs.readFileSync(projectUri)); +// Custom costume from sb2 file (which was downloaded from offline editor) +// This sound should not be available on our servers +const costume = projectZip.readFile('1.svg'); +const costumeData = new Uint8Array(costume); +// Custom sound recording from sb2 file (which was downloaded from offline editor) +// This sound should not be available on our servers +const sound = projectZip.readFile('0.wav'); +const soundData = new Uint8Array(sound); + +test('offline-custom-assets', t => { + const vm = new VirtualMachine(); + // Use a test storage here that does not have any web sources added to it. + const testStorage = new ScratchStorage(); + vm.attachStorage(testStorage); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + + // Verify initial state + t.equals(vm.runtime.targets.length, 2); + const costumes = vm.runtime.targets[1].getCostumes(); + t.equals(costumes.length, 1); + const customCostume = costumes[0]; + t.equals(customCostume.name, 'A_Test_Costume'); + + const storedCostume = customCostume.asset; + t.type(storedCostume, 'object'); + t.deepEquals(storedCostume.data, costumeData); + + const sounds = vm.runtime.targets[1].sprite.sounds; + t.equals(sounds.length, 1); + const customSound = sounds[0]; + t.equals(customSound.name, 'A_Test_Recording'); + const storedSound = customSound.asset; + t.type(storedSound, 'object'); + t.deepEquals(storedSound.data, soundData); + + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); + +}); diff --git a/local-scratch-vm/test/integration/pen.js b/local-scratch-vm/test/integration/pen.js new file mode 100644 index 0000000000000000000000000000000000000000..0045854a7f0ff44aae639850b99b7753ba5abcf7 --- /dev/null +++ b/local-scratch-vm/test/integration/pen.js @@ -0,0 +1,61 @@ +const Worker = require('tiny-worker'); +const path = require('path'); +const test = require('tap').test; + +const Scratch3PenBlocks = require('../../src/extensions/scratch3_pen/index.js'); +const VirtualMachine = require('../../src/index'); +const dispatch = require('../../src/dispatch/central-dispatch'); + +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; + +const uri = path.resolve(__dirname, '../fixtures/pen.sb2'); +const project = readFileToBuffer(uri); + +// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead. +dispatch.workerClass = Worker; + +test('pen', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', () => { + // @todo Additional tests + + const catSprite = vm.runtime.targets[1].sprite; + const [originalCat, cloneCat] = catSprite.clones; + t.notStrictEqual(originalCat, cloneCat); + + /** @type {PenState} */ + const originalPenState = originalCat.getCustomState(Scratch3PenBlocks.STATE_KEY); + + /** @type {PenState} */ + const clonePenState = cloneCat.getCustomState(Scratch3PenBlocks.STATE_KEY); + + t.notStrictEqual(originalPenState, clonePenState); + t.equal(originalPenState.penAttributes.diameter, 51); + t.equal(clonePenState.penAttributes.diameter, 42); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project) + .then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/procedure.js b/local-scratch-vm/test/integration/procedure.js new file mode 100644 index 0000000000000000000000000000000000000000..c738c32450389ccd012c8a2024a2a3092265911b --- /dev/null +++ b/local-scratch-vm/test/integration/procedure.js @@ -0,0 +1,38 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/procedure.sb2'); +const project = readFileToBuffer(uri); + +test('procedure', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/running_project_changed_state.js b/local-scratch-vm/test/integration/running_project_changed_state.js new file mode 100644 index 0000000000000000000000000000000000000000..1af086d5634687eeb8e7bebfc6439dd93ff8d6c6 --- /dev/null +++ b/local-scratch-vm/test/integration/running_project_changed_state.js @@ -0,0 +1,48 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/looks.sb2'); +const project = readFileToBuffer(uri); + +test('Running project should not emit project changed event', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + let projectChanged = false; + vm.on('PROJECT_CHANGED', () => { + projectChanged = true; + }); + + // Evaluate playground data and exit + vm.on('playgroundData', () => { + t.equal(projectChanged, false); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + // The test in unit/project_load_changed_state.js tests + // that loading a project does not emit a project changed + // event. This setup tries to be agnostic of whether that + // test is passing or failing. + projectChanged = false; + + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/saythink-and-wait.js b/local-scratch-vm/test/integration/saythink-and-wait.js new file mode 100644 index 0000000000000000000000000000000000000000..9b42e8ee698880a7157eb7398134bf063cafbdc7 --- /dev/null +++ b/local-scratch-vm/test/integration/saythink-and-wait.js @@ -0,0 +1,37 @@ +const Worker = require('tiny-worker'); +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const dispatch = require('../../src/dispatch/central-dispatch'); + +const uri = path.resolve(__dirname, '../fixtures/saythink-and-wait.sb2'); +const project = readFileToBuffer(uri); + +// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead. +dispatch.workerClass = Worker; + +test('say/think and wait', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, stop the project. + // The test will fail if the project throws. + setTimeout(() => { + vm.stopAll(); + t.end(); + process.nextTick(process.exit); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/sb2-import-extension-monitors.js b/local-scratch-vm/test/integration/sb2-import-extension-monitors.js new file mode 100644 index 0000000000000000000000000000000000000000..5420429bf75f04832cefcd4a8d72e2b979ce7e40 --- /dev/null +++ b/local-scratch-vm/test/integration/sb2-import-extension-monitors.js @@ -0,0 +1,109 @@ +const path = require('path'); +const tap = require('tap'); +const test = tap.test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const {readFileToBuffer, extractProjectJson} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const sb2 = require('../../src/serialization/sb2'); + +const invisibleVideoMonitorProjectUri = path.resolve(__dirname, '../fixtures/invisible-video-monitor.sb2'); +const invisibleVideoMonitorProject = readFileToBuffer(invisibleVideoMonitorProjectUri); + +const visibleVideoMonitorProjectUri = path.resolve( + __dirname, '../fixtures/visible-video-monitor-no-other-video-blocks.sb2'); +const visibleVideoMonitorProject = readFileToBuffer(visibleVideoMonitorProjectUri); + +const visibleVideoMonitorAndBlocksProjectUri = path.resolve( + __dirname, '../fixtures/visible-video-monitor-and-video-blocks.sb2'); +const visibleVideoMonitorAndBlocksProject = extractProjectJson(visibleVideoMonitorAndBlocksProjectUri); + +const invisibleTempoMonitorProjectUri = path.resolve( + __dirname, '../fixtures/invisible-tempo-monitor-no-other-music-blocks.sb2'); +const invisibleTempoMonitorProject = readFileToBuffer(invisibleTempoMonitorProjectUri); + +const visibleTempoMonitorProjectUri = path.resolve( + __dirname, '../fixtures/visible-tempo-monitor-no-other-music-blocks.sb2'); +const visibleTempoMonitorProject = readFileToBuffer(visibleTempoMonitorProjectUri); + +tap.tearDown(() => process.nextTick(process.exit)); + +test('loading sb2 project with invisible video monitor should not load monitor or extension', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(invisibleVideoMonitorProject).then(() => { + t.equal(vm.extensionManager.isExtensionLoaded('videoSensing'), false); + t.equal(vm.runtime._monitorState.size, 0); + t.end(); + }); +}); + +test('loading sb2 project with visible video monitor should not load extension', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(visibleVideoMonitorProject).then(() => { + t.equal(vm.extensionManager.isExtensionLoaded('videoSensing'), false); + t.equal(vm.runtime._monitorState.size, 0); + t.end(); + }); +}); + +// This test looks a little different than the rest because loading a project with +// the video sensing block requires a mock renderer and other setup, so instead +// we are just using deserialize to test what we need instead +test('sb2 project with video sensing blocks and monitor should load extension but not monitor', t => { + const vm = new VirtualMachine(); + + sb2.deserialize(visibleVideoMonitorAndBlocksProject, vm.runtime).then(project => { + // Extension loads but monitor does not + project.extensions.extensionIDs.has('videoSensing'); + // Non-core extension monitors haven't been added to the runtime + t.equal(vm.runtime._monitorState.size, 0); + t.end(); + }); +}); + +test('sb2 project with invisible music monitor should not load monitor or extension', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(invisibleTempoMonitorProject).then(() => { + t.equal(vm.extensionManager.isExtensionLoaded('music'), false); + t.equal(vm.runtime._monitorState.size, 0); + t.end(); + }); +}); + +test('sb2 project with visible music monitor should load monitor and extension', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(visibleTempoMonitorProject).then(() => { + t.equal(vm.extensionManager.isExtensionLoaded('music'), true); + t.equal(vm.runtime._monitorState.size, 1); + t.equal(vm.runtime._monitorState.has('music_getTempo'), true); + t.equal(vm.runtime._monitorState.get('music_getTempo').visible, true); + t.end(); + }); +}); diff --git a/local-scratch-vm/test/integration/sb2_corrupted_png.js b/local-scratch-vm/test/integration/sb2_corrupted_png.js new file mode 100644 index 0000000000000000000000000000000000000000..d68685a360bd7246eb67a1c83d3e951f7b920216 --- /dev/null +++ b/local-scratch-vm/test/integration/sb2_corrupted_png.js @@ -0,0 +1,128 @@ +/** + * This test mocks render breaking on loading a corrupted bitmap costume. + * The VM should handle this safely by displaying a Gray Question Mark, + * but keeping track of the original costume data and serializing the + * original costume data back out. The saved project.json should not + * reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb2'); +const project = readFileToBuffer(projectUri); +const costumeFileName = '1.png'; +const originalCostume = extractAsset(projectUri, costumeFileName); +// We need to get the actual md5 because we hand modified the png to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenCostumeMd5 = md5(originalCostume); + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => { + const base64Image = image.src.split(',')[1]; + const decodedText = Buffer.from(base64Image, 'base64').toString(); + if (decodedText.includes('Here is some')) { + image.onerror(); + } else { + image.onload(); + } + }, 100); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; +let defaultBitmapAssetId; + +tap.setTimeout(30000); + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap; + + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project); +}); + +const test = tap.test; + +test('load sb2 project with corrupted bitmap costume file', t => { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = vm.runtime.targets[1]; + t.equal(greenGuySprite.getName(), 'GreenGuy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const corruptedCostume = greenGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'GreenGuy'); + t.equal(corruptedCostume.assetId, defaultBitmapAssetId); + t.equal(corruptedCostume.dataFormat, 'png'); + // Runtime should have info about broken asset + t.ok(corruptedCostume.broken); + t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5); + + t.end(); +}); + +test('load and then save project with corrupted bitmap costume file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = resavedProject.targets[1]; + t.equal(greenGuySprite.name, 'GreenGuy'); + t.equal(greenGuySprite.costumes.length, 1); + + const corruptedCostume = greenGuySprite.costumes[0]; + t.equal(corruptedCostume.name, 'GreenGuy'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedCostume.assetId, brokenCostumeMd5); + t.equal(corruptedCostume.dataFormat, 'png'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedCostume.broken); + + t.end(); +}); + +test('serializeCostume saves orignal broken costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id); + t.equal(costumeDescs.length, 1); + const costume = costumeDescs[0]; + t.equal(costume.fileName, `${brokenCostumeMd5}.png`); + t.equal(md5(costume.fileContent), brokenCostumeMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb2_corrupted_svg.js b/local-scratch-vm/test/integration/sb2_corrupted_svg.js new file mode 100644 index 0000000000000000000000000000000000000000..26c26a1d8425498a93f679c92588dd01ea2489c0 --- /dev/null +++ b/local-scratch-vm/test/integration/sb2_corrupted_svg.js @@ -0,0 +1,128 @@ +/** + * This test mocks render breaking on loading a corrupted vector costume. + * The VM should handle this safely by displaying a Gray Question Mark, + * but keeping track of the original costume data and serializing the + * original costume data back out. The saved project.json should not + * reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb2'); +const project = readFileToBuffer(projectUri); +const costumeFileName = '1.svg'; +const originalCostume = extractAsset(projectUri, costumeFileName); +// We need to get the actual md5 because we hand modified the svg to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenCostumeMd5 = md5(originalCostume); + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + + setTimeout(() => image.onload(), 1000); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; +let defaultVectorAssetId; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector; + + // Mock renderer breaking on loading a corrupt costume + FakeRenderer.prototype.createSVGSkin = function (svgString) { + // Look for text added to costume to make it a corrupt svg + if (svgString.includes(' { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = vm.runtime.targets[1]; + t.equal(blueGuySprite.getName(), 'Blue Guy'); + t.equal(blueGuySprite.getCostumes().length, 1); + + const corruptedCostume = blueGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'Blue Guy 2'); + t.equal(corruptedCostume.assetId, defaultVectorAssetId); + t.equal(corruptedCostume.dataFormat, 'svg'); + // Runtime should have info about broken asset + t.ok(corruptedCostume.broken); + t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5); + + t.end(); +}); + +test('load and then save project with corrupted vector costume file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = resavedProject.targets[1]; + t.equal(blueGuySprite.name, 'Blue Guy'); + t.equal(blueGuySprite.costumes.length, 1); + + const corruptedCostume = blueGuySprite.costumes[0]; + t.equal(corruptedCostume.name, 'Blue Guy 2'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedCostume.assetId, brokenCostumeMd5); + t.equal(corruptedCostume.dataFormat, 'svg'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedCostume.broken); + + t.end(); +}); + +test('serializeCostume saves orignal broken costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id); + t.equal(costumeDescs.length, 1); + const costume = costumeDescs[0]; + t.equal(costume.fileName, `${brokenCostumeMd5}.svg`); + t.equal(md5(costume.fileContent), brokenCostumeMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb2_missing_png.js b/local-scratch-vm/test/integration/sb2_missing_png.js new file mode 100644 index 0000000000000000000000000000000000000000..30d66730cce844a0ef5112371efa598772b58e96 --- /dev/null +++ b/local-scratch-vm/test/integration/sb2_missing_png.js @@ -0,0 +1,113 @@ +/** + * This test ensures that the VM gracefully handles an sb2 project with + * a missing bitmap costume. The VM should handle this safely by displaying + * a Gray Question Mark, but keeping track of the original costume data + * and serializing the original costume data back out. The saved project.json + * should not reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb2'); +const project = readFileToBuffer(projectUri); + + +const missingCostumeAssetId = 'aadce129bfe4e57f0dd81478f3ed82aa'; + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => image.onload(), 100); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; + +tap.setTimeout(30000); + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project); +}); + +const test = tap.test; + +test('loading sb2 project with missing bitmap costume file', t => { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = vm.runtime.targets[1]; + t.equal(greenGuySprite.getName(), 'GreenGuy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const missingCostume = greenGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'GreenGuy'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap; + t.equal(missingCostume.assetId, defaultVectorAssetId); + t.equal(missingCostume.dataFormat, 'png'); + // Runtime should have info about broken asset + t.ok(missingCostume.broken); + t.equal(missingCostume.broken.assetId, missingCostumeAssetId); + + t.end(); +}); + +test('load and then save sb2 project with missing costume file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = resavedProject.targets[1]; + t.equal(greenGuySprite.name, 'GreenGuy'); + t.equal(greenGuySprite.costumes.length, 1); + + const missingCostume = greenGuySprite.costumes[0]; + t.equal(missingCostume.name, 'GreenGuy'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + t.equal(missingCostume.assetId, missingCostumeAssetId); + t.equal(missingCostume.dataFormat, 'png'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingCostume.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const costumeDescs = serializeCostumes(vm.runtime); + + t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop + t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.png`); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb2_missing_svg.js b/local-scratch-vm/test/integration/sb2_missing_svg.js new file mode 100644 index 0000000000000000000000000000000000000000..f8a9da30d2c12c4435e74fade6e5db7700242767 --- /dev/null +++ b/local-scratch-vm/test/integration/sb2_missing_svg.js @@ -0,0 +1,110 @@ +/** + * This test ensures that the VM gracefully handles an sb2 project with + * a missing vector costume. The VM should handle this safely by displaying + * a Gray Question Mark, but keeping track of the original costume data + * and serializing the original costume data back out. The saved project.json + * should not reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb2'); +const project = readFileToBuffer(projectUri); + +const missingCostumeAssetId = 'beca8009621913e2f5b3111eed2d8210'; + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => image.onload(), 1000); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project); +}); + +const test = tap.test; + +test('loading sb2 project with missing vector costume file', t => { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = vm.runtime.targets[1]; + t.equal(blueGuySprite.getName(), 'Blue Guy'); + t.equal(blueGuySprite.getCostumes().length, 1); + + const missingCostume = blueGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'Blue Guy 2'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector; + t.equal(missingCostume.assetId, defaultVectorAssetId); + t.equal(missingCostume.dataFormat, 'svg'); + // Runtime should have info about broken asset + t.ok(missingCostume.broken); + t.equal(missingCostume.broken.assetId, missingCostumeAssetId); + + t.end(); +}); + +test('load and then save sb2 project with missing costume file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = resavedProject.targets[1]; + t.equal(blueGuySprite.name, 'Blue Guy'); + t.equal(blueGuySprite.costumes.length, 1); + + const missingCostume = blueGuySprite.costumes[0]; + t.equal(missingCostume.name, 'Blue Guy 2'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + t.equal(missingCostume.assetId, missingCostumeAssetId); + t.equal(missingCostume.dataFormat, 'svg'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingCostume.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const costumeDescs = serializeCostumes(vm.runtime); + + t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop + t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.svg`); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb3-roundtrip.js b/local-scratch-vm/test/integration/sb3-roundtrip.js new file mode 100644 index 0000000000000000000000000000000000000000..9dbc339d3eba3381dfc3c1d5a94a25ad35c30ef4 --- /dev/null +++ b/local-scratch-vm/test/integration/sb3-roundtrip.js @@ -0,0 +1,110 @@ +const test = require('tap').test; + +const Blocks = require('../../src/engine/blocks'); +const Clone = require('../../src/util/clone'); +const {loadCostume} = require('../../src/import/load-costume'); +const {loadSound} = require('../../src/import/load-sound'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const Runtime = require('../../src/engine/runtime'); +const sb3 = require('../../src/serialization/sb3'); +const Sprite = require('../../src/sprites/sprite'); + +const defaultCostumeInfo = { + bitmapResolution: 1, + rotationCenterX: 0, + rotationCenterY: 0 +}; + +const defaultSoundInfo = { +}; + +test('sb3-roundtrip', t => { + const runtime1 = new Runtime(); + runtime1.attachStorage(makeTestStorage()); + + const runtime2 = new Runtime(); + runtime2.attachStorage(makeTestStorage()); + + const testRuntimeState = (label, runtime) => { + t.strictEqual(runtime.targets.length, 2, `${label}: target count`); + const [stageClone, spriteClone] = runtime.targets; + + t.strictEqual(stageClone.isOriginal, true); + t.strictEqual(stageClone.isStage, true); + + const stage = stageClone.sprite; + t.strictEqual(stage.name, 'Stage'); + t.strictEqual(stage.clones.length, 1); + t.strictEqual(stage.clones[0], stageClone); + + t.strictEqual(stage.costumes.length, 1); + const [building] = stage.costumes; + t.strictEqual(building.assetId, 'fe5e3566965f9de793beeffce377d054'); + t.strictEqual(building.dataFormat, 'jpg'); + + t.strictEqual(stage.sounds.length, 0); + + t.strictEqual(spriteClone.isOriginal, true); + t.strictEqual(spriteClone.isStage, false); + + const sprite = spriteClone.sprite; + t.strictEqual(sprite.name, 'Sprite'); + t.strictEqual(sprite.clones.length, 1); + t.strictEqual(sprite.clones[0], spriteClone); + + t.strictEqual(sprite.costumes.length, 2); + const [cat, squirrel] = sprite.costumes; + t.strictEqual(cat.assetId, 'f88bf1935daea28f8ca098462a31dbb0'); + t.strictEqual(cat.dataFormat, 'svg'); + t.strictEqual(squirrel.assetId, '7e24c99c1b853e52f8e7f9004416fa34'); + t.strictEqual(squirrel.dataFormat, 'png'); + + t.strictEqual(sprite.sounds.length, 1); + const [meow] = sprite.sounds; + t.strictEqual(meow.md5, '83c36d806dc92327b9e7049a565c6bff.wav'); + }; + + const loadThings = Promise.all([ + loadCostume('fe5e3566965f9de793beeffce377d054.jpg', Clone.simple(defaultCostumeInfo), runtime1), + loadCostume('f88bf1935daea28f8ca098462a31dbb0.svg', Clone.simple(defaultCostumeInfo), runtime1), + loadCostume('7e24c99c1b853e52f8e7f9004416fa34.png', Clone.simple(defaultCostumeInfo), runtime1), + loadSound(Object.assign({md5: '83c36d806dc92327b9e7049a565c6bff.wav'}, defaultSoundInfo), runtime1) + ]); + + const installThings = loadThings.then(results => { + const [building, cat, squirrel, meow] = results; + + const stageBlocks = new Blocks(runtime1); + const stage = new Sprite(stageBlocks, runtime1); + stage.name = 'Stage'; + stage.costumes = [building]; + stage.sounds = []; + const stageClone = stage.createClone(); + stageClone.isStage = true; + + const spriteBlocks = new Blocks(runtime1); + const sprite = new Sprite(spriteBlocks, runtime1); + sprite.name = 'Sprite'; + sprite.costumes = [cat, squirrel]; + sprite.sounds = [meow]; + const spriteClone = sprite.createClone(); + + runtime1.targets = [stageClone, spriteClone]; + + testRuntimeState('original', runtime1); + }); + + const serializeAndDeserialize = installThings.then(() => { + // Doing a JSON `stringify` and `parse` here more accurately simulate a save/load cycle. In particular: + // 1. it ensures that any non-serializable data is thrown away, and + // 2. `sb3.deserialize` and its helpers do some `hasOwnProperty` checks which fail on the object returned by + // `sb3.serialize` but succeed if that object is "flattened" in this way. + const serializedState = JSON.parse(JSON.stringify(sb3.serialize(runtime1))); + return sb3.deserialize(serializedState, runtime2); + }); + + return serializeAndDeserialize.then(({targets}) => { + runtime2.targets = targets; + testRuntimeState('copy', runtime2); + }); +}); diff --git a/local-scratch-vm/test/integration/sb3_corrupted_png.js b/local-scratch-vm/test/integration/sb3_corrupted_png.js new file mode 100644 index 0000000000000000000000000000000000000000..8ef2c1f806088d0aa54fb859c755f67201eccd09 --- /dev/null +++ b/local-scratch-vm/test/integration/sb3_corrupted_png.js @@ -0,0 +1,128 @@ +/** + * This test mocks render breaking on loading a corrupted bitmap costume. + * The VM should handle this safely by displaying a Gray Question Mark, + * but keeping track of the original costume data and serializing the + * original costume data back out. The saved project.json should not + * reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb3'); +const project = readFileToBuffer(projectUri); +const costumeFileName = 'e1320c21995dcf6de10119be7f08c26b.png'; +const originalCostume = extractAsset(projectUri, costumeFileName); +// We need to get the actual md5 because we hand modified the png to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenCostumeMd5 = md5(originalCostume); + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => { + const base64Image = image.src.split(',')[1]; + const decodedText = Buffer.from(base64Image, 'base64').toString(); + if (decodedText.includes('Here is some')) { + image.onerror(); + } else { + image.onload(); + } + }, 100); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; +let defaultBitmapAssetId; + +tap.setTimeout(30000); + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap; + + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project); +}); + +const test = tap.test; + +test('load sb3 project with corrupted bitmap costume file', t => { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = vm.runtime.targets[1]; + t.equal(greenGuySprite.getName(), 'Green Guy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const corruptedCostume = greenGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'Green Guy'); + t.equal(corruptedCostume.assetId, defaultBitmapAssetId); + t.equal(corruptedCostume.dataFormat, 'png'); + // Runtime should have info about broken asset + t.ok(corruptedCostume.broken); + t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5); + + t.end(); +}); + +test('load and then save project with corrupted bitmap costume file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = resavedProject.targets[1]; + t.equal(greenGuySprite.name, 'Green Guy'); + t.equal(greenGuySprite.costumes.length, 1); + + const corruptedCostume = greenGuySprite.costumes[0]; + t.equal(corruptedCostume.name, 'Green Guy'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedCostume.assetId, brokenCostumeMd5); + t.equal(corruptedCostume.dataFormat, 'png'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedCostume.broken); + + t.end(); +}); + +test('serializeCostume saves orignal broken costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id); + t.equal(costumeDescs.length, 1); + const costume = costumeDescs[0]; + t.equal(costume.fileName, `${brokenCostumeMd5}.png`); + t.equal(md5(costume.fileContent), brokenCostumeMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb3_corrupted_sound.js b/local-scratch-vm/test/integration/sb3_corrupted_sound.js new file mode 100644 index 0000000000000000000000000000000000000000..04f69453bd674be07e5adca21dec7a7e7fdd3cf4 --- /dev/null +++ b/local-scratch-vm/test/integration/sb3_corrupted_sound.js @@ -0,0 +1,120 @@ +/** + * This test mocks breaking on loading a corrupted sound. + * The VM should handle this safely by replacing the sound data with the default (empty) sound, + * but keeping track of the original sound data and serializing the + * original sound data back out. The saved project.json should not + * reflect that the sound is broken and should therefore re-attempt + * to load the sound if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeSounds} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/corrupt_sound.sb3'); +const project = readFileToBuffer(projectUri); +const soundFileName = '78618aadd225b1db7bf837fa17dc0568.wav'; +const originalSound = extractAsset(projectUri, soundFileName); +// We need to get the actual md5 because we hand modified the sound file to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenSoundMd5 = md5(originalSound); + +let fakeId = -1; + +const FakeAudioEngine = function () { + return { + decodeSoundPlayer: soundData => { + const soundDataString = soundData.asset.decodeText(); + if (soundDataString.includes('here is some')) { + return Promise.reject(new Error('mock audio engine broke')); + } + + // Otherwise return fake data + return Promise.resolve({ + id: fakeId++, + buffer: { + sampleRate: 1, + length: 1 + } + }); + }, + createBank: () => null + }; +}; + +let vm; +let defaultSoundAssetId; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultSoundAssetId = vm.runtime.storage.defaultAssetId.Sound; + + vm.attachAudioEngine(FakeAudioEngine()); + + return vm.loadProject(project); +}); + +const test = tap.test; + +test('load sb3 project with corrupted sound file', t => { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const catSprite = vm.runtime.targets[1]; + t.equal(catSprite.getName(), 'Sprite1'); + t.equal(catSprite.getSounds().length, 1); + + const corruptedSound = catSprite.getSounds()[0]; + t.equal(corruptedSound.name, 'Boop Sound Recording'); + t.equal(corruptedSound.assetId, defaultSoundAssetId); + t.equal(corruptedSound.dataFormat, 'wav'); + // Runtime should have info about broken asset + t.ok(corruptedSound.broken); + t.equal(corruptedSound.broken.assetId, brokenSoundMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedSound.broken.asset.data), brokenSoundMd5); + + t.end(); +}); + +test('load and then save project with corrupted sound file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const catSprite = resavedProject.targets[1]; + t.equal(catSprite.name, 'Sprite1'); + t.equal(catSprite.sounds.length, 1); + + const corruptedSound = catSprite.sounds[0]; + t.equal(corruptedSound.name, 'Boop Sound Recording'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedSound.assetId, brokenSoundMd5); + t.equal(corruptedSound.dataFormat, 'wav'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedSound.broken); + + t.end(); +}); + +test('serializeSounds saves orignal broken sound', t => { + const soundDescs = serializeSounds(vm.runtime, vm.runtime.targets[1].id); + t.equal(soundDescs.length, 1); + const sound = soundDescs[0]; + t.equal(sound.fileName, `${brokenSoundMd5}.wav`); + t.equal(md5(sound.fileContent), brokenSoundMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb3_corrupted_svg.js b/local-scratch-vm/test/integration/sb3_corrupted_svg.js new file mode 100644 index 0000000000000000000000000000000000000000..281c1c60464ead2704a0e2516c3be844a597de02 --- /dev/null +++ b/local-scratch-vm/test/integration/sb3_corrupted_svg.js @@ -0,0 +1,107 @@ +/** + * This test mocks render breaking on loading a corrupted vector costume. + * The VM should handle this safely by displaying a Gray Question Mark, + * but keeping track of the original costume data and serializing the + * original costume data back out. The saved project.json should not + * reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb3'); +const project = readFileToBuffer(projectUri); +const costumeFileName = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb.svg'; +const originalCostume = extractAsset(projectUri, costumeFileName); +// We need to get the actual md5 because we hand modified the svg to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenCostumeMd5 = md5(originalCostume); + +let vm; +let defaultVectorAssetId; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector; + + // Mock renderer breaking on loading a corrupt costume + FakeRenderer.prototype.createSVGSkin = function (svgString) { + // Look for text added to costume to make it a corrupt svg + if (svgString.includes(' { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = vm.runtime.targets[1]; + t.equal(blueGuySprite.getName(), 'Blue Square Guy'); + t.equal(blueGuySprite.getCostumes().length, 1); + + const corruptedCostume = blueGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'costume1'); + t.equal(corruptedCostume.assetId, defaultVectorAssetId); + t.equal(corruptedCostume.dataFormat, 'svg'); + // Runtime should have info about broken asset + t.ok(corruptedCostume.broken); + t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5); + + t.end(); +}); + +test('load and then save project with corrupted vector costume file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = resavedProject.targets[1]; + t.equal(blueGuySprite.name, 'Blue Square Guy'); + t.equal(blueGuySprite.costumes.length, 1); + + const corruptedCostume = blueGuySprite.costumes[0]; + t.equal(corruptedCostume.name, 'costume1'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedCostume.assetId, brokenCostumeMd5); + t.equal(corruptedCostume.dataFormat, 'svg'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedCostume.broken); + + t.end(); +}); + +test('serializeCostume saves orignal broken costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id); + t.equal(costumeDescs.length, 1); + const costume = costumeDescs[0]; + t.equal(costume.fileName, `${brokenCostumeMd5}.svg`); + t.equal(md5(costume.fileContent), brokenCostumeMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb3_missing_png.js b/local-scratch-vm/test/integration/sb3_missing_png.js new file mode 100644 index 0000000000000000000000000000000000000000..453136ffdbb45e30db45bc98acc391c5f5b16de9 --- /dev/null +++ b/local-scratch-vm/test/integration/sb3_missing_png.js @@ -0,0 +1,113 @@ +/** + * This test ensures that the VM gracefully handles an sb3 project with + * a missing bitmap costume. The VM should handle this safely by displaying + * a Gray Question Mark, but keeping track of the original costume data + * and serializing the original costume data back out. The saved project.json + * should not reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb3'); +const project = readFileToBuffer(projectUri); + + +const missingCostumeAssetId = 'e1320c21995dcf6de10119be7f08c26b'; + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => image.onload(), 100); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; + +tap.setTimeout(30000); + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project); +}); + +const test = tap.test; + +test('loading sb3 project with missing bitmap costume file', t => { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = vm.runtime.targets[1]; + t.equal(greenGuySprite.getName(), 'Green Guy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const missingCostume = greenGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'Green Guy'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap; + t.equal(missingCostume.assetId, defaultVectorAssetId); + t.equal(missingCostume.dataFormat, 'png'); + // Runtime should have info about broken asset + t.ok(missingCostume.broken); + t.equal(missingCostume.broken.assetId, missingCostumeAssetId); + + t.end(); +}); + +test('load and then save sb3 project with missing costume file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = resavedProject.targets[1]; + t.equal(greenGuySprite.name, 'Green Guy'); + t.equal(greenGuySprite.costumes.length, 1); + + const missingCostume = greenGuySprite.costumes[0]; + t.equal(missingCostume.name, 'Green Guy'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + t.equal(missingCostume.assetId, missingCostumeAssetId); + t.equal(missingCostume.dataFormat, 'png'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingCostume.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const costumeDescs = serializeCostumes(vm.runtime); + + t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop + t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.png`); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb3_missing_sound.js b/local-scratch-vm/test/integration/sb3_missing_sound.js new file mode 100644 index 0000000000000000000000000000000000000000..16927bd1071f8f24adf2d7f8ce6099802f79f734 --- /dev/null +++ b/local-scratch-vm/test/integration/sb3_missing_sound.js @@ -0,0 +1,87 @@ +/** + * This test ensures that the VM gracefully handles an sb3 project with + * a missing sound. The project should load without error. + * TODO: handle missing or corrupted sounds by replacing the missing sound data + * with the empty sound file but keeping the info about the original missing / corrupted sound + * so that user data does not get overwritten / lost. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeSounds} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/missing_sound.sb3'); +const project = readFileToBuffer(projectUri); + +const missingSoundAssetId = '78618aadd225b1db7bf837fa17dc0568'; + +let vm; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + + return vm.loadProject(project); +}); + +const test = tap.test; + +test('loading sb3 project with missing sound file', t => { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const catSprite = vm.runtime.targets[1]; + t.equal(catSprite.getSounds().length, 1); + + const missingSound = catSprite.getSounds()[0]; + t.equal(missingSound.name, 'Boop Sound Recording'); + // Sound should have original data but no asset + const defaultSoundAssetId = vm.runtime.storage.defaultAssetId.Sound; + t.equal(missingSound.assetId, defaultSoundAssetId); + t.equal(missingSound.dataFormat, 'wav'); + + // Runtime should have info about broken asset + t.ok(missingSound.broken); + t.equal(missingSound.broken.assetId, missingSoundAssetId); + + t.end(); +}); + +test('load and then save sb3 project with missing sound file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const catSprite = resavedProject.targets[1]; + t.equal(catSprite.name, 'Sprite1'); + t.equal(catSprite.sounds.length, 1); + + const missingSound = catSprite.sounds[0]; + t.equal(missingSound.name, 'Boop Sound Recording'); + // Costume should have both default sound data (e.g. "Gray Question Sound" ^_^) and original data + t.equal(missingSound.assetId, missingSoundAssetId); + t.equal(missingSound.dataFormat, 'wav'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingSound.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const soundDescs = serializeSounds(vm.runtime); + + t.equal(soundDescs.length, 1); // Should only have one sound, the pop sound for the stage + t.not(soundDescs[0].fileName, `${missingSoundAssetId}.wav`); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sb3_missing_svg.js b/local-scratch-vm/test/integration/sb3_missing_svg.js new file mode 100644 index 0000000000000000000000000000000000000000..523f3459ec0800c8b59f5da7129162a4485e6115 --- /dev/null +++ b/local-scratch-vm/test/integration/sb3_missing_svg.js @@ -0,0 +1,90 @@ +/** + * This test ensures that the VM gracefully handles an sb3 project with + * a missing vector costume. The VM should handle this safely by displaying + * a Gray Question Mark, but keeping track of the original costume data + * and serializing the original costume data back out. The saved project.json + * should not reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb3'); +const project = readFileToBuffer(projectUri); + +const missingCostumeAssetId = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb'; + +let vm; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + vm.attachRenderer(new FakeRenderer()); + + return vm.loadProject(project); +}); + +const test = tap.test; + +test('loading sb3 project with missing vector costume file', t => { + t.equal(vm.runtime.targets.length, 2); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = vm.runtime.targets[1]; + t.equal(blueGuySprite.getName(), 'Blue Square Guy'); + t.equal(blueGuySprite.getCostumes().length, 1); + + const missingCostume = blueGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'costume1'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector; + t.equal(missingCostume.assetId, defaultVectorAssetId); + t.equal(missingCostume.dataFormat, 'svg'); + // Runtime should have info about broken asset + t.ok(missingCostume.broken); + t.equal(missingCostume.broken.assetId, missingCostumeAssetId); + + t.end(); +}); + +test('load and then save sb3 project with missing costume file', t => { + const resavedProject = JSON.parse(vm.toJSON()); + + t.equal(resavedProject.targets.length, 2); + + const stage = resavedProject.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = resavedProject.targets[1]; + t.equal(blueGuySprite.name, 'Blue Square Guy'); + t.equal(blueGuySprite.costumes.length, 1); + + const missingCostume = blueGuySprite.costumes[0]; + t.equal(missingCostume.name, 'costume1'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + t.equal(missingCostume.assetId, missingCostumeAssetId); + t.equal(missingCostume.dataFormat, 'svg'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingCostume.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const costumeDescs = serializeCostumes(vm.runtime); + + t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop + t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.svg`); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sensing.js b/local-scratch-vm/test/integration/sensing.js new file mode 100644 index 0000000000000000000000000000000000000000..9deece6253bee9095b221f3d184441a02ce4ed48 --- /dev/null +++ b/local-scratch-vm/test/integration/sensing.js @@ -0,0 +1,38 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const uri = path.resolve(__dirname, '../fixtures/sensing.sb2'); +const project = readFileToBuffer(uri); + +test('sensing', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length > 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/sound.js b/local-scratch-vm/test/integration/sound.js new file mode 100644 index 0000000000000000000000000000000000000000..8ba9e666c542f7f0aa9f9ac825ba87d0b20602c9 --- /dev/null +++ b/local-scratch-vm/test/integration/sound.js @@ -0,0 +1,43 @@ +const Worker = require('tiny-worker'); +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const dispatch = require('../../src/dispatch/central-dispatch'); + +const uri = path.resolve(__dirname, '../fixtures/sound.sb2'); +const project = readFileToBuffer(uri); + +// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead. +dispatch.workerClass = Worker; + +test('sound', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.ok(threads.length > 0); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/sprite2_corrupted_png.js b/local-scratch-vm/test/integration/sprite2_corrupted_png.js new file mode 100644 index 0000000000000000000000000000000000000000..51c2d0775a2d6aaa62420105a074f954e2f18e65 --- /dev/null +++ b/local-scratch-vm/test/integration/sprite2_corrupted_png.js @@ -0,0 +1,127 @@ +/** + * This test mocks render breaking on loading a sprite2 with a + * corrupted bitmap costume. + * The VM should handle this safely by displaying a Gray Question Mark, + * but keeping track of the original costume data and serializing the + * original costume data back out. The saved project.json should not + * reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_png.sprite2'); +const sprite = readFileToBuffer(spriteUri); + +const costumeFileName = '0.png'; +const originalCostume = extractAsset(spriteUri, costumeFileName); +// We need to get the actual md5 because we hand modified the png to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenCostumeMd5 = md5(originalCostume); + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => { + const base64Image = image.src.split(',')[1]; + const decodedText = Buffer.from(base64Image, 'base64').toString(); + if (decodedText.includes('Here is some')) { + image.onerror(); + } else { + image.onload(); + } + }, 100); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; +let defaultBitmapAssetId; + +tap.setTimeout(30000); + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap; + + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project).then(() => vm.addSprite(sprite)); +}); + +const test = tap.test; + +test('load sprite2 with corrupted bitmap costume file', t => { + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = vm.runtime.targets[2]; + t.equal(greenGuySprite.getName(), 'GreenGuy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const corruptedCostume = greenGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'GreenGuy'); + t.equal(corruptedCostume.assetId, defaultBitmapAssetId); + t.equal(corruptedCostume.dataFormat, 'png'); + // Runtime should have info about broken asset + t.ok(corruptedCostume.broken); + t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5); + + t.end(); +}); + +test('load and then save sprite with corrupted costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'GreenGuy'); + t.equal(resavedSprite.costumes.length, 1); + + const corruptedCostume = resavedSprite.costumes[0]; + t.equal(corruptedCostume.name, 'GreenGuy'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedCostume.assetId, brokenCostumeMd5); + t.equal(corruptedCostume.dataFormat, 'png'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedCostume.broken); + + t.end(); +}); + +test('serializeCostume saves orignal broken costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id); + t.equal(costumeDescs.length, 1); + const costume = costumeDescs[0]; + t.equal(costume.fileName, `${brokenCostumeMd5}.png`); + t.equal(md5(costume.fileContent), brokenCostumeMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sprite2_corrupted_svg.js b/local-scratch-vm/test/integration/sprite2_corrupted_svg.js new file mode 100644 index 0000000000000000000000000000000000000000..45b8e6a6b08fd134df29c9cdb59054dfb9daad71 --- /dev/null +++ b/local-scratch-vm/test/integration/sprite2_corrupted_svg.js @@ -0,0 +1,126 @@ +/** + * This test mocks render breaking on loading a sprite2 with a + * corrupted vector costume. + * The VM should handle this safely by displaying a Gray Question Mark, + * but keeping track of the original costume data and serializing the + * original costume data back out. The saved project.json should not + * reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sprite2'); +const sprite = readFileToBuffer(spriteUri); + +const costumeFileName = '0.svg'; +const originalCostume = extractAsset(spriteUri, costumeFileName); +// We need to get the actual md5 because we hand modified the svg to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenCostumeMd5 = md5(originalCostume); + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => image.onload(), 1000); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; +let defaultVectorAssetId; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector; + + // Mock renderer breaking on loading a corrupt costume + FakeRenderer.prototype.createSVGSkin = function (svgString) { + // Look for text added to costume to make it a corrupt svg + if (svgString.includes(' vm.addSprite(sprite)); +}); + +const test = tap.test; + +test('load sprite2 with corrupted vector costume file', t => { + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = vm.runtime.targets[2]; + t.equal(blueGuySprite.getName(), 'Blue Guy'); + t.equal(blueGuySprite.getCostumes().length, 1); + + const corruptedCostume = blueGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'Blue Guy 2'); + t.equal(corruptedCostume.assetId, defaultVectorAssetId); + t.equal(corruptedCostume.dataFormat, 'svg'); + // Runtime should have info about broken asset + t.ok(corruptedCostume.broken); + t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5); + + t.end(); +}); + +test('load and then save sprite with corrupted costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'Blue Guy'); + t.equal(resavedSprite.costumes.length, 1); + + const corruptedCostume = resavedSprite.costumes[0]; + t.equal(corruptedCostume.name, 'Blue Guy 2'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedCostume.assetId, brokenCostumeMd5); + t.equal(corruptedCostume.dataFormat, 'svg'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedCostume.broken); + + t.end(); +}); + +test('serializeCostume saves orignal broken costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id); + t.equal(costumeDescs.length, 1); + const costume = costumeDescs[0]; + t.equal(costume.fileName, `${brokenCostumeMd5}.svg`); + t.equal(md5(costume.fileContent), brokenCostumeMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sprite2_missing_png.js b/local-scratch-vm/test/integration/sprite2_missing_png.js new file mode 100644 index 0000000000000000000000000000000000000000..66d76e4e67dea8edadb84b5e5e9be98a7c8fadf0 --- /dev/null +++ b/local-scratch-vm/test/integration/sprite2_missing_png.js @@ -0,0 +1,109 @@ +/** + * This test ensures that the VM gracefully handles a sprite2 file with + * a missing bitmap costume. The VM should handle this safely by displaying + * a Gray Question Mark, but keeping track of the original costume data + * and serializing the original costume data back out. The saved project.json + * should not reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +// The particular project that we're loading doesn't matter for this test +const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/missing_png.sprite2'); +const sprite = readFileToBuffer(spriteUri); + +const missingCostumeAssetId = 'aadce129bfe4e57f0dd81478f3ed82aa'; + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => image.onload(), 100); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; + +tap.setTimeout(30000); + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project).then(() => vm.addSprite(sprite)); +}); + +const test = tap.test; + +test('loading sprite2 with missing bitmap costume file', t => { + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = vm.runtime.targets[2]; + t.equal(greenGuySprite.getName(), 'GreenGuy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const missingCostume = greenGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'GreenGuy'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + const defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap; + t.equal(missingCostume.assetId, defaultBitmapAssetId); + t.equal(missingCostume.dataFormat, 'png'); + // Runtime should have info about broken asset + t.ok(missingCostume.broken); + t.equal(missingCostume.broken.assetId, missingCostumeAssetId); + + t.end(); +}); + +test('load and then save sprite2 with missing bitmap costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'GreenGuy'); + t.equal(resavedSprite.costumes.length, 1); + + const missingCostume = resavedSprite.costumes[0]; + t.equal(missingCostume.name, 'GreenGuy'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + t.equal(missingCostume.assetId, missingCostumeAssetId); + t.equal(missingCostume.dataFormat, 'png'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingCostume.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id); + + t.equal(costumeDescs.length, 0); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sprite2_missing_svg.js b/local-scratch-vm/test/integration/sprite2_missing_svg.js new file mode 100644 index 0000000000000000000000000000000000000000..e53885f387afb112e22abd5be3277a9c3335102e --- /dev/null +++ b/local-scratch-vm/test/integration/sprite2_missing_svg.js @@ -0,0 +1,107 @@ +/** + * This test ensures that the VM gracefully handles a sprite2 file with + * a missing vector costume. The VM should handle this safely by displaying + * a Gray Question Mark, but keeping track of the original costume data + * and serializing the original costume data back out. The saved project.json + * should not reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +// The particular project that we're loading doesn't matter for this test +const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/missing_svg.sprite2'); +const sprite = readFileToBuffer(spriteUri); + +const missingCostumeAssetId = 'beca8009621913e2f5b3111eed2d8210'; + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => image.onload(), 1000); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project).then(() => vm.addSprite(sprite)); +}); + +const test = tap.test; + +test('loading sprite2 with missing vector costume file', t => { + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = vm.runtime.targets[2]; + t.equal(blueGuySprite.getName(), 'Blue Guy'); + t.equal(blueGuySprite.getCostumes().length, 1); + + const missingCostume = blueGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'Blue Guy 2'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector; + t.equal(missingCostume.assetId, defaultVectorAssetId); + t.equal(missingCostume.dataFormat, 'svg'); + // Runtime should have info about broken asset + t.ok(missingCostume.broken); + t.equal(missingCostume.broken.assetId, missingCostumeAssetId); + + t.end(); +}); + +test('load and then save sprite2 with missing vector costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'Blue Guy'); + t.equal(resavedSprite.costumes.length, 1); + + const missingCostume = resavedSprite.costumes[0]; + t.equal(missingCostume.name, 'Blue Guy 2'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + t.equal(missingCostume.assetId, missingCostumeAssetId); + t.equal(missingCostume.dataFormat, 'svg'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingCostume.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id); + + t.equal(costumeDescs.length, 0); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sprite3_corrupted_png.js b/local-scratch-vm/test/integration/sprite3_corrupted_png.js new file mode 100644 index 0000000000000000000000000000000000000000..80b06ba3f1daa910b8c67e894721bae292142d11 --- /dev/null +++ b/local-scratch-vm/test/integration/sprite3_corrupted_png.js @@ -0,0 +1,127 @@ +/** + * This test mocks render breaking on loading a sprite3 with a + * corrupted bitmap costume. + * The VM should handle this safely by displaying a Gray Question Mark, + * but keeping track of the original costume data and serializing the + * original costume data back out. The saved project.json should not + * reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_png.sprite3'); +const sprite = readFileToBuffer(spriteUri); + +const costumeFileName = 'e1320c21995dcf6de10119be7f08c26b.png'; +const originalCostume = extractAsset(spriteUri, costumeFileName); +// We need to get the actual md5 because we hand modified the png to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenCostumeMd5 = md5(originalCostume); + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => { + const base64Image = image.src.split(',')[1]; + const decodedText = Buffer.from(base64Image, 'base64').toString(); + if (decodedText.includes('Here is some')) { + image.onerror(); + } else { + image.onload(); + } + }, 100); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; +let defaultBitmapAssetId; + +tap.setTimeout(30000); + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap; + + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project).then(() => vm.addSprite(sprite)); +}); + +const test = tap.test; + +test('load sprite3 with corrupted bitmap costume file', t => { + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = vm.runtime.targets[2]; + t.equal(greenGuySprite.getName(), 'Green Guy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const corruptedCostume = greenGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'Green Guy'); + t.equal(corruptedCostume.assetId, defaultBitmapAssetId); + t.equal(corruptedCostume.dataFormat, 'png'); + // Runtime should have info about broken asset + t.ok(corruptedCostume.broken); + t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5); + + t.end(); +}); + +test('load and then save sprite with corrupted costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'Green Guy'); + t.equal(resavedSprite.costumes.length, 1); + + const corruptedCostume = resavedSprite.costumes[0]; + t.equal(corruptedCostume.name, 'Green Guy'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedCostume.assetId, brokenCostumeMd5); + t.equal(corruptedCostume.dataFormat, 'png'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedCostume.broken); + + t.end(); +}); + +test('serializeCostume saves orignal broken costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id); + t.equal(costumeDescs.length, 1); + const costume = costumeDescs[0]; + t.equal(costume.fileName, `${brokenCostumeMd5}.png`); + t.equal(md5(costume.fileContent), brokenCostumeMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sprite3_corrupted_svg.js b/local-scratch-vm/test/integration/sprite3_corrupted_svg.js new file mode 100644 index 0000000000000000000000000000000000000000..9bbe96bc950e799537248b7842ef332e6189308c --- /dev/null +++ b/local-scratch-vm/test/integration/sprite3_corrupted_svg.js @@ -0,0 +1,106 @@ +/** + * This test mocks render breaking on loading a sprite with a + * corrupted vector costume. + * The VM should handle this safely by displaying a Gray Question Mark, + * but keeping track of the original costume data and serializing the + * original costume data back out. The saved project.json should not + * reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const md5 = require('js-md5'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sprite3'); +const sprite = readFileToBuffer(spriteUri); + +const costumeFileName = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb.svg'; +const originalCostume = extractAsset(spriteUri, costumeFileName); +// We need to get the actual md5 because we hand modified the svg to corrupt it +// after we downloaded the project from Scratch +// Loading the project back into the VM will correct the assetId and md5 +const brokenCostumeMd5 = md5(originalCostume); + +let vm; +let defaultVectorAssetId; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector; + + // Mock renderer breaking on loading a corrupt costume + FakeRenderer.prototype.createSVGSkin = function (svgString) { + // Look for text added to costume to make it a corrupt svg + if (svgString.includes(' vm.addSprite(sprite)); +}); + +const test = tap.test; + +test('load sprite3 with corrupted vector costume file', t => { + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = vm.runtime.targets[2]; + t.equal(blueGuySprite.getName(), 'Blue Square Guy'); + t.equal(blueGuySprite.getCostumes().length, 1); + + const corruptedCostume = blueGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'costume1'); + t.equal(corruptedCostume.assetId, defaultVectorAssetId); + t.equal(corruptedCostume.dataFormat, 'svg'); + // Runtime should have info about broken asset + t.ok(corruptedCostume.broken); + t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5); + // Verify that we saved the original asset data + t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5); + + t.end(); +}); + +test('load and then save sprite with corrupted costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'Blue Square Guy'); + t.equal(resavedSprite.costumes.length, 1); + + const corruptedCostume = resavedSprite.costumes[0]; + t.equal(corruptedCostume.name, 'costume1'); + // Resaved project costume should have the metadata that corresponds to the original broken costume + t.equal(corruptedCostume.assetId, brokenCostumeMd5); + t.equal(corruptedCostume.dataFormat, 'svg'); + // Test that we didn't save any data about the costume being broken + t.notOk(corruptedCostume.broken); + + t.end(); +}); + +test('serializeCostume saves orignal broken costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id); + t.equal(costumeDescs.length, 1); + const costume = costumeDescs[0]; + t.equal(costume.fileName, `${brokenCostumeMd5}.svg`); + t.equal(md5(costume.fileContent), brokenCostumeMd5); + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sprite3_missing_png.js b/local-scratch-vm/test/integration/sprite3_missing_png.js new file mode 100644 index 0000000000000000000000000000000000000000..0dfede906f4e1f3569c214458a8d22247d3d542d --- /dev/null +++ b/local-scratch-vm/test/integration/sprite3_missing_png.js @@ -0,0 +1,109 @@ +/** + * This test ensures that the VM gracefully handles a sprite3 file with + * a missing bitmap costume. The VM should handle this safely by displaying + * a Gray Question Mark, but keeping track of the original costume data + * and serializing the original costume data back out. The saved project.json + * should not reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +// The particular project that we're loading doesn't matter for this test +const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/missing_png.sprite3'); +const sprite = readFileToBuffer(spriteUri); + +const missingCostumeAssetId = 'e1320c21995dcf6de10119be7f08c26b'; + +global.Image = function () { + const image = { + width: 1, + height: 1 + }; + setTimeout(() => image.onload(), 100); + return image; +}; + +global.document = { + createElement: () => ({ + // Create mock canvas + getContext: () => ({ + drawImage: () => ({}) + }) + }) +}; + +let vm; + +tap.setTimeout(30000); + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + vm.attachRenderer(new FakeRenderer()); + vm.attachV2BitmapAdapter(new FakeBitmapAdapter()); + + return vm.loadProject(project).then(() => vm.addSprite(sprite)); +}); + +const test = tap.test; + +test('loading sprite3 with missing bitmap costume file', t => { + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const greenGuySprite = vm.runtime.targets[2]; + t.equal(greenGuySprite.getName(), 'Green Guy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const missingCostume = greenGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'Green Guy'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + const defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap; + t.equal(missingCostume.assetId, defaultBitmapAssetId); + t.equal(missingCostume.dataFormat, 'png'); + // Runtime should have info about broken asset + t.ok(missingCostume.broken); + t.equal(missingCostume.broken.assetId, missingCostumeAssetId); + + t.end(); +}); + +test('load and then save sprite3 with missing bitmap costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'Green Guy'); + t.equal(resavedSprite.costumes.length, 1); + + const missingCostume = resavedSprite.costumes[0]; + t.equal(missingCostume.name, 'Green Guy'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + t.equal(missingCostume.assetId, missingCostumeAssetId); + t.equal(missingCostume.dataFormat, 'png'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingCostume.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id); + + t.equal(costumeDescs.length, 0); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/sprite3_missing_svg.js b/local-scratch-vm/test/integration/sprite3_missing_svg.js new file mode 100644 index 0000000000000000000000000000000000000000..6bd0985f884cee4ba0e6e6b579a11d9ac0acad25 --- /dev/null +++ b/local-scratch-vm/test/integration/sprite3_missing_svg.js @@ -0,0 +1,87 @@ +/** + * This test ensures that the VM gracefully handles a sprite3 file with + * a missing vector costume. The VM should handle this safely by displaying + * a Gray Question Mark, but keeping track of the original costume data + * and serializing the original costume data back out. The saved project.json + * should not reflect that the costume is broken and should therefore re-attempt + * to load the costume if the saved project is re-loaded. + */ +const path = require('path'); +const tap = require('tap'); +const makeTestStorage = require('../fixtures/make-test-storage'); +const FakeRenderer = require('../fixtures/fake-renderer'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const {serializeCostumes} = require('../../src/serialization/serialize-assets'); + +// The particular project that we're loading doesn't matter for this test +const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); +const project = readFileToBuffer(projectUri); + +const spriteUri = path.resolve(__dirname, '../fixtures/missing_svg.sprite3'); +const sprite = readFileToBuffer(spriteUri); + +const missingCostumeAssetId = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb'; + +let vm; + +tap.beforeEach(() => { + const storage = makeTestStorage(); + + vm = new VirtualMachine(); + vm.attachStorage(storage); + vm.attachRenderer(new FakeRenderer()); + + return vm.loadProject(project).then(() => vm.addSprite(sprite)); +}); + +const test = tap.test; + +test('loading sprite3 with missing vector costume file', t => { + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + t.ok(stage.isStage); + + const blueGuySprite = vm.runtime.targets[2]; + t.equal(blueGuySprite.getName(), 'Blue Square Guy'); + t.equal(blueGuySprite.getCostumes().length, 1); + + const missingCostume = blueGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'costume1'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector; + t.equal(missingCostume.assetId, defaultVectorAssetId); + t.equal(missingCostume.dataFormat, 'svg'); + // Runtime should have info about broken asset + t.ok(missingCostume.broken); + t.equal(missingCostume.broken.assetId, missingCostumeAssetId); + + t.end(); +}); + +test('load and then save sprite3 with missing vector costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'Blue Square Guy'); + t.equal(resavedSprite.costumes.length, 1); + + const missingCostume = resavedSprite.costumes[0]; + t.equal(missingCostume.name, 'costume1'); + // Costume should have both default cosutme (e.g. Gray Question Mark) data and original data + t.equal(missingCostume.assetId, missingCostumeAssetId); + t.equal(missingCostume.dataFormat, 'svg'); + // Test that we didn't save any data about the costume being broken + t.notOk(missingCostume.broken); + + t.end(); +}); + +test('serializeCostume does not save data for missing costume', t => { + const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id); + + t.equal(costumeDescs.length, 0); + + t.end(); + process.nextTick(process.exit); +}); diff --git a/local-scratch-vm/test/integration/stack-click.js b/local-scratch-vm/test/integration/stack-click.js new file mode 100644 index 0000000000000000000000000000000000000000..b2e2ec17135cd89bffc6232afed4b24ae20cfd6f --- /dev/null +++ b/local-scratch-vm/test/integration/stack-click.js @@ -0,0 +1,59 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/stack-click.sb2'); +const project = readFileToBuffer(projectUri); + +/** + * stack-click.sb2 contains a sprite at (0, 0) with a single stack + * when timer > 100000000 + * move 100 steps + * The intention is to make sure that the stack can be activated by a stack click + * even when the hat predicate is false. + */ +test('stack click activates the stack', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', () => { + // The sprite should have moved 100 to the right + t.equal(vm.editingTarget.x, 100); + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + const blockContainer = vm.runtime.targets[1].blocks; + const allBlocks = blockContainer._blocks; + + // Confirm the editing target is initially at 0 + t.equal(vm.editingTarget.x, 0); + + // Find hat for greater than and click it + for (const blockId in allBlocks) { + if (allBlocks[blockId].opcode === 'event_whengreaterthan') { + blockContainer.blocklyListen({ + blockId: blockId, + element: 'stackclick' + }); + } + } + + // After two seconds, get playground data and stop + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); + }); + }); +}); diff --git a/local-scratch-vm/test/integration/tw-snapshots.js b/local-scratch-vm/test/integration/tw-snapshots.js new file mode 100644 index 0000000000000000000000000000000000000000..fe9e003c8afe520b660275101d7d96d4451d973e --- /dev/null +++ b/local-scratch-vm/test/integration/tw-snapshots.js @@ -0,0 +1,23 @@ +const {test} = require('tap'); +const Snapshots = require('../snapshot/lib'); + +for (const testCase of Snapshots.tests) { + // eslint-disable-next-line no-loop-func + test(testCase.id, async t => { + const expected = Snapshots.getExpectedSnapshot(testCase); + const actual = await Snapshots.generateActualSnapshot(testCase); + const result = Snapshots.compareSnapshots(expected, actual); + if (result === 'VALID') { + t.pass('matches'); + } else if (result === 'INPUT_MODIFIED') { + t.fail('input project changed; run: node test/snapshot --update'); + } else if (result === 'MISSING_SNAPSHOT') { + t.fail('snapshot is missing; run: node test/snapshot --update'); + } else { + // This assertion will always fail, but tap will print out the snapshots + // for comparison. + t.equal(expected, actual, 'did not match; you may have to run: node snapshot-tests --update'); + } + t.end(); + }); +} diff --git a/local-scratch-vm/test/integration/tw_addon_blocks.js b/local-scratch-vm/test/integration/tw_addon_blocks.js new file mode 100644 index 0000000000000000000000000000000000000000..a8a1dc240ae6d446b2888655150aa7534c8ca064 --- /dev/null +++ b/local-scratch-vm/test/integration/tw_addon_blocks.js @@ -0,0 +1,188 @@ +const tap = require('tap'); +const fs = require('fs'); +const path = require('path'); +const VirtualMachine = require('../../src/virtual-machine'); + +const fixtureData = fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'tw-addon-blocks.sb3')); + +const runExecutionTests = compilerEnabled => async test => { + const load = async () => { + const vm = new VirtualMachine(); + vm.setCompilerOptions({ + enabled: compilerEnabled + }); + await vm.loadProject(fixtureData); + vm.on('COMPILE_ERROR', (target, error) => test.fail(`Compile error ${target.getName()} ${error}`)); + return vm; + }; + + const getOutput = vm => vm.runtime.getTargetForStage().lookupVariableByNameAndType('output').value; + + await test.test('simple use', async t => { + t.plan(7); + + const vm = await load(); + + let calledBlock1 = false; + let calledBlock2 = false; + + vm.addAddonBlock({ + procedureCode: 'block 2 %s', + callback: (args, util) => { + calledBlock1 = true; + t.type(util.thread, 'object'); + // may have to update this ID when the project changes to match whatever the ID is for the + // procedures_call block to block 2 %s + t.equal(util.thread.peekStack(), 'c'); + t.same(args, { + 'number or text': 'banana' + }); + }, + arguments: ['number or text'] + }); + + vm.addAddonBlock({ + procedureCode: 'block 3', + // eslint-disable-next-line no-unused-vars + callback: (args, util) => { + calledBlock2 = true; + t.same(args, {}); + }, + arguments: [] + }); + + vm.greenFlag(); + vm.runtime._step(); + + t.equal(getOutput(vm), 'block 1 value'); + t.ok(calledBlock1); + t.ok(calledBlock2); + t.end(); + }); + + await test.test('yield by thread.status = STATUS_PROMISE_WAIT', async t => { + const vm = await load(); + + let threadToResume; + + vm.addAddonBlock({ + procedureCode: 'block 1', + callback: (args, util) => { + util.thread.status = 1; // STATUS_PROMISE_WAIT + threadToResume = util.thread; + }, + arguments: [] + }); + + vm.greenFlag(); + vm.runtime._step(); + if (!threadToResume) { + t.fail('did not run addon block'); + } + + t.equal(getOutput(vm), 'initial value'); + threadToResume.status = 0; // STATUS_RUNNING + vm.runtime._step(); + t.equal(getOutput(vm), 'block 3 value'); + + t.end(); + }); + + await test.test('yield by block utility methods', async t => { + const vm = await load(); + + let shouldYield = true; + + vm.addAddonBlock({ + procedureCode: 'block 1', + callback: (args, util) => { + if (shouldYield) { + util.runtime.requestRedraw(); + util.yield(); + } + }, + arguments: [] + }); + + vm.greenFlag(); + for (let i = 0; i < 10; i++) { + vm.runtime._step(); + } + t.equal(getOutput(vm), 'initial value'); + + shouldYield = false; + vm.runtime._step(); + t.equal(getOutput(vm), 'block 3 value'); + + t.end(); + }); + + await test.test('yield by returning Promise', async t => { + const vm = await load(); + + let resolveCallback; + vm.addAddonBlock({ + procedureCode: 'block 1', + callback: () => new Promise(resolve => { + resolveCallback = resolve; + }), + arguments: [] + }); + + vm.greenFlag(); + vm.runtime._step(); + t.equal(getOutput(vm), 'initial value'); + + resolveCallback(); + // Allow the promise callback to run + await Promise.resolve(); + + vm.runtime._step(); + t.equal(getOutput(vm), 'block 3 value'); + + t.end(); + }); + + test.end(); +}; + +tap.test('with compiler disabled', runExecutionTests(false)); +tap.test('with compiler enabled', runExecutionTests(true)); + +tap.test('block info', t => { + const vm = new VirtualMachine(); + + const BLOCK_INFO_ID = 'a-b'; + + vm.addAddonBlock({ + procedureCode: 'hidden %s', + arguments: ['number or text'], + callback: () => {}, + hidden: true + }); + + let blockInfo = vm.runtime._blockInfo.find(i => i.id === BLOCK_INFO_ID); + t.equal(blockInfo, undefined); + + vm.addAddonBlock({ + procedureCode: 'something %s', + arguments: ['number or text'], + callback: () => {} + }); + + blockInfo = vm.runtime._blockInfo.find(i => i.id === BLOCK_INFO_ID); + t.type(blockInfo.id, 'string'); + t.type(blockInfo.name, 'string'); + t.type(blockInfo.color1, 'string'); + t.type(blockInfo.color2, 'string'); + t.type(blockInfo.color3, 'string'); + t.same(blockInfo.blocks, [ + { + info: {}, + // eslint-disable-next-line max-len + xml: '' + } + ]); + + t.end(); +}); diff --git a/local-scratch-vm/test/integration/tw_edge_activated_hat_returns_promise.js b/local-scratch-vm/test/integration/tw_edge_activated_hat_returns_promise.js new file mode 100644 index 0000000000000000000000000000000000000000..1415a8a058a67efc5e0e2cf84b2c029463c0b6d6 --- /dev/null +++ b/local-scratch-vm/test/integration/tw_edge_activated_hat_returns_promise.js @@ -0,0 +1,62 @@ +const VM = require('../../src/virtual-machine'); +const {test} = require('tap'); +const fs = require('fs'); +const path = require('path'); + +test('edge activated hats returning promises work properly', async t => { + const vm = new VM(); + + // Compiler currently does not support edge activated hats. + vm.runtime.setCompilerOptions({ + enabled: false + }); + + // Modify event_whengreaterthan to return a Promise (like a custom extension would) and allow us + // to replace the value. This is a bit of a hack. + let hatValue = false; + vm.runtime._primitives.event_whengreaterthan = () => Promise.resolve(hatValue); + + // Track how many times the script was executed. + let sayCounter = 0; + vm.runtime.on('SAY', () => { + sayCounter++; + }); + + const projectPath = path.join(__dirname, '..', 'fixtures', 'tw-edge-activated-hat-returns-promise.sb3'); + await vm.loadProject(fs.readFileSync(projectPath)); + + const step = async (count = 1) => { + for (let i = 0; i < count; i++) { + vm.runtime._step(); + // Give promises returned by blocks a chance to resolve. + await Promise.resolve(); + } + }; + + hatValue = false; + await step(10); + t.equal(sayCounter, 0); + + hatValue = true; + await step(); + // promise can't resolve in this tick, so block shouldn't run yet + t.equal(sayCounter, 0); + await step(); + t.equal(sayCounter, 1); + await step(10); + t.equal(sayCounter, 1); + + hatValue = false; + await step(10); + t.equal(sayCounter, 1); + + hatValue = true; + await step(); + t.equal(sayCounter, 1); + await step(); + t.equal(sayCounter, 2); + await step(10); + t.equal(sayCounter, 2); + + t.end(); +}); diff --git a/local-scratch-vm/test/integration/tw_extension_and_block_xml.js b/local-scratch-vm/test/integration/tw_extension_and_block_xml.js new file mode 100644 index 0000000000000000000000000000000000000000..aa2c1647200a5be517edb23464afbe6bf5197aad --- /dev/null +++ b/local-scratch-vm/test/integration/tw_extension_and_block_xml.js @@ -0,0 +1,328 @@ +const fs = require('fs'); +const pathUtil = require('path'); +const htmlparser = require('htmlparser2'); +const {test} = require('tap'); +const VirtualMachine = require('../../src/virtual-machine'); +const Runtime = require('../../src/engine/runtime'); +const ArgumentType = require('../../src/extension-support/argument-type'); +const BlockType = require('../../src/extension-support/block-type'); + +const baseExtensionInfo = { + id: 'xmltest', + name: `<>"'&& Name`, + docsURI: `https://example.com/&''""<<>>`, + menuIconURI: `data:<>&"' category icon`, + blocks: [ + { + blockType: `block type <>&"'`, + opcode: `opcode <>&"'`, + text: `<>&"' [string argument <>&"'] [inputMenu <"'&>] [fieldMenu <"'&>] [image <"'&>]`, + blockIconURI: `'data:<>&"' block icon`, + arguments: { + [`string argument <>&"'`]: { + type: ArgumentType.STRING, + defaultValue: `default string <>&"'` + }, + [`inputMenu <"'&>`]: { + type: ArgumentType.STRING, + menu: `input <>&"'`, + defaultValue: `default input <>&"'` + }, + [`fieldMenu <"'&>`]: { + type: `argument type <>&"'`, + menu: `field <>&"'`, + defaultValue: `default field <>&"'` + }, + [`image <"'&>`]: { + type: ArgumentType.IMAGE, + dataURI: `data:<>&"' image input` + } + } + }, + { + opcode: 'button', + blockType: BlockType.BUTTON, + text: `'"><& button text`, + func: `'"><& func` + } + ], + menus: { + [`input <>&"'`]: { + acceptReporters: true, + items: [ + `1 <>&"`, + `2 <>&"`, + `3 <>&"` + ] + }, + [`field <>&"'`]: { + acceptReporters: false, + items: [ + `1 <>&"`, + `2 <>&"`, + `3 <>&"` + ] + } + } +}; + +test('XML escaped in Runtime.getBlocksXML()', t => { + // While these changes will make the extension unusable in a real editor environment, we still + // want to make sure that these fields are actually being escaped. + const mangledExtension = JSON.parse(JSON.stringify(baseExtensionInfo)); + mangledExtension.color1 = `<"'&amp;color1>`; + mangledExtension.color2 = `<"'&amp;color2>`; + mangledExtension.color3 = `<"'&amp;color3>`; + + const vm = new VirtualMachine(); + vm.extensionManager._registerInternalExtension({ + getInfo: () => mangledExtension + }); + + const xmlList = vm.runtime.getBlocksXML(); + t.type(xmlList, Array, 'getBlocksXML returns array'); + t.equal(xmlList.length, 1, 'array has 1 item'); + + const xmlEntry = xmlList[0]; + t.equal(xmlEntry.id, `xmltest`, 'id worked'); + + const parsedXml = htmlparser.parseDOM(xmlEntry.xml); + t.equal(parsedXml.length, 1, 'xml has 1 root node'); + + /* + Expected XML structure: + + + + + + + default value + + + + + default value + + + default value + + + + */ + + const category = parsedXml[0]; + t.equal(category.name, 'category', 'has '); + t.equal(category.attribs.name, '<>"'&& Name', 'escaped category name'); + t.equal(category.attribs.id, 'xmltest', 'category id'); + t.equal(category.attribs.colour, '<"'&amp;amp;color1>', 'escaped category color'); + t.equal(category.attribs.secondarycolour, '<"'&amp;amp;color2>', 'escaped category color 2'); + t.equal(category.attribs.iconuri, 'data:<>&"' category icon', 'escaped category icon'); + t.equal(category.children.length, 3, 'category has 3 children'); + + // Check docsURI + const docsButton = category.children[0]; + t.equal(docsButton.name, 'button', 'has docs '); +}; + +const testReporter = function (t, reporter) { + t.equal(reporter.json.type, 'test_reporter'); + testCategoryInfo(t, reporter); + t.equal(reporter.json.checkboxInFlyout, true); + t.equal(reporter.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND); + t.equal(reporter.json.output, 'String'); + t.notOk(reporter.json.hasOwnProperty('previousStatement')); + t.notOk(reporter.json.hasOwnProperty('nextStatement')); + t.same(reporter.json.extensions, ['scratch_extension']); + t.equal(reporter.json.message0, '%1 %2simple text'); // "%1 %2" from the block icon + t.notOk(reporter.json.hasOwnProperty('message1')); + t.same(reporter.json.args0, [ + // %1 in message0: the block icon + { + type: 'field_image', + src: 'invalid icon URI', + width: 40, + height: 40 + }, + // %2 in message0: separator between icon and text (only added when there's also an icon) + { + type: 'field_vertical_separator' + } + ]); + t.notOk(reporter.json.hasOwnProperty('args1')); + t.equal(reporter.xml, ''); +}; + +const testInlineImage = function (t, inlineImage) { + t.equal(inlineImage.json.type, 'test_inlineImage'); + testCategoryInfo(t, inlineImage); + t.equal(inlineImage.json.checkboxInFlyout, true); + t.equal(inlineImage.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND); + t.equal(inlineImage.json.output, 'String'); + t.notOk(inlineImage.json.hasOwnProperty('previousStatement')); + t.notOk(inlineImage.json.hasOwnProperty('nextStatement')); + t.notOk(inlineImage.json.extensions && inlineImage.json.extensions.length); // OK if it's absent or empty + t.equal(inlineImage.json.message0, 'text and %1'); // block text followed by inline image + t.notOk(inlineImage.json.hasOwnProperty('message1')); + t.same(inlineImage.json.args0, [ + // %1 in message0: the block icon + { + type: 'field_image', + src: 'invalid image URI', + width: 24, + height: 24, + flip_rtl: false // False by default + } + ]); + t.notOk(inlineImage.json.hasOwnProperty('args1')); + t.equal(inlineImage.xml, ''); +}; + +const testSeparator = function (t, separator) { + t.same(separator.json, null); // should be null or undefined + t.equal(separator.xml, ''); +}; + +const testCommand = function (t, command) { + t.equal(command.json.type, 'test_command'); + testCategoryInfo(t, command); + t.equal(command.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE); + t.assert(command.json.hasOwnProperty('previousStatement')); + t.assert(command.json.hasOwnProperty('nextStatement')); + t.notOk(command.json.extensions && command.json.extensions.length); // OK if it's absent or empty + t.equal(command.json.message0, 'text with %1 %2'); + t.notOk(command.json.hasOwnProperty('message1')); + t.strictSame(command.json.args0[0], { + type: 'input_value', + name: 'ARG' + }); + t.notOk(command.json.hasOwnProperty('args1')); + t.equal(command.xml, + '' + + '' + + 'default text'); +}; + +const testConditional = function (t, conditional) { + t.equal(conditional.json.type, 'test_ifElse'); + testCategoryInfo(t, conditional); + t.equal(conditional.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE); + t.ok(conditional.json.hasOwnProperty('previousStatement')); + t.ok(conditional.json.hasOwnProperty('nextStatement')); + t.notOk(conditional.json.extensions && conditional.json.extensions.length); // OK if it's absent or empty + t.equal(conditional.json.message0, 'test if %1 is spiffy and if so then'); + t.equal(conditional.json.message1, '%1'); // placeholder for substack #1 + t.equal(conditional.json.message2, 'or elsewise'); + t.equal(conditional.json.message3, '%1'); // placeholder for substack #2 + t.notOk(conditional.json.hasOwnProperty('message4')); + t.strictSame(conditional.json.args0[0], { + type: 'input_value', + name: 'THING', + check: 'Boolean' + }); + t.strictSame(conditional.json.args1[0], { + type: 'input_statement', + name: 'SUBSTACK' + }); + t.notOk(conditional.json.hasOwnProperty(conditional.json.args2)); + t.strictSame(conditional.json.args3[0], { + type: 'input_statement', + name: 'SUBSTACK2' + }); + t.notOk(conditional.json.hasOwnProperty('args4')); + t.equal(conditional.xml, ''); +}; + +const testLoop = function (t, loop) { + t.equal(loop.json.type, 'test_loop'); + testCategoryInfo(t, loop); + t.equal(loop.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE); + t.ok(loop.json.hasOwnProperty('previousStatement')); + t.notOk(loop.json.hasOwnProperty('nextStatement')); // isTerminal is set on this block + t.notOk(loop.json.extensions && loop.json.extensions.length); // OK if it's absent or empty + t.equal(loop.json.message0, 'loopty %1 loops'); + t.equal(loop.json.message1, '%1'); // placeholder for substack + t.equal(loop.json.message2, '%1'); // placeholder for loop arrow + t.notOk(loop.json.hasOwnProperty('message3')); + t.strictSame(loop.json.args0[0], { + type: 'input_value', + name: 'MANY' + }); + t.strictSame(loop.json.args1[0], { + type: 'input_statement', + name: 'SUBSTACK' + }); + t.equal(loop.json.lastDummyAlign2, 'RIGHT'); // move loop arrow to right side + t.equal(loop.json.args2[0].type, 'field_image'); + t.equal(loop.json.args2[0].flip_rtl, true); + t.notOk(loop.json.hasOwnProperty('args3')); + t.equal(loop.xml, + ''); +}; + +test('registerExtensionPrimitives', t => { + const runtime = new Runtime(); + + runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => { + const blocksInfo = categoryInfo.blocks; + t.equal(blocksInfo.length, testExtensionInfo.blocks.length); + + blocksInfo.forEach(blockInfo => { + // `true` here means "either an object or a non-empty string but definitely not null or undefined" + t.true(blockInfo.info, 'Every block and pseudo-block must have a non-empty "info" field'); + }); + + // Note that this also implicitly tests that block order is preserved + const [button, reporter, inlineImage, separator, command, conditional, loop] = blocksInfo; + + testButton(t, button); + testReporter(t, reporter); + testInlineImage(t, inlineImage); + testSeparator(t, separator); + testCommand(t, command); + testConditional(t, conditional); + testLoop(t, loop); + + t.end(); + }); + + runtime._registerExtensionPrimitives(testExtensionInfo); +}); + +test('custom field types should be added to block and EXTENSION_FIELD_ADDED callback triggered', t => { + const runtime = new Runtime(); + + runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => { + const blockInfo = categoryInfo.blocks[0]; + + // We expect that for each argument there's a corresponding -tag in the block XML + Object.values(blockInfo.info.arguments).forEach(argument => { + const regex = new RegExp(``); + t.true(regex.test(blockInfo.xml)); + }); + + }); + + let fieldAddedCallbacks = 0; + runtime.on(Runtime.EXTENSION_FIELD_ADDED, () => { + fieldAddedCallbacks++; + }); + + runtime._registerExtensionPrimitives(extensionInfoWithCustomFieldTypes); + + // Extension includes two custom field types + t.equal(fieldAddedCallbacks, 2); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/extension_microbit.js b/local-scratch-vm/test/unit/extension_microbit.js new file mode 100644 index 0000000000000000000000000000000000000000..4c68eb3fa014df5aa48cf1a010981fae20ee4bfd --- /dev/null +++ b/local-scratch-vm/test/unit/extension_microbit.js @@ -0,0 +1,12 @@ +const test = require('tap').test; +// const MicroBit = require('../../src/extensions/scratch3_microbit/index.js'); + +test('displayText', t => { + t.end(); +}); + +test('displayMatrix', t => { + t.end(); +}); + +// etc... diff --git a/local-scratch-vm/test/unit/extension_music.js b/local-scratch-vm/test/unit/extension_music.js new file mode 100644 index 0000000000000000000000000000000000000000..d17069d40efb52af83be3563c507588bdd535e3a --- /dev/null +++ b/local-scratch-vm/test/unit/extension_music.js @@ -0,0 +1,49 @@ +const test = require('tap').test; +const Music = require('../../src/extensions/scratch3_music/index.js'); + +const fakeRuntime = { + getTargetForStage: () => ({tempo: 60}), + on: () => {} // Stub out listener methods used in constructor. +}; + +const blocks = new Music(fakeRuntime); + +const util = { + stackFrame: Object.create(null), + target: { + audioPlayer: null + }, + yield: () => null +}; + +test('playDrum uses 1-indexing and wrap clamps', t => { + // Stub playDrumNum + let playedDrum; + blocks._playDrumNum = (_util, drum) => (playedDrum = drum); + + let args = {DRUM: 1}; + blocks.playDrumForBeats(args, util); + t.strictEqual(playedDrum, 0); + + args = {DRUM: blocks.DRUM_INFO.length + 1}; + blocks.playDrumForBeats(args, util); + t.strictEqual(playedDrum, 0); + + t.end(); +}); + +test('setInstrument uses 1-indexing and wrap clamps', t => { + // Stub getMusicState + const state = {currentInstrument: 0}; + blocks._getMusicState = () => state; + + let args = {INSTRUMENT: 1}; + blocks.setInstrument(args, util); + t.strictEqual(state.currentInstrument, 0); + + args = {INSTRUMENT: blocks.INSTRUMENT_INFO.length + 1}; + blocks.setInstrument(args, util); + t.strictEqual(state.currentInstrument, 0); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/extension_text_to_speech.js b/local-scratch-vm/test/unit/extension_text_to_speech.js new file mode 100644 index 0000000000000000000000000000000000000000..a6fd9dbfa6148c0a4eb3a4ae283386344b3e7d2e --- /dev/null +++ b/local-scratch-vm/test/unit/extension_text_to_speech.js @@ -0,0 +1,44 @@ +const test = require('tap').test; +const TextToSpeech = require('../../src/extensions/scratch3_text2speech/index.js'); + +const fakeStage = { + textToSpeechLanguage: null +}; + +const fakeRuntime = { + getTargetForStage: () => fakeStage, + on: () => {} // Stub out listener methods used in constructor. +}; + +const ext = new TextToSpeech(fakeRuntime); + +test('if no language is saved in the project, use default', t => { + t.strictEqual(ext.getCurrentLanguage(), 'en'); + t.end(); +}); + +test('if an unsupported language is dropped onto the set language block, use default', t => { + ext.setLanguage({LANGUAGE: 'nope'}); + t.strictEqual(ext.getCurrentLanguage(), 'en'); + t.end(); +}); + +test('if a supported language name is dropped onto the set language block, use it', t => { + ext.setLanguage({LANGUAGE: 'español'}); + t.strictEqual(ext.getCurrentLanguage(), 'es'); + t.end(); +}); + +test('get the extension locale for a supported locale that differs', t => { + ext.setLanguage({LANGUAGE: 'ja-hira'}); + t.strictEqual(ext.getCurrentLanguage(), 'ja'); + t.end(); +}); + +test('use localized spoken language name in place of localized written language name', t => { + ext.getEditorLanguage = () => 'es'; + const languageMenu = ext.getLanguageMenu(); + const localizedNameForChineseInSpanish = languageMenu.find(el => el.value === 'zh-cn').text; + t.strictEqual(localizedNameForChineseInSpanish, 'Chino (Mandarín)'); // i.e. should not be 'Chino (simplificado)' + t.end(); +}); diff --git a/local-scratch-vm/test/unit/extension_video_sensing.js b/local-scratch-vm/test/unit/extension_video_sensing.js new file mode 100644 index 0000000000000000000000000000000000000000..22c2adae848dfb4be822de18313e7a18f0446721 --- /dev/null +++ b/local-scratch-vm/test/unit/extension_video_sensing.js @@ -0,0 +1,411 @@ +const {createReadStream} = require('fs'); +const {join} = require('path'); + +const {PNG} = require('pngjs'); +const {test} = require('tap'); + +const {wrapClamp} = require('../../src/util/math-util'); + +const VideoSensing = require('../../src/extensions/scratch3_video_sensing/index.js'); +const VideoMotion = require('../../src/extensions/scratch3_video_sensing/library.js'); + +/** + * Prefix to the mock frame images used to test the video sensing extension. + * @type {string} + */ +const pngPrefix = 'extension_video_sensing_'; + +/** + * Map of frame keys to the image filenames appended to the pngPrefix. + * @type {object} + */ +const framesMap = { + center: 'center', + left: 'left-5', + left2: 'left-10', + down: 'down-10' +}; + +/** + * Asynchronously read a png file and copy its pixel data into a typed array + * VideoMotion will accept. + * @param {string} name - partial filename to read + * @returns {Promise.} pixel data of the image + */ +const readPNG = name => ( + new Promise((resolve, reject) => { + const png = new PNG(); + createReadStream(join(__dirname, `${pngPrefix}${name}.png`)) + .pipe(png) + .on('parsed', () => { + // Copy the RGBA pixel values into a separate typed array and + // cast the array to Uint32, the array format VideoMotion takes. + resolve(new Uint32Array(new Uint8ClampedArray(png.data).buffer)); + }) + .on('error', reject); + }) +); + +/** + * Read all the frames for testing asynchrnously and produce an object with + * keys following the keys in framesMap. + * @returns {object} mapping of keys in framesMap to image data read from disk + */ +const readFrames = (() => { + // Use this immediately invoking function expression (IIFE) to delay reading + // once to the first test that calls readFrames. + let _promise = null; + + return () => { + if (_promise === null) { + _promise = Promise.all(Object.keys(framesMap).map(key => readPNG(framesMap[key]))) + .then(pngs => ( + Object.keys(framesMap).reduce((frames, key, i) => { + frames[key] = pngs[i]; + return frames; + }, {}) + )); + } + return _promise; + }; +})(); + +/** + * Match if actual is within optMargin to expect. If actual is under -180, + * match if actual + 360 is near expect. If actual is over 180, match if actual + * - 360 is near expect. + * @param {number} actual - actual angle in degrees + * @param {number} expect - expected angle in degrees + * @param {number} optMargin - allowed margin between actual and expect in degrees + * @returns {boolean} true if actual is close to expect + */ +const isNearAngle = (actual, expect, optMargin = 10) => ( + (wrapClamp(actual - expect, 0, 359) < optMargin) || + (wrapClamp(actual - expect, 0, 359) > 360 - optMargin) +); + +// A fake scratch-render drawable that will be used by VideoMotion to restrain +// the area considered for motion detection in VideoMotion.getLocalMotion +const fakeDrawable = { + updateCPURenderAttributes () {}, // no-op, since isTouching always returns true + + getFastBounds () { + return { + left: -120, + top: 60, + right: 0, + bottom: -60 + }; + }, + + isTouching () { + return true; + } +}; + +// A fake MotionState used to test the stored values in +// VideoMotion.getLocalMotion, VideoSensing.videoOn and +// VideoSensing.whenMotionGreaterThan. +const fakeMotionState = { + motionFrameNumber: -1, + motionAmount: -1, + motionDirection: -Infinity +}; + +// A fake target referring to the fake drawable and MotionState. +const fakeTarget = { + drawableID: 0, + + getCustomState () { + return fakeMotionState; + }, + setCustomState () {} +}; + +const fakeRuntime = { + targets: [fakeTarget], + + // Without defined devices, VideoSensing will not try to start sampling from + // a video source. + ioDevices: null, + + renderer: { + _allDrawables: [ + fakeDrawable + ] + } +}; + +const fakeBlockUtility = { + target: fakeTarget +}; + +test('detect motionAmount between frames', t => { + t.plan(6); + + return readFrames() + .then(frames => { + const detect = new VideoMotion(); + + // Each of these pairs should have enough motion for the detector. + const framePairs = [ + [frames.center, frames.left], + [frames.center, frames.left2], + [frames.left, frames.left2], + [frames.left, frames.center], + [frames.center, frames.down], + [frames.down, frames.center] + ]; + + // Add both frames of a pair and test for motion. + let index = 0; + for (const [frame1, frame2] of framePairs) { + detect.addFrame(frame1); + detect.addFrame(frame2); + + detect.analyzeFrame(); + t.ok( + detect.motionAmount > 10, + `frame pair ${index + 1} has motion ${detect.motionAmount} over threshold (10)` + ); + index += 1; + } + + t.end(); + }); +}); + +test('detect local motionAmount between frames', t => { + t.plan(6); + + return readFrames() + .then(frames => { + const detect = new VideoMotion(); + + // Each of these pairs should have enough motion for the detector. + const framePairs = [ + [frames.center, frames.left], + [frames.center, frames.left2], + [frames.left, frames.left2], + [frames.left, frames.center], + [frames.center, frames.down], + [frames.down, frames.center] + ]; + + // Add both frames of a pair and test for local motion. + let index = 0; + for (const [frame1, frame2] of framePairs) { + detect.addFrame(frame1); + detect.addFrame(frame2); + + detect.analyzeFrame(); + detect.getLocalMotion(fakeDrawable, fakeMotionState); + t.ok( + fakeMotionState.motionAmount > 10, + `frame pair ${index + 1} has motion ${fakeMotionState.motionAmount} over threshold (10)` + ); + index += 1; + } + + t.end(); + }); +}); + +test('detect motionDirection between frames', t => { + t.plan(6); + + return readFrames() + .then(frames => { + const detect = new VideoMotion(); + + // Each of these pairs is moving in the given direction. Does the detector + // guess a value to that? + const directionMargin = 10; + const framePairs = [ + { + frames: [frames.center, frames.left], + direction: -90 + }, + { + frames: [frames.center, frames.left2], + direction: -90 + }, + { + frames: [frames.left, frames.left2], + direction: -90 + }, + { + frames: [frames.left, frames.center], + direction: 90 + }, + { + frames: [frames.center, frames.down], + direction: 180 + }, + { + frames: [frames.down, frames.center], + direction: 0 + } + ]; + + // Add both frames of a pair and check if the motionDirection is near the + // expected angle. + let index = 0; + for (const {frames: [frame1, frame2], direction} of framePairs) { + detect.addFrame(frame1); + detect.addFrame(frame2); + + detect.analyzeFrame(); + t.ok( + isNearAngle(detect.motionDirection, direction, directionMargin), + `frame pair ${index + 1} is ${detect.motionDirection.toFixed(0)} ` + + `degrees and close to ${direction} degrees` + ); + index += 1; + } + + t.end(); + }); +}); + +test('detect local motionDirection between frames', t => { + t.plan(6); + + return readFrames() + .then(frames => { + const detect = new VideoMotion(); + + // Each of these pairs is moving in the given direction. Does the detector + // guess a value to that? + const directionMargin = 10; + const framePairs = [ + { + frames: [frames.center, frames.left], + direction: -90 + }, + { + frames: [frames.center, frames.left2], + direction: -90 + }, + { + frames: [frames.left, frames.left2], + direction: -90 + }, + { + frames: [frames.left, frames.center], + direction: 90 + }, + { + frames: [frames.center, frames.down], + direction: 180 + }, + { + frames: [frames.down, frames.center], + direction: 0 + } + ]; + + // Add both frames of a pair and check if the local motionDirection is near + // the expected angle. + let index = 0; + for (const {frames: [frame1, frame2], direction} of framePairs) { + detect.addFrame(frame1); + detect.addFrame(frame2); + + detect.analyzeFrame(); + detect.getLocalMotion(fakeDrawable, fakeMotionState); + const motionDirection = fakeMotionState.motionDirection; + t.ok( + isNearAngle(motionDirection, direction, directionMargin), + `frame pair ${index + 1} is ${motionDirection.toFixed(0)} degrees and close to ${direction} degrees` + ); + index += 1; + } + + t.end(); + }); +}); + +test('videoOn returns value dependent on arguments', t => { + t.plan(4); + + return readFrames() + .then(frames => { + const sensing = new VideoSensing(fakeRuntime); + + // With these two frame test if we get expected values depending on the + // arguments to videoOn. + sensing.detect.addFrame(frames.center); + sensing.detect.addFrame(frames.left); + + const motionAmount = sensing.videoOn({ + ATTRIBUTE: VideoSensing.SensingAttribute.MOTION, + SUBJECT: VideoSensing.SensingSubject.STAGE + }, fakeBlockUtility); + t.ok( + motionAmount > 10, + `stage motionAmount ${motionAmount} is over the threshold (10)` + ); + + const localMotionAmount = sensing.videoOn({ + ATTRIBUTE: VideoSensing.SensingAttribute.MOTION, + SUBJECT: VideoSensing.SensingSubject.SPRITE + }, fakeBlockUtility); + t.ok( + localMotionAmount > 10, + `sprite motionAmount ${localMotionAmount} is over the threshold (10)` + ); + + const motionDirection = sensing.videoOn({ + ATTRIBUTE: VideoSensing.SensingAttribute.DIRECTION, + SUBJECT: VideoSensing.SensingSubject.STAGE + }, fakeBlockUtility); + t.ok( + isNearAngle(motionDirection, -90), + `stage motionDirection ${motionDirection.toFixed(0)} degrees is close to ${90} degrees` + ); + + const localMotionDirection = sensing.videoOn({ + ATTRIBUTE: VideoSensing.SensingAttribute.DIRECTION, + SUBJECT: VideoSensing.SensingSubject.SPRITE + }, fakeBlockUtility); + t.ok( + isNearAngle(localMotionDirection, -90), + `sprite motionDirection ${localMotionDirection.toFixed(0)} degrees is close to ${90} degrees` + ); + + t.end(); + }); +}); + +test('whenMotionGreaterThan returns true if local motion meets target', t => { + t.plan(2); + + return readFrames() + .then(frames => { + const sensing = new VideoSensing(fakeRuntime); + + // With these two frame test if we get expected values depending on the + // arguments to whenMotionGreaterThan. + sensing.detect.addFrame(frames.center); + sensing.detect.addFrame(frames.left); + + const over20 = sensing.whenMotionGreaterThan({ + REFERENCE: 20 + }, fakeBlockUtility); + t.ok( + over20, + `enough motion in drawable bounds to reach reference of 20` + ); + + const over80 = sensing.whenMotionGreaterThan({ + REFERENCE: 80 + }, fakeBlockUtility); + t.notOk( + over80, + `not enough motion in drawable bounds to reach reference of 80` + ); + + t.end(); + }); +}); diff --git a/local-scratch-vm/test/unit/extension_video_sensing_center.png b/local-scratch-vm/test/unit/extension_video_sensing_center.png new file mode 100644 index 0000000000000000000000000000000000000000..eed3cb73ef8ace3eab758be452d9ca927fc0a370 Binary files /dev/null and b/local-scratch-vm/test/unit/extension_video_sensing_center.png differ diff --git a/local-scratch-vm/test/unit/extension_video_sensing_down-10.png b/local-scratch-vm/test/unit/extension_video_sensing_down-10.png new file mode 100644 index 0000000000000000000000000000000000000000..109d6b8cafcb5a448ad28361ec3b1bf66d9f3aca Binary files /dev/null and b/local-scratch-vm/test/unit/extension_video_sensing_down-10.png differ diff --git a/local-scratch-vm/test/unit/extension_video_sensing_left-10.png b/local-scratch-vm/test/unit/extension_video_sensing_left-10.png new file mode 100644 index 0000000000000000000000000000000000000000..8a42a119b9d59baac78b9d79737d31158d8565fc Binary files /dev/null and b/local-scratch-vm/test/unit/extension_video_sensing_left-10.png differ diff --git a/local-scratch-vm/test/unit/extension_video_sensing_left-5.png b/local-scratch-vm/test/unit/extension_video_sensing_left-5.png new file mode 100644 index 0000000000000000000000000000000000000000..5e84c3edffb093f62f32ce35c21a94ccd9989b30 Binary files /dev/null and b/local-scratch-vm/test/unit/extension_video_sensing_left-5.png differ diff --git a/local-scratch-vm/test/unit/io_clock.js b/local-scratch-vm/test/unit/io_clock.js new file mode 100644 index 0000000000000000000000000000000000000000..03ee48fb60e01634228e19264c4c3d3e40345293 --- /dev/null +++ b/local-scratch-vm/test/unit/io_clock.js @@ -0,0 +1,37 @@ +const test = require('tap').test; +const Clock = require('../../src/io/clock'); +const Runtime = require('../../src/engine/runtime'); + +test('spec', t => { + const rt = new Runtime(); + const c = new Clock(rt); + + t.type(Clock, 'function'); + t.type(c, 'object'); + t.type(c.projectTimer, 'function'); + t.type(c.pause, 'function'); + t.type(c.resume, 'function'); + t.type(c.resetProjectTimer, 'function'); + t.end(); +}); + +test('cycle', t => { + const rt = new Runtime(); + const c = new Clock(rt); + + t.ok(c.projectTimer() <= 0.1); + setTimeout(() => { + c.resetProjectTimer(); + setTimeout(() => { + // The timer shouldn't advance until all threads have been stepped + t.ok(c.projectTimer() === 0); + c.pause(); + t.ok(c.projectTimer() === 0); + c.resume(); + t.ok(c.projectTimer() === 0); + t.end(); + }, 100); + }, 100); + rt._step(); + t.ok(c.projectTimer() > 0); +}); diff --git a/local-scratch-vm/test/unit/io_cloud.js b/local-scratch-vm/test/unit/io_cloud.js new file mode 100644 index 0000000000000000000000000000000000000000..deacc7c6cc0e50dfe63c41a6afb8bacb7018dc65 --- /dev/null +++ b/local-scratch-vm/test/unit/io_cloud.js @@ -0,0 +1,168 @@ +const test = require('tap').test; +const Cloud = require('../../src/io/cloud'); +const Target = require('../../src/engine/target'); +const Variable = require('../../src/engine/variable'); +const Runtime = require('../../src/engine/runtime'); + +test('spec', t => { + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + + t.type(cloud, 'object'); + t.type(cloud.postData, 'function'); + t.type(cloud.requestCreateVariable, 'function'); + t.type(cloud.requestUpdateVariable, 'function'); + t.type(cloud.requestRenameVariable, 'function'); + t.type(cloud.requestDeleteVariable, 'function'); + t.type(cloud.updateCloudVariable, 'function'); + t.type(cloud.setProvider, 'function'); + t.type(cloud.setStage, 'function'); + t.type(cloud.clear, 'function'); + t.end(); +}); + +test('stage and provider are null initially', t => { + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + + t.strictEquals(cloud.provider, null); + t.strictEquals(cloud.stage, null); + t.end(); +}); + +test('setProvider sets the provider', t => { + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + + const provider = { + foo: 'a fake provider' + }; + + cloud.setProvider(provider); + t.strictEquals(cloud.provider, provider); + + t.end(); +}); + +test('postData update message updates the variable', t => { + const runtime = new Runtime(); + const stage = new Target(runtime); + const fooVar = new Variable( + 'a fake var id', + 'foo', + Variable.SCALAR_TYPE, + true /* isCloud */ + ); + stage.variables[fooVar.id] = fooVar; + + t.strictEquals(fooVar.value, 0); + + const cloud = new Cloud(runtime); + cloud.setStage(stage); + cloud.postData({varUpdate: { + name: 'foo', + value: 3 + }}); + t.strictEquals(fooVar.value, 3); + t.end(); +}); + +test('requestUpdateVariable calls provider\'s updateVariable function', t => { + let updateVariableCalled = false; + let mockVarName = ''; + let mockVarValue = ''; + const mockUpdateVariable = (name, value) => { + updateVariableCalled = true; + mockVarName = name; + mockVarValue = value; + return; + }; + + const provider = { + updateVariable: mockUpdateVariable + }; + + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + cloud.setProvider(provider); + cloud.requestUpdateVariable('foo', 3); + t.equals(updateVariableCalled, true); + t.strictEquals(mockVarName, 'foo'); + t.strictEquals(mockVarValue, 3); + t.end(); +}); + +test('requestCreateVariable calls provider\'s createVariable function', t => { + let createVariableCalled = false; + const mockVariable = new Variable('a var id', 'my var', Variable.SCALAR_TYPE, false); + let mockVarName; + let mockVarValue; + const mockCreateVariable = (name, value) => { + createVariableCalled = true; + mockVarName = name; + mockVarValue = value; + return; + }; + + const provider = { + createVariable: mockCreateVariable + }; + + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + cloud.setProvider(provider); + cloud.requestCreateVariable(mockVariable); + t.equals(createVariableCalled, true); + t.strictEquals(mockVarName, 'my var'); + t.strictEquals(mockVarValue, 0); + // Calling requestCreateVariable does not set isCloud flag on variable + t.strictEquals(mockVariable.isCloud, false); + t.end(); +}); + +test('requestRenameVariable calls provider\'s renameVariable function', t => { + let renameVariableCalled = false; + let mockVarOldName; + let mockVarNewName; + const mockRenameVariable = (oldName, newName) => { + renameVariableCalled = true; + mockVarOldName = oldName; + mockVarNewName = newName; + return; + }; + + const provider = { + renameVariable: mockRenameVariable + }; + + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + cloud.setProvider(provider); + cloud.requestRenameVariable('my var', 'new var name'); + t.equals(renameVariableCalled, true); + t.strictEquals(mockVarOldName, 'my var'); + t.strictEquals(mockVarNewName, 'new var name'); + t.end(); +}); + +test('requestDeleteVariable calls provider\'s deleteVariable function', t => { + let deleteVariableCalled = false; + let mockVarName; + const mockDeleteVariable = name => { + deleteVariableCalled = true; + mockVarName = name; + return; + }; + + const provider = { + deleteVariable: mockDeleteVariable + }; + + const runtime = new Runtime(); + const cloud = new Cloud(runtime); + cloud.setProvider(provider); + cloud.requestDeleteVariable('my var'); + t.equals(deleteVariableCalled, true); + t.strictEquals(mockVarName, 'my var'); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/io_keyboard.js b/local-scratch-vm/test/unit/io_keyboard.js new file mode 100644 index 0000000000000000000000000000000000000000..441cc232151b78e7e958aa2c16c7b7e73e0380f9 --- /dev/null +++ b/local-scratch-vm/test/unit/io_keyboard.js @@ -0,0 +1,105 @@ +const test = require('tap').test; +const Keyboard = require('../../src/io/keyboard'); +const Runtime = require('../../src/engine/runtime'); + +test('spec', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + t.type(k, 'object'); + t.type(k.postData, 'function'); + t.type(k.getKeyIsDown, 'function'); + t.end(); +}); + +test('space key', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + k.postData({ + key: ' ', + isDown: true + }); + t.strictDeepEquals(k._keysPressed, ['space']); + t.strictEquals(k.getKeyIsDown('space'), true); + t.strictEquals(k.getKeyIsDown('any'), true); + t.end(); +}); + +test('letter key', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + k.postData({ + key: 'a', + isDown: true + }); + t.strictDeepEquals(k._keysPressed, ['A']); + t.strictEquals(k.getKeyIsDown(65), true); + t.strictEquals(k.getKeyIsDown('a'), true); + t.strictEquals(k.getKeyIsDown('A'), true); + t.strictEquals(k.getKeyIsDown('any'), true); + t.end(); +}); + +test('number key', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + k.postData({ + key: '1', + isDown: true + }); + t.strictDeepEquals(k._keysPressed, ['1']); + t.strictEquals(k.getKeyIsDown(49), true); + t.strictEquals(k.getKeyIsDown('1'), true); + t.strictEquals(k.getKeyIsDown('any'), true); + t.end(); +}); + +test('non-english key', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + k.postData({ + key: '日', + isDown: true + }); + t.strictDeepEquals(k._keysPressed, ['日']); + t.strictEquals(k.getKeyIsDown('日'), true); + t.strictEquals(k.getKeyIsDown('any'), true); + t.end(); +}); + +/* TW: This test is disabled because we intentionally add support for modifier keys. +test('ignore modifier key', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + k.postData({ + key: 'Shift', + isDown: true + }); + t.strictDeepEquals(k._keysPressed, []); + t.strictEquals(k.getKeyIsDown('any'), false); + t.end(); +}); +*/ + +test('keyup', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + k.postData({ + key: 'ArrowLeft', + isDown: true + }); + k.postData({ + key: 'ArrowLeft', + isDown: false + }); + t.strictDeepEquals(k._keysPressed, []); + t.strictEquals(k.getKeyIsDown('left arrow'), false); + t.strictEquals(k.getKeyIsDown('any'), false); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/io_keyboard_tw.js b/local-scratch-vm/test/unit/io_keyboard_tw.js new file mode 100644 index 0000000000000000000000000000000000000000..a365d11c5dc9368579175b40e45012f37ac1490f --- /dev/null +++ b/local-scratch-vm/test/unit/io_keyboard_tw.js @@ -0,0 +1,112 @@ +const test = require('tap').test; +const Keyboard = require('../../src/io/keyboard'); +const Runtime = require('../../src/engine/runtime'); + +test('extended spec', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + t.type(k.getLastKeyPressed, 'function'); + t.end(); +}); + +test('extended key support', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + k.postData({ + key: 'Backspace', + isDown: true + }); + t.strictDeepEquals(k._keysPressed, ['backspace']); + t.strictEqual(k.getKeyIsDown('backspace'), true); + t.end(); +}); + +test('last key pressed', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + t.strictEqual(k.getLastKeyPressed(), ''); + k.postData({ + key: 'a', + isDown: true + }); + t.strictEqual(k.getLastKeyPressed(), 'a'); + k.postData({ + key: 'b', + isDown: true + }); + t.strictEqual(k.getLastKeyPressed(), 'b'); + t.end(); +}); + +test('holding shift and key, releasing shift, then releasing key', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + // Press Shift+2 to produce @ + k.postData({ + key: '@', + isDown: true, + keyCode: 50 + }); + t.equal(k.getKeyIsDown('2'), false); + t.equal(k.getKeyIsDown('@'), true); + t.equal(k.getKeyIsDown('any'), true); + // Release shift, then release 2 + k.postData({ + key: 'Shift', + isDown: false, + keyCode: 16 + }); + k.postData({ + key: '2', + isDown: false, + keyCode: 50 + }); + t.equal(k.getKeyIsDown('@'), false); + t.equal(k.getKeyIsDown('2'), false); + t.equal(k.getKeyIsDown('any'), false); + + t.end(); +}); + +test('holding shift and key, releasing shift, waiting, then releasing key', t => { + const rt = new Runtime(); + const k = new Keyboard(rt); + + k.postData({ + key: '@', + isDown: true, + keyCode: 50 + }); + t.equal(k.getKeyIsDown('2'), false); + t.equal(k.getKeyIsDown('@'), true); + t.equal(k.getKeyIsDown('any'), true); + k.postData({ + key: 'Shift', + isDown: false, + keyCode: 16 + }); + // But 2 is still being held, so it will send a press event + k.postData({ + key: '2', + isDown: true, + keyCode: 50 + }); + t.equal(k.getKeyIsDown('@'), false); + t.equal(k.getKeyIsDown('2'), true); + t.equal(k.getKeyIsDown('any'), true); + // And now we release 2 + k.postData({ + key: '2', + isDown: false, + keyCode: 50 + }); + t.equal(k.getKeyIsDown('@'), false); + t.equal(k.getKeyIsDown('2'), false); + t.equal(k.getKeyIsDown('any'), false); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/io_mouse.js b/local-scratch-vm/test/unit/io_mouse.js new file mode 100644 index 0000000000000000000000000000000000000000..c1fa21935d83ad77ee9423a3556e2c8c6d541114 --- /dev/null +++ b/local-scratch-vm/test/unit/io_mouse.js @@ -0,0 +1,133 @@ +const test = require('tap').test; +const Mouse = require('../../src/io/mouse'); +const Runtime = require('../../src/engine/runtime'); + +test('spec', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + t.type(m, 'object'); + t.type(m.postData, 'function'); + t.type(m.getClientX, 'function'); + t.type(m.getClientY, 'function'); + t.type(m.getScratchX, 'function'); + t.type(m.getScratchY, 'function'); + t.type(m.getIsDown, 'function'); + t.end(); +}); + +test('mouseUp', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + m.postData({ + x: -20, + y: 10, + isDown: false, + canvasWidth: 480, + canvasHeight: 360 + }); + t.strictEquals(m.getClientX(), -20); + t.strictEquals(m.getClientY(), 10); + t.strictEquals(m.getScratchX(), -240); + t.strictEquals(m.getScratchY(), 170); + t.strictEquals(m.getIsDown(), false); + t.end(); +}); + +test('mouseDown', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + m.postData({ + x: 9.9, + y: 400.1, + isDown: true, + canvasWidth: 480, + canvasHeight: 360 + }); + t.strictEquals(m.getClientX(), 9.9); + t.strictEquals(m.getClientY(), 400.1); + t.strictEquals(m.getScratchX(), -230); + t.strictEquals(m.getScratchY(), -180); + t.strictEquals(m.getIsDown(), true); + t.end(); +}); + +test('at zoomed scale', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + m.postData({ + x: 240, + y: 540, + canvasWidth: 960, + canvasHeight: 720 + }); + t.strictEquals(m.getClientX(), 240); + t.strictEquals(m.getClientY(), 540); + t.strictEquals(m.getScratchX(), -120); + t.strictEquals(m.getScratchY(), -90); + t.end(); +}); + +test('mousedown activating click hats', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + const mouseMoveEvent = { + x: 10, + y: 100, + canvasWidth: 480, + canvasHeight: 360 + }; + + const dummyTarget = { + draggable: false + }; + + const mouseDownEvent = Object.assign({}, mouseMoveEvent, { + isDown: true + }); + + const mouseUpEvent = Object.assign({}, mouseMoveEvent, { + isDown: false + }); + + // Stub activateClickHats and pick function for testing + let ranClickHats = false; + m._activateClickHats = () => { + ranClickHats = true; + }; + m._pickTarget = () => dummyTarget; + + // Mouse move without mousedown + m.postData(mouseMoveEvent); + t.strictEquals(ranClickHats, false); + + // Mouse down event triggers the hats if target is not draggable + dummyTarget.draggable = false; + m.postData(mouseDownEvent); + t.strictEquals(ranClickHats, true); + + // But another mouse move while down doesn't trigger + ranClickHats = false; + m.postData(mouseDownEvent); + t.strictEquals(ranClickHats, false); + + // And it does trigger on mouse up if target is draggable + ranClickHats = false; + dummyTarget.draggable = true; + m.postData(mouseUpEvent); + t.strictEquals(ranClickHats, true); + + // And hats don't trigger if mouse down is outside canvas + ranClickHats = false; + m.postData(Object.assign({}, mouseDownEvent, { + x: 50000, + y: 50 + })); + t.strictEquals(ranClickHats, false); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/io_mouse_tw.js b/local-scratch-vm/test/unit/io_mouse_tw.js new file mode 100644 index 0000000000000000000000000000000000000000..9252caab35a1e0d49cd5bbffc56186dadb81ac70 --- /dev/null +++ b/local-scratch-vm/test/unit/io_mouse_tw.js @@ -0,0 +1,148 @@ +const test = require('tap').test; +const Mouse = require('../../src/io/mouse'); +const Runtime = require('../../src/engine/runtime'); + +test('position clamping', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + const BIG = 9999; + m.postData({ + x: BIG, + y: BIG, + canvasWidth: 480, + canvasHeight: 360 + }); + t.strictEquals(m.getClientX(), BIG); + t.strictEquals(m.getClientY(), BIG); + t.strictEquals(m.getScratchX(), 240); + t.strictEquals(m.getScratchY(), -180); + t.end(); +}); + +test('mouseButtonDown', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + t.strictEquals(m.getButtonIsDown(0), false); + t.strictEquals(m.getButtonIsDown(1), false); + t.strictEquals(m.getButtonIsDown(2), false); + m.postData({ + isDown: true, + button: 0 + }); + t.strictEquals(m.getButtonIsDown(0), true); + t.strictEquals(m.getButtonIsDown(1), false); + t.strictEquals(m.getButtonIsDown(2), false); + m.postData({ + isDown: true, + button: 2 + }); + t.strictEquals(m.getButtonIsDown(0), true); + t.strictEquals(m.getButtonIsDown(1), false); + t.strictEquals(m.getButtonIsDown(2), true); + m.postData({ + isDown: false, + button: 2 + }); + t.strictEquals(m.getButtonIsDown(0), true); + t.strictEquals(m.getButtonIsDown(1), false); + t.strictEquals(m.getButtonIsDown(2), false); + t.end(); +}); + +test('mouseDown with buttons', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + t.strictEquals(m.getIsDown(), false); + m.postData({ + isDown: true, + button: 0 + }); + t.strictEquals(m.getIsDown(), true); + m.postData({ + isDown: true, + button: 2 + }); + t.strictEquals(m.getIsDown(), true); + m.postData({ + isDown: false, + button: 2 + }); + t.strictEquals(m.getIsDown(), false); + t.end(); +}); + +test('missing button is treated as left', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + t.strictEquals(m.getButtonIsDown(0), false); + m.postData({ + isDown: true + }); + t.strictEquals(m.getButtonIsDown(0), true); + m.postData({ + isDown: false + }); + t.strictEquals(m.getButtonIsDown(0), false); + t.end(); +}); + +test('usesRightClickDown', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + t.strictEquals(m.usesRightClickDown, false); + t.strictEquals(m.getButtonIsDown(2), false); + t.strictEquals(m.usesRightClickDown, true); + t.end(); +}); + +test('no rounding when misc limits disabled', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + m.postData({ + x: 241, + y: 541, + canvasWidth: 960, + canvasHeight: 720 + }); + t.equal(m.getScratchX(), -119); + t.equal(m.getScratchY(), -90); + + rt.setRuntimeOptions({ + miscLimits: false + }); + t.equal(m.getScratchX(), -119.5); + t.equal(m.getScratchY(), -90.5); + + t.end(); +}); + +test('accepts 0 as x and y position', t => { + const rt = new Runtime(); + const m = new Mouse(rt); + + m.postData({ + x: 1, + y: 2, + canvasWidth: 480, + canvasHeight: 360 + }); + t.equal(m.getClientX(), 1); + t.equal(m.getClientY(), 2); + + m.postData({ + x: 0, + y: 0, + canvasWidth: 480, + canvasHeight: 360 + }); + t.equal(m.getClientX(), 0); + t.equal(m.getClientY(), 0); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/io_mousewheel.js b/local-scratch-vm/test/unit/io_mousewheel.js new file mode 100644 index 0000000000000000000000000000000000000000..f54d47cc72927b2d3c1cab4ae44fc7c0de115d06 --- /dev/null +++ b/local-scratch-vm/test/unit/io_mousewheel.js @@ -0,0 +1,44 @@ +const test = require('tap').test; +const MouseWheel = require('../../src/io/mouseWheel'); +const Runtime = require('../../src/engine/runtime'); + +test('spec', t => { + const rt = new Runtime(); + const mw = new MouseWheel(rt); + + t.type(mw, 'object'); + t.type(mw.postData, 'function'); + t.end(); +}); + +test('blocks activated by scrolling', t => { + let _startHatsArgs; + const rt = { + startHats: (...args) => { + _startHatsArgs = args; + } + }; + const mw = new MouseWheel(rt); + + _startHatsArgs = null; + mw.postData({ + deltaY: -1 + }); + t.strictEquals(_startHatsArgs[0], 'event_whenkeypressed'); + t.strictEquals(_startHatsArgs[1].KEY_OPTION, 'up arrow'); + + _startHatsArgs = null; + mw.postData({ + deltaY: +1 + }); + t.strictEquals(_startHatsArgs[0], 'event_whenkeypressed'); + t.strictEquals(_startHatsArgs[1].KEY_OPTION, 'down arrow'); + + _startHatsArgs = null; + mw.postData({ + deltaY: 0 + }); + t.strictEquals(_startHatsArgs, null); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/io_scratchBLE.js b/local-scratch-vm/test/unit/io_scratchBLE.js new file mode 100644 index 0000000000000000000000000000000000000000..8da2b3d0b4b8e528c9be806faed7954bfd5a891c --- /dev/null +++ b/local-scratch-vm/test/unit/io_scratchBLE.js @@ -0,0 +1,26 @@ +const test = require('tap').test; +// const ScratchBLE = require('../../src/io/scratchBLE'); + +test('constructor', t => { + t.end(); +}); + +test('waitForSocket', t => { + t.end(); +}); + +test('requestPeripheral', t => { + t.end(); +}); + +test('didReceiveCall', t => { + t.end(); +}); + +test('read', t => { + t.end(); +}); + +test('write', t => { + t.end(); +}); diff --git a/local-scratch-vm/test/unit/io_scratchBT.js b/local-scratch-vm/test/unit/io_scratchBT.js new file mode 100644 index 0000000000000000000000000000000000000000..843526ab6f241d460070f651a27c6b69b7133970 --- /dev/null +++ b/local-scratch-vm/test/unit/io_scratchBT.js @@ -0,0 +1,22 @@ +const test = require('tap').test; +// const ScratchBT = require('../../src/io/scratchBT'); + +test('constructor', t => { + t.end(); +}); + +test('requestPeripheral', t => { + t.end(); +}); + +test('connectPeripheral', t => { + t.end(); +}); + +test('sendMessage', t => { + t.end(); +}); + +test('didReceiveCall', t => { + t.end(); +}); diff --git a/local-scratch-vm/test/unit/io_userData.js b/local-scratch-vm/test/unit/io_userData.js new file mode 100644 index 0000000000000000000000000000000000000000..49dee9f93c544555ced58bd4bba6df39db275a56 --- /dev/null +++ b/local-scratch-vm/test/unit/io_userData.js @@ -0,0 +1,25 @@ +const test = require('tap').test; +const UserData = require('../../src/io/userData'); + +test('spec', t => { + const userData = new UserData(); + + t.type(userData, 'object'); + t.type(userData.postData, 'function'); + t.type(userData.getUsername, 'function'); + t.end(); +}); + +test('getUsername returns empty string initially', t => { + const userData = new UserData(); + + t.strictEquals(userData.getUsername(), ''); + t.end(); +}); + +test('postData sets the username', t => { + const userData = new UserData(); + userData.postData({username: 'TEST'}); + t.strictEquals(userData.getUsername(), 'TEST'); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/maybe_format_message.js b/local-scratch-vm/test/unit/maybe_format_message.js new file mode 100644 index 0000000000000000000000000000000000000000..969e25a2e1b920579e6d09f95c30c041be408b29 --- /dev/null +++ b/local-scratch-vm/test/unit/maybe_format_message.js @@ -0,0 +1,77 @@ +const test = require('tap').test; +const maybeFormatMessage = require('../../src/util/maybe-format-message'); + +const nonMessages = [ + 'hi', + 42, + true, + function () { + return 'unused'; + }, + { + a: 1, + b: 2 + }, + { + id: 'almost a message', + notDefault: 'but missing the "default" property' + }, + { + notId: 'this one is missing the "id" property', + default: 'but has "default"' + } +]; + +const argsQuick = { + speed: 'quick' +}; + +const argsOther = { + speed: 'slow' +}; + +const argsEmpty = {}; + +const simpleMessage = { + id: 'test.simpleMessage', + default: 'The quick brown fox jumped over the lazy dog.' +}; + +const complexMessage = { + id: 'test.complexMessage', + default: '{speed, select, quick {The quick brown fox jumped over the lazy dog.} other {Too slow, Gobo!}}' +}; + +const quickExpectedResult = 'The quick brown fox jumped over the lazy dog.'; +const otherExpectedResult = 'Too slow, Gobo!'; + +test('preserve non-messages', t => { + t.plan(nonMessages.length); + + for (const x of nonMessages) { + const result = maybeFormatMessage(x); + t.strictSame(x, result); + } + + t.end(); +}); + +test('format messages', t => { + const quickResult1 = maybeFormatMessage(simpleMessage); + t.strictNotSame(quickResult1, simpleMessage); + t.same(quickResult1, quickExpectedResult); + + const quickResult2 = maybeFormatMessage(complexMessage, argsQuick); + t.strictNotSame(quickResult2, complexMessage); + t.same(quickResult2, quickExpectedResult); + + const otherResult1 = maybeFormatMessage(complexMessage, argsOther); + t.strictNotSame(otherResult1, complexMessage); + t.same(otherResult1, otherExpectedResult); + + const otherResult2 = maybeFormatMessage(complexMessage, argsEmpty); + t.strictNotSame(otherResult2, complexMessage); + t.same(otherResult2, otherExpectedResult); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/mock-timer.js b/local-scratch-vm/test/unit/mock-timer.js new file mode 100644 index 0000000000000000000000000000000000000000..db5878942e25ac5e665e4591a3d426ef4028e41b --- /dev/null +++ b/local-scratch-vm/test/unit/mock-timer.js @@ -0,0 +1,91 @@ +const test = require('tap').test; +const MockTimer = require('../fixtures/mock-timer'); + +test('spec', t => { + const timer = new MockTimer(); + + t.type(MockTimer, 'function'); + t.type(timer, 'object'); + + // Most members of MockTimer mimic members of Timer. + t.type(timer.startTime, 'number'); + t.type(timer.time, 'function'); + t.type(timer.start, 'function'); + t.type(timer.timeElapsed, 'function'); + t.type(timer.setTimeout, 'function'); + t.type(timer.clearTimeout, 'function'); + + // A few members of MockTimer have no Timer equivalent and should only be used in tests. + t.type(timer.advanceMockTime, 'function'); + t.type(timer.advanceMockTimeAsync, 'function'); + t.type(timer.hasTimeouts, 'function'); + + t.end(); +}); + +test('time', t => { + const timer = new MockTimer(); + const delta = 1; + + const time1 = timer.time(); + const time2 = timer.time(); + timer.advanceMockTime(delta); + const time3 = timer.time(); + + t.equal(time1, time2); + t.equal(time2 + delta, time3); + t.end(); +}); + +test('start / timeElapsed', t => new Promise(resolve => { + const timer = new MockTimer(); + const halfDelay = 1; + const fullDelay = halfDelay + halfDelay; + + timer.start(); + + let timeoutCalled = 0; + + // Wait and measure timer + timer.setTimeout(() => { + t.equal(timeoutCalled, 0); + ++timeoutCalled; + + const timeElapsed = timer.timeElapsed(); + t.equal(timeElapsed, fullDelay); + t.end(); + + resolve(); + }, fullDelay); + + // this should not trigger the callback + timer.advanceMockTime(halfDelay); + + // give the mock timer a chance to run tasks + global.setTimeout(() => { + // we've only mock-waited for half the delay so it should not have run yet + t.equal(timeoutCalled, 0); + + // this should trigger the callback + timer.advanceMockTime(halfDelay); + }, 0); +})); + +test('clearTimeout / hasTimeouts', t => new Promise((resolve, reject) => { + const timer = new MockTimer(); + + const timeoutId = timer.setTimeout(() => { + reject(new Error('Canceled task ran')); + }, 1); + + timer.setTimeout(() => { + resolve('Non-canceled task ran'); + t.end(); + }, 2); + + timer.clearTimeout(timeoutId); + + while (timer.hasTimeouts()) { + timer.advanceMockTime(1); + } +})); diff --git a/local-scratch-vm/test/unit/project_changed_state.js b/local-scratch-vm/test/unit/project_changed_state.js new file mode 100644 index 0000000000000000000000000000000000000000..f910a603f873d6f148aef102f1077eaa2ef53fbf --- /dev/null +++ b/local-scratch-vm/test/unit/project_changed_state.js @@ -0,0 +1,242 @@ +const tap = require('tap'); +const path = require('path'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const makeTestStorage = require('../fixtures/make-test-storage'); +const VirtualMachine = require('../../src/virtual-machine'); + +let vm; +let projectChanged; + +tap.beforeEach(() => { + const projectUri = path.resolve(__dirname, '../fixtures/default.sb2'); + const project = readFileToBuffer(projectUri); + + vm = new VirtualMachine(); + + vm.runtime.addListener('PROJECT_CHANGED', () => { + projectChanged = true; + }); + + vm.attachStorage(makeTestStorage()); + return vm.loadProject(project).then(() => { + // The test in project_load_changed_state.js tests + // that loading a project does not emit a project changed + // event. This setup tries to be agnostic of whether that + // test is passing or failing. + projectChanged = false; + }); +}); + +tap.tearDown(() => process.nextTick(process.exit)); + +const test = tap.test; + +test('Adding a sprite (from sprite2) should emit a project changed event', t => { + const sprite2Uri = path.resolve(__dirname, '../fixtures/cat.sprite2'); + const sprite2 = readFileToBuffer(sprite2Uri); + + vm.addSprite(sprite2).then(() => { + t.equal(projectChanged, true); + t.end(); + }); +}); + +test('Adding a sprite (from sprite3) should emit a project changed event', t => { + const sprite3Uri = path.resolve(__dirname, '../fixtures/cat.sprite3'); + const sprite3 = readFileToBuffer(sprite3Uri); + + vm.addSprite(sprite3).then(() => { + t.equal(projectChanged, true); + t.end(); + }); +}); + +test('Adding a costume should emit a project changed event', t => { + const newCostume = { + name: 'costume1', + baseLayerID: 0, + baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg', + bitmapResolution: 1, + rotationCenterX: 47, + rotationCenterY: 55 + }; + + vm.addCostume('f9a1c175dbe2e5dee472858dd30d16bb.svg', newCostume).then(() => { + t.equal(projectChanged, true); + t.end(); + }); +}); + +test('Adding a costume from library should emit a project changed event', t => { + const newCostume = { + name: 'costume1', + baseLayerID: 0, + baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg', + bitmapResolution: 1, + rotationCenterX: 47, + rotationCenterY: 55 + }; + + vm.addCostumeFromLibrary('f9a1c175dbe2e5dee472858dd30d16bb.svg', newCostume).then(() => { + t.equal(projectChanged, true); + t.end(); + }); +}); + +test('Adding a backdrop should emit a project changed event', t => { + const newCostume = { + name: 'costume1', + baseLayerID: 0, + baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg', + bitmapResolution: 1, + rotationCenterX: 47, + rotationCenterY: 55 + }; + + vm.addBackdrop('f9a1c175dbe2e5dee472858dd30d16bb.svg', newCostume).then(() => { + t.equal(projectChanged, true); + t.end(); + }); +}); + +test('Adding a sound should emit a project changed event', t => { + const newSound = { + soundName: 'meow', + soundID: 0, + md5: '83c36d806dc92327b9e7049a565c6bff.wav', + sampleCount: 18688, + rate: 22050 + }; + + vm.addSound(newSound).then(() => { + t.equal(projectChanged, true); + t.end(); + }); +}); + +test('Deleting a sprite should emit a project changed event', t => { + const spriteId = vm.editingTarget.id; + + vm.deleteSprite(spriteId); + t.equal(projectChanged, true); + t.end(); +}); + +test('Deleting a costume should emit a project changed event', t => { + vm.deleteCostume(0); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Deleting a sound should emit a project changed event', t => { + vm.deleteSound(0); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Reordering a sprite should emit a project changed event', t => { + const sprite3Uri = path.resolve(__dirname, '../fixtures/cat.sprite3'); + const sprite3 = readFileToBuffer(sprite3Uri); + + // Add a new sprite so we have 2 to reorder + vm.addSprite(sprite3).then(() => { + // Reset the project changed flag to ignore change from adding new sprite + projectChanged = false; + t.equal(vm.runtime.targets.filter(target => !target.isStage).length, 2); + vm.reorderTarget(2, 1); + t.equal(projectChanged, true); + t.end(); + }); +}); + +test('Reordering a costume should emit a project changed event', t => { + t.equal(vm.editingTarget.sprite.costumes.length, 2); + const spriteId = vm.editingTarget.id; + const reordered = vm.reorderCostume(spriteId, 1, 0); + t.equal(reordered, true); + t.equal(projectChanged, true); + t.end(); +}); + +test('Reordering a sound should emit a project changed event', t => { + const spriteId = vm.editingTarget.id; + const newSound = { + soundName: 'meow', + soundID: 0, + md5: '83c36d806dc92327b9e7049a565c6bff.wav', + sampleCount: 18688, + rate: 22050 + }; + vm.addSound(newSound).then(() => { + // Reset the project changed flag to ignore change from adding new sound + projectChanged = false; + t.equal(vm.editingTarget.sprite.sounds.length, 2); + const reordered = vm.reorderSound(spriteId, 1, 0); + t.equal(reordered, true); + t.equal(projectChanged, true); + t.end(); + }); +}); + +test('Renaming a sprite should emit a project changed event', t => { + const spriteId = vm.editingTarget.id; + vm.renameSprite(spriteId, 'My Sprite'); + t.equal(projectChanged, true); + t.end(); +}); + +test('Renaming a costume should emit a project changed event', t => { + vm.renameCostume(0, 'My Costume'); + t.equal(projectChanged, true); + t.end(); +}); + +test('Renaming a sound should emit a project changed event', t => { + vm.renameSound(0, 'My Sound'); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Changing sprite info should emit a project changed event', t => { + const newSpritePosition = { + x: 10, + y: 100 + }; + + vm.postSpriteInfo(newSpritePosition); + t.equal(projectChanged, true); + projectChanged = false; + + const newSpriteDirection = { + direction: -30 + }; + + vm.postSpriteInfo(newSpriteDirection); + t.equal(projectChanged, true); + projectChanged = false; + + t.end(); + +}); + +test('Editing a vector costume should emit a project changed event', t => { + const mockSvg = 'svg'; + const mockRotationX = -13; + const mockRotationY = 25; + + vm.updateSvg(0, mockSvg, mockRotationX, mockRotationY); + t.equal(projectChanged, true); + t.end(); +}); + +test('Editing a sound should emit a project changed event', t => { + const mockSoundBuffer = []; + const mockSoundEncoding = []; + + vm.updateSoundBuffer(0, mockSoundBuffer, mockSoundEncoding); + t.equal(projectChanged, true); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/project_changed_state_blocks.js b/local-scratch-vm/test/unit/project_changed_state_blocks.js new file mode 100644 index 0000000000000000000000000000000000000000..f077cb931b2e3c3fe3f86fe3589b56443f4ec36c --- /dev/null +++ b/local-scratch-vm/test/unit/project_changed_state_blocks.js @@ -0,0 +1,465 @@ +const tap = require('tap'); +const path = require('path'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const makeTestStorage = require('../fixtures/make-test-storage'); +const VirtualMachine = require('../../src/virtual-machine'); + +let vm; +let projectChanged; +let blockContainer; + +tap.beforeEach(() => { + const projectUri = path.resolve(__dirname, '../fixtures/default.sb2'); + const project = readFileToBuffer(projectUri); + + vm = new VirtualMachine(); + + vm.runtime.addListener('PROJECT_CHANGED', () => { + projectChanged = true; + }); + + vm.attachStorage(makeTestStorage()); + return vm.loadProject(project).then(() => { + blockContainer = vm.editingTarget.blocks; + + // Add mock blocks to use for tests + blockContainer.createBlock({ + id: 'a parent block', + opcode: 'my_testParentBlock', + fields: {}, + inputs: {} + }); + + + blockContainer.createBlock({ + id: 'a new block', + opcode: 'my_testBlock', + topLevel: true, + x: -10, + y: 35, + fields: { + A_FIELD: { + name: 'A_FIELD', + value: 10 + } + }, + inputs: {}, + parent: 'a block' + }); + + // Reset project changes from new blocks + projectChanged = false; + + }); +}); + +tap.tearDown(() => process.nextTick(process.exit)); + +const test = tap.test; + +test('Creating a block should emit a project changed event', t => { + blockContainer.createBlock({ + id: 'another block', + opcode: 'my_testBlock', + topLevel: true + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Deleting a block should emit a project changed event', t => { + blockContainer.deleteBlock('a new block'); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Changing a block should emit a project changed event', t => { + blockContainer.changeBlock({ + element: 'field', + id: 'a new block', + name: 'A_FIELD', + value: 300 + }); + + t.equal(projectChanged, true); + projectChanged = false; + + blockContainer.changeBlock({ + element: 'checkbox', + id: 'a new block', + value: true + }); + + t.equal(projectChanged, true); + projectChanged = false; + + blockContainer.changeBlock({ + element: 'mutation', + id: 'a new block', + value: '' + }); + + t.equal(projectChanged, true); + + t.end(); +}); + +test('Moving a block to a new position should emit a project changed event', t => { + blockContainer.moveBlock({ + id: 'a new block', + newCoordinate: { + x: -40, + y: 350 + } + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Connecting a block to a new parent should emit a project changed event', t => { + blockContainer.createBlock({ + id: 'another block', + opcode: 'my_testBlock' + }); + + projectChanged = false; + + blockContainer.moveBlock({ + id: 'a new block', + newParent: 'another block' + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Disconnecting a block from another should emit a project changed event', t => { + blockContainer.moveBlock({ + id: 'a new block', + oldParent: 'a parent block' + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Creating a local variable should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'var_create', + varId: 'a new variable', + varName: 'foo', + varType: '', + isLocal: true, + isCloud: false + }); + + t.equal(projectChanged, true); + + projectChanged = false; + + // Creating the same variable twice should not emit a project changed event + blockContainer.blocklyListen({ + type: 'var_create', + varId: 'a new variable', + varName: 'foo', + varType: '', + isLocal: true, + isCloud: false + }); + + t.equal(projectChanged, false); + + t.end(); +}); + +test('Creating a global variable should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'var_create', + varId: 'a new variable', + varName: 'foo', + varType: '', + isLocal: false, + isCloud: false + }); + + t.equal(projectChanged, true); + + projectChanged = false; + + // Creating the same variable twice should not emit a project changed event + blockContainer.blocklyListen({ + type: 'var_create', + varId: 'a new variable', + varName: 'foo', + varType: '', + isLocal: false, + isCloud: false + }); + + t.equal(projectChanged, false); + + t.end(); +}); + +test('Renaming a variable should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'var_create', + varId: 'a new variable', + varName: 'foo', + varType: '', + isLocal: false, + isCloud: false + }); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'var_rename', + varId: 'a new variable', + oldName: 'foo', + newName: 'bar' + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Deleting a variable should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'var_create', + varId: 'a new variable', + varName: 'foo', + varType: '', + isLocal: false, + isCloud: false + }); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'var_delete', + varId: 'a new variable', + varName: 'foo', + varType: '', + isLocal: false, + isCloud: false + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Creating a block comment should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'comment_create', + blockId: 'a new block', + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Creating a workspace comment should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'comment_create', + blockId: null, + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Changing a comment should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'comment_create', + blockId: null, + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'comment_change', + blockId: null, + commentId: 'a new comment', + newContents_: { + minimized: true + }, + oldContents_: { + minimized: false + } + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Attempting to change a comment that does not exist should not emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'comment_change', + blockId: null, + commentId: 'a new comment', + newContents_: { + minimized: true + }, + oldContents_: { + minimized: false + } + }); + + t.equal(projectChanged, false); + t.end(); +}); + +test('Deleting a block comment should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'comment_create', + blockId: 'a new block', + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'comment_delete', + blockId: 'a new block', + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Deleting a workspace comment should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'comment_create', + blockId: null, + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'comment_delete', + blockId: null, + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + t.equal(projectChanged, true); + t.end(); +}); + +test('Deleting a comment that does not exist should not emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'comment_delete', + blockId: null, + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + t.equal(projectChanged, false); + t.end(); +}); + +test('Moving a comment should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'comment_create', + blockId: null, + commentId: 'a new comment', + height: 250, + width: 400, + xy: { + x: -40, + y: 27 + }, + minimized: false, + text: 'comment' + }); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'comment_move', + blockId: null, + commentId: 'a new comment', + oldCoordinate_: { + x: -40, + y: 27 + }, + newCoordinate_: { + x: -35, + y: 50 + } + }); + + t.equal(projectChanged, true); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/project_load_changed_state.js b/local-scratch-vm/test/unit/project_load_changed_state.js new file mode 100644 index 0000000000000000000000000000000000000000..0f510ef410a1eae28a9c5b36bccdeee185660f07 --- /dev/null +++ b/local-scratch-vm/test/unit/project_load_changed_state.js @@ -0,0 +1,30 @@ +const tap = require('tap'); +const path = require('path'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const makeTestStorage = require('../fixtures/make-test-storage'); +const VirtualMachine = require('../../src/virtual-machine'); + +tap.tearDown(() => process.nextTick(process.exit)); + +const test = tap.test; + +// Test that loading a project does not emit a project change +// This is in its own file so that it does not affect the test setup +// and results of the other project changed state tests +test('Loading a project should not emit a project changed event', t => { + const projectUri = path.resolve(__dirname, '../fixtures/default.sb2'); + const project = readFileToBuffer(projectUri); + + const vm = new VirtualMachine(); + + let projectChanged = false; + vm.runtime.addListener('PROJECT_CHANGED', () => { + projectChanged = true; + }); + + vm.attachStorage(makeTestStorage()); + return vm.loadProject(project).then(() => { + t.equal(projectChanged, false); + t.end(); + }); +}); diff --git a/local-scratch-vm/test/unit/serialization_sb2.js b/local-scratch-vm/test/unit/serialization_sb2.js new file mode 100644 index 0000000000000000000000000000000000000000..9b01dcb492064ea0cf56d83f9ae550dbbcbb5e72 --- /dev/null +++ b/local-scratch-vm/test/unit/serialization_sb2.js @@ -0,0 +1,104 @@ +const path = require('path'); +const test = require('tap').test; +const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson; + +const RenderedTarget = require('../../src/sprites/rendered-target'); +const Runtime = require('../../src/engine/runtime'); +const sb2 = require('../../src/serialization/sb2'); + +test('spec', t => { + t.type(sb2, 'object'); + t.type(sb2.deserialize, 'function'); + t.end(); +}); + +test('default', t => { + // Get SB2 JSON (string) + const uri = path.resolve(__dirname, '../fixtures/default.sb2'); + const json = extractProjectJson(uri); + + // Create runtime instance & load SB2 into it + const rt = new Runtime(); + sb2.deserialize(json, rt).then(({targets}) => { + // Test + t.type(json, 'object'); + t.type(rt, 'object'); + t.type(targets, 'object'); + + t.ok(targets[0] instanceof RenderedTarget); + t.type(targets[0].id, 'string'); + t.type(targets[0].blocks, 'object'); + t.type(targets[0].variables, 'object'); + t.type(targets[0].comments, 'object'); + + t.equal(targets[0].isOriginal, true); + t.equal(targets[0].currentCostume, 0); + t.equal(targets[0].isOriginal, true); + t.equal(targets[0].isStage, true); + + t.ok(targets[1] instanceof RenderedTarget); + t.type(targets[1].id, 'string'); + t.type(targets[1].blocks, 'object'); + t.type(targets[1].variables, 'object'); + t.type(targets[1].comments, 'object'); + + t.equal(targets[1].isOriginal, true); + t.equal(targets[1].currentCostume, 0); + t.equal(targets[1].isOriginal, true); + t.equal(targets[1].isStage, false); + t.end(); + }); +}); + +test('data scoping', t => { + // Get SB2 JSON (string) + const uri = path.resolve(__dirname, '../fixtures/data.sb2'); + const json = extractProjectJson(uri); + + // Create runtime instance & load SB2 into it + const rt = new Runtime(); + sb2.deserialize(json, rt).then(({targets}) => { + const globalVariableIds = Object.keys(targets[0].variables); + const localVariableIds = Object.keys(targets[1].variables); + t.equal(targets[0].variables[globalVariableIds[0]].name, 'foo'); + t.equal(targets[1].variables[localVariableIds[0]].name, 'local'); + t.end(); + }); +}); + +test('whenclicked blocks imported separately', t => { + // This sb2 fixture has a single "whenClicked" block on both sprite and stage + const uri = path.resolve(__dirname, '../fixtures/when-clicked.sb2'); + const json = extractProjectJson(uri); + + // Create runtime instance & load SB2 into it + const rt = new Runtime(); + sb2.deserialize(json, rt).then(({targets}) => { + const stage = targets[0]; + t.equal(stage.isStage, true); // Make sure we have the correct target + const stageOpcode = stage.blocks.getBlock(stage.blocks.getScripts()[0]).opcode; + t.equal(stageOpcode, 'event_whenstageclicked'); + + const sprite = targets[1]; + t.equal(sprite.isStage, false); // Make sure we have the correct target + const spriteOpcode = sprite.blocks.getBlock(sprite.blocks.getScripts()[0]).opcode; + t.equal(spriteOpcode, 'event_whenthisspriteclicked'); + + t.end(); + }); +}); + +test('Ordering', t => { + // This SB2 has 3 sprites that have been reordered in scratch 2 + // so the order in the file is not the order specified by the indexInLibrary property. + const uri = path.resolve(__dirname, '../fixtures/ordering.sb2'); + const json = extractProjectJson(uri); + const rt = new Runtime(); + sb2.deserialize(json, rt).then(({targets}) => { + // Would fail with any other ordering. + t.equal(targets[1].sprite.name, 'First'); + t.equal(targets[2].sprite.name, 'Second'); + t.equal(targets[3].sprite.name, 'Third'); + t.end(); + }); +}); diff --git a/local-scratch-vm/test/unit/serialization_sb3.js b/local-scratch-vm/test/unit/serialization_sb3.js new file mode 100644 index 0000000000000000000000000000000000000000..efba61e92f1410bbe838c6d235d990a32c95b5fc --- /dev/null +++ b/local-scratch-vm/test/unit/serialization_sb3.js @@ -0,0 +1,363 @@ +const test = require('tap').test; +const path = require('path'); +const VirtualMachine = require('../../src/index'); +const Runtime = require('../../src/engine/runtime'); +const sb3 = require('../../src/serialization/sb3'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const exampleProjectPath = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2'); +const commentsSB2ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb2'); +const commentsSB3ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb3'); +const commentsSB3NoDupeIds = path.resolve(__dirname, '../fixtures/comments_no_duplicate_id_serialization.sb3'); +const variableReporterSB2ProjectPath = path.resolve(__dirname, '../fixtures/top-level-variable-reporter.sb2'); +const topLevelReportersProjectPath = path.resolve(__dirname, '../fixtures/top-level-reporters.sb3'); +const draggableSB3ProjectPath = path.resolve(__dirname, '../fixtures/draggable.sb3'); +const originSB3ProjectPath = path.resolve(__dirname, '../fixtures/origin.sb3'); +const originAbsentSB3ProjectPath = path.resolve(__dirname, '../fixtures/origin-absent.sb3'); +const FakeRenderer = require('../fixtures/fake-renderer'); + +test('serialize', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(exampleProjectPath)) + .then(() => { + const result = sb3.serialize(vm.runtime); + // @todo Analyze + t.type(JSON.stringify(result), 'string'); + t.end(); + }); +}); + +test('deserialize', t => { + const vm = new VirtualMachine(); + sb3.deserialize('', vm.runtime).then(({targets}) => { + // @todo Analyze + t.type(targets, 'object'); + t.end(); + }); +}); + + +test('serialize sb2 project with comments as sb3', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB2ProjectPath)) + .then(() => { + const result = sb3.serialize(vm.runtime); + + t.type(JSON.stringify(result), 'string'); + t.type(result.targets, 'object'); + t.equal(Array.isArray(result.targets), true); + t.equal(result.targets.length, 2); + + const stage = result.targets[0]; + t.equal(stage.isStage, true); + // The stage has 0 blocks, and 1 workspace comment + t.type(stage.blocks, 'object'); + t.equal(Object.keys(stage.blocks).length, 0); + t.type(stage.comments, 'object'); + t.equal(Object.keys(stage.comments).length, 1); + const stageBlockComments = Object.values(stage.comments).filter(comment => !!comment.blockId); + const stageWorkspaceComments = Object.values(stage.comments).filter(comment => comment.blockId === null); + t.equal(stageBlockComments.length, 0); + t.equal(stageWorkspaceComments.length, 1); + + const sprite = result.targets[1]; + t.equal(sprite.isStage, false); + t.type(sprite.blocks, 'object'); + // Sprite 1 has 6 blocks, 5 block comments, and 1 workspace comment + t.equal(Object.keys(sprite.blocks).length, 6); + t.type(sprite.comments, 'object'); + t.equal(Object.keys(sprite.comments).length, 6); + + const spriteBlockComments = Object.values(sprite.comments).filter(comment => !!comment.blockId); + const spriteWorkspaceComments = Object.values(sprite.comments).filter(comment => comment.blockId === null); + t.equal(spriteBlockComments.length, 5); + t.equal(spriteWorkspaceComments.length, 1); + + t.end(); + }); +}); + +test('deserialize sb3 project with comments', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB3ProjectPath)) + .then(() => { + const runtime = vm.runtime; + + t.type(runtime.targets, 'object'); + t.equal(Array.isArray(runtime.targets), true); + t.equal(runtime.targets.length, 2); + + const stage = runtime.targets[0]; + t.equal(stage.isStage, true); + // The stage has 0 blocks, and 1 workspace comment + t.type(stage.blocks, 'object'); + t.equal(Object.keys(stage.blocks._blocks).length, 0); + t.type(stage.comments, 'object'); + t.equal(Object.keys(stage.comments).length, 1); + const stageBlockComments = Object.values(stage.comments).filter(comment => !!comment.blockId); + const stageWorkspaceComments = Object.values(stage.comments).filter(comment => comment.blockId === null); + t.equal(stageBlockComments.length, 0); + t.equal(stageWorkspaceComments.length, 1); + + const sprite = runtime.targets[1]; + t.equal(sprite.isStage, false); + t.type(sprite.blocks, 'object'); + // Sprite 1 has 6 blocks, 5 block comments, and 1 workspace comment + t.equal(Object.values(sprite.blocks._blocks).filter(block => !block.shadow).length, 6); + t.type(sprite.comments, 'object'); + t.equal(Object.keys(sprite.comments).length, 6); + + const spriteBlockComments = Object.values(sprite.comments).filter(comment => !!comment.blockId); + const spriteWorkspaceComments = Object.values(sprite.comments).filter(comment => comment.blockId === null); + t.equal(spriteBlockComments.length, 5); + t.equal(spriteWorkspaceComments.length, 1); + + t.end(); + }); +}); + +test('deserialize sb3 project with comments - no duplicate id serialization', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB3NoDupeIds)) + .then(() => { + const runtime = vm.runtime; + + t.type(runtime.targets, 'object'); + t.equal(Array.isArray(runtime.targets), true); + t.equal(runtime.targets.length, 2); + + const stage = runtime.targets[0]; + t.equal(stage.isStage, true); + // The stage has 0 blocks, and 0 workspace comment + t.type(stage.blocks, 'object'); + t.equal(Object.keys(stage.blocks._blocks).length, 0); + t.type(stage.comments, 'object'); + t.equal(Object.keys(stage.comments).length, 0); + + const sprite = runtime.targets[1]; + t.equal(sprite.isStage, false); + t.type(sprite.blocks, 'object'); + // Sprite1 has 1 blocks, 1 block comment, and 1 workspace comment + t.equal(Object.values(sprite.blocks._blocks).filter(block => !block.shadow).length, 1); + t.type(sprite.comments, 'object'); + t.equal(Object.keys(sprite.comments).length, 2); + + const spriteBlockComments = Object.values(sprite.comments).filter(comment => !!comment.blockId); + const spriteWorkspaceComments = Object.values(sprite.comments).filter(comment => comment.blockId === null); + t.equal(spriteBlockComments.length, 1); + t.equal(spriteWorkspaceComments.length, 1); + + t.end(); + }); +}); + +test('serializing and deserializing sb3 preserves sprite layer order', t => { + const vm = new VirtualMachine(); + vm.attachRenderer(new FakeRenderer()); + return vm.loadProject(readFileToBuffer(path.resolve(__dirname, '../fixtures/ordering.sb2'))) + .then(() => { + // Target get layer order needs a renderer, + // fake the numbers we would get back from the + // renderer in order to test that they are serialized + // correctly + vm.runtime.targets[0].getLayerOrder = () => 0; + vm.runtime.targets[1].getLayerOrder = () => 20; + vm.runtime.targets[2].getLayerOrder = () => 10; + vm.runtime.targets[3].getLayerOrder = () => 30; + + const result = sb3.serialize(vm.runtime); + + t.type(JSON.stringify(result), 'string'); + t.type(result.targets, 'object'); + t.equal(Array.isArray(result.targets), true); + t.equal(result.targets.length, 4); + + // First check that the sprites are ordered correctly (as they would + // appear in the target pane) + t.equal(result.targets[0].name, 'Stage'); + t.equal(result.targets[1].name, 'First'); + t.equal(result.targets[2].name, 'Second'); + t.equal(result.targets[3].name, 'Third'); + + // Check that they are in the correct layer order (as they would render + // back to front on the stage) + t.equal(result.targets[0].layerOrder, 0); + t.equal(result.targets[1].layerOrder, 2); + t.equal(result.targets[2].layerOrder, 1); + t.equal(result.targets[3].layerOrder, 3); + + return result; + }) + .then(serializedObject => + sb3.deserialize( + JSON.parse(JSON.stringify(serializedObject)), new Runtime(), null, false) + .then(({targets}) => { + // First check that the sprites are ordered correctly (as they would + // appear in the target pane) + t.equal(targets[0].sprite.name, 'Stage'); + t.equal(targets[1].sprite.name, 'First'); + t.equal(targets[2].sprite.name, 'Second'); + t.equal(targets[3].sprite.name, 'Third'); + + // Check that they are in the correct layer order (as they would render + // back to front on the stage) + t.equal(targets[0].layerOrder, 0); + t.equal(targets[1].layerOrder, 2); + t.equal(targets[2].layerOrder, 1); + t.equal(targets[3].layerOrder, 3); + + t.end(); + })); +}); + +test('serializeBlocks', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB3ProjectPath)) + .then(() => { + const blocks = vm.runtime.targets[1].blocks._blocks; + const result = sb3.serializeBlocks(blocks); + // @todo Analyze + t.type(result[0], 'object'); + t.ok(Object.keys(result[0]).length < Object.keys(blocks).length, 'less blocks in serialized format'); + t.ok(Array.isArray(result[1])); + t.end(); + }); +}); + +test('serializeBlocks serializes x and y for topLevel blocks with x,y of 0,0', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(topLevelReportersProjectPath)) + .then(() => { + // Verify that there are 2 blocks and they are both top level + const blocks = vm.runtime.targets[1].blocks._blocks; + const blockIds = Object.keys(blocks); + t.equal(blockIds.length, 2); + const blocksArray = blockIds.map(key => blocks[key]); + t.equal(blocksArray.every(b => b.topLevel), true); + // Simulate cleaning up the blocks by resetting x and y positions to 0 + blockIds.forEach(blockId => { + blocks[blockId].x = 0; + blocks[blockId].y = 0; + }); + const result = sb3.serializeBlocks(blocks); + const serializedBlocks = result[0]; + + t.type(serializedBlocks, 'object'); + const serializedBlockIds = Object.keys(serializedBlocks); + t.equal(serializedBlockIds.length, 2); + const firstBlock = serializedBlocks[serializedBlockIds[0]]; + const secondBlock = serializedBlocks[serializedBlockIds[1]]; + t.equal(firstBlock.x, 0); + t.equal(firstBlock.y, 0); + t.equal(secondBlock.x, 0); + t.equal(secondBlock.y, 0); + + t.end(); + }); +}); + +test('deserializeBlocks', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB3ProjectPath)) + .then(() => { + const blocks = vm.runtime.targets[1].blocks._blocks; + const serialized = sb3.serializeBlocks(blocks)[0]; + const deserialized = sb3.deserializeBlocks(serialized); + t.equal(Object.keys(deserialized).length, Object.keys(blocks).length, 'same number of blocks'); + t.end(); + }); +}); + +test('deserializeBlocks on already deserialized input', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB3ProjectPath)) + .then(() => { + const blocks = vm.runtime.targets[1].blocks._blocks; + const serialized = sb3.serializeBlocks(blocks)[0]; + const deserialized = sb3.deserializeBlocks(serialized); + const deserializedAgain = sb3.deserializeBlocks(deserialized); + t.deepEqual(deserialized, deserializedAgain, 'no change from second pass of deserialize'); + t.end(); + }); +}); + +test('getExtensionIdForOpcode', t => { + t.equal(sb3.getExtensionIdForOpcode('wedo_loopy'), 'wedo'); + + // does not consider CORE to be extensions + t.false(sb3.getExtensionIdForOpcode('control_loopy')); + + // only considers things before the first underscore + t.equal(sb3.getExtensionIdForOpcode('hello_there_loopy'), 'hello'); + + // does not return anything for opcodes with no extension + t.false(sb3.getExtensionIdForOpcode('hello')); + + // forbidden characters must be replaced with '-' + t.equal(sb3.getExtensionIdForOpcode('hi:there/happy_people'), 'hi-there-happy'); + + t.end(); +}); + +test('(#1608) serializeBlocks maintains top level variable reporters', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(variableReporterSB2ProjectPath)) + .then(() => { + const blocks = vm.runtime.targets[0].blocks._blocks; + const result = sb3.serialize(vm.runtime); + // Project should have 1 block, a top-level variable reporter + t.equal(Object.keys(blocks).length, 1); + t.equal(Object.keys(result.targets[0].blocks).length, 1); + + // Make sure deserializing these blocks works + t.doesNotThrow(() => { + sb3.deserialize(JSON.parse(JSON.stringify(result)), vm.runtime); + }); + t.end(); + }); +}); + +test('(#1850) sprite draggability state read when loading SB3 file', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(draggableSB3ProjectPath)) + .then(() => { + const sprite1Obj = vm.runtime.targets.find(target => target.sprite.name === 'Sprite1'); + // Sprite1 in project should have draggable set to true + t.equal(sprite1Obj.draggable, true); + t.end(); + }); +}); + +test('load origin value from SB3 file json metadata', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(originSB3ProjectPath)) + .then(() => { + t.type(vm.runtime.origin, 'string'); + }) + .then(() => vm.loadProject(readFileToBuffer(originAbsentSB3ProjectPath))) + .then(() => { + // After loading a project with an origin, then loading one without an origin, + // origin value should no longer be set. + t.equal(vm.runtime.origin, null); + t.end(); + }); +}); + +test('serialize origin value if it is present', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(originSB3ProjectPath)) + .then(() => { + const result = sb3.serialize(vm.runtime); + t.type(result.meta.origin, 'string'); + t.end(); + }); +}); + +test('do not serialize origin value if it is not present', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(originAbsentSB3ProjectPath)) + .then(() => { + const result = sb3.serialize(vm.runtime); + t.equal(result.meta.origin, undefined); + t.end(); + }); +}); diff --git a/local-scratch-vm/test/unit/spec.js b/local-scratch-vm/test/unit/spec.js new file mode 100644 index 0000000000000000000000000000000000000000..88bad15875e48bf80c9f5860dbfb6000485848c9 --- /dev/null +++ b/local-scratch-vm/test/unit/spec.js @@ -0,0 +1,36 @@ +const test = require('tap').test; +const VirtualMachine = require('../../src/index'); + +test('interface', t => { + const vm = new VirtualMachine(); + t.type(vm, 'object'); + t.type(vm.start, 'function'); + t.type(vm.greenFlag, 'function'); + t.type(vm.setTurboMode, 'function'); + t.type(vm.setCompatibilityMode, 'function'); + t.type(vm.stopAll, 'function'); + t.type(vm.clear, 'function'); + + t.type(vm.getPlaygroundData, 'function'); + t.type(vm.postIOData, 'function'); + + t.type(vm.loadProject, 'function'); + t.type(vm.addSprite, 'function'); + t.type(vm.addCostume, 'function'); + t.type(vm.addBackdrop, 'function'); + t.type(vm.addSound, 'function'); + t.type(vm.deleteCostume, 'function'); + t.type(vm.deleteSound, 'function'); + t.type(vm.renameSprite, 'function'); + t.type(vm.deleteSprite, 'function'); + + t.type(vm.attachRenderer, 'function'); + t.type(vm.blockListener, 'function'); + t.type(vm.flyoutBlockListener, 'function'); + t.type(vm.setEditingTarget, 'function'); + + t.type(vm.emitTargetsUpdate, 'function'); + t.type(vm.emitWorkspaceUpdate, 'function'); + t.type(vm.postSpriteInfo, 'function'); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/sprites_rendered-target.js b/local-scratch-vm/test/unit/sprites_rendered-target.js new file mode 100644 index 0000000000000000000000000000000000000000..e4d98866714c58e95f0c95f996894f6d521e6728 --- /dev/null +++ b/local-scratch-vm/test/unit/sprites_rendered-target.js @@ -0,0 +1,585 @@ +const test = require('tap').test; +const RenderedTarget = require('../../src/sprites/rendered-target'); +const Sprite = require('../../src/sprites/sprite'); +const Runtime = require('../../src/engine/runtime'); +const FakeRenderer = require('../fixtures/fake-renderer'); + +test('clone effects', t => { + // Create two clones and ensure they have different graphic effect objects. + // Regression test for Github issue #224 + const r = new Runtime(); + const spr = new Sprite(null, r); + const a = new RenderedTarget(spr, r); + const b = new RenderedTarget(spr, r); + t.ok(a.effects !== b.effects); + t.end(); +}); + +test('setxy', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + a.setXY(123, 321, true); + t.equals(a.x, 123); + t.equals(a.y, 321); + renderer.getFencedPositionOfDrawable = () => [50, 50]; + a.setXY(100, 100, true); + t.equals(a.x, 50); + t.equals(a.y, 50); + r.setRuntimeOptions({ + fencing: false + }); + a.setXY(100, 100, true); + t.equals(a.x, 100); + t.equals(a.y, 100); + t.end(); +}); + +test('blocks get new id on duplicate', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const rt = new RenderedTarget(s, r); + const block = { + id: 'id1', + topLevel: true, + fields: {} + }; + + rt.blocks.createBlock(block); + + return rt.duplicate().then(duplicate => { + t.notOk(duplicate.blocks._blocks.hasOwnProperty(block.id)); + t.end(); + }); +}); + +test('direction', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + a.setDirection(123); + t.equals(a._getRenderedDirectionAndScale().direction, 123); + t.end(); +}); + +test('setVisible', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + a.setVisible(true); + t.end(); +}); + +test('setSize', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + a.setSize(123); + t.equals(a._getRenderedDirectionAndScale().scale[0], 123); + renderer.getCurrentSkinSize = () => [100, 100]; + a.setSize(99999); + t.equals(a._getRenderedDirectionAndScale().scale[0], 540); + r.setRuntimeOptions({ + fencing: false + }); + a.setSize(99999); + t.equals(a._getRenderedDirectionAndScale().scale[0], 99999); + t.end(); +}); + +test('set and clear effects', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + for (const effect in a.effects) { + a.setEffect(effect, 1); + t.equals(a.effects[effect], 1); + } + a.clearEffects(); + for (const effect in a.effects) { + t.equals(a.effects[effect], 0); + } + t.end(); +}); + +test('setCostume', t => { + const o = new Object(); + const r = new Runtime(); + const s = new Sprite(null, r); + s.costumes = [o]; + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + a.setCostume(0); + t.end(); +}); + +test('deleteCostume', t => { + const o1 = {id: 1}; + const o2 = {id: 2}; + const o3 = {id: 3}; + const o4 = {id: 4}; + const o5 = {id: 5}; + + const r = new Runtime(); + const s = new Sprite(null, r); + s.costumes = [o1, o2, o3]; + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + + // x* Costume 1 * Costume 2 + // Costume 2 => Costume 3 + // Costume 3 + a.setCostume(0); + const deletedCostume = a.deleteCostume(0); + t.equals(a.sprite.costumes.length, 2); + t.equals(a.sprite.costumes[0].id, 2); + t.equals(a.sprite.costumes[1].id, 3); + t.equals(a.currentCostume, 0); + t.deepEqual(deletedCostume, o1); + + // Costume 1 Costume 1 + // x* Costume 2 => * Costume 3 + // Costume 3 + a.sprite.costumes = [o1, o2, o3]; + a.setCostume(1); + const deletedCostume2 = a.deleteCostume(1); + t.equals(a.sprite.costumes.length, 2); + t.equals(a.sprite.costumes[0].id, 1); + t.equals(a.sprite.costumes[1].id, 3); + t.equals(a.currentCostume, 1); + t.deepEqual(deletedCostume2, o2); + + // Costume 1 Costume 1 + // Costume 2 => * Costume 2 + // x* Costume 3 + a.sprite.costumes = [o1, o2, o3]; + a.setCostume(2); + const deletedCostume3 = a.deleteCostume(2); + t.equals(a.sprite.costumes.length, 2); + t.equals(a.sprite.costumes[0].id, 1); + t.equals(a.sprite.costumes[1].id, 2); + t.equals(a.currentCostume, 1); + t.deepEqual(deletedCostume3, o3); + + // Refuses to delete only costume + a.sprite.costumes = [o1]; + a.setCostume(0); + const noDeletedCostume = a.deleteCostume(0); + t.equals(a.sprite.costumes.length, 1); + t.equals(a.sprite.costumes[0].id, 1); + t.equals(a.currentCostume, 0); + t.equal(noDeletedCostume, null); + + // Costume 1 Costume 1 + // x Costume 2 Costume 3 + // Costume 3 => * Costume 4 + // * Costume 4 Costume 5 + // Costume 5 + a.sprite.costumes = [o1, o2, o3, o4, o5]; + a.setCostume(3); + a.deleteCostume(1); + t.equals(a.sprite.costumes.length, 4); + t.equals(a.sprite.costumes[0].id, 1); + t.equals(a.sprite.costumes[1].id, 3); + t.equals(a.sprite.costumes[2].id, 4); + t.equals(a.sprite.costumes[3].id, 5); + t.equals(a.currentCostume, 2); + + // Costume 1 Costume 1 + // * Costume 2 * Costume 2 + // Costume 3 => Costume 3 + // x Costume 4 Costume 5 + // Costume 5 + a.sprite.costumes = [o1, o2, o3, o4, o5]; + a.setCostume(1); + a.deleteCostume(3); + t.equals(a.sprite.costumes.length, 4); + t.equals(a.sprite.costumes[0].id, 1); + t.equals(a.sprite.costumes[1].id, 2); + t.equals(a.sprite.costumes[2].id, 3); + t.equals(a.sprite.costumes[3].id, 5); + t.equals(a.currentCostume, 1); + + // Costume 1 Costume 1 + // * Costume 2 * Costume 2 + // Costume 3 => Costume 3 + // Costume 4 Costume 4 + // x Costume 5 + a.sprite.costumes = [o1, o2, o3, o4, o5]; + a.setCostume(1); + a.deleteCostume(4); + t.equals(a.sprite.costumes.length, 4); + t.equals(a.sprite.costumes[0].id, 1); + t.equals(a.sprite.costumes[1].id, 2); + t.equals(a.sprite.costumes[2].id, 3); + t.equals(a.sprite.costumes[3].id, 4); + t.equals(a.currentCostume, 1); + t.end(); +}); + +test('deleteSound', t => { + const o1 = {id: 1}; + const o2 = {id: 2}; + const o3 = {id: 3}; + + const r = new Runtime(); + const s = new Sprite(null, r); + s.sounds = [o1, o2, o3]; + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + + const firstDeleted = a.deleteSound(0); + t.deepEqual(a.sprite.sounds, [o2, o3]); + t.deepEqual(firstDeleted, o1); + + // Allows deleting the only sound + a.sprite.sounds = [o1]; + a.deleteSound(0); + t.deepEqual(a.sprite.sounds, []); + + t.end(); +}); + +test('setRotationStyle', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + a.setRotationStyle(RenderedTarget.ROTATION_STYLE_NONE); + t.end(); +}); + +test('getBounds', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const renderer = new FakeRenderer(); + r.attachRenderer(renderer); + const a = new RenderedTarget(s, r); + a.renderer = renderer; + t.equals(a.getBounds().top, 0); + a.setXY(241, 241); + t.equals(a.getBounds().top, 241); + t.end(); +}); + +test('isTouchingPoint', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const renderer = new FakeRenderer(); + r.attachRenderer(renderer); + const a = new RenderedTarget(s, r); + a.renderer = renderer; + t.equals(a.isTouchingPoint(), true); + t.end(); +}); + +test('isTouchingEdge', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const renderer = new FakeRenderer(); + r.attachRenderer(renderer); + const a = new RenderedTarget(s, r); + a.renderer = renderer; + t.equals(a.isTouchingEdge(), false); + a.setXY(1000, 1000); + t.equals(a.isTouchingEdge(), true); + t.end(); +}); + +test('isTouchingSprite', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const renderer = new FakeRenderer(); + r.attachRenderer(renderer); + const a = new RenderedTarget(s, r); + a.renderer = renderer; + t.equals(a.isTouchingSprite('fake'), false); + t.end(); +}); + +test('isTouchingColor', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const renderer = new FakeRenderer(); + r.attachRenderer(renderer); + const a = new RenderedTarget(s, r); + a.renderer = renderer; + t.equals(a.isTouchingColor(), false); + t.end(); +}); + +test('colorIsTouchingColor', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const renderer = new FakeRenderer(); + r.attachRenderer(renderer); + const a = new RenderedTarget(s, r); + a.renderer = renderer; + t.equals(a.colorIsTouchingColor(), false); + t.end(); +}); + +test('layers', t => { // TODO this tests fake functionality. Move layering tests into Render. + const r = new Runtime(); + const s = new Sprite(null, r); + const renderer = new FakeRenderer(); + const o = new Object(); + r.attachRenderer(renderer); + const a = new RenderedTarget(s, r); + a.renderer = renderer; + a.goToFront(); + t.equals(a.renderer.order, 5); + a.goBackwardLayers(2); + t.equals(a.renderer.order, 3); + a.goToBack(); + // Note, there are only sprites in this test, no stage, and the addition + // of layer groups, goToBack no longer specifies a minimum order number + t.equals(a.renderer.order, 0); + a.goForwardLayers(1); + t.equals(a.renderer.order, 1); + o.drawableID = 999; + a.goBehindOther(o); + t.equals(a.renderer.order, 1); + t.end(); +}); + +test('getLayerOrder returns result of renderer getDrawableOrder or null if renderer is not attached', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const a = new RenderedTarget(s, r); + + // getLayerOrder should return null if there is no renderer attached to the runtime + t.equal(a.getLayerOrder(), null); + + const renderer = new FakeRenderer(); + r.attachRenderer(renderer); + const b = new RenderedTarget(s, r); + + t.equal(b.getLayerOrder(), 'stub'); + + t.end(); +}); + +test('keepInFence', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const renderer = new FakeRenderer(); + r.attachRenderer(renderer); + const a = new RenderedTarget(s, r); + a.renderer = renderer; + t.equals(a.keepInFence(1000, 1000)[0], 240); + t.equals(a.keepInFence(-1000, 1000)[0], -240); + t.equals(a.keepInFence(1000, 1000)[1], 180); + t.equals(a.keepInFence(1000, -1000)[1], -180); + t.end(); +}); + +test('#stopAll clears graphics effects', t => { + const r = new Runtime(); + const s = new Sprite(null, r); + const a = new RenderedTarget(s, r); + const effectName = 'brightness'; + a.setEffect(effectName, 100); + a.onStopAll(); + t.equals(a.effects[effectName], 0); + t.end(); +}); + +test('#getCostumes returns the costumes', t => { + const r = new Runtime(); + const spr = new Sprite(null, r); + const a = new RenderedTarget(spr, r); + a.sprite.costumes = [{id: 1}, {id: 2}, {id: 3}]; + t.equals(a.getCostumes().length, 3); + t.equals(a.getCostumes()[0].id, 1); + t.equals(a.getCostumes()[1].id, 2); + t.equals(a.getCostumes()[2].id, 3); + t.end(); +}); + +test('#getSounds returns the sounds', t => { + const r = new Runtime(); + const spr = new Sprite(null, r); + const a = new RenderedTarget(spr, r); + const sounds = [1, 2, 3]; + a.sprite.sounds = sounds; + t.equals(a.getSounds(), sounds); + t.end(); +}); + +test('#toJSON returns the sounds and costumes', t => { + const r = new Runtime(); + const spr = new Sprite(null, r); + const a = new RenderedTarget(spr, r); + const sounds = [1, 2, 3]; + a.sprite.sounds = sounds; + a.sprite.costumes = [{id: 1}, {id: 2}, {id: 3}]; + t.same(a.toJSON().sounds, sounds); + t.same(a.toJSON().costumes, a.sprite.costumes); + t.end(); +}); + +test('#addSound does not duplicate names', t => { + const r = new Runtime(); + const spr = new Sprite(null, r); + const a = new RenderedTarget(spr, r); + a.sprite.sounds = [{name: 'first'}]; + a.addSound({name: 'first'}); + t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'first2'}]); + t.end(); +}); + +test('#addCostume does not duplicate names', t => { + const r = new Runtime(); + const spr = new Sprite(null, r); + const a = new RenderedTarget(spr, r); + a.addCostume({name: 'first'}); + a.addCostume({name: 'first'}); + t.equal(a.sprite.costumes.length, 2); + t.equal(a.sprite.costumes[0].name, 'first'); + t.equal(a.sprite.costumes[1].name, 'first2'); + t.end(); +}); + +test('#renameSound does not duplicate names', t => { + const r = new Runtime(); + const spr = new Sprite(null, r); + const a = new RenderedTarget(spr, r); + a.sprite.sounds = [{name: 'first'}, {name: 'second'}]; + a.renameSound(0, 'first'); // Shouldn't increment the name, noop + t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'second'}]); + a.renameSound(1, 'first'); + t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'first2'}]); + t.end(); +}); + +test('#renameCostume does not duplicate names', t => { + const r = new Runtime(); + const spr = new Sprite(null, r); + const a = new RenderedTarget(spr, r); + a.sprite.costumes = [{name: 'first'}, {name: 'second'}]; + a.renameCostume(0, 'first'); // Shouldn't increment the name, noop + t.equal(a.sprite.costumes.length, 2); + t.equal(a.sprite.costumes[0].name, 'first'); + t.equal(a.sprite.costumes[1].name, 'second'); + a.renameCostume(1, 'first'); + t.equal(a.sprite.costumes.length, 2); + t.equal(a.sprite.costumes[0].name, 'first'); + t.equal(a.sprite.costumes[1].name, 'first2'); + t.end(); +}); + +test('#reorderCostume', t => { + const o1 = {id: 0}; + const o2 = {id: 1}; + const o3 = {id: 2}; + const o4 = {id: 3}; + const o5 = {id: 4}; + const r = new Runtime(); + const s = new Sprite(null, r); + s.costumes = [o1, o2, o3, o4, o5]; + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + + const resetCostumes = () => { + a.setCostume(0); + s.costumes = [o1, o2, o3, o4, o5]; + }; + const costumeIds = () => a.sprite.costumes.map(c => c.id); + + resetCostumes(); + t.deepEquals(costumeIds(), [0, 1, 2, 3, 4]); + t.equals(a.currentCostume, 0); + + // Returns false if the costumes are the same and no change occurred + t.equal(a.reorderCostume(3, 3), false); + t.equal(a.reorderCostume(999, 5000), false); // Clamped to the same values. + t.equal(a.reorderCostume(-999, -5000), false); + + // Make sure reordering up and down works and current costume follows + resetCostumes(); + t.equal(a.reorderCostume(0, 3), true); + t.deepEquals(costumeIds(), [1, 2, 3, 0, 4]); + t.equals(a.currentCostume, 3); // Index of id=0 + + resetCostumes(); + a.setCostume(1); + t.equal(a.reorderCostume(3, 1), true); + t.deepEquals(costumeIds(), [0, 3, 1, 2, 4]); + t.equals(a.currentCostume, 2); // Index of id=1 + + // Out of bounds indices get clamped + resetCostumes(); + t.equal(a.reorderCostume(10, 0), true); + t.deepEquals(costumeIds(), [4, 0, 1, 2, 3]); + t.equals(a.currentCostume, 1); // Index of id=0 + + resetCostumes(); + t.equal(a.reorderCostume(2, -1000), true); + t.deepEquals(costumeIds(), [2, 0, 1, 3, 4]); + t.equals(a.currentCostume, 1); // Index of id=0 + + t.end(); +}); + +test('#reorderSound', t => { + const o1 = {id: 0, name: 'name0'}; + const o2 = {id: 1, name: 'name1'}; + const o3 = {id: 2, name: 'name2'}; + const o4 = {id: 3, name: 'name3'}; + const o5 = {id: 4, name: 'name4'}; + const r = new Runtime(); + const s = new Sprite(null, r); + s.sounds = [o1, o2, o3, o4, o5]; + const a = new RenderedTarget(s, r); + const renderer = new FakeRenderer(); + a.renderer = renderer; + + const resetSounds = () => { + s.sounds = [o1, o2, o3, o4, o5]; + }; + const soundIds = () => a.sprite.sounds.map(c => c.id); + + resetSounds(); + t.deepEquals(soundIds(), [0, 1, 2, 3, 4]); + + // Return false if indices are the same and no change occurred. + t.equal(a.reorderSound(3, 3), false); + t.equal(a.reorderSound(100000, 99999), false); // Clamped to the same values + t.equal(a.reorderSound(-100000, -99999), false); + + // Make sure reordering up and down works and current sound follows + resetSounds(); + t.equal(a.reorderSound(0, 3), true); + t.deepEquals(soundIds(), [1, 2, 3, 0, 4]); + + resetSounds(); + t.equal(a.reorderSound(3, 1), true); + t.deepEquals(soundIds(), [0, 3, 1, 2, 4]); + + // Out of bounds indices get clamped + resetSounds(); + t.equal(a.reorderSound(10, 0), true); + t.deepEquals(soundIds(), [4, 0, 1, 2, 3]); + + resetSounds(); + t.equal(a.reorderSound(2, -1000), true); + t.deepEquals(soundIds(), [2, 0, 1, 3, 4]); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_cast.js b/local-scratch-vm/test/unit/tw_cast.js new file mode 100644 index 0000000000000000000000000000000000000000..92763371d210ddad6ea42910eb3fded91c8453df --- /dev/null +++ b/local-scratch-vm/test/unit/tw_cast.js @@ -0,0 +1,83 @@ +const Cast = require('../../src/util/cast'); +const {test} = require('tap'); + +test('Cast.compare with assorted whitespace characters', t => { + t.equal(Cast.compare('', ''), 0); + + t.equal(Cast.compare(' ', ''), 1); + t.equal(Cast.compare('', ' '), -1); + + t.equal(Cast.compare(' ', ' '), -1); + t.equal(Cast.compare(' ', ' '), 1); + + t.equal(Cast.compare(' \u00a0 ', '\r\n'), 1); + t.equal(Cast.compare('\r\n', ' \u00a0 '), -1); + + t.equal(Cast.compare(' 0', 0), 0); + t.equal(Cast.compare(0, ' 0'), 0); + + t.equal(Cast.compare(' 0 ', ' \r\n\u00a0 0 \n\n\n\n'), 0); + t.equal(Cast.compare(' \r\n\u00a0 0 \n\n\n\n', ' 0 '), 0); + + t.equal(Cast.compare(' 0 ', ' \r\n\u00a0 0 \n\n\n\b'), 1); + t.equal(Cast.compare(' \r\n\u00a0 0 \n\n\n\b', ' 0 '), -1); + + t.equal(Cast.compare(' 0', '0'), 0); + t.equal(Cast.compare('0', ' 0'), 0); + + t.equal(Cast.compare('', 0), -1); + t.equal(Cast.compare(0, ''), 1); + + t.equal(Cast.compare(' ', 0), -1); + t.equal(Cast.compare(0, ' '), 1); + + t.equal(Cast.compare('0', ' '), 1); + t.equal(Cast.compare(' ', '0'), -1); + + t.equal(Cast.compare('\n0', '\n-1'), 1); + t.equal(Cast.compare('\n-1', '\n0'), -1); + + t.equal(Cast.compare('', 'false'), -1); + t.equal(Cast.compare('false', ''), 1); + + t.equal(Cast.compare('', ' false'), -1); + t.equal(Cast.compare(' false', ''), 1); + + t.equal(Cast.compare('\n', ' false'), -1); + t.equal(Cast.compare('false', '\n'), 1); + + t.equal(Cast.compare(false, ''), 1); + t.equal(Cast.compare('', false), -1); + + t.equal(Cast.compare(false, ' '), 1); + t.equal(Cast.compare(' ', false), -1); + + t.equal(Cast.compare('\t', '0'), 0); + t.equal(Cast.compare('0', '\t'), 0); + + t.equal(Cast.compare('\t', 0), 0); + t.equal(Cast.compare(0, '\t'), 0); + + t.equal(Cast.compare('\t', ''), 1); + t.equal(Cast.compare('', '\t'), -1); + + t.equal(Cast.compare(' \t ', '0'), 0); + t.equal(Cast.compare('0', ' \t '), 0); + + t.equal(Cast.compare('\r\n \t\u00a0', 0), 0); + t.equal(Cast.compare(0, '\r\n \t\u00a0'), 0); + + t.equal(Cast.compare('\t', false), 0); + t.equal(Cast.compare(false, '\t'), 0); + + t.equal(Cast.compare('\t', 'false'), -1); + t.equal(Cast.compare('false', '\t'), 1); + + t.equal(Cast.compare('\t', '1'), -1); + t.equal(Cast.compare('1', '\t'), 1); + + t.equal(Cast.compare('\t', 1), -1); + t.equal(Cast.compare(1, '\t'), 1); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_clones.js b/local-scratch-vm/test/unit/tw_clones.js new file mode 100644 index 0000000000000000000000000000000000000000..b4a021191af9d2b42db663cb7f909eb720cb1772 --- /dev/null +++ b/local-scratch-vm/test/unit/tw_clones.js @@ -0,0 +1,18 @@ +const Runtime = require('../../src/engine/runtime'); +const Sprite = require('../../src/sprites/sprite'); + +const {test} = require('tap'); + +test('clone counter', t => { + const rt = new Runtime(); + const sprite = new Sprite(null, rt); + const original = sprite.createClone(); + t.equal(rt._cloneCounter, 0); + const clone = original.makeClone(); + t.equal(rt._cloneCounter, 1); + clone.dispose(); + t.equal(rt._cloneCounter, 0); + original.dispose(); + t.equal(rt._cloneCounter, 0); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_compress.js b/local-scratch-vm/test/unit/tw_compress.js new file mode 100644 index 0000000000000000000000000000000000000000..fb5f567631a92acb04a38007bea5863f8c12bd1e --- /dev/null +++ b/local-scratch-vm/test/unit/tw_compress.js @@ -0,0 +1,318 @@ +const {test} = require('tap'); +const compress = require('../../src/serialization/tw-compress-sb3'); +const uid = require('../../src/util/uid'); + +test('handles type INPUT_DIFF_BLOCK_SHADOW (3) compressed inputs', t => { + const data = { + targets: [ + { + isStage: true, + name: 'Stage', + variables: {}, + lists: {}, + broadcasts: {}, + blocks: { + 'CmRa^i]o}QL77;hk:54o': { + opcode: 'looks_switchbackdropto', + next: null, + parent: null, + inputs: { + BACKDROP: [ + 3, + 'cq84G6uywD{m2R,E03Ci', + 'E3/*4H*xk38{=*U;bVWm' + ] + }, + fields: {}, + shadow: false, + topLevel: true, + x: 409, + y: 300 + }, + 'cq84G6uywD{m2R,E03Ci': { + opcode: 'operator_not', + next: null, + parent: 'CmRa^i]o}QL77;hk:54o', + inputs: {}, + fields: {}, + shadow: false, + topLevel: false + }, + 'E3/*4H*xk38{=*U;bVWm': { + opcode: 'looks_backdrops', + next: null, + parent: 'CmRa^i]o}QL77;hk:54o', + inputs: {}, + fields: { + BACKDROP: [ + 'backdrop1', + null + ] + }, + shadow: true, + topLevel: false + } + }, + comments: {}, + currentCostume: 0, + costumes: [], + sounds: [], + volume: 100, + layerOrder: 0, + tempo: 60, + videoTransparency: 50, + videoState: 'on', + textToSpeechLanguage: null + } + ], + monitors: [], + extensions: [], + meta: { + semver: '3.0.0', + vm: '0.2.0', + agent: '' + } + }; + compress(data); + + const blocks = Object.entries(data.targets[0].blocks); + t.equal(blocks.length, 3); + + const [parentId, parentBlock] = blocks.find(i => i[1].opcode === 'looks_switchbackdropto'); + const [inputId, inputBlock] = blocks.find(i => i[1].opcode === 'operator_not'); + const [shadowId, shadowBlock] = blocks.find(i => i[1].opcode === 'looks_backdrops'); + + t.equal(parentBlock.inputs.BACKDROP.length, 3); + t.equal(parentBlock.inputs.BACKDROP[0], 3); + t.equal(parentBlock.inputs.BACKDROP[1], inputId); + t.equal(parentBlock.inputs.BACKDROP[2], shadowId); + + t.equal(inputBlock.parent, parentId); + t.equal(shadowBlock.parent, parentId); + + t.end(); +}); + +test('Compressed IDs will not collide with uncompressed IDs', t => { + const soup = 'abcdefghjijklmnopqstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789'; + const items = [ + [soup, '', ''], + ['', soup, ''], + ['', '', soup] + ]; + for (const [variableSoup, listSoup, broadcastSoup] of items) { + const data = { + targets: [ + { + isStage: true, + name: 'Stage', + variables: Object.fromEntries( + variableSoup.split('').map(id => [id, [id, 0]]) + ), + lists: Object.fromEntries( + listSoup.split('').map(id => [id, [id, []]]) + ), + broadcasts: Object.fromEntries( + broadcastSoup.split('').map(id => [id, id]) + ), + blocks: { + 'CmRa^i]o}QL77;hk:54o': { + opcode: 'looks_switchbackdropto', + next: null, + parent: null, + inputs: { + BACKDROP: [ + 3, + 'cq84G6uywD{m2R,E03Ci', + 'E3/*4H*xk38{=*U;bVWm' + ] + }, + fields: {}, + shadow: false, + topLevel: true, + x: 409, + y: 300 + }, + 'cq84G6uywD{m2R,E03Ci': { + opcode: 'operator_not', + next: null, + parent: 'CmRa^i]o}QL77;hk:54o', + inputs: {}, + fields: {}, + shadow: false, + topLevel: false + }, + 'E3/*4H*xk38{=*U;bVWm': { + opcode: 'looks_backdrops', + next: null, + parent: 'CmRa^i]o}QL77;hk:54o', + inputs: {}, + fields: { + BACKDROP: [ + 'backdrop1', + null + ] + }, + shadow: true, + topLevel: false + } + }, + comments: { + 'ds{.EoY%0^6vO1WH0/9d': { + blockId: null, + x: 400, + y: 401, + width: 402, + height: 403, + minimized: false, + text: '4' + }, + 'blh[bsi@XtCkGh!-J5aa': { + blockId: null, + x: 500, + y: 501, + width: 502, + height: 503, + minimized: false, + text: '5' + }, + '7#YgytOiJHs(Ne6,2i9(': { + blockId: null, + x: 600, + y: 601, + width: 602, + height: 603, + minimized: false, + text: '6' + } + }, + currentCostume: 0, + costumes: [], + sounds: [], + volume: 100, + layerOrder: 0, + tempo: 60, + videoTransparency: 50, + videoState: 'on', + textToSpeechLanguage: null + } + ], + monitors: [], + extensions: [], + meta: { + semver: '3.0.0', + vm: '0.2.0', + agent: '' + } + }; + compress(data); + + const uncompressedIDs = [ + ...Object.keys(data.targets[0].variables), + ...Object.keys(data.targets[0].lists), + ...Object.keys(data.targets[0].broadcasts) + ]; + const compressedIDs = [ + ...Object.keys(data.targets[0].blocks), + ...Object.keys(data.targets[0].comments) + ]; + for (const compressedID of compressedIDs) { + t.notOk(uncompressedIDs.includes(compressedID), `${compressedID} does not collide`); + } + } + + t.end(); +}); + +test('Script execution order is preserved', t => { + const originalBlocks = {}; + + const blockIds = []; + for (let i = 0; i < 1000; i++) { + if (i === 339) { + blockIds.push('muffin'); + } else if (i === 555) { + blockIds.push('555'); + } + blockIds.push(uid()); + } + blockIds.push('apple'); + blockIds.push('-1'); + blockIds.push('45'); + + for (const blockId of blockIds) { + originalBlocks[blockId] = { + opcode: 'event_whenbroadcastreceived', + next: null, + parent: null, + inputs: {}, + fields: { + BROADCAST_OPTION: [ + `broadcast-name-${blockId}`, + `broadcast-id-${blockId}` + ] + }, + shadow: false, + topLevel: true, + x: -10, + y: 420 + }; + } + + const data = { + targets: [ + { + isStage: true, + name: 'Stage', + variables: {}, + lists: {}, + broadcasts: {}, + blocks: originalBlocks, + comments: {}, + currentCostume: 0, + costumes: [], + sounds: [], + volume: 100, + layerOrder: 0, + tempo: 60, + videoTransparency: 50, + videoState: 'on', + textToSpeechLanguage: null + } + ], + monitors: [], + extensions: [], + meta: { + semver: '3.0.0', + vm: '0.2.0', + agent: '' + } + }; + compress(data); + + // Sanity check: Make sure the new object is actually different + const newBlocks = data.targets[0].blocks; + t.not(originalBlocks, newBlocks); + t.notSame(Object.keys(originalBlocks), Object.keys(newBlocks)); + + // Check that the order has not changed + const newBlockValues = Object.values(newBlocks); + t.same(Object.values(originalBlocks), newBlockValues); + t.equal(newBlockValues[0].fields.BROADCAST_OPTION[0], 'broadcast-name-45'); + t.equal(newBlockValues[1].fields.BROADCAST_OPTION[0], 'broadcast-name-555'); + t.equal(newBlockValues[339 + 2].fields.BROADCAST_OPTION[0], 'broadcast-name-muffin'); + t.equal(newBlockValues[newBlockValues.length - 2].fields.BROADCAST_OPTION[0], 'broadcast-name-apple'); + t.equal(newBlockValues[newBlockValues.length - 1].fields.BROADCAST_OPTION[0], 'broadcast-name--1'); + + // Check that the new IDs do not look like array indexes as their enumeration + // order could cause unexpected behavior in other places. + for (const newBlockId of Object.keys(newBlocks)) { + // The actual definition of an array index is: https://tc39.es/ecma262/#array-index + // This approximation is currently good enough + if (!Number.isNaN(+newBlockId)) { + t.fail(`${newBlockId} might be treated as an array index`); + } + } + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_costume_and_sound_inputs.js b/local-scratch-vm/test/unit/tw_costume_and_sound_inputs.js new file mode 100644 index 0000000000000000000000000000000000000000..4294f5ee48d2f00cfeaf6adfbed061e15736c8ac --- /dev/null +++ b/local-scratch-vm/test/unit/tw_costume_and_sound_inputs.js @@ -0,0 +1,57 @@ +const {test} = require('tap'); +const Runtime = require('../../src/engine/runtime'); +const BlockType = require('../../src/extension-support/block-type'); +const ArgumentType = require('../../src/extension-support/argument-type'); + +const extension = { + id: 'costumesoundtest', + name: 'Costume & Sound', + blocks: [ + { + blockType: BlockType.COMMAND, + opcode: 'costume', + text: 'costume [a] [b]', + arguments: { + a: { + type: ArgumentType.COSTUME + }, + b: { + type: ArgumentType.COSTUME, + defaultValue: 'default costume' + } + } + }, + { + blockType: BlockType.COMMAND, + opcode: 'sound', + text: 'sound [a] [b]', + arguments: { + a: { + type: ArgumentType.SOUND + }, + b: { + type: ArgumentType.SOUND, + defaultValue: 'default sound' + } + } + } + ] +}; + +test('COSTUME and SOUND inputs generate correct scratch-blocks XML', t => { + const rt = new Runtime(); + rt.on('EXTENSION_ADDED', info => { + /* eslint-disable max-len */ + t.equal( + info.blocks[0].xml, + 'default costume' + ); + t.equal( + info.blocks[1].xml, + 'default sound' + ); + /* eslint-enable max-len */ + t.end(); + }); + rt._registerExtensionPrimitives(extension); +}); diff --git a/local-scratch-vm/test/unit/tw_costume_import_export.js b/local-scratch-vm/test/unit/tw_costume_import_export.js new file mode 100644 index 0000000000000000000000000000000000000000..2a3eb311bd2a98cf35f502735c2a5f101904051a --- /dev/null +++ b/local-scratch-vm/test/unit/tw_costume_import_export.js @@ -0,0 +1,78 @@ +const { + parseVectorMetadata, + exportCostume +} = require('../../src/serialization/tw-costume-import-export'); +const {test} = require('tap'); + +/* global TextEncoder */ + +test('parseVectorMetadata', t => { + /* eslint-disable max-len */ + t.same( + parseVectorMetadata(''), + [0, 0] + ); + t.same( + parseVectorMetadata(''), + [0, 0] + ); + t.same( + parseVectorMetadata(''), + [-1, 3] + ); + t.same( + parseVectorMetadata(''), + [106.62300344745225, -11.822572945859918] + ); + t.same( + parseVectorMetadata(''), + null + ); + t.same( + parseVectorMetadata(''), + null + ); + t.same( + parseVectorMetadata(''), + null + ); + /* eslint-enable max-len */ + + t.end(); +}); + +test('exportCostume', t => { + // PNG and JPG costumes are exported as-is + t.same(exportCostume({ + dataFormat: 'png', + asset: { + data: new Uint8Array([10, 20, 30]) + } + }), new Uint8Array([10, 20, 30])); + t.same(exportCostume({ + dataFormat: 'jpg', + asset: { + data: new Uint8Array([40, 50, 60]) + } + }), new Uint8Array([40, 50, 60])); + + t.same(exportCostume({ + dataFormat: 'svg', + asset: { + data: new TextEncoder().encode('') + }, + rotationCenterX: 89.339393, + rotationCenterY: -3.7373 + }), new TextEncoder().encode('')); + + t.same(exportCostume({ + dataFormat: 'svg', + asset: { + data: new TextEncoder().encode('') + }, + rotationCenterX: 89.339393, + rotationCenterY: -3.7373 + }), new TextEncoder().encode('')); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_extension_api_common.js b/local-scratch-vm/test/unit/tw_extension_api_common.js new file mode 100644 index 0000000000000000000000000000000000000000..051fcf134fc29fbb6b696acd644df0cb39c86905 --- /dev/null +++ b/local-scratch-vm/test/unit/tw_extension_api_common.js @@ -0,0 +1,34 @@ +const ScratchCommon = require('../../src/extension-support/tw-extension-api-common'); +const {test} = require('tap'); + +test('ArgumentType', t => { + t.equal(ScratchCommon.ArgumentType.ANGLE, 'angle'); + t.end(); +}); + +test('BlockType', t => { + t.equal(ScratchCommon.BlockType.BOOLEAN, 'Boolean'); + t.end(); +}); + +test('TargetType', t => { + t.equal(ScratchCommon.TargetType.SPRITE, 'sprite'); + t.end(); +}); + +test('Cast', t => { + // Cast is thoroughly tested elsewhere. We just want to make sure that the public methods + // don't get deleted unexpectedly. + t.equal(ScratchCommon.Cast.toNumber('5'), 5); + t.equal(ScratchCommon.Cast.toBoolean('true'), true); + t.equal(ScratchCommon.Cast.toString('something'), 'something'); + t.same(ScratchCommon.Cast.toRgbColorList('#abcdef'), [0xab, 0xcd, 0xef]); + t.same(ScratchCommon.Cast.toRgbColorObject('#abcdef'), {r: 0xab, g: 0xcd, b: 0xef}); + t.equal(ScratchCommon.Cast.isWhiteSpace(''), true); + t.equal(ScratchCommon.Cast.compare(1, 2), -1); + t.equal(ScratchCommon.Cast.isInt(5.5), false); + t.type(ScratchCommon.Cast.LIST_INVALID, 'string'); + t.type(ScratchCommon.Cast.LIST_ALL, 'string'); + t.equal(ScratchCommon.Cast.toListIndex('1.5', 10, false), 1); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_extension_manager.js b/local-scratch-vm/test/unit/tw_extension_manager.js new file mode 100644 index 0000000000000000000000000000000000000000..c7b18defb690316f0c929da33c54e46d4b066b30 --- /dev/null +++ b/local-scratch-vm/test/unit/tw_extension_manager.js @@ -0,0 +1,80 @@ +const {test} = require('tap'); +const ExtensionManager = require('../../src/extension-support/extension-manager'); +const VM = require('../../src/virtual-machine'); + +test('isBuiltinExtension', t => { + const fakeRuntime = {}; + const manager = new ExtensionManager(fakeRuntime); + t.equal(manager.isBuiltinExtension('pen'), true); + t.equal(manager.isBuiltinExtension('lksdfjlskdf'), false); + t.end(); +}); + +test('_isValidExtensionURL', t => { + const fakeRuntime = {}; + const manager = new ExtensionManager(fakeRuntime); + t.equal(manager._isValidExtensionURL('fetch'), false); + t.equal(manager._isValidExtensionURL(''), false); + t.equal(manager._isValidExtensionURL('extensions.turbowarp.org/fetch.js'), false); + t.equal(manager._isValidExtensionURL('https://extensions.turbowarp.org/fetch.js'), true); + t.equal(manager._isValidExtensionURL('http://extensions.turbowarp.org/fetch.js'), true); + t.equal(manager._isValidExtensionURL('http://localhost:8000'), true); + t.equal(manager._isValidExtensionURL('data:application/javascript;base64,YWxlcnQoMSk='), true); + t.equal(manager._isValidExtensionURL('file:///home/test/extension.js'), true); + t.end(); +}); + +test('loadExtensionURL, getExtensionURLs, deduplication', async t => { + const vm = new VM(); + + let loadedExtensions = 0; + vm.extensionManager.securityManager.getSandboxMode = () => 'unsandboxed'; + global.document = { + createElement: () => { + loadedExtensions++; + const element = {}; + setTimeout(() => { + global.Scratch.extensions.register({ + getInfo: () => ({ + id: `extension${loadedExtensions}` + }) + }); + }); + return element; + }, + body: { + appendChild: () => {} + } + }; + + const url1 = 'https://turbowarp.org/1.js'; + t.equal(vm.extensionManager.isExtensionURLLoaded(url1), false); + t.same(vm.extensionManager.getExtensionURLs(), {}); + await vm.extensionManager.loadExtensionURL(url1); + t.equal(vm.extensionManager.isExtensionURLLoaded(url1), true); + t.equal(loadedExtensions, 1); + t.same(vm.extensionManager.getExtensionURLs(), { + extension1: url1 + }); + + // Loading the extension again should do nothing. + await vm.extensionManager.loadExtensionURL(url1); + t.equal(vm.extensionManager.isExtensionURLLoaded(url1), true); + t.equal(loadedExtensions, 1); + t.same(vm.extensionManager.getExtensionURLs(), { + extension1: url1 + }); + + // Loading another extension should work + const url2 = 'https://turbowarp.org/2.js'; + t.equal(vm.extensionManager.isExtensionURLLoaded(url2), false); + await vm.extensionManager.loadExtensionURL(url2); + t.equal(vm.extensionManager.isExtensionURLLoaded(url2), true); + t.equal(loadedExtensions, 2); + t.same(vm.extensionManager.getExtensionURLs(), { + extension1: url1, + extension2: url2 + }); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_jsexecute.js b/local-scratch-vm/test/unit/tw_jsexecute.js new file mode 100644 index 0000000000000000000000000000000000000000..97932b161b8f7b47cb05ba02b6b78eef09baefb4 --- /dev/null +++ b/local-scratch-vm/test/unit/tw_jsexecute.js @@ -0,0 +1,76 @@ +const {test} = require('tap'); +const jsexecute = require('../../src/compiler/jsexecute'); +const Cast = require('../../src/util/cast'); +const {stringify} = require('@turbowarp/json'); + +const evaluateRuntimeFunction = functionName => jsexecute.scopedEval(functionName); + +test('runtimeFunctions are valid', t => { + for (const functionName of Object.keys(jsexecute.runtimeFunctions)) { + const fn = evaluateRuntimeFunction(functionName); + t.type(fn, 'function', `${functionName} is function`); + } + t.end(); +}); + +test('all runtimeFunctions can be used together', t => { + const script = Object.keys(jsexecute.runtimeFunctions).join(';'); + jsexecute.scopedEval(script); + t.end(); +}); + +test('comparison functions are equivalent to Cast.compare', t => { + const VALUES = [ + 0, + -0, + 1, + '0', + '', + '.', + true, + false, + 'true', + 'false', + 'true ', + 'apple', + 'Apple', + 'Apple ', + ' 123', + ' 123.0', + '+123.5', + 123, + 0.23, + '0.23', + '.23', + '-.23', + '0.0', + NaN, + 'NaN', + Infinity, + -Infinity, + 'Infinity', + '-Infinity', + '\t', + '\r\n\u00a0' + ]; + const compareEqual = evaluateRuntimeFunction('compareEqual'); + const compareGreaterThan = evaluateRuntimeFunction('compareGreaterThan'); + const compareLessThan = evaluateRuntimeFunction('compareLessThan'); + for (const a of VALUES) { + for (const b of VALUES) { + // Because there are so many tests, calling t.ok() each time is actually quite slow, + // so only call into tap when something failed. + const cast = Cast.compare(a, b); + if (compareEqual(a, b) !== (cast === 0)) { + t.fail(`${stringify(a)} should be === ${stringify(b)}`); + } + if (compareGreaterThan(a, b) !== (cast > 0)) { + t.fail(`${stringify(a)} should be > ${stringify(b)}`); + } + if (compareLessThan(a, b) !== (cast < 0)) { + t.fail(`${stringify(a)} should be < ${stringify(b)}`); + } + } + } + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_number_argument.js b/local-scratch-vm/test/unit/tw_number_argument.js new file mode 100644 index 0000000000000000000000000000000000000000..d8acebd896f38f80b454b56b667f2a99035b545e --- /dev/null +++ b/local-scratch-vm/test/unit/tw_number_argument.js @@ -0,0 +1,77 @@ +const {test} = require('tap'); +const Runtime = require('../../src/engine/runtime'); +const BlockType = require('../../src/extension-support/block-type'); +const ArgumentType = require('../../src/extension-support/argument-type'); + +test('NUMBER argument defaultValue', t => { + const runtime = new Runtime(); + runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => { + /* eslint-disable max-len */ + t.equal( + categoryInfo.blocks[0].xml, + '' + ); + t.equal( + categoryInfo.blocks[1].xml, + '' + ); + t.equal( + categoryInfo.blocks[2].xml, + '0' + ); + t.equal( + categoryInfo.blocks[3].xml, + '0' + ); + /* eslint-enable max-len */ + t.end(); + }); + runtime._registerExtensionPrimitives({ + id: 'testextension', + blocks: [ + { + type: BlockType.COMMAND, + opcode: 'testNone', + text: 'block [a]', + arguments: { + a: { + type: ArgumentType.NUMBER + } + } + }, + { + type: BlockType.COMMAND, + opcode: 'testEmptyString', + text: 'block [a]', + arguments: { + a: { + type: ArgumentType.NUMBER, + defaultValue: '' + } + } + }, + { + type: BlockType.COMMAND, + opcode: 'testZeroString', + text: 'block [a]', + arguments: { + a: { + type: ArgumentType.NUMBER, + defaultValue: '0' + } + } + }, + { + type: BlockType.COMMAND, + opcode: 'testZeroNumber', + text: 'block [a]', + arguments: { + a: { + type: ArgumentType.NUMBER, + defaultValue: 0 + } + } + } + ] + }); +}); diff --git a/local-scratch-vm/test/unit/tw_performance_measure_error.js b/local-scratch-vm/test/unit/tw_performance_measure_error.js new file mode 100644 index 0000000000000000000000000000000000000000..5995e8c07833f9d21b394392d6df4c19b7b9ae86 --- /dev/null +++ b/local-scratch-vm/test/unit/tw_performance_measure_error.js @@ -0,0 +1,19 @@ +const test = require('tap').test; +const fs = require('fs'); +const path = require('path'); +const VirtualMachine = require('../../src/virtual-machine'); + +global.performance = { + mark () { + // No-op + }, + measure () { + throw new Error('Mock error to simulate browser garbage collecting one of the marks before this code runs'); + } +}; + +test('performance.measure() error in loadProject is ignored', async t => { + const vm = new VirtualMachine(); + await vm.loadProject(fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'tw-empty-project.sb3'))); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_sandboxed_extensions.js b/local-scratch-vm/test/unit/tw_sandboxed_extensions.js new file mode 100644 index 0000000000000000000000000000000000000000..6d4c644674add952a68a3f31feacfbebe2c6c899 --- /dev/null +++ b/local-scratch-vm/test/unit/tw_sandboxed_extensions.js @@ -0,0 +1,58 @@ +const {test} = require('tap'); + +/* globals Request */ +global.Request = class { + constructor (url) { + this.url = url; + } +}; +global.fetch = (url, options = {}) => ( + Promise.resolve(`[Response ${url instanceof Request ? url.url : url} options=${JSON.stringify(options)}]`) +); + +// Need to trick the extension API to think it's running in a worker +// It will not actually use this object ever. +global.self = {}; +// This will install extension worker APIs onto `global` +require('../../src/extension-support/extension-worker'); + +test('basic API', t => { + t.type(global.Scratch.extensions.register, 'function'); + t.equal(global.Scratch.ArgumentType.BOOLEAN, 'Boolean'); + t.equal(global.Scratch.BlockType.REPORTER, 'reporter'); + t.end(); +}); + +test('not unsandboxed', t => { + t.not(global.Scratch.extensions.unsandboxed, true); + t.end(); +}); + +test('Cast', t => { + // Cast is thoroughly tested elsewhere + t.equal(global.Scratch.Cast.toString(5), '5'); + t.equal(global.Scratch.Cast.toNumber(' 5'), 5); + t.equal(global.Scratch.Cast.toBoolean('true'), true); + t.end(); +}); + +test('fetch', async t => { + t.equal(await global.Scratch.canFetch('https://untrusted.example/'), true); + t.equal(await global.Scratch.fetch('https://untrusted.example/'), '[Response https://untrusted.example/ options={}]'); + t.equal(await global.Scratch.fetch('https://untrusted.example/', { + method: 'POST' + }), `[Response https://untrusted.example/ options={"method":"POST"}]`); + t.end(); +}); + +test('openWindow', async t => { + t.equal(await global.Scratch.canOpenWindow('https://example.com/'), false); + await t.rejects(global.Scratch.openWindow('https://example.com/'), /^Scratch\.openWindow not supported in sandboxed extensions$/); + t.end(); +}); + +test('redirect', async t => { + t.equal(await global.Scratch.canRedirect('https://example.com/'), false); + await t.rejects(global.Scratch.redirect('https://example.com/'), /^Scratch\.redirect not supported in sandboxed extensions$/); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_scratchx.js b/local-scratch-vm/test/unit/tw_scratchx.js new file mode 100644 index 0000000000000000000000000000000000000000..90ecb1c307427bd4d91907e1b5975b60e85072be --- /dev/null +++ b/local-scratch-vm/test/unit/tw_scratchx.js @@ -0,0 +1,250 @@ +const ScratchXUtilities = require('../../src/extension-support/tw-scratchx-utilities'); +const ScratchExtensions = require('../../src/extension-support/tw-scratchx-compatibility-layer'); +const {test} = require('tap'); + +test('argument index to id', t => { + t.equal(ScratchXUtilities.argumentIndexToId(0), '0'); + t.equal(ScratchXUtilities.argumentIndexToId(1), '1'); + t.equal(ScratchXUtilities.argumentIndexToId(2), '2'); + t.equal(ScratchXUtilities.argumentIndexToId(3), '3'); + t.equal(ScratchXUtilities.argumentIndexToId(39), '39'); + t.equal(ScratchXUtilities.argumentIndexToId(1000000000), '1000000000'); + t.end(); +}); + +test('generate extension id', t => { + t.equal(ScratchXUtilities.generateExtensionId('Spotify'), 'sbxspotify'); + t.equal(ScratchXUtilities.generateExtensionId('Spo _t ify'), 'sbxspotify'); + t.equal(ScratchXUtilities.generateExtensionId('Spo _t $#@! 3ify😮'), 'sbxspot3ify'); + t.end(); +}); + +test('register', t => { + t.type(ScratchExtensions.register, 'function'); + t.end(); +}); + +test('complex extension', async t => { + let stepsMoved = 0; + const moveSteps = n => { + stepsMoved += n; + }; + + let doNothingCalled = false; + const doNothing = () => { + doNothingCalled = true; + }; + + const fetch = (url, callback) => { + callback(`Fetched: ${url}`); + return 'This value should be ignored.'; + }; + + const multiplyAndAppend = (a, b, c) => `${a * b}${c}`; + + const repeat = (string, count, callback) => { + callback(string.repeat(count)); + return 'This value should be ignored.'; + }; + + const touching = sprite => sprite === 'Sprite9'; + + const converted = ScratchExtensions.convert( + 'My Extension', + { + blocks: [ + ['', 'move %n steps', 'moveSteps', 50], + [' ', 'do nothing', 'doNothing', 100, 200], + ['w', 'fetch %m:urls1', 'fetch'], + [' '], + ['r', 'multiply %n by %n and append %s', 'multiplyAndAppend'], + ['R', 'repeat %m.myMenu %n', 'repeat', ''], + ['-'], + ['b', 'touching %s', 'touching', 'Sprite1'] + ], + menus: { + myMenu: ['abc', 'def', 123, true, false], + urls1: ['https://example.com/', 'https://example.org/'] + }, + url: 'https://turbowarp.org/myextensiondocs.html' + }, + { + unusedGarbage: 10, + moveSteps, + doNothing, + fetch, + multiplyAndAppend, + repeat, + touching + } + ); + + const info = converted.getInfo(); + t.equal(info.id, 'sbxmyextension'); + t.equal(info.docsURI, 'https://turbowarp.org/myextensiondocs.html'); + + t.same(info.blocks, [ + { + opcode: 'moveSteps', + text: 'move [0] steps', + blockType: 'command', + arguments: [ + { + type: 'number', + defaultValue: 50 + } + ] + }, + { + opcode: 'doNothing', + text: 'do nothing', + blockType: 'command', + arguments: [] + }, + { + opcode: 'fetch', + text: 'fetch [0]', + blockType: 'command', + arguments: [ + { + type: 'string', + menu: 'urls1' + } + ] + }, + '---', + { + opcode: 'multiplyAndAppend', + text: 'multiply [0] by [1] and append [2]', + blockType: 'reporter', + arguments: [ + { + type: 'number', + defaultValue: 0 + }, + { + type: 'number', + defaultValue: 0 + }, + { + type: 'string', + defaultValue: '' + } + ] + }, + { + opcode: 'repeat', + text: 'repeat [0] [1]', + blockType: 'reporter', + arguments: [ + { + type: 'string', + menu: 'myMenu', + defaultValue: '' + }, + { + type: 'number', + defaultValue: 0 + } + ] + }, + '---', + { + opcode: 'touching', + text: 'touching [0]', + blockType: 'Boolean', + arguments: [ + { + type: 'string', + defaultValue: 'Sprite1' + } + ] + } + ]); + + t.same(info.menus, { + myMenu: { + items: ['abc', 'def', 123, true, false] + }, + urls1: { + items: ['https://example.com/', 'https://example.org/'] + } + }); + + // Now let's make sure that the converter has properly wrapped our functions. + t.equal(stepsMoved, 0); + t.equal(converted.moveSteps({ + 0: 30 + }), undefined); + t.equal(stepsMoved, 30); + + t.equal(doNothingCalled, false); + t.equal(converted.doNothing({}), undefined); + t.equal(doNothingCalled, true); + + t.type(converted.fetch({ + 0: 'https://example.com/' + }).then, 'function'); + t.equal(await converted.fetch({ + 0: 'https://example.com/' + }), 'Fetched: https://example.com/'); + + t.equal(converted.multiplyAndAppend({ + 0: 31, + 1: 7, + 2: 'Cat' + }), '217Cat'); + + t.type(converted.repeat({ + 0: '', + 1: 0 + }).then, 'function'); + t.equal(await converted.repeat({ + 0: 'scratchx', + 1: 3 + }), 'scratchxscratchxscratchx'); + + t.equal(converted.touching({ + 0: 'Sprite1' + }), false); + t.equal(converted.touching({ + 0: 'Sprite9' + }), true); + + t.end(); +}); + +test('display name', t => { + const converted = ScratchExtensions.convert( + 'Internal Name', + { + blocks: [], + displayName: 'Display Name' + }, + { + + } + ); + t.equal(converted.getInfo().name, 'Display Name'); + t.end(); +}); + +test('_getStatus', t => { + const _getStatus = () => ({ + status: 2, + msg: 'Ready' + }); + const converted = ScratchExtensions.convert( + 'Name', + { + blocks: [] + }, + { + _getStatus: _getStatus, + unusedProperty: 10 + } + ); + t.equal(converted._getStatus, _getStatus); + t.equal('unusedProperty' in converted, false); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_stored_settings.js b/local-scratch-vm/test/unit/tw_stored_settings.js new file mode 100644 index 0000000000000000000000000000000000000000..7b88bf5361290061cc7d2bfb4eaaf87f75b6df20 --- /dev/null +++ b/local-scratch-vm/test/unit/tw_stored_settings.js @@ -0,0 +1,100 @@ +const tap = require('tap'); +const path = require('path'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const makeTestStorage = require('../fixtures/make-test-storage'); +const VirtualMachine = require('../../src/virtual-machine'); + +const test = tap.test; + +const makeVM = () => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + return vm; +}; + +for (const file of ['empty-comment.sb3', 'no-comment.sb3']) { + test(`serializes and deserializes settings (${file})`, t => { + const project = readFileToBuffer(path.resolve(__dirname, `../fixtures/tw-stored-settings/${file}`)); + const vm = makeVM(); + vm.loadProject(project).then(() => { + vm.setFramerate(45); + vm.setTurboMode(true); + vm.setInterpolation(true); + vm.setRuntimeOptions({ + maxClones: Infinity, + miscLimits: false, + fencing: false + }); + vm.setStageSize(100, 101); + vm.storeProjectOptions(); + + const newVM = makeVM(); + newVM.loadProject(vm.toJSON()) + .then(() => { + t.equal(newVM.runtime.framerate, vm.runtime.framerate); + t.equal(newVM.runtime.turboMode, vm.runtime.turboMode); + t.same(newVM.runtime.runtimeOptions, vm.runtime.runtimeOptions); + t.equal(newVM.runtime.interpolationEnabled, vm.runtime.interpolationEnabled); + t.equal(newVM.runtime.stageWidth, vm.runtime.stageWidth); + t.equal(newVM.runtime.stageHeight, vm.runtime.stageHeight); + t.end(); + }); + }); + }); +} + +test('Reuses comment if it already exists', t => { + const project = readFileToBuffer(path.resolve(__dirname, `../fixtures/tw-stored-settings/empty-comment.sb3`)); + const vm = makeVM(); + vm.loadProject(project) + .then(() => { + t.equal(Object.keys(vm.runtime.getTargetForStage().comments).length, 1); + vm.setFramerate(99); + vm.storeProjectOptions(); + t.equal(Object.keys(vm.runtime.getTargetForStage().comments).length, 1); + t.end(); + }); +}); + +test('Storing settings emits workspace update only when stage open', t => { + const project = readFileToBuffer(path.resolve(__dirname, `../fixtures/tw-stored-settings/sprite.sb3`)); + const vm = makeVM(); + vm.loadProject(project) + .then(() => { + let didFireUpdate = false; + vm.on('workspaceUpdate', () => { + didFireUpdate = true; + }); + vm.storeProjectOptions(); + t.equal(didFireUpdate, false); + vm.setEditingTarget(vm.runtime.getTargetForStage().id); + vm.storeProjectOptions(); + t.equal(didFireUpdate, true); + t.end(); + }); +}); + +test('Storing settings emits project changed', t => { + const project = readFileToBuffer(path.resolve(__dirname, `../fixtures/tw-stored-settings/sprite.sb3`)); + const vm = makeVM(); + vm.loadProject(project) + .then(() => { + t.plan(1); + vm.on('PROJECT_CHANGED', () => { + t.pass(); + }); + vm.storeProjectOptions(); + t.end(); + }); +}); + +test('Stored turbo mode emits event on VM', async t => { + const vm = makeVM(); + const project = readFileToBuffer(path.resolve(__dirname, '../fixtures/tw-stored-settings/turbo-mode.sb3')); + t.plan(1); + vm.on('TURBO_MODE_ON', () => { + t.pass('emitted TURBO_MODE_ON'); + }); + await vm.loadProject(project); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_unsandboxed_extensions.js b/local-scratch-vm/test/unit/tw_unsandboxed_extensions.js new file mode 100644 index 0000000000000000000000000000000000000000..7f3afe68f7f5fb576dfd79b69e9279d7fa47b51d --- /dev/null +++ b/local-scratch-vm/test/unit/tw_unsandboxed_extensions.js @@ -0,0 +1,245 @@ +const tap = require('tap'); +const UnsandboxedExtensionRunner = require('../../src/extension-support/tw-unsandboxed-extension-runner'); +const VirtualMachine = require('../../src/virtual-machine'); + +// Mock enough of the document API for the extension runner to think it works. +// To more accurately test this, we want to make sure that the URLs we pass in are just strings. +// We use a bit of hacky state here to make our document mock know what function to run +// when a script with a given URL "loads" +const scriptCallbacks = new Map(); +const setScript = (src, callback) => { + scriptCallbacks.set(src, callback); +}; +global.document = { + createElement: tagName => { + if (tagName.toLowerCase() !== 'script') { + throw new Error(`Unknown element: ${tagName}`); + } + return { + tagName: 'SCRIPT', + src: '', + onload: () => {}, + onerror: () => {} + }; + }, + body: { + appendChild: element => { + if (element.tagName === 'SCRIPT') { + setTimeout(() => { + const callback = scriptCallbacks.get(element.src); + if (callback) { + callback(); + element.onload(); + } else { + element.onerror(); + } + }, 50); + } + } + } +}; + +// Mock various DOM APIs for fetching, window opening, redirecting, etc. +/* globals Request */ +global.Request = class { + constructor (url) { + this.url = url; + } +}; +global.fetch = (url, options = {}) => ( + Promise.resolve(`[Response ${url instanceof Request ? url.url : url} options=${JSON.stringify(options)}]`) +); +global.window = { + open: (url, target, features) => `[Window ${url} target=${target || ''} features=${features || ''}]` +}; + +tap.beforeEach(async () => { + scriptCallbacks.clear(); + global.location = { + href: 'https://example.com/' + }; +}); + +const {test} = tap; + +test('basic API', async t => { + t.plan(9); + const vm = new VirtualMachine(); + class MyExtension {} + setScript('https://turbowarp.org/1.js', () => { + t.equal(global.Scratch.vm, vm); + t.equal(global.Scratch.renderer, vm.runtime.renderer); + t.equal(global.Scratch.extensions.unsandboxed, true); + + // These APIs are tested elsewhere, just make sure they're getting exported + t.equal(global.Scratch.ArgumentType.NUMBER, 'number'); + t.equal(global.Scratch.BlockType.REPORTER, 'reporter'); + t.equal(global.Scratch.TargetType.SPRITE, 'sprite'); + t.equal(global.Scratch.Cast.toNumber('3.14'), 3.14); + + global.Scratch.extensions.register(new MyExtension()); + }); + const extensions = await UnsandboxedExtensionRunner.load('https://turbowarp.org/1.js', vm); + t.equal(extensions.length, 1); + t.ok(extensions[0] instanceof MyExtension); + t.end(); +}); + +test('multiple VMs loading extensions', async t => { + const vm1 = new VirtualMachine(); + const vm2 = new VirtualMachine(); + + class Extension1 {} + class Extension2 {} + + let api1 = null; + setScript('https://turbowarp.org/1.js', async () => { + // Even if this extension takes a while to register, we should still have our own + // global.Scratch. + await new Promise(resolve => setTimeout(resolve, 100)); + + if (api1) throw new Error('already ran 1'); + api1 = global.Scratch; + global.Scratch.extensions.register(new Extension1()); + }); + + let api2 = null; + setScript('https://turbowarp.org/2.js', () => { + if (api2) throw new Error('already ran 2'); + api2 = global.Scratch; + global.Scratch.extensions.register(new Extension2()); + }); + + const extensions = await Promise.all([ + UnsandboxedExtensionRunner.load('https://turbowarp.org/1.js', vm1), + UnsandboxedExtensionRunner.load('https://turbowarp.org/2.js', vm2) + ]); + + t.not(api1, api2); + t.type(api1.extensions.register, 'function'); + t.type(api2.extensions.register, 'function'); + t.equal(api1.vm, vm1); + t.equal(api2.vm, vm2); + + t.equal(extensions.length, 2); + t.equal(extensions[0].length, 1); + t.equal(extensions[1].length, 1); + t.ok(extensions[0][0] instanceof Extension1); + t.ok(extensions[1][0] instanceof Extension2); + + t.end(); +}); + +test('register multiple extensions in one script', async t => { + const vm = new VirtualMachine(); + class Extension1 {} + class Extension2 {} + setScript('https://turbowarp.org/multiple.js', () => { + global.Scratch.extensions.register(new Extension1()); + global.Scratch.extensions.register(new Extension2()); + }); + const extensions = await UnsandboxedExtensionRunner.load('https://turbowarp.org/multiple.js', vm); + t.equal(extensions.length, 2); + t.ok(extensions[0] instanceof Extension1); + t.ok(extensions[1] instanceof Extension2); + t.end(); +}); + +test('extension error results in rejection', async t => { + const vm = new VirtualMachine(); + try { + await UnsandboxedExtensionRunner.load('https://turbowarp.org/404.js', vm); + // Above should throw an error as the script will not load successfully + t.fail(); + } catch (e) { + t.pass(); + } + t.end(); +}); + +test('ScratchX', async t => { + const vm = new VirtualMachine(); + setScript('https://turbowarp.org/scratchx.js', () => { + const ext = { + test: () => 2 + }; + const descriptor = { + blocks: [ + ['r', 'test', 'test'] + ] + }; + global.ScratchExtensions.register('Test', descriptor, ext); + }); + const extensions = await UnsandboxedExtensionRunner.load('https://turbowarp.org/scratchx.js', vm); + t.equal(extensions.length, 1); + t.equal(extensions[0].test(), 2); + t.end(); +}); + +test('canFetch', async t => { + // see tw_security_manager.js + const vm = new VirtualMachine(); + UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm); + const result = global.Scratch.canFetch('https://example.com/'); + t.type(result, Promise); + t.equal(await result, true); + t.end(); +}); + +test('fetch', async t => { + const vm = new VirtualMachine(); + UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm); + global.Scratch.canFetch = url => url === 'https://example.com/2'; + await t.rejects(global.Scratch.fetch('https://example.com/1'), /Permission to fetch https:\/\/example.com\/1 rejected/); + await t.rejects(global.Scratch.fetch(new Request('https://example.com/1')), /Permission to fetch https:\/\/example.com\/1 rejected/); + t.equal(await global.Scratch.fetch('https://example.com/2'), '[Response https://example.com/2 options={"redirect":"error"}]'); + t.equal(await global.Scratch.fetch(new Request('https://example.com/2')), '[Response https://example.com/2 options={"redirect":"error"}]'); + t.equal(await global.Scratch.fetch('https://example.com/2', { + // redirect should be ignored and always set to error + redirect: 'follow', + method: 'POST', + body: 'abc' + }), '[Response https://example.com/2 options={"redirect":"error","method":"POST","body":"abc"}]'); + t.end(); +}); + +test('canOpenWindow', async t => { + // see tw_security_manager.js + const vm = new VirtualMachine(); + UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm); + const result = global.Scratch.canOpenWindow('https://example.com/'); + t.type(result, Promise); + t.equal(await result, true); + t.end(); +}); + +test('openWindow', async t => { + const vm = new VirtualMachine(); + UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm); + global.Scratch.canOpenWindow = url => url === 'https://example.com/2'; + await t.rejects(global.Scratch.openWindow('https://example.com/1'), /Permission to open tab https:\/\/example.com\/1 rejected/); + t.equal(await global.Scratch.openWindow('https://example.com/2'), '[Window https://example.com/2 target=_blank features=]'); + t.equal(await global.Scratch.openWindow('https://example.com/2', 'popup=1'), '[Window https://example.com/2 target=_blank features=popup=1]'); + t.end(); +}); + +test('canRedirect', async t => { + // see tw_security_manager.js + const vm = new VirtualMachine(); + UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm); + const result = global.Scratch.canRedirect('https://example.com/'); + t.type(result, Promise); + t.equal(await result, true); + t.end(); +}); + +test('redirect', async t => { + const vm = new VirtualMachine(); + UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm); + global.Scratch.canRedirect = url => url === 'https://example.com/2'; + await t.rejects(global.Scratch.redirect('https://example.com/1'), /Permission to redirect to https:\/\/example.com\/1 rejected/); + t.equal(global.location.href, 'https://example.com/'); + await global.Scratch.redirect('https://example.com/2'); + t.equal(global.location.href, 'https://example.com/2'); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/tw_util_async_limiter.js b/local-scratch-vm/test/unit/tw_util_async_limiter.js new file mode 100644 index 0000000000000000000000000000000000000000..18c85040c2146b355a7bbdea480884b2e1ff8bec --- /dev/null +++ b/local-scratch-vm/test/unit/tw_util_async_limiter.js @@ -0,0 +1,62 @@ +const AsyncLimiter = require('../../src/util/async-limiter'); +const {test} = require('tap'); + +test('Runs callback', async t => { + const callback = async (a, b) => a + b; + + const limiter = new AsyncLimiter(callback, 2); + + t.same(await Promise.all([ + limiter.do(1, 2), + limiter.do(3, 4), + limiter.do(5, 6), + limiter.do(7, 8), + limiter.do(9, 10) + ]), [ + 3, + 7, + 11, + 15, + 19 + ]); + t.end(); +}); + +test('Errors', async t => { + t.plan(1); + const callback = () => Promise.reject('Error123!'); + const limiter = new AsyncLimiter(callback, 10); + try { + await limiter.do(); + } catch (e) { + t.equal(e, 'Error123!'); + } + t.end(); +}); + +test('Limit and queue', async t => { + const calls = []; + const callback = () => new Promise(resolve => { + calls.push({ + resolve + }); + }); + + const limiter = new AsyncLimiter(callback, 5); + + for (let i = 0; i < 12; i++) { + limiter.do(); + } + + t.equal(calls.length, 5); + + calls.forEach(i => i.resolve()); + await Promise.resolve(); + t.equal(calls.length, 10); + + calls.forEach(i => i.resolve()); + await Promise.resolve(); + t.equal(calls.length, 12); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_base64.js b/local-scratch-vm/test/unit/util_base64.js new file mode 100644 index 0000000000000000000000000000000000000000..c18c3533331af48634e738f6f89e46223e930fae --- /dev/null +++ b/local-scratch-vm/test/unit/util_base64.js @@ -0,0 +1,38 @@ +const test = require('tap').test; +const Base64Util = require('../../src/util/base64-util'); + +test('uint8ArrayToBase64', t => { + t.equal(Base64Util.uint8ArrayToBase64(new Uint8Array([0, 50, 80, 200])), 'ADJQyA=='); + t.end(); +}); + +test('arrayBufferToBase64', t => { + t.equal(Base64Util.arrayBufferToBase64(new Uint8Array([0, 50, 80, 200]).buffer), 'ADJQyA=='); + t.end(); +}); + +test('base64ToUint8Array', t => { + t.same(Base64Util.base64ToUint8Array('ADJQyA=='), new Uint8Array([0, 50, 80, 200])); + t.end(); +}); + +test('round trips', t => { + const data = [ + new Uint8Array(new Array(255) + .fill() + .map((_, index) => index) + ), + new Uint8Array(0), + new Uint8Array([10, 90, 0, 255, 255, 255, 10, 2]), + new Uint8Array(10000), + new Uint8Array(1000000) + ]; + for (const uint8array of data) { + const uint8ToBase64 = Base64Util.uint8ArrayToBase64(uint8array); + const bufferToBase64 = Base64Util.arrayBufferToBase64(uint8array.buffer); + t.equal(uint8ToBase64, bufferToBase64); + const decoded = Base64Util.base64ToUint8Array(uint8ToBase64); + t.same(uint8array, decoded); + } + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_cast.js b/local-scratch-vm/test/unit/util_cast.js new file mode 100644 index 0000000000000000000000000000000000000000..bf2bbdff77b5be99d7bce410d9fc4f777556beba --- /dev/null +++ b/local-scratch-vm/test/unit/util_cast.js @@ -0,0 +1,201 @@ +const test = require('tap').test; +const cast = require('../../src/util/cast'); + +test('toNumber', t => { + // Numeric + t.strictEqual(cast.toNumber(0), 0); + t.strictEqual(cast.toNumber(1), 1); + t.strictEqual(cast.toNumber(3.14), 3.14); + + // String + t.strictEqual(cast.toNumber('0'), 0); + t.strictEqual(cast.toNumber('1'), 1); + t.strictEqual(cast.toNumber('3.14'), 3.14); + t.strictEqual(cast.toNumber('0.1e10'), 1000000000); + t.strictEqual(cast.toNumber('foobar'), 0); + + // Boolean + t.strictEqual(cast.toNumber(true), 1); + t.strictEqual(cast.toNumber(false), 0); + t.strictEqual(cast.toNumber('true'), 0); + t.strictEqual(cast.toNumber('false'), 0); + + // Undefined & object + t.strictEqual(cast.toNumber(undefined), 0); + t.strictEqual(cast.toNumber({}), 0); + t.strictEqual(cast.toNumber(NaN), 0); + t.end(); +}); + +test('toBoolean', t => { + // Numeric + t.strictEqual(cast.toBoolean(0), false); + t.strictEqual(cast.toBoolean(1), true); + t.strictEqual(cast.toBoolean(3.14), true); + + // String + t.strictEqual(cast.toBoolean('0'), false); + t.strictEqual(cast.toBoolean('1'), true); + t.strictEqual(cast.toBoolean('3.14'), true); + t.strictEqual(cast.toBoolean('0.1e10'), true); + t.strictEqual(cast.toBoolean('foobar'), true); + + // Boolean + t.strictEqual(cast.toBoolean(true), true); + t.strictEqual(cast.toBoolean(false), false); + + // Undefined & object + t.strictEqual(cast.toBoolean(undefined), false); + t.strictEqual(cast.toBoolean({}), true); + t.end(); +}); + +test('toString', t => { + // Numeric + t.strictEqual(cast.toString(0), '0'); + t.strictEqual(cast.toString(1), '1'); + t.strictEqual(cast.toString(3.14), '3.14'); + + // String + t.strictEqual(cast.toString('0'), '0'); + t.strictEqual(cast.toString('1'), '1'); + t.strictEqual(cast.toString('3.14'), '3.14'); + t.strictEqual(cast.toString('0.1e10'), '0.1e10'); + t.strictEqual(cast.toString('foobar'), 'foobar'); + + // Boolean + t.strictEqual(cast.toString(true), 'true'); + t.strictEqual(cast.toString(false), 'false'); + + // Undefined & object + t.strictEqual(cast.toString(undefined), 'undefined'); + t.strictEqual(cast.toString({}), '[object Object]'); + t.end(); +}); + +test('toRgbColorList', t => { + // Hex (minimal, see "color" util tests) + t.deepEqual(cast.toRgbColorList('#000'), [0, 0, 0]); + t.deepEqual(cast.toRgbColorList('#000000'), [0, 0, 0]); + t.deepEqual(cast.toRgbColorList('#fff'), [255, 255, 255]); + t.deepEqual(cast.toRgbColorList('#ffffff'), [255, 255, 255]); + + // Decimal (minimal, see "color" util tests) + t.deepEqual(cast.toRgbColorList(0), [0, 0, 0]); + t.deepEqual(cast.toRgbColorList(1), [0, 0, 1]); + t.deepEqual(cast.toRgbColorList(16777215), [255, 255, 255]); + + // Malformed + t.deepEqual(cast.toRgbColorList('ffffff'), [0, 0, 0]); + t.deepEqual(cast.toRgbColorList('foobar'), [0, 0, 0]); + t.deepEqual(cast.toRgbColorList('#nothex'), [0, 0, 0]); + t.end(); +}); + +test('toRgbColorObject', t => { + // Hex (minimal, see "color" util tests) + t.deepEqual(cast.toRgbColorObject('#000'), {r: 0, g: 0, b: 0}); + t.deepEqual(cast.toRgbColorObject('#000000'), {r: 0, g: 0, b: 0}); + t.deepEqual(cast.toRgbColorObject('#fff'), {r: 255, g: 255, b: 255}); + t.deepEqual(cast.toRgbColorObject('#ffffff'), {r: 255, g: 255, b: 255}); + + // Decimal (minimal, see "color" util tests) + t.deepEqual(cast.toRgbColorObject(0), {a: 255, r: 0, g: 0, b: 0}); + t.deepEqual(cast.toRgbColorObject(1), {a: 255, r: 0, g: 0, b: 1}); + t.deepEqual(cast.toRgbColorObject(16777215), {a: 255, r: 255, g: 255, b: 255}); + t.deepEqual(cast.toRgbColorObject('0x80010203'), {a: 128, r: 1, g: 2, b: 3}); + + // Malformed + t.deepEqual(cast.toRgbColorObject('ffffff'), {a: 255, r: 0, g: 0, b: 0}); + t.deepEqual(cast.toRgbColorObject('foobar'), {a: 255, r: 0, g: 0, b: 0}); + t.deepEqual(cast.toRgbColorObject('#nothex'), {a: 255, r: 0, g: 0, b: 0}); + t.end(); +}); + +test('compare', t => { + // Numeric + t.strictEqual(cast.compare(0, 0), 0); + t.strictEqual(cast.compare(1, 0), 1); + t.strictEqual(cast.compare(0, 1), -1); + t.strictEqual(cast.compare(1, 1), 0); + + // String + t.strictEqual(cast.compare('0', '0'), 0); + t.strictEqual(cast.compare('0.1e10', '1000000000'), 0); + t.strictEqual(cast.compare('foobar', 'FOOBAR'), 0); + t.ok(cast.compare('dog', 'cat') > 0); + + // Boolean + t.strictEqual(cast.compare(true, true), 0); + t.strictEqual(cast.compare(true, false), 1); + t.strictEqual(cast.compare(false, true), -1); + t.strictEqual(cast.compare(true, true), 0); + + // Undefined & object + t.strictEqual(cast.compare(undefined, undefined), 0); + t.strictEqual(cast.compare(undefined, 'undefined'), 0); + t.strictEqual(cast.compare({}, {}), 0); + t.strictEqual(cast.compare({}, '[object Object]'), 0); + t.end(); +}); + +test('isInt', t => { + // Numeric + t.strictEqual(cast.isInt(0), true); + t.strictEqual(cast.isInt(1), true); + t.strictEqual(cast.isInt(0.0), true); + t.strictEqual(cast.isInt(3.14), false); + t.strictEqual(cast.isInt(NaN), true); + + // String + t.strictEqual(cast.isInt('0'), true); + t.strictEqual(cast.isInt('1'), true); + t.strictEqual(cast.isInt('0.0'), false); + t.strictEqual(cast.isInt('0.1e10'), false); + t.strictEqual(cast.isInt('3.14'), false); + + // Boolean + t.strictEqual(cast.isInt(true), true); + t.strictEqual(cast.isInt(false), true); + + // Undefined & object + t.strictEqual(cast.isInt(undefined), false); + t.strictEqual(cast.isInt({}), false); + t.end(); +}); + +test('toListIndex', t => { + const list = [0, 1, 2, 3, 4, 5]; + const empty = []; + + // Valid + t.strictEqual(cast.toListIndex(1, list.length, false), 1); + t.strictEqual(cast.toListIndex(6, list.length, false), 6); + + // Invalid + t.strictEqual(cast.toListIndex(-1, list.length, false), cast.LIST_INVALID); + t.strictEqual(cast.toListIndex(0.1, list.length, false), cast.LIST_INVALID); + t.strictEqual(cast.toListIndex(0, list.length, false), cast.LIST_INVALID); + t.strictEqual(cast.toListIndex(7, list.length, false), cast.LIST_INVALID); + + // "all" + t.strictEqual(cast.toListIndex('all', list.length, true), cast.LIST_ALL); + t.strictEqual(cast.toListIndex('all', list.length, false), cast.LIST_INVALID); + + // "last" + t.strictEqual(cast.toListIndex('last', list.length, false), list.length); + t.strictEqual(cast.toListIndex('last', empty.length, false), cast.LIST_INVALID); + + // "random" + const random = cast.toListIndex('random', list.length, false); + t.ok(random <= list.length); + t.ok(random > 0); + t.strictEqual(cast.toListIndex('random', empty.length, false), cast.LIST_INVALID); + + // "any" (alias for "random") + const any = cast.toListIndex('any', list.length, false); + t.ok(any <= list.length); + t.ok(any > 0); + t.strictEqual(cast.toListIndex('any', empty.length, false), cast.LIST_INVALID); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_color.js b/local-scratch-vm/test/unit/util_color.js new file mode 100644 index 0000000000000000000000000000000000000000..299a4e9f2c728f075a736eb838c019a89f31c86f --- /dev/null +++ b/local-scratch-vm/test/unit/util_color.js @@ -0,0 +1,135 @@ +const test = require('tap').test; +const color = require('../../src/util/color'); + +/** + * Assert that two HSV colors are similar to each other, within a tolerance. + * @param {Test} t - the Tap test object. + * @param {HSVObject} actual - the first HSV color to compare. + * @param {HSVObject} expected - the other HSV color to compare. + */ +const hsvSimilar = function (t, actual, expected) { + if ((Math.abs(actual.h - expected.h) >= 1) || + (Math.abs(actual.s - expected.s) >= 0.01) || + (Math.abs(actual.v - expected.v) >= 0.01) + ) { + t.fail('HSV colors not similar enough', { + actual: actual, + expected: expected + }); + } +}; + +/** + * Assert that two RGB colors are similar to each other, within a tolerance. + * @param {Test} t - the Tap test object. + * @param {RGBObject} actual - the first RGB color to compare. + * @param {RGBObject} expected - the other RGB color to compare. + */ +const rgbSimilar = function (t, actual, expected) { + if ((Math.abs(actual.r - expected.r) >= 1) || + (Math.abs(actual.g - expected.g) >= 1) || + (Math.abs(actual.b - expected.b) >= 1) + ) { + t.fail('RGB colors not similar enough', { + actual: actual, + expected: expected + }); + } +}; + +test('decimalToHex', t => { + t.strictEqual(color.decimalToHex(0), '#000000'); + t.strictEqual(color.decimalToHex(1), '#000001'); + t.strictEqual(color.decimalToHex(16777215), '#ffffff'); + t.strictEqual(color.decimalToHex(-16777215), '#000001'); + t.strictEqual(color.decimalToHex(99999999), '#5f5e0ff'); + t.end(); +}); + +test('decimalToRgb', t => { + t.deepEqual(color.decimalToRgb(0), {a: 255, r: 0, g: 0, b: 0}); + t.deepEqual(color.decimalToRgb(1), {a: 255, r: 0, g: 0, b: 1}); + t.deepEqual(color.decimalToRgb(16777215), {a: 255, r: 255, g: 255, b: 255}); + t.deepEqual(color.decimalToRgb(-16777215), {a: 255, r: 0, g: 0, b: 1}); + t.deepEqual(color.decimalToRgb(99999999), {a: 5, r: 245, g: 224, b: 255}); + t.end(); +}); + +test('hexToRgb', t => { + t.deepEqual(color.hexToRgb('#000'), {r: 0, g: 0, b: 0}); + t.deepEqual(color.hexToRgb('#000000'), {r: 0, g: 0, b: 0}); + t.deepEqual(color.hexToRgb('#fff'), {r: 255, g: 255, b: 255}); + t.deepEqual(color.hexToRgb('#ffffff'), {r: 255, g: 255, b: 255}); + t.deepEqual(color.hexToRgb('#0fa'), {r: 0, g: 255, b: 170}); + t.deepEqual(color.hexToRgb('#00ffaa'), {r: 0, g: 255, b: 170}); + t.deepEqual(color.hexToRgb('#00FFaA'), {r: 0, g: 255, b: 170}); + + t.deepEqual(color.hexToRgb('000'), {r: 0, g: 0, b: 0}); + t.deepEqual(color.hexToRgb('fff'), {r: 255, g: 255, b: 255}); + t.deepEqual(color.hexToRgb('aBc'), {r: 0xaa, g: 0xbb, b: 0xcc}); + t.deepEqual(color.hexToRgb('00ffaa'), {r: 0, g: 255, b: 170}); + + t.deepEqual(color.hexToRgb('0'), null); + t.deepEqual(color.hexToRgb('hello world'), null); + t.deepEqual(color.hexToRgb('red'), null); + + t.end(); +}); + +test('rgbToHex', t => { + t.strictEqual(color.rgbToHex({r: 0, g: 0, b: 0}), '#000000'); + t.strictEqual(color.rgbToHex({r: 255, g: 255, b: 255}), '#ffffff'); + t.strictEqual(color.rgbToHex({r: 0, g: 255, b: 170}), '#00ffaa'); + t.end(); +}); + +test('rgbToDecimal', t => { + t.strictEqual(color.rgbToDecimal({r: 0, g: 0, b: 0}), 0); + t.strictEqual(color.rgbToDecimal({r: 255, g: 255, b: 255}), 16777215); + t.strictEqual(color.rgbToDecimal({r: 0, g: 255, b: 170}), 65450); + t.end(); +}); + +test('hexToDecimal', t => { + t.strictEqual(color.hexToDecimal('#000'), 0); + t.strictEqual(color.hexToDecimal('#000000'), 0); + t.strictEqual(color.hexToDecimal('#fff'), 16777215); + t.strictEqual(color.hexToDecimal('#ffffff'), 16777215); + t.strictEqual(color.hexToDecimal('#0fa'), 65450); + t.strictEqual(color.hexToDecimal('#00ffaa'), 65450); + t.end(); +}); + +test('hsvToRgb', t => { + rgbSimilar(t, color.hsvToRgb({h: 0, s: 0, v: 0}), {r: 0, g: 0, b: 0}); + rgbSimilar(t, color.hsvToRgb({h: 123, s: 0.1234, v: 0}), {r: 0, g: 0, b: 0}); + rgbSimilar(t, color.hsvToRgb({h: 0, s: 0, v: 1}), {r: 255, g: 255, b: 255}); + rgbSimilar(t, color.hsvToRgb({h: 321, s: 0, v: 1}), {r: 255, g: 255, b: 255}); + rgbSimilar(t, color.hsvToRgb({h: 0, s: 1, v: 1}), {r: 255, g: 0, b: 0}); + rgbSimilar(t, color.hsvToRgb({h: 120, s: 1, v: 1}), {r: 0, g: 255, b: 0}); + rgbSimilar(t, color.hsvToRgb({h: 240, s: 1, v: 1}), {r: 0, g: 0, b: 255}); + t.end(); +}); + +test('rgbToHsv', t => { + hsvSimilar(t, color.rgbToHsv({r: 0, g: 0, b: 0}), {h: 0, s: 0, v: 0}); + hsvSimilar(t, color.rgbToHsv({r: 64, g: 64, b: 64}), {h: 0, s: 0, v: 0.25}); + hsvSimilar(t, color.rgbToHsv({r: 128, g: 128, b: 128}), {h: 0, s: 0, v: 0.5}); + hsvSimilar(t, color.rgbToHsv({r: 192, g: 192, b: 192}), {h: 0, s: 0, v: 0.75}); + hsvSimilar(t, color.rgbToHsv({r: 255, g: 255, b: 255}), {h: 0, s: 0, v: 1}); + hsvSimilar(t, color.rgbToHsv({r: 255, g: 0, b: 0}), {h: 0, s: 1, v: 1}); + hsvSimilar(t, color.rgbToHsv({r: 0, g: 255, b: 0}), {h: 120, s: 1, v: 1}); + hsvSimilar(t, color.rgbToHsv({r: 0, g: 0, b: 255}), {h: 240, s: 1, v: 1}); + t.end(); +}); + +test('mixRgb', t => { + rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, -1), {r: 10, g: 20, b: 30}); + rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0), {r: 10, g: 20, b: 30}); + rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.25), {r: 15, g: 25, b: 35}); + rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.5), {r: 20, g: 30, b: 40}); + rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.75), {r: 25, g: 35, b: 45}); + rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 1), {r: 30, g: 40, b: 50}); + rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 2), {r: 30, g: 40, b: 50}); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_jsonrpc-web-socket.js b/local-scratch-vm/test/unit/util_jsonrpc-web-socket.js new file mode 100644 index 0000000000000000000000000000000000000000..ba03e83ab58524ef28c74756a2688b2947410256 --- /dev/null +++ b/local-scratch-vm/test/unit/util_jsonrpc-web-socket.js @@ -0,0 +1,10 @@ +const test = require('tap').test; +// const JSONRPCWebSocket = require('../../src/util/jsonrpc-web-socket'); + +test('constructor', t => { + t.end(); +}); + +test('dispose', t => { + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_jsonrpc.js b/local-scratch-vm/test/unit/util_jsonrpc.js new file mode 100644 index 0000000000000000000000000000000000000000..33c103b8a4ca13abd6b880eebc26168205618263 --- /dev/null +++ b/local-scratch-vm/test/unit/util_jsonrpc.js @@ -0,0 +1,18 @@ +const test = require('tap').test; +// const JSONRPC = require('../../src/util/jsonrpc'); + +test('constructor', t => { + t.end(); +}); + +test('sendRemoteRequest', t => { + t.end(); +}); + +test('sendRemoteNotification', t => { + t.end(); +}); + +test('didReceiveCall', t => { + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_math.js b/local-scratch-vm/test/unit/util_math.js new file mode 100644 index 0000000000000000000000000000000000000000..c93f17fa64bdfdca6dba74afa173b393b86e3bcf --- /dev/null +++ b/local-scratch-vm/test/unit/util_math.js @@ -0,0 +1,70 @@ +const test = require('tap').test; +const math = require('../../src/util/math-util'); + +test('degToRad', t => { + t.strictEqual(math.degToRad(0), 0); + t.strictEqual(math.degToRad(1), 0.017453292519943295); + t.strictEqual(math.degToRad(180), Math.PI); + t.strictEqual(math.degToRad(360), 2 * Math.PI); + t.strictEqual(math.degToRad(720), 4 * Math.PI); + t.end(); +}); + +test('radToDeg', t => { + t.strictEqual(math.radToDeg(0), 0); + t.strictEqual(math.radToDeg(1), 57.29577951308232); + t.strictEqual(math.radToDeg(180), 10313.240312354817); + t.strictEqual(math.radToDeg(360), 20626.480624709635); + t.strictEqual(math.radToDeg(720), 41252.96124941927); + t.end(); +}); + +test('clamp', t => { + t.strictEqual(math.clamp(0, 0, 10), 0); + t.strictEqual(math.clamp(1, 0, 10), 1); + t.strictEqual(math.clamp(-10, 0, 10), 0); + t.strictEqual(math.clamp(100, 0, 10), 10); + t.end(); +}); + +test('wrapClamp', t => { + t.strictEqual(math.wrapClamp(0, 0, 10), 0); + t.strictEqual(math.wrapClamp(1, 0, 10), 1); + t.strictEqual(math.wrapClamp(-10, 0, 10), 1); + t.strictEqual(math.wrapClamp(100, 0, 10), 1); + t.end(); +}); + +test('tan', t => { + t.strictEqual(math.tan(90), Infinity); + t.strictEqual(math.tan(180), 0); + t.strictEqual(math.tan(-90), -Infinity); + t.strictEqual(math.tan(33), 0.6494075932); + t.end(); +}); + +test('reducedSortOrdering', t => { + t.deepEqual(math.reducedSortOrdering([5, 18, 6, 3]), [1, 3, 2, 0]); + t.deepEqual(math.reducedSortOrdering([5, 1, 56, 19]), [1, 0, 3, 2]); + t.end(); +}); + +test('inclusiveRandIntWithout', t => { + const withRandomValue = function (randValue, ...args) { + const oldMathRandom = Math.random; + Object.assign(global.Math, {random: () => randValue}); + const result = math.inclusiveRandIntWithout(...args); + Object.assign(global.Math, {random: oldMathRandom}); + return result; + }; + + t.strictEqual(withRandomValue(3 / 6, 0, 6, 2), 4); + t.strictEqual(withRandomValue(2 / 6, 0, 6, 2), 3); + t.strictEqual(withRandomValue(1 / 6, 0, 6, 2), 1); + t.strictEqual(withRandomValue(1.9 / 6, 0, 6, 2), 1); + + t.strictEqual(withRandomValue(3 / 4, 10, 14, 10), 14); + t.strictEqual(withRandomValue(0 / 4, 10, 14, 10), 11); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_new-block-ids.js b/local-scratch-vm/test/unit/util_new-block-ids.js new file mode 100644 index 0000000000000000000000000000000000000000..4ddf2f3013e003d4efe08ae596cefb3a601e5ec4 --- /dev/null +++ b/local-scratch-vm/test/unit/util_new-block-ids.js @@ -0,0 +1,64 @@ +const newBlockIds = require('../../src/util/new-block-ids'); +const simpleStack = require('../fixtures/simple-stack'); +const tap = require('tap'); +const test = tap.test; + +let originals; +let newBlocks; + +tap.beforeEach(done => { + originals = simpleStack; + // Will be mutated so make a copy first + newBlocks = JSON.parse(JSON.stringify(simpleStack)); + newBlockIds(newBlocks); + done(); +}); + + +/** + * The structure of the simple stack is: + * moveTo (looks_size) -> stopAllSounds + * The list of blocks is + * 0: moveTo (TO input block: 1, shadow: 2) + * 1: looks_size (parent: 0) + * 2: obscured shadow for moveTo input (parent: 0) + * 3: stopAllSounds (parent: 0) + * Inspect fixtures/simple-stack for the full object. + */ + +test('top-level block IDs have all changed', t => { + newBlocks.forEach((block, i) => { + t.notEqual(block.id, originals[i].id); + }); + t.end(); +}); + +test('input reference is maintained on parent for attached block', t => { + t.equal(newBlocks[0].inputs.TO.block, newBlocks[1].id); + t.end(); +}); + +test('input reference is maintained on parent for obscured shadow', t => { + t.equal(newBlocks[0].inputs.TO.shadow, newBlocks[2].id); + t.end(); +}); + +test('parent reference is maintained for attached input', t => { + t.equal(newBlocks[1].parent, newBlocks[0].id); + t.end(); +}); + +test('parent reference is maintained for obscured shadow', t => { + t.equal(newBlocks[2].parent, newBlocks[0].id); + t.end(); +}); + +test('parent reference is maintained for next block', t => { + t.equal(newBlocks[3].parent, newBlocks[0].id); + t.end(); +}); + +test('next reference is maintained for previous block', t => { + t.equal(newBlocks[0].next, newBlocks[3].id); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_rateLimiter.js b/local-scratch-vm/test/unit/util_rateLimiter.js new file mode 100644 index 0000000000000000000000000000000000000000..1e34d910362145fb353951a787d9cea30c57c999 --- /dev/null +++ b/local-scratch-vm/test/unit/util_rateLimiter.js @@ -0,0 +1,32 @@ +const test = require('tap').test; +const RateLimiter = require('../../src/util/rateLimiter.js'); + +test('rate limiter', t => { + // Create a rate limiter with maximum of 20 sends per second + const rate = 20; + const limiter = new RateLimiter(rate); + + // Simulate time passing with a stubbed timer + let simulatedTime = Date.now(); + limiter._timer = {timeElapsed: () => simulatedTime}; + + // The rate limiter starts with a number of tokens equal to the max rate + t.equal(limiter._count, rate); + + // Running okayToSend a number of times equal to the max rate + // uses up all of the tokens + for (let i = 0; i < rate; i++) { + t.true(limiter.okayToSend()); + // Tokens are counting down + t.equal(limiter._count, rate - (i + 1)); + } + t.false(limiter.okayToSend()); + + // Advance the timer enough so we get exactly one more token + // One extra millisecond is required to get over the threshold + simulatedTime += (1000 / rate) + 1; + t.true(limiter.okayToSend()); + t.false(limiter.okayToSend()); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_string.js b/local-scratch-vm/test/unit/util_string.js new file mode 100644 index 0000000000000000000000000000000000000000..199fa27cfc595dabd12bc7fa90ba96c7ec666705 --- /dev/null +++ b/local-scratch-vm/test/unit/util_string.js @@ -0,0 +1,129 @@ +const test = require('tap').test; +const StringUtil = require('../../src/util/string-util'); + +test('splitFirst', t => { + t.deepEqual(StringUtil.splitFirst('asdf.1234', '.'), ['asdf', '1234']); + t.deepEqual(StringUtil.splitFirst('asdf.', '.'), ['asdf', '']); + t.deepEqual(StringUtil.splitFirst('.1234', '.'), ['', '1234']); + t.deepEqual(StringUtil.splitFirst('foo', '.'), ['foo', null]); + t.end(); +}); + +test('withoutTrailingDigits', t => { + t.strictEqual(StringUtil.withoutTrailingDigits('boeing747'), 'boeing'); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing747 '), 'boeing747 '); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing𝟨'), 'boeing𝟨'); + t.strictEqual(StringUtil.withoutTrailingDigits('boeing 747'), 'boeing '); + t.strictEqual(StringUtil.withoutTrailingDigits('747'), ''); + t.end(); +}); + +test('unusedName', t => { + t.strictEqual( + StringUtil.unusedName( + 'name', + ['not the same name'] + ), + 'name' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name'] + ), + 'name2' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name30'] + ), + 'name' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name', 'name2'] + ), + 'name3' + ); + t.strictEqual( + StringUtil.unusedName( + 'name', + ['name', 'name3'] + ), + 'name2' + ); + t.strictEqual( + StringUtil.unusedName( + 'boeing747', + ['boeing747'] + ), + 'boeing2' // Yup, this matches scratch-flash... + ); + t.end(); +}); + +test('stringify', t => { + const obj = { + a: Infinity, + b: NaN, + c: -Infinity, + d: 23, + e: 'str', + f: { + nested: Infinity + } + }; + const parsed = JSON.parse(StringUtil.stringify(obj)); + t.equal(parsed.a, 0); + t.equal(parsed.b, 0); + t.equal(parsed.c, 0); + t.equal(parsed.d, 23); + t.equal(parsed.e, 'str'); + t.equal(parsed.f.nested, 0); + t.end(); +}); + +test('replaceUnsafeChars', t => { + const empty = ''; + t.equal(StringUtil.replaceUnsafeChars(empty), empty); + + const safe = 'hello'; + t.equal(StringUtil.replaceUnsafeChars(safe), safe); + + const unsafe = '< > & \' "'; + t.equal(StringUtil.replaceUnsafeChars(unsafe), 'lt gt amp apos quot'); + + const single = '&'; + t.equal(StringUtil.replaceUnsafeChars(single), 'amp'); + + const mix = 'b& c\'def_-"'; + t.equal(StringUtil.replaceUnsafeChars(mix), 'ltagtbamp caposdef_-quot'); + + const dupes = '<<&_"_"_&>>'; + t.equal(StringUtil.replaceUnsafeChars(dupes), 'ltltamp_quot_quot_ampgtgt'); + + const emoji = '(>^_^)>'; + t.equal(StringUtil.replaceUnsafeChars(emoji), '(gt^_^)gt'); + + t.end(); +}); + +test('replaceUnsafeChars should handle non strings', t => { + const array = ['hello', 'world']; + t.equal(StringUtil.replaceUnsafeChars(array), String(array)); + + const arrayWithSpecialChar = ['hello', '']; + t.equal(StringUtil.replaceUnsafeChars(arrayWithSpecialChar), 'hello,ltworldgt'); + + const arrayWithNumbers = [1, 2, 3]; + t.equal(StringUtil.replaceUnsafeChars(arrayWithNumbers), '1,2,3'); + + // Objects shouldn't get provided to replaceUnsafeChars, but in the event + // they do, it should just return the object (and log an error) + const object = {hello: 'world'}; + t.equal(StringUtil.replaceUnsafeChars(object), object); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_task-queue.js b/local-scratch-vm/test/unit/util_task-queue.js new file mode 100644 index 0000000000000000000000000000000000000000..6e484a7a4aee6159fc3dc880af1d940c6d77959f --- /dev/null +++ b/local-scratch-vm/test/unit/util_task-queue.js @@ -0,0 +1,192 @@ +const test = require('tap').test; + +const TaskQueue = require('../../src/util/task-queue'); + +const MockTimer = require('../fixtures/mock-timer'); +const testCompare = require('../fixtures/test-compare'); + +// Max tokens = 1000 +// Refill 1000 tokens per second (1 per millisecond) +// Token bucket starts empty +// Max total cost of queued tasks = 10000 tokens = 10 seconds +const makeTestQueue = () => { + const bukkit = new TaskQueue(1000, 1000, { + startingTokens: 0, + maxTotalCost: 10000 + }); + + const mockTimer = new MockTimer(); + bukkit._timer = mockTimer; + mockTimer.start(); + + return bukkit; +}; + +test('spec', t => { + t.type(TaskQueue, 'function'); + const bukkit = makeTestQueue(); + + t.type(bukkit, 'object'); + + t.type(bukkit.length, 'number'); + t.type(bukkit.do, 'function'); + t.type(bukkit.cancel, 'function'); + t.type(bukkit.cancelAll, 'function'); + + t.end(); +}); + +test('constructor', t => { + t.ok(new TaskQueue(1, 1)); + t.ok(new TaskQueue(1, 1, {})); + t.ok(new TaskQueue(1, 1, {startingTokens: 0})); + t.ok(new TaskQueue(1, 1, {maxTotalCost: 999})); + t.ok(new TaskQueue(1, 1, {startingTokens: 0, maxTotalCost: 999})); + t.end(); +}); + +test('run tasks', async t => { + const bukkit = makeTestQueue(); + + const taskResults = []; + + const promises = [ + bukkit.do(() => { + taskResults.push('a'); + testCompare(t, bukkit._timer.timeElapsed(), '>=', 50, 'Costly task must wait'); + }, 50), + bukkit.do(() => { + taskResults.push('b'); + testCompare(t, bukkit._timer.timeElapsed(), '>=', 60, 'Tasks must run in serial'); + }, 10), + bukkit.do(() => { + taskResults.push('c'); + testCompare(t, bukkit._timer.timeElapsed(), '<=', 70, 'Cheap task should run soon'); + }, 1) + ]; + + // advance 10 simulated milliseconds per JS tick + while (bukkit.length > 0) { + await bukkit._timer.advanceMockTimeAsync(10); + } + + return Promise.all(promises).then(() => { + t.deepEqual(taskResults, ['a', 'b', 'c'], 'All tasks must run in correct order'); + t.end(); + }); +}); + +test('cancel', async t => { + const bukkit = makeTestQueue(); + + const taskResults = []; + const goodCancelMessage = 'Task was canceled correctly'; + const afterCancelMessage = 'Task was run correctly'; + const cancelTaskPromise = bukkit.do( + () => { + taskResults.push('nope'); + }, 999); + const cancelCheckPromise = cancelTaskPromise.then( + () => { + t.fail('Task should have been canceled'); + }, + () => { + taskResults.push(goodCancelMessage); + } + ); + const keepTaskPromise = bukkit.do( + () => { + taskResults.push(afterCancelMessage); + testCompare(t, bukkit._timer.timeElapsed(), '<', 10, 'Canceled task must not delay other tasks'); + }, 5); + + // give the bucket a chance to make a mistake + await bukkit._timer.advanceMockTimeAsync(1); + + t.equal(bukkit.length, 2); + const taskWasCanceled = bukkit.cancel(cancelTaskPromise); + t.ok(taskWasCanceled); + t.equal(bukkit.length, 1); + + while (bukkit.length > 0) { + await bukkit._timer.advanceMockTimeAsync(1); + } + + return Promise.all([cancelCheckPromise, keepTaskPromise]).then(() => { + t.deepEqual(taskResults, [goodCancelMessage, afterCancelMessage]); + t.end(); + }); +}); + +test('cancelAll', async t => { + const bukkit = makeTestQueue(); + + const taskResults = []; + const goodCancelMessage1 = 'Task1 was canceled correctly'; + const goodCancelMessage2 = 'Task2 was canceled correctly'; + + const promises = [ + bukkit.do(() => taskResults.push('nope'), 999).then( + () => { + t.fail('Task1 should have been canceled'); + }, + () => { + taskResults.push(goodCancelMessage1); + } + ), + bukkit.do(() => taskResults.push('nah'), 999).then( + () => { + t.fail('Task2 should have been canceled'); + }, + () => { + taskResults.push(goodCancelMessage2); + } + ) + ]; + + // advance time, but not enough that any task should run + await bukkit._timer.advanceMockTimeAsync(100); + + bukkit.cancelAll(); + + // advance enough that both tasks would run if they hadn't been canceled + await bukkit._timer.advanceMockTimeAsync(10000); + + return Promise.all(promises).then(() => { + t.deepEqual(taskResults, [goodCancelMessage1, goodCancelMessage2], 'Tasks should cancel in order'); + t.end(); + }); +}); + +test('max total cost', async t => { + const bukkit = makeTestQueue(); + + let numTasks = 0; + + const task = () => ++numTasks; + + // Fill the queue + for (let i = 0; i < 10; ++i) { + bukkit.do(task, 1000); + } + + // This one should be rejected because the queue is full + bukkit + .do(task, 1000) + .then( + () => { + t.fail('Full queue did not reject task'); + }, + () => { + t.pass(); + } + ); + + while (bukkit.length > 0) { + await bukkit._timer.advanceMockTimeAsync(1000); + } + + // this should be 10 if the last task is rejected or 11 if it runs + t.equal(numTasks, 10); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_timer.js b/local-scratch-vm/test/unit/util_timer.js new file mode 100644 index 0000000000000000000000000000000000000000..5235f54334ec36358298ffaca54c873b568f40d5 --- /dev/null +++ b/local-scratch-vm/test/unit/util_timer.js @@ -0,0 +1,64 @@ +const test = require('tap').test; +const Timer = require('../../src/util/timer'); + +// Stubbed current time +let NOW = 0; + +const testNow = { + now: () => { + NOW += 100; + return NOW; + } +}; + +test('spec', t => { + const timer = new Timer(testNow); + + t.type(Timer, 'function'); + t.type(timer, 'object'); + + t.type(timer.startTime, 'number'); + t.type(timer.time, 'function'); + t.type(timer.start, 'function'); + t.type(timer.timeElapsed, 'function'); + t.type(timer.setTimeout, 'function'); + t.type(timer.clearTimeout, 'function'); + + t.end(); +}); + +test('time', t => { + const timer = new Timer(testNow); + const time = timer.time(); + + t.ok(testNow.now() >= time); + t.end(); +}); + +test('start / timeElapsed', t => { + const timer = new Timer(testNow); + const delay = 100; + const threshold = 1000 / 60; // 60 hz + + // Start timer + timer.start(); + + // Measure timer + const timeElapsed = timer.timeElapsed(); + t.ok(timeElapsed >= 0); + t.ok(timeElapsed >= (delay - threshold) && + timeElapsed <= (delay + threshold)); + t.end(); +}); + +test('setTimeout / clearTimeout', t => new Promise((resolve, reject) => { + const timer = new Timer(testNow); + const cancelId = timer.setTimeout(() => { + reject(new Error('Canceled task ran')); + }, 1); + timer.setTimeout(() => { + resolve('Non-canceled task ran'); + t.end(); + }, 2); + timer.clearTimeout(cancelId); +})); diff --git a/local-scratch-vm/test/unit/util_variable.js b/local-scratch-vm/test/unit/util_variable.js new file mode 100644 index 0000000000000000000000000000000000000000..18b0e768c36632b6f2b849a356e8d8acae15ad23 --- /dev/null +++ b/local-scratch-vm/test/unit/util_variable.js @@ -0,0 +1,83 @@ +const tap = require('tap'); +const Target = require('../../src/engine/target'); +const Runtime = require('../../src/engine/runtime'); +const VariableUtil = require('../../src/util/variable-util'); + +let target1; +let target2; + +tap.beforeEach(() => { + const runtime = new Runtime(); + target1 = new Target(runtime); + target1.blocks.createBlock({ + id: 'a block', + fields: { + VARIABLE: { + id: 'id1', + value: 'foo' + } + } + }); + target1.blocks.createBlock({ + id: 'another block', + fields: { + TEXT: { + value: 'not a variable' + } + } + }); + + target2 = new Target(runtime); + target2.blocks.createBlock({ + id: 'a different block', + fields: { + VARIABLE: { + id: 'id2', + value: 'bar' + } + } + }); + target2.blocks.createBlock({ + id: 'another var block', + fields: { + VARIABLE: { + id: 'id1', + value: 'foo' + } + } + }); + + return Promise.resolve(null); +}); + +const test = tap.test; + +test('get all var refs', t => { + const allVarRefs = VariableUtil.getAllVarRefsForTargets([target1, target2]); + t.equal(Object.keys(allVarRefs).length, 2); + t.equal(allVarRefs.id1.length, 2); + t.equal(allVarRefs.id2.length, 1); + t.equal(allVarRefs['not a variable'], undefined); + + t.end(); +}); + +test('merge variable ids', t => { + // Redo the id for the variable with 'id1' + VariableUtil.updateVariableIdentifiers(target1.blocks.getAllVariableAndListReferences().id1, 'renamed id'); + const varField = target1.blocks.getBlock('a block').fields.VARIABLE; + t.equals(varField.id, 'renamed id'); + t.equals(varField.value, 'foo'); + + t.end(); +}); + +test('merge variable ids but with new name too', t => { + // Redo the id for the variable with 'id1' + VariableUtil.updateVariableIdentifiers(target1.blocks.getAllVariableAndListReferences().id1, 'renamed id', 'baz'); + const varField = target1.blocks.getBlock('a block').fields.VARIABLE; + t.equals(varField.id, 'renamed id'); + t.equals(varField.value, 'baz'); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/util_xml.js b/local-scratch-vm/test/unit/util_xml.js new file mode 100644 index 0000000000000000000000000000000000000000..bb7607a69d9ed12790ef5673939073101078a98b --- /dev/null +++ b/local-scratch-vm/test/unit/util_xml.js @@ -0,0 +1,52 @@ +const test = require('tap').test; +const xml = require('../../src/util/xml-escape'); + +test('escape', t => { + const input = ''; + const output = '<foo bar="he & llo '"></foo>'; + t.strictEqual(xml(input), output); + t.end(); +}); + +test('xmlEscape (more)', t => { + const empty = ''; + t.equal(xml(empty), empty); + + const safe = 'hello'; + t.equal(xml(safe), safe); + + const unsafe = '< > & \' "'; + t.equal(xml(unsafe), '< > & ' "'); + + const single = '&'; + t.equal(xml(single), '&'); + + const mix = 'b& c\'def_-"'; + t.equal(xml(mix), '<a>b& c'def_-"'); + + const dupes = '<<&_"_"_&>>'; + t.equal(xml(dupes), '<<&_"_"_&>>'); + + const emoji = '(>^_^)>'; + t.equal(xml(emoji), '(>^_^)>'); + + t.end(); +}); + +test('xmlEscape should handle non strings', t => { + const array = ['hello', 'world']; + t.equal(xml(array), String(array)); + + const arrayWithSpecialChar = ['hello', '']; + t.equal(xml(arrayWithSpecialChar), 'hello,<world>'); + + const arrayWithNumbers = [1, 2, 3]; + t.equal(xml(arrayWithNumbers), '1,2,3'); + + // Objects shouldn't get provided to replaceUnsafeChars, but in the event + // they do, it should just return the object (and log an error) + const object = {hello: 'world'}; + t.equal(xml(object), object); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/virtual-machine.js b/local-scratch-vm/test/unit/virtual-machine.js new file mode 100644 index 0000000000000000000000000000000000000000..9108739adb1104a443145efec3693529e4e1a6e0 --- /dev/null +++ b/local-scratch-vm/test/unit/virtual-machine.js @@ -0,0 +1,1060 @@ +const tap = require('tap'); +const VirtualMachine = require('../../src/virtual-machine'); +const Sprite = require('../../src/sprites/sprite'); +const Variable = require('../../src/engine/variable'); +const adapter = require('../../src/engine/adapter'); +const events = require('../fixtures/events.json'); +const Renderer = require('../fixtures/fake-renderer'); +const Runtime = require('../../src/engine/runtime'); +const RenderedTarget = require('../../src/sprites/rendered-target'); + +tap.tearDown(() => process.nextTick(process.exit)); + +const test = tap.test; + +test('deleteSound returns function after deleting or null if nothing was deleted', t => { + const vm = new VirtualMachine(); + const rt = new Runtime(); + const sprite = new Sprite(null, rt); + sprite.sounds = [{id: 1}, {id: 2}, {id: 3}]; + const target = new RenderedTarget(sprite, rt); + vm.editingTarget = target; + + const addFun = vm.deleteSound(1); + t.equal(sprite.sounds.length, 2); + t.equal(sprite.sounds[0].id, 1); + t.equal(sprite.sounds[1].id, 3); + t.type(addFun, 'function'); + + const noAddFun = vm.deleteSound(2); + t.equal(sprite.sounds.length, 2); + t.equal(sprite.sounds[0].id, 1); + t.equal(sprite.sounds[1].id, 3); + t.equal(noAddFun, null); + + t.end(); +}); + +test('deleteCostume returns function after deleting or null if nothing was deleted', t => { + const vm = new VirtualMachine(); + const rt = new Runtime(); + const sprite = new Sprite(null, rt); + sprite.costumes = [{id: 1}, {id: 2}, {id: 3}]; + sprite.currentCostume = 0; + const target = new RenderedTarget(sprite, rt); + vm.editingTarget = target; + + const addFun = vm.deleteCostume(1); + t.equal(sprite.costumes.length, 2); + t.equal(sprite.costumes[0].id, 1); + t.equal(sprite.costumes[1].id, 3); + t.type(addFun, 'function'); + + const noAddFun = vm.deleteCostume(2); + t.equal(sprite.costumes.length, 2); + t.equal(sprite.costumes[0].id, 1); + t.equal(sprite.costumes[1].id, 3); + t.equal(noAddFun, null); + + t.end(); +}); + + +test('addSprite throws on invalid string', t => { + const vm = new VirtualMachine(); + vm.addSprite('this is not a sprite') + .catch(e => { + t.equal(e.startsWith('Sprite Upload Error:'), true); + t.end(); + }); +}); + +test('renameSprite throws when there is no sprite with that id', t => { + const vm = new VirtualMachine(); + vm.runtime.getTargetById = () => null; + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('No target with the provided id.') + ); + t.end(); +}); + +test('renameSprite throws when used on a non-sprite target', t => { + const vm = new VirtualMachine(); + const fakeTarget = { + isSprite: () => false + }; + vm.runtime.getTargetById = () => (fakeTarget); + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('Cannot rename non-sprite targets.') + ); + t.end(); +}); + +test('renameSprite throws when there is no sprite for given target', t => { + const vm = new VirtualMachine(); + const fakeTarget = { + sprite: null, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + t.throws( + (() => vm.renameSprite('id', 'name')), + new Error('No sprite associated with this target.') + ); + t.end(); +}); + +test('renameSprite sets the sprite name', t => { + const vm = new VirtualMachine(); + const fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', 'not-original'); + t.equal(fakeTarget.sprite.name, 'not-original'); + t.end(); +}); + +test('renameSprite does not set sprite names to an empty string', t => { + const vm = new VirtualMachine(); + const fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', ''); + t.equal(fakeTarget.sprite.name, 'original'); + t.end(); +}); + +test('renameSprite does not set sprite names to reserved names', t => { + const vm = new VirtualMachine(); + const fakeTarget = { + sprite: {name: 'original'}, + isSprite: () => true + }; + vm.runtime.getTargetById = () => (fakeTarget); + vm.renameSprite('id', '_mouse_'); + t.equal(fakeTarget.sprite.name, 'original'); + t.end(); +}); + +test('renameSprite increments from existing sprite names', t => { + const vm = new VirtualMachine(); + vm.emitTargetsUpdate = () => {}; + + const spr1 = new Sprite(null, vm.runtime); + const target1 = spr1.createClone(); + const spr2 = new Sprite(null, vm.runtime); + const target2 = spr2.createClone(); + + vm.runtime.targets = [target1, target2]; + vm.renameSprite(target1.id, 'foo'); + t.equal(vm.runtime.targets[0].sprite.name, 'foo'); + vm.renameSprite(target2.id, 'foo'); + t.equal(vm.runtime.targets[1].sprite.name, 'foo2'); + t.end(); +}); + +test('renameSprite does not increment when renaming to the same name', t => { + const vm = new VirtualMachine(); + vm.emitTargetsUpdate = () => {}; + + const spr = new Sprite(null, vm.runtime); + spr.name = 'foo'; + const target = spr.createClone(); + + vm.runtime.targets = [target]; + + t.equal(vm.runtime.targets[0].sprite.name, 'foo'); + vm.renameSprite(target.id, 'foo'); + t.equal(vm.runtime.targets[0].sprite.name, 'foo'); + + t.end(); +}); + +test('deleteSprite throws when used on a non-sprite target', t => { + const vm = new VirtualMachine(); + vm.runtime.targets = [{ + id: 'id', + isSprite: () => false + }]; + t.throws( + (() => vm.deleteSprite('id')), + new Error('Cannot delete non-sprite targets.') + ); + t.end(); +}); + +test('deleteSprite throws when there is no sprite for the given target', t => { + const vm = new VirtualMachine(); + vm.runtime.targets = [{ + id: 'id', + isSprite: () => true, + sprite: null + }]; + t.throws( + (() => vm.deleteSprite('id')), + new Error('No sprite associated with this target.') + ); + t.end(); +}); + +test('deleteSprite throws when there is no target with given id', t => { + const vm = new VirtualMachine(); + vm.runtime.targets = [{ + id: 'id', + isSprite: () => true, + sprite: { + name: 'this name' + } + }]; + t.throws( + (() => vm.deleteSprite('id1')), + new Error('No target with the provided id.') + ); + t.end(); +}); + +test('deleteSprite deletes a sprite when given id is associated with a known sprite', t => { + const vm = new VirtualMachine(); + const spr = new Sprite(null, vm.runtime); + const currTarget = spr.createClone(); + + vm.runtime.targets = [currTarget]; + + t.equal(currTarget.sprite.clones.length, 1); + vm.deleteSprite(currTarget.id); + t.equal(currTarget.sprite.clones.length, 0); + t.end(); +}); + +// eslint-disable-next-line max-len +test('deleteSprite sets editing target as null when given sprite is current editing target, and the only target in the runtime', t => { + const vm = new VirtualMachine(); + const spr = new Sprite(null, vm.runtime); + const currTarget = spr.createClone(); + + vm.editingTarget = currTarget; + vm.runtime.targets = [currTarget]; + + vm.deleteSprite(currTarget.id); + + t.equal(vm.runtime.targets.length, 0); + t.equal(vm.editingTarget, null); + t.end(); +}); + +// eslint-disable-next-line max-len +test('deleteSprite updates editingTarget when sprite being deleted is current editing target, and there is another target in the runtime', t => { + const vm = new VirtualMachine(); + const spr1 = new Sprite(null, vm.runtime); + const spr2 = new Sprite(null, vm.runtime); + const currTarget = spr1.createClone(); + const otherTarget = spr2.createClone(); + + vm.emitWorkspaceUpdate = () => null; + + vm.runtime.targets = [currTarget, otherTarget]; + vm.editingTarget = currTarget; + + t.equal(vm.runtime.targets.length, 2); + vm.deleteSprite(currTarget.id); + t.equal(vm.runtime.targets.length, 1); + t.equal(vm.editingTarget.id, otherTarget.id); + + // now let's try them in the other order in the runtime.targets list + + // can't reuse deleted targets + const currTarget2 = spr1.createClone(); + const otherTarget2 = spr2.createClone(); + + vm.runtime.targets = [otherTarget2, currTarget2]; + vm.editingTarget = currTarget2; + + t.equal(vm.runtime.targets.length, 2); + vm.deleteSprite(currTarget2.id); + t.equal(vm.editingTarget.id, otherTarget2.id); + t.equal(vm.runtime.targets.length, 1); + + t.end(); +}); + +test('duplicateSprite throws when there is no target with given id', t => { + const vm = new VirtualMachine(); + vm.runtime.targets = [{ + id: 'id', + isSprite: () => true, + sprite: { + name: 'this name' + } + }]; + t.throws( + (() => vm.duplicateSprite('id1')), + new Error('No target with the provided id') + ); + t.end(); +}); + +test('duplicateSprite throws when used on a non-sprite target', t => { + const vm = new VirtualMachine(); + vm.runtime.targets = [{ + id: 'id', + isSprite: () => false + }]; + t.throws( + (() => vm.duplicateSprite('id')), + new Error('Cannot duplicate non-sprite targets.') + ); + t.end(); +}); + +test('duplicateSprite throws when there is no sprite for the given target', t => { + const vm = new VirtualMachine(); + vm.runtime.targets = [{ + id: 'id', + isSprite: () => true, + sprite: null + }]; + t.throws( + (() => vm.duplicateSprite('id')), + new Error('No sprite associated with this target.') + ); + t.end(); +}); + +test('duplicateSprite duplicates a sprite when given id is associated with known sprite', t => { + const vm = new VirtualMachine(); + const spr = new Sprite(null, vm.runtime); + const currTarget = spr.createClone(); + vm.editingTarget = currTarget; + + vm.emitWorkspaceUpdate = () => null; + + vm.runtime.targets = [currTarget]; + t.equal(vm.runtime.targets.length, 1); + vm.duplicateSprite(currTarget.id).then(() => { + t.equal(vm.runtime.targets.length, 2); + t.end(); + }); + +}); + +test('duplicateSprite assigns duplicated sprite a fresh name', t => { + const vm = new VirtualMachine(); + const spr = new Sprite(null, vm.runtime); + spr.name = 'sprite1'; + const currTarget = spr.createClone(); + vm.editingTarget = currTarget; + + vm.emitWorkspaceUpdate = () => null; + + vm.runtime.targets = [currTarget]; + t.equal(vm.runtime.targets.length, 1); + vm.duplicateSprite(currTarget.id).then(() => { + t.equal(vm.runtime.targets.length, 2); + t.equal(vm.runtime.targets[0].sprite.name, 'sprite1'); + t.equal(vm.runtime.targets[1].sprite.name, 'sprite2'); + t.end(); + }); + +}); + +test('reorderCostume', t => { + const vm = new VirtualMachine(); + vm.emitTargetsUpdate = () => {}; + + const spr = new Sprite(null, vm.runtime); + spr.name = 'foo'; + const target = spr.createClone(); + + // Stub out reorder on target, tested in rendered-target tests. + // Just want to know if it is called with the right params. + let costumeIndex = null; + let newIndex = null; + target.reorderCostume = (_costumeIndex, _newIndex) => { + costumeIndex = _costumeIndex; + newIndex = _newIndex; + return true; // Do not need all the logic about if a reorder occurred. + }; + + vm.runtime.targets = [target]; + + t.equal(vm.reorderCostume('not-a-target', 0, 3), false); + t.equal(costumeIndex, null); + t.equal(newIndex, null); + + t.equal(vm.reorderCostume(target.id, 0, 3), true); + t.equal(costumeIndex, 0); + t.equal(newIndex, 3); + + t.end(); +}); + +test('reorderSound', t => { + const vm = new VirtualMachine(); + vm.emitTargetsUpdate = () => {}; + + const spr = new Sprite(null, vm.runtime); + spr.name = 'foo'; + const target = spr.createClone(); + + // Stub out reorder on target, tested in rendered-target tests. + // Just want to know if it is called with the right params. + let soundIndex = null; + let newIndex = null; + target.reorderSound = (_soundIndex, _newIndex) => { + soundIndex = _soundIndex; + newIndex = _newIndex; + return true; // Do not need all the logic about if a reorder occurred. + }; + + vm.runtime.targets = [target]; + + t.equal(vm.reorderSound('not-a-target', 0, 3), false); + t.equal(soundIndex, null); // Make sure reorder function was not called somehow. + t.equal(newIndex, null); + + t.equal(vm.reorderSound(target.id, 0, 3), true); + t.equal(soundIndex, 0); // Make sure reorder function was called correctly. + t.equal(newIndex, 3); + + t.end(); +}); + +test('shareCostumeToTarget', t => { + const vm = new VirtualMachine(); + const spr1 = new Sprite(null, vm.runtime); + spr1.name = 'foo'; + const target1 = spr1.createClone(); + const costume1 = {name: 'costume1'}; + target1.addCostume(costume1); + + const spr2 = new Sprite(null, vm.runtime); + spr2.name = 'foo'; + const target2 = spr2.createClone(); + const costume2 = {name: 'another costume'}; + target2.addCostume(costume2); + + vm.runtime.targets = [target1, target2]; + vm.editingTarget = vm.runtime.targets[0]; + vm.emitWorkspaceUpdate = () => null; + + vm.shareCostumeToTarget(0, target2.id).then(() => { + t.equal(target2.currentCostume, 1); + t.equal(target2.getCostumes()[1].name, 'costume1'); + t.end(); + }); +}); + +test('shareSoundToTarget', t => { + const vm = new VirtualMachine(); + const spr1 = new Sprite(null, vm.runtime); + spr1.name = 'foo'; + const target1 = spr1.createClone(); + const sound1 = {name: 'sound1'}; + target1.addSound(sound1); + + const spr2 = new Sprite(null, vm.runtime); + spr2.name = 'foo'; + const target2 = spr2.createClone(); + const sound2 = {name: 'another sound'}; + target2.addSound(sound2); + + vm.runtime.targets = [target1, target2]; + vm.editingTarget = vm.runtime.targets[0]; + vm.emitWorkspaceUpdate = () => null; + + vm.shareSoundToTarget(0, target2.id).then(() => { + t.equal(target2.getSounds()[1].name, 'sound1'); + t.end(); + }); +}); + +test('reorderTarget', t => { + const vm = new VirtualMachine(); + vm.emitTargetsUpdate = () => {}; + + vm.runtime.targets = ['a', 'b', 'c', 'd']; + + t.equal(vm.reorderTarget(2, 2), false); + t.deepEqual(vm.runtime.targets, ['a', 'b', 'c', 'd']); + + // Make sure clamping works + t.equal(vm.reorderTarget(-100, -5), false); + t.deepEqual(vm.runtime.targets, ['a', 'b', 'c', 'd']); + + // Reorder upwards + t.equal(vm.reorderTarget(0, 2), true); + t.deepEqual(vm.runtime.targets, ['b', 'c', 'a', 'd']); + + // Reorder downwards + t.equal(vm.reorderTarget(3, 1), true); + t.deepEqual(vm.runtime.targets, ['b', 'd', 'c', 'a']); + + t.end(); +}); + +test('emitWorkspaceUpdate', t => { + const vm = new VirtualMachine(); + const blocksToXML = comments => { + let blockString = 'blocks\n'; + if (comments) { + for (const commentId in comments) { + const comment = comments[commentId]; + blockString += `A Block Comment: ${comment.toXML()}`; + } + + } + return blockString; + }; + vm.runtime.targets = [ + { + isStage: true, + variables: { + global: { + toXML: () => 'global' + } + }, + blocks: { + toXML: blocksToXML + }, + comments: { + aStageComment: { + toXML: () => 'aStageComment', + blockId: null + } + } + }, { + variables: { + unused: { + toXML: () => 'unused' + } + }, + blocks: { + toXML: blocksToXML + }, + comments: { + someBlockComment: { + toXML: () => 'someBlockComment', + blockId: 'someBlockId' + } + } + }, { + variables: { + local: { + toXML: () => 'local' + } + }, + blocks: { + toXML: blocksToXML + }, + comments: { + someOtherComment: { + toXML: () => 'someOtherComment', + blockId: null + }, + aBlockComment: { + toXML: () => 'aBlockComment', + blockId: 'a block' + } + } + } + ]; + vm.editingTarget = vm.runtime.targets[2]; + + let xml = null; + vm.emit = (event, data) => (xml = data.xml); + vm.emitWorkspaceUpdate(); + t.notEqual(xml.indexOf('global'), -1); + t.notEqual(xml.indexOf('local'), -1); + t.equal(xml.indexOf('unused'), -1); + t.notEqual(xml.indexOf('blocks'), -1); + t.equal(xml.indexOf('aStageComment'), -1); + t.equal(xml.indexOf('someBlockComment'), -1); + t.notEqual(xml.indexOf('someOtherComment'), -1); + t.notEqual(xml.indexOf('A Block Comment: aBlockComment'), -1); + t.end(); +}); + +test('drag IO redirect', t => { + const vm = new VirtualMachine(); + const sprite1Info = []; + const sprite2Info = []; + vm.runtime.targets = [ + { + id: 'sprite1', + postSpriteInfo: data => sprite1Info.push(data) + }, { + id: 'sprite2', + postSpriteInfo: data => sprite2Info.push(data), + startDrag: () => {}, + stopDrag: () => {} + } + ]; + vm.editingTarget = vm.runtime.targets[0]; + // Stub emitWorkspace/TargetsUpdate, it needs data we don't care about here + vm.emitWorkspaceUpdate = () => null; + vm.emitTargetsUpdate = () => null; + + // postSpriteInfo should go to the editing target by default`` + vm.postSpriteInfo('sprite1 info'); + t.equal(sprite1Info[0], 'sprite1 info'); + + // postSprite info goes to the drag target if it exists + vm.startDrag('sprite2'); + vm.postSpriteInfo('sprite2 info'); + t.equal(sprite2Info[0], 'sprite2 info'); + + // stop drag should set the editing target + vm.stopDrag('sprite2'); + t.equal(vm.editingTarget.id, 'sprite2'); + + // Then postSpriteInfo should continue posting to the new editing target + vm.postSpriteInfo('sprite2 info 2'); + t.equal(sprite2Info[1], 'sprite2 info 2'); + t.end(); +}); + +test('select original after dragging clone', t => { + const vm = new VirtualMachine(); + let newEditingTargetId = null; + vm.setEditingTarget = id => { + newEditingTargetId = id; + }; + vm.runtime.targets = [ + { + id: 'sprite1_clone', + sprite: {clones: [{id: 'sprite1_original'}]}, + stopDrag: () => {} + }, { + id: 'sprite2', + stopDrag: () => {} + } + ]; + + // Stop drag on a bare target selects that target + vm.stopDrag('sprite2'); + t.equal(newEditingTargetId, 'sprite2'); + + // Stop drag on target with parent sprite selects the 0th clone of that sprite + vm.stopDrag('sprite1_clone'); + t.equal(newEditingTargetId, 'sprite1_original'); + t.end(); +}); + +test('setVariableValue', t => { + const vm = new VirtualMachine(); + const spr = new Sprite(null, vm.runtime); + const target = spr.createClone(); + target.createVariable('a-variable', 'a-name', Variable.SCALAR_TYPE); + + vm.runtime.targets = [target]; + + // Returns false if there is no variable to set + t.equal(vm.setVariableValue(target.id, 'not-a-variable', 100), false); + + // Returns false if there is no target with that id + t.equal(vm.setVariableValue('not-a-target', 'a-variable', 100), false); + + // Returns true and updates the value if variable is present + t.equal(vm.setVariableValue(target.id, 'a-variable', 100), true); + t.equal(target.lookupVariableById('a-variable').value, 100); + + t.end(); +}); + +test('setVariableValue requests update for cloud variable', t => { + const vm = new VirtualMachine(); + const spr = new Sprite(null, vm.runtime); + const target = spr.createClone(); + target.isStage = true; + target.createVariable('a-variable', 'a-name', Variable.SCALAR_TYPE, true /* isCloud */); + + vm.runtime.targets = [target]; + + // Mock cloud io device requestUpdateVariable function + let requestUpdateVarWasCalled = false; + let varName; + let varValue; + vm.runtime.ioDevices.cloud.requestUpdateVariable = (name, value) => { + requestUpdateVarWasCalled = true; + varName = name; + varValue = value; + }; + + vm.setVariableValue(target.id, 'not-a-variable', 100); + t.equal(requestUpdateVarWasCalled, false); + + vm.setVariableValue(target.id, 'a-variable', 100); + t.equal(requestUpdateVarWasCalled, true); + t.equal(varName, 'a-name'); + t.equal(varValue, 100); + + t.end(); +}); + +test('getVariableValue', t => { + const vm = new VirtualMachine(); + const spr = new Sprite(null, vm.runtime); + const target = spr.createClone(); + target.createVariable('a-variable', 'a-name', Variable.SCALAR_TYPE); + + vm.runtime.targets = [target]; + + // Returns null if there is no variable with that id + t.equal(vm.getVariableValue(target.id, 'not-a-variable'), null); + + // Returns null if there is no target with that id + t.equal(vm.getVariableValue('not-a-target', 'a-variable'), null); + + // Returns true and updates the value if variable is present + t.equal(vm.getVariableValue(target.id, 'a-variable'), 0); + vm.setVariableValue(target.id, 'a-variable', 'string'); + t.equal(vm.getVariableValue(target.id, 'a-variable'), 'string'); + + t.end(); +}); + +// Block Listener tests for comment +test('comment_create event updates comment with null position', t => { + const vm = new VirtualMachine(); + const spr = new Sprite(null, vm.runtime); + const target = spr.createClone(); + + target.createComment('a comment', null, 'some text', + null, null, 200, 300, false); + vm.runtime.targets = [target]; + vm.editingTarget = target; + vm.runtime.setEditingTarget(target); + + const comment = target.comments['a comment']; + t.equal(comment.x, null); + t.equal(comment.y, null); + + vm.blockListener(events.createcommentUpdatePosition); + + t.equal(comment.x, 10); + t.equal(comment.y, 20); + + t.end(); +}); + +test('shareBlocksToTarget shares global variables without any name changes', t => { + const vm = new VirtualMachine(); + const runtime = vm.runtime; + const spr1 = new Sprite(null, runtime); + const stage = spr1.createClone(); + stage.isStage = true; + + const spr2 = new Sprite(null, runtime); + const target = spr2.createClone(); + + runtime.targets = [stage, target]; + vm.editingTarget = target; + vm.runtime.setEditingTarget(target); + + stage.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); + t.equal(Object.keys(target.variables).length, 0); + t.equal(Object.keys(stage.variables).length, 1); + t.equal(stage.variables['mock var id'].name, 'a mock variable'); + + + vm.setVariableValue(stage.id, 'mock var id', 10); + t.equal(vm.getVariableValue(stage.id, 'mock var id'), 10); + + target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); + + // Verify the block exists on the target, and that it references the global variable + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + + // Verify that the block does not exist on the stage + t.type(stage.blocks.getBlock('a block'), 'undefined'); + + // Share the block to the stage + vm.shareBlocksToTarget([target.blocks.getBlock('a block')], stage.id, target.id).then(() => { + + // Verify that the block now exists on the target as well as the stage + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + + const newBlockId = Object.keys(stage.blocks._blocks)[0]; + t.type(stage.blocks.getBlock(newBlockId), 'object'); + t.type(stage.blocks.getBlock(newBlockId).fields, 'object'); + t.type(stage.blocks.getBlock(newBlockId).fields.VARIABLE, 'object'); + t.equal(stage.blocks.getBlock(newBlockId).fields.VARIABLE.id, 'mock var id'); + + // Verify the shared block id is different + t.notEqual(newBlockId, 'a block'); + + // Verify that the variables haven't changed, the variable still exists on the + // stage, it should still have the same name and value, and there should be + // no variables on the target. + t.equal(Object.keys(target.variables).length, 0); + t.equal(Object.keys(stage.variables).length, 1); + t.equal(stage.variables['mock var id'].name, 'a mock variable'); + t.equal(vm.getVariableValue(stage.id, 'mock var id'), 10); + + t.end(); + }); +}); + +test('shareBlocksToTarget shares a local variable to the stage, creating a global variable with a new name', t => { + const vm = new VirtualMachine(); + const runtime = vm.runtime; + const spr1 = new Sprite(null, runtime); + const stage = spr1.createClone(); + stage.isStage = true; + + const spr2 = new Sprite(null, runtime); + const target = spr2.createClone(); + + runtime.targets = [stage, target]; + vm.editingTarget = target; + vm.runtime.setEditingTarget(target); + + target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); + t.equal(Object.keys(stage.variables).length, 0); + t.equal(Object.keys(target.variables).length, 1); + t.equal(target.variables['mock var id'].name, 'a mock variable'); + + + vm.setVariableValue(target.id, 'mock var id', 10); + t.equal(vm.getVariableValue(target.id, 'mock var id'), 10); + + target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); + + // Verify the block exists on the target, and that it references the global variable + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + + // Verify that the block does not exist on the stage + t.type(stage.blocks.getBlock('a block'), 'undefined'); + + // Share the block to the stage + vm.shareBlocksToTarget([target.blocks.getBlock('a block')], stage.id, target.id).then(() => { + // Verify that the block still exists on the target and remains unchanged + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + + const newBlockId = Object.keys(stage.blocks._blocks)[0]; + t.type(stage.blocks.getBlock(newBlockId), 'object'); + t.type(stage.blocks.getBlock(newBlockId).fields, 'object'); + t.type(stage.blocks.getBlock(newBlockId).fields.VARIABLE, 'object'); + t.equal(stage.blocks.getBlock(newBlockId).fields.VARIABLE.id, 'StageVarFromLocal_mock var id'); + + // Verify that a new global variable was created, the old one still exists on + // the target and still has the same name and value, and the new one has + // a new name and value 0. + t.equal(Object.keys(target.variables).length, 1); + t.equal(target.variables['mock var id'].name, 'a mock variable'); + t.equal(vm.getVariableValue(target.id, 'mock var id'), 10); + + // Verify that a new variable was created on the stage, with a new name and new id + t.equal(Object.keys(stage.variables).length, 1); + t.type(stage.variables['mock var id'], 'undefined'); + const newGlobalVar = Object.values(stage.variables)[0]; + t.equal(newGlobalVar.name, 'Stage: a mock variable'); + const newId = newGlobalVar.id; + t.notEqual(newId, 'mock var id'); + t.equals(vm.getVariableValue(stage.id, newId), 0); + + t.end(); + }); +}); + +test('shareBlocksToTarget chooses a fresh name for a new global variable checking for conflicts on all sprites', t => { + const vm = new VirtualMachine(); + const runtime = vm.runtime; + const spr1 = new Sprite(null, runtime); + const stage = spr1.createClone(); + stage.isStage = true; + + const spr2 = new Sprite(null, runtime); + const target = spr2.createClone(); + + const spr3 = new Sprite(null, runtime); + const otherTarget = spr3.createClone(); + + runtime.targets = [stage, target, otherTarget]; + vm.editingTarget = target; + vm.runtime.setEditingTarget(target); + + target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE); + t.equal(Object.keys(stage.variables).length, 0); + t.equal(Object.keys(target.variables).length, 1); + t.equal(target.variables['mock var id'].name, 'a mock variable'); + + + vm.setVariableValue(target.id, 'mock var id', 10); + t.equal(vm.getVariableValue(target.id, 'mock var id'), 10); + + target.blocks.createBlock(adapter(events.mockVariableBlock)[0]); + + // Verify the block exists on the target, and that it references the global variable + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + + // Verify that the block does not exist on the stage + t.type(stage.blocks.getBlock('a block'), 'undefined'); + + // Create a variable that conflicts with what will be the new name for the + // new global variable to ensure a fresh name is chosen + otherTarget.createVariable('a different var', 'Stage: a mock variable', Variable.SCALAR_TYPE); + + // Share the block to the stage + vm.shareBlocksToTarget([target.blocks.getBlock('a block')], stage.id, target.id).then(() => { + // Verify that the block still exists on the target and remains unchanged + t.type(target.blocks.getBlock('a block'), 'object'); + t.type(target.blocks.getBlock('a block').fields, 'object'); + t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object'); + t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id'); + + const newBlockId = Object.keys(stage.blocks._blocks)[0]; + t.type(stage.blocks.getBlock(newBlockId), 'object'); + t.type(stage.blocks.getBlock(newBlockId).fields, 'object'); + t.type(stage.blocks.getBlock(newBlockId).fields.VARIABLE, 'object'); + t.equal(stage.blocks.getBlock(newBlockId).fields.VARIABLE.id, 'StageVarFromLocal_mock var id'); + + // Verify that a new global variable was created, the old one still exists on + // the target and still has the same name and value, and the new one has + // a new name and value 0. + t.equal(Object.keys(target.variables).length, 1); + t.equal(target.variables['mock var id'].name, 'a mock variable'); + t.equal(vm.getVariableValue(target.id, 'mock var id'), 10); + + // Verify that a new variable was created on the stage, with a new name and new id + t.equal(Object.keys(stage.variables).length, 1); + t.type(stage.variables['mock var id'], 'undefined'); + const newGlobalVar = Object.values(stage.variables)[0]; + t.equal(newGlobalVar.name, 'Stage: a mock variable2'); + const newId = newGlobalVar.id; + t.notEqual(newId, 'mock var id'); + t.equals(vm.getVariableValue(stage.id, newId), 0); + + t.end(); + }); +}); + +test('shareBlocksToTarget loads extensions that have not yet been loaded', t => { + const vm = new VirtualMachine(); + const runtime = vm.runtime; + const spr1 = new Sprite(null, runtime); + const stage = spr1.createClone(); + runtime.targets = [stage]; + + const fakeBlocks = [ + {opcode: 'pen_something'}, + {opcode: 'translate_something'} + ]; + + // Stub the extension manager + const loadedIds = []; + vm.extensionManager = { + allAsyncExtensionsLoaded: () => Promise.resolve(), + isBuiltinExtension: () => true, + isExtensionLoaded: id => id === 'pen', + loadExtensionIdSync: id => new Promise(resolve => { + loadedIds.push(id); + resolve(); + }) + }; + + vm.shareBlocksToTarget(fakeBlocks, stage.id).then(() => { + // Verify that only the not-loaded extension gets loaded + t.deepEqual(loadedIds, ['translate']); + t.end(); + }); +}); + +test('Setting turbo mode emits events', t => { + let turboMode = null; + + const vm = new VirtualMachine(); + + vm.addListener('TURBO_MODE_ON', () => { + turboMode = true; + }); + vm.addListener('TURBO_MODE_OFF', () => { + turboMode = false; + }); + + vm.setTurboMode(true); + t.equal(turboMode, true); + + vm.setTurboMode(false); + t.equal(turboMode, false); + + t.end(); +}); + +test('Getting the renderer returns the renderer', t => { + const renderer = new Renderer(); + const vm = new VirtualMachine(); + vm.attachRenderer(renderer); + t.equal(vm.renderer, renderer); + t.end(); +}); + +test('Starting the VM emits an event', t => { + let started = false; + const vm = new VirtualMachine(); + vm.addListener('RUNTIME_STARTED', () => { + started = true; + }); + vm.start(); + t.equal(started, true); + t.end(); +}); + +test('vm.greenFlag() emits a PROJECT_START event', t => { + let greenFlagged = false; + const vm = new VirtualMachine(); + vm.addListener('PROJECT_START', () => { + greenFlagged = true; + }); + vm.greenFlag(); + t.equal(greenFlagged, true); + t.end(); +}); + +test('toJSON encodes Infinity/NaN as 0, not null', t => { + const vm = new VirtualMachine(); + const runtime = vm.runtime; + const spr1 = new Sprite(null, runtime); + const stage = spr1.createClone(); + stage.isStage = true; + stage.volume = Infinity; + stage.tempo = NaN; + stage.createVariable('id1', 'name1', ''); + stage.variables.id1.value = Infinity; + stage.createVariable('id2', 'name2', ''); + stage.variables.id1.value = -Infinity; + stage.createVariable('id3', 'name3', ''); + stage.variables.id1.value = NaN; + + runtime.targets = [stage]; + + const json = JSON.parse(vm.toJSON()); + t.equal(json.targets[0].volume, 0); + t.equal(json.targets[0].tempo, 0); + t.equal(json.targets[0].variables.id1[1], 0); + t.equal(json.targets[0].variables.id2[1], 0); + t.equal(json.targets[0].variables.id3[1], 0); + + t.end(); +}); diff --git a/local-scratch-vm/test/unit/virtual-machine_tw.js b/local-scratch-vm/test/unit/virtual-machine_tw.js new file mode 100644 index 0000000000000000000000000000000000000000..b0d2f52c075629c1e93d9aec5ca115ed633b55bf --- /dev/null +++ b/local-scratch-vm/test/unit/virtual-machine_tw.js @@ -0,0 +1,93 @@ +const test = require('tap').test; +const VirtualMachine = require('../../src/virtual-machine'); +const RenderedTarget = require('../../src/sprites/rendered-target'); +const Sprite = require('../../src/sprites/sprite'); +const Variable = require('../../src/engine/variable'); + +test('emitTargetsUpdate targetList is lazy', t => { + const vm = new VirtualMachine(); + let calledToJSON = false; + vm.runtime.targets = [{ + toJSON () { + calledToJSON = true; + return {}; + } + }]; + let targetsUpdateEvent; + vm.on('targetsUpdate', e => { + targetsUpdateEvent = e; + }); + vm.emitTargetsUpdate(); + t.equal(calledToJSON, false); + void targetsUpdateEvent.targetList; // should trigger lazy compute + t.equal(calledToJSON, true); + t.end(); +}); + +test('non-primitive values in lists and variables converted to strings', t => { + const vm = new VirtualMachine(); + const sprite = new Sprite(); + const target = new RenderedTarget(sprite, vm.runtime); + + target.variables.var1 = new Variable('var', 'test var', Variable.SCALAR_TYPE, false); + target.variables.var1.value = null; + + target.variables.var2 = new Variable('var2', 'test var', Variable.SCALAR_TYPE, false); + target.variables.var2.value = undefined; + + target.variables.var3 = new Variable('var3', 'test var', Variable.SCALAR_TYPE, false); + target.variables.var3.value = {}; + + target.variables.var4 = new Variable('var4', 'test var', Variable.SCALAR_TYPE, false); + target.variables.var4.value = 1; + + target.variables.var5 = new Variable('var5', 'test var', Variable.SCALAR_TYPE, false); + target.variables.var5.value = 'abc'; + + target.variables.var6 = new Variable('var6', 'test var', Variable.SCALAR_TYPE, false); + target.variables.var6.value = false; + + target.variables.list = new Variable('list', 'test list', Variable.LIST_TYPE, false); + target.variables.list.value = ['abc', false, 1, null, undefined, {}]; + + vm.runtime.addTarget(target); + + const json = JSON.parse(vm.toJSON()); + + t.deepEqual(json.targets[0].variables.var1[1], 'null'); + t.deepEqual(json.targets[0].variables.var2[1], 'undefined'); + t.deepEqual(json.targets[0].variables.var3[1], '[object Object]'); + t.deepEqual(json.targets[0].variables.var4[1], 1); + t.deepEqual(json.targets[0].variables.var5[1], 'abc'); + t.deepEqual(json.targets[0].variables.var6[1], false); + + t.deepEqual(json.targets[0].lists.list[1], ['abc', false, 1, 'null', 'undefined', '[object Object]']); + + t.end(); +}); + +test('addSound error handling when sprite does not exist', async t => { + t.plan(1); + const vm = new VirtualMachine(); + const id = 'Inva1id5pri731D$!'; + try { + await vm.addSound({ + thisObjectDoesNotMatter: true + }, id); + } catch (e) { + if (e && e.message === `No target with ID: ${id}`) { + t.pass(); + } + } + t.end(); +}); + +test('convertToPackagedRuntime forwards to runtime', t => { + t.plan(1); + const vm = new VirtualMachine(); + vm.runtime.convertToPackagedRuntime = () => { + t.pass(); + }; + vm.convertToPackagedRuntime(); + t.end(); +}); diff --git a/local-scratch-vm/test/unit/vm_collectAssets.js b/local-scratch-vm/test/unit/vm_collectAssets.js new file mode 100644 index 0000000000000000000000000000000000000000..8c356a8fd9e2277926b4edb9df3a8bf42961ac95 --- /dev/null +++ b/local-scratch-vm/test/unit/vm_collectAssets.js @@ -0,0 +1,24 @@ +const test = require('tap').test; + +const RenderedTarget = require('../../src/sprites/rendered-target'); +const Sprite = require('../../src/sprites/sprite'); +const VirtualMachine = require('../../src/virtual-machine'); + +test('collectAssets', t => { + const vm = new VirtualMachine(); + const sprite = new Sprite(null, vm.runtime); + const target = new RenderedTarget(sprite, vm.runtime); + vm.runtime.targets = [target]; + const [ + soundAsset1, + soundAsset2, + costumeAsset1 + ] = [{assetId: 1}, {assetId: 2}, {assetId: 3}]; + sprite.sounds = [{id: 1, asset: soundAsset1}, {id: 2, asset: soundAsset2}]; + sprite.costumes = [{id: 1, asset: costumeAsset1}]; + const assets = vm.assets; + t.type(assets.length, 'number'); + t.equal(assets.length, 3); + t.deepEqual(assets, [soundAsset1, soundAsset2, costumeAsset1]); + t.end(); +}); diff --git a/local-scratch-vm/webpack.config.js b/local-scratch-vm/webpack.config.js new file mode 100644 index 0000000000000000000000000000000000000000..32024609aa27e31b2de9df5cc1cd5c0c7de202e1 --- /dev/null +++ b/local-scratch-vm/webpack.config.js @@ -0,0 +1,141 @@ +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const defaultsDeep = require('lodash.defaultsdeep'); +const path = require('path'); + +const base = { + mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', + devServer: { + contentBase: false, + host: '0.0.0.0', + port: process.env.PORT || 8073 + }, + devtool: 'cheap-module-source-map', + output: { + library: 'VirtualMachine', + filename: '[name].js' + }, + module: { + rules: [{ + test: /\.js$/, + loader: 'babel-loader', + include: path.resolve(__dirname, 'src'), + query: { + presets: [['@babel/preset-env']] + } + }, + { + test: /\.mp3$/, + loader: 'file-loader', + options: { + outputPath: 'media/music/' + } + }] + }, + plugins: [] +}; + +module.exports = [ + // Web-compatible + defaultsDeep({}, base, { + target: 'web', + entry: { + 'scratch-vm': './src/index.js', + 'scratch-vm.min': './src/index.js' + }, + output: { + libraryTarget: 'umd', + path: path.resolve('dist', 'web') + }, + module: { + rules: base.module.rules.concat([ + { + test: require.resolve('./src/index.js'), + loader: 'expose-loader?VirtualMachine' + } + ]) + } + }), + // Node-compatible + defaultsDeep({}, base, { + target: 'node', + entry: { + 'scratch-vm': './src/index.js' + }, + output: { + libraryTarget: 'commonjs2', + path: path.resolve('dist', 'node') + }, + externals: { + 'decode-html': true, + 'format-message': true, + 'htmlparser2': true, + 'immutable': true, + 'jszip': true, + 'minilog': true, + 'scratch-parser': true, + 'socket.io-client': true, + 'text-encoding': true + } + }), + // Playground + defaultsDeep({}, base, { + target: 'web', + entry: { + 'benchmark': './src/playground/benchmark', + 'video-sensing-extension-debug': './src/extensions/scratch3_video_sensing/debug' + }, + output: { + path: path.resolve(__dirname, 'playground'), + filename: '[name].js' + }, + module: { + rules: base.module.rules.concat([ + { + test: require.resolve('./src/index.js'), + loader: 'expose-loader?VirtualMachine' + }, + { + test: require.resolve('./src/extensions/scratch3_video_sensing/debug.js'), + loader: 'expose-loader?Scratch3VideoSensingDebug' + }, + { + test: require.resolve('stats.js/build/stats.min.js'), + loader: 'script-loader' + }, + { + test: require.resolve('scratch-blocks/dist/vertical.js'), + loader: 'expose-loader?Blockly' + }, + { + test: require.resolve('scratch-audio/src/index.js'), + loader: 'expose-loader?AudioEngine' + }, + { + test: require.resolve('scratch-storage/src/index.js'), + loader: 'expose-loader?ScratchStorage' + }, + { + test: require.resolve('scratch-render/src/index.js'), + loader: 'expose-loader?ScratchRender' + } + ]) + }, + performance: { + hints: false + }, + plugins: base.plugins.concat([ + new CopyWebpackPlugin([{ + from: 'node_modules/scratch-blocks/media', + to: 'media' + }, { + from: 'node_modules/scratch-storage/dist/web' + }, { + from: 'node_modules/scratch-render/dist/web' + }, { + from: 'node_modules/scratch-svg-renderer/dist/web' + }, { + from: 'src/playground' + }]) + ]) + }) +];