diff --git a/package-lock.json b/package-lock.json index 884c30bb08..4c3121bb14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3151,6 +3151,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@mediapipe/face_detection": { + "version": "0.4.1646425229", + "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz", + "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg==", + "license": "Apache-2.0" + }, "node_modules/@microbit/microbit-universal-hex": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@microbit/microbit-universal-hex/-/microbit-universal-hex-0.2.2.tgz", @@ -6311,6 +6317,267 @@ "node": ">=4" } }, + "node_modules/@tensorflow-models/face-detection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tensorflow-models/face-detection/-/face-detection-1.0.3.tgz", + "integrity": "sha512-4Ld/vFF8MrdFdrMWhlLKZD4hMW0PNY9OkYeqoCPNZ+LwFyenxAqVaNaWrR8JKp37vw9Nuzp4ILbkal5zPUnA0g==", + "dependencies": { + "rimraf": "^3.0.2", + "tslib": "2.4.0" + }, + "peerDependencies": { + "@mediapipe/face_detection": "~0.4.0", + "@tensorflow/tfjs-backend-webgl": "^4.21.0", + "@tensorflow/tfjs-converter": "^4.21.0", + "@tensorflow/tfjs-core": "^4.21.0" + } + }, + "node_modules/@tensorflow-models/face-detection/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tensorflow-models/face-detection/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/@tensorflow/tfjs": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.22.0.tgz", + "integrity": "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@tensorflow/tfjs-backend-webgl": "4.22.0", + "@tensorflow/tfjs-converter": "4.22.0", + "@tensorflow/tfjs-core": "4.22.0", + "@tensorflow/tfjs-data": "4.22.0", + "@tensorflow/tfjs-layers": "4.22.0", + "argparse": "^1.0.10", + "chalk": "^4.1.0", + "core-js": "3.29.1", + "regenerator-runtime": "^0.13.5", + "yargs": "^16.0.3" + }, + "bin": { + "tfjs-custom-module": "dist/tools/custom_module/cli.js" + } + }, + "node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", + "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-backend-webgl": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", + "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-converter": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", + "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", + "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.7.0", + "@types/seedrandom": "^2.4.28", + "@webgpu/types": "0.1.38", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==" + }, + "node_modules/@tensorflow/tfjs-core/node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@tensorflow/tfjs-core/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@tensorflow/tfjs-core/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@tensorflow/tfjs-core/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@tensorflow/tfjs-data": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz", + "integrity": "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==", + "dependencies": { + "@types/node-fetch": "^2.1.2", + "node-fetch": "~2.6.1", + "string_decoder": "^1.3.0" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0", + "seedrandom": "^3.0.5" + } + }, + "node_modules/@tensorflow/tfjs-data/node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@tensorflow/tfjs-data/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@tensorflow/tfjs-data/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@tensorflow/tfjs-data/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@tensorflow/tfjs-layers": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz", + "integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/core-js": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", + "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@transifex/api": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/@transifex/api/-/api-7.1.4.tgz", @@ -6569,6 +6836,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/markdown-it": { "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", @@ -6610,6 +6882,30 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/node-forge": { "version": "1.3.13", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.13.tgz", @@ -6636,6 +6932,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/offscreencanvas": { + "version": "2019.3.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", + "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -6690,6 +6991,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/seedrandom": { + "version": "2.4.34", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", + "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==" + }, "node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", @@ -7223,6 +7529,11 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", + "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==" + }, "node_modules/@webpack-cli/configtest": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", @@ -7533,7 +7844,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7543,7 +7853,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -10022,7 +10331,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -10323,7 +10631,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -10336,7 +10643,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/color-support": { @@ -12895,7 +13201,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/emojis-list": { @@ -13256,7 +13561,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -16241,7 +16545,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -18654,7 +18957,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -23440,6 +23742,11 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/lookup-closest-locale": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", @@ -30909,7 +31216,6 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true, "license": "MIT" }, "node_modules/regex-cache": { @@ -31324,7 +31630,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -32424,6 +32729,11 @@ "integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q==", "dev": true }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "node_modules/seek-bzip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", @@ -33704,7 +34014,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/sshpk": { @@ -34234,7 +34543,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -34347,7 +34655,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -34485,7 +34792,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -39115,7 +39421,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -39302,7 +39607,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -39346,7 +39650,6 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -39403,10 +39706,13 @@ "version": "11.6.0-gui-standalone", "license": "AGPL-3.0-only", "dependencies": { + "@mediapipe/face_detection": "0.4.1646425229", "@microbit/microbit-universal-hex": "0.2.2", "@scratch/scratch-render": "11.6.0-gui-standalone", "@scratch/scratch-svg-renderer": "11.6.0-gui-standalone", "@scratch/scratch-vm": "11.6.0-gui-standalone", + "@tensorflow-models/face-detection": "^1.0.3", + "@tensorflow/tfjs": "^4.22.0", "arraybuffer-loader": "1.0.8", "autoprefixer": "9.8.8", "balance-text": "3.3.1", diff --git a/packages/scratch-gui/package.json b/packages/scratch-gui/package.json index f1049b92cd..65da82a5e3 100644 --- a/packages/scratch-gui/package.json +++ b/packages/scratch-gui/package.json @@ -57,7 +57,10 @@ "watch": "webpack --watch" }, "dependencies": { + "@mediapipe/face_detection": "0.4.1646425229", "@microbit/microbit-universal-hex": "0.2.2", + "@tensorflow-models/face-detection": "^1.0.3", + "@tensorflow/tfjs": "^4.22.0", "@scratch/scratch-render": "11.6.0-gui-standalone", "@scratch/scratch-svg-renderer": "11.6.0-gui-standalone", "@scratch/scratch-vm": "11.6.0-gui-standalone", diff --git a/packages/scratch-gui/src/containers/blocks.jsx b/packages/scratch-gui/src/containers/blocks.jsx index 84b1e3f3ce..714f306ea5 100644 --- a/packages/scratch-gui/src/containers/blocks.jsx +++ b/packages/scratch-gui/src/containers/blocks.jsx @@ -144,6 +144,12 @@ class Blocks extends React.Component { if (this.props.isVisible) { this.setLocale(); } + + window.addEventListener('load-extension', () => { + this.props.vm.extensionManager.loadExtensionURL('faceSensing').then(() => { + this.handleCategorySelected('faceSensing'); + }); + }); } shouldComponentUpdate (nextProps, nextState) { return ( diff --git a/packages/scratch-gui/src/containers/stage.jsx b/packages/scratch-gui/src/containers/stage.jsx index b13c203575..511f2212b2 100644 --- a/packages/scratch-gui/src/containers/stage.jsx +++ b/packages/scratch-gui/src/containers/stage.jsx @@ -340,16 +340,29 @@ class Stage extends React.Component { } onStartDrag (x, y) { if (this.state.dragId) return; - const drawableId = this.renderer.pick(x, y); + + // Targets with no attached drawable cannot be dragged. + let draggableTargets = this.props.vm.runtime.targets.filter( + target => Number.isFinite(target.drawableID) + ); + + // Because pick queries can be expensive, only perform them for drawables that are currently draggable. + // If we're in the editor, we can drag all targets. Otherwise, filter. + if (!this.props.useEditorDragStyle) { + draggableTargets = draggableTargets.filter( + target => target.draggable + ); + } + if (draggableTargets.length === 0) return; + + const draggableIDs = draggableTargets.map(target => target.drawableID); + const drawableId = this.renderer.pick(x, y, 1, 1, draggableIDs); if (drawableId === null) return; const targetId = this.props.vm.getTargetIdForDrawableId(drawableId); if (targetId === null) return; const target = this.props.vm.runtime.getTargetById(targetId); - // Do not start drag unless in editor drag mode or target is draggable - if (!(this.props.useEditorDragStyle || target.draggable)) return; - // Dragging always brings the target to the front target.goToFront(); diff --git a/packages/scratch-gui/src/lib/alerts/index.jsx b/packages/scratch-gui/src/lib/alerts/index.jsx index dbd4f2ce7d..25deec2410 100644 --- a/packages/scratch-gui/src/lib/alerts/index.jsx +++ b/packages/scratch-gui/src/lib/alerts/index.jsx @@ -212,6 +212,20 @@ const alerts = [ ), iconSpinner: true, level: AlertLevels.SUCCESS + }, + { + alertId: 'loadingExtensionData', + alertType: AlertTypes.STANDARD, + clearList: [], + content: ( + + ), + iconSpinner: true, + level: AlertLevels.SUCCESS } ]; diff --git a/packages/scratch-gui/src/lib/cloud-manager-hoc.jsx b/packages/scratch-gui/src/lib/cloud-manager-hoc.jsx index 26ee48c1ff..7a144e0380 100644 --- a/packages/scratch-gui/src/lib/cloud-manager-hoc.jsx +++ b/packages/scratch-gui/src/lib/cloud-manager-hoc.jsx @@ -101,7 +101,11 @@ const cloudManagerHOC = function (WrappedComponent) { handleExtensionAdded (categoryInfo) { // Note that props.vm.extensionManager.isExtensionLoaded('videoSensing') is still false // at the point of this callback, so it is difficult to reuse the canModifyCloudData logic. - if (categoryInfo.id === 'videoSensing' && this.isConnected()) { + if ( + (categoryInfo.id === 'videoSensing' || + categoryInfo.id === 'faceSensing') && + this.isConnected() + ) { this.disconnectFromCloud(); } } @@ -157,9 +161,18 @@ const cloudManagerHOC = function (WrappedComponent) { isShowingWithId: getIsShowingWithId(loadingState), projectId: state.scratchGui.projectState.projectId, // if you're editing someone else's project, you can't modify cloud data - canModifyCloudData: (!state.scratchGui.mode.hasEverEnteredEditor || ownProps.canSave) && + canModifyCloudData: + (!state.scratchGui.mode.hasEverEnteredEditor || + ownProps.canSave) && // possible security concern if the program attempts to encode webcam data over cloud variables - !ownProps.vm.extensionManager.isExtensionLoaded('videoSensing') + !( + ownProps.vm.extensionManager.isExtensionLoaded( + 'videoSensing' + ) || + ownProps.vm.extensionManager.isExtensionLoaded( + 'faceSensing' + ) + ) }; }; diff --git a/packages/scratch-gui/src/lib/libraries/extensions/faceSensing/faceSensing-small.svg b/packages/scratch-gui/src/lib/libraries/extensions/faceSensing/faceSensing-small.svg new file mode 100644 index 0000000000..2f5cfcd7ab --- /dev/null +++ b/packages/scratch-gui/src/lib/libraries/extensions/faceSensing/faceSensing-small.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/scratch-gui/src/lib/libraries/extensions/faceSensing/faceSensing.png b/packages/scratch-gui/src/lib/libraries/extensions/faceSensing/faceSensing.png new file mode 100644 index 0000000000..a49ea73a28 Binary files /dev/null and b/packages/scratch-gui/src/lib/libraries/extensions/faceSensing/faceSensing.png differ diff --git a/packages/scratch-gui/src/lib/libraries/extensions/index.jsx b/packages/scratch-gui/src/lib/libraries/extensions/index.jsx index b1163e466c..4b297678ed 100644 --- a/packages/scratch-gui/src/lib/libraries/extensions/index.jsx +++ b/packages/scratch-gui/src/lib/libraries/extensions/index.jsx @@ -46,6 +46,9 @@ import gdxforInsetIconURL from './gdxfor/gdxfor-small.svg'; import gdxforConnectionIconURL from './gdxfor/gdxfor-illustration.svg'; import gdxforConnectionSmallIconURL from './gdxfor/gdxfor-small.svg'; +import faceSensingIconURL from './faceSensing/faceSensing.png'; +import faceSensingInsetIconURL from './faceSensing/faceSensing-small.svg'; + export default [ { name: ( @@ -107,6 +110,26 @@ export default [ ), featured: true }, + { + name: ( + + ), + extensionId: 'faceSensing', + iconURL: faceSensingIconURL, + insetIconURL: faceSensingInsetIconURL, + description: ( + + ), + featured: true + }, { name: ( + + + + + + + + \ No newline at end of file diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/index.js b/packages/scratch-gui/src/lib/themes/high-contrast/index.js index 8a5dd94155..419229f72d 100644 --- a/packages/scratch-gui/src/lib/themes/high-contrast/index.js +++ b/packages/scratch-gui/src/lib/themes/high-contrast/index.js @@ -3,6 +3,7 @@ import penIcon from './extensions/penIcon.svg'; import text2speechIcon from './extensions/text2speechIcon.svg'; import translateIcon from './extensions/translateIcon.svg'; import videoSensingIcon from './extensions/videoSensingIcon.svg'; +import faceSensingIcon from './extensions/faceSensingIcon.svg'; const blockColors = { motion: { @@ -101,6 +102,9 @@ const extensions = { }, videoSensing: { blockIconURI: videoSensingIcon + }, + faceSensing: { + blockIconURI: faceSensingIcon } }; diff --git a/packages/scratch-gui/src/lib/vm-listener-hoc.jsx b/packages/scratch-gui/src/lib/vm-listener-hoc.jsx index ea8c89d694..d7e1090777 100644 --- a/packages/scratch-gui/src/lib/vm-listener-hoc.jsx +++ b/packages/scratch-gui/src/lib/vm-listener-hoc.jsx @@ -10,7 +10,7 @@ import {updateBlockDrag} from '../reducers/block-drag'; import {updateMonitors} from '../reducers/monitors'; import {setProjectChanged, setProjectUnchanged} from '../reducers/project-changed'; import {setRunningState, setTurboState, setStartedState} from '../reducers/vm-status'; -import {showExtensionAlert} from '../reducers/alerts'; +import {showExtensionAlert, showStandardAlert, closeAlertWithId} from '../reducers/alerts'; import {updateMicIndicator} from '../reducers/mic-indicator'; /* @@ -46,6 +46,7 @@ const vmListenerHOC = function (WrappedComponent) { this.props.vm.on('PROJECT_START', this.props.onGreenFlag); this.props.vm.on('PERIPHERAL_CONNECTION_LOST_ERROR', this.props.onShowExtensionAlert); this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate); + this.props.vm.on('EXTENSION_DATA_LOADING', this.props.onExtensionDataLoading); } componentDidMount () { @@ -125,6 +126,7 @@ const vmListenerHOC = function (WrappedComponent) { onKeyDown, onKeyUp, onMicListeningUpdate, + onExtensionDataLoading, onMonitorsUpdate, onTargetsUpdate, onProjectChanged, @@ -144,6 +146,7 @@ const vmListenerHOC = function (WrappedComponent) { VMListener.propTypes = { attachKeyboardEvents: PropTypes.bool, onBlockDragUpdate: PropTypes.func.isRequired, + onExtensionDataLoading: PropTypes.func.isRequired, onGreenFlag: PropTypes.func, onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, @@ -205,6 +208,13 @@ const vmListenerHOC = function (WrappedComponent) { }, onMicListeningUpdate: listening => { dispatch(updateMicIndicator(listening)); + }, + onExtensionDataLoading: loading => { + if (loading) { + dispatch(showStandardAlert('loadingExtensionData')); + } else { + dispatch(closeAlertWithId('loadingExtensionData')); + } } }); return connect( diff --git a/packages/scratch-gui/test/unit/util/cloud-manager-hoc.test.jsx b/packages/scratch-gui/test/unit/util/cloud-manager-hoc.test.jsx index cda31e29fb..4267f81f82 100644 --- a/packages/scratch-gui/test/unit/util/cloud-manager-hoc.test.jsx +++ b/packages/scratch-gui/test/unit/util/cloud-manager-hoc.test.jsx @@ -162,6 +162,25 @@ describe('CloudManagerHOC', () => { expect(CloudProvider).not.toHaveBeenCalled(); }); + test('when faceSensing extension is active, the cloud provider is not set on the vm', () => { + const Component = () =>
; + const WrappedComponent = cloudManagerHOC(Component); + vm.extensionManager.isExtensionLoaded = jest.fn(extension => extension === 'faceSensing'); + + mount( + + ); + + expect(vm.setCloudProvider.mock.calls.length).toBe(0); + expect(CloudProvider).not.toHaveBeenCalled(); + }); + test('if the isShowingWithId prop becomes true, it sets the cloud provider on the vm', () => { const Component = () =>
; const WrappedComponent = cloudManagerHOC(Component); diff --git a/packages/scratch-gui/webpack.config.js b/packages/scratch-gui/webpack.config.js index 07277d0329..d0310a96d7 100644 --- a/packages/scratch-gui/webpack.config.js +++ b/packages/scratch-gui/webpack.config.js @@ -84,6 +84,10 @@ const baseConfig = new ScratchWebpackConfigBuilder( context: '../../node_modules/scratch-storage/dist/web', from: 'chunks/fetch-worker.*.{js,js.map}', noErrorOnMissing: true + }, + { + from: '../../node_modules/@mediapipe/face_detection', + to: 'chunks/mediapipe/face_detection' } ] })); diff --git a/packages/scratch-render/src/RenderWebGL.js b/packages/scratch-render/src/RenderWebGL.js index c65e674706..6f7a4b2c9c 100644 --- a/packages/scratch-render/src/RenderWebGL.js +++ b/packages/scratch-render/src/RenderWebGL.js @@ -1069,6 +1069,36 @@ class RenderWebGL extends EventEmitter { return false; } + drawableTouchingScratchRect (drawableID, left, top, right, bottom) { + const drawable = this._allDrawables[drawableID]; + if (!drawable) { + return false; + } + const bounds = new Rectangle(); + bounds.initFromBounds(left, right, bottom, top); + const worldPos = twgl.v3.create(); + + drawable.updateCPURenderAttributes(); + + for (worldPos[1] = bounds.bottom; worldPos[1] <= bounds.top; worldPos[1]++) { + for (worldPos[0] = bounds.left; worldPos[0] <= bounds.right; worldPos[0]++) { + if (drawable.isTouching(worldPos)) { + return true; + } + } + } + return false; + } + + drawableTouchingScratchPoint (drawableID, x, y) { + const drawable = this._allDrawables[drawableID]; + if (!drawable) { + return false; + } + drawable.updateCPURenderAttributes(); + return drawable.isTouching([x, y]); + } + /** * Detect which sprite, if any, is at the given location. * This function will pick all drawables that are visible, unless specific diff --git a/packages/scratch-vm/src/engine/runtime.js b/packages/scratch-vm/src/engine/runtime.js index 452ff51c20..58d383d0c2 100644 --- a/packages/scratch-vm/src/engine/runtime.js +++ b/packages/scratch-vm/src/engine/runtime.js @@ -677,6 +677,10 @@ class Runtime extends EventEmitter { return 'MIC_LISTENING'; } + static get EXTENSION_DATA_LOADING () { + return 'EXTENSION_DATA_LOADING'; + } + /** * Event name for reporting that blocksInfo was updated. * @const {string} @@ -1569,6 +1573,10 @@ class Runtime extends EventEmitter { this.emit(Runtime.MIC_LISTENING, listening); } + emitExtensionLoading (loading) { + this.emit(Runtime.EXTENSION_DATA_LOADING, loading); + } + /** * Retrieve the function associated with the given opcode. * @param {!string} opcode The opcode to look up. diff --git a/packages/scratch-vm/src/extension-support/extension-manager.js b/packages/scratch-vm/src/extension-support/extension-manager.js index 94ce3b1a08..aa0e7bc0b0 100644 --- a/packages/scratch-vm/src/extension-support/extension-manager.js +++ b/packages/scratch-vm/src/extension-support/extension-manager.js @@ -23,7 +23,8 @@ const builtinExtensions = { ev3: () => require('../extensions/scratch3_ev3'), makeymakey: () => require('../extensions/scratch3_makeymakey'), boost: () => require('../extensions/scratch3_boost'), - gdxfor: () => require('../extensions/scratch3_gdx_for') + gdxfor: () => require('../extensions/scratch3_gdx_for'), + faceSensing: () => require('../extensions/scratch3_face_sensing') }; /** diff --git a/packages/scratch-vm/src/extensions/scratch3_face_sensing/index.js b/packages/scratch-vm/src/extensions/scratch3_face_sensing/index.js new file mode 100644 index 0000000000..8a84ec0a8a --- /dev/null +++ b/packages/scratch-vm/src/extensions/scratch3_face_sensing/index.js @@ -0,0 +1,583 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Clone = require('../../util/clone'); +const MathUtil = require('../../util/math-util'); +const formatMessage = require('format-message'); +const Video = require('../../io/video'); +const TargetType = require('../../extension-support/target-type'); +// const Posenet = require('@tensorflow-models/posenet'); + +const FaceDetection = require('@tensorflow-models/face-detection'); +const mediapipePackage = require('@mediapipe/face_detection/package.json'); + +/** + * 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,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48Y2lyY2xlIGZpbGw9IiM0Qzk3RkYiIGN4PSIxNS41IiBjeT0iMTcuNSIgcj0iMS41Ii8+PGNpcmNsZSBmaWxsPSIjNEM5N0ZGIiBjeD0iMjQuNSIgY3k9IjE3LjUiIHI9IjEuNSIvPjxwYXRoIGQ9Ik0yMCA5QzEzLjkyNSA5IDkgMTMuOTI1IDkgMjBzNC45MjUgMTEgMTEgMTEgMTEtNC45MjUgMTEtMTFTMjYuMDc1IDkgMjAgOXptMCAyYTkgOSAwIDExMCAxOCA5IDkgMCAwMTAtMTh6IiBmaWxsPSIjNEM5N0ZGIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNMzUgNGExIDEgMCAwMS45OTMuODgzTDM2IDV2NmExIDEgMCAwMS0xLjk5My4xMTdMMzQgMTFWNmgtNWExIDEgMCAwMS0uOTkzLS44ODNMMjggNWExIDEgMCAwMS44ODMtLjk5M0wyOSA0aDZ6TTUgMzZhMSAxIDAgMDEtLjk5My0uODgzTDQgMzV2LTZhMSAxIDAgMDExLjk5My0uMTE3TDYgMjl2NWg1YTEgMSAwIDAxLjk5My44ODNMMTIgMzVhMSAxIDAgMDEtLjg4My45OTNMMTEgMzZINXoiIGZpbGwtb3BhY2l0eT0iLjUiIGZpbGw9IiM0RDk3RkYiIGZpbGwtcnVsZT0ibm9uemVybyIvPjxwYXRoIGQ9Ik0yMi4xNjggMjEuOTQ1YTEgMSAwIDExMS42NjQgMS4xMUMyMi45NzQgMjQuMzQyIDIxLjY1OCAyNSAyMCAyNXMtMi45NzQtLjY1OC0zLjgzMi0xLjk0NWExIDEgMCAxMTEuNjY0LTEuMTFDMTguMzA3IDIyLjY1OCAxOC45OTIgMjMgMjAgMjNjMS4wMDkgMCAxLjY5My0uMzQyIDIuMTY4LTEuMDU1eiIgZmlsbD0iIzRDOTdGRiIgZmlsbC1ydWxlPSJub256ZXJvIi8+PHBhdGggZD0iTTI5LjcyIDI0LjAyOGEyLjU1NyAyLjU1NyAwIDAwMS44MDgtMS44MDhsLjU0NC0yLjAwOWMuMjUyLS45NDggMS42LS45NDggMS44NTYgMGwuNTQgMi4wMDlhMi41NjMgMi41NjMgMCAwMDEuODEzIDEuODA4bDIuMDA4LjU0NGMuOTQ4LjI1Mi45NDggMS42IDAgMS44NTdsLTIuMDA4LjU0YTIuNTYzIDIuNTYzIDAgMDAtMS44MTMgMS44MDhsLS41NCAyLjAwOWMtLjI1Ni45NTItMS42MDQuOTUyLTEuODU2IDBsLS41NDQtMi4wMDlhMi41NTcgMi41NTcgMCAwMC0xLjgwOS0xLjgwOGwtMi4wMDgtLjU0Yy0uOTQ4LS4yNTYtLjk0OC0xLjYwNSAwLTEuODU3bDIuMDA4LS41NDR6TTUuMDQgNi4zOTZBMS45MTggMS45MTggMCAwMDYuMzk2IDUuMDRsLjQwOC0xLjUwN2MuMTg5LS43MSAxLjItLjcxIDEuMzkyIDBsLjQwNSAxLjUwN2ExLjkyMiAxLjkyMiAwIDAwMS4zNiAxLjM1NmwxLjUwNi40MDhjLjcxLjE5LjcxIDEuMiAwIDEuMzkzbC0xLjUwNy40MDVhMS45MjIgMS45MjIgMCAwMC0xLjM1OSAxLjM1NmwtLjQwNSAxLjUwNmMtLjE5Mi43MTUtMS4yMDMuNzE1LTEuMzkyIDBsLS40MDgtMS41MDZBMS45MTggMS45MTggMCAwMDUuMDQgOC42MDJsLTEuNTA3LS40MDVjLS43MS0uMTkyLS43MS0xLjIwNCAwLTEuMzkzbDEuNTA3LS40MDh6IiBmaWxsPSIjRkZCRjAwIi8+PHBhdGggZD0iTTMxLjU4OSAyMC4wODNsLS41NDQgMi4wMDZhMi4wNTggMi4wNTggMCAwMS0xLjQ1NyAxLjQ1N2wtMi4wMDguNTQ0Yy0xLjQ0LjM4My0xLjQ0IDIuNDMyIDAgMi44MjFsMi4wMS41NGMuNzEuMTkgMS4yNjQuNzQ2IDEuNDU1IDEuNDU2bC41NDQgMi4wMWMuMzgzIDEuNDQ1IDIuNDMzIDEuNDQ1IDIuODIyLS4wMDFsLjU0LTIuMDA5YTIuMDYzIDIuMDYzIDAgMDExLjQ1OS0xLjQ1NWwyLjAwOS0uNTRjMS40NDItLjM5IDEuNDQyLTIuNDQtLjAwMi0yLjgyM2wtMi4wMDYtLjU0M2EyLjA2MiAyLjA2MiAwIDAxLTEuNDYtMS40NTVsLS41NC0yLjAxYy0uMzktMS40NDItMi40MzktMS40NDItMi44MjIuMDAyem0xLjg1Ni4yNTlsLjU0IDIuMDA4YTMuMDYyIDMuMDYyIDAgMDAyLjE2NSAyLjE2bDIuMDA4LjU0NWMuNDU2LjEyLjQ1Ni43NjggMCAuODkxbC0yLjAwNy41NGEzLjA2MiAzLjA2MiAwIDAwLTIuMTY2IDIuMTYybC0uNTQgMi4wMDhjLS4xMjMuNDU4LS43NjkuNDU4LS44OS4wMDJsLS41NDUtMi4wMTFhMy4wNTcgMy4wNTcgMCAwMC0yLjE2Mi0yLjE2MWwtMi4wMDctLjU0Yy0uNDU1LS4xMjMtLjQ1NS0uNzctLjAwMS0uODlsMi4wMS0uNTQ1YTMuMDU3IDMuMDU3IDAgMDAyLjE2LTIuMTYybC41NDQtMi4wMDdjLjEyMi0uNDU2Ljc2OS0uNDU2Ljg5MSAweiIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PHBhdGggZD0iTTYuMzIgMy40MDVsLS40MDcgMS41MDRjLS4xMy40OS0uNTExLjg3LTEuMDA0IDEuMDA1bC0xLjUwNi40MDhjLTEuMjA0LjMyLTEuMjA0IDIuMDMyIDAgMi4zNTdsMS41MDcuNDA1Yy40OS4xMzEuODcyLjUxNCAxLjAwMyAxLjAwM2wuNDA4IDEuNTA4Yy4zMiAxLjIwNyAyLjAzMyAxLjIwNyAyLjM1OCAwbC40MDUtMS41MDdhMS40MjIgMS40MjIgMCAwMTEuMDA1LTEuMDAzbDEuNTA4LS40MDZjMS4yMDQtLjMyNSAxLjIwNC0yLjAzNy0uMDAyLTIuMzU4bC0xLjUwNC0uNDA4YTEuNDIyIDEuNDIyIDAgMDEtMS4wMDctMS4wMDJMOC42OCAzLjQwM2MtLjMyNS0xLjIwNC0yLjAzOC0xLjIwNC0yLjM1OC4wMDJ6bTEuMzkzLjI1OWwuNDA1IDEuNTA2QTIuNDIxIDIuNDIxIDAgMDA5LjgzIDYuODc5bDEuNTA3LjQwOGMuMjE4LjA1OC4yMTguMzY4IDAgLjQyN2wtMS41MDUuNDA1YTIuNDIyIDIuNDIyIDAgMDAtMS43MTMgMS43MWwtLjQwNSAxLjUwNmMtLjA1OS4yMi0uMzY4LjIyLS40MjYuMDAxbC0uNDA5LTEuNTA5YTIuNDE3IDIuNDE3IDAgMDAtMS43MS0xLjcwOGwtMS41MDUtLjQwNWMtLjIxNy0uMDU5LS4yMTctLjM3LS4wMDEtLjQyN0w1LjE3IDYuODhhMi40MTggMi40MTggMCAwMDEuNzA5LTEuNzFsLjQwNy0xLjUwNWMuMDU5LS4yMTguMzY5LS4yMTguNDI3IDB6IiBmaWxsLW9wYWNpdHk9Ii40IiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L2c+PC9zdmc+'; + +/** + * 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,PHN2ZyBoZWlnaHQ9IjQwIiB3aWR0aD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDIzLjg0IDIxLjQ2Ij4KICAgIDxjaXJjbGUgZmlsbD0iI2ZmZiIgY3g9IjguMzUiIGN5PSI5LjY1IiByPSIuOTciLz4KICAgIDxjaXJjbGUgZmlsbD0iI2ZmZiIgY3g9IjE0LjE5IiBjeT0iOS42NSIgcj0iLjk3Ii8+CiAgICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTEuMjcsNC4xNGMtMy45NCwwLTcuMTMsMy4xOS03LjEzLDcuMTNzMy4xOSw3LjEzLDcuMTMsNy4xMyw3LjEzLTMuMTksNy4xMy03LjEzLTMuMTktNy4xMy03LjEzLTcuMTNaTTExLjI3LDUuNDRjMy4yMiwwLDUuODQsMi42MSw1Ljg0LDUuODRzLTIuNjEsNS44NC01Ljg0LDUuODQtNS44NC0yLjYxLTUuODQtNS44NCwyLjYxLTUuODQsNS44NC01Ljg0WiIvPgogICAgPHBhdGggZmlsbD0iI2ZmYmYwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwYjhlNjkiIHN0cm9rZS1taXRlcmxpbWl0PSIyIiBzdHJva2Utd2lkdGg9Ii41cHgiIGQ9Ik0xNy41NywxMy44OGMuNTctLjE1LDEuMDItLjYsMS4xNy0xLjE3bC4zNS0xLjNjLjE2LS42MSwxLjA0LS42MSwxLjIsMGwuMzUsMS4zYy4xNS41Ny42LDEuMDIsMS4xOCwxLjE3bDEuMy4zNWMuNjEuMTYuNjEsMS4wNCwwLDEuMmwtMS4zLjM1Yy0uNTcuMTUtMS4wMi42LTEuMTgsMS4xN2wtLjM1LDEuM2MtLjE3LjYyLTEuMDQuNjItMS4yLDBsLS4zNS0xLjNjLS4xNS0uNTctLjYtMS4wMi0xLjE3LTEuMTdsLTEuMy0uMzVjLS42MS0uMTctLjYxLTEuMDQsMC0xLjJsMS4zLS4zNWgwWk0xLjU3LDIuNDVjLjQzLS4xMi43Ni0uNDUuODgtLjg4bC4yNi0uOThjLjEyLS40Ni43OC0uNDYuOSwwbC4yNi45OGMuMTIuNDMuNDUuNzYuODguODhsLjk4LjI2Yy40Ni4xMi40Ni43OCwwLC45bC0uOTguMjZjLS40My4xMS0uNzcuNDUtLjg4Ljg4bC0uMjYuOThjLS4xMi40Ni0uNzguNDYtLjksMGwtLjI2LS45OGMtLjEyLS40My0uNDUtLjc2LS44OC0uODhsLS45OC0uMjZjLS40Ni0uMTItLjQ2LS43OCwwLS45bC45OC0uMjZaIi8+CiAgICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIuNjgsMTIuNTNjLjItLjMuNi0uMzguOS0uMTguMy4yLjM4LjYuMTguOS0uNTYuODMtMS40MSwxLjI2LTIuNDgsMS4yNnMtMS45My0uNDMtMi40OC0xLjI2Yy0uMi0uMy0uMTItLjcuMTgtLjkuMy0uMi43LS4xMi45LjE4LjMxLjQ2Ljc1LjY4LDEuNDEuNjhzMS4xLS4yMiwxLjQxLS42OFoiLz4KICAgIDxwYXRoIGZpbGw9IiMwYjhlNjkiIGQ9Ik0yMC44OSw2LjA2Yy0uMzEsMC0uNTctLjI1LS41Ny0uNTd2LTMuMjloLTMuMzFjLS4zMSwwLS41Ny0uMjUtLjU3LS41N3MuMjUtLjU3LjU3LS41N2gzLjg4Yy4zMSwwLC41Ny4yNS41Ny41N3YzLjg2YzAsLjMxLS4yNS41Ny0uNTcuNTdaIi8+CiAgICA8cGF0aCBmaWxsPSIjMGI4ZTY5IiBkPSJNNS40NCwyMS40NkgxLjU5Yy0uMzEsMC0uNTctLjI1LS41Ny0uNTd2LTMuODJjMC0uMzEuMjUtLjU3LjU3LS41N3MuNTcuMjUuNTcuNTd2My4yNWgzLjI4Yy4zMSwwLC41Ny4yNS41Ny41N3MtLjI1LjU3LS41Ny41N1oiLz4KPC9zdmc+Cg=='; + +/** + * Class for the motion-related blocks in Scratch 3.0 + * @param {Runtime} runtime - the runtime instantiating this block package. + * @constructor + */ +class Scratch3FaceSensingBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.runtime.emit('EXTENSION_DATA_LOADING', true); + + const model = FaceDetection.SupportedModels.MediaPipeFaceDetector; + const detectorConfig = { + runtime: 'mediapipe', + solutionPath: '/chunks/mediapipe/face_detection', + maxFaces: 1 + }; + + FaceDetection.createDetector(model, detectorConfig) + .catch(() => { + const fallbackConfig = { + runtime: 'mediapipe', + solutionPath: `https://cdn.jsdelivr.net/npm/@mediapipe/face_detection@${mediapipePackage.version}`, + maxFaces: 1 + }; + + return FaceDetection.createDetector(model, fallbackConfig); + }) + .then(detector => { + this.faceDetector = detector; + if (this.runtime.ioDevices) { + this._loop(); + } + }); + + this.cachedSize = 100; + this.cachedTilt = 90; + + // Array of recent boolean values for whether or not a face was detected + this.isDetectedArrayLength = 5; + this.isDetectedArray = new Array(this.isDetectedArrayLength); + this.isDetectedArray.fill(false, 0, this.isDetectedArrayLength); + // Smoothed value for whether or not a face was detected + this.smoothedIsDetected = false; + + this._clearAttachments = this._clearAttachments.bind(this); + this.runtime.on('PROJECT_STOP_ALL', this._clearAttachments); + } + + /** + * After analyzing a frame the amount of milliseconds until another frame + * is analyzed. + * @type {number} + */ + static get INTERVAL () { + return 1000 / 15; + } + + /** + * Dimensions the video stream is analyzed at after its rendered to the + * sample canvas. + * @type {Array.} + */ + static get DIMENSIONS () { + return [480, 360]; + } + + /** + * 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 () { + + } + + /** + * 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, Scratch3FaceSensingBlocks.INTERVAL)); + + const frame = this.runtime.ioDevices.video.getFrame({ + format: Video.FORMAT_IMAGE_DATA, + dimensions: Scratch3FaceSensingBlocks.DIMENSIONS, + cacheTimeout: this.runtime.currentStepTime + }); + if (frame) { + this.faceDetector.estimateFaces(frame).then(faces => { + if (faces && faces.length > 0) { + if (!this.firstTime) { + this.firstTime = true; + this.runtime.emit('EXTENSION_DATA_LOADING', false); + } + this.currentFace = faces[0]; + } else { + this.currentFace = null; + } + this.updateIsDetected(); + }); + } + } + + updateIsDetected () { + this.isDetectedArray.push(!!this.currentFace); + if (this.isDetectedArray.length > this.isDetectedArrayLength) { + this.isDetectedArray.shift(); + } + // if every recent detection is false, set to false + if (this.isDetectedArray.every(item => item === false)) { + this.smoothedIsDetected = false; + } + // if every recent detection is true, set to true + if (this.isDetectedArray.every(item => item === true)) { + this.smoothedIsDetected = true; + } + + // if there's a mix of true and false values, do not change the result + } + + _getFaceSensingState (target) { + let faceSensingState = target.getCustomState(Scratch3FaceSensingBlocks.STATE_KEY); + if (!faceSensingState) { + faceSensingState = Clone.simple(Scratch3FaceSensingBlocks.DEFAULT_FACE_SENSING_STATE); + target.setCustomState(Scratch3FaceSensingBlocks.STATE_KEY, faceSensingState); + } + return faceSensingState; + } + + static get STATE_KEY () { + return 'Scratch.faceSensing'; + } + + static get DEFAULT_FACE_SENSING_STATE () { + return { + attachedToPartNumber: null, + prevX: 0, + offsetX: 0, + prevY: 0, + offsetY: 0, + prevSize: 100, + offsetSize: 0, + prevDirection: 0, + offsetDirection: 0 + }; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + // Enable the video layer + this.runtime.ioDevices.video.enableVideo(); + + // Return extension definition + return { + id: 'faceSensing', + name: formatMessage({ + id: 'faceSensing.categoryName', + default: 'Face Sensing', + description: 'Name of face sensing extension' + }), + blockIconURI: blockIconURI, + menuIconURI: menuIconURI, + blocks: [ + { + opcode: 'goToPart', + text: formatMessage({ + id: 'faceSensing.goToPart', + default: 'go to [PART]', + description: '' + }), + blockType: BlockType.COMMAND, + arguments: { + PART: { + type: ArgumentType.STRING, + menu: 'PART', + defaultValue: '2' + } + }, + filter: [TargetType.SPRITE] + }, + { + opcode: 'pointInFaceTiltDirection', + text: formatMessage({ + id: 'faceSensing.pointInFaceTiltDirection', + default: 'point in direction of face tilt', + description: '' + }), + blockType: BlockType.COMMAND, + filter: [TargetType.SPRITE] + }, + { + opcode: 'setSizeToFaceSize', + text: formatMessage({ + id: 'faceSensing.setSizeToFaceSize', + default: 'set size to face size', + description: '' + }), + blockType: BlockType.COMMAND, + filter: [TargetType.SPRITE] + }, + '---', + { + opcode: 'whenTilted', + text: formatMessage({ + id: 'faceSensing.whenTilted', + default: 'when face tilts [DIRECTION]', + description: '' + }), + blockType: BlockType.HAT, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'TILT', + defaultValue: 'left' + } + } + }, + { + opcode: 'whenSpriteTouchesPart', + text: formatMessage({ + id: 'faceSensing.whenSpriteTouchesPart', + default: 'when this sprite touches a[PART]', + description: '' + }), + arguments: { + PART: { + type: ArgumentType.STRING, + menu: 'PART', + defaultValue: '2' + } + }, + blockType: BlockType.HAT, + filter: [TargetType.SPRITE] + }, + { + opcode: 'whenFaceDetected', + text: formatMessage({ + id: 'faceSensing.whenFaceDetected', + default: 'when a face is detected', + description: '' + }), + blockType: BlockType.HAT + }, + '---', + { + opcode: 'faceIsDetected', + text: formatMessage({ + id: 'faceSensing.faceDetected', + default: 'a face is detected?', + description: '' + }), + blockType: BlockType.BOOLEAN + }, + // { + // opcode: 'attachToPart', + // text: formatMessage({ + // id: 'faceSensing.attachToPart', + // default: 'attach to [PART]', + // description: '' + // }), + // blockType: BlockType.COMMAND, + // arguments: { + // PART: { + // type: ArgumentType.STRING, + // menu: 'PART', + // defaultValue: '2' + // } + // } + // }, + { + opcode: 'faceTilt', + text: formatMessage({ + id: 'faceSensing.faceTilt', + default: 'face tilt', + description: '' + }), + blockType: BlockType.REPORTER + }, + // { + // opcode: 'partX', + // text: formatMessage({ + // id: 'faceSensing.partX', + // default: 'x position of [PART]', + // description: '' + // }), + // arguments: { + // PART: { + // type: ArgumentType.NUMBER, + // menu: 'PART', + // defaultValue: '2' + // } + // }, + // blockType: BlockType.REPORTER + // }, + // { + // opcode: 'partY', + // text: formatMessage({ + // id: 'faceSensing.partY', + // default: 'y position of [PART]', + // description: '' + // }), + // arguments: { + // PART: { + // type: ArgumentType.NUMBER, + // menu: 'PART', + // defaultValue: '2' + // } + // }, + // blockType: BlockType.REPORTER + // }, + { + opcode: 'faceSize', + text: formatMessage({ + id: 'faceSensing.faceSize', + default: 'face size', + description: '' + }), + blockType: BlockType.REPORTER + } + // { + // opcode: 'probability', + // text: formatMessage({ + // id: 'faceSensing.probability', + // default: 'probability of face detection', + // description: '' + // }), + // blockType: BlockType.REPORTER + // }, + // { + // opcode: 'numberOfFaces', + // text: formatMessage({ + // id: 'faceSensing.numberOfFaces', + // default: 'number of faces', + // description: '' + // }), + // blockType: BlockType.REPORTER + // } + ], + menus: { + PART: [ + {text: 'nose', value: '2'}, + {text: 'mouth', value: '3'}, + {text: 'left eye', value: '0'}, + {text: 'right eye', value: '1'}, + {text: 'between eyes', value: '6'}, + {text: 'left ear', value: '4'}, + {text: 'right ear', value: '5'}, + {text: 'top of head', value: '7'} + ], + TILT: [ + {text: 'left', value: 'left'}, + {text: 'right', value: 'right'} + ] + } + }; + } + + getBetweenEyesPosition () { + // center point of a line between the eyes + const leftEye = this.getPartPosition(0); + const rightEye = this.getPartPosition(1); + const betweenEyes = {x: 0, y: 0}; + betweenEyes.x = leftEye.x + ((rightEye.x - leftEye.x) / 2); + betweenEyes.y = leftEye.y + ((rightEye.y - leftEye.y) / 2); + return betweenEyes; + } + + getTopOfHeadPosition () { + // Estimated top of the head point: + // Make a line perpendicular to the line between the eyes, through + // its center, and move upward along it the distance from the point + // between the eyes to the mouth. + const leftEyePos = this.getPartPosition(0); + const rightEyePos = this.getPartPosition(1); + const mouthPos = this.getPartPosition(3); + const dx = rightEyePos.x - leftEyePos.x; + const dy = rightEyePos.y - leftEyePos.y; + const directionRads = Math.atan2(dy, dx) + (Math.PI / 2); + const betweenEyesPos = this.getBetweenEyesPosition(); + const mouthDistance = this.distance(betweenEyesPos, mouthPos); + + const topOfHeadPosition = {x: 0, y: 0}; + topOfHeadPosition.x = betweenEyesPos.x + (mouthDistance * Math.cos(directionRads)); + topOfHeadPosition.y = betweenEyesPos.y + (mouthDistance * Math.sin(directionRads)); + + return topOfHeadPosition; + } + + distance (pointA, pointB) { + const dx = pointA.x - pointB.x; + const dy = pointA.y - pointB.y; + return Math.sqrt((dx * dx) + (dy * dy)); + } + + whenSpriteTouchesPart (args, util) { + if (!this.currentFace) return false; + if (!this.currentFace.keypoints) return false; + const pos = this.getPartPosition(args.PART); + return util.target.isTouchingScratchPoint(pos.x, pos.y); + } + + whenFaceDetected () { + return this.smoothedIsDetected; + } + + faceIsDetected () { + return this.smoothedIsDetected; + } + + numberOfFaces () { + return this.allFaces.length; + } + + probability () { + if (this.currentFace) { + return Math.round(this.currentFace.probability * 100); + } + return 0; + } + + faceSize () { + if (!this.currentFace) return this.cachedSize; + const size = Math.round(this.currentFace.box.height); + this.cachedSize = size; + return size; + } + + getPartPosition (part) { + const defaultPos = {x: 0, y: 0}; + if (!this.currentFace) return defaultPos; + if (!this.currentFace.keypoints) return defaultPos; + if (Number(part) === 6) { + return this.getBetweenEyesPosition(); + } + if (Number(part) === 7) { + return this.getTopOfHeadPosition(); + } + const result = this.currentFace.keypoints[Number(part)]; + if (result) { + const res = this.toScratchCoords(result); + return res; + } + return defaultPos; + } + + toScratchCoords (position) { + return { + x: position.x - 240, + y: 180 - position.y + }; + } + + partX (args) { + return this.getPartPosition(args.PART).x; + } + + partY (args) { + return this.getPartPosition(args.PART).y; + } + + whenTilted (args) { + const TILT_THRESHOLD = 10; + if (args.DIRECTION === 'left') { + return this.faceTilt() < (90 - TILT_THRESHOLD); + } + if (args.DIRECTION === 'right') { + return this.faceTilt() > (90 + TILT_THRESHOLD); + } + return false; + } + + goToPart (args, util) { + if (!this.currentFace) return; + const pos = this.getPartPosition(args.PART); + util.target.setXY(pos.x, pos.y); + } + + pointInFaceTiltDirection (args, util) { + if (!this.currentFace) return; + util.target.setDirection(this.faceTilt()); + } + + setSizeToFaceSize (args, util) { + if (!this.currentFace) return; + util.target.setSize(this.faceSize()); + } + + attachToPart (args, util) { + const state = this._getFaceSensingState(util.target); + state.attachedToPartNumber = args.PART; + state.offsetX = 0; + state.offsetY = 0; + state.prevX = util.target.x; + state.prevY = util.target.y; + state.offsetDirection = 0; + state.prevDirection = util.target.direction; + state.offsetSize = 0; + state.prevSize = util.target.size; + } + + updateAttachments () { + this.runtime.targets.forEach(target => { + const state = this._getFaceSensingState(target); + if (state.attachedToPartNumber) { + const partPos = this.getPartPosition(state.attachedToPartNumber); + if (target.x !== state.prevX) { + state.offsetX += target.x - state.prevX; + } + if (target.y !== state.prevY) { + state.offsetY += target.y - state.prevY; + } + if (target.direction !== state.prevDirection) { + state.offsetDirection += target.direction - state.prevDirection; + } + if (target.size !== state.prevSize) { + state.offsetSize += target.size - state.prevSize; + } + target.setXY(partPos.x + state.offsetX, partPos.y + state.offsetY); + target.setDirection(this.faceTilt() + state.offsetDirection); + target.setSize(this.faceSize() + state.offsetSize); + state.prevX = target.x; + state.prevY = target.y; + state.prevDirection = target.direction; + state.prevSize = target.size; + } + }); + } + + _clearAttachments () { + this.runtime.targets.forEach(target => { + const state = this._getFaceSensingState(target); + state.attachedToPartNumber = null; + }); + } + + faceTilt () { + if (!this.currentFace) return this.cachedTilt; + const leftEyePos = this.getPartPosition(0); + const rightEyePos = this.getPartPosition(1); + const dx = rightEyePos.x - leftEyePos.x; + const dy = rightEyePos.y - leftEyePos.y; + const direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx)); + const tilt = Math.round(direction); + this.cachedTilt = tilt; + return tilt; + } +} + +module.exports = Scratch3FaceSensingBlocks; diff --git a/packages/scratch-vm/src/sprites/rendered-target.js b/packages/scratch-vm/src/sprites/rendered-target.js index e4a12e244d..100a031bcf 100644 --- a/packages/scratch-vm/src/sprites/rendered-target.js +++ b/packages/scratch-vm/src/sprites/rendered-target.js @@ -765,6 +765,20 @@ class RenderedTarget extends Target { return false; } + isTouchingRect (left, top, right, bottom) { + if (this.renderer) { + return this.renderer.drawableTouchingScratchRect(this.drawableID, left, top, right, bottom); + } + return false; + } + + isTouchingScratchPoint (x, y) { + if (this.renderer) { + return this.renderer.drawableTouchingScratchPoint(this.drawableID, x, y); + } + return false; + } + /** * Return whether touching a stage edge. * @return {boolean} True iff the rendered target is touching the stage edge. diff --git a/packages/scratch-vm/src/virtual-machine.js b/packages/scratch-vm/src/virtual-machine.js index 64ad7934fe..0ab6c14c9c 100644 --- a/packages/scratch-vm/src/virtual-machine.js +++ b/packages/scratch-vm/src/virtual-machine.js @@ -147,6 +147,9 @@ class VirtualMachine extends EventEmitter { this.runtime.on(Runtime.MIC_LISTENING, listening => { this.emit(Runtime.MIC_LISTENING, listening); }); + this.runtime.on(Runtime.EXTENSION_DATA_LOADING, loading => { + this.emit(Runtime.EXTENSION_DATA_LOADING, loading); + }); this.runtime.on(Runtime.RUNTIME_STARTED, () => { this.emit(Runtime.RUNTIME_STARTED); });