From 74ecfe2819162bdde9fe28ccf39caacab580901f Mon Sep 17 00:00:00 2001 From: bjoluc Date: Tue, 23 Apr 2024 19:00:11 +0200 Subject: [PATCH] refactor: Refactor encoder page mapping --- src/device-configs/index.d.ts | 4 +- src/mapping/encoders/EncoderMapper.ts | 152 ++++------------------- src/mapping/encoders/EncoderPage.ts | 98 ++++++++------- src/mapping/encoders/EncoderPageGroup.ts | 132 ++++++++++++++++++++ src/mapping/encoders/index.ts | 6 +- 5 files changed, 217 insertions(+), 175 deletions(-) create mode 100644 src/mapping/encoders/EncoderPageGroup.ts diff --git a/src/device-configs/index.d.ts b/src/device-configs/index.d.ts index ba39f36..c09d9eb 100644 --- a/src/device-configs/index.d.ts +++ b/src/device-configs/index.d.ts @@ -208,9 +208,9 @@ export interface DeviceConfig { * The default config is defined in {@link file://./../mapping/encoders/index.ts} */ configureEncoderAssignments?( - defaultEncoderMapping: EncoderMappingConfig, + defaultEncoderMappings: EncoderMappingConfig[], page: MR_FactoryMappingPage, - ): EncoderMappingConfig; + ): EncoderMappingConfig[]; enhanceMapping?(mappingDependencies: { driver: MR_DeviceDriver; diff --git a/src/mapping/encoders/EncoderMapper.ts b/src/mapping/encoders/EncoderMapper.ts index d62eb9b..0f4157d 100644 --- a/src/mapping/encoders/EncoderMapper.ts +++ b/src/mapping/encoders/EncoderMapper.ts @@ -1,18 +1,16 @@ -import { EncoderPage, EncoderPageConfig } from "./EncoderPage"; +import { EncoderMappingDependencies, EncoderPage, EncoderPageConfig } from "./EncoderPage"; +import { EncoderPageGroup } from "./EncoderPageGroup"; import { LedButton } from "/decorators/surface-elements/LedButton"; -import { ChannelSurfaceElements, ControlSectionSurfaceElements } from "/device-configs"; import { Device, MainDevice } from "/devices"; import { SegmentDisplayManager } from "/midi/managers/SegmentDisplayManager"; -import { ChannelTextManager } from "/midi/managers/lcd/ChannelTextManager"; import { GlobalState } from "/state"; -import { ContextVariable } from "/util"; /** * The joint configuration for all "encoder assignments". Each encoder assignment maps a number of * encoder pages to a specified button. Each encoder page specifies host mappings ("assignments") * for an arbitrary number of encoders. */ -export type EncoderMappingConfig = Array<{ +export type EncoderMappingConfig = { /** * A function that – given a `MainDevice` – returns the device's button that will be mapped to the * provided encoder pages. @@ -26,134 +24,38 @@ export type EncoderMappingConfig = Array<{ * each device's activator button. It can be used to add additional host mappings. */ enhanceMapping?: (pages: EncoderPage[], activatorButtons: LedButton[]) => void; -}>; +}; export class EncoderMapper { - private readonly channelElements: ChannelSurfaceElements[]; - private readonly channelTextManagers: ChannelTextManager[]; - - /** An array containing the control buttons of each main device */ - private readonly deviceButtons: ControlSectionSurfaceElements["buttons"][]; - - private readonly mainDevices: MainDevice[]; - - private readonly subPageArea: MR_SubPageArea; - - private activeEncoderPage = new ContextVariable(undefined); + private readonly dependencies: EncoderMappingDependencies; constructor( - private readonly page: MR_FactoryMappingPage, + page: MR_FactoryMappingPage, devices: Device[], - private readonly mixerBankChannels: MR_MixerBankChannel[], - private readonly segmentDisplayManager: SegmentDisplayManager, - private readonly globalState: GlobalState, + mixerBankChannels: MR_MixerBankChannel[], + segmentDisplayManager: SegmentDisplayManager, + globalState: GlobalState, ) { - this.channelElements = devices.flatMap((device) => device.channelElements); - this.channelTextManagers = devices.flatMap((device) => device.lcdManager.channelTextManagers); - this.mainDevices = devices.filter((device) => device instanceof MainDevice) as MainDevice[]; - this.deviceButtons = this.mainDevices.map( - (device) => (device as MainDevice).controlSectionElements.buttons, - ); - - this.subPageArea = page.makeSubPageArea("Encoders"); + const mainDevices = devices.filter((device) => device instanceof MainDevice) as MainDevice[]; + + this.dependencies = { + page, + encoderSubPageArea: page.makeSubPageArea("Encoders"), + mainDevices, + deviceButtons: mainDevices.map( + (device) => (device as MainDevice).controlSectionElements.buttons, + ), + channelElements: devices.flatMap((device) => device.channelElements), + mixerBankChannels, + channelTextManagers: devices.flatMap((device) => device.lcdManager.channelTextManagers), + segmentDisplayManager, + globalState, + }; } - /** - * Takes an array of `EncoderPageConfig`s, splits all pages with more encoder assignments than - * physical encoders into multiple pages and returns the resulting page config array. - */ - private splitEncoderPageConfigs(pages: EncoderPageConfig[]) { - const encoderPageSize = this.channelElements.length; - - return pages.flatMap((page) => { - const assignments = page.assignments; - if (Array.isArray(assignments) && assignments.length > encoderPageSize) { - const chunks = []; - for (let i = 0; i < assignments.length / encoderPageSize; i++) { - chunks.push(assignments.slice(i * encoderPageSize, (i + 1) * encoderPageSize)); - } - return chunks.map((chunk) => ({ - ...page, - assignments: chunk, - })); - } - - return page; - }); - } - - private bindEncoderPagesToAssignButton( - activatorButtons: LedButton[], - pageConfigs: EncoderPageConfig[], - ) { - pageConfigs = this.splitEncoderPageConfigs(pageConfigs); - const pages = pageConfigs.map((pageConfig, pageIndex) => { - return new EncoderPage( - this, - pageConfig, - activatorButtons, - pageIndex, - pageConfigs.length, - this.page, - this.subPageArea, - this.deviceButtons, - this.channelElements, - this.mixerBankChannels, - this.channelTextManagers, - this.segmentDisplayManager, - this.globalState, - ); - }); - - // Bind encoder assign buttons to cycle through sub pages in a round-robin fashion - for (const activatorButton of activatorButtons) { - const activatorButtonValue = activatorButton.mSurfaceValue; - this.page.makeActionBinding( - activatorButtonValue, - pages[0].subPages.default.mAction.mActivate, - ); - - let previousSubPages = pages[0].subPages; - for (const { subPages: currentSubPages } of pages) { - this.page - .makeActionBinding(activatorButtonValue, currentSubPages.default.mAction.mActivate) - .setSubPage(previousSubPages.default); - this.page - .makeActionBinding(activatorButtonValue, currentSubPages.default.mAction.mActivate) - .setSubPage(previousSubPages.flip); - - previousSubPages = currentSubPages; - } - } - - return pages; - } - - applyEncoderMappingConfig(config: EncoderMappingConfig) { - for (const mappingConfig of config) { - const activatorButtons = this.mainDevices.map(mappingConfig.activatorButtonSelector); - const encoderPages = this.bindEncoderPagesToAssignButton( - activatorButtons, - mappingConfig.pages, - ); - - if (mappingConfig.enhanceMapping) { - mappingConfig.enhanceMapping(encoderPages, activatorButtons); - } - } - } - - /** - * This is invoked by an {@link EncoderPage} when one of its subpages gets activated. It keeps - * track of the currently active `EncoderPage` and runs the {@link EncoderPage.onActivated()} and - * {@link EncoderPage.onDeactivated()} callbacks. - */ - onEncoderPageSubPageActivated(context: MR_ActiveDevice, encoderPage: EncoderPage) { - const lastActiveEncoderPage = this.activeEncoderPage.get(context); - if (lastActiveEncoderPage !== encoderPage) { - lastActiveEncoderPage?.onDeactivated(context); - this.activeEncoderPage.set(context, encoderPage); - encoderPage.onActivated(context); + applyEncoderMappingConfigs(configs: EncoderMappingConfig[]) { + for (const config of configs) { + new EncoderPageGroup(this.dependencies, config); } } } diff --git a/src/mapping/encoders/EncoderPage.ts b/src/mapping/encoders/EncoderPage.ts index 4f273e6..eed09c9 100644 --- a/src/mapping/encoders/EncoderPage.ts +++ b/src/mapping/encoders/EncoderPage.ts @@ -1,8 +1,10 @@ import type { EncoderMapper } from "./EncoderMapper"; +import { EncoderPageGroup } from "./EncoderPageGroup"; import { config } from "/config"; import { LedButton } from "/decorators/surface-elements/LedButton"; import { EncoderDisplayMode, LedPushEncoder } from "/decorators/surface-elements/LedPushEncoder"; import { ChannelSurfaceElements, ControlSectionButtons } from "/device-configs"; +import { MainDevice } from "/devices"; import { SegmentDisplayManager } from "/midi/managers/SegmentDisplayManager"; import { ChannelTextManager } from "/midi/managers/lcd/ChannelTextManager"; import { GlobalState } from "/state"; @@ -53,6 +55,22 @@ interface SubPages { flipShift: MR_SubPage; } +export interface EncoderMappingDependencies { + page: MR_FactoryMappingPage; + encoderSubPageArea: MR_SubPageArea; + + mainDevices: MainDevice[]; + + /** An array containing the control buttons of each main device */ + deviceButtons: ControlSectionButtons[]; + + channelElements: ChannelSurfaceElements[]; + mixerBankChannels: MR_MixerBankChannel[]; + channelTextManagers: ChannelTextManager[]; + segmentDisplayManager: SegmentDisplayManager; + globalState: GlobalState; +} + export class EncoderPage implements EncoderPageConfig { public readonly subPages: SubPages; public readonly name: string; @@ -63,20 +81,12 @@ export class EncoderPage implements EncoderPageConfig { private lastSubPageActivationTime = 0; constructor( - private readonly encoderMapper: EncoderMapper, + private readonly encoderPageGroup: EncoderPageGroup, + private dependencies: EncoderMappingDependencies, pageConfig: EncoderPageConfig, public readonly activatorButtons: LedButton[], public readonly index: number, public readonly pagesCount: number, - - private readonly page: MR_FactoryMappingPage, - private readonly subPageArea: MR_SubPageArea, - private readonly deviceButtons: ControlSectionButtons[], - private readonly channelElements: ChannelSurfaceElements[], - private readonly mixerBankChannels: MR_MixerBankChannel[], - private readonly channelTextManagers: ChannelTextManager[], - private readonly segmentDisplayManager: SegmentDisplayManager, - private readonly globalState: GlobalState, ) { this.name = pageConfig.name; this.areAssignmentsChannelRelated = pageConfig.areAssignmentsChannelRelated; @@ -84,7 +94,9 @@ export class EncoderPage implements EncoderPageConfig { const assignmentsConfig = pageConfig.assignments; this.assignments = typeof assignmentsConfig === "function" - ? mixerBankChannels.map((channel, channelIndex) => assignmentsConfig(channel, channelIndex)) + ? dependencies.mixerBankChannels.map((channel, channelIndex) => + assignmentsConfig(channel, channelIndex), + ) : assignmentsConfig; for (const assignment of this.assignments) { @@ -98,13 +110,14 @@ export class EncoderPage implements EncoderPageConfig { } private createSubPages(): SubPages { + const subPageArea = this.dependencies.encoderSubPageArea; const subPageName = `${this.name} ${this.index + 1}`; const subPages: SubPages = { - default: this.subPageArea.makeSubPage(subPageName), - defaultShift: this.subPageArea.makeSubPage(`${subPageName} Shift`), - flip: this.subPageArea.makeSubPage(`${subPageName} Flip`), - flipShift: this.subPageArea.makeSubPage(`${subPageName} Flip Shift`), + default: subPageArea.makeSubPage(subPageName), + defaultShift: subPageArea.makeSubPage(`${subPageName} Shift`), + flip: subPageArea.makeSubPage(`${subPageName} Flip`), + flipShift: subPageArea.makeSubPage(`${subPageName} Flip Shift`), }; subPages.default.mOnActivate = this.onSubPageActivated.bind(this, false, false); @@ -122,7 +135,7 @@ export class EncoderPage implements EncoderPageConfig { enhancer?: (binding: MR_ValueBinding) => void, ): MR_ValueBinding[] { const bindings = subPages.map((subPage) => - this.page.makeValueBinding(surfaceValue, hostValue).setSubPage(subPage), + this.dependencies.page.makeValueBinding(surfaceValue, hostValue).setSubPage(subPage), ); if (enhancer) { bindings.map(enhancer); @@ -131,19 +144,19 @@ export class EncoderPage implements EncoderPageConfig { } private bindSubPages() { - for (const { flip: flipButton } of this.deviceButtons) { - this.page + for (const { flip: flipButton } of this.dependencies.deviceButtons) { + this.dependencies.page .makeActionBinding(flipButton.mSurfaceValue, this.subPages.flip.mAction.mActivate) .setSubPage(this.subPages.default); - this.page + this.dependencies.page .makeActionBinding(flipButton.mSurfaceValue, this.subPages.default.mAction.mActivate) .setSubPage(this.subPages.flip); } - this.globalState.isShiftModeActive.addOnChangeCallback( + this.dependencies.globalState.isShiftModeActive.addOnChangeCallback( (context, isShiftModeActive, mapping) => { if (this.isActive.get(context)) { - const isFlipModeActive = this.globalState.isFlipModeActive.get(context); + const isFlipModeActive = this.dependencies.globalState.isFlipModeActive.get(context); const nextSubPage = [ // Flip mode inactive @@ -163,12 +176,16 @@ export class EncoderPage implements EncoderPageConfig { }, ); - for (const [channelIndex, { encoder, fader }] of this.channelElements.entries()) { - const mSelected = this.mixerBankChannels[channelIndex].mValue.mSelected; + for (const [channelIndex, { encoder, fader }] of this.dependencies.channelElements.entries()) { + const mSelected = this.dependencies.mixerBankChannels[channelIndex].mValue.mSelected; const { - encoderValue = this.page.mCustom.makeHostValueVariable("unassignedEncoderValue"), - pushToggleValue = this.page.mCustom.makeHostValueVariable("unassignedEncoderPushValue"), + encoderValue = this.dependencies.page.mCustom.makeHostValueVariable( + "unassignedEncoderValue", + ), + pushToggleValue = this.dependencies.page.mCustom.makeHostValueVariable( + "unassignedEncoderPushValue", + ), onPush: pushAction, encoderValueDefault, onShiftPush: shiftPushAction, @@ -230,7 +247,7 @@ export class EncoderPage implements EncoderPageConfig { // Shift bindings this.makeMultiSubPageValueBinding( encoder.mPushValue, - this.page.mCustom.makeHostValueVariable("defaultShiftEncoderPushValue"), + this.dependencies.page.mCustom.makeHostValueVariable("defaultShiftEncoderPushValue"), [this.subPages.defaultShift, this.subPages.flipShift], (binding) => { if (shiftPushAction || typeof encoderValueDefault !== "undefined") { @@ -250,62 +267,53 @@ export class EncoderPage implements EncoderPageConfig { } } - private setActivatorButtonLeds(context: MR_ActiveDevice, value: number) { - for (const button of this.activatorButtons) { - button.setLedValue(context, value); - } - } - public onActivated(context: MR_ActiveDevice) { this.isActive.set(context, true); - this.segmentDisplayManager.setAssignment( + this.dependencies.segmentDisplayManager.setAssignment( context, this.pagesCount === 1 ? " " : `${this.index + 1}.${this.pagesCount}`, ); - this.setActivatorButtonLeds(context, 1); - - for (const [encoderIndex, { encoder }] of this.channelElements.entries()) { + for (const [encoderIndex, { encoder }] of this.dependencies.channelElements.entries()) { const assignment = this.assignments[encoderIndex] as EncoderAssignmentConfig | undefined; encoder.displayMode.set(context, assignment?.displayMode ?? EncoderDisplayMode.SingleDot); if (assignment?.encoderValueName) { - this.channelTextManagers[encoderIndex].setParameterNameOverride( + this.dependencies.channelTextManagers[encoderIndex].setParameterNameOverride( context, assignment.encoderValueName, ); } else { - this.channelTextManagers[encoderIndex].clearParameterNameOverride(context); + this.dependencies.channelTextManagers[encoderIndex].clearParameterNameOverride(context); } } - this.globalState.isValueDisplayModeActive.set(context, false); + this.dependencies.globalState.isValueDisplayModeActive.set(context, false); } public onDeactivated(context: MR_ActiveDevice) { this.isActive.set(context, false); - this.setActivatorButtonLeds(context, 0); } private onSubPageActivated(flip: boolean, shift: boolean, context: MR_ActiveDevice) { this.lastSubPageActivationTime = performance.now(); - this.encoderMapper.onEncoderPageSubPageActivated(context, this); + this.encoderPageGroup.onEncoderPageSubPageActivated(context, this); - if (this.globalState.isFlipModeActive.get(context) !== flip) { - this.globalState.isFlipModeActive.set(context, flip); + if (this.dependencies.globalState.isFlipModeActive.get(context) !== flip) { + this.dependencies.globalState.isFlipModeActive.set(context, flip); } if (shift) { // On shift sub pages, all encoder push values are bound to "undefined host values" and thus // keep whatever state they had in the previous binding, hence resetting them here to reliably // detect pushes via `mOnProcessValueChange`: - for (const { encoder } of this.channelElements) { + for (const { encoder } of this.dependencies.channelElements) { encoder.mPushValue.setProcessValue(context, 0); } } else { // If some encoder push values are bound to "undefined host values", reset them too: - for (const [channelIndex, { encoder }] of this.channelElements.entries()) { + for (const [channelIndex, { encoder }] of this.dependencies.channelElements.entries()) { if ( channelIndex < this.assignments.length && !this.assignments[channelIndex].pushToggleValue diff --git a/src/mapping/encoders/EncoderPageGroup.ts b/src/mapping/encoders/EncoderPageGroup.ts new file mode 100644 index 0000000..f96a9d2 --- /dev/null +++ b/src/mapping/encoders/EncoderPageGroup.ts @@ -0,0 +1,132 @@ +import { EncoderMappingConfig } from "./EncoderMapper"; +import { EncoderMappingDependencies, EncoderPage, EncoderPageConfig } from "./EncoderPage"; +import { LedButton } from "/decorators/surface-elements/LedButton"; +import { ContextVariable } from "/util"; + +export class EncoderPageGroup { + private static activeInstance = new ContextVariable(undefined); + + private activeEncoderPage = new ContextVariable(undefined); + private activatorButtons: LedButton[]; + + constructor( + private dependencies: EncoderMappingDependencies, + config: EncoderMappingConfig, + ) { + this.activatorButtons = dependencies.mainDevices.map(config.activatorButtonSelector); + const encoderPages = this.createEncoderPages( + this.splitEncoderPageConfigs(config.pages), + this.activatorButtons, + ); + this.bindEncoderPagesToActivatorButtons(encoderPages); + + if (config.enhanceMapping) { + config.enhanceMapping(encoderPages, this.activatorButtons); + } + } + + /** + * Takes an array of `EncoderPageConfig`s, splits all pages with more encoder assignments than + * physical encoders into multiple pages and returns the resulting page config array. + */ + private splitEncoderPageConfigs(pages: EncoderPageConfig[]) { + const encoderPageSize = this.dependencies.channelElements.length; + + return pages.flatMap((page) => { + const assignments = page.assignments; + if (Array.isArray(assignments) && assignments.length > encoderPageSize) { + const chunks = []; + for (let i = 0; i < assignments.length / encoderPageSize; i++) { + chunks.push(assignments.slice(i * encoderPageSize, (i + 1) * encoderPageSize)); + } + return chunks.map((chunk) => ({ + ...page, + assignments: chunk, + })); + } + + return page; + }); + } + + /** + * Given a list of `EncoderPageConfig`s and the button(s) that cycle through the encoder pages, + * this method creates `EncoderPage`s for them and returns the resulting list of encoder pages. + */ + private createEncoderPages(pageConfigs: EncoderPageConfig[], activatorButtons: LedButton[]) { + return pageConfigs.map((pageConfig, pageIndex) => { + return new EncoderPage( + this, + this.dependencies, + pageConfig, + activatorButtons, + pageIndex, + pageConfigs.length, + ); + }); + } + + private bindEncoderPagesToActivatorButtons(encoderPages: EncoderPage[]) { + // Bind encoder assign buttons to cycle through sub pages in a round-robin fashion + for (const activatorButton of this.activatorButtons) { + const activatorButtonValue = activatorButton.mSurfaceValue; + this.dependencies.page.makeActionBinding( + activatorButtonValue, + encoderPages[0].subPages.default.mAction.mActivate, + ); + + let previousSubPages = encoderPages[0].subPages; + for (const { subPages: currentSubPages } of encoderPages) { + this.dependencies.page + .makeActionBinding(activatorButtonValue, currentSubPages.default.mAction.mActivate) + .setSubPage(previousSubPages.default); + this.dependencies.page + .makeActionBinding(activatorButtonValue, currentSubPages.default.mAction.mActivate) + .setSubPage(previousSubPages.flip); + + previousSubPages = currentSubPages; + } + } + } + + private setActivatorButtonLeds(context: MR_ActiveDevice, value: number) { + for (const button of this.activatorButtons) { + button.setLedValue(context, value); + } + } + + /** + * This is invoked by an {@link EncoderPage} when one of its subpages gets activated. It keeps + * track of the currently active `EncoderPage` and `EncoderPageGroup` and runs their + * (de)activation callbacks. + */ + onEncoderPageSubPageActivated(context: MR_ActiveDevice, encoderPage: EncoderPage) { + const lastActiveEncoderPageGroup = EncoderPageGroup.activeInstance.get(context); + if (lastActiveEncoderPageGroup !== this) { + lastActiveEncoderPageGroup?.onDeactivated(context); + EncoderPageGroup.activeInstance.set(context, this); + this.onActivated(context); + } + + const lastActiveEncoderPage = this.activeEncoderPage.get(context); + if (lastActiveEncoderPage !== encoderPage) { + lastActiveEncoderPage?.onDeactivated(context); + this.activeEncoderPage.set(context, encoderPage); + encoderPage.onActivated(context); + } + } + + onActivated(context: MR_ActiveDevice) { + this.setActivatorButtonLeds(context, 1); + } + + /** + * This is invoked when another `EncoderGroup` is activated. + */ + onDeactivated(context: MR_ActiveDevice) { + this.activeEncoderPage.get(context)?.onDeactivated(context); + this.activeEncoderPage.set(context, undefined); + + this.setActivatorButtonLeds(context, 0); + } +} diff --git a/src/mapping/encoders/index.ts b/src/mapping/encoders/index.ts index 9e4915a..f060c11 100644 --- a/src/mapping/encoders/index.ts +++ b/src/mapping/encoders/index.ts @@ -35,7 +35,7 @@ export function bindEncoders( .followPluginWindowInFocus(); const mStripEffects = mMixerChannel.mInsertAndStripEffects.mStripEffects; - let encoderMappingConfig: EncoderMappingConfig = [ + let encoderMappingConfigs: EncoderMappingConfig[] = [ // Pan (Defining Pan first so it is activated by default) { activatorButtonSelector: (device) => selectAssignButtons(device).pan, @@ -276,8 +276,8 @@ export function bindEncoders( ]; if (deviceConfig.configureEncoderAssignments) { - encoderMappingConfig = deviceConfig.configureEncoderAssignments(encoderMappingConfig, page); + encoderMappingConfigs = deviceConfig.configureEncoderAssignments(encoderMappingConfigs, page); } - encoderMapper.applyEncoderMappingConfig(encoderMappingConfig); + encoderMapper.applyEncoderMappingConfigs(encoderMappingConfigs); }