import React, { ReactNode, Component, SyntheticEvent } from 'react'; import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { PluginType } from './Lv2Plugin'; import ButtonBase from '@material-ui/core/ButtonBase'; import Typography from '@material-ui/core/Typography'; import { SelectIcon } from './PluginIcon'; import { SelectHoverBackground } from './SelectHoverBackground'; import SvgPathBuilder from './SvgPathBuilder'; import Draggable from './Draggable' import Rect from './Rect'; import {PiPedalStateError} from './PiPedalError'; import { PedalBoard, PedalBoardItem, PedalBoardSplitItem, SplitType, } from './PedalBoard'; const START_PEDALBOARD_ITEM_URI = "uri://two-play/piddle/pedalboard#Start"; const END_PEDALBOARD_ITEM_URI = "uri://two-play/piddle/pedalboard#End"; const ENABLED_CONNECTOR_COLOR = "#666"; const DISABLED_CONNECTOR_COLOR = "#CCC"; const CELL_WIDTH: number = 96; const CELL_HEIGHT: number = 64; const FRAME_SIZE: number = 36; const STROKE_WIDTH = 3; const STEREO_STROKE_WIDTH = 6; const SVG_STROKE_WIDTH = "3"; const SVG_STEREO_STROKE_WIDTH = "6"; const EMPTY_ICON_URL = "img/fx_empty.svg"; const TERMINAL_ICON_URL = "img/fx_terminal.svg"; const pedalBoardStyles = (theme: Theme) => createStyles({ scrollContainer: { }, container: { position: "relative", overflow: "visible", }, startItem: { position: "absolute", display: "flex", alignItems: "center", justifyContent: "center", width: CELL_WIDTH, height: CELL_HEIGHT }, endItem: { position: "absolute", display: "flex", alignItems: "center", justifyContent: "center", width: CELL_WIDTH, height: CELL_HEIGHT }, splitItem: { position: "absolute", display: "flex", alignItems: "center", justifyContent: "center", width: CELL_WIDTH, height: CELL_HEIGHT }, splitStart: { position: "absolute", display: "flex", width: CELL_WIDTH, height: CELL_HEIGHT, left: 0, top: 0 }, splitEnd: { position: "absolute", display: "flex", width: CELL_WIDTH, height: CELL_HEIGHT, right: 0, top: 0 }, pedalItem: { position: "absolute", width: CELL_WIDTH, height: CELL_HEIGHT, display: "flex", alignItems: "center", justifyContent: "center", }, iconFrame: { display: "flex", alignItems: "center", justifyContent: "center", background: theme.palette.background.paper, marginLeft: (CELL_WIDTH - FRAME_SIZE) / 2, marginRight: (CELL_WIDTH - FRAME_SIZE) / 2, marginTop: (CELL_HEIGHT - FRAME_SIZE) / 2, marginBottom: (CELL_HEIGHT - FRAME_SIZE) / 2, width: FRAME_SIZE, height: FRAME_SIZE, border: "1pt #666 solid", borderRadius: 6 }, pedalIcon: { width: 24, height: 24, opacity: 0.8 }, connector: { position: "absolute", width: CELL_WIDTH, height: CELL_HEIGHT }, stroke: { position: "absolute", background: "#888" }, stereoStrokeOuter: { position: "absolute", background: "#888" }, stereoStrokeInner: { position: "absolute", background: "#888" }, }); export type OnSelectHandler = (selectedPedal: number) => void; interface PedalBoardProps extends WithStyles { theme: Theme; selectedId?: number; onSelectionChanged?: OnSelectHandler; onDoubleClick?: OnSelectHandler; hasTinyToolBar: boolean; }; interface LayoutSize { width: number; height: number; }; type PedalBoardState = { pedalBoard?: PedalBoard; }; const EMPTY_PEDALS: PedalLayout[] = []; function makeChain(model: PiPedalModel, uiItems?: PedalBoardItem[]): PedalLayout[] { let result: PedalLayout[] = []; if (uiItems) { for (let i = 0; i < uiItems.length; ++i) { let item = uiItems[i]; result.push(new PedalLayout(model, item)); } } return result; } class PedalLayout { uri: string = ""; name: string = ""; pluginType: PluginType = PluginType.Plugin; iconUrl: string = ""; bounds: Rect = new Rect(); inputs: number = 0; outputs: number = 0; stereoInput: boolean = false; stereoOutput: boolean = false; pedalItem?: PedalBoardItem; // Split Layout only. topChildren: PedalLayout[] = EMPTY_PEDALS; bottomChildren: PedalLayout[] = EMPTY_PEDALS; topConnectorY: number = 0; bottomConnectorY: number = 0; static Start(): PedalLayout { let t: PedalLayout = new PedalLayout(); t.uri = START_PEDALBOARD_ITEM_URI; t.iconUrl = TERMINAL_ICON_URL; t.inputs = 0; t.outputs = 1; return t; } static End(): PedalLayout { let t: PedalLayout = new PedalLayout(); t.pluginType = PluginType.UtilityPlugin; t.uri = END_PEDALBOARD_ITEM_URI; t.iconUrl = TERMINAL_ICON_URL; t.inputs = 2; t.outputs = 0; return t; } constructor(model?: PiPedalModel, pedalItem?: PedalBoardItem) { if (model === undefined && pedalItem === undefined) { return; } if (model === undefined || pedalItem === undefined) { throw new Error("Invalid arguments."); } this.pedalItem = pedalItem; this.uri = pedalItem.uri; if (pedalItem.isSplit()) { let splitter = pedalItem as PedalBoardSplitItem; this.pluginType = PluginType.UtilityPlugin; this.topChildren = makeChain(model, splitter.topChain); this.bottomChildren = makeChain(model, splitter.bottomChain); this.inputs = 2; this.outputs = 2; } else if (pedalItem.isEmpty()) { this.pluginType = PluginType.UtilityPlugin; this.iconUrl = EMPTY_ICON_URL; this.inputs = 2; this.outputs = 2; } else if (pedalItem.uri === START_PEDALBOARD_ITEM_URI) { this.pluginType = PluginType.UtilityPlugin; this.iconUrl = TERMINAL_ICON_URL; this.inputs = 0; this.outputs = PiPedalModelFactory.getInstance().jackSettings.get().inputAudioPorts.length; } else if (pedalItem.uri === END_PEDALBOARD_ITEM_URI) { this.pluginType = PluginType.UtilityPlugin; this.iconUrl = TERMINAL_ICON_URL; this.inputs = PiPedalModelFactory.getInstance().jackSettings.get().outputAudioPorts.length; this.outputs = 0; } else { let uiPlugin = model.getUiPlugin(pedalItem.uri); if (uiPlugin != null) { this.pluginType = uiPlugin.plugin_type; this.iconUrl = SelectIcon(uiPlugin.plugin_type,uiPlugin.uri); this.name = uiPlugin.name; this.inputs = uiPlugin.audio_inputs; this.outputs = uiPlugin.audio_outputs; } else { // default to empty plugin. this.pluginType = PluginType.UtilityPlugin; this.name = "#Missing"; this.iconUrl = EMPTY_ICON_URL; this.inputs = 2; this.outputs = 2; } } } isEmpty(): boolean { return (!this.pedalItem) || this.pedalItem.isEmpty(); } isSplitter(): boolean { return this.pedalItem !== undefined && this.pedalItem.isSplit(); } isStart() { return this.uri === START_PEDALBOARD_ITEM_URI; } isEnd() { return this.uri === END_PEDALBOARD_ITEM_URI; } } function* chainIterator(layoutChain: PedalLayout[]): Generator { for (let i = 0; i < layoutChain.length; ++i) { let item = layoutChain[i]; yield item; if (item.isSplitter()) { let g = chainIterator(item.topChildren); while (true) { let v = g.next(); if (v.done) { break; } yield v.value; } g = chainIterator(item.bottomChildren); while (true) { let v = g.next(); if (v.done) { break; } yield v.value; } } } return; } class LayoutParams { nextId: number = 1; cx: number = 0; cy: number = 0; } const PedalBoardView = withStyles(pedalBoardStyles, { withTheme: true })( class extends Component { model: PiPedalModel; frameRef: React.RefObject; scrollRef: React.RefObject; constructor(props: PedalBoardProps) { super(props); this.model = PiPedalModelFactory.getInstance(); if (!props.selectedId) props.selectedId = -1; this.state = { pedalBoard: this.model.pedalBoard.get(), }; this.onPedalBoardChanged = this.onPedalBoardChanged.bind(this); this.frameRef = React.createRef(); this.scrollRef = React.createRef(); this.handleTouchStart = this.handleTouchStart.bind(this); } handleTouchStart(e: any) { // just has to exist to allow Draggable to receive // touchyMove. :-/ } onDragEnd(instanceId: number, clientX: number, clientY: number) { if (!this.currentLayout) return; if (!this.frameRef.current) return; let currentLayout: PedalLayout[] = this.currentLayout; let frameElement = this.frameRef.current; let rc = frameElement.getBoundingClientRect(); clientX -= rc.left; clientY -= rc.top; let it = chainIterator(currentLayout); while (true) { let v = it.next(); if (v.done) break; let item = v.value; if (item.isSplitter() && item.pedalItem) { if (item.bounds.contains(clientX, clientY)) { if (clientX < item.bounds.x + CELL_WIDTH / 2) { this.model.movePedalBoardItemBefore(instanceId, item.pedalItem.instanceId); this.setSelection(instanceId); return; } else if (clientX > item.bounds.right - CELL_WIDTH / 2) { this.model.movePedalBoardItemAfter(instanceId, item.pedalItem.instanceId); this.setSelection(instanceId); return; } } let yMid = (item.bounds.y + item.bounds.bottom) / 2; if (clientX >= item.bounds.x && clientY < yMid && clientY >= item.topChildren[0].bounds.y ) { if (clientX < item.topChildren[0].bounds.x) { let topPedalItem = item.topChildren[0].pedalItem; if (topPedalItem) { this.model.movePedalBoardItemBefore(instanceId, topPedalItem.instanceId); this.setSelection(instanceId); return; } } let lastTop = item.topChildren[item.topChildren.length - 1]; if (clientX >= lastTop.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { if (lastTop.pedalItem) { this.model.movePedalBoardItemAfter(instanceId, lastTop.pedalItem.instanceId); this.setSelection(instanceId); return; } } } if (clientX >= item.bounds.x && clientY > yMid && clientY < item.bottomChildren[0].bounds.bottom ) { if (clientX < item.bottomChildren[0].bounds.x) { let bottomPedalItem = item.bottomChildren[0].pedalItem; if (bottomPedalItem) { this.model.movePedalBoardItemBefore(instanceId, bottomPedalItem.instanceId); this.setSelection(instanceId); return; } } let lastBottom = item.bottomChildren[item.bottomChildren.length-1]; if (clientX >= lastBottom.bounds.right && clientX < item.bounds.right-CELL_WIDTH/2) { if (lastBottom.pedalItem) { this.model.movePedalBoardItemAfter(instanceId, lastBottom.pedalItem.instanceId); this.setSelection(instanceId); return; } } } } else if (item.bounds.contains(clientX, clientY)) { if (item.isStart()) { this.model.movePedalBoardItemToStart(instanceId); this.setSelection(instanceId); return; } else if (item.isEnd()) { this.model.movePedalBoardItemToEnd(instanceId); this.setSelection(instanceId); return; } else { if (item.pedalItem) { let margin = (CELL_WIDTH - FRAME_SIZE) / 2; if (clientX < item.bounds.x + margin) { this.model.movePedalBoardItemBefore(instanceId, item.pedalItem.instanceId); } else if (clientX > item.bounds.right - margin) { this.model.movePedalBoardItemAfter(instanceId, item.pedalItem.instanceId); } else { this.model.movePedalBoardItem(instanceId, item.pedalItem.instanceId); } this.setSelection(instanceId); return; } } } } // delete the plugin. let newId = this.model.setPedalBoardItemEmpty(instanceId); this.setSelection(newId); } onPedalBoardChanged(value?: PedalBoard) { this.setState({ pedalBoard: value }); } componentDidMount() { this.scrollRef.current!.addEventListener("touchstart",this.handleTouchStart, {passive: false}); this.model.pedalBoard.addOnChangedHandler(this.onPedalBoardChanged); } componentWillUnmount() { this.scrollRef.current!.removeEventListener("touchstart",this.handleTouchStart); this.model.pedalBoard.removeOnChangedHandler(this.onPedalBoardChanged); } offsetLayout_(layoutItems: PedalLayout[], offset: number): void { for (let i = 0; i < layoutItems.length; ++i) { let layoutItem = layoutItems[i]; layoutItem.bounds.y += offset; if (layoutItem.isSplitter()) { layoutItem.topConnectorY += offset; layoutItem.bottomConnectorY += offset; this.offsetLayout_(layoutItem.topChildren, offset); this.offsetLayout_(layoutItem.bottomChildren, offset); } } } getSplitterIcon(layoutItem: PedalLayout) { if (layoutItem.pedalItem === undefined) { throw new Error("Invalid splitter"); } let split = layoutItem.pedalItem as PedalBoardSplitItem; if (split.getSplitType() === SplitType.Ab) { if (split.isASelected()) { return "img/fx_split_a.svg"; } else { return "img/fx_split_b.svg"; } } else if (split.getSplitType() === SplitType.Mix) { return "img/fx_dial.svg"; } else { return "img/fx_lr.svg"; } } doLayout2_(lp: LayoutParams, layoutItems: PedalLayout[]): Rect { let bounds = new Rect(); for (let i = 0; i < layoutItems.length; ++i) { let layoutItem = layoutItems[i]; if (layoutItem.isSplitter()) { let x0 = lp.cx; let y0 = lp.cy; layoutItem.bounds.x = x0; layoutItem.bounds.y = y0; layoutItem.bounds.height = CELL_HEIGHT; layoutItem.bounds.width = CELL_WIDTH; lp.cx += CELL_WIDTH; let topBounds = this.doLayout2_(lp, layoutItem.topChildren); if (topBounds.isEmpty()) { topBounds.x = lp.cx; topBounds.width = 0; topBounds.y = y0 - CELL_HEIGHT / 2; topBounds.height = CELL_HEIGHT; } let dyTop = (lp.cy + CELL_HEIGHT / 2)- (topBounds.y + topBounds.height) ; this.offsetLayout_(layoutItem.topChildren, dyTop); topBounds.offset(0, dyTop); bounds.accumulate(topBounds); let topCx = lp.cx; lp.cx = x0; lp.cx += CELL_WIDTH; let bottomBounds = this.doLayout2_(lp, layoutItem.bottomChildren); if (bottomBounds.isEmpty()) { bottomBounds.x = lp.cx; bottomBounds.width = 0; bottomBounds.y = lp.cy; bottomBounds.height = CELL_HEIGHT; } let dyBottom = (lp.cy+CELL_HEIGHT/2)-bottomBounds.y ; this.offsetLayout_(layoutItem.bottomChildren, dyBottom) bottomBounds.offset(0, dyBottom); bounds.accumulate(bottomBounds); lp.cx = Math.max(lp.cx, topCx) + CELL_WIDTH; lp.cy = y0; layoutItem.bounds.width = lp.cx - layoutItem.bounds.x; bounds.accumulate(layoutItem.bounds); if (layoutItem.topChildren.length === 0) { layoutItem.topConnectorY = bounds.y + CELL_HEIGHT / 2; } else { layoutItem.topConnectorY = layoutItem.topChildren[0].bounds.y + CELL_HEIGHT / 2; } if (layoutItem.bottomChildren.length === 0) { layoutItem.bottomConnectorY = bounds.y + bounds.height - CELL_HEIGHT / 2; } else { layoutItem.bottomConnectorY = layoutItem.bottomChildren[0].bounds.y + CELL_HEIGHT / 2; } } else { layoutItem.bounds.x = lp.cx; layoutItem.bounds.y = lp.cy; lp.cx += CELL_WIDTH; layoutItem.bounds.width = CELL_WIDTH; layoutItem.bounds.height = CELL_HEIGHT; bounds.accumulate(layoutItem.bounds); } } return bounds; } doLayout(layoutItems: PedalLayout[]): LayoutSize { const TWO_ROW_HEIGHT = 142 - 14; if (layoutItems.length === 0) { // if the current pedalboard is empty, reserve display space anyway. return {width: 1, height: TWO_ROW_HEIGHT}; } let lp = new LayoutParams(); let bounds = this.doLayout2_(lp, layoutItems); // shift everything down so there are no negative y coordinates. if (bounds.height < TWO_ROW_HEIGHT) { let extra = Math.floor((TWO_ROW_HEIGHT - Math.ceil(bounds.height))/2); this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra )); bounds.height += extra; } else { this.offsetLayout_(layoutItems, -bounds.y); } bounds.height += 14; // for labels that aren't accounted for. return { width: bounds.width, height: bounds.height }; } onItemClick(e: SyntheticEvent, instanceId?: number): void { if (instanceId) { this.setSelection(instanceId); } } setSelection(instanceId: number) { if (this.props.onSelectionChanged) { this.props.onSelectionChanged(instanceId); } } isTouchDevice(): boolean { return (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)); } onItemDoubleClick(event: SyntheticEvent, instanceId?: number): void { event.preventDefault(); event.stopPropagation(); if (this.props.onDoubleClick && instanceId) { this.props.onDoubleClick(instanceId); } } onItemLongClick(event: SyntheticEvent, instanceId?: number): void { event.preventDefault(); event.stopPropagation(); if (!this.isTouchDevice()) { if (this.props.onDoubleClick && instanceId) { this.props.onDoubleClick(instanceId); } } } renderConnector(output: ReactNode[], item: PedalLayout, enabled: boolean): void { // let classes = this.props.classes; let x_ = item.bounds.x + CELL_WIDTH / 2; let y_ = item.bounds.y + CELL_HEIGHT / 2; let isStereo = item.stereoOutput; let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; let svgPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_ + CELL_WIDTH, y_).toString(); if (isStereo) { output.push(( )); output.push(( )); } else { output.push(( )); } } renderSplitConnectors(output: ReactNode[], item: PedalLayout, enabled: boolean, shortSplitOutput: boolean,): void { //let classes = this.props.classes; let x_ = item.bounds.x + CELL_WIDTH / 2; let y_ = item.bounds.y + CELL_HEIGHT / 2; let yTop = item.topConnectorY; let yBottom = item.bottomConnectorY; //let isStereo = item.stereoOutput; let split = item.pedalItem as PedalBoardSplitItem; let topEnabled = enabled && split.isASelected(); let bottomEnabled = enabled && split.isBSelected(); let topColor = topEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; let bottomColor = bottomEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; let topStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yTop).lineTo(x_ + CELL_WIDTH, yTop).toString(); let bottomStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yBottom).lineTo(x_ + CELL_WIDTH, yBottom).toString(); if (item.stereoInput && item.topChildren[0].stereoInput) { output.push(()); output.push(()); } else { output.push(()); } if (item.stereoInput && item.bottomChildren[0].stereoInput) { output.push(()); output.push(()); } else { output.push(()); } let lastTop = item.topChildren[item.topChildren.length - 1]; let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; let xTop = lastTop.bounds.right - CELL_WIDTH / 2; let xBottom = lastBottom.bounds.right - CELL_WIDTH / 2; let xEnd = shortSplitOutput ? item.bounds.right : item.bounds.right + CELL_WIDTH / 2; let xTee0 = item.bounds.right - CELL_WIDTH / 2; let firstPath: string; let secondPath: string; let firstPathStereo: boolean; let secondPathStereo: boolean; let firstPathEnabled: boolean; let secondPathEnabled: boolean; let xTee: number; let monoAdjustment = (STEREO_STROKE_WIDTH - STROKE_WIDTH) / 2; let bottomPathFirst = topEnabled && !bottomEnabled; let topPathFirst = bottomEnabled && !topEnabled; if (bottomPathFirst || (topEnabled && lastTop.stereoOutput)) { // draw the bottom path first. firstPathStereo = item.stereoOutput && lastBottom.stereoOutput; secondPathStereo = item.stereoOutput && lastTop.stereoOutput; xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); firstPathEnabled = bottomEnabled; secondPathEnabled = topEnabled; } else { // draw the top path first. firstPathStereo = item.stereoOutput && lastTop.stereoOutput; secondPathStereo = item.stereoOutput && lastBottom.stereoOutput; xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; firstPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; secondPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); firstPathEnabled = topEnabled; secondPathEnabled = bottomEnabled; } let firstPathColor = firstPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; let secondPathColor = secondPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; if (bottomPathFirst || topPathFirst) { // display stereo strokes with cutoff line. if (firstPathStereo) { output.push(( )); output.push(( )); } else { output.push(( )); } if (secondPathStereo) { output.push(( )); output.push(( )); } else { output.push(( )); } } else { // stereo strokes merge. if (firstPathStereo) { output.push(( )); } else { output.push(( )); } if (secondPathStereo) { output.push(( )); } else { output.push(( )); } // draw stereo inner lines. if (firstPathStereo) { output.push(( )); } if (secondPathStereo) { output.push(( )); } } } getScrollContainer() { let el: HTMLElement | undefined | null= this.scrollRef.current; // actually not here anymore. :-/ It has a reactive definition in MainPage.tsx now. while (el) { if (el.id === "pedalBoardScroll") { return el as HTMLDivElement; } el = el.parentElement; } throw new PiPedalStateError("scroll container not found."); } pedalButton(instanceId: number, iconUrl: string, draggable: boolean, enabled: boolean): ReactNode { let classes = this.props.classes; return (
{ e.preventDefault(); }}> { this.onItemClick(e, instanceId); }} onDoubleClick={(e: SyntheticEvent) => { this.onItemDoubleClick(e, instanceId); }} onContextMenu={(e: SyntheticEvent) => { this.onItemLongClick(e, instanceId); }} > this.getScrollContainer()} onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }} >
Pedal
); } renderConnectors(output: ReactNode[], layoutChain: PedalLayout[], enabled: boolean, shortSplitOutput: boolean): void { let length = layoutChain.length - 1; if (layoutChain.length > 0 && layoutChain[layoutChain.length - 1].isSplitter()) { ++length; } for (let i = 0; i < length; ++i) { let item = layoutChain[i]; if (item.isSplitter()) { let splitter = item.pedalItem as PedalBoardSplitItem; this.renderSplitConnectors(output, item, enabled, i === length - 1 && shortSplitOutput,); this.renderConnectors(output, item.topChildren, enabled && splitter.isASelected(), false); this.renderConnectors(output, item.bottomChildren, enabled && splitter.isBSelected(), false); } else if (item.uri !== END_PEDALBOARD_ITEM_URI) { this.renderConnector(output, item, enabled); } } } renderConnectorFrame(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { let output: ReactNode[] = []; this.renderConnectors(output, layoutChain, true, false); return ( { output } ); } renderChain(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { let classes = this.props.classes; let result: ReactNode[] = []; result.push(this.renderConnectorFrame(layoutChain, layoutSize)); let it = chainIterator(layoutChain); while (true) { let v = it.next(); if (v.done) break; let item = v.value; switch (item.uri) { case START_PEDALBOARD_ITEM_URI: result.push(
); break; case END_PEDALBOARD_ITEM_URI: result.push(
); break; default: if (item.isSplitter()) { result.push(
{this.pedalButton(item.pedalItem?.instanceId ?? -1, this.getSplitterIcon(item), false,true)}
); } else { result.push(
{item.pedalItem?.pluginName}
) result.push(
{this.pedalButton(item.pedalItem?.instanceId ?? -1, item.iconUrl, !item.isEmpty(), item.pedalItem?.isEnabled ?? false)}
); } break; } } return result; } canInputStero(item: PedalLayout): boolean { if (item.pedalItem) { let plugin = this.model.getUiPlugin(item.pedalItem.uri); if (plugin) { return plugin.audio_inputs === 2; } } return true; } canOutputStereo(item: PedalLayout): boolean { if (item.pedalItem) { let plugin = this.model.getUiPlugin(item.pedalItem.uri); if (plugin) { return plugin.audio_outputs === 2; } } return true; } markStereoOutputs(layoutChain: PedalLayout[], stereoInput: boolean, stereoOutput: boolean) { // analyze forward flow. this.markStereoForward(layoutChain, stereoInput); // mark items that feed a mono effect as mono. this.markStereoBackward(layoutChain, stereoOutput); } markStereoBackward(layoutChain: PedalLayout[], stereoOutput: boolean) : boolean { for (let i = layoutChain.length-1; i >= 0; --i) { let item = layoutChain[i]; if (item.isSplitter()) { if (!stereoOutput) { item.stereoOutput = false; } this.markStereoBackward(item.topChildren,stereoOutput); this.markStereoBackward(item.bottomChildren,stereoOutput); let topStereoInput = item.topChildren[0].stereoInput; let bottomStereoInput = item.bottomChildren[0].stereoInput; if (! (topStereoInput || bottomStereoInput)) { let splitItem = item.pedalItem as PedalBoardSplitItem; if (splitItem.getSplitType() !== SplitType.Lr) { item.stereoInput = false; } } } else if (item.isEnd()) { } else if (item.isStart()) { if (!stereoOutput) { item.stereoOutput = false; } return item.stereoOutput; } else if (item.isEmpty()) { if (!stereoOutput) { item.stereoOutput = false; item.stereoInput = false; } } else { if (!stereoOutput) { item.stereoOutput = false; } } stereoOutput = item.stereoInput; } return stereoOutput; } markStereoForward(layoutChain: PedalLayout[],stereoInput: boolean): boolean { if (layoutChain.length === 0) { return stereoInput; } for (let i = 0; i < layoutChain.length; ++i) { let item = layoutChain[i]; if (item.isSplitter()) { let splitter = item.pedalItem as PedalBoardSplitItem; item.stereoInput = stereoInput; let isChainInputStereo = stereoInput; if (splitter.getSplitType() === SplitType.Lr) { isChainInputStereo = false; } let topStereo = this.markStereoForward(item.topChildren, isChainInputStereo); let bottomStereo = this.markStereoForward(item.bottomChildren, isChainInputStereo); if (splitter.getSplitType() === SplitType.Ab) { if (splitter.isASelected()) { item.stereoOutput = topStereo; } else { item.stereoOutput = bottomStereo; } } else { item.stereoOutput = topStereo || bottomStereo; } } else if (item.isStart()) { item.stereoOutput = (PiPedalModelFactory.getInstance().jackSettings.get().inputAudioPorts.length > 1); } else if (item.isEnd()) { item.stereoInput = (PiPedalModelFactory.getInstance().jackSettings.get().outputAudioPorts.length > 1) && stereoInput; return item.stereoInput; } else if (item.isEmpty()) { item.stereoInput = stereoInput; item.stereoOutput = stereoInput; } else { if (stereoInput && this.canOutputStereo(item)) { item.stereoInput = true; } else { item.stereoInput = false; } item.stereoOutput = this.canOutputStereo(item); } stereoInput = item.stereoOutput; } return stereoInput; } currentLayout?: PedalLayout[]; render() { const { classes } = this.props; let layoutChain = makeChain(this.model, this.state.pedalBoard?.items); let start = PedalLayout.Start(); let end = PedalLayout.End(); if (layoutChain.length !== 0) { layoutChain.splice(0, 0, start); layoutChain.splice(layoutChain.length, 0, end); this.markStereoOutputs(layoutChain, true, true); } let layoutSize = this.doLayout(layoutChain); this.currentLayout = layoutChain; // save for mouse processing &c. return (
{this.renderChain(layoutChain, layoutSize)}
); } } ); export default PedalBoardView