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