diff --git a/Jenkinsfile b/Jenkinsfile index 6bc1e0d9b7d4b644e5e4d2b643d2a644408983de..ca13a7681168060b9a2b5bdc199177cb518dbd70 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -49,7 +49,7 @@ pipeline { "<https://github.com/mozilla/hubs/commit/$gitSha|$gitSha> " + "Hubs: ```${gitSha} ${gitMessage}```\n" + "<${smokeURL}?required_version=${env.BUILD_NUMBER}|Smoke Test> - to push:\n" + - "`/mr hubs deploy ${targetS3Url}`" + "`/mr hubs deploy ${env.BUILD_NUMBER} ${targetS3Url}`" ) def payload = 'payload=' + JsonOutput.toJson([ text : text, diff --git a/README.md b/README.md index 503057f930fe9fc7cfe37c01a18990860274f5a7..4e96bc332b06bf6b6ae5f502dd36fb9b893f00c7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ This will allow the CSP checks to pass that are served up by Reticulum so you ca - `disable_telemetry` - If `true` disables Sentry telemetry. - `log_filter` - A `debug` style filter for setting the logging level. - `debug` - If `true` performs verbose logging of Janus and NAF traffic. +- `disableTunnel` - Tunnel vision is on by default. Disable the tunnel vision by this parameter. ## Additional Resources @@ -72,3 +73,4 @@ This will allow the CSP checks to pass that are served up by Reticulum so you ca * [Hubs-Ops](https://github.com/mozilla/hubs-ops) - Infrastructure as code + management tools for running necessary backend services on AWS. [](http://waffle.io/mozilla/socialmr) + diff --git a/package-lock.json b/package-lock.json index 17b5b9ac2b9e54c87e47c0e8fb73460a7ef4c06c..89f73fe4c6e6fbecf2d06e51f2c27b72284ae991 100644 --- a/package-lock.json +++ b/package-lock.json @@ -888,7 +888,7 @@ }, "assert-plus": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, @@ -917,13 +917,13 @@ }, "async-foreach": { "version": "0.1.3", - "resolved": "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", "dev": true }, "asynckit": { "version": "0.4.0", - "resolved": "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, @@ -979,7 +979,7 @@ }, "aws-sign2": { "version": "0.7.0", - "resolved": "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", "dev": true }, @@ -2121,7 +2121,7 @@ }, "block-stream": { "version": "0.0.9", - "resolved": "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "dev": true, "requires": { @@ -2556,7 +2556,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -2586,7 +2586,7 @@ }, "caseless": { "version": "0.12.0", - "resolved": "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, @@ -2954,7 +2954,7 @@ }, "co": { "version": "4.6.0", - "resolved": "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, @@ -3002,7 +3002,7 @@ }, "combined-stream": { "version": "1.0.6", - "resolved": "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "dev": true, "requires": { @@ -3176,7 +3176,7 @@ }, "console-control-strings": { "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, @@ -3521,7 +3521,7 @@ }, "dashdash": { "version": "1.14.1", - "resolved": "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "dev": true, "requires": { @@ -3763,7 +3763,7 @@ }, "delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, @@ -3774,7 +3774,7 @@ }, "delegates": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, @@ -4871,7 +4871,7 @@ }, "extsprintf": { "version": "1.3.0", - "resolved": "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, @@ -5300,13 +5300,13 @@ }, "forever-agent": { "version": "0.6.1", - "resolved": "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", "dev": true }, "form-data": { "version": "2.3.2", - "resolved": "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "dev": true, "requires": { @@ -5837,7 +5837,7 @@ }, "fstream": { "version": "1.0.11", - "resolved": "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "dev": true, "requires": { @@ -5861,7 +5861,7 @@ }, "gauge": { "version": "2.7.4", - "resolved": "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "requires": { @@ -5877,7 +5877,7 @@ "dependencies": { "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "requires": { @@ -5886,7 +5886,7 @@ }, "string-width": { "version": "1.0.2", - "resolved": "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { @@ -5914,7 +5914,7 @@ }, "get-stdin": { "version": "4.0.1", - "resolved": "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", "dev": true }, @@ -5932,7 +5932,7 @@ }, "getpass": { "version": "0.1.7", - "resolved": "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "dev": true, "requires": { @@ -6258,13 +6258,13 @@ }, "har-schema": { "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", "dev": true }, "har-validator": { "version": "5.0.3", - "resolved": "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "dev": true, "requires": { @@ -6274,7 +6274,7 @@ "dependencies": { "ajv": { "version": "5.5.2", - "resolved": "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "dev": true, "requires": { @@ -6286,13 +6286,13 @@ }, "fast-deep-equal": { "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true }, "json-schema-traverse": { "version": "0.3.1", - "resolved": "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true } @@ -6364,7 +6364,7 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true }, @@ -6695,7 +6695,7 @@ }, "http-signature": { "version": "1.2.0", - "resolved": "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "dev": true, "requires": { @@ -6785,7 +6785,7 @@ }, "in-publish": { "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", "dev": true }, @@ -7317,7 +7317,7 @@ }, "is-typedarray": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, @@ -7383,7 +7383,7 @@ }, "isstream": { "version": "0.1.2", - "resolved": "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, @@ -7598,7 +7598,7 @@ }, "json-schema": { "version": "0.2.3", - "resolved": "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "dev": true }, @@ -7616,7 +7616,7 @@ }, "json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, @@ -7638,7 +7638,7 @@ }, "jsprim": { "version": "1.4.1", - "resolved": "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "dev": true, "requires": { @@ -8136,7 +8136,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -8149,7 +8149,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -8198,7 +8198,7 @@ }, "lodash.assign": { "version": "4.2.0", - "resolved": "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", "dev": true }, @@ -8210,7 +8210,7 @@ }, "lodash.clonedeep": { "version": "4.5.0", - "resolved": "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, @@ -8543,7 +8543,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -8859,9 +8859,9 @@ "dev": true }, "naf-janus-adapter": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/naf-janus-adapter/-/naf-janus-adapter-0.11.0.tgz", - "integrity": "sha512-jLwcs4TRj7Dur0bF9RjP9UyjIkdKpwNZ3q/zCXT+ytrHGRXWt4rX+9acdpg2QVEmPMumN+rr9NUPliNWpRxD3w==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/naf-janus-adapter/-/naf-janus-adapter-0.12.0.tgz", + "integrity": "sha512-Zt2QC/7o2haXgaqerby+DCaDYth8CDtsbMBZettsQg4wufDKbK41xhej/KM9Wwgkfopu6YiK6CK4G1kcOgL/RA==", "requires": { "debug": "^3.1.0", "minijanus": "0.6.2", @@ -9043,7 +9043,7 @@ "dependencies": { "semver": { "version": "5.3.0", - "resolved": "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "dev": true } @@ -9129,13 +9129,13 @@ "dependencies": { "ansi-styles": { "version": "2.2.1", - "resolved": "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -9148,7 +9148,7 @@ }, "cross-spawn": { "version": "3.0.1", - "resolved": "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", "dev": true, "requires": { @@ -9158,7 +9158,7 @@ }, "supports-color": { "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true } @@ -9207,7 +9207,7 @@ }, "nopt": { "version": "3.0.6", - "resolved": "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { @@ -9306,7 +9306,7 @@ }, "oauth-sign": { "version": "0.8.2", - "resolved": "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", "dev": true }, @@ -9723,7 +9723,7 @@ }, "parse-json": { "version": "2.2.0", - "resolved": "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { @@ -9790,7 +9790,7 @@ }, "path-exists": { "version": "2.1.0", - "resolved": "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, "requires": { @@ -9850,7 +9850,7 @@ }, "performance-now": { "version": "2.1.0", - "resolved": "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, @@ -10587,7 +10587,7 @@ }, "read-pkg": { "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { @@ -10598,7 +10598,7 @@ "dependencies": { "path-type": { "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { @@ -10609,7 +10609,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -10617,7 +10617,7 @@ }, "read-pkg-up": { "version": "1.0.1", - "resolved": "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, "requires": { @@ -10627,7 +10627,7 @@ "dependencies": { "find-up": { "version": "1.1.2", - "resolved": "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, "requires": { @@ -10708,7 +10708,7 @@ }, "redent": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", "dev": true, "requires": { @@ -11178,7 +11178,7 @@ }, "sass-graph": { "version": "2.2.4", - "resolved": "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", "dev": true, "requires": { @@ -11196,7 +11196,7 @@ }, "cliui": { "version": "3.2.0", - "resolved": "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", "dev": true, "requires": { @@ -11207,7 +11207,7 @@ }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "requires": { @@ -11216,7 +11216,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -11225,7 +11225,7 @@ }, "string-width": { "version": "1.0.2", - "resolved": "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { @@ -11236,13 +11236,13 @@ }, "which-module": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, "yargs": { "version": "7.1.0", - "resolved": "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", "dev": true, "requires": { @@ -11263,7 +11263,7 @@ }, "yargs-parser": { "version": "5.0.0", - "resolved": "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", "dev": true, "requires": { @@ -11313,7 +11313,7 @@ }, "scss-tokenizer": { "version": "0.2.3", - "resolved": "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", "dev": true, "requires": { @@ -11323,7 +11323,7 @@ "dependencies": { "source-map": { "version": "0.4.4", - "resolved": "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { @@ -11918,7 +11918,7 @@ }, "sshpk": { "version": "1.14.2", - "resolved": "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "dev": true, "requires": { @@ -12115,7 +12115,7 @@ }, "strip-indent": { "version": "1.0.1", - "resolved": "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", "dev": true, "requires": { @@ -12518,8 +12518,8 @@ } }, "super-hands": { - "version": "github:mozillareality/aframe-super-hands-component#f8f9781d8b4c487bb544b3986000e85ed5f82fcc", - "from": "super-hands@github:mozillareality/aframe-super-hands-component#f8f9781d8b4c487bb544b3986000e85ed5f82fcc" + "version": "github:mozillareality/aframe-super-hands-component#68d022fd24c6c986ec3af09a3e88b75323e9803f", + "from": "github:mozillareality/aframe-super-hands-component#feature/drawing" }, "supports-color": { "version": "5.4.0", @@ -12564,7 +12564,7 @@ }, "tar": { "version": "2.2.1", - "resolved": "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", "dev": true, "requires": { @@ -12793,7 +12793,7 @@ }, "trim-newlines": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, @@ -12838,7 +12838,7 @@ }, "tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, "requires": { @@ -13323,7 +13323,7 @@ }, "verror": { "version": "1.10.0", - "resolved": "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "dev": true, "requires": { diff --git a/package.json b/package.json index 4599d553834ba4a3a993382e74d973262e2e5ecc..b0f7710f6b8bdcf1c4606b5ca7bc5e44bf815e90 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "jsonschema": "^1.2.2", "jszip": "^3.1.5", "moving-average": "^1.0.0", - "naf-janus-adapter": "^0.11.0", + "naf-janus-adapter": "^0.12.0", "networked-aframe": "github:mozillareality/networked-aframe#master", "nipplejs": "github:mozillareality/nipplejs#mr-social-client/master", "phoenix": "^1.3.0", @@ -54,7 +54,7 @@ "react-dom": "^16.1.1", "react-intl": "^2.4.0", "screenfull": "^3.3.2", - "super-hands": "github:mozillareality/aframe-super-hands-component#f8f9781d8b4c487bb544b3986000e85ed5f82fcc", + "super-hands": "github:mozillareality/aframe-super-hands-component#feature/drawing", "three": "github:mozillareality/three.js#8b1886c384371c3e6305b757d1db7577c5201a9b", "three-pathfinding": "^0.7.0", "three-to-cannon": "1.3.0", diff --git a/scripts/bot/package-lock.json b/scripts/bot/package-lock.json index fc2a26a35abb1f21b7e1d9801159a6eb8f8c69f3..6b4096f478cdf40a22477eb20dda838344af0ecf 100644 --- a/scripts/bot/package-lock.json +++ b/scripts/bot/package-lock.json @@ -4,106 +4,108 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", - "requires": { - "fd-slicer": "1.0.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "agent-base": { + "version": "4.2.0", + "resolved": "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha1-mDi1wzkrliutAx5qTF4QJKvsRc4=", + "dev": true, "requires": { - "minimist": "0.0.8" + "es6-promisify": "^5.0.0" } }, "async-limiter": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=" + "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=", + "dev": true }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha1-h/yqOimDWOCt5uRCz86EB0DRrQQ=", + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "dev": true, "requires": { - "wrappy": "1.0.2" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "debug": { + "version": "2.6.9", + "resolved": "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + "docopt": { + "version": "0.6.2", + "resolved": "https://registry.yarnpkg.com/docopt/-/docopt-0.6.2.tgz", + "integrity": "sha1-so6eIiDaXsSffqW7JKR3h0Be6xE=", + "dev": true }, "es6-promise": { "version": "4.2.4", "resolved": "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha1-3EIhwrFlGHYL2MOaUtjzVvwA7Sk=" + "integrity": "sha1-3EIhwrFlGHYL2MOaUtjzVvwA7Sk=", + "dev": true }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } }, "extract-zip": { "version": "1.6.7", "resolved": "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz", "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "dev": true, "requires": { "concat-stream": "1.6.2", "debug": "2.6.9", @@ -111,229 +113,274 @@ "yauzl": "2.4.1" } }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, - "proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=" + "glob": { + "version": "7.1.2", + "resolved": "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } }, "https-proxy-agent": { "version": "2.2.1", "resolved": "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", "integrity": "sha1-UVUpcPoE1yPgTFbQQXjD+SWSu8A=", + "dev": true, "requires": { - "agent-base": "4.2.0", - "debug": "3.1.0" + "agent-base": "^4.1.0", + "debug": "^3.1.0" }, "dependencies": { "debug": { "version": "3.1.0", "resolved": "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz", "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "dev": true, "requires": { "ms": "2.0.0" } } } }, - "puppeteer": { - "version": "1.3.0", - "resolved": "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.3.0.tgz", - "integrity": "sha1-9XHF8nFTyhZKgYjmMozi5JRoePM=", - "requires": { - "debug": "2.6.9", - "extract-zip": "1.6.7", - "https-proxy-agent": "2.2.1", - "mime": "1.6.0", - "progress": "2.0.0", - "proxy-from-env": "1.0.0", - "rimraf": "2.6.2", - "ws": "3.3.3" - } - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, + "puppeteer": { + "version": "1.3.0", + "resolved": "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.3.0.tgz", + "integrity": "sha1-9XHF8nFTyhZKgYjmMozi5JRoePM=", + "dev": true, + "requires": { + "debug": "^2.6.8", + "extract-zip": "^1.6.5", + "https-proxy-agent": "^2.1.0", + "mime": "^1.3.4", + "progress": "^2.0.0", + "proxy-from-env": "^1.0.0", + "rimraf": "^2.6.1", + "ws": "^3.0.0" } }, "query-string": { "version": "5.1.1", "resolved": "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz", "integrity": "sha1-p4wBK3HBfgXy4/ojGd0zBoLvs8s=", + "dev": true, "requires": { - "decode-uri-component": "0.2.0", - "object-assign": "4.1.1", - "strict-uri-encode": "1.1.0" + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "agent-base": { - "version": "4.2.0", - "resolved": "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz", - "integrity": "sha1-mDi1wzkrliutAx5qTF4QJKvsRc4=", - "requires": { - "es6-promisify": "5.0.0" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "dev": true, "requires": { - "glob": "7.1.2" + "glob": "^7.0.5" } }, - "buffer-from": { - "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz", - "integrity": "sha1-h/yqOimDWOCt5uRCz86EB0DRrQQ=" - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "requires": { - "ms": "2.0.0" - } + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", + "dev": true }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "4.2.4" - } + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, "requires": { - "safe-buffer": "5.1.2" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "safe-buffer": "~5.1.0" } }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "ws": { - "version": "3.3.3", - "resolved": "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz", - "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", - "requires": { - "async-limiter": "1.0.0", - "safe-buffer": "5.1.2", - "ultron": "1.1.1" - } + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=", + "dev": true }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=" + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "ws": { + "version": "3.3.3", + "resolved": "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz", + "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", + "dev": true, "requires": { - "buffer-from": "1.1.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "typedarray": "0.0.6" + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" } }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz", - "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=" - }, - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, "requires": { - "pend": "1.2.0" + "fd-slicer": "~1.0.1" } - }, - "docopt": { - "version": "0.6.2", - "resolved": "https://registry.yarnpkg.com/docopt/-/docopt-0.6.2.tgz", - "integrity": "sha1-so6eIiDaXsSffqW7JKR3h0Be6xE=" } } -} \ No newline at end of file +} diff --git a/scripts/bot/run-bot.js b/scripts/bot/run-bot.js index fbcef11f4121d5cf16f4babadedc58e118075d2d..687f27f8598764a9384dc7023e523bf1e9be6288 100755 --- a/scripts/bot/run-bot.js +++ b/scripts/bot/run-bot.js @@ -21,17 +21,13 @@ function log(...objs) { console.log.call(null, [new Date().toISOString()].concat(objs).join(" ")); } -function error(...objs) { - console.error.call(null, [new Date().toISOString()].concat(objs).join(" ")); -} - (async () => { const browser = await puppeteer.launch({ ignoreHTTPSErrors: true }); const page = await browser.newPage(); await page.setBypassCSP(true); page.on("console", msg => log("PAGE: ", msg.text())); - page.on("error", err => error("ERROR: ", err.toString().split("\n")[0])); - page.on("pageerror", err => error("PAGE ERROR: ", err.toString().split("\n")[0])); + page.on("error", err => log("ERROR: ", err.toString().split("\n")[0])); + page.on("pageerror", err => log("PAGE ERROR: ", err.toString().split("\n")[0])); const baseUrl = options["--url"] || `https://${options["--host"]}/hub.html`; @@ -80,7 +76,30 @@ function error(...objs) { setTimeout(loadFiles, backoff); } }; + await loadFiles(); + + // Do a periodic sanity check of the state of the bots. + setInterval(async function() { + let avatarCounts; + try { + avatarCounts = await page.evaluate(() => ({ + connectionCount: Object.keys(NAF.connection.adapter.occupants).length, + avatarCount: document.querySelectorAll("[networked-avatar]").length - 1 + })); + log(JSON.stringify(avatarCounts)); + } catch (e) { + // Ignore errors. This usually happens when the page is shutting down. + } + // Check for more than two connections to allow for a margin where we have a connection but the a-frame + // entity has not initialized yet. + if (avatarCounts && avatarCounts.connectionCount > 2 && avatarCounts.avatarCount === 0) { + // It seems the bots have dog-piled on to a restarting server, so we're going to shut things down and + // let the hubs-ops bash script restart us. + log("Detected avatar dog-pile. Restarting."); + process.exit(1); + } + }, 60 * 1000); } catch (e) { log("Navigation error", e.message); setTimeout(navigate, 1000); diff --git a/scripts/hab-build-and-push.sh b/scripts/hab-build-and-push.sh index 48693732a8902bc487f72eb87e8ff27a02fa9af5..1380235bd31dc889dece549791c550791ec0f178 100755 --- a/scripts/hab-build-and-push.sh +++ b/scripts/hab-build-and-push.sh @@ -30,3 +30,4 @@ mv dist/*.html dist/pages aws s3 sync --acl public-read --cache-control "max-age=31556926" dist/assets "$TARGET_S3_URL/assets" aws s3 sync --acl public-read --cache-control "no-cache" --delete dist/pages "$TARGET_S3_URL/pages/latest" +aws s3 sync --acl public-read --cache-control "no-cache" --delete dist/pages "$TARGET_S3_URL/pages/releases/${BUILD_NUMBER}" diff --git a/src/assets/hud/spawn_pen-hover.png b/src/assets/hud/spawn_pen-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..eb298cd91498432098056cd69213919574364dee Binary files /dev/null and b/src/assets/hud/spawn_pen-hover.png differ diff --git a/src/assets/hud/spawn_pen.png b/src/assets/hud/spawn_pen.png new file mode 100644 index 0000000000000000000000000000000000000000..62cfbb279046e04e070e55a14c62399ec22b7be7 Binary files /dev/null and b/src/assets/hud/spawn_pen.png differ diff --git a/src/assets/stylesheets/2d-hud.scss b/src/assets/stylesheets/2d-hud.scss index ca0a87c337153e683a0a08964e02c071f0941f62..f8b56e42545b757b425135a2e7fb1a21abf29a62 100644 --- a/src/assets/stylesheets/2d-hud.scss +++ b/src/assets/stylesheets/2d-hud.scss @@ -113,6 +113,13 @@ background-image: url(../hud/bubble_on-hover.png); } +:local(.iconButton.spawn_pen) { + background-image: url(../hud/spawn_pen.png); +} +:local(.iconButton.spawn_pen:hover) { + background-image: url(../hud/spawn_pen-hover.png); +} + :local(.iconButton.freeze) { background-image: url(../hud/freeze_off.png); } diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 857c1840a2d5de3ac814f3f19835af1bb0aed8c3..0748867c4cafa14758b02010af6be0e61390c502 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -1,5 +1,9 @@ { "en": { + "auth.verifying": "Verifying...", + "auth.verified-title": "E-Mail Verified!", + "auth.verified": "Your email has been verified!", + "auth.spoke-verified": "You email has been verified! {br} You can now close this browser tab and return to Spoke.", "entry.screen-prefix": "Enter on ", "entry.desktop-screen": "Screen", "entry.mobile-screen": "Phone", diff --git a/src/components/css-class.js b/src/components/css-class.js index 77882e9bce9e2e00c6d02f02677b9388b8110d14..4d4da5fd332a2a06574a43346b5c801b74067138 100644 --- a/src/components/css-class.js +++ b/src/components/css-class.js @@ -6,6 +6,7 @@ AFRAME.registerComponent("css-class", { schema: { type: "string" }, + multiple: true, init() { this.el.classList.add(this.data); }, diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index c801f76188260c695d7121ef242a00e1d5db4ea9..421b084c309710e9442dd3f44019fdcb0cad94d5 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -3,39 +3,113 @@ const TARGET_TYPE_INTERACTABLE = 2; const TARGET_TYPE_UI = 4; const TARGET_TYPE_INTERACTABLE_OR_UI = TARGET_TYPE_INTERACTABLE | TARGET_TYPE_UI; +/** + * Manages targeting and physical cursor location. Has the following responsibilities: + * + * - Tracking which entities in the scene can be targeted by the cursor (`objects`). + * - Performing a raycast per-frame or on-demand to identify which entity is being currently targeted. + * - Updating the visual presentation and position of the `cursor` entity and `line` component per frame. + * - Sending an event when an entity is targeted or un-targeted. + */ AFRAME.registerComponent("cursor-controller", { - dependencies: ["raycaster", "line"], + dependencies: ["line"], schema: { cursor: { type: "selector" }, camera: { type: "selector" }, - maxDistance: { default: 3 }, - minDistance: { default: 0 }, + far: { default: 3 }, + near: { default: 0 }, cursorColorHovered: { default: "#2F80ED" }, cursorColorUnhovered: { default: "#FFFFFF" }, rayObject: { type: "selector" }, - useMousePos: { default: true }, - drawLine: { default: false } + drawLine: { default: false }, + objects: { default: "" } }, init: function() { this.enabled = true; - this.inVR = false; - this.isMobile = AFRAME.utils.device.isMobile(); this.currentTargetType = TARGET_TYPE_NONE; - this.currentDistance = this.data.maxDistance; + this.currentDistance = this.data.far; this.currentDistanceMod = 0; this.mousePos = new THREE.Vector2(); this.wasCursorHovered = false; - this.origin = new THREE.Vector3(); - this.direction = new THREE.Vector3(); - this.raycasterAttr = this.el.getAttribute("raycaster"); - this.controllerQuaternion = new THREE.Quaternion(); this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered }); this._handleCursorLoaded = this._handleCursorLoaded.bind(this); this.data.cursor.addEventListener("loaded", this._handleCursorLoaded); + + // raycaster state + this.targets = []; + this.intersection = null; + this.raycaster = new THREE.Raycaster(); + this.setDirty = this.setDirty.bind(this); + this.dirty = true; + }, + + update: function() { + this.raycaster.far = this.data.far; + this.raycaster.near = this.data.near; + this.setDirty(); + }, + + play: function() { + this.observer = new MutationObserver(this.setDirty); + this.observer.observe(this.el.sceneEl, { childList: true, attributes: true, subtree: true }); + this.el.sceneEl.addEventListener("object3dset", this.setDirty); + this.el.sceneEl.addEventListener("object3dremove", this.setDirty); + }, + + pause: function() { + this.observer.disconnect(); + this.el.sceneEl.removeEventListener("object3dset", this.setDirty); + this.el.sceneEl.removeEventListener("object3dremove", this.setDirty); + }, + + setDirty: function() { + this.dirty = true; }, + populateEntities: function(selector, target) { + target.length = 0; + const els = this.data.objects ? this.el.sceneEl.querySelectorAll(this.data.objects) : this.el.sceneEl.children; + for (let i = 0; i < els.length; i++) { + if (els[i].object3D) { + target.push(els[i].object3D); + } + } + }, + + emitIntersectionEvents: function(prevIntersection, currIntersection) { + // if we are now intersecting something, and previously we were intersecting nothing or something else + if (currIntersection && (!prevIntersection || currIntersection.object.el !== prevIntersection.object.el)) { + this.data.cursor.emit("raycaster-intersection", { el: currIntersection.object.el }); + } + // if we were intersecting something, but now we are intersecting nothing or something else + if (prevIntersection && (!currIntersection || currIntersection.object.el !== prevIntersection.object.el)) { + this.data.cursor.emit("raycaster-intersection-cleared", { el: prevIntersection.object.el }); + } + }, + + performRaycast: (function() { + const rayObjectRotation = new THREE.Quaternion(); + const rawIntersections = []; + return function performRaycast(targets) { + if (this.data.rayObject) { + const rayObject = this.data.rayObject.object3D; + rayObject.updateMatrixWorld(); + rayObjectRotation.setFromRotationMatrix(rayObject.matrixWorld); + this.raycaster.ray.origin.setFromMatrixPosition(rayObject.matrixWorld); + this.raycaster.ray.direction.set(0, 0, -1).applyQuaternion(rayObjectRotation); + } else { + this.raycaster.setFromCamera(this.mousePos, this.data.camera.components.camera.camera); // camera + } + const prevIntersection = this.intersection; + rawIntersections.length = 0; + this.raycaster.intersectObjects(targets, true, rawIntersections); + this.intersection = rawIntersections.find(x => x.object.el); + this.emitIntersectionEvents(prevIntersection, this.intersection); + }; + })(), + enable: function() { this.enabled = true; }, @@ -45,14 +119,7 @@ AFRAME.registerComponent("cursor-controller", { this.setCursorVisibility(false); }, - updateRay: function() { - this.raycasterAttr.origin = this.origin; - this.raycasterAttr.direction = this.direction; - this.el.setAttribute("raycaster", this.raycasterAttr, true); - }, - tick: (() => { - const rayObjectRotation = new THREE.Quaternion(); const cameraPos = new THREE.Vector3(); return function() { @@ -60,27 +127,20 @@ AFRAME.registerComponent("cursor-controller", { return; } - if (this.data.useMousePos) { - this.setRaycasterWithMousePos(); - } else { - const rayObject = this.data.rayObject.object3D; - rayObjectRotation.setFromRotationMatrix(rayObject.matrixWorld); - this.direction - .set(0, 0, -1) - .applyQuaternion(rayObjectRotation) - .normalize(); - this.origin.setFromMatrixPosition(rayObject.matrixWorld); - this.updateRay(); + if (this.dirty) { + this.populateEntities(this.data.objects, this.targets); + this.dirty = false; } - const isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start"); - if (isGrabbing) { + this.performRaycast(this.targets); + + if (this.isInteracting()) { const distance = Math.min( - this.data.maxDistance, - Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod) + this.data.far, + Math.max(this.data.near, this.currentDistance - this.currentDistanceMod) ); - this.direction.multiplyScalar(distance); - this.data.cursor.object3D.position.addVectors(this.origin, this.direction); + this.data.cursor.object3D.position.copy(this.raycaster.ray.origin); + this.data.cursor.object3D.position.addScaledVector(this.raycaster.ray.direction, distance); } else { this.currentDistanceMod = 0; this.updateDistanceAndTargetType(); @@ -96,7 +156,10 @@ AFRAME.registerComponent("cursor-controller", { } if (this.data.drawLine) { - this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() }); + this.el.setAttribute("line", { + start: this.raycaster.ray.origin.clone(), + end: this.data.cursor.object3D.position.clone() + }); } // The cursor will always be oriented towards the player about its Y axis, so objects held by the cursor will rotate towards the player. @@ -106,26 +169,15 @@ AFRAME.registerComponent("cursor-controller", { }; })(), - setRaycasterWithMousePos: function() { - const camera = this.data.camera.components.camera.camera; - const raycaster = this.el.components.raycaster.raycaster; - raycaster.setFromCamera(this.mousePos, camera); - this.origin.copy(raycaster.ray.origin); - this.direction.copy(raycaster.ray.direction); - this.updateRay(); - }, - updateDistanceAndTargetType: function() { - let intersection = null; - const intersections = this.el.components.raycaster.intersections; - if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) { - intersection = intersections[0]; + const intersection = this.intersection; + if (intersection && intersection.distance <= this.data.far) { this.data.cursor.object3D.position.copy(intersection.point); - this.currentDistance = intersections[0].distance; + this.currentDistance = intersection.distance; } else { - this.currentDistance = this.data.maxDistance; - this.direction.multiplyScalar(this.currentDistance); - this.data.cursor.object3D.position.addVectors(this.origin, this.direction); + this.currentDistance = this.data.far; + this.data.cursor.object3D.position.copy(this.raycaster.ray.origin); + this.data.cursor.object3D.position.addScaledVector(this.raycaster.ray.direction, this.currentDistance); } if (!intersection) { @@ -147,12 +199,15 @@ AFRAME.registerComponent("cursor-controller", { }, forceCursorUpdate: function() { - this.setRaycasterWithMousePos(); - this.el.components.raycaster.checkIntersections(); + this.performRaycast(this.targets); this.updateDistanceAndTargetType(); this.data.cursor.components["static-body"].syncToPhysics(); }, + isInteracting: function() { + return this.data.cursor.components["super-hands"].state.has("grab-start"); + }, + startInteraction: function() { if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) { this.data.cursor.emit("cursor-grab", {}); @@ -161,22 +216,24 @@ AFRAME.registerComponent("cursor-controller", { return false; }, - moveCursor: function(x, y) { - this.mousePos.set(x, y); - }, - endInteraction: function() { this.data.cursor.emit("cursor-release", {}); }, + moveCursor: function(x, y) { + this.mousePos.set(x, y); + }, + changeDistanceMod: function(delta) { - const { minDistance, maxDistance } = this.data; + const { near, far } = this.data; const targetDistanceMod = this.currentDistanceMod + delta; const moddedDistance = this.currentDistance - targetDistanceMod; - if (moddedDistance > maxDistance || moddedDistance < minDistance) { - return; + if (moddedDistance > far || moddedDistance < near) { + return false; } + this.currentDistanceMod = targetDistanceMod; + return true; }, _handleCursorLoaded: function() { @@ -185,6 +242,8 @@ AFRAME.registerComponent("cursor-controller", { }, remove: function() { + this.emitIntersectionEvents(this.intersection, null); + this.intersection = null; this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded); } }); diff --git a/src/components/gltf-bundle.js b/src/components/gltf-bundle.js index 72163ec542922ea21c5c7612b54b193f5c256a97..7ad7840822faac29b7e70f148fe2ff12ae3d6819 100644 --- a/src/components/gltf-bundle.js +++ b/src/components/gltf-bundle.js @@ -28,7 +28,7 @@ AFRAME.registerComponent("gltf-bundle", { const src = new URL(asset.src, this.baseURL).href; const gltfEl = document.createElement("a-entity"); - gltfEl.setAttribute("gltf-model-plus", { src, inflate: true }); + gltfEl.setAttribute("gltf-model-plus", { src, useCache: false, inflate: true }); loaded.push(new Promise(resolve => gltfEl.addEventListener("model-loaded", resolve))); this.el.appendChild(gltfEl); } diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index a5080fe24de9e24302d8c971549d090f4d15ef1b..625fc4c3536a1660b4a908d4d8d6e47e36377f44 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -279,6 +279,7 @@ AFRAME.registerComponent("gltf-model-plus", { schema: { src: { type: "string" }, contentType: { type: "string" }, + useCache: { default: true }, inflate: { default: false } }, @@ -300,6 +301,17 @@ AFRAME.registerComponent("gltf-model-plus", { }); }, + async loadModel(src, contentType, technique, useCache) { + if (useCache) { + if (!GLTFCache[src]) { + GLTFCache[src] = await loadGLTF(src, contentType, technique); + } + return cloneGltf(GLTFCache[src]); + } else { + return await loadGLTF(src, contentType, technique); + } + }, + async applySrc(src, contentType) { try { // If the src attribute is a selector, get the url from the asset item. @@ -319,11 +331,7 @@ AFRAME.registerComponent("gltf-model-plus", { return; } - if (!GLTFCache[src]) { - GLTFCache[src] = loadGLTF(src, contentType, this.preferredTechnique); - } - - const model = cloneGltf(await GLTFCache[src]); + const model = await this.loadModel(src, contentType, this.preferredTechnique, this.data.useCache); // If we started loading something else already // TODO: there should be a way to cancel loading instead diff --git a/src/components/grabbable-toggle.js b/src/components/grabbable-toggle.js new file mode 100644 index 0000000000000000000000000000000000000000..5a11d5698d5ba53cbafeab63286bf142b1699bfb --- /dev/null +++ b/src/components/grabbable-toggle.js @@ -0,0 +1,185 @@ +/* global AFRAME, THREE */ +const inherit = AFRAME.utils.extendDeep; +const physicsCore = require("super-hands/reaction_components/prototypes/physics-grab-proto.js"); +const buttonsCore = require("super-hands/reaction_components/prototypes/buttons-proto.js"); +// new object with all core modules +const base = inherit({}, physicsCore, buttonsCore); +AFRAME.registerComponent( + "grabbable-toggle", + inherit(base, { + schema: { + maxGrabbers: { type: "int", default: NaN }, + invert: { default: false }, + suppressY: { default: false }, + primaryReleaseEvents: { default: ["primary_hand_release"] }, + secondaryReleaseEvents: { default: ["secondary_hand_release"] } + }, + init: function() { + this.GRABBED_STATE = "grabbed"; + this.GRAB_EVENT = "grab-start"; + this.UNGRAB_EVENT = "grab-end"; + this.grabbed = false; + this.grabbers = []; + this.constraints = new Map(); + this.deltaPositionIsValid = false; + this.grabDistance = undefined; + this.grabDirection = { x: 0, y: 0, z: -1 }; + this.grabOffset = { x: 0, y: 0, z: 0 }; + // persistent object speeds up repeat setAttribute calls + this.destPosition = { x: 0, y: 0, z: 0 }; + this.deltaPosition = new THREE.Vector3(); + this.targetPosition = new THREE.Vector3(); + this.physicsInit(); + + this.el.addEventListener(this.GRAB_EVENT, e => this.start(e)); + this.el.addEventListener(this.UNGRAB_EVENT, e => this.end(e)); + this.el.addEventListener("mouseout", e => this.lostGrabber(e)); + + this.toggle = false; + this.lastGrabber = null; + }, + update: function() { + this.physicsUpdate(); + this.xFactor = this.data.invert ? -1 : 1; + this.zFactor = this.data.invert ? -1 : 1; + this.yFactor = (this.data.invert ? -1 : 1) * !this.data.suppressY; + }, + tick: (function() { + const q = new THREE.Quaternion(); + const v = new THREE.Vector3(); + + return function() { + let entityPosition; + if (this.grabber) { + // reflect on z-axis to point in same direction as the laser + this.targetPosition.copy(this.grabDirection); + this.targetPosition + .applyQuaternion(this.grabber.object3D.getWorldQuaternion(q)) + .setLength(this.grabDistance) + .add(this.grabber.object3D.getWorldPosition(v)) + .add(this.grabOffset); + if (this.deltaPositionIsValid) { + // relative position changes work better with nested entities + this.deltaPosition.sub(this.targetPosition); + entityPosition = this.el.getAttribute("position"); + this.destPosition.x = entityPosition.x - this.deltaPosition.x * this.xFactor; + this.destPosition.y = entityPosition.y - this.deltaPosition.y * this.yFactor; + this.destPosition.z = entityPosition.z - this.deltaPosition.z * this.zFactor; + this.el.setAttribute("position", this.destPosition); + } else { + this.deltaPositionIsValid = true; + } + this.deltaPosition.copy(this.targetPosition); + } + }; + })(), + remove: function() { + this.el.removeEventListener(this.GRAB_EVENT, this.start); + this.el.removeEventListener(this.UNGRAB_EVENT, this.end); + this.physicsRemove(); + }, + start: function(evt) { + if (evt.defaultPrevented || !this.startButtonOk(evt)) { + return; + } + // room for more grabbers? + let grabAvailable = !Number.isFinite(this.data.maxGrabbers) || this.grabbers.length < this.data.maxGrabbers; + if (Number.isFinite(this.data.maxGrabbers) && !grabAvailable && this.grabbed) { + this.grabbers[0].components["super-hands"].onGrabEndButton(); + grabAvailable = true; + } + if (this.grabbers.indexOf(evt.detail.hand) === -1 && grabAvailable) { + if (!evt.detail.hand.object3D) { + console.warn("grabbable entities must have an object3D"); + return; + } + this.grabbers.push(evt.detail.hand); + // initiate physics if available, otherwise manual + if (!this.physicsStart(evt) && !this.grabber) { + this.grabber = evt.detail.hand; + this.resetGrabber(); + } + // notify super-hands that the gesture was accepted + if (evt.preventDefault) { + evt.preventDefault(); + } + this.grabbed = true; + this.el.addState(this.GRABBED_STATE); + } + }, + end: function(evt) { + const handIndex = this.grabbers.indexOf(evt.detail.hand); + if (evt.defaultPrevented || !this.endButtonOk(evt)) { + return; + } + + const type = evt.detail && evt.detail.buttonEvent ? evt.detail.buttonEvent.type : null; + + if (this.toggle && this.lastGrabber !== this.grabbers[0]) { + this.toggle = false; + this.lastGrabber = null; + } + + if (handIndex !== -1) { + this.grabber = this.grabbers[0]; + } + + if ((this.isPrimaryRelease(type) && !this.toggle) || this.isSecondaryRelease(type)) { + this.toggle = true; + this.lastGrabber = this.grabbers[0]; + return; + } else if (this.toggle && this.isPrimaryRelease(type)) { + this.toggle = false; + this.lastGrabber = null; + } + + if (handIndex !== -1) { + this.grabbers.splice(handIndex, 1); + this.grabber = this.grabbers[0]; + } + + this.physicsEnd(evt); + if (!this.resetGrabber()) { + this.grabbed = false; + this.el.removeState(this.GRABBED_STATE); + } + if (evt.preventDefault) { + evt.preventDefault(); + } + }, + resetGrabber: (() => { + const objPos = new THREE.Vector3(); + const grabPos = new THREE.Vector3(); + return function() { + if (!this.grabber) { + return false; + } + const raycaster = this.grabber.getAttribute("raycaster"); + this.deltaPositionIsValid = false; + this.grabDistance = this.el.object3D + .getWorldPosition(objPos) + .distanceTo(this.grabber.object3D.getWorldPosition(grabPos)); + if (raycaster) { + this.grabDirection = raycaster.direction; + this.grabOffset = raycaster.origin; + } + return true; + }; + })(), + lostGrabber: function(evt) { + const i = this.grabbers.indexOf(evt.relatedTarget); + // if a queued, non-physics grabber leaves the collision zone, forget it + if (i !== -1 && evt.relatedTarget !== this.grabber && !this.physicsIsConstrained(evt.relatedTarget)) { + this.grabbers.splice(i, 1); + } + }, + + isPrimaryRelease(type) { + return this.data.primaryReleaseEvents.indexOf(type) !== -1; + }, + + isSecondaryRelease(type) { + return this.data.secondaryReleaseEvents.indexOf(type) !== -1; + } + }) +); diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js index a4fe8c9655ecffe3161a86b09fa3b6f6cdbdb19b..1bee398de9521f9a12e7fd0eb12b2af035a89041 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -11,23 +11,23 @@ AFRAME.registerComponent("in-world-hud", { init() { this.mic = this.el.querySelector(".mic"); this.freeze = this.el.querySelector(".freeze"); - this.bubble = this.el.querySelector(".bubble"); + this.pen = this.el.querySelector(".pen"); this.background = this.el.querySelector(".bg"); const renderOrder = window.APP.RENDER_ORDER; this.mic.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; this.freeze.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; - this.bubble.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; + this.pen.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; this.background.object3DMap.mesh.renderORder = renderOrder.HUD_BACKGROUND; this.updateButtonStates = () => { this.mic.setAttribute("icon-button", "active", this.el.sceneEl.is("muted")); this.freeze.setAttribute("icon-button", "active", this.el.sceneEl.is("frozen")); - this.bubble.setAttribute("icon-button", "active", this.el.sceneEl.is("spacebubble")); + this.pen.setAttribute("icon-button", "active", this.el.sceneEl.is("pen")); }; this.updateButtonStates(); this.onStateChange = evt => { - if (!(evt.detail === "muted" || evt.detail === "frozen" || evt.detail === "spacebubble")) return; + if (!(evt.detail === "muted" || evt.detail === "frozen" || evt.detail === "pen")) return; this.updateButtonStates(); }; @@ -39,8 +39,8 @@ AFRAME.registerComponent("in-world-hud", { this.el.emit("action_freeze"); }; - this.onBubbleClick = () => { - this.el.emit("action_space_bubble"); + this.onPenClick = () => { + this.el.emit("spawn_pen"); }; }, @@ -50,7 +50,7 @@ AFRAME.registerComponent("in-world-hud", { this.mic.addEventListener("click", this.onMicClick); this.freeze.addEventListener("click", this.onFreezeClick); - this.bubble.addEventListener("click", this.onBubbleClick); + this.pen.addEventListener("mousedown", this.onPenClick); }, pause() { @@ -59,6 +59,6 @@ AFRAME.registerComponent("in-world-hud", { this.mic.removeEventListener("click", this.onMicClick); this.freeze.removeEventListener("click", this.onFreezeClick); - this.bubble.removeEventListener("click", this.onBubbleClick); + this.pen.removeEventListener("mousedown", this.onPenClick); } }); diff --git a/src/components/input-configurator.js b/src/components/input-configurator.js index df36b38d8c4bb81a2f8fd98824cbd3ecf4ef7f15..356213fbf319e5037bcb21166aa11e794ccc5f94 100644 --- a/src/components/input-configurator.js +++ b/src/components/input-configurator.js @@ -21,7 +21,6 @@ AFRAME.registerComponent("input-configurator", { this.isMobile = AFRAME.utils.device.isMobile(); this.eventHandlers = []; this.controllerQueue = []; - this.hasPointingDevice = false; this.cursor = this.data.cursorController.components["cursor-controller"]; this.gazeTeleporter = this.data.gazeTeleporter.components["teleport-controls"]; this.cameraController = this.data.camera.components["pitch-yaw-rotator"]; @@ -89,12 +88,10 @@ AFRAME.registerComponent("input-configurator", { this.actionEventHandler = new ActionEventHandler(this.el.sceneEl, this.cursor); this.eventHandlers.push(this.actionEventHandler); - this.cursor.el.setAttribute("cursor-controller", "useMousePos", !this.inVR); - if (this.inVR) { this.cameraController.pause(); this.cursorRequiresManagement = true; - this.cursor.el.setAttribute("cursor-controller", "minDistance", 0); + this.cursor.el.setAttribute("cursor-controller", "near", 0); if (this.isMobile) { this.eventHandlers.push(new GearVRMouseEventsHandler(this.cursor, this.gazeTeleporter)); } else { @@ -108,7 +105,7 @@ AFRAME.registerComponent("input-configurator", { this.addLookOnMobile(); } else { this.eventHandlers.push(new MouseEventsHandler(this.cursor, this.cameraController)); - this.cursor.el.setAttribute("cursor-controller", "minDistance", 0.3); + this.cursor.el.setAttribute("cursor-controller", "near", 0.3); } } }, @@ -145,25 +142,30 @@ AFRAME.registerComponent("input-configurator", { }, updateController: function() { - this.hasPointingDevice = this.controllerQueue.length > 0 && this.inVR; - this.cursor.el.setAttribute("cursor-controller", "drawLine", this.hasPointingDevice); - this.cursor.setCursorVisibility(true); + const controllerData = this.controllerQueue.length ? this.controllerQueue[0] : null; - if (this.hasPointingDevice) { - const controllerData = this.controllerQueue[0]; - const hand = controllerData.handedness; + if (controllerData) { this.controller = controllerData.controller; - this.cursor.el.setAttribute("cursor-controller", { - rayObject: hand === "left" ? this.data.leftControllerRayObject : this.data.rightControllerRayObject - }); + this.actionEventHandler.setHandThatAlsoDrivesCursor(this.controller); } else { this.controller = null; - this.cursor.el.setAttribute("cursor-controller", { rayObject: this.data.gazeCursorRayObject }); + this.actionEventHandler.setHandThatAlsoDrivesCursor(null); } - if (this.actionEventHandler) { - this.actionEventHandler.setHandThatAlsoDrivesCursor(this.controller); + let rayObject; + let drawLine; + if (controllerData && this.inVR) { + rayObject = + controllerData.handedness === "left" ? this.data.leftControllerRayObject : this.data.rightControllerRayObject; + drawLine = true; + } else if (this.inVR) { + rayObject = this.data.gazeCursorRayObject; + drawLine = false; + } else { + rayObject = null; + drawLine = false; } + this.cursor.el.setAttribute("cursor-controller", { rayObject, drawLine }); } }); diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 802d582c5a1b1dfac3961420dcf8bfde6d0619b6..da20ee178906bd68a9aa7a23496e208e5676e37f 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -20,6 +20,8 @@ AFRAME.registerComponent("media-loader", { this.onError = this.onError.bind(this); this.showLoader = this.showLoader.bind(this); this.clearLoadingTimeout = this.clearLoadingTimeout.bind(this); + this.shapeAdded = false; + this.hasBakedShapes = false; }, setShapeAndScale(resize) { @@ -27,9 +29,10 @@ AFRAME.registerComponent("media-loader", { const box = getBox(this.el, mesh); const scaleCoefficient = resize ? getScaleCoefficient(0.5, box) : 1; this.el.object3DMap.mesh.scale.multiplyScalar(scaleCoefficient); - if (this.el.body && this.el.body.shapes.length > 1) { + if (this.el.body && this.shapeAdded && this.el.body.shapes.length > 1) { this.el.removeAttribute("shape"); - } else { + this.shapeAdded = false; + } else if (!this.hasBakedShapes) { const center = new THREE.Vector3(); const { min, max } = box; const halfExtents = { @@ -43,6 +46,7 @@ AFRAME.registerComponent("media-loader", { shape: "box", halfExtents: halfExtents }); + this.shapeAdded = true; } }, @@ -72,6 +76,7 @@ AFRAME.registerComponent("media-loader", { this.loadingClip.play(); } this.el.setObject3D("mesh", mesh); + this.hasBakedShapes = !!(this.el.body && this.el.body.shapes.length > 0); this.setShapeAndScale(true); delete this.showLoaderTimeout; }, @@ -146,6 +151,7 @@ AFRAME.registerComponent("media-loader", { "model-loaded", () => { this.clearLoadingTimeout(); + this.hasBakedShapes = !!(this.el.body && this.el.body.shapes.length > (this.shapeAdded ? 1 : 0)); this.setShapeAndScale(this.data.resize); }, { once: true } diff --git a/src/components/sticky-object.js b/src/components/sticky-object.js index d84c82313100622864b9457161b71b842edf6309..61d04814a95b2e30cc79743a039dec1c0a6d5235 100644 --- a/src/components/sticky-object.js +++ b/src/components/sticky-object.js @@ -47,17 +47,21 @@ AFRAME.registerComponent("sticky-object", { }, _onRelease() { + // Happens if the object is still being held by another hand + if (this.el.is("grabbed")) return; + if ( - !this.el.is("grabbed") && this.data.autoLockOnRelease && this.el.body.velocity.lengthSquared() < this.data.autoLockSpeedLimit * this.data.autoLockSpeedLimit ) { this.setLocked(true); } + this.el.body.collisionResponse = true; }, _onGrab() { this.setLocked(false); + this.el.body.collisionResponse = false; }, remove() { diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index 844dc1df1706f2a51f55afd531e41898724fcb77..d2e04e23911cb499c1ce2c9eb7a1a62ab1ae2e53 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -6,13 +6,18 @@ AFRAME.registerComponent("super-networked-interactable", { schema: { hapticsMassVelocityFactor: { default: 0.1 }, - counter: { type: "selector" } + counter: { type: "selector" }, + scrollScaleDelta: { default: 0.1 }, + minScale: { default: 0.1 }, + maxScale: { default: 100 } }, init: function() { this.system = this.el.sceneEl.systems.physics; this.counter = this.data.counter.components["networked-counter"]; this.hand = null; + this.currentScale = new THREE.Vector3(); + this.currentScale.copy(this.el.getAttribute("scale")); NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.networkedEl = networkedEl; @@ -23,31 +28,29 @@ AFRAME.registerComponent("super-networked-interactable", { } }); + this._stateAdded = this._stateAdded.bind(this); this._onGrabStart = this._onGrabStart.bind(this); + this._onGrabEnd = this._onGrabEnd.bind(this); this._onOwnershipLost = this._onOwnershipLost.bind(this); this.el.addEventListener("grab-start", this._onGrabStart); + this.el.addEventListener("grab-end", this._onGrabEnd); this.el.addEventListener("ownership-lost", this._onOwnershipLost); + this.el.addEventListener("stateadded", this._stateAdded); this.system.addComponent(this); }, remove: function() { this.counter.deregister(this.el); this.el.removeEventListener("grab-start", this._onGrabStart); + this.el.removeEventListener("grab-end", this._onGrabEnd); this.el.removeEventListener("ownership-lost", this._onOwnershipLost); + this.el.removeEventListener("stateadded", this._stateAdded); this.system.removeComponent(this); }, - afterStep: function() { - if (this.el.is("grabbed") && this.hand && this.hand.components.hasOwnProperty("haptic-feedback")) { - const hapticFeedback = this.hand.components["haptic-feedback"]; - let velocity = this.el.body.velocity.lengthSquared() * this.el.body.mass * this.data.hapticsMassVelocityFactor; - velocity = Math.min(1, velocity); - hapticFeedback.pulse(velocity); - } - }, - _onGrabStart: function(e) { this.hand = e.detail.hand; + this.hand.emit("haptic_pulse", { intensity: "high" }); if (this.networkedEl && !NAF.utils.isMine(this.networkedEl)) { if (NAF.utils.takeOwnership(this.networkedEl)) { this.el.setAttribute("body", { type: "dynamic" }); @@ -57,6 +60,11 @@ AFRAME.registerComponent("super-networked-interactable", { this.hand = null; } } + this.currentScale.copy(this.el.getAttribute("scale")); + }, + + _onGrabEnd: function(e) { + if (e.detail.hand) e.detail.hand.emit("haptic_pulse", { intensity: "high" }); }, _onOwnershipLost: function() { @@ -64,5 +72,26 @@ AFRAME.registerComponent("super-networked-interactable", { this.el.emit("grab-end", { hand: this.hand }); this.hand = null; this.counter.deregister(this.el); + }, + + _changeScale: function(delta) { + if (this.el.is("grabbed") && this.el.components.hasOwnProperty("stretchable")) { + this.currentScale.addScalar(delta).clampScalar(this.data.minScale, this.data.maxScale); + this.el.setAttribute("scale", this.currentScale); + this.el.components["stretchable"].stretchBody(this.el, this.currentScale); + } + }, + + _stateAdded(evt) { + switch (evt.detail) { + case "scaleUp": + this._changeScale(-this.data.scrollScaleDelta); + break; + case "scaleDown": + this._changeScale(this.data.scrollScaleDelta); + break; + default: + break; + } } }); diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index 0c69e82da5cfe8c62e4aabafd2506fcf11613e3d..425b1c834da9b7a75b54cad155224c33ec8edf7a 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -4,7 +4,7 @@ import { ObjectContentOrigins } from "../object-types"; let nextGrabId = 0; /** - * Spawns networked objects when grabbed. + * Spawns networked objects when grabbed or when a specified event is fired. * @namespace network * @component super-spawner */ @@ -13,7 +13,12 @@ AFRAME.registerComponent("super-spawner", { /** * Source of the media asset the spawner will spawn when grabbed. This can be a gltf, video, or image, or a url that the reticiulm media API can resolve to a gltf, video, or image. */ - src: { default: "https://asset-bundles-prod.reticulum.io/interactables/Ducky/DuckyMesh-438ff8e022.gltf" }, + src: { default: "" }, + + /** + * The template to use for this object + */ + template: { default: "" }, /** * Spawn the object at a custom position, rather than at the center of the spanwer. @@ -27,11 +32,17 @@ AFRAME.registerComponent("super-spawner", { useCustomSpawnRotation: { default: false }, spawnRotation: { type: "vec4" }, + /** + * Spawn the object with a custom scale, rather than copying that of the spawner. + */ + useCustomSpawnScale: { default: false }, + spawnScale: { type: "vec3" }, + /** * The events to emit for programmatically grabbing and releasing objects */ - grabEvents: { default: ["cursor-grab", "hand_grab"] }, - releaseEvents: { default: ["cursor-release", "hand_release"] }, + grabEvents: { default: ["cursor-grab", "primary_hand_grab"] }, + releaseEvents: { default: ["cursor-release", "primary_hand_release"] }, /** * The spawner will become invisible and ungrabbable for this ammount of time after being grabbed. This can prevent rapidly spawning objects. @@ -41,7 +52,22 @@ AFRAME.registerComponent("super-spawner", { /** * Center the spawned object on the hand that grabbed it after it finishes loading. By default the object will be grabbed relative to where the spawner was grabbed */ - centerSpawnedObject: { default: false } + centerSpawnedObject: { default: false }, + + /** + * Optional event to listen for to spawn an object on the preferred superHand + */ + spawnEvent: { type: "string" }, + + /** + * The superHand to use if an object is spawned via spawnEvent + */ + superHand: { type: "selector" }, + + /** + * The cursor superHand to use if an object is spawned via spawnEvent + */ + cursorSuperHand: { type: "selector" } }, init() { @@ -49,16 +75,26 @@ AFRAME.registerComponent("super-spawner", { this.cooldownTimeout = null; this.onGrabStart = this.onGrabStart.bind(this); this.onGrabEnd = this.onGrabEnd.bind(this); + + this.onSpawnEvent = this.onSpawnEvent.bind(this); + + this.sceneEl = document.querySelector("a-scene"); }, play() { this.el.addEventListener("grab-start", this.onGrabStart); this.el.addEventListener("grab-end", this.onGrabEnd); + if (this.data.spawnEvent) { + this.sceneEl.addEventListener(this.data.spawnEvent, this.onSpawnEvent); + } }, pause() { this.el.removeEventListener("grab-start", this.onGrabStart); this.el.removeEventListener("grab-end", this.onGrabEnd); + if (this.data.spawnEvent) { + this.sceneEl.removeEventListener(this.data.spawnEvent, this.onSpawnEvent); + } if (this.cooldownTimeout) { clearTimeout(this.cooldownTimeout); @@ -72,6 +108,36 @@ AFRAME.registerComponent("super-spawner", { this.heldEntities.clear(); }, + async onSpawnEvent() { + const controllerCount = this.el.sceneEl.components["input-configurator"].controllerQueue.length; + const using6DOF = controllerCount > 1 && this.el.sceneEl.is("vr-mode"); + const hand = using6DOF ? this.data.superHand : this.data.cursorSuperHand; + + if (this.cooldownTimeout || !hand) { + return; + } + + const entity = addMedia(this.data.src, this.data.template, ObjectContentOrigins.SPAWNER).entity; + + hand.object3D.getWorldPosition(entity.object3D.position); + hand.object3D.getWorldQuaternion(entity.object3D.quaternion); + if (this.data.useCustomSpawnScale) { + entity.object3D.scale.copy(this.data.spawnScale); + } + + this.activateCooldown(); + + await waitForEvent("body-loaded", entity); + + hand.object3D.getWorldPosition(entity.object3D.position); + + if (!using6DOF) { + for (let i = 0; i < this.data.grabEvents.length; i++) { + hand.emit(this.data.grabEvents[i], { targetEntity: entity }); + } + } + }, + async onGrabStart(e) { if (this.cooldownTimeout) { return; @@ -84,13 +150,15 @@ AFRAME.registerComponent("super-spawner", { const thisGrabId = nextGrabId++; this.heldEntities.set(hand, thisGrabId); - const entity = addMedia(this.data.src, ObjectContentOrigins.SPAWNER).entity; + const entity = addMedia(this.data.src, this.data.template, ObjectContentOrigins.SPAWNER).entity; + entity.object3D.position.copy( this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.object3D.position ); entity.object3D.rotation.copy( this.data.useCustomSpawnRotation ? this.data.spawnRotation : this.el.object3D.rotation ); + entity.object3D.scale.copy(this.data.useCustomSpawnScale ? this.data.spawnScale : this.el.object3D.scale); this.activateCooldown(); diff --git a/src/components/tools/drawing-manager.js b/src/components/tools/drawing-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..08d1ef5e0e8b1b0cfacee02905f1ad7ba6502ca7 --- /dev/null +++ b/src/components/tools/drawing-manager.js @@ -0,0 +1,51 @@ +/** + * Drawing Manager + * Manages what networked-drawings are available to pen components + * @namespace drawing + * @component drawing-manager + */ +AFRAME.registerComponent("drawing-manager", { + init() { + this._onComponentInitialized = this._onComponentInitialized.bind(this); + + this.drawingToPen = new Map(); + }, + + remove() { + if (this.drawingEl) { + this.drawingEl.removeEventListener("componentinitialized", this._onComponentInitialized); + } + }, + + _onComponentInitialized(e) { + if (e.detail.name == "networked-drawing") { + this.drawing = this.drawingEl.components["networked-drawing"]; + } + }, + + createDrawing() { + if (!this.drawingEl) { + this.drawingEl = document.createElement("a-entity"); + this.drawingEl.setAttribute("networked", "template: #interactable-drawing"); + this.el.sceneEl.appendChild(this.drawingEl); + + this.drawingEl.addEventListener("componentinitialized", this._onComponentInitialized); + } + }, + + getDrawing(pen) { + //TODO: future handling of multiple drawings + if (this.drawing && (!this.drawingToPen.has(this.drawing) || this.drawingToPen.get(this.drawing) === pen)) { + this.drawingToPen.set(this.drawing, pen); + return this.drawing; + } + + return null; + }, + + returnDrawing(pen) { + if (this.drawingToPen.has(this.drawing) && this.drawingToPen.get(this.drawing) === pen) { + this.drawingToPen.delete(this.drawing); + } + } +}); diff --git a/src/components/tools/networked-drawing.js b/src/components/tools/networked-drawing.js new file mode 100644 index 0000000000000000000000000000000000000000..518de9cd82489111ce6585861df99cfd26776015 --- /dev/null +++ b/src/components/tools/networked-drawing.js @@ -0,0 +1,540 @@ +/* global THREE */ +/** + * Networked Drawing + * Creates procedurally generated 'lines' (or tubes) that are networked. + * @namespace drawing + * @component networked-drawing + */ + +import SharedBufferGeometryManager from "../../utils/sharedbuffergeometrymanager"; + +const MSG_CONFIRM_CONNECT = 0; +const MSG_BUFFER_DATA = 1; +const MSG_BUFFER_DATA_FULL = 2; + +function copyData(fromArray, toArray, fromIndex, toIndex) { + let i = fromIndex - 1; + let j = -1; + while (i + 1 <= toIndex) { + toArray[++j] = fromArray[++i]; + } +} + +AFRAME.registerComponent("networked-drawing", { + schema: { + segments: { default: 8 }, //the number of "sides" the procedural tube should have + defaultRadius: { default: 0.01 }, //the radius of the procedural tube + maxDrawTimeout: { default: 600000 }, //the maximum time a drawn line will live + maxLines: { default: 50 }, //how many lines can persist before lines older than minDrawTime are removed + maxPointsPerLine: { default: 250 } //the max number of points a single line can have + }, + + init() { + this._receiveData = this._receiveData.bind(this); + + this.networkBuffer = []; + + this.sendNetworkBufferQueue = []; + + this.drawStarted = false; + this.lineStarted = false; + this.remoteLineStarted = false; + + this.receivedBufferParts = 0; + this.bufferIndex = 0; + this.connectedToOwner = false; + this.networkBufferInitialized = false; + + const options = { + vertexColors: THREE.VertexColors + }; + + this.color = new THREE.Color(); + this.radius = this.data.defaultRadius; + this.segments = this.data.segments; + + const material = new THREE.MeshStandardMaterial(options); + this.sharedBufferGeometryManager = new SharedBufferGeometryManager(); + // NOTE: 20 is approximate for how many floats per point are added. + // maxLines + 1 because a line can be currently drawing while at maxLines. + // Multiply by 1/3 (0.333) because 3 floats per vertex (x, y, z). + const maxBufferSize = Math.round(this.data.maxPointsPerLine * 20 * (this.data.maxLines + 1) * 0.333); + this.sharedBufferGeometryManager.addSharedBuffer(0, material, THREE.TriangleStripDrawMode, maxBufferSize); + + this.lastPoint = new THREE.Vector3(); + + this.lastSegments = []; + this.currentSegments = []; + for (let x = 0; x < this.segments; x++) { + this.lastSegments[x] = { + position: new THREE.Vector3(), + normal: new THREE.Vector3() + }; + this.currentSegments[x] = { + position: new THREE.Vector3(), + normal: new THREE.Vector3() + }; + } + + this.sharedBuffer = this.sharedBufferGeometryManager.getSharedBuffer(0); + this.drawing = this.sharedBuffer.drawing; + const sceneEl = document.querySelector("a-scene"); + this.scene = sceneEl.object3D; + this.scene.add(this.drawing); + + this.prevIdx = Object.assign({}, this.sharedBuffer.idx); + this.idx = Object.assign({}, this.sharedBuffer.idx); + this.vertexCount = 0; //number of vertices added for current line (used for line deletion). + this.networkBufferCount = 0; //number of items added to networkBuffer for current line (used for line deletion). + this.currentPointCount = 0; //number of points added for current line (used for maxPointsPerLine). + this.networkBufferHistory = []; //tracks vertexCount and networkBufferCount so that lines can be deleted. + + NAF.connection.onConnect(() => { + NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { + this.networkedEl = networkedEl; + this.networkId = NAF.utils.getNetworkId(this.networkedEl); + this.drawingId = "drawing-" + this.networkId; + NAF.connection.subscribeToDataChannel(this.drawingId, this._receiveData); + }); + }); + }, + + remove() { + NAF.connection.unsubscribeToDataChannel(this.drawingId, this._receiveData); + + this.scene.remove(this.drawing); + }, + + tick() { + const connected = NAF.connection.isConnected() && this.networkedEl; + const isMine = connected && NAF.utils.isMine(this.networkedEl); + + if (!this.connectedToOwner && connected) { + const owner = NAF.utils.getNetworkOwner(this.networkedEl); + if (!isMine && NAF.connection.hasActiveDataChannel(owner)) { + NAF.connection.sendDataGuaranteed(owner, this.drawingId, { + type: MSG_CONFIRM_CONNECT, + clientId: NAF.clientId + }); + this.connectedToOwner = true; + } + } + + if (this.networkBuffer.length > 0 && connected) { + if (!isMine) { + this._drawFromNetwork(); + } else if (this.bufferIndex < this.networkBuffer.length) { + this._broadcastDrawing(); + } + } + + //TODO: handle possibility that a clientId gets stuck in sendNetworkBufferQueue + //if that client disconnects before this executes and an activeDataChannel is opened. + if (isMine && this.sendNetworkBufferQueue.length > 0) { + const connected = []; + for (let i = 0; i < this.sendNetworkBufferQueue.length; i++) { + if (NAF.connection.hasActiveDataChannel(this.sendNetworkBufferQueue[i])) { + connected.push(this.sendNetworkBufferQueue[i]); + } + } + for (let j = 0; j < connected.length; j++) { + const pos = this.sendNetworkBufferQueue.indexOf(connected[j]); + this._sendNetworkBuffer(connected[j]); + this.sendNetworkBufferQueue.splice(pos, 1); + } + } + + this._deleteLines(); + }, + + _broadcastDrawing: (() => { + const copyArray = []; + return function() { + copyArray.length = 0; + copyData(this.networkBuffer, copyArray, this.bufferIndex, this.networkBuffer.length - 1); + this.bufferIndex = this.networkBuffer.length; + NAF.connection.broadcastDataGuaranteed(this.drawingId, { type: MSG_BUFFER_DATA, buffer: copyArray }); + }; + })(), + + _drawFromNetwork: (() => { + const position = new THREE.Vector3(); + const direction = new THREE.Vector3(); + const normal = new THREE.Vector3(); + return function() { + const head = this.networkBuffer[0]; + let didWork = false; + while (head != null && this.networkBuffer.length >= 10) { + position.set(this.networkBuffer[0], this.networkBuffer[1], this.networkBuffer[2]); + direction.set(this.networkBuffer[3], this.networkBuffer[4], this.networkBuffer[5]); + this.radius = Math.round(direction.length() * 1000) / 1000; //radius is encoded as length of direction vector + direction.normalize(); + normal.set(this.networkBuffer[6], this.networkBuffer[7], this.networkBuffer[8]); + this.color.setHex(Math.round(normal.length()) - 1); //color is encoded as length of normal vector + normal.normalize(); + + this.networkBuffer.splice(0, 9); + + if (!this.remoteLineStarted) { + this.startDraw(position, direction, normal); + this.remoteLineStarted = true; + } + + if (this.networkBuffer[0] === null) { + this._endDraw(position, direction, normal); + this.remoteLineStarted = false; + this.networkBuffer.shift(); + } else { + this._draw(position, direction, normal); + didWork = true; + } + } + if (didWork) this._updateBuffer(); + }; + })(), + + _deleteLines() { + const length = this.networkBufferHistory.length; + if (length > 0) { + const now = Date.now(); + const time = this.networkBufferHistory[0].time; + if (length > this.data.maxLines || time + this.data.maxDrawTimeout <= now) { + const datum = this.networkBufferHistory[0]; + if (length > 1) { + datum.idxLength += 2 - (this.segments % 2); + this.networkBufferHistory[1].idxLength -= 2 - (this.segments % 2); + } + this.idx.position = datum.idxLength; + this.idx.uv = datum.idxLength; + this.idx.normal = datum.idxLength; + this.idx.color = datum.idxLength; + this.sharedBuffer.remove(this.prevIdx, this.idx); + this.networkBufferHistory.shift(); + if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) { + this.networkBuffer.splice(0, datum.networkBufferCount); + this.bufferIndex -= datum.networkBufferCount; + } + } + } + }, + + _sendNetworkBuffer: (() => { + const copyArray = []; + //This number needs to be approx. < ~6000 based on napkin math + //see: https://github.com/webrtc/adapter/blob/682e0f2439e139da6c0c406370eae820637b8sc1a/src/js/common_shim.js#L157 + const chunkAmount = 3000; + return function(clientId) { + if (NAF.utils.isMine(this.networkedEl)) { + if (this.networkBuffer.length <= chunkAmount) { + NAF.connection.sendDataGuaranteed(clientId, this.drawingId, { + type: MSG_BUFFER_DATA_FULL, + parts: 1, + buffer: this.networkBuffer + }); + } else { + let start = 0; + let end = 0; + while (end < this.networkBuffer.length) { + end = Math.min(end + chunkAmount, this.networkBuffer.length); + copyArray.length = 0; + copyData(this.networkBuffer, copyArray, start, end - 1); + start = end; + NAF.connection.sendDataGuaranteed(clientId, this.drawingId, { + type: MSG_BUFFER_DATA_FULL, + parts: Math.ceil(this.networkBuffer.length / chunkAmount), + buffer: copyArray + }); + } + } + } + }; + })(), + + _receiveData(_, dataType, data) { + switch (data.type) { + case MSG_CONFIRM_CONNECT: + this.sendNetworkBufferQueue.push(data.clientId); + break; + case MSG_BUFFER_DATA: + if (this.networkBufferInitialized) { + this.networkBuffer.push.apply(this.networkBuffer, data.buffer); + } + break; + case MSG_BUFFER_DATA_FULL: + this.networkBuffer.push.apply(this.networkBuffer, data.buffer); + if (++this.receivedBufferParts >= data.parts) { + this.networkBufferInitialized = true; + } + break; + } + }, + + getLastPoint() { + return this.lastPoint; + }, + + startDraw(position, direction, normal, color, radius) { + if (!NAF.connection.isConnected()) { + return; + } + + this.drawStarted = true; + + if (color) { + this.color.set(color); + } + if (radius) this.radius = radius; + + this.lastPoint.copy(position); + this._addToNetworkBuffer(position, direction, normal); + }, + + draw(position, direction, normal, color, radius) { + if (!NAF.connection.isConnected() || !this.drawStarted) { + return; + } + + if (color && color != "#" + this.color.getHexString().toUpperCase()) { + this.color.set(color); + } + if (radius) this.radius = radius; + + this._addToNetworkBuffer(position, direction, normal); + this._draw(position, direction, normal); + + this._updateBuffer(); + }, + + _draw: (() => { + const capNormal = new THREE.Vector3(); + return function(position, direction, normal, radiusMultiplier = 1.0) { + if (!this.lineStarted) { + this._generateSegments(this.lastSegments, position, direction, normal, this.radius * radiusMultiplier); + + if (this.networkBufferHistory.length === 0) { + //start with CW faceculling order + this._addDegenerateTriangle(); + } else { + //only do the following if the sharedBuffer is not empty + this._restartPrimitive(); + this._addDegenerateTriangle(); + if (this.segments % 2 === 0) { + //flip faceculling order if even numbered segments + this._addDegenerateTriangle(); + } + } + + //get normal for tip of cap + capNormal.copy(direction).negate(); + //get normals for rim of cap + for (let i = 0; i < this.segments; i++) { + this.lastSegments[i].normal.add(capNormal).multiplyScalar(0.5); + } + + this._drawCap(this.lastPoint, this.lastSegments, capNormal); + if (this.segments % 2 !== 0) { + //flip faceculling order if odd numbered segments + this._addDegenerateTriangle(); + } + + this.lineStarted = true; + } else { + this._generateSegments(this.currentSegments, position, direction, normal, this.radius * radiusMultiplier); + this._drawCylinder(); + + if (this.currentPointCount > this.data.maxPointsPerLine) { + this._drawEndCap(position, direction); + this._endLine(); + } + } + this.lastPoint.copy(position); + }; + })(), + + endDraw(position, direction, normal) { + this._endDraw(position, direction, normal); + this._updateBuffer(); + }, + + _endDraw(position, direction, normal) { + if (!this.lineStarted && this.drawStarted) { + this._drawPoint(position); + } else if (this.lineStarted && this.drawStarted) { + this._addToNetworkBuffer(position, direction, normal); + this._draw(position, direction, normal); + this._drawEndCap(position, direction); + } + this._endLine(); + }, + + _drawEndCap: (() => { + const projectedDirection = new THREE.Vector3(); + const projectedPoint = new THREE.Vector3(); + return function(position, direction) { + if (this.lineStarted && this.drawStarted) { + projectedDirection.copy(direction).multiplyScalar(this.radius); + projectedPoint.copy(position).add(projectedDirection); + this._addDegenerateTriangle(); //flip faceculling order before drawing end-cap + this._drawCap(projectedPoint, this.lastSegments, direction); + } + }; + })(), + + _endLine() { + if (!this.drawStarted) return; + + if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) this._pushToNetworkBuffer(null); + + const datum = { + networkBufferCount: this.networkBufferCount, + idxLength: this.vertexCount - 1, + time: Date.now() + }; + this.networkBufferHistory.push(datum); + this.vertexCount = 0; + this.networkBufferCount = 0; + this.currentPointCount = 0; + this.lineStarted = false; + this.drawStarted = false; + }, + + _addToNetworkBuffer: (() => { + const copyDirection = new THREE.Vector3(); + const copyNormal = new THREE.Vector3(); + return function(position, direction, normal) { + if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) { + ++this.currentPointCount; + this._pushToNetworkBuffer(position.x); + this._pushToNetworkBuffer(position.y); + this._pushToNetworkBuffer(position.z); + copyDirection.copy(direction); + copyDirection.setLength(this.radius); //encode radius as length of direction vector + this._pushToNetworkBuffer(copyDirection.x); + this._pushToNetworkBuffer(copyDirection.y); + this._pushToNetworkBuffer(copyDirection.z); + copyNormal.copy(normal); + copyNormal.setLength(this.color.getHex() + 1); //encode color as length, add one in case color is black + this._pushToNetworkBuffer(copyNormal.x); + this._pushToNetworkBuffer(copyNormal.y); + this._pushToNetworkBuffer(copyNormal.z); + } + }; + })(), + + _pushToNetworkBuffer(val) { + ++this.networkBufferCount; + this.networkBuffer.push(val); + }, + + //draw a cylinder from last to current segments + _drawCylinder() { + //average the normals with the normals from the lastSegment + //not a perfect normal calculation, but works well enough + for (let i = 0; i < this.segments; i++) { + this.currentSegments[i].normal.add(this.lastSegments[i].normal).multiplyScalar(0.5); + } + + for (let i = 0; i != this.segments + 1; i++) { + this._addVertex(this.lastSegments[i % this.segments]); + this._addVertex(this.currentSegments[i % this.segments]); + } + + for (let i = 0; i < this.segments; i++) { + this.lastSegments[i].position.copy(this.currentSegments[i].position); + this.lastSegments[i].normal.copy(this.currentSegments[i].normal); + } + }, + + //draw a standalone point in space + _drawPoint: (() => { + const up = new THREE.Vector3(0, 1, 0); + const down = new THREE.Vector3(0, -1, 0); + const left = new THREE.Vector3(1, 0, 0); + const projectedDirection = new THREE.Vector3(); + const projectedPoint = new THREE.Vector3(); + return function(position) { + projectedDirection.copy(up).multiplyScalar(this.radius * 0.75); + projectedPoint.copy(position).add(projectedDirection); + this.lastPoint.copy(projectedPoint); + + projectedDirection.copy(up).multiplyScalar(this.radius * 0.5); + projectedPoint.copy(position).add(projectedDirection); + this._draw(projectedPoint, down, left, 0.75); + + this._draw(position, down, left); + + projectedDirection.copy(down).multiplyScalar(this.radius * 0.5); + projectedPoint.copy(position).add(projectedDirection); + this._draw(projectedPoint, down, left, 0.75); + + projectedDirection.copy(down).multiplyScalar(this.radius * 0.75); + projectedPoint.copy(position).add(projectedDirection); + + this._addDegenerateTriangle(); //discarded + this._drawCap(projectedPoint, this.lastSegments, down); + }; + })(), + + //draw a cap to start/end a line + _drawCap(point, segments, normal) { + let segmentIndex = 0; + for (let i = 0; i < this.segments * 2 - (this.segments % 2); i++) { + if ((i - 2) % 4 === 0) { + this._addVertex({ position: point, normal: normal }); + } else { + this._addVertex(segments[segmentIndex % this.segments]); + if ((i + 1) % 5 !== 0) { + ++segmentIndex; + } + } + } + }, + + _restartPrimitive() { + this.sharedBuffer.restartPrimitive(); + ++this.vertexCount; + }, + + _updateBuffer() { + this.sharedBuffer.update(); + }, + + _addVertex(segment) { + const point = segment.position; + const normal = segment.normal; + this.sharedBuffer.addVertex(point.x, point.y, point.z); + this.sharedBuffer.addColor(this.color.r, this.color.g, this.color.b); + + if (normal) { + this.sharedBuffer.addNormal(normal.x, normal.y, normal.z); + } else { + ++this.sharedBuffer.idx.normal; + } + + ++this.sharedBuffer.idx.uv; + ++this.vertexCount; + }, + + _addDegenerateTriangle() { + this._addVertex(this.lastSegments[0]); + }, + + //calculate the segments for a given point + _generateSegments(segmentsList, point, forward, up, radius) { + const angleIncrement = (Math.PI * 2) / this.segments; + for (let i = 0; i < this.segments; i++) { + const segment = segmentsList[i].position; + this._rotatePointAroundAxis(segment, point, forward, up, angleIncrement * i, radius); + segmentsList[i].normal.subVectors(segment, point).normalize(); + } + }, + + _rotatePointAroundAxis: (() => { + const calculatedDirection = new THREE.Vector3(); + return function(out, point, axis, up, angle, radius) { + calculatedDirection.copy(up); + calculatedDirection.applyAxisAngle(axis, angle); + out.copy(point).add(calculatedDirection.normalize().multiplyScalar(radius)); + }; + })() +}); diff --git a/src/components/tools/pen.js b/src/components/tools/pen.js new file mode 100644 index 0000000000000000000000000000000000000000..855c94e0a1d11f5905dd20f55acdda27ebad227d --- /dev/null +++ b/src/components/tools/pen.js @@ -0,0 +1,188 @@ +/** + * Pen tool + * A tool that allows drawing on networked-drawing components. + * @namespace drawing + * @component pen + */ + +function almostEquals(epsilon, u, v) { + return Math.abs(u.x - v.x) < epsilon && Math.abs(u.y - v.y) < epsilon && Math.abs(u.z - v.z) < epsilon; +} + +AFRAME.registerComponent("pen", { + schema: { + drawFrequency: { default: 5 }, //frequency of polling for drawing points + minDistanceBetweenPoints: { default: 0.01 }, //minimum distance to register new drawing point + camera: { type: "selector" }, + drawingManager: { type: "string" }, + color: { type: "color", default: "#FF0033" }, + availableColors: { + default: [ + "#FF0033", + "#FFFF00", + "#0099FF", + "#00FF33", + "#9900FF", + "#FF6600", + "#8D5524", + "#C68642", + "#E0AC69", + "#F1C27D", + "#FFDBAC", + "#FFFFFF", + "#222222", + "#111111", + "#000000" + ] + }, + radius: { default: 0.01 }, //drawing geometry radius + minRadius: { default: 0.005 }, + maxRadius: { default: 0.2 } + }, + + init() { + this._stateAdded = this._stateAdded.bind(this); + this._stateRemoved = this._stateRemoved.bind(this); + + this.timeSinceLastDraw = 0; + + this.lastPosition = new THREE.Vector3(); + this.lastPosition.copy(this.el.object3D.position); + + this.direction = new THREE.Vector3(1, 0, 0); + + this.currentDrawing = null; + + this.normal = new THREE.Vector3(); + + this.worldPosition = new THREE.Vector3(); + + this.colorIndex = 0; + + this.grabbed = false; + }, + + play() { + this.drawingManager = document.querySelector(this.data.drawingManager).components["drawing-manager"]; + this.drawingManager.createDrawing(); + + this.el.parentNode.addEventListener("stateadded", this._stateAdded); + this.el.parentNode.addEventListener("stateremoved", this._stateRemoved); + }, + + pause() { + this.el.parentNode.removeEventListener("stateadded", this._stateAdded); + this.el.parentNode.removeEventListener("stateremoved", this._stateRemoved); + }, + + update(prevData) { + if (prevData.color != this.data.color) { + this.el.setAttribute("color", this.data.color); + } + if (prevData.radius != this.data.radius) { + this.el.setAttribute("radius", this.data.radius); + } + }, + + tick(t, dt) { + this.el.object3D.getWorldPosition(this.worldPosition); + + if (!almostEquals(0.005, this.worldPosition, this.lastPosition)) { + this.direction.subVectors(this.worldPosition, this.lastPosition).normalize(); + this.lastPosition.copy(this.worldPosition); + } + + if (this.currentDrawing) { + const time = this.timeSinceLastDraw + dt; + if ( + time >= this.data.drawFrequency && + this.currentDrawing.getLastPoint().distanceTo(this.worldPosition) >= this.data.minDistanceBetweenPoints + ) { + this._getNormal(this.normal, this.worldPosition, this.direction); + this.currentDrawing.draw(this.worldPosition, this.direction, this.normal, this.data.color, this.data.radius); + } + + this.timeSinceLastDraw = time % this.data.drawFrequency; + } + }, + + //helper function to get normal of direction of drawing cross direction to camera + _getNormal: (() => { + const directionToCamera = new THREE.Vector3(); + return function(normal, position, direction) { + directionToCamera.subVectors(position, this.data.camera.object3D.position).normalize(); + normal.crossVectors(direction, directionToCamera); + }; + })(), + + _startDraw() { + this.currentDrawing = this.drawingManager.getDrawing(this); + if (this.currentDrawing) { + this.el.object3D.getWorldPosition(this.worldPosition); + this._getNormal(this.normal, this.worldPosition, this.direction); + + this.currentDrawing.startDraw(this.worldPosition, this.direction, this.normal, this.data.color, this.data.radius); + } + }, + + _endDraw() { + if (this.currentDrawing) { + this.timeSinceLastDraw = 0; + this.el.object3D.getWorldPosition(this.worldPosition); + this._getNormal(this.normal, this.worldPosition, this.direction); + this.currentDrawing.endDraw(this.worldPosition, this.direction, this.normal); + this.drawingManager.returnDrawing(this); + this.currentDrawing = null; + } + }, + + _changeColor(mod) { + this.colorIndex = (this.colorIndex + mod + this.data.availableColors.length) % this.data.availableColors.length; + this.data.color = this.data.availableColors[this.colorIndex]; + this.el.setAttribute("color", this.data.color); + }, + + _changeRadius(mod) { + this.data.radius = Math.max(this.data.minRadius, Math.min(this.data.radius + mod, this.data.maxRadius)); + this.el.setAttribute("radius", this.data.radius); + }, + + _stateAdded(evt) { + switch (evt.detail) { + case "activated": + this._startDraw(); + break; + case "colorNext": + this._changeColor(1); + break; + case "colorPrev": + this._changeColor(-1); + break; + case "radiusUp": + this._changeRadius(this.data.minRadius); + break; + case "radiusDown": + this._changeRadius(-this.data.minRadius); + break; + case "grabbed": + this.grabbed = true; + break; + default: + break; + } + }, + + _stateRemoved(evt) { + switch (evt.detail) { + case "activated": + this._endDraw(); + break; + case "grabbed": + this.grabbed = false; + this._endDraw(); + break; + default: + break; + } + } +}); diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index 71b9cbff5b20df5eb8935664f1ae45f5b28bb478..5dde948e65abc8b1ce5ea461a4ac38994047b88c 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -1,5 +1,6 @@ import "./components/gltf-model-plus"; +AFRAME.GLTFModelPlus.registerComponent("duck", "duck"); AFRAME.GLTFModelPlus.registerComponent("quack", "quack"); AFRAME.GLTFModelPlus.registerComponent("sound", "sound"); AFRAME.GLTFModelPlus.registerComponent("collision-filter", "collision-filter"); diff --git a/src/hub.html b/src/hub.html index 9b9161d2a27f6505f9d105b0bd3a1d9dde1831a5..787984a1889fecaf5f2aaa53ad3efe53c539e8c4 100644 --- a/src/hub.html +++ b/src/hub.html @@ -56,6 +56,8 @@ <img id="freeze-off-hover" crossorigin="anonymous" src="./assets/hud/freeze_off-hover.png"> <img id="freeze-on" crossorigin="anonymous" src="./assets/hud/freeze_on.png"> <img id="freeze-on-hover" crossorigin="anonymous" src="./assets/hud/freeze_on-hover.png"> + <img id="spawn-pen" crossorigin="anonymous" src="./assets/hud/spawn_pen.png"> + <img id="spawn-pen-hover" crossorigin="anonymous" src="./assets/hud/spawn_pen-hover.png"> <a-asset-item id="botdefault" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotDefault_Avatar-9f71f8ff22.gltf"></a-asset-item> <a-asset-item id="botbobo" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotBobo_Avatar-f9740a010b.gltf"></a-asset-item> @@ -69,7 +71,6 @@ <a-asset-item id="botwoody" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotWoody_Avatar-0140485a23.gltf"></a-asset-item> <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item> - <a-asset-item id="interactable-duck" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/interactables/Ducky/DuckyMesh-438ff8e022.gltf"></a-asset-item> <a-asset-item id="quack" src="./assets/sfx/quack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> <a-asset-item id="specialquack" src="./assets/sfx/specialquack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> @@ -156,6 +157,8 @@ position-at-box-shape-border="target:.delete-button" destroy-at-extreme-distances rotation + activatable__increase-scale="buttonStartEvents: scroll_right; buttonEndEvents: horizontal_scroll_release; activatedState: scaleUp;" + activatable__decrease-scale="buttonStartEvents: scroll_left; buttonEndEvents: horizontal_scroll_release; activatedState: scaleDown;" > <!-- HACK: rotation component above is required for its side effect of setting YXZ order --> <a-entity class="delete-button" visible-while-frozen> @@ -165,6 +168,46 @@ </a-entity> </template> + <template id="pen-interactable"> + <a-entity + class="interactable toggle" + super-networked-interactable="counter: #pen-counter;" + body="type: dynamic; shape: none; mass: 1;" + grabbable-toggle="maxGrabbers: 1;" + sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;" + hoverable + activatable__draw-hand="buttonStartEvents: secondary_hand_grab; buttonEndEvents: secondary_hand_release;" + activatable__draw-cursor="buttonStartEvents: secondary-cursor-grab; buttonEndEvents: secondary-cursor-release;" + activatable__color-next="buttonStartEvents: next_color, scroll_right; buttonEndEvents: thumb_up, secondary_hand_release, horizontal_scroll_release; activatedState: colorNext;" + activatable__color-prev="buttonStartEvents: previous_color, scroll_left; buttonEndEvents: thumb_up, secondary_hand_release, horizontal_scroll_release; activatedState: colorPrev;" + activatable__increase-radius="buttonStartEvents: increase_radius, scroll_up; buttonEndEvents: thumb_up, secondary_hand_release, vertical_scroll_release; activatedState: radiusUp;" + activatable__decrease-radius="buttonStartEvents: decrease_radius, scroll_down; buttonEndEvents: thumb_up, secondary_hand_release, vertical_scroll_release; activatedState: radiusDown;" + scale="0.5 0.5 0.5" + > + <a-sphere + id="pen" + scale="1.5, 1.5, 1.5" + position="0 -0.18 0" + radius="0.02" + color="#FF0033" + pen="camera: #player-camera; drawingManager: #drawing-manager" + segments-width="16" + segments-height="12" + ></a-sphere> + <a-entity class="delete-button" visible-while-frozen> + <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> + <a-entity text=" value:Delete; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> + </a-entity> + </a-entity> + </template> + + + <template id="interactable-drawing"> + <a-entity + networked-drawing + ></a-entity> + </template> + <template id="paging-toolbar"> <a-entity class="paging-toolbar" visible-to-owner> <a-entity class="prev-button" position="-0.3 0 0"> @@ -199,11 +242,18 @@ <a-mixin id="controller-super-hands" super-hands=" - colliderEvent: collisions; colliderEventProperty: els; - colliderEndEvent: collisions; colliderEndEventProperty: clearedEls; - grabStartButtons: hand_grab; grabEndButtons: hand_release; - stretchStartButtons: hand_grab; stretchEndButtons: hand_release; - dragDropStartButtons: hand_grab; dragDropEndButtons: hand_release;" + colliderEvent: collisions; + colliderEventProperty: els; + colliderEndEvent: collisions; + colliderEndEventProperty: clearedEls; + grabStartButtons: primary_hand_grab, secondary_hand_grab; + grabEndButtons: primary_hand_release, secondary_hand_release; + stretchStartButtons: primary_hand_grab, secondary_hand_grab; + stretchEndButtons: primary_hand_release, secondary_hand_release; + dragDropStartButtons: hand_grab, secondary_hand_grab; + dragDropEndButtons: hand_release, secondary_hand_release; + activateStartButtons: secondary_hand_grab, next_color, previous_color, increase_radius, decrease_radius, scroll_up, scroll_down, scroll_left, scroll_right; + activateEndButtons: secondary_hand_release, vertical_scroll_release, horizontal_scroll_release, thumb_up;" collision-filter="collisionForces: false" physics-collider ></a-mixin> @@ -212,12 +262,16 @@ <!-- Interactables --> <a-entity id="media-counter" networked-counter="max: 10;"></a-entity> + <a-entity id="pen-counter" networked-counter="max: 10;"></a-entity> + + <a-entity id="drawing-manager" drawing-manager></a-entity> + <a-entity id="cursor-controller" cursor-controller=" cursor: #cursor; - camera: #player-camera; " - raycaster="objects: .collidable, .interactable, .ui; far: 3;" + camera: #player-camera; + objects: .collidable, .interactable, .ui;" line="visible: false; color: white; opacity: 0.2;" ></a-entity> @@ -225,17 +279,21 @@ id="cursor" material="depthTest: false; opacity:0.9;" radius="0.02" + segments-width="9" + segments-height="9" static-body="shape: sphere;" collision-filter="collisionForces: false" super-hands=" - colliderEvent: raycaster-intersection; colliderEventProperty: els; - colliderEndEvent: raycaster-intersection-cleared; colliderEndEventProperty: clearedEls; - grabStartButtons: cursor-grab; grabEndButtons: cursor-release; - stretchStartButtons: cursor-grab; stretchEndButtons: cursor-release; - dragDropStartButtons: cursor-grab; dragDropEndButtons: cursor-release;" - segments-height="9" - segments-width="9" - event-repeater="events: raycaster-intersection, raycaster-intersection-cleared; eventSource: #cursor-controller" + colliderEvent: raycaster-intersection; + colliderEndEvent: raycaster-intersection-cleared; + grabStartButtons: cursor-grab, primary_hand_grab, secondary_hand_grab; + grabEndButtons: cursor-release, primary_hand_release, secondary_hand_release; + stretchStartButtons: cursor-grab, primary_hand_grab, secondary_hand_grab; + stretchEndButtons: cursor-release, primary_hand_release, secondary_hand_release; + dragDropStartButtons: cursor-grab, primary_hand_grab, secondary_hand_grab; + dragDropEndButtons: cursor-release, primary_hand_release, secondary_hand_release; + activateStartButtons: secondary-cursor-grab, secondary_hand_grab, next_color, previous_color, increase_radius, decrease_radius, scroll_up, scroll_down, scroll_left, scroll_right; + activateEndButtons: secondary-cursor-release, secondary_hand_release, vertical_scroll_release, horizontal_scroll_release, thumb_up;" ></a-sphere> <!-- Player Rig --> @@ -260,7 +318,7 @@ <a-rounded height="0.13" width="0.48" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Mute Mic; activeTooltipText: Unmute Mic; image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" position="-0.17 0 0.001" class="ui hud mic" material="alphaTest:0.1;"></a-image> <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Pause; activeTooltipText: Resume; image: #freeze-off; hoverImage: #freeze-off-hover; activeImage: #freeze-on; activeHoverImage: #freeze-on-hover" scale="0.2 0.2 0.2" position="0 0 0.005" class="ui hud freeze"></a-image> - <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Enable Bubble; activeTooltipText: Disable Bubble; image: #bubble-off; hoverImage: #bubble-off-hover; activeImage: #bubble-on; activeHoverImage: #bubble-on-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="ui hud bubble" material="alphaTest:0.1;"></a-image> + <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Spawn Pen; activeTooltipText: Spawn Pen; image: #spawn-pen; hoverImage: #spawn-pen-hover; activeImage: #spawn-pen; activeHoverImage: #spawn-pen-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="ui hud pen" material="alphaTest:0.1;"></a-image> <a-rounded visible="false" id="hud-tooltip" height="0.08" width="0.3" color="#000000" position="-0.15 -0.2 0" rotation="-20 0 0" radius="0.025" opacity="0.35" class="hud bg"> <a-entity text="value: Mute Mic; align:center;" position="0.15 0.04 0.001" ></a-entity> </a-rounded> @@ -272,7 +330,7 @@ class="camera" camera position="0 1.6 0" - personal-space-bubble="radius: 0.4" + personal-space-bubble="radius: 0.4;" pitch-yaw-rotator > <a-entity @@ -329,6 +387,7 @@ missOpacity: 0.1; curveShootingSpeed: 12;" haptic-feedback + event-repeater="events: haptic_pulse; eventSource: #cursor" body="type: static; shape: none;" mixin="controller-super-hands" controls-shape-offset @@ -376,6 +435,14 @@ static-body="shape: none;" ></a-entity> + <a-entity + super-spawner=" + template: #pen-interactable; + src: https://asset-bundles-prod.reticulum.io/interactables/DrawingPen/DrawingPen-34fb4aee27.gltf; + spawnEvent: spawn_pen; + superHand: #player-right-controller; + cursorSuperHand: #cursor;" + ></a-entity> </a-scene> <div id="ui-root"></div> diff --git a/src/hub.js b/src/hub.js index 0d5e2837d3d6d36ec116ba2d01baecaa96f2cc4a..6bc7382c84b8ec3cf3af71914a592ca853f76d1b 100644 --- a/src/hub.js +++ b/src/hub.js @@ -127,12 +127,18 @@ import "./components/event-repeater"; import "./components/controls-shape-offset"; import "./components/duck"; import "./components/quack"; +import "./components/grabbable-toggle"; import "./components/cardboard-controls"; import "./components/cursor-controller"; import "./components/nav-mesh-helper"; +import "./systems/tunnel-effect"; + +import "./components/tools/pen"; +import "./components/tools/networked-drawing"; +import "./components/tools/drawing-manager"; import registerNetworkSchemas from "./network-schemas"; import { inGameActions, config as inputConfig } from "./input-mappings"; @@ -140,6 +146,7 @@ import registerTelemetry from "./telemetry"; import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; + import qsTruthy from "./utils/qs_truthy"; const isBotMode = qsTruthy("bot"); @@ -319,8 +326,9 @@ const onReady = async () => { }); const offset = { x: 0, y: 0, z: -1.5 }; + const spawnMediaInfrontOfPlayer = (src, contentOrigin) => { - const { entity, orientation } = addMedia(src, contentOrigin, true); + const { entity, orientation } = addMedia(src, "#interactable-media", contentOrigin, true); orientation.then(or => { entity.setAttribute("offset-relative-to", { @@ -553,7 +561,7 @@ const onReady = async () => { if (/\.gltf/i.test(sceneUrl) || /\.glb/i.test(sceneUrl)) { const resolved = await resolveMedia(sceneUrl, false, 0); const gltfEl = document.createElement("a-entity"); - gltfEl.setAttribute("gltf-model-plus", { src: resolved.raw, inflate: true }); + gltfEl.setAttribute("gltf-model-plus", { src: resolved.raw, useCache: false, inflate: true }); gltfEl.addEventListener("model-loaded", () => initialEnvironmentEl.emit("bundleloaded")); initialEnvironmentEl.appendChild(gltfEl); } else { diff --git a/src/index.js b/src/index.js index 7fddba12131a6d46fea444a42b2001acfe7afeb7..fa64e8923edfb8f103f8ac0d98342e759ad59304 100644 --- a/src/index.js +++ b/src/index.js @@ -3,17 +3,23 @@ import React from "react"; import ReactDOM from "react-dom"; import registerTelemetry from "./telemetry"; import HomeRoot from "./react-components/home-root"; -import InfoDialog from "./react-components/info-dialog.js"; const qs = new URLSearchParams(location.search); registerTelemetry(); -ReactDOM.render( +const { pathname } = document.location; +const sceneId = qs.get("scene_id") || (pathname.startsWith("/scenes/") && pathname.substring(1).split("/")[1]); + +const root = ( <HomeRoot initialEnvironment={qs.get("initial_environment")} - dialogType={ - qs.has("list_signup") ? InfoDialog.dialogTypes.updates : qs.has("report") ? InfoDialog.dialogTypes.report : null - } - />, - document.getElementById("home-root") + sceneId={sceneId} + authVerify={qs.has("auth_topic")} + authTopic={qs.get("auth_topic")} + authToken={qs.get("auth_token")} + authOrigin={qs.get("auth_origin")} + listSignup={qs.has("list_signup")} + report={qs.has("report")} + /> ); +ReactDOM.render(root, document.getElementById("home-root")); diff --git a/src/input-mappings.js b/src/input-mappings.js index ccf44110bc74e38ab80de143c11f2dcad50d2a5c..6b1b4e955b95acfd13c469804c2f1478fd1435a1 100644 --- a/src/input-mappings.js +++ b/src/input-mappings.js @@ -54,41 +54,37 @@ const config = { trackpad_dpad4_pressed_center_down: { right: "action_primary_down" }, trackpad_dpad4_pressed_north_down: { right: "action_primary_down" }, trackpad_dpad4_pressed_south_down: { right: "action_primary_down" }, - trackpadup: { right: "action_primary_up" }, + trackpadup: { left: "action_primary_up", right: "action_primary_up" }, menudown: "thumb_down", menuup: "thumb_up", - gripdown: ["action_grab", "middle_ring_pinky_down"], - gripup: ["action_release", "middle_ring_pinky_up"], + gripdown: ["primary_action_grab", "middle_ring_pinky_down"], + gripup: ["primary_action_release", "middle_ring_pinky_up"], trackpadtouchstart: "thumb_down", trackpadtouchend: "thumb_up", - triggerdown: ["action_grab", "index_down"], - triggerup: ["action_release", "index_up"], - scroll: { right: "move_duck" } + triggerdown: ["secondary_action_grab", "index_down"], + triggerup: ["secondary_action_release", "index_up"], + scroll: { left: "scroll_move", right: "scroll_move" } }, "oculus-touch-controls": { - joystick_dpad4_west: { - right: "snap_rotate_left" - }, - joystick_dpad4_east: { - right: "snap_rotate_right" - }, - gripdown: ["action_grab", "middle_ring_pinky_down"], - gripup: ["action_release", "middle_ring_pinky_up"], - abuttontouchstart: "thumb_down", + joystick_dpad4_west: { right: "snap_rotate_left" }, + joystick_dpad4_east: { right: "snap_rotate_right" }, + gripdown: ["primary_action_grab", "middle_ring_pinky_down"], + gripup: ["primary_action_release", "middle_ring_pinky_up"], + abuttontouchstart: ["thumb_down", "increase_radius"], abuttontouchend: "thumb_up", - bbuttontouchstart: "thumb_down", + bbuttontouchstart: ["thumb_down", "decrease_radius"], bbuttontouchend: "thumb_up", - xbuttontouchstart: "thumb_down", + xbuttontouchstart: ["thumb_down", "increase_radius"], xbuttontouchend: "thumb_up", - ybuttontouchstart: "thumb_down", + ybuttontouchstart: ["thumb_down", "decrease_radius"], ybuttontouchend: "thumb_up", - surfacetouchstart: "thumb_down", + surfacetouchstart: ["thumb_down", "next_color"], surfacetouchend: "thumb_up", thumbsticktouchstart: "thumb_down", thumbsticktouchend: "thumb_up", - triggerdown: ["action_grab", "index_down"], - triggerup: ["action_release", "index_up"], - "axismove.reverseY": { left: "move", right: "move_duck" }, + triggerdown: ["secondary_action_grab", "index_down"], + triggerup: ["secondary_action_release", "index_up"], + "axismove.reverseY": { left: "move", right: "scroll_move" }, abuttondown: "action_primary_down", abuttonup: "action_primary_up" }, @@ -103,16 +99,16 @@ const config = { joystick_dpad4_pressed_west_down: { right: "snap_rotate_left" }, joystick_dpad4_pressed_east_down: { right: "snap_rotate_right" }, trackpaddown: { right: "action_primary_down" }, - trackpadup: { right: "action_primary_up" }, + trackpadup: { left: "action_primary_up", right: "action_primary_up" }, menudown: "thumb_down", menuup: "thumb_up", - gripdown: ["action_grab", "middle_ring_pinky_down"], - gripup: ["action_release", "middle_ring_pinky_up"], + gripdown: ["primary_action_grab", "middle_ring_pinky_down"], + gripup: ["primary_action_release", "middle_ring_pinky_up"], trackpadtouchstart: "thumb_down", trackpadtouchend: "thumb_up", - triggerdown: ["action_grab", "index_down"], - triggerup: ["action_release", "index_up"], - axisMoveWithDeadzone: { left: "move", right: "move_duck" } + triggerdown: ["secondary_action_grab", "index_down"], + triggerup: ["secondary_action_release", "index_up"], + axisMoveWithDeadzone: { left: "move", right: "scroll_move" } }, "daydream-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", @@ -121,7 +117,7 @@ const config = { trackpad_dpad4_pressed_north_down: ["action_primary_down"], trackpad_dpad4_pressed_south_down: ["action_primary_down"], trackpadup: ["action_primary_up"], - axisMoveWithDeadzone: "move_duck" + axisMoveWithDeadzone: "scroll_move" }, "gearvr-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", @@ -130,9 +126,9 @@ const config = { trackpad_dpad4_pressed_north_down: ["action_primary_down"], trackpad_dpad4_pressed_south_down: ["action_primary_down"], trackpadup: ["action_primary_up"], - triggerdown: ["action_primary_down"], - triggerup: ["action_primary_up"], - scroll: "move_duck" + triggerdown: ["action_secondary_down"], + triggerup: ["action_secondary_up"], + scroll: "scroll_move" }, "oculus-go-controls": { trackpad_dpad4_pressed_west_down: "snap_rotate_left", @@ -141,9 +137,9 @@ const config = { trackpad_dpad4_pressed_north_down: ["action_primary_down"], trackpad_dpad4_pressed_south_down: ["action_primary_down"], trackpadup: ["action_primary_up"], - triggerdown: ["action_primary_down"], - triggerup: ["action_primary_up"], - scroll: "move_duck" + triggerdown: ["action_secondary_down"], + triggerup: ["action_secondary_up"], + scroll: "scroll_move" }, keyboard: { m_press: "action_mute", @@ -170,44 +166,6 @@ const config = { arrowright_down: "d_down", arrowright_up: "d_up" } - }, - hud: { - "vive-controls": { - triggerdown: ["action_grab", "index_down"], - triggerup: ["action_release", "index_up"] - }, - "oculus-touch-controls": { - abuttondown: "action_ui_select_down", - abuttonup: "action_ui_select_up", - gripdown: "middle_ring_pinky_down", - gripup: "middle_ring_pinky_up", - abuttontouchstart: "thumb_down", - abuttontouchend: "thumb_up", - bbuttontouchstart: "thumb_down", - bbuttontouchend: "thumb_up", - xbuttontouchstart: "thumb_down", - xbuttontouchend: "thumb_up", - ybuttontouchstart: "thumb_down", - ybuttontouchend: "thumb_up", - surfacetouchstart: "thumb_down", - surfacetouchend: "thumb_up", - thumbsticktouchstart: "thumb_down", - thumbsticktouchend: "thumb_up", - triggertouchstart: "index_down", - triggertouchend: "index_up" - }, - "daydream-controls": { - trackpaddown: { right: "action_ui_select_down" }, - trackpadup: { right: "action_ui_select_up" } - }, - "gearvr-controls": { - trackpaddown: { right: "action_ui_select_down" }, - trackpadup: { right: "action_ui_select_up" } - }, - "oculus-go-controls": { - trackpaddown: { right: "action_ui_select_down" }, - trackpadup: { right: "action_ui_select_up" } - } } } }; diff --git a/src/network-schemas.js b/src/network-schemas.js index 4c8ac07e05625edffe916a549d96ea0a95bc297f..e639aa899a1e931df1b97b25761a4c7080d55a83 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -72,10 +72,12 @@ function registerNetworkSchemas() { template: "#video-template", components: [ { - component: "position" + component: "position", + requiresNetworkUpdate: vectorRequiresUpdate(0.001) }, { - component: "rotation" + component: "rotation", + requiresNetworkUpdate: vectorRequiresUpdate(0.5) }, "visible" ] @@ -104,6 +106,48 @@ function registerNetworkSchemas() { } ] }); + + NAF.schemas.add({ + template: "#interactable-drawing", + components: [ + { + component: "position", + requiresNetworkUpdate: vectorRequiresUpdate(0.001) + }, + { + component: "rotation", + requiresNetworkUpdate: vectorRequiresUpdate(0.5) + }, + "scale", + "networked-drawing" + ] + }); + + NAF.schemas.add({ + template: "#pen-interactable", + components: [ + { + component: "position", + requiresNetworkUpdate: vectorRequiresUpdate(0.001) + }, + { + component: "rotation", + requiresNetworkUpdate: vectorRequiresUpdate(0.5) + }, + "scale", + "media-loader", + { + selector: "#pen", + component: "pen", + property: "radius" + }, + { + selector: "#pen", + component: "pen", + property: "color" + } + ] + }); } export default registerNetworkSchemas; diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index c2db55e5f8b953b8597f79915f645d96f4270b83..052817a81d6aac00a1b2881df7eb73a5268fc79c 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -4,7 +4,7 @@ import cx from "classnames"; import styles from "../assets/stylesheets/2d-hud.scss"; -const TopHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onToggleSpaceBubble }) => ( +const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen }) => ( <div className={cx(styles.container, styles.top, styles.unselectable)}> <div className={cx("ui-interactive", styles.panel, styles.left)}> <div @@ -19,11 +19,7 @@ const TopHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onTo onClick={onToggleFreeze} /> <div className={cx("ui-interactive", styles.panel, styles.right)}> - <div - className={cx(styles.iconButton, styles.bubble, { [styles.active]: spacebubble })} - title={spacebubble ? "Disable Bubble" : "Enable Bubble"} - onClick={onToggleSpaceBubble} - /> + <div className={cx(styles.iconButton, styles.spawn_pen)} title={"Drawing Pen"} onClick={onSpawnPen} /> </div> </div> ); @@ -31,10 +27,9 @@ const TopHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onTo TopHUD.propTypes = { muted: PropTypes.bool, frozen: PropTypes.bool, - spacebubble: PropTypes.bool, onToggleMute: PropTypes.func, onToggleFreeze: PropTypes.func, - onToggleSpaceBubble: PropTypes.func + onSpawnPen: PropTypes.func }; const BottomHUD = ({ onCreateObject, showPhotoPicker, onMediaPicked }) => ( diff --git a/src/react-components/auth-dialog.js b/src/react-components/auth-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..5c7796863c6521b97edbc0d42571d8bea4b10911 --- /dev/null +++ b/src/react-components/auth-dialog.js @@ -0,0 +1,32 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { injectIntl, FormattedMessage } from "react-intl"; +import DialogContainer from "./dialog-container.js"; + +class AuthDialog extends Component { + static propTypes = { + intl: PropTypes.object, + verifying: PropTypes.bool, + authOrigin: PropTypes.string + }; + + render() { + const { authOrigin, verifying } = this.props; + const { formatMessage } = this.props.intl; + const title = verifying ? formatMessage({ id: "auth.verifying" }) : formatMessage({ id: "auth.verified-title" }); + + return ( + <DialogContainer title={title} {...this.props}> + {verifying ? ( + <FormattedMessage id="auth.verifying" /> + ) : authOrigin === "spoke" ? ( + <FormattedMessage id="auth.spoke-verified" values={{ br: <br /> }} /> + ) : ( + <FormattedMessage id="auth.verified" /> + )} + </DialogContainer> + ); + } +} + +export default injectIntl(AuthDialog); diff --git a/src/react-components/create-object-dialog.js b/src/react-components/create-object-dialog.js index c4e922321384825b1c9eee618a1e1a5f5ffa70cc..2636230326838dc4bcabbb73e8d38a8674b2f4da 100644 --- a/src/react-components/create-object-dialog.js +++ b/src/react-components/create-object-dialog.js @@ -1,5 +1,4 @@ import React, { Component } from "react"; -import "aframe"; import PropTypes from "prop-types"; import giphyLogo from "../assets/images/giphy_logo.png"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -7,6 +6,7 @@ import { faPaperclip } from "@fortawesome/free-solid-svg-icons/faPaperclip"; import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes"; import styles from "../assets/stylesheets/create-object-dialog.scss"; import cx from "classnames"; +import DialogContainer from "./dialog-container.js"; const attributionHostnames = { "giphy.com": giphyLogo, @@ -36,8 +36,8 @@ export default class CreateObjectDialog extends Component { }; static propTypes = { - onCreateObject: PropTypes.func, - onCloseDialog: PropTypes.func + onCreate: PropTypes.func, + onClose: PropTypes.func }; componentDidMount() { @@ -67,12 +67,6 @@ export default class CreateObjectDialog extends Component { }); }; - onCreateClicked = e => { - e.preventDefault(); - this.props.onCreateObject(this.state.file || this.state.url || DEFAULT_OBJECT_URL); - this.props.onCloseDialog(); - }; - reset = e => { e.preventDefault(); this.setState({ @@ -83,7 +77,15 @@ export default class CreateObjectDialog extends Component { this.fileInput.value = null; }; + onCreateClicked = e => { + e.preventDefault(); + this.props.onCreate(this.state.file || this.state.url || DEFAULT_OBJECT_URL); + this.props.onClose(); + }; + render() { + const { onCreate, onClose, ...other } = this.props; // eslint-disable-line no-unused-vars + const cancelButton = ( <label className={cx(styles.smallButton, styles.cancelIcon)} onClick={this.reset}> <FontAwesomeIcon icon={faTimes} /> @@ -106,34 +108,36 @@ export default class CreateObjectDialog extends Component { ); return ( - <div> - {isMobile ? mobileInstructions : desktopInstructions} - <form onSubmit={this.onCreateClicked}> - <div className={styles.addMediaForm}> - <input - id={fileInputId} - ref={f => (this.fileInput = f)} - className={styles.hideFileInput} - type="file" - onChange={this.onFileChange} - /> - <div className={styles.inputBorder}> - {this.state.file ? filenameLabel : urlInput} - {this.state.url || this.state.fileName ? cancelButton : uploadButton} - </div> - <div className={styles.buttons}> - <button className={styles.actionButton}> - <span>create</span> - </button> - </div> - {this.state.attributionImage ? ( - <div> - <img src={this.state.attributionImage} /> + <DialogContainer title="Create Object" onClose={onClose} {...other}> + <div> + {isMobile ? mobileInstructions : desktopInstructions} + <form onSubmit={this.onCreateClicked}> + <div className={styles.addMediaForm}> + <input + id={fileInputId} + ref={f => (this.fileInput = f)} + className={styles.hideFileInput} + type="file" + onChange={this.onFileChange} + /> + <div className={styles.inputBorder}> + {this.state.file ? filenameLabel : urlInput} + {this.state.url || this.state.fileName ? cancelButton : uploadButton} + </div> + <div className={styles.buttons}> + <button className={styles.actionButton}> + <span>create</span> + </button> </div> - ) : null} - </div> - </form> - </div> + {this.state.attributionImage ? ( + <div> + <img src={this.state.attributionImage} /> + </div> + ) : null} + </div> + </form> + </div> + </DialogContainer> ); } } diff --git a/src/react-components/create-room-dialog.js b/src/react-components/create-room-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..756cc9bf43736059999a1a66b2fdfe5d856a4e96 --- /dev/null +++ b/src/react-components/create-room-dialog.js @@ -0,0 +1,59 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import DialogContainer from "./dialog-container.js"; + +const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$"; + +export default class CreateObjectDialog extends Component { + static propTypes = { + onCustomScene: PropTypes.func, + onClose: PropTypes.func + }; + + state = { + customRoomName: "", + customSceneUrl: "" + }; + + render() { + const { onCustomScene, onClose, ...other } = this.props; + const onCustomSceneClicked = () => { + onCustomScene(this.state.customRoomName, this.state.customSceneUrl); + onClose(); + }; + + return ( + <DialogContainer title="Create a Room" onClose={onClose} {...other}> + <div> + <div>Choose a name and GLTF URL for your room's scene:</div> + <form onSubmit={onCustomSceneClicked}> + <div className="custom-scene-form"> + <input + type="text" + placeholder="Room name" + className="custom-scene-form__link_field" + value={this.state.customRoomName} + pattern={HUB_NAME_PATTERN} + title="Invalid name, limited to 4 to 64 characters and limited symbols." + onChange={e => this.setState({ customRoomName: e.target.value })} + required + /> + <input + type="url" + placeholder="URL to Scene GLTF or GLB (Optional)" + className="custom-scene-form__link_field" + value={this.state.customSceneUrl} + onChange={e => this.setState({ customSceneUrl: e.target.value })} + /> + <div className="custom-scene-form__buttons"> + <button className="custom-scene-form__action-button"> + <span>create</span> + </button> + </div> + </div> + </form> + </div> + </DialogContainer> + ); + } +} diff --git a/src/react-components/dialog-container.js b/src/react-components/dialog-container.js new file mode 100644 index 0000000000000000000000000000000000000000..18c37956c7408150d59d2385e251817ad9747cb7 --- /dev/null +++ b/src/react-components/dialog-container.js @@ -0,0 +1,57 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +export default class DialogContainer extends Component { + static propTypes = { + title: PropTypes.node, + children: PropTypes.node.isRequired, + onClose: PropTypes.func + }; + + constructor(props) { + super(props); + this.onKeyDown = this.onKeyDown.bind(this); + this.onContainerClicked = this.onContainerClicked.bind(this); + } + + componentDidMount() { + window.addEventListener("keydown", this.onKeyDown); + } + + componentWillUnmount() { + window.removeEventListener("keydown", this.onKeyDown); + } + + onKeyDown(e) { + if (e.key === "Escape") { + this.props.onClose(); + } + } + + onContainerClicked = e => { + if (e.currentTarget === e.target) { + this.props.onClose(); + } + }; + + render() { + return ( + <div className="dialog-overlay"> + <div className="dialog" onClick={this.onContainerClicked}> + <div className="dialog__box"> + <div className="dialog__box__contents"> + {this.props.onClose && ( + <button className="dialog__box__contents__close" onClick={this.props.onClose}> + <span>×</span> + </button> + )} + <div className="dialog__box__contents__title">{this.props.title}</div> + <div className="dialog__box__contents__body">{this.props.children}</div> + <div className="dialog__box__contents__button-container" /> + </div> + </div> + </div> + </div> + ); + } +} diff --git a/src/react-components/help-dialog.js b/src/react-components/help-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..dc0ba425a3f4e9cd12db05c503280fc1c19ce3d7 --- /dev/null +++ b/src/react-components/help-dialog.js @@ -0,0 +1,48 @@ +import React, { Component } from "react"; +import { FormattedMessage } from "react-intl"; +import DialogContainer from "./dialog-container.js"; + +export default class HelpDialog extends Component { + render() { + return ( + <DialogContainer title="Getting Started" {...this.props}> + <div className="info-dialog__help"> + <p>When in a room, other avatars can see and hear you.</p> + <p> + Use your controller's action button to teleport from place to place. If it has a trigger, use it to + pick up objects. + </p> + <p style={{ textAlign: "center" }}> + In VR, <b>look up</b> to find your menu: + <img + className="info-dialog__help__hud" + src="../assets/images/help-hud.png" + srcSet="../assets/images/help-hud@2x.png 2x" + /> + </p> + <p> + The <b>Mic Toggle</b> mutes your mic. + </p> + <p> + The <b>Pause/Resume Toggle</b> pauses all other avatars. You can then block them from having further + interactions with you. + </p> + <p> + The <b>Bubble Toggle</b> hides avatars that enter your personal space. + </p> + <p className="dialog__box__contents__links"> + <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> + <FormattedMessage id="profile.terms_of_use" /> + </a> + <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/PRIVACY.md"> + <FormattedMessage id="profile.privacy_notice" /> + </a> + <a target="_blank" rel="noopener noreferrer" href="/?report"> + <FormattedMessage id="help.report_issue" /> + </a> + </p> + </div> + </DialogContainer> + ); + } +} diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 80df12ea336bfe0c35fae69ef05e38b95f88fde6..450a4bcf2defd7285080bc9583f855f5ac14fdd7 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -8,34 +8,75 @@ import homeVideoWebM from "../assets/video/home.webm"; import homeVideoMp4 from "../assets/video/home.mp4"; import classNames from "classnames"; import { ENVIRONMENT_URLS } from "../assets/environments/environments"; +import { connectToReticulum } from "../utils/phoenix-utils"; import styles from "../assets/stylesheets/index.scss"; import HubCreatePanel from "./hub-create-panel.js"; -import InfoDialog from "./info-dialog.js"; +import AuthDialog from "./auth-dialog.js"; +import ReportDialog from "./report-dialog.js"; +import SlackDialog from "./slack-dialog.js"; +import UpdatesDialog from "./updates-dialog.js"; +import DialogContainer from "./dialog-container.js"; addLocaleData([...en]); class HomeRoot extends Component { static propTypes = { intl: PropTypes.object, - dialogType: PropTypes.symbol, + sceneId: PropTypes.string, + authVerify: PropTypes.bool, + authTopic: PropTypes.string, + authToken: PropTypes.string, + authOrigin: PropTypes.string, + listSignup: PropTypes.bool, + report: PropTypes.bool, initialEnvironment: PropTypes.string }; state = { environments: [], - dialogType: null, + dialog: null, mailingListEmail: "", mailingListPrivacy: false }; componentDidMount() { - this.loadEnvironments(); - this.setState({ dialogType: this.props.dialogType }); + this.closeDialog = this.closeDialog.bind(this); + if (this.props.authVerify) { + this.showAuthDialog(true); + this.verifyAuth().then(this.showAuthDialog); + return; + } + if (this.props.sceneId) { + this.loadEnvironmentFromScene(); + } else { + this.loadEnvironments(); + } this.loadHomeVideo(); + if (this.props.listSignup) { + this.showUpdatesDialog(); + } else if (this.props.report) { + this.showReportDialog(); + } + } + + async verifyAuth() { + const socket = connectToReticulum(); + const channel = socket.channel(this.props.authTopic); + await new Promise((resolve, reject) => + channel + .join() + .receive("ok", resolve) + .receive("error", reject) + ); + channel.push("auth_verified", { token: this.props.authToken }); } + showAuthDialog = verifying => { + this.setState({ dialog: <AuthDialog verifying={verifying} authOrigin={this.props.authOrigin} /> }); + }; + loadHomeVideo = () => { const videoEl = document.querySelector("#background-video"); videoEl.playbackRate = 0.9; @@ -54,12 +95,55 @@ class HomeRoot extends Component { } }; - showDialog = dialogType => { - return e => { - e.preventDefault(); - e.stopPropagation(); - this.setState({ dialogType }); - }; + closeDialog() { + this.setState({ dialog: null }); + } + + showSlackDialog() { + this.setState({ dialog: <SlackDialog onClose={this.closeDialog} /> }); + } + + showReportDialog() { + this.setState({ dialog: <ReportDialog onClose={this.closeDialog} /> }); + } + + showUpdatesDialog() { + this.setState({ + dialog: <UpdatesDialog onClose={this.closeDialog} onSubmittedEmail={() => this.showEmailSubmittedDialog()} /> + }); + } + + showEmailSubmittedDialog() { + this.setState({ + dialog: ( + <DialogContainer onClose={this.closeDialog}> + Great! Please check your e-mail to confirm your subscription. + </DialogContainer> + ) + }); + } + + loadEnvironmentFromScene = async () => { + let sceneUrlBase = "/api/v1/scenes"; + if (process.env.RETICULUM_SERVER) { + sceneUrlBase = `https://${process.env.RETICULUM_SERVER}${sceneUrlBase}`; + } + const sceneInfoUrl = `${sceneUrlBase}/${this.props.sceneId}`; + const resp = await fetch(sceneInfoUrl).then(r => r.json()); + const scene = resp.scenes[0]; + // Transform the scene info into a an environment bundle structure. + this.setState({ + environments: [ + { + // Environment loading doesn't check the content-type, so we force a .glb extension here. + bundle_url: `${scene.model_url}.glb`, + meta: { + title: scene.name, + images: [{ type: "preview-thumbnail", srcset: scene.screenshot_url }] + } + } + ] + }); }; loadEnvironments = () => { @@ -77,12 +161,19 @@ class HomeRoot extends Component { Promise.all(environmentLoads).then(() => this.setState({ environments })); }; + onDialogLinkClicked = trigger => { + return e => { + e.preventDefault(); + e.stopPropagation(); + trigger(); + }; + }; + render() { const mainContentClassNames = classNames({ [styles.mainContent]: true, - [styles.noninteractive]: !!this.state.dialogType + [styles.noninteractive]: !!this.state.dialog }); - const dialogTypes = InfoDialog.dialogTypes; return ( <IntlProvider locale={lang} messages={messages}> @@ -141,7 +232,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.showDialog(dialogTypes.slack)} + onClick={this.onDialogLinkClicked(this.showSlackDialog.bind(this))} > <FormattedMessage id="home.join_us" /> </a> @@ -149,7 +240,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.showDialog(dialogTypes.updates)} + onClick={this.onDialogLinkClicked(this.showUpdatesDialog.bind(this))} > <FormattedMessage id="home.get_updates" /> </a> @@ -157,7 +248,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.showDialog(dialogTypes.report)} + onClick={this.onDialogLinkClicked(this.showReportDialog.bind(this))} > <FormattedMessage id="home.report_issue" /> </a> @@ -191,13 +282,7 @@ class HomeRoot extends Component { <source src={homeVideoWebM} type="video/webm" /> <source src={homeVideoMp4} type="video/mp4" /> </video> - {this.state.dialogType && ( - <InfoDialog - dialogType={this.state.dialogType} - onCloseDialog={() => this.setState({ dialogType: null })} - onSubmittedEmail={() => this.setState({ dialogType: dialogTypes.email_submitted })} - /> - )} + {this.state.dialog} </div> </IntlProvider> ); diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 1abb749612baa6f8b8104cdd034a4d9b9531536e..a39a6d9b492e56d7179c996d5b940eb910f57e25 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -6,13 +6,12 @@ import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { resolveURL, extractUrlBase } from "../utils/resolveURL"; -import InfoDialog from "./info-dialog.js"; +import CreateRoomDialog from "./create-room-dialog.js"; import default_scene_preview_thumbnail from "../assets/images/default_thumbnail.png"; import styles from "../assets/stylesheets/hub-create.scss"; const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$"; -const dialogTypes = InfoDialog.dialogTypes; class HubCreatePanel extends Component { static propTypes = { @@ -178,7 +177,7 @@ class HubCreatePanel extends Component { {environmentTitle} </a> ) : ( - <span className={styles.itle}>environmentTitle</span> + <span className={styles.title}>{environmentTitle}</span> )} {environmentAuthor && environmentAuthor.name && @@ -238,9 +237,8 @@ class HubCreatePanel extends Component { </div> </form> {this.state.showCustomSceneDialog && ( - <InfoDialog - dialogType={dialogTypes.custom_scene} - onCloseDialog={() => this.setState({ showCustomSceneDialog: false })} + <CreateRoomDialog + onClose={() => this.setState({ showCustomSceneDialog: false })} onCustomScene={(name, url) => { this.setState({ showCustomSceneDialog: false, name: name, customSceneUrl: url }, () => this.createHub()); }} diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js deleted file mode 100644 index 5130e54a3de6e20e68f4b79ba052aa98d993d94e..0000000000000000000000000000000000000000 --- a/src/react-components/info-dialog.js +++ /dev/null @@ -1,396 +0,0 @@ -import React, { Component } from "react"; -import copy from "copy-to-clipboard"; -import PropTypes from "prop-types"; -import { FormattedMessage } from "react-intl"; -import formurlencoded from "form-urlencoded"; -import LinkDialog from "./link-dialog.js"; -import CreateObjectDialog from "./create-object-dialog.js"; -const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$"; - -// TODO i18n - -class InfoDialog extends Component { - static dialogTypes = { - slack: Symbol("slack"), - email_submitted: Symbol("email_submitted"), - invite: Symbol("invite"), - safari: Symbol("safari"), - updates: Symbol("updates"), - report: Symbol("report"), - help: Symbol("help"), - link: Symbol("link"), - webvr_recommend: Symbol("webvr_recommend"), - create_object: Symbol("create_object"), - custom_scene: Symbol("custom_scene") - }; - static propTypes = { - dialogType: PropTypes.oneOf(Object.values(InfoDialog.dialogTypes)), - onCloseDialog: PropTypes.func, - onSubmittedEmail: PropTypes.func, - onCreateObject: PropTypes.func, - onCustomScene: PropTypes.func, - linkCode: PropTypes.string - }; - - constructor(props) { - super(props); - - const loc = document.location; - this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`; - this.onKeyDown = this.onKeyDown.bind(this); - this.onContainerClicked = this.onContainerClicked.bind(this); - } - - componentDidMount() { - window.addEventListener("keydown", this.onKeyDown); - } - - componentWillUnmount() { - window.removeEventListener("keydown", this.onKeyDown); - } - - onKeyDown(e) { - if (e.key === "Escape") { - this.props.onCloseDialog(); - } - } - - onContainerClicked = e => { - if (e.currentTarget === e.target) { - this.props.onCloseDialog(); - } - }; - - onCustomSceneClicked = () => { - this.props.onCustomScene(this.state.customRoomName, this.state.customSceneUrl); - this.props.onCloseDialog(); - }; - - shareLinkClicked = () => { - navigator.share({ - title: document.title, - url: this.shareLink - }); - }; - - copyLinkClicked = link => { - copy(link); - this.setState({ copyLinkButtonText: "copied!" }); - }; - - state = { - mailingListEmail: "", - mailingListPrivacy: false, - copyLinkButtonText: "copy", - createObjectUrl: "", - customRoomName: "", - customSceneUrl: "" - }; - - signUpForMailingList = async e => { - e.preventDefault(); - e.stopPropagation(); - if (!this.state.mailingListPrivacy) return; - - const url = "https://www.mozilla.org/en-US/newsletter/"; - - const payload = { - email: this.state.mailingListEmail, - newsletters: "hubs", - privacy: true, - fmt: "H", - source_url: document.location.href - }; - - await fetch(url, { - body: formurlencoded(payload), - method: "POST", - headers: { "content-type": "application/x-www-form-urlencoded" } - }).then(this.props.onSubmittedEmail); - }; - - render() { - if (!this.props.dialogType) { - return <div />; - } - - let dialogTitle = null; - let dialogBody = null; - - switch (this.props.dialogType) { - // TODO i18n, FormattedMessage doesn't play nicely with links - case InfoDialog.dialogTypes.slack: - dialogTitle = "Get in Touch"; - dialogBody = ( - <span> - <p>Want to join the conversation?</p> - <p> - Join us on the{" "} - <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> - WebVR Slack - </a>{" "} - in the{" "} - <a href="https://webvr.slack.com/messages/social" target="_blank" rel="noopener noreferrer"> - #social - </a>{" "} - channel.<br /> - VR meetups every Friday at noon PDT! - </p> - <p> - Or, tweet at{" "} - <a href="https://twitter.com/mozillareality" target="_blank" rel="noopener noreferrer"> - @mozillareality - </a>{" "} - on Twitter. - </p> - </span> - ); - break; - case InfoDialog.dialogTypes.email_submitted: - dialogTitle = ""; - dialogBody = "Great! Please check your e-mail to confirm your subscription."; - break; - case InfoDialog.dialogTypes.invite: - dialogTitle = "Invite Others"; - dialogBody = ( - <div> - <div>Just share the link and they'll join you:</div> - <div className="invite-form"> - <input - type="text" - readOnly - onFocus={e => e.target.select()} - value={this.shareLink} - className="invite-form__link_field" - /> - <div className="invite-form__buttons"> - {navigator.share && ( - <button className="invite-form__action-button" onClick={this.shareLinkClicked}> - <span>share</span> - </button> - )} - <button - className="invite-form__action-button" - onClick={this.copyLinkClicked.bind(this, this.shareLink)} - > - <span>{this.state.copyLinkButtonText}</span> - </button> - </div> - </div> - </div> - ); - break; - case InfoDialog.dialogTypes.safari: - dialogTitle = "Open in Safari"; - dialogBody = ( - <div> - <div>Hubs does not support your current browser on iOS. Copy and paste this link directly in Safari.</div> - <div className="invite-form"> - <input - type="text" - readOnly - onFocus={e => e.target.select()} - value={document.location} - className="invite-form__link_field" - /> - <div className="invite-form__buttons"> - <button - className="invite-form__action-button" - onClick={this.copyLinkClicked.bind(this, document.location)} - > - <span>{this.state.copyLinkButtonText}</span> - </button> - </div> - </div> - </div> - ); - break; - case InfoDialog.dialogTypes.create_object: - dialogTitle = "Create Object"; - dialogBody = ( - <CreateObjectDialog onCreateObject={this.props.onCreateObject} onCloseDialog={this.props.onCloseDialog} /> - ); - break; - case InfoDialog.dialogTypes.custom_scene: - dialogTitle = "Create a Room"; - dialogBody = ( - <div> - <div>Choose a name and GLTF URL for your room's scene:</div> - <form onSubmit={this.onCustomSceneClicked}> - <div className="custom-scene-form"> - <input - type="text" - placeholder="Room name" - className="custom-scene-form__link_field" - value={this.state.customRoomName} - pattern={HUB_NAME_PATTERN} - title="Invalid name, limited to 4 to 64 characters and limited symbols." - onChange={e => this.setState({ customRoomName: e.target.value })} - required - /> - <input - type="url" - placeholder="URL to Scene GLTF or GLB (Optional)" - className="custom-scene-form__link_field" - value={this.state.customSceneUrl} - onChange={e => this.setState({ customSceneUrl: e.target.value })} - /> - <div className="custom-scene-form__buttons"> - <button className="custom-scene-form__action-button"> - <span>create</span> - </button> - </div> - </div> - </form> - </div> - ); - break; - case InfoDialog.dialogTypes.updates: - dialogTitle = ""; - dialogBody = ( - <span> - <p>Sign up to get updates about new features in Hubs.</p> - <form onSubmit={this.signUpForMailingList}> - <div className="mailing-list-form"> - <input - type="email" - value={this.state.mailingListEmail} - onChange={e => this.setState({ mailingListEmail: e.target.value })} - className="mailing-list-form__email_field" - required - placeholder="Your email here" - /> - <label className="mailing-list-form__privacy"> - <input - className="mailing-list-form__privacy_checkbox" - type="checkbox" - required - value={this.state.mailingListPrivacy} - onChange={e => this.setState({ mailingListPrivacy: e.target.checked })} - /> - <span className="mailing-list-form__privacy_label"> - <FormattedMessage id="mailing_list.privacy_label" />{" "} - <a target="_blank" rel="noopener noreferrer" href="https://www.mozilla.org/en-US/privacy/"> - <FormattedMessage id="mailing_list.privacy_link" /> - </a> - </span> - </label> - <input className="mailing-list-form__submit" type="submit" value="Sign Up Now" /> - </div> - </form> - </span> - ); - break; - case InfoDialog.dialogTypes.report: - dialogTitle = "Report an Issue"; - dialogBody = ( - <span> - <p>Need to report a problem?</p> - <p> - You can file a{" "} - <a href="https://github.com/mozilla/hubs/issues" target="_blank" rel="noopener noreferrer"> - GitHub Issue - </a>{" "} - or e-mail us for support at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. - </p> - <p> - You can also find us in{" "} - <a href="https://webvr.slack.com/messages/social" target="_blank" rel="noopener noreferrer"> - #social - </a>{" "} - on the{" "} - <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> - WebVR Slack - </a>. - </p> - </span> - ); - break; - case InfoDialog.dialogTypes.help: - dialogTitle = "Getting Started"; - dialogBody = ( - <div className="info-dialog__help"> - <p>When in a room, other avatars can see and hear you.</p> - <p> - Use your controller's action button to teleport from place to place. If it has a trigger, use it to - pick up objects. - </p> - <p style={{ textAlign: "center" }}> - In VR, <b>look up</b> to find your menu: - <img - className="info-dialog__help__hud" - src="../assets/images/help-hud.png" - srcSet="../assets/images/help-hud@2x.png 2x" - /> - </p> - <p> - The <b>Mic Toggle</b> mutes your mic. - </p> - <p> - The <b>Pause/Resume Toggle</b> pauses all other avatars. You can then block them from having further - interactions with you. - </p> - <p> - The <b>Bubble Toggle</b> hides avatars that enter your personal space. - </p> - <p className="dialog__box__contents__links"> - <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> - <FormattedMessage id="profile.terms_of_use" /> - </a> - <a - target="_blank" - rel="noopener noreferrer" - href="https://github.com/mozilla/hubs/blob/master/PRIVACY.md" - > - <FormattedMessage id="profile.privacy_notice" /> - </a> - <a target="_blank" rel="noopener noreferrer" href="/?report"> - <FormattedMessage id="help.report_issue" /> - </a> - </p> - </div> - ); - break; - case InfoDialog.dialogTypes.webvr_recommend: - dialogTitle = "Enter in VR"; - dialogBody = ( - <div> - <p>To enter Hubs with Oculus or SteamVR, you can use Firefox.</p> - <a className="info-dialog--action-button" href="https://www.mozilla.org/firefox"> - Download Firefox - </a> - <p style={{ fontSize: "0.8em" }}> - For a full list of browsers with experimental VR support, visit{" "} - <a href="https://webvr.rocks" target="_blank" rel="noopener noreferrer"> - WebVR Rocks - </a>. - </p> - </div> - ); - break; - case InfoDialog.dialogTypes.link: - dialogTitle = "Open on Headset"; - dialogBody = <LinkDialog linkCode={this.props.linkCode} />; - break; - } - - return ( - <div className="dialog-overlay"> - <div className="dialog" onClick={this.onContainerClicked}> - <div className="dialog__box"> - <div className="dialog__box__contents"> - <button className="dialog__box__contents__close" onClick={this.props.onCloseDialog}> - <span>×</span> - </button> - <div className="dialog__box__contents__title">{dialogTitle}</div> - <div className="dialog__box__contents__body">{dialogBody}</div> - <div className="dialog__box__contents__button-container" /> - </div> - </div> - </div> - </div> - ); - } -} - -export default InfoDialog; diff --git a/src/react-components/invite-dialog.js b/src/react-components/invite-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..e01fc1282cf835e8e5a65363b3dfc6ee0f4e891f --- /dev/null +++ b/src/react-components/invite-dialog.js @@ -0,0 +1,56 @@ +import React, { Component } from "react"; +import copy from "copy-to-clipboard"; +import DialogContainer from "./dialog-container.js"; + +export default class InviteDialog extends Component { + state = { + copyLinkButtonText: "copy" + }; + + constructor(props) { + super(props); + const loc = document.location; + this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`; + } + + copyLinkClicked = link => { + copy(link); + this.setState({ copyLinkButtonText: "copied!" }); + }; + + shareLinkClicked = () => { + navigator.share({ + title: document.title, + url: this.shareLink + }); + }; + + render() { + return ( + <DialogContainer title="Invite Others" {...this.props}> + <div> + <div>Just share the link and they'll join you:</div> + <div className="invite-form"> + <input + type="text" + readOnly + onFocus={e => e.target.select()} + value={this.shareLink} + className="invite-form__link_field" + /> + <div className="invite-form__buttons"> + {navigator.share && ( + <button className="invite-form__action-button" onClick={this.shareLinkClicked}> + <span>share</span> + </button> + )} + <button className="invite-form__action-button" onClick={this.copyLinkClicked.bind(this, this.shareLink)}> + <span>{this.state.copyLinkButtonText}</span> + </button> + </div> + </div> + </div> + </DialogContainer> + ); + } +} diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js index 80ab360d50d54da5738a9c3c771b090b0d4bc9fa..456090494e4cc9e00799d697530adbe460ae4bd2 100644 --- a/src/react-components/link-dialog.js +++ b/src/react-components/link-dialog.js @@ -2,53 +2,57 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import { FormattedMessage } from "react-intl"; +import DialogContainer from "./dialog-container.js"; import styles from "../assets/stylesheets/link-dialog.scss"; -class LinkDialog extends Component { +export default class LinkDialog extends Component { static propTypes = { linkCode: PropTypes.string }; render() { - if (!this.props.linkCode) { + const { linkCode, ...other } = this.props; + if (!linkCode) { return ( - <div> - <div className={classNames("loading-panel", styles.codeLoadingPanel)}> - <div className="loader-wrap"> - <div className="loader"> - <div className="loader-center" /> + <DialogContainer title="Open on Headset" {...other}> + <div> + <div className={classNames("loading-panel", styles.codeLoadingPanel)}> + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> </div> </div> </div> - </div> + </DialogContainer> ); } return ( - <div> - <div> - <FormattedMessage id="link.in_your_browser" /> - </div> - <a href="https://hub.link" className={styles.domain} target="_blank" rel="noopener noreferrer"> - hub.link - </a> + <DialogContainer title="Open on Headset" {...other}> <div> - <FormattedMessage id="link.enter_code" /> - </div> - <div className={styles.code}> - {this.props.linkCode.split("").map((d, i) => ( - <span className={styles.digit} key={`link_code_${i}`}> - {d} - </span> - ))} - </div> - <div className={styles.keepOpen}> - <FormattedMessage id="link.do_not_close" /> + <div> + <FormattedMessage id="link.in_your_browser" /> + </div> + <a href="https://hub.link" className={styles.domain} target="_blank" rel="noopener noreferrer"> + hub.link + </a> + <div> + <FormattedMessage id="link.enter_code" /> + </div> + <div className={styles.code}> + {linkCode.split("").map((d, i) => ( + <span className={styles.digit} key={`link_code_${i}`}> + {d} + </span> + ))} + </div> + <div className={styles.keepOpen}> + <FormattedMessage id="link.do_not_close" /> + </div> </div> - </div> + </DialogContainer> ); } } - -export default LinkDialog; diff --git a/src/react-components/report-dialog.js b/src/react-components/report-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..36072eed617957090fc2942a5f17047b3ea533ba --- /dev/null +++ b/src/react-components/report-dialog.js @@ -0,0 +1,31 @@ +import React, { Component } from "react"; +import DialogContainer from "./dialog-container.js"; + +export default class ReportDialog extends Component { + render() { + return ( + <DialogContainer title="Report an Issue" {...this.props}> + <span> + <p>Need to report a problem?</p> + <p> + You can file a{" "} + <a href="https://github.com/mozilla/hubs/issues" target="_blank" rel="noopener noreferrer"> + GitHub Issue + </a>{" "} + or e-mail us for support at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. + </p> + <p> + You can also find us in{" "} + <a href="https://webvr.slack.com/messages/social" target="_blank" rel="noopener noreferrer"> + #social + </a>{" "} + on the{" "} + <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> + WebVR Slack + </a>. + </p> + </span> + </DialogContainer> + ); + } +} diff --git a/src/react-components/safari-dialog.js b/src/react-components/safari-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..de96bfbf4545e6496e97a3e0dab81593adad7519 --- /dev/null +++ b/src/react-components/safari-dialog.js @@ -0,0 +1,39 @@ +import React, { Component } from "react"; +import copy from "copy-to-clipboard"; +import DialogContainer from "./dialog-container.js"; + +export default class SafariDialog extends Component { + state = { + copyLinkButtonText: "copy" + }; + + copyLinkClicked = link => { + copy(link); + this.setState({ copyLinkButtonText: "copied!" }); + }; + + render() { + const onCopyClicked = this.copyLinkClicked.bind(this, document.location); + return ( + <DialogContainer title="Open in Safari" {...this.props}> + <div> + <div>Hubs does not support your current browser on iOS. Copy and paste this link directly in Safari.</div> + <div className="invite-form"> + <input + type="text" + readOnly + onFocus={e => e.target.select()} + value={document.location} + className="invite-form__link_field" + /> + <div className="invite-form__buttons"> + <button className="invite-form__action-button" onClick={onCopyClicked}> + <span>{this.state.copyLinkButtonText}</span> + </button> + </div> + </div> + </div> + </DialogContainer> + ); + } +} diff --git a/src/react-components/slack-dialog.js b/src/react-components/slack-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..5a024855ba2a206ebcc4a8a80e92e1460daa4b7a --- /dev/null +++ b/src/react-components/slack-dialog.js @@ -0,0 +1,33 @@ +import React, { Component } from "react"; +import DialogContainer from "./dialog-container.js"; + +export default class SlackDialog extends Component { + render() { + return ( + <DialogContainer title="Get in Touch" {...this.props}> + <span> + <p>Want to join the conversation?</p> + <p> + Join us on the{" "} + <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> + WebVR Slack + </a>{" "} + in the{" "} + <a href="https://webvr.slack.com/messages/social" target="_blank" rel="noopener noreferrer"> + #social + </a>{" "} + channel.<br /> + VR meetups every Friday at noon PDT! + </p> + <p> + Or, tweet at{" "} + <a href="https://twitter.com/mozillareality" target="_blank" rel="noopener noreferrer"> + @mozillareality + </a>{" "} + on Twitter. + </p> + </span> + </DialogContainer> + ); + } +} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 4c2a9ffdfda1af43ee6848b7955fc6752684e3fe..43ec3dba9dab07a3b127b8275fce5355bc4808e6 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -20,7 +20,12 @@ import { } from "./entry-buttons.js"; import { ProfileInfoHeader } from "./profile-info-header.js"; import ProfileEntryPanel from "./profile-entry-panel"; -import InfoDialog from "./info-dialog.js"; +import HelpDialog from "./help-dialog.js"; +import SafariDialog from "./safari-dialog.js"; +import WebVRRecommendDialog from "./webvr-recommend-dialog.js"; +import InviteDialog from "./invite-dialog.js"; +import LinkDialog from "./link-dialog.js"; +import CreateObjectDialog from "./create-object-dialog.js"; import TwoDHUD from "./2d-hud"; import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers"; @@ -79,7 +84,7 @@ class UIRoot extends Component { state = { entryStep: ENTRY_STEPS.start, enterInVR: false, - infoDialogType: null, + dialog: null, linkCode: null, linkCodeCancel: null, @@ -160,6 +165,10 @@ class UIRoot extends Component { this.props.scene.emit("action_space_bubble"); }; + spawnPen = () => { + this.props.scene.emit("spawn_pen"); + }; + handleForcedVREntryType = () => { if (!this.props.forcedVREntryType) return; @@ -264,15 +273,11 @@ class UIRoot extends Component { await this.performDirectEntryFlow(false); }; - linkSafari = async () => { - this.setState({ infoDialogType: InfoDialog.dialogTypes.safari }); - }; - enterVR = async () => { if (this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.maybe) { await this.performDirectEntryFlow(true); } else { - this.setState({ infoDialogType: InfoDialog.dialogTypes.webvr_recommend }); + this.showWebVRRecommendDialog(); } }; @@ -512,24 +517,49 @@ class UIRoot extends Component { }; attemptLink = async () => { - this.setState({ infoDialogType: InfoDialog.dialogTypes.link }); + this.showLinkDialog(); const { code, cancel, onFinished } = await this.props.linkChannel.generateCode(); this.setState({ linkCode: code, linkCodeCancel: cancel }); - onFinished.then(this.handleCloseDialog); + this.showLinkDialog(); + onFinished.then(this.closeDialog); }; - handleCloseDialog = async () => { + closeDialog = async () => { if (this.state.linkCodeCancel) { this.state.linkCodeCancel(); } - this.setState({ infoDialogType: null, linkCode: null, linkCodeCancel: null }); + this.setState({ dialog: null, linkCode: null, linkCodeCancel: null }); }; - handleCreateObject = media => { + createObject = media => { this.props.scene.emit("add_media", media); }; + showHelpDialog() { + this.setState({ dialog: <HelpDialog onClose={this.closeDialog} /> }); + } + + showSafariDialog() { + this.setState({ dialog: <SafariDialog onClose={this.closeDialog} /> }); + } + + showInviteDialog() { + this.setState({ dialog: <InviteDialog onClose={this.closeDialog} /> }); + } + + showCreateObjectDialog() { + this.setState({ dialog: <CreateObjectDialog onCreate={this.createObject} onClose={this.closeDialog} /> }); + } + + showLinkDialog() { + this.setState({ dialog: <LinkDialog linkCode={this.state.linkCode} onClose={this.closeDialog} /> }); + } + + showWebVRRecommendDialog() { + this.setState({ dialog: <WebVRRecommendDialog onClose={this.closeDialog} /> }); + } + render() { if (this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason) { let subtitle = null; @@ -647,7 +677,7 @@ class UIRoot extends Component { <TwoDEntryButton onClick={this.enter2D} /> )} {this.props.availableVREntryTypes.safari === VR_DEVICE_AVAILABILITY.maybe && ( - <SafariEntryButton onClick={this.linkSafari} /> + <SafariEntryButton onClick={this.showSafariDialog} /> )} {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( <GenericEntryButton onClick={this.enterVR} /> @@ -669,10 +699,7 @@ class UIRoot extends Component { </div> )} {screenSharingCheckbox} - <button - className={entryStyles.inviteButton} - onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.invite })} - > + <button className={entryStyles.inviteButton} onClick={() => this.showInviteDialog()}> <FormattedMessage id="entry.invite-others" /> </button> </div> @@ -828,7 +855,7 @@ class UIRoot extends Component { <ProfileInfoHeader name={this.props.store.state.profile.displayName} onClickName={() => this.setState({ showProfileEntry: true })} - onClickHelp={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.help })} + onClickHelp={() => this.showHelpDialog()} /> {entryPanel} {micPanel} @@ -836,7 +863,7 @@ class UIRoot extends Component { </div> ); - const dialogBoxClassNames = classNames({ "ui-interactive": !this.state.infoDialogType, "ui-dialog-box": true }); + const dialogBoxClassNames = classNames({ "ui-interactive": !this.state.dialog, "ui-dialog-box": true }); const dialogBoxContentsClassNames = classNames({ "ui-dialog-box-contents": true, @@ -846,19 +873,10 @@ class UIRoot extends Component { return ( <IntlProvider locale={lang} messages={messages}> <div className="ui"> - <InfoDialog - dialogType={this.state.infoDialogType} - linkCode={this.state.linkCode} - onSubmittedEmail={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.email_submitted })} - onCloseDialog={this.handleCloseDialog} - onCreateObject={this.handleCreateObject} - /> + {this.state.dialog} {this.state.entryStep === ENTRY_STEPS.finished && ( - <button - onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.help })} - className="ui__help-icon" - > + <button onClick={() => this.showHelpDialog()} className="ui__help-icon"> <i className="ui__help-icon__icon"> <FontAwesomeIcon icon={faQuestion} /> </i> @@ -892,11 +910,12 @@ class UIRoot extends Component { onToggleMute={this.toggleMute} onToggleFreeze={this.toggleFreeze} onToggleSpaceBubble={this.toggleSpaceBubble} + onSpawnPen={this.spawnPen} /> {!this.props.availableVREntryTypes.isInHMD && this.props.occupantCount <= 1 && ( <div className={styles.nagButton}> - <button onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.invite })}> + <button onClick={() => this.showInviteDialog()}> <FormattedMessage id="entry.invite-others-nag" /> </button> </div> @@ -909,9 +928,9 @@ class UIRoot extends Component { </div> )} <TwoDHUD.BottomHUD - onCreateObject={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.create_object })} + onCreateObject={() => this.showCreateObjectDialog()} showPhotoPicker={AFRAME.utils.device.isMobile()} - onMediaPicked={this.handleCreateObject} + onMediaPicked={this.createObject} /> </div> ) : null} diff --git a/src/react-components/updates-dialog.js b/src/react-components/updates-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..71d732c2404a366a7bb5e858ba0609bc6d241473 --- /dev/null +++ b/src/react-components/updates-dialog.js @@ -0,0 +1,77 @@ +import React, { Component } from "react"; +import { FormattedMessage } from "react-intl"; +import PropTypes from "prop-types"; +import formurlencoded from "form-urlencoded"; +import DialogContainer from "./dialog-container.js"; + +export default class UpdatesDialog extends Component { + static propTypes = { + onSubmittedEmail: PropTypes.func + }; + + state = { + mailingListEmail: "", + mailingListPrivacy: false + }; + + render() { + const { onSubmittedEmail, ...other } = this.props; + const signUpForMailingList = async e => { + e.preventDefault(); + e.stopPropagation(); + if (!this.state.mailingListPrivacy) return; + + const url = "https://www.mozilla.org/en-US/newsletter/"; + + const payload = { + email: this.state.mailingListEmail, + newsletters: "hubs", + privacy: true, + fmt: "H", + source_url: document.location.href + }; + + await fetch(url, { + body: formurlencoded(payload), + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" } + }).then(onSubmittedEmail); + }; + + return ( + <DialogContainer {...other}> + <span> + <p>Sign up to get updates about new features in Hubs.</p> + <form onSubmit={signUpForMailingList}> + <div className="mailing-list-form"> + <input + type="email" + value={this.state.mailingListEmail} + onChange={e => this.setState({ mailingListEmail: e.target.value })} + className="mailing-list-form__email_field" + required + placeholder="Your email here" + /> + <label className="mailing-list-form__privacy"> + <input + className="mailing-list-form__privacy_checkbox" + type="checkbox" + required + value={this.state.mailingListPrivacy} + onChange={e => this.setState({ mailingListPrivacy: e.target.checked })} + /> + <span className="mailing-list-form__privacy_label"> + <FormattedMessage id="mailing_list.privacy_label" />{" "} + <a target="_blank" rel="noopener noreferrer" href="https://www.mozilla.org/en-US/privacy/"> + <FormattedMessage id="mailing_list.privacy_link" /> + </a> + </span> + </label> + <input className="mailing-list-form__submit" type="submit" value="Sign Up Now" /> + </div> + </form> + </span> + </DialogContainer> + ); + } +} diff --git a/src/react-components/webvr-recommend-dialog.js b/src/react-components/webvr-recommend-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..cd7d9434498a16ac219686d8fb5c264a962abd9e --- /dev/null +++ b/src/react-components/webvr-recommend-dialog.js @@ -0,0 +1,23 @@ +import React, { Component } from "react"; +import DialogContainer from "./dialog-container.js"; + +export default class WebVRRecommendDialog extends Component { + render() { + return ( + <DialogContainer title="Enter in VR" {...this.props}> + <div> + <p>To enter Hubs with Oculus or SteamVR, you can use Firefox.</p> + <a className="info-dialog--action-button" href="https://www.mozilla.org/firefox"> + Download Firefox + </a> + <p style={{ fontSize: "0.8em" }}> + For a full list of browsers with experimental VR support, visit{" "} + <a href="https://webvr.rocks" target="_blank" rel="noopener noreferrer"> + WebVR Rocks + </a>. + </p> + </div> + </DialogContainer> + ); + } +} diff --git a/src/systems/tunnel-effect.js b/src/systems/tunnel-effect.js new file mode 100644 index 0000000000000000000000000000000000000000..8375964060a8c85977fe999ee4f73086f74b75d8 --- /dev/null +++ b/src/systems/tunnel-effect.js @@ -0,0 +1,163 @@ +import "../utils/postprocessing/EffectComposer"; +import "../utils/postprocessing/RenderPass"; +import "../utils/postprocessing/ShaderPass"; +import "../utils/postprocessing/MaskPass"; +import "../utils/shaders/CopyShader"; +import "../utils/shaders/VignetteShader"; +import qsTruthy from "../utils/qs_truthy"; + +const disabledByQueryString = qsTruthy("disableTunnel"); +const CLAMP_SPEED = 0.01; +const CLAMP_RADIUS = 0.001; +const NO_TUNNEL_RADIUS = 10.0; +const NO_TUNNEL_SOFTNESS = 0.0; + +function lerp(start, end, t) { + return (1 - t) * start + t * end; +} + +function f(t) { + const x = t - 1; + return 1 + x * x * x * x * x; +} + +AFRAME.registerSystem("tunneleffect", { + schema: { + targetComponent: { type: "string", default: "character-controller" }, + radius: { type: "number", default: 1.0, min: 0.25 }, + minRadius: { type: "number", default: 0.25, min: 0.1 }, + maxSpeed: { type: "number", default: 0.5, min: 0.1 }, + softest: { type: "number", default: 0.1, min: 0.0 }, + opacity: { type: "number", default: 1, min: 0.0 } + }, + + init: function() { + this.scene = this.el; + this.isMoving = false; + this.isVR = false; + this.dt = 0; + this.isPostProcessingReady = false; + this.characterEl = document.querySelector(`a-entity[${this.data.targetComponent}]`); + if (this.characterEl) { + this._initPostProcessing = this._initPostProcessing.bind(this); + this.characterEl.addEventListener("componentinitialized", this._initPostProcessing); + } else { + console.warn("Could not find target component."); + } + this._enterVR = this._enterVR.bind(this); + this._exitVR = this._exitVR.bind(this); + this.scene.addEventListener("enter-vr", this._enterVR); + this.scene.addEventListener("exit-vr", this._exitVR); + }, + + pause: function() { + if (!this.characterEl) { + return; + } + this.characterEl.removeEventListener("componentinitialized", this._initPostProcessing); + this.scene.removeEventListener("enter-vr", this._enterVR); + this.scene.removeEventListener("exit-vr", this._exitVR); + }, + + play: function() { + this.scene.addEventListener("enter-vr", this._enterVR); + this.scene.addEventListener("exit-vr", this._exitVR); + }, + + tick: function(t, dt) { + this.dt = dt; + + if (disabledByQueryString || !this.isPostProcessingReady || !this.isVR) { + return; + } + + const { maxSpeed, minRadius, softest } = this.data; + const characterSpeed = this.characterComponent.velocity.length(); + const shaderRadius = this.vignettePass.uniforms["radius"].value || NO_TUNNEL_RADIUS; + if (!this.enabled && characterSpeed > CLAMP_SPEED) { + this.enabled = true; + this._bindRenderFunc(); + } else if ( + this.enabled && + characterSpeed < CLAMP_SPEED && + Math.abs(NO_TUNNEL_RADIUS - shaderRadius) < CLAMP_RADIUS + ) { + this.enabled = false; + this._exitTunnel(); + } + if (this.enabled) { + const clampedSpeed = characterSpeed > maxSpeed ? maxSpeed : characterSpeed; + const speedRatio = clampedSpeed / maxSpeed; + this.targetRadius = lerp(NO_TUNNEL_RADIUS, minRadius, f(speedRatio)); + this.targetSoftness = lerp(NO_TUNNEL_SOFTNESS, softest, f(speedRatio)); + this._updateVignettePass(this.targetRadius, this.targetSoftness, this.data.opacity); + } + }, + + _exitTunnel: function() { + this.scene.renderer.render = this.originalRenderFunc; + this.isMoving = false; + }, + + _initPostProcessing: function(event) { + if (event.detail.name === this.data.targetComponent) { + this.characterEl.removeEventListener("componentinitialized", this._initPostProcessing); + this.characterComponent = this.characterEl.components[this.data.targetComponent]; + this._initComposer(); + } + }, + + _enterVR: function() { + this.isVR = true; //TODO: This is called in 2D mode when you press "f", which is bad + }, + + _exitVR: function() { + this._exitTunnel(); + this.isVR = false; + }, + + _initComposer: function() { + this.renderer = this.scene.renderer; + this.camera = this.scene.camera; + this.originalRenderFunc = this.scene.renderer.render; + this.isDigest = false; + const render = this.scene.renderer.render; + const system = this; + this.postProcessingRenderFunc = function() { + if (system.isDigest) { + render.apply(this, arguments); + } else { + system.isDigest = true; + system.composer.render(system.dt); + system.isDigest = false; + } + }; + this.composer = new THREE.EffectComposer(this.renderer); + this.composer.resize(); + this.scenePass = new THREE.RenderPass(this.scene.object3D, this.camera); + this.vignettePass = new THREE.ShaderPass(THREE.VignetteShader); + this._updateVignettePass(this.data.radius, this.data.softness, this.data.opacity); + this.composer.addPass(this.scenePass); + this.composer.addPass(this.vignettePass); + this.isPostProcessingReady = true; + }, + + _updateVignettePass: function(radius, softness, opacity) { + const { width, height } = this.renderer.getSize(); + const pixelRatio = this.renderer.getPixelRatio(); + this.vignettePass.uniforms["radius"].value = radius; + this.vignettePass.uniforms["softness"].value = softness; + this.vignettePass.uniforms["opacity"].value = opacity; + this.vignettePass["resolution"] = new THREE.Uniform(new THREE.Vector2(width * pixelRatio, height * pixelRatio)); + if (!this.vignettePass.renderToScreen) { + this.vignettePass.renderToScreen = true; + } + }, + + /** + * use the render func of the effect composer when we need the postprocessing + */ + _bindRenderFunc: function() { + this.scene.renderer.render = this.postProcessingRenderFunc; + } +}); diff --git a/src/utils/action-event-handler.js b/src/utils/action-event-handler.js index 6288c34e002af618390ab63185a47830067f39f1..472dd813a75ce4bbd11036057550d51105b9be2b 100644 --- a/src/utils/action-event-handler.js +++ b/src/utils/action-event-handler.js @@ -1,29 +1,47 @@ +const VERTICAL_SCROLL_TIMEOUT = 150; +const HORIZONTAL_SCROLL_TIMEOUT = 150; +const SCROLL_THRESHOLD = 0.05; +const SCROLL_MODIFIER = 0.1; + export default class ActionEventHandler { constructor(scene, cursor) { this.scene = scene; this.cursor = cursor; + this.cursorHand = this.cursor.data.cursor.components["super-hands"]; this.isCursorInteracting = false; - this.isCursorInteractingOnGrab = false; this.isTeleporting = false; this.handThatAlsoDrivesCursor = null; this.hovered = false; + this.gotPrimaryDown = false; + this.onPrimaryDown = this.onPrimaryDown.bind(this); this.onPrimaryUp = this.onPrimaryUp.bind(this); - this.onGrab = this.onGrab.bind(this); - this.onRelease = this.onRelease.bind(this); + this.onSecondaryDown = this.onSecondaryDown.bind(this); + this.onSecondaryUp = this.onSecondaryUp.bind(this); + this.onPrimaryGrab = this.onPrimaryGrab.bind(this); + this.onPrimaryRelease = this.onPrimaryRelease.bind(this); + this.onSecondaryGrab = this.onSecondaryGrab.bind(this); + this.onSecondaryRelease = this.onSecondaryRelease.bind(this); this.onCardboardButtonDown = this.onCardboardButtonDown.bind(this); this.onCardboardButtonUp = this.onCardboardButtonUp.bind(this); - this.onMoveDuck = this.onMoveDuck.bind(this); + this.onScrollMove = this.onScrollMove.bind(this); this.addEventListeners(); + + this.lastVerticalScrollTime = 0; + this.lastHorizontalScrollTime = 0; } addEventListeners() { this.scene.addEventListener("action_primary_down", this.onPrimaryDown); this.scene.addEventListener("action_primary_up", this.onPrimaryUp); - this.scene.addEventListener("action_grab", this.onGrab); - this.scene.addEventListener("action_release", this.onRelease); - this.scene.addEventListener("move_duck", this.onMoveDuck); + this.scene.addEventListener("action_secondary_down", this.onSecondaryDown); + this.scene.addEventListener("action_secondary_up", this.onSecondaryUp); + this.scene.addEventListener("primary_action_grab", this.onPrimaryGrab); + this.scene.addEventListener("primary_action_release", this.onPrimaryRelease); + this.scene.addEventListener("secondary_action_grab", this.onSecondaryGrab); + this.scene.addEventListener("secondary_action_release", this.onSecondaryRelease); + this.scene.addEventListener("scroll_move", this.onScrollMove); this.scene.addEventListener("cardboardbuttondown", this.onCardboardButtonDown); // TODO: These should be actions this.scene.addEventListener("cardboardbuttonup", this.onCardboardButtonUp); } @@ -31,97 +49,168 @@ export default class ActionEventHandler { tearDown() { this.scene.removeEventListener("action_primary_down", this.onPrimaryDown); this.scene.removeEventListener("action_primary_up", this.onPrimaryUp); - this.scene.removeEventListener("action_grab", this.onGrab); - this.scene.removeEventListener("action_release", this.onRelease); - this.scene.removeEventListener("move_duck", this.onMoveDuck); + this.scene.removeEventListener("action_secondary_down", this.onSecondaryDown); + this.scene.removeEventListener("action_secondary_up", this.onSecondaryUp); + this.scene.removeEventListener("primary_action_grab", this.onPrimaryGrab); + this.scene.removeEventListener("primary_action_release", this.onPrimaryRelease); + this.scene.removeEventListener("secondary_action_grab", this.onSecondaryGrab); + this.scene.removeEventListener("secondary_action_release", this.onSecondaryRelease); + this.scene.removeEventListener("scroll_move", this.onScrollMove); this.scene.removeEventListener("cardboardbuttondown", this.onCardboardButtonDown); this.scene.removeEventListener("cardboardbuttonup", this.onCardboardButtonUp); } - onMoveDuck(e) { - this.cursor.changeDistanceMod(-e.detail.axis[1] / 8); + onScrollMove(e) { + let scrollY = e.detail.axis[1] * SCROLL_MODIFIER; + scrollY = Math.abs(scrollY) > SCROLL_THRESHOLD ? scrollY : 0; + const changed = this.cursor.changeDistanceMod(-scrollY); //TODO: don't negate this for certain controllers + + let scrollX = e.detail.axis[0] * SCROLL_MODIFIER; + scrollX = Math.abs(scrollX) > SCROLL_THRESHOLD ? scrollX : 0; + + this.isCursorInteracting = this.cursor.isInteracting(); + + if ( + Math.abs(scrollY) > 0 && + (this.lastVerticalScrollTime === 0 || this.lastVerticalScrollTime + VERTICAL_SCROLL_TIMEOUT < Date.now()) + ) { + if (!changed && this.isCursorInteracting && this.isHandThatAlsoDrivesCursor(e.target)) { + this.cursorHand.el.emit(scrollY < 0 ? "scroll_up" : "scroll_down"); + this.cursorHand.el.emit("vertical_scroll_release"); + } else { + e.target.emit(scrollY < 0 ? "scroll_up" : "scroll_down"); + e.target.emit("vertical_scroll_release"); + } + this.lastVerticalScrollTime = Date.now(); + } + + if ( + Math.abs(scrollX) > 0 && + (this.lastHorizontalScrollTime === 0 || this.lastHorizontalScrollTime + HORIZONTAL_SCROLL_TIMEOUT < Date.now()) + ) { + if (this.isCursorInteracting && this.isHandThatAlsoDrivesCursor(e.target)) { + this.cursorHand.el.emit(scrollX < 0 ? "scroll_left" : "scroll_right"); + this.cursorHand.el.emit("horizontal_scroll_release"); + } else { + e.target.emit(scrollX < 0 ? "scroll_left" : "scroll_right"); + e.target.emit("horizontal_scroll_release"); + } + this.lastHorizontalScrollTime = Date.now(); + } } setHandThatAlsoDrivesCursor(handThatAlsoDrivesCursor) { this.handThatAlsoDrivesCursor = handThatAlsoDrivesCursor; } - onGrab(e) { - if (this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { - if (this.isCursorInteracting) { - return; - } else if (e.target.components["super-hands"].state.has("hover-start")) { - e.target.emit("hand_grab"); - return; + isToggle(el) { + return el && el.matches(".toggle, .toggle *"); + } + + isHandThatAlsoDrivesCursor(el) { + return this.handThatAlsoDrivesCursor === el; + } + + onGrab(e, event) { + event = event || e.type; + const superHand = e.target.components["super-hands"]; + const isCursorHand = this.isHandThatAlsoDrivesCursor(e.target); + this.isCursorInteracting = this.cursor.isInteracting(); + if (isCursorHand && !this.isCursorInteracting) { + if (superHand.state.has("hover-start") || superHand.state.get("grab-start")) { + e.target.emit(event); } else { this.isCursorInteracting = this.cursor.startInteraction(); - if (this.isCursorInteracting) { - this.isCursorInteractingOnGrab = true; - } - return; } + } else if (isCursorHand && this.isCursorInteracting) { + this.cursorHand.el.emit(event); } else { - e.target.emit("hand_grab"); - return; + e.target.emit(event); } } - onRelease(e) { + onRelease(e, event) { + event = event || e.type; + const isCursorHand = this.isHandThatAlsoDrivesCursor(e.target); + if (this.isCursorInteracting && isCursorHand) { + //need to check both grab-start and hover-start in the case that the spawner is being grabbed this frame + if (this.isToggle(this.cursorHand.state.get("grab-start") || this.cursorHand.state.get("hover-start"))) { + this.cursorHand.el.emit(event); + this.isCursorInteracting = this.cursor.isInteracting(); + } else { + this.isCursorInteracting = false; + this.cursor.endInteraction(); + } + } else { + e.target.emit(event); + } + } + + onPrimaryGrab(e) { + this.onGrab(e, "primary_hand_grab"); + } + + onPrimaryRelease(e) { + this.onRelease(e, "primary_hand_release"); + } + + onSecondaryGrab(e) { + this.onGrab(e, "secondary_hand_grab"); + } + + onSecondaryRelease(e) { + this.onRelease(e, "secondary_hand_release"); + } + + onDown(e, event) { + this.onGrab(e, event); + if ( - this.isCursorInteracting && - this.isCursorInteractingOnGrab && - this.handThatAlsoDrivesCursor && - this.handThatAlsoDrivesCursor === e.target + this.isHandThatAlsoDrivesCursor(e.target) && + !this.isCursorInteracting && + !this.cursorHand.state.get("grab-start") ) { - this.isCursorInteracting = false; - this.isCursorInteractingOnGrab = false; - this.cursor.endInteraction(); + this.cursor.setCursorVisibility(false); + const button = e.target.components["teleport-controls"].data.button; + e.target.emit(button + "down"); + this.isTeleporting = true; + } + } + + onUp(e, event) { + if (this.isTeleporting && this.isHandThatAlsoDrivesCursor(e.target)) { + const superHand = e.target.components["super-hands"]; + this.cursor.setCursorVisibility(!superHand.state.has("hover-start")); + const button = e.target.components["teleport-controls"].data.button; + e.target.emit(button + "up"); + this.isTeleporting = false; } else { - e.target.emit("hand_release"); + this.onRelease(e, event); } } onPrimaryDown(e) { - if (this.isCursorInteractingOnGrab) return; - if (this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target) { - if (this.isCursorInteracting) { - return; - } else if (e.target.components["super-hands"].state.has("hover-start")) { - e.target.emit("hand_grab"); - return; - } else { - this.isCursorInteracting = this.cursor.startInteraction(); - if (this.isCursorInteracting) return; - } + if (!this.gotPrimaryDown) { + this.onDown(e, "primary_hand_grab"); + this.gotPrimaryDown = true; } - - this.cursor.setCursorVisibility(false); - const button = e.target.components["teleport-controls"].data.button; - e.target.emit(button + "down"); - this.isTeleporting = true; } onPrimaryUp(e) { - if (this.isCursorInteractingOnGrab) return; - const isCursorHand = this.handThatAlsoDrivesCursor && this.handThatAlsoDrivesCursor === e.target; - if (this.isCursorInteracting && isCursorHand) { - this.isCursorInteracting = false; - this.cursor.endInteraction(); - return; + if (this.gotPrimaryDown) { + this.onUp(e, "primary_hand_release"); + } else if (this.isToggle(this.cursorHand.state.get("grab-start") || this.cursorHand.state.get("hover-start"))) { + this.onUp(e, "secondary_hand_release"); } + this.gotPrimaryDown = false; + } - const state = e.target.components["super-hands"].state; - if (state.has("grab-start")) { - e.target.emit("hand_release"); - return; - } + onSecondaryDown(e) { + this.onDown(e, "secondary_hand_grab"); + } - if (isCursorHand) { - this.cursor.setCursorVisibility(!state.has("hover-start")); - } - const button = e.target.components["teleport-controls"].data.button; - e.target.emit(button + "up"); - this.isTeleporting = false; + onSecondaryUp(e) { + this.onUp(e, "secondary_hand_release"); } onCardboardButtonDown(e) { diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index 825865dcac289bc2afa1f8f3ed878b485e682ee1..829d00de9709d3d2ce5214c884edf63b1dac6a35 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -91,12 +91,12 @@ function getOrientation(file, callback) { } let interactableId = 0; -export const addMedia = (src, contentOrigin, resize = false) => { +export const addMedia = (src, template, contentOrigin, resize = false) => { const scene = AFRAME.scenes[0]; const entity = document.createElement("a-entity"); entity.id = "interactable-media-" + interactableId++; - entity.setAttribute("networked", { template: "#interactable-media" }); + entity.setAttribute("networked", { template: template }); entity.setAttribute("media-loader", { resize, src: typeof src === "string" ? src : "" }); scene.appendChild(entity); diff --git a/src/utils/mouse-events-handler.js b/src/utils/mouse-events-handler.js index 0fdb330c6558e397e395908e4b0c09fa8c15d1f0..510cb0cd25dd0ca792c294a73e544c60beb1b084 100644 --- a/src/utils/mouse-events-handler.js +++ b/src/utils/mouse-events-handler.js @@ -1,10 +1,14 @@ // TODO: Make look speed adjustable by the user const HORIZONTAL_LOOK_SPEED = 0.1; const VERTICAL_LOOK_SPEED = 0.06; +const VERTICAL_SCROLL_TIMEOUT = 50; +const HORIZONTAL_SCROLL_TIMEOUT = 50; export default class MouseEventsHandler { constructor(cursor, cameraController) { this.cursor = cursor; + const cursorController = this.cursor.el.getAttribute("cursor-controller"); + this.superHand = cursorController.cursor.components["super-hands"]; this.cameraController = cameraController; this.isLeftButtonDown = false; this.isLeftButtonHandledByCursor = false; @@ -16,6 +20,9 @@ export default class MouseEventsHandler { this.onMouseWheel = this.onMouseWheel.bind(this); this.addEventListeners(); + + this.lastVerticalScrollTime = 0; + this.lastHorizontalScrollTime = 0; } tearDown() { @@ -43,46 +50,83 @@ export default class MouseEventsHandler { } onMouseDown(e) { - const isLeftButton = e.button === 0; - const isRightButton = e.button === 2; - if (isLeftButton) { - this.onLeftButtonDown(); - } else if (isRightButton) { - this.onRightButtonDown(); + switch (e.button) { + case 0: //left button + this.onLeftButtonDown(); + break; + case 1: //middle/scroll button + //TODO: rotation? scaling? + break; + case 2: //right button + this.onRightButtonDown(); + break; } } onLeftButtonDown() { this.isLeftButtonDown = true; + if (this.isToggle(this.superHand.state.get("grab-start"))) { + this.superHand.el.emit("secondary-cursor-grab"); + } this.isLeftButtonHandledByCursor = this.cursor.startInteraction(); } onRightButtonDown() { - if (this.isPointerLocked) { - document.exitPointerLock(); - this.isPointerLocked = false; - } else { - document.body.requestPointerLock(); - this.isPointerLocked = true; + this.isLeftButtonHandledByCursor = this.cursor.isInteracting(); + if (!this.isLeftButtonHandledByCursor) { + if (this.isPointerLocked) { + document.exitPointerLock(); + this.isPointerLocked = false; + } else { + document.body.requestPointerLock(); + this.isPointerLocked = true; + } } } onMouseWheel(e) { - switch (e.deltaMode) { - case e.DOM_DELTA_PIXEL: - this.cursor.changeDistanceMod(e.deltaY / 500); - break; - case e.DOM_DELTA_LINE: - this.cursor.changeDistanceMod(e.deltaY / 10); - break; - case e.DOM_DELTA_PAGE: - this.cursor.changeDistanceMod(e.deltaY / 2); - break; + let changed = true; + if (!e.altKey && !e.shiftKey) { + changed = this.cursor.changeDistanceMod(this.getScrollMod(e.deltaY, e.deltaMode)); + } + + if ( + (!changed || e.shiftKey) && + (this.lastVerticalScrollTime === 0 || this.lastVerticalScrollTime + VERTICAL_SCROLL_TIMEOUT < Date.now()) + ) { + this.superHand.el.emit(e.deltaY > 0 ? "scroll_up" : "scroll_down"); + this.superHand.el.emit("vertical_scroll_release"); + this.lastVerticalScrollTime = Date.now(); + } + + const delta = e.altKey ? e.deltaY : e.deltaX; + if ( + Math.abs(delta) > 0 && + (this.lastHorizontalScrollTime === 0 || this.lastHorizontalScrollTime + HORIZONTAL_SCROLL_TIMEOUT < Date.now()) + ) { + this.superHand.el.emit(delta < 0 ? "scroll_left" : "scroll_right"); + this.superHand.el.emit("horizontal_scroll_release"); + this.lastHorizontalScrollTime = Date.now(); + } + + if (e.altKey) e.preventDefault(); //prevent forward/back on firefox + } + + getScrollMod(delta, deltaMode) { + switch (deltaMode) { + case WheelEvent.DOM_DELTA_PIXEL: + return delta / 500; + case WheelEvent.DOM_DELTA_LINE: + return delta / 10; + case WheelEvent.DOM_DELTA_PAGE: + return delta / 2; } } onMouseMove(e) { - const shouldLook = this.isPointerLocked || (this.isLeftButtonDown && !this.isLeftButtonHandledByCursor); + const shouldLook = + this.isPointerLocked || + (!this.superHand.state.get("grab-start") && this.isLeftButtonDown && !this.isLeftButtonHandledByCursor); if (shouldLook) { this.look(e); } @@ -91,14 +135,30 @@ export default class MouseEventsHandler { } onMouseUp(e) { - const isLeftButton = e.button === 0; - if (!isLeftButton) return; - - if (this.isLeftButtonHandledByCursor) { - this.cursor.endInteraction(); + switch (e.button) { + case 0: //left button + if (this.isToggle(this.superHand.state.get("grab-start"))) { + this.superHand.el.emit("secondary-cursor-release"); + } else { + this.endInteraction(); + } + this.isLeftButtonDown = false; + break; + case 1: //middle/scroll button + break; + case 2: //right button + this.endInteraction(); + break; } + } + + endInteraction() { + this.cursor.endInteraction(); this.isLeftButtonHandledByCursor = false; - this.isLeftButtonDown = false; + } + + isToggle(el) { + return el && el.matches(".toggle, .toggle *"); } look(e) { diff --git a/src/utils/postprocessing/EffectComposer.js b/src/utils/postprocessing/EffectComposer.js new file mode 100644 index 0000000000000000000000000000000000000000..11fe7447817d002e86973b14856a5f2bc4a85de9 --- /dev/null +++ b/src/utils/postprocessing/EffectComposer.js @@ -0,0 +1,167 @@ +THREE.EffectComposer = function(renderer, renderTarget) { + this.renderer = renderer; + this.delta = 0; + window.addEventListener("vrdisplaypresentchange", this.resize.bind(this)); + + if (renderTarget === undefined) { + const parameters = { + minFilter: THREE.LinearFilter, + magFilter: THREE.LinearFilter, + format: THREE.RGBAFormat, + stencilBuffer: false + }; + + const size = renderer.getDrawingBufferSize(); + renderTarget = new THREE.WebGLRenderTarget(size.width, size.height, parameters); + renderTarget.texture.name = "EffectComposer.rt1"; + } + + this.renderTarget1 = renderTarget; + this.renderTarget2 = renderTarget.clone(); + this.renderTarget2.texture.name = "EffectComposer.rt2"; + + this.writeBuffer = this.renderTarget1; + this.readBuffer = this.renderTarget2; + + this.passes = []; + this.maskActive = false; + + // dependencies + + if (THREE.CopyShader === undefined) { + console.error("THREE.EffectComposer relies on THREE.CopyShader"); + } + + if (THREE.ShaderPass === undefined) { + console.error("THREE.EffectComposer relies on THREE.ShaderPass"); + } + + this.copyPass = new THREE.ShaderPass(THREE.CopyShader); +}; + +Object.assign(THREE.EffectComposer.prototype, { + swapBuffers: function(pass) { + if (pass.needsSwap) { + if (this.maskActive) { + const context = this.renderer.context; + context.stencilFunc(context.NOTEQUAL, 1, 0xffffffff); + this.copyPass.render(this.renderer, this.writeBuffer, this.readBuffer, this.delta); + context.stencilFunc(context.EQUAL, 1, 0xffffffff); + } + + const tmp = this.readBuffer; + this.readBuffer = this.writeBuffer; + this.writeBuffer = tmp; + } + + if (THREE.MaskPass !== undefined) { + if (pass instanceof THREE.MaskPass) { + this.maskActive = true; + } else if (pass instanceof THREE.ClearMaskPass) { + this.maskActive = false; + } + } + }, + + addPass: function(pass) { + this.passes.push(pass); + const size = this.renderer.getDrawingBufferSize(); + pass.setSize(size.width, size.height); + }, + + insertPass: function(pass, index) { + this.passes.splice(index, 0, pass); + }, + + render: function(delta, starti) { + const maskActive = this.maskActive; + let pass; + let i; + const il = this.passes.length; + const scope = this; + let currentOnAfterRender; + this.delta = delta; + + for (i = starti || 0; i < il; i++) { + pass = this.passes[i]; + if (pass.enabled === false) continue; + + // If VR mode is enabled and rendering the whole scene is required. + // The pass renders the scene and and postprocessing is resumed before + // submitting the frame to the headset by using the onAfterRender callback. + if (this.renderer.vr.enabled && pass.scene) { + currentOnAfterRender = pass.scene.onAfterRender; + pass.scene.onAfterRender = function() { + // Disable stereo rendering when doing postprocessing + // on a render target. + scope.renderer.vr.enabled = false; + scope.render(delta, i + 1, maskActive); + + // Renable vr mode. + scope.renderer.vr.enabled = true; + }; + + pass.render(this.renderer, this.writeBuffer, this.readBuffer); + + // Restore onAfterRender + pass.scene.onAfterRender = currentOnAfterRender; + this.swapBuffers(pass); + return; + } + + pass.render(this.renderer, this.writeBuffer, this.readBuffer); + this.swapBuffers(pass); + } + }, + + reset: function(renderTarget) { + if (renderTarget === undefined) { + const size = this.renderer.getDrawingBufferSize(); + renderTarget = this.renderTarget1.clone(); + renderTarget.setSize(size.width, size.height); + } + + this.renderTarget1.dispose(); + this.renderTarget2.dispose(); + this.renderTarget1 = renderTarget; + this.renderTarget2 = renderTarget.clone(); + + this.writeBuffer = this.renderTarget1; + this.readBuffer = this.renderTarget2; + }, + + setSize: function(width, height) { + this.renderTarget1.setSize(width, height); + this.renderTarget2.setSize(width, height); + for (let i = 0; i < this.passes.length; i++) { + this.passes[i].setSize(width, height); + } + }, + + resize: function() { + const rendererSize = this.renderer.getDrawingBufferSize(); + this.setSize(rendererSize.width, rendererSize.height); + } +}); + +THREE.Pass = function() { + // if set to true, the pass is processed by the composer + this.enabled = true; + + // if set to true, the pass indicates to swap read and write buffer after rendering + this.needsSwap = true; + + // if set to true, the pass clears its buffer before rendering + this.clear = false; + + // if set to true, the result of the pass is rendered to screen + this.renderToScreen = false; +}; + +Object.assign(THREE.Pass.prototype, { + setSize: function() {}, + + render: function() { + console.error("THREE.Pass: .render() must be implemented in derived pass."); + } +}); diff --git a/src/utils/postprocessing/MaskPass.js b/src/utils/postprocessing/MaskPass.js new file mode 100644 index 0000000000000000000000000000000000000000..ddc5aa98a39be86dc224c282a9e02837e9aa7f14 --- /dev/null +++ b/src/utils/postprocessing/MaskPass.js @@ -0,0 +1,80 @@ +/** + * @author alteredq / http://alteredqualia.com/ + */ + +THREE.MaskPass = function(scene, camera) { + THREE.Pass.call(this); + + this.scene = scene; + this.camera = camera; + + this.clear = true; + this.needsSwap = false; + + this.inverse = false; +}; + +THREE.MaskPass.prototype = Object.assign(Object.create(THREE.Pass.prototype), { + constructor: THREE.MaskPass, + + render: function(renderer, writeBuffer, readBuffer) { + const context = renderer.context; + const state = renderer.state; + + // don't update color or depth + + state.buffers.color.setMask(false); + state.buffers.depth.setMask(false); + + // lock buffers + + state.buffers.color.setLocked(true); + state.buffers.depth.setLocked(true); + + // set up stencil + + let writeValue, clearValue; + + if (this.inverse) { + writeValue = 0; + clearValue = 1; + } else { + writeValue = 1; + clearValue = 0; + } + + state.buffers.stencil.setTest(true); + state.buffers.stencil.setOp(context.REPLACE, context.REPLACE, context.REPLACE); + state.buffers.stencil.setFunc(context.ALWAYS, writeValue, 0xffffffff); + state.buffers.stencil.setClear(clearValue); + + // draw into the stencil buffer + + renderer.render(this.scene, this.camera, readBuffer, this.clear); + renderer.render(this.scene, this.camera, writeBuffer, this.clear); + + // unlock color and depth buffer for subsequent rendering + + state.buffers.color.setLocked(false); + state.buffers.depth.setLocked(false); + + // only render where stencil is set to 1 + + state.buffers.stencil.setFunc(context.EQUAL, 1, 0xffffffff); // draw if == 1 + state.buffers.stencil.setOp(context.KEEP, context.KEEP, context.KEEP); + } +}); + +THREE.ClearMaskPass = function() { + THREE.Pass.call(this); + + this.needsSwap = false; +}; + +THREE.ClearMaskPass.prototype = Object.create(THREE.Pass.prototype); + +Object.assign(THREE.ClearMaskPass.prototype, { + render: function(renderer) { + renderer.state.buffers.stencil.setTest(false); + } +}); diff --git a/src/utils/postprocessing/README.md b/src/utils/postprocessing/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1385db1d2b5b53f0325fe39020b0de2f4d91144d --- /dev/null +++ b/src/utils/postprocessing/README.md @@ -0,0 +1,7 @@ +These files +- EffectComposer.js +- MaskPass.js +- RenderPass.js +- ShaderPass.js +are copied from https://github.com/mrdoob/three.js/blob/dev/examples/js/postprocessing/EffectComposer.js + diff --git a/src/utils/postprocessing/RenderPass.js b/src/utils/postprocessing/RenderPass.js new file mode 100644 index 0000000000000000000000000000000000000000..e7dd98af6f12876c0c179147fb1b3e20836772a8 --- /dev/null +++ b/src/utils/postprocessing/RenderPass.js @@ -0,0 +1,52 @@ +/** + * @author alteredq / http://alteredqualia.com/ + */ + +THREE.RenderPass = function(scene, camera, overrideMaterial, clearColor, clearAlpha) { + THREE.Pass.call(this); + + this.scene = scene; + this.camera = camera; + + this.overrideMaterial = overrideMaterial; + + this.clearColor = clearColor; + this.clearAlpha = clearAlpha !== undefined ? clearAlpha : 0; + + this.clear = true; + this.clearDepth = false; + this.needsSwap = false; +}; + +THREE.RenderPass.prototype = Object.assign(Object.create(THREE.Pass.prototype), { + constructor: THREE.RenderPass, + + render: function(renderer, writeBuffer, readBuffer) { + const oldAutoClear = renderer.autoClear; + renderer.autoClear = false; + + this.scene.overrideMaterial = this.overrideMaterial; + + let oldClearColor, oldClearAlpha; + + if (this.clearColor) { + oldClearColor = renderer.getClearColor().getHex(); + oldClearAlpha = renderer.getClearAlpha(); + + renderer.setClearColor(this.clearColor, this.clearAlpha); + } + + if (this.clearDepth) { + renderer.clearDepth(); + } + + renderer.render(this.scene, this.camera, this.renderToScreen ? null : readBuffer, this.clear); + + if (this.clearColor) { + renderer.setClearColor(oldClearColor, oldClearAlpha); + } + + this.scene.overrideMaterial = null; + renderer.autoClear = oldAutoClear; + } +}); diff --git a/src/utils/postprocessing/ShaderPass.js b/src/utils/postprocessing/ShaderPass.js new file mode 100644 index 0000000000000000000000000000000000000000..d6d25800f86e19df01c8e39c490b3763ecd7ef77 --- /dev/null +++ b/src/utils/postprocessing/ShaderPass.js @@ -0,0 +1,44 @@ +/** + * @author alteredq / http://alteredqualia.com/ + */ + +THREE.ShaderPass = function(shader, textureID) { + THREE.Pass.call(this); + this.textureID = textureID !== undefined ? textureID : "tDiffuse"; + if (shader instanceof THREE.ShaderMaterial) { + this.uniforms = shader.uniforms; + this.material = shader; + } else if (shader) { + this.uniforms = THREE.UniformsUtils.clone(shader.uniforms); + this.material = new THREE.ShaderMaterial({ + defines: Object.assign({}, shader.defines), + uniforms: this.uniforms, + vertexShader: shader.vertexShader, + fragmentShader: shader.fragmentShader + }); + } + + this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + this.scene = new THREE.Scene(); + + this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), null); + this.quad.frustumCulled = false; // Avoid getting clipped + this.scene.add(this.quad); +}; + +THREE.ShaderPass.prototype = Object.assign(Object.create(THREE.Pass.prototype), { + constructor: THREE.ShaderPass, + render: function(renderer, writeBuffer, readBuffer) { + if (this.uniforms[this.textureID]) { + this.uniforms[this.textureID].value = readBuffer.texture; + } + + this.quad.material = this.material; + + if (this.renderToScreen) { + renderer.render(this.scene, this.camera); + } else { + renderer.render(this.scene, this.camera, writeBuffer, this.clear); + } + } +}); diff --git a/src/utils/shaders/CopyShader.js b/src/utils/shaders/CopyShader.js new file mode 100644 index 0000000000000000000000000000000000000000..f970d4a92df1ae3653b45906596963c3c6b7611d --- /dev/null +++ b/src/utils/shaders/CopyShader.js @@ -0,0 +1,38 @@ +/** + * @author alteredq / http://alteredqualia.com/ + * + * Full-screen textured quad shader + */ + +THREE.CopyShader = { + uniforms: { + tDiffuse: { value: null }, + opacity: { value: 1.0 } + }, + + vertexShader: [ + "varying vec2 vUv;", + + "void main() {", + + "vUv = uv;", + "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", + + "}" + ].join("\n"), + + fragmentShader: [ + "uniform float opacity;", + + "uniform sampler2D tDiffuse;", + + "varying vec2 vUv;", + + "void main() {", + + "vec4 texel = texture2D( tDiffuse, vUv );", + "gl_FragColor = opacity * texel;", + + "}" + ].join("\n") +}; diff --git a/src/utils/shaders/README.md b/src/utils/shaders/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6d4b23f4a3954768617953bbea2a897ded60ded5 --- /dev/null +++ b/src/utils/shaders/README.md @@ -0,0 +1,5 @@ +These files +- CopyShader.js +- VignetteShader.js +were copied from https://github.com/mrdoob/three.js/blob/dev/examples/js/shaders/CopyShader.js + diff --git a/src/utils/shaders/VignetteShader.js b/src/utils/shaders/VignetteShader.js new file mode 100644 index 0000000000000000000000000000000000000000..9a424139bc72101aea8f7251f741c9790f851bae --- /dev/null +++ b/src/utils/shaders/VignetteShader.js @@ -0,0 +1,46 @@ +THREE.VignetteShader = { + uniforms: { + tDiffuse: { value: null }, + radius: { value: 0.65 }, + opacity: { value: 0.9 }, + softness: { value: 0.2 }, + resolution: new THREE.Uniform(new THREE.Vector2(1920, 1080)) + }, + + vertexShader: [ + "varying vec2 vUv;", + "void main() {", + "vUv = uv;", + "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", + "}" + ].join("\n"), + + fragmentShader: [ + "uniform sampler2D tDiffuse;", + "uniform float radius;", + "uniform float opacity;", + "uniform float softness;", + "uniform vec2 resolution;", + + "varying vec2 vUv;", + + "void main() {", + "vec4 texel = texture2D( tDiffuse, vUv);", + "float ratio = resolution.x / resolution.y;", + "float centerXOffset = radius / ratio;", + "float leftX = (0.3 + centerXOffset >= 0.5) ? 0.5 - centerXOffset : 0.3;", + "float rightX = (0.7 - centerXOffset <= 0.5) ? 0.5 + centerXOffset : 0.7;", + "vec2 uvLeft = (vUv.xy) - vec2(leftX, 0.5);", + "vec2 uvRight = (vUv.xy) - vec2(rightX, 0.5);", + "uvLeft.x *= ratio;", + "uvRight.x *= ratio;", + "float lenLeft = length(uvLeft);", + "float lenRight = length(uvRight);", + "float vignetteLeft = smoothstep(radius, radius-softness, lenLeft);", + "float vignetteRight = smoothstep(radius, radius-softness, lenRight);", + "float vignette = vignetteLeft + vignetteRight;", + "vec3 final = mix (texel.rgb, texel.rgb * vignette, opacity);", + "gl_FragColor = vec4(final.rgb, 1.0);", + "}" + ].join("\n") +}; diff --git a/src/utils/sharedbuffergeometry.js b/src/utils/sharedbuffergeometry.js new file mode 100644 index 0000000000000000000000000000000000000000..aeebd45ed273e67e2abc22ab00288053fb321781 --- /dev/null +++ b/src/utils/sharedbuffergeometry.js @@ -0,0 +1,117 @@ +export default class SharedBufferGeometry { + constructor(material, primitiveMode, maxBufferSize) { + this.material = material; + this.primitiveMode = primitiveMode; + + console.log("maxBufferSize", maxBufferSize); + this.maxBufferSize = maxBufferSize; + this.geometries = []; + this.current = null; + this.drawing = new THREE.Object3D(); + this.addBuffer(); + } + + restartPrimitive() { + if (this.idx.position >= this.current.attributes.position.count) { + console.error("maxBufferSize limit exceeded"); + } else if (this.idx.position !== 0) { + let prev = (this.idx.position - 1) * 3; + const position = this.current.attributes.position.array; + this.addVertex(position[prev++], position[prev++], position[prev++]); + + this.idx.color++; + this.idx.normal++; + } + } + + remove(prevIdx, idx) { + // Loop through all the attributes: position, color, normal,... + if (this.idx.position > idx.position) { + for (const key in this.idx) { + const componentSize = 3; + let pos = prevIdx[key] * componentSize; + const start = (idx[key] + 1) * componentSize; + const end = this.idx[key] * componentSize; + for (let i = start; i < end; i++) { + this.current.attributes[key].array[pos++] = this.current.attributes[key].array[i]; + } + const diff = idx[key] - prevIdx[key] + 1; + this.idx[key] -= diff; + } + } else { + for (const key in this.idx) { + const diff = idx[key] - prevIdx[key]; + this.idx[key] -= diff; + } + } + + this.update(); + } + + undo(prevIdx) { + this.idx = prevIdx; + this.update(); + } + + addBuffer() { + const geometry = new THREE.BufferGeometry(); + + const vertices = new Float32Array(this.maxBufferSize * 3); + const normals = new Float32Array(this.maxBufferSize * 3); + const colors = new Float32Array(this.maxBufferSize * 3); + + const mesh = new THREE.Mesh(geometry, this.material); + + mesh.drawMode = this.primitiveMode; + + mesh.frustumCulled = false; + mesh.vertices = vertices; + + const object3D = new THREE.Object3D(); + this.drawing.add(object3D); + object3D.add(mesh); + + geometry.setDrawRange(0, 0); + geometry.addAttribute("position", new THREE.BufferAttribute(vertices, 3).setDynamic(true)); + geometry.addAttribute("normal", new THREE.BufferAttribute(normals, 3).setDynamic(true)); + geometry.addAttribute("color", new THREE.BufferAttribute(colors, 3).setDynamic(true)); + + this.previous = null; + if (this.geometries.length > 0) { + this.previous = this.current; + } + + this.idx = { + position: 0, + normal: 0, + color: 0 + }; + + this.geometries.push(geometry); + this.current = geometry; + } + + addColor(r, g, b) { + this.current.attributes.color.setXYZ(this.idx.color++, r, g, b); + } + + addNormal(x, y, z) { + this.current.attributes.normal.setXYZ(this.idx.normal++, x, y, z); + } + + addVertex(x, y, z) { + const buffer = this.current.attributes.position; + if (this.idx.position === buffer.count) { + console.error("maxBufferSize limit exceeded"); + } + buffer.setXYZ(this.idx.position++, x, y, z); + } + + update() { + this.current.setDrawRange(0, this.idx.position); + + this.current.attributes.color.needsUpdate = true; + this.current.attributes.normal.needsUpdate = true; + this.current.attributes.position.needsUpdate = true; + } +} diff --git a/src/utils/sharedbuffergeometrymanager.js b/src/utils/sharedbuffergeometrymanager.js new file mode 100644 index 0000000000000000000000000000000000000000..a3f86d44e73cfa8b6f86cb712d4b9c00d4d5d822 --- /dev/null +++ b/src/utils/sharedbuffergeometrymanager.js @@ -0,0 +1,15 @@ +import SharedBufferGeometry from "./sharedbuffergeometry"; + +export default class SharedBufferGeometryManager { + constructor() { + this.sharedBuffers = {}; + } + + addSharedBuffer(name, material, primitiveMode, maxBufferSize) { + this.sharedBuffers[name] = new SharedBufferGeometry(material, primitiveMode, maxBufferSize); + } + + getSharedBuffer(name) { + return this.sharedBuffers[name]; + } +} diff --git a/webpack.config.js b/webpack.config.js index 966ca07936629936d3fa1ca1efa3f88eb1e4078b..9531ceb26e70881f4bd4b5af7eb4367e2141f007 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -180,7 +180,7 @@ module.exports = (env, argv) => ({ new HTMLWebpackPlugin({ filename: "index.html", template: path.join(__dirname, "src", "index.html"), - chunks: ["vendor", "engine", "index"] + chunks: ["vendor", "index"] }), new HTMLWebpackPlugin({ filename: "hub.html",