diff --git a/Jenkinsfile b/Jenkinsfile index 18a2532f8236797c5f074eadcd9b6c160a9ef22a..5951a3a73926717b32d52927973e298a09cff5c6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,19 @@ +import groovy.json.JsonOutput + +// From https://issues.jenkins-ci.org/browse/JENKINS-44231 + +// Given arbitrary string returns a strongly escaped shell string literal. +// I.e. it will be in single quotes which turns off interpolation of $(...), etc. +// E.g.: 1'2\3\'4 5"6 (groovy string) -> '1'\''2\3\'\''4 5"6' (groovy string which can be safely pasted into shell command). +def shellString(s) { + // Replace ' with '\'' (https://unix.stackexchange.com/a/187654/260156). Then enclose with '...'. + // 1) Why not replace \ with \\? Because '...' does not treat backslashes in a special way. + // 2) And why not use ANSI-C quoting? I.e. we could replace ' with \' + // and enclose using $'...' (https://stackoverflow.com/a/8254156/4839573). + // Because ANSI-C quoting is not yet supported by Dash (default shell in Ubuntu & Debian) (https://unix.stackexchange.com/a/371873). + '\'' + s.replace('\'', '\'\\\'\'') + '\'' +} + pipeline { agent any @@ -6,9 +22,41 @@ pipeline { } stages { + stage('pre-build') { + steps { + sh 'rm -rf ./build ./tmp' + } + } + stage('build') { steps { - build 'reticulum' + script { + def baseAssetsPath = env.BASE_ASSETS_PATH + def assetBundleServer = env.ASSET_BUNDLE_SERVER + def targetS3Url = env.TARGET_S3_URL + def smokeURL = env.SMOKE_URL + def slackURL = env.SLACK_URL + + def habCommand = "sudo /usr/bin/hab-docker-studio -k mozillareality run /bin/bash scripts/hab-build-and-push.sh ${baseAssetsPath} ${assetBundleServer} ${targetS3Url} ${env.BUILD_NUMBER} ${env.GIT_COMMIT}" + sh "/usr/bin/script --return -c ${shellString(habCommand)} /dev/null" + + def gitMessage = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'[%an] %s'").trim() + def gitSha = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim() + def text = ( + "*<http://localhost:8080/job/${env.JOB_NAME}/${env.BUILD_NUMBER}|#${env.BUILD_NUMBER}>* *${env.JOB_NAME}* " + + "<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}`" + ) + def payload = 'payload=' + JsonOutput.toJson([ + text : text, + channel : "#mr-builds", + username : "buildbot", + icon_emoji: ":gift:" + ]) + sh "curl -X POST --data-urlencode ${shellString(payload)} ${slackURL}" + } } } } diff --git a/package.json b/package.json index cae88c47526aef2ed515ceaa4fda82d1a2812622..5c85d209240d059393329cb772c933150f6eb150 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "postinstall": "node ./scripts/postinstall.js", - "start": "cross-env NODE_ENV=development webpack-dev-server", + "start": "cross-env NODE_ENV=development webpack-dev-server --mode=production", "build": "rimraf ./public && cross-env NODE_ENV=production webpack --mode=production", "doc": "node ./scripts/doc/build.js", "prettier": "prettier --write '*.js' 'src/**/*.js'", @@ -26,7 +26,7 @@ "aframe-input-mapping-component": "https://github.com/mozillareality/aframe-input-mapping-component#hubs/master", "aframe-motion-capture-components": "https://github.com/mozillareality/aframe-motion-capture-components#1ca616fa67b627e447b23b35a09da175d8387668", "aframe-physics-extras": "^0.1.3", - "aframe-physics-system": "github:donmccurdy/aframe-physics-system", + "aframe-physics-system": "https://github.com/mozillareality/aframe-physics-system#hubs/master", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", "aframe-teleport-controls": "https://github.com/mozillareality/aframe-teleport-controls#hubs/master", @@ -34,16 +34,13 @@ "classnames": "^2.2.5", "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^4.5.1", + "deepmerge": "^2.1.1", "detect-browser": "^2.1.0", - "device-detect": "^1.0.7", "event-target-shim": "^3.0.1", "form-urlencoded": "^2.0.4", "jsonschema": "^1.2.2", - "mobile-detect": "^1.4.1", - "moment": "^2.22.0", - "moment-timezone": "^0.5.14", "moving-average": "^1.0.0", - "naf-janus-adapter": "^0.9.0", + "naf-janus-adapter": "^0.10.1", "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master", "nipplejs": "https://github.com/mozillareality/nipplejs#mr-social-client/master", "phoenix": "^1.3.0", @@ -87,8 +84,10 @@ "selfsigned": "^1.10.2", "shelljs": "^0.8.1", "style-loader": "^0.20.2", + "url-loader": "^1.0.1", "webpack": "^4.0.1", "webpack-cli": "^2.0.9", - "webpack-dev-server": "^3.0.0" + "webpack-dev-server": "^3.0.0", + "worker-loader": "^2.0.0" } } diff --git a/scripts/bot/package.json b/scripts/bot/package.json index 6cce5823eb1095573315d55ba19227ad718cca6a..d9072e158422a09738b3f266e96046b5e24c61bf 100644 --- a/scripts/bot/package.json +++ b/scripts/bot/package.json @@ -5,7 +5,6 @@ "devDependencies": { "docopt": "^0.6.2", "puppeteer": "1.1.0", - "query-string": "^5.0.1", - "serve": "^6.5.6" + "query-string": "^5.0.1" } } diff --git a/scripts/bot/run-bot.sh b/scripts/bot/run-bot.sh deleted file mode 100755 index aa693b25a11eca48bb83d4cc51115fdf6138f2eb..0000000000000000000000000000000000000000 --- a/scripts/bot/run-bot.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env sh - -script_directory=$(dirname "$0") -script_directory=$(realpath "$script_directory") - -echo 'Installing Hubs dependencies' -cd $script_directory/../.. -yarn -echo 'Building Hubs' -yarn build > /dev/null - -cd $script_directory -echo 'Installing bot dependencies' -yarn -echo 'Running Hubs' -yarn serve --ssl --port 8080 ../../public & -echo 'Running bots' -if [ "$1" != "" ]; then - node run-bot.js --room $1 -else - node run-bot.js -fi - -trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT diff --git a/scripts/bot/yarn.lock b/scripts/bot/yarn.lock index 3d46cd1ee8b87b01aed47b770f6ccf67ac822dab..2a00417d6e3c8f03d8a2e1661aa0a25655511164 100644 --- a/scripts/bot/yarn.lock +++ b/scripts/bot/yarn.lock @@ -2,98 +2,20 @@ # yarn lockfile v1 -accepts@~1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" - dependencies: - mime-types "~2.1.18" - negotiator "0.6.1" - -address@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" - agent-base@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" dependencies: es6-promisify "^5.0.0" -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - -ansi-align@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" - dependencies: - string-width "^2.0.0" - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - dependencies: - color-convert "^1.9.0" - -arch@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.0.tgz#3613aa46149064b3c1f0607919bf1d4786e82889" - -args@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/args/-/args-4.0.0.tgz#5ca24cdba43d4b17111c56616f5f2e9d91933954" - dependencies: - camelcase "5.0.0" - chalk "2.3.2" - leven "2.1.0" - mri "1.1.0" - async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" -async@^1.4.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" -basic-auth@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.0.tgz#015db3f353e02e56377755f962742e8981e7bbba" - dependencies: - safe-buffer "5.1.1" - -bluebird@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - -boxen@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" - dependencies: - ansi-align "^2.0.0" - camelcase "^4.0.0" - chalk "^2.0.1" - cli-boxes "^1.0.0" - string-width "^2.0.0" - term-size "^1.2.0" - widest-line "^2.0.0" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -101,100 +23,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - -camelcase@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - -camelcase@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chalk@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65" - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52" - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^2.0.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -cli-boxes@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" - -clipboardy@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-1.2.3.tgz#0526361bf78724c1f20be248d428e365433c07ef" - dependencies: - arch "^2.1.0" - execa "^0.8.0" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -color-convert@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" - dependencies: - color-name "^1.1.1" - -color-name@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - -compressible@~2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.13.tgz#0d1020ab924b2fdb4d6279875c7d6daba6baa7a9" - dependencies: - mime-db ">= 1.33.0 < 2" - -compression@^1.6.2: - version "1.7.2" - resolved "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69" - dependencies: - accepts "~1.3.4" - bytes "3.0.0" - compressible "~2.0.13" - debug "2.6.9" - on-headers "~1.0.1" - safe-buffer "5.1.1" - vary "~1.1.2" - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -207,27 +35,11 @@ concat-stream@1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" -content-type@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -dargs@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" - -debug@2.6.9, debug@^2.6.0, debug@^2.6.8: +debug@2.6.9, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -239,49 +51,14 @@ debug@^3.1.0: dependencies: ms "2.0.0" -decamelize@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" -deep-extend@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f" - -depd@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - -detect-port@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.2.2.tgz#57a44533632d8bc74ad255676866ca43f96c7469" - dependencies: - address "^1.0.1" - debug "^2.6.0" - docopt@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/docopt/-/docopt-0.6.2.tgz#b28e9e2220da5ec49f7ea5bb24a47787405eeb11" -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - es6-promise@^4.0.3: version "4.2.4" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" @@ -292,42 +69,6 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - extract-zip@^1.6.5: version "1.6.6" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" @@ -343,30 +84,10 @@ fd-slicer@~1.0.1: dependencies: pend "~1.2.0" -filesize@3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - -fs-extra@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - glob@^7.0.5: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -378,42 +99,6 @@ glob@^7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - -handlebars@4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" - dependencies: - async "^1.4.0" - optimist "^0.6.1" - source-map "^0.4.4" - optionalDependencies: - uglify-js "^2.6" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - -http-errors@1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" - dependencies: - depd "1.1.1" - inherits "2.0.3" - setprototypeof "1.0.3" - statuses ">= 1.3.1 < 2" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - https-proxy-agent@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" @@ -421,10 +106,6 @@ https-proxy-agent@^2.1.0: agent-base "^4.1.0" debug "^3.1.0" -iconv-lite@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -432,102 +113,14 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" -ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - -ip@1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - -is-stream@1.1.0, is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - optionalDependencies: - graceful-fs "^4.1.6" - -kind-of@^3.0.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - dependencies: - is-buffer "^1.1.5" - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - -leven@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - -lru-cache@^4.0.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -micro-compress@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micro-compress/-/micro-compress-1.0.0.tgz#53f5a80b4ad0320ca165a559b6e3df145d4f704f" - dependencies: - compression "^1.6.2" - -micro@9.1.4: - version "9.1.4" - resolved "https://registry.yarnpkg.com/micro/-/micro-9.1.4.tgz#dbe655f34bb3390509898ddf3fda12348f5cbaa9" - dependencies: - content-type "1.0.4" - is-stream "1.1.0" - mri "1.1.0" - raw-body "2.3.2" - -"mime-db@>= 1.33.0 < 2", mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - -mime-types@2.1.18, mime-types@~2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - dependencies: - mime-db "~1.33.0" - -mime@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - mime@^1.3.4: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -542,109 +135,34 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - mkdirp@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" dependencies: minimist "0.0.8" -mri@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.0.tgz#5c0a3f29c8ccffbbb1ec941dcec09d71fa32f36a" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - -node-version@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.1.3.tgz#1081c87cce6d2dbbd61d0e51e28c287782678496" - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - dependencies: - path-key "^2.0.0" - object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: wrappy "1" -openssl-self-signed-certificate@1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/openssl-self-signed-certificate/-/openssl-self-signed-certificate-1.1.6.tgz#9d3a4776b1a57e9847350392114ad2f915a83dd4" - -opn@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c" - dependencies: - is-wsl "^1.1.0" - -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - -path-key@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - -path-type@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - dependencies: - pify "^3.0.0" - pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" @@ -657,10 +175,6 @@ proxy-from-env@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - puppeteer@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.1.0.tgz#97fbc2fbbf9ab659e7e202a68ac1ba54b8bc0a25" @@ -682,28 +196,6 @@ query-string@^5.0.1: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -range-parser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - -raw-body@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" - dependencies: - bytes "3.0.0" - http-errors "1.6.2" - iconv-lite "0.4.19" - unpipe "1.0.0" - -rc@^1.0.1, rc@^1.1.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.7.tgz#8a10ca30d588d00464360372b890d06dacd02297" - dependencies: - deep-extend "^0.5.1" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - readable-stream@^2.2.2: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -716,239 +208,38 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -registry-auth-token@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20" - dependencies: - rc "^1.1.6" - safe-buffer "^5.0.1" - -registry-url@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" - dependencies: - rc "^1.0.1" - -repeat-string@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - dependencies: - align-text "^0.1.1" - rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: glob "^7.0.5" -safe-buffer@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - -safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" -send@0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.4.0" - -serve@^6.5.6: - version "6.5.6" - resolved "https://registry.yarnpkg.com/serve/-/serve-6.5.6.tgz#579136688f80f6bf4a618ca8e8cba10dfb4d95e3" - dependencies: - args "4.0.0" - basic-auth "2.0.0" - bluebird "3.5.1" - boxen "1.3.0" - chalk "2.4.0" - clipboardy "1.2.3" - dargs "5.1.0" - detect-port "1.2.2" - filesize "3.6.1" - fs-extra "5.0.0" - handlebars "4.0.11" - ip "1.1.5" - micro "9.1.4" - micro-compress "1.0.0" - mime-types "2.1.18" - node-version "1.1.3" - openssl-self-signed-certificate "1.1.6" - opn "5.3.0" - path-is-inside "1.0.2" - path-type "3.0.0" - send "0.16.2" - update-check "1.3.2" - -setprototypeof@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - -source-map@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - dependencies: - amdefine ">=0.0.4" - -source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - -"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - -statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" -string-width@^2.0.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" dependencies: safe-buffer "~5.1.0" -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - dependencies: - ansi-regex "^3.0.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - -supports-color@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" - dependencies: - has-flag "^3.0.0" - -term-size@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" - dependencies: - execa "^0.7.0" - typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -uglify-js@^2.6: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - ultron@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" -universalify@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" - -unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - -update-check@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.3.2.tgz#460f9e9ab24820367f3edbeb4d4142d9936ff171" - dependencies: - registry-auth-token "3.3.2" - registry-url "3.1.0" - util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - -which@^1.2.9: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - -widest-line@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.0.tgz#0142a4e8a243f8882c0233aa0e0281aa76152273" - dependencies: - string-width "^2.1.1" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -961,19 +252,6 @@ ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" - yauzl@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" diff --git a/scripts/build_local_reticulum.sh b/scripts/build_local_reticulum.sh deleted file mode 100755 index 9a19f9f202b688b27213b0478f9e1a14e67d2620..0000000000000000000000000000000000000000 --- a/scripts/build_local_reticulum.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -if [ ! -e ../reticulum ]; then - echo "This script assumes reticulum is checked out in a sibling to this folder." -fi - -rm -rf ../reticulum/priv/static ; GENERATE_SMOKE_TESTS=true BASE_ASSETS_PATH=https://hubs.local:4000/ yarn build -- --output-path ../reticulum/priv/static diff --git a/scripts/hab-build-and-push.sh b/scripts/hab-build-and-push.sh new file mode 100755 index 0000000000000000000000000000000000000000..f890d0e053a95860c5d758a5288c451ac7bc89f1 --- /dev/null +++ b/scripts/hab-build-and-push.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +export BASE_ASSETS_PATH=$1 +export ASSET_BUNDLE_SERVER=$2 +export TARGET_S3_URL=$3 +export BUILD_NUMBER=$4 +export GIT_COMMIT=$5 +export BUILD_VERSION="${BUILD_NUMBER} (${GIT_COMMIT})" + +# To build + push to S3 run: +# hab studio run "bash scripts/hab-build-and-push.sh" + +# On exit, need to make all files writable so CI can clean on next build +trap 'chmod -R a+rw .' EXIT + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +pushd "$DIR/.." + +mkdir -p .yarn +mkdir -p node_modules +mkdir -p build + +# Yarn expects /usr/local/share +# https://github.com/yarnpkg/yarn/issues/4628 +mkdir -p /usr/local/share + +rm /usr/bin/env +ln -s "$(hab pkg path core/coreutils)/bin/env" /usr/bin/env +hab pkg install -b core/coreutils core/bash core/node core/yarn core/git core/aws-cli + +yarn install --cache-folder .yarn +GENERATE_SMOKE_TESTS=true yarn build --output-path build +mkdir build/pages +mv build/*.html build/pages + +aws s3 sync --acl public-read --cache-control "max-age=31556926" build/assets "$TARGET_S3_URL/assets" +aws s3 sync --acl public-read --cache-control "no-cache" --delete build/pages "$TARGET_S3_URL/pages/latest" diff --git a/scripts/run-local-reticulum.sh b/scripts/run-local-reticulum.sh new file mode 100644 index 0000000000000000000000000000000000000000..976d858cd8dd7adea49ae587995759c431420182 --- /dev/null +++ b/scripts/run-local-reticulum.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +BASE_ASSETS_PATH=https://hubs.local:8080/ DEV_RETICULUM_SERVER=hubs.local:4000 yarn start diff --git a/src/assets/images/media-error.gif b/src/assets/images/media-error.gif new file mode 100644 index 0000000000000000000000000000000000000000..825bb624f1770088bf75eaa515d2e612e61adae9 Binary files /dev/null and b/src/assets/images/media-error.gif differ diff --git a/src/assets/stylesheets/2d-hud.scss b/src/assets/stylesheets/2d-hud.scss index 263569b58b04e599868e601cd0c960cee543c843..475e4615923a6e1359984b1c6ea9be72e9242947 100644 --- a/src/assets/stylesheets/2d-hud.scss +++ b/src/assets/stylesheets/2d-hud.scss @@ -1,3 +1,5 @@ +@import 'shared'; + :local(.container) { position: absolute; top: 10px; @@ -37,9 +39,24 @@ width: 40px; height: 40px; background-size: 100%; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; cursor: pointer; } +:local(.addMediaButton) { + position: absolute; + top: 90px; + background-color: #404040; +} + +:local(.iconButton.small) { + width: 30px; + height: 30px; +} + :local(.iconButton.large) { width: 80px; height: 80px; diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss index 690db56c83a0fee301b0bb95952f6a1622ec9140..58b2259fc71f0097d8f2b032b1fcc9bfa5305251 100644 --- a/src/assets/stylesheets/info-dialog.scss +++ b/src/assets/stylesheets/info-dialog.scss @@ -79,7 +79,7 @@ } } -.invite-form { +.invite-form, .add-media-form { display: flex; flex-direction: column; align-items: center; diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 79d11599544849a83f9982617dafddd3bb6c268a..1f8ae6a1b0986b0576ad3dd567b1fc8bb96c1d86 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -40,6 +40,7 @@ "exit.subtitle.closed": "This room is no longer available.", "exit.subtitle.full": "This room is full, please try again later.", "exit.subtitle.connect_error": "Unable to connect to this room, please try again later.", + "exit.subtitle.version_mismatch": "The version you deployed is not available yet. Your browser will refresh in 5 seconds.", "autoexit.title": "Auto-ending session in ", "autoexit.title_units": " seconds", "autoexit.subtitle": "You have started another session.", diff --git a/src/components/auto-box-collider.js b/src/components/auto-box-collider.js new file mode 100644 index 0000000000000000000000000000000000000000..220ad92ed83ba506aa63f70dc2ee24e087ce61db --- /dev/null +++ b/src/components/auto-box-collider.js @@ -0,0 +1,49 @@ +AFRAME.registerComponent("auto-box-collider", { + schema: { + resize: { default: false }, + resizeLength: { default: 0.5 } + }, + + init() { + this.onLoaded = this.onLoaded.bind(this); + this.el.addEventListener("model-loaded", this.onLoaded); + }, + + remove() { + this.el.removeEventListener("model-loaded", this.onLoaded); + }, + + onLoaded() { + const rotation = this.el.object3D.rotation.clone(); + this.el.object3D.rotation.set(0, 0, 0); + const { min, max } = new THREE.Box3().setFromObject(this.el.object3DMap.mesh); + const halfExtents = new THREE.Vector3() + .addVectors(min.clone().negate(), max) + .multiplyScalar(0.5 / this.el.object3D.scale.x); + this.el.setAttribute("shape", { + shape: "box", + halfExtents: halfExtents, + offset: new THREE.Vector3(0, halfExtents.y, 0) + }); + if (this.data.resize) { + this.resize(min, max); + } + this.el.object3D.rotation.copy(rotation); + this.el.removeAttribute("auto-box-collider"); + }, + + // Adjust the scale such that the object fits within a box of a specified size. + resize(min, max) { + const dX = Math.abs(max.x - min.x); + const dY = Math.abs(max.y - min.y); + const dZ = Math.abs(max.z - min.z); + const lengthOfLongestComponent = Math.max(dX, dY, dZ); + const correctiveFactor = this.data.resizeLength / lengthOfLongestComponent; + const scale = this.el.object3D.scale; + this.el.setAttribute("scale", { + x: scale.x * correctiveFactor, + y: scale.y * correctiveFactor, + z: scale.z * correctiveFactor + }); + } +}); diff --git a/src/components/auto-scale-cannon-physics-body.js b/src/components/auto-scale-cannon-physics-body.js new file mode 100644 index 0000000000000000000000000000000000000000..1633dffef17c70db520a7e8edb7ef3c819734970 --- /dev/null +++ b/src/components/auto-scale-cannon-physics-body.js @@ -0,0 +1,31 @@ +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("auto-scale-cannon-physics-body", { + dependencies: ["body"], + schema: { + equalityEpsilon: { default: 0.001 }, + debounceDelay: { default: 100 } + }, + + init() { + this.body = this.el.components["body"]; + this.prevScale = this.el.object3D.scale.clone(); + this.nextUpdateTime = -1; + }, + + tick(t) { + const scale = this.el.object3D.scale; + // Note: This only checks if the LOCAL scale of the object3D changes. + if (!almostEquals(this.data.equalityEpsilon, scale, this.prevScale)) { + this.prevScale.copy(scale); + this.nextUpdateTime = t + this.data.debounceDelay; + } + + if (this.nextUpdateTime > 0 && t > this.nextUpdateTime) { + this.nextUpdateTime = -1; + this.body.updateCannonScale(); + } + } +}); diff --git a/src/components/character-controller.js b/src/components/character-controller.js index f32debe666107ccfb6144f94e7ca5afe3f9f4168..02322f1a48a7bd6c1f1c137f5ba3e248ff885f5c 100644 --- a/src/components/character-controller.js +++ b/src/components/character-controller.js @@ -18,8 +18,8 @@ AFRAME.registerComponent("character-controller", { }, init: function() { - this.navGroup; - this.navNode; + this.navGroup = null; + this.navNode = null; this.velocity = new THREE.Vector3(0, 0, 0); this.accelerationInput = new THREE.Vector3(0, 0, 0); this.pendingSnapRotationMatrix = new THREE.Matrix4(); @@ -160,7 +160,7 @@ AFRAME.registerComponent("character-controller", { setPositionOnNavMesh: function(startPosition, endPosition, object3D) { const nav = this.el.sceneEl.systems.nav; if (nav.navMesh) { - if (!this.navGroup) { + if (this.navGroup == null) { this.navGroup = nav.getGroup(endPosition); } this.navNode = this.navNode || nav.getNode(endPosition, this.navGroup); diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index e0dda6f0bce57127c09ef5580bf84a497ad2caf8..38be846de6e7f68fbf937be2937459a445099555 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -65,7 +65,7 @@ AFRAME.registerComponent("cursor-controller", { const rayObject = this.data.rayObject.object3D; rayObjectRotation.setFromRotationMatrix(rayObject.matrixWorld); this.direction - .set(0, 0, 1) + .set(0, 0, -1) .applyQuaternion(rayObjectRotation) .normalize(); this.origin.setFromMatrixPosition(rayObject.matrixWorld); diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index 55d62b10e997a943d39c6e593cb35256c67b0e6c..b1195922467eb57dd6b536e688b147c164e502ea 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -74,16 +74,34 @@ function cloneGltf(gltf) { return clone; } -const inflateEntities = function(parentEl, node, gltfPath) { - // setObject3D mutates the node's parent, so we have to copy - const children = node.children.slice(0); +/// Walks the tree of three.js objects starting at the given node, using the GLTF data +/// and template data to construct A-Frame entities and components when necessary. +/// (It's unnecessary to construct entities for subtrees that have no component data +/// or templates associated with any of their nodes.) +/// +/// Returns the A-Frame entity associated with the given node, if one was constructed. +const inflateEntities = function(node, templates, gltfPath) { + // inflate subtrees first so that we can determine whether or not this node needs to be inflated + const childEntities = []; + const children = node.children.slice(0); // setObject3D mutates the node's parent, so we have to copy + for (const child of children) { + const el = inflateEntities(child, templates, gltfPath); + if (el) { + childEntities.push(el); + } + } + + const nodeHasBehavior = node.userData.components || node.name in templates; + if (!nodeHasBehavior && !childEntities.length) { + return null; // we don't need an entity for this node + } const el = document.createElement("a-entity"); + el.append.apply(el, childEntities); // Remove invalid CSS class name characters. const className = (node.name || node.uuid).replace(/[^\w-]/g, ""); el.classList.add(className); - parentEl.appendChild(el); // AFRAME rotation component expects rotations in YXZ, convert it if (node.rotation.order !== "YXZ") { @@ -138,15 +156,11 @@ const inflateEntities = function(parentEl, node, gltfPath) { } } - children.forEach(childNode => { - inflateEntities(el, childNode, gltfPath); - }); - return el; }; -function attachTemplate(root, { selector, templateRoot }) { - const targetEls = root.querySelectorAll(selector); +function attachTemplate(root, name, templateRoot) { + const targetEls = root.querySelectorAll("." + name); for (const el of targetEls) { const root = templateRoot.cloneNode(true); // Merge root element attributes with the target element @@ -168,30 +182,15 @@ function nextTick() { } function cachedLoadGLTF(src, preferredTechnique, onProgress) { - return new Promise((resolve, reject) => { - // Load the gltf model from the cache if it exists. - if (GLTFCache[src]) { - // Use a cloned copy of the cached model. - resolve(cloneGltf(GLTFCache[src])); - } else { - // Otherwise load the new gltf model. + // Load the gltf model from the cache if it exists. + if (!GLTFCache[src]) { + GLTFCache[src] = new Promise((resolve, reject) => { const gltfLoader = new THREE.GLTFLoader(); gltfLoader.preferredTechnique = preferredTechnique; - - gltfLoader.load( - src, - model => { - if (!GLTFCache[src]) { - // Store a cloned copy of the gltf model. - GLTFCache[src] = cloneGltf(model); - } - resolve(model); - }, - onProgress, - reject - ); - } - }); + gltfLoader.load(src, resolve, onProgress, reject); + }); + } + return GLTFCache[src].then(cloneGltf); } /** @@ -203,11 +202,11 @@ function cachedLoadGLTF(src, preferredTechnique, onProgress) { AFRAME.registerComponent("gltf-model-plus", { schema: { src: { type: "string" }, - inflate: { default: false }, - preferredTechnique: { default: AFRAME.utils.device.isMobile() ? "KHR_materials_unlit" : "pbrMetallicRoughness" } + inflate: { default: false } }, init() { + this.preferredTechnique = AFRAME.utils.device.isMobile() ? "KHR_materials_unlit" : "pbrMetallicRoughness"; this.loadTemplates(); }, @@ -216,13 +215,11 @@ AFRAME.registerComponent("gltf-model-plus", { }, loadTemplates() { - this.templates = []; - this.el.querySelectorAll(":scope > template").forEach(templateEl => - this.templates.push({ - selector: templateEl.getAttribute("data-selector"), - templateRoot: document.importNode(templateEl.firstElementChild || templateEl.content.firstElementChild, true) - }) - ); + this.templates = {}; + this.el.querySelectorAll(":scope > template").forEach(templateEl => { + const root = document.importNode(templateEl.firstElementChild || templateEl.content.firstElementChild, true); + this.templates[templateEl.getAttribute("data-name")] = root; + }); }, async applySrc(src) { @@ -245,7 +242,7 @@ AFRAME.registerComponent("gltf-model-plus", { } const gltfPath = THREE.LoaderUtils.extractUrlBase(src); - const model = await cachedLoadGLTF(src, this.data.preferredTechnique); + const model = await cachedLoadGLTF(src, this.preferredTechnique); // If we started loading something else already // TODO: there should be a way to cancel loading instead @@ -260,17 +257,20 @@ AFRAME.registerComponent("gltf-model-plus", { this.el.setObject3D("mesh", this.model); if (this.data.inflate) { - this.inflatedEl = inflateEntities(this.el, this.model, gltfPath); + this.inflatedEl = inflateEntities(this.model, this.templates, gltfPath); + this.el.appendChild(this.inflatedEl); // TODO: Still don't fully understand the lifecycle here and how it differs between browsers, we should dig in more // Wait one tick for the appended custom elements to be connected before attaching templates await nextTick(); if (src != this.lastSrc) return; // TODO: there must be a nicer pattern for this - this.templates.forEach(attachTemplate.bind(null, this.el)); + for (const name in this.templates) { + attachTemplate(this.el, name, this.templates[name]); + } } this.el.emit("model-loaded", { format: "gltf", model: this.model }); } catch (e) { - console.error("Failed to load glTF model", e.message, this); + console.error("Failed to load glTF model", e, this); this.el.emit("model-error", { format: "gltf", src }); } }, diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js index 3b2f3026a56ce64afb9460c6c6de0f37ccc6d969..1bf9e71de656aa9b4972cdf59e534af78599493b 100644 --- a/src/components/ik-controller.js +++ b/src/components/ik-controller.js @@ -39,14 +39,14 @@ function findIKRoot(entity) { */ AFRAME.registerComponent("ik-controller", { schema: { - leftEye: { type: "string", default: ".LeftEye" }, - rightEye: { type: "string", default: ".RightEye" }, - head: { type: "string", default: ".Head" }, - neck: { type: "string", default: ".Neck" }, - leftHand: { type: "string", default: ".LeftHand" }, - rightHand: { type: "string", default: ".RightHand" }, - chest: { type: "string", default: ".Chest" }, - hips: { type: "string", default: ".Hips" }, + leftEye: { type: "string", default: "LeftEye" }, + rightEye: { type: "string", default: "RightEye" }, + head: { type: "string", default: "Head" }, + neck: { type: "string", default: "Neck" }, + leftHand: { type: "string", default: "LeftHand" }, + rightHand: { type: "string", default: "RightHand" }, + chest: { type: "string", default: "Chest" }, + hips: { type: "string", default: "Hips" }, rotationSpeed: { default: 5 } }, @@ -86,46 +86,46 @@ AFRAME.registerComponent("ik-controller", { update(oldData) { if (this.data.leftEye !== oldData.leftEye) { - this.leftEye = this.el.querySelector(this.data.leftEye); + this.leftEye = this.el.object3D.getObjectByName(this.data.leftEye); } if (this.data.rightEye !== oldData.rightEye) { - this.rightEye = this.el.querySelector(this.data.rightEye); + this.rightEye = this.el.object3D.getObjectByName(this.data.rightEye); } if (this.data.head !== oldData.head) { - this.head = this.el.querySelector(this.data.head); + this.head = this.el.object3D.getObjectByName(this.data.head); } if (this.data.neck !== oldData.neck) { - this.neck = this.el.querySelector(this.data.neck); + this.neck = this.el.object3D.getObjectByName(this.data.neck); } if (this.data.leftHand !== oldData.leftHand) { - this.leftHand = this.el.querySelector(this.data.leftHand); + this.leftHand = this.el.object3D.getObjectByName(this.data.leftHand); } if (this.data.rightHand !== oldData.rightHand) { - this.rightHand = this.el.querySelector(this.data.rightHand); + this.rightHand = this.el.object3D.getObjectByName(this.data.rightHand); } if (this.data.chest !== oldData.chest) { - this.chest = this.el.querySelector(this.data.chest); + this.chest = this.el.object3D.getObjectByName(this.data.chest); } if (this.data.hips !== oldData.hips) { - this.hips = this.el.querySelector(this.data.hips); + this.hips = this.el.object3D.getObjectByName(this.data.hips); } // Set middleEye's position to be right in the middle of the left and right eyes. - this.middleEyePosition.addVectors(this.leftEye.object3D.position, this.rightEye.object3D.position); + this.middleEyePosition.addVectors(this.leftEye.position, this.rightEye.position); this.middleEyePosition.divideScalar(2); this.middleEyeMatrix.makeTranslation(this.middleEyePosition.x, this.middleEyePosition.y, this.middleEyePosition.z); this.invMiddleEyeToHead = this.middleEyeMatrix.getInverse(this.middleEyeMatrix); this.invHipsToHeadVector - .addVectors(this.chest.object3D.position, this.neck.object3D.position) - .add(this.head.object3D.position) + .addVectors(this.chest.position, this.neck.position) + .add(this.head.position) .negate(); }, @@ -162,35 +162,30 @@ AFRAME.registerComponent("ik-controller", { // Then position the hips such that the head is aligned with headTransform // (which positions middleEye in line with the hmd) - hips.object3D.position.setFromMatrixPosition(headTransform).add(invHipsToHeadVector); + hips.position.setFromMatrixPosition(headTransform).add(invHipsToHeadVector); // Animate the hip rotation to follow the Y rotation of the camera with some damping. cameraYRotation.setFromRotationMatrix(cameraForward, "YXZ"); cameraYRotation.x = 0; cameraYRotation.z = 0; cameraYQuaternion.setFromEuler(cameraYRotation); - Quaternion.slerp( - hips.object3D.quaternion, - cameraYQuaternion, - hips.object3D.quaternion, - this.data.rotationSpeed * dt / 1000 - ); + Quaternion.slerp(hips.quaternion, cameraYQuaternion, hips.quaternion, this.data.rotationSpeed * dt / 1000); // Take the head orientation computed from the hmd, remove the Y rotation already applied to it by the hips, // and apply it to the head - invHipsQuaternion.copy(hips.object3D.quaternion).inverse(); - head.object3D.quaternion.setFromRotationMatrix(headTransform).premultiply(invHipsQuaternion); + invHipsQuaternion.copy(hips.quaternion).inverse(); + head.quaternion.setFromRotationMatrix(headTransform).premultiply(invHipsQuaternion); - hips.object3D.updateMatrix(); - rootToChest.multiplyMatrices(hips.object3D.matrix, chest.object3D.matrix); + hips.updateMatrix(); + rootToChest.multiplyMatrices(hips.matrix, chest.matrix); invRootToChest.getInverse(rootToChest); this.updateHand(this.hands.left, leftHand, leftController); this.updateHand(this.hands.right, rightHand, rightController); }, - updateHand(handState, hand, controller) { - const handObject3D = hand.object3D; + updateHand(handState, handObject3D, controller) { + const hand = handObject3D.el; const handMatrix = handObject3D.matrix; const controllerObject3D = controller.object3D; diff --git a/src/components/image-plus.js b/src/components/image-plus.js new file mode 100644 index 0000000000000000000000000000000000000000..744020534a6d9f5f6924ea12084572d4eb325109 --- /dev/null +++ b/src/components/image-plus.js @@ -0,0 +1,248 @@ +import GIFWorker from "../workers/gifparsing.worker.js"; +import errorImageSrc from "!!url-loader!../assets/images/media-error.gif"; + +class GIFTexture extends THREE.Texture { + constructor(frames, delays, disposals) { + super(document.createElement("canvas")); + this.image.width = frames[0].width; + this.image.height = frames[0].height; + + this._ctx = this.image.getContext("2d"); + + this.generateMipmaps = false; + this.isVideoTexture = true; + this.minFilter = THREE.NearestFilter; + + this.frames = frames; + this.delays = delays; + this.disposals = disposals; + + this.frame = 0; + this.frameStartTime = Date.now(); + } + + update() { + if (!this.frames || !this.delays || !this.disposals) return; + const now = Date.now(); + if (now - this.frameStartTime > this.delays[this.frame]) { + if (this.disposals[this.frame] === 2) { + this._ctx.clearRect(0, 0, this.image.width, this.image.width); + } + this.frame = (this.frame + 1) % this.frames.length; + this.frameStartTime = now; + this._ctx.drawImage(this.frames[this.frame], 0, 0, this.image.width, this.image.height); + this.needsUpdate = true; + } + } +} + +/** + * Create video element to be used as a texture. + * + * @param {string} src - Url to a video file. + * @returns {Element} Video element. + */ +function createVideoEl(src) { + const videoEl = document.createElement("video"); + videoEl.setAttribute("playsinline", ""); + videoEl.setAttribute("webkit-playsinline", ""); + videoEl.autoplay = true; + videoEl.loop = true; + videoEl.crossOrigin = "anonymous"; + videoEl.src = src; + return videoEl; +} + +const textureLoader = new THREE.TextureLoader(); +textureLoader.setCrossOrigin("anonymous"); + +const textureCache = new Map(); + +const errorImage = new Image(); +errorImage.src = errorImageSrc; +const errorTexture = new THREE.Texture(errorImage); +errorTexture.magFilter = THREE.NearestFilter; +errorImage.onload = () => { + errorTexture.needsUpdate = true; +}; + +AFRAME.registerComponent("image-plus", { + dependencies: ["geometry"], + + schema: { + src: { type: "string" }, + contentType: { type: "string" } + }, + + _fit(w, h) { + const ratio = (h || 1.0) / (w || 1.0); + const geo = this.el.geometry; + let width, height; + if (geo && geo.width) { + if (geo.height && ratio > 1) { + width = geo.width / ratio; + } else { + height = geo.height * ratio; + } + } else if (geo && geo.height) { + width = geo.width / ratio; + } else { + width = Math.min(1.0, 1.0 / ratio); + height = Math.min(1.0, ratio); + } + this.el.setAttribute("geometry", { width, height }); + this.el.setAttribute("shape", { + shape: "box", + halfExtents: { + x: width / 2, + y: height / 2, + z: 0.05 + } + }); + }, + + init() { + const material = new THREE.MeshBasicMaterial(); + material.side = THREE.DoubleSide; + material.transparent = true; + this.el.getObject3D("mesh").material = material; + }, + + remove() { + const material = this.el.getObject3D("mesh").material; + const texture = material.map; + + if (texture === errorTexture) return; + + const url = texture.image.src; + const cacheItem = textureCache.get(url); + cacheItem.count--; + if (cacheItem.count <= 0) { + // Unload the video element to prevent it from continuing to play in the background + if (texture.image instanceof HTMLVideoElement) { + const video = texture.image; + video.pause(); + video.src = ""; + video.load(); + } + + texture.dispose(); + + // THREE never lets go of material refs, long running PR HERE https://github.com/mrdoob/three.js/pull/12464 + // Mitigate the damage a bit by at least breaking the image ref so Image/Video elements can be freed + // TODO: If/when THREE gets fixed, we should be able to safely remove this + delete texture.image; + + textureCache.delete(url); + } + }, + + async loadGIF(url) { + return new Promise((resolve, reject) => { + // TODO: pool workers + const worker = new GIFWorker(); + worker.onmessage = e => { + const [success, frames, delays, disposals] = e.data; + if (!success) { + reject(`error loading gif: ${e.data[1]}`); + return; + } + + let loadCnt = 0; + for (let i = 0; i < frames.length; i++) { + const img = new Image(); + img.onload = e => { + loadCnt++; + frames[i] = e.target; + if (loadCnt === frames.length) { + const texture = new GIFTexture(frames, delays, disposals); + texture.image.src = url; + resolve(texture); + } + }; + img.src = frames[i]; + } + }; + fetch(url, { mode: "cors" }) + .then(r => r.arrayBuffer()) + .then(rawImageData => { + worker.postMessage(rawImageData, [rawImageData]); + }) + .catch(reject); + }); + }, + + loadVideo(url) { + return new Promise((resolve, reject) => { + const videoEl = createVideoEl(url); + + const texture = new THREE.VideoTexture(videoEl); + texture.minFilter = THREE.LinearFilter; + videoEl.addEventListener("loadedmetadata", () => resolve(texture), { once: true }); + videoEl.onerror = reject; + + // If iOS and video is HLS, do some hacks. + if ( + this.el.sceneEl.isIOS && + AFRAME.utils.material.isHLS( + videoEl.src || videoEl.getAttribute("src"), + videoEl.type || videoEl.getAttribute("type") + ) + ) { + // Actually BGRA. Tell shader to correct later. + texture.format = THREE.RGBAFormat; + texture.needsCorrectionBGRA = true; + // Apparently needed for HLS. Tell shader to correct later. + texture.flipY = false; + texture.needsCorrectionFlipY = true; + } + }); + }, + + loadImage(url) { + return new Promise((resolve, reject) => { + textureLoader.load(url, resolve, null, function(xhr) { + reject(`'${url}' could not be fetched (Error code: ${xhr.status}; Response: ${xhr.statusText})`); + }); + }); + }, + + async update() { + let texture; + try { + const url = this.data.src; + const contentType = this.data.contentType; + if (!url) { + return; + } + + if (textureCache.has(url)) { + const cacheItem = textureCache.get(url); + texture = cacheItem.texture; + cacheItem.count++; + } else { + if (url === "error") { + texture = errorTexture; + } else if (contentType === "image/gif") { + texture = await this.loadGIF(url); + } else if (contentType.startsWith("image/")) { + texture = await this.loadImage(url); + } else if (contentType.startsWith("video")) { + texture = await this.loadVideo(url); + } else { + throw new Error(`Unknown centent type: ${contentType}`); + } + + textureCache.set(url, { count: 1, texture }); + } + } catch (e) { + console.error("Error loading media", this.data.src, e); + texture = errorTexture; + } + + const material = this.el.getObject3D("mesh").material; + material.map = texture; + material.needsUpdate = true; + this._fit(texture.image.videoWidth || texture.image.width, texture.image.videoHeight || texture.image.height); + } +}); diff --git a/src/components/input-configurator.js b/src/components/input-configurator.js index 1609b20c7b163504d3dcc46390767bf14c91a09e..df36b38d8c4bb81a2f8fd98824cbd3ecf4ef7f15 100644 --- a/src/components/input-configurator.js +++ b/src/components/input-configurator.js @@ -69,7 +69,7 @@ AFRAME.registerComponent("input-configurator", { this.eventHandlers = []; this.actionEventHandler = null; if (this.lookOnMobile) { - this.lookOnMobile.el.removeComponent("look-on-mobile"); + this.lookOnMobile.el.removeAttribute("look-on-mobile"); this.lookOnMobile = null; } this.cursorRequiresManagement = false; diff --git a/src/components/networked-counter.js b/src/components/networked-counter.js index be9725cf6a50efb822370ab5d4b7196a6bcce3c4..6902658b03eef05429e2de1fdcd8cf48debec090 100644 --- a/src/components/networked-counter.js +++ b/src/components/networked-counter.js @@ -6,130 +6,90 @@ AFRAME.registerComponent("networked-counter", { schema: { max: { default: 3 }, - ttl: { default: 120 }, + ttl: { default: 0 }, grab_event: { type: "string", default: "grab-start" }, release_event: { type: "string", default: "grab-end" } }, - init: function() { - this.count = 0; - this.queue = {}; - this.timeouts = {}; + init() { + this.registeredEls = new Map(); }, - remove: function() { - for (const id in this.queue) { - if (this.queue.hasOwnProperty(id)) { - const item = this.queue[id]; - item.el.removeEventListener(this.data.grab_event, item.onGrabHandler); - item.el.removeEventListener(this.data.release_event, item.onReleaseHandler); - } - } - - for (const id in this.timeouts) { - this._removeTimeout(id); - } + remove() { + this.registeredEls.forEach(({ onGrabHandler, onReleaseHandler, timeout }, el) => { + el.removeEventListener(this.data.grab_event, onGrabHandler); + el.removeEventListener(this.data.release_event, onReleaseHandler); + clearTimeout(timeout); + }); + this.registeredEls.clear(); }, - register: function(networkedEl) { - if (this.data.max <= 0) { - return; - } - - const id = NAF.utils.getNetworkId(networkedEl); - if (id && this.queue.hasOwnProperty(id)) { - return; - } + register(el) { + if (this.data.max <= 0 || this.registeredEls.has(el)) return; - const now = Date.now(); - const grabEventListener = this._onGrabbed.bind(this, id); - const releaseEventListener = this._onReleased.bind(this, id); + const grabEventListener = this._onGrabbed.bind(this, el); + const releaseEventListener = this._onReleased.bind(this, el); - this.queue[id] = { - ts: now, - el: networkedEl, + this.registeredEls.set(el, { + ts: Date.now(), onGrabHandler: grabEventListener, onReleaseHandler: releaseEventListener - }; + }); - networkedEl.addEventListener(this.data.grab_event, grabEventListener); - networkedEl.addEventListener(this.data.release_event, releaseEventListener); + el.addEventListener(this.data.grab_event, grabEventListener); + el.addEventListener(this.data.release_event, releaseEventListener); - this.count++; - - if (!this._isCurrentlyGrabbed(id)) { - this._addTimeout(id); + if (!el.is("grabbed")) { + this._startTimer(el); } this._destroyOldest(); }, - deregister: function(networkedEl) { - const id = NAF.utils.getNetworkId(networkedEl); - if (id && this.queue.hasOwnProperty(id)) { - const item = this.queue[id]; - networkedEl.removeEventListener(this.data.grab_event, item.onGrabHandler); - networkedEl.removeEventListener(this.data.release_event, item.onReleaseHandler); - - delete this.queue[id]; - - this._removeTimeout(id); - delete this.timeouts[id]; - - this.count--; + deregister(el) { + if (this.registeredEls.has(el)) { + const { onGrabHandler, onReleaseHandler, timeout } = this.registeredEls.get(el); + el.removeEventListener(this.data.grab_event, onGrabHandler); + el.removeEventListener(this.data.release_event, onReleaseHandler); + clearTimeout(timeout); + this.registeredEls.delete(el); } }, - _onGrabbed: function(id) { - this._removeTimeout(id); + _onGrabbed(el) { + clearTimeout(this.registeredEls.get(el).timeout); }, - _onReleased: function(id) { - this._removeTimeout(id); - this._addTimeout(id); - this.queue[id].ts = Date.now(); + _onReleased(el) { + this._startTimer(el); + this.registeredEls.get(el).ts = Date.now(); }, - _destroyOldest: function() { - if (this.count > this.data.max) { - let oldest = null, - ts = Number.MAX_VALUE; - for (const id in this.queue) { - if (this.queue.hasOwnProperty(id)) { - if (this.queue[id].ts < ts && !this._isCurrentlyGrabbed(id)) { - oldest = this.queue[id]; - ts = this.queue[id].ts; - } + _destroyOldest() { + if (this.registeredEls.size > this.data.max) { + let oldestEl = null, + minTs = Number.MAX_VALUE; + this.registeredEls.forEach(({ ts }, el) => { + if (ts < minTs && !el.is("grabbed")) { + oldestEl = el; + minTs = ts; } - } - if (ts > 0) { - this.deregister(oldest.el); - this._destroy(oldest.el); - } + }); + this._destroy(oldestEl); } }, - _isCurrentlyGrabbed: function(id) { - const networkedEl = this.queue[id].el; - return networkedEl.is("grabbed"); - }, - - _addTimeout: function(id) { - const timeout = this.data.ttl * 1000; - this.timeouts[id] = setTimeout(() => { - const el = this.queue[id].el; - this.deregister(el); + _startTimer(el) { + if (!this.data.ttl) return; + clearTimeout(this.registeredEls.get(el).timeout); + this.registeredEls.get(el).timeout = setTimeout(() => { this._destroy(el); - }, timeout); - }, - - _removeTimeout: function(id) { - if (this.timeouts.hasOwnProperty(id)) { - clearTimeout(this.timeouts[id]); - } + }, this.data.ttl * 1000); }, - _destroy: function(networkedEl) { - networkedEl.parentNode.removeChild(networkedEl); + _destroy(el) { + // networked-interactable's remvoe will also call deregister, but it will happen async so we do it here as well. + this.deregister(el); + el.parentNode.removeChild(el); } }); diff --git a/src/components/offset-relative-to.js b/src/components/offset-relative-to.js index 3cd45b942ed4573ee9b588a467de456af801f62f..23920e099a94fd50373f6804bf8c02d8cae0910f 100644 --- a/src/components/offset-relative-to.js +++ b/src/components/offset-relative-to.js @@ -12,16 +12,41 @@ AFRAME.registerComponent("offset-relative-to", { }, on: { type: "string" + }, + selfDestruct: { + default: false } }, init() { - this.updateOffset(); - this.el.sceneEl.addEventListener(this.data.on, this.updateOffset.bind(this)); + this.updateOffset = this.updateOffset.bind(this); + if (this.data.on) { + this.el.sceneEl.addEventListener(this.data.on, this.updateOffset); + } else { + this.updateOffset(); + } }, - updateOffset() { - const offsetVector = new THREE.Vector3().copy(this.data.offset); - this.data.target.object3D.localToWorld(offsetVector); - this.el.setAttribute("position", offsetVector); - this.data.target.object3D.getWorldQuaternion(this.el.object3D.quaternion); - } + + updateOffset: (function() { + const offsetVector = new THREE.Vector3(); + return function() { + const obj = this.el.object3D; + const target = this.data.target.object3D; + offsetVector.copy(this.data.offset); + target.localToWorld(offsetVector); + if (obj.parent) { + obj.parent.worldToLocal(offsetVector); + } + obj.position.copy(offsetVector); + // TODO: Hack here to deal with the fact that the rotation component mutates ordering, and we network rotation without sending ordering information + // See https://github.com/networked-aframe/networked-aframe/issues/134 + obj.rotation.order = "YXZ"; + target.getWorldQuaternion(obj.quaternion); + if (this.data.selfDestruct) { + if (this.data.on) { + this.el.sceneEl.removeEventListener(this.data.on, this.updateOffset); + } + this.el.removeAttribute("offset-relative-to"); + } + }; + })() }); diff --git a/src/components/position-at-box-shape-border.js b/src/components/position-at-box-shape-border.js new file mode 100644 index 0000000000000000000000000000000000000000..59776bf6de0c2f12f2b46e40720a4287dee49c9b --- /dev/null +++ b/src/components/position-at-box-shape-border.js @@ -0,0 +1,89 @@ +const PI = Math.PI; +const HALF_PI = PI / 2; +const THREE_HALF_PI = 3 * PI / 2; +const right = new THREE.Vector3(1, 0, 0); +const forward = new THREE.Vector3(0, 0, 1); +const left = new THREE.Vector3(-1, 0, 0); +const back = new THREE.Vector3(0, 0, -1); +const dirs = { + left: { + dir: left, + rotation: THREE_HALF_PI, + halfExtent: "x" + }, + right: { + dir: right, + rotation: HALF_PI, + halfExtent: "x" + }, + forward: { + dir: forward, + rotation: 0, + halfExtent: "z" + }, + back: { + dir: back, + rotation: PI, + halfExtent: "z" + } +}; + +AFRAME.registerComponent("position-at-box-shape-border", { + schema: { + target: { type: "string" }, + dirs: { default: ["left", "right", "forward", "back"] } + }, + + init() { + this.cam = this.el.sceneEl.camera.el.object3D; + }, + + update() { + this.dirs = this.data.dirs.map(d => dirs[d]); + }, + + tick: (function() { + const camWorldPos = new THREE.Vector3(); + const targetPosition = new THREE.Vector3(); + const pointOnBoxFace = new THREE.Vector3(); + return function() { + if (!this.shape) { + this.shape = this.el.components["shape"]; + if (!this.shape) return; + } + if (!this.target) { + this.target = this.el.querySelector(this.data.target).object3D; + if (!this.target) return; + } + const halfExtents = this.shape.data.halfExtents; + this.cam.getWorldPosition(camWorldPos); + + let minSquareDistance = Infinity; + let targetDir = this.dirs[0].dir; + let targetHalfExtent = halfExtents[this.dirs[0].halfExtent]; + let targetRotation = this.dirs[0].rotation; + + for (let i = 0; i < this.dirs.length; i++) { + const dir = this.dirs[i].dir; + const halfExtent = halfExtents[this.dirs[i].halfExtent]; + pointOnBoxFace.copy(dir).multiplyScalar(halfExtent); + this.el.object3D.localToWorld(pointOnBoxFace); + const squareDistance = pointOnBoxFace.distanceToSquared(camWorldPos); + if (squareDistance < minSquareDistance) { + minSquareDistance = squareDistance; + targetDir = dir; + targetHalfExtent = halfExtent; + targetRotation = this.dirs[i].rotation; + } + } + + this.target.position.copy( + targetPosition + .copy(targetDir) + .multiplyScalar(targetHalfExtent) + .add(this.shape.data.offset) + ); + this.target.rotation.set(0, targetRotation, 0); + }; + })() +}); diff --git a/src/components/remove-networked-object-button.js b/src/components/remove-networked-object-button.js new file mode 100644 index 0000000000000000000000000000000000000000..39ec27b754a2dfbb5476c9490ee40e3c30f12f79 --- /dev/null +++ b/src/components/remove-networked-object-button.js @@ -0,0 +1,18 @@ +AFRAME.registerComponent("remove-networked-object-button", { + init() { + this.onClick = () => { + this.targetEl.parentNode.removeChild(this.targetEl); + }; + NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { + this.targetEl = networkedEl; + }); + }, + + play() { + this.el.addEventListener("click", this.onClick); + }, + + pause() { + this.el.removeEventListener("click", this.onClick); + } +}); diff --git a/src/components/sticky-object.js b/src/components/sticky-object.js new file mode 100644 index 0000000000000000000000000000000000000000..d415779078f6a0acb2d6975170cde39cbd80cf47 --- /dev/null +++ b/src/components/sticky-object.js @@ -0,0 +1,130 @@ +/* global THREE, CANNON, AFRAME */ +AFRAME.registerComponent("sticky-object", { + dependencies: ["body", "super-networked-interactable"], + + schema: { + autoLockOnLoad: { default: false }, + autoLockOnRelease: { default: false }, + autoLockSpeedLimit: { default: 0.25 } + }, + + init() { + this._onGrab = this._onGrab.bind(this); + this._onRelease = this._onRelease.bind(this); + this._onBodyLoaded = this._onBodyLoaded.bind(this); + }, + + play() { + this.el.addEventListener("grab-start", this._onGrab); + this.el.addEventListener("grab-end", this._onRelease); + this.el.addEventListener("body-loaded", this._onBodyLoaded); + }, + + pause() { + this.el.removeEventListener("grab-start", this._onGrab); + this.el.removeEventListener("grab-end", this._onRelease); + this.el.removeEventListener("body-loaded", this._onBodyLoaded); + }, + + setLocked(locked) { + if (!NAF.utils.isMine(this.el)) return; + + const mass = this.el.components["super-networked-interactable"].data.mass; + this.locked = locked; + this.el.body.type = locked ? window.CANNON.Body.STATIC : window.CANNON.Body.DYNAMIC; + this.el.setAttribute("body", { + mass: locked ? 0 : mass + }); + }, + + _onBodyLoaded() { + if (this.data.autoLockOnLoad) { + this.setLocked(true); + } + }, + + _onRelease() { + if ( + this.data.autoLockOnRelease && + this.el.body.velocity.lengthSquared() < this.data.autoLockSpeedLimit * this.data.autoLockSpeedLimit + ) { + this.setLocked(true); + } + }, + + _onGrab() { + this.setLocked(false); + }, + + remove() { + if (this.stuckTo) { + const stuckTo = this.stuckTo; + delete this.stuckTo; + stuckTo._unstickObject(); + } + } +}); + +AFRAME.registerComponent("sticky-object-zone", { + dependencies: ["physics"], + init() { + // TODO: position/rotation/impulse need to get updated if the sticky-object-zone moves + this.worldQuaternion = new THREE.Quaternion(); + this.worldPosition = new THREE.Vector3(); + this.el.object3D.getWorldQuaternion(this.worldQuaternion); + this.el.object3D.getWorldPosition(this.worldPosition); + + const dir = new THREE.Vector3(0, 0, 5).applyQuaternion(this.el.object3D.quaternion); + this.bootImpulsePosition = new CANNON.Vec3(0, 0, 0); + this.bootImpulse = new CANNON.Vec3(); + this.bootImpulse.copy(dir); + + this._onCollisions = this._onCollisions.bind(this); + this.el.addEventListener("collisions", this._onCollisions); + }, + + remove() { + this.el.removeEventListener("collisions", this._onCollisions); + }, + + _onCollisions(e) { + e.detail.els.forEach(el => { + const stickyObject = el.components["sticky-object"]; + if (!stickyObject) return; + this._setStuckObject(stickyObject); + }); + if (this.stuckObject) { + e.detail.clearedEls.forEach(el => { + if (this.stuckObject && this.stuckObject.el === el) { + this._unstickObject(); + } + }); + } + }, + + _setStuckObject(stickyObject) { + stickyObject.setLocked(true); + stickyObject.el.object3D.position.copy(this.worldPosition); + stickyObject.el.object3D.quaternion.copy(this.worldQuaternion); + stickyObject.el.body.collisionResponse = false; + stickyObject.stuckTo = this; + + if (this.stuckObject && NAF.utils.isMine(this.stuckObject.el)) { + const el = this.stuckObject.el; + this._unstickObject(); + el.body.applyImpulse(this.bootImpulse, this.bootImpulsePosition); + } + + this.stuckObject = stickyObject; + }, + + _unstickObject() { + // this condition will be false when dragging an object directly from one sticky zone to another + if (this.stuckObject.stuckTo === this) { + this.stuckObject.setLocked(false); + this.stuckObject.el.body.collisionResponse = true; + delete this.stuckObject.stuckTo; + } + delete this.stuckObject; + } +}); diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index 418b8e50f9b6d5b8f3eb7b89f4547e4dd23aee47..3b84bb7ac3b99421024113d527aa8a1ea54da725 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -24,17 +24,17 @@ AFRAME.registerComponent("super-networked-interactable", { } }); - this.grabStartListener = this._onGrabStart.bind(this); - this.ownershipLostListener = this._onOwnershipLost.bind(this); - this.el.addEventListener("grab-start", this.grabStartListener); - this.el.addEventListener("ownership-lost", this.ownershipLostListener); + this._onGrabStart = this._onGrabStart.bind(this); + this._onOwnershipLost = this._onOwnershipLost.bind(this); + this.el.addEventListener("grab-start", this._onGrabStart); + this.el.addEventListener("ownership-lost", this._onOwnershipLost); this.system.addComponent(this); }, remove: function() { this.counter.deregister(this.el); - this.el.removeEventListener("grab-start", this.grabStartListener); - this.el.removeEventListener("ownership-lost", this.ownershipLostListener); + this.el.removeEventListener("grab-start", this._onGrabStart); + this.el.removeEventListener("ownership-lost", this._onOwnershipLost); this.system.removeComponent(this); }, diff --git a/src/components/text-button.js b/src/components/text-button.js index 67af3653dcc4c9e81b59aec376feab6a00eb7fe4..491b482a248099ae8a277ed5cb04dbb638df6f83 100644 --- a/src/components/text-button.js +++ b/src/components/text-button.js @@ -57,3 +57,12 @@ AFRAME.registerComponent("text-button", { this.textEl.setAttribute("text", "color", hovering ? this.data.textHoverColor : this.data.textColor); } }); + +const noop = function() {}; +// TODO: this should ideally be fixed upstream somehow but its pretty tricky since text is just a geometry not a different type of Object3D, and Object3D is what handles raycast checks. +AFRAME.registerComponent("text-raycast-hack", { + dependencies: ["text"], + init() { + this.el.getObject3D("text").raycast = noop; + } +}); diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index d036e0e6c14d308ad07f3458f5f937c3dccdfeda..0552e61c854ad7efe6ed5e97020b512ed5d0feac 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -23,6 +23,7 @@ AFRAME.GLTFModelPlus.registerComponent("shape", "shape"); AFRAME.GLTFModelPlus.registerComponent("visible", "visible"); AFRAME.GLTFModelPlus.registerComponent("spawn-point", "spawn-point"); AFRAME.GLTFModelPlus.registerComponent("hoverable", "hoverable"); +AFRAME.GLTFModelPlus.registerComponent("sticky-zone", "sticky-zone"); AFRAME.GLTFModelPlus.registerComponent("nav-mesh", "nav-mesh", (el, componentName, componentData, gltfPath) => { if (componentData.src) { componentData.src = resolveURL(componentData.src, gltfPath); diff --git a/src/hub.html b/src/hub.html index d53540bac7114ccc2dbf0b1e6ce883a703e1a5c6..2a379ab44607c2318ab7017aa00e12b45e982d50 100644 --- a/src/hub.html +++ b/src/hub.html @@ -40,15 +40,15 @@ vr-mode-ui="enabled: false" pinch-to-move input-configurator=" - gazeCursorRayObject: #player-camera-reverse-z; + gazeCursorRayObject: #player-camera; cursorController: #cursor-controller; gazeTeleporter: #gaze-teleport; camera: #player-camera; playerRig: #player-rig; leftController: #player-left-controller; - leftControllerRayObject: #player-left-controller-reverse-z; + leftControllerRayObject: #player-left-controller; rightController: #player-right-controller; - rightControllerRayObject: #player-right-controller-reverse-z;" + rightControllerRayObject: #player-right-controller;" > <a-assets> @@ -100,11 +100,11 @@ <a-entity class="right-controller"></a-entity> <a-entity class="model" gltf-model-plus="inflate: true"> - <template data-selector=".RootScene"> - <a-entity ik-controller hand-pose__left hand-pose__right animation-mixer space-invader-mesh="meshSelector: .Bot_Skinned"></a-entity> + <template data-name="RootScene"> + <a-entity ik-controller hand-pose__left hand-pose__right animation-mixer space-invader-mesh="meshName: Bot_Skinned"></a-entity> </template> - <template data-selector=".Neck"> + <template data-name="Neck"> <a-entity> <a-entity class="nametag" @@ -116,62 +116,38 @@ </a-entity> </template> - <template data-selector=".Chest"> - <a-entity> - <a-entity personal-space-invader="radius: 0.2; useMaterial: true;" bone-visibility> </a-entity> - <a-entity billboard> - <a-entity - block-button - visible-while-frozen - ui-class-while-frozen - text-button="haptic:#player-right-controller; - textHoverColor: #fff; - textColor: #fff; - backgroundHoverColor: #ea4b54; - backgroundColor: #fff;" - slice9="width: 0.45; - height: 0.2; - left: 53; - top: 53; - right: 10; - bottom: 10; - opacity: 1.3; - src: #tooltip" - position="0 0 .35"> - </a-entity> - <a-entity - visible-while-frozen - text="value:Block; - width:2.5; - align:center;" - position="0 0 0.36"></a-entity> - </a-entity> + <template data-name="Chest"> + <a-entity personal-space-invader="radius: 0.2; useMaterial: true;" bone-visibility> + <a-entity billboard> + <a-entity mixin="rounded-text-button" block-button visible-while-frozen ui-class-while-frozen position="0 0 .35"> </a-entity> + <a-entity visible-while-frozen text="value:Block; width:2.5; align:center;" position="0 0 0.36"></a-entity> </a-entity> + </a-entity> </template> - <template data-selector=".Head"> + <template data-name="Head"> <a-entity networked-audio-source networked-audio-analyser personal-space-invader="radius: 0.15; useMaterial: true;" bone-visibility > - <a-cylinder - static-body - radius="0.13" - height="0.2" - position="0 0.07 0.05" - visible="false" - ></a-cylinder> + <a-cylinder + static-body + radius="0.13" + height="0.2" + position="0 0.07 0.05" + visible="false" + ></a-cylinder> </a-entity> </template> - <template data-selector=".LeftHand"> - <a-entity personal-space-invader="radius: 0.1" bone-visibility></a-entity> + <template data-name="LeftHand"> + <a-entity personal-space-invader="radius: 0.1" bone-visibility></a-entity> </template> - <template data-selector=".RightHand"> - <a-entity personal-space-invader="radius: 0.1" bone-visibility></a-entity> + <template data-name="RightHand"> + <a-entity personal-space-invader="radius: 0.1" bone-visibility></a-entity> </template> </a-entity> </a-entity> @@ -183,27 +159,90 @@ class="interactable" super-networked-interactable="counter: #counter; mass: 1;" body="type: dynamic; shape: none; mass: 1;" + auto-scale-cannon-physics-body grabbable - stretchable="useWorldPosition: true;" + stretchable="useWorldPosition: true; usePhysics: never" hoverable duck + sticky-object="autoLockOnRelease: true;" ></a-entity> </template> + <template id="interactable-model"> + <a-entity + gltf-model-plus="inflate: false;" + class="interactable" + super-networked-interactable="counter: #media-counter; mass: 1;" + body="type: dynamic; shape: none; mass: 1;" + grabbable + stretchable="useWorldPosition: true; usePhysics: never" + hoverable + sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;" + auto-box-collider + position-at-box-shape-border="target:.delete-button" + auto-scale-cannon-physics-body + > + <a-entity class="delete-button" visible-while-frozen scale="0.08 0.08 0.08"> + <a-entity mixin="rounded-text-button" remove-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-image"> + <a-entity + class="interactable" + super-networked-interactable="counter: #media-counter; mass: 1;" + body="type: dynamic; shape: none; mass: 1;" + auto-scale-cannon-physics-body + grabbable + stretchable="useWorldPosition: true; usePhysics: never" + hoverable + geometry="primitive: plane" + image-plus + sticky-object="autoLockOnLoad: true; autoLockOnRelease: true;" + position-at-box-shape-border="target:.delete-button;dirs:forward,back" + > + <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> + + <a-mixin id="rounded-text-button" + text-button=" + haptic:#player-right-controller; + textHoverColor: #fff; + textColor: #fff; + backgroundHoverColor: #ea4b54; + backgroundColor: #fff;" + slice9=" + width: 0.45; + height: 0.2; + left: 53; + top: 53; + right: 10; + bottom: 10; + opacity: 1.3; + src: #tooltip" + ></a-mixin> + <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;" - collision-filter="collisionForces: false" - physics-collider + 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;" + collision-filter="collisionForces: false" + physics-collider ></a-mixin> </a-assets> <!-- Interactables --> <a-entity id="counter" networked-counter="max: 3; ttl: 120"></a-entity> + <a-entity id="media-counter" networked-counter="max: 10;"></a-entity> <a-entity id="cursor-controller" @@ -243,126 +282,123 @@ networked-avatar cardboard-controls > - <a-entity - id="player-hud" - hud-controller="head: #player-camera;" - vr-mode-toggle-visibility - vr-mode-toggle-playing__hud-controller - > - <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0"> - <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-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> - </a-entity> - </a-entity> - - <a-entity - id="player-camera" - class="camera" - camera - position="0 1.6 0" - personal-space-bubble="radius: 0.4" - pitch-yaw-rotator - > - <a-entity - id="gaze-teleport" - position = "0.15 0 0" - teleport-controls=" - cameraRig: #player-rig; - teleportOrigin: #player-camera; - button: gaze-teleport_; - collisionEntities: [nav-mesh]; - drawIncrementally: true; - incrementalDrawMs: 600; - hitOpacity: 0.3; - missOpacity: 0.1; - curveShootingSpeed: 12;" - ></a-entity> - <a-entity id="player-camera-reverse-z" rotation="0 180 0"></a-entity> - </a-entity> - - <a-entity - id="player-left-controller" - class="left-controller" - hand-controls2="left" - tracked-controls - teleport-controls=" - cameraRig: #player-rig; - teleportOrigin: #player-camera; - button: cursor-teleport_; - collisionEntities: [nav-mesh]; - drawIncrementally: true; - incrementalDrawMs: 600; - hitOpacity: 0.3; - missOpacity: 0.1; - curveShootingSpeed: 12;" - haptic-feedback - body="type: static; shape: none;" - mixin="controller-super-hands" - controls-shape-offset - > - <a-entity id="player-left-controller-reverse-z" rotation="0 180 0"></a-entity> + <a-entity + id="player-hud" + hud-controller="head: #player-camera;" + vr-mode-toggle-visibility + vr-mode-toggle-playing__hud-controller + > + <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0"> + <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-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> </a-entity> - + </a-entity> + + <a-entity + id="player-camera" + class="camera" + camera + position="0 1.6 0" + personal-space-bubble="radius: 0.4" + pitch-yaw-rotator + > <a-entity - id="player-right-controller" - class="right-controller" - hand-controls2="right" - tracked-controls + id="gaze-teleport" + position = "0.15 0 0" teleport-controls=" cameraRig: #player-rig; teleportOrigin: #player-camera; - button: cursor-teleport_; + button: gaze-teleport_; collisionEntities: [nav-mesh]; drawIncrementally: true; incrementalDrawMs: 600; hitOpacity: 0.3; missOpacity: 0.1; curveShootingSpeed: 12;" - haptic-feedback - body="type: static; shape: none;" - mixin="controller-super-hands" - controls-shape-offset - > - <a-entity id="player-right-controller-reverse-z" rotation="0 180 0"></a-entity> - </a-entity> + ></a-entity> + </a-entity> + + <a-entity + id="player-left-controller" + class="left-controller" + hand-controls2="left" + tracked-controls + teleport-controls=" + cameraRig: #player-rig; + teleportOrigin: #player-camera; + button: cursor-teleport_; + collisionEntities: [nav-mesh]; + drawIncrementally: true; + incrementalDrawMs: 600; + hitOpacity: 0.3; + missOpacity: 0.1; + curveShootingSpeed: 12;" + haptic-feedback + body="type: static; shape: none;" + mixin="controller-super-hands" + controls-shape-offset + > + </a-entity> + + <a-entity + id="player-right-controller" + class="right-controller" + hand-controls2="right" + tracked-controls + teleport-controls=" + cameraRig: #player-rig; + teleportOrigin: #player-camera; + button: cursor-teleport_; + collisionEntities: [nav-mesh]; + drawIncrementally: true; + incrementalDrawMs: 600; + hitOpacity: 0.3; + missOpacity: 0.1; + curveShootingSpeed: 12;" + haptic-feedback + body="type: static; shape: none;" + mixin="controller-super-hands" + controls-shape-offset + > + </a-entity> + + <a-entity gltf-model-plus="inflate: true;" + class="model"> + <template data-name="RootScene"> + <a-entity + ik-controller + animation-mixer + hand-pose__left + hand-pose__right + hand-pose-controller__left="networkedAvatar:#player-rig;eventSrc:#player-left-controller" + hand-pose-controller__right="networkedAvatar:#player-rig;eventSrc:#player-right-controller" + ></a-entity> + </template> - <a-entity gltf-model-plus="inflate: true;" - class="model"> - <template data-selector=".RootScene"> - <a-entity - ik-controller - animation-mixer - hand-pose__left - hand-pose__right - hand-pose-controller__left="networkedAvatar:#player-rig;eventSrc:#player-left-controller" - hand-pose-controller__right="networkedAvatar:#player-rig;eventSrc:#player-right-controller" - ></a-entity> - </template> - - <template data-selector=".Neck"> - <a-entity> - <a-entity class="nametag" visible="false" text ></a-entity> - </a-entity> - </template> + <template data-name="Neck"> + <a-entity> + <a-entity class="nametag" visible="false" text ></a-entity> + </a-entity> + </template> - <template data-selector=".Head"> - <a-entity visible="false" bone-visibility></a-entity> - </template> + <template data-name="Head"> + <a-entity visible="false" bone-visibility></a-entity> + </template> - <template data-selector=".LeftHand"> - <a-entity bone-visibility></a-entity> - </template> + <template data-name="LeftHand"> + <a-entity bone-visibility></a-entity> + </template> - <template data-selector=".RightHand"> - <a-entity bone-visibility></a-entity> - </template> + <template data-name="RightHand"> + <a-entity bone-visibility></a-entity> + </template> - </a-entity> + </a-entity> </a-entity> <!-- Environment --> @@ -371,6 +407,7 @@ nav-mesh-helper static-body="shape: none;" ></a-entity> + </a-scene> <div id="ui-root"></div> diff --git a/src/hub.js b/src/hub.js index 06369fd1d124211ebeace0c553371ea622295af3..51419d8d636cf18e1c4e292dade6146e156d0ff3 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1,5 +1,6 @@ +console.log(`Hubs version: ${process.env.BUILD_VERSION || "?"}`); + import "./assets/stylesheets/hub.scss"; -import moment from "moment-timezone"; import queryString from "query-string"; import { patchWebGLRenderingContext } from "./utils/webgl"; @@ -63,10 +64,16 @@ import "./components/networked-avatar"; import "./components/css-class"; import "./components/scene-shadow"; import "./components/avatar-replay"; +import "./components/image-plus"; +import "./components/auto-box-collider"; import "./components/pinch-to-move"; import "./components/look-on-mobile"; import "./components/pitch-yaw-rotator"; import "./components/input-configurator"; +import "./components/sticky-object"; +import "./components/auto-scale-cannon-physics-body"; +import "./components/position-at-box-shape-border"; +import "./components/remove-networked-object-button"; import ReactDOM from "react-dom"; import React from "react"; @@ -75,6 +82,7 @@ import HubChannel from "./utils/hub-channel"; import LinkChannel from "./utils/link-channel"; import { connectToReticulum } from "./utils/phoenix-utils"; import { disableiOSZoom } from "./utils/disable-ios-zoom"; +import { addMedia } from "./utils/media-utils"; import "./systems/personal-space-bubble"; import "./systems/app-mode"; @@ -296,11 +304,38 @@ const onReady = async () => { NAF.connection.entities.completeSync(ev.detail.clientId); }); + scene.addEventListener("add_media", e => { + addMedia(e.detail); + }); + + if (qsTruthy("mediaTools")) { + document.addEventListener("paste", e => { + if (e.target.nodeName === "INPUT") return; + + const imgUrl = e.clipboardData.getData("text"); + console.log("Pasted: ", imgUrl, e); + addMedia(imgUrl); + }); + + document.addEventListener("dragover", e => { + e.preventDefault(); + }); + + document.addEventListener("drop", e => { + e.preventDefault(); + const imgUrl = e.dataTransfer.getData("url"); + if (imgUrl) { + console.log("Droped: ", imgUrl); + addMedia(imgUrl); + } + }); + } + if (!qsTruthy("offline")) { document.body.addEventListener("connected", () => { if (!isBotMode) { hubChannel.sendEntryEvent().then(() => { - store.update({ activity: { lastEnteredAt: moment().toJSON() } }); + store.update({ activity: { lastEnteredAt: new Date().toISOString() } }); }); } remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 }); @@ -398,6 +433,16 @@ const onReady = async () => { return; } + if (qs.required_version && process.env.BUILD_VERSION) { + const buildNumber = process.env.BUILD_VERSION.split(" ", 1)[0]; // e.g. "123 (abcd5678)" + if (qs.required_version !== buildNumber) { + remountUI({ roomUnavailableReason: "version_mismatch" }); + setTimeout(() => document.location.reload(), 5000); + exitScene(); + return; + } + } + getAvailableVREntryTypes().then(availableVREntryTypes => { if (availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) { remountUI({ availableVREntryTypes, forcedVREntryType: "gearvr" }); diff --git a/src/index.js b/src/index.js index 2383019035144bc82277e15baa01b034f6244754..32558cd4ec93844c558ce5d45fed8d54824ac01a 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,6 @@ import InfoDialog from "./react-components/info-dialog.js"; import queryString from "query-string"; const qs = queryString.parse(location.search); - registerTelemetry(); ReactDOM.render( diff --git a/src/input-mappings.js b/src/input-mappings.js index c6ce52501b479fa5749437db3da0882659129cb5..ccf44110bc74e38ab80de143c11f2dcad50d2a5c 100644 --- a/src/input-mappings.js +++ b/src/input-mappings.js @@ -149,8 +149,7 @@ const config = { m_press: "action_mute", q_press: "snap_rotate_left", e_press: "snap_rotate_right", - v_press: "action_share_screen", - b_press: "action_select_hud_item", + b_press: "action_share_screen", // We can't create a keyboard behaviour with AFIM yet, // so these will get captured by wasd-to-analog2d diff --git a/src/network-schemas.js b/src/network-schemas.js index ca4d0f401e3420c75ba6be1f69831ff2006f508c..a67b0d02381f0accc53472818dfdcac56951171a 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -95,6 +95,27 @@ function registerNetworkSchemas() { "scale" ] }); + + NAF.schemas.add({ + template: "#interactable-image", + components: [ + { + component: "position", + requiresNetworkUpdate: vectorRequiresUpdate(0.001) + }, + { + component: "rotation", + requiresNetworkUpdate: vectorRequiresUpdate(0.5) + }, + "scale", + "image-plus" + ] + }); + + NAF.schemas.add({ + template: "#interactable-model", + components: ["position", "rotation", "scale", "gltf-model-plus"] + }); } export default registerNetworkSchemas; diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index 141606a83d121f0a280ac5ac6b6d997856f7fc1e..e32a02d3eca931e112eb976b3c1cdcafc55f6dff 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -1,10 +1,30 @@ import React from "react"; import PropTypes from "prop-types"; import cx from "classnames"; +import queryString from "query-string"; import styles from "../assets/stylesheets/2d-hud.scss"; -const TwoDHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onToggleSpaceBubble }) => ( +import FontAwesomeIcon from "@fortawesome/react-fontawesome"; +import faPlus from "@fortawesome/fontawesome-free-solid/faPlus"; + +const qs = queryString.parse(location.search); +function qsTruthy(param) { + const val = qs[param]; + // if the param exists but is not set (e.g. "?foo&bar"), its value is null. + return val === null || /1|on|true/i.test(val); +} +const enableMediaTools = qsTruthy("mediaTools"); + +const TwoDHUD = ({ + muted, + frozen, + spacebubble, + onToggleMute, + onToggleFreeze, + onToggleSpaceBubble, + onClickAddMedia +}) => ( <div className={styles.container}> <div className={cx("ui-interactive", styles.panel, styles.left)}> <div @@ -25,6 +45,15 @@ const TwoDHUD = ({ muted, frozen, spacebubble, onToggleMute, onToggleFreeze, onT onClick={onToggleSpaceBubble} /> </div> + {enableMediaTools ? ( + <div + className={cx("ui-interactive", styles.iconButton, styles.small, styles.addMediaButton)} + title="Add Media" + onClick={onClickAddMedia} + > + <FontAwesomeIcon icon={faPlus} /> + </div> + ) : null} </div> ); @@ -34,7 +63,8 @@ TwoDHUD.propTypes = { spacebubble: PropTypes.bool, onToggleMute: PropTypes.func, onToggleFreeze: PropTypes.func, - onToggleSpaceBubble: PropTypes.func + onToggleSpaceBubble: PropTypes.func, + onClickAddMedia: PropTypes.func }; export default TwoDHUD; diff --git a/src/react-components/entry-buttons.js b/src/react-components/entry-buttons.js index f2551df70f99813c5e74be91e7e627de3374feab..d2bddfd908e9a40044d2349bd0729e38a7a25caf 100644 --- a/src/react-components/entry-buttons.js +++ b/src/react-components/entry-buttons.js @@ -1,7 +1,6 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import PropTypes from "prop-types"; -import MobileDetect from "mobile-detect"; import MobileScreenEntryImg from "../assets/images/mobile_screen_entry.svg"; import DesktopScreenEntryImg from "../assets/images/desktop_screen_entry.svg"; @@ -10,8 +9,6 @@ import GearVREntryImg from "../assets/images/gearvr_entry.svg"; import DaydreamEntryImg from "../assets/images/daydream_entry.svg"; import DeviceEntryImg from "../assets/images/device_entry.svg"; -const mobiledetect = new MobileDetect(navigator.userAgent); - const EntryButton = props => ( <button className="entry-button" onClick={props.onClick}> <img src={props.iconSrc} className="entry-button__icon" /> @@ -45,9 +42,9 @@ EntryButton.propTypes = { export const TwoDEntryButton = props => { const entryButtonProps = { ...props, - iconSrc: mobiledetect.mobile() ? MobileScreenEntryImg : DesktopScreenEntryImg, + iconSrc: AFRAME.utils.device.isMobile() ? MobileScreenEntryImg : DesktopScreenEntryImg, prefixMessageId: "entry.screen-prefix", - mediumMessageId: mobiledetect.mobile() ? "entry.mobile-screen" : "entry.desktop-screen" + mediumMessageId: AFRAME.utils.device.isMobile() ? "entry.mobile-screen" : "entry.desktop-screen" }; return <EntryButton {...entryButtonProps} />; @@ -59,7 +56,7 @@ export const GenericEntryButton = props => { iconSrc: GenericVREntryImg, prefixMessageId: "entry.generic-prefix", mediumMessageId: "entry.generic-medium", - subtitle: mobiledetect.mobile() ? null : "entry.generic-subtitle-desktop" + subtitle: AFRAME.utils.device.isMobile() ? null : "entry.generic-subtitle-desktop" }; return <EntryButton {...entryButtonProps} />; @@ -102,13 +99,13 @@ export const DeviceEntryButton = props => { const entryButtonProps = { ...props, iconSrc: DeviceEntryImg, - prefixMessageId: mobiledetect.mobile() ? "entry.device-prefix-mobile" : "entry.device-prefix-desktop", + prefixMessageId: AFRAME.utils.device.isMobile() ? "entry.device-prefix-mobile" : "entry.device-prefix-desktop", mediumMessageId: "entry.device-medium" }; entryButtonProps.subtitle = entryButtonProps.isInHMD ? "entry.device-subtitle-vr" - : mobiledetect.mobile() ? "entry.device-subtitle-mobile" : "entry.device-subtitle-desktop"; + : AFRAME.utils.device.isMobile() ? "entry.device-subtitle-mobile" : "entry.device-subtitle-desktop"; return <EntryButton {...entryButtonProps} />; }; diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 7e44ce63093ad5ad3df772807794140cab9af2b9..dc10b99ac217609b07bda59422c805c5ea4f1581 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -85,7 +85,7 @@ class HubCreatePanel extends Component { const hub = await res.json(); - if (process.env.NODE_ENV === "production") { + if (process.env.NODE_ENV === "production" || document.location.host === process.env.DEV_RETICULUM_SERVER) { document.location = hub.url; } else { document.location = `/hub.html?hub_id=${hub.hub_id}`; diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js index a04127ff74ff2b4722d0a2fbe1a99dd07568efc0..83b46b9dfee5569044581b44879566a13ec3c934 100644 --- a/src/react-components/info-dialog.js +++ b/src/react-components/info-dialog.js @@ -8,6 +8,8 @@ import LinkDialog from "./link-dialog.js"; // TODO i18n +let lastAddMediaUrl = ""; + class InfoDialog extends Component { static dialogTypes = { slack: Symbol("slack"), @@ -18,12 +20,14 @@ class InfoDialog extends Component { report: Symbol("report"), help: Symbol("help"), link: Symbol("link"), - webvr_recommend: Symbol("webvr_recommend") + webvr_recommend: Symbol("webvr_recommend"), + add_media: Symbol("add_media") }; static propTypes = { dialogType: PropTypes.oneOf(Object.values(InfoDialog.dialogTypes)), onCloseDialog: PropTypes.func, onSubmittedEmail: PropTypes.func, + onAddMedia: PropTypes.func, linkCode: PropTypes.string }; @@ -34,14 +38,17 @@ class InfoDialog extends Component { this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`; this.onKeyDown = this.onKeyDown.bind(this); this.onContainerClicked = this.onContainerClicked.bind(this); + this.onAddMediaClicked = this.onAddMediaClicked.bind(this); } componentDidMount() { window.addEventListener("keydown", this.onKeyDown); + this.setState({ addMediaUrl: lastAddMediaUrl }); } componentWillUnmount() { window.removeEventListener("keydown", this.onKeyDown); + lastAddMediaUrl = this.state.addMediaUrl; } onKeyDown(e) { @@ -56,6 +63,11 @@ class InfoDialog extends Component { } } + onAddMediaClicked() { + this.props.onAddMedia(this.state.addMediaUrl); + this.props.onCloseDialog(); + } + shareLinkClicked = () => { navigator.share({ title: document.title, @@ -71,7 +83,8 @@ class InfoDialog extends Component { state = { mailingListEmail: "", mailingListPrivacy: false, - copyLinkButtonText: "Copy" + copyLinkButtonText: "Copy", + addMediaUrl: "" }; signUpForMailingList = async e => { @@ -184,6 +197,31 @@ class InfoDialog extends Component { </div> ); break; + case InfoDialog.dialogTypes.add_media: + dialogTitle = "Add Media"; + dialogBody = ( + <div> + <div>Tip: You can paste media urls directly into hubs with ctrl+v</div> + <form onSubmit={this.onAddMediaClicked}> + <div className="add-media-form"> + <input + type="url" + placeholder="Image, Video, or GLTF URL" + className="add-media-form__link_field" + value={this.state.addMediaUrl} + onChange={e => this.setState({ addMediaUrl: e.target.value })} + required + /> + <div className="add-media-form__buttons"> + <button className="add-media-form__action-button"> + <span>Add</span> + </button> + </div> + </div> + </form> + </div> + ); + break; case InfoDialog.dialogTypes.updates: dialogTitle = ""; dialogBody = ( diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 9b6c10ea2149fd02899fe217a2ba89b2c33d2187..ce6358cb8ca3063f11fbb35894e7e69f99469840 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -3,7 +3,6 @@ import PropTypes from "prop-types"; import classNames from "classnames"; import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect"; import queryString from "query-string"; -import MobileDetect from "mobile-detect"; import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; import MovingAverage from "moving-average"; @@ -27,8 +26,6 @@ import Footer from "./footer"; import FontAwesomeIcon from "@fortawesome/react-fontawesome"; import faQuestion from "@fortawesome/fontawesome-free-solid/faQuestion"; -const mobiledetect = new MobileDetect(navigator.userAgent); - addLocaleData([...en]); const ENTRY_STEPS = { @@ -329,7 +326,7 @@ class UIRoot extends Component { mediaSource: "screen", // Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything // other than your current monitor that has a different aspect ratio. - width: screen.width / screen.height * 720, + width: 720 * screen.width / screen.height, height: 720, frameRate: 30 } @@ -456,7 +453,7 @@ class UIRoot extends Component { }; shouldShowHmdMicWarning = () => { - if (mobiledetect.mobile()) return false; + if (AFRAME.utils.device.isMobile()) return false; if (!this.state.enterInVR) return false; if (!this.hasHmdMicrophone()) return false; @@ -484,7 +481,7 @@ class UIRoot extends Component { }; onAudioReadyButton = () => { - if (mobiledetect.mobile() && !this.state.enterInVR && screenfull.enabled) { + if (AFRAME.utils.device.isMobile() && !this.state.enterInVR && screenfull.enabled) { screenfull.request(); } @@ -528,6 +525,10 @@ class UIRoot extends Component { this.setState({ infoDialogType: null, linkCode: null, linkCodeCancel: null }); }; + handleAddMedia = url => { + this.props.scene.emit("add_media", url); + }; + render() { if (this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason) { let subtitle = null; @@ -609,7 +610,7 @@ class UIRoot extends Component { // Only show this in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and // will attempt to share your webcam instead! const screenSharingCheckbox = this.props.enableScreenSharing && - !mobiledetect.mobile() && + !AFRAME.utils.device.isMobile() && /firefox/i.test(navigator.userAgent) && ( <label className="entry-panel__screen-sharing"> <input @@ -695,7 +696,7 @@ class UIRoot extends Component { clip: `rect(${maxLevelHeight - Math.floor(this.state.micLevel * maxLevelHeight)}px, 111px, 111px, 0px)` }; const speakerClip = { clip: `rect(${this.state.tonePlaying ? 0 : maxLevelHeight}px, 111px, 111px, 0px)` }; - + const subtitleId = AFRAME.utils.device.isMobile() ? "audio.subtitle-mobile" : "audio.subtitle-desktop"; const audioSetupPanel = this.state.entryStep === ENTRY_STEPS.audio_setup ? ( <div className="audio-setup-panel"> @@ -704,9 +705,7 @@ class UIRoot extends Component { <FormattedMessage id="audio.title" /> </div> <div className="audio-setup-panel__subtitle"> - {(mobiledetect.mobile() || this.state.enterInVR) && ( - <FormattedMessage id={mobiledetect.mobile() ? "audio.subtitle-mobile" : "audio.subtitle-desktop"} /> - )} + {(AFRAME.utils.device.isMobile() || this.state.enterInVR) && <FormattedMessage id={subtitleId} />} </div> <div className="audio-setup-panel__levels"> <div className="audio-setup-panel__levels__icon"> @@ -835,6 +834,7 @@ class UIRoot extends Component { linkCode={this.state.linkCode} onSubmittedEmail={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.email_submitted })} onCloseDialog={this.handleCloseDialog} + onAddMedia={this.handleAddMedia} /> {this.state.entryStep === ENTRY_STEPS.finished && ( @@ -872,6 +872,7 @@ class UIRoot extends Component { onToggleMute={this.toggleMute} onToggleFreeze={this.toggleFreeze} onToggleSpaceBubble={this.toggleSpaceBubble} + onClickAddMedia={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.add_media })} /> <Footer hubName={this.props.hubName} diff --git a/src/storage/store.js b/src/storage/store.js index e4e509ba3c1f1fc41b45d6816ba31f542f9c09cd..1b6d575ddd3f030a9b8e43897bfa907ba2842ffa 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -1,5 +1,5 @@ import { Validator } from "jsonschema"; -import merge from "lodash/merge"; +import merge from "deepmerge"; const LOCAL_STORE_KEY = "___hubs_store"; const STORE_STATE_CACHE_KEY = Symbol(); diff --git a/src/systems/personal-space-bubble.js b/src/systems/personal-space-bubble.js index 238c0b63f70c06502350b83dd354cbb256c755bf..23da15b839cd8130757587b2f717be154b243806 100644 --- a/src/systems/personal-space-bubble.js +++ b/src/systems/personal-space-bubble.js @@ -120,10 +120,10 @@ function createSphereGizmo(radius) { */ AFRAME.registerComponent("space-invader-mesh", { schema: { - meshSelector: { type: "string" } + meshName: { type: "string" } }, - init() { - this.targetMesh = this.el.querySelector(this.data.meshSelector).object3DMap.skinnedmesh; + update() { + this.targetMesh = this.el.object3D.getObjectByName(this.data.meshName); } }); diff --git a/src/utils/disable-ios-zoom.js b/src/utils/disable-ios-zoom.js index c2d17104a6ae4b9b8c99221fe5bec8a8f79cc909..b80639e5b4c4c625330e3e31f19fe4cbf24c2ea3 100644 --- a/src/utils/disable-ios-zoom.js +++ b/src/utils/disable-ios-zoom.js @@ -1,8 +1,5 @@ -import MobileDetect from "mobile-detect"; -const mobiledetect = new MobileDetect(navigator.userAgent); - export function disableiOSZoom() { - if (!mobiledetect.is("iPhone") && !mobiledetect.is("iPad")) return; + if (!AFRAME.utils.device.isIOS()) return; let lastTouchAtMs = 0; diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index aad95ec4e7550ff29181789b9e8a8c299f8961db..d8b87730a98dea1feb8f98793323d4b74a139553 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -1,4 +1,13 @@ -import moment from "moment-timezone"; +const MS_PER_DAY = 1000 * 60 * 60 * 24; +const MS_PER_MONTH = 1000 * 60 * 60 * 24 * 30; + +function isSameMonth(da, db) { + return da.getFullYear() == db.getFullYear() && da.getMonth() == db.getMonth(); +} + +function isSameDay(da, db) { + return isSameMonth(da, db) && da.getDate() == db.getDate(); +} export default class HubChannel { constructor(store) { @@ -52,18 +61,15 @@ export default class HubChannel { return entryTimingFlags; } - const lastEntered = moment(storedLastEnteredAt); - const lastEnteredPst = moment(lastEntered).tz("America/Los_Angeles"); - const nowPst = moment().tz("America/Los_Angeles"); - const dayWindowAgo = moment().subtract(1, "day"); - const monthWindowAgo = moment().subtract(1, "month"); - - entryTimingFlags.isNewDaily = - lastEnteredPst.dayOfYear() !== nowPst.dayOfYear() || lastEnteredPst.year() !== nowPst.year(); - entryTimingFlags.isNewMonthly = - lastEnteredPst.month() !== nowPst.month() || lastEnteredPst.year() !== nowPst.year(); - entryTimingFlags.isNewDayWindow = lastEntered.isBefore(dayWindowAgo); - entryTimingFlags.isNewMonthWindow = lastEntered.isBefore(monthWindowAgo); + const now = new Date(); + const lastEntered = new Date(storedLastEnteredAt); + const msSinceLastEntered = now - lastEntered; + + // note that new daily and new monthly is based on client local time + entryTimingFlags.isNewDaily = !isSameDay(now, lastEntered); + entryTimingFlags.isNewMonthly = !isSameMonth(now, lastEntered); + entryTimingFlags.isNewDayWindow = msSinceLastEntered > MS_PER_DAY; + entryTimingFlags.isNewMonthWindow = msSinceLastEntered > MS_PER_MONTH; return entryTimingFlags; }; diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..9c17813b926e5541eef96b586f8efb133e696f6c --- /dev/null +++ b/src/utils/media-utils.js @@ -0,0 +1,75 @@ +const whitelistedHosts = [/^.*\.reticulum\.io$/, /^.*hubs\.mozilla\.com$/, /^hubs\.local$/]; +const isHostWhitelisted = hostname => !!whitelistedHosts.filter(r => r.test(hostname)).length; + +let resolveMediaUrl = "/api/v1/media"; +if (process.env.NODE_ENV === "development") { + resolveMediaUrl = `https://${process.env.DEV_RETICULUM_SERVER}${resolveMediaUrl}`; +} + +export const resolveFarsparkUrl = async url => { + const parsedUrl = new URL(url); + if ((parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") || isHostWhitelisted(parsedUrl.hostname)) + return url; + + return (await fetch(resolveMediaUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ media: { url } }) + }).then(r => r.json())).raw; +}; + +const fetchContentType = async url => fetch(url, { method: "HEAD" }).then(r => r.headers.get("content-type")); +let interactableId = 0; + +const offset = { x: 0, y: 0, z: -1.5 }; +export const spawnNetworkedImage = (src, contentType) => { + const scene = AFRAME.scenes[0]; + const image = document.createElement("a-entity"); + image.id = "interactable-image-" + interactableId++; + image.setAttribute("networked", { template: "#interactable-image" }); + image.setAttribute("offset-relative-to", { + target: "#player-camera", + offset: offset, + selfDestruct: true + }); + image.setAttribute("image-plus", { src, contentType }); + scene.appendChild(image); + return image; +}; + +export const spawnNetworkedInteractable = src => { + const scene = AFRAME.scenes[0]; + const model = document.createElement("a-entity"); + model.id = "interactable-model-" + interactableId++; + model.setAttribute("networked", { template: "#interactable-model" }); + model.setAttribute("offset-relative-to", { + on: "model-loaded", + target: "#player-camera", + offset: offset, + selfDestruct: true + }); + model.setAttribute("gltf-model-plus", "src", src); + model.setAttribute("auto-box-collider", { resize: true }); + scene.appendChild(model); + return model; +}; + +export const addMedia = async url => { + try { + const farsparkUrl = await resolveFarsparkUrl(url); + console.log("resolved", url, farsparkUrl); + + const contentType = await fetchContentType(farsparkUrl); + + if (contentType.startsWith("image/") || contentType.startsWith("video/")) { + spawnNetworkedImage(farsparkUrl, contentType); + } else if (contentType.startsWith("model/gltf") || url.endsWith(".gltf") || url.endsWith(".glb")) { + spawnNetworkedInteractable(farsparkUrl); + } else { + throw new Error(`Unsupported content type: ${contentType}`); + } + } catch (e) { + console.error("Error adding media", e); + spawnNetworkedImage("error"); + } +}; diff --git a/src/utils/vr-caps-detect.js b/src/utils/vr-caps-detect.js index 8953f12a86f24e9848c08e3619d718a2b0eda615..685fb3ba326cebd1febc8f6d6005c7c846a6d99b 100644 --- a/src/utils/vr-caps-detect.js +++ b/src/utils/vr-caps-detect.js @@ -1,9 +1,6 @@ const { detect } = require("detect-browser"); -import MobileDetect from "mobile-detect"; const browser = detect(); -const deviceDetect = require("device-detect")(); -const mobiledetect = new MobileDetect(navigator.userAgent); // Precision on device detection is fuzzy -- we can sometimes know if a device is definitely // available, or definitely *not* available, and assume it may be available otherwise. @@ -13,12 +10,12 @@ export const VR_DEVICE_AVAILABILITY = { maybe: "maybe" // Implies this device may support this VR platform, but may not be installed or in a compatible browser }; -function isMaybeGearVRCompatibleDevice() { - return navigator.userAgent.match(/\WAndroid\W/); +function isMaybeGearVRCompatibleDevice(ua) { + return /\WAndroid\W/.test(ua); } -function isMaybeDaydreamCompatibleDevice() { - return navigator.userAgent.match(/\WAndroid\W/); +function isMaybeDaydreamCompatibleDevice(ua) { + return /\WAndroid\W/.test(ua); } // Blacklist of VR device name regex matchers that we do not want to consider as valid VR devices @@ -46,8 +43,9 @@ const GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST = [/cardboard/i]; // // This function also detects if the user is already in a headset, and returns the isInHMD key to be `true` if so. export async function getAvailableVREntryTypes() { - const isSamsungBrowser = browser.name === "chrome" && navigator.userAgent.match(/SamsungBrowser/); - const isOculusBrowser = navigator.userAgent.match(/Oculus/); + const ua = navigator.userAgent; + const isSamsungBrowser = browser.name === "chrome" && /SamsungBrowser/.test(ua); + const isOculusBrowser = /Oculus/.test(ua); const isInHMD = isOculusBrowser; // This needs to be kept up-to-date with the latest browsers that can support VR and Hubs. @@ -55,7 +53,7 @@ export async function getAvailableVREntryTypes() { const isWebVRCapableBrowser = window.hasNativeWebVRImplementation; const isDaydreamCapableBrowser = !!(isWebVRCapableBrowser && browser.name === "chrome" && !isSamsungBrowser); - const isIDevice = ["iPhone", "iPad", "iPod"].indexOf(deviceDetect.device) > -1; + const isIDevice = AFRAME.utils.device.isIOS(); const isFirefoxBrowser = browser.name === "firefox"; const isUIWebView = typeof navigator.mediaDevices === "undefined"; @@ -67,7 +65,7 @@ export async function getAvailableVREntryTypes() { ? VR_DEVICE_AVAILABILITY.no : isIDevice && isUIWebView ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.yes; - let generic = mobiledetect.mobile() ? VR_DEVICE_AVAILABILITY.no : VR_DEVICE_AVAILABILITY.maybe; + let generic = AFRAME.utils.device.isMobile() ? VR_DEVICE_AVAILABILITY.no : VR_DEVICE_AVAILABILITY.maybe; let cardboard = VR_DEVICE_AVAILABILITY.no; // We only consider GearVR support as "maybe" and never "yes". The only browser @@ -76,21 +74,21 @@ export async function getAvailableVREntryTypes() { // // If we are in Oculus Browser (ie, we are literally wearing a GearVR) then return 'yes'. let gearvr = VR_DEVICE_AVAILABILITY.no; - if (isMaybeGearVRCompatibleDevice()) { + if (isMaybeGearVRCompatibleDevice(ua)) { gearvr = isOculusBrowser ? VR_DEVICE_AVAILABILITY.yes : VR_DEVICE_AVAILABILITY.maybe; } // For daydream detection, we first check if they are on an Android compatible device, and assume they // may support daydream *unless* this browser has WebVR capabilities, in which case we can do better. let daydream = - isMaybeDaydreamCompatibleDevice() && !isInHMD ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.no; + isMaybeDaydreamCompatibleDevice(ua) && !isInHMD ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.no; if (isWebVRCapableBrowser) { const displays = await navigator.getVRDisplays(); // Generic is supported for non-blacklisted devices and presentable HMDs. generic = displays.find( - d => d.capabilities.canPresent && !GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST.find(r => d.displayName.match(r)) + d => d.capabilities.canPresent && !GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST.find(r => r.test(d.displayName)) ) ? VR_DEVICE_AVAILABILITY.yes : VR_DEVICE_AVAILABILITY.no; @@ -98,12 +96,12 @@ export async function getAvailableVREntryTypes() { cardboard = !isIDevice && !isFirefoxBrowser && - displays.find(d => d.capabilities.canPresent && d.displayName.match(/cardboard/i)) + displays.find(d => d.capabilities.canPresent && /cardboard/i.test(d.displayName)) ? VR_DEVICE_AVAILABILITY.yes : VR_DEVICE_AVAILABILITY.no; // For daydream detection, in a WebVR browser we can increase confidence in daydream compatibility. - const hasDaydreamWebVRDevice = displays.find(d => d.displayName.match(/daydream/i)); + const hasDaydreamWebVRDevice = displays.find(d => /daydream/i.test(d.displayName)); if (hasDaydreamWebVRDevice) { // If we detected daydream via WebVR diff --git a/src/vendor/GLTFLoader.js b/src/vendor/GLTFLoader.js index 7610044b41b6dd9956b2d84984e46671de46e494..754ae5d9460e80e176a928cc53ea75ab7a6f5212 100644 --- a/src/vendor/GLTFLoader.js +++ b/src/vendor/GLTFLoader.js @@ -7,8 +7,11 @@ * @author Tony Parisi / http://www.tonyparisi.com/ * @author Takahiro / https://github.com/takahirox * @author Don McCurdy / https://www.donmccurdy.com + * @author netpro2k / https://github.com/netpro2k */ + import { resolveFarsparkUrl } from "../utils/media-utils" + THREE.GLTFLoader = ( function () { function GLTFLoader( manager ) { @@ -25,7 +28,7 @@ THREE.GLTFLoader = ( function () { crossOrigin: 'Anonymous', - load: function ( url, onLoad, onProgress, onError ) { + load: async function ( url, onLoad, onProgress, onError ) { var scope = this; @@ -37,7 +40,9 @@ THREE.GLTFLoader = ( function () { loader.setResponseType( 'arraybuffer' ); - loader.load( url, function ( data ) { + var farsparkURL = await resolveFarsparkUrl(url); + + loader.load( farsparkURL, function ( data ) { try { @@ -1598,7 +1603,7 @@ THREE.GLTFLoader = ( function () { * @param {number} bufferIndex * @return {Promise<ArrayBuffer>} */ - GLTFParser.prototype.loadBuffer = function ( bufferIndex ) { + GLTFParser.prototype.loadBuffer = async function ( bufferIndex ) { var bufferDef = this.json.buffers[ bufferIndex ]; var loader = this.fileLoader; @@ -1618,9 +1623,11 @@ THREE.GLTFLoader = ( function () { var options = this.options; + var farsparkURL = await resolveFarsparkUrl(resolveURL(bufferDef.uri, options.path)); + return new Promise( function ( resolve, reject ) { - loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { + loader.load( farsparkURL, resolve, undefined, function () { reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); @@ -1784,7 +1791,7 @@ THREE.GLTFLoader = ( function () { * @param {number} textureIndex * @return {Promise<THREE.Texture>} */ - GLTFParser.prototype.loadTexture = function ( textureIndex ) { + GLTFParser.prototype.loadTexture = async function ( textureIndex ) { var parser = this; var json = this.json; @@ -1798,11 +1805,12 @@ THREE.GLTFLoader = ( function () { var sourceURI = source.uri; var isObjectURL = false; - if ( source.bufferView !== undefined ) { + var hasBufferView = source.bufferView !== undefined; + if ( hasBufferView ) { // Load binary image data from bufferView, if provided. - sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { + sourceURI = await parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { isObjectURL = true; var blob = new Blob( [ bufferView ], { type: source.mimeType } ); @@ -1813,6 +1821,11 @@ THREE.GLTFLoader = ( function () { } + var urlToLoad = resolveURL(sourceURI, options.path); + if (!hasBufferView){ + urlToLoad = await resolveFarsparkUrl(urlToLoad); + } + return Promise.resolve( sourceURI ).then( function ( sourceURI ) { // Load Texture resource. @@ -1821,7 +1834,7 @@ THREE.GLTFLoader = ( function () { return new Promise( function ( resolve, reject ) { - loader.load( resolveURL( sourceURI, options.path ), resolve, undefined, reject ); + loader.load( urlToLoad, resolve, undefined, reject ); } ); diff --git a/src/workers/gifparsing.worker.js b/src/workers/gifparsing.worker.js new file mode 100644 index 0000000000000000000000000000000000000000..643a95ab98e52c048ef551a6249e3ef937191389 --- /dev/null +++ b/src/workers/gifparsing.worker.js @@ -0,0 +1,72 @@ +/** + * + * Gif parser by @gtk2k + * https://github.com/gtk2k/gtk2k.github.io/tree/master/animation_gif + * + */ + +const parseGIF = function(gif, successCB, errorCB) { + let pos = 0; + const delayTimes = []; + let graphicControl = null; + const frames = []; + const disposals = []; + let loopCnt = 0; + if ( + gif[0] === 0x47 && + gif[1] === 0x49 && + gif[2] === 0x46 && // 'GIF' + gif[3] === 0x38 && + gif[4] === 0x39 && + gif[5] === 0x61 + ) { + // '89a' + pos += 13 + +!!(gif[10] & 0x80) * Math.pow(2, (gif[10] & 0x07) + 1) * 3; + const gifHeader = gif.subarray(0, pos); + while (gif[pos] && gif[pos] !== 0x3b) { + const offset = pos, + blockId = gif[pos]; + if (blockId === 0x21) { + const label = gif[++pos]; + if ([0x01, 0xfe, 0xf9, 0xff].indexOf(label) !== -1) { + label === 0xf9 && delayTimes.push((gif[pos + 3] + (gif[pos + 4] << 8)) * 10); + label === 0xff && (loopCnt = gif[pos + 15] + (gif[pos + 16] << 8)); + while (gif[++pos]) pos += gif[pos]; + if (label === 0xf9) { + graphicControl = gif.subarray(offset, pos + 1); + disposals.push((graphicControl[3] >> 2) & 0x07); + } + } else { + errorCB && errorCB("parseGIF: unknown label"); + break; + } + } else if (blockId === 0x2c) { + pos += 9; + pos += 1 + +!!(gif[pos] & 0x80) * (Math.pow(2, (gif[pos] & 0x07) + 1) * 3); + while (gif[++pos]) pos += gif[pos]; + const imageData = gif.subarray(offset, pos + 1); + frames.push(URL.createObjectURL(new Blob([gifHeader, graphicControl, imageData]))); + } else { + errorCB && errorCB("parseGIF: unknown blockId"); + break; + } + pos++; + } + } else { + errorCB && errorCB("parseGIF: no GIF89a"); + } + successCB && successCB(delayTimes, loopCnt, frames, disposals); +}; + +self.onmessage = e => { + parseGIF( + new Uint8Array(e.data), + (delays, loopcnt, frames, disposals) => { + self.postMessage([true, frames, delays, disposals]); + }, + err => { + console.error("Error in gif parsing worker", err); + self.postMessage([false, err]); + } + ); +}; diff --git a/webpack.config.js b/webpack.config.js index 7f4daa883ea1e19355c8306afb3c5d4a2e5cf39b..454da02049360c1fc11b4c7bcbe820b6537faf2e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,6 +8,7 @@ const webpack = require("webpack"); const HTMLWebpackPlugin = require("html-webpack-plugin"); const ExtractTextPlugin = require("extract-text-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin"); +const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); const _ = require("lodash"); const SMOKE_PREFIX = "smoke-"; @@ -99,6 +100,7 @@ const config = { useLocalIp: true, public: "hubs.local:8080", port: 8080, + headers: { "Access-Control-Allow-Origin": "*" }, before: function(app) { // networked-aframe makes HEAD requests to the server for time syncing. Respond with an empty body. app.head("*", function(req, res, next) { @@ -129,6 +131,14 @@ const config = { interpolate: "require" } }, + { + test: /\.worker\.js$/, + loader: "worker-loader", + options: { + name: "assets/js/[name]-[hash].js", + publicPath: "/" + } + }, { test: /\.js$/, include: [path.resolve(__dirname, "src")], @@ -186,6 +196,10 @@ const config = { } ] }, + // necessary due to https://github.com/visionmedia/debug/issues/547 + optimization: { + minimizer: [new UglifyJsPlugin({ uglifyOptions: { compress: { collapse_vars: false } } })] + }, plugins: [ // Each output page needs a HTMLWebpackPlugin entry new HTMLWebpackPlugin({ @@ -258,7 +272,8 @@ const config = { NODE_ENV: process.env.NODE_ENV, JANUS_SERVER: process.env.JANUS_SERVER, DEV_RETICULUM_SERVER: process.env.DEV_RETICULUM_SERVER, - ASSET_BUNDLE_SERVER: process.env.ASSET_BUNDLE_SERVER + ASSET_BUNDLE_SERVER: process.env.ASSET_BUNDLE_SERVER, + BUILD_VERSION: process.env.BUILD_VERSION }) }) ] diff --git a/yarn.lock b/yarn.lock index 22663266a5572652e4b21a4a89850964cca3ae6a..6783d21e029245a2a0582351a0f119232f27df17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -182,9 +182,9 @@ aframe-physics-extras@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/aframe-physics-extras/-/aframe-physics-extras-0.1.3.tgz#803e2164fb96c0a80f2d1a81458f3277f262b130" -"aframe-physics-system@github:donmccurdy/aframe-physics-system": +"aframe-physics-system@https://github.com/mozillareality/aframe-physics-system#hubs/master": version "3.1.2" - resolved "https://codeload.github.com/donmccurdy/aframe-physics-system/tar.gz/c142a301e3ce76f88bab817c89daa5b3d4d97815" + resolved "https://github.com/mozillareality/aframe-physics-system#50f5deb1134eb0d43c0435d287eef7037818d3cc" dependencies: browserify "^14.3.0" budo "^10.0.3" @@ -2501,6 +2501,10 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" +deepmerge@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.1.tgz#e862b4e45ea0555072bf51e7fd0d9845170ae768" + define-properties@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" @@ -2627,10 +2631,6 @@ detective@^5.0.2: defined "^1.0.0" minimist "^1.1.1" -device-detect@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/device-detect/-/device-detect-1.0.7.tgz#d4f1aa2fc3afbbc7d4b4dc182b9822dffc50a708" - diff@^2.1.2: version "2.2.3" resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" @@ -4956,7 +4956,7 @@ loader-utils@^0.2.16: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" dependencies: @@ -5266,6 +5266,10 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +mime@^2.0.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" + mime@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b" @@ -5284,9 +5288,9 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" -minijanus@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.6.1.tgz#4d697313d58c4bdf9b762b6252981eaf7134c05f" +minijanus@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.6.2.tgz#c5746bfbbd0573b5c3c47742f16646c6af6f897c" minimalistic-assert@^1.0.0: version "1.0.0" @@ -5362,10 +5366,6 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd dependencies: minimist "0.0.8" -mobile-detect@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.4.1.tgz#f4b67c49bb84bf0437f72e3067deb1c60ad7b23c" - module-deps@^4.0.8: version "4.1.1" resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd" @@ -5406,16 +5406,6 @@ module-deps@^6.0.0: through2 "^2.0.0" xtend "^4.0.0" -moment-timezone@^0.5.14: - version "0.5.14" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.14.tgz#4eb38ff9538b80108ba467a458f3ed4268ccfcb1" - dependencies: - moment ">= 2.9.0" - -"moment@>= 2.9.0", moment@^2.22.0: - version "2.22.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.0.tgz#7921ade01017dd45186e7fee5f424f0b8663a730" - move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -5471,12 +5461,12 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -naf-janus-adapter@^0.9.0: - version "0.9.5" - resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.9.5.tgz#81fcbf068daf66820892544de4d8357614e0152d" +naf-janus-adapter@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.10.1.tgz#e3bd4847dba002d38446d4bea4a6908b26229928" dependencies: debug "^3.1.0" - minijanus "0.6.1" + minijanus "0.6.2" nan@^2.3.0, nan@^2.3.2: version "2.9.1" @@ -7196,7 +7186,7 @@ sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" -schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4.5: +schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" dependencies: @@ -8268,6 +8258,14 @@ url-join@^2.0.2: version "2.0.5" resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" +url-loader@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.0.1.tgz#61bc53f1f184d7343da2728a1289ef8722ea45ee" + dependencies: + loader-utils "^1.1.0" + mime "^2.0.3" + schema-utils "^0.4.3" + url-parse-lax@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" @@ -8673,6 +8671,13 @@ worker-farm@^1.5.2: errno "^0.1.4" xtend "^4.0.1" +worker-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"