From 5212c94a3d634df7e4567a11e7c4cec767aac60d Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Tue, 25 May 2021 17:34:22 +1000 Subject: [PATCH 001/142] Updated packages and added typescript --- package.json | 22 ++- tsconfig.json | 35 +++++ yarn.lock | 414 ++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 351 insertions(+), 120 deletions(-) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index 5fa1a20..b83f00b 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,9 @@ "polygon-clipping": "^0.15.2", "pretty-bytes": "^5.6.0", "raw.macro": "^0.4.2", - "react": "^17.0.1", - "react-dom": "^17.0.1", - "react-konva": "^17.0.1-3", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-konva": "^17.0.2-3", "react-markdown": "4", "react-media": "^2.0.0-rc.1", "react-modal": "^3.12.1", @@ -52,10 +52,10 @@ "simple-peer": "feross/simple-peer#694/head", "simplebar-react": "^2.1.0", "simplify-js": "^1.2.4", - "socket.io-client": "^4.0.0", + "socket.io-client": "^4.1.2", "socket.io-msgpack-parser": "^3.0.1", "source-map-explorer": "^2.5.2", - "theme-ui": "^0.3.1", + "theme-ui": "^0.8.4", "use-image": "^1.0.7", "webrtc-adapter": "^7.7.1" }, @@ -85,6 +85,18 @@ ] }, "devDependencies": { + "@types/color": "^3.0.1", + "@types/deep-diff": "^1.0.0", + "@types/jest": "^26.0.23", + "@types/lodash.get": "^4.4.6", + "@types/node": "^15.6.0", + "@types/react": "^17.0.6", + "@types/react-dom": "^17.0.5", + "@types/react-modal": "^3.12.0", + "@types/react-router-dom": "^5.1.7", + "@types/shortid": "^0.0.29", + "@types/simple-peer": "^9.6.3", + "typescript": "^4.2.4", "worker-loader": "^3.0.8" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..342e6cb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "isolatedModules": true, + "resolveJsonModule": true, + "noEmit": true, + "jsx": "react-jsx", + "sourceMap": true, + "declaration": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "incremental": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules/**/*", + "build/**/*" + ] +} diff --git a/yarn.lock b/yarn.lock index 439014e..e0b4faf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1691,6 +1691,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.13.10": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" + integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.3.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" @@ -1796,6 +1803,24 @@ resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18" integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg== +"@emotion/babel-plugin@^11.3.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz#3a16850ba04d8d9651f07f3fb674b3436a4fb9d7" + integrity sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA== + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/plugin-syntax-jsx" "^7.12.13" + "@babel/runtime" "^7.13.10" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.2" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "^4.0.3" + "@emotion/cache@^10.0.27": version "10.0.29" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" @@ -1817,18 +1842,6 @@ "@emotion/weak-memoize" "^0.2.5" stylis "^4.0.3" -"@emotion/core@^10.0.0": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3" - integrity sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA== - dependencies: - "@babel/runtime" "^7.5.5" - "@emotion/cache" "^10.0.27" - "@emotion/css" "^10.0.27" - "@emotion/serialize" "^0.11.15" - "@emotion/sheet" "0.9.4" - "@emotion/utils" "0.11.3" - "@emotion/core@^10.0.14": version "10.0.35" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.35.tgz#513fcf2e22cd4dfe9d3894ed138c9d7a859af9b3" @@ -1855,19 +1868,26 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.1": +"@emotion/is-prop-valid@^0.8.1": version "0.8.8" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== dependencies: "@emotion/memoize" "0.7.4" +"@emotion/is-prop-valid@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.0.tgz#29ef6be1e946fb4739f9707def860f316f668cde" + integrity sha512-9RkilvXAufQHsSsjQ3PIzSns+pxuX4EW8EbGeSPjZMHuMx6z/MOzb9LpqNieQX4F3mre3NWS2+X3JNRHTQztUQ== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/memoize@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== -"@emotion/memoize@^0.7.1", "@emotion/memoize@^0.7.4": +"@emotion/memoize@^0.7.1", "@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== @@ -1907,6 +1927,17 @@ "@emotion/utils" "^1.0.0" csstype "^3.0.2" +"@emotion/serialize@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" + integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + "@emotion/sheet@0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" @@ -1917,23 +1948,16 @@ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.1.tgz#245f54abb02dfd82326e28689f34c27aa9b2a698" integrity sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g== -"@emotion/styled-base@^10.0.27": - version "10.0.31" - resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a" - integrity sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ== +"@emotion/styled@^11.0.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.3.0.tgz#d63ee00537dfb6ff612e31b0e915c5cf9925a207" + integrity sha512-fUoLcN3BfMiLlRhJ8CuPUMEyKkLEoM+n+UyAbnqGEsCd5IzKQ7VQFLtzpJOaCD2/VR2+1hXQTnSZXVJeiTNltA== dependencies: - "@babel/runtime" "^7.5.5" - "@emotion/is-prop-valid" "0.8.8" - "@emotion/serialize" "^0.11.15" - "@emotion/utils" "0.11.3" - -"@emotion/styled@^10.0.0": - version "10.0.27" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf" - integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q== - dependencies: - "@emotion/styled-base" "^10.0.27" - babel-plugin-emotion "^10.0.27" + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.3.0" + "@emotion/is-prop-valid" "^1.1.0" + "@emotion/serialize" "^1.0.2" + "@emotion/utils" "^1.0.0" "@emotion/stylis@0.8.5": version "0.8.5" @@ -2200,7 +2224,7 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@mdx-js/react@^1.0.0": +"@mdx-js/react@^1.6.22": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.6.22.tgz#ae09b4744fddc74714ee9f9d6f17a66e77c43573" integrity sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg== @@ -2688,60 +2712,76 @@ dependencies: "@babel/runtime" "^7.12.5" -"@theme-ui/color-modes@0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@theme-ui/color-modes/-/color-modes-0.3.5.tgz#e280d1ff8be9f648c161b899e5049cb45a73cb90" - integrity sha512-3n5ExAnp1gAuVVFdGF2rRLyrVsa7qtmUXx+gj1wPJsADq23EE4ctkppC+aIfPFxT196WhR8fjErrVuO7Rh+wAg== +"@theme-ui/color-modes@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@theme-ui/color-modes/-/color-modes-0.8.4.tgz#d09bff439e990bc8751677b1d06adf73534a5571" + integrity sha512-3Ueb6yKBFkyHsLEFlLH3Igl6ZHbVamJHn6YoAwIut0QQrAOAfTSG1vZr/4LJUCILc/U0y9kPvTw7MBpUKi1hWg== dependencies: - "@emotion/core" "^10.0.0" - "@theme-ui/core" "0.3.5" - "@theme-ui/css" "0.3.5" + "@emotion/react" "^11.1.1" + "@theme-ui/core" "0.8.4" + "@theme-ui/css" "0.8.4" deepmerge "^4.2.2" -"@theme-ui/components@0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@theme-ui/components/-/components-0.3.5.tgz#cd0b5a1292f88b3df2c5b6b299b0a6cdc6da52e5" - integrity sha512-RdWwnN43H1Tq80lGCu6icNuYCWoHHNtwH+LJGaGfiPkv/uMXWrwzKPLMiAuYM5b3ofKtmdaAcxZLjqAld97jkw== +"@theme-ui/components@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@theme-ui/components/-/components-0.8.4.tgz#a362d0625f6a6efacc35a2db29fb6c2368257fd2" + integrity sha512-JOP/rABNS2Bu/hWA68Tdt6pUyCtCU+nMMWZAyvj2qDIn2mBrLwBKvvxyrwaGT5tHniIX4oVG57GH1Sb94Rw+mg== dependencies: - "@emotion/core" "^10.0.0" - "@emotion/styled" "^10.0.0" + "@emotion/react" "^11.1.1" + "@emotion/styled" "^11.0.0" "@styled-system/color" "^5.1.2" "@styled-system/should-forward-prop" "^5.1.2" "@styled-system/space" "^5.1.2" - "@theme-ui/css" "0.3.5" + "@theme-ui/css" "0.8.4" + "@types/styled-system" "^5.1.10" -"@theme-ui/core@0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@theme-ui/core/-/core-0.3.5.tgz#096ded31193bfe83c50d09eca0fb086cab2ac7e1" - integrity sha512-80gbG4BW0ZQgZ8TWSG7vY72uXDxmkI/GttjpJee7AJlWVrPh7RCD2E3cuFPjqXzt7o4BJ9lZSHmTXcLzixNtRw== +"@theme-ui/core@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@theme-ui/core/-/core-0.8.4.tgz#64ce2db0b2d50768cb8726e61f9d391cabae0448" + integrity sha512-zHOLJ/Zw024SlQEl7+mpIk2wuNaWRFCr9brYtV+2kyLwQeETIIBXEdmQ6yJ6wc1nSJNs0VOHk/sLRPvreb+5uQ== dependencies: - "@emotion/core" "^10.0.0" - "@theme-ui/css" "0.3.5" + "@emotion/react" "^11.1.1" + "@theme-ui/css" "0.8.4" + "@theme-ui/parse-props" "0.8.4" deepmerge "^4.2.2" -"@theme-ui/css@0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@theme-ui/css/-/css-0.3.5.tgz#cfd228c74bcc7840a7fe9e507858486ee4d403dc" - integrity sha512-XqsyXmifbnHOui1flSq4V7Lb3U+06Dbn2Q/leyr/cRd6Xgc0naiztdmD0MbXNvxgU51a2Ur9hyP4PnO5wE0yRg== - -"@theme-ui/mdx@0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@theme-ui/mdx/-/mdx-0.3.5.tgz#60d26102dcc8b2095269de461dcf8f51fa18cf4c" - integrity sha512-KMf5kkEcItQ3qIj7dston/kBOZc82ST2R0pUcyk/u8ZclX4ingRtZkMxm2zpmxybzdSUY3DIKf2MTK9CxUSpOQ== +"@theme-ui/css@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@theme-ui/css/-/css-0.8.4.tgz#2539c8ccb52054d54593786e5f1e89f118e908c0" + integrity sha512-ZubYp4glaDpsJSd2z38FlwkItvXk8+t0i0323ZWNO7Liqg4t/hJT+7RmtWj1NQ2IPVqTUEmsH/hVdz5SIPu2LA== dependencies: - "@emotion/core" "^10.0.0" - "@emotion/styled" "^10.0.0" - "@mdx-js/react" "^1.0.0" + "@emotion/react" "^11.1.1" + csstype "^3.0.5" -"@theme-ui/theme-provider@0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@theme-ui/theme-provider/-/theme-provider-0.3.5.tgz#98ad67d8e2e38a6347ea604878202f49fea38088" - integrity sha512-C1kVsGyrh/pqO/j4+KSF5IvVW1DOnZoQmpaJ9EjyU4bqY0PCTZfuNdNPfydKaDWiYxrKGXKBeX0xjvLLU6R0zQ== +"@theme-ui/mdx@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@theme-ui/mdx/-/mdx-0.8.4.tgz#86bee495402216f65bd3244303f2248d91ca90d0" + integrity sha512-pI2XalkIV+Ky2q/y+RBuHi9fBxzCECXZDSMYH98FExO3C6X5sdPDJ1u9kDKgbqJxUxTBQlZKSvU+fG9hjN3oQQ== dependencies: - "@emotion/core" "^10.0.0" - "@theme-ui/color-modes" "0.3.5" - "@theme-ui/core" "0.3.5" - "@theme-ui/mdx" "0.3.5" + "@emotion/react" "^11.1.1" + "@emotion/styled" "^11.0.0" + "@mdx-js/react" "^1.6.22" + "@theme-ui/core" "0.8.4" + "@theme-ui/css" "0.8.4" + +"@theme-ui/parse-props@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@theme-ui/parse-props/-/parse-props-0.8.4.tgz#398e54e11768248938c2c505c04bc44042b66409" + integrity sha512-2CgPKsApLVRu9wYzaaC/+ZhmKwohQ5uZylbf0HzVA3X4rGIfs7aktsM16FvCeiTyAn+7Z8MTShgSOSsD0S8l3Q== + dependencies: + "@emotion/react" "^11.1.1" + "@theme-ui/css" "0.8.4" + +"@theme-ui/theme-provider@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@theme-ui/theme-provider/-/theme-provider-0.8.4.tgz#87ba378f2bd4d07af5c04a215db193eb78843e89" + integrity sha512-xYXqs2y9hkZmqSTA76X2dfrAfUw8LDHnBa1Xp6J41Zb4iXcxbLp3Aq1yRT1xGBAAVRGyhfMkYqxSXfNxN8R1ig== + dependencies: + "@emotion/react" "^11.1.1" + "@theme-ui/color-modes" "0.8.4" + "@theme-ui/core" "0.8.4" + "@theme-ui/css" "0.8.4" + "@theme-ui/mdx" "0.8.4" "@types/anymatch@*": version "1.3.1" @@ -2786,11 +2826,35 @@ dependencies: "@babel/types" "^7.3.0" +"@types/color-convert@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" + integrity sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ== + dependencies: + "@types/color-name" "*" + +"@types/color-name@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/color@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.1.tgz#2900490ed04da8116c5058cd5dba3572d5a25071" + integrity sha512-oeUWVaAwI+xINDUx+3F2vJkl/vVB03VChFF/Gl3iQCdbcakjuoJyMOba+3BXRtnBhxZ7uBYqQBi9EpLnvSoztA== + dependencies: + "@types/color-convert" "*" + "@types/component-emitter@^1.2.10": version "1.2.10" resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea" integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg== +"@types/deep-diff@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/deep-diff/-/deep-diff-1.0.0.tgz#7eba3202a99b3a207f758f351f7f86387269fc40" + integrity sha512-ENsJcujGbCU/oXhDfQ12mSo/mCBWodT2tpARZKmatoSrf8+cGRCPi0KVj3I0FORhYZfLXkewXu7AoIWqiBLkNw== + "@types/eslint@^7.2.6": version "7.2.7" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.7.tgz#f7ef1cf0dceab0ae6f9a976a0a9af14ab1baca26" @@ -2824,6 +2888,11 @@ dependencies: "@types/node" "*" +"@types/history@*": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + "@types/html-minifier-terser@^5.0.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50" @@ -2856,6 +2925,14 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" +"@types/jest@^26.0.23": + version "26.0.23" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7" + integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" @@ -2866,6 +2943,18 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash.get@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@types/lodash.get/-/lodash.get-4.4.6.tgz#0c7ac56243dae0f9f09ab6f75b29471e2e777240" + integrity sha512-E6zzjR3GtNig8UJG/yodBeJeIOtgPkMgsLjDU3CbgCAPC++vJ0eCMnJhVpRZb/ENqEFlov1+3K9TKtY4UdWKtQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.170" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" + integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -2884,6 +2973,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.8.tgz#2127bd81949a95c8b7d3240f3254352d72563aec" integrity sha512-z/5Yd59dCKI5kbxauAJgw6dLPzW+TNOItNE00PkpzNwUIEwdj/Lsqwq94H5DdYBX7C13aRA0CY32BK76+neEUA== +"@types/node@^15.6.0": + version "15.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.0.tgz#f0ddca5a61e52627c9dcb771a6039d44694597bc" + integrity sha512-gCYSfQpy+LYhOFTKAeE8BkyGqaxmlFxe+n4DKM6DR0wzw/HISUE/hAmkC/KT8Sw5PCJblqg062b3z9gucv3k0A== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -2904,11 +2998,56 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/react-dom@^17.0.5": + version "17.0.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227" + integrity sha512-ikqukEhH4H9gr4iJCmQVNzTB307kROe3XFfHAOTxOXPOw7lAoEXnM5KWTkzeANGL5Ce6ABfiMl/zJBYNi7ObmQ== + dependencies: + "@types/react" "*" + +"@types/react-modal@^3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.12.0.tgz#91fa86a76fd7fc57e36d2cf9b76efe5735a855a1" + integrity sha512-UnHu/YO73NZLwqFpju/c0tqiswR0UHIfeO16rkfLVJUIMfQsl7X0CBm99H5XXgBMe/PgtQ/Rkud72iuRBr1TeA== + dependencies: + "@types/react" "*" + +"@types/react-router-dom@^5.1.7": + version "5.1.7" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.7.tgz#a126d9ea76079ffbbdb0d9225073eb5797ab7271" + integrity sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.14" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" + integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react@*", "@types/react@^17.0.6": + version "17.0.6" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.6.tgz#0ec564566302c562bf497d73219797a5e0297013" + integrity sha512-u/TtPoF/hrvb63LdukET6ncaplYsvCvmkceasx8oG84/ZCsoLxz9Z/raPBP4lTAiWW1Jb889Y9svHmv8R26dWw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resolve@0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" @@ -2916,11 +3055,28 @@ dependencies: "@types/node" "*" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/seedrandom@2.4.27": version "2.4.27" resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.27.tgz#9db563937dd86915f69092bc43259d2f48578e41" integrity sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE= +"@types/shortid@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-0.0.29.tgz#8093ee0416a6e2bf2aa6338109114b3fbffa0e9b" + integrity sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps= + +"@types/simple-peer@^9.6.3": + version "9.6.3" + resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.6.3.tgz#aa118a57e036f4ce2059a7e25367526a4764206d" + integrity sha512-zrXEBch9tF4NgkZDsGR3c1D0kq99M1bBCjzEyL0PVfEWzCIXrK64TuxRz3XKOx1B0KoEQ9kTs+AhMDuQaHy5RQ== + dependencies: + "@types/node" "*" + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -2931,6 +3087,13 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/styled-system@^5.1.10": + version "5.1.11" + resolved "https://registry.yarnpkg.com/@types/styled-system/-/styled-system-5.1.11.tgz#158849f3b14cdf8bf27a10f0cf87a2c3a89eb680" + integrity sha512-R+JxEZYa5T0HD2urViR/mdklVaGhwbNOtDoWWGQ1+z1CGs/gF1UAKCaS//YwsUwterEKpyaKxgaXyFNKF04GCA== + dependencies: + csstype "^3.0.2" + "@types/tapable@*", "@types/tapable@^1.0.5": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" @@ -3726,7 +3889,7 @@ babel-plugin-jest-hoist@^26.6.2: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-macros@2.8.0, babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.8.0: +babel-plugin-macros@2.8.0, babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.6.1, babel-plugin-macros@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== @@ -5119,6 +5282,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8" integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ== +csstype@^3.0.5: + version "3.0.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" + integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -5634,10 +5802,10 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -engine.io-client@~5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.0.0.tgz#65733887c8999d280e1dd7f241779a2c66e9559e" - integrity sha512-e6GK0Fqvq45Nu/j7YdIVqXtDPvlsggAcfml3QiEiGdJ1qeh7IQU6knxSN3+yy9BmbnXtIfjo1hK4MFyHKdc9mQ== +engine.io-client@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.1.1.tgz#f5c3aaaef1bdc9443aac6ffde48b3b2fb2dc56fc" + integrity sha512-jPFpw2HLL0lhZ2KY0BpZhIJdleQcUO9W1xkIpo0h3d6s+5D6+EV/xgQw9qWOmymszv2WXef/6KUUehyxEKomlQ== dependencies: base64-arraybuffer "0.1.4" component-emitter "~1.3.0" @@ -5680,6 +5848,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +err-code@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + err-code@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" @@ -5796,6 +5969,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.14.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -10694,14 +10872,14 @@ react-dev-utils@^11.0.3: strip-ansi "6.0.0" text-table "0.2.0" -react-dom@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6" - integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug== +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - scheduler "^0.20.1" + scheduler "^0.20.2" react-error-overlay@^6.0.9: version "6.0.9" @@ -10725,13 +10903,13 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-konva@^17.0.1-3: - version "17.0.1-3" - resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-17.0.1-3.tgz#4931c0de642a4ed511ec5aaa2ee113f32d92c68e" - integrity sha512-vbbVyf+IcapVOYu+6w3Wc8BMilQ5zq1P2YaUMSkr4OwcWHvK2hxpJCMldi+wOSOZWoWJBXchWfFvmQHcO55KPA== +react-konva@^17.0.2-3: + version "17.0.2-3" + resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-17.0.2-3.tgz#c5d789de3761981bf1d721e95df8393e5ba47e40" + integrity sha512-6HSRBzJ8omiZaalpOr2LtUrmXfmhsCa3qkr6Wjg4+a0cK3n67cA9QqEcDT9pZU6aNnrieutQrGddNxvXKiG+OA== dependencies: - react-reconciler "~0.26.1" - scheduler "^0.20.1" + react-reconciler "~0.26.2" + scheduler "^0.20.2" react-lifecycles-compat@^3.0.0: version "3.0.4" @@ -10772,14 +10950,14 @@ react-modal@^3.12.1: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react-reconciler@~0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.1.tgz#860952dd359fd870f94895c254271e3a9de3b2d6" - integrity sha512-6E/CvH9zcDmHjhiNJlP0qJ8+3ufnY2b5RWs774Uy8XKWN0l6qfnlkz0XnDacxqj2rbJdq76w9dlFXjPPOQrmqA== +react-reconciler@~0.26.2: + version "0.26.2" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91" + integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - scheduler "^0.20.1" + scheduler "^0.20.2" react-refresh@^0.8.3: version "0.8.3" @@ -10943,10 +11121,10 @@ react-use-gesture@^9.1.3: resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-9.1.3.tgz#92bd143e4f58e69bd424514a5bfccba2a1d62ec0" integrity sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg== -react@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" - integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w== +react@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -11526,10 +11704,10 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.20.1: - version "0.20.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c" - integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -11756,6 +11934,7 @@ simple-peer@feross/simple-peer#694/head: resolved "https://codeload.github.com/feross/simple-peer/tar.gz/0d08d07b83ff3b8c60401688d80642d24dfeffe2" dependencies: debug "^4.0.1" + err-code "^2.0.3" get-browser-rtc "^1.0.0" queue-microtask "^1.1.0" randombytes "^2.0.3" @@ -11842,16 +12021,16 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socket.io-client@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.0.0.tgz#643cc25e5b5bbe37be75ecd317156a3335bb495a" - integrity sha512-27yQxmXJAEYF19Ygyl8FPJ0if0wegpSmkIIbrWJeI7n7ST1JyH8bbD5v3fjjGY5cfCanACJ3dARUAyiVFNrlTQ== +socket.io-client@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.1.2.tgz#95ad7113318ea01fba0860237b96d71e1b1fd2eb" + integrity sha512-RDpWJP4DQT1XeexmeDyDkm0vrFc0+bUsHDKiVGaNISJvJonhQQOMqV9Vwfg0ZpPJ27LCdan7iqTI92FRSOkFWQ== dependencies: "@types/component-emitter" "^1.2.10" backo2 "~1.0.2" component-emitter "~1.3.0" debug "~4.3.1" - engine.io-client "~5.0.0" + engine.io-client "~5.1.1" parseuri "0.0.6" socket.io-parser "~4.0.4" @@ -12513,17 +12692,17 @@ text-table@0.2.0, text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -theme-ui@^0.3.1: - version "0.3.5" - resolved "https://registry.yarnpkg.com/theme-ui/-/theme-ui-0.3.5.tgz#db29a4df0ad6cbf0fde42aab1cd9941c4b813efc" - integrity sha512-yxooGhvkdjFDotDeIFehKo5k6NnLZ3gsLSe8EDe2aDcoWqg1mZjkjjr8EYtVCrK3mk/tYz97AT5BpEnUfamNCQ== +theme-ui@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/theme-ui/-/theme-ui-0.8.4.tgz#c2e2aebd6266d71a9b6be70cfff018fa8337866e" + integrity sha512-vBIixheMRmFf3YuqqonYeWg0dhbshWqUzTIx4ROYLSlkbsGwcCAxSwZBtwxwnTvieaInwNUBvQ+KOZ3HpWbT6A== dependencies: - "@theme-ui/color-modes" "0.3.5" - "@theme-ui/components" "0.3.5" - "@theme-ui/core" "0.3.5" - "@theme-ui/css" "0.3.5" - "@theme-ui/mdx" "0.3.5" - "@theme-ui/theme-provider" "0.3.5" + "@theme-ui/color-modes" "0.8.4" + "@theme-ui/components" "0.8.4" + "@theme-ui/core" "0.8.4" + "@theme-ui/css" "0.8.4" + "@theme-ui/mdx" "0.8.4" + "@theme-ui/theme-provider" "0.8.4" throat@^5.0.0: version "5.0.0" @@ -12786,6 +12965,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" + integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== + typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.37" resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.37.tgz#23e6bffc264b4dfc80603e2a8545bd4750102d42" From 86f15e927424d53545e7f1b80f38f32460c810c0 Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Tue, 25 May 2021 17:35:26 +1000 Subject: [PATCH 002/142] Added types to helpers --- src/{App.js => App.tsx} | 3 +- src/{database.js => database.ts} | 156 ++++++++------- src/global.d.ts | 2 + .../{FakeStorage.js => FakeStorage.ts} | 10 +- .../{KonvaBridge.js => KonvaBridge.tsx} | 4 +- src/helpers/{Settings.js => Settings.ts} | 12 +- src/helpers/{Size.js => Size.ts} | 16 +- src/helpers/{Vector2.js => Vector2.ts} | 82 ++++---- src/helpers/{Vector3.js => Vector3.ts} | 10 +- src/helpers/{actions.js => actions.ts} | 10 +- src/helpers/{babylon.js => babylon.ts} | 2 +- .../{blobToBuffer.js => blobToBuffer.ts} | 6 +- src/helpers/{colors.js => colors.ts} | 17 +- src/helpers/{dice.js => dice.ts} | 8 +- src/helpers/{diff.js => diff.ts} | 6 +- src/helpers/{drawing.js => drawing.ts} | 186 +++++++++++++----- src/helpers/{grid.js => grid.ts} | 95 ++++++--- src/helpers/{image.js => image.ts} | 80 +++++--- src/helpers/{konva.js => konva.tsx} | 56 ++++-- src/helpers/{logging.js => logging.ts} | 2 +- src/helpers/{monsters.js => monsters.ts} | 4 +- src/helpers/{select.js => select.tsx} | 74 ++++--- src/helpers/{shared.js => shared.ts} | 32 +-- src/helpers/{timer.js => timer.ts} | 20 +- src/{index.js => index.tsx} | 1 - src/network/{Connection.js => Connection.ts} | 25 ++- src/network/{Session.js => Session.ts} | 134 +++++++++---- src/react-app-env.d.ts | 1 + src/{serviceWorker.js => serviceWorker.ts} | 13 +- src/{settings.js => settings.ts} | 14 +- src/shortcuts.js | 110 ----------- src/shortcuts.ts | 114 +++++++++++ src/{theme.js => theme.ts} | 0 33 files changed, 798 insertions(+), 507 deletions(-) rename src/{App.js => App.tsx} (97%) rename src/{database.js => database.ts} (80%) create mode 100644 src/global.d.ts rename src/helpers/{FakeStorage.js => FakeStorage.ts} (66%) rename src/helpers/{KonvaBridge.js => KonvaBridge.tsx} (98%) rename src/helpers/{Settings.js => Settings.ts} (85%) rename src/helpers/{Size.js => Size.ts} (72%) rename src/helpers/{Vector2.js => Vector2.ts} (85%) rename src/helpers/{Vector3.js => Vector3.ts} (88%) rename src/helpers/{actions.js => actions.ts} (59%) rename src/helpers/{babylon.js => babylon.ts} (87%) rename src/helpers/{blobToBuffer.js => blobToBuffer.ts} (77%) rename src/helpers/{colors.js => colors.ts} (66%) rename src/helpers/{dice.js => dice.ts} (85%) rename src/helpers/{diff.js => diff.ts} (68%) rename src/helpers/{drawing.js => drawing.ts} (74%) rename src/helpers/{grid.js => grid.ts} (84%) rename src/helpers/{image.js => image.ts} (73%) rename src/helpers/{konva.js => konva.tsx} (84%) rename src/helpers/{logging.js => logging.ts} (78%) rename src/helpers/{monsters.js => monsters.ts} (98%) rename src/helpers/{select.js => select.tsx} (60%) rename src/helpers/{shared.js => shared.ts} (54%) rename src/helpers/{timer.js => timer.ts} (70%) rename src/{index.js => index.tsx} (99%) rename src/network/{Connection.js => Connection.ts} (87%) rename src/network/{Session.js => Session.ts} (78%) create mode 100644 src/react-app-env.d.ts rename src/{serviceWorker.js => serviceWorker.ts} (92%) rename src/{settings.js => settings.ts} (84%) delete mode 100644 src/shortcuts.js create mode 100644 src/shortcuts.ts rename src/{theme.js => theme.ts} (100%) diff --git a/src/App.js b/src/App.tsx similarity index 97% rename from src/App.js rename to src/App.tsx index a77ae2b..0a0427f 100644 --- a/src/App.js +++ b/src/App.tsx @@ -1,8 +1,7 @@ -import React from "react"; import { ThemeProvider } from "theme-ui"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; -import theme from "./theme.js"; +import theme from "./theme"; import Home from "./routes/Home"; import Game from "./routes/Game"; import About from "./routes/About"; diff --git a/src/database.js b/src/database.ts similarity index 80% rename from src/database.js rename to src/database.ts index 5ed9ed2..20ef6a7 100644 --- a/src/database.js +++ b/src/database.ts @@ -1,16 +1,16 @@ // eslint-disable-next-line no-unused-vars -import Dexie, { Version, DexieOptions } from "dexie"; +import Dexie, { Version, DexieOptions, Transaction } from "dexie"; import "dexie-observable"; import shortid from "shortid"; import blobToBuffer from "./helpers/blobToBuffer"; -import { getGridDefaultInset } from "./helpers/grid"; +import { getGridDefaultInset, Grid } from "./helpers/grid"; import { convertOldActionsToShapes } from "./actions"; import { createThumbnail } from "./helpers/image"; // Helper to create a thumbnail for a file in a db -async function createDataThumbnail(data) { - let url; +async function createDataThumbnail(data: any) { + let url: string; if (data?.resolutions?.low?.file) { url = URL.createObjectURL(new Blob([data.resolutions.low.file])); } else { @@ -20,7 +20,8 @@ async function createDataThumbnail(data) { new Promise((resolve) => { let image = new Image(); image.onload = async () => { - const thumbnail = await createThumbnail(image); + // TODO: confirm parameter for type here + const thumbnail = await createThumbnail(image, "file"); resolve(thumbnail); }; image.src = url; @@ -34,13 +35,16 @@ async function createDataThumbnail(data) { * @param {Version} version */ +type VersionCallback = (version: Version) => void + /** * Mapping of version number to their upgrade function * @type {Object.} */ -const versions = { + +const versions: Record = { // v1.2.0 - 1(v) { + 1(v: Version) { v.stores({ maps: "id, owner", states: "mapId", @@ -49,29 +53,29 @@ const versions = { }); }, // v1.2.1 - Move from blob files to array buffers - 2(v) { - v.stores({}).upgrade(async (tx) => { + 2(v: Version) { + v.stores({}).upgrade(async (tx: Transaction) => { const maps = await Dexie.waitFor(tx.table("maps").toArray()); - let mapBuffers = {}; + let mapBuffers: any = {}; for (let map of maps) { mapBuffers[map.id] = await Dexie.waitFor(blobToBuffer(map.file)); } return tx .table("maps") .toCollection() - .modify((map) => { + .modify((map: any) => { map.file = mapBuffers[map.id]; }); }); }, // v1.3.0 - Added new default tokens - 3(v) { - v.stores({}).upgrade((tx) => { + 3(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("states") .toCollection() - .modify((state) => { - function mapTokenId(id) { + .modify((state: any) => { + function mapTokenId(id: any) { switch (id) { case "__default-Axes": return "__default-Barbarian"; @@ -128,23 +132,23 @@ const versions = { }); }, // v1.3.1 - Added show grid option - 4(v) { - v.stores({}).upgrade((tx) => { + 4(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("maps") .toCollection() - .modify((map) => { + .modify((map: any) => { map.showGrid = false; }); }); }, // v1.4.0 - Added fog subtraction - 5(v) { - v.stores({}).upgrade((tx) => { + 5(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("states") .toCollection() - .modify((state) => { + .modify((state: any) => { for (let fogAction of state.fogDrawActions) { if (fogAction.type === "add" || fogAction.type === "edit") { for (let shape of fogAction.shapes) { @@ -156,24 +160,24 @@ const versions = { }); }, // v1.4.2 - Added map resolutions - 6(v) { - v.stores({}).upgrade((tx) => { + 6(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("maps") .toCollection() - .modify((map) => { + .modify((map: any) => { map.resolutions = {}; map.quality = "original"; }); }); }, // v1.5.0 - Fixed default token rogue spelling - 7(v) { - v.stores({}).upgrade((tx) => { + 7(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("states") .toCollection() - .modify((state) => { + .modify((state: any) => { for (let id in state.tokens) { if (state.tokens[id].tokenId === "__default-Rouge") { state.tokens[id].tokenId = "__default-Rogue"; @@ -183,23 +187,23 @@ const versions = { }); }, // v1.5.0 - Added map snap to grid option - 8(v) { - v.stores({}).upgrade((tx) => { + 8(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("maps") .toCollection() - .modify((map) => { + .modify((map: any) => { map.snapToGrid = true; }); }); }, // v1.5.1 - Added lock, visibility and modified to tokens - 9(v) { - v.stores({}).upgrade((tx) => { + 9(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("states") .toCollection() - .modify((state) => { + .modify((state: any) => { for (let id in state.tokens) { state.tokens[id].lastModifiedBy = state.tokens[id].lastEditedBy; delete state.tokens[id].lastEditedBy; @@ -211,51 +215,51 @@ const versions = { }); }, // v1.5.1 - Added token prop category and remove isVehicle bool - 10(v) { - v.stores({}).upgrade((tx) => { + 10(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("tokens") .toCollection() - .modify((token) => { + .modify((token: any) => { token.category = token.isVehicle ? "vehicle" : "character"; delete token.isVehicle; }); }); }, // v1.5.2 - Added automatic cache invalidation to maps - 11(v) { - v.stores({}).upgrade((tx) => { + 11(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("maps") .toCollection() - .modify((map) => { + .modify((map: any) => { map.lastUsed = map.lastModified; }); }); }, // v1.5.2 - Added automatic cache invalidation to tokens - 12(v) { - v.stores({}).upgrade((tx) => { + 12(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("tokens") .toCollection() - .modify((token) => { + .modify((token: any) => { token.lastUsed = token.lastModified; }); }); }, // v1.6.0 - Added map grouping and grid scale and offset - 13(v) { - v.stores({}).upgrade((tx) => { + 13(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("maps") .toCollection() - .modify((map) => { + .modify((map: any) => { map.group = ""; map.grid = { size: { x: map.gridX, y: map.gridY }, inset: getGridDefaultInset( - { size: { x: map.gridX, y: map.gridY }, type: "square" }, + { size: { x: map.gridX, y: map.gridY }, type: "square" } as Grid, map.width, map.height ), @@ -268,21 +272,21 @@ const versions = { }); }, // v1.6.0 - Added token grouping - 14(v) { - v.stores({}).upgrade((tx) => { + 14(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("tokens") .toCollection() - .modify((token) => { + .modify((token: any) => { token.group = ""; }); }); }, // v1.6.1 - Added width and height to tokens - 15(v) { - v.stores({}).upgrade(async (tx) => { + 15(v: Version) { + v.stores({}).upgrade(async (tx: Transaction) => { const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); - let tokenSizes = {}; + let tokenSizes: any = {}; for (let token of tokens) { const url = URL.createObjectURL(new Blob([token.file])); let image = new Image(); @@ -298,31 +302,31 @@ const versions = { return tx .table("tokens") .toCollection() - .modify((token) => { + .modify((token: any) => { token.width = tokenSizes[token.id].width; token.height = tokenSizes[token.id].height; }); }); }, // v1.7.0 - Added note tool - 16(v) { - v.stores({}).upgrade((tx) => { + 16(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("states") .toCollection() - .modify((state) => { + .modify((state: any) => { state.notes = {}; state.editFlags = [...state.editFlags, "notes"]; }); }); }, // 1.7.0 (hotfix) - Optimized fog shape edits to only include needed data - 17(v) { - v.stores({}).upgrade((tx) => { + 17(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("states") .toCollection() - .modify((state) => { + .modify((state: any) => { for (let i = 0; i < state.fogDrawActions.length; i++) { const action = state.fogDrawActions[i]; if (action && action.type === "edit") { @@ -340,12 +344,12 @@ const versions = { }); }, // 1.8.0 - Added note text only mode, converted draw and fog representations - 18(v) { - v.stores({}).upgrade((tx) => { + 18(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("states") .toCollection() - .modify((state) => { + .modify((state: any) => { for (let id in state.notes) { state.notes[id].textOnly = false; } @@ -367,12 +371,12 @@ const versions = { }); }, // 1.8.0 - Add thumbnail to maps and add measurement to grid - 19(v) { - v.stores({}).upgrade(async (tx) => { + 19(v: Version) { + v.stores({}).upgrade(async (tx: Transaction) => { const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) .value; const maps = await Dexie.waitFor(tx.table("maps").toArray()); - const thumbnails = {}; + const thumbnails: any = {}; for (let map of maps) { try { if (map.owner === userId) { @@ -383,19 +387,19 @@ const versions = { return tx .table("maps") .toCollection() - .modify((map) => { + .modify((map: any) => { map.thumbnail = thumbnails[map.id]; map.grid.measurement = { type: "chebyshev", scale: "5ft" }; }); }); }, // 1.8.0 - Add thumbnail to tokens - 20(v) { - v.stores({}).upgrade(async (tx) => { + 20(v: Version) { + v.stores({}).upgrade(async (tx: Transaction) => { const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) .value; const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); - const thumbnails = {}; + const thumbnails: any = {}; for (let token of tokens) { try { if (token.owner === userId) { @@ -406,22 +410,22 @@ const versions = { return tx .table("tokens") .toCollection() - .modify((token) => { + .modify((token: any) => { token.thumbnail = thumbnails[token.id]; }); }); }, // 1.8.0 - Upgrade for Dexie.Observable - 21(v) { + 21(v: Version) { v.stores({}); }, // v1.8.1 - Shorten fog shape ids - 22(v) { - v.stores({}).upgrade((tx) => { + 22(v: Version) { + v.stores({}).upgrade((tx: Transaction) => { return tx .table("states") .toCollection() - .modify((state) => { + .modify((state: any) => { for (let id of Object.keys(state.fogShapes)) { const newId = shortid.generate(); state.fogShapes[newId] = state.fogShapes[id]; @@ -440,7 +444,7 @@ const latestVersion = 22; * @param {Dexie} db * @param {number=} upTo version number to load up to, latest version if undefined */ -export function loadVersions(db, upTo = latestVersion) { +export function loadVersions(db: Dexie, upTo = latestVersion) { for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) { versions[versionNumber](db.version(versionNumber)); } @@ -454,7 +458,7 @@ export function loadVersions(db, upTo = latestVersion) { * @returns {Dexie} */ export function getDatabase( - options, + options: DexieOptions, name = "OwlbearRodeoDB", versionNumber = latestVersion ) { diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..a476569 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,2 @@ +declare module 'pepjs'; +declare module 'socket.io-msgpack-parser'; \ No newline at end of file diff --git a/src/helpers/FakeStorage.js b/src/helpers/FakeStorage.ts similarity index 66% rename from src/helpers/FakeStorage.js rename to src/helpers/FakeStorage.ts index d464924..48fcd7f 100644 --- a/src/helpers/FakeStorage.js +++ b/src/helpers/FakeStorage.ts @@ -2,17 +2,17 @@ * A faked local or session storage used when the user has disabled storage */ class FakeStorage { - data = {}; - key(index) { + data: { [keyName: string ]: any} = {}; + key(index: number) { return Object.keys(this.data)[index] || null; } - getItem(keyName) { + getItem(keyName: string ) { return this.data[keyName] || null; } - setItem(keyName, keyValue) { + setItem(keyName: string, keyValue: any) { this.data[keyName] = keyValue; } - removeItem(keyName) { + removeItem(keyName: string) { delete this.data[keyName]; } clear() { diff --git a/src/helpers/KonvaBridge.js b/src/helpers/KonvaBridge.tsx similarity index 98% rename from src/helpers/KonvaBridge.js rename to src/helpers/KonvaBridge.tsx index b8fb3db..4ffd550 100644 --- a/src/helpers/KonvaBridge.js +++ b/src/helpers/KonvaBridge.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import { useContext } from "react"; import { InteractionEmitterContext, @@ -47,7 +47,7 @@ import { /** * Provide a bridge for konva that forwards our contexts */ -function KonvaBridge({ stageRender, children }) { +function KonvaBridge({ stageRender, children }: { stageRender: any, children: any}) { const mapStageRef = useMapStage(); const auth = useAuth(); const settings = useSettings(); diff --git a/src/helpers/Settings.js b/src/helpers/Settings.ts similarity index 85% rename from src/helpers/Settings.js rename to src/helpers/Settings.ts index 348fc3f..f4c4382 100644 --- a/src/helpers/Settings.js +++ b/src/helpers/Settings.ts @@ -8,7 +8,7 @@ class Settings { currentVersion; storage; - constructor(name) { + constructor(name: string) { this.name = name; // Try and use local storage if it is available, if not mock it with an in memory storage try { @@ -22,30 +22,30 @@ class Settings { this.currentVersion = this.get("__version"); } - version(versionNumber, upgradeFunction) { + version(versionNumber: number, upgradeFunction: Function) { if (versionNumber > this.currentVersion) { this.currentVersion = versionNumber; this.setAll(upgradeFunction(this.getAll())); } } - getAll() { + getAll(): any { return JSON.parse(this.storage.getItem(this.name)); } - get(key) { + get(key: string) { const settings = this.getAll(); return settings && settings[key]; } - setAll(newSettings) { + setAll(newSettings: any) { this.storage.setItem( this.name, JSON.stringify({ ...newSettings, __version: this.currentVersion }) ); } - set(key, value) { + set(key: string, value: string) { let settings = this.getAll(); settings[key] = value; this.setAll(settings); diff --git a/src/helpers/Size.js b/src/helpers/Size.ts similarity index 72% rename from src/helpers/Size.js rename to src/helpers/Size.ts index 8330c45..eddfd43 100644 --- a/src/helpers/Size.js +++ b/src/helpers/Size.ts @@ -8,9 +8,9 @@ class Size extends Vector2 { /** * @param {number} width * @param {number} height - * @param {number=} radius Used to represent hexagon sizes + * @param {number} radius Used to represent hexagon sizes */ - constructor(width, height, radius) { + constructor(width: number, height: number, radius?: number) { super(width, height); this._radius = radius; } @@ -18,35 +18,35 @@ class Size extends Vector2 { /** * @returns {number} */ - get width() { + get width(): number { return this.x; } /** * @param {number} width */ - set width(width) { + set width(width: number) { this.x = width; } /** * @returns {number} */ - get height() { + get height(): number { return this.y; } /** * @param {number} height */ - set height(height) { + set height(height: number) { this.y = height; } /** * @returns {number} */ - get radius() { + get radius(): number { if (this._radius) { return this._radius; } else { @@ -57,7 +57,7 @@ class Size extends Vector2 { /** * @param {number} radius */ - set radius(radius) { + set radius(radius: number) { this._radius = radius; } } diff --git a/src/helpers/Vector2.js b/src/helpers/Vector2.ts similarity index 85% rename from src/helpers/Vector2.js rename to src/helpers/Vector2.ts index ee3ccf3..8204d05 100644 --- a/src/helpers/Vector2.js +++ b/src/helpers/Vector2.ts @@ -5,6 +5,14 @@ import { floorTo as floorToNumber, } from "./shared"; +export type BoundingBox = { + min: Vector2, + max: Vector2, + width: number, + height: number, + center: Vector2 +} + /** * Vector class with x, y and static helper methods */ @@ -12,17 +20,17 @@ class Vector2 { /** * @type {number} x - X component of the vector */ - x; + x: number; /** * @type {number} y - Y component of the vector */ - y; + y: number; /** * @param {number} x * @param {number} y */ - constructor(x, y) { + constructor(x: number, y: number) { this.x = x; this.y = y; } @@ -31,7 +39,7 @@ class Vector2 { * @param {Vector2} p * @returns {number} Length squared of `p` */ - static lengthSquared(p) { + static lengthSquared(p: Vector2): number { return p.x * p.x + p.y * p.y; } @@ -39,7 +47,7 @@ class Vector2 { * @param {Vector2} p * @returns {number} Length of `p` */ - static length(p) { + static setLength(p: Vector2): number { return Math.sqrt(this.lengthSquared(p)); } @@ -47,8 +55,8 @@ class Vector2 { * @param {Vector2} p * @returns {Vector2} `p` normalized, if length of `p` is 0 `{x: 0, y: 0}` is returned */ - static normalize(p) { - const l = this.length(p); + static normalize(p: Vector2): Vector2 { + const l = this.setLength(p); if (l === 0) { return { x: 0, y: 0 }; } @@ -60,7 +68,7 @@ class Vector2 { * @param {Vector2} b * @returns {number} Dot product between `a` and `b` */ - static dot(a, b) { + static dot(a: Vector2, b: Vector2): number { return a.x * b.x + a.y * b.y; } @@ -69,7 +77,7 @@ class Vector2 { * @param {(Vector2 | number)} b * @returns {Vector2} a - b */ - static subtract(a, b) { + static subtract(a: Vector2, b: Vector2 | number): Vector2 { if (typeof b === "number") { return { x: a.x - b, y: a.y - b }; } else { @@ -82,7 +90,7 @@ class Vector2 { * @param {(Vector2 | number)} b * @returns {Vector2} a + b */ - static add(a, b) { + static add(a: Vector2, b: Vector2 | number): Vector2 { if (typeof b === "number") { return { x: a.x + b, y: a.y + b }; } else { @@ -95,7 +103,7 @@ class Vector2 { * @param {(Vector2 | number)} b * @returns {Vector2} a * b */ - static multiply(a, b) { + static multiply(a: Vector2, b: Vector2 | number): Vector2 { if (typeof b === "number") { return { x: a.x * b, y: a.y * b }; } else { @@ -108,7 +116,7 @@ class Vector2 { * @param {(Vector2 | number)} b * @returns {Vector2} a / b */ - static divide(a, b) { + static divide(a: Vector2, b: Vector2 | number): Vector2 { if (typeof b === "number") { return { x: a.x / b, y: a.y / b }; } else { @@ -123,7 +131,7 @@ class Vector2 { * @param {number} angle Angle of rotation in degrees * @returns {Vector2} Rotated point */ - static rotate(point, origin, angle) { + static rotate(point: Vector2, origin: Vector2, angle: number): Vector2 { const cos = Math.cos(toRadians(angle)); const sin = Math.sin(toRadians(angle)); const dif = this.subtract(point, origin); @@ -139,7 +147,7 @@ class Vector2 { * @param {number} angle Angle of rotation in degrees * @returns {Vector2} Rotated direction */ - static rotateDirection(direction, angle) { + static rotateDirection(direction: Vector2, angle: number): Vector2 { return this.rotate(direction, { x: 0, y: 0 }, angle); } @@ -149,7 +157,7 @@ class Vector2 { * @param {(Vector2 | number)} [minimum] Value to compare * @returns {(Vector2 | number)} */ - static min(a, minimum) { + static min(a: Vector2, minimum?: Vector2 | number): Vector2 | number { if (minimum === undefined) { return a.x < a.y ? a.x : a.y; } else if (typeof minimum === "number") { @@ -164,7 +172,7 @@ class Vector2 { * @param {(Vector2 | number)} [maximum] Value to compare * @returns {(Vector2 | number)} */ - static max(a, maximum) { + static max(a: Vector2, maximum?: Vector2 | number): Vector2 | number { if (maximum === undefined) { return a.x > a.y ? a.x : a.y; } else if (typeof maximum === "number") { @@ -180,7 +188,7 @@ class Vector2 { * @param {Vector2} to * @returns {Vector2} */ - static roundTo(p, to) { + static roundTo(p: Vector2, to: Vector2): Vector2 { return { x: roundToNumber(p.x, to.x), y: roundToNumber(p.y, to.y), @@ -193,7 +201,7 @@ class Vector2 { * @param {Vector2} to * @returns {Vector2} */ - static floorTo(p, to) { + static floorTo(p: Vector2, to: Vector2): Vector2 { return { x: floorToNumber(p.x, to.x), y: floorToNumber(p.y, to.y), @@ -204,7 +212,7 @@ class Vector2 { * @param {Vector2} a * @returns {Vector2} The component wise sign of `a` */ - static sign(a) { + static sign(a: Vector2): Vector2 { return { x: Math.sign(a.x), y: Math.sign(a.y) }; } @@ -212,7 +220,7 @@ class Vector2 { * @param {Vector2} a * @returns {Vector2} The component wise absolute of `a` */ - static abs(a) { + static abs(a: Vector2): Vector2 { return { x: Math.abs(a.x), y: Math.abs(a.y) }; } @@ -221,7 +229,7 @@ class Vector2 { * @param {(Vector2 | number)} b * @returns {Vector2} `a` to the power of `b` */ - static pow(a, b) { + static pow(a: Vector2, b: Vector2 | number): Vector2 { if (typeof b === "number") { return { x: Math.pow(a.x, b), y: Math.pow(a.y, b) }; } else { @@ -233,7 +241,7 @@ class Vector2 { * @param {Vector2} a * @returns {number} The dot product between `a` and `a` */ - static dot2(a) { + static dot2(a: Vector2): number { return this.dot(a, a); } @@ -244,7 +252,7 @@ class Vector2 { * @param {number} max * @returns {Vector2} */ - static clamp(a, min, max) { + static clamp(a: Vector2, min: number, max: number): Vector2 { return { x: Math.min(Math.max(a.x, min), max), y: Math.min(Math.max(a.y, min), max), @@ -259,11 +267,11 @@ class Vector2 { * @param {Vector2} b End of the line * @returns {Object} The distance to and the closest point on the line segment */ - static distanceToLine(p, a, b) { + static distanceToLine(p: Vector2, a: Vector2, b: Vector2): Object { const pa = this.subtract(p, a); const ba = this.subtract(b, a); const h = Math.min(Math.max(this.dot(pa, ba) / this.dot(ba, ba), 0), 1); - const distance = this.length(this.subtract(pa, this.multiply(ba, h))); + const distance = this.setLength(this.subtract(pa, this.multiply(ba, h))); const point = this.add(a, this.multiply(ba, h)); return { distance, point }; } @@ -278,7 +286,7 @@ class Vector2 { * @param {Vector2} C End of the curve * @returns {Object} The distance to and the closest point on the curve */ - static distanceToQuadraticBezier(pos, A, B, C) { + static distanceToQuadraticBezier(pos: Vector2, A: Vector2, B: Vector2, C: Vector2): Object { let distance = 0; let point = { x: pos.x, y: pos.y }; @@ -358,7 +366,7 @@ class Vector2 { * @param {Vector2[]} points * @returns {BoundingBox} */ - static getBoundingBox(points) { + static getBoundingBox(points: Vector2[]): BoundingBox { let minX = Number.MAX_VALUE; let maxX = Number.MIN_VALUE; let minY = Number.MAX_VALUE; @@ -389,7 +397,7 @@ class Vector2 { * @param {Vector2[]} points * @returns {boolean} */ - static pointInPolygon(p, points) { + static pointInPolygon(p: Vector2, points: Vector2[]): boolean { const bounds = this.getBoundingBox(points); if ( p.x < bounds.min.x || @@ -422,8 +430,9 @@ class Vector2 { * @param {Vector2} a * @param {Vector2} b * @param {number} threshold + * @returns {boolean} */ - static compare(a, b, threshold) { + static compare(a: Vector2, b: Vector2, threshold: number): boolean { return this.lengthSquared(this.subtract(a, b)) < threshold * threshold; } @@ -431,9 +440,10 @@ class Vector2 { * Returns the distance between two vectors * @param {Vector2} a * @param {Vector2} b + * @returns {number} */ - static distance(a, b) { - return this.length(this.subtract(a, b)); + static distance(a: Vector2, b: Vector2): number { + return this.setLength(this.subtract(a, b)); } /** @@ -443,15 +453,16 @@ class Vector2 { * @param {number} alpha * @returns {Vector2} */ - static lerp(a, b, alpha) { + static lerp(a: Vector2, b: Vector2, alpha: number): Vector2 { return { x: lerpNumber(a.x, b.x, alpha), y: lerpNumber(a.y, b.y, alpha) }; } /** * Returns total length of a an array of points treated as a path * @param {Vector2[]} points the array of points in the path + * @returns {number} */ - static pathLength(points) { + static pathLength(points: Vector2[]): number { let l = 0; for (let i = 1; i < points.length; i++) { l += this.distance(points[i - 1], points[i]); @@ -464,8 +475,9 @@ class Vector2 { * based off of http://depts.washington.edu/acelab/proj/dollar/index.html * @param {Vector2[]} points the points to resample * @param {number} n the number of new points + * @returns {Vector2[]} */ - static resample(points, n) { + static resample(points: Vector2[], n: number): Vector2[] { if (points.length === 0 || n <= 0) { return []; } @@ -501,7 +513,7 @@ class Vector2 { * @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector * @returns {Vector2} */ - static rotate90(p, direction = "clockwise") { + static rotate90(p: Vector2, direction: "counterClockwise" | "clockwise" = "clockwise"): Vector2 { if (direction === "clockwise") { return { x: p.y, y: -p.x }; } else { diff --git a/src/helpers/Vector3.js b/src/helpers/Vector3.ts similarity index 88% rename from src/helpers/Vector3.js rename to src/helpers/Vector3.ts index d29b8b0..169bb83 100644 --- a/src/helpers/Vector3.js +++ b/src/helpers/Vector3.ts @@ -5,22 +5,22 @@ class Vector3 { /** * @type {number} x - X component of the vector */ - x; + x: number; /** * @type {number} y - Y component of the vector */ - y; + y: number; /** * @type {number} z - Z component of the vector */ - z; + z: number; /** * @param {number} x * @param {number} y * @param {number} z */ - constructor(x, y, z) { + constructor(x: number, y: number, z: number) { this.x = x; this.y = y; this.z = z; @@ -31,7 +31,7 @@ class Vector3 { * @param {Vector3} cube * @returns {Vector3} */ - static cubeRound(cube) { + static cubeRound(cube: Vector3): Vector3 { var rX = Math.round(cube.x); var rY = Math.round(cube.y); var rZ = Math.round(cube.z); diff --git a/src/helpers/actions.js b/src/helpers/actions.ts similarity index 59% rename from src/helpers/actions.js rename to src/helpers/actions.ts index 44c51b7..6dd8a31 100644 --- a/src/helpers/actions.js +++ b/src/helpers/actions.ts @@ -1,17 +1,17 @@ import shortid from "shortid"; -export function addPolygonDifferenceToShapes(shape, difference, shapes) { +export function addPolygonDifferenceToShapes(shape: any, difference: any, shapes: any) { for (let i = 0; i < difference.length; i++) { let newId = shortid.generate(); // Holes detected let holes = []; if (difference[i].length > 1) { for (let j = 1; j < difference[i].length; j++) { - holes.push(difference[i][j].map(([x, y]) => ({ x, y }))); + holes.push(difference[i][j].map(([x, y]: [ x: number, y: number ]) => ({ x, y }))); } } - const points = difference[i][0].map(([x, y]) => ({ x, y })); + const points = difference[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y })); shapes[newId] = { ...shape, @@ -24,11 +24,11 @@ export function addPolygonDifferenceToShapes(shape, difference, shapes) { } } -export function addPolygonIntersectionToShapes(shape, intersection, shapes) { +export function addPolygonIntersectionToShapes(shape: any, intersection: any, shapes: any) { for (let i = 0; i < intersection.length; i++) { let newId = shortid.generate(); - const points = intersection[i][0].map(([x, y]) => ({ x, y })); + const points = intersection[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y })); shapes[newId] = { ...shape, diff --git a/src/helpers/babylon.js b/src/helpers/babylon.ts similarity index 87% rename from src/helpers/babylon.js rename to src/helpers/babylon.ts index a70c79c..a713bad 100644 --- a/src/helpers/babylon.js +++ b/src/helpers/babylon.ts @@ -1,7 +1,7 @@ import { Texture } from "@babylonjs/core/Materials/Textures/texture"; // Turn texture load into an async function so it can be awaited -export async function importTextureAsync(url) { +export async function importTextureAsync(url: string) { return new Promise((resolve, reject) => { let texture = new Texture( url, diff --git a/src/helpers/blobToBuffer.js b/src/helpers/blobToBuffer.ts similarity index 77% rename from src/helpers/blobToBuffer.js rename to src/helpers/blobToBuffer.ts index e228557..905f5d5 100644 --- a/src/helpers/blobToBuffer.js +++ b/src/helpers/blobToBuffer.ts @@ -2,7 +2,7 @@ * @param {Blob} blob * @returns {Promise} */ -async function blobToBuffer(blob) { +async function blobToBuffer(blob: Blob): Promise { if (blob.arrayBuffer) { const arrayBuffer = await blob.arrayBuffer(); return new Uint8Array(arrayBuffer); @@ -10,12 +10,12 @@ async function blobToBuffer(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); - function onLoadEnd(event) { + function onLoadEnd(event: any) { reader.removeEventListener("loadend", onLoadEnd, false); if (event.error) { reject(event.error); } else { - resolve(Buffer.from(reader.result)); + resolve(Buffer.from(reader.result as ArrayBuffer)); } } diff --git a/src/helpers/colors.js b/src/helpers/colors.ts similarity index 66% rename from src/helpers/colors.js rename to src/helpers/colors.ts index ccb5e32..7acc610 100644 --- a/src/helpers/colors.js +++ b/src/helpers/colors.ts @@ -1,5 +1,20 @@ +export type Colors = { + blue: string; + orange: string; + red: string; + yellow: string; + purple: string; + green: string; + pink: string; + teal: string; + black: string; + darkGray: string; + lightGray: string; + white: string; +} + // Colors used for the game for theme general UI colors look at theme.js -const colors = { +const colors: Colors = { blue: "rgb(26, 106, 255)", orange: "rgb(255, 116, 51)", red: "rgb(255, 77, 77)", diff --git a/src/helpers/dice.js b/src/helpers/dice.ts similarity index 85% rename from src/helpers/dice.js rename to src/helpers/dice.ts index d30cab8..2eb661e 100644 --- a/src/helpers/dice.js +++ b/src/helpers/dice.ts @@ -4,7 +4,7 @@ import { Vector3 } from "@babylonjs/core/Maths/math"; * Find the number facing up on a mesh instance of a dice * @param {Object} instance The dice instance */ -export function getDiceInstanceRoll(instance) { +export function getDiceInstanceRoll(instance: any) { let highestDot = -1; let highestLocator; for (let locator of instance.getChildTransformNodes()) { @@ -25,7 +25,7 @@ export function getDiceInstanceRoll(instance) { * Find the number facing up on a dice object * @param {Object} dice The Dice object */ -export function getDiceRoll(dice) { +export function getDiceRoll(dice: any) { let number = getDiceInstanceRoll(dice.instance); // If the dice is a d100 add the d10 if (dice.type === "d100") { @@ -42,8 +42,8 @@ export function getDiceRoll(dice) { return { type: dice.type, roll: number }; } -export function getDiceRollTotal(diceRolls) { - return diceRolls.reduce((accumulator, dice) => { +export function getDiceRollTotal(diceRolls: []) { + return diceRolls.reduce((accumulator: number, dice: any) => { if (dice.roll === "unknown") { return accumulator; } else { diff --git a/src/helpers/diff.js b/src/helpers/diff.ts similarity index 68% rename from src/helpers/diff.js rename to src/helpers/diff.ts index c1b503a..784478e 100644 --- a/src/helpers/diff.js +++ b/src/helpers/diff.ts @@ -1,7 +1,7 @@ -import { applyChange, revertChange, diff as deepDiff } from "deep-diff"; +import { applyChange, Diff, revertChange, diff as deepDiff }from "deep-diff"; import get from "lodash.get"; -export function applyChanges(target, changes) { +export function applyChanges(target: LHS, changes: Diff[]) { for (let change of changes) { if (change.path && (change.kind === "E" || change.kind === "A")) { // If editing an object or array ensure that the value exists @@ -15,7 +15,7 @@ export function applyChanges(target, changes) { } } -export function revertChanges(target, changes) { +export function revertChanges(target: LHS, changes: Diff[]) { for (let change of changes) { revertChange(target, true, change); } diff --git a/src/helpers/drawing.js b/src/helpers/drawing.ts similarity index 74% rename from src/helpers/drawing.js rename to src/helpers/drawing.ts index 4491d61..1a43972 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.ts @@ -1,15 +1,20 @@ import simplify from "simplify-js"; -import polygonClipping from "polygon-clipping"; +import polygonClipping, { Geom, Polygon, Ring } from "polygon-clipping"; -import Vector2 from "./Vector2"; +import Vector2, { BoundingBox } from "./Vector2"; +import Size from "./Size" import { toDegrees } from "./shared"; -import { getNearestCellCoordinates, getCellLocation } from "./grid"; +import { Grid, getNearestCellCoordinates, getCellLocation } from "./grid"; /** * @typedef PointsData * @property {Vector2[]} points */ +type PointsData = { + points: Vector2[] +} + /** * @typedef RectData * @property {number} x @@ -18,30 +23,55 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid"; * @property {number} height */ +type RectData = { + x: number, + y: number, + width: number, + height: number +} + /** * @typedef CircleData * @property {number} x * @property {number} y * @property {number} radius */ + +type CircleData = { + x: number, + y: number, + radius: number +} + /** * @typedef FogData * @property {Vector2[]} points - * @property {Vector2[]} holes + * @property {Vector2[][]} holes */ +type FogData = { + points: Vector2[] + holes: Vector2[][] +} + /** * @typedef {(PointsData|RectData|CircleData)} ShapeData */ +type ShapeData = PointsData | RectData | CircleData + /** * @typedef {("line"|"rectangle"|"circle"|"triangle")} ShapeType */ +type ShapeType = "line" | "rectangle" | "circle" | "triangle" + /** * @typedef {("fill"|"stroke")} PathType */ +// type PathType = "fill" | "stroke" + /** * @typedef Path * @property {boolean} blend @@ -53,6 +83,16 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid"; * @property {"path"} type */ +// type Path = { +// blend: boolean, +// color: string, +// data: PointsData, +// id: string, +// pathType: PathType, +// strokeWidth: number, +// type: "path" +// } + /** * @typedef Shape * @property {boolean} blend @@ -64,6 +104,16 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid"; * @property {"shape"} type */ +// type Shape = { +// blend: boolean, +// color: string, +// data: ShapeData, +// id: string, +// shapeType: ShapeType, +// strokeWidth: number, +// type: "shape" +// } + /** * @typedef Fog * @property {string} color @@ -74,29 +124,39 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid"; * @property {boolean} visible */ +type Fog = { + color: string, + data: FogData, + id: string, + strokeWidth: number, + type: "fog", + visible: boolean +} + /** * * @param {ShapeType} type * @param {Vector2} brushPosition * @returns {ShapeData} */ -export function getDefaultShapeData(type, brushPosition) { +export function getDefaultShapeData(type: ShapeType, brushPosition: Vector2): ShapeData | undefined{ + // TODO: handle undefined if no type found if (type === "line") { return { points: [ { x: brushPosition.x, y: brushPosition.y }, { x: brushPosition.x, y: brushPosition.y }, ], - }; + } as PointsData; } else if (type === "circle") { - return { x: brushPosition.x, y: brushPosition.y, radius: 0 }; + return { x: brushPosition.x, y: brushPosition.y, radius: 0 } as CircleData; } else if (type === "rectangle") { return { x: brushPosition.x, y: brushPosition.y, width: 0, height: 0, - }; + } as RectData; } else if (type === "triangle") { return { points: [ @@ -104,7 +164,7 @@ export function getDefaultShapeData(type, brushPosition) { { x: brushPosition.x, y: brushPosition.y }, { x: brushPosition.x, y: brushPosition.y }, ], - }; + } as PointsData; } } @@ -112,7 +172,7 @@ export function getDefaultShapeData(type, brushPosition) { * @param {Vector2} cellSize * @returns {Vector2} */ -export function getGridCellRatio(cellSize) { +export function getGridCellRatio(cellSize: Vector2): Vector2 { if (cellSize.x < cellSize.y) { return { x: cellSize.y / cellSize.x, y: 1 }; } else if (cellSize.y < cellSize.x) { @@ -131,30 +191,34 @@ export function getGridCellRatio(cellSize) { * @returns {ShapeData} */ export function getUpdatedShapeData( - type, - data, - brushPosition, - gridCellNormalizedSize, - mapWidth, - mapHeight -) { + type: ShapeType, + data: ShapeData, + brushPosition: Vector2, + gridCellNormalizedSize: Vector2, + mapWidth: number, + mapHeight: number +): ShapeData | undefined { + // TODO: handle undefined type if (type === "line") { + data = data as PointsData; return { points: [data.points[0], { x: brushPosition.x, y: brushPosition.y }], - }; + } as PointsData; } else if (type === "circle") { + data = data as CircleData; const gridRatio = getGridCellRatio(gridCellNormalizedSize); const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y, }); const scaled = Vector2.multiply(dif, gridRatio); - const distance = Vector2.length(scaled); + const distance = Vector2.setLength(scaled); return { ...data, radius: distance, }; } else if (type === "rectangle") { + data = data as RectData; const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y }); return { ...data, @@ -162,6 +226,7 @@ export function getUpdatedShapeData( height: dif.y, }; } else if (type === "triangle") { + data = data as PointsData; // Convert to absolute coordinates const mapSize = { x: mapWidth, y: mapHeight }; const brushPositionPixel = Vector2.multiply(brushPosition, mapSize); @@ -169,7 +234,7 @@ export function getUpdatedShapeData( const points = data.points; const startPixel = Vector2.multiply(points[0], mapSize); const dif = Vector2.subtract(brushPositionPixel, startPixel); - const length = Vector2.length(dif); + const length = Vector2.setLength(dif); const direction = Vector2.normalize(dif); // Get the angle for a triangle who's width is the same as it's length const angle = Math.atan(length / 2 / (length === 0 ? 1 : length)); @@ -199,10 +264,10 @@ const defaultSimplifySize = 1 / 100; * @param {Vector2} gridCellSize * @param {number} scale */ -export function simplifyPoints(points, gridCellSize, scale) { +export function simplifyPoints(points: Vector2[], gridCellSize: Vector2, scale: number): any { return simplify( points, - (Vector2.min(gridCellSize) * defaultSimplifySize) / scale + (Vector2.min(gridCellSize) as number * defaultSimplifySize) / scale ); } @@ -212,43 +277,50 @@ export function simplifyPoints(points, gridCellSize, scale) { * @param {boolean} ignoreHidden * @returns {Fog[]} */ -export function mergeFogShapes(shapes, ignoreHidden = true) { +export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog[] { if (shapes.length === 0) { return shapes; } - let geometries = []; + let geometries: Geom[] = []; for (let shape of shapes) { if (ignoreHidden && !shape.visible) { continue; } - const shapePoints = shape.data.points.map(({ x, y }) => [x, y]); - const shapeHoles = shape.data.holes.map((hole) => - hole.map(({ x, y }) => [x, y]) + const shapePoints: Ring = shape.data.points.map(({ x, y }) => [x, y]); + const shapeHoles: Polygon = shape.data.holes.map((hole) => + hole.map(({ x, y }: { x: number, y: number }) => [x, y]) ); - let shapeGeom = [[shapePoints, ...shapeHoles]]; + let shapeGeom: Geom = [[shapePoints, ...shapeHoles]]; geometries.push(shapeGeom); } if (geometries.length === 0) { - return geometries; + return []; } try { - let union = polygonClipping.union(...geometries); - let merged = []; + let union = polygonClipping.union(geometries[0], ...geometries.slice(1)); + let merged: Fog[] = []; for (let i = 0; i < union.length; i++) { - let holes = []; + let holes: Vector2[][] = []; if (union[i].length > 1) { for (let j = 1; j < union[i].length; j++) { holes.push(union[i][j].map(([x, y]) => ({ x, y }))); } } + // find the first visible shape + let visibleShape = shapes.find((shape) => ignoreHidden || shape.visible); + if (!visibleShape) { + // TODO: handle if visible shape not found + throw Error; + } merged.push({ // Use the data of the first visible shape as the merge - ...shapes.find((shape) => ignoreHidden || shape.visible), + ...visibleShape, id: `merged-${i}`, data: { points: union[i][0].map(([x, y]) => ({ x, y })), holes, }, + type: "fog" }); } return merged; @@ -263,7 +335,7 @@ export function mergeFogShapes(shapes, ignoreHidden = true) { * @param {boolean} maxPoints Max amount of points per shape to get bounds for * @returns {Vector2.BoundingBox[]} */ -export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) { +export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): BoundingBox[] { let boxes = []; for (let shape of shapes) { if (maxPoints > 0 && shape.data.points.length > maxPoints) { @@ -280,14 +352,26 @@ export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) { * @property {Vector2} end */ +// type Edge = { +// start: Vector2, +// end: Vector2 +// } + /** * @typedef Guide * @property {Vector2} start * @property {Vector2} end * @property {("horizontal"|"vertical")} orientation - * @property {number} + * @property {number} distance */ +type Guide = { + start: Vector2, + end: Vector2, + orientation: "horizontal" | "vertical", + distance: number +} + /** * @param {Vector2} brushPosition Brush position in pixels * @param {Vector2} grid @@ -299,14 +383,14 @@ export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) { * @returns {Guide[]} */ export function getGuidesFromGridCell( - brushPosition, - grid, - gridCellSize, - gridOffset, - gridCellOffset, - snappingSensitivity, - mapSize -) { + brushPosition: Vector2, + grid: Grid, + gridCellSize: Size, + gridOffset: Vector2, + gridCellOffset: Vector2, + snappingSensitivity: number, + mapSize: Vector2 +): Guide[] { let boundingBoxes = []; // Add map bounds boundingBoxes.push( @@ -366,11 +450,11 @@ export function getGuidesFromGridCell( * @returns {Guide[]} */ export function getGuidesFromBoundingBoxes( - brushPosition, - boundingBoxes, - gridCellSize, - snappingSensitivity -) { + brushPosition: Vector2, + boundingBoxes: BoundingBox[], + gridCellSize: Vector2, // TODO: check if this was meant to be of type Size + snappingSensitivity: number +): Guide[] { let horizontalEdges = []; let verticalEdges = []; for (let bounds of boundingBoxes) { @@ -400,7 +484,7 @@ export function getGuidesFromBoundingBoxes( end: { x: bounds.max.x, y: bounds.max.y }, }); } - let guides = []; + let guides: Guide[] = []; for (let edge of verticalEdges) { const distance = Math.abs(brushPosition.x - edge.start.x); if (distance / gridCellSize.x < snappingSensitivity) { @@ -421,8 +505,8 @@ export function getGuidesFromBoundingBoxes( * @param {Guide[]} guides * @returns {Guide[]} */ -export function findBestGuides(brushPosition, guides) { - let bestGuides = []; +export function findBestGuides(brushPosition: Vector2, guides: Guide[]): Guide[] { + let bestGuides: Guide[] = []; let verticalGuide = guides .filter((guide) => guide.orientation === "vertical") .sort((a, b) => a.distance - b.distance)[0]; diff --git a/src/helpers/grid.js b/src/helpers/grid.ts similarity index 84% rename from src/helpers/grid.js rename to src/helpers/grid.ts index 2b3d642..9ec8f40 100644 --- a/src/helpers/grid.js +++ b/src/helpers/grid.ts @@ -14,12 +14,22 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented"); * @property {Vector2} bottomRight Bottom right position of the inset */ +type GridInset = { + topLeft: Vector2, + bottomRight: Vector2 +} + /** * @typedef GridMeasurement * @property {("chebyshev"|"alternating"|"euclidean"|"manhattan")} type * @property {string} scale */ +type GridMeasurement ={ + type: ("chebyshev"|"alternating"|"euclidean"|"manhattan") + scale: string +} + /** * @typedef Grid * @property {GridInset} inset The inset of the grid from the map @@ -27,6 +37,12 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented"); * @property {("square"|"hexVertical"|"hexHorizontal")} type * @property {GridMeasurement} measurement */ +export type Grid = { + inset: GridInset, + size: Vector2, + type: ("square"|"hexVertical"|"hexHorizontal"), + measurement: GridMeasurement +} /** * Gets the size of a grid in pixels taking into account the inset @@ -35,7 +51,7 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented"); * @param {number} baseHeight Height of the grid in pixels before inset * @returns {Size} */ -export function getGridPixelSize(grid, baseWidth, baseHeight) { +export function getGridPixelSize(grid: Grid, baseWidth: number, baseHeight: number): Size { const width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth; const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight; return new Size(width, height); @@ -48,7 +64,7 @@ export function getGridPixelSize(grid, baseWidth, baseHeight) { * @param {number} gridHeight Height of the grid in pixels after inset * @returns {Size} */ -export function getCellPixelSize(grid, gridWidth, gridHeight) { +export function getCellPixelSize(grid: Grid, gridWidth: number, gridHeight: number): Size { switch (grid.type) { case "square": return new Size(gridWidth / grid.size.x, gridHeight / grid.size.y); @@ -72,7 +88,7 @@ export function getCellPixelSize(grid, gridWidth, gridHeight) { * @param {Size} cellSize Cell size in pixels * @returns {Vector2} */ -export function getCellLocation(grid, col, row, cellSize) { +export function getCellLocation(grid: Grid, col: number, row: number, cellSize: Size): Vector2 { switch (grid.type) { case "square": return { @@ -102,7 +118,7 @@ export function getCellLocation(grid, col, row, cellSize) { * @param {Size} cellSize Cell size in pixels * @returns {Vector2} */ -export function getNearestCellCoordinates(grid, x, y, cellSize) { +export function getNearestCellCoordinates(grid: Grid, x: number, y: number, cellSize: Size): Vector2 { switch (grid.type) { case "square": return Vector2.divide(Vector2.floorTo({ x, y }, cellSize), cellSize); @@ -132,7 +148,7 @@ export function getNearestCellCoordinates(grid, x, y, cellSize) { * @param {Size} cellSize Cell size in pixels * @returns {Vector2[]} */ -export function getCellCorners(grid, x, y, cellSize) { +export function getCellCorners(grid: Grid, x: number, y: number, cellSize: Size): Vector2[] { const position = new Vector2(x, y); switch (grid.type) { case "square": @@ -172,8 +188,9 @@ export function getCellCorners(grid, x, y, cellSize) { * Get the height of a grid based off of its width * @param {Grid} grid * @param {number} gridWidth Width of the grid in pixels after inset + * @returns {number} */ -function getGridHeightFromWidth(grid, gridWidth) { +function getGridHeightFromWidth(grid: Grid, gridWidth: number): number{ switch (grid.type) { case "square": return (grid.size.y * gridWidth) / grid.size.x; @@ -195,7 +212,7 @@ function getGridHeightFromWidth(grid, gridWidth) { * @param {number} mapHeight Height of the map in pixels before inset * @returns {GridInset} */ -export function getGridDefaultInset(grid, mapWidth, mapHeight) { +export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset { // Max the width of the inset and figure out the resulting height value const insetHeightNorm = getGridHeightFromWidth(grid, mapWidth) / mapHeight; return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: insetHeightNorm } }; @@ -208,7 +225,7 @@ export function getGridDefaultInset(grid, mapWidth, mapHeight) { * @param {number} mapHeight Height of the map in pixels before inset * @returns {GridInset} */ -export function getGridUpdatedInset(grid, mapWidth, mapHeight) { +export function getGridUpdatedInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset { let inset = grid.inset; // Take current inset width and use it to calculate the new height if (grid.size.x > 0 && grid.size.x > 0) { @@ -226,7 +243,7 @@ export function getGridUpdatedInset(grid, mapWidth, mapHeight) { * @param {Grid} grid * @returns {number} */ -export function getGridMaxZoom(grid) { +export function getGridMaxZoom(grid: Grid): number { if (!grid) { return 10; } @@ -240,7 +257,7 @@ export function getGridMaxZoom(grid) { * @param {("hexVertical"|"hexHorizontal")} type * @returns {Vector2} */ -export function hexCubeToOffset(cube, type) { +export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizontal")) { if (type === "hexVertical") { const x = cube.x + (cube.z + (cube.z & 1)) / 2; const y = cube.z; @@ -257,7 +274,7 @@ export function hexCubeToOffset(cube, type) { * @param {("hexVertical"|"hexHorizontal")} type * @returns {Vector3} */ -export function hexOffsetToCube(offset, type) { +export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizontal")) { if (type === "hexVertical") { const x = offset.x - (offset.y + (offset.y & 1)) / 2; const z = offset.y; @@ -276,8 +293,9 @@ export function hexOffsetToCube(offset, type) { * @param {Grid} grid * @param {Vector2} a * @param {Vector2} b + * @param {Size} cellSize */ -export function gridDistance(grid, a, b, cellSize) { +export function gridDistance(grid: Grid, a: Vector2, b: Vector2, cellSize: Size) { // Get grid coordinates const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize); const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize); @@ -290,8 +308,8 @@ export function gridDistance(grid, a, b, cellSize) { } else if (grid.measurement.type === "alternating") { // Alternating diagonal distance like D&D 3.5 and Pathfinder const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord)); - const max = Vector2.max(delta); - const min = Vector2.min(delta); + const max: any = Vector2.max(delta); + const min: any = Vector2.min(delta); return max - min + Math.floor(1.5 * min); } else if (grid.measurement.type === "euclidean") { return Vector2.distance(aCoord, bCoord); @@ -322,15 +340,25 @@ export function gridDistance(grid, a, b, cellSize) { * @property {number} digits The precision of the scale */ +type GridScale = { + multiplier: number, + unit: string, + digits: number +} + /** * Parse a string representation of scale e.g. 5ft into a `GridScale` * @param {string} scale * @returns {GridScale} */ -export function parseGridScale(scale) { +export function parseGridScale(scale: string): GridScale { if (typeof scale === "string") { const match = scale.match(/(\d*)(\.\d*)?([a-zA-Z]*)/); + // TODO: handle case where match is not found + if (!match) { + throw Error; + } const integer = parseFloat(match[1]); const fractional = parseFloat(match[2]); const unit = match[3] || ""; @@ -352,7 +380,7 @@ export function parseGridScale(scale) { * @param {number} n * @returns {number[]} */ -function factors(n) { +function factors(n: number): number[] { const numbers = Array.from(Array(n + 1), (_, i) => i); return numbers.filter((i) => n % i === 0); } @@ -364,7 +392,7 @@ function factors(n) { * @param {number} b * @returns {number} */ -function gcd(a, b) { +function gcd(a: number, b: number): number { while (b !== 0) { const t = b; b = a % b; @@ -379,7 +407,7 @@ function gcd(a, b) { * @param {number} b * @returns {number[]} */ -function dividers(a, b) { +function dividers(a: number, b: number): number[] { const d = gcd(a, b); return factors(d); } @@ -398,7 +426,7 @@ const maxGridSize = 200; * @param {number} y * @returns {boolean} */ -export function gridSizeVaild(x, y) { +export function gridSizeVaild(x: number, y: number): boolean { return ( x > minGridSize && y > minGridSize && x < maxGridSize && y < maxGridSize ); @@ -408,11 +436,12 @@ export function gridSizeVaild(x, y) { * Finds a grid size for an image by finding the closest size to the average grid size * @param {Image} image * @param {number[]} candidates - * @returns {Vector2} + * @returns {Vector2 | null} */ -function gridSizeHeuristic(image, candidates) { - const width = image.width; - const height = image.height; +function gridSizeHeuristic(image: CanvasImageSource, candidates: number[]): Vector2 | null { + // TODO: check type for Image and CanvasSourceImage + const width: any = image.width; + const height: any = image.height; // Find the best candidate by comparing the absolute z-scores of each axis let bestX = 1; let bestY = 1; @@ -440,17 +469,23 @@ function gridSizeHeuristic(image, candidates) { * Finds the grid size of an image by running the image through a machine learning model * @param {Image} image * @param {number[]} candidates - * @returns {Vector2} + * @returns {Vector2 | null} */ -async function gridSizeML(image, candidates) { - const width = image.width; - const height = image.height; +async function gridSizeML(image: CanvasImageSource, candidates: number[]): Promise { + // TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match + const width: any = image.width; + const height: any = image.height; const ratio = width / height; let canvas = document.createElement("canvas"); let context = canvas.getContext("2d"); canvas.width = 2048; canvas.height = Math.floor(2048 / ratio); + // TODO: handle if context is null + if (!context) { + return null; + } + context.drawImage(image, 0, 0, canvas.width, canvas.height); let imageData = context.getImageData( @@ -507,8 +542,10 @@ async function gridSizeML(image, candidates) { * @param {Image} image * @returns {Vector2} */ -export async function getGridSizeFromImage(image) { - const candidates = dividers(image.width, image.height); +export async function getGridSizeFromImage(image: CanvasImageSource) { + const width: any = image.width; + const height: any = image.height; + const candidates = dividers(width, height); let prediction; // Try and use ML grid detection diff --git a/src/helpers/image.js b/src/helpers/image.ts similarity index 73% rename from src/helpers/image.js rename to src/helpers/image.ts index fbcf7a8..c35b7c3 100644 --- a/src/helpers/image.js +++ b/src/helpers/image.ts @@ -6,13 +6,18 @@ const lightnessDetectionOffset = 0.1; * @param {HTMLImageElement} image * @returns {boolean} True is the image is light */ -export function getImageLightness(image) { +export function getImageLightness(image: HTMLImageElement) { const width = image.width; const height = image.height; let canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; let context = canvas.getContext("2d"); + if (!context) { + // TODO: handle if context is null + return; + } + context.drawImage(image, 0, 0); const imageData = context.getImageData(0, 0, width, height); @@ -44,13 +49,19 @@ export function getImageLightness(image) { * @property {number} height */ +type CanvasImage = { + blob: Blob | null, + width: number, + height: number +} + /** * @param {HTMLCanvasElement} canvas * @param {string} type * @param {number} quality * @returns {Promise} */ -export async function canvasToImage(canvas, type, quality) { +export async function canvasToImage(canvas: HTMLCanvasElement, type: string, quality: number): Promise { return new Promise((resolve) => { canvas.toBlob( (blob) => { @@ -69,7 +80,7 @@ export async function canvasToImage(canvas, type, quality) { * @param {number} quality if image is a jpeg or webp this is the quality setting * @returns {Promise} */ -export async function resizeImage(image, size, type, quality) { +export async function resizeImage(image: HTMLImageElement, size: number, type: string, quality: number): Promise { const width = image.width; const height = image.height; const ratio = width / height; @@ -82,8 +93,10 @@ export async function resizeImage(image, size, type, quality) { canvas.height = size; } let context = canvas.getContext("2d"); - context.drawImage(image, 0, 0, canvas.width, canvas.height); - + // TODO: Add error if context is empty + if (context) { + context.drawImage(image, 0, 0, canvas.width, canvas.height); + } return await canvasToImage(canvas, type, quality); } @@ -96,6 +109,13 @@ export async function resizeImage(image, size, type, quality) { * @property {string} id */ +type ImageFile = { + file: Uint8Array | null, + width: number, + height: number, + type: "file", + id: string +} /** * Create a image file with resolution `size`x`size` with cover cropping * @param {HTMLImageElement} image the image to resize @@ -104,7 +124,7 @@ export async function resizeImage(image, size, type, quality) { * @param {number} quality if image is a jpeg or webp this is the quality setting * @returns {Promise} */ -export async function createThumbnail(image, type, size = 300, quality = 0.5) { +export async function createThumbnail(image: HTMLImageElement, type: string, size = 300, quality = 0.5): Promise { let canvas = document.createElement("canvas"); canvas.width = size; canvas.height = size; @@ -113,31 +133,35 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) { if (ratio > 1) { const center = image.width / 2; const halfHeight = image.height / 2; - context.drawImage( - image, - center - halfHeight, - 0, - image.height, - image.height, - 0, - 0, - canvas.width, - canvas.height - ); + if (context) { + context.drawImage( + image, + center - halfHeight, + 0, + image.height, + image.height, + 0, + 0, + canvas.width, + canvas.height + ); + } } else { const center = image.height / 2; const halfWidth = image.width / 2; - context.drawImage( - image, - 0, - center - halfWidth, - image.width, - image.width, - 0, - 0, - canvas.width, - canvas.height - ); + if (context) { + context.drawImage( + image, + 0, + center - halfWidth, + image.width, + image.width, + 0, + 0, + canvas.width, + canvas.height + ); + } } const thumbnailImage = await canvasToImage(canvas, type, quality); diff --git a/src/helpers/konva.js b/src/helpers/konva.tsx similarity index 84% rename from src/helpers/konva.js rename to src/helpers/konva.tsx index e391a64..3dce716 100644 --- a/src/helpers/konva.js +++ b/src/helpers/konva.tsx @@ -6,9 +6,9 @@ import Color from "color"; import Vector2 from "./Vector2"; // Holes should be wound in the opposite direction as the containing points array -export function HoleyLine({ holes, ...props }) { +export function HoleyLine({ holes, ...props }: { holes: any, props: []}) { // Converted from https://github.com/rfestag/konva/blob/master/src/shapes/Line.ts - function drawLine(points, context, shape) { + function drawLine(points: number[], context: any, shape: any) { const length = points.length; const tension = shape.tension(); const closed = shape.closed(); @@ -76,7 +76,7 @@ export function HoleyLine({ holes, ...props }) { } // Draw points and holes - function sceneFunc(context, shape) { + function sceneFunc(context: any, shape: any) { const points = shape.points(); const closed = shape.closed(); @@ -109,7 +109,7 @@ export function HoleyLine({ holes, ...props }) { return ; } -export function Tick({ x, y, scale, onClick, cross }) { +export function Tick({ x, y, scale, onClick, cross }: { x: any, y: any, scale: any, onClick: any, cross: any}) { const [fill, setFill] = useState("white"); function handleEnter() { setFill("hsl(260, 100%, 80%)"); @@ -144,13 +144,17 @@ export function Tick({ x, y, scale, onClick, cross }) { ); } -export function Trail({ position, size, duration, segments, color }) { - const trailRef = useRef(); - const pointsRef = useRef([]); +interface TrailPoint extends Vector2 { + lifetime: number +} + +export function Trail({ position, size, duration, segments, color }: { position: Vector2, size: any, duration: number, segments: any, color: string }) { + const trailRef: React.MutableRefObject = useRef(); + const pointsRef: React.MutableRefObject = useRef([]); const prevPositionRef = useRef(position); const positionRef = useRef(position); - const circleRef = useRef(); - // Color of the end of the trial + const circleRef: React.MutableRefObject = useRef(); + // Color of the end of the trail const transparentColorRef = useRef( Color(color).lighten(0.5).alpha(0).string() ); @@ -178,7 +182,7 @@ export function Trail({ position, size, duration, segments, color }) { useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(animate); - function animate(time) { + function animate(time: any) { request = requestAnimationFrame(animate); const deltaTime = time - prevTime; prevTime = time; @@ -199,13 +203,13 @@ export function Trail({ position, size, duration, segments, color }) { } // Update the circle position to keep it in sync with the trail - if (circleRef.current) { + if (circleRef && circleRef.current) { circleRef.current.x(positionRef.current.x); circleRef.current.y(positionRef.current.y); } - if (trailRef.current) { - trailRef.current.getLayer().draw(); + if (trailRef && trailRef.current) { + trailRef.current.getLayer()?.draw(); } } @@ -215,14 +219,15 @@ export function Trail({ position, size, duration, segments, color }) { }, []); // Custom scene function for drawing a trail from a line - function sceneFunc(context) { + function sceneFunc(context: any) { // Resample points to ensure a smooth trail const resampledPoints = Vector2.resample(pointsRef.current, segments); if (resampledPoints.length === 0) { return; } // Draws a line offset in the direction perpendicular to its travel direction - const drawOffsetLine = (from, to, alpha) => { + // TODO: check alpha type + const drawOffsetLine = (from: Vector2, to: Vector2, alpha: number) => { const forward = Vector2.normalize(Vector2.subtract(from, to)); // Rotate the forward vector 90 degrees based off of the direction const side = Vector2.rotate90(forward); @@ -254,7 +259,7 @@ export function Trail({ position, size, duration, segments, color }) { // Create a radial gradient from the center of the trail to the tail const gradientCenter = resampledPoints[resampledPoints.length - 1]; const gradientEnd = resampledPoints[0]; - const gradientRadius = Vector2.length( + const gradientRadius = Vector2.setLength( Vector2.subtract(gradientCenter, gradientEnd) ); let gradient = context.createRadialGradient( @@ -297,15 +302,24 @@ Trail.defaultProps = { * @param {Konva.Node} node * @returns {Vector2} */ -export function getRelativePointerPosition(node) { +export function getRelativePointerPosition(node: Konva.Node): { x: number, y: number } | undefined { let transform = node.getAbsoluteTransform().copy(); transform.invert(); - let position = node.getStage().getPointerPosition(); + // TODO: handle possible null value + let position = node.getStage()?.getPointerPosition(); + if (!position) { + // TODO: handle possible null value + return; + } return transform.point(position); } -export function getRelativePointerPositionNormalized(node) { +export function getRelativePointerPositionNormalized(node: Konva.Node): { x: number, y: number } | undefined { const relativePosition = getRelativePointerPosition(node); + if (!relativePosition) { + // TODO: handle possible null value + return; + } return { x: relativePosition.x / node.width(), y: relativePosition.y / node.height(), @@ -317,8 +331,8 @@ export function getRelativePointerPositionNormalized(node) { * @param {number[]} points points in an x, y alternating array * @returns {Vector2[]} a `Vector2` array */ -export function convertPointArray(points) { - return points.reduce((acc, _, i, arr) => { +export function convertPointArray(points: number[]) { + return points.reduce((acc: any[], _, i, arr) => { if (i % 2 === 0) { acc.push({ x: arr[i], y: arr[i + 1] }); } diff --git a/src/helpers/logging.js b/src/helpers/logging.ts similarity index 78% rename from src/helpers/logging.js rename to src/helpers/logging.ts index e8be741..bb547fe 100644 --- a/src/helpers/logging.js +++ b/src/helpers/logging.ts @@ -1,6 +1,6 @@ import { captureException } from "@sentry/react"; -export function logError(error) { +export function logError(error: any): void { console.error(error); if (process.env.REACT_APP_LOGGING === "true") { captureException(error); diff --git a/src/helpers/monsters.js b/src/helpers/monsters.ts similarity index 98% rename from src/helpers/monsters.js rename to src/helpers/monsters.ts index cd64a1c..c07a82e 100644 --- a/src/helpers/monsters.js +++ b/src/helpers/monsters.ts @@ -1,4 +1,4 @@ -const monsters = [ +const monsters: string[] = [ "Aboleth", "Acolyte", "Black Dragon", @@ -295,6 +295,6 @@ const monsters = [ export default monsters; -export function getRandomMonster() { +export function getRandomMonster(): string { return monsters[Math.floor(Math.random() * monsters.length)]; } diff --git a/src/helpers/select.js b/src/helpers/select.tsx similarity index 60% rename from src/helpers/select.js rename to src/helpers/select.tsx index eb81b95..7ad1ef7 100644 --- a/src/helpers/select.js +++ b/src/helpers/select.tsx @@ -8,10 +8,20 @@ import { groupBy } from "./shared"; */ // Helper for generating search results for items -export function useSearch(items, search) { - const [filteredItems, setFilteredItems] = useState([]); - const [filteredItemScores, setFilteredItemScores] = useState({}); - const [fuse, setFuse] = useState(); +export function useSearch(items: [], search: string) { + // TODO: add types to search items -> don't like the never type + const [filteredItems, setFilteredItems]: [ + filteredItems: any, + setFilteredItems: any + ] = useState([]); + const [filteredItemScores, setFilteredItemScores]: [ + filteredItemScores: {}, + setFilteredItemScores: React.Dispatch> + ] = useState({}); + const [fuse, setFuse]: [ + fuse: Fuse | undefined, + setFuse: React.Dispatch | undefined> + ] = useState(); // Update search index when items change useEffect(() => { @@ -21,14 +31,15 @@ export function useSearch(items, search) { // Perform search when search changes useEffect(() => { if (search) { - const query = fuse.search(search); - setFilteredItems(query.map((result) => result.item)); - setFilteredItemScores( - query.reduce( - (acc, value) => ({ ...acc, [value.item.id]: value.score }), - {} - ) + const query = fuse?.search(search); + setFilteredItems(query?.map((result: any) => result.item)); + let reduceResult: {} | undefined = query?.reduce( + (acc: {}, value: any) => ({ ...acc, [value.item.id]: value.score }), + {} ); + if (reduceResult) { + setFilteredItemScores(reduceResult); + } } }, [search, items, fuse]); @@ -36,7 +47,12 @@ export function useSearch(items, search) { } // Helper for grouping items -export function useGroup(items, filteredItems, useFiltered, filteredScores) { +export function useGroup( + items: any[], + filteredItems: any[], + useFiltered: boolean, + filteredScores: any[] +) { const itemsByGroup = groupBy(useFiltered ? filteredItems : items, "group"); // Get the groups of the items sorting by the average score if we're filtering or the alphabetical order // with "" at the start and "default" at the end if not @@ -44,10 +60,10 @@ export function useGroup(items, filteredItems, useFiltered, filteredScores) { if (useFiltered) { itemGroups.sort((a, b) => { const aScore = itemsByGroup[a].reduce( - (acc, item) => (acc + filteredScores[item.id]) / 2 + (acc: any, item: any) => (acc + filteredScores[item.id]) / 2 ); const bScore = itemsByGroup[b].reduce( - (acc, item) => (acc + filteredScores[item.id]) / 2 + (acc: any, item: any) => (acc + filteredScores[item.id]) / 2 ); return aScore - bScore; }); @@ -67,12 +83,12 @@ export function useGroup(items, filteredItems, useFiltered, filteredScores) { // Helper for handling selecting items export function handleItemSelect( - item, - selectMode, - selectedIds, - setSelectedIds, - itemsByGroup, - itemGroups + item: any, + selectMode: any, + selectedIds: number[], + setSelectedIds: any, + itemsByGroup: any, + itemGroups: any ) { if (!item) { setSelectedIds([]); @@ -83,9 +99,9 @@ export function handleItemSelect( setSelectedIds([item.id]); break; case "multiple": - setSelectedIds((prev) => { + setSelectedIds((prev: any[]) => { if (prev.includes(item.id)) { - return prev.filter((id) => id !== item.id); + return prev.filter((id: number) => id !== item.id); } else { return [...prev, item.id]; } @@ -94,32 +110,32 @@ export function handleItemSelect( case "range": // Create items array let items = itemGroups.reduce( - (acc, group) => [...acc, ...itemsByGroup[group]], + (acc: [], group: any) => [...acc, ...itemsByGroup[group]], [] ); // Add all items inbetween the previous selected item and the current selected if (selectedIds.length > 0) { - const mapIndex = items.findIndex((m) => m.id === item.id); + const mapIndex = items.findIndex((m: any) => m.id === item.id); const lastIndex = items.findIndex( - (m) => m.id === selectedIds[selectedIds.length - 1] + (m: any) => m.id === selectedIds[selectedIds.length - 1] ); - let idsToAdd = []; - let idsToRemove = []; + let idsToAdd: number[] = []; + let idsToRemove: number[] = []; const direction = mapIndex > lastIndex ? 1 : -1; for ( let i = lastIndex + direction; direction < 0 ? i >= mapIndex : i <= mapIndex; i += direction ) { - const itemId = items[i].id; + const itemId: number = items[i].id; if (selectedIds.includes(itemId)) { idsToRemove.push(itemId); } else { idsToAdd.push(itemId); } } - setSelectedIds((prev) => { + setSelectedIds((prev: any[]) => { let ids = [...prev, ...idsToAdd]; return ids.filter((id) => !idsToRemove.includes(id)); }); diff --git a/src/helpers/shared.js b/src/helpers/shared.ts similarity index 54% rename from src/helpers/shared.js rename to src/helpers/shared.ts index a9ed206..d1e7913 100644 --- a/src/helpers/shared.js +++ b/src/helpers/shared.ts @@ -1,5 +1,5 @@ -export function omit(obj, keys) { - let tmp = {}; +export function omit(obj:object, keys: string[]) { + let tmp: { [key: string]: any } = {}; for (let [key, value] of Object.entries(obj)) { if (keys.includes(key)) { continue; @@ -9,7 +9,7 @@ export function omit(obj, keys) { return tmp; } -export function fromEntries(iterable) { +export function fromEntries(iterable: any) { if (Object.fromEntries) { return Object.fromEntries(iterable); } @@ -20,32 +20,32 @@ export function fromEntries(iterable) { } // Check to see if all tracks are muted -export function isStreamStopped(stream) { - return stream.getTracks().reduce((a, b) => a && b, { mute: true }); +export function isStreamStopped(stream: any) { + return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true }); } -export function roundTo(x, to) { +export function roundTo(x: number, to: number): number { return Math.round(x / to) * to; } -export function floorTo(x, to) { +export function floorTo(x: number, to: number): number { return Math.floor(x / to) * to; } -export function toRadians(angle) { +export function toRadians(angle: number): number { return angle * (Math.PI / 180); } -export function toDegrees(angle) { +export function toDegrees(angle: number): number { return angle * (180 / Math.PI); } -export function lerp(a, b, alpha) { +export function lerp(a: number, b: number, alpha: number): number { return a * (1 - alpha) + b * alpha; } // Console log an image -export function logImage(url, width, height) { +export function logImage(url: string, width: number, height: number): void { const style = [ "font-size: 1px;", `padding: ${height}px ${width}px;`, @@ -55,19 +55,19 @@ export function logImage(url, width, height) { console.log("%c ", style); } -export function isEmpty(obj) { +export function isEmpty(obj: any): boolean { return Object.keys(obj).length === 0 && obj.constructor === Object; } -export function keyBy(array, key) { +export function keyBy(array: any, key: any) { return array.reduce( - (prev, current) => ({ ...prev, [key ? current[key] : current]: current }), + (prev: any, current: any) => ({ ...prev, [key ? current[key] : current]: current }), {} ); } -export function groupBy(array, key) { - return array.reduce((prev, current) => { +export function groupBy(array: any, key: string) { + return array.reduce((prev: any, current: any) => { const k = current[key]; (prev[k] || (prev[k] = [])).push(current); return prev; diff --git a/src/helpers/timer.js b/src/helpers/timer.ts similarity index 70% rename from src/helpers/timer.js rename to src/helpers/timer.ts index bcbe425..9c014d1 100644 --- a/src/helpers/timer.js +++ b/src/helpers/timer.ts @@ -3,10 +3,22 @@ const MILLISECONDS_IN_MINUTE = 60000; const MILLISECONDS_IN_SECOND = 1000; /** - * Returns a timers duration in milliseconds - * @param {Object} t The object with an hour, minute and second property + * @typedef Time + * @property {number} hour + * @property {number} minute + * @property {number} second */ -export function getHMSDuration(t) { +type Time = { + hour: number, + minute: number, + second: number +} + +/** + * Returns a timers duration in milliseconds + * @param {Time} t The object with an hour, minute and second property + */ +export function getHMSDuration(t: Time) { if (!t) { return 0; } @@ -21,7 +33,7 @@ export function getHMSDuration(t) { * Returns an object with an hour, minute and second property * @param {number} duration The duration in milliseconds */ -export function getDurationHMS(duration) { +export function getDurationHMS(duration: number) { let workingDuration = duration; const hour = Math.floor(workingDuration / MILLISECONDS_IN_HOUR); workingDuration -= hour * MILLISECONDS_IN_HOUR; diff --git a/src/index.js b/src/index.tsx similarity index 99% rename from src/index.js rename to src/index.tsx index d21cd5b..0342c95 100644 --- a/src/index.js +++ b/src/index.tsx @@ -1,4 +1,3 @@ -import React from "react"; import ReactDOM from "react-dom"; import * as Sentry from "@sentry/react"; import App from "./App"; diff --git a/src/network/Connection.js b/src/network/Connection.ts similarity index 87% rename from src/network/Connection.js rename to src/network/Connection.ts index 4871094..dd2f3ea 100644 --- a/src/network/Connection.js +++ b/src/network/Connection.ts @@ -9,23 +9,26 @@ import blobToBuffer from "../helpers/blobToBuffer"; const MAX_BUFFER_SIZE = 16000; class Connection extends SimplePeer { - constructor(props) { + currentChunks: any; + dataChannels: any; + + constructor(props: any) { super(props); - this.currentChunks = {}; + this.currentChunks = {} as Blob; this.dataChannels = {}; this.on("data", this.handleData); this.on("datachannel", this.handleDataChannel); } // Intercept the data event with decoding and chunking support - handleData(packed) { - const unpacked = decode(packed); + handleData(packed: any) { + const unpacked: any = decode(packed); // If the special property __chunked is set and true // The data is a partial chunk of the a larger file // So wait until all chunks are collected and assembled // before emitting the dataComplete event if (unpacked.__chunked) { - let chunk = this.currentChunks[unpacked.id] || { + let chunk: any = this.currentChunks[unpacked.id] || { data: [], count: 0, total: unpacked.total, @@ -57,7 +60,7 @@ class Connection extends SimplePeer { // Custom send function with encoding, chunking and data channel support // Uses `write` to send the data to allow for buffer / backpressure handling - sendObject(object, channel) { + sendObject(object: any, channel: any) { try { const packedData = encode(object); if (packedData.byteLength > MAX_BUFFER_SIZE) { @@ -84,23 +87,25 @@ class Connection extends SimplePeer { // Override the create data channel function to store our own named reference to it // and to use our custom data handler - createDataChannel(channelName, channelConfig, opts) { + createDataChannel(channelName: string, channelConfig: any, opts: any) { + // TODO: resolve createDataChannel + // @ts-ignore const channel = super.createDataChannel(channelName, channelConfig, opts); this.handleDataChannel(channel); return channel; } - handleDataChannel(channel) { + handleDataChannel(channel: any) { const channelName = channel.channelName; this.dataChannels[channelName] = channel; channel.on("data", this.handleData.bind(this)); - channel.on("error", (error) => { + channel.on("error", (error: any) => { this.emit("error", error); }); } // Converted from https://github.com/peers/peerjs/ - chunk(data) { + chunk(data: any) { const chunks = []; const size = data.byteLength; const total = Math.ceil(size / MAX_BUFFER_SIZE); diff --git a/src/network/Session.js b/src/network/Session.ts similarity index 78% rename from src/network/Session.js rename to src/network/Session.ts index b2973cc..abe12c4 100644 --- a/src/network/Session.js +++ b/src/network/Session.ts @@ -1,4 +1,4 @@ -import io from "socket.io-client"; +import io, { Socket } from "socket.io-client"; import msgParser from "socket.io-msgpack-parser"; import { EventEmitter } from "events"; @@ -6,6 +6,7 @@ import Connection from "./Connection"; import { omit } from "../helpers/shared"; import { logError } from "../helpers/logging"; +import { SimplePeerData } from "simple-peer"; /** * @typedef {object} SessionPeer @@ -14,6 +15,12 @@ import { logError } from "../helpers/logging"; * @property {boolean} initiator - Is this peer the initiator of the connection * @property {boolean} ready - Ready for data to be sent */ +type SessionPeer = { + id: string; + connection: Connection; + initiator: boolean; + ready: boolean; +}; /** * @callback peerReply @@ -22,6 +29,8 @@ import { logError } from "../helpers/logging"; * @param {string} channel - The channel to send to */ +type peerReply = (id: string, data: SimplePeerData, channel: string) => void; + /** * Session Status Event - Status of the session has changed * @@ -50,24 +59,24 @@ class Session extends EventEmitter { * * @type {io.Socket} */ - socket; + socket: Socket = io(); /** * A mapping of socket ids to session peers * * @type {Object.} */ - peers; + peers: Record; get id() { return this.socket && this.socket.id; } - _iceServers; + _iceServers: string[] = []; // Store party id and password for reconnect - _gameId; - _password; + _gameId: string = ""; + _password: string = ""; constructor() { super(); @@ -81,6 +90,9 @@ class Session extends EventEmitter { */ async connect() { try { + if (!process.env.REACT_APP_ICE_SERVERS_URL) { + return; + } const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL); if (!response.ok) { throw Error("Unable to fetch ICE servers"); @@ -88,6 +100,9 @@ class Session extends EventEmitter { const data = await response.json(); this._iceServers = data.iceServers; + if (!process.env.REACT_APP_BROKER_URL) { + return; + } this.socket = io(process.env.REACT_APP_BROKER_URL, { withCredentials: true, parser: msgParser, @@ -122,7 +137,7 @@ class Session extends EventEmitter { * @param {object} data * @param {string} channel */ - sendTo(sessionId, eventId, data, channel) { + sendTo(sessionId: string, eventId: string, data: SimplePeerData, channel: string) { if (!(sessionId in this.peers)) { if (!this._addPeer(sessionId, true)) { return; @@ -151,7 +166,11 @@ class Session extends EventEmitter { * @param {MediaStreamTrack} track * @param {MediaStream} stream */ - startStreamTo(sessionId, track, stream) { + startStreamTo( + sessionId: string, + track: MediaStreamTrack, + stream: MediaStream + ) { if (!(sessionId in this.peers)) { if (!this._addPeer(sessionId, true)) { return; @@ -174,7 +193,7 @@ class Session extends EventEmitter { * @param {MediaStreamTrack} track * @param {MediaStream} stream */ - endStreamTo(sessionId, track, stream) { + endStreamTo(sessionId: string, track: MediaStreamTrack, stream: MediaStream) { if (sessionId in this.peers) { this.peers[sessionId].connection.removeTrack(track, stream); } @@ -186,7 +205,7 @@ class Session extends EventEmitter { * @param {string} gameId - the id of the party to join * @param {string} password - the password of the party */ - async joinGame(gameId, password) { + async joinGame(gameId: string, password: string) { if (typeof gameId !== "string" || typeof password !== "string") { console.error( "Unable to join game: invalid game ID or password", @@ -198,7 +217,12 @@ class Session extends EventEmitter { this._gameId = gameId; this._password = password; - this.socket.emit("join_game", gameId, password, process.env.REACT_APP_VERSION); + this.socket.emit( + "join_game", + gameId, + password, + process.env.REACT_APP_VERSION + ); this.emit("status", "joining"); } @@ -208,7 +232,7 @@ class Session extends EventEmitter { * @param {boolean} initiator * @returns {boolean} True if peer was added successfully */ - _addPeer(id, initiator) { + _addPeer(id: string, initiator: boolean) { try { const connection = new Connection({ initiator, @@ -221,15 +245,15 @@ class Session extends EventEmitter { const peer = { id, connection, initiator, ready: false }; - function sendPeer(id, data, channel) { + const sendPeer = (id: string, data: SimplePeerData, channel: any) => { peer.connection.sendObject({ id, data }, channel); - } + }; - function handleSignal(signal) { + const handleSignal = (signal: any) => { this.socket.emit("signal", JSON.stringify({ to: peer.id, signal })); - } + }; - function handleConnect() { + const handleConnect = () => { if (peer.id in this.peers) { this.peers[peer.id].ready = true; } @@ -241,10 +265,14 @@ class Session extends EventEmitter { * @property {SessionPeer} peer * @property {peerReply} reply */ - this.emit("peerConnect", { peer, reply: sendPeer }); - } + const peerConnectEvent: { peer: SessionPeer; reply: peerReply } = { + peer, + reply: sendPeer, + }; + this.emit("peerConnect", peerConnectEvent); + }; - function handleDataComplete(data) { + const handleDataComplete = (data: any) => { /** * Peer Data Event - Data received by a peer * @@ -255,15 +283,30 @@ class Session extends EventEmitter { * @property {object} data * @property {peerReply} reply */ - this.emit("peerData", { + let peerDataEvent: { + peer: SessionPeer; + id: string; + data: any; + reply: peerReply; + } = { peer, id: data.id, data: data.data, reply: sendPeer, - }); - } + }; + console.log(`Data: ${JSON.stringify(data)}`) + this.emit("peerData", peerDataEvent); + }; - function handleDataProgress({ id, count, total }) { + const handleDataProgress = ({ + id, + count, + total, + }: { + id: string; + count: number; + total: number; + }) => { this.emit("peerDataProgress", { peer, id, @@ -271,9 +314,9 @@ class Session extends EventEmitter { total, reply: sendPeer, }); - } + }; - function handleTrack(track, stream) { + const handleTrack = (track: MediaStreamTrack, stream: MediaStream) => { /** * Peer Track Added Event - A `MediaStreamTrack` was added by a peer * @@ -283,7 +326,12 @@ class Session extends EventEmitter { * @property {MediaStreamTrack} track * @property {MediaStream} stream */ - this.emit("peerTrackAdded", { peer, track, stream }); + let peerTrackAddedEvent: { + peer: SessionPeer; + track: MediaStreamTrack; + stream: MediaStream; + } = { peer, track, stream }; + this.emit("peerTrackAdded", peerTrackAddedEvent); track.addEventListener("mute", () => { /** * Peer Track Removed Event - A `MediaStreamTrack` was removed by a peer @@ -294,11 +342,16 @@ class Session extends EventEmitter { * @property {MediaStreamTrack} track * @property {MediaStream} stream */ - this.emit("peerTrackRemoved", { peer, track, stream }); + let peerTrackRemovedEvent: { + peer: SessionPeer; + track: MediaStreamTrack; + stream: MediaStream; + } = { peer, track, stream }; + this.emit("peerTrackRemoved", peerTrackRemovedEvent); }); - } + }; - function handleClose() { + const handleClose = () => { /** * Peer Disconnect Event - A peer has disconnected * @@ -306,14 +359,15 @@ class Session extends EventEmitter { * @type {object} * @property {SessionPeer} peer */ - this.emit("peerDisconnect", { peer }); + let peerDisconnectEvent: { peer: SessionPeer } = { peer }; + this.emit("peerDisconnect", peerDisconnectEvent); if (peer.id in this.peers) { peer.connection.destroy(); this.peers = omit(this.peers, [peer.id]); } - } + }; - function handleError(error) { + const handleError = (error: Error) => { /** * Peer Error Event - An error occured with a peer connection * @@ -322,12 +376,16 @@ class Session extends EventEmitter { * @property {SessionPeer} peer * @property {Error} error */ - this.emit("peerError", { peer, error }); + let peerErrorEvent: { peer: SessionPeer; error: Error } = { + peer, + error, + }; + this.emit("peerError", peerErrorEvent); if (peer.id in this.peers) { peer.connection.destroy(); this.peers = omit(this.peers, [peer.id]); } - } + }; peer.connection.on("signal", handleSignal.bind(this)); peer.connection.on("connect", handleConnect.bind(this)); @@ -363,7 +421,7 @@ class Session extends EventEmitter { this.emit("gameExpired"); } - _handlePlayerJoined(id) { + _handlePlayerJoined(id: string) { /** * Player Joined Event - A player has joined the game * @@ -373,7 +431,7 @@ class Session extends EventEmitter { this.emit("playerJoined", id); } - _handlePlayerLeft(id) { + _handlePlayerLeft(id: string) { /** * Player Left Event - A player has left the game * @@ -387,7 +445,7 @@ class Session extends EventEmitter { } } - _handleSignal(data) { + _handleSignal(data: any) { const { from, signal } = data; if (!(from in this.peers)) { if (!this._addPeer(from, false)) { diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/serviceWorker.js b/src/serviceWorker.ts similarity index 92% rename from src/serviceWorker.js rename to src/serviceWorker.ts index c4838eb..0d79aeb 100644 --- a/src/serviceWorker.js +++ b/src/serviceWorker.ts @@ -20,9 +20,13 @@ const isLocalhost = Boolean( ) ); -export function register(config) { +export function register(config: any) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. + if (!process.env.PUBLIC_URL) { + // TODO: handle is PUBLIC_URL has not been set + return; + } const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin @@ -54,9 +58,9 @@ export function register(config) { } } -function registerValidSW(swUrl, config) { +function registerValidSW(swUrl: string | URL, config: any) { navigator.serviceWorker - .register(swUrl) + .register(swUrl as string) .then(registration => { registration.onupdatefound = () => { const installingWorker = registration.installing; @@ -98,7 +102,8 @@ function registerValidSW(swUrl, config) { }); } -function checkValidServiceWorker(swUrl, config) { +// TODO: handle swUrl -> type has to be handled as RequestInfo OR string | URL +function checkValidServiceWorker(swUrl: any, config: any) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl, { headers: { 'Service-Worker': 'script' } diff --git a/src/settings.js b/src/settings.ts similarity index 84% rename from src/settings.js rename to src/settings.ts index 9264056..5d67c32 100644 --- a/src/settings.js +++ b/src/settings.ts @@ -1,6 +1,6 @@ import Settings from "./helpers/Settings"; -function loadVersions(settings) { +function loadVersions(settings: Settings) { settings.version(1, () => ({ fog: { type: "polygon", @@ -28,17 +28,17 @@ function loadVersions(settings) { }, })); // v1.5.2 - Added full screen support for map and label size - settings.version(2, (prev) => ({ + settings.version(2, (prev: any) => ({ ...prev, map: { fullScreen: false, labelSize: 1 }, })); // v1.7.0 - Added game password - settings.version(3, (prev) => ({ + settings.version(3, (prev: any) => ({ ...prev, game: { usePassword: true }, })); // v1.8.0 - Added pointer color, grid snapping sensitivity and remove measure - settings.version(4, (prev) => { + settings.version(4, (prev: any) => { let newSettings = { ...prev, pointer: { color: "red" }, @@ -48,19 +48,19 @@ function loadVersions(settings) { return newSettings; }); // v1.8.0 - Removed edge snapping for multilayer - settings.version(5, (prev) => { + settings.version(5, (prev: any) => { let newSettings = { ...prev }; delete newSettings.fog.useEdgeSnapping; newSettings.fog.multilayer = false; return newSettings; }); // v1.8.1 - Add show guides toggle - settings.version(6, (prev) => ({ + settings.version(6, (prev: any) => ({ ...prev, fog: { ...prev.fog, showGuides: true }, })); // v1.8.1 - Add fog edit opacity - settings.version(7, (prev) => ({ + settings.version(7, (prev: any) => ({ ...prev, fog: { ...prev.fog, editOpacity: 0.5 }, })); diff --git a/src/shortcuts.js b/src/shortcuts.js deleted file mode 100644 index 35eea4a..0000000 --- a/src/shortcuts.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @param {KeyboardEvent} event - * @returns {boolean} - */ -function hasModifier(event) { - return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; -} - -/** - * Key press without any modifiers and ignoring capitals - * @param {KeyboardEvent} event - * @param {string} key - * @returns {boolean} - */ -function singleKey(event, key) { - return ( - !hasModifier(event) && - (event.key === key || event.key === key.toUpperCase()) - ); -} - -/** - * @param {Keyboard} event - */ -function undo(event) { - const { key, ctrlKey, metaKey, shiftKey } = event; - return (key === "z" || key === "Z") && (ctrlKey || metaKey) && !shiftKey; -} - -/** - * @param {Keyboard} event - */ -function redo(event) { - const { key, ctrlKey, metaKey, shiftKey } = event; - return (key === "z" || key === "Z") && (ctrlKey || metaKey) && shiftKey; -} - -/** - * @param {Keyboard} event - */ -function zoomIn(event) { - const { key, ctrlKey, metaKey } = event; - return (key === "=" || key === "+") && !ctrlKey && !metaKey; -} - -/** - * @param {Keyboard} event - */ -function zoomOut(event) { - const { key, ctrlKey, metaKey } = event; - return (key === "-" || key === "_") && !ctrlKey && !metaKey; -} - -/** - * @callback shortcut - * @param {KeyboardEvent} event - * @returns {boolean} - */ - -/** - * @type {Object.} - */ -const shortcuts = { - // Tools - move: (event) => singleKey(event, " "), - moveTool: (event) => singleKey(event, "w"), - drawingTool: (event) => singleKey(event, "d"), - fogTool: (event) => singleKey(event, "f"), - measureTool: (event) => singleKey(event, "m"), - pointerTool: (event) => singleKey(event, "q"), - noteTool: (event) => singleKey(event, "n"), - // Map editor - gridNudgeUp: ({ key }) => key === "ArrowUp", - gridNudgeLeft: ({ key }) => key === "ArrowLeft", - gridNudgeRight: ({ key }) => key === "ArrowRight", - gridNudgeDown: ({ key }) => key === "ArrowDown", - // Drawing tool - drawBrush: (event) => singleKey(event, "b"), - drawPaint: (event) => singleKey(event, "p"), - drawLine: (event) => singleKey(event, "l"), - drawRect: (event) => singleKey(event, "r"), - drawCircle: (event) => singleKey(event, "c"), - drawTriangle: (event) => singleKey(event, "t"), - drawErase: (event) => singleKey(event, "e"), - drawBlend: (event) => singleKey(event, "o"), - // Fog tool - fogPolygon: (event) => singleKey(event, "p"), - fogRectangle: (event) => singleKey(event, "r"), - fogBrush: (event) => singleKey(event, "b"), - fogToggle: (event) => singleKey(event, "t"), - fogErase: (event) => singleKey(event, "e"), - fogLayer: (event) => singleKey(event, "l"), - fogPreview: (event) => singleKey(event, "f"), - fogCut: (event) => singleKey(event, "c"), - fogFinishPolygon: ({ key }) => key === "Enter", - fogCancelPolygon: ({ key }) => key === "Escape", - // Stage interaction - stageZoomIn: zoomIn, - stageZoomOut: zoomOut, - stagePrecisionZoom: ({ key }) => key === "Shift", - // Select - selectRange: ({ key }) => key === "Shift", - selectMultiple: ({ key }) => key === "Control" || key === "Meta", - // Common - undo, - redo, - delete: ({ key }) => key === "Backspace" || key === "Delete", -}; - -export default shortcuts; diff --git a/src/shortcuts.ts b/src/shortcuts.ts new file mode 100644 index 0000000..fe4947a --- /dev/null +++ b/src/shortcuts.ts @@ -0,0 +1,114 @@ +/** + * @param {KeyboardEvent} event + * @returns {boolean} + */ +function hasModifier(event: KeyboardEvent): boolean { + return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; +} + +/** + * Key press without any modifiers and ignoring capitals + * @param {KeyboardEvent} event + * @param {string} key + * @returns {boolean} + */ +function singleKey(event: KeyboardEvent, key: string): boolean { + return ( + !hasModifier(event) && + (event.key === key || event.key === key.toUpperCase()) + ); +} + +/** + * @param {Keyboard} event + * @returns {string | boolean} + */ +function undo(event: KeyboardEvent): string | boolean { + const { key, ctrlKey, metaKey, shiftKey } = event; + return (key === "z" || key === "Z") && (ctrlKey || metaKey) && !shiftKey; +} + +/** + * @param {Keyboard} event + * @returns {string | boolean} + */ +function redo(event: KeyboardEvent): string | boolean { + const { key, ctrlKey, metaKey, shiftKey } = event; + return (key === "z" || key === "Z") && (ctrlKey || metaKey) && shiftKey; +} + +/** + * @param {Keyboard} event + * @returns {string | boolean} + */ +function zoomIn(event: KeyboardEvent): string | boolean { + const { key, ctrlKey, metaKey } = event; + return (key === "=" || key === "+") && !ctrlKey && !metaKey; +} + +/** + * @param {Keyboard} event + * @returns {string | boolean} + */ +function zoomOut(event: KeyboardEvent): string | boolean { + const { key, ctrlKey, metaKey } = event; + return (key === "-" || key === "_") && !ctrlKey && !metaKey; +} + +/** + * @callback shortcut + * @param {KeyboardEvent} event + * @returns {boolean} + */ + +/** + * @type {Object.} + */ +const shortcuts = { + // Tools + move: (event: KeyboardEvent) => singleKey(event, " "), + moveTool: (event: KeyboardEvent) => singleKey(event, "w"), + drawingTool: (event: KeyboardEvent) => singleKey(event, "d"), + fogTool: (event: KeyboardEvent) => singleKey(event, "f"), + measureTool: (event: KeyboardEvent) => singleKey(event, "m"), + pointerTool: (event: KeyboardEvent) => singleKey(event, "q"), + noteTool: (event: KeyboardEvent) => singleKey(event, "n"), + // Map editor + gridNudgeUp: ({ key }: { key: string}) => key === "ArrowUp", + gridNudgeLeft: ({ key }: { key: string }) => key === "ArrowLeft", + gridNudgeRight: ({ key }: { key: string }) => key === "ArrowRight", + gridNudgeDown: ({ key }: { key: string }) => key === "ArrowDown", + // Drawing tool + drawBrush: (event: KeyboardEvent) => singleKey(event, "b"), + drawPaint: (event: KeyboardEvent) => singleKey(event, "p"), + drawLine: (event: KeyboardEvent) => singleKey(event, "l"), + drawRect: (event: KeyboardEvent) => singleKey(event, "r"), + drawCircle: (event: KeyboardEvent) => singleKey(event, "c"), + drawTriangle: (event: KeyboardEvent) => singleKey(event, "t"), + drawErase: (event: KeyboardEvent) => singleKey(event, "e"), + drawBlend: (event: KeyboardEvent) => singleKey(event, "o"), + // Fog tool + fogPolygon: (event: KeyboardEvent) => singleKey(event, "p"), + fogRectangle: (event: KeyboardEvent) => singleKey(event, "r"), + fogBrush: (event: KeyboardEvent) => singleKey(event, "b"), + fogToggle: (event: KeyboardEvent) => singleKey(event, "t"), + fogErase: (event: KeyboardEvent) => singleKey(event, "e"), + fogLayer: (event: KeyboardEvent) => singleKey(event, "l"), + fogPreview: (event: KeyboardEvent) => singleKey(event, "f"), + fogCut: (event: KeyboardEvent) => singleKey(event, "c"), + fogFinishPolygon: ({ key }: { key: string }) => key === "Enter", + fogCancelPolygon: ({ key }: { key: string }) => key === "Escape", + // Stage interaction + stageZoomIn: zoomIn, + stageZoomOut: zoomOut, + stagePrecisionZoom: ({ key }: { key: string }) => key === "Shift", + // Select + selectRange: ({ key }: { key: string }) => key === "Shift", + selectMultiple: ({ key }: { key: string }) => key === "Control" || key === "Meta", + // Common + undo, + redo, + delete: ({ key }: { key: string }) => key === "Backspace" || key === "Delete", +}; + +export default shortcuts; diff --git a/src/theme.js b/src/theme.ts similarity index 100% rename from src/theme.js rename to src/theme.ts From 6f28db078968ea222116964212c11fcbfdf662fc Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Thu, 27 May 2021 16:10:54 +1000 Subject: [PATCH 003/142] Converted icons to typescript --- src/icons/{AddIcon.js => AddIcon.tsx} | 4 +--- src/icons/{AddPartyMemberIcon.js => AddPartyMemberIcon.tsx} | 2 -- src/icons/{BlendOffIcon.js => BlendOffIcon.tsx} | 2 -- src/icons/{BlendOnIcon.js => BlendOnIcon.tsx} | 2 -- src/icons/{BrushCircleIcon.js => BrushCircleIcon.tsx} | 2 -- src/icons/{BrushLineIcon.js => BrushLineIcon.tsx} | 2 -- src/icons/{BrushPaintIcon.js => BrushPaintIcon.tsx} | 2 -- src/icons/{BrushRectangleIcon.js => BrushRectangleIcon.tsx} | 2 -- src/icons/{BrushStrokeIcon.js => BrushStrokeIcon.tsx} | 2 -- src/icons/{BrushToolIcon.js => BrushToolIcon.tsx} | 2 -- src/icons/{BrushTriangleIcon.js => BrushTriangleIcon.tsx} | 2 -- src/icons/{ChangeNicknameIcon.js => ChangeNicknameIcon.tsx} | 2 -- src/icons/{ClearDiceIcon.js => ClearDiceIcon.tsx} | 2 -- src/icons/{D100Icon.js => D100Icon.tsx} | 2 -- src/icons/{D10Icon.js => D10Icon.tsx} | 2 -- src/icons/{D12Icon.js => D12Icon.tsx} | 2 -- src/icons/{D20Icon.js => D20Icon.tsx} | 2 -- src/icons/{D4Icon.js => D4Icon.tsx} | 2 -- src/icons/{D6Icon.js => D6Icon.tsx} | 2 -- src/icons/{D8Icon.js => D8Icon.tsx} | 2 -- src/icons/{DiceRollsIcon.js => DiceRollsIcon.tsx} | 2 -- src/icons/{DonateIcon.js => DonateIcon.tsx} | 2 -- src/icons/{EditTileIcon.js => EditTileIcon.tsx} | 2 -- src/icons/{EraseAllIcon.js => EraseAllIcon.tsx} | 2 -- src/icons/{EraseToolIcon.js => EraseToolIcon.tsx} | 2 -- src/icons/{ExpandMoreDiceIcon.js => ExpandMoreDiceIcon.tsx} | 4 +--- .../{ExpandMoreDiceTrayIcon.js => ExpandMoreDiceTrayIcon.tsx} | 2 -- src/icons/{ExpandMoreDotIcon.js => ExpandMoreDotIcon.tsx} | 2 -- src/icons/{ExpandMoreIcon.js => ExpandMoreIcon.tsx} | 2 -- src/icons/{FogBrushIcon.js => FogBrushIcon.tsx} | 2 -- src/icons/{FogCutOffIcon.js => FogCutOffIcon.tsx} | 2 -- src/icons/{FogCutOnIcon.js => FogCutOnIcon.tsx} | 2 -- .../{FogMultilayerOffIcon.js => FogMultilayerOffIcon.tsx} | 2 -- src/icons/{FogMultilayerOnIcon.js => FogMultilayerOnIcon.tsx} | 2 -- src/icons/{FogPolygonIcon.js => FogPolygonIcon.tsx} | 2 -- src/icons/{FogPreviewOffIcon.js => FogPreviewOffIcon.tsx} | 2 -- src/icons/{FogPreviewOnIcon.js => FogPreviewOnIcon.tsx} | 2 -- src/icons/{FogRectangleIcon.js => FogRectangleIcon.tsx} | 2 -- src/icons/{FogRemoveIcon.js => FogRemoveIcon.tsx} | 2 -- src/icons/{FogToggleIcon.js => FogToggleIcon.tsx} | 2 -- src/icons/{FogToolIcon.js => FogToolIcon.tsx} | 2 -- src/icons/{FullScreenExitIcon.js => FullScreenExitIcon.tsx} | 2 -- src/icons/{FullScreenIcon.js => FullScreenIcon.tsx} | 2 -- src/icons/{GestureOffIcon.js => GestureOffIcon.tsx} | 2 -- src/icons/{GestureOnIcon.js => GestureOnIcon.tsx} | 2 -- src/icons/{GridOffIcon.js => GridOffIcon.tsx} | 2 -- src/icons/{GridOnIcon.js => GridOnIcon.tsx} | 2 -- src/icons/{GroupIcon.js => GroupIcon.tsx} | 2 -- src/icons/{HelpIcon.js => HelpIcon.tsx} | 2 -- src/icons/{MeasureToolIcon.js => MeasureToolIcon.tsx} | 2 -- src/icons/{MoveIcon.js => MoveIcon.tsx} | 2 -- src/icons/{MoveToolIcon.js => MoveToolIcon.tsx} | 2 -- src/icons/{NoteTextIcon.js => NoteTextIcon.tsx} | 2 -- src/icons/{NoteToolIcon.js => NoteToolIcon.tsx} | 2 -- src/icons/{OfflineIcon.js => OfflineIcon.tsx} | 2 -- src/icons/{PointerToolIcon.js => PointerToolIcon.tsx} | 2 -- src/icons/{ReconnectingIcon.js => ReconnectingIcon.tsx} | 2 -- src/icons/{RedoIcon.js => RedoIcon.tsx} | 2 -- src/icons/{RemoveMapIcon.js => RemoveMapIcon.tsx} | 2 -- src/icons/{RemoveTokenIcon.js => RemoveTokenIcon.tsx} | 2 -- src/icons/{RerollDiceIcon.js => RerollDiceIcon.tsx} | 2 -- src/icons/{ResetMapIcon.js => ResetMapIcon.tsx} | 2 -- src/icons/{SearchIcon.js => SearchIcon.tsx} | 2 -- src/icons/{SelectDiceIcon.js => SelectDiceIcon.tsx} | 2 -- src/icons/{SelectMapIcon.js => SelectMapIcon.tsx} | 2 -- src/icons/{SelectMultipleIcon.js => SelectMultipleIcon.tsx} | 2 -- src/icons/{SelectSingleIcon.js => SelectSingleIcon.tsx} | 2 -- src/icons/{SelectTokensIcon.js => SelectTokensIcon.tsx} | 2 -- src/icons/{SettingsIcon.js => SettingsIcon.tsx} | 2 -- src/icons/{ShareDiceOffIcon.js => ShareDiceOffIcon.tsx} | 2 -- src/icons/{ShareDiceOnIcon.js => ShareDiceOnIcon.tsx} | 2 -- src/icons/{SocialPatreonIcon.js => SocialPatreonIcon.tsx} | 2 -- src/icons/{SocialRedditIcon.js => SocialRedditIcon.tsx} | 2 -- src/icons/{SocialTwitterIcon.js => SocialTwitterIcon.tsx} | 2 -- src/icons/{SocialYouTubeIcon.js => SocialYouTubeIcon.tsx} | 2 -- src/icons/{StartTimerIcon.js => StartTimerIcon.tsx} | 2 -- src/icons/{StreamMuteIcon.js => StreamMuteIcon.tsx} | 4 +--- src/icons/{TokenHideIcon.js => TokenHideIcon.tsx} | 2 -- src/icons/{TokenLockIcon.js => TokenLockIcon.tsx} | 2 -- src/icons/{TokenShowIcon.js => TokenShowIcon.tsx} | 2 -- src/icons/{TokenUnlockIcon.js => TokenUnlockIcon.tsx} | 2 -- src/icons/{UndoIcon.js => UndoIcon.tsx} | 2 -- 82 files changed, 3 insertions(+), 167 deletions(-) rename src/icons/{AddIcon.js => AddIcon.tsx} (90%) rename src/icons/{AddPartyMemberIcon.js => AddPartyMemberIcon.tsx} (95%) rename src/icons/{BlendOffIcon.js => BlendOffIcon.tsx} (96%) rename src/icons/{BlendOnIcon.js => BlendOnIcon.tsx} (95%) rename src/icons/{BrushCircleIcon.js => BrushCircleIcon.tsx} (92%) rename src/icons/{BrushLineIcon.js => BrushLineIcon.tsx} (93%) rename src/icons/{BrushPaintIcon.js => BrushPaintIcon.tsx} (94%) rename src/icons/{BrushRectangleIcon.js => BrushRectangleIcon.tsx} (93%) rename src/icons/{BrushStrokeIcon.js => BrushStrokeIcon.tsx} (94%) rename src/icons/{BrushToolIcon.js => BrushToolIcon.tsx} (94%) rename src/icons/{BrushTriangleIcon.js => BrushTriangleIcon.tsx} (93%) rename src/icons/{ChangeNicknameIcon.js => ChangeNicknameIcon.tsx} (94%) rename src/icons/{ClearDiceIcon.js => ClearDiceIcon.tsx} (95%) rename src/icons/{D100Icon.js => D100Icon.tsx} (98%) rename src/icons/{D10Icon.js => D10Icon.tsx} (97%) rename src/icons/{D12Icon.js => D12Icon.tsx} (97%) rename src/icons/{D20Icon.js => D20Icon.tsx} (98%) rename src/icons/{D4Icon.js => D4Icon.tsx} (95%) rename src/icons/{D6Icon.js => D6Icon.tsx} (97%) rename src/icons/{D8Icon.js => D8Icon.tsx} (98%) rename src/icons/{DiceRollsIcon.js => DiceRollsIcon.tsx} (96%) rename src/icons/{DonateIcon.js => DonateIcon.tsx} (95%) rename src/icons/{EditTileIcon.js => EditTileIcon.tsx} (94%) rename src/icons/{EraseAllIcon.js => EraseAllIcon.tsx} (96%) rename src/icons/{EraseToolIcon.js => EraseToolIcon.tsx} (95%) rename src/icons/{ExpandMoreDiceIcon.js => ExpandMoreDiceIcon.tsx} (92%) rename src/icons/{ExpandMoreDiceTrayIcon.js => ExpandMoreDiceTrayIcon.tsx} (96%) rename src/icons/{ExpandMoreDotIcon.js => ExpandMoreDotIcon.tsx} (94%) rename src/icons/{ExpandMoreIcon.js => ExpandMoreIcon.tsx} (94%) rename src/icons/{FogBrushIcon.js => FogBrushIcon.tsx} (94%) rename src/icons/{FogCutOffIcon.js => FogCutOffIcon.tsx} (96%) rename src/icons/{FogCutOnIcon.js => FogCutOnIcon.tsx} (96%) rename src/icons/{FogMultilayerOffIcon.js => FogMultilayerOffIcon.tsx} (97%) rename src/icons/{FogMultilayerOnIcon.js => FogMultilayerOnIcon.tsx} (96%) rename src/icons/{FogPolygonIcon.js => FogPolygonIcon.tsx} (94%) rename src/icons/{FogPreviewOffIcon.js => FogPreviewOffIcon.tsx} (96%) rename src/icons/{FogPreviewOnIcon.js => FogPreviewOnIcon.tsx} (94%) rename src/icons/{FogRectangleIcon.js => FogRectangleIcon.tsx} (93%) rename src/icons/{FogRemoveIcon.js => FogRemoveIcon.tsx} (95%) rename src/icons/{FogToggleIcon.js => FogToggleIcon.tsx} (95%) rename src/icons/{FogToolIcon.js => FogToolIcon.tsx} (94%) rename src/icons/{FullScreenExitIcon.js => FullScreenExitIcon.tsx} (95%) rename src/icons/{FullScreenIcon.js => FullScreenIcon.tsx} (95%) rename src/icons/{GestureOffIcon.js => GestureOffIcon.tsx} (96%) rename src/icons/{GestureOnIcon.js => GestureOnIcon.tsx} (95%) rename src/icons/{GridOffIcon.js => GridOffIcon.tsx} (96%) rename src/icons/{GridOnIcon.js => GridOnIcon.tsx} (95%) rename src/icons/{GroupIcon.js => GroupIcon.tsx} (94%) rename src/icons/{HelpIcon.js => HelpIcon.tsx} (96%) rename src/icons/{MeasureToolIcon.js => MeasureToolIcon.tsx} (97%) rename src/icons/{MoveIcon.js => MoveIcon.tsx} (96%) rename src/icons/{MoveToolIcon.js => MoveToolIcon.tsx} (96%) rename src/icons/{NoteTextIcon.js => NoteTextIcon.tsx} (95%) rename src/icons/{NoteToolIcon.js => NoteToolIcon.tsx} (95%) rename src/icons/{OfflineIcon.js => OfflineIcon.tsx} (97%) rename src/icons/{PointerToolIcon.js => PointerToolIcon.tsx} (93%) rename src/icons/{ReconnectingIcon.js => ReconnectingIcon.tsx} (97%) rename src/icons/{RedoIcon.js => RedoIcon.tsx} (95%) rename src/icons/{RemoveMapIcon.js => RemoveMapIcon.tsx} (94%) rename src/icons/{RemoveTokenIcon.js => RemoveTokenIcon.tsx} (94%) rename src/icons/{RerollDiceIcon.js => RerollDiceIcon.tsx} (97%) rename src/icons/{ResetMapIcon.js => ResetMapIcon.tsx} (95%) rename src/icons/{SearchIcon.js => SearchIcon.tsx} (95%) rename src/icons/{SelectDiceIcon.js => SelectDiceIcon.tsx} (96%) rename src/icons/{SelectMapIcon.js => SelectMapIcon.tsx} (95%) rename src/icons/{SelectMultipleIcon.js => SelectMultipleIcon.tsx} (96%) rename src/icons/{SelectSingleIcon.js => SelectSingleIcon.tsx} (94%) rename src/icons/{SelectTokensIcon.js => SelectTokensIcon.tsx} (94%) rename src/icons/{SettingsIcon.js => SettingsIcon.tsx} (97%) rename src/icons/{ShareDiceOffIcon.js => ShareDiceOffIcon.tsx} (96%) rename src/icons/{ShareDiceOnIcon.js => ShareDiceOnIcon.tsx} (95%) rename src/icons/{SocialPatreonIcon.js => SocialPatreonIcon.tsx} (95%) rename src/icons/{SocialRedditIcon.js => SocialRedditIcon.tsx} (97%) rename src/icons/{SocialTwitterIcon.js => SocialTwitterIcon.tsx} (96%) rename src/icons/{SocialYouTubeIcon.js => SocialYouTubeIcon.tsx} (96%) rename src/icons/{StartTimerIcon.js => StartTimerIcon.tsx} (96%) rename src/icons/{StreamMuteIcon.js => StreamMuteIcon.tsx} (95%) rename src/icons/{TokenHideIcon.js => TokenHideIcon.tsx} (96%) rename src/icons/{TokenLockIcon.js => TokenLockIcon.tsx} (94%) rename src/icons/{TokenShowIcon.js => TokenShowIcon.tsx} (94%) rename src/icons/{TokenUnlockIcon.js => TokenUnlockIcon.tsx} (95%) rename src/icons/{UndoIcon.js => UndoIcon.tsx} (95%) diff --git a/src/icons/AddIcon.js b/src/icons/AddIcon.tsx similarity index 90% rename from src/icons/AddIcon.js rename to src/icons/AddIcon.tsx index 88db9d1..340ee91 100644 --- a/src/icons/AddIcon.js +++ b/src/icons/AddIcon.tsx @@ -1,6 +1,4 @@ -import React from "react"; - -function AddIcon({ large }) { +function AddIcon({ large }: { large: boolean }) { return ( Date: Thu, 27 May 2021 16:21:50 +1000 Subject: [PATCH 004/142] Edited gitignore for ts build info --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4d29575..4306416 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# typescript +tsconfig.tsbuildinfo \ No newline at end of file From b9d003196a6deb3f62396b39df6211b3360fcd5d Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Thu, 27 May 2021 17:13:14 +1000 Subject: [PATCH 005/142] Converted more files to typescript --- src/global.d.ts | 4 ++- src/maps/{index.js => index.ts} | 0 src/tokens/{index.js => index.ts} | 2 +- .../{DatabaseWorker.js => DatabaseWorker.ts} | 25 +++++++++++-------- 4 files changed, 19 insertions(+), 12 deletions(-) rename src/maps/{index.js => index.ts} (100%) rename src/tokens/{index.js => index.ts} (97%) rename src/workers/{DatabaseWorker.js => DatabaseWorker.ts} (83%) diff --git a/src/global.d.ts b/src/global.d.ts index a476569..64cda09 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,2 +1,4 @@ declare module 'pepjs'; -declare module 'socket.io-msgpack-parser'; \ No newline at end of file +declare module 'socket.io-msgpack-parser'; +declare module 'fake-indexeddb'; +declare module 'fake-indexeddb/lib/FDBKeyRange' \ No newline at end of file diff --git a/src/maps/index.js b/src/maps/index.ts similarity index 100% rename from src/maps/index.js rename to src/maps/index.ts diff --git a/src/tokens/index.js b/src/tokens/index.ts similarity index 97% rename from src/tokens/index.js rename to src/tokens/index.ts index 5a16e82..122975e 100644 --- a/src/tokens/index.js +++ b/src/tokens/index.ts @@ -67,7 +67,7 @@ export const tokenSources = { undead, }; -function getDefaultTokenSize(key) { +function getDefaultTokenSize(key: string) { switch (key) { case "dragon": case "elemental": diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.ts similarity index 83% rename from src/workers/DatabaseWorker.js rename to src/workers/DatabaseWorker.ts index 28d6c9c..6959aee 100644 --- a/src/workers/DatabaseWorker.js +++ b/src/workers/DatabaseWorker.ts @@ -8,16 +8,19 @@ import { encode, decode } from "@msgpack/msgpack"; import { getDatabase } from "../database"; import blobToBuffer from "../helpers/blobToBuffer"; +import { ExportProgress } from "@mitchemmc/dexie-export-import/dist/export"; + +type ProgressCallback = (progress: ExportProgress) => boolean; // Worker to load large amounts of database data on a separate thread let service = { /** * Load either a whole table or individual item from the DB * @param {string} table Table to load from - * @param {string=} key Optional database key to load, if undefined whole table will be loaded + * @param {string} key Optional database key to load, if undefined whole table will be loaded * @param {bool} excludeFiles Optional exclude files from loaded data when using whole table loading */ - async loadData(table, key, excludeFiles = true) { + async loadData(table: string, key: string, excludeFiles: boolean = true) { try { let db = getDatabase({}); if (key) { @@ -26,7 +29,7 @@ let service = { return data; } else { // Load entire table - let items = []; + let items: any[] = []; // Use a cursor instead of toArray to prevent IPC max size error await db.table(table).each((item) => { if (excludeFiles) { @@ -41,7 +44,9 @@ let service = { const packed = encode(items); return Comlink.transfer(packed, [packed.buffer]); } - } catch {} + } catch { + // TODO: throw error in empty catch? + } }, /** @@ -50,7 +55,7 @@ let service = { * @param {string} table * @param {boolean} wait Whether to wait for the put to finish */ - async putData(data, table, wait = true) { + async putData(data: Uint8Array, table: string, wait: boolean = true) { try { let db = getDatabase({}); const decoded = decode(data); @@ -67,14 +72,14 @@ let service = { /** * Export current database - * @param {function} progressCallback + * @param {ProgressCallback} progressCallback * @param {string[]} maps An array of map ids to export * @param {string[]} tokens An array of token ids to export */ - async exportData(progressCallback, maps, tokens) { + async exportData(progressCallback: ProgressCallback, maps: string[], tokens: string[]) { let db = getDatabase({}); - const filter = (table, value) => { + const filter = (table: string, value: any) => { if (table === "maps") { return maps.includes(value.id); } @@ -103,9 +108,9 @@ let service = { * Import into current database * @param {Blob} data * @param {string} databaseName The name of the database to import into - * @param {function} progressCallback + * @param {ProgressCallback} progressCallback */ - async importData(data, databaseName, progressCallback) { + async importData(data: Blob, databaseName: string, progressCallback: ProgressCallback) { const importMeta = await peakImportFile(data); if (!importMeta.data) { throw new Error("Uanble to parse file"); From 349cad53a2a9e520c785c3d8fad33b3685e69f68 Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Fri, 28 May 2021 12:37:33 +1000 Subject: [PATCH 006/142] Converted assets index.js to typescript --- src/docs/assets/{index.js => index.ts} | 0 src/global.d.ts | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) rename src/docs/assets/{index.js => index.ts} (100%) diff --git a/src/docs/assets/index.js b/src/docs/assets/index.ts similarity index 100% rename from src/docs/assets/index.js rename to src/docs/assets/index.ts diff --git a/src/global.d.ts b/src/global.d.ts index 64cda09..7fb08ab 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,4 +1,7 @@ declare module 'pepjs'; declare module 'socket.io-msgpack-parser'; declare module 'fake-indexeddb'; -declare module 'fake-indexeddb/lib/FDBKeyRange' \ No newline at end of file +declare module 'fake-indexeddb/lib/FDBKeyRange'; +declare module '*.glb'; +declare module '*.png'; +declare module '*.mp4'; \ No newline at end of file From 32f6e1fb234ca69ebab76d119f7488268fb36aa8 Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Sat, 29 May 2021 14:20:24 +1000 Subject: [PATCH 007/142] Converted /ml folder to typescript --- src/ml/{Model.js => Model.ts} | 12 +++++++++--- .../{GridSizeModel.js => GridSizeModel.ts} | 15 ++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) rename src/ml/{Model.js => Model.ts} (69%) rename src/ml/gridSize/{GridSizeModel.js => GridSizeModel.ts} (67%) diff --git a/src/ml/Model.js b/src/ml/Model.ts similarity index 69% rename from src/ml/Model.js rename to src/ml/Model.ts index 3809bf4..3c554bd 100644 --- a/src/ml/Model.js +++ b/src/ml/Model.ts @@ -1,15 +1,21 @@ +import { ModelJSON, WeightsManifestConfig } from "@tensorflow/tfjs-core/dist/io/types"; import blobToBuffer from "../helpers/blobToBuffer"; class Model { - constructor(config, weightsMapping) { + config: ModelJSON; + weightsMapping: { [path: string]: string }; + constructor(config: ModelJSON, weightsMapping: { [path: string]: string }) { this.config = config; this.weightsMapping = weightsMapping; } async load() { // Load weights from the manifest then fetch them into an ArrayBuffer - let buffers = []; - const manifest = this.config.weightsManifest[0]; + let buffers: ArrayBuffer[] = []; + if (this.config === undefined) { + return; + } + const manifest = this.config?.weightsManifest[0]; for (let path of manifest.paths) { const url = this.weightsMapping[path]; const response = await fetch(url); diff --git a/src/ml/gridSize/GridSizeModel.js b/src/ml/gridSize/GridSizeModel.ts similarity index 67% rename from src/ml/gridSize/GridSizeModel.js rename to src/ml/gridSize/GridSizeModel.ts index 6775f75..25bde7e 100644 --- a/src/ml/gridSize/GridSizeModel.js +++ b/src/ml/gridSize/GridSizeModel.ts @@ -2,17 +2,21 @@ import Model from "../Model"; import config from "./model.json"; import weights from "./group1-shard1of1.bin"; +import { LayersModel } from "@tensorflow/tfjs"; +import { ModelJSON } from "@tensorflow/tfjs-core/dist/io/types"; class GridSizeModel extends Model { // Store model as static to prevent extra network requests - static model; + static model: LayersModel; // Load tensorflow dynamically - static tf; + + // TODO: find type for tf + static tf: any; constructor() { - super(config, { "group1-shard1of1.bin": weights }); + super(config as ModelJSON, { "group1-shard1of1.bin": weights }); } - async predict(imageData) { + async predict(imageData: ImageData) { if (!GridSizeModel.tf) { GridSizeModel.tf = await import("@tensorflow/tfjs"); } @@ -23,7 +27,8 @@ class GridSizeModel extends Model { } const model = GridSizeModel.model; - const prediction = tf.tidy(() => { + // TODO: check this mess -> changing type on prediction causes issues + const prediction: any = tf.tidy(() => { const image = tf.browser.fromPixels(imageData, 1).toFloat(); const normalized = image.div(tf.scalar(255.0)); const batched = tf.expandDims(normalized); From ecc4f67f3777097b2f07a3789e754fa07d3a12c2 Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Wed, 2 Jun 2021 17:49:31 +1000 Subject: [PATCH 008/142] Converted /modals to typescript --- .../{LoadingOverlay.js => LoadingOverlay.tsx} | 3 +- src/components/{Modal.js => Modal.tsx} | 16 +- src/components/{Slider.js => Slider.tsx} | 19 ++- src/components/map/{Map.js => Map.tsx} | 142 +++++++++++++++--- ...KeyboardContext.js => KeyboardContext.tsx} | 10 +- ...kenDataContext.js => TokenDataContext.tsx} | 103 +++++++------ src/global.d.ts | 3 +- src/helpers/drawing.ts | 40 ++--- src/helpers/grid.ts | 10 +- src/helpers/image.ts | 2 +- src/helpers/select.tsx | 15 +- src/ml/Model.ts | 2 +- ...MemberModal.js => AddPartyMemberModal.tsx} | 11 +- src/modals/{AuthModal.js => AuthModal.tsx} | 18 +-- ...cknameModal.js => ChangeNicknameModal.tsx} | 12 +- .../{ConfirmModal.js => ConfirmModal.tsx} | 14 +- .../{EditGroupModal.js => EditGroupModal.tsx} | 22 ++- .../{EditMapModal.js => EditMapModal.tsx} | 33 ++-- .../{EditTokenModal.js => EditTokenModal.tsx} | 27 ++-- ...rceUpdateModal.js => ForceUpdateModal.tsx} | 5 +- ...meExpiredModal.js => GameExpiredModal.tsx} | 5 +- ...tartedModal.js => GettingStartedModal.tsx} | 5 +- ...rtExportModal.js => ImportExportModal.tsx} | 37 ++--- src/modals/{JoinModal.js => JoinModal.tsx} | 13 +- ...SelectDataModal.js => SelectDataModal.tsx} | 73 +++++---- ...SelectDiceModal.js => SelectDiceModal.tsx} | 14 +- .../{SelectMapModal.js => SelectMapModal.tsx} | 59 +++++--- ...ctTokensModal.js => SelectTokensModal.tsx} | 57 ++++--- .../{SettingsModal.js => SettingsModal.tsx} | 30 ++-- src/modals/{StartModal.js => StartModal.tsx} | 12 +- ...artStreamModal.js => StartStreamModal.tsx} | 15 +- ...StartTimerModal.js => StartTimerModal.tsx} | 18 ++- src/tokens/index.ts | 37 ++++- 33 files changed, 571 insertions(+), 311 deletions(-) rename src/components/{LoadingOverlay.js => LoadingOverlay.tsx} (89%) rename src/components/{Modal.js => Modal.tsx} (75%) rename src/components/{Slider.js => Slider.tsx} (77%) rename src/components/map/{Map.js => Map.tsx} (71%) rename src/contexts/{KeyboardContext.js => KeyboardContext.tsx} (87%) rename src/contexts/{TokenDataContext.js => TokenDataContext.tsx} (65%) rename src/modals/{AddPartyMemberModal.js => AddPartyMemberModal.tsx} (83%) rename src/modals/{AuthModal.js => AuthModal.tsx} (61%) rename src/modals/{ChangeNicknameModal.js => ChangeNicknameModal.tsx} (76%) rename src/modals/{ConfirmModal.js => ConfirmModal.tsx} (79%) rename src/modals/{EditGroupModal.js => EditGroupModal.tsx} (71%) rename src/modals/{EditMapModal.js => EditMapModal.tsx} (85%) rename src/modals/{EditTokenModal.js => EditTokenModal.tsx} (81%) rename src/modals/{ForceUpdateModal.js => ForceUpdateModal.tsx} (79%) rename src/modals/{GameExpiredModal.js => GameExpiredModal.tsx} (79%) rename src/modals/{GettingStartedModal.js => GettingStartedModal.tsx} (80%) rename src/modals/{ImportExportModal.js => ImportExportModal.tsx} (86%) rename src/modals/{JoinModal.js => JoinModal.tsx} (79%) rename src/modals/{SelectDataModal.js => SelectDataModal.tsx} (78%) rename src/modals/{SelectDiceModal.js => SelectDiceModal.tsx} (77%) rename src/modals/{SelectMapModal.js => SelectMapModal.tsx} (90%) rename src/modals/{SelectTokensModal.js => SelectTokensModal.tsx} (86%) rename src/modals/{SettingsModal.js => SettingsModal.tsx} (87%) rename src/modals/{StartModal.js => StartModal.tsx} (83%) rename src/modals/{StartStreamModal.js => StartStreamModal.tsx} (80%) rename src/modals/{StartTimerModal.js => StartTimerModal.tsx} (90%) diff --git a/src/components/LoadingOverlay.js b/src/components/LoadingOverlay.tsx similarity index 89% rename from src/components/LoadingOverlay.js rename to src/components/LoadingOverlay.tsx index 887f9a1..62cff28 100644 --- a/src/components/LoadingOverlay.js +++ b/src/components/LoadingOverlay.tsx @@ -1,9 +1,8 @@ -import React from "react"; import { Box } from "theme-ui"; import Spinner from "./Spinner"; -function LoadingOverlay({ bg }) { +function LoadingOverlay({ bg }: any ) { return ( @@ -46,7 +50,7 @@ function StyledModal({ StyledModal.defaultProps = { allowClose: true, - style: {}, + style: {} }; export default StyledModal; diff --git a/src/components/Slider.js b/src/components/Slider.tsx similarity index 77% rename from src/components/Slider.js rename to src/components/Slider.tsx index fdc601b..d45abac 100644 --- a/src/components/Slider.js +++ b/src/components/Slider.tsx @@ -1,10 +1,19 @@ -import React, { useState } from "react"; -import { Box, Slider as ThemeSlider } from "theme-ui"; +import { useState } from "react"; +import { Box, Slider as ThemeSlider, SliderProps } from "theme-ui"; -function Slider({ min, max, value, ml, mr, labelFunc, ...rest }) { +type SliderModalProps = SliderProps & { + min: number, + max: number, + value: number, + ml: any, + mr: any, + labelFunc: any +} + +function Slider({ min, max, value, ml, mr, labelFunc, ...rest }: SliderModalProps ) { const percentValue = ((value - min) * 100) / (max - min); - const [labelVisible, setLabelVisible] = useState(false); + const [labelVisible, setLabelVisible] = useState(false); return ( @@ -63,7 +72,7 @@ Slider.defaultProps = { value: 0, ml: 0, mr: 0, - labelFunc: (value) => value, + labelFunc: (value: any) => value, }; export default Slider; diff --git a/src/components/map/Map.js b/src/components/map/Map.tsx similarity index 71% rename from src/components/map/Map.js rename to src/components/map/Map.tsx index 340749e..8f8f539 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.tsx @@ -24,6 +24,78 @@ import { EditShapeAction, RemoveShapeAction, } from "../../actions"; +import { Fog, Path, Shape } from "../../helpers/drawing"; +import Session from "../../network/Session"; +import { Grid } from "../../helpers/grid"; +import { ImageFile } from "../../helpers/image"; + +type Resolutions = Record + +export type Map = { + id: string, + name: string, + owner: string, + file: Uint8Array, + quality: string, + resolutions: Resolutions, + grid: Grid, + group: string, + width: number, + height: number, + type: string, + lastUsed: number, + lastModified: number, + created: number, + showGrid: boolean, + snapToGrid: boolean, + thumbnail: ImageFile, +} + +export type Note = { + id: string, + color: string, + lastModified: number, + lastModifiedBy: string, + locked: boolean, + size: number, + text: string, + textOnly: boolean, + visible: boolean, + x: number, + y: number, +} + +export type TokenState = { + id: string, + tokenId: string, + owner: string, + size: number, + label: string, + status: string[], + x: number, + y: number, + lastModifiedBy: string, + lastModified: number, + rotation: number, + locked: boolean, + visible: boolean +} + +interface PathId extends Path { + id: string +} + +interface ShapeId extends Shape { + id: string +} +export type MapState = { + tokens: Record, + drawShapes: PathId | ShapeId, + fogShapes: Fog[], + editFlags: string[], + notes: Note[], + mapId: string, +} function Map({ map, @@ -47,14 +119,36 @@ function Map({ allowNoteEditing, disabledTokens, session, +}: { + map: any + mapState: MapState + mapActions: any, + onMapTokenStateChange: any, + onMapTokenStateRemove: any, + onMapChange: any, + onMapReset: any, + onMapDraw: any, + onMapDrawUndo: any, + onMapDrawRedo: any, + onFogDraw: any, + onFogDrawUndo: any, + onFogDrawRedo: any, + onMapNoteChange: any, + onMapNoteRemove: any, + allowMapDrawing: boolean, + allowFogDrawing: boolean, + allowMapChange: boolean, + allowNoteEditing: boolean, + disabledTokens: any, + session: Session }) { const { tokensById } = useTokenData(); const [selectedToolId, setSelectedToolId] = useState("move"); - const { settings, setSettings } = useSettings(); + const { settings, setSettings }: { settings: any, setSettings: any} = useSettings(); - function handleToolSettingChange(tool, change) { - setSettings((prevSettings) => ({ + function handleToolSettingChange(tool: any, change: any) { + setSettings((prevSettings: any) => ({ ...prevSettings, [tool]: { ...prevSettings[tool], @@ -66,7 +160,7 @@ function Map({ const drawShapes = Object.values(mapState?.drawShapes || {}); const fogShapes = Object.values(mapState?.fogShapes || {}); - function handleToolAction(action) { + function handleToolAction(action: string) { if (action === "eraseAll") { onMapDraw(new RemoveShapeAction(drawShapes.map((s) => s.id))); } @@ -84,27 +178,27 @@ function Map({ } } - function handleMapShapeAdd(shape) { + function handleMapShapeAdd(shape: Shape) { onMapDraw(new AddShapeAction([shape])); } - function handleMapShapesRemove(shapeIds) { + function handleMapShapesRemove(shapeIds: string[]) { onMapDraw(new RemoveShapeAction(shapeIds)); } - function handleFogShapesAdd(shapes) { + function handleFogShapesAdd(shapes: Shape[]) { onFogDraw(new AddShapeAction(shapes)); } - function handleFogShapesCut(shapes) { + function handleFogShapesCut(shapes: Shape[]) { onFogDraw(new CutShapeAction(shapes)); } - function handleFogShapesRemove(shapeIds) { + function handleFogShapesRemove(shapeIds: string[]) { onFogDraw(new RemoveShapeAction(shapeIds)); } - function handleFogShapesEdit(shapes) { + function handleFogShapesEdit(shapes: Shape[]) { onFogDraw(new EditShapeAction(shapes)); } @@ -127,7 +221,7 @@ function Map({ disabledControls.push("note"); } - const disabledSettings = { fog: [], drawing: [] }; + const disabledSettings: { fog: any[], drawing: any[]} = { fog: [], drawing: [] }; if (drawShapes.length === 0) { disabledSettings.drawing.push("erase"); } @@ -166,10 +260,10 @@ function Map({ /> ); - const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); - const [tokenMenuOptions, setTokenMenuOptions] = useState({}); - const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); - function handleTokenMenuOpen(tokenStateId, tokenImage) { + const [isTokenMenuOpen, setIsTokenMenuOpen]: [ isTokenMenuOpen: boolean, setIsTokenMenuOpen: React.Dispatch>] = useState(false); + const [tokenMenuOptions, setTokenMenuOptions]: [ tokenMenuOptions: any, setTokenMenuOptions: any ] = useState({}); + const [tokenDraggingOptions, setTokenDraggingOptions]: [ tokenDraggingOptions: any, setTokenDragginOptions: any ] = useState(); + function handleTokenMenuOpen(tokenStateId: string, tokenImage: any) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); } @@ -200,7 +294,7 @@ function Map({ const tokenDragOverlay = tokenDraggingOptions && ( { + onTokenStateRemove={(state: any) => { onMapTokenStateRemove(state); setTokenDraggingOptions(null); }} @@ -243,7 +337,6 @@ function Map({ ); @@ -254,15 +347,15 @@ function Map({ /> ); - const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); - const [noteMenuOptions, setNoteMenuOptions] = useState({}); - const [noteDraggingOptions, setNoteDraggingOptions] = useState(); - function handleNoteMenuOpen(noteId, noteNode) { + const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); + const [noteMenuOptions, setNoteMenuOptions] = useState({}); + const [noteDraggingOptions, setNoteDraggingOptions]= useState(); + function handleNoteMenuOpen(noteId: string, noteNode: any) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); } - function sortNotes(a, b, noteDraggingOptions) { + function sortNotes(a: any, b: any, noteDraggingOptions: any) { if ( noteDraggingOptions && noteDraggingOptions.dragging && @@ -287,7 +380,6 @@ function Map({ + onNoteDragStart={(e: any, noteId: any) => setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) } onNoteDragEnd={() => @@ -328,7 +420,7 @@ function Map({ dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)} noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup} noteId={noteDraggingOptions && noteDraggingOptions.noteId} - onNoteRemove={(noteId) => { + onNoteRemove={(noteId: any) => { onMapNoteRemove(noteId); setNoteDraggingOptions(null); }} diff --git a/src/contexts/KeyboardContext.js b/src/contexts/KeyboardContext.tsx similarity index 87% rename from src/contexts/KeyboardContext.js rename to src/contexts/KeyboardContext.tsx index 592e60f..43fdf61 100644 --- a/src/contexts/KeyboardContext.js +++ b/src/contexts/KeyboardContext.tsx @@ -3,10 +3,10 @@ import { EventEmitter } from "events"; const KeyboardContext = React.createContext({ keyEmitter: new EventEmitter() }); -export function KeyboardProvider({ children }) { +export function KeyboardProvider({ children }: { children: any}) { const [keyEmitter] = useState(new EventEmitter()); useEffect(() => { - function handleKeyDown(event) { + function handleKeyDown(event: Event) { // Ignore text input if ( event.target instanceof HTMLInputElement || @@ -17,7 +17,7 @@ export function KeyboardProvider({ children }) { keyEmitter.emit("keyDown", event); } - function handleKeyUp(event) { + function handleKeyUp(event: Event) { // Ignore text input if ( event.target instanceof HTMLInputElement || @@ -49,7 +49,7 @@ export function KeyboardProvider({ children }) { * @param {KeyboardEvent} onKeyDown * @param {KeyboardEvent} onKeyUp */ -export function useKeyboard(onKeyDown, onKeyUp) { +export function useKeyboard(onKeyDown: (...args: any[]) => void, onKeyUp: (...args: any[]) => void) { const context = useContext(KeyboardContext); if (context === undefined) { throw new Error("useKeyboard must be used within a KeyboardProvider"); @@ -78,7 +78,7 @@ export function useKeyboard(onKeyDown, onKeyUp) { * Handler to handle a blur event. Useful when using a shortcut that uses the Alt or Cmd * @param {FocusEvent} onBlur */ -export function useBlur(onBlur) { +export function useBlur(onBlur: EventListenerOrEventListenerObject) { useEffect(() => { if (onBlur) { window.addEventListener("blur", onBlur); diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.tsx similarity index 65% rename from src/contexts/TokenDataContext.js rename to src/contexts/TokenDataContext.tsx index 329a509..dd49630 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.tsx @@ -10,13 +10,29 @@ import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; -import { tokens as defaultTokens } from "../tokens"; +import { DefaultToken, FileToken, Token, tokens as defaultTokens } from "../tokens"; -const TokenDataContext = React.createContext(); +type TokenDataContext = { + tokens: Token[]; + ownedTokens: Token[]; + addToken: (token: Token) => Promise; + removeToken: (id: string) => Promise; + removeTokens: (ids: string[]) => Promise; + updateToken: (id: string, update: Partial) => Promise; + updateTokens: (ids: string[], update: Partial) => Promise; + putToken: (token: Token) => Promise; + getToken: (tokenId: string) => Token | undefined + tokensById: { [key: string]: Token; }; + tokensLoading: boolean; + getTokenFromDB: (tokenId: string) => Promise; + loadTokens: (tokenIds: string[]) => Promise; +} + +const TokenDataContext = React.createContext(undefined); const cachedTokenMax = 100; -export function TokenDataProvider({ children }) { +export function TokenDataProvider({ children }: { children: any }) { const { database, databaseStatus, worker } = useDatabase(); const { userId } = useAuth(); @@ -24,7 +40,7 @@ export function TokenDataProvider({ children }) { * Contains all tokens without any file data, * to ensure file data is present call loadTokens */ - const [tokens, setTokens] = useState([]); + const [tokens, setTokens] = useState([]); const [tokensLoading, setTokensLoading] = useState(true); useEffect(() => { @@ -32,13 +48,12 @@ export function TokenDataProvider({ children }) { return; } function getDefaultTokens() { - const defaultTokensWithIds = []; + const defaultTokensWithIds: Required = []; for (let defaultToken of defaultTokens) { defaultTokensWithIds.push({ ...defaultToken, id: `__default-${defaultToken.name}`, owner: userId, - group: "default", }); } return defaultTokensWithIds; @@ -46,19 +61,19 @@ export function TokenDataProvider({ children }) { // Loads tokens without the file data to save memory async function loadTokens() { - let storedTokens = []; + let storedTokens: any = []; // Try to load tokens with worker, fallback to database if failed - const packedTokens = await worker.loadData("tokens"); + const packedTokens: ArrayLike | BufferSource = await worker.loadData("tokens"); if (packedTokens) { storedTokens = decode(packedTokens); } else { console.warn("Unable to load tokens with worker, loading may be slow"); - await database.table("tokens").each((token) => { - const { file, resolutions, ...rest } = token; + await database?.table("tokens").each((token: FileToken) => { + const { file, ...rest } = token; storedTokens.push(rest); }); } - const sortedTokens = storedTokens.sort((a, b) => b.created - a.created); + const sortedTokens = storedTokens.sort((a: any, b: any) => b.created - a.created); const defaultTokensWithIds = getDefaultTokens(); const allTokens = [...sortedTokens, ...defaultTokensWithIds]; setTokens(allTokens); @@ -79,7 +94,7 @@ export function TokenDataProvider({ children }) { const getTokenFromDB = useCallback( async (tokenId) => { - let token = await database.table("tokens").get(tokenId); + let token = await database?.table("tokens").get(tokenId); return token; }, [database] @@ -90,23 +105,23 @@ export function TokenDataProvider({ children }) { * Sorted by when they we're last used */ const updateCache = useCallback(async () => { - const cachedTokens = await database - .table("tokens") - .where("owner") - .notEqual(userId) - .sortBy("lastUsed"); - if (cachedTokens.length > cachedTokenMax) { - const cacheDeleteCount = cachedTokens.length - cachedTokenMax; + const cachedTokens: Token[] | undefined = await database?.table("tokens").where("owner").notEqual(userId).sortBy("lastUsed"); + // TODO: handle undefined cachedTokens + if (!cachedTokens) { + return; + } + if (cachedTokens?.length > cachedTokenMax) { + const cacheDeleteCount = cachedTokens.length - cachedTokenMax const idsToDelete = cachedTokens .slice(0, cacheDeleteCount) .map((token) => token.id); - database.table("tokens").where("id").anyOf(idsToDelete).delete(); + database?.table("tokens").where("id").anyOf(idsToDelete).delete(); } }, [database, userId]); const addToken = useCallback( async (token) => { - await database.table("tokens").add(token); + await database?.table("tokens").add(token); if (token.owner !== userId) { await updateCache(); } @@ -115,23 +130,23 @@ export function TokenDataProvider({ children }) { ); const removeToken = useCallback( - async (id) => { - await database.table("tokens").delete(id); + async (id: string) => { + await database?.table("tokens").delete(id); }, [database] ); const removeTokens = useCallback( - async (ids) => { - await database.table("tokens").bulkDelete(ids); + async (ids: string[]) => { + await database?.table("tokens").bulkDelete(ids); }, [database] ); const updateToken = useCallback( - async (id, update) => { + async (id: string, update: any) => { const change = { lastModified: Date.now(), ...update }; - await database.table("tokens").update(id, change); + await database?.table("tokens").update(id, change); }, [database] ); @@ -140,7 +155,7 @@ export function TokenDataProvider({ children }) { async (ids, update) => { const change = { lastModified: Date.now(), ...update }; await Promise.all( - ids.map((id) => database.table("tokens").update(id, change)) + ids.map((id: string) => database?.table("tokens").update(id, change)) ); }, [database] @@ -148,7 +163,7 @@ export function TokenDataProvider({ children }) { const putToken = useCallback( async (token) => { - await database.table("tokens").put(token); + await database?.table("tokens").put(token); if (token.owner !== userId) { await updateCache(); } @@ -157,13 +172,17 @@ export function TokenDataProvider({ children }) { ); const loadTokens = useCallback( - async (tokenIds) => { - const loadedTokens = await database.table("tokens").bulkGet(tokenIds); - const loadedTokensById = loadedTokens.reduce((obj, token) => { + async (tokenIds: string[]) => { + const loadedTokens: FileToken[] | undefined = await database?.table("tokens").bulkGet(tokenIds); + const loadedTokensById = loadedTokens?.reduce((obj: { [key: string]: FileToken }, token: FileToken) => { obj[token.id] = token; return obj; }, {}); - setTokens((prevTokens) => { + if (!loadedTokensById) { + // TODO: whatever + return; + } + setTokens((prevTokens: Token[]) => { return prevTokens.map((prevToken) => { if (prevToken.id in loadedTokensById) { return loadedTokensById[prevToken.id]; @@ -176,22 +195,13 @@ export function TokenDataProvider({ children }) { [database] ); - const unloadTokens = useCallback(async () => { - setTokens((prevTokens) => { - return prevTokens.map((prevToken) => { - const { file, ...rest } = prevToken; - return rest; - }); - }); - }, []); - // Create DB observable to sync creating and deleting useEffect(() => { if (!database || databaseStatus === "loading") { return; } - function handleTokenChanges(changes) { + function handleTokenChanges(changes: any) { for (let change of changes) { if (change.table === "tokens") { if (change.type === 1) { @@ -230,12 +240,12 @@ export function TokenDataProvider({ children }) { const ownedTokens = tokens.filter((token) => token.owner === userId); - const tokensById = tokens.reduce((obj, token) => { + const tokensById: { [key: string]: Token; } = tokens.reduce((obj: { [key: string]: Token }, token) => { obj[token.id] = token; return obj; }, {}); - const value = { + const value: TokenDataContext = { tokens, ownedTokens, addToken, @@ -249,7 +259,6 @@ export function TokenDataProvider({ children }) { tokensLoading, getTokenFromDB, loadTokens, - unloadTokens, }; return ( @@ -259,7 +268,7 @@ export function TokenDataProvider({ children }) { ); } -export function useTokenData() { +export function useTokenData(): TokenDataContext { const context = useContext(TokenDataContext); if (context === undefined) { throw new Error("useTokenData must be used within a TokenDataProvider"); diff --git a/src/global.d.ts b/src/global.d.ts index 7fb08ab..9d0600f 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -4,4 +4,5 @@ declare module 'fake-indexeddb'; declare module 'fake-indexeddb/lib/FDBKeyRange'; declare module '*.glb'; declare module '*.png'; -declare module '*.mp4'; \ No newline at end of file +declare module '*.mp4'; +declare module '*.bin'; \ No newline at end of file diff --git a/src/helpers/drawing.ts b/src/helpers/drawing.ts index 1a43972..99f5b73 100644 --- a/src/helpers/drawing.ts +++ b/src/helpers/drawing.ts @@ -70,7 +70,7 @@ type ShapeType = "line" | "rectangle" | "circle" | "triangle" * @typedef {("fill"|"stroke")} PathType */ -// type PathType = "fill" | "stroke" +type PathType = "fill" | "stroke" /** * @typedef Path @@ -83,15 +83,15 @@ type ShapeType = "line" | "rectangle" | "circle" | "triangle" * @property {"path"} type */ -// type Path = { -// blend: boolean, -// color: string, -// data: PointsData, -// id: string, -// pathType: PathType, -// strokeWidth: number, -// type: "path" -// } +export type Path = { + blend: boolean, + color: string, + data: PointsData, + id: string, + pathType: PathType, + strokeWidth: number, + type: "path" +} /** * @typedef Shape @@ -104,15 +104,15 @@ type ShapeType = "line" | "rectangle" | "circle" | "triangle" * @property {"shape"} type */ -// type Shape = { -// blend: boolean, -// color: string, -// data: ShapeData, -// id: string, -// shapeType: ShapeType, -// strokeWidth: number, -// type: "shape" -// } +export type Shape = { + blend: boolean, + color: string, + data: ShapeData, + id: string, + shapeType: ShapeType, + strokeWidth: number, + type: "shape" +} /** * @typedef Fog @@ -124,7 +124,7 @@ type ShapeType = "line" | "rectangle" | "circle" | "triangle" * @property {boolean} visible */ -type Fog = { +export type Fog = { color: string, data: FogData, id: string, diff --git a/src/helpers/grid.ts b/src/helpers/grid.ts index 9ec8f40..20e475f 100644 --- a/src/helpers/grid.ts +++ b/src/helpers/grid.ts @@ -38,10 +38,10 @@ type GridMeasurement ={ * @property {GridMeasurement} measurement */ export type Grid = { - inset: GridInset, + inset?: GridInset, size: Vector2, type: ("square"|"hexVertical"|"hexHorizontal"), - measurement: GridMeasurement + measurement?: GridMeasurement } /** @@ -51,7 +51,7 @@ export type Grid = { * @param {number} baseHeight Height of the grid in pixels before inset * @returns {Size} */ -export function getGridPixelSize(grid: Grid, baseWidth: number, baseHeight: number): Size { +export function getGridPixelSize(grid: Required, baseWidth: number, baseHeight: number): Size { const width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth; const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight; return new Size(width, height); @@ -225,7 +225,7 @@ export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: num * @param {number} mapHeight Height of the map in pixels before inset * @returns {GridInset} */ -export function getGridUpdatedInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset { +export function getGridUpdatedInset(grid: Required, mapWidth: number, mapHeight: number): GridInset { let inset = grid.inset; // Take current inset width and use it to calculate the new height if (grid.size.x > 0 && grid.size.x > 0) { @@ -295,7 +295,7 @@ export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizo * @param {Vector2} b * @param {Size} cellSize */ -export function gridDistance(grid: Grid, a: Vector2, b: Vector2, cellSize: Size) { +export function gridDistance(grid: Required, a: Vector2, b: Vector2, cellSize: Size) { // Get grid coordinates const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize); const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize); diff --git a/src/helpers/image.ts b/src/helpers/image.ts index c35b7c3..33477b1 100644 --- a/src/helpers/image.ts +++ b/src/helpers/image.ts @@ -109,7 +109,7 @@ export async function resizeImage(image: HTMLImageElement, size: number, type: s * @property {string} id */ -type ImageFile = { +export type ImageFile = { file: Uint8Array | null, width: number, height: number, diff --git a/src/helpers/select.tsx b/src/helpers/select.tsx index 7ad1ef7..8184add 100644 --- a/src/helpers/select.tsx +++ b/src/helpers/select.tsx @@ -8,7 +8,7 @@ import { groupBy } from "./shared"; */ // Helper for generating search results for items -export function useSearch(items: [], search: string) { +export function useSearch(items: any[], search: string) { // TODO: add types to search items -> don't like the never type const [filteredItems, setFilteredItems]: [ filteredItems: any, @@ -18,10 +18,7 @@ export function useSearch(items: [], search: string) { filteredItemScores: {}, setFilteredItemScores: React.Dispatch> ] = useState({}); - const [fuse, setFuse]: [ - fuse: Fuse | undefined, - setFuse: React.Dispatch | undefined> - ] = useState(); + const [fuse, setFuse] = useState(); // Update search index when items change useEffect(() => { @@ -85,7 +82,7 @@ export function useGroup( export function handleItemSelect( item: any, selectMode: any, - selectedIds: number[], + selectedIds: string[], setSelectedIds: any, itemsByGroup: any, itemGroups: any @@ -120,15 +117,15 @@ export function handleItemSelect( const lastIndex = items.findIndex( (m: any) => m.id === selectedIds[selectedIds.length - 1] ); - let idsToAdd: number[] = []; - let idsToRemove: number[] = []; + let idsToAdd: string[] = []; + let idsToRemove: string[] = []; const direction = mapIndex > lastIndex ? 1 : -1; for ( let i = lastIndex + direction; direction < 0 ? i >= mapIndex : i <= mapIndex; i += direction ) { - const itemId: number = items[i].id; + const itemId: string = items[i].id; if (selectedIds.includes(itemId)) { idsToRemove.push(itemId); } else { diff --git a/src/ml/Model.ts b/src/ml/Model.ts index 3c554bd..98fc567 100644 --- a/src/ml/Model.ts +++ b/src/ml/Model.ts @@ -1,4 +1,4 @@ -import { ModelJSON, WeightsManifestConfig } from "@tensorflow/tfjs-core/dist/io/types"; +import { ModelJSON } from "@tensorflow/tfjs-core/dist/io/types"; import blobToBuffer from "../helpers/blobToBuffer"; class Model { diff --git a/src/modals/AddPartyMemberModal.js b/src/modals/AddPartyMemberModal.tsx similarity index 83% rename from src/modals/AddPartyMemberModal.js rename to src/modals/AddPartyMemberModal.tsx index 7a29568..ece2f7f 100644 --- a/src/modals/AddPartyMemberModal.js +++ b/src/modals/AddPartyMemberModal.tsx @@ -1,9 +1,16 @@ -import React from "react"; import { Box, Label, Text } from "theme-ui"; import Modal from "../components/Modal"; -function AddPartyMemberModal({ isOpen, onRequestClose, gameId }) { +function AddPartyMemberModal({ + isOpen, + onRequestClose, + gameId, +}: { + isOpen: boolean; + onRequestClose: any; + gameId: string; +}) { return ( diff --git a/src/modals/AuthModal.js b/src/modals/AuthModal.tsx similarity index 61% rename from src/modals/AuthModal.js rename to src/modals/AuthModal.tsx index 0c19e13..589d549 100644 --- a/src/modals/AuthModal.js +++ b/src/modals/AuthModal.tsx @@ -1,27 +1,27 @@ -import React, { useState, useRef } from "react"; +import { useState, useRef, ChangeEvent, FormEvent } from "react"; import { Box, Input, Button, Label, Flex } from "theme-ui"; import { useAuth } from "../contexts/AuthContext"; import Modal from "../components/Modal"; -function AuthModal({ isOpen, onSubmit }) { +function AuthModal({ isOpen, onSubmit }: { isOpen: boolean, onSubmit: (newPassword: string) => void}) { const { password, setPassword } = useAuth(); - const [tmpPassword, setTempPassword] = useState(password); + const [tmpPassword, setTempPassword] = useState(password); - function handleChange(event) { - setTempPassword(event.target.value); + function handleChange(event: ChangeEvent): void { + setTempPassword(event.target?.value); } - function handleSubmit(event) { + function handleSubmit(event: FormEvent): void { event.preventDefault(); setPassword(tmpPassword); onSubmit(tmpPassword); } - const inputRef = useRef(); - function focusInput() { - inputRef.current && inputRef.current.focus(); + const inputRef = useRef(); + function focusInput(): void { + inputRef.current && inputRef.current?.focus(); } return ( diff --git a/src/modals/ChangeNicknameModal.js b/src/modals/ChangeNicknameModal.tsx similarity index 76% rename from src/modals/ChangeNicknameModal.js rename to src/modals/ChangeNicknameModal.tsx index 8c6916c..2e4353e 100644 --- a/src/modals/ChangeNicknameModal.js +++ b/src/modals/ChangeNicknameModal.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from "react"; +import { useRef } from "react"; import { Box, Input, Button, Label, Flex } from "theme-ui"; import Modal from "../components/Modal"; @@ -9,10 +9,16 @@ function ChangeNicknameModal({ onChangeSubmit, nickname, onChange, +}: { + isOpen: boolean, + onRequestClose: () => void, + onChangeSubmit: any, + nickname: string, + onChange: any, }) { - const inputRef = useRef(); + const inputRef = useRef(null); function focusInput() { - inputRef.current && inputRef.current.focus(); + inputRef.current && inputRef.current?.focus(); } return ( diff --git a/src/modals/ConfirmModal.js b/src/modals/ConfirmModal.tsx similarity index 79% rename from src/modals/ConfirmModal.js rename to src/modals/ConfirmModal.tsx index 70c1402..16e7598 100644 --- a/src/modals/ConfirmModal.js +++ b/src/modals/ConfirmModal.tsx @@ -1,8 +1,16 @@ -import React from "react"; import { Box, Label, Flex, Button, Text } from "theme-ui"; import Modal from "../components/Modal"; +type ConfirmModalProps = { + isOpen: boolean, + onRequestClose: () => void, + onConfirm: () => void, + confirmText: string, + label: string, + description: string, +} + function ConfirmModal({ isOpen, onRequestClose, @@ -10,12 +18,12 @@ function ConfirmModal({ confirmText, label, description, -}) { +}: ConfirmModalProps ) { return ( diff --git a/src/modals/EditGroupModal.js b/src/modals/EditGroupModal.tsx similarity index 71% rename from src/modals/EditGroupModal.js rename to src/modals/EditGroupModal.tsx index a990bd0..6407274 100644 --- a/src/modals/EditGroupModal.js +++ b/src/modals/EditGroupModal.tsx @@ -1,24 +1,32 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Box, Button, Label, Flex } from "theme-ui"; import Modal from "../components/Modal"; import Select from "../components/Select"; +type EditGroupProps = { + isOpen: boolean, + onRequestClose: () => void, + onChange: any, + groups: string[], + defaultGroup: string | undefined | false, +} + function EditGroupModal({ isOpen, onRequestClose, onChange, groups, defaultGroup, -}) { - const [value, setValue] = useState(); - const [options, setOptions] = useState([]); +}: EditGroupProps) { + const [value, setValue] = useState<{ value: string; label: string; } | undefined>(); + const [options, setOptions] = useState<{ value: string; label: string; }[]>([]); useEffect(() => { if (defaultGroup) { setValue({ value: defaultGroup, label: defaultGroup }); } else { - setValue(); + setValue(undefined); } }, [defaultGroup]); @@ -26,7 +34,7 @@ function EditGroupModal({ setOptions(groups.map((group) => ({ value: group, label: group }))); }, [groups]); - function handleCreate(group) { + function handleCreate(group: string) { const newOption = { value: group, label: group }; setValue(newOption); setOptions((prev) => [...prev, newOption]); @@ -40,7 +48,7 @@ function EditGroupModal({ diff --git a/src/modals/EditMapModal.js b/src/modals/EditMapModal.tsx similarity index 85% rename from src/modals/EditMapModal.js rename to src/modals/EditMapModal.tsx index ad38c8c..16743ae 100644 --- a/src/modals/EditMapModal.js +++ b/src/modals/EditMapModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Button, Flex, Label } from "theme-ui"; import Modal from "../components/Modal"; @@ -12,8 +12,15 @@ import { isEmpty } from "../helpers/shared"; import { getGridDefaultInset } from "../helpers/grid"; import useResponsiveLayout from "../hooks/useResponsiveLayout"; +import { MapState } from "../components/map/Map"; -function EditMapModal({ isOpen, onDone, mapId }) { +type EditMapProps = { + isOpen: boolean, + onDone: any, + mapId: string +} + +function EditMapModal({ isOpen, onDone, mapId }: EditMapProps) { const { updateMap, updateMapState, @@ -23,8 +30,8 @@ function EditMapModal({ isOpen, onDone, mapId }) { } = useMapData(); const [isLoading, setIsLoading] = useState(true); - const [map, setMap] = useState(); - const [mapState, setMapState] = useState(); + const [map, setMap] = useState(); + const [mapState, setMapState] = useState(); // Load full map when modal is opened useEffect(() => { async function loadMap() { @@ -43,8 +50,8 @@ function EditMapModal({ isOpen, onDone, mapId }) { if (isOpen && mapId) { loadMap(); } else { - setMap(); - setMapState(); + setMap(undefined); + setMapState(undefined); } }, [isOpen, mapId, getMapFromDB, getMapStateFromDB, getMap]); @@ -64,19 +71,19 @@ function EditMapModal({ isOpen, onDone, mapId }) { */ // Local cache of map setting changes // Applied when done is clicked or map selection is changed - const [mapSettingChanges, setMapSettingChanges] = useState({}); - const [mapStateSettingChanges, setMapStateSettingChanges] = useState({}); + const [mapSettingChanges, setMapSettingChanges] = useState({}); + const [mapStateSettingChanges, setMapStateSettingChanges] = useState({}); - function handleMapSettingsChange(key, value) { - setMapSettingChanges((prevChanges) => ({ + function handleMapSettingsChange(key: string, value: string) { + setMapSettingChanges((prevChanges: any) => ({ ...prevChanges, [key]: value, lastModified: Date.now(), })); } - function handleMapStateSettingsChange(key, value) { - setMapStateSettingChanges((prevChanges) => ({ + function handleMapStateSettingsChange(key: string, value: string) { + setMapStateSettingChanges((prevChanges: any) => ({ ...prevChanges, [key]: value, })); @@ -137,7 +144,7 @@ function EditMapModal({ isOpen, onDone, mapId }) { void, + tokenId: string, +}; + +function EditTokenModal({ isOpen, onDone, tokenId }: EditModalProps) { const { updateToken, getTokenFromDB } = useTokenData(); const [isLoading, setIsLoading] = useState(true); - const [token, setToken] = useState(); + const [token, setToken] = useState(); useEffect(() => { async function loadToken() { setIsLoading(true); @@ -27,7 +34,7 @@ function EditTokenModal({ isOpen, onDone, tokenId }) { if (isOpen && tokenId) { loadToken(); } else { - setToken(); + setToken(undefined); } }, [isOpen, tokenId, getTokenFromDB]); @@ -41,10 +48,13 @@ function EditTokenModal({ isOpen, onDone, tokenId }) { onDone(); } - const [tokenSettingChanges, setTokenSettingChanges] = useState({}); + const [tokenSettingChanges, setTokenSettingChanges] = useState({}); - function handleTokenSettingsChange(key, value) { - setTokenSettingChanges((prevChanges) => ({ ...prevChanges, [key]: value })); + function handleTokenSettingsChange(key: any, value: any) { + setTokenSettingChanges((prevChanges: any) => ({ + ...prevChanges, + [key]: value, + })); } async function applyTokenChanges() { @@ -72,8 +82,7 @@ function EditTokenModal({ isOpen, onDone, tokenId }) { isOpen={isOpen} onRequestClose={handleClose} style={{ - maxWidth: layout.modalSize, - width: "calc(100% - 16px)", + content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" }, }} > diff --git a/src/modals/GameExpiredModal.js b/src/modals/GameExpiredModal.tsx similarity index 79% rename from src/modals/GameExpiredModal.js rename to src/modals/GameExpiredModal.tsx index 981a2b7..23458d1 100644 --- a/src/modals/GameExpiredModal.js +++ b/src/modals/GameExpiredModal.tsx @@ -1,14 +1,13 @@ -import React from "react"; import { Box, Label, Flex, Button, Text } from "theme-ui"; import Modal from "../components/Modal"; -function GameExpiredModal({ isOpen, onRequestClose }) { +function GameExpiredModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void }) { return ( diff --git a/src/modals/GettingStartedModal.js b/src/modals/GettingStartedModal.tsx similarity index 80% rename from src/modals/GettingStartedModal.js rename to src/modals/GettingStartedModal.tsx index 9a8e157..15608fe 100644 --- a/src/modals/GettingStartedModal.js +++ b/src/modals/GettingStartedModal.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Box, Label, Text } from "theme-ui"; import raw from "raw.macro"; @@ -8,12 +7,12 @@ import Link from "../components/Link"; const gettingStarted = raw("../docs/howTo/gettingStarted.md"); -function GettingStartedModal({ isOpen, onRequestClose }) { +function GettingStartedModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void } ) { return ( diff --git a/src/modals/ImportExportModal.js b/src/modals/ImportExportModal.tsx similarity index 86% rename from src/modals/ImportExportModal.js rename to src/modals/ImportExportModal.tsx index b6e083c..79f59f2 100644 --- a/src/modals/ImportExportModal.js +++ b/src/modals/ImportExportModal.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect } from "react"; +import { useRef, useState, useEffect } from "react"; import { Box, Label, Text, Button, Flex } from "theme-ui"; import { saveAs } from "file-saver"; import * as Comlink from "comlink"; @@ -16,24 +16,25 @@ import { useDatabase } from "../contexts/DatabaseContext"; import SelectDataModal from "./SelectDataModal"; import { getDatabase } from "../database"; +import { Map, MapState, TokenState } from "../components/map/Map"; const importDBName = "OwlbearRodeoImportDB"; -function ImportExportModal({ isOpen, onRequestClose }) { +function ImportExportModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void}) { const { worker } = useDatabase(); const { userId } = useAuth(); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(); + const [error, setError] = useState(); const backgroundTaskRunningRef = useRef(false); - const fileInputRef = useRef(); + const fileInputRef = useRef(); const [showImportSelector, setShowImportSelector] = useState(false); const [showExportSelector, setShowExportSelector] = useState(false); const { addToast } = useToasts(); - function addSuccessToast(message, maps, tokens) { + function addSuccessToast(message: string, maps: any, tokens: TokenState[]) { const mapText = `${maps.length} map${maps.length > 1 ? "s" : ""}`; const tokenText = `${tokens.length} token${tokens.length > 1 ? "s" : ""}`; if (maps.length > 0 && tokens.length > 0) { @@ -53,11 +54,11 @@ function ImportExportModal({ isOpen, onRequestClose }) { const loadingProgressRef = useRef(0); - function handleDBProgress({ completedRows, totalRows }) { + function handleDBProgress({ completedRows, totalRows }: { completedRows: number, totalRows: number }) { loadingProgressRef.current = completedRows / totalRows; } - async function handleImportDatabase(file) { + async function handleImportDatabase(file: File) { setIsLoading(true); backgroundTaskRunningRef.current = true; try { @@ -94,7 +95,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { } useEffect(() => { - function handleBeforeUnload(event) { + function handleBeforeUnload(event: any) { if (backgroundTaskRunningRef.current) { event.returnValue = "Database is still processing, are you sure you want to leave?"; @@ -121,7 +122,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { setShowImportSelector(false); } - async function handleImportSelectorConfirm(checkedMaps, checkedTokens) { + async function handleImportSelectorConfirm(checkedMaps: any, checkedTokens: TokenState[]) { setIsLoading(true); backgroundTaskRunningRef.current = true; setShowImportSelector(false); @@ -131,11 +132,11 @@ function ImportExportModal({ isOpen, onRequestClose }) { const db = getDatabase({}); try { // Keep track of a mapping of old token ids to new ones to apply them to the map states - let newTokenIds = {}; + let newTokenIds: {[id: string]: string} = {}; if (checkedTokens.length > 0) { const tokenIds = checkedTokens.map((token) => token.id); - const tokensToAdd = await importDB.table("tokens").bulkGet(tokenIds); - let newTokens = []; + const tokensToAdd: TokenState[] = await importDB.table("tokens").bulkGet(tokenIds); + let newTokens: TokenState[] = []; for (let token of tokensToAdd) { const newId = shortid.generate(); newTokenIds[token.id] = newId; @@ -146,12 +147,12 @@ function ImportExportModal({ isOpen, onRequestClose }) { } if (checkedMaps.length > 0) { - const mapIds = checkedMaps.map((map) => map.id); + const mapIds = checkedMaps.map((map: any) => map.id); const mapsToAdd = await importDB.table("maps").bulkGet(mapIds); let newMaps = []; let newStates = []; for (let map of mapsToAdd) { - let state = await importDB.table("states").get(map.id); + let state: MapState = await importDB.table("states").get(map.id); // Apply new token ids to imported state for (let tokenState of Object.values(state.tokens)) { if (tokenState.tokenId in newTokenIds) { @@ -179,7 +180,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { backgroundTaskRunningRef.current = false; } - function exportSelectorFilter(table, value) { + function exportSelectorFilter(table: any, value: Map | TokenState) { // Only show owned maps and tokens if (table === "maps" || table === "tokens") { if (value.owner === userId) { @@ -197,7 +198,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { setShowExportSelector(false); } - async function handleExportSelectorConfirm(checkedMaps, checkedTokens) { + async function handleExportSelectorConfirm(checkedMaps: Map[], checkedTokens: TokenState[]) { setShowExportSelector(false); setIsLoading(true); backgroundTaskRunningRef.current = true; @@ -238,7 +239,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { Select import or export then select the data you wish to use )} - setError()} /> + setError(undefined)} /> ) { + setGameId(event.target?.value); } - function handleSubmit(event) { + function handleSubmit(event: FormEvent) { event.preventDefault(); history.push(`/game/${gameId}`); } - const inputRef = useRef(); + const inputRef = useRef(); function focusInput() { inputRef.current && inputRef.current.focus(); } @@ -27,6 +27,7 @@ function JoinModal({ isOpen, onRequestClose }) { isOpen={isOpen} onRequestClose={onRequestClose} onAfterOpen={focusInput} + > ({}); + const [tokensByMap, setTokensByMap] = useState({}); + const [tokens, setTokens] = useState({}); const [isLoading, setIsLoading] = useState(false); const hasMaps = Object.values(maps).length > 0; @@ -29,9 +38,9 @@ function SelectDataModal({ if (isOpen && databaseName) { setIsLoading(true); const db = getDatabase({ addons: [] }, databaseName); - let loadedMaps = {}; - let loadedTokensByMap = {}; - let loadedTokens = {}; + let loadedMaps: any = []; + let loadedTokensByMap: any = {}; + let loadedTokens: any = []; await db .table("maps") .filter((map) => filter("maps", map, map.id)) @@ -44,7 +53,7 @@ function SelectDataModal({ .each((state) => { loadedTokensByMap[state.mapId] = new Set( Object.values(state.tokens).map( - (tokenState) => tokenState.tokenId + (tokenState: any) => tokenState.tokenId ) ); }); @@ -73,9 +82,9 @@ function SelectDataModal({ }, [isOpen, databaseName, filter]); // An object mapping a tokenId to how many checked maps it is currently used in - const [tokenUsedCount, setTokenUsedCount] = useState({}); + const [tokenUsedCount, setTokenUsedCount] = useState({}); useEffect(() => { - let tokensUsed = {}; + let tokensUsed: any = {}; for (let mapId in maps) { if (maps[mapId].checked && mapId in tokensByMap) { for (let tokenId of tokensByMap[mapId]) { @@ -89,7 +98,7 @@ function SelectDataModal({ } setTokenUsedCount(tokensUsed); // Update tokens to ensure used tokens are checked - setTokens((prevTokens) => { + setTokens((prevTokens: any) => { let newTokens = { ...prevTokens }; for (let id in newTokens) { if (id in tokensUsed) { @@ -101,13 +110,13 @@ function SelectDataModal({ }, [maps, tokensByMap]); function handleConfirm() { - let checkedMaps = Object.values(maps).filter((map) => map.checked); - let checkedTokens = Object.values(tokens).filter((token) => token.checked); + let checkedMaps = Object.values(maps).filter((map: any) => map.checked); + let checkedTokens = Object.values(tokens).filter((token: any) => token.checked); onConfirm(checkedMaps, checkedTokens); } - function handleSelectMapsChanged(event) { - setMaps((prevMaps) => { + function handleSelectMapsChanged(event: ChangeEvent) { + setMaps((prevMaps: any) => { let newMaps = { ...prevMaps }; for (let id in newMaps) { newMaps[id].checked = event.target.checked; @@ -116,7 +125,7 @@ function SelectDataModal({ }); // If all token select is unchecked then ensure all tokens are unchecked if (!event.target.checked && !tokensSelectChecked) { - setTokens((prevTokens) => { + setTokens((prevTokens: any) => { let newTokens = { ...prevTokens }; for (let id in newTokens) { newTokens[id].checked = false; @@ -126,14 +135,14 @@ function SelectDataModal({ } } - function handleMapChange(event, map) { - setMaps((prevMaps) => ({ + function handleMapChange(event: ChangeEvent, map: any) { + setMaps((prevMaps: any) => ({ ...prevMaps, [map.id]: { ...map, checked: event.target.checked }, })); // If all token select is unchecked then ensure tokens assosiated to this map are unchecked if (!event.target.checked && !tokensSelectChecked) { - setTokens((prevTokens) => { + setTokens((prevTokens: any) => { let newTokens = { ...prevTokens }; for (let id in newTokens) { if (tokensByMap[map.id].has(id) && tokenUsedCount[id] === 1) { @@ -145,8 +154,8 @@ function SelectDataModal({ } } - function handleSelectTokensChange(event) { - setTokens((prevTokens) => { + function handleSelectTokensChange(event: ChangeEvent) { + setTokens((prevTokens: any) => { let newTokens = { ...prevTokens }; for (let id in newTokens) { if (!(id in tokenUsedCount)) { @@ -157,8 +166,8 @@ function SelectDataModal({ }); } - function handleTokenChange(event, token) { - setTokens((prevTokens) => ({ + function handleTokenChange(event: ChangeEvent, token: any) { + setTokens((prevTokens: any) => ({ ...prevTokens, [token.id]: { ...token, checked: event.target.checked }, })); @@ -167,14 +176,14 @@ function SelectDataModal({ // Some tokens are checked not by maps or all tokens are checked by maps const tokensSelectChecked = Object.values(tokens).some( - (token) => !(token.id in tokenUsedCount) && token.checked - ) || Object.values(tokens).every((token) => token.id in tokenUsedCount); + (token: any) => !(token.id in tokenUsedCount) && token.checked + ) || Object.values(tokens).every((token: any) => token.id in tokenUsedCount); return ( - {Object.values(maps).map((map) => ( + {Object.values(maps).map((map: any) => ( Tokens - {Object.values(tokens).map((token) => ( + {Object.values(tokens).map((token: any) => ( @@ -185,13 +191,13 @@ function SettingsModal({ isOpen, onRequestClose }) { Import / Export Data - {storageEstimate && ( + {storageEstimate !&& ( - Storage Used: {prettyBytes(storageEstimate.usage)} of{" "} - {prettyBytes(storageEstimate.quota)} ( + Storage Used: {prettyBytes(storageEstimate.usage as number)} of{" "} + {prettyBytes(storageEstimate.quota as number)} ( {Math.round( - (storageEstimate.usage / Math.max(storageEstimate.quota, 1)) * + (storageEstimate.usage as number / Math.max(storageEstimate.quota as number, 1)) * 100 )} %) diff --git a/src/modals/StartModal.js b/src/modals/StartModal.tsx similarity index 83% rename from src/modals/StartModal.js rename to src/modals/StartModal.tsx index 7aa23fd..58f0c70 100644 --- a/src/modals/StartModal.js +++ b/src/modals/StartModal.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from "react"; +import { ChangeEvent, useRef } from "react"; import { Box, Label, Input, Button, Flex, Checkbox } from "theme-ui"; import { useHistory } from "react-router-dom"; import shortid from "shortid"; @@ -9,20 +9,20 @@ import useSetting from "../hooks/useSetting"; import Modal from "../components/Modal"; -function StartModal({ isOpen, onRequestClose }) { +function StartModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void}) { let history = useHistory(); const { password, setPassword } = useAuth(); - function handlePasswordChange(event) { + function handlePasswordChange(event: ChangeEvent) { setPassword(event.target.value); } const [usePassword, setUsePassword] = useSetting("game.usePassword"); - function handleUsePasswordChange(event) { + function handleUsePasswordChange(event: ChangeEvent) { setUsePassword(event.target.checked); } - function handleSubmit(event) { + function handleSubmit(event: ChangeEvent) { event.preventDefault(); if (!usePassword) { setPassword(""); @@ -30,7 +30,7 @@ function StartModal({ isOpen, onRequestClose }) { history.push(`/game/${shortid.generate()}`); } - const inputRef = useRef(); + const inputRef = useRef(); function focusInput() { inputRef.current && inputRef.current.focus(); } diff --git a/src/modals/StartStreamModal.js b/src/modals/StartStreamModal.tsx similarity index 80% rename from src/modals/StartStreamModal.js rename to src/modals/StartStreamModal.tsx index 9d44f98..03ac49c 100644 --- a/src/modals/StartStreamModal.js +++ b/src/modals/StartStreamModal.tsx @@ -1,8 +1,19 @@ -import React from "react"; import { Box, Text, Button, Label, Flex } from "theme-ui"; import Modal from "../components/Modal"; +type StartStreamProps = { + isOpen: boolean, + onRequestClose: () => void, + isSupported: boolean, + unavailableMessage: string, + stream: MediaStream, + noAudioTrack: boolean, + noAudioMessage: string, + onStreamStart: any, + onStreamEnd: any, +} + function StartStreamModal({ isOpen, onRequestClose, @@ -13,7 +24,7 @@ function StartStreamModal({ noAudioMessage, onStreamStart, onStreamEnd, -}) { +}: StartStreamProps) { return ( diff --git a/src/modals/StartTimerModal.js b/src/modals/StartTimerModal.tsx similarity index 90% rename from src/modals/StartTimerModal.js rename to src/modals/StartTimerModal.tsx index b6fdd94..d2a021f 100644 --- a/src/modals/StartTimerModal.js +++ b/src/modals/StartTimerModal.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from "react"; +import { ChangeEvent, useRef } from "react"; import { Box, Label, Input, Button, Flex, Text } from "theme-ui"; import Modal from "../components/Modal"; @@ -7,14 +7,22 @@ import { getHMSDuration, getDurationHMS } from "../helpers/timer"; import useSetting from "../hooks/useSetting"; +type StartTimerProps = { + isOpen: boolean, + onRequestClose: () => void, + onTimerStart: any, + onTimerStop: any, + timer: any, +} + function StartTimerModal({ isOpen, onRequestClose, onTimerStart, onTimerStop, timer, -}) { - const inputRef = useRef(); +}: StartTimerProps) { + const inputRef = useRef(); function focusInput() { inputRef.current && inputRef.current.focus(); } @@ -23,7 +31,7 @@ function StartTimerModal({ const [minute, setMinute] = useSetting("timer.minute"); const [second, setSecond] = useSetting("timer.second"); - function handleSubmit(event) { + function handleSubmit(event: ChangeEvent) { event.preventDefault(); if (timer) { onTimerStop(); @@ -44,7 +52,7 @@ function StartTimerModal({ paddingLeft: 0, }; - function parseValue(value, max) { + function parseValue(value: string, max: number) { const num = parseInt(value); if (isNaN(num)) { return 0; diff --git a/src/tokens/index.ts b/src/tokens/index.ts index 122975e..9933d8f 100644 --- a/src/tokens/index.ts +++ b/src/tokens/index.ts @@ -32,6 +32,7 @@ import undead from "./Undead.png"; import warlock from "./Warlock.png"; import wizard from "./Wizard.png"; import unknown from "./Unknown.png"; +import { ImageFile } from "../helpers/image"; export const tokenSources = { barbarian, @@ -80,7 +81,40 @@ function getDefaultTokenSize(key: string) { } } -export const tokens = Object.keys(tokenSources).map((key) => ({ +type TokenCategory = "character" | "vehicle" | "prop" + +export type Token = { + id: string, + name: string, + defaultSize: number, + category: TokenCategory, + hideInSidebar: boolean, + width: number, + height: number, + owner: string, + type: string, + group: string | undefined, + created: number, + lastModified: number, + lastUsed: number, +} + +export interface DefaultToken extends Omit { + id?: string, + owner?: string, + created?: number, + lastModified?: number, + lastUsed?: number, + key: string, + type: "default", + group: "default", +} +export interface FileToken extends Token { + file: Uint8Array, + thumbnail: ImageFile, + type: "file", +} +export const tokens: DefaultToken[] = Object.keys(tokenSources).map((key) => ({ key, name: Case.capital(key), type: "default", @@ -89,6 +123,7 @@ export const tokens = Object.keys(tokenSources).map((key) => ({ hideInSidebar: false, width: 256, height: 256, + group: "default", })); export const unknownSource = unknown; From c590adf83692ccb1d7f8fbb23213ef4e6e8f1ae9 Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Wed, 2 Jun 2021 19:06:45 +1000 Subject: [PATCH 009/142] Converted /components/banner to typescript --- src/components/banner/{Banner.js => Banner.tsx} | 9 +++++++-- .../banner/{ErrorBanner.js => ErrorBanner.tsx} | 3 +-- .../banner/{OfflineBanner.js => OfflineBanner.tsx} | 3 +-- .../banner/{ReconnectBanner.js => ReconnectBanner.tsx} | 3 +-- 4 files changed, 10 insertions(+), 8 deletions(-) rename src/components/banner/{Banner.js => Banner.tsx} (85%) rename src/components/banner/{ErrorBanner.js => ErrorBanner.tsx} (76%) rename src/components/banner/{OfflineBanner.js => OfflineBanner.tsx} (91%) rename src/components/banner/{ReconnectBanner.js => ReconnectBanner.tsx} (91%) diff --git a/src/components/banner/Banner.js b/src/components/banner/Banner.tsx similarity index 85% rename from src/components/banner/Banner.js rename to src/components/banner/Banner.tsx index 6a64017..5e8eb08 100644 --- a/src/components/banner/Banner.js +++ b/src/components/banner/Banner.tsx @@ -1,4 +1,3 @@ -import React from "react"; import Modal from "react-modal"; import { useThemeUI, Close } from "theme-ui"; @@ -8,6 +7,12 @@ function Banner({ children, allowClose, backgroundColor, +}: { + isOpen: boolean, + onRequestClose: any, + children: any, + allowClose: boolean, + backgroundColor?: any }) { const { theme } = useThemeUI(); @@ -18,7 +23,7 @@ function Banner({ style={{ overlay: { bottom: "0", top: "initial", zIndex: 2000 }, content: { - backgroundColor: backgroundColor || theme.colors.highlight, + backgroundColor: backgroundColor || theme.colors?.highlight, color: "hsl(210, 50%, 96%)", top: "initial", left: "50%", diff --git a/src/components/banner/ErrorBanner.js b/src/components/banner/ErrorBanner.tsx similarity index 76% rename from src/components/banner/ErrorBanner.js rename to src/components/banner/ErrorBanner.tsx index 6bb3574..cdfac99 100644 --- a/src/components/banner/ErrorBanner.js +++ b/src/components/banner/ErrorBanner.tsx @@ -1,9 +1,8 @@ -import React from "react"; import { Box, Text } from "theme-ui"; import Banner from "./Banner"; -function ErrorBanner({ error, onRequestClose }) { +function ErrorBanner({ error, onRequestClose }: { error: Error | undefined, onRequestClose: any }) { return ( diff --git a/src/components/banner/OfflineBanner.js b/src/components/banner/OfflineBanner.tsx similarity index 91% rename from src/components/banner/OfflineBanner.js rename to src/components/banner/OfflineBanner.tsx index d5d90ad..2680e0a 100644 --- a/src/components/banner/OfflineBanner.js +++ b/src/components/banner/OfflineBanner.tsx @@ -1,10 +1,9 @@ -import React from "react"; import { Flex } from "theme-ui"; import Banner from "./Banner"; import OfflineIcon from "../../icons/OfflineIcon"; -function OfflineBanner({ isOpen }) { +function OfflineBanner({ isOpen }: { isOpen: boolean }) { return ( Date: Thu, 3 Jun 2021 15:31:18 +1000 Subject: [PATCH 010/142] Added all files successfully converted --- package.json | 4 +- src/components/dice/DiceTrayOverlay.js | 4 +- src/components/map/Map.tsx | 13 +- ...oadingOverlay.js => MapLoadingOverlay.tsx} | 9 +- ...mberButton.js => AddPartyMemberButton.tsx} | 2 +- ...nameButton.js => ChangeNicknameButton.tsx} | 10 +- .../party/{DiceRoll.js => DiceRoll.tsx} | 7 +- .../party/{DiceRolls.js => DiceRolls.tsx} | 14 ++- .../{DiceTrayButton.js => DiceTrayButton.tsx} | 6 +- .../party/{Nickname.js => Nickname.tsx} | 4 +- src/components/party/{Party.js => Party.tsx} | 36 +++--- src/components/party/PartyState.ts | 39 ++++++ ...tStreamButton.js => StartStreamButton.tsx} | 10 +- ...artTimerButton.js => StartTimerButton.tsx} | 2 +- .../party/{Stream.js => Stream.tsx} | 19 +-- src/components/party/{Timer.js => Timer.tsx} | 6 +- .../{AuthContext.js => AuthContext.tsx} | 20 +-- ...DatabaseContext.js => DatabaseContext.tsx} | 25 ++-- ...adingContext.js => DiceLoadingContext.tsx} | 14 ++- .../{GridContext.js => GridContext.tsx} | 17 ++- ...ourceContext.js => ImageSourceContext.tsx} | 33 ++--- .../{MapDataContext.js => MapDataContext.tsx} | 117 +++++++++++------- ...onContext.js => MapInteractionContext.tsx} | 20 +-- ...oadingContext.js => MapLoadingContext.tsx} | 12 +- ...MapStageContext.js => MapStageContext.tsx} | 2 +- .../{PartyContext.js => PartyContext.tsx} | 8 +- .../{PlayerContext.js => PlayerContext.tsx} | 23 ++-- ...SettingsContext.js => SettingsContext.tsx} | 4 +- src/dice/{Dice.js => Dice.ts} | 48 ++++--- .../diceTray/{DiceTray.js => DiceTray.ts} | 41 ++++-- .../galaxy/{GalaxyDice.js => GalaxyDice.ts} | 12 +- .../{GemstoneDice.js => GemstoneDice.ts} | 18 +-- src/dice/glass/{GlassDice.js => GlassDice.ts} | 18 +-- src/dice/{index.js => index.ts} | 9 +- src/dice/iron/{IronDice.js => IronDice.ts} | 13 +- .../nebula/{NebulaDice.js => NebulaDice.ts} | 11 +- .../{SunriseDice.js => SunriseDice.ts} | 11 +- .../sunset/{SunsetDice.js => SunsetDice.ts} | 11 +- .../walnut/{WalnutDice.js => WalnutDice.ts} | 13 +- src/helpers/babylon.ts | 2 +- src/hooks/{useDebounce.js => useDebounce.tsx} | 2 +- ...etworkedState.js => useNetworkedState.tsx} | 26 ++-- src/maps/index.ts | 3 +- src/modals/EditMapModal.tsx | 2 +- src/modals/SelectMapModal.tsx | 12 +- src/modals/StartStreamModal.tsx | 4 +- ...AndTokens.js => NetworkedMapAndTokens.tsx} | 115 ++++++++--------- ...dMapPointer.js => NetworkedMapPointer.tsx} | 27 ++-- .../{NetworkedParty.js => NetworkedParty.tsx} | 52 ++++---- src/network/Session.ts | 4 +- src/routes/{About.js => About.tsx} | 1 - src/routes/{Donate.js => Donate.tsx} | 34 +++-- src/routes/{FAQ.js => FAQ.tsx} | 1 - src/routes/{Game.js => Game.tsx} | 12 +- src/routes/{HowTo.js => HowTo.tsx} | 1 - .../{ReleaseNotes.js => ReleaseNotes.tsx} | 1 - tsconfig.json | 1 + yarn.lock | 82 ++++++------ 58 files changed, 622 insertions(+), 445 deletions(-) rename src/components/map/{MapLoadingOverlay.js => MapLoadingOverlay.tsx} (92%) rename src/components/party/{AddPartyMemberButton.js => AddPartyMemberButton.tsx} (92%) rename src/components/party/{ChangeNicknameButton.js => ChangeNicknameButton.tsx} (77%) rename src/components/party/{DiceRoll.js => DiceRoll.tsx} (60%) rename src/components/party/{DiceRolls.js => DiceRolls.tsx} (87%) rename src/components/party/{DiceTrayButton.js => DiceTrayButton.tsx} (92%) rename src/components/party/{Nickname.js => Nickname.tsx} (75%) rename src/components/party/{Party.js => Party.tsx} (79%) create mode 100644 src/components/party/PartyState.ts rename src/components/party/{StartStreamButton.js => StartStreamButton.tsx} (88%) rename src/components/party/{StartTimerButton.js => StartTimerButton.tsx} (87%) rename src/components/party/{Stream.js => Stream.tsx} (90%) rename src/components/party/{Timer.js => Timer.tsx} (91%) rename src/contexts/{AuthContext.js => AuthContext.tsx} (60%) rename src/contexts/{DatabaseContext.js => DatabaseContext.tsx} (69%) rename src/contexts/{DiceLoadingContext.js => DiceLoadingContext.tsx} (65%) rename src/contexts/{GridContext.js => GridContext.tsx} (93%) rename src/contexts/{ImageSourceContext.js => ImageSourceContext.tsx} (77%) rename src/contexts/{MapDataContext.js => MapDataContext.tsx} (69%) rename src/contexts/{MapInteractionContext.js => MapInteractionContext.tsx} (82%) rename src/contexts/{MapLoadingContext.js => MapLoadingContext.tsx} (80%) rename src/contexts/{MapStageContext.js => MapStageContext.tsx} (85%) rename src/contexts/{PartyContext.js => PartyContext.tsx} (71%) rename src/contexts/{PlayerContext.js => PlayerContext.tsx} (78%) rename src/contexts/{SettingsContext.js => SettingsContext.tsx} (86%) rename src/dice/{Dice.js => Dice.ts} (67%) rename src/dice/diceTray/{DiceTray.js => DiceTray.ts} (84%) rename src/dice/galaxy/{GalaxyDice.js => GalaxyDice.ts} (68%) rename src/dice/gemstone/{GemstoneDice.js => GemstoneDice.ts} (79%) rename src/dice/glass/{GlassDice.js => GlassDice.ts} (80%) rename src/dice/{index.js => index.ts} (87%) rename src/dice/iron/{IronDice.js => IronDice.ts} (73%) rename src/dice/nebula/{NebulaDice.js => NebulaDice.ts} (73%) rename src/dice/sunrise/{SunriseDice.js => SunriseDice.ts} (73%) rename src/dice/sunset/{SunsetDice.js => SunsetDice.ts} (73%) rename src/dice/walnut/{WalnutDice.js => WalnutDice.ts} (80%) rename src/hooks/{useDebounce.js => useDebounce.tsx} (86%) rename src/hooks/{useNetworkedState.js => useNetworkedState.tsx} (85%) rename src/network/{NetworkedMapAndTokens.js => NetworkedMapAndTokens.tsx} (81%) rename src/network/{NetworkedMapPointer.js => NetworkedMapPointer.tsx} (86%) rename src/network/{NetworkedParty.js => NetworkedParty.tsx} (69%) rename src/routes/{About.js => About.tsx} (98%) rename src/routes/{Donate.js => Donate.tsx} (80%) rename src/routes/{FAQ.js => FAQ.tsx} (98%) rename src/routes/{Game.js => Game.tsx} (92%) rename src/routes/{HowTo.js => HowTo.tsx} (99%) rename src/routes/{ReleaseNotes.js => ReleaseNotes.tsx} (99%) diff --git a/package.json b/package.json index b83f00b..6f426c6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@msgpack/msgpack": "^2.4.1", "@sentry/react": "^6.2.2", "@stripe/stripe-js": "^1.13.1", - "@tensorflow/tfjs": "^3.3.0", + "@tensorflow/tfjs": "^3.6.0", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.0.2", @@ -87,7 +87,9 @@ "devDependencies": { "@types/color": "^3.0.1", "@types/deep-diff": "^1.0.0", + "@types/file-saver": "^2.0.2", "@types/jest": "^26.0.23", + "@types/lodash.clonedeep": "^4.5.6", "@types/lodash.get": "^4.4.6", "@types/node": "^15.6.0", "@types/react": "^17.0.6", diff --git a/src/components/dice/DiceTrayOverlay.js b/src/components/dice/DiceTrayOverlay.js index 62028a3..2c19088 100644 --- a/src/components/dice/DiceTrayOverlay.js +++ b/src/components/dice/DiceTrayOverlay.js @@ -23,7 +23,7 @@ import useSetting from "../../hooks/useSetting"; function DiceTrayOverlay({ isOpen, shareDice, - onShareDiceChage, + onShareDiceChange, diceRolls, onDiceRollsChange, }) { @@ -345,7 +345,7 @@ function DiceTrayOverlay({ onDiceTraySizeChange={setDiceTraySize} diceTraySize={diceTraySize} shareDice={shareDice} - onShareDiceChange={onShareDiceChage} + onShareDiceChange={onShareDiceChange} loading={isLoading} /> {isLoading && ( diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index 8f8f539..90448d5 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -29,15 +29,14 @@ import Session from "../../network/Session"; import { Grid } from "../../helpers/grid"; import { ImageFile } from "../../helpers/image"; -type Resolutions = Record - +export type Resolutions = Record export type Map = { id: string, name: string, owner: string, - file: Uint8Array, - quality: string, - resolutions: Resolutions, + file?: Uint8Array, + quality?: string, + resolutions?: Resolutions, grid: Grid, group: string, width: number, @@ -48,7 +47,7 @@ export type Map = { created: number, showGrid: boolean, snapToGrid: boolean, - thumbnail: ImageFile, + thumbnail?: ImageFile, } export type Note = { @@ -92,7 +91,7 @@ export type MapState = { tokens: Record, drawShapes: PathId | ShapeId, fogShapes: Fog[], - editFlags: string[], + editFlags: ["drawing", "tokens", "notes", "fog"], notes: Note[], mapId: string, } diff --git a/src/components/map/MapLoadingOverlay.js b/src/components/map/MapLoadingOverlay.tsx similarity index 92% rename from src/components/map/MapLoadingOverlay.js rename to src/components/map/MapLoadingOverlay.tsx index e3b77b6..9c191b3 100644 --- a/src/components/map/MapLoadingOverlay.js +++ b/src/components/map/MapLoadingOverlay.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Box } from "theme-ui"; import { useMapLoading } from "../../contexts/MapLoadingContext"; @@ -8,8 +7,11 @@ import LoadingBar from "../LoadingBar"; function MapLoadingOverlay() { const { isLoading, loadingProgressRef } = useMapLoading(); + if (!isLoading) { + return null; + } + return ( - isLoading && ( - ) - ); + ); } export default MapLoadingOverlay; diff --git a/src/components/party/AddPartyMemberButton.js b/src/components/party/AddPartyMemberButton.tsx similarity index 92% rename from src/components/party/AddPartyMemberButton.js rename to src/components/party/AddPartyMemberButton.tsx index b2932b1..4e80e1d 100644 --- a/src/components/party/AddPartyMemberButton.js +++ b/src/components/party/AddPartyMemberButton.tsx @@ -4,7 +4,7 @@ import { IconButton } from "theme-ui"; import AddPartyMemberModal from "../../modals/AddPartyMemberModal"; import AddPartyMemberIcon from "../../icons/AddPartyMemberIcon"; -function AddPartyMemberButton({ gameId }) { +function AddPartyMemberButton({ gameId }: { gameId: string }) { const [isAddModalOpen, setIsAddModalOpen] = useState(false); function openModal() { setIsAddModalOpen(true); diff --git a/src/components/party/ChangeNicknameButton.js b/src/components/party/ChangeNicknameButton.tsx similarity index 77% rename from src/components/party/ChangeNicknameButton.js rename to src/components/party/ChangeNicknameButton.tsx index 20231af..be599e3 100644 --- a/src/components/party/ChangeNicknameButton.js +++ b/src/components/party/ChangeNicknameButton.tsx @@ -1,10 +1,10 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, ChangeEvent } from "react"; import { IconButton } from "theme-ui"; import ChangeNicknameModal from "../../modals/ChangeNicknameModal"; import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon"; -function ChangeNicknameButton({ nickname, onChange }) { +function ChangeNicknameButton({ nickname, onChange }: { nickname: string, onChange: any}) { const [isChangeModalOpen, setIsChangeModalOpen] = useState(false); function openModal() { setIsChangeModalOpen(true); @@ -19,14 +19,14 @@ function ChangeNicknameButton({ nickname, onChange }) { setChangedNickname(nickname); }, [nickname]); - function handleChangeSubmit(event) { + function handleChangeSubmit(event: Event) { event.preventDefault(); onChange(changedNickname); closeModal(); } - function handleChange(event) { - setChangedNickname(event.target.value); + function handleChange(event: ChangeEvent) { + setChangedNickname(event.target?.value); } return ( <> diff --git a/src/components/party/DiceRoll.js b/src/components/party/DiceRoll.tsx similarity index 60% rename from src/components/party/DiceRoll.js rename to src/components/party/DiceRoll.tsx index f04d11b..e770205 100644 --- a/src/components/party/DiceRoll.js +++ b/src/components/party/DiceRoll.tsx @@ -1,13 +1,12 @@ -import React from "react"; import { Flex, Box, Text } from "theme-ui"; -function DiceRoll({ rolls, type, children }) { +function DiceRoll({ rolls, type, children }: { rolls: any, type: string, children: any}) { return ( {children} {rolls - .filter((d) => d.type === type && d.roll !== "unknown") - .map((dice, index) => ( + .filter((d: any) => d.type === type && d.roll !== "unknown") + .map((dice: any, index: string | number) => ( {dice.roll} diff --git a/src/components/party/DiceRolls.js b/src/components/party/DiceRolls.tsx similarity index 87% rename from src/components/party/DiceRolls.js rename to src/components/party/DiceRolls.tsx index b20b3bd..3798595 100644 --- a/src/components/party/DiceRolls.js +++ b/src/components/party/DiceRolls.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { Flex, Text, IconButton } from "theme-ui"; import DiceRollsIcon from "../../icons/DiceRollsIcon"; @@ -24,14 +24,14 @@ const diceIcons = [ { type: "d100", Icon: D100Icon }, ]; -function DiceRolls({ rolls }) { +function DiceRolls({ rolls }: { rolls: any }) { const total = getDiceRollTotal(rolls); - const [expanded, setExpanded] = useState(false); + const [expanded, setExpanded] = useState(false); let expandedRolls = []; for (let icon of diceIcons) { - if (rolls.some((roll) => roll.type === icon.type)) { + if (rolls.some((roll: any) => roll.type === icon.type)) { expandedRolls.push( @@ -40,8 +40,11 @@ function DiceRolls({ rolls }) { } } + if (total <= 0) { + return null; + } + return ( - total > 0 && ( )} - ) ); } diff --git a/src/components/party/DiceTrayButton.js b/src/components/party/DiceTrayButton.tsx similarity index 92% rename from src/components/party/DiceTrayButton.js rename to src/components/party/DiceTrayButton.tsx index d4591a9..1a00263 100644 --- a/src/components/party/DiceTrayButton.js +++ b/src/components/party/DiceTrayButton.tsx @@ -13,10 +13,10 @@ const DiceTrayOverlay = React.lazy(() => import("../dice/DiceTrayOverlay")); function DiceTrayButton({ shareDice, - onShareDiceChage, + onShareDiceChange, diceRolls, onDiceRollsChange, -}) { +}: { shareDice: boolean, onShareDiceChange: any, diceRolls: [], onDiceRollsChange: any}) { const [isExpanded, setIsExpanded] = useState(false); const [fullScreen] = useSetting("map.fullScreen"); @@ -69,7 +69,7 @@ function DiceTrayButton({ diff --git a/src/components/party/Nickname.js b/src/components/party/Nickname.tsx similarity index 75% rename from src/components/party/Nickname.js rename to src/components/party/Nickname.tsx index 12cdd33..00b316a 100644 --- a/src/components/party/Nickname.js +++ b/src/components/party/Nickname.tsx @@ -1,10 +1,10 @@ -import React from "react"; import { Text, Flex } from "theme-ui"; import Stream from "./Stream"; import DiceRolls from "./DiceRolls"; -function Nickname({ nickname, stream, diceRolls }) { +// TODO: check if stream is a required or optional param +function Nickname({ nickname, stream, diceRolls }: { nickname: string, stream?: any, diceRolls: any}) { return ( ({ ...prevState, timer: newTimer })); + function handleTimerStart(newTimer: PartyTimer) { + setPlayerState((prevState: any) => ({ ...prevState, timer: newTimer })); } function handleTimerStop() { - setPlayerState((prevState) => ({ ...prevState, timer: null })); + setPlayerState((prevState: any) => ({ ...prevState, timer: null })); } useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(update); let counter = 0; - function update(time) { + function update(time: any) { request = requestAnimationFrame(update); const deltaTime = time - prevTime; prevTime = time; @@ -45,14 +46,14 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }) { counter += deltaTime; // Update timer every second if (counter > 1000) { - const newTimer = { + const newTimer: PartyTimer = { ...playerState.timer, current: playerState.timer.current - counter, }; if (newTimer.current < 0) { - setPlayerState((prevState) => ({ ...prevState, timer: null })); + setPlayerState((prevState: any) => ({ ...prevState, timer: null })); } else { - setPlayerState((prevState) => ({ ...prevState, timer: newTimer })); + setPlayerState((prevState: any) => ({ ...prevState, timer: newTimer })); } counter = 0; } @@ -63,13 +64,13 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }) { }; }, [playerState.timer, setPlayerState]); - function handleNicknameChange(newNickname) { - setPlayerState((prevState) => ({ ...prevState, nickname: newNickname })); + function handleNicknameChange(newNickname: string) { + setPlayerState((prevState: any) => ({ ...prevState, nickname: newNickname })); } - function handleDiceRollsChange(newDiceRolls) { + function handleDiceRollsChange(newDiceRolls: number[]) { setPlayerState( - (prevState) => ({ + (prevState: PlayerDice) => ({ ...prevState, dice: { share: shareDice, rolls: newDiceRolls }, }), @@ -77,9 +78,9 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }) { ); } - function handleShareDiceChange(newShareDice) { + function handleShareDiceChange(newShareDice: boolean) { setShareDice(newShareDice); - setPlayerState((prevState) => ({ + setPlayerState((prevState: PlayerInfo) => ({ ...prevState, dice: { ...prevState.dice, share: newShareDice }, })); @@ -122,6 +123,7 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }) { height: "calc(100% - 232px)", }} > + {/* TODO: check if stream is required here */} diff --git a/src/components/party/PartyState.ts b/src/components/party/PartyState.ts new file mode 100644 index 0000000..194de30 --- /dev/null +++ b/src/components/party/PartyState.ts @@ -0,0 +1,39 @@ +/** + * @typedef {object} Timer + * @property {number} current + * @property {number} max + */ +export type Timer = { + current: number, + max: number +} + +/** + * @typedef {object} PlayerDice + * @property {boolean} share + * @property {[]} rolls + */ +export type PlayerDice = { share: boolean, rolls: [] } + +/** + * @typedef {object} PlayerInfo + * @property {string} nickname + * @property {Timer | null} timer + * @property {PlayerDice} dice + * @property {string} sessionId + * @property {string} userId + */ +export type PlayerInfo = { + nickname: string, + timer: Timer | null, + dice: PlayerDice, + sessionId: string, + userId: string +} + +/** + * @typedef {object} PartyState + * @property {string} player + * @property {PlayerInfo} playerInfo + */ +export type PartyState = { [player: string]: PlayerInfo } \ No newline at end of file diff --git a/src/components/party/StartStreamButton.js b/src/components/party/StartStreamButton.tsx similarity index 88% rename from src/components/party/StartStreamButton.js rename to src/components/party/StartStreamButton.tsx index df41e9d..23e8de0 100644 --- a/src/components/party/StartStreamButton.js +++ b/src/components/party/StartStreamButton.tsx @@ -6,7 +6,7 @@ import Link from "../Link"; import StartStreamModal from "../../modals/StartStreamModal"; -function StartStreamButton({ onStreamStart, onStreamEnd, stream }) { +function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamStart: any, onStreamEnd: any, stream: any}) { const [isStreamModalOpoen, setIsStreamModalOpen] = useState(false); function openModal() { setIsStreamModalOpen(true); @@ -44,7 +44,9 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }) { const [noAudioTrack, setNoAudioTrack] = useState(false); function handleStreamStart() { - navigator.mediaDevices + // Must be defined this way in typescript due to open issue - https://github.com/microsoft/TypeScript/issues/33232 + const mediaDevices = navigator.mediaDevices as any; + mediaDevices .getDisplayMedia({ video: true, audio: { @@ -53,10 +55,10 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }) { echoCancellation: false, }, }) - .then((localStream) => { + .then((localStream: { getTracks: () => any; }) => { const tracks = localStream.getTracks(); - const hasAudio = tracks.some((track) => track.kind === "audio"); + const hasAudio = tracks.some((track: { kind: string; }) => track.kind === "audio"); setNoAudioTrack(!hasAudio); // Ensure an audio track is present diff --git a/src/components/party/StartTimerButton.js b/src/components/party/StartTimerButton.tsx similarity index 87% rename from src/components/party/StartTimerButton.js rename to src/components/party/StartTimerButton.tsx index 266ba6f..3c413fc 100644 --- a/src/components/party/StartTimerButton.js +++ b/src/components/party/StartTimerButton.tsx @@ -4,7 +4,7 @@ import { IconButton } from "theme-ui"; import StartTimerModal from "../../modals/StartTimerModal"; import StartTimerIcon from "../../icons/StartTimerIcon"; -function StartTimerButton({ onTimerStart, onTimerStop, timer }) { +function StartTimerButton({ onTimerStart, onTimerStop, timer }: { onTimerStart: any, onTimerStop: any, timer: any }) { const [isTimerModalOpen, setIsTimerModalOpen] = useState(false); function openModal() { diff --git a/src/components/party/Stream.js b/src/components/party/Stream.tsx similarity index 90% rename from src/components/party/Stream.js rename to src/components/party/Stream.tsx index 631f4e1..6dc5eec 100644 --- a/src/components/party/Stream.js +++ b/src/components/party/Stream.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, ChangeEvent } from "react"; import { Text, IconButton, Box, Flex } from "theme-ui"; import StreamMuteIcon from "../../icons/StreamMuteIcon"; @@ -6,13 +6,13 @@ import StreamMuteIcon from "../../icons/StreamMuteIcon"; import Banner from "../banner/Banner"; import Slider from "../Slider"; -function Stream({ stream, nickname }) { +function Stream({ stream, nickname }: { stream: MediaStream, nickname: string }) { const [streamVolume, setStreamVolume] = useState(1); const [showStreamInteractBanner, setShowStreamInteractBanner] = useState( false ); const [streamMuted, setStreamMuted] = useState(false); - const audioRef = useRef(); + const audioRef = useRef(); useEffect(() => { if (audioRef.current) { @@ -44,8 +44,8 @@ function Stream({ stream, nickname }) { } } - function handleVolumeChange(event) { - const volume = parseFloat(event.target.value); + function handleVolumeChange(event: ChangeEvent) { + const volume = parseFloat(event.target?.value); setStreamVolume(volume); } @@ -63,7 +63,8 @@ function Stream({ stream, nickname }) { setTimeout(() => { setIsVolumeControlAvailable(audio.volume === 0.5); audio.volume = prevVolume; - }, [100]); + // TODO: check if this supposed to be a number or number[] + }, 100); } audio.addEventListener("playing", checkVolumeControlAvailable); @@ -74,10 +75,10 @@ function Stream({ stream, nickname }) { }, []); // Use an audio context gain node to control volume to go past 100% - const audioGainRef = useRef(); + const audioGainRef = useRef(); useEffect(() => { - let audioContext; - if (stream && !streamMuted && isVolumeControlAvailable) { + let audioContext: AudioContext; + if (stream && !streamMuted && isVolumeControlAvailable && audioGainRef) { audioContext = new AudioContext(); let source = audioContext.createMediaStreamSource(stream); let gainNode = audioContext.createGain(); diff --git a/src/components/party/Timer.js b/src/components/party/Timer.tsx similarity index 91% rename from src/components/party/Timer.js rename to src/components/party/Timer.tsx index 51b576a..3f208ce 100644 --- a/src/components/party/Timer.js +++ b/src/components/party/Timer.tsx @@ -4,8 +4,8 @@ import { Box, Progress } from "theme-ui"; import usePortal from "../../hooks/usePortal"; -function Timer({ timer, index }) { - const progressBarRef = useRef(); +function Timer({ timer, index }: { timer: any, index: number}) { + const progressBarRef = useRef(); useEffect(() => { if (progressBarRef.current && timer) { @@ -16,7 +16,7 @@ function Timer({ timer, index }) { useEffect(() => { let request = requestAnimationFrame(animate); let previousTime = performance.now(); - function animate(time) { + function animate(time: any) { request = requestAnimationFrame(animate); const deltaTime = time - previousTime; previousTime = time; diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.tsx similarity index 60% rename from src/contexts/AuthContext.js rename to src/contexts/AuthContext.tsx index 868fe7e..70f44ea 100644 --- a/src/contexts/AuthContext.js +++ b/src/contexts/AuthContext.tsx @@ -1,13 +1,16 @@ -import React, { useState, useEffect, useContext } from "react"; +import React, { useState, useEffect, useContext, SetStateAction } from "react"; import shortid from "shortid"; import { useDatabase } from "./DatabaseContext"; import FakeStorage from "../helpers/FakeStorage"; -const AuthContext = React.createContext(); +type AuthContext = { userId: string; password: string; setPassword: React.Dispatch; } -let storage; +// TODO: check what default value we want here +const AuthContext = React.createContext(undefined); + +let storage: Storage | FakeStorage; try { sessionStorage.setItem("__test", "__test"); sessionStorage.removeItem("__test"); @@ -17,28 +20,29 @@ try { storage = new FakeStorage(); } -export function AuthProvider({ children }) { +export function AuthProvider({ children }: { children: any }) { const { database, databaseStatus } = useDatabase(); - const [password, setPassword] = useState(storage.getItem("auth") || ""); + const [password, setPassword] = useState(storage.getItem("auth") || ""); useEffect(() => { storage.setItem("auth", password); }, [password]); - const [userId, setUserId] = useState(); + // TODO: check pattern here -> undefined or empty default values + const [userId, setUserId]: [ userId: string, setUserId: React.Dispatch> ] = useState(""); useEffect(() => { if (!database || databaseStatus === "loading") { return; } async function loadUserId() { - const storedUserId = await database.table("user").get("userId"); + const storedUserId = await database?.table("user").get("userId"); if (storedUserId) { setUserId(storedUserId.value); } else { const id = shortid.generate(); setUserId(id); - database.table("user").add({ key: "userId", value: id }); + database?.table("user").add({ key: "userId", value: id }); } } diff --git a/src/contexts/DatabaseContext.js b/src/contexts/DatabaseContext.tsx similarity index 69% rename from src/contexts/DatabaseContext.js rename to src/contexts/DatabaseContext.tsx index 5e1e3b5..8a9ee08 100644 --- a/src/contexts/DatabaseContext.js +++ b/src/contexts/DatabaseContext.tsx @@ -1,20 +1,25 @@ -import React, { useState, useEffect, useContext } from "react"; -import * as Comlink from "comlink"; +import React, { useState, useEffect, useContext, SetStateAction } from "react"; +import Comlink, { Remote } from "comlink"; import ErrorBanner from "../components/banner/ErrorBanner"; import { getDatabase } from "../database"; +//@ts-ignore import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax +import Dexie from "dexie"; -const DatabaseContext = React.createContext(); +type DatabaseContext = { database: Dexie | undefined; databaseStatus: any; databaseError: Error | undefined; worker: Remote; } + +// TODO: check what default we want here +const DatabaseContext = React.createContext< DatabaseContext | undefined>(undefined); const worker = Comlink.wrap(new DatabaseWorker()); -export function DatabaseProvider({ children }) { - const [database, setDatabase] = useState(); - const [databaseStatus, setDatabaseStatus] = useState("loading"); - const [databaseError, setDatabaseError] = useState(); +export function DatabaseProvider({ children }: { children: any}) { + const [database, setDatabase]: [ database: Dexie | undefined, setDatabase: React.Dispatch>] = useState(); + const [databaseStatus, setDatabaseStatus]: [ datebaseStatus: any, setDatabaseStatus: React.Dispatch>] = useState("loading"); + const [databaseError, setDatabaseError]: [ databaseError: Error | undefined, setDatabaseError: React.Dispatch>] = useState(); useEffect(() => { // Create a test database and open it to see if indexedDB is enabled @@ -43,7 +48,7 @@ export function DatabaseProvider({ children }) { window.indexedDB.deleteDatabase("__test"); }; - function handleDatabaseError(event) { + function handleDatabaseError(event: any) { event.preventDefault(); if (event.reason?.message.startsWith("QuotaExceededError")) { setDatabaseError({ @@ -77,14 +82,14 @@ export function DatabaseProvider({ children }) { {children} setDatabaseError()} + onRequestClose={() => setDatabaseError(undefined)} /> ); } -export function useDatabase() { +export function useDatabase(): DatabaseContext { const context = useContext(DatabaseContext); if (context === undefined) { throw new Error("useDatabase must be used within a DatabaseProvider"); diff --git a/src/contexts/DiceLoadingContext.js b/src/contexts/DiceLoadingContext.tsx similarity index 65% rename from src/contexts/DiceLoadingContext.js rename to src/contexts/DiceLoadingContext.tsx index 8bc9ed3..5f6fc43 100644 --- a/src/contexts/DiceLoadingContext.js +++ b/src/contexts/DiceLoadingContext.tsx @@ -1,8 +1,14 @@ -import React, { useState, useContext } from "react"; +import React, { useState, useContext, ReactChild } from "react"; -const DiceLoadingContext = React.createContext(); +type DiceLoadingContext = { + assetLoadStart: any, + assetLoadFinish: any, + isLoading: boolean, +} -export function DiceLoadingProvider({ children }) { +const DiceLoadingContext = React.createContext(undefined); + +export function DiceLoadingProvider({ children }: { children: ReactChild }) { const [loadingAssetCount, setLoadingAssetCount] = useState(0); function assetLoadStart() { @@ -28,7 +34,7 @@ export function DiceLoadingProvider({ children }) { ); } -export function useDiceLoading() { +export function useDiceLoading(): DiceLoadingContext { const context = useContext(DiceLoadingContext); if (context === undefined) { throw new Error("useDiceLoading must be used within a DiceLoadingProvider"); diff --git a/src/contexts/GridContext.js b/src/contexts/GridContext.tsx similarity index 93% rename from src/contexts/GridContext.js rename to src/contexts/GridContext.tsx index a4b173f..f7390d1 100644 --- a/src/contexts/GridContext.js +++ b/src/contexts/GridContext.tsx @@ -15,11 +15,20 @@ import { getGridPixelSize, getCellPixelSize, Grid } from "../helpers/grid"; * @property {number} gridStrokeWidth Stroke width of the grid in pixels * @property {Vector2} gridCellPixelOffset Offset of the grid cells to convert the center position of hex cells to the top left */ +type GridContextValue = { + grid: Grid, + gridPixelSize: Size, + gridCellPixelSize: Size, + gridCellNormalizedSize: Size, + gridOffset: Vector2, + gridStrokeWidth: number, + gridCellPixelOffset: Vector2 +} /** * @type {GridContextValue} */ -const defaultValue = { +const defaultValue: GridContextValue = { grid: { size: new Vector2(0, 0), inset: { topLeft: new Vector2(0, 0), bottomRight: new Vector2(1, 1) }, @@ -57,11 +66,11 @@ export const GridCellPixelOffsetContext = React.createContext( const defaultStrokeWidth = 1 / 10; -export function GridProvider({ grid: inputGrid, width, height, children }) { +export function GridProvider({ grid: inputGrid, width, height, children }: { grid: Required, width: number, height: number, children: any }) { let grid = inputGrid; - if (!grid?.size.x || !grid?.size.y) { - grid = defaultValue.grid; + if (!grid.size.x || !grid.size.y) { + grid = defaultValue.grid as Required; } const [gridPixelSize, setGridPixelSize] = useState( diff --git a/src/contexts/ImageSourceContext.js b/src/contexts/ImageSourceContext.tsx similarity index 77% rename from src/contexts/ImageSourceContext.js rename to src/contexts/ImageSourceContext.tsx index b558383..c566300 100644 --- a/src/contexts/ImageSourceContext.js +++ b/src/contexts/ImageSourceContext.tsx @@ -1,27 +1,28 @@ -import React, { useContext, useState, useEffect } from "react"; +import React, { useContext, useState, useEffect, ReactChild } from "react"; +import { ImageFile } from "../helpers/image"; import { omit } from "../helpers/shared"; -export const ImageSourcesStateContext = React.createContext(); -export const ImageSourcesUpdaterContext = React.createContext(() => {}); +export const ImageSourcesStateContext = React.createContext(undefined) as any; +export const ImageSourcesUpdaterContext = React.createContext(() => {}) as any; /** * Helper to manage sharing of custom image sources between uses of useImageSource */ -export function ImageSourcesProvider({ children }) { - const [imageSources, setImageSources] = useState({}); +export function ImageSourcesProvider({ children }: { children: ReactChild }) { + const [imageSources, setImageSources] = useState({}); // Revoke url when no more references useEffect(() => { - let sourcesToCleanup = []; - for (let source of Object.values(imageSources)) { + let sourcesToCleanup: any = []; + for (let source of Object.values(imageSources) as any) { if (source.references <= 0) { URL.revokeObjectURL(source.url); sourcesToCleanup.push(source.id); } } if (sourcesToCleanup.length > 0) { - setImageSources((prevSources) => omit(prevSources, sourcesToCleanup)); + setImageSources((prevSources: any) => omit(prevSources, sourcesToCleanup)); } }, [imageSources]); @@ -37,7 +38,7 @@ export function ImageSourcesProvider({ children }) { /** * Get id from image data */ -function getImageFileId(data, thumbnail) { +function getImageFileId(data: any, thumbnail: ImageFile) { if (thumbnail) { return `${data.id}-thumbnail`; } @@ -48,7 +49,7 @@ function getImageFileId(data, thumbnail) { } else if (!data.file) { // Fallback to the highest resolution const resolutionArray = Object.keys(data.resolutions); - const resolution = resolutionArray[resolutionArray.length - 1]; + const resolution: any = resolutionArray[resolutionArray.length - 1]; return `${data.id}-${resolution.id}`; } } @@ -58,14 +59,14 @@ function getImageFileId(data, thumbnail) { /** * Helper function to load either file or default image into a URL */ -export function useImageSource(data, defaultSources, unknownSource, thumbnail) { - const imageSources = useContext(ImageSourcesStateContext); +export function useImageSource(data: any, defaultSources: string, unknownSource: string, thumbnail: ImageFile) { + const imageSources: any = useContext(ImageSourcesStateContext); if (imageSources === undefined) { throw new Error( "useImageSource must be used within a ImageSourcesProvider" ); } - const setImageSources = useContext(ImageSourcesUpdaterContext); + const setImageSources: any = useContext(ImageSourcesUpdaterContext); if (setImageSources === undefined) { throw new Error( "useImageSource must be used within a ImageSourcesProvider" @@ -78,9 +79,9 @@ export function useImageSource(data, defaultSources, unknownSource, thumbnail) { } const id = getImageFileId(data, thumbnail); - function updateImageSource(file) { + function updateImageSource(file: File) { if (file) { - setImageSources((prevSources) => { + setImageSources((prevSources: any) => { if (id in prevSources) { // Check if the image source is already added return { @@ -124,7 +125,7 @@ export function useImageSource(data, defaultSources, unknownSource, thumbnail) { return () => { // Decrease references - setImageSources((prevSources) => { + setImageSources((prevSources: any) => { if (id in prevSources) { return { ...prevSources, diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.tsx similarity index 69% rename from src/contexts/MapDataContext.js rename to src/contexts/MapDataContext.tsx index ddd8dce..1f6f83e 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.tsx @@ -4,6 +4,7 @@ import React, { useContext, useCallback, useRef, + ReactChild, } from "react"; import * as Comlink from "comlink"; import { decode, encode } from "@msgpack/msgpack"; @@ -12,42 +13,66 @@ import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; import { maps as defaultMaps } from "../maps"; +import { Map, MapState, Note, TokenState } from "../components/map/Map"; +import { Fog } from "../helpers/drawing"; -const MapDataContext = React.createContext(); + +// TODO: fix differences in types between default maps and imported maps +type MapDataContext = { + maps: Array, + ownedMaps: Array + mapStates: MapState[], + addMap: (map: Map) => void, + removeMap: (id: string) => void, + removeMaps: (ids: string[]) => void, + resetMap: (id: string) => void, + updateMap: (id: string, update: Partial) => void, + updateMaps: (ids: string[], update: Partial) => void, + updateMapState: (id: string, update: Partial) => void, + putMap: (map: Map) => void, + getMap: (id: string) => Map | undefined, + getMapFromDB: (id: string) => Promise, + mapsLoading: boolean, + getMapStateFromDB: (id: string) => Promise, +} + +const MapDataContext = React.createContext(undefined); // Maximum number of maps to keep in the cache const cachedMapMax = 15; -const defaultMapState = { - tokens: {}, - drawShapes: {}, - fogShapes: {}, +const defaultMapState: MapState = { + mapId: "", + tokens: {} as Record, + drawShapes: {} as any, + fogShapes: {} as Fog[], // Flags to determine what other people can edit - editFlags: ["drawing", "tokens", "notes"], - notes: {}, + editFlags: ["drawing", "tokens", "notes", "fog"], + notes: {} as Note[], }; -export function MapDataProvider({ children }) { +export function MapDataProvider({ children }: { children: ReactChild }) { const { database, databaseStatus, worker } = useDatabase(); const { userId } = useAuth(); - const [maps, setMaps] = useState([]); - const [mapStates, setMapStates] = useState([]); - const [mapsLoading, setMapsLoading] = useState(true); + const [maps, setMaps] = useState>([]); + const [mapStates, setMapStates] = useState([]); + const [mapsLoading, setMapsLoading] = useState(true); - // Load maps from the database and ensure state is properly setup + // Load maps from the database and ensure state is properly seup useEffect(() => { if (!userId || !database || databaseStatus === "loading") { return; } - async function getDefaultMaps() { - const defaultMapsWithIds = []; + async function getDefaultMaps(): Promise { + const defaultMapsWithIds: Array = []; for (let i = 0; i < defaultMaps.length; i++) { const defaultMap = defaultMaps[i]; - const id = `__default-${defaultMap.name}`; + const mapId = `__default-${defaultMap.name}`; defaultMapsWithIds.push({ ...defaultMap, - id, + lastUsed: Date.now() + i, + id: mapId, owner: userId, // Emulate the time increasing to avoid sort errors created: Date.now() + i, @@ -57,9 +82,9 @@ export function MapDataProvider({ children }) { group: "default", }); // Add a state for the map if there isn't one already - const state = await database.table("states").get(id); + const state = await database?.table("states").get(mapId); if (!state) { - await database.table("states").add({ ...defaultMapState, mapId: id }); + await database?.table("states").add({ ...defaultMapState, mapId: mapId }); } } return defaultMapsWithIds; @@ -67,24 +92,24 @@ export function MapDataProvider({ children }) { // Loads maps without the file data to save memory async function loadMaps() { - let storedMaps = []; + let storedMaps: Map[] = []; // Try to load maps with worker, fallback to database if failed const packedMaps = await worker.loadData("maps"); // let packedMaps; if (packedMaps) { - storedMaps = decode(packedMaps); + storedMaps = decode(packedMaps) as Map[]; } else { console.warn("Unable to load maps with worker, loading may be slow"); - await database.table("maps").each((map) => { + await database?.table("maps").each((map) => { const { file, resolutions, ...rest } = map; storedMaps.push(rest); }); } const sortedMaps = storedMaps.sort((a, b) => b.created - a.created); const defaultMapsWithIds = await getDefaultMaps(); - const allMaps = [...sortedMaps, ...defaultMapsWithIds]; + const allMaps: Array = [...sortedMaps, ...defaultMapsWithIds]; setMaps(allMaps); - const storedStates = await database.table("states").toArray(); + const storedStates = await database?.table("states").toArray() as MapState[]; setMapStates(storedStates); setMapsLoading(false); } @@ -103,7 +128,7 @@ export function MapDataProvider({ children }) { const getMapFromDB = useCallback( async (mapId) => { - let map = await database.table("maps").get(mapId); + let map = await database?.table("maps").get(mapId) as Map; return map; }, [database] @@ -111,7 +136,7 @@ export function MapDataProvider({ children }) { const getMapStateFromDB = useCallback( async (mapId) => { - let mapState = await database.table("states").get(mapId); + let mapState = await database?.table("states").get(mapId) as MapState; return mapState; }, [database] @@ -122,30 +147,26 @@ export function MapDataProvider({ children }) { * Sorted by when they we're last used */ const updateCache = useCallback(async () => { - const cachedMaps = await database - .table("maps") - .where("owner") - .notEqual(userId) - .sortBy("lastUsed"); + const cachedMaps = await database?.table("maps").where("owner").notEqual(userId).sortBy("lastUsed") as Map[]; if (cachedMaps.length > cachedMapMax) { const cacheDeleteCount = cachedMaps.length - cachedMapMax; const idsToDelete = cachedMaps .slice(0, cacheDeleteCount) - .map((map) => map.id); - database.table("maps").where("id").anyOf(idsToDelete).delete(); + .map((map: Map) => map.id); + database?.table("maps").where("id").anyOf(idsToDelete).delete(); } }, [database, userId]); /** * Adds a map to the database, also adds an assosiated state for that map - * @param {Object} map map to add + * @param {Map} map map to add */ const addMap = useCallback( async (map) => { // Just update map database as react state will be updated with an Observable const state = { ...defaultMapState, mapId: map.id }; - await database.table("maps").add(map); - await database.table("states").add(state); + await database?.table("maps").add(map); + await database?.table("states").add(state); if (map.owner !== userId) { await updateCache(); } @@ -155,16 +176,16 @@ export function MapDataProvider({ children }) { const removeMap = useCallback( async (id) => { - await database.table("maps").delete(id); - await database.table("states").delete(id); + await database?.table("maps").delete(id); + await database?.table("states").delete(id); }, [database] ); const removeMaps = useCallback( async (ids) => { - await database.table("maps").bulkDelete(ids); - await database.table("states").bulkDelete(ids); + await database?.table("maps").bulkDelete(ids); + await database?.table("states").bulkDelete(ids); }, [database] ); @@ -172,7 +193,7 @@ export function MapDataProvider({ children }) { const resetMap = useCallback( async (id) => { const state = { ...defaultMapState, mapId: id }; - await database.table("states").put(state); + await database?.table("states").put(state); return state; }, [database] @@ -183,10 +204,10 @@ export function MapDataProvider({ children }) { // fake-indexeddb throws an error when updating maps in production. // Catch that error and use put when it fails try { - await database.table("maps").update(id, update); + await database?.table("maps").update(id, update); } catch (error) { const map = (await getMapFromDB(id)) || {}; - await database.table("maps").put({ ...map, id, ...update }); + await database?.table("maps").put({ ...map, id, ...update }); } }, [database, getMapFromDB] @@ -195,7 +216,7 @@ export function MapDataProvider({ children }) { const updateMaps = useCallback( async (ids, update) => { await Promise.all( - ids.map((id) => database.table("maps").update(id, update)) + ids.map((id: string) => database?.table("maps").update(id, update)) ); }, [database] @@ -203,7 +224,7 @@ export function MapDataProvider({ children }) { const updateMapState = useCallback( async (id, update) => { - await database.table("states").update(id, update); + await database?.table("states").update(id, update); }, [database] ); @@ -223,7 +244,7 @@ export function MapDataProvider({ children }) { false ); if (!success) { - await database.table("maps").put(map); + await database?.table("maps").put(map); } if (map.owner !== userId) { await updateCache(); @@ -238,13 +259,13 @@ export function MapDataProvider({ children }) { return; } - function handleMapChanges(changes) { + function handleMapChanges(changes: any) { for (let change of changes) { if (change.table === "maps") { if (change.type === 1) { // Created - const map = change.obj; - const state = { ...defaultMapState, mapId: map.id }; + const map: Map = change.obj; + const state: MapState = { ...defaultMapState, mapId: map.id }; setMaps((prevMaps) => [map, ...prevMaps]); setMapStates((prevStates) => [state, ...prevStates]); } else if (change.type === 2) { diff --git a/src/contexts/MapInteractionContext.js b/src/contexts/MapInteractionContext.tsx similarity index 82% rename from src/contexts/MapInteractionContext.js rename to src/contexts/MapInteractionContext.tsx index 1132999..1c3364b 100644 --- a/src/contexts/MapInteractionContext.js +++ b/src/contexts/MapInteractionContext.tsx @@ -1,16 +1,16 @@ -import React, { useContext } from "react"; +import React, { ReactChild, useContext } from "react"; import useDebounce from "../hooks/useDebounce"; -export const StageScaleContext = React.createContext(); -export const DebouncedStageScaleContext = React.createContext(); -export const StageWidthContext = React.createContext(); -export const StageHeightContext = React.createContext(); -export const SetPreventMapInteractionContext = React.createContext(); -export const MapWidthContext = React.createContext(); -export const MapHeightContext = React.createContext(); -export const InteractionEmitterContext = React.createContext(); +export const StageScaleContext = React.createContext(undefined) as any; +export const DebouncedStageScaleContext = React.createContext(undefined) as any; +export const StageWidthContext = React.createContext(undefined) as any; +export const StageHeightContext = React.createContext(undefined) as any; +export const SetPreventMapInteractionContext = React.createContext(undefined) as any; +export const MapWidthContext = React.createContext(undefined) as any; +export const MapHeightContext = React.createContext(undefined) as any; +export const InteractionEmitterContext = React.createContext(undefined) as any; -export function MapInteractionProvider({ value, children }) { +export function MapInteractionProvider({ value, children }: { value: any, children: ReactChild[]}) { const { stageScale, stageWidth, diff --git a/src/contexts/MapLoadingContext.js b/src/contexts/MapLoadingContext.tsx similarity index 80% rename from src/contexts/MapLoadingContext.js rename to src/contexts/MapLoadingContext.tsx index 94cb5b6..d4f0fd4 100644 --- a/src/contexts/MapLoadingContext.js +++ b/src/contexts/MapLoadingContext.tsx @@ -1,9 +1,9 @@ import React, { useState, useRef, useContext } from "react"; import { omit, isEmpty } from "../helpers/shared"; -const MapLoadingContext = React.createContext(); +const MapLoadingContext = React.createContext(undefined); -export function MapLoadingProvider({ children }) { +export function MapLoadingProvider({ children }: { children: any}) { const [loadingAssetCount, setLoadingAssetCount] = useState(0); function assetLoadStart() { @@ -14,9 +14,9 @@ export function MapLoadingProvider({ children }) { setLoadingAssetCount((prevLoadingAssets) => prevLoadingAssets - 1); } - const assetProgressRef = useRef({}); - const loadingProgressRef = useRef(null); - function assetProgressUpdate({ id, count, total }) { + const assetProgressRef = useRef({}); + const loadingProgressRef = useRef(null); + function assetProgressUpdate({ id, count, total }: { id: string, count: number, total: number }) { if (count === total) { assetProgressRef.current = omit(assetProgressRef.current, [id]); } else { @@ -28,7 +28,7 @@ export function MapLoadingProvider({ children }) { if (!isEmpty(assetProgressRef.current)) { let total = 0; let count = 0; - for (let progress of Object.values(assetProgressRef.current)) { + for (let progress of Object.values(assetProgressRef.current) as any) { total += progress.total; count += progress.count; } diff --git a/src/contexts/MapStageContext.js b/src/contexts/MapStageContext.tsx similarity index 85% rename from src/contexts/MapStageContext.js rename to src/contexts/MapStageContext.tsx index c5c3952..cd15161 100644 --- a/src/contexts/MapStageContext.js +++ b/src/contexts/MapStageContext.tsx @@ -3,7 +3,7 @@ import React, { useContext } from "react"; const MapStageContext = React.createContext({ mapStageRef: { current: null }, }); -export const MapStageProvider = MapStageContext.Provider; +export const MapStageProvider: any = MapStageContext.Provider; export function useMapStage() { const context = useContext(MapStageContext); diff --git a/src/contexts/PartyContext.js b/src/contexts/PartyContext.tsx similarity index 71% rename from src/contexts/PartyContext.js rename to src/contexts/PartyContext.tsx index 905112e..716076f 100644 --- a/src/contexts/PartyContext.js +++ b/src/contexts/PartyContext.tsx @@ -1,12 +1,14 @@ import React, { useState, useEffect, useContext } from "react"; +import { PartyState } from "../components/party/PartyState"; +import Session from "../network/Session"; -const PartyContext = React.createContext(); +const PartyContext = React.createContext(undefined); -export function PartyProvider({ session, children }) { +export function PartyProvider({ session, children }: { session: Session, children: any}) { const [partyState, setPartyState] = useState({}); useEffect(() => { - function handleSocketPartyState(partyState) { + function handleSocketPartyState(partyState: PartyState) { if (partyState) { const { [session.id]: _, ...otherMembersState } = partyState; setPartyState(otherMembersState); diff --git a/src/contexts/PlayerContext.js b/src/contexts/PlayerContext.tsx similarity index 78% rename from src/contexts/PlayerContext.js rename to src/contexts/PlayerContext.tsx index 26cd95c..4ee863c 100644 --- a/src/contexts/PlayerContext.js +++ b/src/contexts/PlayerContext.tsx @@ -6,11 +6,13 @@ import { useAuth } from "./AuthContext"; import { getRandomMonster } from "../helpers/monsters"; import useNetworkedState from "../hooks/useNetworkedState"; +import Session from "../network/Session"; +import { PlayerInfo } from "../components/party/PartyState"; -export const PlayerStateContext = React.createContext(); -export const PlayerUpdaterContext = React.createContext(() => {}); +export const PlayerStateContext = React.createContext(undefined); +export const PlayerUpdaterContext = React.createContext(() => {}); -export function PlayerProvider({ session, children }) { +export function PlayerProvider({ session, children }: { session: Session, children: any}) { const { userId } = useAuth(); const { database, databaseStatus } = useDatabase(); @@ -33,16 +35,16 @@ export function PlayerProvider({ session, children }) { return; } async function loadNickname() { - const storedNickname = await database.table("user").get("nickname"); + const storedNickname = await database?.table("user").get("nickname"); if (storedNickname !== undefined) { - setPlayerState((prevState) => ({ + setPlayerState((prevState: PlayerInfo) => ({ ...prevState, nickname: storedNickname.value, })); } else { const name = getRandomMonster(); - setPlayerState((prevState) => ({ ...prevState, nickname: name })); - database.table("user").add({ key: "nickname", value: name }); + setPlayerState((prevState: any) => ({ ...prevState, nickname: name })); + database?.table("user").add({ key: "nickname", value: name }); } } @@ -63,7 +65,7 @@ export function PlayerProvider({ session, children }) { useEffect(() => { if (userId) { - setPlayerState((prevState) => { + setPlayerState((prevState: PlayerInfo) => { if (prevState) { return { ...prevState, @@ -77,7 +79,8 @@ export function PlayerProvider({ session, children }) { useEffect(() => { function updateSessionId() { - setPlayerState((prevState) => { + setPlayerState((prevState: PlayerInfo) => { + // TODO: check useNetworkState requirements here if (prevState) { return { ...prevState, @@ -92,7 +95,7 @@ export function PlayerProvider({ session, children }) { updateSessionId(); } - function handleSocketStatus(status) { + function handleSocketStatus(status: string) { if (status === "joined") { updateSessionId(); } diff --git a/src/contexts/SettingsContext.js b/src/contexts/SettingsContext.tsx similarity index 86% rename from src/contexts/SettingsContext.js rename to src/contexts/SettingsContext.tsx index c9df1e7..1e17934 100644 --- a/src/contexts/SettingsContext.js +++ b/src/contexts/SettingsContext.tsx @@ -9,14 +9,14 @@ const SettingsContext = React.createContext({ const settingsProvider = getSettings(); -export function SettingsProvider({ children }) { +export function SettingsProvider({ children }: { children: any }) { const [settings, setSettings] = useState(settingsProvider.getAll()); useEffect(() => { settingsProvider.setAll(settings); }, [settings]); - const value = { + const value: { settings: any, setSettings: any} = { settings, setSettings, }; diff --git a/src/dice/Dice.js b/src/dice/Dice.ts similarity index 67% rename from src/dice/Dice.js rename to src/dice/Dice.ts index 23dee18..785d8a8 100644 --- a/src/dice/Dice.js +++ b/src/dice/Dice.ts @@ -1,7 +1,7 @@ import { Vector3 } from "@babylonjs/core/Maths/math"; import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; -import { PhysicsImpostor } from "@babylonjs/core/Physics/physicsImpostor"; +import { PhysicsImpostor, PhysicsImpostorParameters } from "@babylonjs/core/Physics/physicsImpostor"; import d4Source from "./shared/d4.glb"; import d6Source from "./shared/d6.glb"; @@ -13,6 +13,7 @@ import d100Source from "./shared/d100.glb"; import { lerp } from "../helpers/shared"; import { importTextureAsync } from "../helpers/babylon"; +import { BaseTexture, InstancedMesh, Material, Mesh, Scene, Texture } from "@babylonjs/core"; const minDiceRollSpeed = 600; const maxDiceRollSpeed = 800; @@ -20,10 +21,10 @@ const maxDiceRollSpeed = 800; class Dice { static instanceCount = 0; - static async loadMeshes(material, scene, sourceOverrides) { - let meshes = {}; - const addToMeshes = async (type, defaultSource) => { - let source = sourceOverrides ? sourceOverrides[type] : defaultSource; + static async loadMeshes(material: Material, scene: Scene, sourceOverrides?: any): Promise> { + let meshes: any = {}; + const addToMeshes = async (type: string | number, defaultSource: any) => { + let source: string = sourceOverrides ? sourceOverrides[type] : defaultSource; const mesh = await this.loadMesh(source, material, scene); meshes[type] = mesh; }; @@ -39,7 +40,7 @@ class Dice { return meshes; } - static async loadMesh(source, material, scene) { + static async loadMesh(source: string, material: Material, scene: Scene) { let mesh = (await SceneLoader.ImportMeshAsync("", source, "", scene)) .meshes[1]; mesh.setParent(null); @@ -51,15 +52,16 @@ class Dice { return mesh; } - static async loadMaterial(materialName, textures, scene) { + static async loadMaterial(materialName: string, textures: any, scene: Scene) { let pbr = new PBRMaterial(materialName, scene); - let [albedo, normal, metalRoughness] = await Promise.all([ + let [albedo, normal, metalRoughness]: [albedo: BaseTexture, normal: Texture, metalRoughness: Texture] = await Promise.all([ importTextureAsync(textures.albedo), importTextureAsync(textures.normal), importTextureAsync(textures.metalRoughness), ]); pbr.albedoTexture = albedo; - pbr.normalTexture = normal; + // pbr.normalTexture = normal; + pbr.bumpTexture = normal; pbr.metallicTexture = metalRoughness; pbr.useRoughnessFromMetallicTextureAlpha = false; pbr.useRoughnessFromMetallicTextureGreen = true; @@ -67,11 +69,16 @@ class Dice { return pbr; } - static createInstanceFromMesh(mesh, name, physicalProperties, scene) { + static createInstanceFromMesh(mesh: Mesh, name: string, physicalProperties: PhysicsImpostorParameters, scene: Scene) { let instance = mesh.createInstance(name); instance.position = mesh.position; for (let child of mesh.getChildTransformNodes()) { - const locator = child.clone(); + // TODO: type correctly another time -> should not be any + const locator: any = child.clone(child.name, instance); + // TODO: handle possible null value + if (!locator) { + throw Error + } locator.setAbsolutePosition(child.getAbsolutePosition()); locator.name = child.name; instance.addChild(locator); @@ -87,7 +94,7 @@ class Dice { return instance; } - static getDicePhysicalProperties(diceType) { + static getDicePhysicalProperties(diceType: string) { switch (diceType) { case "d4": return { mass: 4, friction: 4 }; @@ -107,17 +114,18 @@ class Dice { } } - static roll(instance) { - instance.physicsImpostor.setLinearVelocity(Vector3.Zero()); - instance.physicsImpostor.setAngularVelocity(Vector3.Zero()); + static roll(instance: Mesh) { + instance.physicsImpostor?.setLinearVelocity(Vector3.Zero()); + instance.physicsImpostor?.setAngularVelocity(Vector3.Zero()); const scene = instance.getScene(); - const diceTraySingle = scene.getNodeByID("dice_tray_single"); + // TODO: remove any typing in this function -> this is just to get it working + const diceTraySingle: any = scene.getNodeByID("dice_tray_single"); const diceTrayDouble = scene.getNodeByID("dice_tray_double"); - const visibleDiceTray = diceTraySingle.isVisible + const visibleDiceTray: any = diceTraySingle?.isVisible ? diceTraySingle : diceTrayDouble; - const trayBounds = visibleDiceTray.getBoundingInfo().boundingBox; + const trayBounds = visibleDiceTray?.getBoundingInfo().boundingBox; const position = new Vector3( trayBounds.center.x + (Math.random() * 2 - 1), @@ -142,13 +150,13 @@ class Dice { .normalizeToNew() .scale(lerp(minDiceRollSpeed, maxDiceRollSpeed, Math.random())); - instance.physicsImpostor.applyImpulse( + instance.physicsImpostor?.applyImpulse( impulse, instance.physicsImpostor.getObjectCenter() ); } - static createInstance(mesh, physicalProperties, scene) { + static createInstanceMesh(mesh: Mesh, physicalProperties: PhysicsImpostorParameters, scene: Scene): InstancedMesh { this.instanceCount++; return this.createInstanceFromMesh( diff --git a/src/dice/diceTray/DiceTray.js b/src/dice/diceTray/DiceTray.ts similarity index 84% rename from src/dice/diceTray/DiceTray.js rename to src/dice/diceTray/DiceTray.ts index 1e9ec71..e2237bd 100644 --- a/src/dice/diceTray/DiceTray.js +++ b/src/dice/diceTray/DiceTray.ts @@ -3,7 +3,9 @@ import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { PhysicsImpostor } from "@babylonjs/core/Physics/physicsImpostor"; import { Mesh } from "@babylonjs/core/Meshes/mesh"; +//@ts-ignore import singleMeshSource from "./single.glb"; +//@ts-ignore import doubleMeshSource from "./double.glb"; import singleAlbedo from "./singleAlbedo.jpg"; @@ -15,12 +17,15 @@ import doubleMetalRoughness from "./doubleMetalRoughness.jpg"; import doubleNormal from "./doubleNormal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; +import { Scene, ShadowGenerator, Texture } from "@babylonjs/core"; class DiceTray { _size; + get size() { return this._size; } + set size(newSize) { this._size = newSize; const wallOffsetWidth = this.collisionSize / 2 + this.width / 2 - 0.5; @@ -32,21 +37,24 @@ class DiceTray { this.singleMesh.isVisible = newSize === "single"; this.doubleMesh.isVisible = newSize === "double"; } + scene; shadowGenerator; + get width() { return this.size === "single" ? 10 : 20; } + height = 20; collisionSize = 50; - wallTop; - wallRight; - wallBottom; - wallLeft; - singleMesh; - doubleMesh; + wallTop: any; + wallRight: any; + wallBottom: any; + wallLeft: any; + singleMesh: any; + doubleMesh: any; - constructor(initialSize, scene, shadowGenerator) { + constructor(initialSize: string, scene: Scene, shadowGenerator: ShadowGenerator) { this._size = initialSize; this.scene = scene; this.shadowGenerator = shadowGenerator; @@ -57,7 +65,7 @@ class DiceTray { await this.loadMeshes(); } - createCollision(name, x, y, z, friction) { + createCollision(name: string, x: number, y: number, z: number, friction: number) { let collision = Mesh.CreateBox( name, this.collisionSize, @@ -126,6 +134,15 @@ class DiceTray { doubleAlbedoTexture, doubleNormalTexture, doubleMetalRoughnessTexture, + ]: [ + singleMeshes: any, + doubleMeshes: any, + singleAlbedoTexture: Texture, + singleNormalTexture: Texture, + singleMetalRoughnessTexture: Texture, + doubleAlbedoTexture: Texture, + doubleNormalTexture: Texture, + doubleMetalRoughnessTexture: Texture ] = await Promise.all([ SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene), SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene), @@ -142,7 +159,9 @@ class DiceTray { this.singleMesh.name = "dice_tray"; let singleMaterial = new PBRMaterial("dice_tray_mat_single", this.scene); singleMaterial.albedoTexture = singleAlbedoTexture; - singleMaterial.normalTexture = singleNormalTexture; + // TODO: ask Mitch about texture + // singleMaterial.normalTexture = singleNormalTexture; + singleMaterial.bumpTexture = singleNormalTexture; singleMaterial.metallicTexture = singleMetalRoughnessTexture; singleMaterial.useRoughnessFromMetallicTextureAlpha = false; singleMaterial.useRoughnessFromMetallicTextureGreen = true; @@ -158,7 +177,9 @@ class DiceTray { this.doubleMesh.name = "dice_tray"; let doubleMaterial = new PBRMaterial("dice_tray_mat_double", this.scene); doubleMaterial.albedoTexture = doubleAlbedoTexture; - doubleMaterial.normalTexture = doubleNormalTexture; + // TODO: ask Mitch about texture + //doubleMaterial.normalTexture = doubleNormalTexture; + doubleMaterial.bumpTexture = doubleNormalTexture; doubleMaterial.metallicTexture = doubleMetalRoughnessTexture; doubleMaterial.useRoughnessFromMetallicTextureAlpha = false; doubleMaterial.useRoughnessFromMetallicTextureGreen = true; diff --git a/src/dice/galaxy/GalaxyDice.js b/src/dice/galaxy/GalaxyDice.ts similarity index 68% rename from src/dice/galaxy/GalaxyDice.js rename to src/dice/galaxy/GalaxyDice.ts index ebe8548..9947f27 100644 --- a/src/dice/galaxy/GalaxyDice.js +++ b/src/dice/galaxy/GalaxyDice.ts @@ -1,3 +1,4 @@ +import { InstancedMesh, Material, Mesh, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; @@ -5,10 +6,10 @@ import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; class GalaxyDice extends Dice { - static meshes; - static material; + static meshes: Record; + static material: Material; - static async load(scene) { + static async load(scene: Scene) { if (!this.material) { this.material = await this.loadMaterial( "galaxy_pbr", @@ -21,12 +22,13 @@ class GalaxyDice extends Dice { } } - static createInstance(diceType, scene) { + // TODO: check static -> rename function? + static createInstance(diceType: string, scene: Scene): InstancedMesh { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } - return Dice.createInstance( + return super.createInstanceMesh( this.meshes[diceType], this.getDicePhysicalProperties(diceType), scene diff --git a/src/dice/gemstone/GemstoneDice.js b/src/dice/gemstone/GemstoneDice.ts similarity index 79% rename from src/dice/gemstone/GemstoneDice.js rename to src/dice/gemstone/GemstoneDice.ts index 4545912..b6a2043 100644 --- a/src/dice/gemstone/GemstoneDice.js +++ b/src/dice/gemstone/GemstoneDice.ts @@ -8,17 +8,18 @@ import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; +import { Material, Mesh, Scene } from "@babylonjs/core"; class GemstoneDice extends Dice { - static meshes; - static material; + static meshes: Record; + static material: Material; - static getDicePhysicalProperties(diceType) { + static getDicePhysicalProperties(diceType: string) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.5, friction: properties.friction }; } - static async loadMaterial(materialName, textures, scene) { + static async loadMaterial(materialName: string, textures: any, scene: Scene) { let pbr = new PBRMaterial(materialName, scene); let [albedo, normal, metalRoughness] = await Promise.all([ importTextureAsync(textures.albedo), @@ -26,7 +27,8 @@ class GemstoneDice extends Dice { importTextureAsync(textures.metalRoughness), ]); pbr.albedoTexture = albedo; - pbr.normalTexture = normal; + // TODO: ask Mitch about texture + pbr.bumpTexture = normal; pbr.metallicTexture = metalRoughness; pbr.useRoughnessFromMetallicTextureAlpha = false; pbr.useRoughnessFromMetallicTextureGreen = true; @@ -41,7 +43,7 @@ class GemstoneDice extends Dice { return pbr; } - static async load(scene) { + static async load(scene: Scene) { if (!this.material) { this.material = await this.loadMaterial( "gemstone_pbr", @@ -54,12 +56,12 @@ class GemstoneDice extends Dice { } } - static createInstance(diceType, scene) { + static createInstance(diceType: string, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } - return Dice.createInstance( + return Dice.createInstanceMesh( this.meshes[diceType], this.getDicePhysicalProperties(diceType), scene diff --git a/src/dice/glass/GlassDice.js b/src/dice/glass/GlassDice.ts similarity index 80% rename from src/dice/glass/GlassDice.js rename to src/dice/glass/GlassDice.ts index ec063dc..a745487 100644 --- a/src/dice/glass/GlassDice.js +++ b/src/dice/glass/GlassDice.ts @@ -8,17 +8,18 @@ import mask from "./mask.png"; import normal from "./normal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; +import { Material, Mesh, Scene } from "@babylonjs/core"; class GlassDice extends Dice { - static meshes; - static material; + static meshes: Record; + static material: Material; - static getDicePhysicalProperties(diceType) { + static getDicePhysicalProperties(diceType: string) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.5, friction: properties.friction }; } - static async loadMaterial(materialName, textures, scene) { + static async loadMaterial(materialName: string, textures: any, scene: Scene) { let pbr = new PBRMaterial(materialName, scene); let [albedo, normal, mask] = await Promise.all([ importTextureAsync(textures.albedo), @@ -26,7 +27,8 @@ class GlassDice extends Dice { importTextureAsync(textures.mask), ]); pbr.albedoTexture = albedo; - pbr.normalTexture = normal; + // pbr.normalTexture = normal; + pbr.bumpTexture = normal; pbr.roughness = 0.25; pbr.metallic = 0; pbr.subSurface.isRefractionEnabled = true; @@ -43,7 +45,7 @@ class GlassDice extends Dice { return pbr; } - static async load(scene) { + static async load(scene: Scene) { if (!this.material) { this.material = await this.loadMaterial( "glass_pbr", @@ -56,12 +58,12 @@ class GlassDice extends Dice { } } - static createInstance(diceType, scene) { + static createInstance(diceType: string, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } - return Dice.createInstance( + return Dice.createInstanceMesh( this.meshes[diceType], this.getDicePhysicalProperties(diceType), scene diff --git a/src/dice/index.js b/src/dice/index.ts similarity index 87% rename from src/dice/index.js rename to src/dice/index.ts index 62e22b9..82cf7f0 100644 --- a/src/dice/index.js +++ b/src/dice/index.ts @@ -17,8 +17,11 @@ import SunsetPreview from "./sunset/preview.png"; import WalnutPreview from "./walnut/preview.png"; import GlassPreview from "./glass/preview.png"; import GemstonePreview from "./gemstone/preview.png"; +import Dice from "./Dice"; -export const diceClasses = { +type DiceClasses = Record; + +export const diceClasses: DiceClasses = { galaxy: GalaxyDice, nebula: NebulaDice, sunrise: SunriseDice, @@ -29,7 +32,9 @@ export const diceClasses = { gemstone: GemstoneDice, }; -export const dicePreviews = { +type DicePreview = Record; + +export const dicePreviews: DicePreview = { galaxy: GalaxyPreview, nebula: NebulaPreview, sunrise: SunrisePreview, diff --git a/src/dice/iron/IronDice.js b/src/dice/iron/IronDice.ts similarity index 73% rename from src/dice/iron/IronDice.js rename to src/dice/iron/IronDice.ts index 917ab6c..2e9c5b7 100644 --- a/src/dice/iron/IronDice.js +++ b/src/dice/iron/IronDice.ts @@ -1,3 +1,4 @@ +import { Material, Mesh, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; @@ -5,15 +6,15 @@ import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; class IronDice extends Dice { - static meshes; - static material; + static meshes: Record; + static material: Material; - static getDicePhysicalProperties(diceType) { + static getDicePhysicalProperties(diceType: string) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 2, friction: properties.friction }; } - static async load(scene) { + static async load(scene: Scene) { if (!this.material) { this.material = await this.loadMaterial( "iron_pbr", @@ -26,12 +27,12 @@ class IronDice extends Dice { } } - static createInstance(diceType, scene) { + static createInstance(diceType: string, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } - return Dice.createInstance( + return Dice.createInstanceMesh( this.meshes[diceType], this.getDicePhysicalProperties(diceType), scene diff --git a/src/dice/nebula/NebulaDice.js b/src/dice/nebula/NebulaDice.ts similarity index 73% rename from src/dice/nebula/NebulaDice.js rename to src/dice/nebula/NebulaDice.ts index fe687f1..eb08d26 100644 --- a/src/dice/nebula/NebulaDice.js +++ b/src/dice/nebula/NebulaDice.ts @@ -1,3 +1,4 @@ +import { Material, Mesh, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; @@ -5,10 +6,10 @@ import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; class NebulaDice extends Dice { - static meshes; - static material; + static meshes: Record; + static material: Material; - static async load(scene) { + static async load(scene: Scene) { if (!this.material) { this.material = await this.loadMaterial( "neubula_pbr", @@ -21,12 +22,12 @@ class NebulaDice extends Dice { } } - static createInstance(diceType, scene) { + static createInstance(diceType: string, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } - return Dice.createInstance( + return Dice.createInstanceMesh( this.meshes[diceType], this.getDicePhysicalProperties(diceType), scene diff --git a/src/dice/sunrise/SunriseDice.js b/src/dice/sunrise/SunriseDice.ts similarity index 73% rename from src/dice/sunrise/SunriseDice.js rename to src/dice/sunrise/SunriseDice.ts index d92f6f0..233886c 100644 --- a/src/dice/sunrise/SunriseDice.js +++ b/src/dice/sunrise/SunriseDice.ts @@ -1,3 +1,4 @@ +import { Material, Mesh, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; @@ -5,10 +6,10 @@ import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; class SunriseDice extends Dice { - static meshes; - static material; + static meshes: Record; + static material: Material; - static async load(scene) { + static async load(scene: Scene) { if (!this.material) { this.material = await this.loadMaterial( "sunrise_pbr", @@ -21,12 +22,12 @@ class SunriseDice extends Dice { } } - static createInstance(diceType, scene) { + static createInstance(diceType: string, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } - return Dice.createInstance( + return super.createInstanceMesh( this.meshes[diceType], this.getDicePhysicalProperties(diceType), scene diff --git a/src/dice/sunset/SunsetDice.js b/src/dice/sunset/SunsetDice.ts similarity index 73% rename from src/dice/sunset/SunsetDice.js rename to src/dice/sunset/SunsetDice.ts index c0e6884..7e66a20 100644 --- a/src/dice/sunset/SunsetDice.js +++ b/src/dice/sunset/SunsetDice.ts @@ -1,3 +1,4 @@ +import { Material, Mesh, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; @@ -5,10 +6,10 @@ import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; class SunsetDice extends Dice { - static meshes; - static material; + static meshes: Record; + static material: Material; - static async load(scene) { + static async load(scene: Scene) { if (!this.material) { this.material = await this.loadMaterial( "sunset_pbr", @@ -21,12 +22,12 @@ class SunsetDice extends Dice { } } - static createInstance(diceType, scene) { + static createInstance(diceType: string, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } - return Dice.createInstance( + return super.createInstanceMesh( this.meshes[diceType], this.getDicePhysicalProperties(diceType), scene diff --git a/src/dice/walnut/WalnutDice.js b/src/dice/walnut/WalnutDice.ts similarity index 80% rename from src/dice/walnut/WalnutDice.js rename to src/dice/walnut/WalnutDice.ts index b5e5755..979808e 100644 --- a/src/dice/walnut/WalnutDice.js +++ b/src/dice/walnut/WalnutDice.ts @@ -11,6 +11,7 @@ import d10Source from "./d10.glb"; import d12Source from "./d12.glb"; import d20Source from "./d20.glb"; import d100Source from "./d100.glb"; +import { Material, Mesh, Scene } from "@babylonjs/core"; const sourceOverrides = { d4: d4Source, @@ -23,15 +24,15 @@ const sourceOverrides = { }; class WalnutDice extends Dice { - static meshes; - static material; + static meshes: Record; + static material: Material; - static getDicePhysicalProperties(diceType) { + static getDicePhysicalProperties(diceType: string) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.4, friction: properties.friction }; } - static async load(scene) { + static async load(scene: Scene) { if (!this.material) { this.material = await this.loadMaterial( "walnut_pbr", @@ -48,12 +49,12 @@ class WalnutDice extends Dice { } } - static createInstance(diceType, scene) { + static createInstance(diceType: string, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } - return Dice.createInstance( + return super.createInstanceMesh( this.meshes[diceType], this.getDicePhysicalProperties(diceType), scene diff --git a/src/helpers/babylon.ts b/src/helpers/babylon.ts index a713bad..230a07c 100644 --- a/src/helpers/babylon.ts +++ b/src/helpers/babylon.ts @@ -1,7 +1,7 @@ import { Texture } from "@babylonjs/core/Materials/Textures/texture"; // Turn texture load into an async function so it can be awaited -export async function importTextureAsync(url: string) { +export async function importTextureAsync(url: string): Promise { return new Promise((resolve, reject) => { let texture = new Texture( url, diff --git a/src/hooks/useDebounce.js b/src/hooks/useDebounce.tsx similarity index 86% rename from src/hooks/useDebounce.js rename to src/hooks/useDebounce.tsx index bf82898..1ab9dea 100644 --- a/src/hooks/useDebounce.js +++ b/src/hooks/useDebounce.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -function useDebounce(value, delay) { +function useDebounce(value: any, delay: number): any { const [debouncedValue, setDebouncedValue] = useState(); useEffect(() => { diff --git a/src/hooks/useNetworkedState.js b/src/hooks/useNetworkedState.tsx similarity index 85% rename from src/hooks/useNetworkedState.js rename to src/hooks/useNetworkedState.tsx index 7dbf356..7737661 100644 --- a/src/hooks/useNetworkedState.js +++ b/src/hooks/useNetworkedState.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useCallback } from "react"; import useDebounce from "./useDebounce"; import { diff, applyChanges } from "../helpers/diff"; +import Session from "../network/Session"; /** * @callback setNetworkedState @@ -9,6 +10,8 @@ import { diff, applyChanges } from "../helpers/diff"; * @param {boolean} sync Whether to sync the update with the session * @param {boolean} force Whether to force a full update, usefull when partialUpdates is enabled */ +// TODO: check parameter requirements here +type setNetworkedState = (update: any, sync?: boolean, force?: boolean) => void /** * Helper to sync a react state to a `Session` @@ -23,13 +26,13 @@ import { diff, applyChanges } from "../helpers/diff"; * @returns {[any, setNetworkedState]} */ function useNetworkedState( - initialState, - session, - eventName, - debounceRate = 500, - partialUpdates = true, - partialUpdatesKey = "id" -) { + initialState: any, + session: Session, + eventName: string, + debounceRate: number = 500, + partialUpdates: boolean = true, + partialUpdatesKey: string = "id" +): [any, setNetworkedState] { const [state, _setState] = useState(initialState); // Used to control whether the state needs to be sent to the socket const dirtyRef = useRef(false); @@ -62,6 +65,9 @@ function useNetworkedState( ) { const changes = diff(lastSyncedStateRef.current, debouncedState); if (changes) { + if (!debouncedState) { + return; + } const update = { id: debouncedState[partialUpdatesKey], changes }; session.socket.emit(`${eventName}_update`, update); } @@ -81,13 +87,13 @@ function useNetworkedState( ]); useEffect(() => { - function handleSocketEvent(data) { + function handleSocketEvent(data: any) { _setState(data); lastSyncedStateRef.current = data; } - function handleSocketUpdateEvent(update) { - _setState((prevState) => { + function handleSocketUpdateEvent(update: any) { + _setState((prevState: any) => { if (prevState && prevState[partialUpdatesKey] === update.id) { let newState = { ...prevState }; applyChanges(newState, update.changes); diff --git a/src/maps/index.ts b/src/maps/index.ts index 2b55878..ca3a553 100644 --- a/src/maps/index.ts +++ b/src/maps/index.ts @@ -1,4 +1,5 @@ import Case from "case"; +import { Map } from "../components/map/Map"; import blankImage from "./Blank Grid 22x22.jpg"; import grassImage from "./Grass Grid 22x22.jpg"; @@ -18,7 +19,7 @@ export const mapSources = { wood: woodImage, }; -export const maps = Object.keys(mapSources).map((key) => ({ +export const maps: Array> = Object.keys(mapSources).map((key) => ({ key, name: Case.capital(key), grid: { diff --git a/src/modals/EditMapModal.tsx b/src/modals/EditMapModal.tsx index 16743ae..6f9a53f 100644 --- a/src/modals/EditMapModal.tsx +++ b/src/modals/EditMapModal.tsx @@ -43,7 +43,7 @@ function EditMapModal({ isOpen, onDone, mapId }: EditMapProps) { } const mapState = await getMapStateFromDB(mapId); setMap(loadingMap); - setMapState(mapState); + setMapState(mapState as MapState); setIsLoading(false); } diff --git a/src/modals/SelectMapModal.tsx b/src/modals/SelectMapModal.tsx index 4bb5c2e..88ce28b 100644 --- a/src/modals/SelectMapModal.tsx +++ b/src/modals/SelectMapModal.tsx @@ -30,7 +30,7 @@ import { useAuth } from "../contexts/AuthContext"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; import shortcuts from "../shortcuts"; -import { MapState } from "../components/map/Map"; +import { Map, MapState } from "../components/map/Map"; type SelectMapProps = { isOpen: boolean, @@ -175,7 +175,7 @@ function SelectMapModal({ clearFileInput(); } - async function handleImageUpload(file: any) { + async function handleImageUpload(file: File) { if (!file) { return Promise.reject(); } @@ -313,7 +313,7 @@ function SelectMapModal({ // The map selected in the modal const [selectedMapIds, setSelectedMapIds] = useState([]); - const selectedMaps = ownedMaps.filter((map: any) => + const selectedMaps: Map[] = ownedMaps.filter((map: Map) => selectedMapIds.includes(map.id) ); const selectedMapStates = mapStates.filter((state: MapState) => @@ -499,11 +499,15 @@ function SelectMapModal({ + <> {(isLoading || mapsLoading) && } + setIsEditModalOpen(false)} - mapId={selectedMaps.length === 1 && selectedMaps[0].id} + // TODO: check with Mitch what to do here if length > 1 + //selectedMaps.length === 1 && + mapId={selectedMaps[0].id} /> void, isSupported: boolean, - unavailableMessage: string, + unavailableMessage: JSX.Element, stream: MediaStream, noAudioTrack: boolean, - noAudioMessage: string, + noAudioMessage: JSX.Element, onStreamStart: any, onStreamEnd: any, } diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.tsx similarity index 81% rename from src/network/NetworkedMapAndTokens.js rename to src/network/NetworkedMapAndTokens.tsx index d38413a..b692616 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef } from "react"; import { useToasts } from "react-toast-notifications"; import { useTokenData } from "../contexts/TokenDataContext"; @@ -14,11 +14,13 @@ import useDebounce from "../hooks/useDebounce"; import useNetworkedState from "../hooks/useNetworkedState"; // Load session for auto complete -// eslint-disable-next-line no-unused-vars import Session from "./Session"; -import Map from "../components/map/Map"; +import Map, { MapState, Resolutions, TokenState } from "../components/map/Map"; import Tokens from "../components/token/Tokens"; +import { PartyState } from "../components/party/PartyState"; +import Action from "../actions/Action"; +import { Token } from "../tokens"; const defaultMapActions = { mapDrawActions: [], @@ -35,10 +37,10 @@ const defaultMapActions = { /** * @param {NetworkedMapProps} props */ -function NetworkedMapAndTokens({ session }) { +function NetworkedMapAndTokens({ session }: { session: Session }) { const { addToast } = useToasts(); const { userId } = useAuth(); - const partyState = useParty(); + const partyState: PartyState = useParty(); const { assetLoadStart, assetLoadFinish, @@ -49,8 +51,8 @@ function NetworkedMapAndTokens({ session }) { const { putToken, getTokenFromDB } = useTokenData(); const { putMap, updateMap, getMapFromDB, updateMapState } = useMapData(); - const [currentMap, setCurrentMap] = useState(null); - const [currentMapState, setCurrentMapState] = useNetworkedState( + const [currentMap, setCurrentMap] = useState(null); + const [currentMapState, setCurrentMapState]: [ currentMapState: MapState, setCurrentMapState: any] = useNetworkedState( null, session, "map_state", @@ -67,8 +69,8 @@ function NetworkedMapAndTokens({ session }) { "mapId" ); - async function loadAssetManifestFromMap(map, mapState) { - const assets = {}; + async function loadAssetManifestFromMap(map: any, mapState: MapState) { + const assets: any = {}; if (map.type === "file") { const { id, lastModified, owner } = map; assets[`map-${id}`] = { type: "map", id, lastModified, owner }; @@ -90,20 +92,20 @@ function NetworkedMapAndTokens({ session }) { setAssetManifest({ mapId: map.id, assets }, true, true); } - function compareAssets(a, b) { + function compareAssets(a: any, b: any) { return a.type === b.type && a.id === b.id; } // Return true if an asset is out of date - function assetNeedsUpdate(oldAsset, newAsset) { + function assetNeedsUpdate(oldAsset: any, newAsset: any) { return ( compareAssets(oldAsset, newAsset) && oldAsset.lastModified < newAsset.lastModified ); } - function addAssetIfNeeded(asset) { - setAssetManifest((prevManifest) => { + function addAssetIfNeeded(asset: any) { + setAssetManifest((prevManifest: any) => { if (prevManifest?.assets) { const id = asset.type === "map" ? `map-${asset.id}` : `token-${asset.id}`; @@ -133,7 +135,7 @@ function NetworkedMapAndTokens({ session }) { } async function requestAssetsIfNeeded() { - for (let asset of Object.values(assetManifest.assets)) { + for (let asset of Object.values(assetManifest.assets) as any) { if ( asset.owner === userId || requestingAssetsRef.current.has(asset.id) @@ -200,14 +202,14 @@ function NetworkedMapAndTokens({ session }) { debouncedMapState && debouncedMapState.mapId && currentMap && - currentMap.owner === userId && + currentMap?.owner === userId && database ) { updateMapState(debouncedMapState.mapId, debouncedMapState); } }, [currentMap, debouncedMapState, userId, database, updateMapState]); - async function handleMapChange(newMap, newMapState) { + async function handleMapChange(newMap: any, newMapState: any) { // Clear map before sending new one setCurrentMap(null); session.socket?.emit("map", null); @@ -229,15 +231,15 @@ function NetworkedMapAndTokens({ session }) { await loadAssetManifestFromMap(newMap, newMapState); } - function handleMapReset(newMapState) { + function handleMapReset(newMapState: any) { setCurrentMapState(newMapState, true, true); setMapActions(defaultMapActions); } - const [mapActions, setMapActions] = useState(defaultMapActions); + const [mapActions, setMapActions] = useState(defaultMapActions); - function addMapActions(actions, indexKey, actionsKey, shapesKey) { - setMapActions((prevMapActions) => { + function addMapActions(actions: Action[], indexKey: string, actionsKey: any, shapesKey: any) { + setMapActions((prevMapActions: any) => { const newActions = [ ...prevMapActions[actionsKey].slice(0, prevMapActions[indexKey] + 1), ...actions, @@ -250,7 +252,7 @@ function NetworkedMapAndTokens({ session }) { }; }); // Update map state by performing the actions on it - setCurrentMapState((prevMapState) => { + setCurrentMapState((prevMapState: any) => { if (prevMapState) { let shapes = prevMapState[shapesKey]; for (let action of actions) { @@ -264,20 +266,20 @@ function NetworkedMapAndTokens({ session }) { }); } - function updateActionIndex(change, indexKey, actionsKey, shapesKey) { - const prevIndex = mapActions[indexKey]; + function updateActionIndex(change: any, indexKey: any, actionsKey: any, shapesKey: any) { + const prevIndex: any = mapActions[indexKey]; const newIndex = Math.min( Math.max(mapActions[indexKey] + change, -1), mapActions[actionsKey].length - 1 ); - setMapActions((prevMapActions) => ({ + setMapActions((prevMapActions: Action[]) => ({ ...prevMapActions, [indexKey]: newIndex, })); // Update map state by either performing the actions or undoing them - setCurrentMapState((prevMapState) => { + setCurrentMapState((prevMapState: any) => { if (prevMapState) { let shapes = prevMapState[shapesKey]; if (prevIndex < newIndex) { @@ -303,7 +305,7 @@ function NetworkedMapAndTokens({ session }) { return newIndex; } - function handleMapDraw(action) { + function handleMapDraw(action: Action) { addMapActions( [action], "mapDrawActionIndex", @@ -320,7 +322,7 @@ function NetworkedMapAndTokens({ session }) { updateActionIndex(1, "mapDrawActionIndex", "mapDrawActions", "drawShapes"); } - function handleFogDraw(action) { + function handleFogDraw(action: Action) { addMapActions( [action], "fogDrawActionIndex", @@ -338,16 +340,16 @@ function NetworkedMapAndTokens({ session }) { } // If map changes clear map actions - const previousMapIdRef = useRef(); + const previousMapIdRef = useRef(); useEffect(() => { - if (currentMap && currentMap.id !== previousMapIdRef.current) { + if (currentMap && currentMap?.id !== previousMapIdRef.current) { setMapActions(defaultMapActions); - previousMapIdRef.current = currentMap.id; + previousMapIdRef.current = currentMap?.id; } }, [currentMap]); - function handleNoteChange(note) { - setCurrentMapState((prevMapState) => ({ + function handleNoteChange(note: any) { + setCurrentMapState((prevMapState: any) => ({ ...prevMapState, notes: { ...prevMapState.notes, @@ -356,8 +358,8 @@ function NetworkedMapAndTokens({ session }) { })); } - function handleNoteRemove(noteId) { - setCurrentMapState((prevMapState) => ({ + function handleNoteRemove(noteId: string) { + setCurrentMapState((prevMapState: any) => ({ ...prevMapState, notes: omit(prevMapState.notes, [noteId]), })); @@ -367,17 +369,17 @@ function NetworkedMapAndTokens({ session }) { * Token state */ - async function handleMapTokenStateCreate(tokenState) { + async function handleMapTokenStateCreate(tokenState: TokenState) { if (!currentMap || !currentMapState) { return; } // If file type token send the token to the other peers - const token = await getTokenFromDB(tokenState.tokenId); + const token: Token = await getTokenFromDB(tokenState.tokenId); if (token && token.type === "file") { const { id, lastModified, owner } = token; addAssetIfNeeded({ type: "token", id, lastModified, owner }); } - setCurrentMapState((prevMapState) => ({ + setCurrentMapState((prevMapState: any) => ({ ...prevMapState, tokens: { ...prevMapState.tokens, @@ -386,11 +388,11 @@ function NetworkedMapAndTokens({ session }) { })); } - function handleMapTokenStateChange(change) { + function handleMapTokenStateChange(change: any) { if (!currentMapState) { return; } - setCurrentMapState((prevMapState) => { + setCurrentMapState((prevMapState: any) => { let tokens = { ...prevMapState.tokens }; for (let id in change) { if (id in tokens) { @@ -405,22 +407,21 @@ function NetworkedMapAndTokens({ session }) { }); } - function handleMapTokenStateRemove(tokenState) { - setCurrentMapState((prevMapState) => { + function handleMapTokenStateRemove(tokenState: any) { + setCurrentMapState((prevMapState: any) => { const { [tokenState.id]: old, ...rest } = prevMapState.tokens; return { ...prevMapState, tokens: rest }; }); } useEffect(() => { - async function handlePeerData({ id, data, reply }) { + // TODO: edit Map type with appropriate resolutions + async function handlePeerData({ id, data, reply }: { id: string, data: any, reply: any}) { if (id === "mapRequest") { const map = await getMapFromDB(data); - function replyWithMap(preview, resolution) { + function replyWithMap(preview?: string | undefined, resolution?: any) { let response = { ...map, - resolutions: undefined, - file: undefined, thumbnail: undefined, // Remove last modified so if there is an error // during the map request the cache is invalid @@ -429,13 +430,13 @@ function NetworkedMapAndTokens({ session }) { lastUsed: Date.now(), }; // Send preview if available - if (map.resolutions[preview]) { - response.resolutions = { [preview]: map.resolutions[preview] }; + if (preview !== undefined && map.resolutions && map.resolutions[preview]) { + response.resolutions = { [preview]: map.resolutions[preview] } as Resolutions; reply("mapResponse", response, "map"); } // Send full map at the desired resolution if available - if (map.resolutions[resolution]) { - response.file = map.resolutions[resolution].file; + if (map.resolutions && map.resolutions[resolution]) { + response.file = map.resolutions[resolution].file as Uint8Array; } else if (map.file) { // The resolution might not exist for other users so send the file instead response.file = map.file; @@ -506,7 +507,7 @@ function NetworkedMapAndTokens({ session }) { } } - function handlePeerDataProgress({ id, total, count }) { + function handlePeerDataProgress({ id, total, count }: { id: string, total: number, count: number}) { if (count === 1) { // Corresponding asset load finished called in token and map response assetLoadStart(); @@ -514,7 +515,7 @@ function NetworkedMapAndTokens({ session }) { assetProgressUpdate({ id, total, count }); } - async function handleSocketMap(map) { + async function handleSocketMap(map: any) { if (map) { if (map.type === "file") { const fullMap = await getMapFromDB(map.id); @@ -540,31 +541,31 @@ function NetworkedMapAndTokens({ session }) { const canChangeMap = !isLoading; - const canEditMapDrawing = + const canEditMapDrawing: any = currentMap && currentMapState && (currentMapState.editFlags.includes("drawing") || - currentMap.owner === userId); + currentMap?.owner === userId); const canEditFogDrawing = currentMap && currentMapState && - (currentMapState.editFlags.includes("fog") || currentMap.owner === userId); + (currentMapState.editFlags.includes("fog") || currentMap?.owner === userId); const canEditNotes = currentMap && currentMapState && (currentMapState.editFlags.includes("notes") || - currentMap.owner === userId); + currentMap?.owner === userId); - const disabledMapTokens = {}; + const disabledMapTokens: { [key: string]: any } = {}; // If we have a map and state and have the token permission disabled // and are not the map owner if ( currentMapState && currentMap && !currentMapState.editFlags.includes("tokens") && - currentMap.owner !== userId + currentMap?.owner !== userId ) { for (let token of Object.values(currentMapState.tokens)) { if (token.owner !== userId) { diff --git a/src/network/NetworkedMapPointer.js b/src/network/NetworkedMapPointer.tsx similarity index 86% rename from src/network/NetworkedMapPointer.js rename to src/network/NetworkedMapPointer.tsx index 6398eb1..8b58304 100644 --- a/src/network/NetworkedMapPointer.js +++ b/src/network/NetworkedMapPointer.tsx @@ -8,11 +8,12 @@ import { isEmpty } from "../helpers/shared"; import Vector2 from "../helpers/Vector2"; import useSetting from "../hooks/useSetting"; +import Session from "./Session"; // Send pointer updates every 50ms (20fps) const sendTickRate = 50; -function NetworkedMapPointer({ session, active }) { +function NetworkedMapPointer({ session, active }: { session: Session, active: boolean }) { const { userId } = useAuth(); const [localPointerState, setLocalPointerState] = useState({}); const [pointerColor] = useSetting("pointer.color"); @@ -38,12 +39,12 @@ function NetworkedMapPointer({ session, active }) { // Send pointer updates every sendTickRate to peers to save on bandwidth // We use requestAnimationFrame as setInterval was being blocked during // re-renders on Chrome with Windows - const ownPointerUpdateRef = useRef(); + const ownPointerUpdateRef: React.MutableRefObject<{ position: any; visible: boolean; id: any; color: any; } | undefined | null > = useRef(); useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(update); let counter = 0; - function update(time) { + function update(time: any) { request = requestAnimationFrame(update); const deltaTime = time - prevTime; counter += deltaTime; @@ -70,7 +71,7 @@ function NetworkedMapPointer({ session, active }) { }; }, []); - function updateOwnPointerState(position, visible) { + function updateOwnPointerState(position: any, visible: boolean) { setLocalPointerState((prev) => ({ ...prev, [userId]: { position, visible, id: userId, color: pointerColor }, @@ -83,24 +84,24 @@ function NetworkedMapPointer({ session, active }) { }; } - function handleOwnPointerDown(position) { + function handleOwnPointerDown(position: any) { updateOwnPointerState(position, true); } - function handleOwnPointerMove(position) { + function handleOwnPointerMove(position: any) { updateOwnPointerState(position, true); } - function handleOwnPointerUp(position) { + function handleOwnPointerUp(position: any) { updateOwnPointerState(position, false); } // Handle pointer data receive - const interpolationsRef = useRef({}); + const interpolationsRef: React.MutableRefObject = useRef({}); useEffect(() => { // TODO: Handle player disconnect while pointer visible - function handleSocketPlayerPointer(pointer) { - const interpolations = interpolationsRef.current; + function handleSocketPlayerPointer(pointer: any) { + const interpolations: any = interpolationsRef.current; const id = pointer.id; if (!(id in interpolations)) { interpolations[id] = { @@ -145,8 +146,8 @@ function NetworkedMapPointer({ session, active }) { function animate() { request = requestAnimationFrame(animate); const time = performance.now(); - let interpolatedPointerState = {}; - for (let interp of Object.values(interpolationsRef.current)) { + let interpolatedPointerState: any = {}; + for (let interp of Object.values(interpolationsRef.current) as any) { if (!interp.from || !interp.to) { continue; } @@ -191,7 +192,7 @@ function NetworkedMapPointer({ session, active }) { return ( - {Object.values(localPointerState).map((pointer) => ( + {Object.values(localPointerState).map((pointer: any) => ( (null); const [partyStreams, setPartyStreams] = useState({}); const { addToast } = useToasts(); - function handleStreamStart(localStream) { + function handleStreamStart(localStream: MediaStream) { setStream(localStream); const tracks = localStream.getTracks(); for (let track of tracks) { // Only add the audio track of the stream to the remote peer if (track.kind === "audio") { for (let player of Object.values(partyState)) { - session.startStreamTo(player.sessionId, track, localStream); + props.session.startStreamTo(player.sessionId, track, localStream); } } } @@ -48,16 +50,16 @@ function NetworkedParty({ gameId, session }) { // Only sending audio so only remove the audio track if (track.kind === "audio") { for (let player of Object.values(partyState)) { - session.endStreamTo(player.sessionId, track, localStream); + props.session.endStreamTo(player.sessionId, track, localStream); } } } }, - [session, partyState] + [props.session, partyState] ); // Keep a reference to players who have just joined to show the joined notification - const joinedPlayersRef = useRef([]); + const joinedPlayersRef = useRef([]); useEffect(() => { if (joinedPlayersRef.current.length > 0) { for (let id of joinedPlayersRef.current) { @@ -70,12 +72,12 @@ function NetworkedParty({ gameId, session }) { }, [partyState, addToast]); useEffect(() => { - function handlePlayerJoined(sessionId) { + function handlePlayerJoined(sessionId: string) { if (stream) { const tracks = stream.getTracks(); for (let track of tracks) { if (track.kind === "audio") { - session.startStreamTo(sessionId, track, stream); + props.session.startStreamTo(sessionId, track, stream); } } } @@ -84,20 +86,20 @@ function NetworkedParty({ gameId, session }) { joinedPlayersRef.current.push(sessionId); } - function handlePlayerLeft(sessionId) { + function handlePlayerLeft(sessionId: string) { if (partyState[sessionId]) { addToast(`${partyState[sessionId].nickname} left the party`); } } - function handlePeerTrackAdded({ peer, stream: remoteStream }) { + function handlePeerTrackAdded({ peer, stream: remoteStream }: { peer: SessionPeer, stream: MediaStream}) { setPartyStreams((prevStreams) => ({ ...prevStreams, [peer.id]: remoteStream, })); } - function handlePeerTrackRemoved({ peer, stream: remoteStream }) { + function handlePeerTrackRemoved({ peer, stream: remoteStream }: { peer: SessionPeer, stream: MediaStream }) { if (isStreamStopped(remoteStream)) { setPartyStreams((prevStreams) => omit(prevStreams, [peer.id])); } else { @@ -108,16 +110,16 @@ function NetworkedParty({ gameId, session }) { } } - session.on("playerJoined", handlePlayerJoined); - session.on("playerLeft", handlePlayerLeft); - session.on("peerTrackAdded", handlePeerTrackAdded); - session.on("peerTrackRemoved", handlePeerTrackRemoved); + props.session.on("playerJoined", handlePlayerJoined); + props.session.on("playerLeft", handlePlayerLeft); + props.session.on("peerTrackAdded", handlePeerTrackAdded); + props.session.on("peerTrackRemoved", handlePeerTrackRemoved); return () => { - session.off("playerJoined", handlePlayerJoined); - session.off("playerLeft", handlePlayerLeft); - session.off("peerTrackAdded", handlePeerTrackAdded); - session.off("peerTrackRemoved", handlePeerTrackRemoved); + props.session.off("playerJoined", handlePlayerJoined); + props.session.off("playerLeft", handlePlayerLeft); + props.session.off("peerTrackAdded", handlePeerTrackAdded); + props.session.off("peerTrackRemoved", handlePeerTrackRemoved); }; }); @@ -140,7 +142,7 @@ function NetworkedParty({ gameId, session }) { return ( <> (); - const [stripe, setStripe] = useState(); + const [stripe, setStripe]: [ stripe: Stripe | undefined, setStripe: React.Dispatch] = useState(); useEffect(() => { import("@stripe/stripe-js").then(({ loadStripe }) => { - loadStripe(process.env.REACT_APP_STRIPE_API_KEY) + loadStripe(process.env.REACT_APP_STRIPE_API_KEY as string) .then((stripe) => { - setStripe(stripe); - setLoading(false); + if (stripe) { + setStripe(stripe); + setLoading(false); + } }) .catch((error) => { logError(error); + // TODO: check setError -> cannot work with value as a string setError(error.message); setLoading(false); }); }); }, []); - async function handleSubmit(event) { + async function handleSubmit(event: any) { event.preventDefault(); if (loading) { return; @@ -64,9 +73,9 @@ function Donate() { } ); const session = await response.json(); - const result = await stripe.redirectToCheckout({ sessionId: session.id }); + const result = await stripe?.redirectToCheckout({ sessionId: session.id }); - if (result.error) { + if (result?.error) { setError(result.error.message); } } @@ -74,10 +83,11 @@ function Donate() { const [selectedPrice, setSelectedPrice] = useState("Medium"); const [value, setValue] = useState(15); - function handlePriceChange(price) { + function handlePriceChange(price: Price) { setValue(price.value); setSelectedPrice(price.name); } + return ( setValue(e.target.value)} + onChange={(e: any) => setValue(e.target.value)} /> )} @@ -159,7 +169,7 @@ function Donate() {
{loading && } - setError()} /> + setError(undefined)} />
); } diff --git a/src/routes/FAQ.js b/src/routes/FAQ.tsx similarity index 98% rename from src/routes/FAQ.js rename to src/routes/FAQ.tsx index 29c90e7..a1c1d1b 100644 --- a/src/routes/FAQ.js +++ b/src/routes/FAQ.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Flex, Text, Box } from "theme-ui"; import raw from "raw.macro"; diff --git a/src/routes/Game.js b/src/routes/Game.tsx similarity index 92% rename from src/routes/Game.js rename to src/routes/Game.tsx index 190fb86..204fd3f 100644 --- a/src/routes/Game.js +++ b/src/routes/Game.tsx @@ -25,7 +25,7 @@ import NetworkedParty from "../network/NetworkedParty"; import Session from "../network/Session"; function Game() { - const { id: gameId } = useParams(); + const { id: gameId }: { id: string } = useParams(); const { password } = useAuth(); const { databaseStatus } = useDatabase(); @@ -44,9 +44,9 @@ function Game() { }, [session]); // Handle session errors - const [peerError, setPeerError] = useState(null); + const [peerError, setPeerError]: [ peerError: any, setPeerError: React.Dispatch] = useState(null); useEffect(() => { - function handlePeerError({ error }) { + function handlePeerError({ error }: { error: any }) { if (error.code === "ERR_WEBRTC_SUPPORT") { setPeerError("WebRTC not supported."); } else if (error.code === "ERR_CREATE_OFFER") { @@ -60,7 +60,7 @@ function Game() { }, [session]); useEffect(() => { - function handleStatus(status) { + function handleStatus(status: any) { setSessionStatus(status); } @@ -92,7 +92,7 @@ function Game() { } }, [gameId, password, databaseStatus, session, sessionStatus]); - function handleAuthSubmit(newPassword) { + function handleAuthSubmit(newPassword: string) { if (databaseStatus !== "loading") { session.joinGame(gameId, newPassword); } @@ -100,7 +100,7 @@ function Game() { // A ref to the Konva stage // the ref will be assigned in the MapInteraction component - const mapStageRef = useRef(); + const mapStageRef: React.MutableRefObject = useRef(); return ( diff --git a/src/routes/HowTo.js b/src/routes/HowTo.tsx similarity index 99% rename from src/routes/HowTo.js rename to src/routes/HowTo.tsx index ff445b9..67690bb 100644 --- a/src/routes/HowTo.js +++ b/src/routes/HowTo.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Flex, Text } from "theme-ui"; import raw from "raw.macro"; import { useLocation } from "react-router-dom"; diff --git a/src/routes/ReleaseNotes.js b/src/routes/ReleaseNotes.tsx similarity index 99% rename from src/routes/ReleaseNotes.js rename to src/routes/ReleaseNotes.tsx index da984ba..7e0ccf0 100644 --- a/src/routes/ReleaseNotes.js +++ b/src/routes/ReleaseNotes.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Flex, Text } from "theme-ui"; import raw from "raw.macro"; import { useLocation } from "react-router-dom"; diff --git a/tsconfig.json b/tsconfig.json index 342e6cb..7ba5df8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "outDir": "./dist", "target": "es6", "lib": [ "dom", diff --git a/yarn.lock b/yarn.lock index e0b4faf..0b1a73e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2603,35 +2603,35 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" -"@tensorflow/tfjs-backend-cpu@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.3.0.tgz#aa0a3ed2c6237a6e0c169678c5bd4b5a88766b1c" - integrity sha512-DLctv+PUZni26kQW1hq8jwQQ8u+GGc/p764WQIC4/IDagGtfGAUW1mHzWcTxtni2l4re1VrwE41ogWLhv4sGHg== +"@tensorflow/tfjs-backend-cpu@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.6.0.tgz#4e64a7cf1c33b203f71f8f77cd7b0ac1ef25a871" + integrity sha512-ZpAs17hPdKXadbtNjAsymYUILe8V7+pY4fYo8j25nfDTW/HfBpyAwsHPbMcA/n5zyJ7ZJtGKFcCUv1sl24KL1Q== dependencies: "@types/seedrandom" "2.4.27" seedrandom "2.4.3" -"@tensorflow/tfjs-backend-webgl@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.3.0.tgz#29dd665f6a856c9defcb9108164f845e1fdcd02e" - integrity sha512-GWCtXbrjPTyye3ooId9GlcNDwnIMskZarUpNIQ5g/zeISLfwEQoutA/UqJF+HzuEHgGMsWFkmaO3xKVT7UMpdg== +"@tensorflow/tfjs-backend-webgl@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.6.0.tgz#1ea1a73abea8d6324fd81aedf7f187ab6eb73692" + integrity sha512-zp7l4TmD1khgeSux/Ujaaj8M/v+e8JVIKjOci6HCGaeMNrn74lTSH9oqGPWKUCmpZME17/V0LfRHK34ddmrPSA== dependencies: - "@tensorflow/tfjs-backend-cpu" "3.3.0" + "@tensorflow/tfjs-backend-cpu" "3.6.0" "@types/offscreencanvas" "~2019.3.0" "@types/seedrandom" "2.4.27" "@types/webgl-ext" "0.0.30" "@types/webgl2" "0.0.5" seedrandom "2.4.3" -"@tensorflow/tfjs-converter@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-converter/-/tfjs-converter-3.3.0.tgz#d9f2ffd0fbdbb47c07d5fd7c3e5dc180cff317aa" - integrity sha512-k57wN4yelePhmO9orcT/wzGMIuyedrMpVtg0FhxpV6BQu0+TZ/ti3W4Kb97GWJsoHKXMoing9SnioKfVnBW6hw== +"@tensorflow/tfjs-converter@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-converter/-/tfjs-converter-3.6.0.tgz#32b3ff31b47e29630a82e30fbe01708facad7fd6" + integrity sha512-9MtatbTSvo3gpEulYI6+byTA3OeXSMT2lzyGAegXO9nMxsvjR01zBvlZ5SmsNyecNh6fMSzdL2+cCdQfQtsIBg== -"@tensorflow/tfjs-core@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-core/-/tfjs-core-3.3.0.tgz#3d26bd03cb58e0ecf46c96d118c39c4a90b7f5ed" - integrity sha512-6G+LcCiQBl4Kza5mDbWbf8QSWBTW3l7SDjGhQzMO1ITtQatHzxkuHGHcJ4CTUJvNA0JmKf4QJWOvlFqEmxwyLQ== +"@tensorflow/tfjs-core@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-core/-/tfjs-core-3.6.0.tgz#6b4d8175790bdff78868eabe6adc6442eb4dc276" + integrity sha512-bb2c3zwK4SgXZRvkTiC7EhCpWbCGp0GMd+1/3Vo2/Z54jiLB/h3sXIgHQrTNiWwhKPtst/xxA+MsslFlvD0A5w== dependencies: "@types/offscreencanvas" "~2019.3.0" "@types/seedrandom" "2.4.27" @@ -2639,30 +2639,30 @@ node-fetch "~2.6.1" seedrandom "2.4.3" -"@tensorflow/tfjs-data@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-data/-/tfjs-data-3.3.0.tgz#ba943bd6a486fa4cb3ca312c12646ea4dcf6cce4" - integrity sha512-0x28tRe6RJu5GmYq3IYN2GNnOgXU0nY+o6zZrlijkK+W3vjSTJlZzaBSifoeD6J8gzVpjs8W8qd/JKHQ1MQp8w== +"@tensorflow/tfjs-data@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-data/-/tfjs-data-3.6.0.tgz#af2f03cffb75ad8e4c2f46e192e392d9b7f977ed" + integrity sha512-5KU7fnU7cj/opb4aCNDoW4qma64ggDwI0PCs5KEO41T3waVHDLk6bjlFlBVRdjfZqvM0K6EfWEyoiXzdvz/Ieg== dependencies: "@types/node-fetch" "^2.1.2" node-fetch "~2.6.1" -"@tensorflow/tfjs-layers@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-layers/-/tfjs-layers-3.3.0.tgz#d2097c5b22ec12e5fdbe470a88ca0a34a95ca11f" - integrity sha512-qO+TL2I29vWUiuFcQJXNyayWFYagwR+SIfbex8p5jjYaCGHGwE5GQcrH+ngoCgKZxm5tdMvYJsJPnih2M3fYzQ== +"@tensorflow/tfjs-layers@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-layers/-/tfjs-layers-3.6.0.tgz#5358af559fc8baed304b3e567319fe93f1aa46a6" + integrity sha512-B7EHwAT6KFqhKzdf0e2Sr6haj9qpqpyEATV8OCPHdk+g8z2AGXOLlFfbgW6vCMjy1wb5jzYqCyZDoY3EWdgJAw== -"@tensorflow/tfjs@^3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@tensorflow/tfjs/-/tfjs-3.3.0.tgz#db92099dd48c0eb1c1673f705125d2b57496a1a3" - integrity sha512-xo22GCUCGcPtNGIdDpLPrp9ms3atXmzX8AF4y3aIBEwK5KlvGe+ZhcoQ2xEOCPQGBr7NB7AO6rwT8gRoziAHVg== +"@tensorflow/tfjs@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@tensorflow/tfjs/-/tfjs-3.6.0.tgz#e65956cd40c96523e3f5ec7a58a4bef9ef5e349c" + integrity sha512-uLDMDzyRkJa3fYBeR6etQTFD/t+nkQIH/DznL9hxmYoIYG8PigY2gcrc482TAvsdhiuvxCZ9rl5SyDtP93MvxQ== dependencies: - "@tensorflow/tfjs-backend-cpu" "3.3.0" - "@tensorflow/tfjs-backend-webgl" "3.3.0" - "@tensorflow/tfjs-converter" "3.3.0" - "@tensorflow/tfjs-core" "3.3.0" - "@tensorflow/tfjs-data" "3.3.0" - "@tensorflow/tfjs-layers" "3.3.0" + "@tensorflow/tfjs-backend-cpu" "3.6.0" + "@tensorflow/tfjs-backend-webgl" "3.6.0" + "@tensorflow/tfjs-converter" "3.6.0" + "@tensorflow/tfjs-core" "3.6.0" + "@tensorflow/tfjs-data" "3.6.0" + "@tensorflow/tfjs-layers" "3.6.0" argparse "^1.0.10" chalk "^4.1.0" core-js "3" @@ -2873,6 +2873,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/file-saver@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.2.tgz#bd593ccfaee42ff94a5c1c83bf69ae9be83493b9" + integrity sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA== + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -2943,6 +2948,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash.clonedeep@^4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz#3b6c40a0affe0799a2ce823b440a6cf33571d32b" + integrity sha512-cE1jYr2dEg1wBImvXlNtp0xDoS79rfEdGozQVgliDZj1uERH4k+rmEMTudP9b4VQ8O6nRb5gPqft0QzEQGMQgA== + dependencies: + "@types/lodash" "*" + "@types/lodash.get@^4.4.6": version "4.4.6" resolved "https://registry.yarnpkg.com/@types/lodash.get/-/lodash.get-4.4.6.tgz#0c7ac56243dae0f9f09ab6f75b29471e2e777240" From 123ebd880a2294a90f816d412b96530b40325ed7 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 8 Jul 2021 12:00:47 +1000 Subject: [PATCH 011/142] Format --- src/components/map/Map.tsx | 188 +++++++++++++++-------------- src/components/map/MapTiles.js | 12 +- src/components/token/TokenTiles.js | 12 +- src/contexts/MapStageContext.tsx | 4 +- src/helpers/Vector2.ts | 9 +- src/helpers/konva.tsx | 42 +++++-- 6 files changed, 151 insertions(+), 116 deletions(-) diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index ab8906f..fe03609 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -31,72 +31,70 @@ import Session from "../../network/Session"; import { Grid } from "../../helpers/grid"; import { ImageFile } from "../../helpers/image"; -export type Resolutions = Record +export type Resolutions = Record; export type Map = { - id: string, - name: string, - owner: string, - file?: Uint8Array, - quality?: string, - resolutions?: Resolutions, - grid: Grid, - group: string, - width: number, - height: number, - type: string, - lastUsed: number, - lastModified: number, - created: number, - showGrid: boolean, - snapToGrid: boolean, - thumbnail?: ImageFile, -} + id: string; + name: string; + owner: string; + file?: Uint8Array; + quality?: string; + resolutions?: Resolutions; + grid: Grid; + group: string; + width: number; + height: number; + type: string; + lastUsed: number; + lastModified: number; + created: number; + showGrid: boolean; + snapToGrid: boolean; + thumbnail?: ImageFile; +}; export type Note = { - id: string, - color: string, - lastModified: number, - lastModifiedBy: string, - locked: boolean, - size: number, - text: string, - textOnly: boolean, - visible: boolean, - x: number, - y: number, -} + id: string; + color: string; + lastModified: number; + lastModifiedBy: string; + locked: boolean; + size: number; + text: string; + textOnly: boolean; + visible: boolean; + x: number; + y: number; +}; export type TokenState = { - id: string, - tokenId: string, - owner: string, - size: number, - label: string, - status: string[], - x: number, - y: number, - lastModifiedBy: string, - lastModified: number, - rotation: number, - locked: boolean, - visible: boolean -} + id: string; + tokenId: string; + owner: string; + size: number; + category: string; + label: string; + statuses: string[]; + x: number; + y: number; + lastModifiedBy: string; + lastModified: number; + rotation: number; + locked: boolean; + visible: boolean; + type: "default" | "file"; + outline: any; + width: number; + height: number; +}; -interface PathId extends Path { - id: string -} - -interface ShapeId extends Shape { - id: string -} export type MapState = { - tokens: Record, - drawShapes: PathId | ShapeId, - fogShapes: Fog[], - editFlags: ["drawing", "tokens", "notes", "fog"], - notes: Note[], - mapId: string, -} + tokens: Record; + drawShapes: Record; + fogShapes: Record; + editFlags: ["drawing", "tokens", "notes", "fog"]; + notes: Record; + mapId: string; +}; function Map({ map, @@ -121,34 +119,35 @@ function Map({ disabledTokens, session, }: { - map: any - mapState: MapState - mapActions: any, - onMapTokenStateChange: any, - onMapTokenStateRemove: any, - onMapChange: any, - onMapReset: any, - onMapDraw: any, - onMapDrawUndo: any, - onMapDrawRedo: any, - onFogDraw: any, - onFogDrawUndo: any, - onFogDrawRedo: any, - onMapNoteChange: any, - onMapNoteRemove: any, - allowMapDrawing: boolean, - allowFogDrawing: boolean, - allowMapChange: boolean, - allowNoteEditing: boolean, - disabledTokens: any, - session: Session + map: any; + mapState: MapState; + mapActions: any; + onMapTokenStateChange: any; + onMapTokenStateRemove: any; + onMapChange: any; + onMapReset: any; + onMapDraw: any; + onMapDrawUndo: any; + onMapDrawRedo: any; + onFogDraw: any; + onFogDrawUndo: any; + onFogDrawRedo: any; + onMapNoteChange: any; + onMapNoteRemove: any; + allowMapDrawing: boolean; + allowFogDrawing: boolean; + allowMapChange: boolean; + allowNoteEditing: boolean; + disabledTokens: any; + session: Session; }) { const { addToast } = useToasts(); const { tokensById } = useTokenData(); const [selectedToolId, setSelectedToolId] = useState("move"); - const { settings, setSettings }: { settings: any, setSettings: any} = useSettings(); + const { settings, setSettings }: { settings: any; setSettings: any } = + useSettings(); function handleToolSettingChange(tool: any, change: any) { setSettings((prevSettings: any) => ({ @@ -224,7 +223,10 @@ function Map({ disabledControls.push("note"); } - const disabledSettings: { fog: any[], drawing: any[]} = { fog: [], drawing: [] }; + const disabledSettings: { fog: any[]; drawing: any[] } = { + fog: [], + drawing: [], + }; if (drawShapes.length === 0) { disabledSettings.drawing.push("erase"); } @@ -263,9 +265,18 @@ function Map({ /> ); - const [isTokenMenuOpen, setIsTokenMenuOpen]: [ isTokenMenuOpen: boolean, setIsTokenMenuOpen: React.Dispatch>] = useState(false); - const [tokenMenuOptions, setTokenMenuOptions]: [ tokenMenuOptions: any, setTokenMenuOptions: any ] = useState({}); - const [tokenDraggingOptions, setTokenDraggingOptions]: [ tokenDraggingOptions: any, setTokenDragginOptions: any ] = useState(); + const [isTokenMenuOpen, setIsTokenMenuOpen]: [ + isTokenMenuOpen: boolean, + setIsTokenMenuOpen: React.Dispatch> + ] = useState(false); + const [tokenMenuOptions, setTokenMenuOptions]: [ + tokenMenuOptions: any, + setTokenMenuOptions: any + ] = useState({}); + const [tokenDraggingOptions, setTokenDraggingOptions]: [ + tokenDraggingOptions: any, + setTokenDragginOptions: any + ] = useState(); function handleTokenMenuOpen(tokenStateId: string, tokenImage: any) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); @@ -338,10 +349,7 @@ function Map({ const mapGrid = map && map.showGrid && ; const mapMeasure = ( - + ); const mapPointer = ( @@ -353,7 +361,7 @@ function Map({ const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); const [noteMenuOptions, setNoteMenuOptions] = useState({}); - const [noteDraggingOptions, setNoteDraggingOptions]= useState(); + const [noteDraggingOptions, setNoteDraggingOptions] = useState(); function handleNoteMenuOpen(noteId: string, noteNode: any) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js index 811bb0c..c7832b6 100644 --- a/src/components/map/MapTiles.js +++ b/src/components/map/MapTiles.js @@ -11,12 +11,8 @@ import { getGroupItems } from "../../helpers/group"; import { useGroup } from "../../contexts/GroupContext"; function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) { - const { - selectedGroupIds, - selectMode, - onGroupOpen, - onGroupSelect, - } = useGroup(); + const { selectedGroupIds, selectMode, onGroupOpen, onGroupSelect } = + useGroup(); function renderTile(group) { if (group.type === "item") { @@ -66,4 +62,8 @@ function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) { ); } +MapTiles.defaultProps = { + subgroup: false, +}; + export default MapTiles; diff --git a/src/components/token/TokenTiles.js b/src/components/token/TokenTiles.js index 55b5948..eabaaa5 100644 --- a/src/components/token/TokenTiles.js +++ b/src/components/token/TokenTiles.js @@ -12,12 +12,8 @@ import { getGroupItems } from "../../helpers/group"; import { useGroup } from "../../contexts/GroupContext"; function TokenTiles({ tokensById, onTokenEdit, subgroup }) { - const { - selectedGroupIds, - selectMode, - onGroupOpen, - onGroupSelect, - } = useGroup(); + const { selectedGroupIds, selectMode, onGroupOpen, onGroupSelect } = + useGroup(); function renderTile(group) { if (group.type === "item") { @@ -70,4 +66,8 @@ function TokenTiles({ tokensById, onTokenEdit, subgroup }) { ); } +TokenTiles.defaultProps = { + subgroup: false, +}; + export default TokenTiles; diff --git a/src/contexts/MapStageContext.tsx b/src/contexts/MapStageContext.tsx index cd15161..8f9a30b 100644 --- a/src/contexts/MapStageContext.tsx +++ b/src/contexts/MapStageContext.tsx @@ -1,8 +1,6 @@ import React, { useContext } from "react"; -const MapStageContext = React.createContext({ - mapStageRef: { current: null }, -}); +const MapStageContext = React.createContext({ current: null }); export const MapStageProvider: any = MapStageContext.Provider; export function useMapStage() { diff --git a/src/helpers/Vector2.ts b/src/helpers/Vector2.ts index 8127ed7..2f9ef6b 100644 --- a/src/helpers/Vector2.ts +++ b/src/helpers/Vector2.ts @@ -44,10 +44,11 @@ class Vector2 { } /** + * Returns the length of vector `p` Note: magnitude to not conflict with native length property * @param {Vector2} p * @returns {number} Length of `p` */ - static setLength(p: Vector2): number { + static magnitude(p: Vector2): number { return Math.sqrt(this.lengthSquared(p)); } @@ -56,7 +57,7 @@ class Vector2 { * @returns {Vector2} `p` normalized, if length of `p` is 0 `{x: 0, y: 0}` is returned */ static normalize(p: Vector2): Vector2 { - const l = this.setLength(p); + const l = this.magnitude(p); if (l === 0) { return { x: 0, y: 0 }; } @@ -271,7 +272,7 @@ class Vector2 { const pa = this.subtract(p, a); const ba = this.subtract(b, a); const h = Math.min(Math.max(this.dot(pa, ba) / this.dot(ba, ba), 0), 1); - const distance = this.setLength(this.subtract(pa, this.multiply(ba, h))); + const distance = this.magnitude(this.subtract(pa, this.multiply(ba, h))); const point = this.add(a, this.multiply(ba, h)); return { distance, point }; } @@ -443,7 +444,7 @@ class Vector2 { * @returns {number} */ static distance(a: Vector2, b: Vector2): number { - return this.setLength(this.subtract(a, b)); + return this.magnitude(this.subtract(a, b)); } /** diff --git a/src/helpers/konva.tsx b/src/helpers/konva.tsx index 3dce716..d26d959 100644 --- a/src/helpers/konva.tsx +++ b/src/helpers/konva.tsx @@ -6,7 +6,7 @@ import Color from "color"; import Vector2 from "./Vector2"; // Holes should be wound in the opposite direction as the containing points array -export function HoleyLine({ holes, ...props }: { holes: any, props: []}) { +export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) { // Converted from https://github.com/rfestag/konva/blob/master/src/shapes/Line.ts function drawLine(points: number[], context: any, shape: any) { const length = points.length; @@ -109,7 +109,19 @@ export function HoleyLine({ holes, ...props }: { holes: any, props: []}) { return ; } -export function Tick({ x, y, scale, onClick, cross }: { x: any, y: any, scale: any, onClick: any, cross: any}) { +export function Tick({ + x, + y, + scale, + onClick, + cross, +}: { + x: any; + y: any; + scale: any; + onClick: any; + cross: any; +}) { const [fill, setFill] = useState("white"); function handleEnter() { setFill("hsl(260, 100%, 80%)"); @@ -145,10 +157,22 @@ export function Tick({ x, y, scale, onClick, cross }: { x: any, y: any, scale: a } interface TrailPoint extends Vector2 { - lifetime: number + lifetime: number; } -export function Trail({ position, size, duration, segments, color }: { position: Vector2, size: any, duration: number, segments: any, color: string }) { +export function Trail({ + position, + size, + duration, + segments, + color, +}: { + position: Vector2; + size: any; + duration: number; + segments: any; + color: string; +}) { const trailRef: React.MutableRefObject = useRef(); const pointsRef: React.MutableRefObject = useRef([]); const prevPositionRef = useRef(position); @@ -259,7 +283,7 @@ export function Trail({ position, size, duration, segments, color }: { position: // Create a radial gradient from the center of the trail to the tail const gradientCenter = resampledPoints[resampledPoints.length - 1]; const gradientEnd = resampledPoints[0]; - const gradientRadius = Vector2.setLength( + const gradientRadius = Vector2.magnitude( Vector2.subtract(gradientCenter, gradientEnd) ); let gradient = context.createRadialGradient( @@ -302,7 +326,9 @@ Trail.defaultProps = { * @param {Konva.Node} node * @returns {Vector2} */ -export function getRelativePointerPosition(node: Konva.Node): { x: number, y: number } | undefined { +export function getRelativePointerPosition( + node: Konva.Node +): { x: number; y: number } | undefined { let transform = node.getAbsoluteTransform().copy(); transform.invert(); // TODO: handle possible null value @@ -314,7 +340,9 @@ export function getRelativePointerPosition(node: Konva.Node): { x: number, y: nu return transform.point(position); } -export function getRelativePointerPositionNormalized(node: Konva.Node): { x: number, y: number } | undefined { +export function getRelativePointerPositionNormalized( + node: Konva.Node +): { x: number; y: number } | undefined { const relativePosition = getRelativePointerPosition(node); if (!relativePosition) { // TODO: handle possible null value From 45a4443dd1f7da0835e07d4b04801d1cb1ac26dc Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 8 Jul 2021 12:01:02 +1000 Subject: [PATCH 012/142] Add standalone type definitions --- src/types/Asset.ts | 8 +++++ src/types/Dice.ts | 20 +++++++++++ src/types/Drawing.ts | 74 ++++++++++++++++++++++++++++++++++++++++ src/types/Fog.ts | 29 ++++++++++++++++ src/types/Grid.ts | 26 ++++++++++++++ src/types/Group.ts | 13 +++++++ src/types/Map.ts | 36 +++++++++++++++++++ src/types/MapState.ts | 15 ++++++++ src/types/Note.ts | 13 +++++++ src/types/Outline.ts | 21 ++++++++++++ src/types/PartyState.ts | 3 ++ src/types/PlayerState.ts | 10 ++++++ src/types/Pointer.ts | 12 +++++++ src/types/Settings.ts | 34 ++++++++++++++++++ src/types/Timer.ts | 10 ++++++ src/types/Token.ts | 31 +++++++++++++++++ src/types/TokenState.ts | 33 ++++++++++++++++++ 17 files changed, 388 insertions(+) create mode 100644 src/types/Asset.ts create mode 100644 src/types/Dice.ts create mode 100644 src/types/Drawing.ts create mode 100644 src/types/Fog.ts create mode 100644 src/types/Grid.ts create mode 100644 src/types/Group.ts create mode 100644 src/types/Map.ts create mode 100644 src/types/MapState.ts create mode 100644 src/types/Note.ts create mode 100644 src/types/Outline.ts create mode 100644 src/types/PartyState.ts create mode 100644 src/types/PlayerState.ts create mode 100644 src/types/Pointer.ts create mode 100644 src/types/Settings.ts create mode 100644 src/types/Timer.ts create mode 100644 src/types/Token.ts create mode 100644 src/types/TokenState.ts diff --git a/src/types/Asset.ts b/src/types/Asset.ts new file mode 100644 index 0000000..f19fbca --- /dev/null +++ b/src/types/Asset.ts @@ -0,0 +1,8 @@ +export type Asset = { + file: Uint8Array; + width: number; + height: number; + id: string; + owner: string; + mime: string; +}; diff --git a/src/types/Dice.ts b/src/types/Dice.ts new file mode 100644 index 0000000..7cf36b8 --- /dev/null +++ b/src/types/Dice.ts @@ -0,0 +1,20 @@ +import { InstancedMesh } from "@babylonjs/core"; + +export type DiceType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20"; + +export type DiceRoll = { + type: DiceType; + roll: number | "unknown"; +}; + +export type Dice = { + type: DiceType; + instance: InstancedMesh; + asleep: boolean; + d10Instance?: InstancedMesh; +}; + +export type DiceState = { + share: boolean; + rolls: DiceRoll[]; +}; diff --git a/src/types/Drawing.ts b/src/types/Drawing.ts new file mode 100644 index 0000000..ecef0f7 --- /dev/null +++ b/src/types/Drawing.ts @@ -0,0 +1,74 @@ +import Vector2 from "../helpers/Vector2"; + +export type DrawingToolType = + | "brush" + | "paint" + | "line" + | "rectangle" + | "circle" + | "triangle" + | "erase"; + +export type DrawingToolSettings = { + type: DrawingToolType; + color: string; + useBlending: boolean; +}; + +export type PointsData = { + points: Vector2[]; +}; + +export type RectData = { + x: number; + y: number; + width: number; + height: number; +}; + +export type CircleData = { + x: number; + y: number; + radius: number; +}; + +export type BaseDrawing = { + blend: boolean; + color: string; + id: string; + strokeWidth: number; +}; + +export type BaseShape = BaseDrawing & { + type: "shape"; +}; + +export type Line = BaseShape & { + shapeType: "line"; + data: PointsData; +}; + +export type Rectangle = BaseShape & { + shapeType: "rectangle"; + data: RectData; +}; + +export type Circle = BaseShape & { + shapeType: "circle"; + data: CircleData; +}; + +export type Triangle = BaseShape & { + shapeType: "triangle"; + data: PointsData; +}; + +export type Shape = Line | Rectangle | Circle | Triangle; + +export type Path = BaseDrawing & { + type: "path"; + pathType: "fill" | "stroke"; + data: PointsData; +}; + +export type Drawing = Shape | Path; diff --git a/src/types/Fog.ts b/src/types/Fog.ts new file mode 100644 index 0000000..ba54f36 --- /dev/null +++ b/src/types/Fog.ts @@ -0,0 +1,29 @@ +import Vector2 from "../helpers/Vector2"; + +export type FogToolType = + | "polygon" + | "rectangle" + | "brush" + | "toggle" + | "remove"; + +export type FogToolSettings = { + type: FogToolType; + multilayer: boolean; + preview: boolean; + useFogCut: boolean; +}; + +export type FogData = { + points: Vector2[]; + holes: Vector2[][]; +}; + +export type Fog = { + color: string; + data: FogData; + id: string; + strokeWidth: number; + type: "fog"; + visible: boolean; +}; diff --git a/src/types/Grid.ts b/src/types/Grid.ts new file mode 100644 index 0000000..9ff6ea5 --- /dev/null +++ b/src/types/Grid.ts @@ -0,0 +1,26 @@ +import Vector2 from "../helpers/Vector2"; + +export type GridInset = { + topLeft: Vector2; + bottomRight: Vector2; +}; + +export type GridMeasurementType = + | "chebyshev" + | "alternating" + | "euclidean" + | "manhattan"; + +export type GridMeasurement = { + type: GridMeasurementType; + scale: string; +}; + +export type GridType = "square" | "hexVertical" | "hexHorizontal"; + +export type Grid = { + inset: GridInset; + size: Vector2; + type: GridType; + measurement: GridMeasurement; +}; diff --git a/src/types/Group.ts b/src/types/Group.ts new file mode 100644 index 0000000..37faa00 --- /dev/null +++ b/src/types/Group.ts @@ -0,0 +1,13 @@ +export type GroupItem = { + id: string; + type: "item"; +}; + +export type GroupContainer = { + id: string; + type: "group"; + items: GroupItem[]; + name: string; +}; + +export type Group = GroupItem | GroupContainer; diff --git a/src/types/Map.ts b/src/types/Map.ts new file mode 100644 index 0000000..bed1abb --- /dev/null +++ b/src/types/Map.ts @@ -0,0 +1,36 @@ +import { Grid } from "./Grid"; + +export type BaseMap = { + id: string; + name: string; + owner: string; + grid: Grid; + width: number; + height: number; + type: string; + lastModified: number; + created: number; + showGrid: boolean; + snapToGrid: boolean; +}; + +export type DefaultMap = BaseMap & { + type: "default"; + key: string; +}; + +export type FileMapResolutions = { + low?: string; + medium?: string; + high?: string; + ultra?: string; +}; + +export type FileMap = BaseMap & { + type: "file"; + file: string; + resolutions: FileMapResolutions; + thumbnail: string; +}; + +export type Map = DefaultMap | FileMap; diff --git a/src/types/MapState.ts b/src/types/MapState.ts new file mode 100644 index 0000000..7b40179 --- /dev/null +++ b/src/types/MapState.ts @@ -0,0 +1,15 @@ +import { Drawing } from "./Drawing"; +import { Fog } from "./Fog"; +import { Note } from "./Note"; +import { TokenState } from "./TokenState"; + +export type EditFlag = "drawing" | "tokens" | "notes" | "fog"; + +export type MapState = { + tokens: Record; + drawShapes: Record; + fogShapes: Record; + editFlags: Array; + notes: Record; + mapId: string; +}; diff --git a/src/types/Note.ts b/src/types/Note.ts new file mode 100644 index 0000000..4e1b02c --- /dev/null +++ b/src/types/Note.ts @@ -0,0 +1,13 @@ +export type Note = { + id: string; + color: string; + lastModified: number; + lastModifiedBy: string; + locked: boolean; + size: number; + text: string; + textOnly: boolean; + visible: boolean; + x: number; + y: number; +}; diff --git a/src/types/Outline.ts b/src/types/Outline.ts new file mode 100644 index 0000000..d045cc4 --- /dev/null +++ b/src/types/Outline.ts @@ -0,0 +1,21 @@ +export type CircleOutline = { + type: "circle"; + x: number; + y: number; + radius: number; +}; + +export type RectOutline = { + type: "rect"; + width: number; + height: number; + x: number; + y: number; +}; + +export type PathOutline = { + type: "path"; + points: number[]; +}; + +export type Outline = CircleOutline | RectOutline | PathOutline; diff --git a/src/types/PartyState.ts b/src/types/PartyState.ts new file mode 100644 index 0000000..e9a453c --- /dev/null +++ b/src/types/PartyState.ts @@ -0,0 +1,3 @@ +import { PlayerState } from "./PlayerState"; + +export type PartyState = Record; diff --git a/src/types/PlayerState.ts b/src/types/PlayerState.ts new file mode 100644 index 0000000..4aa2f97 --- /dev/null +++ b/src/types/PlayerState.ts @@ -0,0 +1,10 @@ +import { Timer } from "./Timer"; +import { DiceState } from "./Dice"; + +export type PlayerState = { + nickname: string; + timer?: Timer; + dice: DiceState; + sessionId?: string; + userId?: string; +}; diff --git a/src/types/Pointer.ts b/src/types/Pointer.ts new file mode 100644 index 0000000..77e69b0 --- /dev/null +++ b/src/types/Pointer.ts @@ -0,0 +1,12 @@ +import Vector2 from "../helpers/Vector2"; + +export type PointerToolSettings = { + color: string; +}; + +export type PointerState = { + position: Vector2; + visible: boolean; + id: string; + color: string; +}; diff --git a/src/types/Settings.ts b/src/types/Settings.ts new file mode 100644 index 0000000..453a8d3 --- /dev/null +++ b/src/types/Settings.ts @@ -0,0 +1,34 @@ +import { Duration } from "./Timer"; +import { DrawingToolSettings } from "./Drawing"; +import { FogToolSettings } from "./Fog"; +import { PointerToolSettings } from "./Pointer"; + +export type DrawingSettings = DrawingToolSettings; +export type FogSettings = FogToolSettings & { + editOpacity: number; + showGuides: boolean; +}; +export type DiceSettings = { + shareDice: boolean; + style: string; +}; +export type GameSettings = { + usePassword: boolean; +}; +export type MapSettings = { + fullScreen: boolean; + labeSize: number; + gridSnappingSensitivity: number; +}; +export type PointerSettings = PointerToolSettings; +export type TimerSettings = Duration; + +export type Settings = { + dice: DiceSettings; + drawing: DrawingSettings; + fog: FogSettings; + game: GameSettings; + map: MapSettings; + pointer: PointerSettings; + timer: TimerSettings; +}; diff --git a/src/types/Timer.ts b/src/types/Timer.ts new file mode 100644 index 0000000..eb199b3 --- /dev/null +++ b/src/types/Timer.ts @@ -0,0 +1,10 @@ +export type Duration = { + hour: number; + minute: number; + second: number; +}; + +export type Timer = { + current: number; + max: number; +}; diff --git a/src/types/Token.ts b/src/types/Token.ts new file mode 100644 index 0000000..b9241c4 --- /dev/null +++ b/src/types/Token.ts @@ -0,0 +1,31 @@ +import { Outline } from "./Outline"; + +export type TokenCategory = "character" | "vehicle" | "prop"; + +export type BaseToken = { + id: string; + name: string; + defaultSize: number; + defaultCategory: TokenCategory; + defaultLabel: string; + hideInSidebar: boolean; + width: number; + height: number; + owner: string; + created: number; + lastModified: number; + outline: Outline; +}; + +export type DefaultToken = BaseToken & { + type: "default"; + key: string; +}; + +export type FileToken = BaseToken & { + type: "file"; + file: string; + thumbnail: string; +}; + +export type Token = DefaultToken | FileToken; diff --git a/src/types/TokenState.ts b/src/types/TokenState.ts new file mode 100644 index 0000000..c5360f8 --- /dev/null +++ b/src/types/TokenState.ts @@ -0,0 +1,33 @@ +import { Outline } from "./Outline"; + +export type BaseTokenState = { + id: string; + tokenId: string; + owner: string; + size: number; + category: string; + label: string; + statuses: string[]; + x: number; + y: number; + lastModifiedBy: string; + lastModified: number; + rotation: number; + locked: boolean; + visible: boolean; + outline: Outline; + width: number; + height: number; +}; + +export type DefaultTokenState = BaseTokenState & { + type: "default"; + key: string; +}; + +export type FileTokenState = BaseTokenState & { + type: "file"; + file: string; +}; + +export type TokenState = DefaultTokenState | FileTokenState; From ecfab87aa0883604a25c3a0e1c120caaf4c26456 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 9 Jul 2021 16:22:35 +1000 Subject: [PATCH 013/142] More typescript --- package.json | 5 +- src/components/{Search.js => Search.tsx} | 5 +- .../dice/{DiceButton.js => DiceButton.tsx} | 16 +- ...DiceButtonCount.js => DiceButtonCount.tsx} | 2 +- .../dice/{DiceButtons.js => DiceButtons.tsx} | 35 +++- ...DiceInteraction.js => DiceInteraction.tsx} | 102 ++++++--- .../dice/{DiceResults.js => DiceResults.tsx} | 15 +- .../dice/{DiceTile.js => DiceTile.tsx} | 12 +- .../dice/{DiceTiles.js => DiceTiles.tsx} | 17 +- ...DiceTrayOverlay.js => DiceTrayOverlay.tsx} | 77 ++++--- ...lectDiceButton.js => SelectDiceButton.tsx} | 22 +- .../drag/{Draggable.js => Draggable.tsx} | 9 +- .../drag/{Droppable.js => Droppable.tsx} | 7 +- src/components/map/Map.tsx | 68 ------ .../tile/{LazyTile.js => LazyTile.tsx} | 6 +- .../{SortableTile.js => SortableTile.tsx} | 18 +- .../{SortableTiles.js => SortableTiles.tsx} | 24 ++- ...verlay.js => SortableTilesDragOverlay.tsx} | 13 +- src/components/tile/{Tile.js => Tile.tsx} | 14 +- .../{TileActionBar.js => TileActionBar.tsx} | 10 +- .../{TilesContainer.js => TilesContainer.tsx} | 6 +- .../{TilesOverlay.js => TilesOverlay.tsx} | 49 +++-- .../{AssetsContext.js => AssetsContext.tsx} | 180 +++++++--------- src/contexts/AuthContext.tsx | 6 +- src/contexts/DragContext.js | 75 ------- src/contexts/DragContext.tsx | 68 ++++++ .../{GroupContext.js => GroupContext.tsx} | 84 ++++++-- src/contexts/MapDataContext.tsx | 4 +- ...TileDragContext.js => TileDragContext.tsx} | 59 ++++-- .../{UserIdContext.js => UserIdContext.tsx} | 16 +- src/dice/Dice.ts | 56 ++++- src/dice/index.ts | 6 +- src/dice/walnut/WalnutDice.ts | 5 +- src/helpers/Vector2.ts | 30 ++- src/helpers/dice.ts | 3 +- src/helpers/drawing.ts | 198 +++++------------- src/helpers/grid.ts | 131 ++++++------ src/helpers/{group.js => group.ts} | 166 +++++++-------- src/helpers/image.ts | 125 +++++------ src/helpers/{map.js => map.ts} | 92 ++++---- src/helpers/shared.ts | 34 +-- src/helpers/{token.js => token.ts} | 84 ++++++-- src/hooks/useDebounce.tsx | 4 +- src/modals/AuthModal.tsx | 9 +- src/modals/ChangeNicknameModal.tsx | 20 +- src/modals/ConfirmModal.tsx | 16 +- src/modals/EditGroupModal.tsx | 73 ------- src/modals/EditMapModal.tsx | 5 +- src/modals/EditTokenModal.tsx | 4 +- src/modals/GameExpiredModal.tsx | 7 +- src/modals/GettingStartedModal.tsx | 10 +- .../{GroupNameModal.js => GroupNameModal.tsx} | 22 +- src/modals/ImportExportModal.tsx | 35 ++-- src/modals/JoinModal.tsx | 12 +- src/modals/SelectDataModal.tsx | 94 +++++---- src/modals/SelectDiceModal.tsx | 24 ++- src/modals/SelectMapModal.tsx | 24 ++- src/modals/SelectTokensModal.tsx | 17 +- src/tokens/index.ts | 31 --- src/types/Dice.ts | 13 +- src/types/Drawing.ts | 4 + src/types/Grid.ts | 13 ++ src/types/Map.ts | 1 + src/types/external/image.outline.d.ts | 14 ++ tsconfig.json | 6 +- yarn.lock | 171 ++++++++------- 66 files changed, 1350 insertions(+), 1233 deletions(-) rename src/components/{Search.js => Search.tsx} (88%) rename src/components/dice/{DiceButton.js => DiceButton.tsx} (58%) rename src/components/dice/{DiceButtonCount.js => DiceButtonCount.tsx} (87%) rename src/components/dice/{DiceButtons.js => DiceButtons.tsx} (83%) rename src/components/dice/{DiceInteraction.js => DiceInteraction.tsx} (65%) rename src/components/dice/{DiceResults.js => DiceResults.tsx} (90%) rename src/components/dice/{DiceTile.js => DiceTile.tsx} (57%) rename src/components/dice/{DiceTiles.js => DiceTiles.tsx} (74%) rename src/components/dice/{DiceTrayOverlay.js => DiceTrayOverlay.tsx} (83%) rename src/components/dice/{SelectDiceButton.js => SelectDiceButton.tsx} (66%) rename src/components/drag/{Draggable.js => Draggable.tsx} (69%) rename src/components/drag/{Droppable.js => Droppable.tsx} (62%) rename src/components/tile/{LazyTile.js => LazyTile.tsx} (79%) rename src/components/tile/{SortableTile.js => SortableTile.tsx} (84%) rename src/components/tile/{SortableTiles.js => SortableTiles.tsx} (79%) rename src/components/tile/{SortableTilesDragOverlay.js => SortableTilesDragOverlay.tsx} (90%) rename src/components/tile/{Tile.js => Tile.tsx} (91%) rename src/components/tile/{TileActionBar.js => TileActionBar.tsx} (90%) rename src/components/tile/{TilesContainer.js => TilesContainer.tsx} (87%) rename src/components/tile/{TilesOverlay.js => TilesOverlay.tsx} (81%) rename src/contexts/{AssetsContext.js => AssetsContext.tsx} (67%) delete mode 100644 src/contexts/DragContext.js create mode 100644 src/contexts/DragContext.tsx rename src/contexts/{GroupContext.js => GroupContext.tsx} (70%) rename src/contexts/{TileDragContext.js => TileDragContext.tsx} (78%) rename src/contexts/{UserIdContext.js => UserIdContext.tsx} (64%) rename src/helpers/{group.js => group.ts} (59%) rename src/helpers/{map.js => map.ts} (74%) rename src/helpers/{token.js => token.ts} (81%) delete mode 100644 src/modals/EditGroupModal.tsx rename src/modals/{GroupNameModal.js => GroupNameModal.tsx} (67%) create mode 100644 src/types/external/image.outline.d.ts diff --git a/package.json b/package.json index 28677fc..5e9be27 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "react-markdown": "4", "react-media": "^2.0.0-rc.1", "react-modal": "^3.12.1", - "react-resize-detector": "4.2.3", + "react-resize-detector": "^6.7.4", "react-router-dom": "^5.1.2", "react-router-hash-link": "^2.2.2", "react-scripts": "^4.0.3", @@ -63,7 +63,7 @@ "socket.io-client": "^4.1.2", "socket.io-msgpack-parser": "^3.0.1", "source-map-explorer": "^2.5.2", - "theme-ui": "^0.8.4", + "theme-ui": "^0.10.0", "use-image": "^1.0.7", "uuid": "^8.3.2", "webrtc-adapter": "^7.7.1" @@ -107,6 +107,7 @@ "@types/react-router-dom": "^5.1.7", "@types/shortid": "^0.0.29", "@types/simple-peer": "^9.6.3", + "@types/uuid": "^8.3.1", "typescript": "^4.2.4", "worker-loader": "^3.0.8" } diff --git a/src/components/Search.js b/src/components/Search.tsx similarity index 88% rename from src/components/Search.js rename to src/components/Search.tsx index df00aa1..762de5c 100644 --- a/src/components/Search.js +++ b/src/components/Search.tsx @@ -1,9 +1,8 @@ -import React from "react"; -import { Box, Input } from "theme-ui"; +import { Box, Input, InputProps } from "theme-ui"; import SearchIcon from "../icons/SearchIcon"; -function Search(props) { +function Search(props: InputProps) { return (
void; + selectedDice: DefaultDice; + onDone: (dice: DefaultDice) => void; +}; + +function DiceTiles({ + dice, + onDiceSelect, + selectedDice, + onDone, +}: DiceTileProps) { const layout = useResponsiveLayout(); return ( @@ -29,7 +41,6 @@ function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) { isSelected={selectedDice && dice.key === selectedDice.key} onDiceSelect={onDiceSelect} onDone={onDone} - size={layout.tileSize} /> ))} diff --git a/src/components/dice/DiceTrayOverlay.js b/src/components/dice/DiceTrayOverlay.tsx similarity index 83% rename from src/components/dice/DiceTrayOverlay.js rename to src/components/dice/DiceTrayOverlay.tsx index 2c19088..4f2a779 100644 --- a/src/components/dice/DiceTrayOverlay.js +++ b/src/components/dice/DiceTrayOverlay.tsx @@ -1,10 +1,11 @@ -import React, { useRef, useCallback, useEffect, useState } from "react"; +import { useRef, useCallback, useEffect, useState } from "react"; import { Vector3 } from "@babylonjs/core/Maths/math"; import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight"; import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator"; import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture"; import { Box } from "theme-ui"; +// @ts-ignore import environment from "../../dice/environment.dds"; import DiceInteraction from "./DiceInteraction"; @@ -19,6 +20,16 @@ import { useDiceLoading } from "../../contexts/DiceLoadingContext"; import { getDiceRoll } from "../../helpers/dice"; import useSetting from "../../hooks/useSetting"; +import { DefaultDice, DiceMesh, DiceRoll, DiceType } from "../../types/Dice"; +import { Scene } from "@babylonjs/core"; + +type DiceTrayOverlayProps = { + isOpen: boolean; + shareDice: boolean; + onShareDiceChange: () => void; + diceRolls: DiceRoll[]; + onDiceRollsChange: (newRolls: DiceRoll[]) => void; +}; function DiceTrayOverlay({ isOpen, @@ -26,17 +37,18 @@ function DiceTrayOverlay({ onShareDiceChange, diceRolls, onDiceRollsChange, -}) { - const sceneRef = useRef(); - const shadowGeneratorRef = useRef(); - const diceRefs = useRef([]); +}: DiceTrayOverlayProps) { + const sceneRef = useRef(); + const shadowGeneratorRef = useRef(); + const diceRefs = useRef([]); const sceneVisibleRef = useRef(false); const sceneInteractionRef = useRef(false); // Add to the counter to ingore sleep values const sceneKeepAwakeRef = useRef(0); - const diceTrayRef = useRef(); + const diceTrayRef = useRef(); - const [diceTraySize, setDiceTraySize] = useState("single"); + const [diceTraySize, setDiceTraySize] = + useState<"single" | "double">("single"); const { assetLoadStart, assetLoadFinish, isLoading } = useDiceLoading(); const [fullScreen] = useSetting("map.fullScreen"); @@ -50,7 +62,7 @@ function DiceTrayOverlay({ } // Forces rendering for 1 second - function forceRender() { + function forceRender(): () => void { // Force rerender sceneKeepAwakeRef.current++; let triggered = false; @@ -97,7 +109,7 @@ function DiceTrayOverlay({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - async function initializeScene(scene) { + async function initializeScene(scene: Scene) { handleAssetLoadStart(); let light = new DirectionalLight( "DirectionalLight", @@ -124,16 +136,14 @@ function DiceTrayOverlay({ handleAssetLoadFinish(); } - function update(scene) { - function getDiceSpeed(dice) { - const diceSpeed = dice.instance.physicsImpostor - .getLinearVelocity() - .length(); + function update(scene: Scene) { + function getDiceSpeed(dice: DiceMesh) { + const diceSpeed = + dice.instance.physicsImpostor?.getLinearVelocity()?.length() || 0; // If the dice is a d100 check the d10 as well - if (dice.type === "d100") { - const d10Speed = dice.d10Instance.physicsImpostor - .getLinearVelocity() - .length(); + if (dice.d10Instance) { + const d10Speed = + dice.d10Instance.physicsImpostor?.getLinearVelocity()?.length() || 0; return Math.max(diceSpeed, d10Speed); } else { return diceSpeed; @@ -157,14 +167,14 @@ function DiceTrayOverlay({ const dice = die[i]; const speed = getDiceSpeed(dice); // If the speed has been below 0.01 for 1s set dice to sleep - if (speed < 0.01 && !dice.sleepTimout) { - dice.sleepTimout = setTimeout(() => { + if (speed < 0.01 && !dice.sleepTimeout) { + dice.sleepTimeout = setTimeout(() => { dice.asleep = true; }, 1000); - } else if (speed > 0.5 && (dice.asleep || dice.sleepTimout)) { + } else if (speed > 0.5 && (dice.asleep || dice.sleepTimeout)) { dice.asleep = false; - clearTimeout(dice.sleepTimout); - dice.sleepTimout = null; + dice.sleepTimeout && clearTimeout(dice.sleepTimeout); + dice.sleepTimeout = undefined; } } @@ -173,14 +183,14 @@ function DiceTrayOverlay({ } } - function handleDiceAdd(style, type) { + function handleDiceAdd(style: typeof Dice, type: DiceType) { const scene = sceneRef.current; const shadowGenerator = shadowGeneratorRef.current; if (scene && shadowGenerator) { const instance = style.createInstance(type, scene); shadowGenerator.addShadowCaster(instance); - Dice.roll(instance); - let dice = { type, instance, asleep: false }; + style.roll(instance); + let dice: DiceMesh = { type, instance, asleep: false }; // If we have a d100 add a d10 as well if (type === "d100") { const d10Instance = style.createInstance("d10", scene); @@ -196,7 +206,7 @@ function DiceTrayOverlay({ const die = diceRefs.current; for (let dice of die) { dice.instance.dispose(); - if (dice.type === "d100") { + if (dice.d10Instance) { dice.d10Instance.dispose(); } } @@ -208,14 +218,14 @@ function DiceTrayOverlay({ const die = diceRefs.current; for (let dice of die) { Dice.roll(dice.instance); - if (dice.type === "d100") { + if (dice.d10Instance) { Dice.roll(dice.d10Instance); } dice.asleep = false; } } - async function handleDiceLoad(dice) { + async function handleDiceLoad(dice: DefaultDice) { handleAssetLoadStart(); const scene = sceneRef.current; if (scene) { @@ -230,10 +240,13 @@ function DiceTrayOverlay({ }); useEffect(() => { - let renderTimeout; - let renderCleanup; + let renderTimeout: NodeJS.Timeout; + let renderCleanup: () => void; function handleResize() { const map = document.querySelector(".map"); + if (!map) { + return; + } const mapRect = map.getBoundingClientRect(); const availableWidth = mapRect.width - 108; // Subtract padding @@ -283,7 +296,7 @@ function DiceTrayOverlay({ return; } - let newRolls = []; + let newRolls: DiceRoll[] = []; for (let i = 0; i < die.length; i++) { const dice = die[i]; let roll = getDiceRoll(dice); diff --git a/src/components/dice/SelectDiceButton.js b/src/components/dice/SelectDiceButton.tsx similarity index 66% rename from src/components/dice/SelectDiceButton.js rename to src/components/dice/SelectDiceButton.tsx index f5c9fda..02c30fa 100644 --- a/src/components/dice/SelectDiceButton.js +++ b/src/components/dice/SelectDiceButton.tsx @@ -1,10 +1,22 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { IconButton } from "theme-ui"; import SelectDiceIcon from "../../icons/SelectDiceIcon"; import SelectDiceModal from "../../modals/SelectDiceModal"; -function SelectDiceButton({ onDiceChange, currentDice, disabled }) { +import { DefaultDice } from "../../types/Dice"; + +type SelectDiceButtonProps = { + onDiceChange: (dice: DefaultDice) => void; + currentDice: DefaultDice; + disabled: boolean; +}; + +function SelectDiceButton({ + onDiceChange, + currentDice, + disabled, +}: SelectDiceButtonProps) { const [isModalOpen, setIsModalOpen] = useState(false); function openModal() { @@ -14,7 +26,7 @@ function SelectDiceButton({ onDiceChange, currentDice, disabled }) { setIsModalOpen(false); } - function handleDone(dice) { + function handleDone(dice: DefaultDice) { onDiceChange(dice); closeModal(); } @@ -39,4 +51,8 @@ function SelectDiceButton({ onDiceChange, currentDice, disabled }) { ); } +SelectDiceButton.defaultProps = { + disabled: false, +}; + export default SelectDiceButton; diff --git a/src/components/drag/Draggable.js b/src/components/drag/Draggable.tsx similarity index 69% rename from src/components/drag/Draggable.js rename to src/components/drag/Draggable.tsx index e138e94..a0b9268 100644 --- a/src/components/drag/Draggable.js +++ b/src/components/drag/Draggable.tsx @@ -1,7 +1,14 @@ import React from "react"; import { useDraggable } from "@dnd-kit/core"; +import { Data } from "@dnd-kit/core/dist/store/types"; -function Draggable({ id, children, data }) { +type DraggableProps = { + id: string; + children: React.ReactNode; + data: Data; +}; + +function Draggable({ id, children, data }: DraggableProps) { const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id, data, diff --git a/src/components/drag/Droppable.js b/src/components/drag/Droppable.tsx similarity index 62% rename from src/components/drag/Droppable.js rename to src/components/drag/Droppable.tsx index d11d00f..3213e6e 100644 --- a/src/components/drag/Droppable.js +++ b/src/components/drag/Droppable.tsx @@ -1,7 +1,12 @@ import React from "react"; import { useDroppable } from "@dnd-kit/core"; -function Droppable({ id, children, disabled, ...props }) { +type DroppableProps = React.HTMLAttributes & { + id: string; + disabled: boolean; +}; + +function Droppable({ id, children, disabled, ...props }: DroppableProps) { const { setNodeRef } = useDroppable({ id, disabled }); return ( diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index fe03609..01426e4 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -26,75 +26,7 @@ import { EditShapeAction, RemoveShapeAction, } from "../../actions"; -import { Fog, Path, Shape } from "../../helpers/drawing"; import Session from "../../network/Session"; -import { Grid } from "../../helpers/grid"; -import { ImageFile } from "../../helpers/image"; - -export type Resolutions = Record; -export type Map = { - id: string; - name: string; - owner: string; - file?: Uint8Array; - quality?: string; - resolutions?: Resolutions; - grid: Grid; - group: string; - width: number; - height: number; - type: string; - lastUsed: number; - lastModified: number; - created: number; - showGrid: boolean; - snapToGrid: boolean; - thumbnail?: ImageFile; -}; - -export type Note = { - id: string; - color: string; - lastModified: number; - lastModifiedBy: string; - locked: boolean; - size: number; - text: string; - textOnly: boolean; - visible: boolean; - x: number; - y: number; -}; - -export type TokenState = { - id: string; - tokenId: string; - owner: string; - size: number; - category: string; - label: string; - statuses: string[]; - x: number; - y: number; - lastModifiedBy: string; - lastModified: number; - rotation: number; - locked: boolean; - visible: boolean; - type: "default" | "file"; - outline: any; - width: number; - height: number; -}; - -export type MapState = { - tokens: Record; - drawShapes: Record; - fogShapes: Record; - editFlags: ["drawing", "tokens", "notes", "fog"]; - notes: Record; - mapId: string; -}; function Map({ map, diff --git a/src/components/tile/LazyTile.js b/src/components/tile/LazyTile.tsx similarity index 79% rename from src/components/tile/LazyTile.js rename to src/components/tile/LazyTile.tsx index 54eaa06..d6dca84 100644 --- a/src/components/tile/LazyTile.js +++ b/src/components/tile/LazyTile.tsx @@ -1,11 +1,11 @@ import React from "react"; -import { Box } from "theme-ui"; +import { Box, ThemeUIStyleObject } from "theme-ui"; import { useInView } from "react-intersection-observer"; -function LazyTile({ children }) { +function LazyTile({ children }: { children: React.ReactNode }) { const [ref, inView] = useInView({ triggerOnce: false }); - const sx = inView + const sx: ThemeUIStyleObject = inView ? {} : { width: "100%", height: "0", paddingTop: "100%", position: "relative" }; diff --git a/src/components/tile/SortableTile.js b/src/components/tile/SortableTile.tsx similarity index 84% rename from src/components/tile/SortableTile.js rename to src/components/tile/SortableTile.tsx index cc9c8a0..5cf3905 100644 --- a/src/components/tile/SortableTile.js +++ b/src/components/tile/SortableTile.tsx @@ -6,6 +6,16 @@ import { animated, useSpring } from "react-spring"; import { GROUP_ID_PREFIX } from "../../contexts/TileDragContext"; +type SortableTileProps = { + id: string; + disableGrouping: boolean; + disableSorting: boolean; + hidden: boolean; + children: React.ReactNode; + isDragging: boolean; + cursor: string; +}; + function SortableTile({ id, disableGrouping, @@ -14,7 +24,7 @@ function SortableTile({ children, isDragging, cursor, -}) { +}: SortableTileProps) { const { attributes, listeners, @@ -35,7 +45,7 @@ function SortableTile({ }; // Sort div left aligned - const sortDropStyle = { + const sortDropStyle: React.CSSProperties = { position: "absolute", left: "-5px", top: 0, @@ -46,7 +56,7 @@ function SortableTile({ }; // Group div center aligned - const groupDropStyle = { + const groupDropStyle: React.CSSProperties = { position: "absolute", top: 0, left: 0, @@ -55,7 +65,7 @@ function SortableTile({ borderWidth: "4px", borderRadius: "4px", borderStyle: - over?.id === `${GROUP_ID_PREFIX}${id}` && active.id !== id + over?.id === `${GROUP_ID_PREFIX}${id}` && active?.id !== id ? "solid" : "none", }; diff --git a/src/components/tile/SortableTiles.js b/src/components/tile/SortableTiles.tsx similarity index 79% rename from src/components/tile/SortableTiles.js rename to src/components/tile/SortableTiles.tsx index a42bfbd..9c8b97b 100644 --- a/src/components/tile/SortableTiles.js +++ b/src/components/tile/SortableTiles.tsx @@ -15,8 +15,14 @@ import { GROUP_SORTABLE_ID, } from "../../contexts/TileDragContext"; import { useGroup } from "../../contexts/GroupContext"; +import { Group } from "../../types/Group"; -function SortableTiles({ renderTile, subgroup }) { +type SortableTilesProps = { + renderTile: (group: Group) => React.ReactNode; + subgroup: boolean; +}; + +function SortableTiles({ renderTile, subgroup }: SortableTilesProps) { const dragId = useTileDragId(); const dragCursor = useTileDragCursor(); const overGroupId = useTileOverGroupId(); @@ -38,14 +44,14 @@ function SortableTiles({ renderTile, subgroup }) { const sortableId = subgroup ? GROUP_SORTABLE_ID : BASE_SORTABLE_ID; // Only populate selected groups if needed - let selectedGroupIds = []; + let selectedGroupIds: string[] = []; if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) { selectedGroupIds = allSelectedIds; } - const disableSorting = (openGroupId && !subgroup) || filter; - const disableGrouping = subgroup || disableSorting || filter; + const disableSorting = !!((openGroupId && !subgroup) || filter); + const disableGrouping = !!(subgroup || disableSorting || filter); - function renderSortableGroup(group, selectedGroups) { + function renderSortableGroup(group: Group, selectedGroups: Group[]) { if (overGroupId === group.id && dragId && group.id !== dragId) { // If dragging over a group render a preview of that group const previewGroup = moveGroupsInto( @@ -61,7 +67,7 @@ function SortableTiles({ renderTile, subgroup }) { function renderTiles() { const groupsByIds = keyBy(activeGroups, "id"); const selectedGroupIdsSet = new Set(selectedGroupIds); - let selectedGroups = []; + let selectedGroups: Group[] = []; let hasSelectedContainerGroup = false; for (let groupId of selectedGroupIds) { const group = groupsByIds[groupId]; @@ -72,8 +78,8 @@ function SortableTiles({ renderTile, subgroup }) { } } } - return activeGroups.map((group) => { - const isDragging = dragId && selectedGroupIdsSet.has(group.id); + return activeGroups.map((group: Group) => { + const isDragging = dragId !== null && selectedGroupIdsSet.has(group.id); const disableTileGrouping = disableGrouping || isDragging || hasSelectedContainerGroup; return ( @@ -84,7 +90,7 @@ function SortableTiles({ renderTile, subgroup }) { disableSorting={disableSorting} hidden={group.id === openGroupId} isDragging={isDragging} - cursor={dragCursor} + cursor={dragCursor || ""} > {renderSortableGroup(group, selectedGroups)} diff --git a/src/components/tile/SortableTilesDragOverlay.js b/src/components/tile/SortableTilesDragOverlay.tsx similarity index 90% rename from src/components/tile/SortableTilesDragOverlay.js rename to src/components/tile/SortableTilesDragOverlay.tsx index 848b793..64d6000 100644 --- a/src/components/tile/SortableTilesDragOverlay.js +++ b/src/components/tile/SortableTilesDragOverlay.tsx @@ -8,8 +8,17 @@ import Vector2 from "../../helpers/Vector2"; import { useTileDragId } from "../../contexts/TileDragContext"; import { useGroup } from "../../contexts/GroupContext"; +import { Group } from "../../types/Group"; -function SortableTilesDragOverlay({ renderTile, subgroup }) { +type SortableTilesDragOverlayProps = { + renderTile: (group: Group) => React.ReactNode; + subgroup: boolean; +}; + +function SortableTilesDragOverlay({ + renderTile, + subgroup, +}: SortableTilesDragOverlayProps) { const dragId = useTileDragId(); const { groups, @@ -27,7 +36,7 @@ function SortableTilesDragOverlay({ renderTile, subgroup }) { : groups; // Only populate selected groups if needed - let selectedGroupIds = []; + let selectedGroupIds: string[] = []; if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) { selectedGroupIds = allSelectedIds; } diff --git a/src/components/tile/Tile.js b/src/components/tile/Tile.tsx similarity index 91% rename from src/components/tile/Tile.js rename to src/components/tile/Tile.tsx index c550657..220c483 100644 --- a/src/components/tile/Tile.js +++ b/src/components/tile/Tile.tsx @@ -3,6 +3,18 @@ import { Flex, IconButton, Box, Text, Badge } from "theme-ui"; import EditTileIcon from "../../icons/EditTileIcon"; +type TileProps = { + title: string; + isSelected: boolean; + onSelect: () => void; + onEdit: () => void; + onDoubleClick: () => void; + canEdit: boolean; + badges: React.ReactChild[]; + editTitle: string; + children: React.ReactNode; +}; + function Tile({ title, isSelected, @@ -13,7 +25,7 @@ function Tile({ badges, editTitle, children, -}) { +}: TileProps) { return ( void; + addTitle: string; +}; + +function TileActionBar({ onAdd, addTitle }: TileActionBarProps) { const { selectMode, onSelectModeChange, @@ -33,7 +37,7 @@ function TileActionBar({ onAdd, addTitle }) { outlineOffset: "0px", }, }} - onFocus={() => onGroupSelect()} + onFocus={() => onGroupSelect(undefined)} > onFilterChange(e.target.value)} /> onGroupSelect()} + onClick={() => onGroupSelect(undefined)} > group.id === openGroupId); + const group = groups.find((group: Group) => group.id === openGroupId); if (!openGroupId) { return null; } + const groupName = group && group.type === "group" && group.name; + return ( <> - {group?.name} + {groupName} onGroupSelect()} + onClick={() => onGroupSelect(undefined)} > setIsGroupNameModalOpen(false)} /> diff --git a/src/contexts/AssetsContext.js b/src/contexts/AssetsContext.tsx similarity index 67% rename from src/contexts/AssetsContext.js rename to src/contexts/AssetsContext.tsx index 4da276f..9d64c9c 100644 --- a/src/contexts/AssetsContext.js +++ b/src/contexts/AssetsContext.tsx @@ -8,49 +8,20 @@ import { useDatabase } from "./DatabaseContext"; import useDebounce from "../hooks/useDebounce"; import { omit } from "../helpers/shared"; +import { Asset } from "../types/Asset"; -/** - * @typedef Asset - * @property {string} id - * @property {number} width - * @property {number} height - * @property {Uint8Array} file - * @property {string} mime - * @property {string} owner - */ +type AssetsContext = { + getAsset: (assetId: string) => Promise; + addAssets: (assets: Asset[]) => void; + putAsset: (asset: Asset) => void; +}; -/** - * @callback getAsset - * @param {string} assetId - * @returns {Promise} - */ - -/** - * @callback addAssets - * @param {Asset[]} assets - */ - -/** - * @callback putAsset - * @param {Asset} asset - */ - -/** - * @typedef AssetsContext - * @property {getAsset} getAsset - * @property {addAssets} addAssets - * @property {putAsset} putAsset - */ - -/** - * @type {React.Context} - */ -const AssetsContext = React.createContext(); +const AssetsContext = React.createContext(undefined); // 100 MB max cache size const maxCacheSize = 1e8; -export function AssetsProvider({ children }) { +export function AssetsProvider({ children }: { children: React.ReactNode }) { const { worker, database, databaseStatus } = useDatabase(); useEffect(() => { @@ -61,33 +32,39 @@ export function AssetsProvider({ children }) { const getAsset = useCallback( async (assetId) => { - return await database.table("assets").get(assetId); + if (database) { + return await database.table("assets").get(assetId); + } }, [database] ); const addAssets = useCallback( async (assets) => { - await database.table("assets").bulkAdd(assets); + if (database) { + await database.table("assets").bulkAdd(assets); + } }, [database] ); const putAsset = useCallback( async (asset) => { - // Check for broadcast channel and attempt to use worker to put map to avoid UI lockup - // Safari doesn't support BC so fallback to single thread - if (window.BroadcastChannel) { - const packedAsset = encode(asset); - const success = await worker.putData( - Comlink.transfer(packedAsset, [packedAsset.buffer]), - "assets" - ); - if (!success) { + if (database) { + // Check for broadcast channel and attempt to use worker to put map to avoid UI lockup + // Safari doesn't support BC so fallback to single thread + if (window.BroadcastChannel) { + const packedAsset = encode(asset); + const success = await worker.putData( + Comlink.transfer(packedAsset, [packedAsset.buffer]), + "assets" + ); + if (!success) { + await database.table("assets").put(asset); + } + } else { await database.table("assets").put(asset); } - } else { - await database.table("assets").put(asset); } }, [database, worker] @@ -119,35 +96,38 @@ export function useAssets() { * @property {number} references */ -/** - * @type React.Context> - */ -export const AssetURLsStateContext = React.createContext(); +type AssetURL = { + url: string | null; + id: string; + references: number; +}; -/** - * @type React.Context>> - */ -export const AssetURLsUpdaterContext = React.createContext(); +type AssetURLs = Record; + +export const AssetURLsStateContext = + React.createContext(undefined); + +export const AssetURLsUpdaterContext = + React.createContext< + React.Dispatch> | undefined + >(undefined); /** * Helper to manage sharing of custom image sources between uses of useAssetURL */ -export function AssetURLsProvider({ children }) { - const [assetURLs, setAssetURLs] = useState({}); +export function AssetURLsProvider({ children }: { children: React.ReactNode }) { + const [assetURLs, setAssetURLs] = useState({}); const { database } = useDatabase(); // Keep track of the assets that need to be loaded - const [assetKeys, setAssetKeys] = useState([]); + const [assetKeys, setAssetKeys] = useState([]); // Load assets after 100ms const loadingDebouncedAssetURLs = useDebounce(assetURLs, 100); // Update the asset keys to load when a url is added without an asset attached useEffect(() => { - if (!loadingDebouncedAssetURLs) { - return; - } - let keysToLoad = []; + let keysToLoad: string[] = []; for (let url of Object.values(loadingDebouncedAssetURLs)) { if (url.url === null) { keysToLoad.push(url.id); @@ -159,8 +139,9 @@ export function AssetURLsProvider({ children }) { }, [loadingDebouncedAssetURLs]); // Get the new assets whenever the keys change - const assets = useLiveQuery( - () => database?.table("assets").where("id").anyOf(assetKeys).toArray(), + const assets = useLiveQuery( + () => + database?.table("assets").where("id").anyOf(assetKeys).toArray() || [], [database, assetKeys] ); @@ -197,7 +178,7 @@ export function AssetURLsProvider({ children }) { let urlsToCleanup = []; for (let url of Object.values(prevURLs)) { if (url.references <= 0) { - URL.revokeObjectURL(url.url); + url.url && URL.revokeObjectURL(url.url); urlsToCleanup.push(url.id); } } @@ -220,13 +201,13 @@ export function AssetURLsProvider({ children }) { /** * Helper function to load either file or default asset into a URL - * @param {string} assetId - * @param {"file"|"default"} type - * @param {Object.} defaultSources - * @param {string|undefined} unknownSource - * @returns {string|undefined} */ -export function useAssetURL(assetId, type, defaultSources, unknownSource) { +export function useAssetURL( + assetId: string, + type: "file" | "default", + defaultSources: Record, + unknownSource?: string +) { const assetURLs = useContext(AssetURLsStateContext); if (assetURLs === undefined) { throw new Error("useAssetURL must be used within a AssetURLsProvider"); @@ -242,7 +223,7 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) { } function updateAssetURL() { - function increaseReferences(prevURLs) { + function increaseReferences(prevURLs: AssetURLs): AssetURLs { return { ...prevURLs, [assetId]: { @@ -252,13 +233,13 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) { }; } - function createReference(prevURLs) { + function createReference(prevURLs: AssetURLs): AssetURLs { return { ...prevURLs, [assetId]: { url: null, id: assetId, references: 1 }, }; } - setAssetURLs((prevURLs) => { + setAssetURLs?.((prevURLs) => { if (assetId in prevURLs) { // Check if the asset url is already added and increase references return increaseReferences(prevURLs); @@ -303,36 +284,29 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) { return unknownSource; } -/** - * @typedef FileData - * @property {string} file - * @property {"file"} type - * @property {string} thumbnail - * @property {string=} quality - * @property {Object.=} resolutions - */ +type FileData = { + file: string; + type: "file"; + thumbnail: string; + quality?: string; + resolutions?: Record; +}; -/** - * @typedef DefaultData - * @property {string} key - * @property {"default"} type - */ +type DefaultData = { + key: string; + type: "default"; +}; /** * Load a map or token into a URL taking into account a thumbnail and multiple resolutions - * @param {FileData|DefaultData} data - * @param {Object.} defaultSources - * @param {string|undefined} unknownSource - * @param {boolean} thumbnail - * @returns {string|undefined} */ export function useDataURL( - data, - defaultSources, - unknownSource, + data: FileData | DefaultData, + defaultSources: Record, + unknownSource: string | undefined, thumbnail = false ) { - const [assetId, setAssetId] = useState(); + const [assetId, setAssetId] = useState(); useEffect(() => { if (!data) { @@ -344,7 +318,11 @@ export function useDataURL( } else { if (thumbnail) { setAssetId(data.thumbnail); - } else if (data.resolutions && data.quality !== "original") { + } else if ( + data.resolutions && + data.quality && + data.quality !== "original" + ) { setAssetId(data.resolutions[data.quality]); } else { setAssetId(data.file); @@ -356,7 +334,7 @@ export function useDataURL( }, [data, thumbnail]); const assetURL = useAssetURL( - assetId, + assetId || "", data?.type, defaultSources, unknownSource diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 3ebd255..f5f2979 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -2,9 +2,11 @@ import React, { useState, useEffect, useContext } from "react"; import FakeStorage from "../helpers/FakeStorage"; -type AuthContext = { password: string; setPassword: React.Dispatch }; +type AuthContext = { + password: string; + setPassword: React.Dispatch>; +}; -// TODO: check what default value we want here const AuthContext = React.createContext(undefined); let storage: Storage | FakeStorage; diff --git a/src/contexts/DragContext.js b/src/contexts/DragContext.js deleted file mode 100644 index 2614b0f..0000000 --- a/src/contexts/DragContext.js +++ /dev/null @@ -1,75 +0,0 @@ -// eslint-disable-next-line no-unused-vars -import React, { useRef, ReactNode } from "react"; -import { - DndContext, - useDndContext, - useDndMonitor, - // eslint-disable-next-line no-unused-vars - DragEndEvent, -} from "@dnd-kit/core"; - -/** - * Wrap a dnd-kit DndContext with a position monitor to get the - * active drag element on drag end - * TODO: use look into fixing this upstream - * Related: https://github.com/clauderic/dnd-kit/issues/238 - */ - -/** - * @typedef DragEndOverlayEvent - * @property {DOMRect} overlayNodeClientRect - * - * @typedef {DragEndEvent & DragEndOverlayEvent} DragEndWithOverlayProps - */ - -/** - * @callback DragEndWithOverlayEvent - * @param {DragEndWithOverlayProps} props - */ - -/** - * @typedef CustomDragProps - * @property {DragEndWithOverlayEvent=} onDragEnd - * @property {ReactNode} children - */ - -/** - * @param {CustomDragProps} props - */ -function DragPositionMonitor({ children, onDragEnd }) { - const { overlayNode } = useDndContext(); - - const overlayNodeClientRectRef = useRef(); - function handleDragMove() { - if (overlayNode?.nodeRef?.current) { - overlayNodeClientRectRef.current = overlayNode.nodeRef.current.getBoundingClientRect(); - } - } - - function handleDragEnd(props) { - onDragEnd && - onDragEnd({ - ...props, - overlayNodeClientRect: overlayNodeClientRectRef.current, - }); - } - useDndMonitor({ onDragEnd: handleDragEnd, onDragMove: handleDragMove }); - - return children; -} - -/** - * TODO: Import Props interface from dnd-kit with conversion to Typescript - * @param {CustomDragProps} props - */ -function DragContext({ children, onDragEnd, ...props }) { - return ( - - - {children} - - - ); -} - -export default DragContext; diff --git a/src/contexts/DragContext.tsx b/src/contexts/DragContext.tsx new file mode 100644 index 0000000..2cf8f6a --- /dev/null +++ b/src/contexts/DragContext.tsx @@ -0,0 +1,68 @@ +import { useRef } from "react"; +import { + DndContext, + useDndContext, + useDndMonitor, + DragEndEvent, +} from "@dnd-kit/core"; + +import { Props } from "@dnd-kit/core/dist/components/DndContext/DndContext"; + +/** + * Wrap a dnd-kit DndContext with a position monitor to get the + * active drag element on drag end + * TODO: use look into fixing this upstream + * Related: https://github.com/clauderic/dnd-kit/issues/238 + */ + +type DragEndWithOverlayEvent = { + overlayNodeClientRect?: DOMRect; +}; + +export type CustomDragEndEvent = DragEndWithOverlayEvent & DragEndEvent; + +type CustomDragProps = { + onDragEnd?: (event: CustomDragEndEvent) => void; + ; +}; + +function DragPositionMonitor({ onDragEnd }: CustomDragProps) { + const { overlayNode } = useDndContext(); + + const overlayNodeClientRectRef = useRef(); + function handleDragMove() { + if (overlayNode?.nodeRef?.current) { + overlayNodeClientRectRef.current = + overlayNode.nodeRef.current.getBoundingClientRect(); + } + } + + function handleDragEnd(props: DragEndEvent) { + onDragEnd && + onDragEnd({ + ...props, + overlayNodeClientRect: overlayNodeClientRectRef.current, + }); + } + useDndMonitor({ onDragEnd: handleDragEnd, onDragMove: handleDragMove }); + + return null; +} + +/** + * @param {CustomDragProps} props + */ +function DragContext({ + children, + onDragEnd, + ...props +}: CustomDragProps & Props) { + return ( + + + {children} + + ); +} + +export default DragContext; diff --git a/src/contexts/GroupContext.js b/src/contexts/GroupContext.tsx similarity index 70% rename from src/contexts/GroupContext.js rename to src/contexts/GroupContext.tsx index 3b2138d..fd667fb 100644 --- a/src/contexts/GroupContext.js +++ b/src/contexts/GroupContext.tsx @@ -7,8 +7,40 @@ import { useKeyboard, useBlur } from "./KeyboardContext"; import { getGroupItems, groupsFromIds } from "../helpers/group"; import shortcuts from "../shortcuts"; +import { Group, GroupContainer, GroupItem } from "../types/Group"; -const GroupContext = React.createContext(); +type GroupContext = { + groups: Group[]; + activeGroups: Group[]; + openGroupId: string | undefined; + openGroupItems: Group[]; + filter: string | undefined; + filteredGroupItems: GroupItem[]; + selectedGroupIds: string[]; + selectMode: any; + onSelectModeChange: React.Dispatch< + React.SetStateAction<"single" | "multiple" | "range"> + >; + onGroupOpen: (groupId: string) => void; + onGroupClose: () => void; + onGroupsChange: ( + newGroups: Group[] | GroupItem[], + groupId: string | undefined + ) => void; + onGroupSelect: (groupId: string | undefined) => void; + onFilterChange: React.Dispatch>; +}; + +const GroupContext = React.createContext(undefined); + +type GroupProviderProps = { + groups: Group[]; + itemNames: Record; + onGroupsChange: (groups: Group[]) => void; + onGroupsSelect: (groupIds: string[]) => void; + disabled: boolean; + children: React.ReactNode; +}; export function GroupProvider({ groups, @@ -17,16 +49,17 @@ export function GroupProvider({ onGroupsSelect, disabled, children, -}) { - const [selectedGroupIds, setSelectedGroupIds] = useState([]); +}: GroupProviderProps) { + const [selectedGroupIds, setSelectedGroupIds] = useState([]); // Either single, multiple or range - const [selectMode, setSelectMode] = useState("single"); + const [selectMode, setSelectMode] = + useState<"single" | "multiple" | "range">("single"); /** * Group Open */ - const [openGroupId, setOpenGroupId] = useState(); - const [openGroupItems, setOpenGroupItems] = useState([]); + const [openGroupId, setOpenGroupId] = useState(); + const [openGroupItems, setOpenGroupItems] = useState([]); useEffect(() => { if (openGroupId) { const openGroups = groupsFromIds([openGroupId], groups); @@ -37,29 +70,29 @@ export function GroupProvider({ // Close group if we can't find it // This can happen if it was deleted or all it's items were deleted setOpenGroupItems([]); - setOpenGroupId(); + setOpenGroupId(undefined); } } else { setOpenGroupItems([]); } }, [openGroupId, groups]); - function handleGroupOpen(groupId) { + function handleGroupOpen(groupId: string) { setSelectedGroupIds([]); setOpenGroupId(groupId); } function handleGroupClose() { setSelectedGroupIds([]); - setOpenGroupId(); + setOpenGroupId(undefined); } /** * Search */ - const [filter, setFilter] = useState(); - const [filteredGroupItems, setFilteredGroupItems] = useState([]); - const [fuse, setFuse] = useState(); + const [filter, setFilter] = useState(); + const [filteredGroupItems, setFilteredGroupItems] = useState([]); + const [fuse, setFuse] = useState>(); // Update search index when items change useEffect(() => { let items = []; @@ -76,10 +109,10 @@ export function GroupProvider({ // Perform search when search changes useEffect(() => { - if (filter) { + if (filter && fuse) { const query = fuse.search(filter); setFilteredGroupItems(query.map((result) => result.item)); - setOpenGroupId(); + setOpenGroupId(undefined); } else { setFilteredGroupItems([]); } @@ -96,23 +129,30 @@ export function GroupProvider({ : groups; /** + * @param {Group[] | GroupItem[]} newGroups * @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object */ - function handleGroupsChange(newGroups, groupId) { + function handleGroupsChange( + newGroups: Group[] | GroupItem[], + groupId: string | undefined + ) { if (groupId) { // If a group is specidifed then update that group with the new items const groupIndex = groups.findIndex((group) => group.id === groupId); let updatedGroups = cloneDeep(groups); const group = updatedGroups[groupIndex]; - updatedGroups[groupIndex] = { ...group, items: newGroups }; + updatedGroups[groupIndex] = { + ...group, + items: newGroups, + } as GroupContainer; onGroupsChange(updatedGroups); } else { onGroupsChange(newGroups); } } - function handleGroupSelect(groupId) { - let groupIds = []; + function handleGroupSelect(groupId: string | undefined) { + let groupIds: string[] = []; if (groupId) { switch (selectMode) { case "single": @@ -133,8 +173,8 @@ export function GroupProvider({ const lastIndex = activeGroups.findIndex( (g) => g.id === selectedGroupIds[selectedGroupIds.length - 1] ); - let idsToAdd = []; - let idsToRemove = []; + let idsToAdd: string[] = []; + let idsToRemove: string[] = []; const direction = currentIndex > lastIndex ? 1 : -1; for ( let i = lastIndex + direction; @@ -166,7 +206,7 @@ export function GroupProvider({ /** * Shortcuts */ - function handleKeyDown(event) { + function handleKeyDown(event: React.KeyboardEvent) { if (disabled) { return; } @@ -178,7 +218,7 @@ export function GroupProvider({ } } - function handleKeyUp(event) { + function handleKeyUp(event: React.KeyboardEvent) { if (disabled) { return; } diff --git a/src/contexts/MapDataContext.tsx b/src/contexts/MapDataContext.tsx index b913d00..0114145 100644 --- a/src/contexts/MapDataContext.tsx +++ b/src/contexts/MapDataContext.tsx @@ -9,7 +9,9 @@ import { useLiveQuery } from "dexie-react-hooks"; import { useDatabase } from "./DatabaseContext"; -import { Map, MapState, Note } from "../components/map/Map"; +import { Map } from "../types/Map"; +import { MapState } from "../types/MapState"; +import { Note } from "../types/Note"; import { removeGroupsItems } from "../helpers/group"; diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.tsx similarity index 78% rename from src/contexts/TileDragContext.js rename to src/contexts/TileDragContext.tsx index 3a0f52f..2a3844c 100644 --- a/src/contexts/TileDragContext.js +++ b/src/contexts/TileDragContext.tsx @@ -6,19 +6,26 @@ import { useSensor, useSensors, closestCenter, + RectEntry, } from "@dnd-kit/core"; -import DragContext from "./DragContext"; +import DragContext, { CustomDragEndEvent } from "./DragContext"; +import { DragStartEvent, DragOverEvent, ViewRect } from "@dnd-kit/core"; +import { DragCancelEvent } from "@dnd-kit/core/dist/types"; import { useGroup } from "./GroupContext"; import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group"; +import Vector2 from "../helpers/Vector2"; import usePreventSelect from "../hooks/usePreventSelect"; -const TileDragIdContext = React.createContext(); -const TileOverGroupIdContext = React.createContext(); -const TileDragCursorContext = React.createContext(); +const TileDragIdContext = + React.createContext(undefined); +const TileOverGroupIdContext = + React.createContext(undefined); +const TileDragCursorContext = + React.createContext(undefined); export const BASE_SORTABLE_ID = "__base__"; export const GROUP_SORTABLE_ID = "__group__"; @@ -27,7 +34,7 @@ export const UNGROUP_ID = "__ungroup__"; export const ADD_TO_MAP_ID = "__add__"; // Custom rectIntersect that takes a point -function rectIntersection(rects, point) { +function rectIntersection(rects: RectEntry[], point: Vector2) { for (let rect of rects) { const [id, bounds] = rect; if ( @@ -44,13 +51,21 @@ function rectIntersection(rects, point) { return null; } +type TileDragProviderProps = { + onDragAdd?: (selectedGroupIds: string[], rect: DOMRect) => void; + onDragStart?: (event: DragStartEvent) => void; + onDragEnd?: (event: CustomDragEndEvent) => void; + onDragCancel?: (event: DragCancelEvent) => void; + children?: React.ReactNode; +}; + export function TileDragProvider({ onDragAdd, onDragStart, onDragEnd, onDragCancel, children, -}) { +}: TileDragProviderProps) { const { groups, activeGroups, @@ -71,23 +86,23 @@ export function TileDragProvider({ const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); - const [dragId, setDragId] = useState(null); - const [overId, setOverId] = useState(null); + const [dragId, setDragId] = useState(null); + const [overId, setOverId] = useState(null); const [dragCursor, setDragCursor] = useState("pointer"); const [preventSelect, resumeSelect] = usePreventSelect(); - const [overGroupId, setOverGroupId] = useState(null); + const [overGroupId, setOverGroupId] = useState(null); useEffect(() => { setOverGroupId( (overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9)) || null ); }, [overId]); - function handleDragStart(event) { - const { active, over } = event; + function handleDragStart(event: DragStartEvent) { + const { active } = event; setDragId(active.id); - setOverId(over?.id || null); + setOverId(null); if (!selectedGroupIds.includes(active.id)) { onGroupSelect(active.id); } @@ -98,7 +113,7 @@ export function TileDragProvider({ preventSelect(); } - function handleDragOver(event) { + function handleDragOver(event: DragOverEvent) { const { over } = event; setOverId(over?.id || null); @@ -116,7 +131,7 @@ export function TileDragProvider({ } } - function handleDragEnd(event) { + function handleDragEnd(event: CustomDragEndEvent) { const { active, over, overlayNodeClientRect } = event; setDragId(null); @@ -130,7 +145,7 @@ export function TileDragProvider({ selectedIndices = selectedIndices.sort((a, b) => a - b); if (over.id.startsWith(GROUP_ID_PREFIX)) { - onGroupSelect(); + onGroupSelect(undefined); // Handle tile group const overId = over.id.slice(9); if (overId !== active.id) { @@ -143,10 +158,12 @@ export function TileDragProvider({ ); } } else if (over.id === UNGROUP_ID) { - onGroupSelect(); - // Handle tile ungroup - const newGroups = ungroup(groups, openGroupId, selectedIndices); - onGroupsChange(newGroups); + if (openGroupId) { + onGroupSelect(undefined); + // Handle tile ungroup + const newGroups = ungroup(groups, openGroupId, selectedIndices); + onGroupsChange(newGroups, undefined); + } } else if (over.id === ADD_TO_MAP_ID) { onDragAdd && overlayNodeClientRect && @@ -168,7 +185,7 @@ export function TileDragProvider({ onDragEnd && onDragEnd(event); } - function handleDragCancel(event) { + function handleDragCancel(event: DragCancelEvent) { setDragId(null); setOverId(null); setDragCursor("pointer"); @@ -178,7 +195,7 @@ export function TileDragProvider({ onDragCancel && onDragCancel(event); } - function customCollisionDetection(rects, rect) { + function customCollisionDetection(rects: RectEntry[], rect: ViewRect) { const rectCenter = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, diff --git a/src/contexts/UserIdContext.js b/src/contexts/UserIdContext.tsx similarity index 64% rename from src/contexts/UserIdContext.js rename to src/contexts/UserIdContext.tsx index 72d3bb8..7630c38 100644 --- a/src/contexts/UserIdContext.js +++ b/src/contexts/UserIdContext.tsx @@ -1,12 +1,10 @@ import React, { useEffect, useState, useContext } from "react"; import { useDatabase } from "./DatabaseContext"; -/** - * @type {React.Context} - */ -const UserIdContext = React.createContext(); -export function UserIdProvider({ children }) { +const UserIdContext = React.createContext(undefined); + +export function UserIdProvider({ children }: { children: React.ReactNode }) { const { database, databaseStatus } = useDatabase(); const [userId, setUserId] = useState(); @@ -15,9 +13,11 @@ export function UserIdProvider({ children }) { return; } async function loadUserId() { - const storedUserId = await database.table("user").get("userId"); - if (storedUserId) { - setUserId(storedUserId.value); + if (database) { + const storedUserId = await database.table("user").get("userId"); + if (storedUserId) { + setUserId(storedUserId.value); + } } } diff --git a/src/dice/Dice.ts b/src/dice/Dice.ts index 785d8a8..275680b 100644 --- a/src/dice/Dice.ts +++ b/src/dice/Dice.ts @@ -1,7 +1,10 @@ import { Vector3 } from "@babylonjs/core/Maths/math"; import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; -import { PhysicsImpostor, PhysicsImpostorParameters } from "@babylonjs/core/Physics/physicsImpostor"; +import { + PhysicsImpostor, + PhysicsImpostorParameters, +} from "@babylonjs/core/Physics/physicsImpostor"; import d4Source from "./shared/d4.glb"; import d6Source from "./shared/d6.glb"; @@ -13,7 +16,15 @@ import d100Source from "./shared/d100.glb"; import { lerp } from "../helpers/shared"; import { importTextureAsync } from "../helpers/babylon"; -import { BaseTexture, InstancedMesh, Material, Mesh, Scene, Texture } from "@babylonjs/core"; +import { + BaseTexture, + InstancedMesh, + Material, + Mesh, + Scene, + Texture, +} from "@babylonjs/core"; +import { DiceType } from "../types/Dice"; const minDiceRollSpeed = 600; const maxDiceRollSpeed = 800; @@ -21,10 +32,16 @@ const maxDiceRollSpeed = 800; class Dice { static instanceCount = 0; - static async loadMeshes(material: Material, scene: Scene, sourceOverrides?: any): Promise> { + static async loadMeshes( + material: Material, + scene: Scene, + sourceOverrides?: any + ): Promise> { let meshes: any = {}; const addToMeshes = async (type: string | number, defaultSource: any) => { - let source: string = sourceOverrides ? sourceOverrides[type] : defaultSource; + let source: string = sourceOverrides + ? sourceOverrides[type] + : defaultSource; const mesh = await this.loadMesh(source, material, scene); meshes[type] = mesh; }; @@ -54,7 +71,11 @@ class Dice { static async loadMaterial(materialName: string, textures: any, scene: Scene) { let pbr = new PBRMaterial(materialName, scene); - let [albedo, normal, metalRoughness]: [albedo: BaseTexture, normal: Texture, metalRoughness: Texture] = await Promise.all([ + let [albedo, normal, metalRoughness]: [ + albedo: BaseTexture, + normal: Texture, + metalRoughness: Texture + ] = await Promise.all([ importTextureAsync(textures.albedo), importTextureAsync(textures.normal), importTextureAsync(textures.metalRoughness), @@ -69,7 +90,12 @@ class Dice { return pbr; } - static createInstanceFromMesh(mesh: Mesh, name: string, physicalProperties: PhysicsImpostorParameters, scene: Scene) { + static createInstanceFromMesh( + mesh: Mesh, + name: string, + physicalProperties: PhysicsImpostorParameters, + scene: Scene + ) { let instance = mesh.createInstance(name); instance.position = mesh.position; for (let child of mesh.getChildTransformNodes()) { @@ -77,7 +103,7 @@ class Dice { const locator: any = child.clone(child.name, instance); // TODO: handle possible null value if (!locator) { - throw Error + throw Error; } locator.setAbsolutePosition(child.getAbsolutePosition()); locator.name = child.name; @@ -114,7 +140,7 @@ class Dice { } } - static roll(instance: Mesh) { + static roll(instance: InstancedMesh) { instance.physicsImpostor?.setLinearVelocity(Vector3.Zero()); instance.physicsImpostor?.setAngularVelocity(Vector3.Zero()); @@ -156,7 +182,11 @@ class Dice { ); } - static createInstanceMesh(mesh: Mesh, physicalProperties: PhysicsImpostorParameters, scene: Scene): InstancedMesh { + static createInstanceMesh( + mesh: Mesh, + physicalProperties: PhysicsImpostorParameters, + scene: Scene + ): InstancedMesh { this.instanceCount++; return this.createInstanceFromMesh( @@ -166,6 +196,14 @@ class Dice { scene ); } + + static async load(scene: Scene) { + throw new Error(`Unable to load ${scene}`); + } + + static createInstance(diceType: DiceType, scene: Scene): InstancedMesh { + throw new Error(`No instance available for ${diceType} in ${scene}`); + } } export default Dice; diff --git a/src/dice/index.ts b/src/dice/index.ts index 82cf7f0..5c71cbc 100644 --- a/src/dice/index.ts +++ b/src/dice/index.ts @@ -19,7 +19,9 @@ import GlassPreview from "./glass/preview.png"; import GemstonePreview from "./gemstone/preview.png"; import Dice from "./Dice"; -type DiceClasses = Record; +import { DefaultDice } from "../types/Dice"; + +type DiceClasses = Record; export const diceClasses: DiceClasses = { galaxy: GalaxyDice, @@ -45,7 +47,7 @@ export const dicePreviews: DicePreview = { gemstone: GemstonePreview, }; -export const dice = Object.keys(diceClasses).map((key) => ({ +export const dice: DefaultDice[] = Object.keys(diceClasses).map((key) => ({ key, name: Case.capital(key), class: diceClasses[key], diff --git a/src/dice/walnut/WalnutDice.ts b/src/dice/walnut/WalnutDice.ts index 979808e..9813f11 100644 --- a/src/dice/walnut/WalnutDice.ts +++ b/src/dice/walnut/WalnutDice.ts @@ -12,6 +12,7 @@ import d12Source from "./d12.glb"; import d20Source from "./d20.glb"; import d100Source from "./d100.glb"; import { Material, Mesh, Scene } from "@babylonjs/core"; +import { DiceType } from "../../types/Dice"; const sourceOverrides = { d4: d4Source, @@ -24,7 +25,7 @@ const sourceOverrides = { }; class WalnutDice extends Dice { - static meshes: Record; + static meshes: Record; static material: Material; static getDicePhysicalProperties(diceType: string) { @@ -49,7 +50,7 @@ class WalnutDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/helpers/Vector2.ts b/src/helpers/Vector2.ts index 2f9ef6b..8e727db 100644 --- a/src/helpers/Vector2.ts +++ b/src/helpers/Vector2.ts @@ -6,12 +6,12 @@ import { } from "./shared"; export type BoundingBox = { - min: Vector2, - max: Vector2, - width: number, - height: number, - center: Vector2 -} + min: Vector2; + max: Vector2; + width: number; + height: number; + center: Vector2; +}; /** * Vector class with x, y and static helper methods @@ -287,7 +287,12 @@ class Vector2 { * @param {Vector2} C End of the curve * @returns {Object} The distance to and the closest point on the curve */ - static distanceToQuadraticBezier(pos: Vector2, A: Vector2, B: Vector2, C: Vector2): Object { + static distanceToQuadraticBezier( + pos: Vector2, + A: Vector2, + B: Vector2, + C: Vector2 + ): Object { let distance = 0; let point = { x: pos.x, y: pos.y }; @@ -514,7 +519,10 @@ class Vector2 { * @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector * @returns {Vector2} */ - static rotate90(p: Vector2, direction: "counterClockwise" | "clockwise" = "clockwise"): Vector2 { + static rotate90( + p: Vector2, + direction: "counterClockwise" | "clockwise" = "clockwise" + ): Vector2 { if (direction === "clockwise") { return { x: p.y, y: -p.x }; } else { @@ -527,7 +535,7 @@ class Vector2 { * @param {Vector2[]} points * @returns {Vector2} */ - static centroid(points) { + static centroid(points: Vector2[]): Vector2 { let center = { x: 0, y: 0 }; for (let point of points) { center.x += point.x; @@ -544,7 +552,7 @@ class Vector2 { * @param {Vector2[]} points * @returns {boolean} */ - static rectangular(points) { + static rectangular(points: Vector2[]): boolean { if (points.length !== 4) { return false; } @@ -567,7 +575,7 @@ class Vector2 { * @param {Vector2[]} points * @returns {boolean} */ - static circular(points, threshold = 0.1) { + static circular(points: Vector2[], threshold = 0.1): boolean { const centroid = this.centroid(points); let distances = []; for (let point of points) { diff --git a/src/helpers/dice.ts b/src/helpers/dice.ts index 2eb661e..8cb086d 100644 --- a/src/helpers/dice.ts +++ b/src/helpers/dice.ts @@ -1,4 +1,5 @@ import { Vector3 } from "@babylonjs/core/Maths/math"; +import { DiceRoll } from "../types/Dice"; /** * Find the number facing up on a mesh instance of a dice @@ -42,7 +43,7 @@ export function getDiceRoll(dice: any) { return { type: dice.type, roll: number }; } -export function getDiceRollTotal(diceRolls: []) { +export function getDiceRollTotal(diceRolls: DiceRoll[]) { return diceRolls.reduce((accumulator: number, dice: any) => { if (dice.roll === "unknown") { return accumulator; diff --git a/src/helpers/drawing.ts b/src/helpers/drawing.ts index 3b18051..92b04db 100644 --- a/src/helpers/drawing.ts +++ b/src/helpers/drawing.ts @@ -2,136 +2,19 @@ import simplify from "simplify-js"; import polygonClipping, { Geom, Polygon, Ring } from "polygon-clipping"; import Vector2, { BoundingBox } from "./Vector2"; -import Size from "./Size" +import Size from "./Size"; import { toDegrees } from "./shared"; -import { Grid, getNearestCellCoordinates, getCellLocation } from "./grid"; +import { getNearestCellCoordinates, getCellLocation } from "./grid"; -/** - * @typedef PointsData - * @property {Vector2[]} points - */ - -type PointsData = { - points: Vector2[] -} - -/** - * @typedef RectData - * @property {number} x - * @property {number} y - * @property {number} width - * @property {number} height - */ - -type RectData = { - x: number, - y: number, - width: number, - height: number -} - -/** - * @typedef CircleData - * @property {number} x - * @property {number} y - * @property {number} radius - */ - -type CircleData = { - x: number, - y: number, - radius: number -} - -/** - * @typedef FogData - * @property {Vector2[]} points - * @property {Vector2[][]} holes - */ - -type FogData = { - points: Vector2[] - holes: Vector2[][] -} - -/** - * @typedef {(PointsData|RectData|CircleData)} ShapeData - */ - -type ShapeData = PointsData | RectData | CircleData - -/** - * @typedef {("line"|"rectangle"|"circle"|"triangle")} ShapeType - */ - -type ShapeType = "line" | "rectangle" | "circle" | "triangle" - -/** - * @typedef {("fill"|"stroke")} PathType - */ - -type PathType = "fill" | "stroke" - -/** - * @typedef Path - * @property {boolean} blend - * @property {string} color - * @property {PointsData} data - * @property {string} id - * @property {PathType} pathType - * @property {number} strokeWidth - * @property {"path"} type - */ - -export type Path = { - blend: boolean, - color: string, - data: PointsData, - id: string, - pathType: PathType, - strokeWidth: number, - type: "path" -} - -/** - * @typedef Shape - * @property {boolean} blend - * @property {string} color - * @property {ShapeData} data - * @property {string} id - * @property {ShapeType} shapeType - * @property {number} strokeWidth - * @property {"shape"} type - */ - -export type Shape = { - blend: boolean, - color: string, - data: ShapeData, - id: string, - shapeType: ShapeType, - strokeWidth: number, - type: "shape" -} - -/** - * @typedef Fog - * @property {string} color - * @property {FogData} data - * @property {string} id - * @property {number} strokeWidth - * @property {"fog"} type - * @property {boolean} visible - */ - -export type Fog = { - color: string, - data: FogData, - id: string, - strokeWidth: number, - type: "fog", - visible: boolean -} +import { + ShapeType, + ShapeData, + PointsData, + RectData, + CircleData, +} from "../types/Drawing"; +import { Fog } from "../types/Fog"; +import { Grid } from "../types/Grid"; /** * @@ -139,24 +22,26 @@ export type Fog = { * @param {Vector2} brushPosition * @returns {ShapeData} */ -export function getDefaultShapeData(type: ShapeType, brushPosition: Vector2): ShapeData | undefined{ - // TODO: handle undefined if no type found +export function getDefaultShapeData( + type: ShapeType, + brushPosition: Vector2 +): ShapeData { if (type === "line") { return { points: [ { x: brushPosition.x, y: brushPosition.y }, { x: brushPosition.x, y: brushPosition.y }, ], - } as PointsData; + }; } else if (type === "circle") { - return { x: brushPosition.x, y: brushPosition.y, radius: 0 } as CircleData; + return { x: brushPosition.x, y: brushPosition.y, radius: 0 }; } else if (type === "rectangle") { return { x: brushPosition.x, y: brushPosition.y, width: 0, height: 0, - } as RectData; + }; } else if (type === "triangle") { return { points: [ @@ -164,7 +49,9 @@ export function getDefaultShapeData(type: ShapeType, brushPosition: Vector2): Sh { x: brushPosition.x, y: brushPosition.y }, { x: brushPosition.x, y: brushPosition.y }, ], - } as PointsData; + }; + } else { + throw new Error("Shape type not implemented"); } } @@ -197,15 +84,14 @@ export function getUpdatedShapeData( gridCellNormalizedSize: Vector2, mapWidth: number, mapHeight: number -): ShapeData | undefined { - // TODO: handle undefined type +): ShapeData { if (type === "line") { data = data as PointsData; return { points: [data.points[0], { x: brushPosition.x, y: brushPosition.y }], } as PointsData; } else if (type === "circle") { - data = data as CircleData; + data = data as CircleData; const gridRatio = getGridCellRatio(gridCellNormalizedSize); const dif = Vector2.subtract(brushPosition, { x: data.x, @@ -254,6 +140,8 @@ export function getUpdatedShapeData( Vector2.add(Vector2.multiply(rightDirNorm, sideLength), points[0]), ], }; + } else { + throw new Error("Shape type not implemented"); } } @@ -262,7 +150,10 @@ export function getUpdatedShapeData( * @param {Vector2[]} points * @param {number} tolerance */ -export function simplifyPoints(points: Vector2[], tolerance: number): Vector2[] { +export function simplifyPoints( + points: Vector2[], + tolerance: number +): Vector2[] { return simplify(points, tolerance); } @@ -272,7 +163,10 @@ export function simplifyPoints(points: Vector2[], tolerance: number): Vector2[] * @param {boolean} ignoreHidden * @returns {Fog[]} */ -export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog[] { +export function mergeFogShapes( + shapes: Fog[], + ignoreHidden: boolean = true +): Fog[] { if (shapes.length === 0) { return shapes; } @@ -283,7 +177,7 @@ export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog } const shapePoints: Ring = shape.data.points.map(({ x, y }) => [x, y]); const shapeHoles: Polygon = shape.data.holes.map((hole) => - hole.map(({ x, y }: { x: number, y: number }) => [x, y]) + hole.map(({ x, y }: { x: number; y: number }) => [x, y]) ); let shapeGeom: Geom = [[shapePoints, ...shapeHoles]]; geometries.push(shapeGeom); @@ -315,7 +209,7 @@ export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog points: union[i][0].map(([x, y]) => ({ x, y })), holes, }, - type: "fog" + type: "fog", }); } return merged; @@ -330,7 +224,10 @@ export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog * @param {boolean} maxPoints Max amount of points per shape to get bounds for * @returns {Vector2.BoundingBox[]} */ -export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): BoundingBox[] { +export function getFogShapesBoundingBoxes( + shapes: Fog[], + maxPoints = 0 +): BoundingBox[] { let boxes = []; for (let shape of shapes) { if (maxPoints > 0 && shape.data.points.length > maxPoints) { @@ -361,11 +258,11 @@ export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): Boundin */ type Guide = { - start: Vector2, - end: Vector2, - orientation: "horizontal" | "vertical", - distance: number -} + start: Vector2; + end: Vector2; + orientation: "horizontal" | "vertical"; + distance: number; +}; /** * @param {Vector2} brushPosition Brush position in pixels @@ -382,7 +279,7 @@ export function getGuidesFromGridCell( grid: Grid, gridCellSize: Size, gridOffset: Vector2, - gridCellOffset: Vector2, + gridCellOffset: Vector2, snappingSensitivity: number, mapSize: Vector2 ): Guide[] { @@ -500,7 +397,10 @@ export function getGuidesFromBoundingBoxes( * @param {Guide[]} guides * @returns {Guide[]} */ -export function findBestGuides(brushPosition: Vector2, guides: Guide[]): Guide[] { +export function findBestGuides( + brushPosition: Vector2, + guides: Guide[] +): Guide[] { let bestGuides: Guide[] = []; let verticalGuide = guides .filter((guide) => guide.orientation === "vertical") diff --git a/src/helpers/grid.ts b/src/helpers/grid.ts index fa5d422..52b906b 100644 --- a/src/helpers/grid.ts +++ b/src/helpers/grid.ts @@ -8,42 +8,6 @@ import { logError } from "./logging"; const SQRT3 = 1.73205; const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented"); -/** - * @typedef GridInset - * @property {Vector2} topLeft Top left position of the inset - * @property {Vector2} bottomRight Bottom right position of the inset - */ - -type GridInset = { - topLeft: Vector2, - bottomRight: Vector2 -} - -/** - * @typedef GridMeasurement - * @property {("chebyshev"|"alternating"|"euclidean"|"manhattan")} type - * @property {string} scale - */ - -type GridMeasurement ={ - type: ("chebyshev"|"alternating"|"euclidean"|"manhattan") - scale: string -} - -/** - * @typedef Grid - * @property {GridInset} inset The inset of the grid from the map - * @property {Vector2} size The number of columns and rows of the grid as `x` and `y` - * @property {("square"|"hexVertical"|"hexHorizontal")} type - * @property {GridMeasurement} measurement - */ -export type Grid = { - inset?: GridInset, - size: Vector2, - type: ("square"|"hexVertical"|"hexHorizontal"), - measurement?: GridMeasurement -} - /** * Gets the size of a grid in pixels taking into account the inset * @param {Grid} grid @@ -51,7 +15,11 @@ export type Grid = { * @param {number} baseHeight Height of the grid in pixels before inset * @returns {Size} */ -export function getGridPixelSize(grid: Required, baseWidth: number, baseHeight: number): Size { +export function getGridPixelSize( + grid: Required, + baseWidth: number, + baseHeight: number +): Size { const width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth; const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight; return new Size(width, height); @@ -64,7 +32,11 @@ export function getGridPixelSize(grid: Required, baseWidth: number, baseHe * @param {number} gridHeight Height of the grid in pixels after inset * @returns {Size} */ -export function getCellPixelSize(grid: Grid, gridWidth: number, gridHeight: number): Size { +export function getCellPixelSize( + grid: Grid, + gridWidth: number, + gridHeight: number +): Size { if (grid.size.x === 0 || grid.size.y === 0) { return new Size(0, 0); } @@ -91,7 +63,12 @@ export function getCellPixelSize(grid: Grid, gridWidth: number, gridHeight: numb * @param {Size} cellSize Cell size in pixels * @returns {Vector2} */ -export function getCellLocation(grid: Grid, col: number, row: number, cellSize: Size): Vector2 { +export function getCellLocation( + grid: Grid, + col: number, + row: number, + cellSize: Size +): Vector2 { switch (grid.type) { case "square": return { @@ -121,7 +98,12 @@ export function getCellLocation(grid: Grid, col: number, row: number, cellSize: * @param {Size} cellSize Cell size in pixels * @returns {Vector2} */ -export function getNearestCellCoordinates(grid: Grid, x: number, y: number, cellSize: Size): Vector2 { +export function getNearestCellCoordinates( + grid: Grid, + x: number, + y: number, + cellSize: Size +): Vector2 { switch (grid.type) { case "square": return Vector2.divide(Vector2.floorTo({ x, y }, cellSize), cellSize); @@ -151,7 +133,12 @@ export function getNearestCellCoordinates(grid: Grid, x: number, y: number, cell * @param {Size} cellSize Cell size in pixels * @returns {Vector2[]} */ -export function getCellCorners(grid: Grid, x: number, y: number, cellSize: Size): Vector2[] { +export function getCellCorners( + grid: Grid, + x: number, + y: number, + cellSize: Size +): Vector2[] { const position = new Vector2(x, y); switch (grid.type) { case "square": @@ -193,7 +180,7 @@ export function getCellCorners(grid: Grid, x: number, y: number, cellSize: Size) * @param {number} gridWidth Width of the grid in pixels after inset * @returns {number} */ -function getGridHeightFromWidth(grid: Grid, gridWidth: number): number{ +function getGridHeightFromWidth(grid: Grid, gridWidth: number): number { switch (grid.type) { case "square": return (grid.size.y * gridWidth) / grid.size.x; @@ -215,7 +202,11 @@ function getGridHeightFromWidth(grid: Grid, gridWidth: number): number{ * @param {number} mapHeight Height of the map in pixels before inset * @returns {GridInset} */ -export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset { +export function getGridDefaultInset( + grid: Grid, + mapWidth: number, + mapHeight: number +): GridInset { // Max the width of the inset and figure out the resulting height value const insetHeightNorm = getGridHeightFromWidth(grid, mapWidth) / mapHeight; return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: insetHeightNorm } }; @@ -228,7 +219,11 @@ export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: num * @param {number} mapHeight Height of the map in pixels before inset * @returns {GridInset} */ -export function getGridUpdatedInset(grid: Required, mapWidth: number, mapHeight: number): GridInset { +export function getGridUpdatedInset( + grid: Required, + mapWidth: number, + mapHeight: number +): GridInset { let inset = { topLeft: { ...grid.inset.topLeft }, bottomRight: { ...grid.inset.bottomRight }, @@ -263,7 +258,10 @@ export function getGridMaxZoom(grid: Grid): number { * @param {("hexVertical"|"hexHorizontal")} type * @returns {Vector2} */ -export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizontal")) { +export function hexCubeToOffset( + cube: Vector3, + type: "hexVertical" | "hexHorizontal" +) { if (type === "hexVertical") { const x = cube.x + (cube.z + (cube.z & 1)) / 2; const y = cube.z; @@ -280,7 +278,10 @@ export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizont * @param {("hexVertical"|"hexHorizontal")} type * @returns {Vector3} */ -export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizontal")) { +export function hexOffsetToCube( + offset: Vector2, + type: "hexVertical" | "hexHorizontal" +) { if (type === "hexVertical") { const x = offset.x - (offset.y + (offset.y & 1)) / 2; const z = offset.y; @@ -301,7 +302,12 @@ export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizo * @param {Vector2} b * @param {Size} cellSize */ -export function gridDistance(grid: Required, a: Vector2, b: Vector2, cellSize: Size) { +export function gridDistance( + grid: Required, + a: Vector2, + b: Vector2, + cellSize: Size +) { // Get grid coordinates const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize); const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize); @@ -315,7 +321,9 @@ export function gridDistance(grid: Required, a: Vector2, b: Vector2, cellS const min: any = Vector2.min(delta); return max - min + Math.floor(1.5 * min); } else if (grid.measurement.type === "euclidean") { - return Vector2.magnitude(Vector2.divide(Vector2.subtract(a, b), cellSize)); + return Vector2.magnitude( + Vector2.divide(Vector2.subtract(a, b), cellSize) + ); } else if (grid.measurement.type === "manhattan") { return Math.abs(aCoord.x - bCoord.x) + Math.abs(aCoord.y - bCoord.y); } @@ -331,24 +339,13 @@ export function gridDistance(grid: Required, a: Vector2, b: Vector2, cellS 2 ); } else if (grid.measurement.type === "euclidean") { - return Vector2.magnitude(Vector2.divide(Vector2.subtract(a, b), cellSize)); + return Vector2.magnitude( + Vector2.divide(Vector2.subtract(a, b), cellSize) + ); } } } -/** - * @typedef GridScale - * @property {number} multiplier The number multiplier of the scale - * @property {string} unit The unit of the scale - * @property {number} digits The precision of the scale - */ - -type GridScale = { - multiplier: number, - unit: string, - digits: number -} - /** * Parse a string representation of scale e.g. 5ft into a `GridScale` * @param {string} scale @@ -441,7 +438,10 @@ export function gridSizeVaild(x: number, y: number): boolean { * @param {number[]} candidates * @returns {Vector2 | null} */ -function gridSizeHeuristic(image: CanvasImageSource, candidates: number[]): Vector2 | null { +function gridSizeHeuristic( + image: CanvasImageSource, + candidates: number[] +): Vector2 | null { // TODO: check type for Image and CanvasSourceImage const width: any = image.width; const height: any = image.height; @@ -474,7 +474,10 @@ function gridSizeHeuristic(image: CanvasImageSource, candidates: number[]): Vect * @param {number[]} candidates * @returns {Vector2 | null} */ -async function gridSizeML(image: CanvasImageSource, candidates: number[]): Promise { +async function gridSizeML( + image: CanvasImageSource, + candidates: number[] +): Promise { // TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match const width: any = image.width; const height: any = image.height; diff --git a/src/helpers/group.js b/src/helpers/group.ts similarity index 59% rename from src/helpers/group.js rename to src/helpers/group.ts index dd8571b..17103c3 100644 --- a/src/helpers/group.js +++ b/src/helpers/group.ts @@ -2,34 +2,14 @@ import { v4 as uuid } from "uuid"; import cloneDeep from "lodash.clonedeep"; import { keyBy } from "./shared"; - -/** - * @typedef GroupItem - * @property {string} id - * @property {"item"} type - */ - -/** - * @typedef GroupContainer - * @property {string} id - * @property {"group"} type - * @property {GroupItem[]} items - * @property {string} name - */ - -/** - * @typedef {GroupItem|GroupContainer} Group - */ +import { Group, GroupContainer, GroupItem } from "../types/Group"; /** * Transform an array of group ids to their groups - * @param {string[]} groupIds - * @param {Group[]} groups - * @return {Group[[]} */ -export function groupsFromIds(groupIds, groups) { +export function groupsFromIds(groupIds: string[], groups: Group[]): Group[] { const groupsByIds = keyBy(groups, "id"); - const filteredGroups = []; + const filteredGroups: Group[] = []; for (let groupId of groupIds) { if (groupId in groupsByIds) { filteredGroups.push(groupsByIds[groupId]); @@ -40,10 +20,8 @@ export function groupsFromIds(groupIds, groups) { /** * Get all items from a group including all sub groups - * @param {Group} group - * @return {GroupItem[]} */ -export function getGroupItems(group) { +export function getGroupItems(group: Group): GroupItem[] { if (group.type === "group") { let groups = []; for (let item of group.items) { @@ -57,14 +35,14 @@ export function getGroupItems(group) { /** * Transform an array of groups into their assosiated items - * @param {Group[]} groups - * @param {any[]} allItems - * @param {string} itemKey - * @returns {any[]} */ -export function itemsFromGroups(groups, allItems, itemKey = "id") { +export function itemsFromGroups( + groups: Group[], + allItems: Item[], + itemKey = "id" +): Item[] { const allItemsById = keyBy(allItems, itemKey); - const groupedItems = []; + const groupedItems: Item[] = []; for (let group of groups) { const groupItems = getGroupItems(group); @@ -76,47 +54,52 @@ export function itemsFromGroups(groups, allItems, itemKey = "id") { } /** - * Combine two groups - * @param {Group} a - * @param {Group} b - * @returns {GroupContainer} + * Combine a group and a group item */ -export function combineGroups(a, b) { - if (a.type === "item") { - return { - id: uuid(), - type: "group", - items: [a, b], - name: "", - }; - } - if (a.type === "group") { - return { - id: a.id, - type: "group", - items: [...a.items, b], - name: a.name, - }; +export function combineGroups(a: Group, b: Group): GroupContainer { + switch (a.type) { + case "item": + if (b.type !== "item") { + throw new Error("Unable to combine two GroupContainers"); + } + return { + id: uuid(), + type: "group", + items: [a, b], + name: "", + }; + case "group": + if (b.type !== "item") { + throw new Error("Unable to combine two GroupContainers"); + } + return { + id: a.id, + type: "group", + items: [...a.items, b], + name: a.name, + }; + default: + throw new Error("Group type not implemented"); } } /** * Immutably move group at indices `indices` into group at index `into` - * @param {Group[]} groups - * @param {number} into - * @param {number[]} indices - * @returns {Group[]} */ -export function moveGroupsInto(groups, into, indices) { +export function moveGroupsInto( + groups: Group[], + into: number, + indices: number[] +): Group[] { const newGroups = cloneDeep(groups); const intoGroup = newGroups[into]; - let fromGroups = []; + let fromGroups: Group[] = []; for (let i of indices) { fromGroups.push(newGroups[i]); } - let combined = intoGroup; + let combined: Group = intoGroup; for (let fromGroup of fromGroups) { combined = combineGroups(combined, fromGroup); } @@ -133,12 +116,12 @@ export function moveGroupsInto(groups, into, indices) { /** * Immutably move group at indices `indices` to index `to` - * @param {Group[]} groups - * @param {number} into - * @param {number[]} indices - * @returns {Group[]} */ -export function moveGroups(groups, to, indices) { +export function moveGroups( + groups: Group[], + to: number, + indices: number[] +): Group[] { const newGroups = cloneDeep(groups); let fromGroups = []; @@ -160,28 +143,31 @@ export function moveGroups(groups, to, indices) { /** * Move items from a sub group to the start of the base group - * @param {Group[]} groups - * @param {string} fromId The id of the group to move from - * @param {number[]} indices The indices of the items in the group + * @param fromId The id of the group to move from + * @param indices The indices of the items in the group */ -export function ungroup(groups, fromId, indices) { +export function ungroup(groups: Group[], fromId: string, indices: number[]) { const newGroups = cloneDeep(groups); - let fromIndex = newGroups.findIndex((group) => group.id === fromId); + const fromIndex = newGroups.findIndex((group) => group.id === fromId); + const from = newGroups[fromIndex]; + if (from.type !== "group") { + throw new Error(`Unable to ungroup ${fromId}, not a group`); + } - let items = []; + let items: GroupItem[] = []; for (let i of indices) { - items.push(newGroups[fromIndex].items[i]); + items.push(from.items[i]); } // Remove items from previous group for (let item of items) { - const i = newGroups[fromIndex].items.findIndex((el) => el.id === item.id); - newGroups[fromIndex].items.splice(i, 1); + const i = from.items.findIndex((el) => el.id === item.id); + from.items.splice(i, 1); } // If we have no more items in the group delete it - if (newGroups[fromIndex].items.length === 0) { + if (from.items.length === 0) { newGroups.splice(fromIndex, 1); } @@ -193,11 +179,8 @@ export function ungroup(groups, fromId, indices) { /** * Recursively find a group within a group array - * @param {Group[]} groups - * @param {string} groupId - * @returns {Group} */ -export function findGroup(groups, groupId) { +export function findGroup(groups: Group[], groupId: string): Group | undefined { for (let group of groups) { if (group.id === groupId) { return group; @@ -213,11 +196,9 @@ export function findGroup(groups, groupId) { /** * Transform and item array to a record of item ids to item names - * @param {any[]} items - * @param {string=} itemKey */ -export function getItemNames(items, itemKey = "id") { - let names = {}; +export function getItemNames(items: any[], itemKey: string = "id") { + let names: Record = {}; for (let item of items) { names[item[itemKey]] = item.name; } @@ -226,15 +207,20 @@ export function getItemNames(items, itemKey = "id") { /** * Immutably rename a group - * @param {Group[]} groups - * @param {string} groupId - * @param {string} newName */ -export function renameGroup(groups, groupId, newName) { +export function renameGroup( + groups: Group[], + groupId: string, + newName: string +): Group[] { let newGroups = cloneDeep(groups); const groupIndex = newGroups.findIndex((group) => group.id === groupId); + const group = groups[groupIndex]; + if (group.type !== "group") { + throw new Error(`Unable to rename group ${groupId}, not of type group`); + } if (groupIndex >= 0) { - newGroups[groupIndex].name = newName; + group.name = newName; } return newGroups; } @@ -244,7 +230,7 @@ export function renameGroup(groups, groupId, newName) { * @param {Group[]} groups * @param {string[]} itemIds */ -export function removeGroupsItems(groups, itemIds) { +export function removeGroupsItems(groups: Group[], itemIds: string[]): Group[] { let newGroups = cloneDeep(groups); for (let i = newGroups.length - 1; i >= 0; i--) { @@ -258,11 +244,11 @@ export function removeGroupsItems(groups, itemIds) { for (let j = items.length - 1; j >= 0; j--) { const item = items[j]; if (itemIds.includes(item.id)) { - newGroups[i].items.splice(j, 1); + group.items.splice(j, 1); } } // Remove group if no items are left - if (newGroups[i].items.length === 0) { + if (group.items.length === 0) { newGroups.splice(i, 1); } } diff --git a/src/helpers/image.ts b/src/helpers/image.ts index 6a666f2..605bd83 100644 --- a/src/helpers/image.ts +++ b/src/helpers/image.ts @@ -3,13 +3,15 @@ import imageOutline from "image-outline"; import blobToBuffer from "./blobToBuffer"; import Vector2 from "./Vector2"; +import { Outline } from "../types/Outline"; + const lightnessDetectionOffset = 0.1; /** * @param {HTMLImageElement} image * @returns {boolean} True is the image is light */ -export function getImageLightness(image: HTMLImageElement) { +export function getImageLightness(image: HTMLImageElement): boolean { const width = image.width; const height = image.height; let canvas = document.createElement("canvas"); @@ -17,8 +19,7 @@ export function getImageLightness(image: HTMLImageElement) { canvas.height = height; let context = canvas.getContext("2d"); if (!context) { - // TODO: handle if context is null - return; + return false; } context.drawImage(image, 0, 0); @@ -45,18 +46,12 @@ export function getImageLightness(image: HTMLImageElement) { return norm + lightnessDetectionOffset >= 0; } -/** - * @typedef CanvasImage - * @property {Blob|null} blob The blob of the resized image, `null` if the image was unable to be resized to that dimension - * @property {number} width - * @property {number} height - */ - type CanvasImage = { - blob: Blob | null, - width: number, - height: number -} + file: Uint8Array; + width: number; + height: number; + mime: string; +}; /** * @param {HTMLCanvasElement} canvas @@ -64,11 +59,25 @@ type CanvasImage = { * @param {number} quality * @returns {Promise} */ -export async function canvasToImage(canvas: HTMLCanvasElement, type: string, quality: number): Promise { +export async function canvasToImage( + canvas: HTMLCanvasElement, + type: string, + quality: number +): Promise { return new Promise((resolve) => { canvas.toBlob( - (blob) => { - resolve({ blob, width: canvas.width, height: canvas.height }); + async (blob) => { + if (blob) { + const file = await blobToBuffer(blob); + resolve({ + file, + width: canvas.width, + height: canvas.height, + mime: type, + }); + } else { + resolve(undefined); + } }, type, quality @@ -81,9 +90,14 @@ export async function canvasToImage(canvas: HTMLCanvasElement, type: string, qua * @param {number} size the size of the longest edge of the new image * @param {string} type the mime type of the image * @param {number} quality if image is a jpeg or webp this is the quality setting - * @returns {Promise} + * @returns {Promise} */ -export async function resizeImage(image: HTMLImageElement, size: number, type: string, quality: number): Promise { +export async function resizeImage( + image: HTMLImageElement, + size: number, + type: string, + quality: number +): Promise { const width = image.width; const height = image.height; const ratio = width / height; @@ -96,37 +110,27 @@ export async function resizeImage(image: HTMLImageElement, size: number, type: s canvas.height = size; } let context = canvas.getContext("2d"); - // TODO: Add error if context is empty if (context) { context.drawImage(image, 0, 0, canvas.width, canvas.height); + } else { + return undefined; } return await canvasToImage(canvas, type, quality); } -/** - * @typedef ImageAsset - * @property {number} width - * @property {number} height - * @property {Uint8Array} file - * @property {string} mime - */ - -export type ImageFile = { - file: Uint8Array | null, - width: number, - height: number, - type: "file", - id: string -} /** * Create a image file with resolution `size`x`size` with cover cropping * @param {HTMLImageElement} image the image to resize * @param {string} type the mime type of the image * @param {number} size the width and height of the thumbnail * @param {number} quality if image is a jpeg or webp this is the quality setting - * @returns {Promise} */ -export async function createThumbnail(image: HTMLImageElement, type: string, size = 300, quality = 0.5): Promise { +export async function createThumbnail( + image: HTMLImageElement, + type: string, + size = 300, + quality = 0.5 +): Promise { let canvas = document.createElement("canvas"); canvas.width = size; canvas.height = size; @@ -166,55 +170,20 @@ export async function createThumbnail(image: HTMLImageElement, type: string, siz } } - const thumbnailImage = await canvasToImage(canvas, type, quality); - - let thumbnailBuffer = null; - if (thumbnailImage.blob) { - thumbnailBuffer = await blobToBuffer(thumbnailImage.blob); - } - return { - file: thumbnailBuffer, - width: thumbnailImage.width, - height: thumbnailImage.height, - mime: type, - }; + return await canvasToImage(canvas, type, quality); } -/** - * @typedef CircleOutline - * @property {"circle"} type - * @property {number} x - Center X of the circle - * @property {number} y - Center Y of the circle - * @property {number} radius - */ - -/** - * @typedef RectOutline - * @property {"rect"} type - * @property {number} width - * @property {number} height - * @property {number} x - Leftmost X position of the rect - * @property {number} y - Topmost Y position of the rect - */ - -/** - * @typedef PathOutline - * @property {"path"} type - * @property {number[]} points - Alternating x, y coordinates zipped together - */ - -/** - * @typedef {CircleOutline|RectOutline|PathOutline} Outline - */ - /** * Get the outline of an image * @param {HTMLImageElement} image * @returns {Outline} */ -export function getImageOutline(image, maxPoints = 100) { +export function getImageOutline( + image: HTMLImageElement, + maxPoints: number = 100 +): Outline { // Basic rect outline for fail conditions - const defaultOutline = { + const defaultOutline: Outline = { type: "rect", x: 0, y: 0, diff --git a/src/helpers/map.js b/src/helpers/map.ts similarity index 74% rename from src/helpers/map.js rename to src/helpers/map.ts index c493698..17241a7 100644 --- a/src/helpers/map.js +++ b/src/helpers/map.ts @@ -10,14 +10,16 @@ import { } from "./grid"; import Vector2 from "./Vector2"; -const defaultMapProps = { - showGrid: false, - snapToGrid: true, - quality: "original", - group: "", +import { Map, FileMapResolutions, FileMap } from "../types/Map"; +import { Asset } from "../types/Asset"; + +type Resolution = { + size: number; + quality: number; + id: "low" | "medium" | "high" | "ultra"; }; -const mapResolutions = [ +const mapResolutions: Resolution[] = [ { size: 30, // Pixels per grid quality: 0.5, // JPEG compression quality @@ -33,30 +35,35 @@ const mapResolutions = [ * @param {any} map * @returns {undefined|string} */ -export function getMapPreviewAsset(map) { - const res = map.resolutions; - switch (map.quality) { - case "low": - return; - case "medium": - return res.low; - case "high": - return res.medium; - case "ultra": - return res.medium; - case "original": - if (res.medium) { - return res.medium; - } else if (res.low) { +export function getMapPreviewAsset(map: Map): string | undefined { + if (map.type === "file") { + const res = map.resolutions; + switch (map.quality) { + case "low": + return; + case "medium": return res.low; - } - return; - default: - return; + case "high": + return res.medium; + case "ultra": + return res.medium; + case "original": + if (res.medium) { + return res.medium; + } else if (res.low) { + return res.low; + } + return; + default: + return; + } } } -export async function createMapFromFile(file, userId) { +export async function createMapFromFile( + file: File, + userId: string +): Promise<{ map: Map; assets: Asset[] }> { let image = new Image(); const buffer = await blobToBuffer(file); @@ -107,10 +114,10 @@ export async function createMapFromFile(file, userId) { gridSize = { x: 22, y: 22 }; } - let assets = []; + let assets: Asset[] = []; // Create resolutions - const resolutions = {}; + const resolutions: FileMapResolutions = {}; for (let resolution of mapResolutions) { const resolutionPixelSize = Vector2.multiply(gridSize, resolution.size); if ( @@ -119,20 +126,16 @@ export async function createMapFromFile(file, userId) { ) { const resized = await resizeImage( image, - Vector2.max(resolutionPixelSize), + Vector2.max(resolutionPixelSize) as number, file.type, resolution.quality ); - if (resized.blob) { + if (resized) { const assetId = uuid(); resolutions[resolution.id] = assetId; - const resizedBuffer = await blobToBuffer(resized.blob); const asset = { - file: resizedBuffer, - width: resized.width, - height: resized.height, + ...resized, id: assetId, - mime: file.type, owner: userId, }; assets.push(asset); @@ -141,12 +144,11 @@ export async function createMapFromFile(file, userId) { } // Create thumbnail const thumbnailImage = await createThumbnail(image, file.type); - const thumbnail = { - ...thumbnailImage, - id: uuid(), - owner: userId, - }; - assets.push(thumbnail); + const thumbnailId = uuid(); + if (thumbnailImage) { + const thumbnail = { ...thumbnailImage, id: thumbnailId, owner: userId }; + assets.push(thumbnail); + } const fileAsset = { id: uuid(), @@ -158,11 +160,11 @@ export async function createMapFromFile(file, userId) { }; assets.push(fileAsset); - const map = { + const map: FileMap = { name, resolutions, file: fileAsset.id, - thumbnail: thumbnail.id, + thumbnail: thumbnailId, type: "file", grid: { size: gridSize, @@ -183,7 +185,9 @@ export async function createMapFromFile(file, userId) { created: Date.now(), lastModified: Date.now(), owner: userId, - ...defaultMapProps, + showGrid: false, + snapToGrid: true, + quality: "original", }; URL.revokeObjectURL(url); diff --git a/src/helpers/shared.ts b/src/helpers/shared.ts index b63505c..438d166 100644 --- a/src/helpers/shared.ts +++ b/src/helpers/shared.ts @@ -1,5 +1,5 @@ -export function omit(obj:object, keys: string[]) { - let tmp: { [key: string]: any } = {}; +export function omit(obj: Record, keys: string[]) { + let tmp: Record = {}; for (let [key, value] of Object.entries(obj)) { if (keys.includes(key)) { continue; @@ -9,18 +9,21 @@ export function omit(obj:object, keys: string[]) { return tmp; } -export function fromEntries(iterable: any) { +export function fromEntries(iterable: Iterable<[string | number, any]>) { if (Object.fromEntries) { return Object.fromEntries(iterable); } - return [...iterable].reduce((obj, [key, val]) => { - obj[key] = val; - return obj; - }, {}); + return [...iterable].reduce( + (obj: Record, [key, val]) => { + obj[key] = val; + return obj; + }, + {} + ); } // Check to see if all tracks are muted -export function isStreamStopped(stream: any) { +export function isStreamStopped(stream: MediaStream) { return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true }); } @@ -55,19 +58,22 @@ export function logImage(url: string, width: number, height: number): void { console.log("%c ", style); } -export function isEmpty(obj: any): boolean { +export function isEmpty(obj: Object): boolean { return Object.keys(obj).length === 0 && obj.constructor === Object; } -export function keyBy(array: any, key: any) { +export function keyBy(array: Type[], key: string): Record { return array.reduce( - (prev: any, current: any) => ({ ...prev, [key ? current[key] : current]: current }), + (prev: any, current: any) => ({ + ...prev, + [key ? current[key] : current]: current, + }), {} ); } -export function groupBy(array: any, key: string) { - return array.reduce((prev: any, current: any) => { +export function groupBy(array: Record[], key: string) { + return array.reduce((prev: Record, current) => { const k = current[key]; (prev[k] || (prev[k] = [])).push(current); return prev; @@ -76,7 +82,7 @@ export function groupBy(array: any, key: string) { export const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform); -export function shuffle(array) { +export function shuffle(array: Type[]) { let temp = [...array]; var currentIndex = temp.length, randomIndex; diff --git a/src/helpers/token.js b/src/helpers/token.ts similarity index 81% rename from src/helpers/token.js rename to src/helpers/token.ts index d5eefc4..cf941b5 100644 --- a/src/helpers/token.js +++ b/src/helpers/token.ts @@ -1,12 +1,22 @@ import { v4 as uuid } from "uuid"; import Case from "case"; +import { Stage } from "konva/types/Stage"; import blobToBuffer from "./blobToBuffer"; import { createThumbnail, getImageOutline } from "./image"; import Vector2 from "./Vector2"; -export function createTokenState(token, position, userId) { - let tokenState = { +import { Token, FileToken } from "../types/Token"; +import { TokenState, BaseTokenState } from "../types/TokenState"; +import { Asset } from "../types/Asset"; +import { Outline } from "../types/Outline"; + +export function createTokenState( + token: Token, + position: Vector2, + userId: string +): TokenState { + let tokenState: BaseTokenState = { id: uuid(), tokenId: token.id, owner: userId, @@ -21,20 +31,29 @@ export function createTokenState(token, position, userId) { rotation: 0, locked: false, visible: true, - type: token.type, outline: token.outline, width: token.width, height: token.height, }; if (token.type === "file") { - tokenState.file = token.file; - } else if (token.type === "default") { - tokenState.key = token.key; + return { + ...tokenState, + type: "file", + file: token.file, + }; + } else { + return { + ...tokenState, + type: "default", + key: token.key, + }; } - return tokenState; } -export async function createTokenFromFile(file, userId) { +export async function createTokenFromFile( + file: File, + userId: string +): Promise<{ token: Token; assets: Asset[] }> { if (!file) { return Promise.reject(); } @@ -77,10 +96,13 @@ export async function createTokenFromFile(file, userId) { return new Promise((resolve, reject) => { image.onload = async function () { - let assets = []; + let assets: Asset[] = []; const thumbnailImage = await createThumbnail(image, file.type); - const thumbnail = { ...thumbnailImage, id: uuid(), owner: userId }; - assets.push(thumbnail); + const thumbnailId = uuid(); + if (thumbnailImage) { + const thumbnail = { ...thumbnailImage, id: thumbnailId, owner: userId }; + assets.push(thumbnail); + } const fileAsset = { id: uuid(), @@ -94,10 +116,10 @@ export async function createTokenFromFile(file, userId) { const outline = getImageOutline(image); - const token = { + const token: FileToken = { name, defaultSize, - thumbnail: thumbnail.id, + thumbnail: thumbnailId, file: fileAsset.id, id: uuid(), type: "file", @@ -107,7 +129,6 @@ export async function createTokenFromFile(file, userId) { defaultCategory: "character", defaultLabel: "", hideInSidebar: false, - group: "", width: image.width, height: image.height, outline, @@ -122,12 +143,15 @@ export async function createTokenFromFile(file, userId) { } export function clientPositionToMapPosition( - mapStage, - clientPosition, + mapStage: Stage, + clientPosition: Vector2, checkMapBounds = true -) { +): Vector2 | undefined { const mapImage = mapStage.findOne("#mapImage"); const map = document.querySelector(".map"); + if (!map) { + return; + } const mapRect = map.getBoundingClientRect(); // Check map bounds @@ -158,7 +182,11 @@ export function clientPositionToMapPosition( return normalizedPosition; } -export function getScaledOutline(tokenState, tokenWidth, tokenHeight) { +export function getScaledOutline( + tokenState: TokenState, + tokenWidth: number, + tokenHeight: number +): Outline { let outline = tokenState.outline; if (outline.type === "rect") { return { @@ -187,14 +215,23 @@ export function getScaledOutline(tokenState, tokenWidth, tokenHeight) { } export class Intersection { + outline; + position; + center; + rotation; + points: Vector2[] | undefined; /** - * * @param {Outline} outline * @param {Vector2} position - Top left position of the token * @param {Vector2} center - Center position of the token * @param {number} rotation - Rotation of the token in degrees */ - constructor(outline, position, center, rotation) { + constructor( + outline: Outline, + position: Vector2, + center: Vector2, + rotation: number + ) { this.outline = outline; this.position = position; this.center = center; @@ -253,8 +290,11 @@ export class Intersection { * @param {Vector2} point * @returns {boolean} */ - intersects(point) { - if (this.outline.type === "rect" || this.outline.type === "path") { + intersects(point: Vector2) { + if ( + this.points && + (this.outline.type === "rect" || this.outline.type === "path") + ) { return Vector2.pointInPolygon(point, this.points); } else if (this.outline.type === "circle") { return Vector2.distance(this.center, point) < this.outline.radius; diff --git a/src/hooks/useDebounce.tsx b/src/hooks/useDebounce.tsx index 1ab9dea..e5a3242 100644 --- a/src/hooks/useDebounce.tsx +++ b/src/hooks/useDebounce.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -function useDebounce(value: any, delay: number): any { - const [debouncedValue, setDebouncedValue] = useState(); +function useDebounce(value: Type, delay: number): Type { + const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timeout = setTimeout(() => { diff --git a/src/modals/AuthModal.tsx b/src/modals/AuthModal.tsx index 589d549..ae32f5a 100644 --- a/src/modals/AuthModal.tsx +++ b/src/modals/AuthModal.tsx @@ -5,7 +5,12 @@ import { useAuth } from "../contexts/AuthContext"; import Modal from "../components/Modal"; -function AuthModal({ isOpen, onSubmit }: { isOpen: boolean, onSubmit: (newPassword: string) => void}) { +type AuthModalProps = { + isOpen: boolean; + onSubmit: (newPassword: string) => void; +}; + +function AuthModal({ isOpen, onSubmit }: AuthModalProps) { const { password, setPassword } = useAuth(); const [tmpPassword, setTempPassword] = useState(password); @@ -19,7 +24,7 @@ function AuthModal({ isOpen, onSubmit }: { isOpen: boolean, onSubmit: (newPassw onSubmit(tmpPassword); } - const inputRef = useRef(); + const inputRef = useRef(null); function focusInput(): void { inputRef.current && inputRef.current?.focus(); } diff --git a/src/modals/ChangeNicknameModal.tsx b/src/modals/ChangeNicknameModal.tsx index 2e4353e..b196dec 100644 --- a/src/modals/ChangeNicknameModal.tsx +++ b/src/modals/ChangeNicknameModal.tsx @@ -3,22 +3,24 @@ import { Box, Input, Button, Label, Flex } from "theme-ui"; import Modal from "../components/Modal"; +type ChangeNicknameModalProps = { + isOpen: boolean; + onRequestClose: () => void; + onChangeSubmit: any; + nickname: string; + onChange: any; +}; + function ChangeNicknameModal({ isOpen, onRequestClose, onChangeSubmit, nickname, onChange, -}: { - isOpen: boolean, - onRequestClose: () => void, - onChangeSubmit: any, - nickname: string, - onChange: any, -}) { - const inputRef = useRef(null); +}: ChangeNicknameModalProps) { + const inputRef = useRef(null); function focusInput() { - inputRef.current && inputRef.current?.focus(); + inputRef.current?.focus(); } return ( diff --git a/src/modals/ConfirmModal.tsx b/src/modals/ConfirmModal.tsx index 16e7598..acb6d7f 100644 --- a/src/modals/ConfirmModal.tsx +++ b/src/modals/ConfirmModal.tsx @@ -3,13 +3,13 @@ import { Box, Label, Flex, Button, Text } from "theme-ui"; import Modal from "../components/Modal"; type ConfirmModalProps = { - isOpen: boolean, - onRequestClose: () => void, - onConfirm: () => void, - confirmText: string, - label: string, - description: string, -} + isOpen: boolean; + onRequestClose: () => void; + onConfirm: () => void; + confirmText: string; + label: string; + description: string; +}; function ConfirmModal({ isOpen, @@ -18,7 +18,7 @@ function ConfirmModal({ confirmText, label, description, -}: ConfirmModalProps ) { +}: ConfirmModalProps) { return ( void, - onChange: any, - groups: string[], - defaultGroup: string | undefined | false, -} - -function EditGroupModal({ - isOpen, - onRequestClose, - onChange, - groups, - defaultGroup, -}: EditGroupProps) { - const [value, setValue] = useState<{ value: string; label: string; } | undefined>(); - const [options, setOptions] = useState<{ value: string; label: string; }[]>([]); - - useEffect(() => { - if (defaultGroup) { - setValue({ value: defaultGroup, label: defaultGroup }); - } else { - setValue(undefined); - } - }, [defaultGroup]); - - useEffect(() => { - setOptions(groups.map((group) => ({ value: group, label: group }))); - }, [groups]); - - function handleCreate(group: string) { - const newOption = { value: group, label: group }; - setValue(newOption); - setOptions((prev) => [...prev, newOption]); - } - - function handleChange() { - onChange(value ? value.value : ""); - } - - return ( - - - - handleImagesUpload(event.target.files)} + onChange={(event) => + event.target.files && + handleImagesUpload(Array.from(event.target.files)) + } type="file" accept="image/jpeg, image/gif, image/png, image/webp" style={{ display: "none" }} diff --git a/src/modals/SelectTokensModal.tsx b/src/modals/SelectTokensModal.tsx index bc3cac8..4e9cfb7 100644 --- a/src/modals/SelectTokensModal.tsx +++ b/src/modals/SelectTokensModal.tsx @@ -79,7 +79,7 @@ function SelectTokensModal({ useState(false); const largeImageWarningFiles = useRef(); - async function handleImagesUpload(files: FileList) { + async function handleImagesUpload(files: File[]) { if (navigator.storage) { // Attempt to enable persistant storage await navigator.storage.persist(); @@ -231,10 +231,14 @@ function SelectTokensModal({ }} shouldCloseOnEsc={!isDraggingToken} > - + handleImagesUpload(files)} + dropText="Drop token to import" + > ) => - event.target.files && handleImagesUpload(event.target.files) + onChange={(event) => + event.target.files && + handleImagesUpload(Array.from(event.target.files)) } type="file" accept="image/jpeg, image/gif, image/png, image/webp" diff --git a/src/shortcuts.ts b/src/shortcuts.ts index fe4947a..b8deaa1 100644 --- a/src/shortcuts.ts +++ b/src/shortcuts.ts @@ -19,96 +19,76 @@ function singleKey(event: KeyboardEvent, key: string): boolean { ); } -/** - * @param {Keyboard} event - * @returns {string | boolean} - */ -function undo(event: KeyboardEvent): string | boolean { +function undo(event: KeyboardEvent): boolean { const { key, ctrlKey, metaKey, shiftKey } = event; return (key === "z" || key === "Z") && (ctrlKey || metaKey) && !shiftKey; } -/** - * @param {Keyboard} event - * @returns {string | boolean} - */ -function redo(event: KeyboardEvent): string | boolean { +function redo(event: KeyboardEvent): boolean { const { key, ctrlKey, metaKey, shiftKey } = event; return (key === "z" || key === "Z") && (ctrlKey || metaKey) && shiftKey; } -/** - * @param {Keyboard} event - * @returns {string | boolean} - */ -function zoomIn(event: KeyboardEvent): string | boolean { +function zoomIn(event: KeyboardEvent): boolean { const { key, ctrlKey, metaKey } = event; return (key === "=" || key === "+") && !ctrlKey && !metaKey; } -/** - * @param {Keyboard} event - * @returns {string | boolean} - */ -function zoomOut(event: KeyboardEvent): string | boolean { +function zoomOut(event: KeyboardEvent): boolean { const { key, ctrlKey, metaKey } = event; return (key === "-" || key === "_") && !ctrlKey && !metaKey; } -/** - * @callback shortcut - * @param {KeyboardEvent} event - * @returns {boolean} - */ +type Shortcut = (event: KeyboardEvent) => boolean; /** * @type {Object.} */ -const shortcuts = { +const shortcuts: Record = { // Tools - move: (event: KeyboardEvent) => singleKey(event, " "), - moveTool: (event: KeyboardEvent) => singleKey(event, "w"), - drawingTool: (event: KeyboardEvent) => singleKey(event, "d"), - fogTool: (event: KeyboardEvent) => singleKey(event, "f"), - measureTool: (event: KeyboardEvent) => singleKey(event, "m"), - pointerTool: (event: KeyboardEvent) => singleKey(event, "q"), - noteTool: (event: KeyboardEvent) => singleKey(event, "n"), + move: (event) => singleKey(event, " "), + moveTool: (event) => singleKey(event, "w"), + drawingTool: (event) => singleKey(event, "d"), + fogTool: (event) => singleKey(event, "f"), + measureTool: (event) => singleKey(event, "m"), + pointerTool: (event) => singleKey(event, "q"), + noteTool: (event) => singleKey(event, "n"), // Map editor - gridNudgeUp: ({ key }: { key: string}) => key === "ArrowUp", - gridNudgeLeft: ({ key }: { key: string }) => key === "ArrowLeft", - gridNudgeRight: ({ key }: { key: string }) => key === "ArrowRight", - gridNudgeDown: ({ key }: { key: string }) => key === "ArrowDown", + gridNudgeUp: ({ key }) => key === "ArrowUp", + gridNudgeLeft: ({ key }) => key === "ArrowLeft", + gridNudgeRight: ({ key }) => key === "ArrowRight", + gridNudgeDown: ({ key }) => key === "ArrowDown", // Drawing tool - drawBrush: (event: KeyboardEvent) => singleKey(event, "b"), - drawPaint: (event: KeyboardEvent) => singleKey(event, "p"), - drawLine: (event: KeyboardEvent) => singleKey(event, "l"), - drawRect: (event: KeyboardEvent) => singleKey(event, "r"), - drawCircle: (event: KeyboardEvent) => singleKey(event, "c"), - drawTriangle: (event: KeyboardEvent) => singleKey(event, "t"), - drawErase: (event: KeyboardEvent) => singleKey(event, "e"), - drawBlend: (event: KeyboardEvent) => singleKey(event, "o"), + drawBrush: (event) => singleKey(event, "b"), + drawPaint: (event) => singleKey(event, "p"), + drawLine: (event) => singleKey(event, "l"), + drawRect: (event) => singleKey(event, "r"), + drawCircle: (event) => singleKey(event, "c"), + drawTriangle: (event) => singleKey(event, "t"), + drawErase: (event) => singleKey(event, "e"), + drawBlend: (event) => singleKey(event, "o"), // Fog tool - fogPolygon: (event: KeyboardEvent) => singleKey(event, "p"), - fogRectangle: (event: KeyboardEvent) => singleKey(event, "r"), - fogBrush: (event: KeyboardEvent) => singleKey(event, "b"), - fogToggle: (event: KeyboardEvent) => singleKey(event, "t"), - fogErase: (event: KeyboardEvent) => singleKey(event, "e"), - fogLayer: (event: KeyboardEvent) => singleKey(event, "l"), - fogPreview: (event: KeyboardEvent) => singleKey(event, "f"), - fogCut: (event: KeyboardEvent) => singleKey(event, "c"), - fogFinishPolygon: ({ key }: { key: string }) => key === "Enter", - fogCancelPolygon: ({ key }: { key: string }) => key === "Escape", + fogPolygon: (event) => singleKey(event, "p"), + fogRectangle: (event) => singleKey(event, "r"), + fogBrush: (event) => singleKey(event, "b"), + fogToggle: (event) => singleKey(event, "t"), + fogErase: (event) => singleKey(event, "e"), + fogLayer: (event) => singleKey(event, "l"), + fogPreview: (event) => singleKey(event, "f"), + fogCut: (event) => singleKey(event, "c"), + fogFinishPolygon: ({ key }) => key === "Enter", + fogCancelPolygon: ({ key }) => key === "Escape", // Stage interaction stageZoomIn: zoomIn, stageZoomOut: zoomOut, - stagePrecisionZoom: ({ key }: { key: string }) => key === "Shift", + stagePrecisionZoom: ({ key }) => key === "Shift", // Select - selectRange: ({ key }: { key: string }) => key === "Shift", - selectMultiple: ({ key }: { key: string }) => key === "Control" || key === "Meta", + selectRange: ({ key }) => key === "Shift", + selectMultiple: ({ key }) => key === "Control" || key === "Meta", // Common undo, redo, - delete: ({ key }: { key: string }) => key === "Backspace" || key === "Delete", + delete: ({ key }) => key === "Backspace" || key === "Delete", }; export default shortcuts; diff --git a/src/types/Drawing.ts b/src/types/Drawing.ts index fc8883d..868bc4e 100644 --- a/src/types/Drawing.ts +++ b/src/types/Drawing.ts @@ -1,4 +1,5 @@ import Vector2 from "../helpers/Vector2"; +import { Color } from "../helpers/colors"; export type DrawingToolType = | "brush" @@ -11,7 +12,7 @@ export type DrawingToolType = export type DrawingToolSettings = { type: DrawingToolType; - color: string; + color: Color; useBlending: boolean; }; diff --git a/src/types/Fog.ts b/src/types/Fog.ts index ba54f36..205cc01 100644 --- a/src/types/Fog.ts +++ b/src/types/Fog.ts @@ -1,4 +1,5 @@ import Vector2 from "../helpers/Vector2"; +import { Color } from "../helpers/colors"; export type FogToolType = | "polygon" @@ -20,7 +21,7 @@ export type FogData = { }; export type Fog = { - color: string; + color: Color; data: FogData; id: string; strokeWidth: number; diff --git a/src/types/Note.ts b/src/types/Note.ts index 4e1b02c..9ad3f5c 100644 --- a/src/types/Note.ts +++ b/src/types/Note.ts @@ -1,6 +1,8 @@ +import { Color } from "../helpers/colors"; + export type Note = { id: string; - color: string; + color: Color; lastModified: number; lastModifiedBy: string; locked: boolean; diff --git a/src/types/Pointer.ts b/src/types/Pointer.ts index 77e69b0..254cc4b 100644 --- a/src/types/Pointer.ts +++ b/src/types/Pointer.ts @@ -1,12 +1,13 @@ import Vector2 from "../helpers/Vector2"; +import { Color } from "../helpers/colors"; export type PointerToolSettings = { - color: string; + color: Color; }; export type PointerState = { position: Vector2; visible: boolean; id: string; - color: string; + color: Color; }; From 569ed696fc9c301beb5a09cafb49af98d98239b8 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 13 Jul 2021 08:59:28 +1000 Subject: [PATCH 017/142] Typescript --- package.json | 1 + src/components/dice/DiceTiles.tsx | 5 +- .../map/{DragOverlay.js => DragOverlay.tsx} | 26 +++++-- .../map/{MapControls.js => MapControls.tsx} | 18 ++++- .../map/{MapEditBar.js => MapEditBar.tsx} | 31 ++++++-- ...SelectMapButton.js => SelectMapButton.tsx} | 0 src/contexts/MapDataContext.tsx | 77 +++++++++++-------- src/contexts/SettingsContext.tsx | 17 ++-- src/contexts/TokenDataContext.tsx | 49 ++++++++---- src/hooks/{useSetting.js => useSetting.ts} | 8 +- src/modals/EditTokenModal.tsx | 5 +- src/modals/GameExpiredModal.tsx | 4 +- src/modals/GettingStartedModal.tsx | 4 +- src/modals/GroupNameModal.tsx | 8 +- src/modals/ImportExportModal.tsx | 3 +- src/modals/JoinModal.tsx | 4 +- src/modals/SelectDataModal.tsx | 33 ++++---- src/modals/SelectDiceModal.tsx | 8 +- src/modals/SelectMapModal.tsx | 14 ++-- src/modals/SelectTokensModal.tsx | 8 +- src/modals/SettingsModal.tsx | 19 +++-- src/modals/StartModal.tsx | 12 ++- src/modals/StartStreamModal.tsx | 25 +++--- src/modals/StartTimerModal.tsx | 36 +++++---- src/types/Events.ts | 14 ++++ yarn.lock | 7 ++ 26 files changed, 298 insertions(+), 138 deletions(-) rename src/components/map/{DragOverlay.js => DragOverlay.tsx} (80%) rename src/components/map/{MapControls.js => MapControls.tsx} (92%) rename src/components/map/{MapEditBar.js => MapEditBar.tsx} (87%) rename src/components/map/{SelectMapButton.js => SelectMapButton.tsx} (100%) rename src/hooks/{useSetting.js => useSetting.ts} (68%) create mode 100644 src/types/Events.ts diff --git a/package.json b/package.json index cc32910..a9d8f31 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@types/jest": "^26.0.23", "@types/lodash.clonedeep": "^4.5.6", "@types/lodash.get": "^4.4.6", + "@types/lodash.set": "^4.3.6", "@types/node": "^15.6.0", "@types/react": "^17.0.6", "@types/react-dom": "^17.0.5", diff --git a/src/components/dice/DiceTiles.tsx b/src/components/dice/DiceTiles.tsx index f6a5cb0..33a9086 100644 --- a/src/components/dice/DiceTiles.tsx +++ b/src/components/dice/DiceTiles.tsx @@ -5,12 +5,13 @@ import DiceTile from "./DiceTile"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; import { DefaultDice } from "../../types/Dice"; +import { DiceSelectEventHandler } from "../../types/Events"; type DiceTileProps = { dice: DefaultDice[]; - onDiceSelect: (dice: DefaultDice) => void; + onDiceSelect: DiceSelectEventHandler; selectedDice: DefaultDice; - onDone: (dice: DefaultDice) => void; + onDone: DiceSelectEventHandler; }; function DiceTiles({ diff --git a/src/components/map/DragOverlay.js b/src/components/map/DragOverlay.tsx similarity index 80% rename from src/components/map/DragOverlay.js rename to src/components/map/DragOverlay.tsx index 57f140e..641fb43 100644 --- a/src/components/map/DragOverlay.js +++ b/src/components/map/DragOverlay.tsx @@ -1,26 +1,38 @@ -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Box, IconButton } from "theme-ui"; +import { Node } from "konva/types/Node"; import RemoveTokenIcon from "../../icons/RemoveTokenIcon"; -function DragOverlay({ dragging, node, onRemove }) { +type DragOverlayProps = { + dragging: boolean; + node: Node; + onRemove: () => void; +}; + +function DragOverlay({ dragging, node, onRemove }: DragOverlayProps) { const [isRemoveHovered, setIsRemoveHovered] = useState(false); - const removeTokenRef = useRef(); + const removeTokenRef = useRef(null); // Detect token hover on remove icon manually to support touch devices useEffect(() => { - const map = document.querySelector(".map"); - const mapRect = map.getBoundingClientRect(); - function detectRemoveHover() { if (!node || !dragging || !removeTokenRef.current) { return; } + const map = document.querySelector(".map"); + if (!map) { + return; + } + const mapRect = map.getBoundingClientRect(); const stage = node.getStage(); if (!stage) { return; } const pointerPosition = stage.getPointerPosition(); + if (!pointerPosition) { + return; + } const screenSpacePointerPosition = { x: pointerPosition.x + mapRect.left, y: pointerPosition.y + mapRect.top, @@ -41,7 +53,7 @@ function DragOverlay({ dragging, node, onRemove }) { } } - let handler; + let handler: NodeJS.Timeout; if (node && dragging) { handler = setInterval(detectRemoveHover, 100); } diff --git a/src/components/map/MapControls.js b/src/components/map/MapControls.tsx similarity index 92% rename from src/components/map/MapControls.js rename to src/components/map/MapControls.tsx index 44b3f77..92a32a8 100644 --- a/src/components/map/MapControls.js +++ b/src/components/map/MapControls.tsx @@ -21,6 +21,22 @@ import FullScreenExitIcon from "../../icons/FullScreenExitIcon"; import NoteToolIcon from "../../icons/NoteToolIcon"; import useSetting from "../../hooks/useSetting"; +import { Map } from "../../types/Map"; +import { MapState } from "../../types/MapState"; + +type MapControlsProps = { + onMapChange: () => void; + onMapReset: () => void; + currentMap?: Map; + currentMapState?: MapState; + selectedToolId: string; + onSelectedToolChange: () => void; + toolSettings: any; + onToolSettingChange: () => void; + onToolAction: () => void; + disabledControls: string[]; + disabledSettings: string[]; +}; function MapContols({ onMapChange, @@ -34,7 +50,7 @@ function MapContols({ onToolAction, disabledControls, disabledSettings, -}) { +}: MapControlsProps) { const [isExpanded, setIsExpanded] = useState(true); const [fullScreen, setFullScreen] = useSetting("map.fullScreen"); diff --git a/src/components/map/MapEditBar.js b/src/components/map/MapEditBar.tsx similarity index 87% rename from src/components/map/MapEditBar.js rename to src/components/map/MapEditBar.tsx index c1ec6a4..d944e05 100644 --- a/src/components/map/MapEditBar.js +++ b/src/components/map/MapEditBar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Flex, Close, IconButton } from "theme-ui"; import { groupsFromIds, itemsFromGroups } from "../../helpers/group"; @@ -13,8 +13,27 @@ import { useMapData } from "../../contexts/MapDataContext"; import { useKeyboard } from "../../contexts/KeyboardContext"; import shortcuts from "../../shortcuts"; +import { Map } from "../../types/Map"; +import { + MapChangeEventHandler, + MapResetEventHandler, +} from "../../types/Events"; -function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { +type MapEditBarProps = { + currentMap?: Map; + disabled: boolean; + onMapChange: MapChangeEventHandler; + onMapReset: MapResetEventHandler; + onLoad: (loading: boolean) => void; +}; + +function MapEditBar({ + currentMap, + disabled, + onMapChange, + onMapReset, + onLoad, +}: MapEditBarProps) { const [hasMapState, setHasMapState] = useState(false); const { maps, mapStates, removeMaps, resetMap } = useMapData(); @@ -56,11 +75,11 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { setIsMapsRemoveModalOpen(false); const selectedMaps = getSelectedMaps(); const selectedMapIds = selectedMaps.map((map) => map.id); - onGroupSelect(); + onGroupSelect(undefined); await removeMaps(selectedMapIds); // Removed the map from the map screen if needed if (currentMap && selectedMapIds.includes(currentMap.id)) { - onMapChange(null, null); + onMapChange(undefined, undefined); } onLoad(false); } @@ -84,7 +103,7 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { /** * Shortcuts */ - function handleKeyDown(event) { + function handleKeyDown(event: KeyboardEvent) { if (disabled) { return; } @@ -117,7 +136,7 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { onGroupSelect()} + onClick={() => onGroupSelect(undefined)} /> Promise; +export type RemoveMapsEventHandler = (ids: string[]) => Promise; +export type ResetMapEventHandler = (id: string) => Promise; +export type UpdateMapEventHanlder = ( + id: string, + update: Partial +) => Promise; +export type UpdateMapStateEventHandler = ( + id: string, + update: Partial +) => Promise; +export type GetMapStateEventHandler = ( + id: string +) => Promise; +export type GetMapEventHandler = (id: string) => Promise; +export type UpdateMapGroupsEventHandler = (groups: Group[]) => Promise; + type MapDataContext = { maps: Array; mapStates: MapState[]; - addMap: (map: Map) => void; - removeMaps: (ids: string[]) => void; - resetMap: (id: string) => void; - updateMap: (id: string, update: Partial) => void; - updateMapState: (id: string, update: Partial) => void; - getMapState: (id: string) => Promise; - getMap: (id: string) => Promise; + /** Adds a map to the database, also adds an assosiated state and group for that map */ + addMap: AddMapEventHandler; + removeMaps: RemoveMapsEventHandler; + resetMap: ResetMapEventHandler; + updateMap: UpdateMapEventHanlder; + updateMapState: UpdateMapStateEventHandler; + getMapState: GetMapStateEventHandler; + getMap: GetMapEventHandler; mapsLoading: boolean; - updateMapGroups: (groups: any) => void; + updateMapGroups: UpdateMapGroupsEventHandler; mapsById: Record; - mapGroups: any[]; + mapGroups: Group[]; }; const MapDataContext = React.createContext(undefined); -const defaultMapState = { +const defaultMapState: Pick< + MapState, + "tokens" | "drawShapes" | "fogShapes" | "editFlags" | "notes" +> = { tokens: {}, drawShapes: {}, fogShapes: {}, // Flags to determine what other people can edit editFlags: ["drawing", "tokens", "notes", "fog"], - notes: {} as Note[], + notes: {}, }; export function MapDataProvider({ children }: { children: React.ReactNode }) { @@ -68,7 +89,7 @@ export function MapDataProvider({ children }: { children: React.ReactNode }) { [database] ); - const [mapGroups, setMapGroups] = useState([]); + const [mapGroups, setMapGroups] = useState([]); useEffect(() => { async function updateMapGroups() { const group = await database?.table("groups").get("maps"); @@ -79,27 +100,23 @@ export function MapDataProvider({ children }: { children: React.ReactNode }) { } }, [mapGroupQuery, database]); - const getMap = useCallback( - async (mapId: string) => { - let map = (await database?.table("maps").get(mapId)) as Map; + const getMap = useCallback( + async (mapId) => { + let map = await database?.table("maps").get(mapId); return map; }, [database] ); - const getMapState = useCallback( + const getMapState = useCallback( async (mapId) => { - let mapState = (await database?.table("states").get(mapId)) as MapState; + let mapState = await database?.table("states").get(mapId); return mapState; }, [database] ); - /** - * Adds a map to the database, also adds an assosiated state and group for that map - * @param {Object} map map to add - */ - const addMap = useCallback( + const addMap = useCallback( async (map) => { if (database) { // Just update map database as react state will be updated with an Observable @@ -115,7 +132,7 @@ export function MapDataProvider({ children }: { children: React.ReactNode }) { [database] ); - const removeMaps = useCallback( + const removeMaps = useCallback( async (ids) => { if (database) { const maps = await database.table("maps").bulkGet(ids); @@ -143,30 +160,30 @@ export function MapDataProvider({ children }: { children: React.ReactNode }) { [database] ); - const resetMap = useCallback( + const resetMap = useCallback( async (id) => { - const state = { ...defaultMapState, mapId: id }; + const state: MapState = { ...defaultMapState, mapId: id }; await database?.table("states").put(state); return state; }, [database] ); - const updateMap = useCallback( + const updateMap = useCallback( async (id, update) => { await database?.table("maps").update(id, update); }, [database] ); - const updateMapState = useCallback( + const updateMapState = useCallback( async (id, update) => { await database?.table("states").update(id, update); }, [database] ); - const updateMapGroups = useCallback( + const updateMapGroups = useCallback( async (groups) => { // Update group state immediately to avoid animation delay setMapGroups(groups); diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx index 1e17934..097c005 100644 --- a/src/contexts/SettingsContext.tsx +++ b/src/contexts/SettingsContext.tsx @@ -2,21 +2,26 @@ import React, { useState, useEffect, useContext } from "react"; import { getSettings } from "../settings"; -const SettingsContext = React.createContext({ - settings: {}, - setSettings: () => {}, -}); +import { Settings } from "../types/Settings"; + +type SettingsContext = { + settings: Settings; + setSettings: React.Dispatch>; +}; + +const SettingsContext = + React.createContext(undefined); const settingsProvider = getSettings(); export function SettingsProvider({ children }: { children: any }) { - const [settings, setSettings] = useState(settingsProvider.getAll()); + const [settings, setSettings] = useState(settingsProvider.getAll()); useEffect(() => { settingsProvider.setAll(settings); }, [settings]); - const value: { settings: any, setSettings: any} = { + const value = { settings, setSettings, }; diff --git a/src/contexts/TokenDataContext.tsx b/src/contexts/TokenDataContext.tsx index 3da939d..fadc712 100644 --- a/src/contexts/TokenDataContext.tsx +++ b/src/contexts/TokenDataContext.tsx @@ -9,20 +9,37 @@ import { useLiveQuery } from "dexie-react-hooks"; import { useDatabase } from "./DatabaseContext"; -import { Token } from "../tokens"; import { removeGroupsItems } from "../helpers/group"; +import { Token } from "../types/Token"; +import { Group } from "../types/Group"; + +export type AddTokenEventHandler = (token: Token) => Promise; +export type RemoveTokensEventHandler = (ids: string[]) => Promise; +export type UpdateTokenEventHandler = ( + id: string, + update: Partial +) => Promise; +export type GetTokenEventHandler = ( + tokenId: string +) => Promise; +export type UpdateTokenGroupsEventHandler = (groups: any[]) => Promise; +export type UpdateTokensHiddenEventHandler = ( + ids: string[], + hideInSidebar: boolean +) => Promise; + type TokenDataContext = { tokens: Token[]; - addToken: (token: Token) => Promise; - tokenGroups: any[]; - removeTokens: (ids: string[]) => Promise; - updateToken: (id: string, update: Partial) => Promise; - getToken: (tokenId: string) => Promise; + addToken: AddTokenEventHandler; + tokenGroups: Group[]; + removeTokens: RemoveTokensEventHandler; + updateToken: UpdateTokenEventHandler; + getToken: GetTokenEventHandler; tokensById: Record; tokensLoading: boolean; - updateTokenGroups: (groups: any[]) => void; - updateTokensHidden: (ids: string[], hideInSidebar: boolean) => void; + updateTokenGroups: UpdateTokenGroupsEventHandler; + updateTokensHidden: UpdateTokensHiddenEventHandler; }; const TokenDataContext = @@ -44,7 +61,7 @@ export function TokenDataProvider({ children }: { children: React.ReactNode }) { [database] ); - const [tokenGroups, setTokenGroups] = useState([]); + const [tokenGroups, setTokenGroups] = useState([]); useEffect(() => { async function updateTokenGroups() { const group = await database?.table("groups").get("tokens"); @@ -55,7 +72,7 @@ export function TokenDataProvider({ children }: { children: React.ReactNode }) { } }, [tokenGroupQuery, database]); - const getToken = useCallback( + const getToken = useCallback( async (tokenId) => { let token = await database?.table("tokens").get(tokenId); return token; @@ -64,7 +81,7 @@ export function TokenDataProvider({ children }: { children: React.ReactNode }) { ); // Add token and add it to the token group - const addToken = useCallback( + const addToken = useCallback( async (token) => { if (database) { await database.table("tokens").add(token); @@ -77,7 +94,7 @@ export function TokenDataProvider({ children }: { children: React.ReactNode }) { [database] ); - const removeTokens = useCallback( + const removeTokens = useCallback( async (ids) => { if (database) { const tokens = await database.table("tokens").bulkGet(ids); @@ -100,14 +117,14 @@ export function TokenDataProvider({ children }: { children: React.ReactNode }) { [database] ); - const updateToken = useCallback( + const updateToken = useCallback( async (id, update) => { await database?.table("tokens").update(id, update); }, [database] ); - const updateTokensHidden = useCallback( + const updateTokensHidden = useCallback( async (ids: string[], hideInSidebar: boolean) => { await Promise.all( ids.map((id) => database?.table("tokens").update(id, { hideInSidebar })) @@ -116,8 +133,8 @@ export function TokenDataProvider({ children }: { children: React.ReactNode }) { [database] ); - const updateTokenGroups = useCallback( - async (groups) => { + const updateTokenGroups = useCallback( + async (groups: Group[]) => { // Update group state immediately to avoid animation delay setTokenGroups(groups); await database?.table("groups").update("tokens", { items: groups }); diff --git a/src/hooks/useSetting.js b/src/hooks/useSetting.ts similarity index 68% rename from src/hooks/useSetting.js rename to src/hooks/useSetting.ts index f8553e7..293dc90 100644 --- a/src/hooks/useSetting.js +++ b/src/hooks/useSetting.ts @@ -5,14 +5,14 @@ import { useSettings } from "../contexts/SettingsContext"; /** * Helper to get and set nested settings that are saved in local storage - * @param {String} path The path to the setting within the Settings object provided by the SettingsContext + * @param {string} path The path to the setting within the Settings object provided by the SettingsContext */ -function useSetting(path) { +function useSetting(path: string): [Type, (value: Type) => void] { const { settings, setSettings } = useSettings(); - const setting = get(settings, path); + const setting = get(settings, path) as Type; - const setSetting = (value) => + const setSetting = (value: Type) => setSettings((prev) => { const updated = set({ ...prev }, path, value); return updated; diff --git a/src/modals/EditTokenModal.tsx b/src/modals/EditTokenModal.tsx index e830322..655c7fe 100644 --- a/src/modals/EditTokenModal.tsx +++ b/src/modals/EditTokenModal.tsx @@ -9,13 +9,16 @@ import TokenPreview from "../components/token/TokenPreview"; import { isEmpty } from "../helpers/shared"; import useResponsiveLayout from "../hooks/useResponsiveLayout"; + import { Token } from "../types/Token"; +import { UpdateTokenEventHandler } from "../contexts/TokenDataContext"; + type EditModalProps = { isOpen: boolean; onDone: () => void; token: Token; - onUpdateToken: (id: string, update: Partial) => void; + onUpdateToken: UpdateTokenEventHandler; }; function EditTokenModal({ diff --git a/src/modals/GameExpiredModal.tsx b/src/modals/GameExpiredModal.tsx index b71ee90..11d8228 100644 --- a/src/modals/GameExpiredModal.tsx +++ b/src/modals/GameExpiredModal.tsx @@ -2,9 +2,11 @@ import { Box, Label, Flex, Button, Text } from "theme-ui"; import Modal from "../components/Modal"; +import { RequestCloseEventHandler } from "../types/Events"; + type GameExpiredModalProps = { isOpen: boolean; - onRequestClose: () => void; + onRequestClose: RequestCloseEventHandler; }; function GameExpiredModal({ isOpen, onRequestClose }: GameExpiredModalProps) { diff --git a/src/modals/GettingStartedModal.tsx b/src/modals/GettingStartedModal.tsx index a24015a..019195e 100644 --- a/src/modals/GettingStartedModal.tsx +++ b/src/modals/GettingStartedModal.tsx @@ -5,11 +5,13 @@ import Modal from "../components/Modal"; import Markdown from "../components/Markdown"; import Link from "../components/Link"; +import { RequestCloseEventHandler } from "../types/Events"; + const gettingStarted = raw("../docs/howTo/gettingStarted.md"); type GettingStartedModalProps = { isOpen: boolean; - onRequestClose: () => void; + onRequestClose: RequestCloseEventHandler; }; function GettingStartedModal({ diff --git a/src/modals/GroupNameModal.tsx b/src/modals/GroupNameModal.tsx index 11b4ff4..556325d 100644 --- a/src/modals/GroupNameModal.tsx +++ b/src/modals/GroupNameModal.tsx @@ -3,11 +3,15 @@ import { Box, Input, Button, Label, Flex } from "theme-ui"; import Modal from "../components/Modal"; +import { RequestCloseEventHandler } from "../types/Events"; + +export type GroupNameEventHandler = (name: string) => void; + type GroupNameModalProps = { isOpen: boolean; - onRequestClose: () => void; + onRequestClose: RequestCloseEventHandler; name: string; - onSubmit: (name: string) => void; + onSubmit: GroupNameEventHandler; }; function GroupNameModal({ diff --git a/src/modals/ImportExportModal.tsx b/src/modals/ImportExportModal.tsx index 3c6a543..06d8b90 100644 --- a/src/modals/ImportExportModal.tsx +++ b/src/modals/ImportExportModal.tsx @@ -21,6 +21,7 @@ import { Map } from "../types/Map"; import { MapState } from "../types/MapState"; import { Token } from "../types/Token"; import { Group } from "../types/Group"; +import { RequestCloseEventHandler } from "../types/Events"; const importDBName = "OwlbearRodeoImportDB"; @@ -36,7 +37,7 @@ function ImportExportModal({ onRequestClose, }: { isOpen: boolean; - onRequestClose: () => void; + onRequestClose: RequestCloseEventHandler; }) { const { worker } = useDatabase(); const userId = useUserId(); diff --git a/src/modals/JoinModal.tsx b/src/modals/JoinModal.tsx index 0e79ec6..7ec0118 100644 --- a/src/modals/JoinModal.tsx +++ b/src/modals/JoinModal.tsx @@ -4,9 +4,11 @@ import { useHistory } from "react-router-dom"; import Modal from "../components/Modal"; +import { RequestCloseEventHandler } from "../types/Events"; + type JoinModalProps = { isOpen: boolean; - onRequestClose: () => void; + onRequestClose: RequestCloseEventHandler; }; function JoinModal({ isOpen, onRequestClose }: JoinModalProps) { diff --git a/src/modals/SelectDataModal.tsx b/src/modals/SelectDataModal.tsx index f5a99e5..ce3306f 100644 --- a/src/modals/SelectDataModal.tsx +++ b/src/modals/SelectDataModal.tsx @@ -12,21 +12,7 @@ import { Map } from "../types/Map"; import { Group, GroupContainer } from "../types/Group"; import { MapState } from "../types/MapState"; import { Token } from "../types/Token"; - -type SelectDataProps = { - isOpen: boolean; - onRequestClose: () => void; - onConfirm: ( - checkedMaps: SelectData[], - checkedTokens: SelectData[], - checkedMapGroups: Group[], - checkedTokenGroups: Group[] - ) => void; - confirmText: string; - label: string; - databaseName: string; - filter: (table: string, data: Map | MapState | Token, id: string) => boolean; -}; +import { RequestCloseEventHandler } from "../types/Events"; export type SelectData = { name: string; @@ -35,6 +21,23 @@ export type SelectData = { checked: boolean; }; +export type ConfirmDataEventHandler = ( + checkedMaps: SelectData[], + checkedTokens: SelectData[], + checkedMapGroups: Group[], + checkedTokenGroups: Group[] +) => void; + +type SelectDataProps = { + isOpen: boolean; + onRequestClose: RequestCloseEventHandler; + onConfirm: ConfirmDataEventHandler; + confirmText: string; + label: string; + databaseName: string; + filter: (table: string, data: Map | MapState | Token, id: string) => boolean; +}; + type DataRecord = Record; function SelectDataModal({ diff --git a/src/modals/SelectDiceModal.tsx b/src/modals/SelectDiceModal.tsx index 08f21bc..ce11c86 100644 --- a/src/modals/SelectDiceModal.tsx +++ b/src/modals/SelectDiceModal.tsx @@ -8,12 +8,16 @@ import { dice } from "../dice"; import useResponsiveLayout from "../hooks/useResponsiveLayout"; +import { + DiceSelectEventHandler, + RequestCloseEventHandler, +} from "../types/Events"; import { DefaultDice } from "../types/Dice"; type SelectDiceProps = { isOpen: boolean; - onRequestClose: () => void; - onDone: (dice: DefaultDice) => void; + onRequestClose: RequestCloseEventHandler; + onDone: DiceSelectEventHandler; defaultDice: DefaultDice; }; diff --git a/src/modals/SelectMapModal.tsx b/src/modals/SelectMapModal.tsx index 85ff501..b21a4f3 100644 --- a/src/modals/SelectMapModal.tsx +++ b/src/modals/SelectMapModal.tsx @@ -31,13 +31,17 @@ import { GroupProvider } from "../contexts/GroupContext"; import { TileDragProvider } from "../contexts/TileDragContext"; import { Map } from "../types/Map"; -import { MapState } from "../types/MapState"; +import { + MapChangeEventHandler, + MapResetEventHandler, + RequestCloseEventHandler, +} from "../types/Events"; type SelectMapProps = { isOpen: boolean; - onDone: () => void; - onMapChange: (map?: Map, mapState?: MapState) => void; - onMapReset: (newState: MapState) => void; + onDone: RequestCloseEventHandler; + onMapChange: MapChangeEventHandler; + onMapReset: MapResetEventHandler; currentMap?: Map; }; @@ -294,7 +298,7 @@ function SelectMapModal({ void; - onMapTokensStateCreate: (states: TokenState[]) => void; + onRequestClose: RequestCloseEventHandler; + onMapTokensStateCreate: MapTokensStateCreateHandler; }; function SelectTokensModal({ diff --git a/src/modals/SettingsModal.tsx b/src/modals/SettingsModal.tsx index ca026a4..394507a 100644 --- a/src/modals/SettingsModal.tsx +++ b/src/modals/SettingsModal.tsx @@ -21,24 +21,27 @@ import useSetting from "../hooks/useSetting"; import ConfirmModal from "./ConfirmModal"; import ImportExportModal from "./ImportExportModal"; -import { MapState } from "../components/map/Map"; + +import { MapState } from "../types/MapState"; +import { RequestCloseEventHandler } from "../types/Events"; function SettingsModal({ isOpen, onRequestClose, }: { isOpen: boolean; - onRequestClose: () => void; + onRequestClose: RequestCloseEventHandler; }) { const { database, databaseStatus } = useDatabase(); const userId = useUserId(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [labelSize, setLabelSize] = useSetting("map.labelSize"); - const [gridSnappingSensitivity, setGridSnappingSensitivity] = useSetting( - "map.gridSnappingSensitivity" - ); - const [showFogGuides, setShowFogGuides] = useSetting("fog.showGuides"); - const [fogEditOpacity, setFogEditOpacity] = useSetting("fog.editOpacity"); + const [labelSize, setLabelSize] = useSetting("map.labelSize"); + const [gridSnappingSensitivity, setGridSnappingSensitivity] = + useSetting("map.gridSnappingSensitivity"); + const [showFogGuides, setShowFogGuides] = + useSetting("fog.showGuides"); + const [fogEditOpacity, setFogEditOpacity] = + useSetting("fog.editOpacity"); const [storageEstimate, setStorageEstimate] = useState(); const [isImportExportModalOpen, setIsImportExportModalOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); diff --git a/src/modals/StartModal.tsx b/src/modals/StartModal.tsx index 58f0c70..e747810 100644 --- a/src/modals/StartModal.tsx +++ b/src/modals/StartModal.tsx @@ -9,7 +9,15 @@ import useSetting from "../hooks/useSetting"; import Modal from "../components/Modal"; -function StartModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void}) { +import { RequestCloseEventHandler } from "../types/Events"; + +function StartModal({ + isOpen, + onRequestClose, +}: { + isOpen: boolean; + onRequestClose: RequestCloseEventHandler; +}) { let history = useHistory(); const { password, setPassword } = useAuth(); @@ -17,7 +25,7 @@ function StartModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClos setPassword(event.target.value); } - const [usePassword, setUsePassword] = useSetting("game.usePassword"); + const [usePassword, setUsePassword] = useSetting("game.usePassword"); function handleUsePasswordChange(event: ChangeEvent) { setUsePassword(event.target.checked); } diff --git a/src/modals/StartStreamModal.tsx b/src/modals/StartStreamModal.tsx index 4ab6cb3..4a2b6e6 100644 --- a/src/modals/StartStreamModal.tsx +++ b/src/modals/StartStreamModal.tsx @@ -2,17 +2,22 @@ import { Box, Text, Button, Label, Flex } from "theme-ui"; import Modal from "../components/Modal"; +import { RequestCloseEventHandler } from "../types/Events"; + +export type StreamStartEventHandler = () => void; +export type StreamEndEventHandler = (stream: MediaStream) => void; + type StartStreamProps = { - isOpen: boolean, - onRequestClose: () => void, - isSupported: boolean, - unavailableMessage: JSX.Element, - stream: MediaStream, - noAudioTrack: boolean, - noAudioMessage: JSX.Element, - onStreamStart: any, - onStreamEnd: any, -} + isOpen: boolean; + onRequestClose: RequestCloseEventHandler; + isSupported: boolean; + unavailableMessage: JSX.Element; + stream: MediaStream; + noAudioTrack: boolean; + noAudioMessage: JSX.Element; + onStreamStart: StreamStartEventHandler; + onStreamEnd: StreamEndEventHandler; +}; function StartStreamModal({ isOpen, diff --git a/src/modals/StartTimerModal.tsx b/src/modals/StartTimerModal.tsx index d2a021f..78c09b7 100644 --- a/src/modals/StartTimerModal.tsx +++ b/src/modals/StartTimerModal.tsx @@ -7,13 +7,19 @@ import { getHMSDuration, getDurationHMS } from "../helpers/timer"; import useSetting from "../hooks/useSetting"; +import { RequestCloseEventHandler } from "../types/Events"; +import { Timer } from "../types/Timer"; + +export type TimerStartEventHandler = (event: Timer) => void; +export type TimerStopEventHandler = () => void; + type StartTimerProps = { - isOpen: boolean, - onRequestClose: () => void, - onTimerStart: any, - onTimerStop: any, - timer: any, -} + isOpen: boolean; + onRequestClose: RequestCloseEventHandler; + onTimerStart: TimerStartEventHandler; + onTimerStop: TimerStopEventHandler; + timer?: Timer; +}; function StartTimerModal({ isOpen, @@ -27,9 +33,9 @@ function StartTimerModal({ inputRef.current && inputRef.current.focus(); } - const [hour, setHour] = useSetting("timer.hour"); - const [minute, setMinute] = useSetting("timer.minute"); - const [second, setSecond] = useSetting("timer.second"); + const [hour, setHour] = useSetting("timer.hour"); + const [minute, setMinute] = useSetting("timer.minute"); + const [second, setSecond] = useSetting("timer.second"); function handleSubmit(event: ChangeEvent) { event.preventDefault(); @@ -85,10 +91,10 @@ function StartTimerModal({ setHour(parseValue(e.target.value, 24))} type="number" - disabled={timer} + disabled={!!timer} min={0} max={24} /> @@ -97,11 +103,11 @@ function StartTimerModal({ setMinute(parseValue(e.target.value, 59))} type="number" ref={inputRef} - disabled={timer} + disabled={!!timer} min={0} max={59} /> @@ -110,10 +116,10 @@ function StartTimerModal({ setSecond(parseValue(e.target.value, 59))} type="number" - disabled={timer} + disabled={!!timer} min={0} max={59} /> diff --git a/src/types/Events.ts b/src/types/Events.ts new file mode 100644 index 0000000..ee99c72 --- /dev/null +++ b/src/types/Events.ts @@ -0,0 +1,14 @@ +import { DefaultDice } from "./Dice"; +import { Map } from "./Map"; +import { MapState } from "./MapState"; +import { TokenState } from "./TokenState"; + +export type MapChangeEventHandler = (map?: Map, mapState?: MapState) => void; + +export type MapResetEventHandler = (newState: MapState) => void; + +export type DiceSelectEventHandler = (dice: DefaultDice) => void; + +export type RequestCloseEventHandler = () => void; + +export type MapTokensStateCreateHandler = (states: TokenState[]) => void; diff --git a/yarn.lock b/yarn.lock index f275668..ecef399 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2998,6 +2998,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.set@^4.3.6": + version "4.3.6" + resolved "https://registry.yarnpkg.com/@types/lodash.set/-/lodash.set-4.3.6.tgz#33e635c2323f855359225df6a5c8c6f1f1908264" + integrity sha512-ZeGDDlnRYTvS31Laij0RsSaguIUSBTYIlJFKL3vm3T2OAZAQj2YpSvVWJc0WiG4jqg9fGX6PAPGvDqBcHfSgFg== + dependencies: + "@types/lodash" "*" + "@types/lodash@*": version "4.14.170" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" From 68c1c6db0c0e615ff5a8a1e0081956d07816d813 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 13 Jul 2021 18:50:18 +1000 Subject: [PATCH 018/142] Typescript --- src/components/map/Map.tsx | 6 +- .../map/{MapDrawing.js => MapDrawing.tsx} | 170 ++++++++++++------ src/components/party/PartyState.ts | 39 ---- src/contexts/MapInteractionContext.tsx | 49 +++-- src/contexts/MapStageContext.tsx | 7 +- src/helpers/colors.ts | 1 + src/helpers/konva.tsx | 2 - ...useGridSnapping.js => useGridSnapping.tsx} | 13 +- src/routes/Game.tsx | 3 +- src/types/Drawing.ts | 19 +- 10 files changed, 186 insertions(+), 123 deletions(-) rename src/components/map/{MapDrawing.js => MapDrawing.tsx} (61%) delete mode 100644 src/components/party/PartyState.ts rename src/hooks/{useGridSnapping.js => useGridSnapping.tsx} (86%) diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index 01426e4..2f6618b 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -255,9 +255,9 @@ function Map({ const mapDrawing = ( diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.tsx similarity index 61% rename from src/components/map/MapDrawing.js rename to src/components/map/MapDrawing.tsx index ccce9fd..118f5e2 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import shortid from "shortid"; import { Group, Line, Rect, Circle } from "react-konva"; @@ -25,14 +25,34 @@ import { getRelativePointerPosition } from "../../helpers/konva"; import useGridSnapping from "../../hooks/useGridSnapping"; +import { Map } from "../../types/Map"; +import { + Drawing, + DrawingToolSettings, + drawingToolIsShape, + Shape, +} from "../../types/Drawing"; + +export type DrawingAddEventHanlder = (drawing: Drawing) => void; +export type DrawingsRemoveEventHandler = (drawingIds: string[]) => void; + +type MapDrawingProps = { + map: Map; + drawings: Drawing[]; + onDrawingAdd: DrawingAddEventHanlder; + onDrawingsRemove: DrawingsRemoveEventHandler; + active: boolean; + toolSettings: DrawingToolSettings; +}; + function MapDrawing({ map, - shapes, - onShapeAdd, - onShapesRemove, + drawings, + onDrawingAdd: onShapeAdd, + onDrawingsRemove: onShapesRemove, active, toolSettings, -}) { +}: MapDrawingProps) { const stageScale = useDebouncedStageScale(); const mapWidth = useMapWidth(); const mapHeight = useMapHeight(); @@ -42,11 +62,12 @@ function MapDrawing({ const gridStrokeWidth = useGridStrokeWidth(); const mapStageRef = useMapStage(); - const [drawingShape, setDrawingShape] = useState(null); + const [drawing, setDrawing] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); - const [erasingShapes, setErasingShapes] = useState([]); + const [erasingDrawings, setErasingDrawings] = useState([]); const shouldHover = toolSettings.type === "erase" && active; + const isBrush = toolSettings.type === "brush" || toolSettings.type === "paint"; const isShape = @@ -64,8 +85,14 @@ function MapDrawing({ const mapStage = mapStageRef.current; function getBrushPosition() { + if (!mapStage) { + return; + } const mapImage = mapStage.findOne("#mapImage"); let position = getRelativePointerPosition(mapImage); + if (!position) { + return; + } if (map.snapToGrid && isShape) { position = snapPositionToGrid(position); } @@ -77,36 +104,46 @@ function MapDrawing({ function handleBrushDown() { const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } const commonShapeData = { color: toolSettings.color, blend: toolSettings.useBlending, id: shortid.generate(), }; + const type = toolSettings.type; if (isBrush) { - setDrawingShape({ + setDrawing({ type: "path", - pathType: toolSettings.type === "brush" ? "stroke" : "fill", + pathType: type === "brush" ? "stroke" : "fill", data: { points: [brushPosition] }, - strokeWidth: toolSettings.type === "brush" ? 1 : 0, + strokeWidth: type === "brush" ? 1 : 0, ...commonShapeData, }); - } else if (isShape) { - setDrawingShape({ + } else if (isShape && drawingToolIsShape(type)) { + setDrawing({ type: "shape", - shapeType: toolSettings.type, - data: getDefaultShapeData(toolSettings.type, brushPosition), + shapeType: type, + data: getDefaultShapeData(type, brushPosition), strokeWidth: toolSettings.type === "line" ? 1 : 0, ...commonShapeData, - }); + } as Shape); } setIsBrushDown(true); } function handleBrushMove() { const brushPosition = getBrushPosition(); - if (isBrushDown && drawingShape) { + if (!brushPosition) { + return; + } + if (isBrushDown && drawing) { if (isBrush) { - setDrawingShape((prevShape) => { + setDrawing((prevShape) => { + if (prevShape?.type !== "path") { + return prevShape; + } const prevPoints = prevShape.data.points; if ( Vector2.compare( @@ -127,63 +164,68 @@ function MapDrawing({ }; }); } else if (isShape) { - setDrawingShape((prevShape) => ({ - ...prevShape, - data: getUpdatedShapeData( - prevShape.shapeType, - prevShape.data, - brushPosition, - gridCellNormalizedSize, - mapWidth, - mapHeight - ), - })); + setDrawing((prevShape) => { + if (prevShape?.type !== "shape") { + return prevShape; + } + return { + ...prevShape, + data: getUpdatedShapeData( + prevShape.shapeType, + prevShape.data, + brushPosition, + gridCellNormalizedSize, + mapWidth, + mapHeight + ), + } as Shape; + }); } } } function handleBrushUp() { - if (isBrush && drawingShape) { - if (drawingShape.data.points.length > 1) { - onShapeAdd(drawingShape); + if (isBrush && drawing && drawing.type === "path") { + if (drawing.data.points.length > 1) { + onShapeAdd(drawing); } - } else if (isShape && drawingShape) { - onShapeAdd(drawingShape); + } else if (isShape && drawing) { + onShapeAdd(drawing); } eraseHoveredShapes(); - setDrawingShape(null); + setDrawing(null); setIsBrushDown(false); } - interactionEmitter.on("dragStart", handleBrushDown); - interactionEmitter.on("drag", handleBrushMove); - interactionEmitter.on("dragEnd", handleBrushUp); + interactionEmitter?.on("dragStart", handleBrushDown); + interactionEmitter?.on("drag", handleBrushMove); + interactionEmitter?.on("dragEnd", handleBrushUp); return () => { - interactionEmitter.off("dragStart", handleBrushDown); - interactionEmitter.off("drag", handleBrushMove); - interactionEmitter.off("dragEnd", handleBrushUp); + interactionEmitter?.off("dragStart", handleBrushDown); + interactionEmitter?.off("drag", handleBrushMove); + interactionEmitter?.off("dragEnd", handleBrushUp); }; }); - function handleShapeOver(shape, isDown) { + function handleShapeOver(shape: Drawing, isDown: boolean) { if (shouldHover && isDown) { - if (erasingShapes.findIndex((s) => s.id === shape.id) === -1) { - setErasingShapes((prevShapes) => [...prevShapes, shape]); + if (erasingDrawings.findIndex((s) => s.id === shape.id) === -1) { + setErasingDrawings((prevShapes) => [...prevShapes, shape]); } } } function eraseHoveredShapes() { - if (erasingShapes.length > 0) { - onShapesRemove(erasingShapes.map((shape) => shape.id)); - setErasingShapes([]); + if (erasingDrawings.length > 0) { + onShapesRemove(erasingDrawings.map((shape) => shape.id)); + setErasingDrawings([]); } } - function renderShape(shape) { + function renderDrawing(shape: Drawing) { const defaultProps = { key: shape.id, onMouseMove: () => handleShapeOver(shape, isBrushDown), @@ -200,7 +242,11 @@ function MapDrawing({ return ( [...acc, point.x * mapWidth, point.y * mapHeight], + (acc: number[], point) => [ + ...acc, + point.x * mapWidth, + point.y * mapHeight, + ], [] )} stroke={colors[shape.color] || shape.color} @@ -238,7 +284,11 @@ function MapDrawing({ return ( [...acc, point.x * mapWidth, point.y * mapHeight], + (acc: number[], point) => [ + ...acc, + point.x * mapWidth, + point.y * mapHeight, + ], [] )} closed={true} @@ -249,7 +299,11 @@ function MapDrawing({ return ( [...acc, point.x * mapWidth, point.y * mapHeight], + (acc: number[], point) => [ + ...acc, + point.x * mapWidth, + point.y * mapHeight, + ], [] )} strokeWidth={gridStrokeWidth * shape.strokeWidth} @@ -262,19 +316,19 @@ function MapDrawing({ } } - function renderErasingShape(shape) { - const eraseShape = { - ...shape, - color: "#BB99FF", + function renderErasingDrawing(drawing: Drawing) { + const eraseShape: Drawing = { + ...drawing, + color: "primary", }; - return renderShape(eraseShape); + return renderDrawing(eraseShape); } return ( - {shapes.map(renderShape)} - {drawingShape && renderShape(drawingShape)} - {erasingShapes.length > 0 && erasingShapes.map(renderErasingShape)} + {drawings.map(renderDrawing)} + {drawing && renderDrawing(drawing)} + {erasingDrawings.length > 0 && erasingDrawings.map(renderErasingDrawing)} ); } diff --git a/src/components/party/PartyState.ts b/src/components/party/PartyState.ts deleted file mode 100644 index 194de30..0000000 --- a/src/components/party/PartyState.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @typedef {object} Timer - * @property {number} current - * @property {number} max - */ -export type Timer = { - current: number, - max: number -} - -/** - * @typedef {object} PlayerDice - * @property {boolean} share - * @property {[]} rolls - */ -export type PlayerDice = { share: boolean, rolls: [] } - -/** - * @typedef {object} PlayerInfo - * @property {string} nickname - * @property {Timer | null} timer - * @property {PlayerDice} dice - * @property {string} sessionId - * @property {string} userId - */ -export type PlayerInfo = { - nickname: string, - timer: Timer | null, - dice: PlayerDice, - sessionId: string, - userId: string -} - -/** - * @typedef {object} PartyState - * @property {string} player - * @property {PlayerInfo} playerInfo - */ -export type PartyState = { [player: string]: PlayerInfo } \ No newline at end of file diff --git a/src/contexts/MapInteractionContext.tsx b/src/contexts/MapInteractionContext.tsx index 1c3364b..7362c86 100644 --- a/src/contexts/MapInteractionContext.tsx +++ b/src/contexts/MapInteractionContext.tsx @@ -1,16 +1,45 @@ -import React, { ReactChild, useContext } from "react"; +import React, { useContext } from "react"; +import { EventEmitter } from "stream"; import useDebounce from "../hooks/useDebounce"; -export const StageScaleContext = React.createContext(undefined) as any; -export const DebouncedStageScaleContext = React.createContext(undefined) as any; -export const StageWidthContext = React.createContext(undefined) as any; -export const StageHeightContext = React.createContext(undefined) as any; -export const SetPreventMapInteractionContext = React.createContext(undefined) as any; -export const MapWidthContext = React.createContext(undefined) as any; -export const MapHeightContext = React.createContext(undefined) as any; -export const InteractionEmitterContext = React.createContext(undefined) as any; +type MapInteraction = { + stageScale: number; + stageWidth: number; + stageHeight: number; + setPreventMapInteraction: React.Dispatch>; + mapWidth: number; + mapHeight: number; + interactionEmitter: EventEmitter | null; +}; -export function MapInteractionProvider({ value, children }: { value: any, children: ReactChild[]}) { +export const StageScaleContext = + React.createContext(undefined); +export const DebouncedStageScaleContext = + React.createContext(undefined); +export const StageWidthContext = + React.createContext(undefined); +export const StageHeightContext = + React.createContext(undefined); +export const SetPreventMapInteractionContext = + React.createContext( + undefined + ); +export const MapWidthContext = + React.createContext(undefined); +export const MapHeightContext = + React.createContext(undefined); +export const InteractionEmitterContext = + React.createContext( + undefined + ); + +export function MapInteractionProvider({ + value, + children, +}: { + value: MapInteraction; + children: React.ReactNode; +}) { const { stageScale, stageWidth, diff --git a/src/contexts/MapStageContext.tsx b/src/contexts/MapStageContext.tsx index 8f9a30b..684b297 100644 --- a/src/contexts/MapStageContext.tsx +++ b/src/contexts/MapStageContext.tsx @@ -1,7 +1,10 @@ import React, { useContext } from "react"; +import { Stage } from "konva/types/Stage"; -const MapStageContext = React.createContext({ current: null }); -export const MapStageProvider: any = MapStageContext.Provider; +type MapStage = React.MutableRefObject; + +const MapStageContext = React.createContext(undefined); +export const MapStageProvider = MapStageContext.Provider; export function useMapStage() { const context = useContext(MapStageContext); diff --git a/src/helpers/colors.ts b/src/helpers/colors.ts index a89fd18..946aae2 100644 --- a/src/helpers/colors.ts +++ b/src/helpers/colors.ts @@ -12,6 +12,7 @@ const colors = { darkGray: "rgb(90, 90, 90)", lightGray: "rgb(179, 179, 179)", white: "rgb(255, 255, 255)", + primary: "hsl(260, 100%, 80%)", }; export type Colors = typeof colors; diff --git a/src/helpers/konva.tsx b/src/helpers/konva.tsx index d26d959..7eb8edc 100644 --- a/src/helpers/konva.tsx +++ b/src/helpers/konva.tsx @@ -331,10 +331,8 @@ export function getRelativePointerPosition( ): { x: number; y: number } | undefined { let transform = node.getAbsoluteTransform().copy(); transform.invert(); - // TODO: handle possible null value let position = node.getStage()?.getPointerPosition(); if (!position) { - // TODO: handle possible null value return; } return transform.point(position); diff --git a/src/hooks/useGridSnapping.js b/src/hooks/useGridSnapping.tsx similarity index 86% rename from src/hooks/useGridSnapping.js rename to src/hooks/useGridSnapping.tsx index aa52040..3fc68a5 100644 --- a/src/hooks/useGridSnapping.js +++ b/src/hooks/useGridSnapping.tsx @@ -19,11 +19,14 @@ import { * @param {number=} snappingSensitivity 1 = Always snap, 0 = never snap if undefined the default user setting will be used * @param {boolean=} useCorners Snap to grid cell corners */ -function useGridSnapping(snappingSensitivity, useCorners = true) { - const [defaultSnappingSensitivity] = useSetting( +function useGridSnapping( + snappingSensitivity: number | undefined = undefined, + useCorners: boolean = true +) { + const [defaultSnappingSensitivity] = useSetting( "map.gridSnappingSensitivity" ); - snappingSensitivity = + let gridSnappingSensitivity = snappingSensitivity === undefined ? defaultSnappingSensitivity : snappingSensitivity; @@ -36,7 +39,7 @@ function useGridSnapping(snappingSensitivity, useCorners = true) { /** * @param {Vector2} node The node to snap */ - function snapPositionToGrid(position) { + function snapPositionToGrid(position: Vector2) { // Account for grid offset let offsetPosition = Vector2.subtract( Vector2.subtract(position, gridOffset), @@ -70,7 +73,7 @@ function useGridSnapping(snappingSensitivity, useCorners = true) { const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint); if ( distanceToSnapPoint < - Vector2.min(gridCellPixelSize) * snappingSensitivity + (Vector2.min(gridCellPixelSize) as number) * gridSnappingSensitivity ) { // Reverse grid offset let offsetSnapPoint = Vector2.add( diff --git a/src/routes/Game.tsx b/src/routes/Game.tsx index 2301c56..8111021 100644 --- a/src/routes/Game.tsx +++ b/src/routes/Game.tsx @@ -28,6 +28,7 @@ import NetworkedMapAndTokens from "../network/NetworkedMapAndTokens"; import NetworkedParty from "../network/NetworkedParty"; import Session from "../network/Session"; +import { Stage } from "konva/types/Stage"; function Game() { const { id: gameId }: { id: string } = useParams(); @@ -110,7 +111,7 @@ function Game() { // A ref to the Konva stage // the ref will be assigned in the MapInteraction component - const mapStageRef: React.MutableRefObject = useRef(); + const mapStageRef = useRef(null); return ( diff --git a/src/types/Drawing.ts b/src/types/Drawing.ts index 868bc4e..3ad60f7 100644 --- a/src/types/Drawing.ts +++ b/src/types/Drawing.ts @@ -37,7 +37,7 @@ export type ShapeData = PointsData | RectData | CircleData; export type BaseDrawing = { blend: boolean; - color: string; + color: Color; id: string; strokeWidth: number; }; @@ -46,8 +46,6 @@ export type BaseShape = BaseDrawing & { type: "shape"; }; -export type ShapeType = "line" | "rectangle" | "circle" | "triangle"; - export type Line = BaseShape & { shapeType: "line"; data: PointsData; @@ -68,6 +66,12 @@ export type Triangle = BaseShape & { data: PointsData; }; +export type ShapeType = + | Line["shapeType"] + | Rectangle["shapeType"] + | Circle["shapeType"] + | Triangle["shapeType"]; + export type Shape = Line | Rectangle | Circle | Triangle; export type Path = BaseDrawing & { @@ -77,3 +81,12 @@ export type Path = BaseDrawing & { }; export type Drawing = Shape | Path; + +export function drawingToolIsShape(type: DrawingToolType): type is ShapeType { + return ( + type === "line" || + type === "rectangle" || + type === "circle" || + type === "triangle" + ); +} From d80bfa2f1e9c61db70cf457c2c0435ea76350530 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 16 Jul 2021 14:55:33 +1000 Subject: [PATCH 019/142] typescript --- src/actions/{Action.js => Action.ts} | 29 +-- src/actions/AddShapeAction.js | 15 -- src/actions/AddStatesAction.ts | 21 +++ src/actions/CutFogAction.ts | 41 ++++ src/actions/CutShapeAction.js | 38 ---- src/actions/EditShapeAction.js | 17 -- src/actions/EditStatesAction.ts | 23 +++ src/actions/RemoveShapeAction.js | 13 -- src/actions/RemoveStatesAction.ts | 21 +++ src/actions/SubtractFogAction.ts | 32 ++++ src/actions/SubtractShapeAction.js | 32 ---- src/actions/index.js | 13 -- src/actions/index.ts | 13 ++ src/components/Slider.tsx | 2 +- src/components/banner/Banner.tsx | 15 +- src/components/banner/ErrorBanner.tsx | 8 +- src/components/dice/DiceInteraction.tsx | 2 +- src/components/map/Map.tsx | 119 ++++++------ src/components/map/MapControls.tsx | 93 ++++++---- .../map/{MapEditor.js => MapEditor.tsx} | 45 +++-- src/components/map/{MapFog.js => MapFog.tsx} | 67 ++++++- .../map/{MapGrid.js => MapGrid.tsx} | 10 +- .../{MapGridEditor.js => MapGridEditor.tsx} | 31 +++- src/components/map/SelectMapButton.tsx | 18 +- src/components/party/ChangeNicknameButton.tsx | 8 +- src/components/party/DiceRoll.tsx | 14 +- src/components/party/DiceRolls.tsx | 48 ++--- src/components/party/DiceTrayButton.tsx | 7 +- src/components/party/Nickname.tsx | 10 +- src/components/party/Party.tsx | 33 +++- src/components/party/StartStreamButton.tsx | 18 +- src/components/party/StartTimerButton.tsx | 10 +- src/components/party/Stream.tsx | 22 ++- src/components/party/Timer.tsx | 6 +- src/contexts/AssetsContext.tsx | 18 +- src/contexts/DatabaseContext.tsx | 14 +- src/contexts/DiceLoadingContext.tsx | 16 +- src/contexts/GridContext.tsx | 34 ++-- src/contexts/GroupContext.tsx | 175 ++++++++++-------- src/contexts/ImageSourceContext.tsx | 158 ---------------- src/contexts/MapDataContext.tsx | 5 +- src/contexts/PartyContext.tsx | 10 +- src/contexts/PlayerContext.tsx | 36 ++-- src/contexts/SettingsContext.tsx | 2 +- src/contexts/TileDragContext.tsx | 31 +++- src/contexts/TokenDataContext.tsx | 2 +- src/database.ts | 16 +- src/dice/Dice.ts | 69 ++++--- src/dice/diceTray/DiceTray.ts | 57 +++--- src/dice/galaxy/GalaxyDice.ts | 9 +- src/dice/gemstone/GemstoneDice.ts | 16 +- src/dice/glass/GlassDice.ts | 24 ++- src/dice/iron/IronDice.ts | 10 +- src/dice/nebula/NebulaDice.ts | 8 +- src/dice/sunrise/SunriseDice.ts | 8 +- src/dice/sunset/SunsetDice.ts | 8 +- src/dice/walnut/WalnutDice.ts | 8 +- src/global.d.ts | 20 +- src/helpers/FakeStorage.ts | 4 +- src/helpers/Vector2.ts | 51 +++-- src/helpers/actions.ts | 36 +++- src/helpers/blobToBuffer.ts | 10 +- src/helpers/dexie.js | 48 ----- src/helpers/dice.ts | 18 +- src/helpers/diff.ts | 6 +- src/helpers/drawing.ts | 13 +- src/helpers/grid.ts | 45 +++-- src/helpers/group.ts | 6 +- src/helpers/konva.tsx | 73 ++++---- src/helpers/logging.ts | 2 +- src/helpers/map.ts | 4 +- src/helpers/select.tsx | 146 --------------- src/helpers/shared.ts | 12 +- src/helpers/timer.ts | 18 +- src/hooks/useGridSnapping.tsx | 2 +- .../{useImageCenter.js => useImageCenter.tsx} | 16 +- src/hooks/{useMapImage.js => useMapImage.tsx} | 0 src/hooks/useNetworkedState.tsx | 44 +++-- src/ml/gridSize/GridSizeModel.ts | 6 +- src/modals/AddPartyMemberModal.tsx | 2 +- src/modals/ChangeNicknameModal.tsx | 6 +- src/modals/EditMapModal.tsx | 51 +++-- src/modals/EditTokenModal.tsx | 3 +- src/modals/ImportExportModal.tsx | 14 +- src/modals/SelectMapModal.tsx | 2 +- src/modals/SelectTokensModal.tsx | 2 +- src/modals/StartModal.tsx | 2 +- src/modals/StartTimerModal.tsx | 2 +- src/network/Connection.ts | 22 +-- src/network/NetworkedMapAndTokens.tsx | 174 ++++++++++------- src/network/NetworkedMapPointer.tsx | 24 +-- src/network/Session.ts | 22 ++- src/routes/Donate.tsx | 23 +-- src/routes/Game.tsx | 7 +- src/types/Action.ts | 5 + src/types/Asset.ts | 4 + src/types/Dice.ts | 24 ++- src/types/Drawing.ts | 2 + src/types/Fog.ts | 2 + src/types/Map.ts | 18 +- src/types/MapState.ts | 4 +- src/{upgrade.js => upgrade.ts} | 101 +++++----- src/workers/DatabaseWorker.ts | 14 +- 103 files changed, 1402 insertions(+), 1336 deletions(-) rename src/actions/{Action.js => Action.ts} (67%) delete mode 100644 src/actions/AddShapeAction.js create mode 100644 src/actions/AddStatesAction.ts create mode 100644 src/actions/CutFogAction.ts delete mode 100644 src/actions/CutShapeAction.js delete mode 100644 src/actions/EditShapeAction.js create mode 100644 src/actions/EditStatesAction.ts delete mode 100644 src/actions/RemoveShapeAction.js create mode 100644 src/actions/RemoveStatesAction.ts create mode 100644 src/actions/SubtractFogAction.ts delete mode 100644 src/actions/SubtractShapeAction.js delete mode 100644 src/actions/index.js create mode 100644 src/actions/index.ts rename src/components/map/{MapEditor.js => MapEditor.tsx} (85%) rename src/components/map/{MapFog.js => MapFog.tsx} (91%) rename src/components/map/{MapGrid.js => MapGrid.tsx} (74%) rename src/components/map/{MapGridEditor.js => MapGridEditor.tsx} (89%) delete mode 100644 src/contexts/ImageSourceContext.tsx delete mode 100644 src/helpers/dexie.js delete mode 100644 src/helpers/select.tsx rename src/hooks/{useImageCenter.js => useImageCenter.tsx} (88%) rename src/hooks/{useMapImage.js => useMapImage.tsx} (100%) create mode 100644 src/types/Action.ts rename src/{upgrade.js => upgrade.ts} (92%) diff --git a/src/actions/Action.js b/src/actions/Action.ts similarity index 67% rename from src/actions/Action.js rename to src/actions/Action.ts index a147f2b..a6f12fe 100644 --- a/src/actions/Action.js +++ b/src/actions/Action.ts @@ -1,40 +1,31 @@ -// Load Diff for auto complete -// eslint-disable-next-line no-unused-vars import { Diff } from "deep-diff"; import { diff, revertChanges } from "../helpers/diff"; import cloneDeep from "lodash.clonedeep"; -/** - * @callback ActionUpdate - * @param {any} state - */ - /** * Implementation of the Command Pattern * Wraps an update function with internal state to support undo */ -class Action { +class Action { /** * The update function called with the current state and should return the updated state * This is implemented in the child class - * - * @type {ActionUpdate} */ - update; + update(state: State): State { + return state; + } /** * The changes caused by the last state update - * @type {Diff} */ - changes; + changes: Diff[] | undefined; /** * Executes the action update on the state - * @param {any} state The current state to update - * @returns {any} The updated state + * @param {State} state The current state to update */ - execute(state) { + execute(state: State): State { if (state && this.update) { let newState = this.update(cloneDeep(state)); this.changes = diff(state, newState); @@ -45,10 +36,10 @@ class Action { /** * Reverts the changes caused by the last call of `execute` - * @param {any} state The current state to perform the undo on - * @returns {any} The state with the last changes reverted + * @param {State} state The current state to perform the undo on + * @returns {State} The state with the last changes reverted */ - undo(state) { + undo(state: State): State { if (state && this.changes) { let revertedState = cloneDeep(state); revertChanges(revertedState, this.changes); diff --git a/src/actions/AddShapeAction.js b/src/actions/AddShapeAction.js deleted file mode 100644 index 5147d05..0000000 --- a/src/actions/AddShapeAction.js +++ /dev/null @@ -1,15 +0,0 @@ -import Action from "./Action"; - -class AddShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - for (let shape of shapes) { - shapesById[shape.id] = shape; - } - return shapesById; - }; - } -} - -export default AddShapeAction; diff --git a/src/actions/AddStatesAction.ts b/src/actions/AddStatesAction.ts new file mode 100644 index 0000000..14d9053 --- /dev/null +++ b/src/actions/AddStatesAction.ts @@ -0,0 +1,21 @@ +import Action from "./Action"; + +import { ID } from "../types/Action"; + +class AddStatesAction extends Action> { + states: State[]; + + constructor(states: State[]) { + super(); + this.states = states; + } + + update(statesById: Record) { + for (let state of this.states) { + statesById[state.id] = state; + } + return statesById; + } +} + +export default AddStatesAction; diff --git a/src/actions/CutFogAction.ts b/src/actions/CutFogAction.ts new file mode 100644 index 0000000..779a921 --- /dev/null +++ b/src/actions/CutFogAction.ts @@ -0,0 +1,41 @@ +import polygonClipping from "polygon-clipping"; + +import Action from "./Action"; +import { + addPolygonDifferenceToFog, + addPolygonIntersectionToFog, + fogToGeometry, +} from "../helpers/actions"; + +import { Fog, FogState } from "../types/Fog"; + +class CutFogAction extends Action { + fogs: Fog[]; + + constructor(fog: Fog[]) { + super(); + this.fogs = fog; + } + + update(fogsById: FogState): FogState { + let actionGeom = this.fogs.map(fogToGeometry); + let cutFogs: FogState = {}; + for (let fog of Object.values(fogsById)) { + const fogGeom = fogToGeometry(fog); + try { + const difference = polygonClipping.difference(fogGeom, ...actionGeom); + const intersection = polygonClipping.intersection( + fogGeom, + ...actionGeom + ); + addPolygonDifferenceToFog(fog, difference, cutFogs); + addPolygonIntersectionToFog(fog, intersection, cutFogs); + } catch { + console.error("Unable to find intersection for fogs"); + } + } + return cutFogs; + } +} + +export default CutFogAction; diff --git a/src/actions/CutShapeAction.js b/src/actions/CutShapeAction.js deleted file mode 100644 index 59688e6..0000000 --- a/src/actions/CutShapeAction.js +++ /dev/null @@ -1,38 +0,0 @@ -import polygonClipping from "polygon-clipping"; - -import Action from "./Action"; -import { - addPolygonDifferenceToShapes, - addPolygonIntersectionToShapes, - shapeToGeometry, -} from "../helpers/actions"; - -class CutShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - let actionGeom = shapes.map(shapeToGeometry); - let cutShapes = {}; - for (let shape of Object.values(shapesById)) { - const shapeGeom = shapeToGeometry(shape); - try { - const difference = polygonClipping.difference( - shapeGeom, - ...actionGeom - ); - const intersection = polygonClipping.intersection( - shapeGeom, - ...actionGeom - ); - addPolygonDifferenceToShapes(shape, difference, cutShapes); - addPolygonIntersectionToShapes(shape, intersection, cutShapes); - } catch { - console.error("Unable to find intersection for shapes"); - } - } - return cutShapes; - }; - } -} - -export default CutShapeAction; diff --git a/src/actions/EditShapeAction.js b/src/actions/EditShapeAction.js deleted file mode 100644 index e531df5..0000000 --- a/src/actions/EditShapeAction.js +++ /dev/null @@ -1,17 +0,0 @@ -import Action from "./Action"; - -class EditShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - for (let edit of shapes) { - if (edit.id in shapesById) { - shapesById[edit.id] = { ...shapesById[edit.id], ...edit }; - } - } - return shapesById; - }; - } -} - -export default EditShapeAction; diff --git a/src/actions/EditStatesAction.ts b/src/actions/EditStatesAction.ts new file mode 100644 index 0000000..247828c --- /dev/null +++ b/src/actions/EditStatesAction.ts @@ -0,0 +1,23 @@ +import Action from "./Action"; + +import { ID } from "../types/Action"; + +class EditStatesAction extends Action> { + edits: Partial[]; + + constructor(edits: Partial[]) { + super(); + this.edits = edits; + } + + update(statesById: Record) { + for (let edit of this.edits) { + if (edit.id !== undefined && edit.id in statesById) { + statesById[edit.id] = { ...statesById[edit.id], ...edit }; + } + } + return statesById; + } +} + +export default EditStatesAction; diff --git a/src/actions/RemoveShapeAction.js b/src/actions/RemoveShapeAction.js deleted file mode 100644 index baa2df0..0000000 --- a/src/actions/RemoveShapeAction.js +++ /dev/null @@ -1,13 +0,0 @@ -import Action from "./Action"; -import { omit } from "../helpers/shared"; - -class RemoveShapeAction extends Action { - constructor(shapeIds) { - super(); - this.update = (shapesById) => { - return omit(shapesById, shapeIds); - }; - } -} - -export default RemoveShapeAction; diff --git a/src/actions/RemoveStatesAction.ts b/src/actions/RemoveStatesAction.ts new file mode 100644 index 0000000..aa422ad --- /dev/null +++ b/src/actions/RemoveStatesAction.ts @@ -0,0 +1,21 @@ +import Action from "./Action"; +import { omit } from "../helpers/shared"; + +import { ID } from "../types/Action"; + +class RemoveStatesAction extends Action< + Record +> { + stateIds: string[]; + + constructor(stateIds: string[]) { + super(); + this.stateIds = stateIds; + } + + update(statesById: Record) { + return omit(statesById, this.stateIds); + } +} + +export default RemoveStatesAction; diff --git a/src/actions/SubtractFogAction.ts b/src/actions/SubtractFogAction.ts new file mode 100644 index 0000000..9fa6ff7 --- /dev/null +++ b/src/actions/SubtractFogAction.ts @@ -0,0 +1,32 @@ +import polygonClipping from "polygon-clipping"; + +import Action from "./Action"; +import { addPolygonDifferenceToFog, fogToGeometry } from "../helpers/actions"; + +import { Fog, FogState } from "../types/Fog"; + +class SubtractFogAction extends Action { + fogs: Fog[]; + + constructor(fogs: Fog[]) { + super(); + this.fogs = fogs; + } + + update(fogsById: FogState): FogState { + const actionGeom = this.fogs.map(fogToGeometry); + let subtractedFogs: FogState = {}; + for (let fog of Object.values(fogsById)) { + const fogGeom = fogToGeometry(fog); + try { + const difference = polygonClipping.difference(fogGeom, ...actionGeom); + addPolygonDifferenceToFog(fog, difference, subtractedFogs); + } catch { + console.error("Unable to find difference for fogs"); + } + } + return subtractedFogs; + } +} + +export default SubtractFogAction; diff --git a/src/actions/SubtractShapeAction.js b/src/actions/SubtractShapeAction.js deleted file mode 100644 index 13f915a..0000000 --- a/src/actions/SubtractShapeAction.js +++ /dev/null @@ -1,32 +0,0 @@ -import polygonClipping from "polygon-clipping"; - -import Action from "./Action"; -import { - addPolygonDifferenceToShapes, - shapeToGeometry, -} from "../helpers/actions"; - -class SubtractShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - const actionGeom = shapes.map(shapeToGeometry); - let subtractedShapes = {}; - for (let shape of Object.values(shapesById)) { - const shapeGeom = shapeToGeometry(shape); - try { - const difference = polygonClipping.difference( - shapeGeom, - ...actionGeom - ); - addPolygonDifferenceToShapes(shape, difference, subtractedShapes); - } catch { - console.error("Unable to find difference for shapes"); - } - } - return subtractedShapes; - }; - } -} - -export default SubtractShapeAction; diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index 7822cc1..0000000 --- a/src/actions/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import AddShapeAction from "./AddShapeAction"; -import CutShapeAction from "./CutShapeAction"; -import EditShapeAction from "./EditShapeAction"; -import RemoveShapeAction from "./RemoveShapeAction"; -import SubtractShapeAction from "./SubtractShapeAction"; - -export { - AddShapeAction, - CutShapeAction, - EditShapeAction, - RemoveShapeAction, - SubtractShapeAction, -}; diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000..4b37526 --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,13 @@ +import AddStatesAction from "./AddStatesAction"; +import CutFogAction from "./CutFogAction"; +import EditStatesAction from "./EditStatesAction"; +import RemoveStatesAction from "./RemoveStatesAction"; +import SubtractFogAction from "./SubtractFogAction"; + +export { + AddStatesAction, + CutFogAction, + EditStatesAction, + RemoveStatesAction, + SubtractFogAction, +}; diff --git a/src/components/Slider.tsx b/src/components/Slider.tsx index 96785c7..9a28e28 100644 --- a/src/components/Slider.tsx +++ b/src/components/Slider.tsx @@ -78,7 +78,7 @@ Slider.defaultProps = { value: 0, ml: 0, mr: 0, - labelFunc: (value: any) => value, + labelFunc: (value: number) => value, }; export default Slider; diff --git a/src/components/banner/Banner.tsx b/src/components/banner/Banner.tsx index 5e8eb08..66278da 100644 --- a/src/components/banner/Banner.tsx +++ b/src/components/banner/Banner.tsx @@ -1,5 +1,7 @@ import Modal from "react-modal"; import { useThemeUI, Close } from "theme-ui"; +import { RequestCloseEventHandler } from "../../types/Events"; +import CSS from "csstype"; function Banner({ isOpen, @@ -8,11 +10,11 @@ function Banner({ allowClose, backgroundColor, }: { - isOpen: boolean, - onRequestClose: any, - children: any, - allowClose: boolean, - backgroundColor?: any + isOpen: boolean; + onRequestClose: RequestCloseEventHandler; + children: React.ReactNode; + allowClose: boolean; + backgroundColor?: CSS.Property.Color; }) { const { theme } = useThemeUI(); @@ -23,7 +25,8 @@ function Banner({ style={{ overlay: { bottom: "0", top: "initial", zIndex: 2000 }, content: { - backgroundColor: backgroundColor || theme.colors?.highlight, + backgroundColor: + backgroundColor || (theme.colors?.highlight as CSS.Property.Color), color: "hsl(210, 50%, 96%)", top: "initial", left: "50%", diff --git a/src/components/banner/ErrorBanner.tsx b/src/components/banner/ErrorBanner.tsx index cdfac99..1dbedff 100644 --- a/src/components/banner/ErrorBanner.tsx +++ b/src/components/banner/ErrorBanner.tsx @@ -2,7 +2,13 @@ import { Box, Text } from "theme-ui"; import Banner from "./Banner"; -function ErrorBanner({ error, onRequestClose }: { error: Error | undefined, onRequestClose: any }) { +function ErrorBanner({ + error, + onRequestClose, +}: { + error: Error | undefined; + onRequestClose; +}) { return ( diff --git a/src/components/dice/DiceInteraction.tsx b/src/components/dice/DiceInteraction.tsx index 8fd9d50..7b7fb68 100644 --- a/src/components/dice/DiceInteraction.tsx +++ b/src/components/dice/DiceInteraction.tsx @@ -35,7 +35,7 @@ type DiceInteractionProps = { canvas: HTMLCanvasElement | WebGLRenderingContext; }) => void; onPointerDown: () => void; - onPointerUp: () => any; + onPointerUp: () => void; }; function DiceInteraction({ diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index 2f6618b..e9c84e7 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -21,12 +21,21 @@ import NoteMenu from "../note/NoteMenu"; import NoteDragOverlay from "../note/NoteDragOverlay"; import { - AddShapeAction, - CutShapeAction, - EditShapeAction, - RemoveShapeAction, + AddStatesAction, + CutFogAction, + EditStatesAction, + RemoveStatesAction, } from "../../actions"; import Session from "../../network/Session"; +import { Drawing } from "../../types/Drawing"; +import { Fog } from "../../types/Fog"; +import { Map, MapToolId } from "../../types/Map"; +import { MapState } from "../../types/MapState"; +import { Settings } from "../../types/Settings"; +import { + MapChangeEventHandler, + MapResetEventHandler, +} from "../../types/Events"; function Map({ map, @@ -51,43 +60,39 @@ function Map({ disabledTokens, session, }: { - map: any; + map: Map; mapState: MapState; - mapActions: any; - onMapTokenStateChange: any; - onMapTokenStateRemove: any; - onMapChange: any; - onMapReset: any; - onMapDraw: any; - onMapDrawUndo: any; - onMapDrawRedo: any; - onFogDraw: any; - onFogDrawUndo: any; - onFogDrawRedo: any; - onMapNoteChange: any; - onMapNoteRemove: any; + mapActions: ; + onMapTokenStateChange: ; + onMapTokenStateRemove: ; + onMapChange: MapChangeEventHandler; + onMapReset: MapResetEventHandler; + onMapDraw: ; + onMapDrawUndo: ; + onMapDrawRedo: ; + onFogDraw: ; + onFogDrawUndo: ; + onFogDrawRedo: ; + onMapNoteChange: ; + onMapNoteRemove: ; allowMapDrawing: boolean; allowFogDrawing: boolean; allowMapChange: boolean; allowNoteEditing: boolean; - disabledTokens: any; + disabledTokens: ; session: Session; }) { const { addToast } = useToasts(); const { tokensById } = useTokenData(); - const [selectedToolId, setSelectedToolId] = useState("move"); - const { settings, setSettings }: { settings: any; setSettings: any } = - useSettings(); + const [selectedToolId, setSelectedToolId] = useState("move"); + const { settings, setSettings } = useSettings(); - function handleToolSettingChange(tool: any, change: any) { - setSettings((prevSettings: any) => ({ + function handleToolSettingChange(change: Partial) { + setSettings((prevSettings) => ({ ...prevSettings, - [tool]: { - ...prevSettings[tool], - ...change, - }, + ...change, })); } @@ -96,7 +101,7 @@ function Map({ function handleToolAction(action: string) { if (action === "eraseAll") { - onMapDraw(new RemoveShapeAction(drawShapes.map((s) => s.id))); + onMapDraw(new RemoveStatesAction(drawShapes.map((s) => s.id))); } if (action === "mapUndo") { onMapDrawUndo(); @@ -112,28 +117,28 @@ function Map({ } } - function handleMapShapeAdd(shape: Shape) { - onMapDraw(new AddShapeAction([shape])); + function handleMapShapeAdd(shape: Drawing) { + onMapDraw(new AddStatesAction([shape])); } function handleMapShapesRemove(shapeIds: string[]) { - onMapDraw(new RemoveShapeAction(shapeIds)); + onMapDraw(new RemoveStatesAction(shapeIds)); } - function handleFogShapesAdd(shapes: Shape[]) { - onFogDraw(new AddShapeAction(shapes)); + function handleFogShapesAdd(shapes: Fog[]) { + onFogDraw(new AddStatesAction(shapes)); } - function handleFogShapesCut(shapes: Shape[]) { - onFogDraw(new CutShapeAction(shapes)); + function handleFogShapesCut(shapes: Fog[]) { + onFogDraw(new CutFogAction(shapes)); } function handleFogShapesRemove(shapeIds: string[]) { - onFogDraw(new RemoveShapeAction(shapeIds)); + onFogDraw(new RemoveStatesAction(shapeIds)); } - function handleFogShapesEdit(shapes: Shape[]) { - onFogDraw(new EditShapeAction(shapes)); + function handleFogShapesEdit(shapes: Partial[]) { + onFogDraw(new EditStatesAction(shapes)); } const disabledControls = []; @@ -155,7 +160,10 @@ function Map({ disabledControls.push("note"); } - const disabledSettings: { fog: any[]; drawing: any[] } = { + const disabledSettings: { + fog: string[]; + drawing: string[]; + } = { fog: [], drawing: [], }; @@ -197,19 +205,10 @@ function Map({ /> ); - const [isTokenMenuOpen, setIsTokenMenuOpen]: [ - isTokenMenuOpen: boolean, - setIsTokenMenuOpen: React.Dispatch> - ] = useState(false); - const [tokenMenuOptions, setTokenMenuOptions]: [ - tokenMenuOptions: any, - setTokenMenuOptions: any - ] = useState({}); - const [tokenDraggingOptions, setTokenDraggingOptions]: [ - tokenDraggingOptions: any, - setTokenDragginOptions: any - ] = useState(); - function handleTokenMenuOpen(tokenStateId: string, tokenImage: any) { + const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); + const [tokenMenuOptions, setTokenMenuOptions] = useState({}); + const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); + function handleTokenMenuOpen(tokenStateId: string, tokenImage) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); } @@ -240,7 +239,7 @@ function Map({ const tokenDragOverlay = tokenDraggingOptions && ( { + onTokenStateRemove={(state) => { onMapTokenStateRemove(state); setTokenDraggingOptions(null); }} @@ -292,14 +291,14 @@ function Map({ ); const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); - const [noteMenuOptions, setNoteMenuOptions] = useState({}); - const [noteDraggingOptions, setNoteDraggingOptions] = useState(); - function handleNoteMenuOpen(noteId: string, noteNode: any) { + const [noteMenuOptions, setNoteMenuOptions] = useState({}); + const [noteDraggingOptions, setNoteDraggingOptions] = useState(); + function handleNoteMenuOpen(noteId: string, noteNode) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); } - function sortNotes(a: any, b: any, noteDraggingOptions: any) { + function sortNotes(a, b, noteDraggingOptions) { if ( noteDraggingOptions && noteDraggingOptions.dragging && @@ -338,7 +337,7 @@ function Map({ allowNoteEditing && (selectedToolId === "note" || selectedToolId === "move") } - onNoteDragStart={(e: any, noteId: any) => + onNoteDragStart={(e, noteId) => setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) } onNoteDragEnd={() => @@ -364,7 +363,7 @@ function Map({ dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)} noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup} noteId={noteDraggingOptions && noteDraggingOptions.noteId} - onNoteRemove={(noteId: any) => { + onNoteRemove={(noteId) => { onMapNoteRemove(noteId); setNoteDraggingOptions(null); }} diff --git a/src/components/map/MapControls.tsx b/src/components/map/MapControls.tsx index 92a32a8..88e87ff 100644 --- a/src/components/map/MapControls.tsx +++ b/src/components/map/MapControls.tsx @@ -1,4 +1,4 @@ -import React, { useState, Fragment } from "react"; +import { useState, Fragment } from "react"; import { IconButton, Flex, Box } from "theme-ui"; import RadioIconButton from "../RadioIconButton"; @@ -21,21 +21,26 @@ import FullScreenExitIcon from "../../icons/FullScreenExitIcon"; import NoteToolIcon from "../../icons/NoteToolIcon"; import useSetting from "../../hooks/useSetting"; -import { Map } from "../../types/Map"; +import { Map, MapTool, MapToolId } from "../../types/Map"; import { MapState } from "../../types/MapState"; +import { + MapChangeEventHandler, + MapResetEventHandler, +} from "../../types/Events"; +import { Settings } from "../../types/Settings"; type MapControlsProps = { - onMapChange: () => void; - onMapReset: () => void; + onMapChange: MapChangeEventHandler; + onMapReset: MapResetEventHandler; currentMap?: Map; currentMapState?: MapState; - selectedToolId: string; - onSelectedToolChange: () => void; - toolSettings: any; - onToolSettingChange: () => void; - onToolAction: () => void; + selectedToolId: MapToolId; + onSelectedToolChange: (toolId: MapToolId) => void; + toolSettings: Settings; + onToolSettingChange: (change: Partial) => void; + onToolAction: (actionId: string) => void; disabledControls: string[]; - disabledSettings: string[]; + disabledSettings: Partial>; }; function MapContols({ @@ -54,7 +59,7 @@ function MapContols({ const [isExpanded, setIsExpanded] = useState(true); const [fullScreen, setFullScreen] = useSetting("map.fullScreen"); - const toolsById = { + const toolsById: Record = { move: { id: "move", icon: , @@ -89,7 +94,14 @@ function MapContols({ title: "Note Tool (N)", }, }; - const tools = ["move", "fog", "drawing", "measure", "pointer", "note"]; + const tools: MapToolId[] = [ + "move", + "fog", + "drawing", + "measure", + "pointer", + "note", + ]; const sections = [ { @@ -174,32 +186,41 @@ function MapContols({ function getToolSettings() { const Settings = toolsById[selectedToolId].SettingsComponent; - if (Settings) { - return ( - - - onToolSettingChange(selectedToolId, change) - } - onToolAction={onToolAction} - disabledActions={disabledSettings[selectedToolId]} - /> - - ); - } else { + if ( + !Settings || + selectedToolId === "move" || + selectedToolId === "measure" || + selectedToolId === "note" + ) { return null; } + return ( + + + onToolSettingChange({ + [selectedToolId]: { + ...toolSettings[selectedToolId], + ...change, + }, + }) + } + onToolAction={onToolAction} + disabledActions={disabledSettings[selectedToolId]} + /> + + ); } return ( diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.tsx similarity index 85% rename from src/components/map/MapEditor.js rename to src/components/map/MapEditor.tsx index 1040468..de4efdf 100644 --- a/src/components/map/MapEditor.js +++ b/src/components/map/MapEditor.tsx @@ -21,8 +21,17 @@ import GridOffIcon from "../../icons/GridOffIcon"; import MapGrid from "./MapGrid"; import MapGridEditor from "./MapGridEditor"; +import { Map } from "../../types/Map"; +import { GridInset } from "../../types/Grid"; -function MapEditor({ map, onSettingsChange }) { +type MapSettingsChangeEventHandler = (change: Partial) => void; + +type MapEditorProps = { + map: Map; + onSettingsChange: MapSettingsChangeEventHandler; +}; + +function MapEditor({ map, onSettingsChange }: MapEditorProps) { const [mapImage] = useMapImage(map); const [stageWidth, setStageWidth] = useState(1); @@ -36,12 +45,17 @@ function MapEditor({ map, onSettingsChange }) { const mapLayerRef = useRef(); const [preventMapInteraction, setPreventMapInteraction] = useState(false); - function handleResize(width, height) { - setStageWidth(width); - setStageHeight(height); + function handleResize(width?: number, height?: number): void { + if (width) { + setStageWidth(width); + } + + if (height) { + setStageHeight(height); + } } - const containerRef = useRef(); + const containerRef = useRef(null); usePreventOverscroll(containerRef); const [mapWidth, mapHeight] = useImageCenter( @@ -67,17 +81,21 @@ function MapEditor({ map, onSettingsChange }) { preventMapInteraction ); - function handleGridChange(inset) { - onSettingsChange("grid", { - ...map.grid, - inset, + function handleGridChange(inset: GridInset) { + onSettingsChange({ + grid: { + ...map.grid, + inset, + }, }); } function handleMapReset() { - onSettingsChange("grid", { - ...map.grid, - inset: defaultInset, + onSettingsChange({ + grid: { + ...map.grid, + inset: defaultInset, + }, }); } @@ -120,8 +138,9 @@ function MapEditor({ map, onSettingsChange }) { > ( + stageRender={(children: React.ReactNode) => ( void; +type FogCutEventHandler = (fog: Fog[]) => void; +type FogRemoveEventHandler = (fogId: string[]) => void; +type FogEditEventHandler = (edit: Partial[]) => void; +type FogErrorEventHandler = (message: string) => void; + +type MapFogProps = { + map: Map; + shapes: Fog[]; + onShapesAdd: FogAddEventHandler; + onShapesCut: FogCutEventHandler; + onShapesRemove: FogRemoveEventHandler; + onShapesEdit: FogEditEventHandler; + onShapeError: FogErrorEventHandler; + active: boolean; + toolSettings: FogToolSettings; + editable: boolean; +}; + function MapFog({ map, shapes, @@ -58,7 +81,7 @@ function MapFog({ active, toolSettings, editable, -}) { +}: MapFogProps) { const stageScale = useDebouncedStageScale(); const mapWidth = useMapWidth(); const mapHeight = useMapHeight(); @@ -76,7 +99,7 @@ function MapFog({ const [editOpacity] = useSetting("fog.editOpacity"); const mapStageRef = useMapStage(); - const [drawingShape, setDrawingShape] = useState(null); + const [drawingShape, setDrawingShape] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); const [editingShapes, setEditingShapes] = useState([]); @@ -84,7 +107,7 @@ function MapFog({ const [fogShapes, setFogShapes] = useState(shapes); // Bounding boxes for guides const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]); - const [guides, setGuides] = useState([]); + const [guides, setGuides] = useState([]); const shouldHover = active && @@ -108,8 +131,14 @@ function MapFog({ const mapStage = mapStageRef.current; function getBrushPosition(snapping = true) { + if (!mapStage) { + return; + } const mapImage = mapStage.findOne("#mapImage"); let position = getRelativePointerPosition(mapImage); + if (!position) { + return; + } if (shouldUseGuides && snapping) { for (let guide of guides) { if (guide.orientation === "vertical") { @@ -129,6 +158,9 @@ function MapFog({ function handleBrushDown() { if (toolSettings.type === "brush") { const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } setDrawingShape({ type: "fog", data: { @@ -143,6 +175,9 @@ function MapFog({ } if (toolSettings.type === "rectangle") { const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } setDrawingShape({ type: "fog", data: { @@ -166,7 +201,13 @@ function MapFog({ function handleBrushMove() { if (toolSettings.type === "brush" && isBrushDown && drawingShape) { const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } setDrawingShape((prevShape) => { + if (!prevShape) { + return prevShape; + } const prevPoints = prevShape.data.points; if ( Vector2.compare( @@ -193,7 +234,13 @@ function MapFog({ if (toolSettings.type === "rectangle" && isBrushDown && drawingShape) { const prevPoints = drawingShape.data.points; const brushPosition = getBrushPosition(); + if (!brushPosition) { + return; + } setDrawingShape((prevShape) => { + if (!prevShape) { + return prevShape; + } return { ...prevShape, data: { @@ -223,7 +270,7 @@ function MapFog({ const shapesToSubtract = shapes.filter((shape) => cut ? !shape.visible : shape.visible ); - const subtractAction = new SubtractShapeAction(shapesToSubtract); + const subtractAction = new SubtractFogAction(shapesToSubtract); const state = subtractAction.execute({ [drawingShape.id]: drawingShape, }); @@ -235,7 +282,7 @@ function MapFog({ if (drawingShapes.length > 0) { if (cut) { // Run a pre-emptive cut action to check whether we've cut anything - const cutAction = new CutShapeAction(drawingShapes); + const cutAction = new CutFogAction(drawingShapes); const state = cutAction.execute(keyBy(shapes, "id")); if (Object.keys(state).length === shapes.length) { @@ -300,7 +347,7 @@ function MapFog({ function handlePointerMove() { if (shouldUseGuides) { - let guides = []; + let guides: Guide[] = []; const brushPosition = getBrushPosition(false); const absoluteBrushPosition = Vector2.multiply(brushPosition, { x: mapWidth, @@ -393,7 +440,7 @@ function MapFog({ const shapesToSubtract = shapes.filter((shape) => cut ? !shape.visible : shape.visible ); - const subtractAction = new SubtractShapeAction(shapesToSubtract); + const subtractAction = new SubtractFogAction(shapesToSubtract); const state = subtractAction.execute({ [polygonShape.id]: polygonShape, }); @@ -405,7 +452,7 @@ function MapFog({ if (polygonShapes.length > 0) { if (cut) { // Run a pre-emptive cut action to check whether we've cut anything - const cutAction = new CutShapeAction(polygonShapes); + const cutAction = new CutFogAction(polygonShapes); const state = cutAction.execute(keyBy(shapes, "id")); if (Object.keys(state).length === shapes.length) { diff --git a/src/components/map/MapGrid.js b/src/components/map/MapGrid.tsx similarity index 74% rename from src/components/map/MapGrid.js rename to src/components/map/MapGrid.tsx index be5e9ba..6f04e02 100644 --- a/src/components/map/MapGrid.js +++ b/src/components/map/MapGrid.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import useImage from "use-image"; import { useDataURL } from "../../contexts/AssetsContext"; @@ -8,8 +8,9 @@ import { mapSources as defaultMapSources } from "../../maps"; import { getImageLightness } from "../../helpers/image"; import Grid from "../Grid"; +import { Map } from "../../types/Map"; -function MapGrid({ map }) { +function MapGrid({ map }: { map: Map }) { let mapSourceMap = map; const mapURL = useDataURL( mapSourceMap, @@ -17,13 +18,14 @@ function MapGrid({ map }) { undefined, map.type === "file" ); - const [mapImage, mapLoadingStatus] = useImage(mapURL); + + const [mapImage, mapLoadingStatus] = useImage(mapURL || ""); const [isImageLight, setIsImageLight] = useState(true); // When the map changes find the average lightness of its pixels useEffect(() => { - if (mapLoadingStatus === "loaded") { + if (mapLoadingStatus === "loaded" && mapImage) { setIsImageLight(getImageLightness(mapImage)); } }, [mapImage, mapLoadingStatus]); diff --git a/src/components/map/MapGridEditor.js b/src/components/map/MapGridEditor.tsx similarity index 89% rename from src/components/map/MapGridEditor.js rename to src/components/map/MapGridEditor.tsx index a059180..75254e9 100644 --- a/src/components/map/MapGridEditor.js +++ b/src/components/map/MapGridEditor.tsx @@ -1,5 +1,6 @@ -import React, { useRef } from "react"; +import { useRef } from "react"; import { Group, Circle, Rect } from "react-konva"; +import { KonvaEventObject, Node } from "konva/types/Node"; import { useDebouncedStageScale, @@ -12,8 +13,15 @@ import { useKeyboard } from "../../contexts/KeyboardContext"; import Vector2 from "../../helpers/Vector2"; import shortcuts from "../../shortcuts"; +import { Map } from "../../types/Map"; +import { GridInset } from "../../types/Grid"; -function MapGridEditor({ map, onGridChange }) { +type MapGridEditorProps = { + map: Map; + onGridChange: (inset: GridInset) => void; +}; + +function MapGridEditor({ map, onGridChange }: MapGridEditorProps) { const stageScale = useDebouncedStageScale(); const mapWidth = useMapWidth(); const mapHeight = useMapHeight(); @@ -39,21 +47,21 @@ function MapGridEditor({ map, onGridChange }) { } const handlePositions = getHandlePositions(); - const handlePreviousPositionRef = useRef(); + const handlePreviousPositionRef = useRef(); - function handleScaleCircleDragStart(event) { + function handleScaleCircleDragStart(event: KonvaEventObject) { const handle = event.target; const position = getHandleNormalizedPosition(handle); handlePreviousPositionRef.current = position; } - function handleScaleCircleDragMove(event) { + function handleScaleCircleDragMove(event: KonvaEventObject) { const handle = event.target; onGridChange(getHandleInset(handle)); handlePreviousPositionRef.current = getHandleNormalizedPosition(handle); } - function handleScaleCircleDragEnd(event) { + function handleScaleCircleDragEnd(event: KonvaEventObject) { onGridChange(getHandleInset(event.target)); setPreventMapInteraction(false); } @@ -66,11 +74,14 @@ function MapGridEditor({ map, onGridChange }) { setPreventMapInteraction(false); } - function getHandleInset(handle) { + function getHandleInset(handle: Node): GridInset { const name = handle.name(); // Find distance and direction of dragging const previousPosition = handlePreviousPositionRef.current; + if (!previousPosition) { + return map.grid.inset; + } const position = getHandleNormalizedPosition(handle); const distance = Vector2.distance(previousPosition, position); const direction = Vector2.normalize( @@ -154,7 +165,7 @@ function MapGridEditor({ map, onGridChange }) { } } - function nudgeGrid(direction, scale) { + function nudgeGrid(direction: Vector2, scale: number) { const inset = map.grid.inset; const gridSizeNormalized = Vector2.divide( Vector2.subtract(inset.bottomRight, inset.topLeft), @@ -170,7 +181,7 @@ function MapGridEditor({ map, onGridChange }) { }); } - function handleKeyDown(event) { + function handleKeyDown(event: KeyboardEvent) { const nudgeAmount = event.shiftKey ? 2 : 0.5; if (shortcuts.gridNudgeUp(event)) { // Stop arrow up/down scrolling if overflowing @@ -191,7 +202,7 @@ function MapGridEditor({ map, onGridChange }) { useKeyboard(handleKeyDown); - function getHandleNormalizedPosition(handle) { + function getHandleNormalizedPosition(handle: Node) { return Vector2.divide({ x: handle.x(), y: handle.y() }, mapSize); } diff --git a/src/components/map/SelectMapButton.tsx b/src/components/map/SelectMapButton.tsx index 18c1654..aae8164 100644 --- a/src/components/map/SelectMapButton.tsx +++ b/src/components/map/SelectMapButton.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { IconButton } from "theme-ui"; import SelectMapModal from "../../modals/SelectMapModal"; @@ -6,6 +6,20 @@ import SelectMapIcon from "../../icons/SelectMapIcon"; import { useMapData } from "../../contexts/MapDataContext"; import { useUserId } from "../../contexts/UserIdContext"; +import { + MapChangeEventHandler, + MapResetEventHandler, +} from "../../types/Events"; +import { Map } from "../../types/Map"; +import { MapState } from "../../types/MapState"; + +type SelectMapButtonProps = { + onMapChange: MapChangeEventHandler; + onMapReset: MapResetEventHandler; + currentMap?: Map; + currentMapState?: MapState; + disabled: boolean; +}; function SelectMapButton({ onMapChange, @@ -13,7 +27,7 @@ function SelectMapButton({ currentMap, currentMapState, disabled, -}) { +}: SelectMapButtonProps) { const [isModalOpen, setIsModalOpen] = useState(false); const { updateMapState } = useMapData(); diff --git a/src/components/party/ChangeNicknameButton.tsx b/src/components/party/ChangeNicknameButton.tsx index be599e3..a3a478f 100644 --- a/src/components/party/ChangeNicknameButton.tsx +++ b/src/components/party/ChangeNicknameButton.tsx @@ -4,7 +4,13 @@ import { IconButton } from "theme-ui"; import ChangeNicknameModal from "../../modals/ChangeNicknameModal"; import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon"; -function ChangeNicknameButton({ nickname, onChange }: { nickname: string, onChange: any}) { +function ChangeNicknameButton({ + nickname, + onChange, +}: { + nickname: string; + onChange; +}) { const [isChangeModalOpen, setIsChangeModalOpen] = useState(false); function openModal() { setIsChangeModalOpen(true); diff --git a/src/components/party/DiceRoll.tsx b/src/components/party/DiceRoll.tsx index e770205..f6ccaee 100644 --- a/src/components/party/DiceRoll.tsx +++ b/src/components/party/DiceRoll.tsx @@ -1,12 +1,20 @@ import { Flex, Box, Text } from "theme-ui"; -function DiceRoll({ rolls, type, children }: { rolls: any, type: string, children: any}) { +function DiceRoll({ + rolls, + type, + children, +}: { + rolls; + type: string; + children; +}) { return ( {children} {rolls - .filter((d: any) => d.type === type && d.roll !== "unknown") - .map((dice: any, index: string | number) => ( + .filter((d) => d.type === type && d.roll !== "unknown") + .map((dice, index: string | number) => ( {dice.roll} diff --git a/src/components/party/DiceRolls.tsx b/src/components/party/DiceRolls.tsx index 3798595..99eb8c6 100644 --- a/src/components/party/DiceRolls.tsx +++ b/src/components/party/DiceRolls.tsx @@ -24,14 +24,14 @@ const diceIcons = [ { type: "d100", Icon: D100Icon }, ]; -function DiceRolls({ rolls }: { rolls: any }) { +function DiceRolls({ rolls }: { rolls }) { const total = getDiceRollTotal(rolls); const [expanded, setExpanded] = useState(false); let expandedRolls = []; for (let icon of diceIcons) { - if (rolls.some((roll: any) => roll.type === icon.type)) { + if (rolls.some((roll) => roll.type === icon.type)) { expandedRolls.push( @@ -45,29 +45,29 @@ function DiceRolls({ rolls }: { rolls: any }) { } return ( - - - setExpanded(!expanded)} - > - - - - {total} - - - {expanded && ( - - {expandedRolls} - - )} + + + setExpanded(!expanded)} + > + + + + {total} + + {expanded && ( + + {expandedRolls} + + )} + ); } diff --git a/src/components/party/DiceTrayButton.tsx b/src/components/party/DiceTrayButton.tsx index 1a00263..51ba962 100644 --- a/src/components/party/DiceTrayButton.tsx +++ b/src/components/party/DiceTrayButton.tsx @@ -16,7 +16,12 @@ function DiceTrayButton({ onShareDiceChange, diceRolls, onDiceRollsChange, -}: { shareDice: boolean, onShareDiceChange: any, diceRolls: [], onDiceRollsChange: any}) { +}: { + shareDice: boolean; + onShareDiceChange; + diceRolls: []; + onDiceRollsChange; +}) { const [isExpanded, setIsExpanded] = useState(false); const [fullScreen] = useSetting("map.fullScreen"); diff --git a/src/components/party/Nickname.tsx b/src/components/party/Nickname.tsx index 00b316a..2510d81 100644 --- a/src/components/party/Nickname.tsx +++ b/src/components/party/Nickname.tsx @@ -4,7 +4,15 @@ import Stream from "./Stream"; import DiceRolls from "./DiceRolls"; // TODO: check if stream is a required or optional param -function Nickname({ nickname, stream, diceRolls }: { nickname: string, stream?: any, diceRolls: any}) { +function Nickname({ + nickname, + stream, + diceRolls, +}: { + nickname: string; + stream?; + diceRolls; +}) { return ( ({ ...prevState, timer: newTimer })); + setPlayerState((prevState) => ({ ...prevState, timer: newTimer })); } function handleTimerStop() { - setPlayerState((prevState: any) => ({ ...prevState, timer: null })); + setPlayerState((prevState) => ({ ...prevState, timer: null })); } useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(update); let counter = 0; - function update(time: any) { + function update(time) { request = requestAnimationFrame(update); const deltaTime = time - prevTime; prevTime = time; @@ -51,9 +68,9 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }: { g current: playerState.timer.current - counter, }; if (newTimer.current < 0) { - setPlayerState((prevState: any) => ({ ...prevState, timer: null })); + setPlayerState((prevState) => ({ ...prevState, timer: null })); } else { - setPlayerState((prevState: any) => ({ ...prevState, timer: newTimer })); + setPlayerState((prevState) => ({ ...prevState, timer: newTimer })); } counter = 0; } @@ -65,7 +82,7 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }: { g }, [playerState.timer, setPlayerState]); function handleNicknameChange(newNickname: string) { - setPlayerState((prevState: any) => ({ ...prevState, nickname: newNickname })); + setPlayerState((prevState) => ({ ...prevState, nickname: newNickname })); } function handleDiceRollsChange(newDiceRolls: number[]) { diff --git a/src/components/party/StartStreamButton.tsx b/src/components/party/StartStreamButton.tsx index 23e8de0..14cc877 100644 --- a/src/components/party/StartStreamButton.tsx +++ b/src/components/party/StartStreamButton.tsx @@ -6,7 +6,15 @@ import Link from "../Link"; import StartStreamModal from "../../modals/StartStreamModal"; -function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamStart: any, onStreamEnd: any, stream: any}) { +function StartStreamButton({ + onStreamStart, + onStreamEnd, + stream, +}: { + onStreamStart; + onStreamEnd; + stream; +}) { const [isStreamModalOpoen, setIsStreamModalOpen] = useState(false); function openModal() { setIsStreamModalOpen(true); @@ -45,7 +53,7 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamSta function handleStreamStart() { // Must be defined this way in typescript due to open issue - https://github.com/microsoft/TypeScript/issues/33232 - const mediaDevices = navigator.mediaDevices as any; + const mediaDevices = navigator.mediaDevices; mediaDevices .getDisplayMedia({ video: true, @@ -55,10 +63,12 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }: { onStreamSta echoCancellation: false, }, }) - .then((localStream: { getTracks: () => any; }) => { + .then((localStream: { getTracks }) => { const tracks = localStream.getTracks(); - const hasAudio = tracks.some((track: { kind: string; }) => track.kind === "audio"); + const hasAudio = tracks.some( + (track: { kind: string }) => track.kind === "audio" + ); setNoAudioTrack(!hasAudio); // Ensure an audio track is present diff --git a/src/components/party/StartTimerButton.tsx b/src/components/party/StartTimerButton.tsx index 3c413fc..8072226 100644 --- a/src/components/party/StartTimerButton.tsx +++ b/src/components/party/StartTimerButton.tsx @@ -4,7 +4,15 @@ import { IconButton } from "theme-ui"; import StartTimerModal from "../../modals/StartTimerModal"; import StartTimerIcon from "../../icons/StartTimerIcon"; -function StartTimerButton({ onTimerStart, onTimerStop, timer }: { onTimerStart: any, onTimerStop: any, timer: any }) { +function StartTimerButton({ + onTimerStart, + onTimerStop, + timer, +}: { + onTimerStart; + onTimerStop; + timer; +}) { const [isTimerModalOpen, setIsTimerModalOpen] = useState(false); function openModal() { diff --git a/src/components/party/Stream.tsx b/src/components/party/Stream.tsx index 6dc5eec..58f7844 100644 --- a/src/components/party/Stream.tsx +++ b/src/components/party/Stream.tsx @@ -6,13 +6,18 @@ import StreamMuteIcon from "../../icons/StreamMuteIcon"; import Banner from "../banner/Banner"; import Slider from "../Slider"; -function Stream({ stream, nickname }: { stream: MediaStream, nickname: string }) { +function Stream({ + stream, + nickname, +}: { + stream: MediaStream; + nickname: string; +}) { const [streamVolume, setStreamVolume] = useState(1); - const [showStreamInteractBanner, setShowStreamInteractBanner] = useState( - false - ); + const [showStreamInteractBanner, setShowStreamInteractBanner] = + useState(false); const [streamMuted, setStreamMuted] = useState(false); - const audioRef = useRef(); + const audioRef = useRef(); useEffect(() => { if (audioRef.current) { @@ -51,9 +56,8 @@ function Stream({ stream, nickname }: { stream: MediaStream, nickname: string }) // Platforms like iOS don't allow you to control audio volume // Detect this by trying to change the audio volume - const [isVolumeControlAvailable, setIsVolumeControlAvailable] = useState( - true - ); + const [isVolumeControlAvailable, setIsVolumeControlAvailable] = + useState(true); useEffect(() => { let audio = audioRef.current; function checkVolumeControlAvailable() { @@ -75,7 +79,7 @@ function Stream({ stream, nickname }: { stream: MediaStream, nickname: string }) }, []); // Use an audio context gain node to control volume to go past 100% - const audioGainRef = useRef(); + const audioGainRef = useRef(); useEffect(() => { let audioContext: AudioContext; if (stream && !streamMuted && isVolumeControlAvailable && audioGainRef) { diff --git a/src/components/party/Timer.tsx b/src/components/party/Timer.tsx index 3f208ce..9762868 100644 --- a/src/components/party/Timer.tsx +++ b/src/components/party/Timer.tsx @@ -4,8 +4,8 @@ import { Box, Progress } from "theme-ui"; import usePortal from "../../hooks/usePortal"; -function Timer({ timer, index }: { timer: any, index: number}) { - const progressBarRef = useRef(); +function Timer({ timer, index }: { timer; index: number }) { + const progressBarRef = useRef(); useEffect(() => { if (progressBarRef.current && timer) { @@ -16,7 +16,7 @@ function Timer({ timer, index }: { timer: any, index: number}) { useEffect(() => { let request = requestAnimationFrame(animate); let previousTime = performance.now(); - function animate(time: any) { + function animate(time) { request = requestAnimationFrame(animate); const deltaTime = time - previousTime; previousTime = time; diff --git a/src/contexts/AssetsContext.tsx b/src/contexts/AssetsContext.tsx index 9d64c9c..8f2266c 100644 --- a/src/contexts/AssetsContext.tsx +++ b/src/contexts/AssetsContext.tsx @@ -10,10 +10,16 @@ import useDebounce from "../hooks/useDebounce"; import { omit } from "../helpers/shared"; import { Asset } from "../types/Asset"; +export type GetAssetEventHanlder = ( + assetId: string +) => Promise; +export type AddAssetsEventHandler = (assets: Asset[]) => Promise; +export type PutAssetEventsHandler = (asset: Asset) => Promise; + type AssetsContext = { - getAsset: (assetId: string) => Promise; - addAssets: (assets: Asset[]) => void; - putAsset: (asset: Asset) => void; + getAsset: GetAssetEventHanlder; + addAssets: AddAssetsEventHandler; + putAsset: PutAssetEventsHandler; }; const AssetsContext = React.createContext(undefined); @@ -30,7 +36,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) { } }, [worker, databaseStatus]); - const getAsset = useCallback( + const getAsset = useCallback( async (assetId) => { if (database) { return await database.table("assets").get(assetId); @@ -39,7 +45,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) { [database] ); - const addAssets = useCallback( + const addAssets = useCallback( async (assets) => { if (database) { await database.table("assets").bulkAdd(assets); @@ -48,7 +54,7 @@ export function AssetsProvider({ children }: { children: React.ReactNode }) { [database] ); - const putAsset = useCallback( + const putAsset = useCallback( async (asset) => { if (database) { // Check for broadcast channel and attempt to use worker to put map to avoid UI lockup diff --git a/src/contexts/DatabaseContext.tsx b/src/contexts/DatabaseContext.tsx index 167ef6b..ec68448 100644 --- a/src/contexts/DatabaseContext.tsx +++ b/src/contexts/DatabaseContext.tsx @@ -8,24 +8,28 @@ import { getDatabase } from "../database"; //@ts-ignore import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax +import { DatabaseWorkerService } from "../workers/DatabaseWorker"; + +export type DatabaseStatus = "loading" | "disabled" | "upgrading" | "loaded"; type DatabaseContext = { database: Dexie | undefined; - databaseStatus: any; + databaseStatus: DatabaseStatus; databaseError: Error | undefined; - worker: Comlink.Remote; + worker: Comlink.Remote; }; -// TODO: check what default we want here const DatabaseContext = React.createContext(undefined); -const worker = Comlink.wrap(new DatabaseWorker()); +const worker: Comlink.Remote = Comlink.wrap( + new DatabaseWorker() +); export function DatabaseProvider({ children }: { children: React.ReactNode }) { const [database, setDatabase] = useState(); const [databaseStatus, setDatabaseStatus] = - useState<"loading" | "disabled" | "upgrading" | "loaded">("loading"); + useState("loading"); const [databaseError, setDatabaseError] = useState(); useEffect(() => { diff --git a/src/contexts/DiceLoadingContext.tsx b/src/contexts/DiceLoadingContext.tsx index 5f6fc43..97b6f57 100644 --- a/src/contexts/DiceLoadingContext.tsx +++ b/src/contexts/DiceLoadingContext.tsx @@ -1,12 +1,16 @@ import React, { useState, useContext, ReactChild } from "react"; -type DiceLoadingContext = { - assetLoadStart: any, - assetLoadFinish: any, - isLoading: boolean, -} +export type AssetLoadStartEventHandler = () => void; +export type AssetLoadFinishEventHandler = () => void; -const DiceLoadingContext = React.createContext(undefined); +type DiceLoadingContext = { + assetLoadStart: AssetLoadStartEventHandler; + assetLoadFinish: AssetLoadFinishEventHandler; + isLoading: boolean; +}; + +const DiceLoadingContext = + React.createContext(undefined); export function DiceLoadingProvider({ children }: { children: ReactChild }) { const [loadingAssetCount, setLoadingAssetCount] = useState(0); diff --git a/src/contexts/GridContext.tsx b/src/contexts/GridContext.tsx index f7390d1..81f1f7d 100644 --- a/src/contexts/GridContext.tsx +++ b/src/contexts/GridContext.tsx @@ -2,8 +2,8 @@ import React, { useContext, useState, useEffect } from "react"; import Vector2 from "../helpers/Vector2"; import Size from "../helpers/Size"; -// eslint-disable-next-line no-unused-vars -import { getGridPixelSize, getCellPixelSize, Grid } from "../helpers/grid"; +import { getGridPixelSize, getCellPixelSize } from "../helpers/grid"; +import { Grid } from "../types/Grid"; /** * @typedef GridContextValue @@ -16,14 +16,14 @@ import { getGridPixelSize, getCellPixelSize, Grid } from "../helpers/grid"; * @property {Vector2} gridCellPixelOffset Offset of the grid cells to convert the center position of hex cells to the top left */ type GridContextValue = { - grid: Grid, - gridPixelSize: Size, - gridCellPixelSize: Size, - gridCellNormalizedSize: Size, - gridOffset: Vector2, - gridStrokeWidth: number, - gridCellPixelOffset: Vector2 -} + grid: Grid; + gridPixelSize: Size; + gridCellPixelSize: Size; + gridCellNormalizedSize: Size; + gridOffset: Vector2; + gridStrokeWidth: number; + gridCellPixelOffset: Vector2; +}; /** * @type {GridContextValue} @@ -66,11 +66,21 @@ export const GridCellPixelOffsetContext = React.createContext( const defaultStrokeWidth = 1 / 10; -export function GridProvider({ grid: inputGrid, width, height, children }: { grid: Required, width: number, height: number, children: any }) { +export function GridProvider({ + grid: inputGrid, + width, + height, + children, +}: { + grid: Grid; + width: number; + height: number; + children: React.ReactNode; +}) { let grid = inputGrid; if (!grid.size.x || !grid.size.y) { - grid = defaultValue.grid as Required; + grid = defaultValue.grid; } const [gridPixelSize, setGridPixelSize] = useState( diff --git a/src/contexts/GroupContext.tsx b/src/contexts/GroupContext.tsx index f8dd6bc..1916268 100644 --- a/src/contexts/GroupContext.tsx +++ b/src/contexts/GroupContext.tsx @@ -9,26 +9,41 @@ import { getGroupItems, groupsFromIds } from "../helpers/group"; import shortcuts from "../shortcuts"; import { Group, GroupContainer, GroupItem } from "../types/Group"; +export type GroupSelectMode = "single" | "multiple" | "range"; +export type GroupSelectModeChangeEventHandler = ( + selectMode: GroupSelectMode +) => void; +export type GroupOpenEventHandler = (groupId: string) => void; +export type GroupCloseEventHandler = () => void; +export type GroupsChangeEventHandler = (newGroups: Group[]) => void; +export type SubgroupsChangeEventHandler = ( + items: GroupItem[], + groupId: string +) => void; +export type GroupSelectEventHandler = (groupId: string) => void; +export type GroupsSelectEventHandler = (groupIds: string[]) => void; +export type GroupClearSelectionEventHandler = () => void; +export type GroupFilterChangeEventHandler = (filter: string) => void; +export type GroupClearFilterEventHandler = () => void; + type GroupContext = { groups: Group[]; - activeGroups: Group[]; + activeGroups: Group[] | GroupItem[]; openGroupId: string | undefined; - openGroupItems: Group[]; + openGroupItems: GroupItem[]; filter: string | undefined; filteredGroupItems: GroupItem[]; selectedGroupIds: string[]; - selectMode: any; - onSelectModeChange: React.Dispatch< - React.SetStateAction<"single" | "multiple" | "range"> - >; - onGroupOpen: (groupId: string) => void; - onGroupClose: () => void; - onGroupsChange: ( - newGroups: Group[] | GroupItem[], - groupId: string | undefined - ) => void; - onGroupSelect: (groupId: string | undefined) => void; - onFilterChange: React.Dispatch>; + selectMode: GroupSelectMode; + onSelectModeChange: GroupSelectModeChangeEventHandler; + onGroupOpen: GroupOpenEventHandler; + onGroupClose: GroupCloseEventHandler; + onGroupsChange: GroupsChangeEventHandler; + onSubgroupChange: SubgroupsChangeEventHandler; + onGroupSelect: GroupSelectEventHandler; + onClearSelection: GroupClearSelectionEventHandler; + onFilterChange: GroupFilterChangeEventHandler; + onFilterClear: GroupClearFilterEventHandler; }; const GroupContext = React.createContext(undefined); @@ -36,8 +51,8 @@ const GroupContext = React.createContext(undefined); type GroupProviderProps = { groups: Group[]; itemNames: Record; - onGroupsChange: (groups: Group[]) => void; - onGroupsSelect: (groupIds: string[]) => void; + onGroupsChange: GroupsChangeEventHandler; + onGroupsSelect: GroupsSelectEventHandler; disabled: boolean; children: React.ReactNode; }; @@ -51,15 +66,13 @@ export function GroupProvider({ children, }: GroupProviderProps) { const [selectedGroupIds, setSelectedGroupIds] = useState([]); - // Either single, multiple or range - const [selectMode, setSelectMode] = - useState<"single" | "multiple" | "range">("single"); + const [selectMode, setSelectMode] = useState("single"); /** * Group Open */ const [openGroupId, setOpenGroupId] = useState(); - const [openGroupItems, setOpenGroupItems] = useState([]); + const [openGroupItems, setOpenGroupItems] = useState([]); useEffect(() => { if (openGroupId) { const openGroups = groupsFromIds([openGroupId], groups); @@ -128,81 +141,78 @@ export function GroupProvider({ ? filteredGroupItems : groups; - /** - * @param {Group[] | GroupItem[]} newGroups - * @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object - */ - function handleGroupsChange( - newGroups: Group[] | GroupItem[], - groupId: string | undefined - ) { - if (groupId) { - // If a group is specidifed then update that group with the new items - const groupIndex = groups.findIndex((group) => group.id === groupId); - let updatedGroups = cloneDeep(groups); - const group = updatedGroups[groupIndex]; + function handleGroupsChange(newGroups: Group[]) { + onGroupsChange(newGroups); + } + + function handleSubgroupChange(items: GroupItem[], groupId: string) { + const groupIndex = groups.findIndex((group) => group.id === groupId); + let updatedGroups = cloneDeep(groups); + const group = updatedGroups[groupIndex]; + if (group.type === "group") { updatedGroups[groupIndex] = { ...group, - items: newGroups, - } as GroupContainer; + items, + }; onGroupsChange(updatedGroups); } else { - onGroupsChange(newGroups); + throw new Error(`Group ${group} not a subgroup`); } } - function handleGroupSelect(groupId: string | undefined) { + function handleGroupSelect(groupId: string) { let groupIds: string[] = []; - if (groupId) { - switch (selectMode) { - case "single": - groupIds = [groupId]; - break; - case "multiple": - if (selectedGroupIds.includes(groupId)) { - groupIds = selectedGroupIds.filter((id) => id !== groupId); - } else { - groupIds = [...selectedGroupIds, groupId]; - } - break; - case "range": - if (selectedGroupIds.length > 0) { - const currentIndex = activeGroups.findIndex( - (g) => g.id === groupId - ); - const lastIndex = activeGroups.findIndex( - (g) => g.id === selectedGroupIds[selectedGroupIds.length - 1] - ); - let idsToAdd: string[] = []; - let idsToRemove: string[] = []; - const direction = currentIndex > lastIndex ? 1 : -1; - for ( - let i = lastIndex + direction; - direction < 0 ? i >= currentIndex : i <= currentIndex; - i += direction - ) { - const id = activeGroups[i].id; - if (selectedGroupIds.includes(id)) { - idsToRemove.push(id); - } else { - idsToAdd.push(id); - } + switch (selectMode) { + case "single": + groupIds = [groupId]; + break; + case "multiple": + if (selectedGroupIds.includes(groupId)) { + groupIds = selectedGroupIds.filter((id) => id !== groupId); + } else { + groupIds = [...selectedGroupIds, groupId]; + } + break; + case "range": + if (selectedGroupIds.length > 0) { + const currentIndex = activeGroups.findIndex((g) => g.id === groupId); + const lastIndex = activeGroups.findIndex( + (g) => g.id === selectedGroupIds[selectedGroupIds.length - 1] + ); + let idsToAdd: string[] = []; + let idsToRemove: string[] = []; + const direction = currentIndex > lastIndex ? 1 : -1; + for ( + let i = lastIndex + direction; + direction < 0 ? i >= currentIndex : i <= currentIndex; + i += direction + ) { + const id = activeGroups[i].id; + if (selectedGroupIds.includes(id)) { + idsToRemove.push(id); + } else { + idsToAdd.push(id); } - groupIds = [...selectedGroupIds, ...idsToAdd].filter( - (id) => !idsToRemove.includes(id) - ); - } else { - groupIds = [groupId]; } - break; - default: - groupIds = []; - } + groupIds = [...selectedGroupIds, ...idsToAdd].filter( + (id) => !idsToRemove.includes(id) + ); + } else { + groupIds = [groupId]; + } + break; + default: + groupIds = []; } setSelectedGroupIds(groupIds); onGroupsSelect(groupIds); } + function handleClearSelection() { + setSelectedGroupIds([]); + onGroupsSelect([]); + } + /** * Shortcuts */ @@ -239,7 +249,7 @@ export function GroupProvider({ useBlur(handleBlur); - const value = { + const value: GroupContext = { groups, activeGroups, openGroupId, @@ -252,8 +262,11 @@ export function GroupProvider({ onGroupOpen: handleGroupOpen, onGroupClose: handleGroupClose, onGroupsChange: handleGroupsChange, + onSubgroupChange: handleSubgroupChange, onGroupSelect: handleGroupSelect, + onClearSelection: handleClearSelection, onFilterChange: setFilter, + onFilterClear: () => setFilter(undefined), }; return ( diff --git a/src/contexts/ImageSourceContext.tsx b/src/contexts/ImageSourceContext.tsx deleted file mode 100644 index c566300..0000000 --- a/src/contexts/ImageSourceContext.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { useContext, useState, useEffect, ReactChild } from "react"; -import { ImageFile } from "../helpers/image"; - -import { omit } from "../helpers/shared"; - -export const ImageSourcesStateContext = React.createContext(undefined) as any; -export const ImageSourcesUpdaterContext = React.createContext(() => {}) as any; - -/** - * Helper to manage sharing of custom image sources between uses of useImageSource - */ -export function ImageSourcesProvider({ children }: { children: ReactChild }) { - const [imageSources, setImageSources] = useState({}); - - // Revoke url when no more references - useEffect(() => { - let sourcesToCleanup: any = []; - for (let source of Object.values(imageSources) as any) { - if (source.references <= 0) { - URL.revokeObjectURL(source.url); - sourcesToCleanup.push(source.id); - } - } - if (sourcesToCleanup.length > 0) { - setImageSources((prevSources: any) => omit(prevSources, sourcesToCleanup)); - } - }, [imageSources]); - - return ( - - - {children} - - - ); -} - -/** - * Get id from image data - */ -function getImageFileId(data: any, thumbnail: ImageFile) { - if (thumbnail) { - return `${data.id}-thumbnail`; - } - if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - return `${data.id}-${data.quality}`; - } else if (!data.file) { - // Fallback to the highest resolution - const resolutionArray = Object.keys(data.resolutions); - const resolution: any = resolutionArray[resolutionArray.length - 1]; - return `${data.id}-${resolution.id}`; - } - } - return data.id; -} - -/** - * Helper function to load either file or default image into a URL - */ -export function useImageSource(data: any, defaultSources: string, unknownSource: string, thumbnail: ImageFile) { - const imageSources: any = useContext(ImageSourcesStateContext); - if (imageSources === undefined) { - throw new Error( - "useImageSource must be used within a ImageSourcesProvider" - ); - } - const setImageSources: any = useContext(ImageSourcesUpdaterContext); - if (setImageSources === undefined) { - throw new Error( - "useImageSource must be used within a ImageSourcesProvider" - ); - } - - useEffect(() => { - if (!data || data.type !== "file") { - return; - } - const id = getImageFileId(data, thumbnail); - - function updateImageSource(file: File) { - if (file) { - setImageSources((prevSources: any) => { - if (id in prevSources) { - // Check if the image source is already added - return { - ...prevSources, - [id]: { - ...prevSources[id], - // Increase references - references: prevSources[id].references + 1, - }, - }; - } else { - const url = URL.createObjectURL(new Blob([file])); - return { - ...prevSources, - [id]: { url, id, references: 1 }, - }; - } - }); - } - } - - if (thumbnail) { - updateImageSource(data.thumbnail.file); - } else if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - updateImageSource(data.resolutions[data.quality].file); - } - // If no file available fallback to the highest resolution - else if (!data.file) { - const resolutionArray = Object.keys(data.resolutions); - updateImageSource( - data.resolutions[resolutionArray[resolutionArray.length - 1]].file - ); - } else { - updateImageSource(data.file); - } - } else { - updateImageSource(data.file); - } - - return () => { - // Decrease references - setImageSources((prevSources: any) => { - if (id in prevSources) { - return { - ...prevSources, - [id]: { - ...prevSources[id], - references: prevSources[id].references - 1, - }, - }; - } else { - return prevSources; - } - }); - }; - }, [data, unknownSource, thumbnail, setImageSources]); - - if (!data) { - return unknownSource; - } - - if (data.type === "default") { - return defaultSources[data.key]; - } - - if (data.type === "file") { - const id = getImageFileId(data, thumbnail); - return imageSources[id]?.url; - } - - return unknownSource; -} diff --git a/src/contexts/MapDataContext.tsx b/src/contexts/MapDataContext.tsx index fd12cf9..5cd76ef 100644 --- a/src/contexts/MapDataContext.tsx +++ b/src/contexts/MapDataContext.tsx @@ -53,10 +53,7 @@ type MapDataContext = { const MapDataContext = React.createContext(undefined); -const defaultMapState: Pick< - MapState, - "tokens" | "drawShapes" | "fogShapes" | "editFlags" | "notes" -> = { +const defaultMapState: Omit = { tokens: {}, drawShapes: {}, fogShapes: {}, diff --git a/src/contexts/PartyContext.tsx b/src/contexts/PartyContext.tsx index 716076f..12e7406 100644 --- a/src/contexts/PartyContext.tsx +++ b/src/contexts/PartyContext.tsx @@ -1,10 +1,16 @@ import React, { useState, useEffect, useContext } from "react"; -import { PartyState } from "../components/party/PartyState"; import Session from "../network/Session"; +import { PartyState } from "../types/PartyState"; + const PartyContext = React.createContext(undefined); -export function PartyProvider({ session, children }: { session: Session, children: any}) { +type PartyProviderProps = { + session: Session; + children: React.ReactNode; +}; + +export function PartyProvider({ session, children }: PartyProviderProps) { const [partyState, setPartyState] = useState({}); useEffect(() => { diff --git a/src/contexts/PlayerContext.tsx b/src/contexts/PlayerContext.tsx index 4f0f4eb..e7d7003 100644 --- a/src/contexts/PlayerContext.tsx +++ b/src/contexts/PlayerContext.tsx @@ -5,29 +5,32 @@ import { useUserId } from "./UserIdContext"; import { getRandomMonster } from "../helpers/monsters"; -import useNetworkedState from "../hooks/useNetworkedState"; +import useNetworkedState, { + SetNetworkedState, +} from "../hooks/useNetworkedState"; import Session from "../network/Session"; -import { PlayerInfo } from "../components/party/PartyState"; +import { PlayerState } from "../types/PlayerState"; -export const PlayerStateContext = React.createContext(undefined); -export const PlayerUpdaterContext = React.createContext(() => {}); +export const PlayerStateContext = + React.createContext(undefined); +export const PlayerUpdaterContext = + React.createContext | undefined>(undefined); -export function PlayerProvider({ - session, - children, -}: { +type PlayerProviderProps = { session: Session; children: React.ReactNode; -}) { +}; + +export function PlayerProvider({ session, children }: PlayerProviderProps) { const userId = useUserId(); const { database, databaseStatus } = useDatabase(); - const [playerState, setPlayerState] = useNetworkedState( + const [playerState, setPlayerState] = useNetworkedState( { nickname: "", - timer: null, + timer: undefined, dice: { share: false, rolls: [] }, - sessionId: null, + sessionId: undefined, userId, }, session, @@ -43,13 +46,13 @@ export function PlayerProvider({ async function loadNickname() { const storedNickname = await database?.table("user").get("nickname"); if (storedNickname !== undefined) { - setPlayerState((prevState: PlayerInfo) => ({ + setPlayerState((prevState) => ({ ...prevState, nickname: storedNickname.value, })); } else { const name = getRandomMonster(); - setPlayerState((prevState: any) => ({ ...prevState, nickname: name })); + setPlayerState((prevState) => ({ ...prevState, nickname: name })); database?.table("user").add({ key: "nickname", value: name }); } } @@ -71,7 +74,7 @@ export function PlayerProvider({ useEffect(() => { if (userId) { - setPlayerState((prevState: PlayerInfo) => { + setPlayerState((prevState) => { if (prevState) { return { ...prevState, @@ -85,8 +88,7 @@ export function PlayerProvider({ useEffect(() => { function updateSessionId() { - setPlayerState((prevState: PlayerInfo) => { - // TODO: check useNetworkState requirements here + setPlayerState((prevState) => { if (prevState) { return { ...prevState, diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx index 097c005..40ba49a 100644 --- a/src/contexts/SettingsContext.tsx +++ b/src/contexts/SettingsContext.tsx @@ -14,7 +14,7 @@ const SettingsContext = const settingsProvider = getSettings(); -export function SettingsProvider({ children }: { children: any }) { +export function SettingsProvider({ children }: { children: React.ReactNode }) { const [settings, setSettings] = useState(settingsProvider.getAll()); useEffect(() => { diff --git a/src/contexts/TileDragContext.tsx b/src/contexts/TileDragContext.tsx index 2a3844c..e344833 100644 --- a/src/contexts/TileDragContext.tsx +++ b/src/contexts/TileDragContext.tsx @@ -19,6 +19,7 @@ import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group"; import Vector2 from "../helpers/Vector2"; import usePreventSelect from "../hooks/usePreventSelect"; +import { GroupItem } from "../types/Group"; const TileDragIdContext = React.createContext(undefined); @@ -72,7 +73,9 @@ export function TileDragProvider({ openGroupId, selectedGroupIds, onGroupsChange, + onSubgroupChange, onGroupSelect, + onClearSelection, filter, } = useGroup(); @@ -145,24 +148,28 @@ export function TileDragProvider({ selectedIndices = selectedIndices.sort((a, b) => a - b); if (over.id.startsWith(GROUP_ID_PREFIX)) { - onGroupSelect(undefined); + onClearSelection(); // Handle tile group const overId = over.id.slice(9); if (overId !== active.id) { const overGroupIndex = activeGroups.findIndex( (group) => group.id === overId ); - onGroupsChange( - moveGroupsInto(activeGroups, overGroupIndex, selectedIndices), - openGroupId + const newGroups = moveGroupsInto( + activeGroups, + overGroupIndex, + selectedIndices ); + if (!openGroupId) { + onGroupsChange(newGroups); + } } } else if (over.id === UNGROUP_ID) { if (openGroupId) { - onGroupSelect(undefined); + onClearSelection(); // Handle tile ungroup const newGroups = ungroup(groups, openGroupId, selectedIndices); - onGroupsChange(newGroups, undefined); + onGroupsChange(newGroups); } } else if (over.id === ADD_TO_MAP_ID) { onDragAdd && @@ -173,10 +180,16 @@ export function TileDragProvider({ const overGroupIndex = activeGroups.findIndex( (group) => group.id === over.id ); - onGroupsChange( - moveGroups(activeGroups, overGroupIndex, selectedIndices), - openGroupId + const newGroups = moveGroups( + activeGroups, + overGroupIndex, + selectedIndices ); + if (openGroupId) { + onSubgroupChange(newGroups as GroupItem[], openGroupId); + } else { + onGroupsChange(newGroups); + } } } diff --git a/src/contexts/TokenDataContext.tsx b/src/contexts/TokenDataContext.tsx index fadc712..19f5d6a 100644 --- a/src/contexts/TokenDataContext.tsx +++ b/src/contexts/TokenDataContext.tsx @@ -23,7 +23,7 @@ export type UpdateTokenEventHandler = ( export type GetTokenEventHandler = ( tokenId: string ) => Promise; -export type UpdateTokenGroupsEventHandler = (groups: any[]) => Promise; +export type UpdateTokenGroupsEventHandler = (groups: Group[]) => Promise; export type UpdateTokensHiddenEventHandler = ( ids: string[], hideInSidebar: boolean diff --git a/src/database.ts b/src/database.ts index 1874ec6..0c565da 100644 --- a/src/database.ts +++ b/src/database.ts @@ -2,7 +2,7 @@ import Dexie, { DexieOptions } from "dexie"; import { v4 as uuid } from "uuid"; -import { loadVersions } from "./upgrade"; +import { loadVersions, UpgradeEventHandler } from "./upgrade"; import { getDefaultMaps } from "./maps"; import { getDefaultTokens } from "./tokens"; @@ -10,7 +10,7 @@ import { getDefaultTokens } from "./tokens"; * Populate DB with initial data * @param {Dexie} db */ -function populate(db) { +function populate(db: Dexie) { db.on("populate", () => { const userId = uuid(); db.table("user").add({ key: "userId", value: userId }); @@ -35,16 +35,16 @@ function populate(db) { * @param {string=} name * @param {number=} versionNumber * @param {boolean=} populateData - * @param {import("./upgrade").OnUpgrade=} onUpgrade + * @param {UpgradeEventHandler=} onUpgrade * @returns {Dexie} */ export function getDatabase( options: DexieOptions, - name = "OwlbearRodeoDB", - versionNumber = undefined, - populateData = true, - onUpgrade = undefined -) { + name: string | undefined = "OwlbearRodeoDB", + versionNumber: number | undefined = undefined, + populateData: boolean | undefined = true, + onUpgrade: UpgradeEventHandler | undefined = undefined +): Dexie { let db = new Dexie(name, options); loadVersions(db, versionNumber, onUpgrade); if (populateData) { diff --git a/src/dice/Dice.ts b/src/dice/Dice.ts index 275680b..0264711 100644 --- a/src/dice/Dice.ts +++ b/src/dice/Dice.ts @@ -16,15 +16,13 @@ import d100Source from "./shared/d100.glb"; import { lerp } from "../helpers/shared"; import { importTextureAsync } from "../helpers/babylon"; +import { InstancedMesh, Material, Mesh, Scene } from "@babylonjs/core"; import { - BaseTexture, - InstancedMesh, - Material, - Mesh, - Scene, - Texture, -} from "@babylonjs/core"; -import { DiceType } from "../types/Dice"; + DiceType, + BaseDiceTextureSources, + isDiceMeshes, + DiceMeshes, +} from "../types/Dice"; const minDiceRollSpeed = 600; const maxDiceRollSpeed = 800; @@ -35,13 +33,11 @@ class Dice { static async loadMeshes( material: Material, scene: Scene, - sourceOverrides?: any - ): Promise> { - let meshes: any = {}; - const addToMeshes = async (type: string | number, defaultSource: any) => { - let source: string = sourceOverrides - ? sourceOverrides[type] - : defaultSource; + sourceOverrides?: Record + ): Promise { + let meshes: Partial = {}; + const addToMeshes = async (type: DiceType, defaultSource: string) => { + let source = sourceOverrides ? sourceOverrides[type] : defaultSource; const mesh = await this.loadMesh(source, material, scene); meshes[type] = mesh; }; @@ -54,12 +50,16 @@ class Dice { addToMeshes("d20", d20Source), addToMeshes("d100", d100Source), ]); - return meshes; + if (isDiceMeshes(meshes)) { + return meshes; + } else { + throw new Error("Dice meshes failed to load, missing mesh source"); + } } static async loadMesh(source: string, material: Material, scene: Scene) { let mesh = (await SceneLoader.ImportMeshAsync("", source, "", scene)) - .meshes[1]; + .meshes[1] as Mesh; mesh.setParent(null); mesh.material = material; @@ -69,19 +69,18 @@ class Dice { return mesh; } - static async loadMaterial(materialName: string, textures: any, scene: Scene) { + static async loadMaterial( + materialName: string, + textures: BaseDiceTextureSources, + scene: Scene + ) { let pbr = new PBRMaterial(materialName, scene); - let [albedo, normal, metalRoughness]: [ - albedo: BaseTexture, - normal: Texture, - metalRoughness: Texture - ] = await Promise.all([ + let [albedo, normal, metalRoughness] = await Promise.all([ importTextureAsync(textures.albedo), importTextureAsync(textures.normal), importTextureAsync(textures.metalRoughness), ]); pbr.albedoTexture = albedo; - // pbr.normalTexture = normal; pbr.bumpTexture = normal; pbr.metallicTexture = metalRoughness; pbr.useRoughnessFromMetallicTextureAlpha = false; @@ -98,12 +97,10 @@ class Dice { ) { let instance = mesh.createInstance(name); instance.position = mesh.position; - for (let child of mesh.getChildTransformNodes()) { - // TODO: type correctly another time -> should not be any - const locator: any = child.clone(child.name, instance); - // TODO: handle possible null value + for (let child of mesh.getChildMeshes()) { + const locator = child.clone(child.name, instance); if (!locator) { - throw Error; + throw new Error("Unable to clone dice locator"); } locator.setAbsolutePosition(child.getAbsolutePosition()); locator.name = child.name; @@ -120,7 +117,7 @@ class Dice { return instance; } - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { switch (diceType) { case "d4": return { mass: 4, friction: 4 }; @@ -133,7 +130,7 @@ class Dice { return { mass: 7, friction: 4 }; case "d12": return { mass: 8, friction: 4 }; - case "20": + case "d20": return { mass: 10, friction: 4 }; default: return { mass: 10, friction: 4 }; @@ -145,12 +142,14 @@ class Dice { instance.physicsImpostor?.setAngularVelocity(Vector3.Zero()); const scene = instance.getScene(); - // TODO: remove any typing in this function -> this is just to get it working - const diceTraySingle: any = scene.getNodeByID("dice_tray_single"); - const diceTrayDouble = scene.getNodeByID("dice_tray_double"); - const visibleDiceTray: any = diceTraySingle?.isVisible + const diceTraySingle = scene.getMeshByID("dice_tray_single"); + const diceTrayDouble = scene.getMeshByID("dice_tray_double"); + const visibleDiceTray = diceTraySingle?.isVisible ? diceTraySingle : diceTrayDouble; + if (!visibleDiceTray) { + throw new Error("No dice tray to roll in"); + } const trayBounds = visibleDiceTray?.getBoundingInfo().boundingBox; const position = new Vector3( diff --git a/src/dice/diceTray/DiceTray.ts b/src/dice/diceTray/DiceTray.ts index e2237bd..78ffd86 100644 --- a/src/dice/diceTray/DiceTray.ts +++ b/src/dice/diceTray/DiceTray.ts @@ -2,10 +2,9 @@ import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { PhysicsImpostor } from "@babylonjs/core/Physics/physicsImpostor"; import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { AbstractMesh, Scene, ShadowGenerator } from "@babylonjs/core"; -//@ts-ignore import singleMeshSource from "./single.glb"; -//@ts-ignore import doubleMeshSource from "./double.glb"; import singleAlbedo from "./singleAlbedo.jpg"; @@ -17,7 +16,6 @@ import doubleMetalRoughness from "./doubleMetalRoughness.jpg"; import doubleNormal from "./doubleNormal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; -import { Scene, ShadowGenerator, Texture } from "@babylonjs/core"; class DiceTray { _size; @@ -30,12 +28,12 @@ class DiceTray { this._size = newSize; const wallOffsetWidth = this.collisionSize / 2 + this.width / 2 - 0.5; const wallOffsetHeight = this.collisionSize / 2 + this.height / 2 - 0.5; - this.wallTop.position.z = -wallOffsetHeight; - this.wallRight.position.x = -wallOffsetWidth; - this.wallBottom.position.z = wallOffsetHeight; - this.wallLeft.position.x = wallOffsetWidth; - this.singleMesh.isVisible = newSize === "single"; - this.doubleMesh.isVisible = newSize === "double"; + if (this.wallTop) this.wallTop.position.z = -wallOffsetHeight; + if (this.wallRight) this.wallRight.position.x = -wallOffsetWidth; + if (this.wallBottom) this.wallBottom.position.z = wallOffsetHeight; + if (this.wallLeft) this.wallLeft.position.x = wallOffsetWidth; + if (this.singleMesh) this.singleMesh.isVisible = newSize === "single"; + if (this.doubleMesh) this.doubleMesh.isVisible = newSize === "double"; } scene; @@ -44,17 +42,21 @@ class DiceTray { get width() { return this.size === "single" ? 10 : 20; } - + height = 20; collisionSize = 50; - wallTop: any; - wallRight: any; - wallBottom: any; - wallLeft: any; - singleMesh: any; - doubleMesh: any; + wallTop?: Mesh; + wallRight?: Mesh; + wallBottom?: Mesh; + wallLeft?: Mesh; + singleMesh?: AbstractMesh; + doubleMesh?: AbstractMesh; - constructor(initialSize: string, scene: Scene, shadowGenerator: ShadowGenerator) { + constructor( + initialSize: string, + scene: Scene, + shadowGenerator: ShadowGenerator + ) { this._size = initialSize; this.scene = scene; this.shadowGenerator = shadowGenerator; @@ -65,7 +67,13 @@ class DiceTray { await this.loadMeshes(); } - createCollision(name: string, x: number, y: number, z: number, friction: number) { + createCollision( + name: string, + x: number, + y: number, + z: number, + friction: number + ): Mesh { let collision = Mesh.CreateBox( name, this.collisionSize, @@ -134,15 +142,6 @@ class DiceTray { doubleAlbedoTexture, doubleNormalTexture, doubleMetalRoughnessTexture, - ]: [ - singleMeshes: any, - doubleMeshes: any, - singleAlbedoTexture: Texture, - singleNormalTexture: Texture, - singleMetalRoughnessTexture: Texture, - doubleAlbedoTexture: Texture, - doubleNormalTexture: Texture, - doubleMetalRoughnessTexture: Texture ] = await Promise.all([ SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene), SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene), @@ -159,8 +158,6 @@ class DiceTray { this.singleMesh.name = "dice_tray"; let singleMaterial = new PBRMaterial("dice_tray_mat_single", this.scene); singleMaterial.albedoTexture = singleAlbedoTexture; - // TODO: ask Mitch about texture - // singleMaterial.normalTexture = singleNormalTexture; singleMaterial.bumpTexture = singleNormalTexture; singleMaterial.metallicTexture = singleMetalRoughnessTexture; singleMaterial.useRoughnessFromMetallicTextureAlpha = false; @@ -177,8 +174,6 @@ class DiceTray { this.doubleMesh.name = "dice_tray"; let doubleMaterial = new PBRMaterial("dice_tray_mat_double", this.scene); doubleMaterial.albedoTexture = doubleAlbedoTexture; - // TODO: ask Mitch about texture - //doubleMaterial.normalTexture = doubleNormalTexture; doubleMaterial.bumpTexture = doubleNormalTexture; doubleMaterial.metallicTexture = doubleMetalRoughnessTexture; doubleMaterial.useRoughnessFromMetallicTextureAlpha = false; diff --git a/src/dice/galaxy/GalaxyDice.ts b/src/dice/galaxy/GalaxyDice.ts index 9947f27..c05de32 100644 --- a/src/dice/galaxy/GalaxyDice.ts +++ b/src/dice/galaxy/GalaxyDice.ts @@ -1,12 +1,14 @@ -import { InstancedMesh, Material, Mesh, Scene } from "@babylonjs/core"; +import { InstancedMesh, Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class GalaxyDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; static async load(scene: Scene) { @@ -22,8 +24,7 @@ class GalaxyDice extends Dice { } } - // TODO: check static -> rename function? - static createInstance(diceType: string, scene: Scene): InstancedMesh { + static createInstance(diceType: DiceType, scene: Scene): InstancedMesh { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/gemstone/GemstoneDice.ts b/src/dice/gemstone/GemstoneDice.ts index b6a2043..bc4653b 100644 --- a/src/dice/gemstone/GemstoneDice.ts +++ b/src/dice/gemstone/GemstoneDice.ts @@ -1,5 +1,6 @@ import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { Color3 } from "@babylonjs/core/Maths/math"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; @@ -8,18 +9,22 @@ import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { BaseDiceTextureSources, DiceMeshes, DiceType } from "../../types/Dice"; class GemstoneDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.5, friction: properties.friction }; } - static async loadMaterial(materialName: string, textures: any, scene: Scene) { + static async loadMaterial( + materialName: string, + textures: BaseDiceTextureSources, + scene: Scene + ) { let pbr = new PBRMaterial(materialName, scene); let [albedo, normal, metalRoughness] = await Promise.all([ importTextureAsync(textures.albedo), @@ -27,7 +32,6 @@ class GemstoneDice extends Dice { importTextureAsync(textures.metalRoughness), ]); pbr.albedoTexture = albedo; - // TODO: ask Mitch about texture pbr.bumpTexture = normal; pbr.metallicTexture = metalRoughness; pbr.useRoughnessFromMetallicTextureAlpha = false; @@ -56,7 +60,7 @@ class GemstoneDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/glass/GlassDice.ts b/src/dice/glass/GlassDice.ts index a745487..a631048 100644 --- a/src/dice/glass/GlassDice.ts +++ b/src/dice/glass/GlassDice.ts @@ -1,5 +1,6 @@ import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; import { Color3 } from "@babylonjs/core/Maths/math"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; @@ -8,18 +9,28 @@ import mask from "./mask.png"; import normal from "./normal.jpg"; import { importTextureAsync } from "../../helpers/babylon"; -import { Material, Mesh, Scene } from "@babylonjs/core"; + +import { BaseDiceTextureSources, DiceMeshes, DiceType } from "../../types/Dice"; + +type GlassDiceTextureSources = Pick< + BaseDiceTextureSources, + "albedo" | "normal" +> & { mask: string }; class GlassDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.5, friction: properties.friction }; } - static async loadMaterial(materialName: string, textures: any, scene: Scene) { + static async loadGlassMaterial( + materialName: string, + textures: GlassDiceTextureSources, + scene: Scene + ) { let pbr = new PBRMaterial(materialName, scene); let [albedo, normal, mask] = await Promise.all([ importTextureAsync(textures.albedo), @@ -27,7 +38,6 @@ class GlassDice extends Dice { importTextureAsync(textures.mask), ]); pbr.albedoTexture = albedo; - // pbr.normalTexture = normal; pbr.bumpTexture = normal; pbr.roughness = 0.25; pbr.metallic = 0; @@ -47,7 +57,7 @@ class GlassDice extends Dice { static async load(scene: Scene) { if (!this.material) { - this.material = await this.loadMaterial( + this.material = await this.loadGlassMaterial( "glass_pbr", { albedo, mask, normal }, scene @@ -58,7 +68,7 @@ class GlassDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/iron/IronDice.ts b/src/dice/iron/IronDice.ts index 2e9c5b7..c9a7be8 100644 --- a/src/dice/iron/IronDice.ts +++ b/src/dice/iron/IronDice.ts @@ -1,15 +1,17 @@ -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class IronDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 2, friction: properties.friction }; } @@ -27,7 +29,7 @@ class IronDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/nebula/NebulaDice.ts b/src/dice/nebula/NebulaDice.ts index eb08d26..662b569 100644 --- a/src/dice/nebula/NebulaDice.ts +++ b/src/dice/nebula/NebulaDice.ts @@ -1,12 +1,14 @@ -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class NebulaDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; static async load(scene: Scene) { @@ -22,7 +24,7 @@ class NebulaDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/sunrise/SunriseDice.ts b/src/dice/sunrise/SunriseDice.ts index 233886c..2d1df72 100644 --- a/src/dice/sunrise/SunriseDice.ts +++ b/src/dice/sunrise/SunriseDice.ts @@ -1,12 +1,14 @@ -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class SunriseDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; static async load(scene: Scene) { @@ -22,7 +24,7 @@ class SunriseDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/sunset/SunsetDice.ts b/src/dice/sunset/SunsetDice.ts index 7e66a20..bfa107f 100644 --- a/src/dice/sunset/SunsetDice.ts +++ b/src/dice/sunset/SunsetDice.ts @@ -1,12 +1,14 @@ -import { Material, Mesh, Scene } from "@babylonjs/core"; +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; import metalRoughness from "./metalRoughness.jpg"; import normal from "./normal.jpg"; +import { DiceMeshes, DiceType } from "../../types/Dice"; + class SunsetDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; static async load(scene: Scene) { @@ -22,7 +24,7 @@ class SunsetDice extends Dice { } } - static createInstance(diceType: string, scene: Scene) { + static createInstance(diceType: DiceType, scene: Scene) { if (!this.material || !this.meshes) { throw Error("Dice not loaded, call load before creating an instance"); } diff --git a/src/dice/walnut/WalnutDice.ts b/src/dice/walnut/WalnutDice.ts index 9813f11..c489723 100644 --- a/src/dice/walnut/WalnutDice.ts +++ b/src/dice/walnut/WalnutDice.ts @@ -1,3 +1,4 @@ +import { Material, Scene } from "@babylonjs/core"; import Dice from "../Dice"; import albedo from "./albedo.jpg"; @@ -11,8 +12,7 @@ import d10Source from "./d10.glb"; import d12Source from "./d12.glb"; import d20Source from "./d20.glb"; import d100Source from "./d100.glb"; -import { Material, Mesh, Scene } from "@babylonjs/core"; -import { DiceType } from "../../types/Dice"; +import { DiceMeshes, DiceType } from "../../types/Dice"; const sourceOverrides = { d4: d4Source, @@ -25,10 +25,10 @@ const sourceOverrides = { }; class WalnutDice extends Dice { - static meshes: Record; + static meshes: DiceMeshes; static material: Material; - static getDicePhysicalProperties(diceType: string) { + static getDicePhysicalProperties(diceType: DiceType) { let properties = super.getDicePhysicalProperties(diceType); return { mass: properties.mass * 1.4, friction: properties.friction }; } diff --git a/src/global.d.ts b/src/global.d.ts index 8e26e5a..3d962a4 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -2,8 +2,20 @@ declare module "pepjs"; declare module "socket.io-msgpack-parser"; declare module "fake-indexeddb"; declare module "fake-indexeddb/lib/FDBKeyRange"; -declare module "*.glb"; -declare module "*.png"; -declare module "*.mp4"; -declare module "*.bin"; +declare module "*.glb" { + const source: string; + export default source; +} +declare module "*.png" { + const source: string; + export default source; +} +declare module "*.mp4" { + const source: string; + export default source; +} +declare module "*.bin" { + const source: string; + export default source; +} declare module "react-router-hash-link"; diff --git a/src/helpers/FakeStorage.ts b/src/helpers/FakeStorage.ts index 48fcd7f..b4af379 100644 --- a/src/helpers/FakeStorage.ts +++ b/src/helpers/FakeStorage.ts @@ -2,11 +2,11 @@ * A faked local or session storage used when the user has disabled storage */ class FakeStorage { - data: { [keyName: string ]: any} = {}; + data: { [keyName: string]: any } = {}; key(index: number) { return Object.keys(this.data)[index] || null; } - getItem(keyName: string ) { + getItem(keyName: string) { return this.data[keyName] || null; } setItem(keyName: string, keyValue: any) { diff --git a/src/helpers/Vector2.ts b/src/helpers/Vector2.ts index 8e727db..f3f6e37 100644 --- a/src/helpers/Vector2.ts +++ b/src/helpers/Vector2.ts @@ -153,36 +153,51 @@ class Vector2 { } /** - * Returns the min of `value` and `minimum`, if `minimum` is undefined component wise min is returned instead + * Returns the min of `a` and `b` * @param {Vector2} a - * @param {(Vector2 | number)} [minimum] Value to compare - * @returns {(Vector2 | number)} + * @param {Vector2 | number} b Value to compare + * @returns {Vector2} */ - static min(a: Vector2, minimum?: Vector2 | number): Vector2 | number { - if (minimum === undefined) { - return a.x < a.y ? a.x : a.y; - } else if (typeof minimum === "number") { - return { x: Math.min(a.x, minimum), y: Math.min(a.y, minimum) }; + static min(a: Vector2, b: Vector2 | number): Vector2 { + if (typeof b === "number") { + return { x: Math.min(a.x, b), y: Math.min(a.y, b) }; } else { - return { x: Math.min(a.x, minimum.x), y: Math.min(a.y, minimum.y) }; + return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y) }; } } + /** - * Returns the max of `a` and `maximum`, if `maximum` is undefined component wise max is returned instead + * Returns the component wise minimum of `a` * @param {Vector2} a - * @param {(Vector2 | number)} [maximum] Value to compare - * @returns {(Vector2 | number)} + * @returns {number} */ - static max(a: Vector2, maximum?: Vector2 | number): Vector2 | number { - if (maximum === undefined) { - return a.x > a.y ? a.x : a.y; - } else if (typeof maximum === "number") { - return { x: Math.max(a.x, maximum), y: Math.max(a.y, maximum) }; + static componentMin(a: Vector2): number { + return a.x < a.y ? a.x : a.y; + } + + /** + * Returns the max of `a` and `b` + * @param {Vector2} a + * @param {Vector2 | number} b Value to compare + * @returns {Vector2} + */ + static max(a: Vector2, b: Vector2 | number): Vector2 { + if (typeof b === "number") { + return { x: Math.max(a.x, b), y: Math.max(a.y, b) }; } else { - return { x: Math.max(a.x, maximum.x), y: Math.max(a.y, maximum.y) }; + return { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y) }; } } + /** + * Returns the component wise maximum of `a` + * @param {Vector2} a + * @returns {number)} + */ + static componentMax(a: Vector2): number { + return a.x > a.y ? a.x : a.y; + } + /** * Rounds `p` to the nearest value of `to` * @param {Vector2} p diff --git a/src/helpers/actions.ts b/src/helpers/actions.ts index a1870b8..a50b77f 100644 --- a/src/helpers/actions.ts +++ b/src/helpers/actions.ts @@ -1,20 +1,31 @@ +import { MultiPolygon, Ring, Polygon, Geom } from "polygon-clipping"; import shortid from "shortid"; +import { Fog, FogState } from "../types/Fog"; -export function addPolygonDifferenceToShapes(shape: any, difference: any, shapes: any) { +export function addPolygonDifferenceToFog( + fog: Fog, + difference: MultiPolygon, + shapes: FogState +) { for (let i = 0; i < difference.length; i++) { let newId = shortid.generate(); // Holes detected let holes = []; if (difference[i].length > 1) { for (let j = 1; j < difference[i].length; j++) { - holes.push(difference[i][j].map(([x, y]: [ x: number, y: number ]) => ({ x, y }))); + holes.push( + difference[i][j].map(([x, y]: [x: number, y: number]) => ({ x, y })) + ); } } - const points = difference[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y })); + const points = difference[i][0].map(([x, y]: [x: number, y: number]) => ({ + x, + y, + })); shapes[newId] = { - ...shape, + ...fog, id: newId, data: { points, @@ -24,11 +35,18 @@ export function addPolygonDifferenceToShapes(shape: any, difference: any, shapes } } -export function addPolygonIntersectionToShapes(shape: any, intersection: any, shapes: any) { +export function addPolygonIntersectionToFog( + shape: Fog, + intersection: MultiPolygon, + shapes: FogState +) { for (let i = 0; i < intersection.length; i++) { let newId = shortid.generate(); - const points = intersection[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y })); + const points = intersection[i][0].map(([x, y]: [x: number, y: number]) => ({ + x, + y, + })); shapes[newId] = { ...shape, @@ -43,9 +61,9 @@ export function addPolygonIntersectionToShapes(shape: any, intersection: any, sh } } -export function shapeToGeometry(shape) { - const shapePoints = shape.data.points.map(({ x, y }) => [x, y]); - const shapeHoles = shape.data.holes.map((hole) => +export function fogToGeometry(fog: Fog): Geom { + const shapePoints: Ring = fog.data.points.map(({ x, y }) => [x, y]); + const shapeHoles: Polygon = fog.data.holes.map((hole) => hole.map(({ x, y }) => [x, y]) ); return [[shapePoints, ...shapeHoles]]; diff --git a/src/helpers/blobToBuffer.ts b/src/helpers/blobToBuffer.ts index 905f5d5..f97396a 100644 --- a/src/helpers/blobToBuffer.ts +++ b/src/helpers/blobToBuffer.ts @@ -7,16 +7,12 @@ async function blobToBuffer(blob: Blob): Promise { const arrayBuffer = await blob.arrayBuffer(); return new Uint8Array(arrayBuffer); } else { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const reader = new FileReader(); - function onLoadEnd(event: any) { + function onLoadEnd() { reader.removeEventListener("loadend", onLoadEnd, false); - if (event.error) { - reject(event.error); - } else { - resolve(Buffer.from(reader.result as ArrayBuffer)); - } + resolve(Buffer.from(reader.result as ArrayBuffer)); } reader.addEventListener("loadend", onLoadEnd, false); diff --git a/src/helpers/dexie.js b/src/helpers/dexie.js deleted file mode 100644 index 9c82927..0000000 --- a/src/helpers/dexie.js +++ /dev/null @@ -1,48 +0,0 @@ -import set from "lodash.set"; -import unset from "lodash.unset"; -import cloneDeep from "lodash.clonedeep"; - -/** - * Remove all empty values from an object recursively - * @param {Object} obj - */ -function trimArraysInObject(obj) { - for (let key in obj) { - const value = obj[key]; - if (Array.isArray(value)) { - let arr = []; - for (let i = 0; i < value.length; i++) { - const el = value[i]; - if (typeof el === "object") { - arr.push(trimArraysInObject(el)); - } else if (el !== undefined) { - arr.push(el); - } - } - obj[key] = arr; - } else if (typeof obj[key] === "object") { - obj[key] = trimArraysInObject(obj[key]); - } - } - return obj; -} - -export function applyObservableChange(change) { - // Custom application of dexie change to fix issue with array indices being wrong - // https://github.com/dfahlander/Dexie.js/issues/1176 - // TODO: Fix dexie observable source - let obj = cloneDeep(change.oldObj); - const changes = Object.entries(change.mods).reverse(); - for (let [key, value] of changes) { - if (value === null) { - unset(obj, key); - } else { - obj = set(obj, key, value); - } - } - - // Trim empty values from calling unset on arrays - obj = trimArraysInObject(obj); - - return obj; -} diff --git a/src/helpers/dice.ts b/src/helpers/dice.ts index 8cb086d..1251506 100644 --- a/src/helpers/dice.ts +++ b/src/helpers/dice.ts @@ -1,13 +1,15 @@ +import { InstancedMesh, TransformNode } from "@babylonjs/core"; import { Vector3 } from "@babylonjs/core/Maths/math"; -import { DiceRoll } from "../types/Dice"; + +import { DiceMesh, DiceRoll } from "../types/Dice"; /** * Find the number facing up on a mesh instance of a dice * @param {Object} instance The dice instance */ -export function getDiceInstanceRoll(instance: any) { +export function getDiceInstanceRoll(instance: InstancedMesh) { let highestDot = -1; - let highestLocator; + let highestLocator: TransformNode | undefined = undefined; for (let locator of instance.getChildTransformNodes()) { let dif = locator .getAbsolutePosition() @@ -19,17 +21,19 @@ export function getDiceInstanceRoll(instance: any) { highestLocator = locator; } } + if (!highestLocator) { + return 0; + } return parseInt(highestLocator.name.slice(12)); } /** * Find the number facing up on a dice object - * @param {Object} dice The Dice object */ -export function getDiceRoll(dice: any) { +export function getDiceRoll(dice: DiceMesh) { let number = getDiceInstanceRoll(dice.instance); // If the dice is a d100 add the d10 - if (dice.type === "d100") { + if (dice.d10Instance) { const d10Number = getDiceInstanceRoll(dice.d10Instance); // Both zero set to 100 if (d10Number === 0 && number === 0) { @@ -44,7 +48,7 @@ export function getDiceRoll(dice: any) { } export function getDiceRollTotal(diceRolls: DiceRoll[]) { - return diceRolls.reduce((accumulator: number, dice: any) => { + return diceRolls.reduce((accumulator: number, dice) => { if (dice.roll === "unknown") { return accumulator; } else { diff --git a/src/helpers/diff.ts b/src/helpers/diff.ts index 784478e..9eee365 100644 --- a/src/helpers/diff.ts +++ b/src/helpers/diff.ts @@ -1,7 +1,7 @@ -import { applyChange, Diff, revertChange, diff as deepDiff }from "deep-diff"; +import { applyChange, Diff, revertChange, diff as deepDiff } from "deep-diff"; import get from "lodash.get"; -export function applyChanges(target: LHS, changes: Diff[]) { +export function applyChanges(target: LHS, changes: Diff[]) { for (let change of changes) { if (change.path && (change.kind === "E" || change.kind === "A")) { // If editing an object or array ensure that the value exists @@ -15,7 +15,7 @@ export function applyChanges(target: LHS, changes: Diff[]) { } } -export function revertChanges(target: LHS, changes: Diff[]) { +export function revertChanges(target: LHS, changes: Diff[]) { for (let change of changes) { revertChange(target, true, change); } diff --git a/src/helpers/drawing.ts b/src/helpers/drawing.ts index 92b04db..a4d2dee 100644 --- a/src/helpers/drawing.ts +++ b/src/helpers/drawing.ts @@ -238,17 +238,6 @@ export function getFogShapesBoundingBoxes( return boxes; } -/** - * @typedef Edge - * @property {Vector2} start - * @property {Vector2} end - */ - -// type Edge = { -// start: Vector2, -// end: Vector2 -// } - /** * @typedef Guide * @property {Vector2} start @@ -257,7 +246,7 @@ export function getFogShapesBoundingBoxes( * @property {number} distance */ -type Guide = { +export type Guide = { start: Vector2; end: Vector2; orientation: "horizontal" | "vertical"; diff --git a/src/helpers/grid.ts b/src/helpers/grid.ts index 52b906b..7c329f6 100644 --- a/src/helpers/grid.ts +++ b/src/helpers/grid.ts @@ -4,6 +4,7 @@ import Vector2 from "./Vector2"; import Size from "./Size"; import { logError } from "./logging"; +import { Grid, GridInset, GridScale } from "../types/Grid"; const SQRT3 = 1.73205; const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented"); @@ -180,7 +181,10 @@ export function getCellCorners( * @param {number} gridWidth Width of the grid in pixels after inset * @returns {number} */ -function getGridHeightFromWidth(grid: Grid, gridWidth: number): number { +function getGridHeightFromWidth( + grid: Pick, + gridWidth: number +): number { switch (grid.type) { case "square": return (grid.size.y * gridWidth) / grid.size.x; @@ -203,7 +207,7 @@ function getGridHeightFromWidth(grid: Grid, gridWidth: number): number { * @returns {GridInset} */ export function getGridDefaultInset( - grid: Grid, + grid: Pick, mapWidth: number, mapHeight: number ): GridInset { @@ -220,7 +224,7 @@ export function getGridDefaultInset( * @returns {GridInset} */ export function getGridUpdatedInset( - grid: Required, + grid: Grid, mapWidth: number, mapHeight: number ): GridInset { @@ -303,7 +307,7 @@ export function hexOffsetToCube( * @param {Size} cellSize */ export function gridDistance( - grid: Required, + grid: Grid, a: Vector2, b: Vector2, cellSize: Size @@ -313,12 +317,14 @@ export function gridDistance( const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize); if (grid.type === "square") { if (grid.measurement.type === "chebyshev") { - return Vector2.max(Vector2.abs(Vector2.subtract(aCoord, bCoord))); + return Vector2.componentMax( + Vector2.abs(Vector2.subtract(aCoord, bCoord)) + ); } else if (grid.measurement.type === "alternating") { // Alternating diagonal distance like D&D 3.5 and Pathfinder const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord)); - const max: any = Vector2.max(delta); - const min: any = Vector2.min(delta); + const max = Vector2.componentMax(delta); + const min = Vector2.componentMin(delta); return max - min + Math.floor(1.5 * min); } else if (grid.measurement.type === "euclidean") { return Vector2.magnitude( @@ -434,17 +440,16 @@ export function gridSizeVaild(x: number, y: number): boolean { /** * Finds a grid size for an image by finding the closest size to the average grid size - * @param {Image} image + * @param {HTMLImageElement} image * @param {number[]} candidates * @returns {Vector2 | null} */ function gridSizeHeuristic( - image: CanvasImageSource, + image: HTMLImageElement, candidates: number[] ): Vector2 | null { - // TODO: check type for Image and CanvasSourceImage - const width: any = image.width; - const height: any = image.height; + const width = image.width; + const height = image.height; // Find the best candidate by comparing the absolute z-scores of each axis let bestX = 1; let bestY = 1; @@ -470,17 +475,17 @@ function gridSizeHeuristic( /** * Finds the grid size of an image by running the image through a machine learning model - * @param {Image} image + * @param {HTMLImageElement} image * @param {number[]} candidates * @returns {Vector2 | null} */ async function gridSizeML( - image: CanvasImageSource, + image: HTMLImageElement, candidates: number[] ): Promise { // TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match - const width: any = image.width; - const height: any = image.height; + const width = image.width; + const height = image.height; const ratio = width / height; let canvas = document.createElement("canvas"); let context = canvas.getContext("2d"); @@ -545,12 +550,12 @@ async function gridSizeML( /** * Finds the grid size of an image by either using a ML model or falling back to a heuristic - * @param {Image} image + * @param {HTMLImageElement} image * @returns {Vector2} */ -export async function getGridSizeFromImage(image: CanvasImageSource) { - const width: any = image.width; - const height: any = image.height; +export async function getGridSizeFromImage(image: HTMLImageElement) { + const width = image.width; + const height = image.height; const candidates = dividers(width, height); let prediction; diff --git a/src/helpers/group.ts b/src/helpers/group.ts index 17103c3..ccbd0d3 100644 --- a/src/helpers/group.ts +++ b/src/helpers/group.ts @@ -197,10 +197,12 @@ export function findGroup(groups: Group[], groupId: string): Group | undefined { /** * Transform and item array to a record of item ids to item names */ -export function getItemNames(items: any[], itemKey: string = "id") { +export function getItemNames( + items: Item[] +) { let names: Record = {}; for (let item of items) { - names[item[itemKey]] = item.name; + names[item.id] = item.name; } return names; } diff --git a/src/helpers/konva.tsx b/src/helpers/konva.tsx index 7eb8edc..776d4a6 100644 --- a/src/helpers/konva.tsx +++ b/src/helpers/konva.tsx @@ -1,14 +1,23 @@ import React, { useState, useEffect, useRef } from "react"; -import { Line, Group, Path, Circle } from "react-konva"; -// eslint-disable-next-line no-unused-vars import Konva from "konva"; +import { Line, Group, Path, Circle } from "react-konva"; +import { LineConfig } from "konva/types/shapes/Line"; import Color from "color"; + import Vector2 from "./Vector2"; +type HoleyLineProps = { + holes: number[][]; +} & LineConfig; + // Holes should be wound in the opposite direction as the containing points array -export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) { +export function HoleyLine({ holes, ...props }: HoleyLineProps) { // Converted from https://github.com/rfestag/konva/blob/master/src/shapes/Line.ts - function drawLine(points: number[], context: any, shape: any) { + function drawLine( + points: number[], + context: Konva.Context, + shape: Konva.Line + ) { const length = points.length; const tension = shape.tension(); const closed = shape.closed(); @@ -76,7 +85,7 @@ export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) { } // Draw points and holes - function sceneFunc(context: any, shape: any) { + function sceneFunc(context: Konva.Context, shape: Konva.Line) { const points = shape.points(); const closed = shape.closed(); @@ -106,22 +115,18 @@ export function HoleyLine({ holes, ...props }: { holes: any; props: [] }) { } } - return ; + return ; } -export function Tick({ - x, - y, - scale, - onClick, - cross, -}: { - x: any; - y: any; - scale: any; - onClick: any; - cross: any; -}) { +type TickProps = { + x: number; + y: number; + scale: number; + onClick: (evt: Konva.KonvaEventObject) => void; + cross: boolean; +}; + +export function Tick({ x, y, scale, onClick, cross }: TickProps) { const [fill, setFill] = useState("white"); function handleEnter() { setFill("hsl(260, 100%, 80%)"); @@ -160,19 +165,21 @@ interface TrailPoint extends Vector2 { lifetime: number; } +type TrailProps = { + position: Vector2; + size: number; + duration: number; + segments: number; + color: string; +}; + export function Trail({ position, size, duration, segments, color, -}: { - position: Vector2; - size: any; - duration: number; - segments: any; - color: string; -}) { +}: TrailProps) { const trailRef: React.MutableRefObject = useRef(); const pointsRef: React.MutableRefObject = useRef([]); const prevPositionRef = useRef(position); @@ -206,7 +213,7 @@ export function Trail({ useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(animate); - function animate(time: any) { + function animate(time: number) { request = requestAnimationFrame(animate); const deltaTime = time - prevTime; prevTime = time; @@ -243,14 +250,13 @@ export function Trail({ }, []); // Custom scene function for drawing a trail from a line - function sceneFunc(context: any) { + function sceneFunc(context: CanvasRenderingContext2D) { // Resample points to ensure a smooth trail const resampledPoints = Vector2.resample(pointsRef.current, segments); if (resampledPoints.length === 0) { return; } // Draws a line offset in the direction perpendicular to its travel direction - // TODO: check alpha type const drawOffsetLine = (from: Vector2, to: Vector2, alpha: number) => { const forward = Vector2.normalize(Vector2.subtract(from, to)); // Rotate the forward vector 90 degrees based off of the direction @@ -328,7 +334,7 @@ Trail.defaultProps = { */ export function getRelativePointerPosition( node: Konva.Node -): { x: number; y: number } | undefined { +): Vector2 | undefined { let transform = node.getAbsoluteTransform().copy(); transform.invert(); let position = node.getStage()?.getPointerPosition(); @@ -340,10 +346,9 @@ export function getRelativePointerPosition( export function getRelativePointerPositionNormalized( node: Konva.Node -): { x: number; y: number } | undefined { +): Vector2 | undefined { const relativePosition = getRelativePointerPosition(node); if (!relativePosition) { - // TODO: handle possible null value return; } return { @@ -357,8 +362,8 @@ export function getRelativePointerPositionNormalized( * @param {number[]} points points in an x, y alternating array * @returns {Vector2[]} a `Vector2` array */ -export function convertPointArray(points: number[]) { - return points.reduce((acc: any[], _, i, arr) => { +export function convertPointArray(points: number[]): Vector2[] { + return points.reduce((acc: Vector2[], _, i, arr) => { if (i % 2 === 0) { acc.push({ x: arr[i], y: arr[i + 1] }); } diff --git a/src/helpers/logging.ts b/src/helpers/logging.ts index bb547fe..b7c9a75 100644 --- a/src/helpers/logging.ts +++ b/src/helpers/logging.ts @@ -1,6 +1,6 @@ import { captureException } from "@sentry/react"; -export function logError(error: any): void { +export function logError(error: Error): void { console.error(error); if (process.env.REACT_APP_LOGGING === "true") { captureException(error); diff --git a/src/helpers/map.ts b/src/helpers/map.ts index 17241a7..13382a2 100644 --- a/src/helpers/map.ts +++ b/src/helpers/map.ts @@ -32,8 +32,6 @@ const mapResolutions: Resolution[] = [ /** * Get the asset id of the preview file to send for a map - * @param {any} map - * @returns {undefined|string} */ export function getMapPreviewAsset(map: Map): string | undefined { if (map.type === "file") { @@ -126,7 +124,7 @@ export async function createMapFromFile( ) { const resized = await resizeImage( image, - Vector2.max(resolutionPixelSize) as number, + Vector2.componentMax(resolutionPixelSize), file.type, resolution.quality ); diff --git a/src/helpers/select.tsx b/src/helpers/select.tsx deleted file mode 100644 index 8184add..0000000 --- a/src/helpers/select.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useEffect, useState } from "react"; -import Fuse from "fuse.js"; - -import { groupBy } from "./shared"; - -/** - * Helpers for the SelectMapModal and SelectTokenModal - */ - -// Helper for generating search results for items -export function useSearch(items: any[], search: string) { - // TODO: add types to search items -> don't like the never type - const [filteredItems, setFilteredItems]: [ - filteredItems: any, - setFilteredItems: any - ] = useState([]); - const [filteredItemScores, setFilteredItemScores]: [ - filteredItemScores: {}, - setFilteredItemScores: React.Dispatch> - ] = useState({}); - const [fuse, setFuse] = useState(); - - // Update search index when items change - useEffect(() => { - setFuse(new Fuse(items, { keys: ["name", "group"], includeScore: true })); - }, [items]); - - // Perform search when search changes - useEffect(() => { - if (search) { - const query = fuse?.search(search); - setFilteredItems(query?.map((result: any) => result.item)); - let reduceResult: {} | undefined = query?.reduce( - (acc: {}, value: any) => ({ ...acc, [value.item.id]: value.score }), - {} - ); - if (reduceResult) { - setFilteredItemScores(reduceResult); - } - } - }, [search, items, fuse]); - - return [filteredItems, filteredItemScores]; -} - -// Helper for grouping items -export function useGroup( - items: any[], - filteredItems: any[], - useFiltered: boolean, - filteredScores: any[] -) { - const itemsByGroup = groupBy(useFiltered ? filteredItems : items, "group"); - // Get the groups of the items sorting by the average score if we're filtering or the alphabetical order - // with "" at the start and "default" at the end if not - let itemGroups = Object.keys(itemsByGroup); - if (useFiltered) { - itemGroups.sort((a, b) => { - const aScore = itemsByGroup[a].reduce( - (acc: any, item: any) => (acc + filteredScores[item.id]) / 2 - ); - const bScore = itemsByGroup[b].reduce( - (acc: any, item: any) => (acc + filteredScores[item.id]) / 2 - ); - return aScore - bScore; - }); - } else { - itemGroups.sort((a, b) => { - if (a === "" || b === "default") { - return -1; - } - if (b === "" || a === "default") { - return 1; - } - return a.localeCompare(b); - }); - } - return [itemsByGroup, itemGroups]; -} - -// Helper for handling selecting items -export function handleItemSelect( - item: any, - selectMode: any, - selectedIds: string[], - setSelectedIds: any, - itemsByGroup: any, - itemGroups: any -) { - if (!item) { - setSelectedIds([]); - return; - } - switch (selectMode) { - case "single": - setSelectedIds([item.id]); - break; - case "multiple": - setSelectedIds((prev: any[]) => { - if (prev.includes(item.id)) { - return prev.filter((id: number) => id !== item.id); - } else { - return [...prev, item.id]; - } - }); - break; - case "range": - // Create items array - let items = itemGroups.reduce( - (acc: [], group: any) => [...acc, ...itemsByGroup[group]], - [] - ); - - // Add all items inbetween the previous selected item and the current selected - if (selectedIds.length > 0) { - const mapIndex = items.findIndex((m: any) => m.id === item.id); - const lastIndex = items.findIndex( - (m: any) => m.id === selectedIds[selectedIds.length - 1] - ); - let idsToAdd: string[] = []; - let idsToRemove: string[] = []; - const direction = mapIndex > lastIndex ? 1 : -1; - for ( - let i = lastIndex + direction; - direction < 0 ? i >= mapIndex : i <= mapIndex; - i += direction - ) { - const itemId: string = items[i].id; - if (selectedIds.includes(itemId)) { - idsToRemove.push(itemId); - } else { - idsToAdd.push(itemId); - } - } - setSelectedIds((prev: any[]) => { - let ids = [...prev, ...idsToAdd]; - return ids.filter((id) => !idsToRemove.includes(id)); - }); - } else { - setSelectedIds([item.id]); - } - break; - default: - setSelectedIds([]); - } -} diff --git a/src/helpers/shared.ts b/src/helpers/shared.ts index 438d166..d1a0773 100644 --- a/src/helpers/shared.ts +++ b/src/helpers/shared.ts @@ -23,8 +23,9 @@ export function fromEntries(iterable: Iterable<[string | number, any]>) { } // Check to see if all tracks are muted -export function isStreamStopped(stream: MediaStream) { - return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true }); +export function isStreamStopped(stream: MediaStream): boolean { + // TODO: Check what this thing actually does + return stream.getTracks().reduce((a, b) => a && b, { muted: true }).muted; } export function roundTo(x: number, to: number): number { @@ -62,9 +63,12 @@ export function isEmpty(obj: Object): boolean { return Object.keys(obj).length === 0 && obj.constructor === Object; } -export function keyBy(array: Type[], key: string): Record { +export function keyBy>( + array: Type[], + key: string +): Record { return array.reduce( - (prev: any, current: any) => ({ + (prev, current) => ({ ...prev, [key ? current[key] : current]: current, }), diff --git a/src/helpers/timer.ts b/src/helpers/timer.ts index 9c014d1..757bf8d 100644 --- a/src/helpers/timer.ts +++ b/src/helpers/timer.ts @@ -1,24 +1,14 @@ +import { Duration } from "../types/Timer"; + const MILLISECONDS_IN_HOUR = 3600000; const MILLISECONDS_IN_MINUTE = 60000; const MILLISECONDS_IN_SECOND = 1000; -/** - * @typedef Time - * @property {number} hour - * @property {number} minute - * @property {number} second - */ -type Time = { - hour: number, - minute: number, - second: number -} - /** * Returns a timers duration in milliseconds * @param {Time} t The object with an hour, minute and second property */ -export function getHMSDuration(t: Time) { +export function getHMSDuration(t: Duration): number { if (!t) { return 0; } @@ -33,7 +23,7 @@ export function getHMSDuration(t: Time) { * Returns an object with an hour, minute and second property * @param {number} duration The duration in milliseconds */ -export function getDurationHMS(duration: number) { +export function getDurationHMS(duration: number): Duration { let workingDuration = duration; const hour = Math.floor(workingDuration / MILLISECONDS_IN_HOUR); workingDuration -= hour * MILLISECONDS_IN_HOUR; diff --git a/src/hooks/useGridSnapping.tsx b/src/hooks/useGridSnapping.tsx index 3fc68a5..52676f2 100644 --- a/src/hooks/useGridSnapping.tsx +++ b/src/hooks/useGridSnapping.tsx @@ -73,7 +73,7 @@ function useGridSnapping( const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint); if ( distanceToSnapPoint < - (Vector2.min(gridCellPixelSize) as number) * gridSnappingSensitivity + Vector2.componentMin(gridCellPixelSize) * gridSnappingSensitivity ) { // Reverse grid offset let offsetSnapPoint = Vector2.add( diff --git a/src/hooks/useImageCenter.js b/src/hooks/useImageCenter.tsx similarity index 88% rename from src/hooks/useImageCenter.js rename to src/hooks/useImageCenter.tsx index 3ffa355..e8a86f6 100644 --- a/src/hooks/useImageCenter.js +++ b/src/hooks/useImageCenter.tsx @@ -1,5 +1,17 @@ import { useEffect, useRef } from "react"; +type useImageCenterProps = { + data: + stageRef: + stageWidth: number; + stageHeight: number; + stageTranslateRef: + setStageScale: + imageLayerRef: + containerRef: + responsive?: boolean +} + function useImageCenter( data, stageRef, @@ -14,8 +26,8 @@ function useImageCenter( const stageRatio = stageWidth / stageHeight; const imageRatio = data ? data.width / data.height : 1; - let imageWidth; - let imageHeight; + let imageWidth: number; + let imageHeight: number; if (stageRatio > imageRatio) { imageWidth = data ? stageHeight / (data.height / data.width) : stageWidth; imageHeight = stageHeight; diff --git a/src/hooks/useMapImage.js b/src/hooks/useMapImage.tsx similarity index 100% rename from src/hooks/useMapImage.js rename to src/hooks/useMapImage.tsx diff --git a/src/hooks/useNetworkedState.tsx b/src/hooks/useNetworkedState.tsx index fe7274c..294430e 100644 --- a/src/hooks/useNetworkedState.tsx +++ b/src/hooks/useNetworkedState.tsx @@ -1,39 +1,45 @@ import { useEffect, useState, useRef, useCallback } from "react"; import cloneDeep from "lodash.clonedeep"; +import { Diff } from "deep-diff"; import useDebounce from "./useDebounce"; import { diff, applyChanges } from "../helpers/diff"; import Session from "../network/Session"; /** - * @callback setNetworkedState - * @param {any} update The updated state or a state function passed into setState - * @param {boolean} sync Whether to sync the update with the session - * @param {boolean} force Whether to force a full update, usefull when partialUpdates is enabled + * @param update The updated state or a state function passed into setState + * @param sync Whether to sync the update with the session + * @param force Whether to force a full update, usefull when partialUpdates is enabled */ -// TODO: check parameter requirements here -type setNetworkedState = (update: any, sync?: boolean, force?: boolean) => void +export type SetNetworkedState = ( + update: React.SetStateAction, + sync?: boolean, + force?: boolean +) => void; + +type Update = { + id: string; + changes: Diff[]; +}; /** * Helper to sync a react state to a `Session` * - * @param {any} initialState + * @param {S} initialState * @param {Session} session `Session` instance * @param {string} eventName Name of the event to send to the session * @param {number} debounceRate Amount to debounce before sending to the session (ms) * @param {boolean} partialUpdates Allow sending of partial updates to the session * @param {string} partialUpdatesKey Key to lookup in the state to identify a partial update - * - * @returns {[any, setNetworkedState]} */ -function useNetworkedState( - initialState: any, +function useNetworkedState( + initialState: S, session: Session, eventName: string, debounceRate: number = 500, partialUpdates: boolean = true, partialUpdatesKey: string = "id" -): [any, setNetworkedState] { +): [S, SetNetworkedState] { const [state, _setState] = useState(initialState); // Used to control whether the state needs to be sent to the socket const dirtyRef = useRef(false); @@ -42,9 +48,9 @@ function useNetworkedState( const forceUpdateRef = useRef(false); // Update dirty at the same time as state - const setState = useCallback((update, sync = true, force = false) => { - dirtyRef.current = sync; - forceUpdateRef.current = force; + const setState = useCallback>((update, sync, force) => { + dirtyRef.current = sync || false; + forceUpdateRef.current = force || false; _setState(update); }, []); @@ -54,7 +60,7 @@ function useNetworkedState( }, [eventName]); const debouncedState = useDebounce(state, debounceRate); - const lastSyncedStateRef = useRef(); + const lastSyncedStateRef = useRef(); useEffect(() => { if (session.socket && dirtyRef.current) { // If partial updates enabled, send just the changes to the socket @@ -88,13 +94,13 @@ function useNetworkedState( ]); useEffect(() => { - function handleSocketEvent(data: any) { + function handleSocketEvent(data: S) { _setState(data); lastSyncedStateRef.current = data; } - function handleSocketUpdateEvent(update: any) { - _setState((prevState: any) => { + function handleSocketUpdateEvent(update: Update) { + _setState((prevState) => { if (prevState && prevState[partialUpdatesKey] === update.id) { let newState = { ...prevState }; applyChanges(newState, update.changes); diff --git a/src/ml/gridSize/GridSizeModel.ts b/src/ml/gridSize/GridSizeModel.ts index 25bde7e..565cf68 100644 --- a/src/ml/gridSize/GridSizeModel.ts +++ b/src/ml/gridSize/GridSizeModel.ts @@ -10,8 +10,7 @@ class GridSizeModel extends Model { static model: LayersModel; // Load tensorflow dynamically - // TODO: find type for tf - static tf: any; + static tf; constructor() { super(config as ModelJSON, { "group1-shard1of1.bin": weights }); } @@ -27,8 +26,7 @@ class GridSizeModel extends Model { } const model = GridSizeModel.model; - // TODO: check this mess -> changing type on prediction causes issues - const prediction: any = tf.tidy(() => { + const prediction = tf.tidy(() => { const image = tf.browser.fromPixels(imageData, 1).toFloat(); const normalized = image.div(tf.scalar(255.0)); const batched = tf.expandDims(normalized); diff --git a/src/modals/AddPartyMemberModal.tsx b/src/modals/AddPartyMemberModal.tsx index ece2f7f..e4ed209 100644 --- a/src/modals/AddPartyMemberModal.tsx +++ b/src/modals/AddPartyMemberModal.tsx @@ -8,7 +8,7 @@ function AddPartyMemberModal({ gameId, }: { isOpen: boolean; - onRequestClose: any; + onRequestClose; gameId: string; }) { return ( diff --git a/src/modals/ChangeNicknameModal.tsx b/src/modals/ChangeNicknameModal.tsx index b196dec..151c1bb 100644 --- a/src/modals/ChangeNicknameModal.tsx +++ b/src/modals/ChangeNicknameModal.tsx @@ -5,10 +5,10 @@ import Modal from "../components/Modal"; type ChangeNicknameModalProps = { isOpen: boolean; - onRequestClose: () => void; - onChangeSubmit: any; + onRequestClose; + onChangeSubmit; nickname: string; - onChange: any; + onChange; }; function ChangeNicknameModal({ diff --git a/src/modals/EditMapModal.tsx b/src/modals/EditMapModal.tsx index 8cc662b..a444eae 100644 --- a/src/modals/EditMapModal.tsx +++ b/src/modals/EditMapModal.tsx @@ -12,14 +12,18 @@ import { getGridDefaultInset } from "../helpers/grid"; import useResponsiveLayout from "../hooks/useResponsiveLayout"; import { Map } from "../types/Map"; import { MapState } from "../types/MapState"; +import { + UpdateMapEventHanlder, + UpdateMapStateEventHandler, +} from "../contexts/MapDataContext"; type EditMapProps = { isOpen: boolean; onDone: () => void; map: Map; mapState: MapState; - onUpdateMap: (id: string, update: Partial) => void; - onUpdateMapState: (id: string, update: Partial) => void; + onUpdateMap: UpdateMapEventHanlder; + onUpdateMapState: UpdateMapStateEventHandler; }; function EditMapModal({ @@ -48,52 +52,45 @@ function EditMapModal({ */ // Local cache of map setting changes // Applied when done is clicked or map selection is changed - const [mapSettingChanges, setMapSettingChanges] = useState({}); - const [mapStateSettingChanges, setMapStateSettingChanges] = useState({}); + const [mapSettingChanges, setMapSettingChanges] = useState>({}); + const [mapStateSettingChanges, setMapStateSettingChanges] = useState< + Partial + >({}); - function handleMapSettingsChange(key: string, value: string) { - setMapSettingChanges((prevChanges: any) => ({ + function handleMapSettingsChange(change: Partial) { + setMapSettingChanges((prevChanges) => ({ ...prevChanges, - [key]: value, - lastModified: Date.now(), + ...change, })); } - function handleMapStateSettingsChange(key: string, value: string) { - setMapStateSettingChanges((prevChanges: any) => ({ + function handleMapStateSettingsChange(change: Partial) { + setMapStateSettingChanges((prevChanges) => ({ ...prevChanges, - [key]: value, + ...change, })); } async function applyMapChanges() { if (!isEmpty(mapSettingChanges) || !isEmpty(mapStateSettingChanges)) { // Ensure grid values are positive - let verifiedChanges = { ...mapSettingChanges }; - if ("grid" in verifiedChanges && "size" in verifiedChanges.grid) { + let verifiedChanges: Partial = { ...mapSettingChanges }; + if (verifiedChanges.grid) { verifiedChanges.grid.size.x = verifiedChanges.grid.size.x || 1; verifiedChanges.grid.size.y = verifiedChanges.grid.size.y || 1; } // Ensure inset isn't flipped - if ("grid" in verifiedChanges && "inset" in verifiedChanges.grid) { + if (verifiedChanges.grid) { const inset = verifiedChanges.grid.inset; if ( inset.topLeft.x > inset.bottomRight.x || inset.topLeft.y > inset.bottomRight.y ) { - if ("size" in verifiedChanges.grid) { - verifiedChanges.grid.inset = getGridDefaultInset( - { size: verifiedChanges.grid.size, type: map.grid.type }, - map.width, - map.height - ); - } else { - verifiedChanges.grid.inset = getGridDefaultInset( - map.grid, - map.width, - map.height - ); - } + verifiedChanges.grid.inset = getGridDefaultInset( + { size: verifiedChanges.grid.size, type: map.grid.type }, + map.width, + map.height + ); } } await onUpdateMap(map.id, mapSettingChanges); diff --git a/src/modals/EditTokenModal.tsx b/src/modals/EditTokenModal.tsx index 655c7fe..cef1c27 100644 --- a/src/modals/EditTokenModal.tsx +++ b/src/modals/EditTokenModal.tsx @@ -43,8 +43,9 @@ function EditTokenModal({ Partial >({}); + // TODO: CHANGE MAP BACK? OR CHANGE THIS TO PARTIAL function handleTokenSettingsChange(key: string, value: Pick) { - setTokenSettingChanges((prevChanges: any) => ({ + setTokenSettingChanges((prevChanges) => ({ ...prevChanges, [key]: value, })); diff --git a/src/modals/ImportExportModal.tsx b/src/modals/ImportExportModal.tsx index 06d8b90..56ffa14 100644 --- a/src/modals/ImportExportModal.tsx +++ b/src/modals/ImportExportModal.tsx @@ -46,7 +46,7 @@ function ImportExportModal({ const [error, setError] = useState(); const backgroundTaskRunningRef = useRef(false); - const fileInputRef = useRef(); + const fileInputRef = useRef(); const [showImportSelector, setShowImportSelector] = useState(false); const [showExportSelector, setShowExportSelector] = useState(false); @@ -124,7 +124,7 @@ function ImportExportModal({ } useEffect(() => { - function handleBeforeUnload(event: any) { + function handleBeforeUnload(event) { if (backgroundTaskRunningRef.current) { event.returnValue = "Database is still processing, are you sure you want to leave?"; @@ -204,7 +204,7 @@ function ImportExportModal({ let newMaps: Map[] = []; let newStates: MapState[] = []; if (checkedMaps.length > 0) { - const mapIds = checkedMaps.map((map: any) => map.id); + const mapIds = checkedMaps.map((map) => map.id); const mapsToAdd = await importDB.table("maps").bulkGet(mapIds); for (let map of mapsToAdd) { let state: MapState = await importDB.table("states").get(map.id); @@ -257,7 +257,7 @@ function ImportExportModal({ const assetsToAdd = await importDB .table("assets") .bulkGet(Object.keys(newAssetIds)); - let newAssets: any[] = []; + let newAssets = []; for (let asset of assetsToAdd) { if (asset) { newAssets.push({ @@ -271,7 +271,7 @@ function ImportExportModal({ } // Add map groups with new ids - let newMapGroups: any[] = []; + let newMapGroups = []; if (checkedMapGroups.length > 0) { for (let group of checkedMapGroups) { if (group.type === "item") { @@ -290,7 +290,7 @@ function ImportExportModal({ } // Add token groups with new ids - let newTokenGroups: any[] = []; + let newTokenGroups = []; if (checkedTokenGroups.length > 0) { for (let group of checkedTokenGroups) { if (group.type === "item") { @@ -299,7 +299,7 @@ function ImportExportModal({ newTokenGroups.push({ ...group, id: uuid(), - items: group.items.map((item: any) => ({ + items: group.items.map((item) => ({ ...item, id: newTokenIds[item.id], })), diff --git a/src/modals/SelectMapModal.tsx b/src/modals/SelectMapModal.tsx index b21a4f3..4769a4b 100644 --- a/src/modals/SelectMapModal.tsx +++ b/src/modals/SelectMapModal.tsx @@ -81,7 +81,7 @@ function SelectMapModal({ * Image Upload */ - const fileInputRef = useRef(); + const fileInputRef = useRef(); const [isLoading, setIsLoading] = useState(false); const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = diff --git a/src/modals/SelectTokensModal.tsx b/src/modals/SelectTokensModal.tsx index 94bdb53..b170948 100644 --- a/src/modals/SelectTokensModal.tsx +++ b/src/modals/SelectTokensModal.tsx @@ -76,7 +76,7 @@ function SelectTokensModal({ * Image Upload */ - const fileInputRef = useRef(); + const fileInputRef = useRef(); const [isLoading, setIsLoading] = useState(false); const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = diff --git a/src/modals/StartModal.tsx b/src/modals/StartModal.tsx index e747810..1886dd4 100644 --- a/src/modals/StartModal.tsx +++ b/src/modals/StartModal.tsx @@ -38,7 +38,7 @@ function StartModal({ history.push(`/game/${shortid.generate()}`); } - const inputRef = useRef(); + const inputRef = useRef(); function focusInput() { inputRef.current && inputRef.current.focus(); } diff --git a/src/modals/StartTimerModal.tsx b/src/modals/StartTimerModal.tsx index 78c09b7..c70a280 100644 --- a/src/modals/StartTimerModal.tsx +++ b/src/modals/StartTimerModal.tsx @@ -28,7 +28,7 @@ function StartTimerModal({ onTimerStop, timer, }: StartTimerProps) { - const inputRef = useRef(); + const inputRef = useRef(); function focusInput() { inputRef.current && inputRef.current.focus(); } diff --git a/src/network/Connection.ts b/src/network/Connection.ts index 5fc6814..995ad9d 100644 --- a/src/network/Connection.ts +++ b/src/network/Connection.ts @@ -9,26 +9,26 @@ import blobToBuffer from "../helpers/blobToBuffer"; const MAX_BUFFER_SIZE = 16000; class Connection extends SimplePeer { - currentChunks: any; - dataChannels: any; + currentChunks; + dataChannels; - constructor(props: any) { + constructor(props) { super(props); - this.currentChunks = {} as Blob; + this.currentChunks = {}; this.dataChannels = {}; this.on("data", this.handleData); this.on("datachannel", this.handleDataChannel); } // Intercept the data event with decoding and chunking support - handleData(packed: any) { - const unpacked: any = decode(packed); + handleData(packed) { + const unpacked = decode(packed); // If the special property __chunked is set and true // The data is a partial chunk of the a larger file // So wait until all chunks are collected and assembled // before emitting the dataComplete event if (unpacked.__chunked) { - let chunk: any = this.currentChunks[unpacked.id] || { + let chunk = this.currentChunks[unpacked.id] || { data: [], count: 0, total: unpacked.total, @@ -65,7 +65,7 @@ class Connection extends SimplePeer { * @param {string=} channel * @param {string=} chunkId Optional ID to use for chunking */ - sendObject(object: any, channel?: string, chunkId?: string) { + sendObject(object, channel?: string, chunkId?: string) { try { const packedData = encode(object); const chunks = this.chunk(packedData, chunkId); @@ -83,7 +83,7 @@ class Connection extends SimplePeer { // Override the create data channel function to store our own named reference to it // and to use our custom data handler - createDataChannel(channelName: string, channelConfig: any, opts: any) { + createDataChannel(channelName: string, channelConfig, opts) { // TODO: resolve createDataChannel // @ts-ignore const channel = super.createDataChannel(channelName, channelConfig, opts); @@ -91,11 +91,11 @@ class Connection extends SimplePeer { return channel; } - handleDataChannel(channel: any) { + handleDataChannel(channel) { const channelName = channel.channelName; this.dataChannels[channelName] = channel; channel.on("data", this.handleData.bind(this)); - channel.on("error", (error: any) => { + channel.on("error", (error) => { this.emit("error", error); }); } diff --git a/src/network/NetworkedMapAndTokens.tsx b/src/network/NetworkedMapAndTokens.tsx index 5ce0f22..1474bd9 100644 --- a/src/network/NetworkedMapAndTokens.tsx +++ b/src/network/NetworkedMapAndTokens.tsx @@ -18,16 +18,40 @@ import Session from "./Session"; import Action from "../actions/Action"; -import Map, { - MapState, - Map as MapType, - TokenState, -} from "../components/map/Map"; +import Map from "../components/map/Map"; import TokenBar from "../components/token/TokenBar"; import GlobalImageDrop from "../components/image/GlobalImageDrop"; -const defaultMapActions = { +import { Map as MapType } from "../types/Map"; +import { MapState } from "../types/MapState"; +import { + Asset, + AssetManifest, + AssetManifestAsset, + AssetManifestAssets, +} from "../types/Asset"; +import { TokenState } from "../types/TokenState"; +import { Drawing, DrawingState } from "../types/Drawing"; +import { Fog, FogState } from "../types/Fog"; + +type MapActions = { + mapDrawActions: Action[]; + mapDrawActionIndex: number; + fogDrawActions: Action[]; + fogDrawActionIndex: number; +}; + +type MapActionsKey = keyof Pick< + MapActions, + "mapDrawActions" | "fogDrawActions" +>; +type MapActionsIndexKey = keyof Pick< + MapActions, + "mapDrawActionIndex" | "fogDrawActionIndex" +>; + +const defaultMapActions: MapActions = { mapDrawActions: [], mapDrawActionIndex: -1, fogDrawActions: [], @@ -51,26 +75,32 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { const { updateMapState } = useMapData(); const { getAsset, putAsset } = useAssets(); - const [currentMap, setCurrentMap] = useState(null); - const [currentMapState, setCurrentMapState]: [ - currentMapState: MapState, - setCurrentMapState: any - ] = useNetworkedState(null, session, "map_state", 500, true, "mapId"); - const [assetManifest, setAssetManifest] = useNetworkedState( - null, - session, - "manifest", - 500, - true, - "mapId" - ); + const [currentMap, setCurrentMap] = useState(null); + const [currentMapState, setCurrentMapState] = + useNetworkedState( + null, + session, + "map_state", + 500, + true, + "mapId" + ); + const [assetManifest, setAssetManifest] = + useNetworkedState( + null, + session, + "manifest", + 500, + true, + "mapId" + ); async function loadAssetManifestFromMap(map: MapType, mapState: MapState) { - const assets = {}; + const assets: AssetManifestAssets = {}; const { owner } = map; let processedTokens = new Set(); for (let tokenState of Object.values(mapState.tokens)) { - if (tokenState.file && !processedTokens.has(tokenState.file)) { + if (tokenState.type === "file" && !processedTokens.has(tokenState.file)) { processedTokens.add(tokenState.file); assets[tokenState.file] = { id: tokenState.file, @@ -80,9 +110,11 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } if (map.type === "file") { assets[map.thumbnail] = { id: map.thumbnail, owner }; - const qualityId = map.resolutions[map.quality]; - if (qualityId) { - assets[qualityId] = { id: qualityId, owner }; + if (map.quality !== "original") { + const qualityId = map.resolutions[map.quality]; + if (qualityId) { + assets[qualityId] = { id: qualityId, owner }; + } } else { assets[map.file] = { id: map.file, owner }; } @@ -90,8 +122,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { setAssetManifest({ mapId: map.id, assets }, true, true); } - function addAssetsIfNeeded(assets: any[]) { - setAssetManifest((prevManifest: any) => { + function addAssetsIfNeeded(assets: AssetManifestAsset[]) { + setAssetManifest((prevManifest) => { if (prevManifest?.assets) { let newAssets = { ...prevManifest.assets }; for (let asset of assets) { @@ -116,7 +148,10 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } async function requestAssetsIfNeeded() { - for (let asset of Object.values(assetManifest.assets) as any) { + if (!assetManifest) { + return; + } + for (let asset of Object.values(assetManifest.assets)) { if ( asset.owner === userId || requestingAssetsRef.current.has(asset.id) @@ -144,7 +179,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { if (cachedAsset) { requestingAssetsRef.current.delete(asset.id); - } else { + } else if (owner.sessionId) { assetLoadStart(asset.id); session.sendTo(owner.sessionId, "assetRequest", asset); } @@ -181,7 +216,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } }, [currentMap, debouncedMapState, userId, database, updateMapState]); - async function handleMapChange(newMap: any, newMapState: any) { + async function handleMapChange(newMap, newMapState) { // Clear map before sending new one setCurrentMap(null); session.socket?.emit("map", null); @@ -199,20 +234,20 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { await loadAssetManifestFromMap(newMap, newMapState); } - function handleMapReset(newMapState: any) { + function handleMapReset(newMapState) { setCurrentMapState(newMapState, true, true); setMapActions(defaultMapActions); } - const [mapActions, setMapActions] = useState(defaultMapActions); + const [mapActions, setMapActions] = useState(defaultMapActions); function addMapActions( - actions: Action[], - indexKey: string, - actionsKey: any, - shapesKey: any + actions: Action[], + indexKey: MapActionsIndexKey, + actionsKey: MapActionsKey, + shapesKey: "drawShapes" | "fogShapes" ) { - setMapActions((prevMapActions: any) => { + setMapActions((prevMapActions) => { const newActions = [ ...prevMapActions[actionsKey].slice(0, prevMapActions[indexKey] + 1), ...actions, @@ -225,39 +260,40 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { }; }); // Update map state by performing the actions on it - setCurrentMapState((prevMapState: any) => { - if (prevMapState) { - let shapes = prevMapState[shapesKey]; - for (let action of actions) { - shapes = action.execute(shapes); - } - return { - ...prevMapState, - [shapesKey]: shapes, - }; + setCurrentMapState((prevMapState) => { + if (!prevMapState) { + return prevMapState; } + let shapes = prevMapState[shapesKey]; + for (let action of actions) { + shapes = action.execute(shapes); + } + return { + ...prevMapState, + [shapesKey]: shapes, + }; }); } function updateActionIndex( - change: any, - indexKey: any, - actionsKey: any, - shapesKey: any + change, + indexKey: MapActionsIndexKey, + actionsKey: MapActionsKey, + shapesKey: "drawShapes" | "fogShapes" ) { - const prevIndex: any = mapActions[indexKey]; + const prevIndex = mapActions[indexKey]; const newIndex = Math.min( Math.max(mapActions[indexKey] + change, -1), mapActions[actionsKey].length - 1 ); - setMapActions((prevMapActions: Action[]) => ({ + setMapActions((prevMapActions) => ({ ...prevMapActions, [indexKey]: newIndex, })); // Update map state by either performing the actions or undoing them - setCurrentMapState((prevMapState: any) => { + setCurrentMapState((prevMapState) => { if (prevMapState) { let shapes = prevMapState[shapesKey]; if (prevIndex < newIndex) { @@ -283,7 +319,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { return newIndex; } - function handleMapDraw(action: Action) { + function handleMapDraw(action: Action) { addMapActions( [action], "mapDrawActionIndex", @@ -300,7 +336,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { updateActionIndex(1, "mapDrawActionIndex", "mapDrawActions", "drawShapes"); } - function handleFogDraw(action: Action) { + function handleFogDraw(action: Action) { addMapActions( [action], "fogDrawActionIndex", @@ -318,7 +354,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } // If map changes clear map actions - const previousMapIdRef = useRef(); + const previousMapIdRef = useRef(); useEffect(() => { if (currentMap && currentMap?.id !== previousMapIdRef.current) { setMapActions(defaultMapActions); @@ -326,8 +362,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } }, [currentMap]); - function handleNoteChange(note: any) { - setCurrentMapState((prevMapState: any) => ({ + function handleNoteChange(note) { + setCurrentMapState((prevMapState) => ({ ...prevMapState, notes: { ...prevMapState.notes, @@ -337,7 +373,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { } function handleNoteRemove(noteId: string) { - setCurrentMapState((prevMapState: any) => ({ + setCurrentMapState((prevMapState) => ({ ...prevMapState, notes: omit(prevMapState.notes, [noteId]), })); @@ -352,7 +388,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { return; } - let assets = []; + let assets: AssetManifestAsset[] = []; for (let tokenState of tokenStates) { if (tokenState.type === "file") { assets.push({ id: tokenState.file, owner: tokenState.owner }); @@ -371,11 +407,11 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { }); } - function handleMapTokenStateChange(change: any) { + function handleMapTokenStateChange(change) { if (!currentMapState) { return; } - setCurrentMapState((prevMapState: any) => { + setCurrentMapState((prevMapState) => { let tokens = { ...prevMapState.tokens }; for (let id in change) { if (id in tokens) { @@ -390,8 +426,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { }); } - function handleMapTokenStateRemove(tokenState: any) { - setCurrentMapState((prevMapState: any) => { + function handleMapTokenStateRemove(tokenState) { + setCurrentMapState((prevMapState) => { const { [tokenState.id]: old, ...rest } = prevMapState.tokens; return { ...prevMapState, tokens: rest }; }); @@ -404,8 +440,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { reply, }: { id: string; - data: any; - reply: any; + data; + reply; }) { if (id === "assetRequest") { const asset = await getAsset(data.id); @@ -440,7 +476,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { assetProgressUpdate({ id, total, count }); } - async function handleSocketMap(map: any) { + async function handleSocketMap(map) { if (map) { setCurrentMap(map); } else { @@ -461,7 +497,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { const canChangeMap = !isLoading; - const canEditMapDrawing: any = + const canEditMapDrawing = currentMap && currentMapState && (currentMapState.editFlags.includes("drawing") || @@ -478,7 +514,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { (currentMapState.editFlags.includes("notes") || currentMap?.owner === userId); - const disabledMapTokens: { [key: string]: any } = {}; + const disabledMapTokens = {}; // If we have a map and state and have the token permission disabled // and are not the map owner if ( diff --git a/src/network/NetworkedMapPointer.tsx b/src/network/NetworkedMapPointer.tsx index 2d81a78..b28ab12 100644 --- a/src/network/NetworkedMapPointer.tsx +++ b/src/network/NetworkedMapPointer.tsx @@ -46,13 +46,13 @@ function NetworkedMapPointer({ // We use requestAnimationFrame as setInterval was being blocked during // re-renders on Chrome with Windows const ownPointerUpdateRef: React.MutableRefObject< - { position: any; visible: boolean; id: any; color: any } | undefined | null + { position; visible: boolean; id; color } | undefined | null > = useRef(); useEffect(() => { let prevTime = performance.now(); let request = requestAnimationFrame(update); let counter = 0; - function update(time: any) { + function update(time) { request = requestAnimationFrame(update); const deltaTime = time - prevTime; counter += deltaTime; @@ -79,7 +79,7 @@ function NetworkedMapPointer({ }; }, []); - function updateOwnPointerState(position: any, visible: boolean) { + function updateOwnPointerState(position, visible: boolean) { setLocalPointerState((prev) => ({ ...prev, [userId]: { position, visible, id: userId, color: pointerColor }, @@ -92,24 +92,24 @@ function NetworkedMapPointer({ }; } - function handleOwnPointerDown(position: any) { + function handleOwnPointerDown(position) { updateOwnPointerState(position, true); } - function handleOwnPointerMove(position: any) { + function handleOwnPointerMove(position) { updateOwnPointerState(position, true); } - function handleOwnPointerUp(position: any) { + function handleOwnPointerUp(position) { updateOwnPointerState(position, false); } // Handle pointer data receive - const interpolationsRef: React.MutableRefObject = useRef({}); + const interpolationsRef: React.MutableRefObject = useRef({}); useEffect(() => { // TODO: Handle player disconnect while pointer visible - function handleSocketPlayerPointer(pointer: any) { - const interpolations: any = interpolationsRef.current; + function handleSocketPlayerPointer(pointer) { + const interpolations = interpolationsRef.current; const id = pointer.id; if (!(id in interpolations)) { interpolations[id] = { @@ -154,8 +154,8 @@ function NetworkedMapPointer({ function animate() { request = requestAnimationFrame(animate); const time = performance.now(); - let interpolatedPointerState: any = {}; - for (let interp of Object.values(interpolationsRef.current) as any) { + let interpolatedPointerState = {}; + for (let interp of Object.values(interpolationsRef.current)) { if (!interp.from || !interp.to) { continue; } @@ -200,7 +200,7 @@ function NetworkedMapPointer({ return ( - {Object.values(localPointerState).map((pointer: any) => ( + {Object.values(localPointerState).map((pointer) => ( { + const handleSignal = (signal) => { this.socket.emit("signal", JSON.stringify({ to: peer.id, signal })); }; @@ -269,9 +275,9 @@ class Session extends EventEmitter { * @property {peerReply} reply */ this.emit("peerConnect", { peer, reply }); - } + }; - const handleDataComplete = (data: any) => { + const handleDataComplete = (data) => { /** * Peer Data Event - Data received by a peer * @@ -285,7 +291,7 @@ class Session extends EventEmitter { let peerDataEvent: { peer: SessionPeer; id: string; - data: any; + data; reply: peerReply; } = { peer, @@ -293,7 +299,7 @@ class Session extends EventEmitter { data: data.data, reply: reply, }; - console.log(`Data: ${JSON.stringify(data)}`) + console.log(`Data: ${JSON.stringify(data)}`); this.emit("peerData", peerDataEvent); }; @@ -444,7 +450,7 @@ class Session extends EventEmitter { } } - _handleSignal(data: any) { + _handleSignal(data) { const { from, signal } = data; if (!(from in this.peers)) { if (!this._addPeer(from, false)) { diff --git a/src/routes/Donate.tsx b/src/routes/Donate.tsx index 94d3c78..718a1a0 100644 --- a/src/routes/Donate.tsx +++ b/src/routes/Donate.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; import { Box, Flex, @@ -18,7 +18,7 @@ import LoadingOverlay from "../components/LoadingOverlay"; import { logError } from "../helpers/logging"; import { Stripe } from "@stripe/stripe-js"; -type Price = { price?: string, name: string, value: number } +type Price = { price?: string; name: string; value: number }; const prices: Price[] = [ { price: "$5.00", name: "Small", value: 5 }, @@ -32,11 +32,9 @@ function Donate() { const hasDonated = query.has("success"); const [loading, setLoading] = useState(true); - // TODO: check with Mitch about changes here from useState(null) - // TODO: typing with error a little messy - const [error, setError]= useState(); + const [error, setError] = useState(undefined); - const [stripe, setStripe]: [ stripe: Stripe | undefined, setStripe: React.Dispatch] = useState(); + const [stripe, setStripe] = useState(); useEffect(() => { import("@stripe/stripe-js").then(({ loadStripe }) => { loadStripe(process.env.REACT_APP_STRIPE_API_KEY as string) @@ -55,7 +53,7 @@ function Donate() { }); }, []); - async function handleSubmit(event: any) { + async function handleSubmit(event: FormEvent) { event.preventDefault(); if (loading) { return; @@ -76,7 +74,8 @@ function Donate() { const result = await stripe?.redirectToCheckout({ sessionId: session.id }); if (result?.error) { - setError(result.error.message); + const stripeError = new Error(result.error.message); + setError(stripeError); } } @@ -87,7 +86,7 @@ function Donate() { setValue(price.value); setSelectedPrice(price.name); } - + return ( setValue(e.target.value)} + onChange={(e: ChangeEvent) => + setValue(parseInt(e.target.value)) + } /> )} @@ -169,7 +170,7 @@ function Donate() {