diff --git a/.erb/configs/webpack.config.base.ts b/.erb/configs/webpack.config.base.ts index 051ab8ea..7a7aa417 100644 --- a/.erb/configs/webpack.config.base.ts +++ b/.erb/configs/webpack.config.base.ts @@ -5,6 +5,8 @@ import webpack from 'webpack' import webpackPaths from './webpack.paths' import { dependencies as externals } from '../../release/app/package.json' +import path from 'path' +import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin' const configuration: webpack.Configuration = { externals: [...Object.keys(externals || {})], @@ -47,6 +49,9 @@ const configuration: webpack.Configuration = { new webpack.EnvironmentPlugin({ NODE_ENV: 'production', }), + new TsconfigPathsPlugin({ + baseUrl: path.join(__dirname, '../..'), // path to tsconfig.json directory + }) as any, ], } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d9165248..29fdd2c8 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] + "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig", "esbenp.prettier-vscode"] } diff --git a/package-lock.json b/package-lock.json index 7454cd3f..d8f29b7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@emotion/styled": "^11.8.1", "@mui/icons-material": "^5.8.2", "@mui/material": "^5.8.2", + "@radix-ui/react-popover": "1.0.5", "@reduxjs/toolkit": "^1.8.2", "@sentry/react": "^7.0.0", "@sentry/tracing": "^7.0.0", @@ -92,6 +93,8 @@ "ts-jest": "^28.0.2", "ts-loader": "^9.3.0", "ts-node": "^10.7.0", + "tsconfig-paths": "^4.2.0", + "tsconfig-paths-webpack-plugin": "^4.0.1", "typescript": "^4.6.4", "url-loader": "^4.1.1", "webpack": "^5.72.1", @@ -954,6 +957,32 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, + "node_modules/@floating-ui/core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", + "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" + }, + "node_modules/@floating-ui/dom": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", + "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", + "dependencies": { + "@floating-ui/core": "^0.7.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz", + "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==", + "dependencies": { + "@floating-ui/dom": "^0.5.3", + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -1876,6 +1905,283 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.3.tgz", + "integrity": "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", + "integrity": "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.2.tgz", + "integrity": "sha512-spwXlNTfeIprt+kaEWE/qYuYT3ZAqJiAGjN/JgdvgVDTu8yc+HuX+WOWXrKliKnLnwck0F6JDkqIERncnih+4A==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.5.tgz", + "integrity": "sha512-GRHZ8yD12MrN2NLobHPE8Rb5uHTxd9x372DE9PPNnBjpczAQHcZ5ne0KXG4xpf+RDdXSzdLv9ym6mYJCDTaUZg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.1.1", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.2.tgz", + "integrity": "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz", + "integrity": "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz", + "integrity": "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz", + "integrity": "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.0.tgz", + "integrity": "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@reduxjs/toolkit": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.2.tgz", @@ -3423,6 +3729,17 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -5376,6 +5693,11 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/detect-port": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz", @@ -6711,6 +7033,19 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -7689,6 +8024,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -8429,6 +8772,14 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -15095,6 +15446,51 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", @@ -15119,6 +15515,28 @@ "react-dom": ">=16.8" } }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -16768,19 +17186,6 @@ "webpack": "^5.0.0" } }, - "node_modules/ts-loader/node_modules/enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/ts-node": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", @@ -16833,11 +17238,47 @@ "node": ">=0.4.0" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.1.tgz", + "integrity": "sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/tunnel": { "version": "0.0.6", @@ -17019,6 +17460,39 @@ "node": ">=4" } }, + "node_modules/use-callback-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", + "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-memo-one": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", @@ -17027,6 +17501,27 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz", @@ -17587,19 +18082,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/enhanced-resolve": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", - "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -18544,6 +19026,28 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, + "@floating-ui/core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", + "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" + }, + "@floating-ui/dom": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", + "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", + "requires": { + "@floating-ui/core": "^0.7.3" + } + }, + "@floating-ui/react-dom": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz", + "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==", + "requires": { + "@floating-ui/dom": "^0.5.3", + "use-isomorphic-layout-effect": "^1.1.1" + } + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -19166,6 +19670,218 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==" }, + "@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-arrow": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-dismissable-layer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.3.tgz", + "integrity": "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.2" + } + }, + "@radix-ui/react-focus-guards": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", + "integrity": "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-focus-scope": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.2.tgz", + "integrity": "sha512-spwXlNTfeIprt+kaEWE/qYuYT3ZAqJiAGjN/JgdvgVDTu8yc+HuX+WOWXrKliKnLnwck0F6JDkqIERncnih+4A==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0" + } + }, + "@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + } + }, + "@radix-ui/react-popover": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.5.tgz", + "integrity": "sha512-GRHZ8yD12MrN2NLobHPE8Rb5uHTxd9x372DE9PPNnBjpczAQHcZ5ne0KXG4xpf+RDdXSzdLv9ym6mYJCDTaUZg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.1.1", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + }, + "@radix-ui/react-portal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.2.tgz", + "integrity": "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + } + }, + "@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + } + }, + "@radix-ui/react-use-escape-keydown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz", + "integrity": "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-use-rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz", + "integrity": "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.0" + } + }, + "@radix-ui/react-use-size": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz", + "integrity": "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + } + }, + "@radix-ui/rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.0.tgz", + "integrity": "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, "@reduxjs/toolkit": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.2.tgz", @@ -20560,6 +21276,14 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "requires": { + "tslib": "^2.0.0" + } + }, "array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -22042,6 +22766,11 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "detect-port": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz", @@ -23094,6 +23823,16 @@ "once": "^1.4.0" } }, + "enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -23850,6 +24589,11 @@ "has-symbols": "^1.0.1" } }, + "get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" + }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -24401,6 +25145,14 @@ "side-channel": "^1.0.4" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -29185,6 +29937,27 @@ "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", "dev": true }, + "react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "requires": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + } + }, + "react-remove-scroll-bar": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "requires": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + } + }, "react-router": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", @@ -29202,6 +29975,16 @@ "react-router": "6.3.0" } }, + "react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "requires": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + } + }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -30458,18 +31241,6 @@ "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4" - }, - "dependencies": { - "enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - } } }, "ts-node": { @@ -30501,11 +31272,40 @@ } } }, + "tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "requires": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + } + } + }, + "tsconfig-paths-webpack-plugin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.1.tgz", + "integrity": "sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^4.1.2" + } + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tunnel": { "version": "0.0.6", @@ -30633,12 +31433,35 @@ "prepend-http": "^2.0.0" } }, + "use-callback-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", + "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==", + "requires": { + "tslib": "^2.0.0" + } + }, + "use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "requires": {} + }, "use-memo-one": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==", "requires": {} }, + "use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "requires": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + } + }, "use-sync-external-store": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz", @@ -30830,18 +31653,6 @@ "terser-webpack-plugin": "^5.1.3", "watchpack": "^2.3.1", "webpack-sources": "^3.2.3" - }, - "dependencies": { - "enhanced-resolve": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", - "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - } } }, "webpack-bundle-analyzer": { diff --git a/package.json b/package.json index e4a52255..4e0131a1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never", "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", "start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer", - "start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .", + "start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only -r tsconfig-paths/register .", "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts", "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", "start:visualizer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.visualizer.dev.ts", @@ -35,6 +35,7 @@ "@emotion/styled": "^11.8.1", "@mui/icons-material": "^5.8.2", "@mui/material": "^5.8.2", + "@radix-ui/react-popover": "1.0.5", "@reduxjs/toolkit": "^1.8.2", "@sentry/react": "^7.0.0", "@sentry/tracing": "^7.0.0", @@ -116,6 +117,8 @@ "ts-jest": "^28.0.2", "ts-loader": "^9.3.0", "ts-node": "^10.7.0", + "tsconfig-paths": "^4.2.0", + "tsconfig-paths-webpack-plugin": "^4.0.1", "typescript": "^4.6.4", "url-loader": "^4.1.1", "webpack": "^5.72.1", diff --git a/src/app.config.ts b/src/app.config.ts new file mode 100644 index 00000000..31d3f7ee --- /dev/null +++ b/src/app.config.ts @@ -0,0 +1,9 @@ +export const AppConfig = { + midi: { + throttledWaitMS: 1000 / 60, + updateIntervalMS: 1000, + }, + dmx: { + updateIntervalMS: 1000, + }, +} diff --git a/src/features/bpm/engine/Link.ts b/src/features/bpm/engine/Link.ts new file mode 100644 index 00000000..320e5f75 --- /dev/null +++ b/src/features/bpm/engine/Link.ts @@ -0,0 +1,42 @@ +import TapTempoEngine from './TapTempoEngine' +import { TimeState } from '../shared/TimeState' +import NodeLink from 'node-link' + +export const createBpmController = () => { + const nodeLink = new NodeLink() + + nodeLink.setIsPlaying(true) + nodeLink.enableStartStopSync(true) + nodeLink.enable(true) + + const tapTempoEngine = new TapTempoEngine() + + let _lastFrameTime = 0 + // Todo: Desimate dt in this context + function getNextTimeState(): TimeState { + let currentTime = Date.now() + const dt = currentTime - _lastFrameTime + + _lastFrameTime = currentTime + + return { + ...nodeLink.getSessionInfoCurrent(), + dt: dt, + quantum: 4.0, + } + } + + function tapTempo() { + tapTempoEngine.tap((newBpm) => { + nodeLink.setTempo(newBpm) + }) + } + + return { + getNextTimeState, + tapTempo, + nodeLink, + } +} + +export type BPMController = ReturnType diff --git a/src/main/engine/TapTempoEngine.ts b/src/features/bpm/engine/TapTempoEngine.ts similarity index 98% rename from src/main/engine/TapTempoEngine.ts rename to src/features/bpm/engine/TapTempoEngine.ts index 97ab9242..671d0d8d 100644 --- a/src/main/engine/TapTempoEngine.ts +++ b/src/features/bpm/engine/TapTempoEngine.ts @@ -1,4 +1,4 @@ -import {DynamicSustainRollingAverage, dynamicSustain} from '../../shared/RollingAverage' +import {DynamicSustainRollingAverage, dynamicSustain} from '../../utils/RollingAverage' const BPM_LIMIT = 1000 const SLEEP_TIME = 3000 // milliseconds between taps after which the engine resets diff --git a/src/features/bpm/engine/index.ts b/src/features/bpm/engine/index.ts new file mode 100644 index 00000000..6b532dda --- /dev/null +++ b/src/features/bpm/engine/index.ts @@ -0,0 +1,70 @@ +import { RealtimeState, initRealtimeState } from 'features/redux/realtimeStore' +import { TimeState } from '../shared/TimeState' +import { createBpmController } from './Link' +import { UserCommand } from 'features/shared/engine/ipc_channels' +export * from './Link' +export const createRealtimeManager = ({ + next, + onUpdate, +}: { + next: ( + previousState: { realtime: RealtimeState }, + newStates: { time: TimeState } + ) => RealtimeState + onUpdate: (newStates: { time: TimeState; realtime: RealtimeState }) => void +}) => { + const bpmController = createBpmController() + const realtimeStateRef: { current: RealtimeState } = { + current: initRealtimeState(), + } + + const createRealtimeLoop = (callback: () => void) => { + const realtimeStateInterval = setInterval(callback, 1000 / 90) + + return { + dispose() { + clearInterval(realtimeStateInterval) + }, + } + } + + return { + realtimeStateRef, + bpmController, + start() { + return createRealtimeLoop(() => { + const nextTimeState = bpmController.getNextTimeState() + + realtimeStateRef.current = next( + { realtime: realtimeStateRef.current }, + { time: nextTimeState } + ) + + onUpdate({ realtime: realtimeStateRef.current, time: nextTimeState }) + }) + }, + } +} + +export type RealtimeManager = ReturnType + +export const onLinkUserCommand = ( + command: UserCommand, + realtimeManager: RealtimeManager +) => { + const realtimeState = realtimeManager.realtimeStateRef.current + const { nodeLink, tapTempo } = realtimeManager.bpmController + if (command.type === 'IncrementTempo') { + nodeLink.setTempo(realtimeState.time.bpm + command.amount) + } else if (command.type === 'SetLinkEnabled') { + nodeLink.enable(command.isEnabled) + } else if (command.type === 'EnableStartStopSync') { + nodeLink.enableStartStopSync(command.isEnabled) + } else if (command.type === 'SetIsPlaying') { + nodeLink.setIsPlaying(command.isPlaying) + } else if (command.type === 'SetBPM') { + nodeLink.setTempo(command.bpm) + } else if (command.type === 'TapTempo') { + tapTempo() + } +} diff --git a/src/shared/TimeState.ts b/src/features/bpm/shared/TimeState.ts similarity index 100% rename from src/shared/TimeState.ts rename to src/features/bpm/shared/TimeState.ts diff --git a/src/shared/randomizer.ts b/src/features/bpm/shared/randomizer.ts similarity index 83% rename from src/shared/randomizer.ts rename to src/features/bpm/shared/randomizer.ts index 2bf7f5a3..831e1ba1 100644 --- a/src/shared/randomizer.ts +++ b/src/features/bpm/shared/randomizer.ts @@ -1,5 +1,6 @@ import { TimeState, isNewPeriod } from './TimeState' -import { lerp } from '../math/util' +import { lerp } from '../../utils/math/util' +import { indexArray } from 'features/utils/util' type Normalized = number @@ -123,3 +124,32 @@ export function updateIndexes( return nextState } + +export const getNewRandomizerState = ({ + previousRandomizerState, + size, + beatsLast, + options, + timeState, +}: { + previousRandomizerState: RandomizerState + size: number + beatsLast: number + options: RandomizerOptions + timeState: TimeState +}) => { + let newRandomizerState = resizeRandomizer( + previousRandomizerState ?? initRandomizerState(), + size + ) + + newRandomizerState = updateIndexes( + beatsLast, + newRandomizerState, + timeState, + indexArray(size), + options + ) + + return newRandomizerState +} diff --git a/src/renderer/overlays/Devices.tsx b/src/features/devices/react/overlays/Devices.tsx similarity index 95% rename from src/renderer/overlays/Devices.tsx rename to src/features/devices/react/overlays/Devices.tsx index bb3fc206..62f8ecdd 100644 --- a/src/renderer/overlays/Devices.tsx +++ b/src/features/devices/react/overlays/Devices.tsx @@ -1,8 +1,8 @@ import styled from 'styled-components' -import { useControlSelector, useTypedSelector } from '../redux/store' -import { setConnectionsMenu } from '../redux/guiSlice' +import { useControlSelector, useTypedSelector } from '../../../../renderer/redux/store' +import { setConnectionsMenu } from '../../../ui/redux/guiSlice' import { useDispatch } from 'react-redux' -import { setDmxConnectable, setMidiConnectable } from '../redux/controlSlice' +import { setDmxConnectable, setMidiConnectable } from '../../../../renderer/redux/controlSlice' import CloseIcon from '@mui/icons-material/Close' import IconButton from '@mui/material/IconButton' import { diff --git a/src/renderer/overlays/DmxTroubleshoot.tsx b/src/features/devices/react/overlays/DmxTroubleshoot.tsx similarity index 97% rename from src/renderer/overlays/DmxTroubleshoot.tsx rename to src/features/devices/react/overlays/DmxTroubleshoot.tsx index fdaa94a4..d5b57fb8 100644 --- a/src/renderer/overlays/DmxTroubleshoot.tsx +++ b/src/features/devices/react/overlays/DmxTroubleshoot.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components' import IconButton from '@mui/material/IconButton' import CopyIcon from '@mui/icons-material/ContentCopy' import { useState } from 'react' -import useSafeCallback from 'renderer/hooks/useSafeCallback' +import useSafeCallback from 'features/ui/react/hooks/useSafeCallback' function copyToClipboard(text: string) { return navigator.clipboard.writeText(text) diff --git a/src/shared/connection.ts b/src/features/devices/shared/connection.ts similarity index 100% rename from src/shared/connection.ts rename to src/features/devices/shared/connection.ts diff --git a/src/features/dmx/channel.config.react.tsx b/src/features/dmx/channel.config.react.tsx new file mode 100644 index 00000000..defb748b --- /dev/null +++ b/src/features/dmx/channel.config.react.tsx @@ -0,0 +1,153 @@ +import HSpad, { ColorChannelProps } from 'features/ui/react/base/HSpad' +import { + DmxNumberField, + Info, + Row, + createChannelComponents, +} from './react/fixtures/channels/editor/core' +import ColorPicker from 'features/ui/react/base/ColorPicker' +import Checkbox from 'features/ui/react/base/LabelledCheckbox' +import Input from 'features/ui/react/base/Input' +import styled from 'styled-components' +import { AxisDir, axisDirList } from './shared/dmxFixtures' +import Select from 'features/ui/react/base/Select' +import ColorMapChannel from './react/fixtures/channels/editor/ColorMapChannel' + +const Sp2 = styled.div` + width: 1rem; +` + +export const ChannelComponents = createChannelComponents({ + color: ({ ch, updateChannel }) => { + const colorProps: ColorChannelProps = { + hue: ch.color.hue, + saturation: ch.color.saturation, + onChange: (newHue, newSaturation) => { + updateChannel({ + type: 'color', + color: { + hue: newHue, + saturation: newSaturation, + }, + }) + }, + } + + return ( + <> + {/* {getCustomColorChannelName(ch.color)} */} + + + + ) + }, + master: ({ ch, updateChannel }) => { + return ( + <> + + + + + + updateChannel({ + ...ch, + isOnOff, + }) + } + /> + + ) + }, + strobe: ({ ch }) => { + return ( + <> + + + + + ) + }, + axis: ({ ch, updateChannel }) => { + return ( + <> + + Direction: + + updateChannel({ + ...ch, + name: newName, + }) + } + /> + + updateChannel({ + ...ch, + isControllable, + }) + } + /> + {ch.isControllable && ( + <> + + + + + + )} + + ) + }, +}) diff --git a/src/features/dmx/channel.config.ts b/src/features/dmx/channel.config.ts new file mode 100644 index 00000000..a19ef4c8 --- /dev/null +++ b/src/features/dmx/channel.config.ts @@ -0,0 +1,212 @@ +import { rLerp } from 'features/utils/math/range' +import { + AxisDir, + DMX_DEFAULT_VALUE, + DMX_MAX_VALUE, + FlattenedFixture, +} from './shared/dmxFixtures' +import { DMX_MIN_VALUE } from './shared/dmxFixtures' +import { ChannelType, GetFixturePayload } from './shared/dmxFixtures' + +import { findClosest } from 'features/utils/math/util' +import { getColorChannelLevel } from './shared/dmxColors' +import { StrictParams, getParam, Params } from '../params/shared/params' +import { calculate_axis_channel } from './engine/channelUtils' +import { Window2D_t } from 'features/shared/shared/window' +import { getBrightness } from 'features/params/engine' + +type GetContext = { + ch: GetFixturePayload + params: Partial + fixture: FlattenedFixture + master: number + randomizerLevel: number + movingWindow: Window2D_t +} + +export type ChannelConfig = { + /** + * This happens on react side + */ + default: (options: any) => Omit, 'type'> + /** + * this happens on engine side + */ + getValueFromDevice?: (ctx: GetContext) => number +} +// TODO: improve this typing +const createChannelConfig = () => { + return < + T extends { + [k in ChannelType]: ChannelConfig + } + >(config: { + [k in keyof T]: k extends ChannelType ? T[k] : never + }) => { + return Object.entries(config).reduce((prev, [key, config]) => { + // @ts-ignore figure out proper typing + prev[key as ChannelType] = { + ...config, + + default: (...args) => { + return { + ...config.default(...args), + type: key, + } + }, + } + return prev + // @ts-ignore figure out proper typing + }, {} as { [k in keyof T]: Omit & { default: (options?: Parameters[0]) => GetFixturePayload } }) + } +} + +/** + * TODO: one possible issue is that engine code and renderer code might be bundled together + * should eventually split the config between engine / react one all details on channels are consolidated + */ +/** + * This seems to be the conversion point between params and channel + */ + +export const channelConfig = createChannelConfig()({ + master: { + default: () => ({ + min: DMX_MIN_VALUE, + max: DMX_MAX_VALUE, + isOnOff: false, + }), + getValueFromDevice({ + master, + ch, + randomizerLevel, + fixture, + params, + movingWindow, + }) { + const level = + getBrightness(params, randomizerLevel, fixture.window, movingWindow) * + master + if (ch.isOnOff) { + return level > 0.5 ? ch.max : ch.min + } else { + return rLerp(ch, level) + } + }, + }, + custom: { + default: ({ name = 'custom' }: { name?: string } = {}) => ({ + name, + default: DMX_MIN_VALUE, + isControllable: false, + min: DMX_MIN_VALUE, + max: DMX_MAX_VALUE, + }), + getValueFromDevice({ ch, params }) { + const customParam = params[ch.name] + if (customParam === undefined) { + return ch.default + } else { + return rLerp(ch, customParam) + } + }, + }, + + colorMap: { + default: () => ({ + colors: [{ max: 0, hue: 0, saturation: 1.0 }], + }), + getValueFromDevice({ ch, params }) { + const _colors = ch.colors + const hue = params.hue + const saturation = params.saturation + if (hue !== undefined && saturation !== undefined) { + let closestColor = findClosest( + _colors.map((color) => { + return [color, color.hue, color.saturation * 2] + }), + hue, + saturation * 2 + ) + return closestColor?.max ?? DMX_DEFAULT_VALUE + } else { + return DMX_DEFAULT_VALUE + } + }, + }, + axis: { + default: ({ + dir = 'x', + isFine = false, + }: { dir?: AxisDir; isFine?: boolean } = {}) => ({ + dir, + isFine, + min: DMX_MIN_VALUE, + max: DMX_MAX_VALUE, + }), + getValueFromDevice({ ch, fixture, params }) { + if (ch.dir === 'x') { + return calculate_axis_channel( + ch, + params.xAxis, + fixture.window?.x?.pos, + params.xMirror, + fixture + ) + } else { + return calculate_axis_channel( + ch, + params.yAxis, + fixture.window?.y?.pos, + undefined, // No y-mirroring yet + fixture + ) + } + }, + }, + strobe: { + default: ({ invert = false }: { invert?: boolean } = {}) => + invert + ? { + default_solid: DMX_MAX_VALUE, + default_strobe: DMX_MIN_VALUE, + } + : { + default_solid: DMX_MIN_VALUE, + default_strobe: DMX_MAX_VALUE, + }, + getValueFromDevice({ ch, params }) { + return params.strobe !== undefined && params.strobe > 0.5 + ? ch.default_strobe + : ch.default_solid + }, + }, + + color: { + default: ({ + hue = 0, + saturation = 1.0, + }: { hue?: number; saturation?: number } = {}) => ({ + color: { + hue, + saturation, + }, + }), + getValueFromDevice({ ch, fixture, randomizerLevel, params, movingWindow }) { + const brightness = getBrightness( + params, + randomizerLevel, + fixture.window, + movingWindow + ) + return ( + getColorChannelLevel( + getParam(params, 'hue'), + getParam(params, 'saturation'), + brightness, + ch.color + ) * DMX_MAX_VALUE + ) + }, + }, +}) diff --git a/src/features/dmx/engine/channelUtils.ts b/src/features/dmx/engine/channelUtils.ts new file mode 100644 index 00000000..7b195608 --- /dev/null +++ b/src/features/dmx/engine/channelUtils.ts @@ -0,0 +1,44 @@ +import { + DMX_MAX_VALUE, + AxisDir, + DMX_MIN_VALUE, + FlattenedFixture, + GetFixturePayload, +} from '../shared/dmxFixtures' +import { Normalized } from '../../utils/math/util' +import { rLerp } from '../../utils/math/range' + +import { applyMirror } from '../shared/dmxUtil' + +export function calculate_axis_channel( + ch: GetFixturePayload<'axis'>, + axis_param: Normalized | undefined, + fixture_position: Normalized | undefined, + mirror_param: Normalized | undefined, + fixture: FlattenedFixture +) { + if (axis_param === undefined) return 0 + + let mirrored_param = + fixture_position && fixture_position > 0.5 + ? applyMirror(axis_param, mirror_param) + : axis_param + + if (ch.isFine) { + const step_count = axis_range(fixture, ch.dir) + const step_delta = 1 / step_count + let remainder = mirrored_param % step_delta + let remainder_ratio = remainder / step_delta + return remainder_ratio * DMX_MAX_VALUE + } else { + return Math.floor(rLerp(ch, mirrored_param)) + } +} + +function axis_range(fixture: FlattenedFixture, dir: AxisDir) { + for (const [_channel_num, ch] of fixture.channels) { + if (ch.type === 'axis' && ch.dir === dir && !ch.isFine) + return ch.max - ch.min + } + return DMX_MAX_VALUE - DMX_MIN_VALUE +} diff --git a/src/main/engine/dmxConnection.ts b/src/features/dmx/engine/dmxConnection.ts similarity index 85% rename from src/main/engine/dmxConnection.ts rename to src/features/dmx/engine/dmxConnection.ts index 3d99ffc3..01d45d3e 100644 --- a/src/main/engine/dmxConnection.ts +++ b/src/features/dmx/engine/dmxConnection.ts @@ -6,7 +6,7 @@ import { ConnectionId, DmxDevice_t, SerialportInfo, -} from '../../shared/connection' +} from '../../devices/shared/connection' const ENTTEC_PRO_DMX_STARTCODE = 0x00 const ENTTEC_PRO_START_OF_MSG = 0x7e @@ -19,6 +19,9 @@ const DMX_SEND_INTERVAL = 1000 / 40 let _readyToWrite = true let _connection: null | SerialPort = null let _config: Config +let maintainTimeout: NodeJS.Timer | undefined + +const supportedDMXDevices = ['DMX', 'ENTTEC', 'FTDI'] export type UpdatePayload = DmxConnections @@ -57,15 +60,29 @@ function dmxDevice(port: SerialportInfo): DmxDevice_t { export function maintain(config: Config) { _config = config maintainConnection() - setInterval(() => { + const interval = setInterval(() => { sendUniverse(_config.getChannels()) }, DMX_SEND_INTERVAL) + + return { + dispose() { + clearInterval(interval) + close() + if (maintainTimeout) clearTimeout(maintainTimeout) + }, + } } export function listPorts(): Promise { return SerialPort.list() } +function close() { + _connection?.close() + _connection?.destroy() + _connection = null +} + async function maintainConnection() { const availablePorts = await SerialPort.list() @@ -79,9 +96,7 @@ async function maintainConnection() { !connectable.find((c) => c === _connection?.path) || !_connection.isOpen ) { - _connection.close() - _connection.destroy() - _connection = null + close() } } else { const portToConnect = availablePorts.find( @@ -100,7 +115,7 @@ async function maintainConnection() { _config.onUpdate(status) - setTimeout(maintainConnection, _config.update_ms) + maintainTimeout = setTimeout(maintainConnection, _config.update_ms) } function connect(path: string) { @@ -131,9 +146,9 @@ function start() {} function isDmxDevice_t(port: SerialportInfo) { if ( - port.manufacturer?.includes('DMX') || - port.manufacturer?.includes('ENTTEC') || - port.manufacturer?.includes('FTDI') + supportedDMXDevices.some((manufacturerName) => + port.manufacturer?.includes(manufacturerName) + ) ) return true return false diff --git a/src/features/dmx/engine/dmxEngine.ts b/src/features/dmx/engine/dmxEngine.ts new file mode 100644 index 00000000..9025fbea --- /dev/null +++ b/src/features/dmx/engine/dmxEngine.ts @@ -0,0 +1,112 @@ +import { + DMX_MAX_VALUE, + DMX_DEFAULT_VALUE, + DMX_NUM_CHANNELS, + FlattenedFixture, + FixtureChannel, + DmxValue, + ChannelType, +} from '../shared/dmxFixtures' +import { Params } from '../../params/shared/params' +import { RandomizerState } from '../../bpm/shared/randomizer' +import { CleanReduxState } from '../../../renderer/redux/store' +import { getFixturesInGroups, flatten_fixtures } from '../shared/dmxUtil' +import { indexArray, zip } from '../../utils/util' +import { SplitState } from 'renderer/redux/realtimeStore' +import { ChannelConfig, channelConfig } from '../channel.config' +import { getMovingWindow } from 'features/params/engine' + +export function getChannels({ state }: { state: CleanReduxState }) { + const universe = state.dmx.universe + const all_fixtures = flatten_fixtures(universe, state.dmx.fixtureTypesByID) + + const channels = Array(DMX_NUM_CHANNELS).fill(0) + + return { + channels, + all_fixtures, + } +} + +type OutChannelConfig = ReturnType + +export function getDmxValue( + ch: FixtureChannel, + params: Partial, + fixture: FlattenedFixture, + master: number, + randomizerLevel: number +): DmxValue { + const movingWindow = getMovingWindow(params) + const channelTypeConfig = channelConfig[ch.type] as ChannelConfig + if (!channelTypeConfig || !channelTypeConfig.getValueFromDevice) + return DMX_DEFAULT_VALUE + + return channelTypeConfig.getValueFromDevice({ + ch, + fixture, + master, + movingWindow, + params, + randomizerLevel, + }) +} + +export function calculateDmxOut( + { + state, + splitStates, + }: { + state: CleanReduxState + splitStates: SplitState[] + }, + { all_fixtures, channels }: OutChannelConfig +) { + const scenes = state.control.light + const activeScene = scenes.byId[scenes.active] + + const applyFixtures = ( + fixtures: FlattenedFixture[], + outputParams: Partial, + randomizerState: RandomizerState + ) => { + fixtures.forEach((fixture, i) => { + fixture.channels.forEach(([outputChannel, channelType]) => { + let new_channel_value = DMX_DEFAULT_VALUE + let current_channel_value = channels[outputChannel - 1] + if (fixture.intensity <= (outputParams.intensity ?? 1)) { + new_channel_value = getDmxValue( + channelType, + outputParams, + fixture, + state.control.master, + randomizerState[i]?.level ?? 1 + ) + } + channels[outputChannel - 1] = Math.max( + new_channel_value, + current_channel_value + ) + }) + }) + } + + for (const [{ outputParams, randomizer }, splitScene] of zip( + splitStates, + activeScene.splitScenes + )) { + const splitGroups = splitScene.groups + + const splitSceneFixtures = getFixturesInGroups(all_fixtures, splitGroups) + + applyFixtures(splitSceneFixtures, outputParams, randomizer) + } + + // Apply any overwrites + indexArray(DMX_NUM_CHANNELS).forEach((i) => { + const overwrite = state.mixer.overwrites[i] + if (overwrite !== undefined) { + channels[i] = overwrite * DMX_MAX_VALUE + } + }) +} diff --git a/src/renderer/dmx/ColorMapChannel.tsx b/src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx similarity index 88% rename from src/renderer/dmx/ColorMapChannel.tsx rename to src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx index da6065c1..3d821935 100644 --- a/src/renderer/dmx/ColorMapChannel.tsx +++ b/src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx @@ -1,24 +1,24 @@ import { useDispatch } from 'react-redux' import styled from 'styled-components' -import { DMX_MAX_VALUE } from '../../shared/dmxFixtures' -import NumberField from '../base/NumberField' +import { DMX_MAX_VALUE, GetFixturePayload } from '../../../../shared/dmxFixtures' +import NumberField from '../../../../../ui/react/base/NumberField' import { addColorMapColor, setColorMapColor, removeColorMapColor, -} from '../redux/dmxSlice' +} from '../../../../../fixtures/redux/fixturesSlice' import { IconButton } from '@mui/material' import Add from '@mui/icons-material/Add' import Remove from '@mui/icons-material/Remove' -import HSpad, { ColorChannelProps } from 'renderer/base/HSpad' -import { ChannelColorMap } from '../../shared/dmxFixtures' +import HSpad, { ColorChannelProps } from 'features/ui/react/base/HSpad' + import { useState } from 'react' -import wrapClick from 'renderer/base/wrapClick' -import { lerp } from 'math/util' -import ColorPicker from 'renderer/base/ColorPicker' +import wrapClick from 'features/ui/react/base/wrapClick' +import { lerp } from 'features/utils/math/util' +import ColorPicker from 'features/ui/react/base/ColorPicker' interface Props { - ch: ChannelColorMap + ch: GetFixturePayload<'colorMap'> fixtureID: string channelIndex: number } diff --git a/src/features/dmx/react/fixtures/channels/editor/core.tsx b/src/features/dmx/react/fixtures/channels/editor/core.tsx new file mode 100644 index 00000000..96f2d3f0 --- /dev/null +++ b/src/features/dmx/react/fixtures/channels/editor/core.tsx @@ -0,0 +1,58 @@ +import { + ChannelType, + DMX_MAX_VALUE, + DMX_MIN_VALUE, + FixtureChannel, +} from 'features/dmx/shared/dmxFixtures' +import NumberField from '../../../../../ui/react/base/NumberField' +import { FC, createContext, useContext } from 'react' +import styled from 'styled-components' + +export const ChannelContext = createContext({ + updateChannel(_newChannel: FixtureChannel) {}, +}) + +export function DmxNumberField< + Ch extends FixtureChannel, + Key extends keyof Ch +>({ ch, field, label }: { ch: Ch; field: Key; label: string }) { + const channelFixture = useContext(ChannelContext) + return ( + + channelFixture.updateChannel({ + ...ch, + [field]: newVal, + }) + } + /> + ) +} + +export const Row = styled.div` + display: flex; + align-items: center; +` + +export const Info = styled.div` + font-size: 0.9rem; + margin-right: 0.5rem; +` + +export const createChannelComponents = < + T extends { + [k in ChannelType]: FC<{ + ch: Extract + updateChannel(newChannel: FixtureChannel): void + fixtureID: string + channelIndex: number + }> + } +>(config: { [k in keyof T]: k extends ChannelType ? T[k] : never }) => { + return config +} diff --git a/src/features/dmx/react/fixtures/channels/editor/index.tsx b/src/features/dmx/react/fixtures/channels/editor/index.tsx new file mode 100644 index 00000000..0ef521f7 --- /dev/null +++ b/src/features/dmx/react/fixtures/channels/editor/index.tsx @@ -0,0 +1,78 @@ +import styled from 'styled-components' +import { useDispatch } from 'react-redux' +import Select from '../../../../../ui/react/base/Select' +import { + FixtureChannel, + channelTypes, + initFixtureChannel, +} from '../../../../shared/dmxFixtures' +import { editFixtureChannel } from '../../../../../fixtures/redux/fixturesSlice' +import { FixtureChannelItemProps } from '../list/FixtureChannelItem' +import { ChannelComponents } from 'features/dmx/channel.config.react' +import { ChannelContext, Info, Row } from './core' + +interface Props extends FixtureChannelItemProps { + ch: FixtureChannel +} + +export default function FixtureChannelPopup(props: Props) { + const { ch, fixtureID, channelIndex } = props + const dispatch = useDispatch() + + return ( + + + Type: + -
activeScene.modulators[index].lfo.period + ) + const sceneId = useControlSelector((state) => state.light.active) + + const dispatch = useDispatch() + + function onChange(newVal: number) { + dispatch(setPeriod({ index: index, newVal: newVal })) + } + + const wrapperStyle: React.CSSProperties = { + flex: 1, + textAlign: 'right', + marginLeft: '0.4rem', + } + + const min = 0.25 + const max = 32 + + return ( + <> + + + + + ) +} diff --git a/src/renderer/scenes/LfoVisualizer.tsx b/src/features/modulation/react/lfo/LfoVisualizer.tsx similarity index 87% rename from src/renderer/scenes/LfoVisualizer.tsx rename to src/features/modulation/react/lfo/LfoVisualizer.tsx index d06df6dd..16c59147 100644 --- a/src/renderer/scenes/LfoVisualizer.tsx +++ b/src/features/modulation/react/lfo/LfoVisualizer.tsx @@ -1,9 +1,9 @@ import { GetValueFromPhase } from '../../shared/oscillator' import { useDispatch } from 'react-redux' -import useDragBasic from '../hooks/useDragBasic' -import { incrementModulator } from '../redux/controlSlice' -import { useActiveLightScene } from '../redux/store' -import { secondaryEnabled } from 'renderer/base/keyUtil' +import useDragBasic from '../../../ui/react/hooks/useDragBasic' +import { incrementModulator } from '../../../../renderer/redux/controlSlice' +import { useActiveLightScene } from '../../../../renderer/redux/store' +import { secondaryEnabled } from 'features/ui/react/base/keyUtil' type Props = { index: number diff --git a/src/features/modulation/redux/reducer.ts b/src/features/modulation/redux/reducer.ts new file mode 100644 index 00000000..247eaf0a --- /dev/null +++ b/src/features/modulation/redux/reducer.ts @@ -0,0 +1,102 @@ +import { PayloadAction } from '@reduxjs/toolkit' +import { LightScene_t } from 'features/scenes/shared/Scenes' +import { clamp, clampNormalized } from 'features/utils/math/util' + +import { LfoShape } from '../shared/oscillator' +import { initModulator } from '../shared/modulation' +import { ActionState, createTypedReducers, modifyActiveLightScene } from 'renderer/redux/controlSlice/reducers/core' + +const modifyScene = { + light: { + active: modifyActiveLightScene, + byId: ( + state: ActionState, + sceneId: string, + callback: (scene: LightScene_t) => void + ) => { + const scene = state.light.byId[sceneId] + if (scene) { + callback(scene) + } + }, + }, +} + +export interface IncrementModulatorPayload { + index: number + flip: number + phaseShift: number + skew: number + symmetricSkew: number +} + +export const modulationActionReducer = createTypedReducers({ + // Update modulators + setModulatorShape: ( + state, + { payload }: PayloadAction<{ index: number; shape: LfoShape }> + ) => { + modifyActiveLightScene(state, (scene) => { + scene.modulators[payload.index].lfo.shape = payload.shape + }) + }, + setPeriod: ( + state, + { + payload, + }: PayloadAction<{ index: number; newVal: number; sceneId?: string }> + ) => { + const handle = (scene: LightScene_t) => { + scene.modulators[payload.index].lfo.period = payload.newVal + } + if (payload.sceneId) { + modifyScene.light.byId(state, payload.sceneId, handle) + } else { + modifyActiveLightScene(state, handle) + } + }, + incrementPeriod: ( + state, + { payload }: PayloadAction<{ index: number; amount: number }> + ) => { + modifyActiveLightScene(state, (scene) => { + scene.modulators[payload.index].lfo.period = clamp( + scene.modulators[payload.index].lfo.period + payload.amount, + 0.25, + 16 + ) + }) + }, + incrementModulator: ( + state, + { payload }: PayloadAction + ) => { + modifyActiveLightScene(state, (scene) => { + const modulator = scene.modulators[payload.index] + modulator.lfo.flip = clampNormalized(modulator.lfo.flip + payload.flip) + modulator.lfo.phaseShift = clampNormalized( + modulator.lfo.phaseShift + payload.phaseShift + ) + modulator.lfo.skew = clampNormalized(modulator.lfo.skew + payload.skew) + modulator.lfo.symmetricSkew = clampNormalized( + modulator.lfo.symmetricSkew + payload.symmetricSkew + ) + }) + }, + // Create and Delete modulators + addModulator: (state, _: PayloadAction) => { + modifyActiveLightScene(state, (scene) => { + scene.modulators.push(initModulator(scene.splitScenes.length)) + }) + }, + removeModulator: (state, { payload }: PayloadAction) => { + modifyActiveLightScene(state, (scene) => { + scene.modulators.splice(payload, 1) + }) + }, + resetModulator: (state, { payload }: PayloadAction) => { + modifyActiveLightScene(state, (scene) => { + scene.modulators[payload] = initModulator(scene.splitScenes.length) + }) + }, +}) diff --git a/src/features/modulation/shared/modulation.ts b/src/features/modulation/shared/modulation.ts new file mode 100644 index 00000000..71b198ec --- /dev/null +++ b/src/features/modulation/shared/modulation.ts @@ -0,0 +1,20 @@ +import { Modulation } from '../../params/shared/params' +import { Lfo, GetRamp } from './oscillator' + +export interface Modulator { + lfo: Lfo + splitModulations: Modulation[] +} + +export function initModulation(): Modulation { + return {} +} + +export function initModulator(splitCount: number): Modulator { + return { + lfo: GetRamp(), + splitModulations: Array(splitCount) + .fill(0) + .map(() => initModulation()), + } +} diff --git a/src/shared/oscillator.ts b/src/features/modulation/shared/oscillator.ts similarity index 93% rename from src/shared/oscillator.ts rename to src/features/modulation/shared/oscillator.ts index 3a2d10ff..3a80504d 100644 --- a/src/shared/oscillator.ts +++ b/src/features/modulation/shared/oscillator.ts @@ -1,5 +1,5 @@ -import { Normalized } from '../math/util' -import { skewPower3, skewSymmetric } from '../math/skew' +import { Normalized } from '../../utils/math/util' +import { skewPower3, skewSymmetric } from '../../utils/math/skew' // const SKEW_FN = skewBezier2 const SKEW_FN = skewPower3 diff --git a/src/features/params/engine/index.ts b/src/features/params/engine/index.ts new file mode 100644 index 00000000..1e88c838 --- /dev/null +++ b/src/features/params/engine/index.ts @@ -0,0 +1,79 @@ +import { Window2D_t } from 'features/shared/shared/window' +import { + DefaultParam, + Params, + StrictParams, + defaultOutputParams, + getParam, +} from '../shared/params' +import { Normalized, clampNormalized } from 'features/utils/math/util' +import { getWindowMultiplier2D } from 'features/dmx/shared/dmxUtil' +import { applyRandomization } from 'features/bpm/shared/randomizer' + +/** + * used on engine side on led and dmx + * @param params + * @returns + */ +export function getMovingWindow(params: Partial): Window2D_t { + const x = + params.x !== undefined && params.width !== undefined + ? { pos: params.x, width: params.width } + : undefined + + const y = + params.y !== undefined && params.height !== undefined + ? { pos: params.y, width: params.height } + : undefined + + return { + x: x, + y: y, + } +} + +export function getBrightness( + params: Partial, + randomizerLevel: Normalized, + fixtureWindow: Window2D_t, + movingWindow: Window2D_t +): Normalized { + const unrandomizedBrightness = + getParam(params, 'brightness') * + getWindowMultiplier2D(fixtureWindow, movingWindow) + return applyRandomization( + unrandomizedBrightness, + randomizerLevel, + getParam(params, 'randomize') + ) +} + +export const createOutputParams = ( + { + baseParams, + allParamKeys, + }: { + baseParams: Partial + + allParamKeys: string[] + }, + transform: (params: { + param: DefaultParam | string + baseParam: number + }) => number +) => { + const _getOutputParam = ( + baseParam: number | undefined, + param: DefaultParam | string + ) => { + if (baseParam === undefined) return undefined + return clampNormalized(transform({ param, baseParam })) + } + const outputParams = defaultOutputParams() + + allParamKeys.forEach((param) => { + outputParams[param] = _getOutputParam(baseParams[param], param) + }) + + return outputParams +} diff --git a/src/renderer/controls/ADSR.tsx b/src/features/params/react/controls/ADSR.tsx similarity index 92% rename from src/renderer/controls/ADSR.tsx rename to src/features/params/react/controls/ADSR.tsx index ecb4dd9f..1110958a 100644 --- a/src/renderer/controls/ADSR.tsx +++ b/src/features/params/react/controls/ADSR.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components' -import DraggableNumber from '../base/DraggableNumber' -import { lerp } from '../../math/util' -import useDragMapped from '../hooks/useDragMapped' +import DraggableNumber from '../../../ui/react/base/DraggableNumber' +import { lerp } from '../../../utils/math/util' +import useDragMapped from '../../../ui/react/hooks/useDragMapped' export interface Control { val: number diff --git a/src/renderer/controls/ADSRWrapper.tsx b/src/features/params/react/controls/ADSRWrapper.tsx similarity index 86% rename from src/renderer/controls/ADSRWrapper.tsx rename to src/features/params/react/controls/ADSRWrapper.tsx index 4f7d38cd..c853a1a8 100644 --- a/src/renderer/controls/ADSRWrapper.tsx +++ b/src/features/params/react/controls/ADSRWrapper.tsx @@ -1,6 +1,6 @@ import ADSR, { Control } from './ADSR' -import { useActiveLightScene } from '../redux/store' -import { setRandomizer } from '../redux/controlSlice' +import { useActiveLightScene } from '../../../../renderer/redux/store' +import { setRandomizer } from '../../../../renderer/redux/controlSlice' import { useDispatch } from 'react-redux' interface Props { diff --git a/src/renderer/controls/HsvPad.tsx b/src/features/params/react/controls/HsvPad.tsx similarity index 76% rename from src/renderer/controls/HsvPad.tsx rename to src/features/params/react/controls/HsvPad.tsx index 5760bdb3..1237822f 100644 --- a/src/renderer/controls/HsvPad.tsx +++ b/src/features/params/react/controls/HsvPad.tsx @@ -1,7 +1,7 @@ import SVpad from './SVpad' import Hue from './Hue' -import MidiOverlay_xy from '../base/MidiOverlay_xy' -import { SliderMidiOverlay } from '../base/MidiOverlay' +import MidiOverlay_xy from 'features/midi/react/MidiOverlay_xy' +import { RangeMidiOverlay } from 'features/midi/react/MidiOverlay' import styled from 'styled-components' interface Props { @@ -19,9 +19,9 @@ export default function HsvPad({ splitIndex }: Props) { > - + - + ) : ( diff --git a/src/renderer/controls/Hue.tsx b/src/features/params/react/controls/Hue.tsx similarity index 84% rename from src/renderer/controls/Hue.tsx rename to src/features/params/react/controls/Hue.tsx index 3acf8490..940f567e 100644 --- a/src/renderer/controls/Hue.tsx +++ b/src/features/params/react/controls/Hue.tsx @@ -1,8 +1,8 @@ -import useDragMapped from '../hooks/useDragMapped' +import useDragMapped from '../../../ui/react/hooks/useDragMapped' import { useDispatch } from 'react-redux' -import { setBaseParams } from '../redux/controlSlice' +import { setBaseParams } from '../../../../renderer/redux/controlSlice' import styled from 'styled-components' -import { useBaseParam } from 'renderer/redux/store' +import { useBaseParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/renderer/controls/ParamAddButton.tsx b/src/features/params/react/controls/ParamAddButton.tsx similarity index 89% rename from src/renderer/controls/ParamAddButton.tsx rename to src/features/params/react/controls/ParamAddButton.tsx index d3beabbf..c022da8b 100644 --- a/src/renderer/controls/ParamAddButton.tsx +++ b/src/features/params/react/controls/ParamAddButton.tsx @@ -1,19 +1,20 @@ import IconButton from '@mui/material/IconButton' import AddIcon from '@mui/icons-material/Add' import { useState, FunctionComponent } from 'react' -import { useBaseParams, useDmxSelector } from 'renderer/redux/store' +import { useDmxSelector } from 'renderer/redux/store' import styled from 'styled-components' -import Popup from '../base/Popup' +import Popup from '../../../ui/react/base/Popup' import { useDispatch } from 'react-redux' -import { DefaultParam, Params, defaultParamsList } from 'shared/params' +import { DefaultParam, Params, defaultParamsList } from 'features/params/shared/params' import { setBaseParams } from 'renderer/redux/controlSlice' -import { initParams } from 'shared/params' +import { initParams } from 'features/params/shared/params' import IntensityIcon from '@mui/icons-material/LocalFireDepartment' import StrobeIcon from '@mui/icons-material/LightMode' import RandomizeIcon from '@mui/icons-material/Shuffle' import PositionIcon from '@mui/icons-material/PictureInPicture' -import axisIconSrc from '../../../assets/axis.svg' -import { getCustomChannels } from 'renderer/redux/dmxSlice' +import axisIconSrc from '../../../../../assets/axis.svg' +import { getCustomChannels } from 'features/fixtures/redux/fixturesSlice' +import { useBaseParams } from 'features/params/redux' interface Props { splitIndex: number @@ -45,7 +46,7 @@ const initialParams = initParams() function getOptions( custom_channels: Set, - baseParams: Params + baseParams: Partial ): (DefaultParam | ParamBundle | string)[] { const paramOptions: (DefaultParam | ParamBundle | string)[] = defaultParamsList.filter((param) => { @@ -110,7 +111,7 @@ export default function ParamAddButton({ splitIndex }: Props) { key={option} onClick={(e) => { e.preventDefault() - const newParams: Params = {} + const newParams: Partial = {} if (option === 'axis' || option === 'position') { for (const param of paramBundles[option]) { newParams[param] = initialParams[param] ?? 0 diff --git a/src/renderer/controls/ParamCursor.tsx b/src/features/params/react/controls/ParamCursor.tsx similarity index 53% rename from src/renderer/controls/ParamCursor.tsx rename to src/features/params/react/controls/ParamCursor.tsx index 5711cef7..31ce1b7e 100644 --- a/src/renderer/controls/ParamCursor.tsx +++ b/src/features/params/react/controls/ParamCursor.tsx @@ -1,20 +1,20 @@ -import { useOutputParam } from '../redux/realtimeStore' +import { useOutputParam } from 'features/params/redux' import { DefaultParam } from '../../shared/params' -import SliderCursor from '../base/SliderCursor' +import SliderCursor from '../../../ui/react/base/SliderCursor' -interface Props { - param: DefaultParam | string +interface Props { + param: Param radius: number orientation: 'vertical' | 'horizontal' splitIndex: number } -export default function ParamCursor({ +export default function ParamCursor({ param, radius, orientation, splitIndex, -}: Props) { +}: Props) { const value = useOutputParam(param, splitIndex) return ( diff --git a/src/renderer/controls/ParamSlider.tsx b/src/features/params/react/controls/ParamSlider.tsx similarity index 74% rename from src/renderer/controls/ParamSlider.tsx rename to src/features/params/react/controls/ParamSlider.tsx index 92b5a054..1744b8c3 100644 --- a/src/renderer/controls/ParamSlider.tsx +++ b/src/features/params/react/controls/ParamSlider.tsx @@ -1,19 +1,22 @@ import { DefaultParam } from '../../shared/params' -import SliderBase from '../base/SliderBase' -import SliderCursor from '../base/SliderCursor' -import { useBaseParam } from '../redux/store' +import SliderBase from '../../../ui/react/base/SliderBase' +import SliderCursor from '../../../ui/react/base/SliderCursor' import { useDispatch } from 'react-redux' -import { setBaseParams } from '../redux/controlSlice' +import { setBaseParams } from '../../../../renderer/redux/controlSlice' import ParamCursor from './ParamCursor' -import { SliderMidiOverlay } from '../base/MidiOverlay' +import { RangeMidiOverlay } from 'features/midi/react/MidiOverlay' import ParamXButton from './ParamXButton' +import { useBaseParam } from 'features/params/redux' -interface Props { - param: DefaultParam | string +interface Props { + param: Param splitIndex: number } -export default function ParamSlider({ param, splitIndex }: Props) { +export default function ParamSlider({ + param, + splitIndex, +}: Props) { const radius = 0.4 const value = useBaseParam(param, splitIndex) @@ -65,12 +68,12 @@ export default function ParamSlider({ param, splitIndex }: Props) { } return splitIndex === 0 ? ( - {content} - + ) : (
{content}
) diff --git a/src/renderer/controls/ParamXButton.tsx b/src/features/params/react/controls/ParamXButton.tsx similarity index 72% rename from src/renderer/controls/ParamXButton.tsx rename to src/features/params/react/controls/ParamXButton.tsx index dd1968b4..abe962e8 100644 --- a/src/renderer/controls/ParamXButton.tsx +++ b/src/features/params/react/controls/ParamXButton.tsx @@ -1,14 +1,17 @@ import styled from 'styled-components' -import { DefaultParam } from 'shared/params' +import { DefaultParam } from 'features/params/shared/params' import { useDispatch } from 'react-redux' import { deleteBaseParams } from 'renderer/redux/controlSlice' -interface Props { +interface Props { splitIndex: number - params: readonly (DefaultParam | string)[] + params: readonly Param[] } -export default function ParamXButton({ splitIndex, params }: Props) { +export default function ParamXButton({ + splitIndex, + params, +}: Props) { const dispatch = useDispatch() const onClick = () => { diff --git a/src/renderer/controls/ParamsControl.tsx b/src/features/params/react/controls/ParamsControl.tsx similarity index 93% rename from src/renderer/controls/ParamsControl.tsx rename to src/features/params/react/controls/ParamsControl.tsx index 629b30ca..8afbdce2 100644 --- a/src/renderer/controls/ParamsControl.tsx +++ b/src/features/params/react/controls/ParamsControl.tsx @@ -6,7 +6,7 @@ import Randomizer from './Randomizer' import XYAxispad from './XYAxisPad' import ParamAddButton from './ParamAddButton' import { useDmxSelector } from 'renderer/redux/store' -import { getCustomChannels } from 'renderer/redux/dmxSlice' +import { getCustomChannels } from 'features/fixtures/redux/fixturesSlice' interface Params { splitIndex: number diff --git a/src/renderer/controls/Randomizer.tsx b/src/features/params/react/controls/Randomizer.tsx similarity index 87% rename from src/renderer/controls/Randomizer.tsx rename to src/features/params/react/controls/Randomizer.tsx index 50113058..7c0b2f60 100644 --- a/src/renderer/controls/Randomizer.tsx +++ b/src/features/params/react/controls/Randomizer.tsx @@ -1,13 +1,14 @@ import styled from 'styled-components' import ADSRWrapper from './ADSRWrapper' -import { useActiveLightScene, useBaseParam } from '../redux/store' +import { useActiveLightScene } from '../../../../renderer/redux/store' import RandomizerVisualizer from './RandomizerVisualizer' -import DraggableNumber from '../base/DraggableNumber' +import DraggableNumber from '../../../ui/react/base/DraggableNumber' import { useDispatch } from 'react-redux' -import { setRandomizer } from '../redux/controlSlice' -import Slider from '../base/Slider' +import { setRandomizer } from '../../../../renderer/redux/controlSlice' +import Slider from '../../../ui/react/base/Slider' import ParamXButton from './ParamXButton' import ParamSlider from './ParamSlider' +import { useBaseParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/renderer/controls/RandomizerVisualizer.tsx b/src/features/params/react/controls/RandomizerVisualizer.tsx similarity index 92% rename from src/renderer/controls/RandomizerVisualizer.tsx rename to src/features/params/react/controls/RandomizerVisualizer.tsx index dc9627a3..e9c5dcf9 100644 --- a/src/renderer/controls/RandomizerVisualizer.tsx +++ b/src/features/params/react/controls/RandomizerVisualizer.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components' import { useRealtimeSelector } from 'renderer/redux/realtimeStore' -import { useBaseParam } from 'renderer/redux/store' -import { applyRandomization } from 'shared/randomizer' +import { applyRandomization } from 'features/bpm/shared/randomizer' +import { useBaseParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/renderer/controls/SVCursor.tsx b/src/features/params/react/controls/SVCursor.tsx similarity index 85% rename from src/renderer/controls/SVCursor.tsx rename to src/features/params/react/controls/SVCursor.tsx index cc95cabc..73465aef 100644 --- a/src/renderer/controls/SVCursor.tsx +++ b/src/features/params/react/controls/SVCursor.tsx @@ -1,6 +1,5 @@ -import { useOutputParam } from '../redux/realtimeStore' -import Cursor from '../base/Cursor' -import { useBaseParam } from 'renderer/redux/store' +import { useBaseParam, useOutputParam } from 'features/params/redux' +import Cursor from '../../../ui/react/base/Cursor' interface Props { splitIndex: number diff --git a/src/renderer/controls/SVpad.tsx b/src/features/params/react/controls/SVpad.tsx similarity index 87% rename from src/renderer/controls/SVpad.tsx rename to src/features/params/react/controls/SVpad.tsx index d826eac0..bbcd225d 100644 --- a/src/renderer/controls/SVpad.tsx +++ b/src/features/params/react/controls/SVpad.tsx @@ -1,9 +1,9 @@ import styled from 'styled-components' -import useDragMapped from '../hooks/useDragMapped' +import useDragMapped from '../../../ui/react/hooks/useDragMapped' import { useDispatch } from 'react-redux' -import { setBaseParams } from '../redux/controlSlice' +import { setBaseParams } from '../../../../renderer/redux/controlSlice' import { SVCursorBase, SVCursorOutput } from './SVCursor' -import { useOutputParam } from '../redux/realtimeStore' +import { useOutputParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/renderer/controls/XYAxisCursor.tsx b/src/features/params/react/controls/XYAxisCursor.tsx similarity index 75% rename from src/renderer/controls/XYAxisCursor.tsx rename to src/features/params/react/controls/XYAxisCursor.tsx index e7e9e331..34a9bbbf 100644 --- a/src/renderer/controls/XYAxisCursor.tsx +++ b/src/features/params/react/controls/XYAxisCursor.tsx @@ -1,6 +1,6 @@ -import { useOutputParam } from '../redux/realtimeStore' -import Cursor from '../base/Cursor' -import { applyMirror } from 'shared/dmxUtil' +import { useOutputParam } from 'features/params/redux' +import Cursor from '../../../ui/react/base/Cursor' +import { applyMirror } from 'features/dmx/shared/dmxUtil' interface Props { splitIndex: number diff --git a/src/renderer/controls/XYAxisPad.tsx b/src/features/params/react/controls/XYAxisPad.tsx similarity index 91% rename from src/renderer/controls/XYAxisPad.tsx rename to src/features/params/react/controls/XYAxisPad.tsx index 6f7cc3f7..5abf87f6 100644 --- a/src/renderer/controls/XYAxisPad.tsx +++ b/src/features/params/react/controls/XYAxisPad.tsx @@ -1,11 +1,11 @@ -import useDragMapped from '../hooks/useDragMapped' +import useDragMapped from '../../../ui/react/hooks/useDragMapped' import { useDispatch } from 'react-redux' -import { setBaseParams } from '../redux/controlSlice' +import { setBaseParams } from '../../../../renderer/redux/controlSlice' import XYAxisCursor from './XYAxisCursor' import styled from 'styled-components' -import { useBaseParam } from 'renderer/redux/store' import ParamXButton from './ParamXButton' import { paramBundles } from './ParamAddButton' +import { useBaseParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/renderer/controls/XYCursor.tsx b/src/features/params/react/controls/XYCursor.tsx similarity index 83% rename from src/renderer/controls/XYCursor.tsx rename to src/features/params/react/controls/XYCursor.tsx index 0666520f..fcd0c478 100644 --- a/src/renderer/controls/XYCursor.tsx +++ b/src/features/params/react/controls/XYCursor.tsx @@ -1,6 +1,7 @@ -import { useOutputParam } from '../redux/realtimeStore' -import Cursor from '../base/Cursor' -import { useBaseParam } from '../redux/store' + +import { useBaseParam, useOutputParam } from 'features/params/redux' +import Cursor from '../../../ui/react/base/Cursor' + interface Props { splitIndex: number diff --git a/src/renderer/controls/XYWindow.tsx b/src/features/params/react/controls/XYWindow.tsx similarity index 81% rename from src/renderer/controls/XYWindow.tsx rename to src/features/params/react/controls/XYWindow.tsx index 9cd1265f..e62afecc 100644 --- a/src/renderer/controls/XYWindow.tsx +++ b/src/features/params/react/controls/XYWindow.tsx @@ -1,5 +1,6 @@ -import { useOutputParam } from '../redux/realtimeStore' -import Window2D from '../base/Window2D' + +import { useOutputParam } from 'features/params/redux' +import Window2D from '../../../ui/react/base/Window2D' interface Props { splitIndex: number diff --git a/src/renderer/controls/XyParamsPad.tsx b/src/features/params/react/controls/XyParamsPad.tsx similarity index 85% rename from src/renderer/controls/XyParamsPad.tsx rename to src/features/params/react/controls/XyParamsPad.tsx index 9a53b52d..21a6835d 100644 --- a/src/renderer/controls/XyParamsPad.tsx +++ b/src/features/params/react/controls/XyParamsPad.tsx @@ -1,14 +1,14 @@ -import useDragMapped from '../hooks/useDragMapped' +import useDragMapped from '../../../ui/react/hooks/useDragMapped' import { useDispatch } from 'react-redux' -import { setBaseParams, incrementBaseParams } from '../redux/controlSlice' +import { setBaseParams, incrementBaseParams } from '../../../../renderer/redux/controlSlice' import { XYCursorBase, XYCursorOutput } from './XYCursor' import XYWindow from './XYWindow' import styled from 'styled-components' import ParamXButton from './ParamXButton' -import { useBaseParam } from 'renderer/redux/store' -import MidiOverlay_xy from '../base/MidiOverlay_xy' +import MidiOverlay_xy from 'features/midi/react/MidiOverlay_xy' import { paramBundles } from './ParamAddButton' -import { secondaryEnabled } from 'renderer/base/keyUtil' +import { secondaryEnabled } from 'features/ui/react/base/keyUtil' +import { useBaseParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/features/params/redux/index.ts b/src/features/params/redux/index.ts new file mode 100644 index 00000000..985a1a6c --- /dev/null +++ b/src/features/params/redux/index.ts @@ -0,0 +1,62 @@ +import { + DefaultParam, + Params, + defaultOutputParams, +} from 'features/params/shared/params' + +import { useActiveLightScene } from 'renderer/redux/store' +import { useRealtimeSelector } from 'renderer/redux/realtimeStore' + +export function useBaseParam( + param: Param, + splitIndex: number +): number | undefined { + const baseParam = useActiveLightScene((state) => { + return state.splitScenes[splitIndex].baseParams[param] + }) + return baseParam +} + +export function useBaseParams(splitIndex: number): Partial { + const baseParams = useActiveLightScene((state) => { + return state.splitScenes[splitIndex].baseParams + }) + return baseParams +} + +export function useModParam( + param: DefaultParam | string, + modIndex: number, + splitIndex: number +) { + return useActiveLightScene((scene) => { + return scene.modulators[modIndex].splitModulations[splitIndex][param] + }) +} + +export function useOutputParam( + param: Param, + splitIndex: number +): number { + const outputParam = useRealtimeSelector((state) => { + return state.splitStates[splitIndex]?.outputParams?.[param] + }) + if (outputParam === undefined) { + console.error( + `useOutputParam called on undefined output param ${param}. That's probably not what you wanted.` + ) + return 0 + } else { + return outputParam + } +} + +export function useOutputParams(splitIndex: number): Partial { + const params = useRealtimeSelector((state) => { + return state.splitStates[splitIndex]?.outputParams + }) + if (params === undefined) { + return defaultOutputParams() + } + return params +} diff --git a/src/features/params/redux/reducer.ts b/src/features/params/redux/reducer.ts new file mode 100644 index 00000000..0322fe7b --- /dev/null +++ b/src/features/params/redux/reducer.ts @@ -0,0 +1,93 @@ +import { PayloadAction } from '@reduxjs/toolkit' + +import { DefaultParam, Params } from '../shared/params' +import { clampNormalized } from 'features/utils/math/util' +import { + createTypedReducers, + modifyActiveLightScene, +} from 'renderer/redux/controlSlice/reducers/core' + +type ParamsAction = PayloadAction<{ + splitIndex: number + params: Partial +}> + +type ParamAction = PayloadAction<{ + splitIndex: number + paramKey: DefaultParam | string + value: number | undefined +}> + +export interface SetModulationPayload { + splitIndex: number + modIndex: number + param: DefaultParam | string + value: number | undefined +} + +export const paramsActionReducer = createTypedReducers({ + setBaseParams: (state, { payload: { params, splitIndex } }: ParamsAction) => { + for (let [key, value] of Object.entries(params)) { + modifyActiveLightScene(state, (scene) => { + const baseParams = scene.splitScenes[splitIndex].baseParams + baseParams[key] = value + }) + } + }, + setBaseParam: ( + state, + { payload: { paramKey, value, splitIndex } }: ParamAction + ) => { + modifyActiveLightScene(state, (scene) => { + const baseParams = scene.splitScenes[splitIndex].baseParams + baseParams[paramKey] = value + }) + }, + deleteBaseParams: ( + state, + { + payload: { params, splitIndex }, + }: PayloadAction<{ + splitIndex: number + params: readonly (DefaultParam | string)[] + }> + ) => { + for (const param of params) { + modifyActiveLightScene(state, (scene) => { + const baseParams = scene.splitScenes[splitIndex].baseParams + delete baseParams[param] + + // Now remove the params from any modulators + scene.modulators.forEach((modulator) => { + const modulation = modulator.splitModulations[splitIndex] + delete modulation[param] + }) + }) + } + }, + incrementBaseParams: ( + state, + { payload: { params, splitIndex } }: ParamsAction + ) => { + for (let [key, amount] of Object.entries(params)) { + modifyActiveLightScene(state, (scene) => { + if (amount !== undefined) { + const baseParams = scene.splitScenes[splitIndex].baseParams + const currentVal = baseParams[key as DefaultParam] + if (currentVal !== undefined) { + baseParams[key as DefaultParam] = clampNormalized( + currentVal + amount + ) + } + } + }) + } + }, + // TODO: move this to modulators reducer once Params type is not needed + setModulation: (state, { payload }: PayloadAction) => { + const { splitIndex, modIndex, param, value } = payload + modifyActiveLightScene(state, (scene) => { + scene.modulators[modIndex].splitModulations[splitIndex][param] = value + }) + }, +}) diff --git a/src/shared/params.ts b/src/features/params/shared/params.ts similarity index 59% rename from src/shared/params.ts rename to src/features/params/shared/params.ts index 36ea1d18..c3b04eeb 100644 --- a/src/shared/params.ts +++ b/src/features/params/shared/params.ts @@ -1,3 +1,9 @@ +import { + DmxState, + getCustomChannels, +} from 'features/fixtures/redux/fixturesSlice' +import { Pretty } from 'features/shared/shared/type-utils' + export type DefaultParam = | 'hue' | 'saturation' @@ -13,9 +19,12 @@ export type DefaultParam = | 'yAxis' | 'xMirror' -export type Params = { [key: string]: number | undefined } +export type StrictParams = { [key in DefaultParam]: number | undefined } +export type Params = Pretty< + StrictParams & { [key: string]: number | undefined } +> -export function initBaseParams(): Params { +export function initBaseParams(): Partial { return { hue: 0.5, saturation: 0.5, @@ -26,19 +35,8 @@ export function initBaseParams(): Params { // Params as they export function initParams(): { [key in DefaultParam]: number } { return { - hue: 0.5, - saturation: 0.5, - brightness: 0.5, - x: 0.5, - width: 1.0, - y: 0.5, - height: 1.0, - intensity: 1.0, - strobe: 0.0, + ...defaultParams, randomize: 1.0, - xAxis: 0.5, - yAxis: 0.5, - xMirror: 0.0, } } @@ -58,15 +56,22 @@ const defaultParams: { [key in DefaultParam]: number } = { xMirror: 0.0, } -export function getParam(params: Params, param: DefaultParam): number { +export function getParam( + params: Partial, + param: DefaultParam +): number { return params[param] ?? defaultParams[param] } -export function defaultOutputParams(): Params { +export function getAllParamKeys(dmx: DmxState): string[] { + return (defaultParamsList as string[]).concat( + Array.from(getCustomChannels(dmx)) + ) +} + +export function defaultOutputParams(): Partial { return { - hue: 0.5, - saturation: 0.5, - brightness: 0.5, + ...initBaseParams(), // x: 0.5, // width: 1.0, // y: 0.5, @@ -96,8 +101,4 @@ export const defaultParamsList: DefaultParam[] = [ 'xMirror', ] -export type Modulation = Params - -export function initModulation(): Modulation { - return {} -} +export type Modulation = Partial diff --git a/src/features/params/shared/utils.ts b/src/features/params/shared/utils.ts new file mode 100644 index 00000000..c4610918 --- /dev/null +++ b/src/features/params/shared/utils.ts @@ -0,0 +1,16 @@ +import { BaseColors, hsv2rgb } from 'features/utils/baseColors' +import { StrictParams } from './params' + +export function getBaseColors(params: Partial): BaseColors { + const [r, g, b] = hsv2rgb( + params.hue ?? 0, + params.saturation ?? 0, + params.brightness ?? 0 + ) + + return { + red: r, + green: g, + blue: b, + } +} diff --git a/src/shared/autoScene.ts b/src/features/scenes/engine/autoScene.ts similarity index 68% rename from src/shared/autoScene.ts rename to src/features/scenes/engine/autoScene.ts index 121a060c..179dd133 100644 --- a/src/shared/autoScene.ts +++ b/src/features/scenes/engine/autoScene.ts @@ -1,8 +1,8 @@ -import { randomElementExcludeCurrent } from './util' -import { AutoScene_t } from './Scenes' -import { TimeState, isNewPeriod } from './TimeState' -import { CleanReduxState } from '../renderer/redux/store' -import { RealtimeState } from '../renderer/redux/realtimeStore' +import { randomElementExcludeCurrent } from '../../utils/util' +import { AutoScene_t } from '../shared/Scenes' +import { TimeState, isNewPeriod } from '../../bpm/shared/TimeState' +import { CleanReduxState } from '../../../renderer/redux/store' +import { RealtimeState } from '../../../renderer/redux/realtimeStore' type OnNewScene = (id: string) => void @@ -23,14 +23,22 @@ const _lastUserModified = { visual: initUserModified(), } -export function handleAutoScene( - lastRtState: RealtimeState, - nextTimeState: TimeState, - controlState: CleanReduxState, - onNewLightScene: OnNewScene, - onNewVisualScene: OnNewScene -) { - const { light, visual } = controlState.control +export function handleAutoScene({ + onNew, + states, +}: { + states: { + // previous realtimeState + realtimeState: RealtimeState + timeState: TimeState + controlState: CleanReduxState + } + onNew: { + lightScene: OnNewScene + visualScene: OnNewScene + } +}) { + const { light, visual } = states.controlState.control let possibleLightIds = light.ids.filter((id) => { const lightScene = light.byId[id] @@ -45,15 +53,15 @@ export function handleAutoScene( if ( isNewScene( light.active, - lastRtState.time.beats, - nextTimeState, + states.realtimeState.time.beats, + states.timeState, light.auto, _lastUserModified.light ) ) { const newScene = randomElementExcludeCurrent(possibleLightIds, light.active) _lastUserModified.light.scene = newScene - onNewLightScene(newScene) + onNew.lightScene(newScene) } let possibleVisualIds = visual.ids.filter((id) => { @@ -66,8 +74,8 @@ export function handleAutoScene( if ( isNewScene( visual.active, - lastRtState.time.beats, - nextTimeState, + states.realtimeState.time.beats, + states.timeState, visual.auto, _lastUserModified.visual ) @@ -77,7 +85,7 @@ export function handleAutoScene( visual.active ) _lastUserModified.visual.scene = newScene - onNewVisualScene(newScene) + onNew.visualScene(newScene) } } diff --git a/src/renderer/controls/MasterSlider.tsx b/src/features/scenes/react/controls/MasterSlider.tsx similarity index 66% rename from src/renderer/controls/MasterSlider.tsx rename to src/features/scenes/react/controls/MasterSlider.tsx index 3d8a31e0..ed6bb024 100644 --- a/src/renderer/controls/MasterSlider.tsx +++ b/src/features/scenes/react/controls/MasterSlider.tsx @@ -1,15 +1,15 @@ -import Slider from '../base/Slider' -import { useControlSelector } from '../redux/store' +import Slider from '../../../ui/react/base/Slider' +import { useControlSelector } from '../../../../renderer/redux/store' import { useDispatch } from 'react-redux' -import { setMaster } from '../redux/controlSlice' -import { SliderMidiOverlay } from '../base/MidiOverlay' +import { setMaster } from '../../../../renderer/redux/controlSlice' +import { RangeMidiOverlay } from 'features/midi/react/MidiOverlay' export default function MasterSlider() { const master = useControlSelector((state) => state.master) const dispatch = useDispatch() return ( - - + ) } diff --git a/src/renderer/controls/UndoRedo.tsx b/src/features/scenes/react/controls/UndoRedo.tsx similarity index 97% rename from src/renderer/controls/UndoRedo.tsx rename to src/features/scenes/react/controls/UndoRedo.tsx index 16cc4bbb..e34792cc 100644 --- a/src/renderer/controls/UndoRedo.tsx +++ b/src/features/scenes/react/controls/UndoRedo.tsx @@ -4,7 +4,7 @@ import { undoActionTypes, UndoGroup, ReduxState, -} from '../redux/store' +} from '../../../../renderer/redux/store' import { useDispatch } from 'react-redux' import UndoIcon from '@mui/icons-material/Undo' import RedoIcon from '@mui/icons-material/Redo' diff --git a/src/renderer/scenes/AutoScene.tsx b/src/features/scenes/react/scenes/AutoScene.tsx similarity index 84% rename from src/renderer/scenes/AutoScene.tsx rename to src/features/scenes/react/scenes/AutoScene.tsx index 635b9130..a7c9dc46 100644 --- a/src/renderer/scenes/AutoScene.tsx +++ b/src/features/scenes/react/scenes/AutoScene.tsx @@ -1,15 +1,15 @@ -import Slider from '../base/Slider' +import Slider from '../../../ui/react/base/Slider' import styled from 'styled-components' import { useDispatch } from 'react-redux' -import { useControlSelector } from '../redux/store' +import { useControlSelector } from '../../../../renderer/redux/store' import { setAutoSceneEnabled, setAutoSceneBombacity, setAutoScenePeriod, -} from '../redux/controlSlice' +} from '../../../../renderer/redux/controlSlice' import { SceneType } from '../../shared/Scenes' -import DraggableNumber from '../base/DraggableNumber' -import { SliderMidiOverlay } from 'renderer/base/MidiOverlay' +import DraggableNumber from '../../../ui/react/base/DraggableNumber' +import { RangeMidiOverlay } from 'features/midi/react/MidiOverlay' export default function AutoScene({ sceneType }: { sceneType: SceneType }) { const dispatch = useDispatch() @@ -64,7 +64,7 @@ export default function AutoScene({ sceneType }: { sceneType: SceneType }) { }} /> {sceneType === 'light' && ( - @@ -75,7 +75,7 @@ export default function AutoScene({ sceneType }: { sceneType: SceneType }) { onChange={onBombacityChange} color={enabled ? '#3d5e' : undefined} /> - + )}
) diff --git a/src/renderer/scenes/GroupSelection.tsx b/src/features/scenes/react/scenes/GroupSelection.tsx similarity index 96% rename from src/renderer/scenes/GroupSelection.tsx rename to src/features/scenes/react/scenes/GroupSelection.tsx index 9a233b7f..33d5e793 100644 --- a/src/renderer/scenes/GroupSelection.tsx +++ b/src/features/scenes/react/scenes/GroupSelection.tsx @@ -4,13 +4,13 @@ import RemoveIcon from '@mui/icons-material/Remove' import { useState } from 'react' import { useActiveLightScene, useDmxSelector } from 'renderer/redux/store' import styled from 'styled-components' -import Popup from '../base/Popup' +import Popup from '../../../ui/react/base/Popup' import { useDispatch } from 'react-redux' import { removeSplitSceneByIndex, setSceneGroup, } from 'renderer/redux/controlSlice' -import { getSortedGroups } from 'shared/dmxUtil' +import { getSortedGroups } from 'features/dmx/shared/dmxUtil' interface Props { splitIndex: number diff --git a/src/renderer/scenes/Scene.tsx b/src/features/scenes/react/scenes/Scene.tsx similarity index 94% rename from src/renderer/scenes/Scene.tsx rename to src/features/scenes/react/scenes/Scene.tsx index 28442334..addb95de 100644 --- a/src/renderer/scenes/Scene.tsx +++ b/src/features/scenes/react/scenes/Scene.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { useControlSelector } from '../redux/store' +import { useControlSelector } from '../../../../renderer/redux/store' import { useDispatch } from 'react-redux' import { setActiveScene, @@ -9,14 +9,14 @@ import { setActiveSceneName, copyActiveScene, setActiveSceneAutoEnabled, -} from '../redux/controlSlice' +} from '../../../../renderer/redux/controlSlice' import { IconButton } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import AddIcon from '@mui/icons-material/Add' import DisableIcon from '@mui/icons-material/DoNotDisturb' -import Slider from '../base/Slider' -import { ButtonMidiOverlay } from '../base/MidiOverlay' -import Input from '../base/Input' +import Slider from '../../../ui/react/base/Slider' +import { ButtonMidiOverlay } from 'features/midi/react/MidiOverlay' +import Input from '../../../ui/react/base/Input' import { Draggable } from 'react-beautiful-dnd' import DragHandleIcon from '@mui/icons-material/DragHandle' import CopyIcon from '@mui/icons-material/FileCopy' @@ -104,7 +104,7 @@ export function Scene({ sceneType, index, id }: Props) { action={{ type: 'setActiveSceneIndex', sceneType: sceneType, - index: index, + val: index }} > randomizer: RandomizerOptions // true = include group | false = include not group groups: { [key: string]: boolean | undefined } diff --git a/src/features/shared/engine/emissions.ts b/src/features/shared/engine/emissions.ts new file mode 100644 index 00000000..0d795f69 --- /dev/null +++ b/src/features/shared/engine/emissions.ts @@ -0,0 +1,47 @@ +import type { + MainCommand, + UserCommand, +} from 'features/shared/engine/ipc_channels' +import type { PayloadAction } from '@reduxjs/toolkit' +import type { RealtimeState } from 'renderer/redux/realtimeStore' +import type * as dmxConnection from 'features/dmx/engine/dmxConnection' +import type * as midiConnection from 'features/midi/engine/midiConnection' +import { CleanReduxState } from 'renderer/redux/store' +import { VisualizerResource } from 'features/visualizer/threejs/VisualizerManager' + +export type API = { + renderer: { + subscriptions: { + main_command: [command: MainCommand] + dispatch: [action: PayloadAction] + new_time_state: [realtimeState: RealtimeState] + midi_connection_update: [payload: midiConnection.UpdatePayload] + dmx_connection_update: [payload: dmxConnection.UpdatePayload] + } + mutations: { + new_control_state: [new_state: CleanReduxState] + user_command: [command: UserCommand] + open_visualizer: [] + } + queries: { + get_local_filepaths: { + input: [title: string, fileFilters: Electron.FileFilter[]] + output: string[] + } + load_file: { + input: [title: string, fileFilters: Electron.FileFilter[]] + output: string + } + save_file: { + input: [title: string, data: string, fileFilters: Electron.FileFilter[]] + output: void + } + } + } + + visualizer: { + subscriptions: { + new_visualizer_state: [payload: VisualizerResource] + } + } +} diff --git a/src/features/shared/engine/index.ts b/src/features/shared/engine/index.ts new file mode 100644 index 00000000..cab1df2b --- /dev/null +++ b/src/features/shared/engine/index.ts @@ -0,0 +1,17 @@ +export const createDisposer = () => { + const disposer = { + _disposeables: [] as { dispose(): void }[], + push(disposeable: T) { + disposer._disposeables.push(disposeable) + return disposeable + }, + dispose() { + disposer._disposeables.forEach((disposeable) => { + disposeable.dispose() + }) + disposer._disposeables = [] + }, + } + + return disposer; +} diff --git a/src/shared/ipc_channels.ts b/src/features/shared/engine/ipc_channels.ts similarity index 92% rename from src/shared/ipc_channels.ts rename to src/features/shared/engine/ipc_channels.ts index 1d870a51..d16563a7 100644 --- a/src/shared/ipc_channels.ts +++ b/src/features/shared/engine/ipc_channels.ts @@ -1,4 +1,4 @@ -export default { +const channels = { new_time_state: 'new_time_state', dmx_connection_update: 'dmx_connection_update', midi_connection_update: 'midi_connection_update', @@ -12,6 +12,10 @@ export default { main_command: 'main_command', } as const +export default channels + +export type ChannelNames = typeof channels[keyof typeof channels] + export interface SetLinkEnabled { type: 'SetLinkEnabled' isEnabled: boolean diff --git a/src/features/shared/react/index.ts b/src/features/shared/react/index.ts new file mode 100644 index 00000000..4b317442 --- /dev/null +++ b/src/features/shared/react/index.ts @@ -0,0 +1,7 @@ +export const animationLoop = (cb: () => void) => { + const loop = () => { + cb() + requestAnimationFrame(loop) + } + loop() +} diff --git a/src/shared/fixState.ts b/src/features/shared/redux/fixState.ts similarity index 84% rename from src/shared/fixState.ts rename to src/features/shared/redux/fixState.ts index a907c471..2cb8df25 100644 --- a/src/shared/fixState.ts +++ b/src/features/shared/redux/fixState.ts @@ -1,18 +1,25 @@ -import { DeviceState } from 'renderer/redux/deviceState' -import { DmxState } from 'renderer/redux/dmxSlice' -import { initLedState } from 'renderer/redux/ledState' -import { CleanReduxState } from '../renderer/redux/store' -import { ColorChannel } from './dmxColors' -import { DmxValue, FixtureChannel, initChannelCustom } from './dmxFixtures' -import { Modulator } from './modulation' -import { Modulation, Params } from './params' -import { RandomizerOptions } from './randomizer' +/** + * This file seems to directly only relate to file saving +src/features/fileSaving/shared/save.ts +src/features/fileSaving/react/autosave.ts + */ +import { DeviceState } from 'features/midi/redux' +import { DmxState } from 'features/fixtures/redux/fixturesSlice' +import { initLedState } from 'features/led/redux/ledState' +import { CleanReduxState } from '../../../renderer/redux/store' +import { ColorChannel } from 'features/dmx/shared/dmxColors' +import { DmxValue, FixtureChannel } from 'features/dmx/shared/dmxFixtures' +import { Modulator } from '../../modulation/shared/modulation' +import { Modulation, Params } from '../../params/shared/params' +import { RandomizerOptions } from '../../bpm/shared/randomizer' + import { LightScenes_t, LightScene_t, SplitScene_t, VisualScenes_t, -} from './Scenes' +} from '../../scenes/shared/Scenes' +import { channelConfig } from 'features/dmx/channel.config' type Deprecated_ChannelOther = { type: 'other' @@ -112,11 +119,12 @@ export function fixDmxState(dmx: DmxState) { } fixture.channels[i] = newChannel } else if (channel.type === 'other') { - const newChannel = initChannelCustom('Other') + const newChannel = channelConfig.custom.default({ name: 'Other' }) + newChannel.default = channel.default fixture.channels[i] = newChannel } else if (channel.type === 'reset') { - const newChannel = initChannelCustom('Reset') + const newChannel = channelConfig.custom.default({ name: 'Reset' }) fixture.channels[i] = newChannel } } diff --git a/src/features/shared/shared/type-utils.ts b/src/features/shared/shared/type-utils.ts new file mode 100644 index 00000000..7383fd00 --- /dev/null +++ b/src/features/shared/shared/type-utils.ts @@ -0,0 +1,5 @@ +export type Pretty = T extends object + ? {} & { + [P in keyof T]: T[P] + } + : T diff --git a/src/shared/window.ts b/src/features/shared/shared/window.ts similarity index 70% rename from src/shared/window.ts rename to src/features/shared/shared/window.ts index 37fcfcec..641a3f84 100644 --- a/src/shared/window.ts +++ b/src/features/shared/shared/window.ts @@ -1,4 +1,4 @@ -import { Normalized } from '../math/util' +import { Normalized } from '../../utils/math/util' export type Window = { pos: Normalized diff --git a/src/renderer/base/Button.tsx b/src/features/ui/react/base/Button.tsx similarity index 69% rename from src/renderer/base/Button.tsx rename to src/features/ui/react/base/Button.tsx index 46b6d7d0..c7505088 100644 --- a/src/renderer/base/Button.tsx +++ b/src/features/ui/react/base/Button.tsx @@ -8,16 +8,17 @@ type Props = { export default function Button({ label, fontSize = '0.9rem', onClick }: Props) { return ( - + {label} - + ) } -const Root = styled.div` +export const StyledButton = styled.button` background-color: #222; color: #fff; border: 1px solid #888; border-radius: 3px; padding: 0.1rem 0.2rem; + font-size: 0.9rem; ` diff --git a/src/renderer/base/ColorPicker.tsx b/src/features/ui/react/base/ColorPicker.tsx similarity index 91% rename from src/renderer/base/ColorPicker.tsx rename to src/features/ui/react/base/ColorPicker.tsx index 336d8390..57621bc5 100644 --- a/src/renderer/base/ColorPicker.tsx +++ b/src/features/ui/react/base/ColorPicker.tsx @@ -1,10 +1,10 @@ import styled from 'styled-components' -import { Normalized } from 'math/util' +import { Normalized } from 'features/utils/math/util' import { approximateStandardColor, colorByName, standardColorNames, -} from '../../shared/dmxColors' +} from 'features/dmx/shared/dmxColors' import wrapClick from './wrapClick' interface Props { diff --git a/src/renderer/base/Cursor.tsx b/src/features/ui/react/base/Cursor.tsx similarity index 100% rename from src/renderer/base/Cursor.tsx rename to src/features/ui/react/base/Cursor.tsx diff --git a/src/renderer/base/Divider.tsx b/src/features/ui/react/base/Divider.tsx similarity index 100% rename from src/renderer/base/Divider.tsx rename to src/features/ui/react/base/Divider.tsx diff --git a/src/features/ui/react/base/DraggableNumber.tsx b/src/features/ui/react/base/DraggableNumber.tsx new file mode 100644 index 00000000..ea6699e8 --- /dev/null +++ b/src/features/ui/react/base/DraggableNumber.tsx @@ -0,0 +1,218 @@ +import { DownIcon, UpIcon } from 'features/ui/react/icons/arrows' +import { clamp } from 'features/utils/math/util' +import useDragBasic from '../hooks/useDragBasic' +import styled from 'styled-components' +import { double_incremented, halve_incremented } from 'features/utils/util' +import React, { MutableRefObject } from 'react' +import { secondaryEnabled } from './keyUtil' + +type Type = 'continuous' | 'snap' + +interface UseDraggableNumber { + adjustments: { primary: T; secondary: T } + value: number + min: number + max: number + onChange: (newVal: number) => void + + valueRef: MutableRefObject + movementRef: MutableRefObject + suffix?: string +} + +interface Props + extends Omit { + style?: React.CSSProperties + noArrows?: boolean + adjustments?: { primary?: Type; secondary?: Type } +} + +// This is really bad react behavior... But IDK what else to do +// This only works if you can't drag more than 1 thing at a time +let globalMovementRef = { current: 0 } +let globalValueRef = { current: 0 } + +const useDraggableNumber = ( + { + onChange, + min, + value, + valueRef, + movementRef, + max, + adjustments: adjustmentsConfig, + suffix, + }: UseDraggableNumber, + { + calculate, + toString: toStringConfig, + }: { + toString: Partial<{ + [k in T]: (v: number) => string + }> + calculate: { + [k in T]: (params: { + valueRef: MutableRefObject + movementRef: MutableRefObject + d: number + }) => void + } + } +) => { + const speedAdjust = 500 / (max - min) + const [dragContainer, onMouseDown] = useDragBasic((e) => { + const dx = e.movementX + const dy = -e.movementY + // need to rename this better + const d = (dx + dy) / speedAdjust + + // use when control key pressed + if (secondaryEnabled(e)) { + calculate[adjustmentsConfig.secondary]({ movementRef, valueRef, d }) + } else { + calculate[adjustmentsConfig.primary]({ movementRef, valueRef, d }) + } + + valueRef.current = clamp(valueRef.current, min, max) + + onChange(valueRef.current) + }) + + const valueToString = (v: number) => { + const toString = toStringConfig[adjustmentsConfig.primary] + + let valueString = toString ? toString(v) : v.toFixed(2) + + if (suffix) valueString += suffix + + return valueString + } + + const onUp = () => { + const doubled = double_incremented(value) + onChange(Math.min(doubled, max)) + } + + const onDown = () => { + const halved = halve_incremented(value) + onChange(Math.max(halved, min)) + } + + return { + dragContainer, + onMouseDown: (e: React.MouseEvent) => { + movementRef.current = 0 + valueRef.current = value + onMouseDown(e) + }, + valueToString, + onUp, + onDown, + } +} + +// type AdjustmentsConfig = { +// ToString: Partial<{ +// [k in Type]: (v: number) => string +// }> +// } + +export default function DraggableNumber({ + adjustments: adjustmentsConfig = { primary: 'snap', secondary: 'continuous' }, + value, + style, + noArrows, + ...useDraggableNumberProps +}: Props) { + const adjustments = Object.assign( + { primary: 'snap', secondary: 'continuous' }, + adjustmentsConfig + ) + const { dragContainer, onMouseDown, valueToString, onDown, onUp } = + useDraggableNumber( + { + movementRef: globalMovementRef, + value, + valueRef: globalValueRef, + adjustments, + ...useDraggableNumberProps, + }, + { + toString: { + snap(v) { + if (Number.isInteger(v)) return v.toString() + return v.toFixed(2) + }, + }, + calculate: { + continuous: ({ valueRef, d }) => { + valueRef.current += d / 2 + }, + snap: ({ movementRef, valueRef, d }) => { + if (Number.isInteger(valueRef.current)) { + movementRef.current += d + if (movementRef.current > 1) { + valueRef.current += 1 + movementRef.current = 0 + } else if (movementRef.current < -1) { + valueRef.current -= 1 + movementRef.current = 0 + } + } else { + // We are somewhere between integers. + // "Snap" to the next integer + const floor = Math.floor(valueRef.current) + const ceil = Math.ceil(valueRef.current) + valueRef.current += d + if (valueRef.current > ceil) { + valueRef.current = ceil + } else if (valueRef.current < floor) { + valueRef.current = floor + } + } + }, + }, + } + ) + + return ( + + + {valueToString(value)} + + + {!noArrows && ( + <> + + + + + + + + )} + + + ) +} + +const Root = styled.div` + display: flex; + align-items: center; + background-color: #0005; +` +const DragArea = styled.div` + padding: 0.5rem 0.3rem 0.5rem 0.7rem; + cursor: move; + flex: 1; +` +const Col = styled.div` + display: flex; + flex-direction: column; + align-items: center; +` +const Arrow = styled.div` + cursor: pointer; + padding: 0.4rem; + font-size: 0; // for some reason this is necessary to make the svg render correctly? +` diff --git a/src/renderer/base/Dropdown.tsx b/src/features/ui/react/base/Dropdown.tsx similarity index 100% rename from src/renderer/base/Dropdown.tsx rename to src/features/ui/react/base/Dropdown.tsx diff --git a/src/renderer/base/ExternalLink.tsx b/src/features/ui/react/base/ExternalLink.tsx similarity index 94% rename from src/renderer/base/ExternalLink.tsx rename to src/features/ui/react/base/ExternalLink.tsx index 288da036..08e71e55 100644 --- a/src/renderer/base/ExternalLink.tsx +++ b/src/features/ui/react/base/ExternalLink.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' import { shell } from 'electron' -import wrapClick from '../base/wrapClick' +import wrapClick from './wrapClick' import OpenInNewIcon from '@mui/icons-material/OpenInNew' interface Props { diff --git a/src/renderer/base/FullscreenModal.tsx b/src/features/ui/react/base/FullscreenModal.tsx similarity index 95% rename from src/renderer/base/FullscreenModal.tsx rename to src/features/ui/react/base/FullscreenModal.tsx index cb98b9e4..cf985f5c 100644 --- a/src/renderer/base/FullscreenModal.tsx +++ b/src/features/ui/react/base/FullscreenModal.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import zIndexes from '../zIndexes' +import zIndexes from '../../../../renderer/zIndexes' import wrapClick from './wrapClick' export interface ModalButton { diff --git a/src/renderer/base/GroupPicker.tsx b/src/features/ui/react/base/GroupPicker.tsx similarity index 98% rename from src/renderer/base/GroupPicker.tsx rename to src/features/ui/react/base/GroupPicker.tsx index f5c4acb3..04d58413 100644 --- a/src/renderer/base/GroupPicker.tsx +++ b/src/features/ui/react/base/GroupPicker.tsx @@ -3,7 +3,7 @@ import EditIcon from '@mui/icons-material/Edit' import { TextField } from '@mui/material' import { useState } from 'react' import styled from 'styled-components' -import Popup from '../base/Popup' +import Popup from './Popup' import { Button } from '@mui/material' import wrapClick from './wrapClick' diff --git a/src/renderer/base/HSpad.tsx b/src/features/ui/react/base/HSpad.tsx similarity index 95% rename from src/renderer/base/HSpad.tsx rename to src/features/ui/react/base/HSpad.tsx index 47358286..a25fff43 100644 --- a/src/renderer/base/HSpad.tsx +++ b/src/features/ui/react/base/HSpad.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components' import useDragMapped from '../hooks/useDragMapped' import Cursor from './Cursor' -import { Normalized } from 'math/util' +import { Normalized } from 'features/utils/math/util' export interface ColorChannelProps { hue: Normalized diff --git a/src/renderer/base/Input.tsx b/src/features/ui/react/base/Input.tsx similarity index 100% rename from src/renderer/base/Input.tsx rename to src/features/ui/react/base/Input.tsx diff --git a/src/renderer/base/LabelledCheckbox.tsx b/src/features/ui/react/base/LabelledCheckbox.tsx similarity index 100% rename from src/renderer/base/LabelledCheckbox.tsx rename to src/features/ui/react/base/LabelledCheckbox.tsx diff --git a/src/renderer/base/NumberField.tsx b/src/features/ui/react/base/NumberField.tsx similarity index 92% rename from src/renderer/base/NumberField.tsx rename to src/features/ui/react/base/NumberField.tsx index 3335b580..3a13c531 100644 --- a/src/renderer/base/NumberField.tsx +++ b/src/features/ui/react/base/NumberField.tsx @@ -1,5 +1,5 @@ import { TextField } from '@mui/material' -import { clampMaybe } from '../../math/util' +import { clampMaybe } from '../../../utils/math/util' interface Props2 { val: number diff --git a/src/renderer/base/Popup.tsx b/src/features/ui/react/base/Popup.tsx similarity index 97% rename from src/renderer/base/Popup.tsx rename to src/features/ui/react/base/Popup.tsx index 12f9fbce..f77e6a10 100644 --- a/src/renderer/base/Popup.tsx +++ b/src/features/ui/react/base/Popup.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' import IconButton from '@mui/material/IconButton' -import zIndexes from '../zIndexes' +import zIndexes from '../../../../renderer/zIndexes' import CloseIcon from '@mui/icons-material/Close' import useBounds from '../hooks/useBounds' diff --git a/src/renderer/base/Select.tsx b/src/features/ui/react/base/Select.tsx similarity index 100% rename from src/renderer/base/Select.tsx rename to src/features/ui/react/base/Select.tsx diff --git a/src/renderer/base/SelectList.tsx b/src/features/ui/react/base/SelectList.tsx similarity index 100% rename from src/renderer/base/SelectList.tsx rename to src/features/ui/react/base/SelectList.tsx diff --git a/src/renderer/base/Slider.tsx b/src/features/ui/react/base/Slider.tsx similarity index 100% rename from src/renderer/base/Slider.tsx rename to src/features/ui/react/base/Slider.tsx diff --git a/src/renderer/base/SliderBase.tsx b/src/features/ui/react/base/SliderBase.tsx similarity index 100% rename from src/renderer/base/SliderBase.tsx rename to src/features/ui/react/base/SliderBase.tsx diff --git a/src/renderer/base/SliderCursor.tsx b/src/features/ui/react/base/SliderCursor.tsx similarity index 100% rename from src/renderer/base/SliderCursor.tsx rename to src/features/ui/react/base/SliderCursor.tsx diff --git a/src/renderer/base/SplitPane.tsx b/src/features/ui/react/base/SplitPane.tsx similarity index 97% rename from src/renderer/base/SplitPane.tsx rename to src/features/ui/react/base/SplitPane.tsx index e0c72fc0..e691707a 100644 --- a/src/renderer/base/SplitPane.tsx +++ b/src/features/ui/react/base/SplitPane.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import useDragMapped from '../hooks/useDragMapped' -import { clamp } from '../../math/util' +import { clamp } from '../../../utils/math/util' type Props = { type: 'vertical' | 'horizontal' diff --git a/src/renderer/base/Toggle.tsx b/src/features/ui/react/base/Toggle.tsx similarity index 100% rename from src/renderer/base/Toggle.tsx rename to src/features/ui/react/base/Toggle.tsx diff --git a/src/renderer/base/ToggleButton.tsx b/src/features/ui/react/base/ToggleButton.tsx similarity index 100% rename from src/renderer/base/ToggleButton.tsx rename to src/features/ui/react/base/ToggleButton.tsx diff --git a/src/renderer/base/Window2D.tsx b/src/features/ui/react/base/Window2D.tsx similarity index 92% rename from src/renderer/base/Window2D.tsx rename to src/features/ui/react/base/Window2D.tsx index 6e6a0c5a..c4c404ce 100644 --- a/src/renderer/base/Window2D.tsx +++ b/src/features/ui/react/base/Window2D.tsx @@ -1,4 +1,4 @@ -import { Window2D_t } from '../../shared/window' +import { Window2D_t } from '../../../shared/shared/window' interface Props { window2D: Window2D_t diff --git a/src/renderer/base/Window2D2.tsx b/src/features/ui/react/base/Window2D2.tsx similarity index 97% rename from src/renderer/base/Window2D2.tsx rename to src/features/ui/react/base/Window2D2.tsx index 608ea1d9..7336808c 100644 --- a/src/renderer/base/Window2D2.tsx +++ b/src/features/ui/react/base/Window2D2.tsx @@ -1,4 +1,4 @@ -import { Window2D_t } from '../../shared/window' +import { Window2D_t } from '../../../shared/shared/window' interface Props { window2D: Window2D_t diff --git a/src/renderer/base/XyPad.tsx b/src/features/ui/react/base/XyPad.tsx similarity index 100% rename from src/renderer/base/XyPad.tsx rename to src/features/ui/react/base/XyPad.tsx diff --git a/src/renderer/base/keyUtil.ts b/src/features/ui/react/base/keyUtil.ts similarity index 100% rename from src/renderer/base/keyUtil.ts rename to src/features/ui/react/base/keyUtil.ts diff --git a/src/renderer/base/wrapClick.tsx b/src/features/ui/react/base/wrapClick.tsx similarity index 100% rename from src/renderer/base/wrapClick.tsx rename to src/features/ui/react/base/wrapClick.tsx diff --git a/src/renderer/hooks/useBounds.ts b/src/features/ui/react/hooks/useBounds.ts similarity index 100% rename from src/renderer/hooks/useBounds.ts rename to src/features/ui/react/hooks/useBounds.ts diff --git a/src/renderer/hooks/useDragBasic.ts b/src/features/ui/react/hooks/useDragBasic.ts similarity index 100% rename from src/renderer/hooks/useDragBasic.ts rename to src/features/ui/react/hooks/useDragBasic.ts diff --git a/src/renderer/hooks/useDragMapped.ts b/src/features/ui/react/hooks/useDragMapped.ts similarity index 100% rename from src/renderer/hooks/useDragMapped.ts rename to src/features/ui/react/hooks/useDragMapped.ts diff --git a/src/renderer/hooks/useHover.ts b/src/features/ui/react/hooks/useHover.ts similarity index 100% rename from src/renderer/hooks/useHover.ts rename to src/features/ui/react/hooks/useHover.ts diff --git a/src/renderer/hooks/useMousePosition.ts b/src/features/ui/react/hooks/useMousePosition.ts similarity index 93% rename from src/renderer/hooks/useMousePosition.ts rename to src/features/ui/react/hooks/useMousePosition.ts index c0a5ff45..32514d8a 100644 --- a/src/renderer/hooks/useMousePosition.ts +++ b/src/features/ui/react/hooks/useMousePosition.ts @@ -1,4 +1,4 @@ -import { Point } from 'math/point' +import { Point } from 'features/utils/math/point' import { useEffect, useState } from 'react' // Apparently there's no way to get the position of the mouse without a mouse event... diff --git a/src/renderer/hooks/useSafeCallback.ts b/src/features/ui/react/hooks/useSafeCallback.ts similarity index 100% rename from src/renderer/hooks/useSafeCallback.ts rename to src/features/ui/react/hooks/useSafeCallback.ts diff --git a/src/features/ui/react/icons/IconButton.tsx b/src/features/ui/react/icons/IconButton.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/icons/arrows.tsx b/src/features/ui/react/icons/arrows.tsx similarity index 100% rename from src/renderer/icons/arrows.tsx rename to src/features/ui/react/icons/arrows.tsx diff --git a/src/renderer/images/Thick.png b/src/features/ui/react/images/Thick.png similarity index 100% rename from src/renderer/images/Thick.png rename to src/features/ui/react/images/Thick.png diff --git a/src/renderer/images/Thick_bg.png b/src/features/ui/react/images/Thick_bg.png similarity index 100% rename from src/renderer/images/Thick_bg.png rename to src/features/ui/react/images/Thick_bg.png diff --git a/src/renderer/images/Thin.png b/src/features/ui/react/images/Thin.png similarity index 100% rename from src/renderer/images/Thin.png rename to src/features/ui/react/images/Thin.png diff --git a/src/renderer/images/Thin_bg.png b/src/features/ui/react/images/Thin_bg.png similarity index 100% rename from src/renderer/images/Thin_bg.png rename to src/features/ui/react/images/Thin_bg.png diff --git a/src/features/ui/react/theme/index.tsx b/src/features/ui/react/theme/index.tsx new file mode 100644 index 00000000..cf86fa36 --- /dev/null +++ b/src/features/ui/react/theme/index.tsx @@ -0,0 +1,23 @@ +import { ThemeProvider } from 'styled-components' +import { ThemeProvider as MuiThemeProvider } from '@emotion/react' +import * as themes from './theme' +import { createTheme } from '@mui/material' +import { ReactNode } from 'react' + +const theme = themes.dark() +const muiTheme = createTheme({ + palette: { + mode: 'dark', + }, +}) +export const CaptivateThemeProvider = ({ + children, +}: { + children: ReactNode +}) => { + return ( + + {children} + + ) +} diff --git a/src/renderer/theme.ts b/src/features/ui/react/theme/theme.ts similarity index 100% rename from src/renderer/theme.ts rename to src/features/ui/react/theme/theme.ts diff --git a/src/renderer/redux/guiSlice.ts b/src/features/ui/redux/guiSlice.ts similarity index 95% rename from src/renderer/redux/guiSlice.ts rename to src/features/ui/redux/guiSlice.ts index 27cfb2b4..e22c5ef3 100644 --- a/src/renderer/redux/guiSlice.ts +++ b/src/features/ui/redux/guiSlice.ts @@ -1,11 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { SaveInfo } from '../../shared/save' +import { SaveInfo } from '../../fileSaving/shared/save' import { MidiConnections, DmxConnections, initDmxConnections, initMidiConnections, -} from '../../shared/connection' +} from '../../devices/shared/connection' export type Page = | 'Universe' diff --git a/src/renderer/Duration.ts b/src/features/utils/Duration.ts similarity index 100% rename from src/renderer/Duration.ts rename to src/features/utils/Duration.ts diff --git a/src/shared/RollingAverage.ts b/src/features/utils/RollingAverage.ts similarity index 100% rename from src/shared/RollingAverage.ts rename to src/features/utils/RollingAverage.ts diff --git a/src/shared/baseColors.tsx b/src/features/utils/baseColors.tsx similarity index 86% rename from src/shared/baseColors.tsx rename to src/features/utils/baseColors.tsx index 6e07b3a2..82abab2b 100644 --- a/src/shared/baseColors.tsx +++ b/src/features/utils/baseColors.tsx @@ -1,5 +1,4 @@ -import { Normalized } from '../math/util' -import { Params } from './params' +import { Normalized } from './math/util' export type BaseColor = 'red' | 'green' | 'blue' @@ -52,20 +51,6 @@ export function hsi2rgb(h: Normalized, s: Normalized, i: Normalized): RGB { return [r1 + m, g1 + m, b1 + m] } -export function getBaseColors(params: Params): BaseColors { - const [r, g, b] = hsv2rgb( - params.hue ?? 0, - params.saturation ?? 0, - params.brightness ?? 0 - ) - - return { - red: r, - green: g, - blue: b, - } -} - export function getBaseColorsFromHsv( h: number, s: number, diff --git a/src/math/bezier.ts b/src/features/utils/math/bezier.ts similarity index 100% rename from src/math/bezier.ts rename to src/features/utils/math/bezier.ts diff --git a/src/math/point.ts b/src/features/utils/math/point.ts similarity index 100% rename from src/math/point.ts rename to src/features/utils/math/point.ts diff --git a/src/math/range.ts b/src/features/utils/math/range.ts similarity index 100% rename from src/math/range.ts rename to src/features/utils/math/range.ts diff --git a/src/math/size.ts b/src/features/utils/math/size.ts similarity index 100% rename from src/math/size.ts rename to src/features/utils/math/size.ts diff --git a/src/math/skew.ts b/src/features/utils/math/skew.ts similarity index 100% rename from src/math/skew.ts rename to src/features/utils/math/skew.ts diff --git a/src/math/util.ts b/src/features/utils/math/util.ts similarity index 98% rename from src/math/util.ts rename to src/features/utils/math/util.ts index db7d527d..7ff51f1a 100644 --- a/src/math/util.ts +++ b/src/features/utils/math/util.ts @@ -1,4 +1,4 @@ -import { zip } from '../shared/util' +import { zip } from '../util' export type Normalized = number // 0 to 1 diff --git a/src/shared/util.ts b/src/features/utils/util.ts similarity index 97% rename from src/shared/util.ts rename to src/features/utils/util.ts index 65dafa30..76972d22 100644 --- a/src/shared/util.ts +++ b/src/features/utils/util.ts @@ -1,5 +1,5 @@ -import { random } from '../math/util' -import { Range } from '../math/range' +import { random } from './math/util' +import { Range } from './math/range' export function randomElement(items: Type[]) { return items[randomIndex(items.length)] diff --git a/src/main/engine/createVisualizerWindow.ts b/src/features/visualizer/engine/createVisualizerWindow.ts similarity index 61% rename from src/main/engine/createVisualizerWindow.ts rename to src/features/visualizer/engine/createVisualizerWindow.ts index 3b387fa3..d7c5050b 100644 --- a/src/main/engine/createVisualizerWindow.ts +++ b/src/features/visualizer/engine/createVisualizerWindow.ts @@ -1,26 +1,26 @@ -import path from 'path'; -import { app, BrowserWindow } from 'electron'; -import { resolveHtmlPath } from './util'; +import path from 'path' +import { app, BrowserWindow } from 'electron' +import { resolveHtmlPath } from 'features/util' export interface VisualizerContainer { - visualizer: BrowserWindow | null; + visualizer: BrowserWindow | null } export default function createVisualizerWindow( visualizerContainer: VisualizerContainer ) { if (visualizerContainer.visualizer) { - console.warn('Tried to open a visualizer twice'); - return; + console.warn('Tried to open a visualizer twice') + return } const RESOURCES_PATH = app.isPackaged ? path.join(process.resourcesPath, 'assets') - : path.join(__dirname, '../../assets'); + : path.join(__dirname, '../../assets') const getAssetPath = (...paths: string[]): string => { - return path.join(RESOURCES_PATH, ...paths); - }; + return path.join(RESOURCES_PATH, ...paths) + } visualizerContainer.visualizer = new BrowserWindow({ show: false, @@ -32,19 +32,21 @@ export default function createVisualizerWindow( webSecurity: false, nodeIntegration: false, }, - }); + }) - visualizerContainer.visualizer.loadURL(resolveHtmlPath('index.html')); + visualizerContainer.visualizer.loadURL( + resolveHtmlPath('visualizer', 'index.html') + ) // visualizerContainer.visualizer.loadURL(`data:text/html;charset=utf-8,${html}`) visualizerContainer.visualizer.on('ready-to-show', () => { if (!visualizerContainer.visualizer) { - throw new Error('"visualizer" is not defined'); + throw new Error('"visualizer" is not defined') } - visualizerContainer.visualizer.show(); - }); + visualizerContainer.visualizer.show() + }) visualizerContainer.visualizer.on('closed', () => { - visualizerContainer.visualizer = null; - }); + visualizerContainer.visualizer = null + }) } diff --git a/assets/no_media.png b/src/features/visualizer/images/no_media.png similarity index 100% rename from assets/no_media.png rename to src/features/visualizer/images/no_media.png diff --git a/src/images/particle.png b/src/features/visualizer/images/particle.png similarity index 100% rename from src/images/particle.png rename to src/features/visualizer/images/particle.png diff --git a/src/renderer/visualizer/ActiveEffect.tsx b/src/features/visualizer/react/ActiveEffect.tsx similarity index 93% rename from src/renderer/visualizer/ActiveEffect.tsx rename to src/features/visualizer/react/ActiveEffect.tsx index 4d521a55..a48fa607 100644 --- a/src/renderer/visualizer/ActiveEffect.tsx +++ b/src/features/visualizer/react/ActiveEffect.tsx @@ -1,12 +1,12 @@ import styled from 'styled-components' -import Select from '../base/Select' +import Select from '../../ui/react/base/Select' import { effectTypes, effectDisplayNames, effectTypeFromDisplayName, EffectConfig, initEffectConfig, -} from 'visualizer/threejs/effects/effectConfigs' +} from 'features/visualizer/threejs/effects/effectConfigs' import { activeVisualSceneEffect_set } from 'renderer/redux/controlSlice' import { useDispatch } from 'react-redux' import { useActiveVisualScene } from 'renderer/redux/store' diff --git a/src/renderer/visualizer/Effect.tsx b/src/features/visualizer/react/Effect.tsx similarity index 97% rename from src/renderer/visualizer/Effect.tsx rename to src/features/visualizer/react/Effect.tsx index c389c3d9..1b4be7ee 100644 --- a/src/renderer/visualizer/Effect.tsx +++ b/src/features/visualizer/react/Effect.tsx @@ -9,7 +9,7 @@ import { IconButton } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import DragHandleIcon from '@mui/icons-material/DragHandle' import { Draggable } from 'react-beautiful-dnd' -import { effectDisplayNames } from 'visualizer/threejs/effects/effectConfigs' +import { effectDisplayNames } from 'features/visualizer/threejs/effects/effectConfigs' interface Props { index: number diff --git a/src/renderer/visualizer/EffectEditor.tsx b/src/features/visualizer/react/EffectEditor.tsx similarity index 95% rename from src/renderer/visualizer/EffectEditor.tsx rename to src/features/visualizer/react/EffectEditor.tsx index a1e6c131..95c9d561 100644 --- a/src/renderer/visualizer/EffectEditor.tsx +++ b/src/features/visualizer/react/EffectEditor.tsx @@ -1,14 +1,14 @@ import cloneDeep from 'lodash.clonedeep' import deepEqual from 'deep-equal' import { useState, useEffect } from 'react' -import { EffectConfig } from '../../visualizer/threejs/effects/effectConfigs' +import { EffectConfig } from '../threejs/effects/effectConfigs' import { Button } from '@mui/material' import LayerEditor from './LayerEditor' -import Select from 'renderer/base/Select' +import Select from 'features/ui/react/base/Select' import { visualizerTypeList, initLayerConfig, -} from '../../visualizer/threejs/layers/LayerConfig' +} from '../threejs/layers/LayerConfig' import makeControls from './makeControls' interface Props { diff --git a/src/renderer/visualizer/EffectList.tsx b/src/features/visualizer/react/EffectList.tsx similarity index 94% rename from src/renderer/visualizer/EffectList.tsx rename to src/features/visualizer/react/EffectList.tsx index 54f1f4f2..df7d265f 100644 --- a/src/renderer/visualizer/EffectList.tsx +++ b/src/features/visualizer/react/EffectList.tsx @@ -8,9 +8,9 @@ import { } from 'renderer/redux/controlSlice' import { IconButton } from '@mui/material' import AddIcon from '@mui/icons-material/Add' -import { indexArray } from '../../shared/util' +import { indexArray } from '../../utils/util' import { useActiveVisualScene, useTypedSelector } from 'renderer/redux/store' -import { initEffectConfig } from 'visualizer/threejs/effects/effectConfigs' +import { initEffectConfig } from 'features/visualizer/threejs/effects/effectConfigs' interface Props {} diff --git a/src/renderer/visualizer/Effects.tsx b/src/features/visualizer/react/Effects.tsx similarity index 100% rename from src/renderer/visualizer/Effects.tsx rename to src/features/visualizer/react/Effects.tsx diff --git a/src/renderer/visualizer/FPS.tsx b/src/features/visualizer/react/FPS.tsx similarity index 85% rename from src/renderer/visualizer/FPS.tsx rename to src/features/visualizer/react/FPS.tsx index 583537a8..11bcdbf1 100644 --- a/src/renderer/visualizer/FPS.tsx +++ b/src/features/visualizer/react/FPS.tsx @@ -1,5 +1,5 @@ import { useRef } from 'react' -import {DynamicSustainRollingAverage, dynamicSustain} from 'shared/RollingAverage' +import {DynamicSustainRollingAverage, dynamicSustain} from 'features/utils/RollingAverage' function getRollingAverage() { const avg = new DynamicSustainRollingAverage(60, dynamicSustain(20, 0.3)) diff --git a/src/renderer/visualizer/FileList.tsx b/src/features/visualizer/react/FileList.tsx similarity index 93% rename from src/renderer/visualizer/FileList.tsx rename to src/features/visualizer/react/FileList.tsx index 1a91dbbe..d817e31d 100644 --- a/src/renderer/visualizer/FileList.tsx +++ b/src/features/visualizer/react/FileList.tsx @@ -1,17 +1,17 @@ import styled from 'styled-components' import { useEffect } from 'react' -import { getLocalFilepaths } from 'renderer/ipcHandler' +import * as api from 'renderer/api' import { IconButton } from '@mui/material' import AddIcon from '@mui/icons-material/Add' import path from 'path-browserify' import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' import DragHandle from '@mui/icons-material/DragHandle' -import { reorderArray } from 'shared/util' +import { reorderArray } from 'features/utils/util' import { FileFilter } from 'electron/renderer' import { videoExtensions, imageExtensions, -} from 'visualizer/threejs/layers/LocalMediaConfig' +} from 'features/visualizer/threejs/layers/LocalMediaConfig' const localMediaFileFilters: FileFilter[] = [ { name: 'Media', extensions: [...videoExtensions, ...imageExtensions] }, @@ -24,7 +24,7 @@ interface Props { export default function FileList(props: Props) { let onAdd = () => { - getLocalFilepaths('Select Media', localMediaFileFilters) + api.queries.get_local_filepaths('Select Media', localMediaFileFilters) .then(onAddSuccess) .catch((_err) => {}) } diff --git a/src/renderer/visualizer/LayerEditor.tsx b/src/features/visualizer/react/LayerEditor.tsx similarity index 92% rename from src/renderer/visualizer/LayerEditor.tsx rename to src/features/visualizer/react/LayerEditor.tsx index 178bdddd..a6932954 100644 --- a/src/renderer/visualizer/LayerEditor.tsx +++ b/src/features/visualizer/react/LayerEditor.tsx @@ -1,16 +1,16 @@ import cloneDeep from 'lodash.clonedeep' import deepEqual from 'deep-equal' import { useEffect, useState } from 'react' -import { LayerConfig } from '../../visualizer/threejs/layers/LayerConfig' +import { LayerConfig } from '../threejs/layers/LayerConfig' import { objectFits, orderTypes, -} from '../../visualizer/threejs/layers/LocalMediaConfig' -import { fontTypes } from '../../visualizer/threejs/fonts/FontType' +} from '../threejs/layers/LocalMediaConfig' +import { fontTypes } from '../threejs/fonts/FontType' import { Button } from '@mui/material' import makeControls from './makeControls' import FileList from './FileList' -import { spaceShapes } from 'visualizer/threejs/layers/Space' +import { spaceShapes } from 'features/visualizer/threejs/layers/Space' interface Props { config: LayerConfig diff --git a/src/renderer/visualizer/OpenVisualizerButton.tsx b/src/features/visualizer/react/OpenVisualizerButton.tsx similarity index 79% rename from src/renderer/visualizer/OpenVisualizerButton.tsx rename to src/features/visualizer/react/OpenVisualizerButton.tsx index 26a38f9f..6d2b982e 100644 --- a/src/renderer/visualizer/OpenVisualizerButton.tsx +++ b/src/features/visualizer/react/OpenVisualizerButton.tsx @@ -1,14 +1,15 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew' import IconButton from '@mui/material/IconButton' +import * as api from 'renderer/api' import styled from 'styled-components' -import { send_open_visualizer } from '../ipcHandler' + interface Props {} export default function OpenVisualizerButton({}: Props) { return ( - + diff --git a/src/renderer/visualizer/Visualizer.tsx b/src/features/visualizer/react/Visualizer.tsx similarity index 81% rename from src/renderer/visualizer/Visualizer.tsx rename to src/features/visualizer/react/Visualizer.tsx index 6e1062b7..3152d15f 100644 --- a/src/renderer/visualizer/Visualizer.tsx +++ b/src/features/visualizer/react/Visualizer.tsx @@ -1,10 +1,10 @@ import { useRef, useEffect } from 'react' -import FPS from '../visualizer/FPS' -import VisualizerManager from 'visualizer/threejs/VisualizerManager' +import FPS from './FPS' +import VisualizerManager from 'features/visualizer/threejs/VisualizerManager' import styled from 'styled-components' -import { realtimeStore, useRealtimeSelector } from '../redux/realtimeStore' -import { store, getCleanReduxState } from '../redux/store' -import OpenVisualizerButton from 'renderer/visualizer/OpenVisualizerButton' +import { realtimeStore, useRealtimeSelector } from '../../../renderer/redux/realtimeStore' +import { store, getCleanReduxState } from '../../../renderer/redux/store' +import OpenVisualizerButton from './OpenVisualizerButton' export default function Visualizer() { const visualizerDiv = useRef(null) diff --git a/src/renderer/visualizer/VisualizerSceneEditor.tsx b/src/features/visualizer/react/VisualizerSceneEditor.tsx similarity index 82% rename from src/renderer/visualizer/VisualizerSceneEditor.tsx rename to src/features/visualizer/react/VisualizerSceneEditor.tsx index e7141f21..20411b0a 100644 --- a/src/renderer/visualizer/VisualizerSceneEditor.tsx +++ b/src/features/visualizer/react/VisualizerSceneEditor.tsx @@ -1,14 +1,14 @@ import styled from 'styled-components' -import { useActiveVisualScene } from '../redux/store' -import Select from '../base/Select' +import { useActiveVisualScene } from '../../../renderer/redux/store' +import Select from 'features/ui/react/base/Select' import { visualizerTypeList, initLayerConfig, layerDisplayNames, layerTypeFromDisplayName, -} from '../../visualizer/threejs/layers/LayerConfig' +} from '../threejs/layers/LayerConfig' import { useDispatch } from 'react-redux' -import { setVisualSceneConfig } from '../redux/controlSlice' +import { setVisualSceneConfig } from '../../../renderer/redux/controlSlice' import LayerEditor from './LayerEditor' interface Props {} diff --git a/src/renderer/visualizer/makeControls.tsx b/src/features/visualizer/react/makeControls.tsx similarity index 95% rename from src/renderer/visualizer/makeControls.tsx rename to src/features/visualizer/react/makeControls.tsx index 7ac9d88f..44870e90 100644 --- a/src/renderer/visualizer/makeControls.tsx +++ b/src/features/visualizer/react/makeControls.tsx @@ -1,10 +1,10 @@ import { IconButton, Slider, Switch, TextField } from '@mui/material' -import Select from 'renderer/base/Select' +import Select from 'features/ui/react/base/Select' import styled from 'styled-components' import AddIcon from '@mui/icons-material/Add' -import { MultilineInput } from 'renderer/base/Input' -import { Range } from 'math/range' -import { secondaryEnabled } from 'renderer/base/keyUtil' +import { MultilineInput } from 'features/ui/react/base/Input' +import { Range } from 'features/utils/math/range' +import { secondaryEnabled } from 'features/ui/react/base/keyUtil' // NOTE: This file is littered with @ts-ignore and smelly casts. // It's a necessary evil to simplify Config controls... But use with caution diff --git a/src/visualizer/ipcChannels.ts b/src/features/visualizer/shared/ipcChannels.ts similarity index 85% rename from src/visualizer/ipcChannels.ts rename to src/features/visualizer/shared/ipcChannels.ts index 5628700e..7659161c 100644 --- a/src/visualizer/ipcChannels.ts +++ b/src/features/visualizer/shared/ipcChannels.ts @@ -1,3 +1,3 @@ export default { new_visualizer_state: 'new_visualizer_state', -} +} as const diff --git a/src/visualizer/threejs/EffectManager.ts b/src/features/visualizer/threejs/EffectManager.ts similarity index 100% rename from src/visualizer/threejs/EffectManager.ts rename to src/features/visualizer/threejs/EffectManager.ts diff --git a/src/visualizer/threejs/UpdateResource.ts b/src/features/visualizer/threejs/UpdateResource.ts similarity index 74% rename from src/visualizer/threejs/UpdateResource.ts rename to src/features/visualizer/threejs/UpdateResource.ts index 73bce5e2..bc113d3d 100644 --- a/src/visualizer/threejs/UpdateResource.ts +++ b/src/features/visualizer/threejs/UpdateResource.ts @@ -1,14 +1,14 @@ -import { Params } from '../../shared/params' -import { TimeState } from '../../shared/TimeState' -import { isNewPeriod, beatsIn, beatsLeft } from '../../shared/TimeState' -import { LightScene_t } from '../../shared/Scenes' -import { Size } from 'math/size' -import { Range, rLerp } from 'math/range' +import { StrictParams } from '../../params/shared/params' +import { TimeState } from '../../bpm/shared/TimeState' +import { isNewPeriod, beatsIn, beatsLeft } from '../../bpm/shared/TimeState' +import { LightScene_t } from '../../scenes/shared/Scenes' +import { Size } from 'features/utils/math/size' +import { Range, rLerp } from 'features/utils/math/range' interface UpdateData { dt: number time: TimeState - params: Params + params: Partial scene: LightScene_t master: number size: Size @@ -17,7 +17,7 @@ interface UpdateData { export default class UpdateResource { dt: number time: TimeState - params: Params + params: Partial scene: LightScene_t master: number size: Size @@ -48,7 +48,7 @@ export default class UpdateResource { } msPerPeriod(beatsPerPeriod: number) { - return beatsPerPeriod / this.time.bpm * 60000 + return (beatsPerPeriod / this.time.bpm) * 60000 } beatsIn(period: number) { diff --git a/src/visualizer/threejs/VisualizerManager.ts b/src/features/visualizer/threejs/VisualizerManager.ts similarity index 96% rename from src/visualizer/threejs/VisualizerManager.ts rename to src/features/visualizer/threejs/VisualizerManager.ts index 9f10a3b9..547bc242 100644 --- a/src/visualizer/threejs/VisualizerManager.ts +++ b/src/features/visualizer/threejs/VisualizerManager.ts @@ -1,6 +1,6 @@ import * as THREE from 'three' -import { RealtimeState } from '../../renderer/redux/realtimeStore' -import { CleanReduxState } from '../../renderer/redux/store' +import { RealtimeState } from '../../../renderer/redux/realtimeStore' +import { CleanReduxState } from '../../../renderer/redux/store' import equal from 'deep-equal' import UpdateResource from './UpdateResource' import { EffectsConfig, initEffectsConfig } from './effects/effectConfigs' diff --git a/src/visualizer/threejs/effects/AdaptiveToneMapping.ts b/src/features/visualizer/threejs/effects/AdaptiveToneMapping.ts similarity index 100% rename from src/visualizer/threejs/effects/AdaptiveToneMapping.ts rename to src/features/visualizer/threejs/effects/AdaptiveToneMapping.ts diff --git a/src/visualizer/threejs/effects/AfterImage.ts b/src/features/visualizer/threejs/effects/AfterImage.ts similarity index 91% rename from src/visualizer/threejs/effects/AfterImage.ts rename to src/features/visualizer/threejs/effects/AfterImage.ts index cb9448f6..7d360c61 100644 --- a/src/visualizer/threejs/effects/AfterImage.ts +++ b/src/features/visualizer/threejs/effects/AfterImage.ts @@ -1,7 +1,7 @@ import { AfterimagePass } from 'three/examples/jsm/postprocessing/AfterImagePass' import EffectBase from './EffectBase' import { AfterImageConfig } from './effectConfigs' -import { mapFn } from '../../../shared/util' +import { mapFn } from '../../../utils/util' const mapDamp = mapFn(0.3) diff --git a/src/visualizer/threejs/effects/CustomPassShader.ts b/src/features/visualizer/threejs/effects/CustomPassShader.ts similarity index 100% rename from src/visualizer/threejs/effects/CustomPassShader.ts rename to src/features/visualizer/threejs/effects/CustomPassShader.ts diff --git a/src/visualizer/threejs/effects/DotScreen.ts b/src/features/visualizer/threejs/effects/DotScreen.ts similarity index 100% rename from src/visualizer/threejs/effects/DotScreen.ts rename to src/features/visualizer/threejs/effects/DotScreen.ts diff --git a/src/visualizer/threejs/effects/Effect.ts b/src/features/visualizer/threejs/effects/Effect.ts similarity index 100% rename from src/visualizer/threejs/effects/Effect.ts rename to src/features/visualizer/threejs/effects/Effect.ts diff --git a/src/visualizer/threejs/effects/EffectBase.ts b/src/features/visualizer/threejs/effects/EffectBase.ts similarity index 100% rename from src/visualizer/threejs/effects/EffectBase.ts rename to src/features/visualizer/threejs/effects/EffectBase.ts diff --git a/src/visualizer/threejs/effects/Film.ts b/src/features/visualizer/threejs/effects/Film.ts similarity index 100% rename from src/visualizer/threejs/effects/Film.ts rename to src/features/visualizer/threejs/effects/Film.ts diff --git a/src/visualizer/threejs/effects/Glitch.ts b/src/features/visualizer/threejs/effects/Glitch.ts similarity index 100% rename from src/visualizer/threejs/effects/Glitch.ts rename to src/features/visualizer/threejs/effects/Glitch.ts diff --git a/src/visualizer/threejs/effects/HalfTone.ts b/src/features/visualizer/threejs/effects/HalfTone.ts similarity index 100% rename from src/visualizer/threejs/effects/HalfTone.ts rename to src/features/visualizer/threejs/effects/HalfTone.ts diff --git a/src/visualizer/threejs/effects/LightSync.ts b/src/features/visualizer/threejs/effects/LightSync.ts similarity index 97% rename from src/visualizer/threejs/effects/LightSync.ts rename to src/features/visualizer/threejs/effects/LightSync.ts index 9a089c93..b90337fb 100644 --- a/src/visualizer/threejs/effects/LightSync.ts +++ b/src/features/visualizer/threejs/effects/LightSync.ts @@ -4,10 +4,10 @@ import fragmentShader from '../shaders/LightSync.frag' import { Uniform } from 'three' import UpdateResource from '../UpdateResource' import { Strobe } from '../util/animations' -import { getBaseColors } from 'shared/baseColors' import CustomPassShader from './CustomPassShader' import EffectBase from './EffectBase' import { LightSyncConfig } from './effectConfigs' +import { getBaseColors } from 'features/params/shared/utils' type LightSyncShader = CustomPassShader<{ tDiffuse: Uniform diff --git a/src/visualizer/threejs/effects/Pixel.ts b/src/features/visualizer/threejs/effects/Pixel.ts similarity index 100% rename from src/visualizer/threejs/effects/Pixel.ts rename to src/features/visualizer/threejs/effects/Pixel.ts diff --git a/src/visualizer/threejs/effects/RenderLayer.ts b/src/features/visualizer/threejs/effects/RenderLayer.ts similarity index 100% rename from src/visualizer/threejs/effects/RenderLayer.ts rename to src/features/visualizer/threejs/effects/RenderLayer.ts diff --git a/src/visualizer/threejs/effects/UnrealBloom.ts b/src/features/visualizer/threejs/effects/UnrealBloom.ts similarity index 100% rename from src/visualizer/threejs/effects/UnrealBloom.ts rename to src/features/visualizer/threejs/effects/UnrealBloom.ts diff --git a/src/visualizer/threejs/effects/effectConfigs.ts b/src/features/visualizer/threejs/effects/effectConfigs.ts similarity index 100% rename from src/visualizer/threejs/effects/effectConfigs.ts rename to src/features/visualizer/threejs/effects/effectConfigs.ts diff --git a/src/visualizer/threejs/fonts/FontType.ts b/src/features/visualizer/threejs/fonts/FontType.ts similarity index 100% rename from src/visualizer/threejs/fonts/FontType.ts rename to src/features/visualizer/threejs/fonts/FontType.ts diff --git a/src/visualizer/threejs/fonts/LICENSE b/src/features/visualizer/threejs/fonts/LICENSE similarity index 100% rename from src/visualizer/threejs/fonts/LICENSE rename to src/features/visualizer/threejs/fonts/LICENSE diff --git a/src/visualizer/threejs/fonts/README b/src/features/visualizer/threejs/fonts/README similarity index 100% rename from src/visualizer/threejs/fonts/README rename to src/features/visualizer/threejs/fonts/README diff --git a/src/visualizer/threejs/fonts/droid/NOTICE b/src/features/visualizer/threejs/fonts/droid/NOTICE similarity index 100% rename from src/visualizer/threejs/fonts/droid/NOTICE rename to src/features/visualizer/threejs/fonts/droid/NOTICE diff --git a/src/visualizer/threejs/fonts/droid/README.txt b/src/features/visualizer/threejs/fonts/droid/README.txt similarity index 100% rename from src/visualizer/threejs/fonts/droid/README.txt rename to src/features/visualizer/threejs/fonts/droid/README.txt diff --git a/src/visualizer/threejs/fonts/droid/droid_sans_bold.typeface.json b/src/features/visualizer/threejs/fonts/droid/droid_sans_bold.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/droid/droid_sans_bold.typeface.json rename to src/features/visualizer/threejs/fonts/droid/droid_sans_bold.typeface.json diff --git a/src/visualizer/threejs/fonts/droid/droid_sans_mono_regular.typeface.json b/src/features/visualizer/threejs/fonts/droid/droid_sans_mono_regular.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/droid/droid_sans_mono_regular.typeface.json rename to src/features/visualizer/threejs/fonts/droid/droid_sans_mono_regular.typeface.json diff --git a/src/visualizer/threejs/fonts/droid/droid_sans_regular.typeface.json b/src/features/visualizer/threejs/fonts/droid/droid_sans_regular.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/droid/droid_sans_regular.typeface.json rename to src/features/visualizer/threejs/fonts/droid/droid_sans_regular.typeface.json diff --git a/src/visualizer/threejs/fonts/droid/droid_serif_bold.typeface.json b/src/features/visualizer/threejs/fonts/droid/droid_serif_bold.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/droid/droid_serif_bold.typeface.json rename to src/features/visualizer/threejs/fonts/droid/droid_serif_bold.typeface.json diff --git a/src/visualizer/threejs/fonts/droid/droid_serif_regular.typeface.json b/src/features/visualizer/threejs/fonts/droid/droid_serif_regular.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/droid/droid_serif_regular.typeface.json rename to src/features/visualizer/threejs/fonts/droid/droid_serif_regular.typeface.json diff --git a/src/visualizer/threejs/fonts/fonts.ts b/src/features/visualizer/threejs/fonts/fonts.ts similarity index 100% rename from src/visualizer/threejs/fonts/fonts.ts rename to src/features/visualizer/threejs/fonts/fonts.ts diff --git a/src/visualizer/threejs/fonts/gentilis_bold.typeface.json b/src/features/visualizer/threejs/fonts/gentilis_bold.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/gentilis_bold.typeface.json rename to src/features/visualizer/threejs/fonts/gentilis_bold.typeface.json diff --git a/src/visualizer/threejs/fonts/gentilis_regular.typeface.json b/src/features/visualizer/threejs/fonts/gentilis_regular.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/gentilis_regular.typeface.json rename to src/features/visualizer/threejs/fonts/gentilis_regular.typeface.json diff --git a/src/visualizer/threejs/fonts/helvetiker_bold.typeface.json b/src/features/visualizer/threejs/fonts/helvetiker_bold.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/helvetiker_bold.typeface.json rename to src/features/visualizer/threejs/fonts/helvetiker_bold.typeface.json diff --git a/src/visualizer/threejs/fonts/helvetiker_regular.typeface.json b/src/features/visualizer/threejs/fonts/helvetiker_regular.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/helvetiker_regular.typeface.json rename to src/features/visualizer/threejs/fonts/helvetiker_regular.typeface.json diff --git a/src/visualizer/threejs/fonts/optimer_bold.typeface.json b/src/features/visualizer/threejs/fonts/optimer_bold.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/optimer_bold.typeface.json rename to src/features/visualizer/threejs/fonts/optimer_bold.typeface.json diff --git a/src/visualizer/threejs/fonts/optimer_regular.typeface.json b/src/features/visualizer/threejs/fonts/optimer_regular.typeface.json similarity index 100% rename from src/visualizer/threejs/fonts/optimer_regular.typeface.json rename to src/features/visualizer/threejs/fonts/optimer_regular.typeface.json diff --git a/src/visualizer/threejs/fonts/ttf/kenpixel.ttf b/src/features/visualizer/threejs/fonts/ttf/kenpixel.ttf similarity index 100% rename from src/visualizer/threejs/fonts/ttf/kenpixel.ttf rename to src/features/visualizer/threejs/fonts/ttf/kenpixel.ttf diff --git a/src/visualizer/threejs/layers/Cube.ts b/src/features/visualizer/threejs/layers/Cube.ts similarity index 100% rename from src/visualizer/threejs/layers/Cube.ts rename to src/features/visualizer/threejs/layers/Cube.ts diff --git a/src/visualizer/threejs/layers/CubeSphere.ts b/src/features/visualizer/threejs/layers/CubeSphere.ts similarity index 92% rename from src/visualizer/threejs/layers/CubeSphere.ts rename to src/features/visualizer/threejs/layers/CubeSphere.ts index 78462e83..193c6453 100644 --- a/src/visualizer/threejs/layers/CubeSphere.ts +++ b/src/features/visualizer/threejs/layers/CubeSphere.ts @@ -2,9 +2,9 @@ import * as THREE from 'three' import LayerBase from './LayerBase' import { Vector3 } from 'three' import UpdateResource from '../UpdateResource' -import { randomRanged } from '../../../math/util' -import { Range, rLerp } from '../../../math/range' -import { mapFn } from '../../../shared/util' +import { randomRanged } from '../../../utils/math/util' +import { Range, rLerp } from '../../../utils/math/range' +import { mapFn } from '../../../utils/util' export interface CubeSphereConfig { type: 'CubeSphere' diff --git a/src/visualizer/threejs/layers/Cubes.ts b/src/features/visualizer/threejs/layers/Cubes.ts similarity index 95% rename from src/visualizer/threejs/layers/Cubes.ts rename to src/features/visualizer/threejs/layers/Cubes.ts index 7212dc8e..ebbc9497 100644 --- a/src/visualizer/threejs/layers/Cubes.ts +++ b/src/features/visualizer/threejs/layers/Cubes.ts @@ -1,7 +1,7 @@ import * as THREE from 'three' import LayerBase from './LayerBase' -import { randomRanged } from '../../../math/util' -import { skewPower } from '../../../math/skew' +import { randomRanged } from '../../../utils/math/util' +import { skewPower } from '../../../utils/math/skew' import { Vector3 } from 'three' import { Strobe } from '../util/animations' import { colorFromHSV } from '../util/util' diff --git a/src/visualizer/threejs/layers/LayerBase.ts b/src/features/visualizer/threejs/layers/LayerBase.ts similarity index 100% rename from src/visualizer/threejs/layers/LayerBase.ts rename to src/features/visualizer/threejs/layers/LayerBase.ts diff --git a/src/visualizer/threejs/layers/LayerConfig.ts b/src/features/visualizer/threejs/layers/LayerConfig.ts similarity index 100% rename from src/visualizer/threejs/layers/LayerConfig.ts rename to src/features/visualizer/threejs/layers/LayerConfig.ts diff --git a/src/visualizer/threejs/layers/LocalMedia.ts b/src/features/visualizer/threejs/layers/LocalMedia.ts similarity index 100% rename from src/visualizer/threejs/layers/LocalMedia.ts rename to src/features/visualizer/threejs/layers/LocalMedia.ts diff --git a/src/visualizer/threejs/layers/LocalMediaConfig.ts b/src/features/visualizer/threejs/layers/LocalMediaConfig.ts similarity index 100% rename from src/visualizer/threejs/layers/LocalMediaConfig.ts rename to src/features/visualizer/threejs/layers/LocalMediaConfig.ts diff --git a/src/visualizer/threejs/layers/Run.ts b/src/features/visualizer/threejs/layers/Run.ts similarity index 96% rename from src/visualizer/threejs/layers/Run.ts rename to src/features/visualizer/threejs/layers/Run.ts index f1c5695f..81281f92 100644 --- a/src/visualizer/threejs/layers/Run.ts +++ b/src/features/visualizer/threejs/layers/Run.ts @@ -2,7 +2,7 @@ import * as THREE from 'three' import {Vector3} from 'three' import LayerBase from './LayerBase' import UpdateResource from '../UpdateResource' -import { zeroArray } from '../../../shared/util' +import { zeroArray } from '../../../utils/util' // import { Range } from '../../../math/range' // const Y_RANGE: Range = { @@ -58,7 +58,7 @@ function section(mat: THREE.Material) { function firstRow(): Vector3[] { const n = Math.floor(RADIANS / radiansPerStep) - + return zeroArray(n).map(_ => new THREE.Vector3(0,)) -} \ No newline at end of file +} diff --git a/src/visualizer/threejs/layers/Space.ts b/src/features/visualizer/threejs/layers/Space.ts similarity index 95% rename from src/visualizer/threejs/layers/Space.ts rename to src/features/visualizer/threejs/layers/Space.ts index 3f0fa35f..acbdcc51 100644 --- a/src/visualizer/threejs/layers/Space.ts +++ b/src/features/visualizer/threejs/layers/Space.ts @@ -2,9 +2,9 @@ import * as THREE from 'three' import { Vector3 } from 'three' import LayerBase from './LayerBase' import UpdateResource from '../UpdateResource' -import { randomRanged } from '../../../math/util' -import { Range, rLerp } from '../../../math/range' -import { mapFn } from '../../../shared/util' +import { randomRanged } from '../../../utils/math/util' +import { Range, rLerp } from '../../../utils/math/range' +import { mapFn } from '../../../utils/util' import { snapToMultipleOf2 } from '../util/util' const MIN_XY = -2000 diff --git a/src/visualizer/threejs/layers/Spheres.ts b/src/features/visualizer/threejs/layers/Spheres.ts similarity index 92% rename from src/visualizer/threejs/layers/Spheres.ts rename to src/features/visualizer/threejs/layers/Spheres.ts index b1993cfb..a2c3e374 100644 --- a/src/visualizer/threejs/layers/Spheres.ts +++ b/src/features/visualizer/threejs/layers/Spheres.ts @@ -1,12 +1,12 @@ import * as THREE from 'three' import LayerBase from './LayerBase' -import { randomRanged } from '../../../math/util' -import { skewPower3 } from '../../../math/skew' +import { randomRanged } from '../../../utils/math/util' +import { skewPower3 } from '../../../utils/math/skew' import { Strobe } from '../util/animations' import { colorFromHSV } from '../util/util' import UpdateResource from '../UpdateResource' -import { indexArray } from '../../../shared/util' -import { mapFn } from '../../../shared/util' +import { indexArray } from '../../../utils/util' +import { mapFn } from '../../../utils/util' const RATIO_MIN = 0.02 const RATIO_MAX = 0.1 diff --git a/src/visualizer/threejs/layers/TextParticles.ts b/src/features/visualizer/threejs/layers/TextParticles.ts similarity index 97% rename from src/visualizer/threejs/layers/TextParticles.ts rename to src/features/visualizer/threejs/layers/TextParticles.ts index ce7d6c92..90ec22d8 100644 --- a/src/visualizer/threejs/layers/TextParticles.ts +++ b/src/features/visualizer/threejs/layers/TextParticles.ts @@ -4,14 +4,14 @@ import LayerBase from './LayerBase' import { particles } from '../util/particles' import { textOutlineShapesAndHoles, textBounds } from '../util/text' import { colorFromHSV, distance } from '../util/util' -import { lerp, randomRanged } from '../../../math/util' +import { lerp, randomRanged } from '../../../utils/math/util' import { gravity, ParticleState } from '../util/particlePhysics' import { TextParticlesConfig } from './TextParticlesConfig' import shaders from '../shaders/shaders' import UpdateResource from '../UpdateResource' -import { mapFn } from '../../../shared/util' +import { mapFn } from '../../../utils/util' import { snapToMultipleOf2 } from '../util/util' -import { rLerp } from 'math/range' +import { rLerp } from 'features/utils/math/range' const attrib = { position: 'position', diff --git a/src/visualizer/threejs/layers/TextParticlesConfig.ts b/src/features/visualizer/threejs/layers/TextParticlesConfig.ts similarity index 94% rename from src/visualizer/threejs/layers/TextParticlesConfig.ts rename to src/features/visualizer/threejs/layers/TextParticlesConfig.ts index 9e4a2081..82d19429 100644 --- a/src/visualizer/threejs/layers/TextParticlesConfig.ts +++ b/src/features/visualizer/threejs/layers/TextParticlesConfig.ts @@ -1,6 +1,6 @@ import { Physics } from '../util/particlePhysics' import { FontType } from '../fonts/FontType' -import { Range } from 'math/range' +import { Range } from 'features/utils/math/range' export interface TextParticlesConfig { type: 'TextParticles' diff --git a/src/visualizer/threejs/layers/TextSpin.ts b/src/features/visualizer/threejs/layers/TextSpin.ts similarity index 97% rename from src/visualizer/threejs/layers/TextSpin.ts rename to src/features/visualizer/threejs/layers/TextSpin.ts index 83d32284..e73a782d 100644 --- a/src/visualizer/threejs/layers/TextSpin.ts +++ b/src/features/visualizer/threejs/layers/TextSpin.ts @@ -12,7 +12,7 @@ import { Spin, Wobble, Strobe } from '../util/animations' import { colorFromHSV } from '../util/util' import UpdateResource from '../UpdateResource' import { TextSpinConfig } from './TextSpinConfig' -import { mapFn } from 'shared/util' +import { mapFn } from 'features/utils/util' const mapSize = mapFn(1.2, { min: 0.1, max: 2 }) diff --git a/src/visualizer/threejs/layers/TextSpinConfig.ts b/src/features/visualizer/threejs/layers/TextSpinConfig.ts similarity index 100% rename from src/visualizer/threejs/layers/TextSpinConfig.ts rename to src/features/visualizer/threejs/layers/TextSpinConfig.ts diff --git a/src/visualizer/threejs/layers/constructLayer.ts b/src/features/visualizer/threejs/layers/constructLayer.ts similarity index 100% rename from src/visualizer/threejs/layers/constructLayer.ts rename to src/features/visualizer/threejs/layers/constructLayer.ts diff --git a/src/visualizer/threejs/shaders/LightSync.frag b/src/features/visualizer/threejs/shaders/LightSync.frag similarity index 100% rename from src/visualizer/threejs/shaders/LightSync.frag rename to src/features/visualizer/threejs/shaders/LightSync.frag diff --git a/src/visualizer/threejs/shaders/LightSync.vert b/src/features/visualizer/threejs/shaders/LightSync.vert similarity index 100% rename from src/visualizer/threejs/shaders/LightSync.vert rename to src/features/visualizer/threejs/shaders/LightSync.vert diff --git a/src/visualizer/threejs/shaders/particles.frag b/src/features/visualizer/threejs/shaders/particles.frag similarity index 100% rename from src/visualizer/threejs/shaders/particles.frag rename to src/features/visualizer/threejs/shaders/particles.frag diff --git a/src/visualizer/threejs/shaders/particles.vert b/src/features/visualizer/threejs/shaders/particles.vert similarity index 100% rename from src/visualizer/threejs/shaders/particles.vert rename to src/features/visualizer/threejs/shaders/particles.vert diff --git a/src/visualizer/threejs/shaders/shaders.ts b/src/features/visualizer/threejs/shaders/shaders.ts similarity index 100% rename from src/visualizer/threejs/shaders/shaders.ts rename to src/features/visualizer/threejs/shaders/shaders.ts diff --git a/src/visualizer/threejs/util/LoadQueue.ts b/src/features/visualizer/threejs/util/LoadQueue.ts similarity index 100% rename from src/visualizer/threejs/util/LoadQueue.ts rename to src/features/visualizer/threejs/util/LoadQueue.ts diff --git a/src/visualizer/threejs/util/LocalMediaQueue.ts b/src/features/visualizer/threejs/util/LocalMediaQueue.ts similarity index 92% rename from src/visualizer/threejs/util/LocalMediaQueue.ts rename to src/features/visualizer/threejs/util/LocalMediaQueue.ts index e633edab..24893ebc 100644 --- a/src/visualizer/threejs/util/LocalMediaQueue.ts +++ b/src/features/visualizer/threejs/util/LocalMediaQueue.ts @@ -1,10 +1,10 @@ import * as THREE from 'three' import { pathUrl } from './loaders' import LoadQueue from './LoadQueue' -import { randomIndexExcludeCurrent } from '../../../shared/util' -import no_media_image from '../../../../assets/no_media.png' +import { randomIndexExcludeCurrent } from '../../../utils/util' +import no_media_image from '../../images/no_media.png' import { LocalMediaConfig } from '../layers/LocalMediaConfig' -import { Size, defaultSize, fit, cover } from '../../../math/size' +import { Size, defaultSize, fit, cover } from '../../../utils/math/size' import { getMediaData, MediaData, releaseMediaData } from './MediaData' const MIN_DELTA = 200 // ms diff --git a/src/visualizer/threejs/util/MediaData.ts b/src/features/visualizer/threejs/util/MediaData.ts similarity index 97% rename from src/visualizer/threejs/util/MediaData.ts rename to src/features/visualizer/threejs/util/MediaData.ts index f029bbbf..af4c0423 100644 --- a/src/visualizer/threejs/util/MediaData.ts +++ b/src/features/visualizer/threejs/util/MediaData.ts @@ -1,7 +1,7 @@ import * as THREE from 'three' import { loadVideo, releaseVideo, loadImage } from './loaders' import { imageExtensions, videoExtensions } from '../layers/LocalMediaConfig' -import { Size } from '../../../math/size' +import { Size } from '../../../utils/math/size' interface MediaDataBase { size: Size diff --git a/src/visualizer/threejs/util/animations.ts b/src/features/visualizer/threejs/util/animations.ts similarity index 95% rename from src/visualizer/threejs/util/animations.ts rename to src/features/visualizer/threejs/util/animations.ts index 2ce9aafe..68fee958 100644 --- a/src/visualizer/threejs/util/animations.ts +++ b/src/features/visualizer/threejs/util/animations.ts @@ -1,4 +1,4 @@ -import { skewPower } from '../../../math/skew' +import { skewPower } from '../../../utils/math/skew' export class Strobe { // Resets to 1, then is decremented according to strobe speed diff --git a/src/visualizer/threejs/util/loaders.ts b/src/features/visualizer/threejs/util/loaders.ts similarity index 96% rename from src/visualizer/threejs/util/loaders.ts rename to src/features/visualizer/threejs/util/loaders.ts index b218886d..84f573ea 100644 --- a/src/visualizer/threejs/util/loaders.ts +++ b/src/features/visualizer/threejs/util/loaders.ts @@ -1,5 +1,5 @@ import * as THREE from 'three' -import { randomRanged } from '../../../math/util' +import { randomRanged } from '../../../utils/math/util' export function pathUrl(path: string) { return `file://` + path diff --git a/src/visualizer/threejs/util/particlePhysics.ts b/src/features/visualizer/threejs/util/particlePhysics.ts similarity index 97% rename from src/visualizer/threejs/util/particlePhysics.ts rename to src/features/visualizer/threejs/util/particlePhysics.ts index 431597bf..6c5d67df 100644 --- a/src/visualizer/threejs/util/particlePhysics.ts +++ b/src/features/visualizer/threejs/util/particlePhysics.ts @@ -1,4 +1,4 @@ -import { randomRanged } from "math/util" +import { randomRanged } from "features/utils/math/util" export interface ParticleState { x: number diff --git a/src/visualizer/threejs/util/particles.ts b/src/features/visualizer/threejs/util/particles.ts similarity index 90% rename from src/visualizer/threejs/util/particles.ts rename to src/features/visualizer/threejs/util/particles.ts index af08863b..849a3e71 100644 --- a/src/visualizer/threejs/util/particles.ts +++ b/src/features/visualizer/threejs/util/particles.ts @@ -1,5 +1,5 @@ import * as THREE from 'three' -import particleSrc from '../../../images/particle.png' +import particleSrc from '../../images/particle.png' import { TextureLoader } from 'three' export const particles = { diff --git a/src/visualizer/threejs/util/text.ts b/src/features/visualizer/threejs/util/text.ts similarity index 100% rename from src/visualizer/threejs/util/text.ts rename to src/features/visualizer/threejs/util/text.ts diff --git a/src/visualizer/threejs/util/util.ts b/src/features/visualizer/threejs/util/util.ts similarity index 95% rename from src/visualizer/threejs/util/util.ts rename to src/features/visualizer/threejs/util/util.ts index 5baa9d86..a683267c 100644 --- a/src/visualizer/threejs/util/util.ts +++ b/src/features/visualizer/threejs/util/util.ts @@ -1,6 +1,6 @@ import { PerspectiveCamera } from 'three' import convert from 'color-convert' -import { Normalized, lerp } from '../../../math/util' +import { Normalized, lerp } from '../../../utils/math/util' export function colorFromHSV(h: number, s: number, v: number) { return Number('0x' + convert.hsv.hex([h * 360, s * 50, v * 100])) diff --git a/src/main/engine/api/core.ts b/src/main/engine/api/core.ts new file mode 100644 index 00000000..d5b8134b --- /dev/null +++ b/src/main/engine/api/core.ts @@ -0,0 +1,175 @@ +import { IpcMain, WebContents } from 'electron' +import { VisualizerContainer } from 'features/visualizer/engine/createVisualizerWindow' +import ipc_channels from '../../../features/shared/engine/ipc_channels' +import visualizerChannels from '../../../features/visualizer/shared/ipcChannels' + +export type Context = { + renderer: WebContents + visualizerContainer: VisualizerContainer + realtimeManager: RealtimeManager + ipcMain: IpcMain + new_control_state: (new_state: CleanReduxState) => void +} + +export const createRendererPublishers = < + _Emissions extends Partial<{ + [k in typeof ipc_channels[keyof typeof ipc_channels]]: [...any] + }> +>( + renderer: Electron.WebContents, + config: { [k in keyof _Emissions]: true } +): { + [k in keyof _Emissions]: ( + ...args: _Emissions[k] extends any[] ? _Emissions[k] : [] + ) => void +} => { + return (Object.keys(config) as (keyof _Emissions)[]).reduce( + (previous, key) => { + previous[key] = (...args) => renderer.send(key as string, ...args) + return previous + }, + {} as { + [k in keyof _Emissions]: ( + ...args: _Emissions[k] extends any[] ? _Emissions[k] : [] + ) => void + } + ) +} + +export const createVisualizerPublishers = < + _Emissions extends Partial<{ + [k in typeof visualizerChannels[keyof typeof visualizerChannels]]: [...any] + }> +>( + visualizerContainer: VisualizerContainer, + config: { [k in keyof _Emissions]: true } +): { + [k in keyof _Emissions]: ( + ...args: _Emissions[k] extends any[] ? _Emissions[k] : [] + ) => void +} => { + return (Object.keys(config) as (keyof _Emissions)[]).reduce( + (previous, key) => { + previous[key] = (...args) => { + const visualizer = visualizerContainer.visualizer + if (visualizer) { + visualizer.webContents.send(key as string, ...args) + } + } + return previous + }, + {} as { + [k in keyof _Emissions]: ( + ...args: _Emissions[k] extends any[] ? _Emissions[k] : [] + ) => void + } + ) +} + +import { IpcMainInvokeEvent } from 'electron' +import { API } from 'features/shared/engine/emissions' +import { CleanReduxState } from 'renderer/redux/store' +import { RealtimeManager } from 'features/bpm/engine' + +export const createQuery = < + Channel extends keyof API['renderer']['queries'] +>(config: { + channel: Channel + resolve: ( + context: Context, + ...args: API['renderer']['queries'][Channel]['input'] + ) => + | Promise + | API['renderer']['queries'][Channel]['output'] +}) => { + return { + [config.channel]: ( + _event: IpcMainInvokeEvent, + context: Context, + ...args: any[] + ) => { + return config.resolve(context, ...(args as any)) + }, + } +} + +export const createMutation = < + Channel extends keyof API['renderer']['mutations'] +>(config: { + channel: Channel + resolve: ( + context: Context, + ...args: API['renderer']['mutations'][Channel] + ) => Promise | any +}) => { + return { + [config.channel]: ( + _event: Electron.IpcMainEvent, + context: Context, + ...args: any[] + ) => { + return config.resolve(context, ...(args as any)) + }, + } +} + +export const createMutations = (handlers: { + [k: string]: ( + _event: Electron.IpcMainEvent, + context: Context, + ...args: any[] + ) => any +}) => { + return (context: Context) => { + const disposeableHandlers = Object.entries(handlers).map( + ([channel, resolve]) => { + const handler = (_event: Electron.IpcMainEvent, ...args: any[]) => { + return resolve(_event, context, ...args) + } + context.ipcMain.on(channel, handler) + return { + channel, + handler, + } + } + ) + return { + dispose: () => { + disposeableHandlers.forEach(({ channel, handler }) => { + context.ipcMain.removeListener(channel, handler) + }) + }, + } + } +} + +export const createQueries = (handlers: { + [k: string]: ( + _event: Electron.IpcMainInvokeEvent, + context: Context, + ...args: any[] + ) => any +}) => { + return (context: Context) => { + const disposeableHandlers = Object.entries(handlers).map( + ([channel, resolve]) => { + const handler = ( + _event: Electron.IpcMainInvokeEvent, + ...args: any[] + ) => { + return resolve(_event, context, ...args) + } + + context.ipcMain.handle(channel, handler) + return { channel, handler } + } + ) + return { + dispose() { + disposeableHandlers.forEach(({ channel }) => { + context.ipcMain.removeHandler(channel) + }) + }, + } + } +} diff --git a/src/main/engine/api/index.ts b/src/main/engine/api/index.ts new file mode 100644 index 00000000..a30a886a --- /dev/null +++ b/src/main/engine/api/index.ts @@ -0,0 +1,21 @@ +import { Context } from './core' +import { mutations } from './mutations' +import { queries } from './queries' +import { publishers } from './subscriptions' + +export const createApi = (context: Context) => { + const events = { + publishers: publishers(context), + mutations: mutations(context), + queries: queries(context), + } + return { + ...events, + dispose() { + events.mutations.dispose() + events.queries.dispose() + }, + } +} + +export type IPC_Callbacks = ReturnType diff --git a/src/main/engine/api/mutations.ts b/src/main/engine/api/mutations.ts new file mode 100644 index 00000000..a7e5feb9 --- /dev/null +++ b/src/main/engine/api/mutations.ts @@ -0,0 +1,28 @@ +import ipcChannels from '../../../features/shared/engine/ipc_channels' + +import { onLinkUserCommand } from 'features/bpm/engine' +import { UserCommand } from 'features/shared/engine/ipc_channels' +import { CleanReduxState } from 'renderer/redux/store' +import openVisualizerWindow from '../../../features/visualizer/engine/createVisualizerWindow' +import { createMutation, createMutations } from './core' + +export const mutations = createMutations({ + ...createMutation({ + channel: ipcChannels.new_control_state, + resolve: (ctx, new_state: CleanReduxState) => { + ctx.new_control_state(new_state) + }, + }), + ...createMutation({ + channel: ipcChannels.user_command, + resolve: (context, command: UserCommand) => { + onLinkUserCommand(command, context.realtimeManager) + }, + }), + ...createMutation({ + channel: ipcChannels.open_visualizer, + resolve: (context) => { + openVisualizerWindow(context.visualizerContainer) + }, + }), +}) diff --git a/src/main/engine/api/queries.ts b/src/main/engine/api/queries.ts new file mode 100644 index 00000000..93bdcb75 --- /dev/null +++ b/src/main/engine/api/queries.ts @@ -0,0 +1,8 @@ +import { createQueries } from './core' +import * as fileApi from 'features/fileSaving/engine/api' + +export const queries = createQueries({ + ...fileApi.load, + ...fileApi.save, + ...fileApi.getFilePaths, +}) diff --git a/src/main/engine/api/subscriptions.ts b/src/main/engine/api/subscriptions.ts new file mode 100644 index 00000000..5edc45df --- /dev/null +++ b/src/main/engine/api/subscriptions.ts @@ -0,0 +1,27 @@ +import { API } from 'features/shared/engine/emissions' +import { + Context, + createRendererPublishers, + createVisualizerPublishers, +} from './core' + +export const publishers = (context: Context) => { + return { + ...createRendererPublishers( + context.renderer, + { + dispatch: true, + dmx_connection_update: true, + main_command: true, + midi_connection_update: true, + new_time_state: true, + } + ), + ...createVisualizerPublishers( + context.visualizerContainer, + { + new_visualizer_state: true, + } + ), + } +} diff --git a/src/main/engine/dmxEngine.ts b/src/main/engine/dmxEngine.ts deleted file mode 100644 index 08eaedb4..00000000 --- a/src/main/engine/dmxEngine.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - DMX_MAX_VALUE, - DMX_DEFAULT_VALUE, - DMX_NUM_CHANNELS, - FlattenedFixture, -} from '../../shared/dmxFixtures' -import { Params } from '../../shared/params' -import { RandomizerState } from '../../shared/randomizer' -import { CleanReduxState } from '../../renderer/redux/store' -import { - getDmxValue, - getFixturesInGroups, - flatten_fixtures, -} from '../../shared/dmxUtil' -import { indexArray, zip } from '../../shared/util' -import { TimeState } from '../../shared/TimeState' -import { SplitState } from 'renderer/redux/realtimeStore' - -export function calculateDmx( - state: CleanReduxState, - splitStates: SplitState[], - timeState: TimeState -): number[] { - const universe = state.dmx.universe - const all_fixtures = flatten_fixtures(universe, state.dmx.fixtureTypesByID) - - let channels = Array(DMX_NUM_CHANNELS).fill(0) - - if (timeState.isPlaying) { - const scenes = state.control.light - const activeScene = scenes.byId[scenes.active] - - const applyFixtures = ( - fixtures: FlattenedFixture[], - outputParams: Params, - randomizerState: RandomizerState - ) => { - fixtures.forEach((fixture, i) => { - fixture.channels.forEach(([outputChannel, channelType]) => { - let new_channel_value = DMX_DEFAULT_VALUE - let current_channel_value = channels[outputChannel - 1] - if (fixture.intensity <= (outputParams.intensity ?? 1)) { - new_channel_value = getDmxValue( - channelType, - outputParams, - fixture, - state.control.master, - randomizerState[i]?.level ?? 1 - ) - } - channels[outputChannel - 1] = Math.max( - new_channel_value, - current_channel_value - ) - }) - }) - } - - for (const [{ outputParams, randomizer }, splitScene] of zip( - splitStates, - activeScene.splitScenes - )) { - const splitGroups = splitScene.groups - - const splitSceneFixtures = getFixturesInGroups(all_fixtures, splitGroups) - - applyFixtures(splitSceneFixtures, outputParams, randomizer) - } - - // Apply any overwrites - indexArray(DMX_NUM_CHANNELS).forEach((i) => { - const overwrite = state.mixer.overwrites[i] - if (overwrite !== undefined) { - channels[i] = overwrite * DMX_MAX_VALUE - } - }) - } - - return channels -} diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index 00a18951..0b081902 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -1,240 +1,274 @@ -import { WebContents } from 'electron' -import * as DmxConnection from './dmxConnection' -import * as MidiConnection from './midiConnection' -import NodeLink from 'node-link' -import { ipcSetup, IPC_Callbacks } from './ipcHandler' +import { WebContents, ipcMain } from 'electron' +import * as DmxConnection from 'features/dmx/engine/dmxConnection' +import * as MidiConnection from 'features/midi/engine/midiConnection' + import { CleanReduxState } from '../../renderer/redux/store' -import { - RealtimeState, - initRealtimeState, - SplitState, -} from '../../renderer/redux/realtimeStore' -import { TimeState } from '../../shared/TimeState' -import { - initRandomizerState, - resizeRandomizer, - updateIndexes, -} from '../../shared/randomizer' -import { getOutputParams } from '../../shared/modulation' -import { handleMessage } from './handleMidi' -import openVisualizerWindow, { - VisualizerContainer, -} from './createVisualizerWindow' -import { calculateDmx } from './dmxEngine' -import { handleAutoScene } from '../../shared/autoScene' +import { RealtimeState, SplitState } from '../../renderer/redux/realtimeStore' +import { TimeState } from '../../features/bpm/shared/TimeState' +import { getNewRandomizerState } from '../../features/bpm/shared/randomizer' +import { createModulationTransformer } from '../../features/modulation/engine' +import { onMidiMessageInput } from 'features/midi/engine/handleMidi' +import { VisualizerContainer } from '../../features/visualizer/engine/createVisualizerWindow' +import { calculateDmxOut, getChannels } from 'features/dmx/engine/dmxEngine' +import { handleAutoScene } from '../../features/scenes/engine/autoScene' import { setActiveScene } from '../../renderer/redux/controlSlice' -import TapTempoEngine from './TapTempoEngine' -import { flatten_fixtures, getFixturesInGroups } from '../../shared/dmxUtil' -import { ThrottleMap } from './midiConnection' -import { MidiMessage, midiInputID } from '../../shared/midi' -import { getAllParamKeys } from '../../renderer/redux/dmxSlice' -import { indexArray } from '../../shared/util' -import WledManager from './wled/wled_manager' - -let _nodeLink = new NodeLink() -_nodeLink.setIsPlaying(true) -_nodeLink.enableStartStopSync(true) -_nodeLink.enable(true) -let _ipcCallbacks: IPC_Callbacks | null = null -let _controlState: CleanReduxState | null = null -let _realtimeState: RealtimeState = initRealtimeState() -let _lastFrameTime = 0 -const _tapTempoEngine = new TapTempoEngine() -function _tapTempo() { - _tapTempoEngine.tap((newBpm) => { - _nodeLink.setTempo(newBpm) +import { + flatten_fixtures, + getFixturesInGroups, +} from '../../features/dmx/shared/dmxUtil' +import WledManager from '../../features/led/engine/wled_manager' + +import { createApi, IPC_Callbacks } from './api' +import { createOutputParams } from 'features/params/engine' +import { getAllParamKeys } from 'features/params/shared/params' +import { FlattenedFixture } from 'features/dmx/shared/dmxFixtures' +import { Modulator } from 'features/modulation/shared/modulation' +import { SplitScene_t } from 'features/scenes/shared/Scenes' +import { createRealtimeManager } from 'features/bpm/engine' +import { createDisposer } from 'features/shared/engine' +import { AppConfig } from 'app.config' + +// TODO: this should live in control state feature +const createControlStateManager = () => { + let resolveFirstState: () => void + const firstControlState = new Promise((resolve) => { + resolveFirstState = resolve }) -} -const _midiThrottle = new ThrottleMap((message: MidiMessage) => { - if (_controlState !== null && _ipcCallbacks !== null) { - handleMessage( - message, - _controlState, - _realtimeState, - _nodeLink, - _ipcCallbacks.send_dispatch, - _tapTempo - ) + let cancelled = false + + const controlStateRef = { + current: null as CleanReduxState | null, } -}, 1000 / 60) -export function getIpcCallbacks() { - return _ipcCallbacks + return { + set(state: CleanReduxState) { + if (!controlStateRef.current) resolveFirstState() + controlStateRef.current = state + }, + stateRef: controlStateRef, + + dispose() { + cancelled = true + }, + + waitForFirstControlState: (cb: () => void | Promise) => { + return firstControlState.then(async () => { + if (!cancelled) await cb() + }) + }, + } } -export function start( +const disposer = createDisposer() + +export async function start( renderer: WebContents, visualizerContainer: VisualizerContainer ) { - _ipcCallbacks = ipcSetup({ - renderer: renderer, - visualizerContainer: visualizerContainer, - on_new_control_state: (newState) => { - _controlState = newState - }, - on_user_command: (command) => { - if (command.type === 'IncrementTempo') { - _nodeLink.setTempo(_realtimeState.time.bpm + command.amount) - } else if (command.type === 'SetLinkEnabled') { - _nodeLink.enable(command.isEnabled) - } else if (command.type === 'EnableStartStopSync') { - _nodeLink.enableStartStopSync(command.isEnabled) - } else if (command.type === 'SetIsPlaying') { - _nodeLink.setIsPlaying(command.isPlaying) - } else if (command.type === 'SetBPM') { - _nodeLink.setTempo(command.bpm) - } else if (command.type === 'TapTempo') { - _tapTempo() - } + const realtimeManager = createRealtimeManager({ + next(previousStates, newStates) { + const nextRealtimeState = getNextRealtimeState( + { + controlState: controlStateManager.stateRef.current!, + realtimeState: previousStates.realtime, + timeState: newStates.time, + }, + api + ) + return nextRealtimeState }, - on_open_visualizer: () => { - openVisualizerWindow(visualizerContainer) + onUpdate(newStates) { + api.publishers.new_time_state(newStates.realtime) + api.publishers.new_visualizer_state({ + rt: newStates.realtime, + state: controlStateManager.stateRef.current!, + }) }, }) - // We're currently calculating the realtimeState 90x per second. - // The renderer should have a new realtime state on each animation frame (assuming a refresh rate of 60 hz) - setInterval(() => { - const nextTimeState = getNextTimeState() - if (_ipcCallbacks !== null && _controlState !== null) { - _realtimeState = getNextRealtimeState( - _realtimeState, - nextTimeState, - _ipcCallbacks, - _controlState - ) - _ipcCallbacks.send_time_state(_realtimeState) - _ipcCallbacks.send_visualizer_state({ - rt: _realtimeState, - state: _controlState, + const controlStateManager = disposer.push(createControlStateManager()) + + const api = disposer.push( + createApi({ + ipcMain, + realtimeManager, + new_control_state: (newState) => { + controlStateManager.set(newState) + }, + renderer, + visualizerContainer, + }) + ) + + await controlStateManager.waitForFirstControlState(() => { + // We're currently calculating the realtimeState 90x per second. + // The renderer should have a new realtime state on each animation frame (assuming a refresh rate of 60 hz) + disposer.push(realtimeManager.start()) + + disposer.push( + DmxConnection.maintain({ + update_ms: AppConfig.dmx.updateIntervalMS, + onUpdate: (dmxStatus) => { + api.publishers.dmx_connection_update(dmxStatus) + }, + getChannels: () => realtimeManager.realtimeStateRef.current.dmxOut, + getConnectable: () => { + return controlStateManager.stateRef.current!.control.device + .connectable.dmx + }, }) - } - }, 1000 / 90) - return _ipcCallbacks + ) + + disposer.push( + MidiConnection.maintain({ + throttledWaitMS: AppConfig.midi.throttledWaitMS, + update_ms: AppConfig.midi.updateIntervalMS, + onUpdate: (activeDevices) => { + api.publishers.midi_connection_update(activeDevices) + }, + onMessage: (message) => { + onMidiMessageInput( + message, + { + state: controlStateManager.stateRef.current!, + realtime: realtimeManager.realtimeStateRef.current, + }, + realtimeManager.bpmController.nodeLink, + api.publishers.dispatch, + realtimeManager.bpmController.tapTempo + ) + }, + getConnectable: () => { + return controlStateManager.stateRef.current!.control.device + .connectable.midi + }, + }) + ) + + disposer.push( + new WledManager( + () => controlStateManager.stateRef.current, + () => realtimeManager.realtimeStateRef.current + ) + ) + }) + + return api } export function stop() { - _ipcCallbacks = null + disposer.dispose() } -DmxConnection.maintain({ - update_ms: 1000, - onUpdate: (dmxStatus) => { - if (_ipcCallbacks !== null) - _ipcCallbacks.send_dmx_connection_update(dmxStatus) - }, - getChannels: () => _realtimeState.dmxOut, - getConnectable: () => { - return _controlState ? _controlState.control.device.connectable.dmx : [] +const createTrackOutputs = ( + states: { + realtimeState: RealtimeState + timeState: TimeState }, -}) + { + allParamKeys, + fixtures, + modulators, + }: { + fixtures: FlattenedFixture[] + allParamKeys: string[] + modulators: Modulator[] + } +) => { + const { realtimeState, timeState } = states + return (sceneTrack: SplitScene_t, trackIndex: number): SplitState => { + const previousRandomizer = realtimeState.splitStates[trackIndex]?.randomizer + const modulations = createModulationTransformer({ + beats: timeState.beats, + modulators, + trackIndex, + }) + const outputParams = createOutputParams( + { allParamKeys, baseParams: sceneTrack.baseParams }, + (options) => modulations.apply(options) + ) -MidiConnection.maintain({ - update_ms: 1000, - onUpdate: (activeDevices) => { - if (_ipcCallbacks !== null) - _ipcCallbacks.send_midi_connection_update(activeDevices) - }, - onMessage: (message) => { - _midiThrottle.call(midiInputID(message), message) - }, - getConnectable: () => { - return _controlState ? _controlState.control.device.connectable.midi : [] - }, -}) + const splitSceneFixtures = getFixturesInGroups(fixtures, sceneTrack.groups) -// Todo: Desimate dt in this context -function getNextTimeState(): TimeState { - let currentTime = Date.now() - const dt = currentTime - _lastFrameTime + const splitSceneFixturesWithinEpicness = splitSceneFixtures.filter( + (fixture) => fixture.intensity <= (outputParams.intensity ?? 1) + ) - _lastFrameTime = currentTime + const newRandomizerState = getNewRandomizerState({ + previousRandomizerState: previousRandomizer, + beatsLast: realtimeState.time.beats, + options: sceneTrack.randomizer, + size: splitSceneFixturesWithinEpicness.length, + timeState, + }) - return { - ..._nodeLink.getSessionInfoCurrent(), - dt: dt, - quantum: 4.0, + return { + outputParams, + randomizer: newRandomizerState, + } } } function getNextRealtimeState( - realtimeState: RealtimeState, - nextTimeState: TimeState, - ipcCallbacks: IPC_Callbacks, - controlState: CleanReduxState + states: { + realtimeState: RealtimeState + timeState: TimeState + controlState: CleanReduxState + }, + api: IPC_Callbacks ): RealtimeState { const scene = - controlState.control.light.byId[controlState.control.light.active] - const dmx = controlState.dmx + states.controlState.control.light.byId[ + states.controlState.control.light.active + ] + const dmx = states.controlState.dmx const allParamKeys = getAllParamKeys(dmx) - handleAutoScene( - realtimeState, - nextTimeState, - controlState, - (newLightScene) => { - ipcCallbacks.send_dispatch( - setActiveScene({ - sceneType: 'light', - val: newLightScene, - }) - ) + handleAutoScene({ + states, + onNew: { + lightScene: (lightScene) => + api.publishers.dispatch( + setActiveScene({ + sceneType: 'light', + val: lightScene, + }) + ), + visualScene: (visualScene) => + api.publishers.dispatch( + setActiveScene({ + sceneType: 'visual', + val: visualScene, + }) + ), }, - (newVisualScene) => { - ipcCallbacks.send_dispatch( - setActiveScene({ - sceneType: 'visual', - val: newVisualScene, - }) - ) - } - ) + }) const fixtures = flatten_fixtures(dmx.universe, dmx.fixtureTypesByID) - const splitStates: SplitState[] = scene.splitScenes.map( - (splitScene, splitIndex) => { - const splitOutputParams = getOutputParams( - nextTimeState.beats, - scene, - splitIndex, - allParamKeys - ) - let splitSceneFixtures = getFixturesInGroups(fixtures, splitScene.groups) - let splitSceneFixturesWithinEpicness = splitSceneFixtures.filter( - (fixture) => fixture.intensity <= (splitOutputParams.intensity ?? 1) - ) - - let newRandomizerState = resizeRandomizer( - realtimeState.splitStates[splitIndex]?.randomizer ?? - initRandomizerState(), - splitSceneFixturesWithinEpicness.length - ) + // create scene tracks outputs + const trackOutputs: SplitState[] = scene.splitScenes.map( + createTrackOutputs(states, { + allParamKeys, + fixtures, + modulators: scene.modulators, + }) + ) - newRandomizerState = updateIndexes( - realtimeState.time.beats, - newRandomizerState, - nextTimeState, - indexArray(splitSceneFixturesWithinEpicness.length), - splitScene.randomizer - ) + const outChannelConfig = getChannels({ state: states.controlState }) - return { - outputParams: splitOutputParams, - randomizer: newRandomizerState, - } - } - ) + if (states.timeState.isPlaying) { + // send data to output devices + calculateDmxOut( + { splitStates: trackOutputs, state: states.controlState }, + outChannelConfig + ) + } return { - time: nextTimeState, - dmxOut: calculateDmx(controlState, splitStates, nextTimeState), - splitStates, + time: states.timeState, + dmxOut: outChannelConfig.channels, + splitStates: trackOutputs, } } - -new WledManager( - () => _controlState, - () => _realtimeState -) diff --git a/src/main/engine/ipcHandler.ts b/src/main/engine/ipcHandler.ts deleted file mode 100644 index cf377196..00000000 --- a/src/main/engine/ipcHandler.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { ipcMain, WebContents, dialog } from 'electron' -import ipcChannels, { - UserCommand, - MainCommand, -} from '../../shared/ipc_channels' -import ipcChannelsVisualizer from '../../visualizer/ipcChannels' -import { CleanReduxState } from '../../renderer/redux/store' -import { RealtimeState } from '../../renderer/redux/realtimeStore' -import * as dmxConnection from './dmxConnection' -import * as midiConnection from './midiConnection' -import { PayloadAction } from '@reduxjs/toolkit' -import { promises } from 'fs' -import { VisualizerResource } from '../../visualizer/threejs/VisualizerManager' -import { VisualizerContainer } from './createVisualizerWindow' - -interface Config { - renderer: WebContents - visualizerContainer: VisualizerContainer - on_new_control_state: (new_state: CleanReduxState) => void - on_user_command: (command: UserCommand) => void - on_open_visualizer: () => void -} - -let _config: Config - -export function ipcSetup(config: Config) { - _config = config - - ipcMain.on(ipcChannels.new_control_state, (_e, new_state: CleanReduxState) => - _config.on_new_control_state(new_state) - ) - - ipcMain.on(ipcChannels.user_command, (_e, command: UserCommand) => { - _config.on_user_command(command) - }) - - ipcMain.on(ipcChannels.open_visualizer, (_e) => { - _config.on_open_visualizer() - }) - - return { - send_dmx_connection_update: (payload: dmxConnection.UpdatePayload) => - _config.renderer.send(ipcChannels.dmx_connection_update, payload), - send_midi_connection_update: (payload: midiConnection.UpdatePayload) => - _config.renderer.send(ipcChannels.midi_connection_update, payload), - send_time_state: (time_state: RealtimeState) => - _config.renderer.send(ipcChannels.new_time_state, time_state), - send_dispatch: (action: PayloadAction) => - _config.renderer.send(ipcChannels.dispatch, action), - send_visualizer_state: (payload: VisualizerResource) => { - const visualizer = _config.visualizerContainer.visualizer - if (visualizer) { - visualizer.webContents.send( - ipcChannelsVisualizer.new_visualizer_state, - payload - ) - } - }, - send_main_command: (command: MainCommand) => { - _config.renderer.send(ipcChannels.main_command, command) - }, - } -} - -export type IPC_Callbacks = ReturnType - -ipcMain.handle( - ipcChannels.load_file, - async (_event, title: string, fileFilters: Electron.FileFilter[]) => { - const dialogResult = await dialog.showOpenDialog({ - title: title, - filters: fileFilters, - properties: ['openFile'], - }) - if (!dialogResult.canceled && dialogResult.filePaths.length > 0) { - return await promises.readFile(dialogResult.filePaths[0], 'utf8') - } else { - throw new Error('User cancelled the file load') - } - } -) - -ipcMain.handle( - ipcChannels.save_file, - async ( - _event, - title: string, - data: string, - fileFilters: Electron.FileFilter[] - ) => { - const dialogResult = await dialog.showSaveDialog({ - title: title, - filters: fileFilters, - properties: ['createDirectory'], - }) - if (!dialogResult.canceled && dialogResult.filePath !== undefined) { - return await promises.writeFile(dialogResult.filePath, data) - } else { - throw new Error('User cancelled the file save') - } - } -) - -ipcMain.handle( - ipcChannels.get_local_filepaths, - async (_event, title: string, fileFilters: Electron.FileFilter[]) => { - const dialogResult = await dialog.showOpenDialog({ - title: title, - filters: fileFilters, - properties: ['openFile', 'multiSelections'], - }) - if (!dialogResult.canceled && dialogResult.filePaths.length > 0) { - return dialogResult.filePaths - } else { - throw new Error('User cancelled the file load') - } - } -) diff --git a/src/main/engine/util.ts b/src/main/engine/util.ts deleted file mode 100644 index a2a0bd9f..00000000 --- a/src/main/engine/util.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint import/prefer-default-export: off, import/no-mutable-exports: off */ -import { URL } from 'url' -import path from 'path' - -export let resolveHtmlPath: (htmlFileName: string) => string - -if (process.env.NODE_ENV === 'development') { - const port = process.env.PORT || 1213 - resolveHtmlPath = (htmlFileName: string) => { - const url = new URL(`http://localhost:${port}`) - url.pathname = htmlFileName - return url.href - } -} else { - resolveHtmlPath = (htmlFileName: string) => { - return `file://${path.resolve( - __dirname, - '../visualizer/', - htmlFileName - )}` - } -} diff --git a/src/main/fakeLog.ts b/src/main/fakeLog.ts deleted file mode 100644 index c0ff7c99..00000000 --- a/src/main/fakeLog.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { dialog } from 'electron'; - -export default function fakeLog(s: string) { - dialog.showErrorBox(s, ''); -} diff --git a/src/main/main.ts b/src/main/main.ts index f0314e23..a87410bf 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -12,12 +12,12 @@ import path from 'path' import { app, BrowserWindow, shell, dialog } from 'electron' import { autoUpdater } from 'electron-updater' import log from 'electron-log' -import MenuBuilder from './menu' +import MenuBuilder from '../features/menu/engine/menu' import { resolveHtmlPath } from './util' import * as engine from './engine/engine' -import { VisualizerContainer } from './engine/createVisualizerWindow' +import { VisualizerContainer } from '../features/visualizer/engine/createVisualizerWindow' import './prevent_sleep' -import './electron_error_logging' +import '../features/logging/engine/electron_error_logging' export default class AppUpdater { constructor() { log.transports.file.level = 'info' @@ -85,7 +85,7 @@ const createWindow = async () => { }, }) - mainWindow.loadURL(resolveHtmlPath('index.html')) + mainWindow.loadURL(resolveHtmlPath('renderer', 'index.html')) mainWindow.on('ready-to-show', () => { if (!mainWindow) { @@ -131,7 +131,10 @@ const createWindow = async () => { shell.openExternal(url) }) - const ipcCallbacks = engine.start(mainWindow.webContents, visualizerContainer) + const ipcCallbacks = await engine.start( + mainWindow.webContents, + visualizerContainer + ) const menuBuilder = new MenuBuilder(mainWindow, { ipcCallbacks }) menuBuilder.buildMenu() diff --git a/src/main/util.ts b/src/main/util.ts index 4d86f7ef..bf7465a9 100644 --- a/src/main/util.ts +++ b/src/main/util.ts @@ -1,18 +1,21 @@ /* eslint import/prefer-default-export: off, import/no-mutable-exports: off */ -import { URL } from 'url'; -import path from 'path'; +import { URL } from 'url' +import path from 'path' -export let resolveHtmlPath: (htmlFileName: string) => string; +export let resolveHtmlPath: ( + folder: 'renderer' | 'visualizer', + htmlFileName: string +) => string if (process.env.NODE_ENV === 'development') { - const port = process.env.PORT || 1212; - resolveHtmlPath = (htmlFileName: string) => { - const url = new URL(`http://localhost:${port}`); - url.pathname = htmlFileName; - return url.href; - }; + const port = process.env.PORT || 1212 + resolveHtmlPath = (_folder, htmlFileName: string) => { + const url = new URL(`http://localhost:${port}`) + url.pathname = htmlFileName + return url.href + } } else { - resolveHtmlPath = (htmlFileName: string) => { - return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; - }; + resolveHtmlPath = (folder, htmlFileName: string) => { + return `file://${path.resolve(__dirname, `../${folder}/`, htmlFileName)}` + } } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index dd9dc188..4f5206e0 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,15 +1,15 @@ import styled from 'styled-components' -import Video from './pages/VisualizerPage' -import Modulation from './pages/Scenes' -import Universe from './pages/Universe' -import Share from './pages/Share' -import Mixer from './pages/Mixer' -import MenuBar from './menu/MenuBar' +import Video from './pages/visualizer/page' +import Modulation from './pages/scenes/page' +import Universe from './pages/universe/page' +import Share from './pages/share/page' +import Mixer from './pages/mixer/page' +import MenuBar from '../features/menu/react/SideBar' import { useTypedSelector } from './redux/store' import FullscreenOverlay from './overlays/FullscreenOverlay' -import ErrorBoundarySentry from './error-boundary/ErrorBoundarySentry' -import BottomStatus from './menu/BottomStatus' -import LedPage from './pages/LedPage' +import ErrorBoundarySentry from '../features/logging/react/error-boundary/ErrorBoundarySentry' +import BottomStatus from '../features/menu/react/BottomStatus' +import LedPage from './pages/led/page' export default function App() { const activePage = useTypedSelector((state) => state.gui.activePage) diff --git a/src/renderer/GlobalStyle.tsx b/src/renderer/GlobalStyle.tsx index d70581d1..afc3e4f8 100644 --- a/src/renderer/GlobalStyle.tsx +++ b/src/renderer/GlobalStyle.tsx @@ -1,4 +1,5 @@ import { createGlobalStyle } from 'styled-components' +import zIndexes from './zIndexes' export default createGlobalStyle` body { @@ -13,4 +14,23 @@ export default createGlobalStyle` *::-webkit-scrollbar { display: none; } + + /* resets */ + p { + margin: 0; + } + button { + appearance: none; + border-style: none; + padding: 0; + background-color: transparent; + color: inherit; + font-family: inherit; /* 1 */ + font-size: 0.9rem; + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ + } + [data-radix-popper-content-wrapper]{ + z-index: ${zIndexes.popover} !important; + } ` diff --git a/src/renderer/_Template.tsx b/src/renderer/_Template.tsx deleted file mode 100644 index 3d2cc3e6..00000000 --- a/src/renderer/_Template.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import styled from 'styled-components' - -interface Props {} - -export default function _({}: Props) { - return -} - -const Root = styled.div`` diff --git a/src/renderer/api/core/ipcMutations.ts b/src/renderer/api/core/ipcMutations.ts new file mode 100644 index 00000000..a1394565 --- /dev/null +++ b/src/renderer/api/core/ipcMutations.ts @@ -0,0 +1,57 @@ +import ipc_channels from 'features/shared/engine/ipc_channels' + +export const createMutations = < + _Emissions extends Partial<{ + [k in typeof ipc_channels[keyof typeof ipc_channels]]: [...any] + }> +>( + ipcRenderer: Electron.IpcRenderer, + config: { [k in keyof _Emissions]: true } +): { + [k in keyof _Emissions]: ( + ...args: _Emissions[k] extends any[] ? _Emissions[k] : [] + ) => void +} => { + return (Object.keys(config) as (keyof _Emissions)[]).reduce( + (previous, key) => { + previous[key] = (...args) => ipcRenderer.send(key as string, ...args) + return previous + }, + {} as { + [k in keyof _Emissions]: ( + ...args: _Emissions[k] extends any[] ? _Emissions[k] : [] + ) => void + } + ) +} + +type GetInput = T extends { input: infer U } ? U : [] +type GetOutput = T extends { output: infer U } ? U : void + +export const createQueries = < + _Emissions extends Partial<{ + [k: string]: { + input: [...any] + output: any + } + }> +>( + ipcRenderer: Electron.IpcRenderer, + config: { [k in keyof _Emissions]: true } +): { + [k in keyof _Emissions]: ( + ...args: GetInput<_Emissions[k]> + ) => Promise> +} => { + return (Object.keys(config) as (keyof _Emissions)[]).reduce( + (previous, key) => { + previous[key] = (...args) => ipcRenderer.invoke(key as string, ...args) + return previous + }, + {} as { + [k in keyof _Emissions]: ( + ...args: GetInput<_Emissions[k]> + ) => Promise> + } + ) +} diff --git a/src/renderer/api/core/ipcSubscription.ts b/src/renderer/api/core/ipcSubscription.ts new file mode 100644 index 00000000..27d4c6c3 --- /dev/null +++ b/src/renderer/api/core/ipcSubscription.ts @@ -0,0 +1,25 @@ +import { IpcRenderer } from 'electron' +import ipc_channels from '../../../features/shared/engine/ipc_channels' + +// @ts-ignore: Typescript doesn't recognize the globals set in "src/main/preload.js" +const ipcRenderer: IpcRenderer = window.electron.ipcRenderer + +// TODO: not sure why we do this +let _subscribers: any + +export const subscribeIpc = < + _Emissions extends Partial<{ + [k in typeof ipc_channels[keyof typeof ipc_channels]]: [...any] + }> +>(subscribers: { + [k in keyof _Emissions]: ( + ...args: _Emissions[k] extends any[] ? _Emissions[k] : [] + ) => void +}) => { + _subscribers = subscribers + for (const [key, callback] of Object.entries( + _subscribers as typeof subscribers + )) { + ipcRenderer.on(key, (payload) => callback(payload)) + } +} diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts new file mode 100644 index 00000000..d0296553 --- /dev/null +++ b/src/renderer/api/index.ts @@ -0,0 +1,3 @@ +export { mutations } from './mutations' +export * as subscriptions from './subscriptions' +export { queries } from './queries' diff --git a/src/renderer/api/mutations.ts b/src/renderer/api/mutations.ts new file mode 100644 index 00000000..75902041 --- /dev/null +++ b/src/renderer/api/mutations.ts @@ -0,0 +1,15 @@ +import { IpcRenderer } from 'electron' +import { createMutations } from './core/ipcMutations' +import { API } from 'features/shared/engine/emissions' + +// @ts-ignore: Typescript doesn't recognize the globals set in "src/main/preload.js" +const ipcRenderer: IpcRenderer = window.electron.ipcRenderer + +export const mutations = createMutations( + ipcRenderer, + { + new_control_state: true, + open_visualizer: true, + user_command: true, + } +) diff --git a/src/renderer/api/queries.ts b/src/renderer/api/queries.ts new file mode 100644 index 00000000..9d531741 --- /dev/null +++ b/src/renderer/api/queries.ts @@ -0,0 +1,12 @@ +import { IpcRenderer } from 'electron' +import { createQueries } from './core/ipcMutations' +import { API } from 'features/shared/engine/emissions' + +// @ts-ignore: Typescript doesn't recognize the globals set in "src/main/preload.js" +const ipcRenderer: IpcRenderer = window.electron.ipcRenderer + +export const queries = createQueries(ipcRenderer, { + get_local_filepaths: true, + load_file: true, + save_file: true, +}) diff --git a/src/renderer/api/subscriptions.ts b/src/renderer/api/subscriptions.ts new file mode 100644 index 00000000..4124f07c --- /dev/null +++ b/src/renderer/api/subscriptions.ts @@ -0,0 +1,92 @@ +import { + initRealtimeState, + realtimeStore, + update as updateRealtimeStore, +} from '../redux/realtimeStore' + +import { subscribeIpc } from './core/ipcSubscription' +import { getCleanReduxState, store } from '../redux/store' +import { load } from '../../features/fileSaving/react/SaveLoad' +import { + setDmx, + setMidi, + setSaving, + setLoading, + setNewProjectDialog, +} from '../../features/ui/redux/guiSlice' +import { + getUndoGroup, + undoAction, + redoAction, +} from '../../features/scenes/react/controls/UndoRedo' +import { getSaveConfig } from 'features/fileSaving/shared/save' +import { mutations } from './mutations' +import { autoSave } from '../../features/fileSaving/react/autosave' +import { animationLoop } from 'features/shared/react' +import { API } from 'features/shared/engine/emissions' + +type Context = { + store: typeof store +} + +export const subcribe = ({ store }: Context) => { + let _frequentlyUpdatedRealtimeState = initRealtimeState() + + const getUpatedRealtimeState = () => _frequentlyUpdatedRealtimeState + + autoSave(store) + + subscribeIpc({ + dmx_connection_update: (payload) => { + store.dispatch(setDmx(payload)) + }, + midi_connection_update: (payload) => { + store.dispatch(setMidi(payload)) + }, + new_time_state: (newRealtimeState) => { + _frequentlyUpdatedRealtimeState = newRealtimeState + }, + dispatch: (action) => { + store.dispatch(action) + }, + main_command: (command) => { + const getGroup = () => getUndoGroup(store.getState()) + if (command.type === 'undo') { + const group = getGroup() + if (group !== null) { + store.dispatch(undoAction(group)) + } + } else if (command.type === 'redo') { + const group = getGroup() + if (group !== null) { + store.dispatch(redoAction(group)) + } + } else if (command.type === 'load') { + load() + .then((state) => + store.dispatch( + setLoading({ + state, + config: getSaveConfig(state), + }) + ) + ) + .catch((err) => console.warn(err)) + } else if (command.type === 'save') { + store.dispatch(setSaving(true)) + } else if (command.type === 'new-project') { + store.dispatch(setNewProjectDialog(true)) + } + }, + }) + + animationLoop(() => { + realtimeStore.dispatch(updateRealtimeStore(getUpatedRealtimeState())) + }) + + mutations.new_control_state(getCleanReduxState(store.getState())) + + store.subscribe(() => + mutations.new_control_state(getCleanReduxState(store.getState())) + ) +} diff --git a/src/renderer/base/DraggableNumber.tsx b/src/renderer/base/DraggableNumber.tsx deleted file mode 100644 index 6ee2f699..00000000 --- a/src/renderer/base/DraggableNumber.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { DownIcon, UpIcon } from 'renderer/icons/arrows' -import { clamp } from 'math/util' -import useDragBasic from '../hooks/useDragBasic' -import styled from 'styled-components' -import { double_incremented, halve_incremented } from 'shared/util' -import React from 'react' -import { secondaryEnabled } from './keyUtil' - -type Type = 'continuous' | 'snap' - -interface Props { - type?: Type - secondaryBehavior?: Type - value: number - min: number - max: number - onChange: (newVal: number) => void - style?: React.CSSProperties - suffix?: string - noArrows?: boolean -} - -// This is really bad react behavior... But IDK what else to do -// This only works if you can't drag more than 1 thing at a time -let globalMovement = 0 -let globalValue = 0 - -export default function DraggableNumber({ - type = 'snap', - secondaryBehavior = 'continuous', - value, - min, - max, - onChange, - style, - suffix, - noArrows, -}: Props) { - const speedAdjust = 500 / (max - min) - - const [dragContainer, onMouseDown] = useDragBasic((e) => { - const dx = e.movementX - const dy = -e.movementY - const d = (dx + dy) / speedAdjust - - const adjust_continuous = () => { - globalValue += d / 2 - } - const adjust_snap = () => { - if (Number.isInteger(globalValue)) { - globalMovement += d - if (globalMovement > 1) { - globalValue += 1 - globalMovement = 0 - } else if (globalMovement < -1) { - globalValue -= 1 - globalMovement = 0 - } - } else { - // We are somewhere between integers. - // "Snap" to the next integer - const floor = Math.floor(globalValue) - const ceil = Math.ceil(globalValue) - globalValue += d - if (globalValue > ceil) { - globalValue = ceil - } else if (globalValue < floor) { - globalValue = floor - } - } - } - - if (secondaryEnabled(e)) { - if (secondaryBehavior === 'snap') { - adjust_snap() - } else { - adjust_continuous() - } - } else { - if (type === 'snap') { - adjust_snap() - } else { - adjust_continuous() - } - } - - globalValue = clamp(globalValue, min, max) - - onChange(globalValue) - }) - - function onMouseDownWrapper(e: React.MouseEvent) { - globalMovement = 0 - globalValue = value - onMouseDown(e) - } - - let valueString = - type === 'snap' && Number.isInteger(value) - ? value.toString() - : value.toFixed(2) - - if (suffix) valueString += suffix - - const onUp = () => { - const doubled = double_incremented(value) - onChange(Math.min(doubled, max)) - } - - const onDown = () => { - const halved = halve_incremented(value) - onChange(Math.max(halved, min)) - } - - return ( - - - {valueString} - - - {!noArrows && ( - <> - - - - - - - - )} - - - ) -} - -const Root = styled.div` - display: flex; - align-items: center; - background-color: #0005; -` -const DragArea = styled.div` - padding: 0.5rem 0.3rem 0.5rem 0.7rem; - cursor: move; -` -const Col = styled.div` - display: flex; - flex-direction: column; - align-items: center; -` -const Arrow = styled.div` - cursor: pointer; - padding: 0.4rem; - font-size: 0; // for some reason this is necessary to make the svg render correctly? -` diff --git a/src/renderer/base/MidiOverlay.tsx b/src/renderer/base/MidiOverlay.tsx deleted file mode 100644 index a644fef4..00000000 --- a/src/renderer/base/MidiOverlay.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import styled from 'styled-components' -import { getActionID, MidiAction } from '../redux/deviceState' -import { - midiListen, - midiSetSliderAction, - removeMidiAction, -} from '../redux/controlSlice' -import { useDeviceSelector } from '../redux/store' -import { useDispatch } from 'react-redux' -import DraggableNumber from './DraggableNumber' -import Button from './Button' - -interface Props { - children?: React.ReactNode - action: MidiAction - style?: React.CSSProperties -} - -export function ButtonMidiOverlay({ children, action, style }: Props) { - const isEditing = useDeviceSelector((state) => state.isEditing) - const controlledAction = useDeviceSelector((state) => { - return state.buttonActions[getActionID(action)] || null - }) - const isListening = useDeviceSelector((state) => { - if (!state.listening) return false - return getActionID(state.listening) === getActionID(action) - }) - const dispatch = useDispatch() - - const onClick = () => { - dispatch(midiListen(action)) - } - - return ( - - {children} - {isEditing && ( - - {controlledAction && ( - <> - {controlledAction.inputID} - - - )} - - )} - - ) -} - -export function SliderMidiOverlay({ children, action, style }: Props) { - const isEditing = useDeviceSelector((state) => state.isEditing) - const controlledAction = useDeviceSelector((state) => { - return state.sliderActions[getActionID(action)] || null - }) - const isListening = useDeviceSelector((state) => { - if (!state.listening) return false - return getActionID(state.listening) === getActionID(action) - }) - const dispatch = useDispatch() - - const onClick = () => { - dispatch(midiListen(action)) - } - - const onChangeMin = (newVal: number) => { - dispatch( - midiSetSliderAction({ - ...controlledAction, - options: { - ...controlledAction.options, - min: newVal, - }, - }) - ) - } - - const onChangeMax = (newVal: number) => { - dispatch( - midiSetSliderAction({ - ...controlledAction, - options: { - ...controlledAction.options, - max: newVal, - }, - }) - ) - } - - const onClickMode = () => { - dispatch( - midiSetSliderAction({ - ...controlledAction, - options: { - ...controlledAction.options, - mode: controlledAction.options.mode === 'hold' ? 'toggle' : 'hold', - }, - }) - ) - } - - const onClickMode_cc = () => { - dispatch( - midiSetSliderAction({ - ...controlledAction, - options: { - ...controlledAction.options, - mode: - controlledAction.options.mode === 'relative' - ? 'absolute' - : 'relative', - }, - }) - ) - } - - const onClickValue = (value: 'max' | 'velocity') => () => { - dispatch( - midiSetSliderAction({ - ...controlledAction, - options: { - ...controlledAction.options, - value: value === 'max' ? 'velocity' : 'max', - }, - }) - ) - } - - const minMaxStyle: React.CSSProperties = { - padding: '0.1rem 0.2rem', - margin: '0.2rem', - color: 'white', - backgroundColor: '#0009', - } - - return ( - - {children} - {isEditing && ( - - - {controlledAction && ( - <> - {controlledAction.inputID} - - - - - - {controlledAction.options.type === 'note' && ( - <> -