From 1d129d77ff450b9f99626cb3fdffd8238db83ae1 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Sat, 1 Apr 2023 02:08:33 -0400 Subject: [PATCH 01/98] refactor: organize scene files --- src/renderer/pages/Scenes.tsx | 2 +- src/renderer/scenes/{ => modulators}/ModulationMatrix.tsx | 0 src/renderer/scenes/{ => modulators}/ModulationSlider.tsx | 8 ++++---- src/renderer/scenes/{ => modulators}/ModulatorControl.tsx | 6 +++--- src/renderer/scenes/{ => modulators}/Modulators.tsx | 2 +- src/renderer/scenes/{ => modulators}/NewModulator.tsx | 2 +- src/renderer/scenes/{ => modulators/lfo}/LfoCursor.tsx | 8 ++++---- src/renderer/scenes/{ => modulators/lfo}/LfoMenu.tsx | 8 ++++---- src/renderer/scenes/{ => modulators/lfo}/LfoPeriod.tsx | 8 +++++--- .../scenes/{ => modulators/lfo}/LfoVisualizer.tsx | 8 ++++---- 10 files changed, 27 insertions(+), 25 deletions(-) rename src/renderer/scenes/{ => modulators}/ModulationMatrix.tsx (100%) rename src/renderer/scenes/{ => modulators}/ModulationSlider.tsx (95%) rename src/renderer/scenes/{ => modulators}/ModulatorControl.tsx (83%) rename src/renderer/scenes/{ => modulators}/Modulators.tsx (91%) rename src/renderer/scenes/{ => modulators}/NewModulator.tsx (90%) rename src/renderer/scenes/{ => modulators/lfo}/LfoCursor.tsx (67%) rename src/renderer/scenes/{ => modulators/lfo}/LfoMenu.tsx (90%) rename src/renderer/scenes/{ => modulators/lfo}/LfoPeriod.tsx (74%) rename src/renderer/scenes/{ => modulators/lfo}/LfoVisualizer.tsx (88%) diff --git a/src/renderer/pages/Scenes.tsx b/src/renderer/pages/Scenes.tsx index 3fb6062e..1c27336f 100644 --- a/src/renderer/pages/Scenes.tsx +++ b/src/renderer/pages/Scenes.tsx @@ -1,6 +1,6 @@ import React from 'react' import StatusBar from '../menu/StatusBar' -import Modulators from '../scenes/Modulators' +import Modulators from '../scenes/modulators/Modulators' import SceneSelection from '../scenes/SceneSelection' import SplitPane from '../base/SplitPane' import styled from 'styled-components' diff --git a/src/renderer/scenes/ModulationMatrix.tsx b/src/renderer/scenes/modulators/ModulationMatrix.tsx similarity index 100% rename from src/renderer/scenes/ModulationMatrix.tsx rename to src/renderer/scenes/modulators/ModulationMatrix.tsx diff --git a/src/renderer/scenes/ModulationSlider.tsx b/src/renderer/scenes/modulators/ModulationSlider.tsx similarity index 95% rename from src/renderer/scenes/ModulationSlider.tsx rename to src/renderer/scenes/modulators/ModulationSlider.tsx index 9402a61e..b45aa502 100644 --- a/src/renderer/scenes/ModulationSlider.tsx +++ b/src/renderer/scenes/modulators/ModulationSlider.tsx @@ -1,14 +1,14 @@ import { useState } from 'react' import { useDispatch } from 'react-redux' -import { DefaultParam } from '../../shared/params' +import { DefaultParam } from '../../../shared/params' import { useActiveLightScene, useBaseParams, useDmxSelector, useModParam, -} from '../redux/store' -import { setModulation } from '../redux/controlSlice' -import useDragMapped from '../hooks/useDragMapped' +} from '../../redux/store' +import { setModulation } from '../../redux/controlSlice' +import useDragMapped from '../../hooks/useDragMapped' import styled from 'styled-components' import Popup from 'renderer/base/Popup' import { indexArray } from 'shared/util' diff --git a/src/renderer/scenes/ModulatorControl.tsx b/src/renderer/scenes/modulators/ModulatorControl.tsx similarity index 83% rename from src/renderer/scenes/ModulatorControl.tsx rename to src/renderer/scenes/modulators/ModulatorControl.tsx index 10ab4975..63fa05f8 100644 --- a/src/renderer/scenes/ModulatorControl.tsx +++ b/src/renderer/scenes/modulators/ModulatorControl.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components' -import LfoMenu from './LfoMenu' -import LfoVisualizer from './LfoVisualizer' -import LfoCursor from './LfoCursor' +import LfoMenu from './lfo/LfoMenu' +import LfoVisualizer from './lfo/LfoVisualizer' +import LfoCursor from './lfo/LfoCursor' import ModulationMatrix from './ModulationMatrix' type Props = { diff --git a/src/renderer/scenes/Modulators.tsx b/src/renderer/scenes/modulators/Modulators.tsx similarity index 91% rename from src/renderer/scenes/Modulators.tsx rename to src/renderer/scenes/modulators/Modulators.tsx index c89b4187..fcd7bf32 100644 --- a/src/renderer/scenes/Modulators.tsx +++ b/src/renderer/scenes/modulators/Modulators.tsx @@ -1,4 +1,4 @@ -import { useActiveLightScene } from '../redux/store' +import { useActiveLightScene } from '../../redux/store' import ModulatorControl from './ModulatorControl' import NewModulator from './NewModulator' diff --git a/src/renderer/scenes/NewModulator.tsx b/src/renderer/scenes/modulators/NewModulator.tsx similarity index 90% rename from src/renderer/scenes/NewModulator.tsx rename to src/renderer/scenes/modulators/NewModulator.tsx index 84a27605..664c5fc4 100644 --- a/src/renderer/scenes/NewModulator.tsx +++ b/src/renderer/scenes/modulators/NewModulator.tsx @@ -1,6 +1,6 @@ import { useDispatch } from 'react-redux' import AddIcon from '@mui/icons-material/Add' -import { addModulator } from '../redux/controlSlice' +import { addModulator } from '../../redux/controlSlice' export default function NewModulator() { const dispatch = useDispatch() diff --git a/src/renderer/scenes/LfoCursor.tsx b/src/renderer/scenes/modulators/lfo/LfoCursor.tsx similarity index 67% rename from src/renderer/scenes/LfoCursor.tsx rename to src/renderer/scenes/modulators/lfo/LfoCursor.tsx index c2ec19e9..6a776ced 100644 --- a/src/renderer/scenes/LfoCursor.tsx +++ b/src/renderer/scenes/modulators/lfo/LfoCursor.tsx @@ -1,7 +1,7 @@ -import { useRealtimeSelector } from '../redux/realtimeStore' -import Cursor from '../base/Cursor' -import { GetValueFromPhase, GetPhase } from '../../shared/oscillator' -import { useActiveLightScene } from '../redux/store' +import { useRealtimeSelector } from '../../../redux/realtimeStore' +import Cursor from '../../../base/Cursor' +import { GetValueFromPhase, GetPhase } from '../../../../shared/oscillator' +import { useActiveLightScene } from '../../../redux/store' export default function LfoCursor({ index, diff --git a/src/renderer/scenes/LfoMenu.tsx b/src/renderer/scenes/modulators/lfo/LfoMenu.tsx similarity index 90% rename from src/renderer/scenes/LfoMenu.tsx rename to src/renderer/scenes/modulators/lfo/LfoMenu.tsx index 2f1d47ad..ae422a76 100644 --- a/src/renderer/scenes/LfoMenu.tsx +++ b/src/renderer/scenes/modulators/lfo/LfoMenu.tsx @@ -1,18 +1,18 @@ -import { LfoShape } from '../../shared/oscillator' +import { LfoShape } from '../../../../shared/oscillator' import { useDispatch } from 'react-redux' -import { useActiveLightScene } from '../redux/store' +import { useActiveLightScene } from '../../../redux/store' import { resetModulator, removeModulator, setModulatorShape, -} from '../redux/controlSlice' +} from '../../../redux/controlSlice' import CloseIcon from '@mui/icons-material/Close' import IconButton from '@mui/material/IconButton' import MenuItem from '@mui/material/MenuItem' import Select from '@mui/material/Select' import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore' import LfoPeriod from './LfoPeriod' -import Divider from '../base/Divider' +import Divider from '../../../base/Divider' type Props = { index: number diff --git a/src/renderer/scenes/LfoPeriod.tsx b/src/renderer/scenes/modulators/lfo/LfoPeriod.tsx similarity index 74% rename from src/renderer/scenes/LfoPeriod.tsx rename to src/renderer/scenes/modulators/lfo/LfoPeriod.tsx index 8851e20f..ff39895e 100644 --- a/src/renderer/scenes/LfoPeriod.tsx +++ b/src/renderer/scenes/modulators/lfo/LfoPeriod.tsx @@ -1,7 +1,7 @@ import { useDispatch } from 'react-redux' -import { useActiveLightScene } from '../redux/store' -import { setPeriod } from '../redux/controlSlice' -import DraggableNumber from '../base/DraggableNumber' +import { useActiveLightScene } from '../../../redux/store' +import { setPeriod } from '../../../redux/controlSlice' +import DraggableNumber from '../../../base/DraggableNumber' type Props = { index: number @@ -19,6 +19,7 @@ export default function LfoPeriod({ index }: Props) { } return ( + + ) } diff --git a/src/renderer/scenes/LfoVisualizer.tsx b/src/renderer/scenes/modulators/lfo/LfoVisualizer.tsx similarity index 88% rename from src/renderer/scenes/LfoVisualizer.tsx rename to src/renderer/scenes/modulators/lfo/LfoVisualizer.tsx index d06df6dd..e0cbff4c 100644 --- a/src/renderer/scenes/LfoVisualizer.tsx +++ b/src/renderer/scenes/modulators/lfo/LfoVisualizer.tsx @@ -1,8 +1,8 @@ -import { GetValueFromPhase } from '../../shared/oscillator' +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 useDragBasic from '../../../hooks/useDragBasic' +import { incrementModulator } from '../../../redux/controlSlice' +import { useActiveLightScene } from '../../../redux/store' import { secondaryEnabled } from 'renderer/base/keyUtil' type Props = { From ec159168d26bf3bb26556f0fde43f69470e86de1 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Mon, 3 Apr 2023 00:10:38 -0400 Subject: [PATCH 02/98] feat: add midi control to lfo period --- .vscode/extensions.json | 2 +- package-lock.json | 777 +++++++++++++++++- package.json | 1 + src/main/engine/handleMidi.ts | 12 + src/renderer/base/MidiOverlay.tsx | 229 +++++- src/renderer/base/midi-overlay-styles.css | 12 + src/renderer/redux/controlSlice.ts | 31 +- src/renderer/redux/deviceState.ts | 15 + .../scenes/modulators/lfo/LfoPeriod.tsx | 41 +- 9 files changed, 1101 insertions(+), 19 deletions(-) create mode 100644 src/renderer/base/midi-overlay-styles.css 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 39cf5a3e..12f16fc2 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", @@ -951,6 +952,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", @@ -1873,6 +1900,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", @@ -3411,6 +3715,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", @@ -5364,6 +5679,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", @@ -7661,6 +7981,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", @@ -8401,6 +8729,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", @@ -15055,6 +15391,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", @@ -15079,6 +15460,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", @@ -16790,8 +17193,7 @@ "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", @@ -16973,6 +17375,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", @@ -16981,6 +17416,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", @@ -18498,6 +18954,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", @@ -19120,6 +19598,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", @@ -20505,6 +21195,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", @@ -21987,6 +22685,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", @@ -23786,6 +24489,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", @@ -24337,6 +25045,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", @@ -29112,6 +29828,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", @@ -29129,6 +29866,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", @@ -30425,8 +31172,7 @@ "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", @@ -30554,12 +31300,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", diff --git a/package.json b/package.json index dbedf1fa..abab7ce3 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,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", diff --git a/src/main/engine/handleMidi.ts b/src/main/engine/handleMidi.ts index e96ef401..b670b7dc 100644 --- a/src/main/engine/handleMidi.ts +++ b/src/main/engine/handleMidi.ts @@ -9,6 +9,7 @@ import { setAutoSceneBombacity, setMaster, setBaseParams, + setPeriod, } from '../../renderer/redux/controlSlice' import NodeLink from 'node-link' import { PayloadAction } from '@reduxjs/toolkit' @@ -107,6 +108,7 @@ export function handleMessage( const sliderAction = Object.entries(midiState.sliderActions).find( ([_actionId, action]) => action.inputID === input.id )?.[1] + if (sliderAction) { const action = sliderAction.action const getOldVal = () => { @@ -146,6 +148,16 @@ export function handleMessage( nodeLink.setTempo(newVal) } else if (action.type === 'tapTempo') { tapTempo() + } else if (action.type === 'setModulationParam') { + if (action.param === 'period') { + dispatch( + setPeriod({ + index: action.index, + newVal: newVal, + sceneId: action.sceneId, + }) + ) + } } } const op = sliderAction.options diff --git a/src/renderer/base/MidiOverlay.tsx b/src/renderer/base/MidiOverlay.tsx index a644fef4..696a2e40 100644 --- a/src/renderer/base/MidiOverlay.tsx +++ b/src/renderer/base/MidiOverlay.tsx @@ -1,7 +1,9 @@ import styled from 'styled-components' -import { getActionID, MidiAction } from '../redux/deviceState' +import * as Popover from '@radix-ui/react-popover' +import { getActionID, MidiAction, SliderAction } from '../redux/deviceState' import { midiListen, + midiStopListening, midiSetSliderAction, removeMidiAction, } from '../redux/controlSlice' @@ -9,7 +11,8 @@ import { useDeviceSelector } from '../redux/store' import { useDispatch } from 'react-redux' import DraggableNumber from './DraggableNumber' import Button from './Button' - +import './midi-overlay-styles.css' +import { useEffect, useRef } from 'react' interface Props { children?: React.ReactNode action: MidiAction @@ -193,10 +196,232 @@ export function SliderMidiOverlay({ children, action, style }: Props) { ) } +interface RangeProps extends Props { + min?: number + max?: number +} + +export function RangeMidiOverlay({ + children, + action, + style, + min, + max, +}: RangeProps) { + const isEditing = useDeviceSelector((state) => state.isEditing) + const controlledAction = useDeviceSelector((state) => { + return state.sliderActions[getActionID(action)] || null + }) + const controlledRef = useRef() + + const isListening = useDeviceSelector((state) => { + if (!state.listening) return false + return getActionID(state.listening) === getActionID(action) + }) + const dispatch = useDispatch() + + const onListen = () => { + dispatch(midiListen(action)) + } + const onStopListen = () => { + dispatch(midiStopListening()) + } + + const onSelect = (open: boolean) => { + if (open) { + if (!controlledAction) onListen() + } else { + onStopListen() + } + } + + 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', + } + + const getState = (): keyof typeof StateColor | undefined => { + if (isListening) return 'listening' + if (controlledAction) return 'controlled' + return undefined + } + + useEffect(() => { + if (isListening && controlledRef.current !== controlledAction) { + onStopListen() + } + controlledRef.current = controlledAction + }, [isListening, controlledAction]) + + return ( + + {children} + {isEditing && ( + + + + + {controlledAction && ( + + <> + + {controlledAction.inputID} + dispatch(removeMidiAction(action))} + > + X + + + + + + {controlledAction.options.type === 'note' && ( + <> + {/* void) => { + const loop = () => { + cb() + requestAnimationFrame(loop) + } + loop() +} 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/features/ui/react/theme.ts b/src/features/ui/react/theme/theme.ts similarity index 100% rename from src/features/ui/react/theme.ts rename to src/features/ui/react/theme/theme.ts diff --git a/src/features/visualizer/react/FileList.tsx b/src/features/visualizer/react/FileList.tsx index 9977cdb5..feb4029b 100644 --- a/src/features/visualizer/react/FileList.tsx +++ b/src/features/visualizer/react/FileList.tsx @@ -1,6 +1,6 @@ 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' @@ -24,7 +24,7 @@ interface Props { export default function FileList(props: Props) { let onAdd = () => { - getLocalFilepaths('Select Media', localMediaFileFilters) + api.queries.getLocalFilepaths('Select Media', localMediaFileFilters) .then(onAddSuccess) .catch((_err) => {}) } diff --git a/src/features/visualizer/react/OpenVisualizerButton.tsx b/src/features/visualizer/react/OpenVisualizerButton.tsx index 431d32ee..b9abeb01 100644 --- a/src/features/visualizer/react/OpenVisualizerButton.tsx +++ b/src/features/visualizer/react/OpenVisualizerButton.tsx @@ -1,6 +1,6 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew' import IconButton from '@mui/material/IconButton' -import { send_open_visualizer } from 'renderer/ipcHandler' +import * as api from 'renderer/api' import styled from 'styled-components' @@ -9,7 +9,7 @@ interface Props {} export default function OpenVisualizerButton({}: Props) { return ( - + 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..ed87f3fe --- /dev/null +++ b/src/renderer/api/index.ts @@ -0,0 +1,3 @@ +export * as mutations from './mutations' +export * as subscriptions from './subscriptions' +export * as queries from './queries' diff --git a/src/renderer/api/mutations.ts b/src/renderer/api/mutations.ts new file mode 100644 index 00000000..49aaebec --- /dev/null +++ b/src/renderer/api/mutations.ts @@ -0,0 +1,18 @@ +import ipc_channels, { + UserCommand, +} from '../../features/shared/engine/ipc_channels' +import { IpcRenderer } from 'electron' +import { CleanReduxState } from '../redux/store' + +// @ts-ignore: Typescript doesn't recognize the globals set in "src/main/preload.js" +const ipcRenderer: IpcRenderer = window.electron.ipcRenderer + +export function send_control_state(cleanState: CleanReduxState) { + ipcRenderer.send(ipc_channels.new_control_state, cleanState) +} +export function send_user_command(command: UserCommand) { + ipcRenderer.send(ipc_channels.user_command, command) +} +export function send_open_visualizer() { + ipcRenderer.send(ipc_channels.open_visualizer) +} diff --git a/src/renderer/api/queries.ts b/src/renderer/api/queries.ts new file mode 100644 index 00000000..50b44f96 --- /dev/null +++ b/src/renderer/api/queries.ts @@ -0,0 +1,16 @@ +import ipc_channels from '../../features/shared/engine/ipc_channels' +import { IpcRenderer } from 'electron' + +// @ts-ignore: Typescript doesn't recognize the globals set in "src/main/preload.js" +const ipcRenderer: IpcRenderer = window.electron.ipcRenderer + +export async function getLocalFilepaths( + title: string, + fileFilters: Electron.FileFilter[] +): Promise { + return ipcRenderer.invoke( + ipc_channels.get_local_filepaths, + title, + fileFilters + ) +} diff --git a/src/renderer/api/subscriptions.ts b/src/renderer/api/subscriptions.ts new file mode 100644 index 00000000..bebd53d7 --- /dev/null +++ b/src/renderer/api/subscriptions.ts @@ -0,0 +1,105 @@ +import { MainCommand } from '../../features/shared/engine/ipc_channels' +import { + RealtimeState, + initRealtimeState, + realtimeStore, + update as updateRealtimeStore, +} from '../redux/realtimeStore' +import * as dmxConnection from 'features/dmx/engine/dmxConnection' +import * as midiConnection from 'features/midi/engine/midiConnection' +import { PayloadAction } from '@reduxjs/toolkit' +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 '../redux/guiSlice' +import { + getUndoGroup, + undoAction, + redoAction, +} from '../../features/scenes/react/controls/UndoRedo' +import { getSaveConfig } from 'features/fileSaving/shared/save' +import * as mutations from './mutations' +import { autoSave } from '../../features/fileSaving/react/autosave' +import { animationLoop } from 'features/shared/react' + +type Emissions = { + main_command: [command: MainCommand] + dispatch: [action: PayloadAction] + new_time_state: [realtimeState: RealtimeState] + midi_connection_update: [payload: midiConnection.UpdatePayload] + dmx_connection_update: [payload: dmxConnection.UpdatePayload] +} + +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.send_control_state(getCleanReduxState(store.getState())) + + store.subscribe(() => + mutations.send_control_state(getCleanReduxState(store.getState())) + ) +} diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index f04d9525..19c80b84 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -1,106 +1,24 @@ import './3rd-party/initFirebase' import '../features/logging/react/initErrorLogging' import { render } from 'react-dom' -import { ThemeProvider } from 'styled-components' import GlobalStyle from './GlobalStyle' import App from './App' -import * as themes from '../features/ui/react/theme' +import { CaptivateThemeProvider } from '../features/ui/react/theme' import { Provider } from 'react-redux' -import { store, getCleanReduxState } from './redux/store' -import { - setDmx, - setMidi, - setSaving, - setLoading, - setNewProjectDialog, -} from './redux/guiSlice' -import { - realtimeStore, - realtimeContext, - initRealtimeState, - update as updateRealtimeStore, -} from './redux/realtimeStore' -import { ipc_setup, send_control_state } from './ipcHandler' -import { ThemeProvider as MuiThemeProvider } from '@emotion/react' -import { createTheme } from '@mui/material/styles' -import { autoSave } from '../features/fileSaving/react/autosave' -import { getUndoGroup, undoAction, redoAction } from '../features/scenes/react/controls/UndoRedo' -import { load } from '../features/fileSaving/react/SaveLoad' -import { getSaveConfig } from 'features/fileSaving/shared/save' +import { store } from './redux/store' +import { realtimeStore, realtimeContext } from './redux/realtimeStore' -const theme = themes.dark() -const muiTheme = createTheme({ - palette: { - mode: 'dark', - }, -}) -let _frequentlyUpdatedRealtimeState = initRealtimeState() +import * as api from './api' -autoSave(store) - -ipc_setup({ - on_dmx_connection_update: (payload) => { - store.dispatch(setDmx(payload)) - }, - on_midi_connection_update: (payload) => { - store.dispatch(setMidi(payload)) - }, - on_time_state: (newRealtimeState) => { - _frequentlyUpdatedRealtimeState = newRealtimeState - }, - on_dispatch: (action) => { - store.dispatch(action) - }, - on_main_command: (command) => { - if (command.type === 'undo') { - const group = getUndoGroup(store.getState()) - if (group !== null) { - store.dispatch(undoAction(group)) - } - } else if (command.type === 'redo') { - const group = getUndoGroup(store.getState()) - 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)) - } - }, -}) - -function animateRealtimeState() { - realtimeStore.dispatch(updateRealtimeStore(_frequentlyUpdatedRealtimeState)) - requestAnimationFrame(animateRealtimeState) -} - -animateRealtimeState() - -send_control_state(getCleanReduxState(store.getState())) - -store.subscribe(() => send_control_state(getCleanReduxState(store.getState()))) +api.subscriptions.subcribe({ store }) render( - - - - - - + + + + , document.getElementById('root') diff --git a/src/renderer/ipcHandler.ts b/src/renderer/ipcHandler.ts deleted file mode 100644 index 08066ec1..00000000 --- a/src/renderer/ipcHandler.ts +++ /dev/null @@ -1,67 +0,0 @@ -import ipc_channels, { UserCommand, MainCommand } from '../features/shared/engine/ipc_channels' -import { CleanReduxState } from './redux/store' -import { RealtimeState } from './redux/realtimeStore' -import * as dmxConnection from 'features/dmx/engine/dmxConnection' -import * as midiConnection from 'features/midi/engine/midiConnection' -import { PayloadAction } from '@reduxjs/toolkit' - -interface Config { - on_dmx_connection_update: (payload: dmxConnection.UpdatePayload) => void - on_midi_connection_update: (payload: midiConnection.UpdatePayload) => void - on_time_state: (time_state: RealtimeState) => void - on_dispatch: (action: PayloadAction) => void - on_main_command: (command: MainCommand) => void -} - -let _config: Config - -// @ts-ignore: Typescript doesn't recognize the globals set in "src/main/preload.js" -const ipcRenderer = window.electron.ipcRenderer - -export function ipc_setup(config: Config) { - _config = config - - ipcRenderer.on( - ipc_channels.dmx_connection_update, - (payload: dmxConnection.UpdatePayload) => - _config.on_dmx_connection_update(payload) - ) - - ipcRenderer.on( - ipc_channels.midi_connection_update, - (payload: midiConnection.UpdatePayload) => - _config.on_midi_connection_update(payload) - ) - - ipcRenderer.on(ipc_channels.new_time_state, (realtimeState: RealtimeState) => - _config.on_time_state(realtimeState) - ) - - ipcRenderer.on(ipc_channels.dispatch, (action: PayloadAction) => - _config.on_dispatch(action) - ) - - ipcRenderer.on(ipc_channels.main_command, (command: MainCommand) => - _config.on_main_command(command) - ) -} - -export function send_control_state(cleanState: CleanReduxState) { - ipcRenderer.send(ipc_channels.new_control_state, cleanState) -} -export function send_user_command(command: UserCommand) { - ipcRenderer.send(ipc_channels.user_command, command) -} -export function send_open_visualizer() { - ipcRenderer.send(ipc_channels.open_visualizer) -} -export async function getLocalFilepaths( - title: string, - fileFilters: Electron.FileFilter[] -): Promise { - return ipcRenderer.invoke( - ipc_channels.get_local_filepaths, - title, - fileFilters - ) -} From 5aae70e36fc0663ab66e093d70231c312c2e40e7 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 03:12:10 -0400 Subject: [PATCH 64/98] refactor: engine cleanup --- src/main/engine/engine.ts | 2 ++ src/main/engine/ipcHandler.ts | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index 8fdd5488..35d404b6 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -43,6 +43,8 @@ let _controlState: CleanReduxState | null = null let _realtimeState: RealtimeState = initRealtimeState() const _midiThrottle = new ThrottleMap((message: MidiMessage) => { + // TODO: maybe we could cancel the throttle on close and initialize throttle after callbacks and control state are initialized + // to avoid null checks if (_controlState !== null && _ipcCallbacks !== null) { handleMessage( message, diff --git a/src/main/engine/ipcHandler.ts b/src/main/engine/ipcHandler.ts index def7a1cb..29198727 100644 --- a/src/main/engine/ipcHandler.ts +++ b/src/main/engine/ipcHandler.ts @@ -63,6 +63,8 @@ export function ipcSetup(config: Config) { export type IPC_Callbacks = ReturnType -ipcMain.handle(fileApi.load.channel, fileApi.load.resolve) -ipcMain.handle(fileApi.save.channel, fileApi.save.resolve) -ipcMain.handle(fileApi.getFilePaths.channel, fileApi.getFilePaths.resolve) +const handlers = [fileApi.load, fileApi.save, fileApi.getFilePaths] + +handlers.forEach((handler) => { + ipcMain.handle(handler.channel, handler.resolve) +}) From 251cfea1ea6169a7db9809685d465aee43688cfc Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 05:15:49 -0400 Subject: [PATCH 65/98] refactor: api calls --- src/features/fileSaving/engine/api/core.ts | 8 - .../fileSaving/engine/api/getFilePaths.ts | 10 +- src/features/fileSaving/engine/api/load.ts | 10 +- src/features/fileSaving/engine/api/save.ts | 13 +- src/features/menu/engine/menu.ts | 34 ++-- src/features/shared/engine/emissions.ts | 42 +++++ .../visualizer/shared}/ipcChannels.ts | 2 +- src/main/engine/api/core.ts | 153 ++++++++++++++++++ src/main/engine/api/index.ts | 14 ++ src/main/engine/api/mutations.ts | 28 ++++ src/main/engine/api/queries.ts | 8 + src/main/engine/api/subscriptions.ts | 41 +++++ src/main/engine/engine.ts | 39 ++--- src/main/engine/ipcHandler.ts | 70 -------- src/renderer/api/subscriptions.ts | 21 +-- src/visualizer/ipcHandler.ts | 2 +- 16 files changed, 336 insertions(+), 159 deletions(-) create mode 100644 src/features/shared/engine/emissions.ts rename src/{visualizer => features/visualizer/shared}/ipcChannels.ts (85%) create mode 100644 src/main/engine/api/core.ts create mode 100644 src/main/engine/api/index.ts create mode 100644 src/main/engine/api/mutations.ts create mode 100644 src/main/engine/api/queries.ts create mode 100644 src/main/engine/api/subscriptions.ts delete mode 100644 src/main/engine/ipcHandler.ts diff --git a/src/features/fileSaving/engine/api/core.ts b/src/features/fileSaving/engine/api/core.ts index ccc5bca3..e69de29b 100644 --- a/src/features/fileSaving/engine/api/core.ts +++ b/src/features/fileSaving/engine/api/core.ts @@ -1,8 +0,0 @@ -import { IpcMainInvokeEvent } from 'electron' - -export const createHandler = (config: { - channel: string - resolve: (event: IpcMainInvokeEvent, ...args: any[]) => Promise | any -}) => { - return config -} diff --git a/src/features/fileSaving/engine/api/getFilePaths.ts b/src/features/fileSaving/engine/api/getFilePaths.ts index 1cc97d8d..dc75f245 100644 --- a/src/features/fileSaving/engine/api/getFilePaths.ts +++ b/src/features/fileSaving/engine/api/getFilePaths.ts @@ -1,13 +1,9 @@ import { dialog } from 'electron' import ipcChannels from '../../../shared/engine/ipc_channels' -import { createHandler } from './core' -export default createHandler({ +import { createQuery } from 'main/engine/api/core' +export default createQuery({ channel: ipcChannels.get_local_filepaths, - resolve: async ( - _event, - title: string, - fileFilters: Electron.FileFilter[] - ) => { + resolve: async (_, title, fileFilters) => { const dialogResult = await dialog.showOpenDialog({ title: title, filters: fileFilters, diff --git a/src/features/fileSaving/engine/api/load.ts b/src/features/fileSaving/engine/api/load.ts index 6b5c1675..91bd109c 100644 --- a/src/features/fileSaving/engine/api/load.ts +++ b/src/features/fileSaving/engine/api/load.ts @@ -1,14 +1,10 @@ import { dialog } from 'electron' import ipcChannels from '../../../shared/engine/ipc_channels' -import { createHandler } from './core' import fs from 'fs' -export default createHandler({ +import { createQuery } from 'main/engine/api/core' +export default createQuery({ channel: ipcChannels.load_file, - resolve: async ( - _event, - title: string, - fileFilters: Electron.FileFilter[] - ) => { + resolve: async (_, title, fileFilters) => { const dialogResult = await dialog.showOpenDialog({ title: title, filters: fileFilters, diff --git a/src/features/fileSaving/engine/api/save.ts b/src/features/fileSaving/engine/api/save.ts index a6d01195..9cdbd2cb 100644 --- a/src/features/fileSaving/engine/api/save.ts +++ b/src/features/fileSaving/engine/api/save.ts @@ -1,15 +1,10 @@ import { dialog } from 'electron' import ipcChannels from '../../../shared/engine/ipc_channels' -import { createHandler } from './core' import fs from 'fs' -export default createHandler({ +import { createQuery } from 'main/engine/api/core' +export default createQuery({ channel: ipcChannels.save_file, - resolve: async ( - _event, - title: string, - data: string, - fileFilters: Electron.FileFilter[] - ) => { + resolve: async (_, title, data, fileFilters) => { const dialogResult = await dialog.showSaveDialog({ title: title, filters: fileFilters, @@ -20,5 +15,5 @@ export default createHandler({ } else { throw new Error('User cancelled the file save') } - } + }, }) diff --git a/src/features/menu/engine/menu.ts b/src/features/menu/engine/menu.ts index 3be2c341..539d438f 100644 --- a/src/features/menu/engine/menu.ts +++ b/src/features/menu/engine/menu.ts @@ -6,7 +6,7 @@ import { MenuItemConstructorOptions, dialog, } from 'electron' -import { IPC_Callbacks } from '../../../main/engine/ipcHandler' +import { IPC_Callbacks } from '../../../main/engine/api' import { listPorts } from 'features/dmx/engine/dmxConnection' import { toggleLedEnabled, @@ -90,14 +90,14 @@ export default class MenuBuilder { label: 'New Project', accelerator: 'Command+N', click: () => { - this.res.ipcCallbacks.send_main_command({ type: 'new-project' }) + this.res.ipcCallbacks.publishers.main_command({ type: 'new-project' }) }, }, { label: 'Save', accelerator: 'Command+S', click: () => { - this.res.ipcCallbacks.send_main_command({ type: 'save' }) + this.res.ipcCallbacks.publishers.main_command({ type: 'save' }) }, }, // { @@ -109,7 +109,7 @@ export default class MenuBuilder { label: 'Load', accelerator: 'Command+O', click: () => { - this.res.ipcCallbacks.send_main_command({ type: 'load' }) + this.res.ipcCallbacks.publishers.main_command({ type: 'load' }) }, }, // { @@ -126,14 +126,14 @@ export default class MenuBuilder { label: 'Undo', accelerator: 'Command+Z', click: () => { - this.res.ipcCallbacks.send_main_command({ type: 'undo' }) + this.res.ipcCallbacks.publishers.main_command({ type: 'undo' }) }, }, { label: 'Redo', accelerator: 'Shift+Command+Z', click: () => { - this.res.ipcCallbacks.send_main_command({ type: 'redo' }) + this.res.ipcCallbacks.publishers.main_command({ type: 'redo' }) }, }, { type: 'separator' }, @@ -174,13 +174,13 @@ export default class MenuBuilder { { label: 'Toggle Visuals (Alpha)', click: () => { - this.res.ipcCallbacks.send_dispatch(toggleVideoEnabled()) + this.res.ipcCallbacks.publishers.dispatch(toggleVideoEnabled()) }, }, { label: 'Toggle Led (Alpha)', click: () => { - this.res.ipcCallbacks.send_dispatch(toggleLedEnabled()) + this.res.ipcCallbacks.publishers.dispatch(toggleLedEnabled()) }, }, ], @@ -198,13 +198,13 @@ export default class MenuBuilder { { label: 'Toggle Visuals (Alpha)', click: () => { - this.res.ipcCallbacks.send_dispatch(toggleVideoEnabled()) + this.res.ipcCallbacks.publishers.dispatch(toggleVideoEnabled()) }, }, { label: 'Toggle Led (Alpha)', click: () => { - this.res.ipcCallbacks.send_dispatch(toggleLedEnabled()) + this.res.ipcCallbacks.publishers.dispatch(toggleLedEnabled()) }, }, ], @@ -285,21 +285,21 @@ export default class MenuBuilder { label: 'New Project', accelerator: 'Ctrl+N', click: () => { - this.res.ipcCallbacks.send_main_command({ type: 'new-project' }) + this.res.ipcCallbacks.publishers.main_command({ type: 'new-project' }) }, }, { label: 'Save', accelerator: 'Ctrl+S', click: () => { - this.res.ipcCallbacks.send_main_command({ type: 'save' }) + this.res.ipcCallbacks.publishers.main_command({ type: 'save' }) }, }, { label: 'Load', accelerator: 'Ctrl+O', click: () => { - this.res.ipcCallbacks.send_main_command({ type: 'load' }) + this.res.ipcCallbacks.publishers.main_command({ type: 'load' }) }, }, ], @@ -336,13 +336,13 @@ export default class MenuBuilder { { label: 'Toggle Visuals (Alpha)', click: () => { - this.res.ipcCallbacks.send_dispatch(toggleVideoEnabled()) + this.res.ipcCallbacks.publishers.dispatch(toggleVideoEnabled()) }, }, { label: 'Toggle Led (Alpha)', click: () => { - this.res.ipcCallbacks.send_dispatch(toggleLedEnabled()) + this.res.ipcCallbacks.publishers.dispatch(toggleLedEnabled()) }, }, ] @@ -359,13 +359,13 @@ export default class MenuBuilder { { label: 'Toggle Visuals (Alpha)', click: () => { - this.res.ipcCallbacks.send_dispatch(toggleVideoEnabled()) + this.res.ipcCallbacks.publishers.dispatch(toggleVideoEnabled()) }, }, { label: 'Toggle Led (Alpha)', click: () => { - this.res.ipcCallbacks.send_dispatch(toggleLedEnabled()) + this.res.ipcCallbacks.publishers.dispatch(toggleLedEnabled()) }, }, ], diff --git a/src/features/shared/engine/emissions.ts b/src/features/shared/engine/emissions.ts new file mode 100644 index 00000000..1d502e62 --- /dev/null +++ b/src/features/shared/engine/emissions.ts @@ -0,0 +1,42 @@ +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: [title: string, fileFilters: Electron.FileFilter[]] + load_file: [title: string, fileFilters: Electron.FileFilter[]] + save_file: [ + title: string, + data: string, + fileFilters: Electron.FileFilter[] + ] + } + } + + visualizer: { + subscriptions: { + new_visualizer_state: [payload: VisualizerResource] + } + } +} 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/main/engine/api/core.ts b/src/main/engine/api/core.ts new file mode 100644 index 00000000..8f6afbc1 --- /dev/null +++ b/src/main/engine/api/core.ts @@ -0,0 +1,153 @@ +import { IpcMain, WebContents } from 'electron' +import { VisualizerContainer } from 'features/visualizer/engine/createVisualizerWindow' +import { RealtimeState } from 'renderer/redux/realtimeStore' +import ipc_channels from '../../../features/shared/engine/ipc_channels' +import visualizerChannels from '../../../features/visualizer/shared/ipcChannels' + +export type Context = { + renderer: WebContents + visualizerContainer: VisualizerContainer + realtimeState: RealtimeState + 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' + +export const createQuery = < + Channel extends keyof API['renderer']['queries'] +>(config: { + channel: Channel + resolve: ( + context: Context, + ...args: API['renderer']['queries'][Channel] + ) => Promise | any +}) => { + return { + channel: config.channel, + resolve: (_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 { + channel: config.channel, + resolve: ( + _event: Electron.IpcMainEvent, + context: Context, + ...args: any[] + ) => { + return config.resolve(context, ...(args as any)) + }, + } +} + +export const createMutations = ( + handlers: { + channel: string + resolve: ( + _event: Electron.IpcMainEvent, + context: Context, + ...args: any[] + ) => any + }[] +) => { + return (context: Context) => { + handlers.forEach((handler) => { + context.ipcMain.on(handler.channel, (_event, ...args) => { + handler.resolve(_event, context, ...args) + }) + }) + } +} + +export const createQueries = ( + handlers: { + channel: string + resolve: ( + _event: Electron.IpcMainInvokeEvent, + context: Context, + ...args: any[] + ) => any + }[] +) => { + return (context: Context) => { + handlers.forEach((handler) => { + context.ipcMain.handle(handler.channel, (_event, ...args) => { + handler.resolve(_event, context, ...args) + }) + }) + } +} diff --git a/src/main/engine/api/index.ts b/src/main/engine/api/index.ts new file mode 100644 index 00000000..f86e1712 --- /dev/null +++ b/src/main/engine/api/index.ts @@ -0,0 +1,14 @@ +import { Context } from './core' +import { mutations } from './mutations' +import { queries } from './queries' +import { publishers } from './subscriptions' + +export const createApi = (context: Context) => { + return { + publishers: publishers(context), + mutations: mutations(context), + queries: queries(context), + } +} + +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..d8530e8e --- /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/Link' +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.realtimeState) + }, + }), + 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..5f7f2080 --- /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..8edfe0f3 --- /dev/null +++ b/src/main/engine/api/subscriptions.ts @@ -0,0 +1,41 @@ +import { API } from 'features/shared/engine/emissions' +import { + Context, + createRendererPublishers, + createVisualizerPublishers, +} from './core' + +export const publishers = (context: Context) => { + /** + + + send_visualizer_state: (payload: VisualizerResource) => { + const visualizer = _config.visualizerContainer.visualizer + if (visualizer) { + visualizer.webContents.send( + ipcChannelsVisualizer.new_visualizer_state, + payload + ) + } + }, + + */ + 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/engine.ts b/src/main/engine/engine.ts index 35d404b6..5bc9fb41 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -1,7 +1,7 @@ -import { WebContents } from 'electron' +import { WebContents, ipcMain } from 'electron' import * as DmxConnection from 'features/dmx/engine/dmxConnection' import * as MidiConnection from 'features/midi/engine/midiConnection' -import { ipcSetup, IPC_Callbacks } from './ipcHandler' + import { CleanReduxState } from '../../renderer/redux/store' import { RealtimeState, @@ -16,9 +16,7 @@ import { } from '../../features/bpm/shared/randomizer' import { getOutputParams } from '../../features/modulation/shared/modulation' import { handleMessage } from 'features/midi/engine/handleMidi' -import openVisualizerWindow, { - VisualizerContainer, -} from '../../features/visualizer/engine/createVisualizerWindow' +import { VisualizerContainer } from '../../features/visualizer/engine/createVisualizerWindow' import { calculateDmx } from 'features/dmx/engine/dmxEngine' import { handleAutoScene } from '../../features/scenes/engine/autoScene' import { setActiveScene } from '../../renderer/redux/controlSlice' @@ -33,11 +31,10 @@ import { indexArray } from '../../features/utils/util' import WledManager from '../../features/led/engine/wled_manager' import { getNextTimeState, - onLinkUserCommand, _nodeLink, _tapTempo, } from 'features/bpm/engine/Link' - +import { createApi, IPC_Callbacks } from './api' let _ipcCallbacks: IPC_Callbacks | null = null let _controlState: CleanReduxState | null = null let _realtimeState: RealtimeState = initRealtimeState() @@ -51,7 +48,7 @@ const _midiThrottle = new ThrottleMap((message: MidiMessage) => { _controlState, _realtimeState, _nodeLink, - _ipcCallbacks.send_dispatch, + _ipcCallbacks.publishers.dispatch, _tapTempo ) } @@ -65,16 +62,14 @@ export function start( renderer: WebContents, visualizerContainer: VisualizerContainer ) { - _ipcCallbacks = ipcSetup({ - renderer: renderer, - visualizerContainer: visualizerContainer, - on_new_control_state: (newState) => { + _ipcCallbacks = createApi({ + ipcMain, + realtimeState: _realtimeState, + new_control_state: (newState) => { _controlState = newState }, - on_user_command: (command) => onLinkUserCommand(command, _realtimeState), - on_open_visualizer: () => { - openVisualizerWindow(visualizerContainer) - }, + renderer, + visualizerContainer, }) // We're currently calculating the realtimeState 90x per second. @@ -88,8 +83,8 @@ export function start( _ipcCallbacks, _controlState ) - _ipcCallbacks.send_time_state(_realtimeState) - _ipcCallbacks.send_visualizer_state({ + _ipcCallbacks.publishers.new_time_state(_realtimeState) + _ipcCallbacks.publishers.new_visualizer_state({ rt: _realtimeState, state: _controlState, }) @@ -106,7 +101,7 @@ DmxConnection.maintain({ update_ms: 1000, onUpdate: (dmxStatus) => { if (_ipcCallbacks !== null) - _ipcCallbacks.send_dmx_connection_update(dmxStatus) + _ipcCallbacks.publishers.dmx_connection_update(dmxStatus) }, getChannels: () => _realtimeState.dmxOut, getConnectable: () => { @@ -118,7 +113,7 @@ MidiConnection.maintain({ update_ms: 1000, onUpdate: (activeDevices) => { if (_ipcCallbacks !== null) - _ipcCallbacks.send_midi_connection_update(activeDevices) + _ipcCallbacks.publishers.midi_connection_update(activeDevices) }, onMessage: (message) => { _midiThrottle.call(midiInputID(message), message) @@ -144,7 +139,7 @@ function getNextRealtimeState( nextTimeState, controlState, (newLightScene) => { - ipcCallbacks.send_dispatch( + ipcCallbacks.publishers.dispatch( setActiveScene({ sceneType: 'light', val: newLightScene, @@ -152,7 +147,7 @@ function getNextRealtimeState( ) }, (newVisualScene) => { - ipcCallbacks.send_dispatch( + ipcCallbacks.publishers.dispatch( setActiveScene({ sceneType: 'visual', val: newVisualScene, diff --git a/src/main/engine/ipcHandler.ts b/src/main/engine/ipcHandler.ts deleted file mode 100644 index 29198727..00000000 --- a/src/main/engine/ipcHandler.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ipcMain, WebContents } from 'electron' -import ipcChannels, { - UserCommand, - MainCommand, -} from '../../features/shared/engine/ipc_channels' -import ipcChannelsVisualizer from '../../visualizer/ipcChannels' -import { CleanReduxState } from '../../renderer/redux/store' -import { RealtimeState } from '../../renderer/redux/realtimeStore' -import * as dmxConnection from 'features/dmx/engine/dmxConnection' -import * as midiConnection from 'features/midi/engine/midiConnection' -import { PayloadAction } from '@reduxjs/toolkit' -import { VisualizerResource } from '../../features/visualizer/threejs/VisualizerManager' -import { VisualizerContainer } from '../../features/visualizer/engine/createVisualizerWindow' -import * as fileApi from 'features/fileSaving/engine/api' -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 - -const handlers = [fileApi.load, fileApi.save, fileApi.getFilePaths] - -handlers.forEach((handler) => { - ipcMain.handle(handler.channel, handler.resolve) -}) diff --git a/src/renderer/api/subscriptions.ts b/src/renderer/api/subscriptions.ts index bebd53d7..afe46d82 100644 --- a/src/renderer/api/subscriptions.ts +++ b/src/renderer/api/subscriptions.ts @@ -1,13 +1,9 @@ -import { MainCommand } from '../../features/shared/engine/ipc_channels' import { - RealtimeState, initRealtimeState, realtimeStore, update as updateRealtimeStore, } from '../redux/realtimeStore' -import * as dmxConnection from 'features/dmx/engine/dmxConnection' -import * as midiConnection from 'features/midi/engine/midiConnection' -import { PayloadAction } from '@reduxjs/toolkit' + import { subscribeIpc } from './core/ipcSubscription' import { getCleanReduxState, store } from '../redux/store' import { load } from '../../features/fileSaving/react/SaveLoad' @@ -27,14 +23,7 @@ import { getSaveConfig } from 'features/fileSaving/shared/save' import * as mutations from './mutations' import { autoSave } from '../../features/fileSaving/react/autosave' import { animationLoop } from 'features/shared/react' - -type Emissions = { - main_command: [command: MainCommand] - dispatch: [action: PayloadAction] - new_time_state: [realtimeState: RealtimeState] - midi_connection_update: [payload: midiConnection.UpdatePayload] - dmx_connection_update: [payload: dmxConnection.UpdatePayload] -} +import { API } from 'features/shared/engine/emissions' type Context = { store: typeof store @@ -47,7 +36,7 @@ export const subcribe = ({ store }: Context) => { autoSave(store) - subscribeIpc({ + subscribeIpc({ dmx_connection_update: (payload) => { store.dispatch(setDmx(payload)) }, @@ -92,9 +81,7 @@ export const subcribe = ({ store }: Context) => { }) animationLoop(() => { - realtimeStore.dispatch( - updateRealtimeStore(getUpatedRealtimeState()) - ) + realtimeStore.dispatch(updateRealtimeStore(getUpatedRealtimeState())) }) mutations.send_control_state(getCleanReduxState(store.getState())) diff --git a/src/visualizer/ipcHandler.ts b/src/visualizer/ipcHandler.ts index 47ed9815..be005a61 100644 --- a/src/visualizer/ipcHandler.ts +++ b/src/visualizer/ipcHandler.ts @@ -1,4 +1,4 @@ -import ipc_channels from './ipcChannels' +import ipc_channels from '../features/visualizer/shared/ipcChannels' import { VisualizerResource } from '../features/visualizer/threejs/VisualizerManager' interface Config { From 6f70dff03749c83d7202431a9abce0220ce2e65a Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 16:08:01 -0400 Subject: [PATCH 66/98] refactor: add dispose to engine --- src/features/dmx/engine/dmxConnection.ts | 31 ++++-- src/features/midi/engine/midiConnection.ts | 11 +- .../scenes/react/controls/ParamAddButton.tsx | 2 +- .../features/visualizer/images}/no_media.png | Bin .../threejs/util/LocalMediaQueue.ts | 2 +- src/main/engine/api/core.ts | 100 +++++++++++------- src/main/engine/api/index.ts | 9 +- src/main/engine/api/mutations.ts | 10 +- src/main/engine/api/queries.ts | 10 +- src/main/engine/api/subscriptions.ts | 14 --- src/main/engine/engine.ts | 82 +++++++++----- 11 files changed, 170 insertions(+), 101 deletions(-) rename {assets => src/features/visualizer/images}/no_media.png (100%) diff --git a/src/features/dmx/engine/dmxConnection.ts b/src/features/dmx/engine/dmxConnection.ts index accb6cca..01d45d3e 100644 --- a/src/features/dmx/engine/dmxConnection.ts +++ b/src/features/dmx/engine/dmxConnection.ts @@ -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/midi/engine/midiConnection.ts b/src/features/midi/engine/midiConnection.ts index b9514ff9..3e348623 100644 --- a/src/features/midi/engine/midiConnection.ts +++ b/src/features/midi/engine/midiConnection.ts @@ -22,9 +22,18 @@ const inputs: Inputs = {} type Inputs = { [portName: string]: Input } export function maintain(config: Config) { - setInterval(() => { + const interval = setInterval(() => { updateInputs(config) }, config.update_ms) + return { + dispose() { + clearInterval(interval) + Object.entries(inputs).forEach(([inputName, input]) => { + input.closePort() + delete inputs[inputName] + }) + }, + } } function updateInputs(config: Config) { diff --git a/src/features/scenes/react/controls/ParamAddButton.tsx b/src/features/scenes/react/controls/ParamAddButton.tsx index 754e048e..641280bf 100644 --- a/src/features/scenes/react/controls/ParamAddButton.tsx +++ b/src/features/scenes/react/controls/ParamAddButton.tsx @@ -12,7 +12,7 @@ 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 axisIconSrc from '../../../../../assets/axis.svg' import { getCustomChannels } from 'features/dmx/redux/dmxSlice' interface Props { 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/features/visualizer/threejs/util/LocalMediaQueue.ts b/src/features/visualizer/threejs/util/LocalMediaQueue.ts index c747cabc..24893ebc 100644 --- a/src/features/visualizer/threejs/util/LocalMediaQueue.ts +++ b/src/features/visualizer/threejs/util/LocalMediaQueue.ts @@ -2,7 +2,7 @@ import * as THREE from 'three' import { pathUrl } from './loaders' import LoadQueue from './LoadQueue' import { randomIndexExcludeCurrent } from '../../../utils/util' -import no_media_image from '../../../../assets/no_media.png' +import no_media_image from '../../images/no_media.png' import { LocalMediaConfig } from '../layers/LocalMediaConfig' import { Size, defaultSize, fit, cover } from '../../../utils/math/size' import { getMediaData, MediaData, releaseMediaData } from './MediaData' diff --git a/src/main/engine/api/core.ts b/src/main/engine/api/core.ts index 8f6afbc1..5edd47ea 100644 --- a/src/main/engine/api/core.ts +++ b/src/main/engine/api/core.ts @@ -12,8 +12,6 @@ export type Context = { new_control_state: (new_state: CleanReduxState) => void } - - export const createRendererPublishers = < _Emissions extends Partial<{ [k in typeof ipc_channels[keyof typeof ipc_channels]]: [...any] @@ -56,10 +54,7 @@ export const createVisualizerPublishers = < previous[key] = (...args) => { const visualizer = visualizerContainer.visualizer if (visualizer) { - visualizer.webContents.send( - key as string, - ...args - ) + visualizer.webContents.send(key as string, ...args) } } return previous @@ -86,8 +81,11 @@ export const createQuery = < ) => Promise | any }) => { return { - channel: config.channel, - resolve: (_event: IpcMainInvokeEvent, context: Context, ...args: any[]) => { + [config.channel]: ( + _event: IpcMainInvokeEvent, + context: Context, + ...args: any[] + ) => { return config.resolve(context, ...(args as any)) }, } @@ -103,8 +101,7 @@ export const createMutation = < ) => Promise | any }) => { return { - channel: config.channel, - resolve: ( + [config.channel]: ( _event: Electron.IpcMainEvent, context: Context, ...args: any[] @@ -114,40 +111,63 @@ export const createMutation = < } } -export const createMutations = ( - handlers: { - channel: string - resolve: ( - _event: Electron.IpcMainEvent, - context: Context, - ...args: any[] - ) => any - }[] -) => { +export const createMutations = (handlers: { + [k: string]: ( + _event: Electron.IpcMainEvent, + context: Context, + ...args: any[] + ) => any +}) => { return (context: Context) => { - handlers.forEach((handler) => { - context.ipcMain.on(handler.channel, (_event, ...args) => { - handler.resolve(_event, context, ...args) - }) - }) + const disposeableHandlers = Object.entries(handlers).map( + ([channel, resolve]) => { + const handler = (_event: Electron.IpcMainEvent, ...args: any[]) => { + 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: { - channel: string - resolve: ( - _event: Electron.IpcMainInvokeEvent, - context: Context, - ...args: any[] - ) => any - }[] -) => { +export const createQueries = (handlers: { + [k: string]: ( + _event: Electron.IpcMainInvokeEvent, + context: Context, + ...args: any[] + ) => any +}) => { return (context: Context) => { - handlers.forEach((handler) => { - context.ipcMain.handle(handler.channel, (_event, ...args) => { - handler.resolve(_event, context, ...args) - }) - }) + const disposeableHandlers = Object.entries(handlers).map( + ([channel, resolve]) => { + const handler = ( + _event: Electron.IpcMainInvokeEvent, + ...args: any[] + ) => { + 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 index f86e1712..a30a886a 100644 --- a/src/main/engine/api/index.ts +++ b/src/main/engine/api/index.ts @@ -4,11 +4,18 @@ import { queries } from './queries' import { publishers } from './subscriptions' export const createApi = (context: Context) => { - return { + 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 index d8530e8e..601e02c4 100644 --- a/src/main/engine/api/mutations.ts +++ b/src/main/engine/api/mutations.ts @@ -6,23 +6,23 @@ import { CleanReduxState } from 'renderer/redux/store' import openVisualizerWindow from '../../../features/visualizer/engine/createVisualizerWindow' import { createMutation, createMutations } from './core' -export const mutations = createMutations([ - createMutation({ +export const mutations = createMutations({ + ...createMutation({ channel: ipcChannels.new_control_state, resolve: (ctx, new_state: CleanReduxState) => { ctx.new_control_state(new_state) }, }), - createMutation({ + ...createMutation({ channel: ipcChannels.user_command, resolve: (context, command: UserCommand) => { onLinkUserCommand(command, context.realtimeState) }, }), - createMutation({ + ...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 index 5f7f2080..93bdcb75 100644 --- a/src/main/engine/api/queries.ts +++ b/src/main/engine/api/queries.ts @@ -1,8 +1,8 @@ import { createQueries } from './core' import * as fileApi from 'features/fileSaving/engine/api' -export const queries = createQueries([ - fileApi.load, - fileApi.save, - fileApi.getFilePaths, -]) +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 index 8edfe0f3..5edc45df 100644 --- a/src/main/engine/api/subscriptions.ts +++ b/src/main/engine/api/subscriptions.ts @@ -6,20 +6,6 @@ import { } from './core' export const publishers = (context: Context) => { - /** - - - send_visualizer_state: (payload: VisualizerResource) => { - const visualizer = _config.visualizerContainer.visualizer - if (visualizer) { - visualizer.webContents.send( - ipcChannelsVisualizer.new_visualizer_state, - payload - ) - } - }, - - */ return { ...createRendererPublishers( context.renderer, diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index 5bc9fb41..a9cf9e62 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -36,16 +36,39 @@ import { } from 'features/bpm/engine/Link' import { createApi, IPC_Callbacks } from './api' let _ipcCallbacks: IPC_Callbacks | null = null -let _controlState: CleanReduxState | null = null +// let _controlState: CleanReduxState | null = null let _realtimeState: RealtimeState = initRealtimeState() +// TODO: this should live in control state feature +const controlStateManager = () => { + let _controlState: CleanReduxState | null = null + let resolveFirstState: () => void + const firstControlState = new Promise((resolve) => { + resolveFirstState = resolve + }) + return { + set(state: CleanReduxState) { + if (!_controlState) resolveFirstState() + _controlState = state + }, + + waitForFirstControlState: firstControlState, + + get() { + return _controlState + }, + } +} + +const controlState = controlStateManager() + const _midiThrottle = new ThrottleMap((message: MidiMessage) => { // TODO: maybe we could cancel the throttle on close and initialize throttle after callbacks and control state are initialized // to avoid null checks - if (_controlState !== null && _ipcCallbacks !== null) { + if (controlState.get() !== null && _ipcCallbacks !== null) { handleMessage( message, - _controlState, + controlState.get(), _realtimeState, _nodeLink, _ipcCallbacks.publishers.dispatch, @@ -66,34 +89,39 @@ export function start( ipcMain, realtimeState: _realtimeState, new_control_state: (newState) => { - _controlState = newState + controlState.set(newState) }, renderer, visualizerContainer, }) - // 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.publishers.new_time_state(_realtimeState) - _ipcCallbacks.publishers.new_visualizer_state({ - rt: _realtimeState, - state: _controlState, - }) - } - }, 1000 / 90) + // TODO: now we don't need null checks for control state + controlState.waitForFirstControlState.then(() => { + // 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.get() !== null) { + _realtimeState = getNextRealtimeState( + _realtimeState, + nextTimeState, + _ipcCallbacks, + controlState.get() + ) + _ipcCallbacks.publishers.new_time_state(_realtimeState) + _ipcCallbacks.publishers.new_visualizer_state({ + rt: _realtimeState, + state: controlState.get(), + }) + } + }, 1000 / 90) + }) + return _ipcCallbacks } export function stop() { + _ipcCallbacks?.dispose() _ipcCallbacks = null } @@ -105,7 +133,9 @@ DmxConnection.maintain({ }, getChannels: () => _realtimeState.dmxOut, getConnectable: () => { - return _controlState ? _controlState.control.device.connectable.dmx : [] + return controlState.get() + ? controlState.get().control.device.connectable.dmx + : [] }, }) @@ -119,7 +149,9 @@ MidiConnection.maintain({ _midiThrottle.call(midiInputID(message), message) }, getConnectable: () => { - return _controlState ? _controlState.control.device.connectable.midi : [] + return controlState.get() + ? controlState.get().control.device.connectable.midi + : [] }, }) @@ -200,6 +232,6 @@ function getNextRealtimeState( } new WledManager( - () => _controlState, + () => controlState.get(), () => _realtimeState ) From dbcf2f44d2531e40fdfcd0f0a341f59fc6e30120 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 16:15:08 -0400 Subject: [PATCH 67/98] fix: queries and mutations --- src/main/engine/api/core.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/engine/api/core.ts b/src/main/engine/api/core.ts index 5edd47ea..4b774c99 100644 --- a/src/main/engine/api/core.ts +++ b/src/main/engine/api/core.ts @@ -122,7 +122,7 @@ export const createMutations = (handlers: { const disposeableHandlers = Object.entries(handlers).map( ([channel, resolve]) => { const handler = (_event: Electron.IpcMainEvent, ...args: any[]) => { - resolve(_event, context, ...args) + return resolve(_event, context, ...args) } context.ipcMain.on(channel, handler) return { @@ -155,7 +155,7 @@ export const createQueries = (handlers: { _event: Electron.IpcMainInvokeEvent, ...args: any[] ) => { - resolve(_event, context, ...args) + return resolve(_event, context, ...args) } context.ipcMain.handle(channel, handler) From 9cfffa2c8faee213c98558c3043d774d0e68c602 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 16:20:06 -0400 Subject: [PATCH 68/98] fix: control state types --- src/main/engine/engine.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index a9cf9e62..d0ddeb53 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -41,23 +41,23 @@ let _realtimeState: RealtimeState = initRealtimeState() // TODO: this should live in control state feature const controlStateManager = () => { - let _controlState: CleanReduxState | null = null let resolveFirstState: () => void const firstControlState = new Promise((resolve) => { resolveFirstState = resolve }) - return { + + const manager = { set(state: CleanReduxState) { - if (!_controlState) resolveFirstState() - _controlState = state + if (!manager.controlState) resolveFirstState() + manager.controlState = state }, + controlState: null as CleanReduxState | null, waitForFirstControlState: firstControlState, - get() { - return _controlState - }, + } + return manager } const controlState = controlStateManager() @@ -65,10 +65,10 @@ const controlState = controlStateManager() const _midiThrottle = new ThrottleMap((message: MidiMessage) => { // TODO: maybe we could cancel the throttle on close and initialize throttle after callbacks and control state are initialized // to avoid null checks - if (controlState.get() !== null && _ipcCallbacks !== null) { + if (controlState.controlState !== null && _ipcCallbacks !== null) { handleMessage( message, - controlState.get(), + controlState.controlState, _realtimeState, _nodeLink, _ipcCallbacks.publishers.dispatch, @@ -101,17 +101,17 @@ export function start( // 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.get() !== null) { + if (_ipcCallbacks !== null && controlState.controlState !== null) { _realtimeState = getNextRealtimeState( _realtimeState, nextTimeState, _ipcCallbacks, - controlState.get() + controlState.controlState ) _ipcCallbacks.publishers.new_time_state(_realtimeState) _ipcCallbacks.publishers.new_visualizer_state({ rt: _realtimeState, - state: controlState.get(), + state: controlState.controlState, }) } }, 1000 / 90) @@ -133,8 +133,8 @@ DmxConnection.maintain({ }, getChannels: () => _realtimeState.dmxOut, getConnectable: () => { - return controlState.get() - ? controlState.get().control.device.connectable.dmx + return controlState.controlState + ? controlState.controlState.control.device.connectable.dmx : [] }, }) @@ -149,8 +149,8 @@ MidiConnection.maintain({ _midiThrottle.call(midiInputID(message), message) }, getConnectable: () => { - return controlState.get() - ? controlState.get().control.device.connectable.midi + return controlState.controlState + ? controlState.controlState.control.device.connectable.midi : [] }, }) @@ -232,6 +232,6 @@ function getNextRealtimeState( } new WledManager( - () => controlState.get(), + () => controlState.controlState, () => _realtimeState ) From 379cbf621cb8c84b661e91b38765b09227e89e75 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 17:02:32 -0400 Subject: [PATCH 69/98] refactor: cleanup disposeables --- src/features/led/engine/wled_manager.ts | 2 +- src/features/midi/engine/midiConnection.ts | 7 + src/main/engine/engine.ts | 192 ++++++++++++--------- 3 files changed, 116 insertions(+), 85 deletions(-) diff --git a/src/features/led/engine/wled_manager.ts b/src/features/led/engine/wled_manager.ts index ff10d418..a76419cf 100644 --- a/src/features/led/engine/wled_manager.ts +++ b/src/features/led/engine/wled_manager.ts @@ -58,7 +58,7 @@ export default class WledManager { } } - release() { + dispose() { for (const [_mdns, device] of Object.entries(this.devices)) { if (device !== undefined) { device.release() diff --git a/src/features/midi/engine/midiConnection.ts b/src/features/midi/engine/midiConnection.ts index 3e348623..4072739e 100644 --- a/src/features/midi/engine/midiConnection.ts +++ b/src/features/midi/engine/midiConnection.ts @@ -114,4 +114,11 @@ export class ThrottleMap { } throttled(arg) } + + dispose() { + Object.entries(this.throttles).forEach(([id, throttled]) => { + throttled?.cancel() + delete this.throttles[id] + }) + } } diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index d0ddeb53..b3b84a3f 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -35,8 +35,7 @@ import { _tapTempo, } from 'features/bpm/engine/Link' import { createApi, IPC_Callbacks } from './api' -let _ipcCallbacks: IPC_Callbacks | null = null -// let _controlState: CleanReduxState | null = null + let _realtimeState: RealtimeState = initRealtimeState() // TODO: this should live in control state feature @@ -46,6 +45,8 @@ const controlStateManager = () => { resolveFirstState = resolve }) + let cancelled = false + const manager = { set(state: CleanReduxState) { if (!manager.controlState) resolveFirstState() @@ -53,108 +54,136 @@ const controlStateManager = () => { }, controlState: null as CleanReduxState | null, - waitForFirstControlState: firstControlState, - + dispose() { + cancelled = true + }, + waitForFirstControlState: (cb: () => void) => { + firstControlState.then(() => { + if (!cancelled) cb() + }) + }, } return manager } -const controlState = controlStateManager() - -const _midiThrottle = new ThrottleMap((message: MidiMessage) => { - // TODO: maybe we could cancel the throttle on close and initialize throttle after callbacks and control state are initialized - // to avoid null checks - if (controlState.controlState !== null && _ipcCallbacks !== null) { - handleMessage( - message, - controlState.controlState, - _realtimeState, - _nodeLink, - _ipcCallbacks.publishers.dispatch, - _tapTempo - ) - } -}, 1000 / 60) - -export function getIpcCallbacks() { - return _ipcCallbacks +const disposer = { + _disposeables: [] as { dispose(): void }[], + push(disposeable: T) { + disposer._disposeables.push(disposeable) + return disposeable + }, + dispose() { + disposer._disposeables.forEach((disposeable) => { + disposeable.dispose() + }) + disposer._disposeables = [] + }, } +const controlState = disposer.push(controlStateManager()) + export function start( renderer: WebContents, visualizerContainer: VisualizerContainer ) { - _ipcCallbacks = createApi({ - ipcMain, - realtimeState: _realtimeState, - new_control_state: (newState) => { - controlState.set(newState) - }, - renderer, - visualizerContainer, - }) + const ipcCallbacks = disposer.push( + createApi({ + ipcMain, + realtimeState: _realtimeState, + new_control_state: (newState) => { + controlState.set(newState) + }, + renderer, + visualizerContainer, + }) + ) // TODO: now we don't need null checks for control state - controlState.waitForFirstControlState.then(() => { + controlState.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) - setInterval(() => { + const realtimeStateInterval = setInterval(() => { const nextTimeState = getNextTimeState() - if (_ipcCallbacks !== null && controlState.controlState !== null) { - _realtimeState = getNextRealtimeState( + + _realtimeState = getNextRealtimeState( + _realtimeState, + nextTimeState, + ipcCallbacks, + controlState.controlState! + ) + ipcCallbacks.publishers.new_time_state(_realtimeState) + ipcCallbacks.publishers.new_visualizer_state({ + rt: _realtimeState, + state: controlState.controlState!, + }) + }, 1000 / 90) + + disposer.push({ + dispose() { + clearInterval(realtimeStateInterval) + }, + }) + + const _midiThrottle = disposer.push( + new ThrottleMap((message: MidiMessage) => { + // TODO: maybe we could cancel the throttle on close and initialize throttle after callbacks and control state are initialized + // to avoid null checks + + handleMessage( + message, + controlState.controlState!, _realtimeState, - nextTimeState, - _ipcCallbacks, - controlState.controlState + _nodeLink, + ipcCallbacks.publishers.dispatch, + _tapTempo ) - _ipcCallbacks.publishers.new_time_state(_realtimeState) - _ipcCallbacks.publishers.new_visualizer_state({ - rt: _realtimeState, - state: controlState.controlState, - }) - } - }, 1000 / 90) + }, 1000 / 60) + ) + + disposer.push( + DmxConnection.maintain({ + update_ms: 1000, + onUpdate: (dmxStatus) => { + ipcCallbacks.publishers.dmx_connection_update(dmxStatus) + }, + getChannels: () => _realtimeState.dmxOut, + getConnectable: () => { + return controlState.controlState!.control.device.connectable.dmx + }, + }) + ) + + disposer.push( + MidiConnection.maintain({ + update_ms: 1000, + onUpdate: (activeDevices) => { + ipcCallbacks.publishers.midi_connection_update(activeDevices) + }, + onMessage: (message) => { + _midiThrottle.call(midiInputID(message), message) + }, + getConnectable: () => { + return controlState.controlState!.control.device.connectable.midi + }, + }) + ) + + disposer.push( + new WledManager( + () => controlState.controlState, + () => _realtimeState + ) + ) }) - return _ipcCallbacks + return ipcCallbacks } export function stop() { - _ipcCallbacks?.dispose() - _ipcCallbacks = null + disposer.dispose() } -DmxConnection.maintain({ - update_ms: 1000, - onUpdate: (dmxStatus) => { - if (_ipcCallbacks !== null) - _ipcCallbacks.publishers.dmx_connection_update(dmxStatus) - }, - getChannels: () => _realtimeState.dmxOut, - getConnectable: () => { - return controlState.controlState - ? controlState.controlState.control.device.connectable.dmx - : [] - }, -}) - -MidiConnection.maintain({ - update_ms: 1000, - onUpdate: (activeDevices) => { - if (_ipcCallbacks !== null) - _ipcCallbacks.publishers.midi_connection_update(activeDevices) - }, - onMessage: (message) => { - _midiThrottle.call(midiInputID(message), message) - }, - getConnectable: () => { - return controlState.controlState - ? controlState.controlState.control.device.connectable.midi - : [] - }, -}) - function getNextRealtimeState( realtimeState: RealtimeState, nextTimeState: TimeState, @@ -230,8 +259,3 @@ function getNextRealtimeState( splitStates, } } - -new WledManager( - () => controlState.controlState, - () => _realtimeState -) From 54ec74a418bf5da0a67df9e14b892010e3c72536 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 17:06:05 -0400 Subject: [PATCH 70/98] refactor: rename ipc --- src/main/engine/engine.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index b3b84a3f..07c4d6a5 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -87,7 +87,7 @@ export function start( renderer: WebContents, visualizerContainer: VisualizerContainer ) { - const ipcCallbacks = disposer.push( + const api = disposer.push( createApi({ ipcMain, realtimeState: _realtimeState, @@ -109,11 +109,11 @@ export function start( _realtimeState = getNextRealtimeState( _realtimeState, nextTimeState, - ipcCallbacks, + api, controlState.controlState! ) - ipcCallbacks.publishers.new_time_state(_realtimeState) - ipcCallbacks.publishers.new_visualizer_state({ + api.publishers.new_time_state(_realtimeState) + api.publishers.new_visualizer_state({ rt: _realtimeState, state: controlState.controlState!, }) @@ -135,7 +135,7 @@ export function start( controlState.controlState!, _realtimeState, _nodeLink, - ipcCallbacks.publishers.dispatch, + api.publishers.dispatch, _tapTempo ) }, 1000 / 60) @@ -145,7 +145,7 @@ export function start( DmxConnection.maintain({ update_ms: 1000, onUpdate: (dmxStatus) => { - ipcCallbacks.publishers.dmx_connection_update(dmxStatus) + api.publishers.dmx_connection_update(dmxStatus) }, getChannels: () => _realtimeState.dmxOut, getConnectable: () => { @@ -158,7 +158,7 @@ export function start( MidiConnection.maintain({ update_ms: 1000, onUpdate: (activeDevices) => { - ipcCallbacks.publishers.midi_connection_update(activeDevices) + api.publishers.midi_connection_update(activeDevices) }, onMessage: (message) => { _midiThrottle.call(midiInputID(message), message) @@ -177,7 +177,7 @@ export function start( ) }) - return ipcCallbacks + return api } export function stop() { @@ -187,7 +187,7 @@ export function stop() { function getNextRealtimeState( realtimeState: RealtimeState, nextTimeState: TimeState, - ipcCallbacks: IPC_Callbacks, + api: IPC_Callbacks, controlState: CleanReduxState ): RealtimeState { const scene = @@ -200,7 +200,7 @@ function getNextRealtimeState( nextTimeState, controlState, (newLightScene) => { - ipcCallbacks.publishers.dispatch( + api.publishers.dispatch( setActiveScene({ sceneType: 'light', val: newLightScene, @@ -208,7 +208,7 @@ function getNextRealtimeState( ) }, (newVisualScene) => { - ipcCallbacks.publishers.dispatch( + api.publishers.dispatch( setActiveScene({ sceneType: 'visual', val: newVisualScene, From 974c1d2eeeac93fc1fe68b650c93114fd1adf81e Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 17:35:35 -0400 Subject: [PATCH 71/98] refactor: fixtures redux --- src/features/dmx/react/ColorMapChannel.tsx | 2 +- src/features/dmx/react/EditGroups.tsx | 2 +- src/features/dmx/react/FixtureChannelItem.tsx | 2 +- src/features/dmx/react/FixtureChannelPopup.tsx | 2 +- src/features/dmx/react/FixtureChannels.tsx | 2 +- src/features/dmx/react/FixtureCursor.tsx | 2 +- src/features/dmx/react/FixturePlacement.tsx | 2 +- src/features/dmx/react/MyFixture.tsx | 2 +- src/features/dmx/react/MyFixtures.tsx | 2 +- src/features/dmx/react/Subfixtures.tsx | 2 +- src/features/dmx/react/UniverseSlot.tsx | 2 +- src/features/fileSaving/shared/save.ts | 2 +- .../{dmx/redux/dmxSlice.ts => fixtures/redux/fixturesSlice.ts} | 2 +- src/features/led/react/LedFixtureDefinition.tsx | 2 +- src/features/led/react/LedFixtureList.tsx | 2 +- src/features/led/react/LedFixturePlacement.tsx | 2 +- src/features/modulation/react/ModulationMatrix.tsx | 2 +- src/features/modulation/react/ModulationSlider.tsx | 2 +- src/features/scenes/react/controls/ParamAddButton.tsx | 2 +- src/features/scenes/react/controls/ParamsControl.tsx | 2 +- src/features/shared/redux/fixState.ts | 2 +- src/main/engine/engine.ts | 2 +- src/renderer/redux/initState.ts | 2 +- src/renderer/redux/store.ts | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) rename src/features/{dmx/redux/dmxSlice.ts => fixtures/redux/fixturesSlice.ts} (99%) diff --git a/src/features/dmx/react/ColorMapChannel.tsx b/src/features/dmx/react/ColorMapChannel.tsx index 808126b8..7b363c68 100644 --- a/src/features/dmx/react/ColorMapChannel.tsx +++ b/src/features/dmx/react/ColorMapChannel.tsx @@ -6,7 +6,7 @@ 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' diff --git a/src/features/dmx/react/EditGroups.tsx b/src/features/dmx/react/EditGroups.tsx index 4fbc9b18..738ea19b 100644 --- a/src/features/dmx/react/EditGroups.tsx +++ b/src/features/dmx/react/EditGroups.tsx @@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux' import { addActiveFixtureTypeGroup, removeActiveFixtureTypeGroup, -} from '../redux/dmxSlice' +} from '../../fixtures/redux/fixturesSlice' import { getSortedGroups, getSortedGroupsForFixtureType } from 'features/dmx/shared/dmxUtil' import GroupPicker from 'features/ui/react/base/GroupPicker' diff --git a/src/features/dmx/react/FixtureChannelItem.tsx b/src/features/dmx/react/FixtureChannelItem.tsx index 63b5ad5a..14645dcb 100644 --- a/src/features/dmx/react/FixtureChannelItem.tsx +++ b/src/features/dmx/react/FixtureChannelItem.tsx @@ -6,7 +6,7 @@ import FixtureChannelPopup from 'features/dmx/react/FixtureChannelPopup' import Popup from '../../ui/react/base/Popup' import RemoveIcon from '@mui/icons-material/Remove' import { FixtureChannel } from '../shared/dmxFixtures' -import { removeFixtureChannel } from '../redux/dmxSlice' +import { removeFixtureChannel } from '../../fixtures/redux/fixturesSlice' import { getCustomColorChannelName } from '../shared/dmxColors' import { ChannelToggle } from './Subfixtures' diff --git a/src/features/dmx/react/FixtureChannelPopup.tsx b/src/features/dmx/react/FixtureChannelPopup.tsx index d78e2c63..24da44cd 100644 --- a/src/features/dmx/react/FixtureChannelPopup.tsx +++ b/src/features/dmx/react/FixtureChannelPopup.tsx @@ -12,7 +12,7 @@ import { } from '../shared/dmxFixtures' import NumberField from '../../ui/react/base/NumberField' import Input from '../../ui/react/base/Input' -import { editFixtureChannel } from '../redux/dmxSlice' +import { editFixtureChannel } from '../../fixtures/redux/fixturesSlice' import Checkbox from '../../ui/react/base/LabelledCheckbox' import HSpad, { ColorChannelProps } from 'features/ui/react/base/HSpad' import { FixtureChannelItemProps } from './FixtureChannelItem' diff --git a/src/features/dmx/react/FixtureChannels.tsx b/src/features/dmx/react/FixtureChannels.tsx index a1b3a81f..adf0b0eb 100644 --- a/src/features/dmx/react/FixtureChannels.tsx +++ b/src/features/dmx/react/FixtureChannels.tsx @@ -4,7 +4,7 @@ import { useDmxSelector } from '../../../renderer/redux/store' import { useDispatch } from 'react-redux' import { indexArray } from '../../utils/util' import { initFixtureChannel } from '../shared/dmxFixtures' -import { addFixtureChannel } from '../redux/dmxSlice' +import { addFixtureChannel } from '../../fixtures/redux/fixturesSlice' import { IconButton } from '@mui/material' import AddIcon from '@mui/icons-material/Add' import FixtureChannelItem from 'features/dmx/react/FixtureChannelItem' diff --git a/src/features/dmx/react/FixtureCursor.tsx b/src/features/dmx/react/FixtureCursor.tsx index 5a575f11..b45d673b 100644 --- a/src/features/dmx/react/FixtureCursor.tsx +++ b/src/features/dmx/react/FixtureCursor.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useDispatch } from 'react-redux' import { useDmxSelector } from '../../../renderer/redux/store' import Cursor from '../../ui/react/base/Cursor' -import { setSelectedFixture } from '../redux/dmxSlice' +import { setSelectedFixture } from '../../fixtures/redux/fixturesSlice' import Window2D2 from '../../ui/react/base/Window2D2' export default function FixtureCursor({ index }: { index: number }) { diff --git a/src/features/dmx/react/FixturePlacement.tsx b/src/features/dmx/react/FixturePlacement.tsx index e1c75eff..dba833af 100644 --- a/src/features/dmx/react/FixturePlacement.tsx +++ b/src/features/dmx/react/FixturePlacement.tsx @@ -3,7 +3,7 @@ import { useDmxSelector } from '../../../renderer/redux/store' import FixtureCursor from './FixtureCursor' import useDragMapped from '../../ui/react/hooks/useDragMapped' import { useDispatch } from 'react-redux' -import { setFixtureWindow, incrementFixtureWindow } from '../redux/dmxSlice' +import { setFixtureWindow, incrementFixtureWindow } from '../../fixtures/redux/fixturesSlice' import { secondaryEnabled } from 'features/ui/react/base/keyUtil' export default function FixturePlacement() { diff --git a/src/features/dmx/react/MyFixture.tsx b/src/features/dmx/react/MyFixture.tsx index 4b1d9985..1fd71935 100644 --- a/src/features/dmx/react/MyFixture.tsx +++ b/src/features/dmx/react/MyFixture.tsx @@ -6,7 +6,7 @@ import { setEditedFixture, updateFixtureType, deleteFixtureType, -} from '../redux/dmxSlice' +} from '../../fixtures/redux/fixturesSlice' import Input from '../../ui/react/base/Input' import Slider from '@mui/material/Slider' import styled from 'styled-components' diff --git a/src/features/dmx/react/MyFixtures.tsx b/src/features/dmx/react/MyFixtures.tsx index c537d28c..0949e640 100644 --- a/src/features/dmx/react/MyFixtures.tsx +++ b/src/features/dmx/react/MyFixtures.tsx @@ -2,7 +2,7 @@ import { useDmxSelector } from '../../../renderer/redux/store' import MyFixture from './MyFixture' import AddIcon from '@mui/icons-material/Add' import { IconButton } from '@mui/material' -import { addFixtureType } from '../redux/dmxSlice' +import { addFixtureType } from '../../fixtures/redux/fixturesSlice' import { useDispatch } from 'react-redux' import { initFixtureType } from 'features/dmx/shared/dmxFixtures' import styled from 'styled-components' diff --git a/src/features/dmx/react/Subfixtures.tsx b/src/features/dmx/react/Subfixtures.tsx index c3081f84..60066c3c 100644 --- a/src/features/dmx/react/Subfixtures.tsx +++ b/src/features/dmx/react/Subfixtures.tsx @@ -10,7 +10,7 @@ import { removeSubFixture, replaceActiveFixtureTypeSubFixture, setActiveSubFixture, -} from '../redux/dmxSlice' +} from '../../fixtures/redux/fixturesSlice' import { SubFixture } from '../shared/dmxFixtures' import RemoveIcon from '@mui/icons-material/Remove' import wrapClick from '../../ui/react/base/wrapClick' diff --git a/src/features/dmx/react/UniverseSlot.tsx b/src/features/dmx/react/UniverseSlot.tsx index eb461fae..312254c2 100644 --- a/src/features/dmx/react/UniverseSlot.tsx +++ b/src/features/dmx/react/UniverseSlot.tsx @@ -8,7 +8,7 @@ import { setFixtureWindowEnabled, addFixture, removeFixture, -} from '../redux/dmxSlice' +} from '../../fixtures/redux/fixturesSlice' import ToggleButton from '../../ui/react/base/ToggleButton' import Popup from '../../ui/react/base/Popup' import { useState } from 'react' diff --git a/src/features/fileSaving/shared/save.ts b/src/features/fileSaving/shared/save.ts index c6f04c64..d271be01 100644 --- a/src/features/fileSaving/shared/save.ts +++ b/src/features/fileSaving/shared/save.ts @@ -1,5 +1,5 @@ import { LightScenes_t, VisualScenes_t } from 'features/scenes/shared/Scenes' -import { DmxState } from 'features/dmx/redux/dmxSlice' +import { DmxState } from 'features/fixtures/redux/fixturesSlice' import { DeviceState } from 'features/midi/redux' import { fixLightScenes, diff --git a/src/features/dmx/redux/dmxSlice.ts b/src/features/fixtures/redux/fixturesSlice.ts similarity index 99% rename from src/features/dmx/redux/dmxSlice.ts rename to src/features/fixtures/redux/fixturesSlice.ts index 21d8737f..0430fa23 100644 --- a/src/features/dmx/redux/dmxSlice.ts +++ b/src/features/fixtures/redux/fixturesSlice.ts @@ -9,7 +9,7 @@ import { SubFixture, } from 'features/dmx/shared/dmxFixtures' import { clampNormalized } from '../../utils/math/util' -import { defaultParamsList } from '../shared/params' +import { defaultParamsList } from '../../dmx/shared/params' import { initLedState, LedState } from '../../led/redux/ledState' import { initLedFixture, LedFixture } from '../../led/shared/ledFixtures' import { Point } from '../../utils/math/point' diff --git a/src/features/led/react/LedFixtureDefinition.tsx b/src/features/led/react/LedFixtureDefinition.tsx index 1666ed53..b34ce540 100644 --- a/src/features/led/react/LedFixtureDefinition.tsx +++ b/src/features/led/react/LedFixtureDefinition.tsx @@ -4,7 +4,7 @@ import { removeLedFixture, setActiveLedFixture, updateActiveLedFixture, -} from 'features/dmx/redux/dmxSlice' +} from 'features/fixtures/redux/fixturesSlice' import { useDmxSelector } from 'renderer/redux/store' import { LedFixture, MAX_LED_COUNT } from 'features/led/shared/ledFixtures' import styled from 'styled-components' diff --git a/src/features/led/react/LedFixtureList.tsx b/src/features/led/react/LedFixtureList.tsx index 6cd46e9c..7a6bd79f 100644 --- a/src/features/led/react/LedFixtureList.tsx +++ b/src/features/led/react/LedFixtureList.tsx @@ -5,7 +5,7 @@ import LedFixtureDefinition from './LedFixtureDefinition' import AddIcon from '@mui/icons-material/Add' import { IconButton } from '@mui/material' import { useDispatch } from 'react-redux' -import { addLedFixture } from 'features/dmx/redux/dmxSlice' +import { addLedFixture } from 'features/fixtures/redux/fixturesSlice' interface Props {} diff --git a/src/features/led/react/LedFixturePlacement.tsx b/src/features/led/react/LedFixturePlacement.tsx index 9ac9252f..9ac8b7fb 100644 --- a/src/features/led/react/LedFixturePlacement.tsx +++ b/src/features/led/react/LedFixturePlacement.tsx @@ -7,7 +7,7 @@ import { addLedFixturePoint, removeLedFixturePoint, updateLedFixturePoint, -} from 'features/dmx/redux/dmxSlice' +} from 'features/fixtures/redux/fixturesSlice' import { secondaryEnabled } from 'features/ui/react/base/keyUtil' import { fMap, indexArray } from 'features/utils/util' import LedFixturePoints from './LedFixturePoints' diff --git a/src/features/modulation/react/ModulationMatrix.tsx b/src/features/modulation/react/ModulationMatrix.tsx index 5eee4086..56eee426 100644 --- a/src/features/modulation/react/ModulationMatrix.tsx +++ b/src/features/modulation/react/ModulationMatrix.tsx @@ -6,7 +6,7 @@ import { import styled from 'styled-components' import { indexArray } from 'features/utils/util' import ModulationSlider, { AddModulationButton } from './ModulationSlider' -import { getAllParamKeys } from 'features/dmx/redux/dmxSlice' +import { getAllParamKeys } from 'features/fixtures/redux/fixturesSlice' export default function ModulationMatrix({ index }: { index: number }) { const numSplits = useActiveLightScene((scene) => scene.splitScenes.length) diff --git a/src/features/modulation/react/ModulationSlider.tsx b/src/features/modulation/react/ModulationSlider.tsx index 2fc307b9..80fb44bb 100644 --- a/src/features/modulation/react/ModulationSlider.tsx +++ b/src/features/modulation/react/ModulationSlider.tsx @@ -12,7 +12,7 @@ import useDragMapped from '../../ui/react/hooks/useDragMapped' import styled from 'styled-components' import Popup from 'features/ui/react/base/Popup' import { indexArray } from 'features/utils/util' -import { getAllParamKeys } from 'features/dmx/redux/dmxSlice' +import { getAllParamKeys } from 'features/fixtures/redux/fixturesSlice' interface Props { splitIndex: number diff --git a/src/features/scenes/react/controls/ParamAddButton.tsx b/src/features/scenes/react/controls/ParamAddButton.tsx index 641280bf..b1145664 100644 --- a/src/features/scenes/react/controls/ParamAddButton.tsx +++ b/src/features/scenes/react/controls/ParamAddButton.tsx @@ -13,7 +13,7 @@ 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 'features/dmx/redux/dmxSlice' +import { getCustomChannels } from 'features/fixtures/redux/fixturesSlice' interface Props { splitIndex: number diff --git a/src/features/scenes/react/controls/ParamsControl.tsx b/src/features/scenes/react/controls/ParamsControl.tsx index 6467dc98..8afbdce2 100644 --- a/src/features/scenes/react/controls/ParamsControl.tsx +++ b/src/features/scenes/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 'features/dmx/redux/dmxSlice' +import { getCustomChannels } from 'features/fixtures/redux/fixturesSlice' interface Params { splitIndex: number diff --git a/src/features/shared/redux/fixState.ts b/src/features/shared/redux/fixState.ts index 4b696972..fd8641a4 100644 --- a/src/features/shared/redux/fixState.ts +++ b/src/features/shared/redux/fixState.ts @@ -4,7 +4,7 @@ src/features/fileSaving/shared/save.ts src/features/fileSaving/react/autosave.ts */ import { DeviceState } from 'features/midi/redux' -import { DmxState } from 'features/dmx/redux/dmxSlice' +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' diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index 07c4d6a5..775d10ac 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -26,7 +26,7 @@ import { } from '../../features/dmx/shared/dmxUtil' import { ThrottleMap } from 'features/midi/engine/midiConnection' import { MidiMessage, midiInputID } from 'features/midi/shared/midi' -import { getAllParamKeys } from '../../features/dmx/redux/dmxSlice' +import { getAllParamKeys } from '../../features/fixtures/redux/fixturesSlice' import { indexArray } from '../../features/utils/util' import WledManager from '../../features/led/engine/wled_manager' import { diff --git a/src/renderer/redux/initState.ts b/src/renderer/redux/initState.ts index 712ad8a8..e33ceeb5 100644 --- a/src/renderer/redux/initState.ts +++ b/src/renderer/redux/initState.ts @@ -1,4 +1,4 @@ -import { initDmxState } from 'features/dmx/redux/dmxSlice' +import { initDmxState } from 'features/fixtures/redux/fixturesSlice' import { initGuiState } from 'renderer/redux/guiSlice' import { initControlState } from 'renderer/redux/controlSlice' import { initMixerState } from 'renderer/redux/mixerSlice' diff --git a/src/renderer/redux/store.ts b/src/renderer/redux/store.ts index 25ed5ca9..c1e34e6f 100644 --- a/src/renderer/redux/store.ts +++ b/src/renderer/redux/store.ts @@ -5,7 +5,7 @@ import { PayloadAction, } from '@reduxjs/toolkit' import { useSelector, TypedUseSelectorHook } from 'react-redux' -import dmxReducer, { DmxState } from '../../features/dmx/redux/dmxSlice' +import dmxReducer, { DmxState } from '../../features/fixtures/redux/fixturesSlice' import guiReducer from './guiSlice' import controlReducer, { ControlState } from './controlSlice' import { LightScene_t } from '../../features/scenes/shared/Scenes' From 238c353d0f37b50d5a015a00db89d5e11389d806 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 17:59:10 -0400 Subject: [PATCH 72/98] refactor: cleanup fixture configuration react --- .../channels/editor}/ColorMapChannel.tsx | 8 ++++---- .../channels/editor/index.tsx} | 16 ++++++++-------- .../channels/list}/FixtureChannelItem.tsx | 14 +++++++------- .../channels/list/index.tsx} | 10 +++++----- .../dmx/react/{ => fixtures/list}/EditGroups.tsx | 2 +- .../list/FixtureItem.tsx} | 8 ++++---- .../react/{ => fixtures/list}/Subfixtures.tsx | 12 ++++++------ .../{MyFixtures.tsx => fixtures/list/index.tsx} | 6 +++--- .../{ => fixtures/universe}/FixtureCursor.tsx | 8 ++++---- .../{ => fixtures/universe}/FixturePlacement.tsx | 6 +++--- .../{ => fixtures/universe}/UniverseSlot.tsx | 14 +++++++------- .../universe/index.tsx} | 4 ++-- src/renderer/pages/universe/page.tsx | 4 ++-- 13 files changed, 56 insertions(+), 56 deletions(-) rename src/features/dmx/react/{ => fixtures/channels/editor}/ColorMapChannel.tsx (94%) rename src/features/dmx/react/{FixtureChannelPopup.tsx => fixtures/channels/editor/index.tsx} (90%) rename src/features/dmx/react/{ => fixtures/channels/list}/FixtureChannelItem.tsx (87%) rename src/features/dmx/react/{FixtureChannels.tsx => fixtures/channels/list/index.tsx} (83%) rename src/features/dmx/react/{ => fixtures/list}/EditGroups.tsx (95%) rename src/features/dmx/react/{MyFixture.tsx => fixtures/list/FixtureItem.tsx} (95%) rename src/features/dmx/react/{ => fixtures/list}/Subfixtures.tsx (94%) rename src/features/dmx/react/{MyFixtures.tsx => fixtures/list/index.tsx} (88%) rename src/features/dmx/react/{ => fixtures/universe}/FixtureCursor.tsx (80%) rename src/features/dmx/react/{ => fixtures/universe}/FixturePlacement.tsx (91%) rename src/features/dmx/react/{ => fixtures/universe}/UniverseSlot.tsx (93%) rename src/features/dmx/react/{MyUniverse.tsx => fixtures/universe/index.tsx} (94%) diff --git a/src/features/dmx/react/ColorMapChannel.tsx b/src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx similarity index 94% rename from src/features/dmx/react/ColorMapChannel.tsx rename to src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx index 7b363c68..86fd520b 100644 --- a/src/features/dmx/react/ColorMapChannel.tsx +++ b/src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx @@ -1,17 +1,17 @@ import { useDispatch } from 'react-redux' import styled from 'styled-components' -import { DMX_MAX_VALUE } from '../shared/dmxFixtures' -import NumberField from '../../ui/react/base/NumberField' +import { DMX_MAX_VALUE } from '../../../../shared/dmxFixtures' +import NumberField from '../../../../../ui/react/base/NumberField' import { addColorMapColor, setColorMapColor, removeColorMapColor, -} from '../../fixtures/redux/fixturesSlice' +} 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 'features/ui/react/base/HSpad' -import { ChannelColorMap } from '../shared/dmxFixtures' +import { ChannelColorMap } from '../../../../shared/dmxFixtures' import { useState } from 'react' import wrapClick from 'features/ui/react/base/wrapClick' import { lerp } from 'features/utils/math/util' diff --git a/src/features/dmx/react/FixtureChannelPopup.tsx b/src/features/dmx/react/fixtures/channels/editor/index.tsx similarity index 90% rename from src/features/dmx/react/FixtureChannelPopup.tsx rename to src/features/dmx/react/fixtures/channels/editor/index.tsx index 24da44cd..749c518c 100644 --- a/src/features/dmx/react/FixtureChannelPopup.tsx +++ b/src/features/dmx/react/fixtures/channels/editor/index.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' import { useDispatch } from 'react-redux' -import Select from '../../ui/react/base/Select' +import Select from '../../../../../ui/react/base/Select' import { FixtureChannel, channelTypes, @@ -9,14 +9,14 @@ import { axisDirList, DMX_MAX_VALUE, DMX_MIN_VALUE, -} from '../shared/dmxFixtures' -import NumberField from '../../ui/react/base/NumberField' -import Input from '../../ui/react/base/Input' -import { editFixtureChannel } from '../../fixtures/redux/fixturesSlice' -import Checkbox from '../../ui/react/base/LabelledCheckbox' +} from '../../../../shared/dmxFixtures' +import NumberField from '../../../../../ui/react/base/NumberField' +import Input from '../../../../../ui/react/base/Input' +import { editFixtureChannel } from '../../../../../fixtures/redux/fixturesSlice' +import Checkbox from '../../../../../ui/react/base/LabelledCheckbox' import HSpad, { ColorChannelProps } from 'features/ui/react/base/HSpad' -import { FixtureChannelItemProps } from './FixtureChannelItem' -import ColorMapChannel from 'features/dmx/react/ColorMapChannel' +import { FixtureChannelItemProps } from '../list/FixtureChannelItem' +import ColorMapChannel from './ColorMapChannel' import ColorPicker from 'features/ui/react/base/ColorPicker' interface Props extends FixtureChannelItemProps { diff --git a/src/features/dmx/react/FixtureChannelItem.tsx b/src/features/dmx/react/fixtures/channels/list/FixtureChannelItem.tsx similarity index 87% rename from src/features/dmx/react/FixtureChannelItem.tsx rename to src/features/dmx/react/fixtures/channels/list/FixtureChannelItem.tsx index 14645dcb..f20a621e 100644 --- a/src/features/dmx/react/FixtureChannelItem.tsx +++ b/src/features/dmx/react/fixtures/channels/list/FixtureChannelItem.tsx @@ -1,14 +1,14 @@ import styled from 'styled-components' -import { useDmxSelector } from '../../../renderer/redux/store' +import { useDmxSelector } from '../../../../../../renderer/redux/store' import { useDispatch } from 'react-redux' import { IconButton } from '@mui/material' -import FixtureChannelPopup from 'features/dmx/react/FixtureChannelPopup' -import Popup from '../../ui/react/base/Popup' +import FixtureChannelPopup from 'features/dmx/react/fixtures/channels/editor' +import Popup from '../../../../../ui/react/base/Popup' import RemoveIcon from '@mui/icons-material/Remove' -import { FixtureChannel } from '../shared/dmxFixtures' -import { removeFixtureChannel } from '../../fixtures/redux/fixturesSlice' -import { getCustomColorChannelName } from '../shared/dmxColors' -import { ChannelToggle } from './Subfixtures' +import { FixtureChannel } from '../../../../shared/dmxFixtures' +import { removeFixtureChannel } from '../../../../../fixtures/redux/fixturesSlice' +import { getCustomColorChannelName } from '../../../../shared/dmxColors' +import { ChannelToggle } from '../../list/Subfixtures' export interface FixtureChannelItemProps { fixtureID: string diff --git a/src/features/dmx/react/FixtureChannels.tsx b/src/features/dmx/react/fixtures/channels/list/index.tsx similarity index 83% rename from src/features/dmx/react/FixtureChannels.tsx rename to src/features/dmx/react/fixtures/channels/list/index.tsx index adf0b0eb..b48d9355 100644 --- a/src/features/dmx/react/FixtureChannels.tsx +++ b/src/features/dmx/react/fixtures/channels/list/index.tsx @@ -1,13 +1,13 @@ import { useState } from 'react' import styled from 'styled-components' -import { useDmxSelector } from '../../../renderer/redux/store' +import { useDmxSelector } from '../../../../../../renderer/redux/store' import { useDispatch } from 'react-redux' -import { indexArray } from '../../utils/util' -import { initFixtureChannel } from '../shared/dmxFixtures' -import { addFixtureChannel } from '../../fixtures/redux/fixturesSlice' +import { indexArray } from '../../../../../utils/util' +import { initFixtureChannel } from '../../../../shared/dmxFixtures' +import { addFixtureChannel } from '../../../../../fixtures/redux/fixturesSlice' import { IconButton } from '@mui/material' import AddIcon from '@mui/icons-material/Add' -import FixtureChannelItem from 'features/dmx/react/FixtureChannelItem' +import FixtureChannelItem from 'features/dmx/react/fixtures/channels/list/FixtureChannelItem' interface Props { fixtureID: string diff --git a/src/features/dmx/react/EditGroups.tsx b/src/features/dmx/react/fixtures/list/EditGroups.tsx similarity index 95% rename from src/features/dmx/react/EditGroups.tsx rename to src/features/dmx/react/fixtures/list/EditGroups.tsx index 738ea19b..998a0d99 100644 --- a/src/features/dmx/react/EditGroups.tsx +++ b/src/features/dmx/react/fixtures/list/EditGroups.tsx @@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux' import { addActiveFixtureTypeGroup, removeActiveFixtureTypeGroup, -} from '../../fixtures/redux/fixturesSlice' +} from '../../../../fixtures/redux/fixturesSlice' import { getSortedGroups, getSortedGroupsForFixtureType } from 'features/dmx/shared/dmxUtil' import GroupPicker from 'features/ui/react/base/GroupPicker' diff --git a/src/features/dmx/react/MyFixture.tsx b/src/features/dmx/react/fixtures/list/FixtureItem.tsx similarity index 95% rename from src/features/dmx/react/MyFixture.tsx rename to src/features/dmx/react/fixtures/list/FixtureItem.tsx index 1fd71935..374960e2 100644 --- a/src/features/dmx/react/MyFixture.tsx +++ b/src/features/dmx/react/fixtures/list/FixtureItem.tsx @@ -1,16 +1,16 @@ import React from 'react' import { IconButton } from '@mui/material' -import { useDmxSelector } from '../../../renderer/redux/store' +import { useDmxSelector } from '../../../../../renderer/redux/store' import { useDispatch } from 'react-redux' import { setEditedFixture, updateFixtureType, deleteFixtureType, -} from '../../fixtures/redux/fixturesSlice' -import Input from '../../ui/react/base/Input' +} from '../../../../fixtures/redux/fixturesSlice' +import Input from '../../../../ui/react/base/Input' import Slider from '@mui/material/Slider' import styled from 'styled-components' -import FixtureChannels from 'features/dmx/react/FixtureChannels' +import FixtureChannels from 'features/dmx/react/fixtures/channels/list' import { Button } from '@mui/material' import ExpandLessIcon from '@mui/icons-material/ExpandLess' import EditGroups from './EditGroups' diff --git a/src/features/dmx/react/Subfixtures.tsx b/src/features/dmx/react/fixtures/list/Subfixtures.tsx similarity index 94% rename from src/features/dmx/react/Subfixtures.tsx rename to src/features/dmx/react/fixtures/list/Subfixtures.tsx index 60066c3c..2c690150 100644 --- a/src/features/dmx/react/Subfixtures.tsx +++ b/src/features/dmx/react/fixtures/list/Subfixtures.tsx @@ -1,4 +1,4 @@ -import { useActiveFixtureType, useDmxSelector } from '../../../renderer/redux/store' +import { useActiveFixtureType, useDmxSelector } from '../../../../../renderer/redux/store' import styled from 'styled-components' import { IconButton } from '@mui/material' import AddIcon from '@mui/icons-material/Add' @@ -10,12 +10,12 @@ import { removeSubFixture, replaceActiveFixtureTypeSubFixture, setActiveSubFixture, -} from '../../fixtures/redux/fixturesSlice' -import { SubFixture } from '../shared/dmxFixtures' +} from '../../../../fixtures/redux/fixturesSlice' +import { SubFixture } from '../../../shared/dmxFixtures' import RemoveIcon from '@mui/icons-material/Remove' -import wrapClick from '../../ui/react/base/wrapClick' -import { hsvaForCss, separateHue } from '../../utils/baseColors' -import Input from '../../ui/react/base/Input' +import wrapClick from '../../../../ui/react/base/wrapClick' +import { hsvaForCss, separateHue } from '../../../../utils/baseColors' +import Input from '../../../../ui/react/base/Input' import Slider from 'features/ui/react/base/Slider' import XyPad from 'features/ui/react/base/XyPad' import GroupPicker from 'features/ui/react/base/GroupPicker' diff --git a/src/features/dmx/react/MyFixtures.tsx b/src/features/dmx/react/fixtures/list/index.tsx similarity index 88% rename from src/features/dmx/react/MyFixtures.tsx rename to src/features/dmx/react/fixtures/list/index.tsx index 0949e640..ed115f8f 100644 --- a/src/features/dmx/react/MyFixtures.tsx +++ b/src/features/dmx/react/fixtures/list/index.tsx @@ -1,8 +1,8 @@ -import { useDmxSelector } from '../../../renderer/redux/store' -import MyFixture from './MyFixture' +import { useDmxSelector } from '../../../../../renderer/redux/store' +import MyFixture from './FixtureItem' import AddIcon from '@mui/icons-material/Add' import { IconButton } from '@mui/material' -import { addFixtureType } from '../../fixtures/redux/fixturesSlice' +import { addFixtureType } from '../../../../fixtures/redux/fixturesSlice' import { useDispatch } from 'react-redux' import { initFixtureType } from 'features/dmx/shared/dmxFixtures' import styled from 'styled-components' diff --git a/src/features/dmx/react/FixtureCursor.tsx b/src/features/dmx/react/fixtures/universe/FixtureCursor.tsx similarity index 80% rename from src/features/dmx/react/FixtureCursor.tsx rename to src/features/dmx/react/fixtures/universe/FixtureCursor.tsx index b45d673b..170c6883 100644 --- a/src/features/dmx/react/FixtureCursor.tsx +++ b/src/features/dmx/react/fixtures/universe/FixtureCursor.tsx @@ -1,9 +1,9 @@ import React from 'react' import { useDispatch } from 'react-redux' -import { useDmxSelector } from '../../../renderer/redux/store' -import Cursor from '../../ui/react/base/Cursor' -import { setSelectedFixture } from '../../fixtures/redux/fixturesSlice' -import Window2D2 from '../../ui/react/base/Window2D2' +import { useDmxSelector } from '../../../../../renderer/redux/store' +import Cursor from '../../../../ui/react/base/Cursor' +import { setSelectedFixture } from '../../../../fixtures/redux/fixturesSlice' +import Window2D2 from '../../../../ui/react/base/Window2D2' export default function FixtureCursor({ index }: { index: number }) { const fixture = useDmxSelector((state) => state.universe[index]) diff --git a/src/features/dmx/react/FixturePlacement.tsx b/src/features/dmx/react/fixtures/universe/FixturePlacement.tsx similarity index 91% rename from src/features/dmx/react/FixturePlacement.tsx rename to src/features/dmx/react/fixtures/universe/FixturePlacement.tsx index dba833af..c19ed487 100644 --- a/src/features/dmx/react/FixturePlacement.tsx +++ b/src/features/dmx/react/fixtures/universe/FixturePlacement.tsx @@ -1,9 +1,9 @@ import styled from 'styled-components' -import { useDmxSelector } from '../../../renderer/redux/store' +import { useDmxSelector } from '../../../../../renderer/redux/store' import FixtureCursor from './FixtureCursor' -import useDragMapped from '../../ui/react/hooks/useDragMapped' +import useDragMapped from '../../../../ui/react/hooks/useDragMapped' import { useDispatch } from 'react-redux' -import { setFixtureWindow, incrementFixtureWindow } from '../../fixtures/redux/fixturesSlice' +import { setFixtureWindow, incrementFixtureWindow } from '../../../../fixtures/redux/fixturesSlice' import { secondaryEnabled } from 'features/ui/react/base/keyUtil' export default function FixturePlacement() { diff --git a/src/features/dmx/react/UniverseSlot.tsx b/src/features/dmx/react/fixtures/universe/UniverseSlot.tsx similarity index 93% rename from src/features/dmx/react/UniverseSlot.tsx rename to src/features/dmx/react/fixtures/universe/UniverseSlot.tsx index 312254c2..7bfffd70 100644 --- a/src/features/dmx/react/UniverseSlot.tsx +++ b/src/features/dmx/react/fixtures/universe/UniverseSlot.tsx @@ -1,21 +1,21 @@ import styled from 'styled-components' -import { useDmxSelector } from '../../../renderer/redux/store' -import { Fixture, FixtureType } from '../shared/dmxFixtures' -import { Slot_t } from './MyUniverse' +import { useDmxSelector } from '../../../../../renderer/redux/store' +import { Fixture, FixtureType } from '../../../shared/dmxFixtures' +import { Slot_t } from '.' import { useDispatch } from 'react-redux' import { setSelectedFixture, setFixtureWindowEnabled, addFixture, removeFixture, -} from '../../fixtures/redux/fixturesSlice' -import ToggleButton from '../../ui/react/base/ToggleButton' -import Popup from '../../ui/react/base/Popup' +} from '../../../../fixtures/redux/fixturesSlice' +import ToggleButton from '../../../../ui/react/base/ToggleButton' +import Popup from '../../../../ui/react/base/Popup' import { useState } from 'react' import { TextField, IconButton } from '@mui/material' import RemoveIcon from '@mui/icons-material/Remove' import AddIcon from '@mui/icons-material/Add' -import { clamp } from '../../utils/math/util' +import { clamp } from '../../../../utils/math/util' function ChannelSpan({ start, count }: { start: number; count: number }) { const end = start + count - 1 diff --git a/src/features/dmx/react/MyUniverse.tsx b/src/features/dmx/react/fixtures/universe/index.tsx similarity index 94% rename from src/features/dmx/react/MyUniverse.tsx rename to src/features/dmx/react/fixtures/universe/index.tsx index f91bbe0f..031a0933 100644 --- a/src/features/dmx/react/MyUniverse.tsx +++ b/src/features/dmx/react/fixtures/universe/index.tsx @@ -1,5 +1,5 @@ -import { Universe, Fixture } from '../shared/dmxFixtures' -import { useDmxSelector } from '../../../renderer/redux/store' +import { Universe, Fixture } from '../../../shared/dmxFixtures' +import { useDmxSelector } from '../../../../../renderer/redux/store' import FixturePlacement from './FixturePlacement' import UniverseSlot from './UniverseSlot' import styled from 'styled-components' diff --git a/src/renderer/pages/universe/page.tsx b/src/renderer/pages/universe/page.tsx index beaf6208..c46e9504 100644 --- a/src/renderer/pages/universe/page.tsx +++ b/src/renderer/pages/universe/page.tsx @@ -1,7 +1,7 @@ import SplitPane from '../../../features/ui/react/base/SplitPane' import styled from 'styled-components' -import MyFixtures from '../../../features/dmx/react/MyFixtures' -import MyUniverse from '../../../features/dmx/react/MyUniverse' +import MyFixtures from '../../../features/dmx/react/fixtures/list' +import MyUniverse from '../../../features/dmx/react/fixtures/universe' import StatusBar from '../../../features/menu/react/StatusBar' export default function Universe() { From 0ec36efa02fb4420fd300fa8d8a673977fbec6f5 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 18:34:36 -0400 Subject: [PATCH 73/98] refactor: easier fixture api --- .../channels/editor/ColorMapChannel.tsx | 6 +- src/features/dmx/shared/dmxFixtures.ts | 76 +++++-------------- src/features/dmx/shared/dmxUtil.ts | 5 +- src/features/midi/shared/actions.ts | 7 +- src/features/shared/shared/type-utils.ts | 5 ++ 5 files changed, 33 insertions(+), 66 deletions(-) create mode 100644 src/features/shared/shared/type-utils.ts diff --git a/src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx b/src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx index 86fd520b..3d821935 100644 --- a/src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx +++ b/src/features/dmx/react/fixtures/channels/editor/ColorMapChannel.tsx @@ -1,6 +1,6 @@ import { useDispatch } from 'react-redux' import styled from 'styled-components' -import { DMX_MAX_VALUE } from '../../../../shared/dmxFixtures' +import { DMX_MAX_VALUE, GetFixturePayload } from '../../../../shared/dmxFixtures' import NumberField from '../../../../../ui/react/base/NumberField' import { addColorMapColor, @@ -11,14 +11,14 @@ import { IconButton } from '@mui/material' import Add from '@mui/icons-material/Add' import Remove from '@mui/icons-material/Remove' import HSpad, { ColorChannelProps } from 'features/ui/react/base/HSpad' -import { ChannelColorMap } from '../../../../shared/dmxFixtures' + import { useState } from 'react' 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/shared/dmxFixtures.ts b/src/features/dmx/shared/dmxFixtures.ts index 94fcded1..9c79299b 100644 --- a/src/features/dmx/shared/dmxFixtures.ts +++ b/src/features/dmx/shared/dmxFixtures.ts @@ -1,3 +1,4 @@ +import { Pretty } from 'features/shared/shared/type-utils' import { Window2D_t } from '../../shared/shared/window' import { ColorChannel } from './dmxColors' import { nanoid } from 'nanoid' @@ -13,70 +14,33 @@ export type DmxValue = number // 0 - 255 export type AxisDir = 'x' | 'y' export const axisDirList = ['x', 'y'] -type ChannelMaster = { - type: 'master' - min: DmxValue - max: DmxValue - isOnOff: boolean -} - -type ChannelColor = { - type: 'color' - color: ColorChannel -} - -type ChannelStrobe = { - type: 'strobe' - default_strobe: DmxValue - default_solid: DmxValue -} - -type ChannelOther = { - type: 'other' - default: DmxValue -} - -export type ChannelAxis = { - type: 'axis' - dir: AxisDir - isFine: boolean - min: DmxValue - max: DmxValue -} - export type ColorMapColor = { max: number; hue: number; saturation: number } -export type ChannelColorMap = { - type: 'colorMap' - colors: ColorMapColor[] +export type FixtureApi = { + custom: { name: string; default: DmxValue; min: DmxValue; max: DmxValue } + reset: { resetVal: DmxValue } + other: { default: DmxValue } + colorMap: { colors: ColorMapColor[] } + axis: { dir: AxisDir; isFine: boolean; min: DmxValue; max: DmxValue } + strobe: { default_strobe: DmxValue; default_solid: DmxValue } + color: { color: ColorChannel } + master: { min: DmxValue; max: DmxValue; isOnOff: boolean } } -type ChannelReset = { - type: 'reset' - resetVal: DmxValue -} +export type ChannelType = keyof FixtureApi -export type ChannelCustom = { - type: 'custom' - name: string - default: DmxValue - min: DmxValue - max: DmxValue -} +type ConvertApiToPayload = { + [k in ChannelType]: Pretty +}[ChannelType] + +export type GetFixturePayload = Extract< + ConvertApiToPayload, + { type: Type } +> export const defaultCustomChannels = ['speed'] -export type FixtureChannel = - | ChannelMaster - | ChannelColor - | ChannelStrobe - | ChannelAxis - | ChannelColorMap - | ChannelOther - | ChannelReset - | ChannelCustom - -export type ChannelType = FixtureChannel['type'] +export type FixtureChannel = ConvertApiToPayload export const channelTypes: ChannelType[] = [ 'master', diff --git a/src/features/dmx/shared/dmxUtil.ts b/src/features/dmx/shared/dmxUtil.ts index ed7166c6..9b187fb8 100644 --- a/src/features/dmx/shared/dmxUtil.ts +++ b/src/features/dmx/shared/dmxUtil.ts @@ -6,11 +6,12 @@ import { Fixture, Universe, DMX_DEFAULT_VALUE, - ChannelAxis, + FixtureType, AxisDir, DMX_MIN_VALUE, FlattenedFixture, + GetFixturePayload, } from './dmxFixtures' import { getParam, Params } from './params' import { findClosest, lerp, Normalized } from '../../utils/math/util' @@ -238,7 +239,7 @@ export function getSortedGroups( } function calculate_axis_channel( - ch: ChannelAxis, + ch: GetFixturePayload<'axis'>, axis_param: Normalized | undefined, fixture_position: Normalized | undefined, mirror_param: Normalized | undefined, diff --git a/src/features/midi/shared/actions.ts b/src/features/midi/shared/actions.ts index a927026e..ed86b704 100644 --- a/src/features/midi/shared/actions.ts +++ b/src/features/midi/shared/actions.ts @@ -7,6 +7,7 @@ import type { PayloadAction, } from '@reduxjs/toolkit' import { ButtonFunction, SlidersFunction } from './config' +import { Pretty } from 'features/shared/shared/type-utils' /** * From T, pick a set of properties whose keys are in the union K @@ -65,11 +66,7 @@ type GetReduxPayload = T extends CaseReducer< : {} : never -type Pretty = T extends object - ? {} & { - [P in keyof T]: T[P] - } - : T + export type GetMidiAction = T extends UserCommand['type'] 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 From d01ebffaff730da4572aad194d8591209c27bfe1 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Tue, 4 Apr 2023 21:16:35 -0400 Subject: [PATCH 74/98] refactor: setup channel config --- src/features/dmx/channel.config.ts | 197 ++++++++++++++++++++++++ src/features/dmx/engine/channelUtils.ts | 94 +++++++++++ src/features/dmx/engine/dmxEngine.ts | 3 +- src/features/dmx/shared/dmxFixtures.ts | 55 +------ src/features/dmx/shared/dmxUtil.ts | 154 +----------------- src/main/engine/engine.ts | 3 +- 6 files changed, 304 insertions(+), 202 deletions(-) create mode 100644 src/features/dmx/channel.config.ts create mode 100644 src/features/dmx/engine/channelUtils.ts diff --git a/src/features/dmx/channel.config.ts b/src/features/dmx/channel.config.ts new file mode 100644 index 00000000..d21a96ea --- /dev/null +++ b/src/features/dmx/channel.config.ts @@ -0,0 +1,197 @@ +import { rLerp } from 'features/utils/math/range' +import { + 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 { Params, getParam } from './shared/params' +import { calculate_axis_channel, getBrightness } from './engine/channelUtils' +import { Window2D_t } from 'features/shared/shared/window' + +type GetContext = { + ch: GetFixturePayload + params: Params + fixture: FlattenedFixture + master: number + randomizerLevel: number + movingWindow: Window2D_t +} + +export type ChannelConfig = { + /** + * This happens on react side + */ + default: () => Omit, 'type'> + /** + * this happens on engine side + */ + getValueFromDevice?: (ctx: GetContext) => number +} + +const createChannelConfig = < + T extends { [k in ChannelType]: ChannelConfig } +>( + config: T +) => { + return Object.entries(config).reduce((prev, [key, config]) => { + // @ts-ignore figure out proper typing + prev[key as keyof T] = { + ...config, + default: (...args) => { + return { + ...config.default(...args), + type: key + } + }, + } + return prev + // @ts-ignore figure out proper typing + }, {} as { [k in keyof T]: Omit & { default: () => GetFixturePayload } }) +} + +/** + * 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 + */ +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', + default: DMX_MIN_VALUE, + 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) + } + }, + }, + reset: { + default: () => ({ resetVal: DMX_MAX_VALUE }), + }, + 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, + 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: () => ({ + 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 + }, + }, + other: { + default: () => ({ + default: DMX_MIN_VALUE, + }), + getValueFromDevice({ ch }) { + return ch.default + }, + }, + color: { + default: () => ({ + color: { + hue: 0, + saturation: 1.0, + }, + }), + 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..88bd3e33 --- /dev/null +++ b/src/features/dmx/engine/channelUtils.ts @@ -0,0 +1,94 @@ +import { Window2D_t } from '../../shared/shared/window' +import { + DmxValue, + DMX_MAX_VALUE, + FixtureChannel, + DMX_DEFAULT_VALUE, + AxisDir, + DMX_MIN_VALUE, + FlattenedFixture, + GetFixturePayload, + ChannelType, +} from '../shared/dmxFixtures' +import { getParam, Params } from '../shared/params' +import { Normalized } from '../../utils/math/util' +import { rLerp } from '../../utils/math/range' +import { applyRandomization } from '../../bpm/shared/randomizer' + +import { + applyMirror, + getMovingWindow, + getWindowMultiplier2D, +} from '../shared/dmxUtil' +import { ChannelConfig, channelConfig } from '../channel.config' + +export function getDmxValue( + ch: FixtureChannel, + params: Params, + 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 getBrightness( + params: Params, + 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 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/features/dmx/engine/dmxEngine.ts b/src/features/dmx/engine/dmxEngine.ts index e87df1e8..3d0e3c8c 100644 --- a/src/features/dmx/engine/dmxEngine.ts +++ b/src/features/dmx/engine/dmxEngine.ts @@ -8,13 +8,14 @@ import { Params } from '../shared/params' import { RandomizerState } from '../../bpm/shared/randomizer' import { CleanReduxState } from '../../../renderer/redux/store' import { - getDmxValue, + getFixturesInGroups, flatten_fixtures, } from '../shared/dmxUtil' import { indexArray, zip } from '../../utils/util' import { TimeState } from '../../bpm/shared/TimeState' import { SplitState } from 'renderer/redux/realtimeStore' +import { getDmxValue } from './channelUtils' export function calculateDmx( state: CleanReduxState, diff --git a/src/features/dmx/shared/dmxFixtures.ts b/src/features/dmx/shared/dmxFixtures.ts index 9c79299b..b0bdb462 100644 --- a/src/features/dmx/shared/dmxFixtures.ts +++ b/src/features/dmx/shared/dmxFixtures.ts @@ -2,6 +2,7 @@ import { Pretty } from 'features/shared/shared/type-utils' import { Window2D_t } from '../../shared/shared/window' import { ColorChannel } from './dmxColors' import { nanoid } from 'nanoid' +import { channelConfig } from '../channel.config' export const DMX_MIN_VALUE = 0 export const DMX_MAX_VALUE = 255 @@ -56,58 +57,8 @@ export const channelTypes: ChannelType[] = [ export function initFixtureChannel( type?: FixtureChannel['type'] ): FixtureChannel { - if (type === 'color') { - return { - type: type, - color: { - hue: 0, - saturation: 1.0, - }, - } - } else if (type === 'other') { - return { - type: type, - default: DMX_MIN_VALUE, - } - } else if (type === 'strobe') { - return { - type: type, - default_solid: DMX_MIN_VALUE, - default_strobe: DMX_MAX_VALUE, - } - } else if (type === 'axis') { - return { - type: type, - dir: 'x', - isFine: false, - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - } - } else if (type === 'colorMap') { - return { - type: type, - colors: [{ max: 0, hue: 0, saturation: 1.0 }], - } - } else if (type === 'reset') { - return { - type: 'reset', - resetVal: DMX_MAX_VALUE, - } - } else if (type === 'custom') { - return { - type: 'custom', - name: 'custom', - default: DMX_MIN_VALUE, - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - } - } - return { - type: 'master', - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - isOnOff: false, - } + if (!type) return channelConfig.master.default() + return channelConfig[type].default() } export type FixtureType = { diff --git a/src/features/dmx/shared/dmxUtil.ts b/src/features/dmx/shared/dmxUtil.ts index 9b187fb8..537e6e0e 100644 --- a/src/features/dmx/shared/dmxUtil.ts +++ b/src/features/dmx/shared/dmxUtil.ts @@ -1,23 +1,13 @@ import { Window, Window2D_t } from '../../shared/shared/window' import { - DmxValue, - DMX_MAX_VALUE, FixtureChannel, Fixture, Universe, - DMX_DEFAULT_VALUE, - FixtureType, - AxisDir, - DMX_MIN_VALUE, FlattenedFixture, - GetFixturePayload, } from './dmxFixtures' -import { getParam, Params } from './params' -import { findClosest, lerp, Normalized } from '../../utils/math/util' -import { rLerp } from '../../utils/math/range' -import { applyRandomization } from '../../bpm/shared/randomizer' -import { getColorChannelLevel } from './dmxColors' +import { Params } from './params' +import { lerp, Normalized } from '../../utils/math/util' export function getWindowMultiplier2D( fixtureWindow: Window2D_t, @@ -52,108 +42,11 @@ export function applyMirror( return (mirroredDoubleNorm + 1) / 2 } -export function getDmxValue( - ch: FixtureChannel, - params: Params, - fixture: FlattenedFixture, - master: number, - randomizerLevel: number -): DmxValue { - const movingWindow = getMovingWindow(params) - - switch (ch.type) { - case 'master': - 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) - } - case 'other': - return ch.default - case 'color': - const brightness = getBrightness( - params, - randomizerLevel, - fixture.window, - movingWindow - ) - return ( - getColorChannelLevel( - getParam(params, 'hue'), - getParam(params, 'saturation'), - brightness, - ch.color - ) * DMX_MAX_VALUE - ) - case 'strobe': - return params.strobe !== undefined && params.strobe > 0.5 - ? ch.default_strobe - : ch.default_solid - case 'axis': - 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 - ) - } - case 'colorMap': - 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 - } - case 'custom': - const customParam = params[ch.name] - if (customParam === undefined) { - return ch.default - } else { - return rLerp(ch, customParam) - } - default: - return DMX_DEFAULT_VALUE - } -} - -export function getBrightness( - params: Params, - randomizerLevel: Normalized, - fixtureWindow: Window2D_t, - movingWindow: Window2D_t -): Normalized { - const unrandomizedBrightness = - getParam(params, 'brightness') * - getWindowMultiplier2D(fixtureWindow, movingWindow) - return applyRandomization( - unrandomizedBrightness, - randomizerLevel, - getParam(params, 'randomize') - ) -} - +/** + * used on engine side on led and dmx + * @param params + * @returns + */ export function getMovingWindow(params: Params): Window2D_t { const x = params.x !== undefined && params.width !== undefined @@ -238,39 +131,6 @@ export function getSortedGroups( return Array.from(groupSet.keys()).sort((a, b) => (a > b ? 1 : -1)) } -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 -} - export function flatten_fixture( fixture: Fixture, fixture_type: FixtureType, diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index 775d10ac..8062a5f9 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -81,12 +81,11 @@ const disposer = { }, } -const controlState = disposer.push(controlStateManager()) - export function start( renderer: WebContents, visualizerContainer: VisualizerContainer ) { + const controlState = disposer.push(controlStateManager()) const api = disposer.push( createApi({ ipcMain, From 71992f6c4557193a6581ca61fe0597b87c665e0d Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 01:41:34 -0400 Subject: [PATCH 75/98] refactor: params feature --- src/features/dmx/channel.config.ts | 9 +- src/features/dmx/engine/channelUtils.ts | 29 +- src/features/dmx/engine/dmxEngine.ts | 4 +- src/features/dmx/shared/dmxUtil.ts | 23 - src/features/fixtures/redux/fixturesSlice.ts | 7 - src/features/led/shared/ledFixtures.ts | 7 +- .../modulation/react/ModulationMatrix.tsx | 2 +- .../modulation/react/ModulationSlider.tsx | 10 +- src/features/modulation/redux/reducer.ts | 103 +++ src/features/modulation/shared/modulation.ts | 12 +- src/features/params/engine/index.ts | 43 ++ .../react/controls/ADSR.tsx | 0 .../react/controls/ADSRWrapper.tsx | 0 .../react/controls/HsvPad.tsx | 0 .../{scenes => params}/react/controls/Hue.tsx | 2 +- .../react/controls/ParamAddButton.tsx | 11 +- .../react/controls/ParamCursor.tsx | 6 +- .../react/controls/ParamSlider.tsx | 4 +- .../react/controls/ParamXButton.tsx | 4 +- .../react/controls/ParamsControl.tsx | 0 .../react/controls/Randomizer.tsx | 3 +- .../react/controls/RandomizerVisualizer.tsx | 2 +- .../react/controls/SVCursor.tsx | 3 +- .../react/controls/SVpad.tsx | 2 +- .../react/controls/XYAxisCursor.tsx | 2 +- .../react/controls/XYAxisPad.tsx | 2 +- .../react/controls/XYCursor.tsx | 5 +- .../react/controls/XYWindow.tsx | 3 +- .../react/controls/XyParamsPad.tsx | 2 +- src/features/params/redux/index.ts | 70 ++ src/features/params/redux/reducer.ts | 92 +++ src/features/{dmx => params}/shared/params.ts | 10 +- src/features/params/shared/utils.ts | 16 + .../scenes/react/scenes/SplitScenes.tsx | 2 +- src/features/scenes/shared/Scenes.ts | 4 +- src/features/shared/redux/fixState.ts | 2 +- src/features/utils/baseColors.tsx | 15 - .../visualizer/threejs/UpdateResource.ts | 6 +- .../visualizer/threejs/effects/LightSync.ts | 2 +- src/main/engine/engine.ts | 3 +- .../redux/controlSlice/reducers/actions.ts | 605 +++++++----------- src/renderer/redux/controlSlice/slice.ts | 1 + src/renderer/redux/realtimeStore.ts | 31 +- src/renderer/redux/store.ts | 32 +- 44 files changed, 619 insertions(+), 572 deletions(-) create mode 100644 src/features/modulation/redux/reducer.ts create mode 100644 src/features/params/engine/index.ts rename src/features/{scenes => params}/react/controls/ADSR.tsx (100%) rename src/features/{scenes => params}/react/controls/ADSRWrapper.tsx (100%) rename src/features/{scenes => params}/react/controls/HsvPad.tsx (100%) rename src/features/{scenes => params}/react/controls/Hue.tsx (95%) rename src/features/{scenes => params}/react/controls/ParamAddButton.tsx (94%) rename src/features/{scenes => params}/react/controls/ParamCursor.tsx (71%) rename src/features/{scenes => params}/react/controls/ParamSlider.tsx (94%) rename src/features/{scenes => params}/react/controls/ParamXButton.tsx (88%) rename src/features/{scenes => params}/react/controls/ParamsControl.tsx (100%) rename src/features/{scenes => params}/react/controls/Randomizer.tsx (95%) rename src/features/{scenes => params}/react/controls/RandomizerVisualizer.tsx (96%) rename src/features/{scenes => params}/react/controls/SVCursor.tsx (87%) rename src/features/{scenes => params}/react/controls/SVpad.tsx (95%) rename src/features/{scenes => params}/react/controls/XYAxisCursor.tsx (89%) rename src/features/{scenes => params}/react/controls/XYAxisPad.tsx (97%) rename src/features/{scenes => params}/react/controls/XYCursor.tsx (84%) rename src/features/{scenes => params}/react/controls/XYWindow.tsx (88%) rename src/features/{scenes => params}/react/controls/XyParamsPad.tsx (97%) create mode 100644 src/features/params/redux/index.ts create mode 100644 src/features/params/redux/reducer.ts rename src/features/{dmx => params}/shared/params.ts (82%) create mode 100644 src/features/params/shared/utils.ts diff --git a/src/features/dmx/channel.config.ts b/src/features/dmx/channel.config.ts index d21a96ea..243af672 100644 --- a/src/features/dmx/channel.config.ts +++ b/src/features/dmx/channel.config.ts @@ -9,13 +9,14 @@ import { ChannelType, GetFixturePayload } from './shared/dmxFixtures' import { findClosest } from 'features/utils/math/util' import { getColorChannelLevel } from './shared/dmxColors' -import { Params, getParam } from './shared/params' -import { calculate_axis_channel, getBrightness } from './engine/channelUtils' +import { Params, getParam } 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: Params + params: Partial fixture: FlattenedFixture master: number randomizerLevel: number @@ -45,7 +46,7 @@ const createChannelConfig = < default: (...args) => { return { ...config.default(...args), - type: key + type: key, } }, } diff --git a/src/features/dmx/engine/channelUtils.ts b/src/features/dmx/engine/channelUtils.ts index 88bd3e33..c77ad118 100644 --- a/src/features/dmx/engine/channelUtils.ts +++ b/src/features/dmx/engine/channelUtils.ts @@ -1,4 +1,3 @@ -import { Window2D_t } from '../../shared/shared/window' import { DmxValue, DMX_MAX_VALUE, @@ -10,21 +9,17 @@ import { GetFixturePayload, ChannelType, } from '../shared/dmxFixtures' -import { getParam, Params } from '../shared/params' +import { Params } from '../../params/shared/params' import { Normalized } from '../../utils/math/util' import { rLerp } from '../../utils/math/range' -import { applyRandomization } from '../../bpm/shared/randomizer' -import { - applyMirror, - getMovingWindow, - getWindowMultiplier2D, -} from '../shared/dmxUtil' +import { applyMirror } from '../shared/dmxUtil' import { ChannelConfig, channelConfig } from '../channel.config' +import { getMovingWindow } from 'features/params/engine' export function getDmxValue( ch: FixtureChannel, - params: Params, + params: Partial, fixture: FlattenedFixture, master: number, randomizerLevel: number @@ -44,22 +39,6 @@ export function getDmxValue( }) } -export function getBrightness( - params: Params, - 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 function calculate_axis_channel( ch: GetFixturePayload<'axis'>, axis_param: Normalized | undefined, diff --git a/src/features/dmx/engine/dmxEngine.ts b/src/features/dmx/engine/dmxEngine.ts index 3d0e3c8c..b04e742d 100644 --- a/src/features/dmx/engine/dmxEngine.ts +++ b/src/features/dmx/engine/dmxEngine.ts @@ -4,7 +4,7 @@ import { DMX_NUM_CHANNELS, FlattenedFixture, } from '../shared/dmxFixtures' -import { Params } from '../shared/params' +import { Params } from '../../params/shared/params' import { RandomizerState } from '../../bpm/shared/randomizer' import { CleanReduxState } from '../../../renderer/redux/store' import { @@ -33,7 +33,7 @@ export function calculateDmx( const applyFixtures = ( fixtures: FlattenedFixture[], - outputParams: Params, + outputParams: Partial, randomizerState: RandomizerState ) => { fixtures.forEach((fixture, i) => { diff --git a/src/features/dmx/shared/dmxUtil.ts b/src/features/dmx/shared/dmxUtil.ts index 537e6e0e..00d72cc8 100644 --- a/src/features/dmx/shared/dmxUtil.ts +++ b/src/features/dmx/shared/dmxUtil.ts @@ -6,7 +6,6 @@ import { FixtureType, FlattenedFixture, } from './dmxFixtures' -import { Params } from './params' import { lerp, Normalized } from '../../utils/math/util' export function getWindowMultiplier2D( @@ -42,28 +41,6 @@ export function applyMirror( return (mirroredDoubleNorm + 1) / 2 } -/** - * used on engine side on led and dmx - * @param params - * @returns - */ -export function getMovingWindow(params: Params): 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 getFixturesInGroups( fixtures: FlattenedFixture[], scene_groups: { [key: string]: boolean | undefined } diff --git a/src/features/fixtures/redux/fixturesSlice.ts b/src/features/fixtures/redux/fixturesSlice.ts index 0430fa23..a10adf01 100644 --- a/src/features/fixtures/redux/fixturesSlice.ts +++ b/src/features/fixtures/redux/fixturesSlice.ts @@ -9,7 +9,6 @@ import { SubFixture, } from 'features/dmx/shared/dmxFixtures' import { clampNormalized } from '../../utils/math/util' -import { defaultParamsList } from '../../dmx/shared/params' import { initLedState, LedState } from '../../led/redux/ledState' import { initLedFixture, LedFixture } from '../../led/shared/ledFixtures' import { Point } from '../../utils/math/point' @@ -38,12 +37,6 @@ export function getCustomChannels(dmx: DmxState): Set { return result } -export function getAllParamKeys(dmx: DmxState): string[] { - return (defaultParamsList as string[]).concat( - Array.from(getCustomChannels(dmx)) - ) -} - interface SetFixtureWindowPayload { x: number y: number diff --git a/src/features/led/shared/ledFixtures.ts b/src/features/led/shared/ledFixtures.ts index 2ad3094b..8fd6e0e0 100644 --- a/src/features/led/shared/ledFixtures.ts +++ b/src/features/led/shared/ledFixtures.ts @@ -1,9 +1,10 @@ import { distanceBetween, pLerp, Point } from '../../utils/math/point' import { BaseColors, getBaseColorsFromHsv } from '../../utils/baseColors' -import { getMovingWindow, getWindowMultiplier2D } from '../../dmx/shared/dmxUtil' -import { getParam, Params } from '../../dmx/shared/params' +import { getWindowMultiplier2D } from '../../dmx/shared/dmxUtil' +import { getParam, Params } from '../../params/shared/params' import { indexArray } from '../../utils/util' import { Window2D_t } from '../../shared/shared/window' +import { getMovingWindow } from 'features/params/engine' export const MAX_LED_COUNT = 367 @@ -28,7 +29,7 @@ export function initLedFixture(): LedFixture { } export function getLedValues( - params: Params, + params: Partial, ledFixture: LedFixture, master: number ): BaseColors[] { diff --git a/src/features/modulation/react/ModulationMatrix.tsx b/src/features/modulation/react/ModulationMatrix.tsx index 56eee426..6167dd9a 100644 --- a/src/features/modulation/react/ModulationMatrix.tsx +++ b/src/features/modulation/react/ModulationMatrix.tsx @@ -6,7 +6,7 @@ import { import styled from 'styled-components' import { indexArray } from 'features/utils/util' import ModulationSlider, { AddModulationButton } from './ModulationSlider' -import { getAllParamKeys } from 'features/fixtures/redux/fixturesSlice' +import { getAllParamKeys } from 'features/params/redux' export default function ModulationMatrix({ index }: { index: number }) { const numSplits = useActiveLightScene((scene) => scene.splitScenes.length) diff --git a/src/features/modulation/react/ModulationSlider.tsx b/src/features/modulation/react/ModulationSlider.tsx index 80fb44bb..590e1487 100644 --- a/src/features/modulation/react/ModulationSlider.tsx +++ b/src/features/modulation/react/ModulationSlider.tsx @@ -1,23 +1,21 @@ import { useState } from 'react' import { useDispatch } from 'react-redux' -import { DefaultParam } from '../../dmx/shared/params' +import { DefaultParam } from '../../params/shared/params' import { useActiveLightScene, - useBaseParams, useDmxSelector, - useModParam, } from '../../../renderer/redux/store' import { setModulation } from '../../../renderer/redux/controlSlice' import useDragMapped from '../../ui/react/hooks/useDragMapped' import styled from 'styled-components' import Popup from 'features/ui/react/base/Popup' import { indexArray } from 'features/utils/util' -import { getAllParamKeys } from 'features/fixtures/redux/fixturesSlice' +import { getAllParamKeys, useBaseParams, useModParam } from 'features/params/redux' interface Props { splitIndex: number modIndex: number - param: DefaultParam | string + param: DefaultParam } export default function ModulationSlider({ @@ -128,7 +126,7 @@ function ParamEditor({ }: { splitIndex: number modIndex: number - param: DefaultParam | string + param: DefaultParam }) { const modVal = useModParam(param, modIndex, splitIndex) const dispatch = useDispatch() diff --git a/src/features/modulation/redux/reducer.ts b/src/features/modulation/redux/reducer.ts new file mode 100644 index 00000000..59fbf103 --- /dev/null +++ b/src/features/modulation/redux/reducer.ts @@ -0,0 +1,103 @@ +import { PayloadAction } from '@reduxjs/toolkit' +import { LightScene_t } from 'features/scenes/shared/Scenes' +import { clamp, clampNormalized } from 'features/utils/math/util' +import { + ActionState, + createTypedReducers, + modifyActiveLightScene, +} from 'renderer/redux/controlSlice/reducers/actions' +import { LfoShape } from '../shared/oscillator' +import { initModulator } from '../shared/modulation' + +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({ + 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 + ) + }) + }, + 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 index bbd7b5c4..7307fd03 100644 --- a/src/features/modulation/shared/modulation.ts +++ b/src/features/modulation/shared/modulation.ts @@ -1,8 +1,12 @@ -import { initModulation, DefaultParam, Modulation } from '../../dmx/shared/params' +import { + initModulation, + DefaultParam, + Modulation, +} from '../../params/shared/params' import { Lfo, GetValue, GetRamp } from './oscillator' import { LightScene_t } from '../../scenes/shared/Scenes' import { clampNormalized } from '../../utils/math/util' -import { defaultOutputParams } from '../../dmx/shared/params' +import { defaultOutputParams } from '../../params/shared/params' export interface Modulator { lfo: Lfo @@ -27,7 +31,7 @@ export function getOutputParams( beats: number, scene: LightScene_t, splitIndex: number, - allParamKeys: string[] + allParamKeys: DefaultParam[] ) { const outputParams = defaultOutputParams() const baseParams = scene.splitScenes[splitIndex].baseParams @@ -45,7 +49,7 @@ export function getOutputParams( function getOutputParam( baseParam: number | undefined, - param: DefaultParam | string, + param: DefaultParam, snapshots: ModSnapshot[] ) { if (baseParam === undefined) return undefined diff --git a/src/features/params/engine/index.ts b/src/features/params/engine/index.ts new file mode 100644 index 00000000..63774517 --- /dev/null +++ b/src/features/params/engine/index.ts @@ -0,0 +1,43 @@ +import { Window2D_t } from "features/shared/shared/window" +import { Params, getParam } from "../shared/params" +import { Normalized } 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') + ) +} diff --git a/src/features/scenes/react/controls/ADSR.tsx b/src/features/params/react/controls/ADSR.tsx similarity index 100% rename from src/features/scenes/react/controls/ADSR.tsx rename to src/features/params/react/controls/ADSR.tsx diff --git a/src/features/scenes/react/controls/ADSRWrapper.tsx b/src/features/params/react/controls/ADSRWrapper.tsx similarity index 100% rename from src/features/scenes/react/controls/ADSRWrapper.tsx rename to src/features/params/react/controls/ADSRWrapper.tsx diff --git a/src/features/scenes/react/controls/HsvPad.tsx b/src/features/params/react/controls/HsvPad.tsx similarity index 100% rename from src/features/scenes/react/controls/HsvPad.tsx rename to src/features/params/react/controls/HsvPad.tsx diff --git a/src/features/scenes/react/controls/Hue.tsx b/src/features/params/react/controls/Hue.tsx similarity index 95% rename from src/features/scenes/react/controls/Hue.tsx rename to src/features/params/react/controls/Hue.tsx index 327a235b..940f567e 100644 --- a/src/features/scenes/react/controls/Hue.tsx +++ b/src/features/params/react/controls/Hue.tsx @@ -2,7 +2,7 @@ import useDragMapped from '../../../ui/react/hooks/useDragMapped' import { useDispatch } from 'react-redux' 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/features/scenes/react/controls/ParamAddButton.tsx b/src/features/params/react/controls/ParamAddButton.tsx similarity index 94% rename from src/features/scenes/react/controls/ParamAddButton.tsx rename to src/features/params/react/controls/ParamAddButton.tsx index b1145664..c022da8b 100644 --- a/src/features/scenes/react/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 '../../../ui/react/base/Popup' import { useDispatch } from 'react-redux' -import { DefaultParam, Params, defaultParamsList } from 'features/dmx/shared/params' +import { DefaultParam, Params, defaultParamsList } from 'features/params/shared/params' import { setBaseParams } from 'renderer/redux/controlSlice' -import { initParams } from 'features/dmx/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 '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/features/scenes/react/controls/ParamCursor.tsx b/src/features/params/react/controls/ParamCursor.tsx similarity index 71% rename from src/features/scenes/react/controls/ParamCursor.tsx rename to src/features/params/react/controls/ParamCursor.tsx index d6d82c90..b647d5a9 100644 --- a/src/features/scenes/react/controls/ParamCursor.tsx +++ b/src/features/params/react/controls/ParamCursor.tsx @@ -1,9 +1,9 @@ -import { useOutputParam } from '../../../../renderer/redux/realtimeStore' -import { DefaultParam } from '../../../dmx/shared/params' +import { useOutputParam } from 'features/params/redux' +import { DefaultParam } from '../../shared/params' import SliderCursor from '../../../ui/react/base/SliderCursor' interface Props { - param: DefaultParam | string + param: DefaultParam radius: number orientation: 'vertical' | 'horizontal' splitIndex: number diff --git a/src/features/scenes/react/controls/ParamSlider.tsx b/src/features/params/react/controls/ParamSlider.tsx similarity index 94% rename from src/features/scenes/react/controls/ParamSlider.tsx rename to src/features/params/react/controls/ParamSlider.tsx index e2d886a6..b33306c3 100644 --- a/src/features/scenes/react/controls/ParamSlider.tsx +++ b/src/features/params/react/controls/ParamSlider.tsx @@ -1,12 +1,12 @@ -import { DefaultParam } from '../../../dmx/shared/params' +import { DefaultParam } from '../../shared/params' import SliderBase from '../../../ui/react/base/SliderBase' import SliderCursor from '../../../ui/react/base/SliderCursor' -import { useBaseParam } from '../../../../renderer/redux/store' import { useDispatch } from 'react-redux' import { setBaseParams } from '../../../../renderer/redux/controlSlice' import ParamCursor from './ParamCursor' import { SliderMidiOverlay } from 'features/midi/react/MidiOverlay' import ParamXButton from './ParamXButton' +import { useBaseParam } from 'features/params/redux' interface Props { param: DefaultParam diff --git a/src/features/scenes/react/controls/ParamXButton.tsx b/src/features/params/react/controls/ParamXButton.tsx similarity index 88% rename from src/features/scenes/react/controls/ParamXButton.tsx rename to src/features/params/react/controls/ParamXButton.tsx index e93e5fcd..295514e3 100644 --- a/src/features/scenes/react/controls/ParamXButton.tsx +++ b/src/features/params/react/controls/ParamXButton.tsx @@ -1,11 +1,11 @@ import styled from 'styled-components' -import { DefaultParam } from 'features/dmx/shared/params' +import { DefaultParam } from 'features/params/shared/params' import { useDispatch } from 'react-redux' import { deleteBaseParams } from 'renderer/redux/controlSlice' interface Props { splitIndex: number - params: readonly (DefaultParam | string)[] + params: readonly DefaultParam[] } export default function ParamXButton({ splitIndex, params }: Props) { diff --git a/src/features/scenes/react/controls/ParamsControl.tsx b/src/features/params/react/controls/ParamsControl.tsx similarity index 100% rename from src/features/scenes/react/controls/ParamsControl.tsx rename to src/features/params/react/controls/ParamsControl.tsx diff --git a/src/features/scenes/react/controls/Randomizer.tsx b/src/features/params/react/controls/Randomizer.tsx similarity index 95% rename from src/features/scenes/react/controls/Randomizer.tsx rename to src/features/params/react/controls/Randomizer.tsx index 7c5fa3cb..7c0b2f60 100644 --- a/src/features/scenes/react/controls/Randomizer.tsx +++ b/src/features/params/react/controls/Randomizer.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' import ADSRWrapper from './ADSRWrapper' -import { useActiveLightScene, useBaseParam } from '../../../../renderer/redux/store' +import { useActiveLightScene } from '../../../../renderer/redux/store' import RandomizerVisualizer from './RandomizerVisualizer' import DraggableNumber from '../../../ui/react/base/DraggableNumber' import { useDispatch } from 'react-redux' @@ -8,6 +8,7 @@ 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/features/scenes/react/controls/RandomizerVisualizer.tsx b/src/features/params/react/controls/RandomizerVisualizer.tsx similarity index 96% rename from src/features/scenes/react/controls/RandomizerVisualizer.tsx rename to src/features/params/react/controls/RandomizerVisualizer.tsx index 81228fed..e9c5dcf9 100644 --- a/src/features/scenes/react/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 'features/bpm/shared/randomizer' +import { useBaseParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/features/scenes/react/controls/SVCursor.tsx b/src/features/params/react/controls/SVCursor.tsx similarity index 87% rename from src/features/scenes/react/controls/SVCursor.tsx rename to src/features/params/react/controls/SVCursor.tsx index bf7edf14..73465aef 100644 --- a/src/features/scenes/react/controls/SVCursor.tsx +++ b/src/features/params/react/controls/SVCursor.tsx @@ -1,6 +1,5 @@ -import { useOutputParam } from '../../../../renderer/redux/realtimeStore' +import { useBaseParam, useOutputParam } from 'features/params/redux' import Cursor from '../../../ui/react/base/Cursor' -import { useBaseParam } from 'renderer/redux/store' interface Props { splitIndex: number diff --git a/src/features/scenes/react/controls/SVpad.tsx b/src/features/params/react/controls/SVpad.tsx similarity index 95% rename from src/features/scenes/react/controls/SVpad.tsx rename to src/features/params/react/controls/SVpad.tsx index 1356db8f..bbcd225d 100644 --- a/src/features/scenes/react/controls/SVpad.tsx +++ b/src/features/params/react/controls/SVpad.tsx @@ -3,7 +3,7 @@ import useDragMapped from '../../../ui/react/hooks/useDragMapped' import { useDispatch } from 'react-redux' import { setBaseParams } from '../../../../renderer/redux/controlSlice' import { SVCursorBase, SVCursorOutput } from './SVCursor' -import { useOutputParam } from '../../../../renderer/redux/realtimeStore' +import { useOutputParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/features/scenes/react/controls/XYAxisCursor.tsx b/src/features/params/react/controls/XYAxisCursor.tsx similarity index 89% rename from src/features/scenes/react/controls/XYAxisCursor.tsx rename to src/features/params/react/controls/XYAxisCursor.tsx index 0a2a3a29..34a9bbbf 100644 --- a/src/features/scenes/react/controls/XYAxisCursor.tsx +++ b/src/features/params/react/controls/XYAxisCursor.tsx @@ -1,4 +1,4 @@ -import { useOutputParam } from '../../../../renderer/redux/realtimeStore' +import { useOutputParam } from 'features/params/redux' import Cursor from '../../../ui/react/base/Cursor' import { applyMirror } from 'features/dmx/shared/dmxUtil' diff --git a/src/features/scenes/react/controls/XYAxisPad.tsx b/src/features/params/react/controls/XYAxisPad.tsx similarity index 97% rename from src/features/scenes/react/controls/XYAxisPad.tsx rename to src/features/params/react/controls/XYAxisPad.tsx index 36106b70..5abf87f6 100644 --- a/src/features/scenes/react/controls/XYAxisPad.tsx +++ b/src/features/params/react/controls/XYAxisPad.tsx @@ -3,9 +3,9 @@ import { useDispatch } from 'react-redux' 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/features/scenes/react/controls/XYCursor.tsx b/src/features/params/react/controls/XYCursor.tsx similarity index 84% rename from src/features/scenes/react/controls/XYCursor.tsx rename to src/features/params/react/controls/XYCursor.tsx index 9d975ada..fcd0c478 100644 --- a/src/features/scenes/react/controls/XYCursor.tsx +++ b/src/features/params/react/controls/XYCursor.tsx @@ -1,6 +1,7 @@ -import { useOutputParam } from '../../../../renderer/redux/realtimeStore' + +import { useBaseParam, useOutputParam } from 'features/params/redux' import Cursor from '../../../ui/react/base/Cursor' -import { useBaseParam } from '../../../../renderer/redux/store' + interface Props { splitIndex: number diff --git a/src/features/scenes/react/controls/XYWindow.tsx b/src/features/params/react/controls/XYWindow.tsx similarity index 88% rename from src/features/scenes/react/controls/XYWindow.tsx rename to src/features/params/react/controls/XYWindow.tsx index 2c70865d..e62afecc 100644 --- a/src/features/scenes/react/controls/XYWindow.tsx +++ b/src/features/params/react/controls/XYWindow.tsx @@ -1,4 +1,5 @@ -import { useOutputParam } from '../../../../renderer/redux/realtimeStore' + +import { useOutputParam } from 'features/params/redux' import Window2D from '../../../ui/react/base/Window2D' interface Props { diff --git a/src/features/scenes/react/controls/XyParamsPad.tsx b/src/features/params/react/controls/XyParamsPad.tsx similarity index 97% rename from src/features/scenes/react/controls/XyParamsPad.tsx rename to src/features/params/react/controls/XyParamsPad.tsx index 90806968..21a6835d 100644 --- a/src/features/scenes/react/controls/XyParamsPad.tsx +++ b/src/features/params/react/controls/XyParamsPad.tsx @@ -5,10 +5,10 @@ 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 'features/midi/react/MidiOverlay_xy' import { paramBundles } from './ParamAddButton' 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..c2faaca3 --- /dev/null +++ b/src/features/params/redux/index.ts @@ -0,0 +1,70 @@ +import { + DefaultParam, + Params, + defaultOutputParams, + defaultParamsList, +} from 'features/params/shared/params' +import { + DmxState, + getCustomChannels, +} from 'features/fixtures/redux/fixturesSlice' +import { useActiveLightScene } from 'renderer/redux/store' +import { useRealtimeSelector } from 'renderer/redux/realtimeStore' + +export function getAllParamKeys(dmx: DmxState): DefaultParam[] { + return defaultParamsList.concat(Array.from(getCustomChannels(dmx))) +} + +export function useBaseParam( + param: DefaultParam, + 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, + modIndex: number, + splitIndex: number +) { + return useActiveLightScene((scene) => { + return scene.modulators[modIndex].splitModulations[splitIndex][param] + }) +} + +export function useOutputParam( + param: DefaultParam, + 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..3823ea30 --- /dev/null +++ b/src/features/params/redux/reducer.ts @@ -0,0 +1,92 @@ +import { PayloadAction } from '@reduxjs/toolkit' +import { + createTypedReducers, + modifyActiveLightScene, +} from 'renderer/redux/controlSlice/reducers/actions' +import { DefaultParam, Params } from '../shared/params' +import { clampNormalized } from 'features/utils/math/util' + +type ParamsAction = PayloadAction<{ + splitIndex: number + params: Partial +}> + +type ParamAction = PayloadAction<{ + splitIndex: number + paramKey: DefaultParam + value: number | undefined +}> + +export interface SetModulationPayload { + splitIndex: number + modIndex: number + param: DefaultParam + 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[] + }> + ) => { + 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/features/dmx/shared/params.ts b/src/features/params/shared/params.ts similarity index 82% rename from src/features/dmx/shared/params.ts rename to src/features/params/shared/params.ts index 36ea1d18..93cfee58 100644 --- a/src/features/dmx/shared/params.ts +++ b/src/features/params/shared/params.ts @@ -13,9 +13,9 @@ export type DefaultParam = | 'yAxis' | 'xMirror' -export type Params = { [key: string]: number | undefined } +export type Params = { [key in DefaultParam]: number | undefined } -export function initBaseParams(): Params { +export function initBaseParams(): Partial { return { hue: 0.5, saturation: 0.5, @@ -58,11 +58,11 @@ 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 defaultOutputParams(): Partial { return { hue: 0.5, saturation: 0.5, @@ -96,7 +96,7 @@ export const defaultParamsList: DefaultParam[] = [ 'xMirror', ] -export type Modulation = Params +export type Modulation = Partial export function initModulation(): Modulation { return {} diff --git a/src/features/params/shared/utils.ts b/src/features/params/shared/utils.ts new file mode 100644 index 00000000..df4c58d5 --- /dev/null +++ b/src/features/params/shared/utils.ts @@ -0,0 +1,16 @@ +import { BaseColors, hsv2rgb } from 'features/utils/baseColors' +import { Params } 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/features/scenes/react/scenes/SplitScenes.tsx b/src/features/scenes/react/scenes/SplitScenes.tsx index 6a1a017e..9bdd3a34 100644 --- a/src/features/scenes/react/scenes/SplitScenes.tsx +++ b/src/features/scenes/react/scenes/SplitScenes.tsx @@ -1,4 +1,4 @@ -import ParamsControl from 'features/scenes/react/controls/ParamsControl' +import ParamsControl from 'features/params/react/controls/ParamsControl' import { useActiveLightScene, useControlSelector } from 'renderer/redux/store' import { indexArray } from 'features/utils/util' import styled from 'styled-components' diff --git a/src/features/scenes/shared/Scenes.ts b/src/features/scenes/shared/Scenes.ts index 4a8d33d7..50510c9f 100644 --- a/src/features/scenes/shared/Scenes.ts +++ b/src/features/scenes/shared/Scenes.ts @@ -1,4 +1,4 @@ -import { Params, initBaseParams } from '../../dmx/shared/params' +import { Params, initBaseParams } from '../../params/shared/params' import { Modulator, initModulator } from '../../modulation/shared/modulation' import { RandomizerOptions, initRandomizerOptions } from '../../bpm/shared/randomizer' import { nanoid } from 'nanoid' @@ -18,7 +18,7 @@ export interface SceneBase { } export interface SplitScene_t { - baseParams: Params + baseParams: Partial randomizer: RandomizerOptions // true = include group | false = include not group groups: { [key: string]: boolean | undefined } diff --git a/src/features/shared/redux/fixState.ts b/src/features/shared/redux/fixState.ts index fd8641a4..4c509290 100644 --- a/src/features/shared/redux/fixState.ts +++ b/src/features/shared/redux/fixState.ts @@ -10,7 +10,7 @@ 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 '../../dmx/shared/params' +import { Modulation, Params } from '../../params/shared/params' import { RandomizerOptions } from '../../bpm/shared/randomizer' import { LightScenes_t, diff --git a/src/features/utils/baseColors.tsx b/src/features/utils/baseColors.tsx index 9055b841..82abab2b 100644 --- a/src/features/utils/baseColors.tsx +++ b/src/features/utils/baseColors.tsx @@ -1,5 +1,4 @@ import { Normalized } from './math/util' -import { Params } from '../dmx/shared/params' 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/features/visualizer/threejs/UpdateResource.ts b/src/features/visualizer/threejs/UpdateResource.ts index e8698ac8..b845fc81 100644 --- a/src/features/visualizer/threejs/UpdateResource.ts +++ b/src/features/visualizer/threejs/UpdateResource.ts @@ -1,4 +1,4 @@ -import { Params } from '../../dmx/shared/params' +import { Params } 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' @@ -8,7 +8,7 @@ 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 diff --git a/src/features/visualizer/threejs/effects/LightSync.ts b/src/features/visualizer/threejs/effects/LightSync.ts index cf5f35d4..b90337fb 100644 --- a/src/features/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 'features/utils/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/main/engine/engine.ts b/src/main/engine/engine.ts index 8062a5f9..ef5de189 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -26,7 +26,7 @@ import { } from '../../features/dmx/shared/dmxUtil' import { ThrottleMap } from 'features/midi/engine/midiConnection' import { MidiMessage, midiInputID } from 'features/midi/shared/midi' -import { getAllParamKeys } from '../../features/fixtures/redux/fixturesSlice' + import { indexArray } from '../../features/utils/util' import WledManager from '../../features/led/engine/wled_manager' import { @@ -35,6 +35,7 @@ import { _tapTempo, } from 'features/bpm/engine/Link' import { createApi, IPC_Callbacks } from './api' +import { getAllParamKeys } from 'features/params/redux' let _realtimeState: RealtimeState = initRealtimeState() diff --git a/src/renderer/redux/controlSlice/reducers/actions.ts b/src/renderer/redux/controlSlice/reducers/actions.ts index 47dc2389..1bb3cf02 100644 --- a/src/renderer/redux/controlSlice/reducers/actions.ts +++ b/src/renderer/redux/controlSlice/reducers/actions.ts @@ -1,12 +1,11 @@ import { ReorderParams } from '../../../../features/utils/util' -import { clampNormalized, clamp } from '../../../../features/utils/math/util' -import { initModulator } from '../../../../features/modulation/shared/modulation' + import { nanoid } from 'nanoid' import { RandomizerOptions } from '../../../../features/bpm/shared/randomizer' import cloneDeep from 'lodash.clonedeep' import { LayerConfig } from '../../../../features/visualizer/threejs/layers/LayerConfig' import { EffectConfig } from '../../../../features/visualizer/threejs/effects/effectConfigs' -import { LfoShape } from '../../../../features/modulation/shared/oscillator' + import { reorderArray } from '../../../../features/utils/util' import { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit' import { @@ -21,28 +20,14 @@ import { initVisualScene, } from '../../../../features/scenes/shared/Scenes' -import { DefaultParam, Params } from '../../../../features/dmx/shared/params' +import { paramsActionReducer } from 'features/params/redux/reducer' +import { modulationActionReducer } from 'features/modulation/redux/reducer' export interface ActionState extends ScenesStateBundle { master: number } -export interface IncrementModulatorPayload { - index: number - flip: number - phaseShift: number - skew: number - symmetricSkew: number -} - -export interface SetModulationPayload { - splitIndex: number - modIndex: number - param: DefaultParam | string - value: number | undefined -} - -function modifyActiveLightScene( +export function modifyActiveLightScene( state: ActionState, callback: (scene: LightScene_t) => void ) { @@ -52,22 +37,6 @@ function modifyActiveLightScene( } } -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) - } - }, - }, -} - function modifyActiveVisualScene( state: ActionState, callback: (scene: VisualScene_t) => void @@ -94,372 +63,236 @@ type ScopedAction = PayloadAction<{ val: T }> -type ParamsAction = PayloadAction<{ - splitIndex: number - params: Params -}> - -type ParamAction = PayloadAction<{ - splitIndex: number - paramKey: DefaultParam - value: number | undefined -}> - -const createTypedReducers = < - State, - Reducers extends SliceCaseReducers = SliceCaseReducers +export const createTypedReducers = < + Reducers extends SliceCaseReducers = SliceCaseReducers >( - _stateType: State, reducers: Reducers ) => { return reducers } -export const actions = createTypedReducers({} as ActionState, { - setMaster: (state, { payload }: PayloadAction) => { - state.master = payload - }, - // ===================== LIGHT & VISUAL SCENES =========================== - setAutoSceneEnabled: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - state[sceneType].auto.enabled = val - }, - setAutoSceneBombacity: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - state[sceneType].auto.epicness = val - }, - setAutoScenePeriod: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - state[sceneType].auto.period = val - }, - newScene: (state, { payload }: PayloadAction) => { - const scenes = state[payload] - const id = nanoid() - scenes.ids.push(id) - scenes.byId[id] = payload === 'light' ? initLightScene() : initVisualScene() - scenes.active = id - }, - removeScene: ( - state, - { payload: { sceneType, val } }: ScopedAction<{ index: number }> - ) => { - const scenes = state[sceneType] - const id = scenes.ids[val.index] - scenes.ids.splice(val.index, 1) - delete scenes.byId[id] - // This is necessary in a world where you can delete the active scene... Which you currently can't - // if (state.active === id) { - // state.active = state.ids[0] - // } - }, - setActiveScene: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - state[sceneType].active = val - }, - setActiveSceneIndex: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - const scenes = state[sceneType] - if (val > -1 && scenes.ids.length > val) { - scenes.active = scenes.ids[val] - } else { - console.error('Tried to set the scene to an out-of-bounds index') - } - }, - setActiveSceneBombacity: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - modifyActiveScene(state, sceneType, (scene) => { - scene.epicness = val - }) - }, - setActiveSceneAutoEnabled: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - modifyActiveScene(state, sceneType, (scene) => { - scene.autoEnabled = val - }) - }, - setActiveSceneName: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - modifyActiveScene(state, sceneType, (scene) => { - scene.name = val - }) - }, - reorderScene: ( - state, - { payload: { sceneType, val } }: ScopedAction - ) => { - reorderArray(state[sceneType].ids, val) - }, - copyActiveScene: (state, { payload }: PayloadAction) => { - const scenes = state[payload] - const id = nanoid() - scenes.ids.push(id) - scenes.byId[id] = cloneDeep(scenes.byId[scenes.active]) - }, - sortScenesByBombacity: (state, { payload }: PayloadAction) => { - const scenes = state[payload] - scenes.ids.sort((idLeft, idRight) => { - const leftScene = scenes.byId[idLeft] - const rightScene = scenes.byId[idRight] - if (leftScene && rightScene) - return leftScene.epicness - rightScene.epicness - return 0 - }) - }, - autoBombacity: (state, { payload }: PayloadAction) => { - const scenes = state[payload] - scenes.ids.forEach((id, i) => { - const scene = scenes.byId[id] - if (scene) scene.epicness = i / (scenes.ids.length - 1) - }) - }, +export const actions = { + ...createTypedReducers({ + setMaster: (state, { payload }: PayloadAction) => { + state.master = payload + }, + // ===================== LIGHT & VISUAL SCENES =========================== + setAutoSceneEnabled: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + state[sceneType].auto.enabled = val + }, + setAutoSceneBombacity: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + state[sceneType].auto.epicness = val + }, + setAutoScenePeriod: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + state[sceneType].auto.period = val + }, + newScene: (state, { payload }: PayloadAction) => { + const scenes = state[payload] + const id = nanoid() + scenes.ids.push(id) + scenes.byId[id] = + payload === 'light' ? initLightScene() : initVisualScene() + scenes.active = id + }, + removeScene: ( + state, + { payload: { sceneType, val } }: ScopedAction<{ index: number }> + ) => { + const scenes = state[sceneType] + const id = scenes.ids[val.index] + scenes.ids.splice(val.index, 1) + delete scenes.byId[id] + // This is necessary in a world where you can delete the active scene... Which you currently can't + // if (state.active === id) { + // state.active = state.ids[0] + // } + }, + setActiveScene: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + state[sceneType].active = val + }, + setActiveSceneIndex: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + const scenes = state[sceneType] + if (val > -1 && scenes.ids.length > val) { + scenes.active = scenes.ids[val] + } else { + console.error('Tried to set the scene to an out-of-bounds index') + } + }, + setActiveSceneBombacity: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + modifyActiveScene(state, sceneType, (scene) => { + scene.epicness = val + }) + }, + setActiveSceneAutoEnabled: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + modifyActiveScene(state, sceneType, (scene) => { + scene.autoEnabled = val + }) + }, + setActiveSceneName: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + modifyActiveScene(state, sceneType, (scene) => { + scene.name = val + }) + }, + reorderScene: ( + state, + { payload: { sceneType, val } }: ScopedAction + ) => { + reorderArray(state[sceneType].ids, val) + }, + copyActiveScene: (state, { payload }: PayloadAction) => { + const scenes = state[payload] + const id = nanoid() + scenes.ids.push(id) + scenes.byId[id] = cloneDeep(scenes.byId[scenes.active]) + }, + sortScenesByBombacity: (state, { payload }: PayloadAction) => { + const scenes = state[payload] + scenes.ids.sort((idLeft, idRight) => { + const leftScene = scenes.byId[idLeft] + const rightScene = scenes.byId[idRight] + if (leftScene && rightScene) + return leftScene.epicness - rightScene.epicness + return 0 + }) + }, + autoBombacity: (state, { payload }: PayloadAction) => { + const scenes = state[payload] + scenes.ids.forEach((id, i) => { + const scene = scenes.byId[id] + if (scene) scene.epicness = i / (scenes.ids.length - 1) + }) + }, - // ===================== LIGHT SCENES ONLY =========================== - resetLightScenes: (state, { payload }: PayloadAction) => { - state.light = payload - }, - 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 - ) - }) - }, - 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) - }) - }, - setModulation: (state, { payload }: PayloadAction) => { - const { splitIndex, modIndex, param, value } = payload - modifyActiveLightScene(state, (scene) => { - scene.modulators[modIndex].splitModulations[splitIndex][param] = value - }) - }, - setBaseParams: (state, { payload: { params, splitIndex } }: ParamsAction) => { - for (let [key, value] of Object.entries(params)) { + // ===================== LIGHT SCENES ONLY =========================== + resetLightScenes: (state, { payload }: PayloadAction) => { + state.light = payload + }, + ...paramsActionReducer, + ...modulationActionReducer, + + setRandomizer: ( + state, + { + payload: { key, value, splitIndex }, + }: PayloadAction<{ + key: keyof RandomizerOptions + value: number + splitIndex: number + }> + ) => { modifyActiveLightScene(state, (scene) => { - const baseParams = scene.splitScenes[splitIndex].baseParams - baseParams[key] = value + scene.splitScenes[splitIndex].randomizer[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) { + }, + addSplitScene: (state, {}: PayloadAction) => { modifyActiveLightScene(state, (scene) => { - const baseParams = scene.splitScenes[splitIndex].baseParams - delete baseParams[param] - - // Now remove the params from any modulators + scene.splitScenes.push(initSplitScene()) + scene.modulators.forEach((modulator) => { + modulator.splitModulations.push({}) + }) + }) + }, + removeSplitSceneByIndex: (state, { payload }: PayloadAction) => { + modifyActiveLightScene(state, (scene) => { + scene.splitScenes.splice(payload, 1) scene.modulators.forEach((modulator) => { - const modulation = modulator.splitModulations[splitIndex] - delete modulation[param] + modulator.splitModulations.splice(payload, 1) }) }) - } - }, - incrementBaseParams: ( - state, - { payload: { params, splitIndex } }: ParamsAction - ) => { - for (let [key, amount] of Object.entries(params)) { + }, + setSceneGroup: ( + state, + { + payload: { index, group, val }, + }: PayloadAction<{ + index: number + group: string + val: boolean | undefined + }> + ) => { 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 - ) - } + if (val === undefined) { + delete scene.splitScenes[index].groups[group] + } else { + scene.splitScenes[index].groups[group] = val } }) - } - }, - setRandomizer: ( - state, - { - payload: { key, value, splitIndex }, - }: PayloadAction<{ - key: keyof RandomizerOptions - value: number - splitIndex: number - }> - ) => { - modifyActiveLightScene(state, (scene) => { - scene.splitScenes[splitIndex].randomizer[key] = value - }) - }, - addSplitScene: (state, {}: PayloadAction) => { - modifyActiveLightScene(state, (scene) => { - scene.splitScenes.push(initSplitScene()) - scene.modulators.forEach((modulator) => { - modulator.splitModulations.push({}) + }, + // ===================== VISUAL SCENES ONLY =========================== + resetVisualScenes: (state, { payload }: PayloadAction) => { + state.visual = payload + }, + setVisualSceneConfig: (state, { payload }: PayloadAction) => { + modifyActiveVisualScene(state, (scene) => (scene.config = payload)) + }, + activeVisualSceneEffect_add: ( + state, + { payload }: PayloadAction + ) => { + modifyActiveVisualScene(state, (scene) => { + scene.effectsConfig.push(payload) + scene.activeEffectIndex = scene.effectsConfig.length - 1 + }) + }, + activeVisualSceneEffect_set: ( + state, + { payload }: PayloadAction + ) => { + modifyActiveVisualScene(state, (scene) => { + scene.effectsConfig[scene.activeEffectIndex] = payload }) - }) - }, - removeSplitSceneByIndex: (state, { payload }: PayloadAction) => { - modifyActiveLightScene(state, (scene) => { - scene.splitScenes.splice(payload, 1) - scene.modulators.forEach((modulator) => { - modulator.splitModulations.splice(payload, 1) + }, + activeVisualSceneEffect_removeIndex: ( + state, + { payload }: PayloadAction + ) => { + modifyActiveVisualScene(state, (scene) => { + scene.effectsConfig.splice(payload, 1) + if (scene.activeEffectIndex >= scene.effectsConfig.length) { + scene.activeEffectIndex = scene.effectsConfig.length - 1 + } }) - }) - }, - setSceneGroup: ( - state, - { - payload: { index, group, val }, - }: PayloadAction<{ - index: number - group: string - val: boolean | undefined - }> - ) => { - modifyActiveLightScene(state, (scene) => { - if (val === undefined) { - delete scene.splitScenes[index].groups[group] - } else { - scene.splitScenes[index].groups[group] = val - } - }) - }, - // ===================== VISUAL SCENES ONLY =========================== - resetVisualScenes: (state, { payload }: PayloadAction) => { - state.visual = payload - }, - setVisualSceneConfig: (state, { payload }: PayloadAction) => { - modifyActiveVisualScene(state, (scene) => (scene.config = payload)) - }, - activeVisualSceneEffect_add: ( - state, - { payload }: PayloadAction - ) => { - modifyActiveVisualScene(state, (scene) => { - scene.effectsConfig.push(payload) - scene.activeEffectIndex = scene.effectsConfig.length - 1 - }) - }, - activeVisualSceneEffect_set: ( - state, - { payload }: PayloadAction - ) => { - modifyActiveVisualScene(state, (scene) => { - scene.effectsConfig[scene.activeEffectIndex] = payload - }) - }, - activeVisualSceneEffect_removeIndex: ( - state, - { payload }: PayloadAction - ) => { - modifyActiveVisualScene(state, (scene) => { - scene.effectsConfig.splice(payload, 1) - if (scene.activeEffectIndex >= scene.effectsConfig.length) { - scene.activeEffectIndex = scene.effectsConfig.length - 1 - } - }) - }, - activeVisualSceneEffect_reorder: ( - state, - { payload }: PayloadAction - ) => { - modifyActiveVisualScene(state, (scene) => { - reorderArray(scene.effectsConfig, payload) - }) - }, - activeVisualScene_setActiveEffectIndex: ( - state, - { payload }: PayloadAction - ) => { - modifyActiveVisualScene( + }, + activeVisualSceneEffect_reorder: ( + state, + { payload }: PayloadAction + ) => { + modifyActiveVisualScene(state, (scene) => { + reorderArray(scene.effectsConfig, payload) + }) + }, + activeVisualScene_setActiveEffectIndex: ( state, - (scene) => (scene.activeEffectIndex = payload) - ) - }, -}) + { payload }: PayloadAction + ) => { + modifyActiveVisualScene( + state, + (scene) => (scene.activeEffectIndex = payload) + ) + }, + }), +} diff --git a/src/renderer/redux/controlSlice/slice.ts b/src/renderer/redux/controlSlice/slice.ts index 49bd1ada..48ea91d5 100644 --- a/src/renderer/redux/controlSlice/slice.ts +++ b/src/renderer/redux/controlSlice/slice.ts @@ -25,6 +25,7 @@ export const scenesSlice = createSlice({ initialState: initControlState(), reducers: { ...actions, + // ===================== MIDI =========================================== midiListen: (state, action) => midiActions.listen(state.device, action), midiStopListening: (state) => midiActions.stopListening(state.device), diff --git a/src/renderer/redux/realtimeStore.ts b/src/renderer/redux/realtimeStore.ts index 2aae7eaf..613bd0c5 100644 --- a/src/renderer/redux/realtimeStore.ts +++ b/src/renderer/redux/realtimeStore.ts @@ -7,7 +7,7 @@ import { } from 'react-redux' import React from 'react' import { initTimeState, TimeState } from '../../features/bpm/shared/TimeState' -import { defaultOutputParams, DefaultParam, Params } from '../../features/dmx/shared/params' +import { Params } from '../../features/params/shared/params' import { RandomizerState } from '../../features/bpm/shared/randomizer' function initDmxOut(): number[] { @@ -15,7 +15,7 @@ function initDmxOut(): number[] { } export interface SplitState { - outputParams: Params + outputParams: Partial randomizer: RandomizerState } @@ -69,30 +69,3 @@ export type RealtimeDispatch = typeof realtimeStore.dispatch export const useRealtimeSelector: TypedUseSelectorHook = createSelectorHook(realtimeContext) export const useRealtimeDispatch = createDispatchHook(realtimeContext) - -export function useOutputParam( - param: DefaultParam | string, - 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): Params { - const params = useRealtimeSelector((state) => { - return state.splitStates[splitIndex]?.outputParams - }) - if (params === undefined) { - return defaultOutputParams() - } - return params -} diff --git a/src/renderer/redux/store.ts b/src/renderer/redux/store.ts index c1e34e6f..9bbc182b 100644 --- a/src/renderer/redux/store.ts +++ b/src/renderer/redux/store.ts @@ -5,7 +5,9 @@ import { PayloadAction, } from '@reduxjs/toolkit' import { useSelector, TypedUseSelectorHook } from 'react-redux' -import dmxReducer, { DmxState } from '../../features/fixtures/redux/fixturesSlice' +import dmxReducer, { + DmxState, +} from '../../features/fixtures/redux/fixturesSlice' import guiReducer from './guiSlice' import controlReducer, { ControlState } from './controlSlice' import { LightScene_t } from '../../features/scenes/shared/Scenes' @@ -13,7 +15,6 @@ import mixerReducer from './mixerSlice' import undoable, { StateWithHistory } from 'redux-undo' import { DeviceState } from 'features/midi/redux' import { VisualScene_t, SceneType } from '../../features/scenes/shared/Scenes' -import { DefaultParam, Params } from '../../features/dmx/shared/params' import { SaveInfo } from 'features/fileSaving/shared/save' import eventLogger from './eventLogger' import { FixtureType } from 'features/dmx/shared/dmxFixtures' @@ -248,30 +249,3 @@ export function useActiveFixtureType( export function useDeviceSelector(getVal: (midi: DeviceState) => T) { return useTypedSelector((state) => getVal(state.control.present.device)) } - -export function useBaseParam( - param: DefaultParam | string, - splitIndex: number -): number | undefined { - const baseParam = useActiveLightScene((state) => { - return state.splitScenes[splitIndex].baseParams[param] - }) - return baseParam -} - -export function useBaseParams(splitIndex: number): Params { - 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] - }) -} From a1ab270f2edde4e5e29904e72613f1218213c5e0 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 01:49:16 -0400 Subject: [PATCH 76/98] refactor: revert params types for now --- src/features/modulation/react/ModulationSlider.tsx | 10 +++++++--- src/features/modulation/shared/modulation.ts | 4 ++-- src/features/params/redux/index.ts | 8 +++++--- src/features/params/redux/reducer.ts | 2 +- src/features/params/shared/params.ts | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/features/modulation/react/ModulationSlider.tsx b/src/features/modulation/react/ModulationSlider.tsx index 590e1487..68560398 100644 --- a/src/features/modulation/react/ModulationSlider.tsx +++ b/src/features/modulation/react/ModulationSlider.tsx @@ -10,12 +10,16 @@ import useDragMapped from '../../ui/react/hooks/useDragMapped' import styled from 'styled-components' import Popup from 'features/ui/react/base/Popup' import { indexArray } from 'features/utils/util' -import { getAllParamKeys, useBaseParams, useModParam } from 'features/params/redux' +import { + getAllParamKeys, + useBaseParams, + useModParam, +} from 'features/params/redux' interface Props { splitIndex: number modIndex: number - param: DefaultParam + param: DefaultParam | string } export default function ModulationSlider({ @@ -126,7 +130,7 @@ function ParamEditor({ }: { splitIndex: number modIndex: number - param: DefaultParam + param: DefaultParam | string }) { const modVal = useModParam(param, modIndex, splitIndex) const dispatch = useDispatch() diff --git a/src/features/modulation/shared/modulation.ts b/src/features/modulation/shared/modulation.ts index 7307fd03..22319a0f 100644 --- a/src/features/modulation/shared/modulation.ts +++ b/src/features/modulation/shared/modulation.ts @@ -31,7 +31,7 @@ export function getOutputParams( beats: number, scene: LightScene_t, splitIndex: number, - allParamKeys: DefaultParam[] + allParamKeys: string[] ) { const outputParams = defaultOutputParams() const baseParams = scene.splitScenes[splitIndex].baseParams @@ -49,7 +49,7 @@ export function getOutputParams( function getOutputParam( baseParam: number | undefined, - param: DefaultParam, + param: DefaultParam | string, snapshots: ModSnapshot[] ) { if (baseParam === undefined) return undefined diff --git a/src/features/params/redux/index.ts b/src/features/params/redux/index.ts index c2faaca3..e3dad91d 100644 --- a/src/features/params/redux/index.ts +++ b/src/features/params/redux/index.ts @@ -11,8 +11,10 @@ import { import { useActiveLightScene } from 'renderer/redux/store' import { useRealtimeSelector } from 'renderer/redux/realtimeStore' -export function getAllParamKeys(dmx: DmxState): DefaultParam[] { - return defaultParamsList.concat(Array.from(getCustomChannels(dmx))) +export function getAllParamKeys(dmx: DmxState): string[] { + return (defaultParamsList as string[]).concat( + Array.from(getCustomChannels(dmx)) + ) } export function useBaseParam( @@ -33,7 +35,7 @@ export function useBaseParams(splitIndex: number): Partial { } export function useModParam( - param: DefaultParam, + param: DefaultParam | string, modIndex: number, splitIndex: number ) { diff --git a/src/features/params/redux/reducer.ts b/src/features/params/redux/reducer.ts index 3823ea30..e3e6cef1 100644 --- a/src/features/params/redux/reducer.ts +++ b/src/features/params/redux/reducer.ts @@ -20,7 +20,7 @@ type ParamAction = PayloadAction<{ export interface SetModulationPayload { splitIndex: number modIndex: number - param: DefaultParam + param: DefaultParam | string value: number | undefined } diff --git a/src/features/params/shared/params.ts b/src/features/params/shared/params.ts index 93cfee58..1afeec85 100644 --- a/src/features/params/shared/params.ts +++ b/src/features/params/shared/params.ts @@ -13,7 +13,7 @@ export type DefaultParam = | 'yAxis' | 'xMirror' -export type Params = { [key in DefaultParam]: number | undefined } +export type Params = { [key: string]: number | undefined } export function initBaseParams(): Partial { return { From 3412196ae8b4b9ff605b362ee4a6eec365292176 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 02:46:13 -0400 Subject: [PATCH 77/98] refactor: more strict params --- src/features/dmx/channel.config.ts | 4 ++-- src/features/led/shared/ledFixtures.ts | 4 ++-- src/features/params/engine/index.ts | 14 +++++++------- src/features/params/shared/params.ts | 14 +++++++++++--- src/features/params/shared/utils.ts | 4 ++-- src/features/visualizer/threejs/UpdateResource.ts | 8 ++++---- 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/features/dmx/channel.config.ts b/src/features/dmx/channel.config.ts index 243af672..ebf4931c 100644 --- a/src/features/dmx/channel.config.ts +++ b/src/features/dmx/channel.config.ts @@ -9,14 +9,14 @@ import { ChannelType, GetFixturePayload } from './shared/dmxFixtures' import { findClosest } from 'features/utils/math/util' import { getColorChannelLevel } from './shared/dmxColors' -import { Params, getParam } from '../params/shared/params' +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 + params: Partial fixture: FlattenedFixture master: number randomizerLevel: number diff --git a/src/features/led/shared/ledFixtures.ts b/src/features/led/shared/ledFixtures.ts index 8fd6e0e0..b66780e3 100644 --- a/src/features/led/shared/ledFixtures.ts +++ b/src/features/led/shared/ledFixtures.ts @@ -1,7 +1,7 @@ import { distanceBetween, pLerp, Point } from '../../utils/math/point' import { BaseColors, getBaseColorsFromHsv } from '../../utils/baseColors' import { getWindowMultiplier2D } from '../../dmx/shared/dmxUtil' -import { getParam, Params } from '../../params/shared/params' +import { getParam, StrictParams } from '../../params/shared/params' import { indexArray } from '../../utils/util' import { Window2D_t } from '../../shared/shared/window' import { getMovingWindow } from 'features/params/engine' @@ -29,7 +29,7 @@ export function initLedFixture(): LedFixture { } export function getLedValues( - params: Partial, + params: Partial, ledFixture: LedFixture, master: number ): BaseColors[] { diff --git a/src/features/params/engine/index.ts b/src/features/params/engine/index.ts index 63774517..81a1baff 100644 --- a/src/features/params/engine/index.ts +++ b/src/features/params/engine/index.ts @@ -1,15 +1,15 @@ -import { Window2D_t } from "features/shared/shared/window" -import { Params, getParam } from "../shared/params" -import { Normalized } from "features/utils/math/util" -import { getWindowMultiplier2D } from "features/dmx/shared/dmxUtil" -import { applyRandomization } from "features/bpm/shared/randomizer" +import { Window2D_t } from 'features/shared/shared/window' +import { StrictParams, getParam } from '../shared/params' +import { Normalized } 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 { +export function getMovingWindow(params: Partial): Window2D_t { const x = params.x !== undefined && params.width !== undefined ? { pos: params.x, width: params.width } @@ -27,7 +27,7 @@ export function getMovingWindow(params: Partial): Window2D_t { } export function getBrightness( - params: Partial, + params: Partial, randomizerLevel: Normalized, fixtureWindow: Window2D_t, movingWindow: Window2D_t diff --git a/src/features/params/shared/params.ts b/src/features/params/shared/params.ts index 1afeec85..47a6bf0b 100644 --- a/src/features/params/shared/params.ts +++ b/src/features/params/shared/params.ts @@ -1,3 +1,5 @@ +import { Pretty } from 'features/shared/shared/type-utils' + export type DefaultParam = | 'hue' | 'saturation' @@ -13,9 +15,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(): Partial { +export function initBaseParams(): Partial { return { hue: 0.5, saturation: 0.5, @@ -58,7 +63,10 @@ const defaultParams: { [key in DefaultParam]: number } = { xMirror: 0.0, } -export function getParam(params: Partial, param: DefaultParam): number { +export function getParam( + params: Partial, + param: DefaultParam +): number { return params[param] ?? defaultParams[param] } diff --git a/src/features/params/shared/utils.ts b/src/features/params/shared/utils.ts index df4c58d5..c4610918 100644 --- a/src/features/params/shared/utils.ts +++ b/src/features/params/shared/utils.ts @@ -1,7 +1,7 @@ import { BaseColors, hsv2rgb } from 'features/utils/baseColors' -import { Params } from './params' +import { StrictParams } from './params' -export function getBaseColors(params: Partial): BaseColors { +export function getBaseColors(params: Partial): BaseColors { const [r, g, b] = hsv2rgb( params.hue ?? 0, params.saturation ?? 0, diff --git a/src/features/visualizer/threejs/UpdateResource.ts b/src/features/visualizer/threejs/UpdateResource.ts index b845fc81..bc113d3d 100644 --- a/src/features/visualizer/threejs/UpdateResource.ts +++ b/src/features/visualizer/threejs/UpdateResource.ts @@ -1,4 +1,4 @@ -import { Params } from '../../params/shared/params' +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' @@ -8,7 +8,7 @@ import { Range, rLerp } from 'features/utils/math/range' interface UpdateData { dt: number time: TimeState - params: Partial + params: Partial scene: LightScene_t master: number size: Size @@ -17,7 +17,7 @@ interface UpdateData { export default class UpdateResource { dt: number time: TimeState - params: Partial + 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) { From 9b88dee2e4846769bb2eb8419c45da239b8f0f74 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 04:45:41 -0400 Subject: [PATCH 78/98] feat: uniform api --- src/features/fileSaving/react/SaveLoad.tsx | 22 ++++--- src/features/fileSaving/react/autosave.ts | 16 ------ src/features/menu/react/StatusBar/Bpm.tsx | 2 +- .../menu/react/StatusBar/LinkButton.tsx | 2 +- .../menu/react/StatusBar/StartStopButton.tsx | 2 +- .../react/StatusBar/StartStopSyncButton.tsx | 2 +- .../menu/react/StatusBar/TapTempo.tsx | 2 +- src/features/shared/engine/emissions.ts | 19 ++++--- src/features/shared/engine/ipc_channels.ts | 6 +- src/features/visualizer/react/FileList.tsx | 2 +- .../visualizer/react/OpenVisualizerButton.tsx | 2 +- src/main/engine/api/core.ts | 6 +- src/renderer/api/core/ipcMutations.ts | 57 +++++++++++++++++++ src/renderer/api/index.ts | 4 +- src/renderer/api/mutations.ts | 23 ++++---- src/renderer/api/queries.ts | 18 +++--- src/renderer/api/subscriptions.ts | 6 +- 17 files changed, 120 insertions(+), 71 deletions(-) create mode 100644 src/renderer/api/core/ipcMutations.ts diff --git a/src/features/fileSaving/react/SaveLoad.tsx b/src/features/fileSaving/react/SaveLoad.tsx index 11fe5861..b8f3d25d 100644 --- a/src/features/fileSaving/react/SaveLoad.tsx +++ b/src/features/fileSaving/react/SaveLoad.tsx @@ -3,8 +3,12 @@ import styled from 'styled-components' import SaveIcon from '@mui/icons-material/Save' import LoadIcon from '@mui/icons-material/FileOpen' import IconButton from '@mui/material/IconButton' -import { store, applySave, useTypedSelector } from '../../../renderer/redux/store' -import { saveFile, loadFile, captivateFileFilters } from './autosave' +import { + store, + applySave, + useTypedSelector, +} from '../../../renderer/redux/store' +import { captivateFileFilters } from './autosave' import Popup from 'features/ui/react/base/Popup' import { Button, Checkbox } from '@mui/material' import { @@ -17,9 +21,10 @@ import { } from 'features/fileSaving/shared/save' import { useDispatch } from 'react-redux' import { setSaving, setLoading } from 'renderer/redux/guiSlice' +import { queries } from 'renderer/api' export async function load() { - const serializedSaveState = await loadFile('Load Scenes', [ + const serializedSaveState = await queries.load_file('Load Scenes', [ captivateFileFilters.captivate, ]) const saveState: SaveState = JSON.parse(serializedSaveState) @@ -40,12 +45,11 @@ function save(config: SaveConfig) { } const serializedSaveState = JSON.stringify(saveState) - saveFile('Save Scenes', serializedSaveState, [captivateFileFilters.captivate]) - .then((err) => { - if (err) { - console.error(err) - } - }) + queries + .save_file('Save Scenes', serializedSaveState, [ + captivateFileFilters.captivate, + ]) + .then(() => {}) .catch((err) => { console.error(err) }) diff --git a/src/features/fileSaving/react/autosave.ts b/src/features/fileSaving/react/autosave.ts index 04753d31..67244372 100644 --- a/src/features/fileSaving/react/autosave.ts +++ b/src/features/fileSaving/react/autosave.ts @@ -5,7 +5,6 @@ import { getCleanReduxState, store, } from '../../../renderer/redux/store' -import ipcChannels from '../../shared/engine/ipc_channels' import AutoSavedVal, { printTimePassed } from './AutoSavedVal' import fixState from '../../shared/redux/fixState' import defaultState from '../../../renderer/redux/defaultState' @@ -64,18 +63,3 @@ const ipcRenderer = window.electron.ipcRenderer export const captivateFileFilters = { captivate: { name: 'Captivate', extensions: ['captivate'] }, } - -export async function loadFile( - title: string, - fileFilters: Electron.FileFilter[] -): Promise { - return ipcRenderer.invoke(ipcChannels.load_file, title, fileFilters) -} - -export async function saveFile( - title: string, - data: string, - fileFilters: Electron.FileFilter[] -): Promise { - return ipcRenderer.invoke(ipcChannels.save_file, title, data, fileFilters) -} diff --git a/src/features/menu/react/StatusBar/Bpm.tsx b/src/features/menu/react/StatusBar/Bpm.tsx index 4790eba3..b0de578b 100644 --- a/src/features/menu/react/StatusBar/Bpm.tsx +++ b/src/features/menu/react/StatusBar/Bpm.tsx @@ -9,7 +9,7 @@ export default function BPM() { const [dragContainer, onMouseDown] = useDragBasic((e) => { const dx = e.movementX / 3 const dy = -e.movementY / 3 - api.mutations.send_user_command({ type: 'IncrementTempo', amount: dx + dy }) + api.mutations.user_command({ type: 'IncrementTempo', amount: dx + dy }) }) return ( diff --git a/src/features/menu/react/StatusBar/LinkButton.tsx b/src/features/menu/react/StatusBar/LinkButton.tsx index 2b3a0844..461a7868 100644 --- a/src/features/menu/react/StatusBar/LinkButton.tsx +++ b/src/features/menu/react/StatusBar/LinkButton.tsx @@ -18,7 +18,7 @@ export default function LinkButton() { return (
{ - api.mutations.send_user_command({ type: 'SetLinkEnabled', isEnabled: !isEnabled }) + api.mutations.user_command({ type: 'SetLinkEnabled', isEnabled: !isEnabled }) }} style={style} > diff --git a/src/features/menu/react/StatusBar/StartStopButton.tsx b/src/features/menu/react/StatusBar/StartStopButton.tsx index 40d1a24e..bfb66a97 100644 --- a/src/features/menu/react/StatusBar/StartStopButton.tsx +++ b/src/features/menu/react/StatusBar/StartStopButton.tsx @@ -10,7 +10,7 @@ export default function StartStopButton() { return ( - api.mutations.send_user_command({ + api.mutations.user_command({ type: 'SetIsPlaying', isPlaying: !time.isPlaying, }) diff --git a/src/features/menu/react/StatusBar/StartStopSyncButton.tsx b/src/features/menu/react/StatusBar/StartStopSyncButton.tsx index 70c1997d..3d88d28e 100644 --- a/src/features/menu/react/StatusBar/StartStopSyncButton.tsx +++ b/src/features/menu/react/StatusBar/StartStopSyncButton.tsx @@ -17,7 +17,7 @@ export default function StartStopSyncButton({}: Props) { return ( - api.mutations.send_user_command({ + api.mutations.user_command({ type: 'EnableStartStopSync', isEnabled: !sssEnabled, }) diff --git a/src/features/menu/react/StatusBar/TapTempo.tsx b/src/features/menu/react/StatusBar/TapTempo.tsx index d098f27e..d4bb2ded 100644 --- a/src/features/menu/react/StatusBar/TapTempo.tsx +++ b/src/features/menu/react/StatusBar/TapTempo.tsx @@ -12,7 +12,7 @@ export default function TapTempo({}: Props) { type: 'TapTempo', }} > - {/* { - api.queries.getLocalFilepaths('Select Media', localMediaFileFilters) + api.queries.get_local_filepaths('Select Media', localMediaFileFilters) .then(onAddSuccess) .catch((_err) => {}) } diff --git a/src/features/visualizer/react/OpenVisualizerButton.tsx b/src/features/visualizer/react/OpenVisualizerButton.tsx index b9abeb01..6d2b982e 100644 --- a/src/features/visualizer/react/OpenVisualizerButton.tsx +++ b/src/features/visualizer/react/OpenVisualizerButton.tsx @@ -9,7 +9,7 @@ interface Props {} export default function OpenVisualizerButton({}: Props) { return ( - + diff --git a/src/main/engine/api/core.ts b/src/main/engine/api/core.ts index 4b774c99..c40e9af6 100644 --- a/src/main/engine/api/core.ts +++ b/src/main/engine/api/core.ts @@ -77,8 +77,10 @@ export const createQuery = < channel: Channel resolve: ( context: Context, - ...args: API['renderer']['queries'][Channel] - ) => Promise | any + ...args: API['renderer']['queries'][Channel]['input'] + ) => + | Promise + | API['renderer']['queries'][Channel]['output'] }) => { return { [config.channel]: ( 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/index.ts b/src/renderer/api/index.ts index ed87f3fe..d0296553 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -1,3 +1,3 @@ -export * as mutations from './mutations' +export { mutations } from './mutations' export * as subscriptions from './subscriptions' -export * as queries from './queries' +export { queries } from './queries' diff --git a/src/renderer/api/mutations.ts b/src/renderer/api/mutations.ts index 49aaebec..75902041 100644 --- a/src/renderer/api/mutations.ts +++ b/src/renderer/api/mutations.ts @@ -1,18 +1,15 @@ -import ipc_channels, { - UserCommand, -} from '../../features/shared/engine/ipc_channels' import { IpcRenderer } from 'electron' -import { CleanReduxState } from '../redux/store' +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 function send_control_state(cleanState: CleanReduxState) { - ipcRenderer.send(ipc_channels.new_control_state, cleanState) -} -export function send_user_command(command: UserCommand) { - ipcRenderer.send(ipc_channels.user_command, command) -} -export function send_open_visualizer() { - ipcRenderer.send(ipc_channels.open_visualizer) -} +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 index 50b44f96..9d531741 100644 --- a/src/renderer/api/queries.ts +++ b/src/renderer/api/queries.ts @@ -1,16 +1,12 @@ -import ipc_channels from '../../features/shared/engine/ipc_channels' 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 async function getLocalFilepaths( - title: string, - fileFilters: Electron.FileFilter[] -): Promise { - return ipcRenderer.invoke( - ipc_channels.get_local_filepaths, - title, - fileFilters - ) -} +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 index afe46d82..2cd883a3 100644 --- a/src/renderer/api/subscriptions.ts +++ b/src/renderer/api/subscriptions.ts @@ -20,7 +20,7 @@ import { redoAction, } from '../../features/scenes/react/controls/UndoRedo' import { getSaveConfig } from 'features/fileSaving/shared/save' -import * as mutations from './mutations' +import { mutations } from './mutations' import { autoSave } from '../../features/fileSaving/react/autosave' import { animationLoop } from 'features/shared/react' import { API } from 'features/shared/engine/emissions' @@ -84,9 +84,9 @@ export const subcribe = ({ store }: Context) => { realtimeStore.dispatch(updateRealtimeStore(getUpatedRealtimeState())) }) - mutations.send_control_state(getCleanReduxState(store.getState())) + mutations.new_control_state(getCleanReduxState(store.getState())) store.subscribe(() => - mutations.send_control_state(getCleanReduxState(store.getState())) + mutations.new_control_state(getCleanReduxState(store.getState())) ) } From 52d715256f8481d5c26ff0002de06641571d88c1 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 06:33:51 -0400 Subject: [PATCH 79/98] remove: param dependancy from modulation engine --- src/features/modulation/engine/index.ts | 39 +++++++++++++ src/features/modulation/shared/modulation.ts | 61 ++------------------ src/features/params/engine/index.ts | 41 ++++++++++++- src/features/params/shared/params.ts | 4 -- src/main/engine/engine.ts | 13 +++-- 5 files changed, 93 insertions(+), 65 deletions(-) create mode 100644 src/features/modulation/engine/index.ts diff --git a/src/features/modulation/engine/index.ts b/src/features/modulation/engine/index.ts new file mode 100644 index 00000000..fa6813c0 --- /dev/null +++ b/src/features/modulation/engine/index.ts @@ -0,0 +1,39 @@ +import { Modulation } from '../../params/shared/params' +import { GetValue } from '../shared/oscillator' +import { LightScene_t } from '../../scenes/shared/Scenes' + +interface ModSnapshot { + modulation: Modulation + lfoVal: number +} + +export const createModulationTransformer = ({ + scene, + splitIndex, + beats, +}: { + scene: LightScene_t + splitIndex: number + beats: number +}) => { + const snapshots: ModSnapshot[] = scene.modulators.map((modulator) => ({ + modulation: modulator.splitModulations[splitIndex], + lfoVal: GetValue(modulator.lfo, beats), + })) + + return { + transform({ baseParam, param }: { baseParam: number; param: string }) { + return snapshots.reduce((sum, { modulation, lfoVal }) => { + const modAmount = modulation[param] + if (modAmount === undefined) { + return sum + } else { + const modAmountMapped = modAmount * 2 - 1 // from -1 to 1 + const lfoValMapped = lfoVal * 2 - 1 // from -1 to 1 + const addedModulation = (modAmountMapped * lfoValMapped) / 2 // from -0.5 to -0.5 + return sum + addedModulation + } + }, baseParam) + }, + } +} diff --git a/src/features/modulation/shared/modulation.ts b/src/features/modulation/shared/modulation.ts index 22319a0f..71b198ec 100644 --- a/src/features/modulation/shared/modulation.ts +++ b/src/features/modulation/shared/modulation.ts @@ -1,18 +1,15 @@ -import { - initModulation, - DefaultParam, - Modulation, -} from '../../params/shared/params' -import { Lfo, GetValue, GetRamp } from './oscillator' -import { LightScene_t } from '../../scenes/shared/Scenes' -import { clampNormalized } from '../../utils/math/util' -import { defaultOutputParams } from '../../params/shared/params' +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(), @@ -21,49 +18,3 @@ export function initModulator(splitCount: number): Modulator { .map(() => initModulation()), } } - -interface ModSnapshot { - modulation: Modulation - lfoVal: number -} - -export function getOutputParams( - beats: number, - scene: LightScene_t, - splitIndex: number, - allParamKeys: string[] -) { - const outputParams = defaultOutputParams() - const baseParams = scene.splitScenes[splitIndex].baseParams - const snapshots: ModSnapshot[] = scene.modulators.map((modulator) => ({ - modulation: modulator.splitModulations[splitIndex], - lfoVal: GetValue(modulator.lfo, beats), - })) - - allParamKeys.forEach((param) => { - outputParams[param] = getOutputParam(baseParams[param], param, snapshots) - }) - - return outputParams -} - -function getOutputParam( - baseParam: number | undefined, - param: DefaultParam | string, - snapshots: ModSnapshot[] -) { - if (baseParam === undefined) return undefined - return clampNormalized( - snapshots.reduce((sum, { modulation, lfoVal }) => { - const modAmount = modulation[param] - if (modAmount === undefined) { - return sum - } else { - const modAmountMapped = modAmount * 2 - 1 // from -1 to 1 - const lfoValMapped = lfoVal * 2 - 1 // from -1 to 1 - const addedModulation = (modAmountMapped * lfoValMapped) / 2 // from -0.5 to -0.5 - return sum + addedModulation - } - }, baseParam) - ) -} diff --git a/src/features/params/engine/index.ts b/src/features/params/engine/index.ts index 81a1baff..f591f47a 100644 --- a/src/features/params/engine/index.ts +++ b/src/features/params/engine/index.ts @@ -1,8 +1,14 @@ import { Window2D_t } from 'features/shared/shared/window' -import { StrictParams, getParam } from '../shared/params' -import { Normalized } from 'features/utils/math/util' +import { + DefaultParam, + 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' +import { LightScene_t } from 'features/scenes/shared/Scenes' /** * used on engine side on led and dmx @@ -41,3 +47,34 @@ export function getBrightness( getParam(params, 'randomize') ) } + +export const createOutputParams = ( + { + scene, + splitIndex, + allParamKeys, + }: { + scene: LightScene_t + splitIndex: number + 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() + const baseParams = scene.splitScenes[splitIndex].baseParams + allParamKeys.forEach((param) => { + outputParams[param] = _getOutputParam(baseParams[param], param) + }) + + return outputParams +} diff --git a/src/features/params/shared/params.ts b/src/features/params/shared/params.ts index 47a6bf0b..877c243b 100644 --- a/src/features/params/shared/params.ts +++ b/src/features/params/shared/params.ts @@ -105,7 +105,3 @@ export const defaultParamsList: DefaultParam[] = [ ] export type Modulation = Partial - -export function initModulation(): Modulation { - return {} -} diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index ef5de189..65df957a 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -14,7 +14,7 @@ import { resizeRandomizer, updateIndexes, } from '../../features/bpm/shared/randomizer' -import { getOutputParams } from '../../features/modulation/shared/modulation' +import { createModulationTransformer } from '../../features/modulation/engine' import { handleMessage } from 'features/midi/engine/handleMidi' import { VisualizerContainer } from '../../features/visualizer/engine/createVisualizerWindow' import { calculateDmx } from 'features/dmx/engine/dmxEngine' @@ -36,6 +36,7 @@ import { } from 'features/bpm/engine/Link' import { createApi, IPC_Callbacks } from './api' import { getAllParamKeys } from 'features/params/redux' +import { createOutputParams } from 'features/params/engine' let _realtimeState: RealtimeState = initRealtimeState() @@ -221,12 +222,16 @@ function getNextRealtimeState( const splitStates: SplitState[] = scene.splitScenes.map( (splitScene, splitIndex) => { - const splitOutputParams = getOutputParams( - nextTimeState.beats, + const modTransformer = createModulationTransformer({ + beats: nextTimeState.beats, scene, splitIndex, - allParamKeys + }) + const splitOutputParams = createOutputParams( + { allParamKeys, scene, splitIndex }, + (options) => modTransformer.transform(options) ) + let splitSceneFixtures = getFixturesInGroups(fixtures, splitScene.groups) let splitSceneFixturesWithinEpicness = splitSceneFixtures.filter( (fixture) => fixture.intensity <= (splitOutputParams.intensity ?? 1) From 01a3edae1b8b5ab57295288fa5313e52df87b113 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 15:46:52 -0400 Subject: [PATCH 80/98] refactor: consolidate channel react components into single config --- src/features/dmx/channel.config.react.tsx | 146 ++++++++++++ .../react/fixtures/channels/editor/index.tsx | 211 +++++------------- src/features/params/shared/params.ts | 17 +- 3 files changed, 206 insertions(+), 168 deletions(-) create mode 100644 src/features/dmx/channel.config.react.tsx diff --git a/src/features/dmx/channel.config.react.tsx b/src/features/dmx/channel.config.react.tsx new file mode 100644 index 00000000..1f29c37a --- /dev/null +++ b/src/features/dmx/channel.config.react.tsx @@ -0,0 +1,146 @@ +import HSpad, { ColorChannelProps } from 'features/ui/react/base/HSpad' +import { + DmxNumberField, + Info, + Row, + createChannelComponents, +} from './react/fixtures/channels/editor' +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, + }) + } + /> + + ) + }, + other: ({ ch }) => { + return + }, + strobe: ({ ch }) => { + return ( + <> + + + + + ) + }, + axis: ({ ch, updateChannel }) => { + return ( + <> + + Direction: + + updateChannel({ + ...ch, + name: newName, + }) + } + /> + + + + + + + ) + }, +}) diff --git a/src/features/dmx/react/fixtures/channels/editor/index.tsx b/src/features/dmx/react/fixtures/channels/editor/index.tsx index 749c518c..85ff2261 100644 --- a/src/features/dmx/react/fixtures/channels/editor/index.tsx +++ b/src/features/dmx/react/fixtures/channels/editor/index.tsx @@ -5,24 +5,36 @@ import { FixtureChannel, channelTypes, initFixtureChannel, - AxisDir, - axisDirList, DMX_MAX_VALUE, DMX_MIN_VALUE, + ChannelType, } from '../../../../shared/dmxFixtures' import NumberField from '../../../../../ui/react/base/NumberField' -import Input from '../../../../../ui/react/base/Input' import { editFixtureChannel } from '../../../../../fixtures/redux/fixturesSlice' -import Checkbox from '../../../../../ui/react/base/LabelledCheckbox' -import HSpad, { ColorChannelProps } from 'features/ui/react/base/HSpad' import { FixtureChannelItemProps } from '../list/FixtureChannelItem' -import ColorMapChannel from './ColorMapChannel' -import ColorPicker from 'features/ui/react/base/ColorPicker' + +import { FC, createContext, useContext } from 'react' +import { ChannelComponents } from 'features/dmx/channel.config.react' interface Props extends FixtureChannelItemProps { ch: FixtureChannel } +export const createChannelComponents = < + T extends { + [k in ChannelType]: FC<{ + ch: Extract + updateChannel(newChannel: FixtureChannel): void + fixtureID: string + channelIndex: number + }> + } +>( + config: T +) => { + return config +} + export default function FixtureChannelPopup(props: Props) { const { ch, fixtureID, channelIndex } = props const dispatch = useDispatch() @@ -51,6 +63,32 @@ export default function FixtureChannelPopup(props: Props) { ) } +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, + }) + } + /> + ) +} + function Fields({ ch, fixtureID, channelIndex }: Props) { const dispatch = useDispatch() @@ -63,152 +101,23 @@ function Fields({ ch, fixtureID, channelIndex }: Props) { }) ) } + const Component = ChannelComponents[ch.type] - function dmxNumberField( - ch: Ch, - field: Key, - label: string - ) { - return ( - - updateChannel({ - ...ch, - [field]: newVal, - }) - } - /> - ) - } - - if (ch.type === 'color') { - 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)} */} - - - - ) - } else if (ch.type === 'master') { - return ( - <> - {dmxNumberField(ch, 'min', 'Min')} - - {dmxNumberField(ch, 'max', 'MAX')} - - - updateChannel({ - ...ch, - isOnOff, - }) - } - /> - - ) - } else if (ch.type === 'other') { - return dmxNumberField(ch, 'default', 'Default') - } else if (ch.type === 'strobe') { - return ( - <> - {dmxNumberField(ch, 'default_solid', 'Solid')} - - {dmxNumberField(ch, 'default_strobe', 'Strobe')} - - ) - } else if (ch.type === 'axis') { - return ( - <> - - Direction: - - updateChannel({ - ...ch, - name: newName, - }) - } - /> - {dmxNumberField(ch, 'default', 'Default')} - - {dmxNumberField(ch, 'min', 'Min')} - - {dmxNumberField(ch, 'max', 'Max')} - - ) - } else { - return null - } + + ) } -const Row = styled.div` +export const Row = styled.div` display: flex; align-items: center; ` @@ -219,11 +128,7 @@ const Content = styled.div` } ` -const Sp2 = styled.div` - width: 1rem; -` - -const Info = styled.div` +export const Info = styled.div` font-size: 0.9rem; margin-right: 0.5rem; ` diff --git a/src/features/params/shared/params.ts b/src/features/params/shared/params.ts index 877c243b..f1416dc8 100644 --- a/src/features/params/shared/params.ts +++ b/src/features/params/shared/params.ts @@ -31,19 +31,8 @@ export function initBaseParams(): Partial { // 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, } } @@ -72,9 +61,7 @@ export function getParam( export function defaultOutputParams(): Partial { return { - hue: 0.5, - saturation: 0.5, - brightness: 0.5, + ...initBaseParams(), // x: 0.5, // width: 1.0, // y: 0.5, From 83acee4b0a0b1120f8641973acafccb0bee5333f Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 17:17:43 -0400 Subject: [PATCH 81/98] refactor: dmxOut --- src/features/dmx/channel.config.ts | 5 +- src/features/dmx/engine/dmxEngine.ts | 115 ++++++++++++++------------- src/main/engine/engine.ts | 10 ++- 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/src/features/dmx/channel.config.ts b/src/features/dmx/channel.config.ts index ebf4931c..c8875371 100644 --- a/src/features/dmx/channel.config.ts +++ b/src/features/dmx/channel.config.ts @@ -56,9 +56,12 @@ const createChannelConfig = < } /** - * one possible issue is that engine code and renderer code might be bundled together + * 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: () => ({ diff --git a/src/features/dmx/engine/dmxEngine.ts b/src/features/dmx/engine/dmxEngine.ts index b04e742d..5ec31919 100644 --- a/src/features/dmx/engine/dmxEngine.ts +++ b/src/features/dmx/engine/dmxEngine.ts @@ -7,75 +7,80 @@ import { 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 { getFixturesInGroups, flatten_fixtures } from '../shared/dmxUtil' import { indexArray, zip } from '../../utils/util' -import { TimeState } from '../../bpm/shared/TimeState' import { SplitState } from 'renderer/redux/realtimeStore' import { getDmxValue } from './channelUtils' -export function calculateDmx( - state: CleanReduxState, - splitStates: SplitState[], - timeState: TimeState -): number[] { +export function getChannels({ state }: { state: CleanReduxState }) { const universe = state.dmx.universe const all_fixtures = flatten_fixtures(universe, state.dmx.fixtureTypesByID) - let channels = Array(DMX_NUM_CHANNELS).fill(0) + const channels = Array(DMX_NUM_CHANNELS).fill(0) - if (timeState.isPlaying) { - const scenes = state.control.light - const activeScene = scenes.byId[scenes.active] + return { + channels, + all_fixtures, + } +} - 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 +type OutChannelConfig = ReturnType + +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) + for (const [{ outputParams, randomizer }, splitScene] of zip( + splitStates, + activeScene.splitScenes + )) { + const splitGroups = splitScene.groups - applyFixtures(splitSceneFixtures, outputParams, randomizer) - } + const splitSceneFixtures = getFixturesInGroups(all_fixtures, splitGroups) - // Apply any overwrites - indexArray(DMX_NUM_CHANNELS).forEach((i) => { - const overwrite = state.mixer.overwrites[i] - if (overwrite !== undefined) { - channels[i] = overwrite * DMX_MAX_VALUE - } - }) + applyFixtures(splitSceneFixtures, outputParams, randomizer) } - return channels + // 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/main/engine/engine.ts b/src/main/engine/engine.ts index 65df957a..e8a0c7a1 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -17,7 +17,7 @@ import { import { createModulationTransformer } from '../../features/modulation/engine' import { handleMessage } from 'features/midi/engine/handleMidi' import { VisualizerContainer } from '../../features/visualizer/engine/createVisualizerWindow' -import { calculateDmx } from 'features/dmx/engine/dmxEngine' +import { calculateDmxOut, getChannels } from 'features/dmx/engine/dmxEngine' import { handleAutoScene } from '../../features/scenes/engine/autoScene' import { setActiveScene } from '../../renderer/redux/controlSlice' import { @@ -258,9 +258,15 @@ function getNextRealtimeState( } ) + const outChannelConfig = getChannels({ state: controlState }) + + if (nextTimeState.isPlaying) { + calculateDmxOut({ splitStates, state: controlState }, outChannelConfig) + } + return { time: nextTimeState, - dmxOut: calculateDmx(controlState, splitStates, nextTimeState), + dmxOut: outChannelConfig.channels, splitStates, } } From dec72ee18a46410dadf18e665c2651a1152628bc Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 17:24:43 -0400 Subject: [PATCH 82/98] refactor: dmx engine --- src/features/dmx/engine/channelUtils.ts | 29 ------------------------- src/features/dmx/engine/dmxEngine.ts | 28 +++++++++++++++++++++++- src/main/engine/engine.ts | 4 ++-- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/features/dmx/engine/channelUtils.ts b/src/features/dmx/engine/channelUtils.ts index c77ad118..7b195608 100644 --- a/src/features/dmx/engine/channelUtils.ts +++ b/src/features/dmx/engine/channelUtils.ts @@ -1,43 +1,14 @@ import { - DmxValue, DMX_MAX_VALUE, - FixtureChannel, - DMX_DEFAULT_VALUE, AxisDir, DMX_MIN_VALUE, FlattenedFixture, GetFixturePayload, - ChannelType, } from '../shared/dmxFixtures' -import { Params } from '../../params/shared/params' import { Normalized } from '../../utils/math/util' import { rLerp } from '../../utils/math/range' import { applyMirror } from '../shared/dmxUtil' -import { ChannelConfig, channelConfig } from '../channel.config' -import { getMovingWindow } from 'features/params/engine' - -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 calculate_axis_channel( ch: GetFixturePayload<'axis'>, diff --git a/src/features/dmx/engine/dmxEngine.ts b/src/features/dmx/engine/dmxEngine.ts index 5ec31919..9025fbea 100644 --- a/src/features/dmx/engine/dmxEngine.ts +++ b/src/features/dmx/engine/dmxEngine.ts @@ -3,6 +3,9 @@ import { 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' @@ -10,7 +13,8 @@ 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 { getDmxValue } from './channelUtils' +import { ChannelConfig, channelConfig } from '../channel.config' +import { getMovingWindow } from 'features/params/engine' export function getChannels({ state }: { state: CleanReduxState }) { const universe = state.dmx.universe @@ -26,6 +30,28 @@ export function getChannels({ state }: { state: CleanReduxState }) { 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, diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index e8a0c7a1..b65140c3 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -232,8 +232,8 @@ function getNextRealtimeState( (options) => modTransformer.transform(options) ) - let splitSceneFixtures = getFixturesInGroups(fixtures, splitScene.groups) - let splitSceneFixturesWithinEpicness = splitSceneFixtures.filter( + const splitSceneFixtures = getFixturesInGroups(fixtures, splitScene.groups) + const splitSceneFixturesWithinEpicness = splitSceneFixtures.filter( (fixture) => fixture.intensity <= (splitOutputParams.intensity ?? 1) ) From 61f0879bbecdb0b72376b898da69625378c1dcf9 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 17:37:48 -0400 Subject: [PATCH 83/98] refactor: more cleanup --- src/features/devices/react/overlays/Devices.tsx | 2 +- src/{renderer => features/dmx}/redux/mixerSlice.ts | 0 src/features/fileSaving/react/SaveLoad.tsx | 2 +- src/features/fileSaving/react/overlays/NewProjectDialog.tsx | 2 +- src/features/menu/engine/menu.ts | 2 +- src/features/menu/react/BlackoutButton.tsx | 2 +- src/features/menu/react/SideBar/index.tsx | 2 +- src/features/menu/react/StatusBar/index.tsx | 2 +- src/features/modulation/redux/reducer.ts | 2 ++ src/{renderer => features/ui}/redux/guiSlice.ts | 4 ++-- src/renderer/api/subscriptions.ts | 2 +- src/renderer/overlays/FullscreenOverlay.tsx | 2 +- src/renderer/pages/mixer/page.tsx | 2 +- src/renderer/redux/initState.ts | 4 ++-- src/renderer/redux/store.ts | 4 ++-- 15 files changed, 18 insertions(+), 16 deletions(-) rename src/{renderer => features/dmx}/redux/mixerSlice.ts (100%) rename src/{renderer => features/ui}/redux/guiSlice.ts (95%) diff --git a/src/features/devices/react/overlays/Devices.tsx b/src/features/devices/react/overlays/Devices.tsx index 3f448d97..62f8ecdd 100644 --- a/src/features/devices/react/overlays/Devices.tsx +++ b/src/features/devices/react/overlays/Devices.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' import { useControlSelector, useTypedSelector } from '../../../../renderer/redux/store' -import { setConnectionsMenu } from '../../../../renderer/redux/guiSlice' +import { setConnectionsMenu } from '../../../ui/redux/guiSlice' import { useDispatch } from 'react-redux' import { setDmxConnectable, setMidiConnectable } from '../../../../renderer/redux/controlSlice' import CloseIcon from '@mui/icons-material/Close' diff --git a/src/renderer/redux/mixerSlice.ts b/src/features/dmx/redux/mixerSlice.ts similarity index 100% rename from src/renderer/redux/mixerSlice.ts rename to src/features/dmx/redux/mixerSlice.ts diff --git a/src/features/fileSaving/react/SaveLoad.tsx b/src/features/fileSaving/react/SaveLoad.tsx index b8f3d25d..884dc6c4 100644 --- a/src/features/fileSaving/react/SaveLoad.tsx +++ b/src/features/fileSaving/react/SaveLoad.tsx @@ -20,7 +20,7 @@ import { getSaveConfig, } from 'features/fileSaving/shared/save' import { useDispatch } from 'react-redux' -import { setSaving, setLoading } from 'renderer/redux/guiSlice' +import { setSaving, setLoading } from 'features/ui/redux/guiSlice' import { queries } from 'renderer/api' export async function load() { diff --git a/src/features/fileSaving/react/overlays/NewProjectDialog.tsx b/src/features/fileSaving/react/overlays/NewProjectDialog.tsx index b4b4eec8..50c3b053 100644 --- a/src/features/fileSaving/react/overlays/NewProjectDialog.tsx +++ b/src/features/fileSaving/react/overlays/NewProjectDialog.tsx @@ -4,7 +4,7 @@ import initState from '../../../../renderer/redux/initState' import defaultState from '../../../../renderer/redux/defaultState' import { resetState } from '../../../../renderer/redux/store' import { Button } from '@mui/material' -import { setNewProjectDialog } from 'renderer/redux/guiSlice' +import { setNewProjectDialog } from 'features/ui/redux/guiSlice' interface Props {} diff --git a/src/features/menu/engine/menu.ts b/src/features/menu/engine/menu.ts index 539d438f..2b66fbc2 100644 --- a/src/features/menu/engine/menu.ts +++ b/src/features/menu/engine/menu.ts @@ -11,7 +11,7 @@ import { listPorts } from 'features/dmx/engine/dmxConnection' import { toggleLedEnabled, toggleVideoEnabled, -} from '../../../renderer/redux/guiSlice' +} from '../../ui/redux/guiSlice' interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { selector?: string diff --git a/src/features/menu/react/BlackoutButton.tsx b/src/features/menu/react/BlackoutButton.tsx index 7f17d1c9..e38c14cd 100644 --- a/src/features/menu/react/BlackoutButton.tsx +++ b/src/features/menu/react/BlackoutButton.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useDispatch } from 'react-redux' import { useTypedSelector } from '../../../renderer/redux/store' import OfflineBoltIcon from '@mui/icons-material/OfflineBolt' -import { setBlackout } from '../../../renderer/redux/guiSlice' +import { setBlackout } from '../../ui/redux/guiSlice' export default function BlackoutButton() { const dispatch = useDispatch() diff --git a/src/features/menu/react/SideBar/index.tsx b/src/features/menu/react/SideBar/index.tsx index b1267771..6419cc16 100644 --- a/src/features/menu/react/SideBar/index.tsx +++ b/src/features/menu/react/SideBar/index.tsx @@ -8,7 +8,7 @@ import VisualsIcon from 'features/ui/react/images/Thick.png' import MixerIcon from '@mui/icons-material/BarChart' import { useTypedSelector } from '../../../../renderer/redux/store' import { useDispatch } from 'react-redux' -import { setActivePage, Page } from '../../../../renderer/redux/guiSlice' +import { setActivePage, Page } from '../../../ui/redux/guiSlice' import MasterSlider from '../../../scenes/react/controls/MasterSlider' const selectedBorder = 0.2 //rem diff --git a/src/features/menu/react/StatusBar/index.tsx b/src/features/menu/react/StatusBar/index.tsx index 9f5e3f36..47ef637b 100644 --- a/src/features/menu/react/StatusBar/index.tsx +++ b/src/features/menu/react/StatusBar/index.tsx @@ -8,7 +8,7 @@ import PianoIcon from '@mui/icons-material/Piano' import { useDeviceSelector, useTypedSelector } from '../../../../renderer/redux/store' import { useDispatch } from 'react-redux' import { midiSetIsEditing } from '../../../../renderer/redux/controlSlice' -import { setConnectionsMenu } from '../../../../renderer/redux/guiSlice' +import { setConnectionsMenu } from '../../../ui/redux/guiSlice' import TapTempo from './TapTempo' import StartStopButton from './StartStopButton' import SaveLoad from '../../../fileSaving/react/SaveLoad' diff --git a/src/features/modulation/redux/reducer.ts b/src/features/modulation/redux/reducer.ts index 59fbf103..3daf666c 100644 --- a/src/features/modulation/redux/reducer.ts +++ b/src/features/modulation/redux/reducer.ts @@ -34,6 +34,7 @@ export interface IncrementModulatorPayload { } export const modulationActionReducer = createTypedReducers({ + // Update modulators setModulatorShape: ( state, { payload }: PayloadAction<{ index: number; shape: LfoShape }> @@ -85,6 +86,7 @@ export const modulationActionReducer = createTypedReducers({ ) }) }, + // Create and Delete modulators addModulator: (state, _: PayloadAction) => { modifyActiveLightScene(state, (scene) => { scene.modulators.push(initModulator(scene.splitScenes.length)) 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 7a59e737..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 '../../features/fileSaving/shared/save' +import { SaveInfo } from '../../fileSaving/shared/save' import { MidiConnections, DmxConnections, initDmxConnections, initMidiConnections, -} from '../../features/devices/shared/connection' +} from '../../devices/shared/connection' export type Page = | 'Universe' diff --git a/src/renderer/api/subscriptions.ts b/src/renderer/api/subscriptions.ts index 2cd883a3..4124f07c 100644 --- a/src/renderer/api/subscriptions.ts +++ b/src/renderer/api/subscriptions.ts @@ -13,7 +13,7 @@ import { setSaving, setLoading, setNewProjectDialog, -} from '../redux/guiSlice' +} from '../../features/ui/redux/guiSlice' import { getUndoGroup, undoAction, diff --git a/src/renderer/overlays/FullscreenOverlay.tsx b/src/renderer/overlays/FullscreenOverlay.tsx index a7e4e8fb..7dc4647f 100644 --- a/src/renderer/overlays/FullscreenOverlay.tsx +++ b/src/renderer/overlays/FullscreenOverlay.tsx @@ -1,6 +1,6 @@ import { useDispatch } from 'react-redux' import { useTypedSelector } from 'renderer/redux/store' -import { setBlackout } from '../redux/guiSlice' +import { setBlackout } from '../../features/ui/redux/guiSlice' import styled from 'styled-components' import zIndexes from '../zIndexes' import Blackout from './Blackout' diff --git a/src/renderer/pages/mixer/page.tsx b/src/renderer/pages/mixer/page.tsx index 2547c0d7..930b1248 100644 --- a/src/renderer/pages/mixer/page.tsx +++ b/src/renderer/pages/mixer/page.tsx @@ -10,7 +10,7 @@ import { setChannelsPerPage, setOverwrite, clearOverwrites, -} from '../../redux/mixerSlice' +} from '../../../features/dmx/redux/mixerSlice' import { useRealtimeSelector } from '../../redux/realtimeStore' import StatusBar from '../../../features/menu/react/StatusBar' import React from 'react' diff --git a/src/renderer/redux/initState.ts b/src/renderer/redux/initState.ts index e33ceeb5..e8856191 100644 --- a/src/renderer/redux/initState.ts +++ b/src/renderer/redux/initState.ts @@ -1,7 +1,7 @@ import { initDmxState } from 'features/fixtures/redux/fixturesSlice' -import { initGuiState } from 'renderer/redux/guiSlice' +import { initGuiState } from 'features/ui/redux/guiSlice' import { initControlState } from 'renderer/redux/controlSlice' -import { initMixerState } from 'renderer/redux/mixerSlice' +import { initMixerState } from 'features/dmx/redux/mixerSlice' import { CleanReduxState } from './store' export default function initState(): CleanReduxState { diff --git a/src/renderer/redux/store.ts b/src/renderer/redux/store.ts index 9bbc182b..182482b7 100644 --- a/src/renderer/redux/store.ts +++ b/src/renderer/redux/store.ts @@ -8,10 +8,10 @@ import { useSelector, TypedUseSelectorHook } from 'react-redux' import dmxReducer, { DmxState, } from '../../features/fixtures/redux/fixturesSlice' -import guiReducer from './guiSlice' +import guiReducer from '../../features/ui/redux/guiSlice' import controlReducer, { ControlState } from './controlSlice' import { LightScene_t } from '../../features/scenes/shared/Scenes' -import mixerReducer from './mixerSlice' +import mixerReducer from '../../features/dmx/redux/mixerSlice' import undoable, { StateWithHistory } from 'redux-undo' import { DeviceState } from 'features/midi/redux' import { VisualScene_t, SceneType } from '../../features/scenes/shared/Scenes' From 272083b4a7c17546e8c2ddfe585092f342aaf633 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Wed, 5 Apr 2023 21:28:56 -0400 Subject: [PATCH 84/98] fix: actions circular dep --- src/features/modulation/redux/reducer.ts | 7 +-- src/features/params/redux/reducer.ts | 7 +-- src/main/engine/engine.ts | 6 ++- .../redux/controlSlice/reducers/actions.ts | 54 +++---------------- .../redux/controlSlice/reducers/core.ts | 50 +++++++++++++++++ src/renderer/redux/controlSlice/slice.ts | 3 +- 6 files changed, 70 insertions(+), 57 deletions(-) create mode 100644 src/renderer/redux/controlSlice/reducers/core.ts diff --git a/src/features/modulation/redux/reducer.ts b/src/features/modulation/redux/reducer.ts index 3daf666c..247eaf0a 100644 --- a/src/features/modulation/redux/reducer.ts +++ b/src/features/modulation/redux/reducer.ts @@ -1,13 +1,10 @@ import { PayloadAction } from '@reduxjs/toolkit' import { LightScene_t } from 'features/scenes/shared/Scenes' import { clamp, clampNormalized } from 'features/utils/math/util' -import { - ActionState, - createTypedReducers, - modifyActiveLightScene, -} from 'renderer/redux/controlSlice/reducers/actions' + import { LfoShape } from '../shared/oscillator' import { initModulator } from '../shared/modulation' +import { ActionState, createTypedReducers, modifyActiveLightScene } from 'renderer/redux/controlSlice/reducers/core' const modifyScene = { light: { diff --git a/src/features/params/redux/reducer.ts b/src/features/params/redux/reducer.ts index e3e6cef1..a2bf766e 100644 --- a/src/features/params/redux/reducer.ts +++ b/src/features/params/redux/reducer.ts @@ -1,10 +1,11 @@ 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/actions' -import { DefaultParam, Params } from '../shared/params' -import { clampNormalized } from 'features/utils/math/util' +} from 'renderer/redux/controlSlice/reducers/core' type ParamsAction = PayloadAction<{ splitIndex: number diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index b65140c3..8bc6e9fb 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -232,7 +232,11 @@ function getNextRealtimeState( (options) => modTransformer.transform(options) ) - const splitSceneFixtures = getFixturesInGroups(fixtures, splitScene.groups) + const splitSceneFixtures = getFixturesInGroups( + fixtures, + splitScene.groups + ) + const splitSceneFixturesWithinEpicness = splitSceneFixtures.filter( (fixture) => fixture.intensity <= (splitOutputParams.intensity ?? 1) ) diff --git a/src/renderer/redux/controlSlice/reducers/actions.ts b/src/renderer/redux/controlSlice/reducers/actions.ts index 1bb3cf02..60f719ad 100644 --- a/src/renderer/redux/controlSlice/reducers/actions.ts +++ b/src/renderer/redux/controlSlice/reducers/actions.ts @@ -7,70 +7,30 @@ import { LayerConfig } from '../../../../features/visualizer/threejs/layers/Laye import { EffectConfig } from '../../../../features/visualizer/threejs/effects/effectConfigs' import { reorderArray } from '../../../../features/utils/util' -import { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit' +import { PayloadAction } from '@reduxjs/toolkit' import { - LightScene_t, LightScenes_t, - VisualScene_t, VisualScenes_t, SceneType, initSplitScene, - ScenesStateBundle, initLightScene, initVisualScene, } from '../../../../features/scenes/shared/Scenes' import { paramsActionReducer } from 'features/params/redux/reducer' import { modulationActionReducer } from 'features/modulation/redux/reducer' - -export interface ActionState extends ScenesStateBundle { - master: number -} - -export function modifyActiveLightScene( - state: ActionState, - callback: (scene: LightScene_t) => void -) { - const scene = state.light.byId[state.light.active] - if (scene) { - callback(scene) - } -} - -function modifyActiveVisualScene( - state: ActionState, - callback: (scene: VisualScene_t) => void -) { - const scene = state.visual.byId[state.visual.active] - if (scene) { - callback(scene) - } -} - -function modifyActiveScene( - state: ActionState, - sceneType: SceneType, - callback: (scene: VisualScene_t | LightScene_t) => void -) { - const scene = state[sceneType].byId[state[sceneType].active] - if (scene) { - callback(scene) - } -} +import { + createTypedReducers, + modifyActiveLightScene, + modifyActiveScene, + modifyActiveVisualScene, +} from './core' type ScopedAction = PayloadAction<{ sceneType: SceneType val: T }> -export const createTypedReducers = < - Reducers extends SliceCaseReducers = SliceCaseReducers ->( - reducers: Reducers -) => { - return reducers -} - export const actions = { ...createTypedReducers({ setMaster: (state, { payload }: PayloadAction) => { diff --git a/src/renderer/redux/controlSlice/reducers/core.ts b/src/renderer/redux/controlSlice/reducers/core.ts new file mode 100644 index 00000000..43896fe2 --- /dev/null +++ b/src/renderer/redux/controlSlice/reducers/core.ts @@ -0,0 +1,50 @@ +import { SliceCaseReducers } from '@reduxjs/toolkit' +import { + LightScene_t, + SceneType, + ScenesStateBundle, + VisualScene_t, +} from 'features/scenes/shared/Scenes' + +export interface ActionState extends ScenesStateBundle { + master: number +} + +export function modifyActiveLightScene( + state: ActionState, + callback: (scene: LightScene_t) => void +) { + const scene = state.light.byId[state.light.active] + if (scene) { + callback(scene) + } +} + +export function modifyActiveVisualScene( + state: ActionState, + callback: (scene: VisualScene_t) => void +) { + const scene = state.visual.byId[state.visual.active] + if (scene) { + callback(scene) + } +} + +export function modifyActiveScene( + state: ActionState, + sceneType: SceneType, + callback: (scene: VisualScene_t | LightScene_t) => void +) { + const scene = state[sceneType].byId[state[sceneType].active] + if (scene) { + callback(scene) + } +} + +export const createTypedReducers = < + Reducers extends SliceCaseReducers = SliceCaseReducers +>( + reducers: Reducers +) => { + return reducers +} diff --git a/src/renderer/redux/controlSlice/slice.ts b/src/renderer/redux/controlSlice/slice.ts index 48ea91d5..ee29896a 100644 --- a/src/renderer/redux/controlSlice/slice.ts +++ b/src/renderer/redux/controlSlice/slice.ts @@ -6,7 +6,8 @@ import { initLightScene, initVisualScene, } from '../../../features/scenes/shared/Scenes' -import { ActionState, actions } from './reducers/actions' +import { actions } from './reducers/actions' +import { ActionState } from './reducers/core' export interface ControlState extends ActionState { device: DeviceState From d85df73e30e83ff84d5a4d1194159537b5a9c3c2 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Thu, 6 Apr 2023 00:19:55 -0400 Subject: [PATCH 85/98] fix: channel react circular dep --- src/features/dmx/channel.config.react.tsx | 2 +- .../react/fixtures/channels/editor/core.tsx | 60 +++++++++++++++++++ .../react/fixtures/channels/editor/index.tsx | 58 +----------------- 3 files changed, 62 insertions(+), 58 deletions(-) create mode 100644 src/features/dmx/react/fixtures/channels/editor/core.tsx diff --git a/src/features/dmx/channel.config.react.tsx b/src/features/dmx/channel.config.react.tsx index 1f29c37a..13bb55e4 100644 --- a/src/features/dmx/channel.config.react.tsx +++ b/src/features/dmx/channel.config.react.tsx @@ -4,7 +4,7 @@ import { Info, Row, createChannelComponents, -} from './react/fixtures/channels/editor' +} 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' 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..61ff9d49 --- /dev/null +++ b/src/features/dmx/react/fixtures/channels/editor/core.tsx @@ -0,0 +1,60 @@ +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: T +) => { + return config +} diff --git a/src/features/dmx/react/fixtures/channels/editor/index.tsx b/src/features/dmx/react/fixtures/channels/editor/index.tsx index 85ff2261..0ef521f7 100644 --- a/src/features/dmx/react/fixtures/channels/editor/index.tsx +++ b/src/features/dmx/react/fixtures/channels/editor/index.tsx @@ -5,36 +5,16 @@ import { FixtureChannel, channelTypes, initFixtureChannel, - DMX_MAX_VALUE, - DMX_MIN_VALUE, - ChannelType, } from '../../../../shared/dmxFixtures' -import NumberField from '../../../../../ui/react/base/NumberField' import { editFixtureChannel } from '../../../../../fixtures/redux/fixturesSlice' import { FixtureChannelItemProps } from '../list/FixtureChannelItem' - -import { FC, createContext, useContext } from 'react' import { ChannelComponents } from 'features/dmx/channel.config.react' +import { ChannelContext, Info, Row } from './core' interface Props extends FixtureChannelItemProps { ch: FixtureChannel } -export const createChannelComponents = < - T extends { - [k in ChannelType]: FC<{ - ch: Extract - updateChannel(newChannel: FixtureChannel): void - fixtureID: string - channelIndex: number - }> - } ->( - config: T -) => { - return config -} - export default function FixtureChannelPopup(props: Props) { const { ch, fixtureID, channelIndex } = props const dispatch = useDispatch() @@ -63,32 +43,6 @@ export default function FixtureChannelPopup(props: Props) { ) } -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, - }) - } - /> - ) -} - function Fields({ ch, fixtureID, channelIndex }: Props) { const dispatch = useDispatch() @@ -117,18 +71,8 @@ function Fields({ ch, fixtureID, channelIndex }: Props) { ) } -export const Row = styled.div` - display: flex; - align-items: center; -` - const Content = styled.div` & > * { margin-bottom: 1rem; } ` - -export const Info = styled.div` - font-size: 0.9rem; - margin-right: 0.5rem; -` From 79530b6781d346714c36ff675b67971da840fc6d Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Thu, 6 Apr 2023 03:00:16 -0400 Subject: [PATCH 86/98] fix: store causing firebase analytics issue --- src/features/modulation/react/ModulationMatrix.tsx | 2 +- src/features/modulation/react/ModulationSlider.tsx | 8 ++------ src/features/params/redux/index.ts | 12 +----------- src/features/params/shared/params.ts | 10 ++++++++++ src/main/engine/engine.ts | 2 +- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/features/modulation/react/ModulationMatrix.tsx b/src/features/modulation/react/ModulationMatrix.tsx index 6167dd9a..83872ea3 100644 --- a/src/features/modulation/react/ModulationMatrix.tsx +++ b/src/features/modulation/react/ModulationMatrix.tsx @@ -6,7 +6,7 @@ import { import styled from 'styled-components' import { indexArray } from 'features/utils/util' import ModulationSlider, { AddModulationButton } from './ModulationSlider' -import { getAllParamKeys } from 'features/params/redux' +import { getAllParamKeys } from 'features/params/shared/params' export default function ModulationMatrix({ index }: { index: number }) { const numSplits = useActiveLightScene((scene) => scene.splitScenes.length) diff --git a/src/features/modulation/react/ModulationSlider.tsx b/src/features/modulation/react/ModulationSlider.tsx index 68560398..548015de 100644 --- a/src/features/modulation/react/ModulationSlider.tsx +++ b/src/features/modulation/react/ModulationSlider.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { useDispatch } from 'react-redux' -import { DefaultParam } from '../../params/shared/params' +import { DefaultParam, getAllParamKeys } from '../../params/shared/params' import { useActiveLightScene, useDmxSelector, @@ -10,11 +10,7 @@ import useDragMapped from '../../ui/react/hooks/useDragMapped' import styled from 'styled-components' import Popup from 'features/ui/react/base/Popup' import { indexArray } from 'features/utils/util' -import { - getAllParamKeys, - useBaseParams, - useModParam, -} from 'features/params/redux' +import { useBaseParams, useModParam } from 'features/params/redux' interface Props { splitIndex: number diff --git a/src/features/params/redux/index.ts b/src/features/params/redux/index.ts index e3dad91d..4622fbc6 100644 --- a/src/features/params/redux/index.ts +++ b/src/features/params/redux/index.ts @@ -2,21 +2,11 @@ import { DefaultParam, Params, defaultOutputParams, - defaultParamsList, } from 'features/params/shared/params' -import { - DmxState, - getCustomChannels, -} from 'features/fixtures/redux/fixturesSlice' + import { useActiveLightScene } from 'renderer/redux/store' import { useRealtimeSelector } from 'renderer/redux/realtimeStore' -export function getAllParamKeys(dmx: DmxState): string[] { - return (defaultParamsList as string[]).concat( - Array.from(getCustomChannels(dmx)) - ) -} - export function useBaseParam( param: DefaultParam, splitIndex: number diff --git a/src/features/params/shared/params.ts b/src/features/params/shared/params.ts index f1416dc8..c3b04eeb 100644 --- a/src/features/params/shared/params.ts +++ b/src/features/params/shared/params.ts @@ -1,3 +1,7 @@ +import { + DmxState, + getCustomChannels, +} from 'features/fixtures/redux/fixturesSlice' import { Pretty } from 'features/shared/shared/type-utils' export type DefaultParam = @@ -59,6 +63,12 @@ export function getParam( return params[param] ?? defaultParams[param] } +export function getAllParamKeys(dmx: DmxState): string[] { + return (defaultParamsList as string[]).concat( + Array.from(getCustomChannels(dmx)) + ) +} + export function defaultOutputParams(): Partial { return { ...initBaseParams(), diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index 8bc6e9fb..e2c4852c 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -35,8 +35,8 @@ import { _tapTempo, } from 'features/bpm/engine/Link' import { createApi, IPC_Callbacks } from './api' -import { getAllParamKeys } from 'features/params/redux' import { createOutputParams } from 'features/params/engine' +import { getAllParamKeys } from 'features/params/shared/params' let _realtimeState: RealtimeState = initRealtimeState() From ce864e3f74ffcbf2943a7da7061640eb09c2eb64 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Thu, 6 Apr 2023 06:32:04 -0400 Subject: [PATCH 87/98] fix: midi overlay styling --- src/features/midi/react/MidiOverlay.tsx | 150 +++++++++++------- .../midi/react/midi-overlay-styles.css | 12 -- src/features/modulation/react/lfo/LfoMenu.tsx | 1 - .../modulation/react/lfo/LfoPeriod.tsx | 11 +- src/features/ui/react/base/Button.tsx | 7 +- .../ui/react/base/DraggableNumber.tsx | 1 + src/renderer/GlobalStyle.tsx | 16 ++ 7 files changed, 127 insertions(+), 71 deletions(-) delete mode 100644 src/features/midi/react/midi-overlay-styles.css diff --git a/src/features/midi/react/MidiOverlay.tsx b/src/features/midi/react/MidiOverlay.tsx index 650be615..7773f41f 100644 --- a/src/features/midi/react/MidiOverlay.tsx +++ b/src/features/midi/react/MidiOverlay.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components' +import styled, { css, keyframes } from 'styled-components' import * as Popover from '@radix-ui/react-popover' import { SliderAction } from '../redux' import { @@ -10,8 +10,7 @@ import { import { useDeviceSelector } from '../../../renderer/redux/store' import { useDispatch } from 'react-redux' import DraggableNumber from '../../ui/react/base/DraggableNumber' -import Button from '../../ui/react/base/Button' -import './midi-overlay-styles.css' +import Button, { StyledButton } from '../../ui/react/base/Button' import { useEffect, useRef } from 'react' import { getActionID } from '../redux' import { MidiActions } from '../shared/actions' @@ -332,59 +331,71 @@ export function RangeMidiOverlay({ {controlledAction && ( - + <> - - {controlledAction.inputID} - dispatch(removeMidiAction(action))} +

- X - - - - - - {controlledAction.options.type === 'note' && ( - <> + {controlledAction.inputID} +

+ + + {controlledAction.options.type === 'note' && ( + <> + + )} + + + {controlledAction && ( + <> +

- Remove - - - + {controlledAction.inputID} +

+ + + {controlSlot} + + Midi Listen + + + Remove + + + + )}
- )} -
- +
+ + )}
) @@ -302,7 +345,7 @@ const StateColor = { `, } as const -const TriggerOverlay = styled(Popover.Trigger)<{ +const TriggerOverlay = styled.button<{ state: keyof typeof StateColor | undefined }>` position: absolute; @@ -323,36 +366,9 @@ const TriggerOverlay = styled(Popover.Trigger)<{ color: black; ` -const Overlay = styled.div<{ selected: boolean }>` - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - cursor: pointer; - border: ${(props) => props.selected && '2px solid white'}; - background: #56fd56b7; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - align-content: center; - color: black; -` - const MinMax = styled.div` display: flex; flex-wrap: wrap-reverse; font-size: 0.75rem; justify-content: center; ` - -function X({ action }: { action: MidiActions }) { - const dispatch = useDispatch() - const onClick = () => dispatch(removeMidiAction(action)) - return ( -
- X -
- ) -} From 2a612392a902bd5cbb2f0e0a8a98679a24576a18 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Sat, 8 Apr 2023 15:47:51 -0400 Subject: [PATCH 90/98] refactor: fixture library errors --- src/features/dmx/channel.config.ts | 97 +++++---- src/features/dmx/shared/dmxFixtures.ts | 57 ------ src/features/shared/redux/fixState.ts | 8 +- src/renderer/dmx/FixtureChannelPopup.tsx | 240 ----------------------- src/shared/dmxFixtures.ts | 207 ------------------- tools/qlc_fixture_parser/index.ts | 5 +- tools/qlc_fixture_parser/qlc_channel.ts | 41 ++-- 7 files changed, 77 insertions(+), 578 deletions(-) delete mode 100644 src/renderer/dmx/FixtureChannelPopup.tsx delete mode 100644 src/shared/dmxFixtures.ts diff --git a/src/features/dmx/channel.config.ts b/src/features/dmx/channel.config.ts index c8875371..ca9883e8 100644 --- a/src/features/dmx/channel.config.ts +++ b/src/features/dmx/channel.config.ts @@ -1,5 +1,6 @@ import { rLerp } from 'features/utils/math/range' import { + AxisDir, DMX_DEFAULT_VALUE, DMX_MAX_VALUE, FlattenedFixture, @@ -27,32 +28,37 @@ export type ChannelConfig = { /** * This happens on react side */ - default: () => Omit, 'type'> + default: (options: any) => Omit, 'type'> /** * this happens on engine side */ getValueFromDevice?: (ctx: GetContext) => number } -const createChannelConfig = < - T extends { [k in ChannelType]: ChannelConfig } ->( - config: T -) => { - return Object.entries(config).reduce((prev, [key, config]) => { - // @ts-ignore figure out proper typing - prev[key as keyof T] = { - ...config, - default: (...args) => { - return { - ...config.default(...args), - type: key, - } - }, +const createChannelConfig = () => { + return < + T extends { + [k in ChannelType]: ChannelConfig } - return prev - // @ts-ignore figure out proper typing - }, {} as { [k in keyof T]: Omit & { default: () => GetFixturePayload } }) + >(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 } }) + } } /** @@ -62,7 +68,8 @@ const createChannelConfig = < /** * This seems to be the conversion point between params and channel */ -export const channelConfig = createChannelConfig({ + +export const channelConfig = createChannelConfig()({ master: { default: () => ({ min: DMX_MIN_VALUE, @@ -88,9 +95,10 @@ export const channelConfig = createChannelConfig({ }, }, custom: { - default: () => ({ - name: 'custom', + default: ({ name = 'custom' }: { name?: string } = {}) => ({ + name, default: DMX_MIN_VALUE, + isControllable: false, min: DMX_MIN_VALUE, max: DMX_MAX_VALUE, }), @@ -103,9 +111,7 @@ export const channelConfig = createChannelConfig({ } }, }, - reset: { - default: () => ({ resetVal: DMX_MAX_VALUE }), - }, + colorMap: { default: () => ({ colors: [{ max: 0, hue: 0, saturation: 1.0 }], @@ -129,9 +135,12 @@ export const channelConfig = createChannelConfig({ }, }, axis: { - default: () => ({ - dir: 'x', - isFine: false, + default: ({ + dir = 'x', + isFine = false, + }: { dir?: AxisDir; isFine?: boolean } = {}) => ({ + dir, + isFine, min: DMX_MIN_VALUE, max: DMX_MAX_VALUE, }), @@ -156,29 +165,31 @@ export const channelConfig = createChannelConfig({ }, }, strobe: { - default: () => ({ - default_solid: DMX_MIN_VALUE, - default_strobe: DMX_MAX_VALUE, - }), + 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 }, }, - other: { - default: () => ({ - default: DMX_MIN_VALUE, - }), - getValueFromDevice({ ch }) { - return ch.default - }, - }, + color: { - default: () => ({ + default: ({ + hue = 0, + saturation = 1.0, + }: { hue?: number; saturation?: number } = {}) => ({ color: { - hue: 0, - saturation: 1.0, + hue, + saturation, }, }), getValueFromDevice({ ch, fixture, randomizerLevel, params, movingWindow }) { diff --git a/src/features/dmx/shared/dmxFixtures.ts b/src/features/dmx/shared/dmxFixtures.ts index b33f1b42..e613a12c 100644 --- a/src/features/dmx/shared/dmxFixtures.ts +++ b/src/features/dmx/shared/dmxFixtures.ts @@ -116,60 +116,3 @@ export type FlattenedFixture = { window: Window2D_t groups: string[] } - -export function initChannelColorMap( - colors: ColorMapColor[] -): FixtureApi['colorMap'] { - return { - colors, - } -} - -export function initChannelStrobe(): FixtureApi['strobe'] { - return { - default_solid: DMX_MIN_VALUE, - default_strobe: DMX_MAX_VALUE, - } -} - -export function initChannelAxis( - dir: AxisDir, - isFine: boolean -): FixtureApi['axis'] { - return { - dir, - isFine, - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - } -} - -export function initChannelColor( - hue: number, - saturation: number -): FixtureApi['color'] { - return { - color: { - hue, - saturation, - }, - } -} - -export function initChannelMaster(): FixtureApi['master'] { - return { - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - isOnOff: false, - } -} - -export function initChannelCustom(name: string): FixtureApi['custom'] { - return { - name, - default: DMX_MIN_VALUE, - isControllable: false, - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - } -} diff --git a/src/features/shared/redux/fixState.ts b/src/features/shared/redux/fixState.ts index 071ec40b..2cb8df25 100644 --- a/src/features/shared/redux/fixState.ts +++ b/src/features/shared/redux/fixState.ts @@ -12,13 +12,14 @@ 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 { initChannelCustom } from 'shared/dmxFixtures' + import { LightScenes_t, LightScene_t, SplitScene_t, VisualScenes_t, } from '../../scenes/shared/Scenes' +import { channelConfig } from 'features/dmx/channel.config' type Deprecated_ChannelOther = { type: 'other' @@ -118,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/renderer/dmx/FixtureChannelPopup.tsx b/src/renderer/dmx/FixtureChannelPopup.tsx deleted file mode 100644 index 6a6ef953..00000000 --- a/src/renderer/dmx/FixtureChannelPopup.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import styled from 'styled-components' -import { useDispatch } from 'react-redux' -import Select from '../base/Select' -import { - FixtureChannel, - channelTypes, - initFixtureChannel, - AxisDir, - axisDirList, - DMX_MAX_VALUE, - DMX_MIN_VALUE, -} from '../../shared/dmxFixtures' -import NumberField from '../base/NumberField' -import Input from '../base/Input' -import { editFixtureChannel } from '../redux/dmxSlice' -import Checkbox from '../base/LabelledCheckbox' -import HSpad, { ColorChannelProps } from 'renderer/base/HSpad' -import { FixtureChannelItemProps } from './FixtureChannelItem' -import ColorMapChannel from './ColorMapChannel' -import ColorPicker from 'renderer/base/ColorPicker' - -interface Props extends FixtureChannelItemProps { - ch: FixtureChannel -} - -export default function FixtureChannelPopup(props: Props) { - const { ch, fixtureID, channelIndex } = props - const dispatch = useDispatch() - - return ( - - - Type: - - updateChannel({ - ...ch, - dir: newAxisDir as AxisDir, - }) - } - /> - - - updateChannel({ - ...ch, - isFine, - }) - } - /> - {!ch.isFine && ( - <> - - {dmxNumberField(ch, 'min', 'Min')} - - {dmxNumberField(ch, 'max', 'Max')} - - )} - - ) - } else if (ch.type === 'colorMap') { - return ( - - ) - } else if (ch.type === 'custom') { - return ( - <> - - updateChannel({ - ...ch, - name: newName, - }) - } - /> - {dmxNumberField(ch, 'default', 'Default')} - - - updateChannel({ - ...ch, - isControllable, - }) - } - /> - {ch.isControllable && ( - <> - - {dmxNumberField(ch, 'min', 'Min')} - - {dmxNumberField(ch, 'max', 'Max')} - - )} - - ) - } else { - return null - } -} - -const Row = styled.div` - display: flex; - align-items: center; -` - -const Content = styled.div` - & > * { - margin-bottom: 1rem; - } -` - -const Sp2 = styled.div` - width: 1rem; -` - -const Info = styled.div` - font-size: 0.9rem; - margin-right: 0.5rem; -` diff --git a/src/shared/dmxFixtures.ts b/src/shared/dmxFixtures.ts deleted file mode 100644 index 44caf5f2..00000000 --- a/src/shared/dmxFixtures.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { Window2D_t } from '../shared/window' -import { ColorChannel } from './dmxColors' -import { nanoid } from 'nanoid' - -export const DMX_MIN_VALUE = 0 -export const DMX_MAX_VALUE = 255 -export const DMX_NUM_CHANNELS = 512 -export const DMX_DEFAULT_VALUE = 0 - -export type DmxChannel = number // 1 - 512 -export type DmxValue = number // 0 - 255 - -export type AxisDir = 'x' | 'y' -export const axisDirList = ['x', 'y'] - -type ChannelMaster = { - type: 'master' - min: DmxValue - max: DmxValue - isOnOff: boolean -} - -type ChannelColor = { - type: 'color' - color: ColorChannel -} - -type ChannelStrobe = { - type: 'strobe' - default_strobe: DmxValue - default_solid: DmxValue -} - -export type ChannelAxis = { - type: 'axis' - dir: AxisDir - isFine: boolean - min: DmxValue - max: DmxValue -} - -export type ColorMapColor = { max: number; hue: number; saturation: number } - -export type ChannelColorMap = { - type: 'colorMap' - colors: ColorMapColor[] -} - -export type ChannelCustom = { - type: 'custom' - name: string - default: DmxValue - - isControllable: boolean - min: DmxValue - max: DmxValue -} - -export const defaultCustomChannels = ['speed'] - -export type FixtureChannel = - | ChannelMaster - | ChannelColor - | ChannelStrobe - | ChannelAxis - | ChannelColorMap - | ChannelCustom - -export type ChannelType = FixtureChannel['type'] - -export const channelTypes: ChannelType[] = [ - 'master', - 'color', - 'colorMap', - 'strobe', - 'axis', - 'custom', -] - -export function initFixtureChannel( - type?: FixtureChannel['type'] -): FixtureChannel { - if (type === 'color') { - initChannelColor(0, 1) - } else if (type === 'strobe') { - return initChannelStrobe() - } else if (type === 'axis') { - return initChannelAxis('x', false) - } else if (type === 'colorMap') { - return initChannelColorMap([{ max: 0, hue: 0, saturation: 1.0 }]) - } else if (type === 'custom') { - return initChannelCustom('Custom') - } - return initChannelMaster() -} - -export function initChannelColorMap(colors: ColorMapColor[]): ChannelColorMap { - return { - type: 'colorMap', - colors, - } -} - -export function initChannelStrobe(): ChannelStrobe { - return { - type: 'strobe', - default_solid: DMX_MIN_VALUE, - default_strobe: DMX_MAX_VALUE, - } -} - -export function initChannelAxis(dir: AxisDir, isFine: boolean): ChannelAxis { - return { - type: 'axis', - dir, - isFine, - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - } -} - -export function initChannelColor( - hue: number, - saturation: number -): ChannelColor { - return { - type: 'color', - color: { - hue, - saturation, - }, - } -} - -export function initChannelMaster(): ChannelMaster { - return { - type: 'master', - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - isOnOff: false, - } -} - -export function initChannelCustom(name: string): ChannelCustom { - return { - type: 'custom', - name, - default: DMX_MIN_VALUE, - isControllable: false, - min: DMX_MIN_VALUE, - max: DMX_MAX_VALUE, - } -} - -export type FixtureType = { - id: string - name: string - intensity: number - manufacturer?: string - channels: FixtureChannel[] - subFixtures: SubFixture[] - groups: string[] -} - -export function initFixtureType(): FixtureType { - return { - id: nanoid(), - name: 'Name', - manufacturer: 'Manufacturer', - intensity: 0, - channels: [initFixtureChannel()], - subFixtures: [], - groups: [], - } -} - -export interface Fixture { - ch: number - type: string // FixtureType id - window: Window2D_t - groups: string[] -} - -export type Universe = Fixture[] - -export type SubFixture = { - name: string - intensity?: number - channels: number[] // Channel indexes from the parent fixture - relative_window?: Window2D_t - groups: string[] -} - -export function initSubFixture(): SubFixture { - return { - name: 'Name', - channels: [], - groups: [], - } -} - -export type FlattenedFixture = { - intensity: number - channels: [number, FixtureChannel][] - window: Window2D_t - groups: string[] -} diff --git a/tools/qlc_fixture_parser/index.ts b/tools/qlc_fixture_parser/index.ts index 64ccd04e..6cd58ae2 100644 --- a/tools/qlc_fixture_parser/index.ts +++ b/tools/qlc_fixture_parser/index.ts @@ -3,8 +3,9 @@ import dir from 'node-dir' import fs from 'fs/promises' import { QlcFixtureDef } from './qlc_fixture_def' import { QlcChannel, convert_qlc_channel } from './qlc_channel' -import { FixtureType } from 'shared/dmxFixtures' + import { nanoid } from '@reduxjs/toolkit' +import { FixtureType } from 'features/dmx/shared/dmxFixtures' const QLC_EXT = 'qxf' const QLC_DEFINITION_KEY = 'FixtureDefinition' @@ -13,7 +14,7 @@ const OUTPUT_PATH = './assets/captivate_fixtures.db' const PRETTY = false main().catch((err) => { - console.error(`To run this script, you must first copy the fixtures folder from + console.error(`To run this script, you must first copy the fixtures folder from https://github.com/mcallegari/qlcplus/tree/master/resources/fixtures into tools/qlc_fixture_parser diff --git a/tools/qlc_fixture_parser/qlc_channel.ts b/tools/qlc_fixture_parser/qlc_channel.ts index da0c6671..0af2cde9 100644 --- a/tools/qlc_fixture_parser/qlc_channel.ts +++ b/tools/qlc_fixture_parser/qlc_channel.ts @@ -1,14 +1,6 @@ -import { - DMX_MAX_VALUE, - DMX_MIN_VALUE, - FixtureChannel, - initChannelAxis, - initChannelColor, - initChannelColorMap, - initChannelCustom, - initChannelMaster, - initChannelStrobe, -} from '../../src/shared/dmxFixtures' +import { channelConfig } from 'features/dmx/channel.config' +import { FixtureChannel } from 'features/dmx/shared/dmxFixtures' + export interface QlcChannel { // Name and Preset are XML attributes, so they need to be prefixed with something @@ -28,42 +20,39 @@ export function convert_qlc_channel(ch: QlcChannel): FixtureChannel { } if (preset === 'Custom') { - return initChannelCustom('Custom') + return channelConfig.custom.default() } if (preset.includes('Intensity')) { if (preset.includes('Fine')) { - return initChannelCustom(name) + return channelConfig.custom.default({ name }) } else if (preset.includes('Dimmer')) { - return initChannelMaster() + return channelConfig.master.default() } else { const tail = preset.slice(9) - if (tail === 'White') return initChannelColor(0, 0) + if (tail === 'White') + return channelConfig.color.default({ hue: 0, saturation: 0 }) const hue = get_hue(tail) if (hue !== null) { - return initChannelColor(hue, 1) + return channelConfig.color.default({ hue, saturation: 1 }) } - initChannelCustom(name) } } if (preset.includes('Position')) { const dir = preset.includes('Pan') || preset.includes('XAxis') ? 'x' : 'y' const isFine = preset.includes('Fine') - return initChannelAxis(dir, isFine) + return channelConfig.axis.default({ dir, isFine }) } - if (ch['@_Preset'] === 'ShutterStrobeSlowFast') return initChannelStrobe() + if (ch['@_Preset'] === 'ShutterStrobeSlowFast') + return channelConfig.strobe.default() if (ch['@_Preset'] === 'ShutterStrobeFastSlow') { - return { - type: 'strobe', - default_solid: DMX_MAX_VALUE, - default_strobe: DMX_MIN_VALUE, - } + return channelConfig.strobe.default({ invert: true }) } if (ch['@_Preset'] === 'ColorMacro') { // TODO: Actually read color map colors - return initChannelColorMap([{ max: 0, hue: 0, saturation: 1.0 }]) + return channelConfig.colorMap.default() } - return initChannelCustom(name) + return channelConfig.custom.default({ name }) } export function get_hue(color: string): number | null { From aa50abb33d64dc1a97cd535122349be6965fe125 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Sat, 8 Apr 2023 15:59:05 -0400 Subject: [PATCH 91/98] fix: types for generic params --- src/features/params/react/controls/ParamCursor.tsx | 8 ++++---- src/features/params/react/controls/ParamSlider.tsx | 9 ++++++--- src/features/params/react/controls/ParamXButton.tsx | 9 ++++++--- src/features/params/redux/index.ts | 8 ++++---- src/features/params/redux/reducer.ts | 4 ++-- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/features/params/react/controls/ParamCursor.tsx b/src/features/params/react/controls/ParamCursor.tsx index b647d5a9..31ce1b7e 100644 --- a/src/features/params/react/controls/ParamCursor.tsx +++ b/src/features/params/react/controls/ParamCursor.tsx @@ -2,19 +2,19 @@ import { useOutputParam } from 'features/params/redux' import { DefaultParam } from '../../shared/params' import SliderCursor from '../../../ui/react/base/SliderCursor' -interface Props { - param: DefaultParam +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/features/params/react/controls/ParamSlider.tsx b/src/features/params/react/controls/ParamSlider.tsx index 61db0235..1744b8c3 100644 --- a/src/features/params/react/controls/ParamSlider.tsx +++ b/src/features/params/react/controls/ParamSlider.tsx @@ -8,12 +8,15 @@ import { RangeMidiOverlay } from 'features/midi/react/MidiOverlay' import ParamXButton from './ParamXButton' import { useBaseParam } from 'features/params/redux' -interface Props { - param: DefaultParam +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) diff --git a/src/features/params/react/controls/ParamXButton.tsx b/src/features/params/react/controls/ParamXButton.tsx index 295514e3..abe962e8 100644 --- a/src/features/params/react/controls/ParamXButton.tsx +++ b/src/features/params/react/controls/ParamXButton.tsx @@ -3,12 +3,15 @@ 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[] + 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/features/params/redux/index.ts b/src/features/params/redux/index.ts index 4622fbc6..985a1a6c 100644 --- a/src/features/params/redux/index.ts +++ b/src/features/params/redux/index.ts @@ -7,8 +7,8 @@ import { import { useActiveLightScene } from 'renderer/redux/store' import { useRealtimeSelector } from 'renderer/redux/realtimeStore' -export function useBaseParam( - param: DefaultParam, +export function useBaseParam( + param: Param, splitIndex: number ): number | undefined { const baseParam = useActiveLightScene((state) => { @@ -34,8 +34,8 @@ export function useModParam( }) } -export function useOutputParam( - param: DefaultParam, +export function useOutputParam( + param: Param, splitIndex: number ): number { const outputParam = useRealtimeSelector((state) => { diff --git a/src/features/params/redux/reducer.ts b/src/features/params/redux/reducer.ts index a2bf766e..0322fe7b 100644 --- a/src/features/params/redux/reducer.ts +++ b/src/features/params/redux/reducer.ts @@ -14,7 +14,7 @@ type ParamsAction = PayloadAction<{ type ParamAction = PayloadAction<{ splitIndex: number - paramKey: DefaultParam + paramKey: DefaultParam | string value: number | undefined }> @@ -49,7 +49,7 @@ export const paramsActionReducer = createTypedReducers({ payload: { params, splitIndex }, }: PayloadAction<{ splitIndex: number - params: readonly DefaultParam[] + params: readonly (DefaultParam | string)[] }> ) => { for (const param of params) { From e5522626c43e8bfbc0e760cfaf62e18fce1e1083 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Sat, 8 Apr 2023 19:49:32 -0400 Subject: [PATCH 92/98] fix: types --- src/features/dmx/channel.config.ts | 2 +- src/features/dmx/react/fixtures/channels/editor/core.tsx | 4 +--- src/features/midi/shared/actions.ts | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/features/dmx/channel.config.ts b/src/features/dmx/channel.config.ts index ca9883e8..a19ef4c8 100644 --- a/src/features/dmx/channel.config.ts +++ b/src/features/dmx/channel.config.ts @@ -34,7 +34,7 @@ export type ChannelConfig = { */ getValueFromDevice?: (ctx: GetContext) => number } - +// TODO: improve this typing const createChannelConfig = () => { return < T extends { diff --git a/src/features/dmx/react/fixtures/channels/editor/core.tsx b/src/features/dmx/react/fixtures/channels/editor/core.tsx index 61ff9d49..96f2d3f0 100644 --- a/src/features/dmx/react/fixtures/channels/editor/core.tsx +++ b/src/features/dmx/react/fixtures/channels/editor/core.tsx @@ -53,8 +53,6 @@ export const createChannelComponents = < channelIndex: number }> } ->( - config: T -) => { +>(config: { [k in keyof T]: k extends ChannelType ? T[k] : never }) => { return config } diff --git a/src/features/midi/shared/actions.ts b/src/features/midi/shared/actions.ts index ed86b704..25a9d42e 100644 --- a/src/features/midi/shared/actions.ts +++ b/src/features/midi/shared/actions.ts @@ -34,7 +34,7 @@ export type Reductions = RemoveAutoProps<{ setPeriod: { newVal: true } }> -type AllTrue = T extends { [k in infer Key]: unknown } +type AllTrue = T extends { [k in infer Key]?: unknown } ? Partial<{ [k in Key]: true }> : never @@ -66,8 +66,6 @@ type GetReduxPayload = T extends CaseReducer< : {} : never - - export type GetMidiAction = T extends UserCommand['type'] ? Extract From 72479b289c5313eed9d69e9ce381cc6e181e2f26 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Sat, 8 Apr 2023 19:57:06 -0400 Subject: [PATCH 93/98] fix: reductions type --- src/features/midi/shared/actions.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/features/midi/shared/actions.ts b/src/features/midi/shared/actions.ts index 25a9d42e..cdbcc1da 100644 --- a/src/features/midi/shared/actions.ts +++ b/src/features/midi/shared/actions.ts @@ -73,17 +73,20 @@ export type GetMidiAction = ? Pretty<{ type: T } & GetReduxPayload> : never +type ApplyTransformations = RemoveProps< + GetMidiAction, + k extends keyof Reductions ? Reductions[k] : {} +> + export type MidiActions = { - [k in AllowedMidiActions]: Pretty< - RemoveProps, Reductions[k]> - > + [k in AllowedMidiActions]: Pretty> }[AllowedMidiActions] export type Config = { buttons: Partial<{ - [k in Keys]: ButtonFunction, Reductions[k]>> + [k in Keys]: ButtonFunction> }> range: Partial<{ - [k in Keys]: SlidersFunction, Reductions[k]>> + [k in Keys]: SlidersFunction> }> } From 39e924853dab286a61b4494c23674808b7984c36 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Sun, 9 Apr 2023 04:02:33 -0400 Subject: [PATCH 94/98] fix: midi config type --- src/features/midi/engine/midi.config.ts | 2 -- src/features/midi/shared/config.ts | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/features/midi/engine/midi.config.ts b/src/features/midi/engine/midi.config.ts index 95a600f0..2a43eaad 100644 --- a/src/features/midi/engine/midi.config.ts +++ b/src/features/midi/engine/midi.config.ts @@ -8,8 +8,6 @@ import { import { AllowedMidiActions, Config } from '../shared/actions' import { createMidiConfig } from '../shared/config' - - export default createMidiConfig>( { buttons: { diff --git a/src/features/midi/shared/config.ts b/src/features/midi/shared/config.ts index d3be0d70..1ebd5be1 100644 --- a/src/features/midi/shared/config.ts +++ b/src/features/midi/shared/config.ts @@ -11,8 +11,7 @@ type MidiContext = { state: CleanReduxState } - -export type SlidersFunction = { +export type SlidersFunction = { set: ( input: { action: Action @@ -25,7 +24,7 @@ export type SlidersFunction = { key?: (action: Action) => string } -export type ButtonFunction = { +export type ButtonFunction = { set: ( input: { action: Action From 65aadb79d7a0227d78741dc76beada54937dc28e Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Sun, 9 Apr 2023 22:03:15 -0400 Subject: [PATCH 95/98] fix: no output params --- src/main/engine/engine.ts | 10 +++++----- src/main/main.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index e2c4852c..5b21b38c 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -60,9 +60,9 @@ const controlStateManager = () => { cancelled = true }, - waitForFirstControlState: (cb: () => void) => { - firstControlState.then(() => { - if (!cancelled) cb() + waitForFirstControlState: (cb: () => void | Promise) => { + return firstControlState.then(async () => { + if (!cancelled) await cb() }) }, } @@ -83,7 +83,7 @@ const disposer = { }, } -export function start( +export async function start( renderer: WebContents, visualizerContainer: VisualizerContainer ) { @@ -101,7 +101,7 @@ export function start( ) // TODO: now we don't need null checks for control state - controlState.waitForFirstControlState(() => { + await controlState.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) const realtimeStateInterval = setInterval(() => { diff --git a/src/main/main.ts b/src/main/main.ts index 1ac36e50..f6a08ffa 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -131,7 +131,7 @@ 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() From 3eb03d162746b80396680a83204945a8b798aa7b Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Mon, 10 Apr 2023 17:38:20 -0400 Subject: [PATCH 96/98] refactor: more cleanup --- src/app.config.ts | 9 + src/features/bpm/engine/Link.ts | 67 ++-- src/features/bpm/engine/index.ts | 70 ++++ src/features/bpm/shared/randomizer.ts | 30 ++ src/features/midi/engine/handleMidi.ts | 15 +- src/features/midi/engine/midiConnection.ts | 19 +- src/features/modulation/engine/index.ts | 16 +- src/features/params/engine/index.ts | 10 +- src/features/scenes/engine/autoScene.ts | 36 +- src/features/shared/engine/index.ts | 17 + .../engine/createVisualizerWindow.ts | 34 +- src/main/engine/api/core.ts | 4 +- src/main/engine/api/mutations.ts | 4 +- src/main/engine/engine.ts | 322 +++++++++--------- src/main/engine/util.ts | 22 -- src/main/main.ts | 7 +- src/main/util.ts | 27 +- 17 files changed, 418 insertions(+), 291 deletions(-) create mode 100644 src/app.config.ts create mode 100644 src/features/bpm/engine/index.ts create mode 100644 src/features/shared/engine/index.ts delete mode 100644 src/main/engine/util.ts 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 index 56009182..320e5f75 100644 --- a/src/features/bpm/engine/Link.ts +++ b/src/features/bpm/engine/Link.ts @@ -1,53 +1,42 @@ -import { UserCommand } from 'features/shared/engine/ipc_channels' import TapTempoEngine from './TapTempoEngine' import { TimeState } from '../shared/TimeState' import NodeLink from 'node-link' -import { RealtimeState } from 'renderer/redux/realtimeStore' -export const _nodeLink = new NodeLink() +export const createBpmController = () => { + const nodeLink = new NodeLink() -_nodeLink.setIsPlaying(true) -_nodeLink.enableStartStopSync(true) -_nodeLink.enable(true) + nodeLink.setIsPlaying(true) + nodeLink.enableStartStopSync(true) + nodeLink.enable(true) -const _tapTempoEngine = new TapTempoEngine() + const tapTempoEngine = new TapTempoEngine() -export function _tapTempo() { - _tapTempoEngine.tap((newBpm) => { - _nodeLink.setTempo(newBpm) - }) -} + let _lastFrameTime = 0 + // Todo: Desimate dt in this context + function getNextTimeState(): TimeState { + let currentTime = Date.now() + const dt = currentTime - _lastFrameTime -export const onLinkUserCommand = ( - command: UserCommand, - _realtimeState: RealtimeState -) => { - 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() - } -} + _lastFrameTime = currentTime -let _lastFrameTime = 0 -// Todo: Desimate dt in this context -export function getNextTimeState(): TimeState { - let currentTime = Date.now() - const dt = currentTime - _lastFrameTime + return { + ...nodeLink.getSessionInfoCurrent(), + dt: dt, + quantum: 4.0, + } + } - _lastFrameTime = currentTime + function tapTempo() { + tapTempoEngine.tap((newBpm) => { + nodeLink.setTempo(newBpm) + }) + } return { - ..._nodeLink.getSessionInfoCurrent(), - dt: dt, - quantum: 4.0, + getNextTimeState, + tapTempo, + nodeLink, } } + +export type BPMController = ReturnType 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/features/bpm/shared/randomizer.ts b/src/features/bpm/shared/randomizer.ts index 2e88f3e5..831e1ba1 100644 --- a/src/features/bpm/shared/randomizer.ts +++ b/src/features/bpm/shared/randomizer.ts @@ -1,5 +1,6 @@ import { TimeState, isNewPeriod } from './TimeState' 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/features/midi/engine/handleMidi.ts b/src/features/midi/engine/handleMidi.ts index fa321f96..658931f7 100644 --- a/src/features/midi/engine/handleMidi.ts +++ b/src/features/midi/engine/handleMidi.ts @@ -34,16 +34,15 @@ export type MidiConfig = { const midiConfig = _midiConfig as MidiConfig -export function handleMessage( +export function onMidiMessageInput( message: MidiMessage, - state: CleanReduxState, - rt_state: RealtimeState, + states: { state: CleanReduxState; realtime: RealtimeState }, nodeLink: NodeLink, dispatch: (action: PayloadAction) => void, tapTempo: () => void ) { const input = getInput(message) - const midiState = state.control.device + const midiState = states.state.control.device if (midiState.isEditing && midiState.listening) { /** @@ -102,7 +101,13 @@ export function handleMessage( /** * Receive Midi Data */ - const context = { dispatch, nodeLink, rt_state, state, tapTempo } + const context = { + dispatch, + nodeLink, + rt_state: states.realtime, + state: states.state, + tapTempo, + } const buttonAction = Object.entries(midiState.buttonActions).find( ([_actionId, action]) => action.inputID === input.id )?.[1] diff --git a/src/features/midi/engine/midiConnection.ts b/src/features/midi/engine/midiConnection.ts index 4072739e..379306cf 100644 --- a/src/features/midi/engine/midiConnection.ts +++ b/src/features/midi/engine/midiConnection.ts @@ -1,5 +1,5 @@ import { Input } from 'midi' -import { parseMessage, MidiMessage } from '../shared/midi' +import { parseMessage, MidiMessage, midiInputID } from '../shared/midi' import { MidiConnections, ConnectionId, @@ -15,6 +15,7 @@ interface Config { onUpdate: (activeDevices: UpdatePayload) => void onMessage: (message: MessagePayload) => void getConnectable: () => ConnectionId[] + throttledWaitMS: number } const refInput = new Input() @@ -22,9 +23,22 @@ const inputs: Inputs = {} type Inputs = { [portName: string]: Input } export function maintain(config: Config) { + const throttleMap = new ThrottleMap( + config.onMessage, + config.throttledWaitMS + ) + + const alteredConfig: Config = { + ...config, + onMessage: (message) => { + throttleMap.call(midiInputID(message), message) + }, + } + const interval = setInterval(() => { - updateInputs(config) + updateInputs(alteredConfig) }, config.update_ms) + return { dispose() { clearInterval(interval) @@ -32,6 +46,7 @@ export function maintain(config: Config) { input.closePort() delete inputs[inputName] }) + throttleMap.dispose() }, } } diff --git a/src/features/modulation/engine/index.ts b/src/features/modulation/engine/index.ts index fa6813c0..fc4e1f27 100644 --- a/src/features/modulation/engine/index.ts +++ b/src/features/modulation/engine/index.ts @@ -1,6 +1,6 @@ import { Modulation } from '../../params/shared/params' import { GetValue } from '../shared/oscillator' -import { LightScene_t } from '../../scenes/shared/Scenes' +import { Modulator } from '../shared/modulation' interface ModSnapshot { modulation: Modulation @@ -8,21 +8,21 @@ interface ModSnapshot { } export const createModulationTransformer = ({ - scene, - splitIndex, + modulators, + trackIndex, beats, }: { - scene: LightScene_t - splitIndex: number + modulators: Modulator[] + trackIndex: number beats: number }) => { - const snapshots: ModSnapshot[] = scene.modulators.map((modulator) => ({ - modulation: modulator.splitModulations[splitIndex], + const snapshots: ModSnapshot[] = modulators.map((modulator) => ({ + modulation: modulator.splitModulations[trackIndex], lfoVal: GetValue(modulator.lfo, beats), })) return { - transform({ baseParam, param }: { baseParam: number; param: string }) { + apply({ baseParam, param }: { baseParam: number; param: string }) { return snapshots.reduce((sum, { modulation, lfoVal }) => { const modAmount = modulation[param] if (modAmount === undefined) { diff --git a/src/features/params/engine/index.ts b/src/features/params/engine/index.ts index f591f47a..c370d06d 100644 --- a/src/features/params/engine/index.ts +++ b/src/features/params/engine/index.ts @@ -1,6 +1,7 @@ import { Window2D_t } from 'features/shared/shared/window' import { DefaultParam, + Params, StrictParams, defaultOutputParams, getParam, @@ -50,12 +51,11 @@ export function getBrightness( export const createOutputParams = ( { - scene, - splitIndex, + baseParams, allParamKeys, }: { - scene: LightScene_t - splitIndex: number + baseParams: Partial + allParamKeys: string[] }, transform: (params: { @@ -71,7 +71,7 @@ export const createOutputParams = ( return clampNormalized(transform({ param, baseParam })) } const outputParams = defaultOutputParams() - const baseParams = scene.splitScenes[splitIndex].baseParams + allParamKeys.forEach((param) => { outputParams[param] = _getOutputParam(baseParams[param], param) }) diff --git a/src/features/scenes/engine/autoScene.ts b/src/features/scenes/engine/autoScene.ts index f11b90fe..179dd133 100644 --- a/src/features/scenes/engine/autoScene.ts +++ b/src/features/scenes/engine/autoScene.ts @@ -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/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/features/visualizer/engine/createVisualizerWindow.ts b/src/features/visualizer/engine/createVisualizerWindow.ts index 43e6275f..d7c5050b 100644 --- a/src/features/visualizer/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 '../../../main/engine/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/src/main/engine/api/core.ts b/src/main/engine/api/core.ts index c40e9af6..d5b8134b 100644 --- a/src/main/engine/api/core.ts +++ b/src/main/engine/api/core.ts @@ -1,13 +1,12 @@ import { IpcMain, WebContents } from 'electron' import { VisualizerContainer } from 'features/visualizer/engine/createVisualizerWindow' -import { RealtimeState } from 'renderer/redux/realtimeStore' import ipc_channels from '../../../features/shared/engine/ipc_channels' import visualizerChannels from '../../../features/visualizer/shared/ipcChannels' export type Context = { renderer: WebContents visualizerContainer: VisualizerContainer - realtimeState: RealtimeState + realtimeManager: RealtimeManager ipcMain: IpcMain new_control_state: (new_state: CleanReduxState) => void } @@ -70,6 +69,7 @@ export const createVisualizerPublishers = < 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'] diff --git a/src/main/engine/api/mutations.ts b/src/main/engine/api/mutations.ts index 601e02c4..a7e5feb9 100644 --- a/src/main/engine/api/mutations.ts +++ b/src/main/engine/api/mutations.ts @@ -1,6 +1,6 @@ import ipcChannels from '../../../features/shared/engine/ipc_channels' -import { onLinkUserCommand } from 'features/bpm/engine/Link' +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' @@ -16,7 +16,7 @@ export const mutations = createMutations({ ...createMutation({ channel: ipcChannels.user_command, resolve: (context, command: UserCommand) => { - onLinkUserCommand(command, context.realtimeState) + onLinkUserCommand(command, context.realtimeManager) }, }), ...createMutation({ diff --git a/src/main/engine/engine.ts b/src/main/engine/engine.ts index 5b21b38c..0b081902 100644 --- a/src/main/engine/engine.ts +++ b/src/main/engine/engine.ts @@ -3,19 +3,11 @@ 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 { RealtimeState, SplitState } from '../../renderer/redux/realtimeStore' import { TimeState } from '../../features/bpm/shared/TimeState' -import { - initRandomizerState, - resizeRandomizer, - updateIndexes, -} from '../../features/bpm/shared/randomizer' +import { getNewRandomizerState } from '../../features/bpm/shared/randomizer' import { createModulationTransformer } from '../../features/modulation/engine' -import { handleMessage } from 'features/midi/engine/handleMidi' +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' @@ -24,24 +16,20 @@ import { flatten_fixtures, getFixturesInGroups, } from '../../features/dmx/shared/dmxUtil' -import { ThrottleMap } from 'features/midi/engine/midiConnection' -import { MidiMessage, midiInputID } from 'features/midi/shared/midi' - -import { indexArray } from '../../features/utils/util' import WledManager from '../../features/led/engine/wled_manager' -import { - getNextTimeState, - _nodeLink, - _tapTempo, -} from 'features/bpm/engine/Link' + import { createApi, IPC_Callbacks } from './api' import { createOutputParams } from 'features/params/engine' import { getAllParamKeys } from 'features/params/shared/params' - -let _realtimeState: RealtimeState = initRealtimeState() +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 controlStateManager = () => { +const createControlStateManager = () => { let resolveFirstState: () => void const firstControlState = new Promise((resolve) => { resolveFirstState = resolve @@ -49,12 +37,16 @@ const controlStateManager = () => { let cancelled = false - const manager = { + const controlStateRef = { + current: null as CleanReduxState | null, + } + + return { set(state: CleanReduxState) { - if (!manager.controlState) resolveFirstState() - manager.controlState = state + if (!controlStateRef.current) resolveFirstState() + controlStateRef.current = state }, - controlState: null as CleanReduxState | null, + stateRef: controlStateRef, dispose() { cancelled = true @@ -66,114 +58,98 @@ const controlStateManager = () => { }) }, } - return manager } -const disposer = { - _disposeables: [] as { dispose(): void }[], - push(disposeable: T) { - disposer._disposeables.push(disposeable) - return disposeable - }, - dispose() { - disposer._disposeables.forEach((disposeable) => { - disposeable.dispose() - }) - disposer._disposeables = [] - }, -} +const disposer = createDisposer() export async function start( renderer: WebContents, visualizerContainer: VisualizerContainer ) { - const controlState = disposer.push(controlStateManager()) + const realtimeManager = createRealtimeManager({ + next(previousStates, newStates) { + const nextRealtimeState = getNextRealtimeState( + { + controlState: controlStateManager.stateRef.current!, + realtimeState: previousStates.realtime, + timeState: newStates.time, + }, + api + ) + return nextRealtimeState + }, + onUpdate(newStates) { + api.publishers.new_time_state(newStates.realtime) + api.publishers.new_visualizer_state({ + rt: newStates.realtime, + state: controlStateManager.stateRef.current!, + }) + }, + }) + + const controlStateManager = disposer.push(createControlStateManager()) + const api = disposer.push( createApi({ ipcMain, - realtimeState: _realtimeState, + realtimeManager, new_control_state: (newState) => { - controlState.set(newState) + controlStateManager.set(newState) }, renderer, visualizerContainer, }) ) - // TODO: now we don't need null checks for control state - await controlState.waitForFirstControlState(() => { + 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) - const realtimeStateInterval = setInterval(() => { - const nextTimeState = getNextTimeState() - - _realtimeState = getNextRealtimeState( - _realtimeState, - nextTimeState, - api, - controlState.controlState! - ) - api.publishers.new_time_state(_realtimeState) - api.publishers.new_visualizer_state({ - rt: _realtimeState, - state: controlState.controlState!, - }) - }, 1000 / 90) - - disposer.push({ - dispose() { - clearInterval(realtimeStateInterval) - }, - }) - - const _midiThrottle = disposer.push( - new ThrottleMap((message: MidiMessage) => { - // TODO: maybe we could cancel the throttle on close and initialize throttle after callbacks and control state are initialized - // to avoid null checks - - handleMessage( - message, - controlState.controlState!, - _realtimeState, - _nodeLink, - api.publishers.dispatch, - _tapTempo - ) - }, 1000 / 60) - ) + disposer.push(realtimeManager.start()) disposer.push( DmxConnection.maintain({ - update_ms: 1000, + update_ms: AppConfig.dmx.updateIntervalMS, onUpdate: (dmxStatus) => { api.publishers.dmx_connection_update(dmxStatus) }, - getChannels: () => _realtimeState.dmxOut, + getChannels: () => realtimeManager.realtimeStateRef.current.dmxOut, getConnectable: () => { - return controlState.controlState!.control.device.connectable.dmx + return controlStateManager.stateRef.current!.control.device + .connectable.dmx }, }) ) disposer.push( MidiConnection.maintain({ - update_ms: 1000, + throttledWaitMS: AppConfig.midi.throttledWaitMS, + update_ms: AppConfig.midi.updateIntervalMS, onUpdate: (activeDevices) => { api.publishers.midi_connection_update(activeDevices) }, onMessage: (message) => { - _midiThrottle.call(midiInputID(message), message) + onMidiMessageInput( + message, + { + state: controlStateManager.stateRef.current!, + realtime: realtimeManager.realtimeStateRef.current, + }, + realtimeManager.bpmController.nodeLink, + api.publishers.dispatch, + realtimeManager.bpmController.tapTempo + ) }, getConnectable: () => { - return controlState.controlState!.control.device.connectable.midi + return controlStateManager.stateRef.current!.control.device + .connectable.midi }, }) ) disposer.push( new WledManager( - () => controlState.controlState, - () => _realtimeState + () => controlStateManager.stateRef.current, + () => realtimeManager.realtimeStateRef.current ) ) }) @@ -185,92 +161,114 @@ export function stop() { disposer.dispose() } +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) + ) + + const splitSceneFixtures = getFixturesInGroups(fixtures, sceneTrack.groups) + + const splitSceneFixturesWithinEpicness = splitSceneFixtures.filter( + (fixture) => fixture.intensity <= (outputParams.intensity ?? 1) + ) + + const newRandomizerState = getNewRandomizerState({ + previousRandomizerState: previousRandomizer, + beatsLast: realtimeState.time.beats, + options: sceneTrack.randomizer, + size: splitSceneFixturesWithinEpicness.length, + timeState, + }) + + return { + outputParams, + randomizer: newRandomizerState, + } + } +} + function getNextRealtimeState( - realtimeState: RealtimeState, - nextTimeState: TimeState, - api: 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) => { - api.publishers.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) => { - api.publishers.dispatch( - setActiveScene({ - sceneType: 'visual', - val: newVisualScene, - }) - ) - } - ) + }) const fixtures = flatten_fixtures(dmx.universe, dmx.fixtureTypesByID) - const splitStates: SplitState[] = scene.splitScenes.map( - (splitScene, splitIndex) => { - const modTransformer = createModulationTransformer({ - beats: nextTimeState.beats, - scene, - splitIndex, - }) - const splitOutputParams = createOutputParams( - { allParamKeys, scene, splitIndex }, - (options) => modTransformer.transform(options) - ) - - const splitSceneFixtures = getFixturesInGroups( - fixtures, - splitScene.groups - ) - - const splitSceneFixturesWithinEpicness = splitSceneFixtures.filter( - (fixture) => fixture.intensity <= (splitOutputParams.intensity ?? 1) - ) - - let newRandomizerState = resizeRandomizer( - realtimeState.splitStates[splitIndex]?.randomizer ?? - initRandomizerState(), - splitSceneFixturesWithinEpicness.length - ) - - newRandomizerState = updateIndexes( - realtimeState.time.beats, - newRandomizerState, - nextTimeState, - indexArray(splitSceneFixturesWithinEpicness.length), - splitScene.randomizer - ) - - return { - outputParams: splitOutputParams, - randomizer: newRandomizerState, - } - } + // create scene tracks outputs + const trackOutputs: SplitState[] = scene.splitScenes.map( + createTrackOutputs(states, { + allParamKeys, + fixtures, + modulators: scene.modulators, + }) ) - const outChannelConfig = getChannels({ state: controlState }) + const outChannelConfig = getChannels({ state: states.controlState }) - if (nextTimeState.isPlaying) { - calculateDmxOut({ splitStates, state: controlState }, outChannelConfig) + if (states.timeState.isPlaying) { + // send data to output devices + calculateDmxOut( + { splitStates: trackOutputs, state: states.controlState }, + outChannelConfig + ) } return { - time: nextTimeState, + time: states.timeState, dmxOut: outChannelConfig.channels, - splitStates, + splitStates: trackOutputs, } } 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/main.ts b/src/main/main.ts index f6a08ffa..a87410bf 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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 = await 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)}` + } } From 70a00d74eb0dc53ee9c0b8d1ac4d68110cfbd137 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Mon, 10 Apr 2023 22:33:29 -0400 Subject: [PATCH 97/98] refactor: draggable number --- src/features/midi/react/MidiOverlay.tsx | 4 +- src/features/params/engine/index.ts | 1 - .../ui/react/base/DraggableNumber.tsx | 164 +++++++++++------- 3 files changed, 107 insertions(+), 62 deletions(-) diff --git a/src/features/midi/react/MidiOverlay.tsx b/src/features/midi/react/MidiOverlay.tsx index 24c8b960..05d32f43 100644 --- a/src/features/midi/react/MidiOverlay.tsx +++ b/src/features/midi/react/MidiOverlay.tsx @@ -147,7 +147,7 @@ export function RangeMidiOverlay({ )} & { + valueRef: MutableRefObject + movementRef: MutableRefObject + adjustments: { primary: Type; secondary: Type } +}) => { 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 + const adjustments = { + continuous: () => { + valueRef.current += d / 2 + }, + snap: () => { + 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 + } } - } - } + }, + } as const + // use when control key pressed if (secondaryEnabled(e)) { - if (secondaryBehavior === 'snap') { - adjust_snap() - } else { - adjust_continuous() - } + adjustments[adjustmentsConfig.secondary]() } else { - if (type === 'snap') { - adjust_snap() - } else { - adjust_continuous() - } + adjustments[adjustmentsConfig.primary]() } - globalValue = clamp(globalValue, min, max) + valueRef.current = clamp(valueRef.current, min, max) - onChange(globalValue) + onChange(valueRef.current) }) function onMouseDownWrapper(e: React.MouseEvent) { - globalMovement = 0 - globalValue = value + movementRef.current = 0 + valueRef.current = value onMouseDown(e) } - let valueString = - type === 'snap' && Number.isInteger(value) - ? value.toString() - : value.toFixed(2) + return { + dragContainer, + onMouseDownWrapper, + } +} + +// type AdjustmentsConfig = { +// ToString: Partial<{ +// [k in Type]: (v: number) => string +// }> +// } + +export default function DraggableNumber({ + adjustments: adjustmentsConfig = { primary: 'snap', secondary: 'continuous' }, + + value, + min, + max, + onChange, + style, + suffix, + noArrows, +}: Props) { + const adjustments = Object.assign( + { primary: 'snap', secondary: 'continuous' }, + adjustmentsConfig + ) + const { dragContainer, onMouseDownWrapper } = useDraggableNumber({ + max, + min, + movementRef: globalMovementRef, + onChange, + value, + valueRef: globalValueRef, + adjustments, + }) + + const adjustmentsToString: Partial<{ + [k in Type]: (v: number) => string + }> = { + snap(v: number) { + if (Number.isInteger(v)) return v.toString() + return v.toFixed(2) + }, + } as const + + const valueToString = (v: number) => { + const toString = adjustmentsToString[adjustments.primary] + + let valueString = toString ? toString(v) : v.toFixed(2) + + if (suffix) valueString += suffix + + return valueString + } - if (suffix) valueString += suffix + const valueString = valueToString(value) const onUp = () => { const doubled = double_incremented(value) From e745941d47e1f2a12039d38b20e7c9beb56a35f3 Mon Sep 17 00:00:00 2001 From: Ryan Kauk Date: Mon, 10 Apr 2023 23:01:44 -0400 Subject: [PATCH 98/98] refactor: use draggable number --- .../ui/react/base/DraggableNumber.tsx | 215 ++++++++++-------- 1 file changed, 116 insertions(+), 99 deletions(-) diff --git a/src/features/ui/react/base/DraggableNumber.tsx b/src/features/ui/react/base/DraggableNumber.tsx index e146fac1..ea6699e8 100644 --- a/src/features/ui/react/base/DraggableNumber.tsx +++ b/src/features/ui/react/base/DraggableNumber.tsx @@ -8,15 +8,23 @@ import { secondaryEnabled } from './keyUtil' type Type = 'continuous' | 'snap' -interface Props { - adjustments?: { primary?: Type; secondary?: Type } +interface UseDraggableNumber { + adjustments: { primary: T; secondary: T } value: number min: number max: number onChange: (newVal: number) => void - style?: React.CSSProperties + + 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 @@ -24,59 +32,45 @@ interface Props { let globalMovementRef = { current: 0 } let globalValueRef = { current: 0 } -const useDraggableNumber = ({ - onChange, - min, - value, - valueRef, - movementRef, - max, - adjustments: adjustmentsConfig, -}: Pick & { - valueRef: MutableRefObject - movementRef: MutableRefObject - adjustments: { primary: Type; secondary: Type } -}) => { +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 - const adjustments = { - continuous: () => { - valueRef.current += d / 2 - }, - snap: () => { - 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 - } - } - }, - } as const - // use when control key pressed if (secondaryEnabled(e)) { - adjustments[adjustmentsConfig.secondary]() + calculate[adjustmentsConfig.secondary]({ movementRef, valueRef, d }) } else { - adjustments[adjustmentsConfig.primary]() + calculate[adjustmentsConfig.primary]({ movementRef, valueRef, d }) } valueRef.current = clamp(valueRef.current, min, max) @@ -84,15 +78,36 @@ const useDraggableNumber = ({ onChange(valueRef.current) }) - function onMouseDownWrapper(e: React.MouseEvent) { - movementRef.current = 0 - valueRef.current = value - onMouseDown(e) + 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, - onMouseDownWrapper, + onMouseDown: (e: React.MouseEvent) => { + movementRef.current = 0 + valueRef.current = value + onMouseDown(e) + }, + valueToString, + onUp, + onDown, } } @@ -104,64 +119,66 @@ const useDraggableNumber = ({ export default function DraggableNumber({ adjustments: adjustmentsConfig = { primary: 'snap', secondary: 'continuous' }, - value, - min, - max, - onChange, style, - suffix, noArrows, + ...useDraggableNumberProps }: Props) { const adjustments = Object.assign( { primary: 'snap', secondary: 'continuous' }, adjustmentsConfig ) - const { dragContainer, onMouseDownWrapper } = useDraggableNumber({ - max, - min, - movementRef: globalMovementRef, - onChange, - value, - valueRef: globalValueRef, - adjustments, - }) - - const adjustmentsToString: Partial<{ - [k in Type]: (v: number) => string - }> = { - snap(v: number) { - if (Number.isInteger(v)) return v.toString() - return v.toFixed(2) - }, - } as const - - const valueToString = (v: number) => { - const toString = adjustmentsToString[adjustments.primary] - - let valueString = toString ? toString(v) : v.toFixed(2) - - if (suffix) valueString += suffix - - return valueString - } - - const valueString = valueToString(value) - - const onUp = () => { - const doubled = double_incremented(value) - onChange(Math.min(doubled, max)) - } - - const onDown = () => { - const halved = halve_incremented(value) - onChange(Math.max(halved, min)) - } + 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 ( - - {valueString} + + {valueToString(value)} {!noArrows && (