Skip to content

Commit

Permalink
detect, mark & disable loops
Browse files Browse the repository at this point in the history
  • Loading branch information
msarilar committed May 20, 2024
1 parent a734076 commit 4739c4b
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 78 deletions.
85 changes: 65 additions & 20 deletions src/Midispatcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import Modal from "react-modal";

import { CanvasWidget, ZoomCanvasAction, InputType } from "@projectstorm/react-canvas-core";
import { Box, TextField } from "@mui/material";
import { Alert, AlertColor, Box, Snackbar, TextField } from "@mui/material";
import * as WebMidi from "webmidi";
import Disqus, { CommentCount } from "disqus-react"
import LZString from "lz-string";
Expand Down Expand Up @@ -35,17 +35,6 @@ window.addEventListener("keydown", (event: any) => {
}
});

engine.getPortFactories().registerFactory(new MachinePortFactory());

const model = new MidispatcherDiagramModel(commandManager)
model.setGridSize(25);
engine.setModel(model);

const eventBus = engine.getActionEventBus();
const action = eventBus.getActionsForType(InputType.MOUSE_WHEEL)[0];
eventBus.deregisterAction(action);
eventBus.registerAction(new ZoomCanvasAction( { inverseZoom: true }));

interface MidispatcherState {

readonly update: boolean;
Expand Down Expand Up @@ -99,6 +88,27 @@ let demoIndex = 0;

const Midispatcher: React.FunctionComponent = () => {

const [toastOpened, setToastOpened] = React.useState<{ severity: AlertColor, content: string } | undefined>(undefined);
const handleCloseToast = (e?: React.SyntheticEvent | Event, reason?: string) => {

if (reason === 'clickaway') {

return;
}

setToastOpened(undefined);
};

const onCycleDetected = () => {

setToastOpened({ severity: "warning", content: "Cycle detected! To avoid infinite feedback loop, red links are disabled" });
};

const onCycleCleared = () => {

setToastOpened({ severity: "success", content: "Cycle(s) cleared" });
};

const [state, dispatch] = React.useReducer(midispatcherReducer, defaultMidispatcherState);
engine.getLinkFactories().registerFactory(new MidiLinkFactory());

Expand All @@ -113,7 +123,16 @@ const Midispatcher: React.FunctionComponent = () => {
(node as MachineNodeModel).dispose();
});

const model = new MidispatcherDiagramModel(commandManager);
const model = new MidispatcherDiagramModel(commandManager, onCycleDetected, onCycleCleared);
model.onCycleDetected = () => {

setToastOpened({ severity: "warning", content: "Cycle detected! To avoid infinite feedback loop, red links are disabled" });
};
model.onCycleCleared = () => {

setToastOpened({ severity: "success", content: "Cycle(s) cleared" });
};

try {

const json = fromJson(action.result.data);
Expand Down Expand Up @@ -149,6 +168,8 @@ const Midispatcher: React.FunctionComponent = () => {

engine.getNodeFactories().deregisterFactory("machine");
engine.getNodeFactories().registerFactory(new MachineNodeFactory(state.machineFactories));
const totalDevices = action.result.inputs.length + action.result.outputs.length;
setToastOpened({ severity: "success", content: `${totalDevices} MIDI device(s) loaded!` });

return {

Expand Down Expand Up @@ -185,7 +206,21 @@ const Midispatcher: React.FunctionComponent = () => {
}
}

// need this to run before first render:
React.useMemo(() => {

const model = new MidispatcherDiagramModel(commandManager, onCycleDetected, onCycleCleared);
model.setGridSize(25);
engine.setModel(model);
}, []);

React.useEffect(() => {
engine.getPortFactories().registerFactory(new MachinePortFactory());

const eventBus = engine.getActionEventBus();
const action = eventBus.getActionsForType(InputType.MOUSE_WHEEL)[0];
eventBus.deregisterAction(action);
eventBus.registerAction(new ZoomCanvasAction( { inverseZoom: true }));

Object.values(MachineType).forEach((key) => {

Expand All @@ -198,7 +233,6 @@ const Midispatcher: React.FunctionComponent = () => {
}
});
engine.getNodeFactories().registerFactory(new MachineNodeFactory(state.machineFactories));
dispatch({ type: MidispatcherActionType.Refresh, result: {} });

if (navigator.requestMIDIAccess != undefined) {

Expand All @@ -207,13 +241,13 @@ const Midispatcher: React.FunctionComponent = () => {
.then(() => dispatch({ type: MidispatcherActionType.MidiLoaded, result: { inputs: WebMidi.WebMidi.inputs, outputs: WebMidi.WebMidi.outputs } }))
.catch(err => {

alert("Can't connect to your MIDI devices:\r\n" + err);
setToastOpened({ severity: "error", content: `Can't connect to your MIDI devices:\r\n${err}` });
dispatch({ type: MidispatcherActionType.MidiLoaded, result: { inputs: WebMidi.WebMidi.inputs, outputs: WebMidi.WebMidi.outputs } });
});
}
else {

alert("Can't connect to your MIDI devices:\r\nWeb MIDI API is not available on your browser");
setToastOpened({ severity: "error", content: "Can't connect to your MIDI devices:\r\nWeb MIDI API is not available on your browser" });
}

fetch("https://raw.githubusercontent.com/msarilar/midispatcher/main/saves/demos.json")
Expand All @@ -230,7 +264,9 @@ const Midispatcher: React.FunctionComponent = () => {
.catch(err => {

alert("Issue when trying to retrieve demos:\r\n" + err);
})
});

dispatch({ type: MidispatcherActionType.Refresh, result: {} });
}, []);

function onDrop<T>(event: React.DragEvent<T>) {
Expand Down Expand Up @@ -336,9 +372,6 @@ const Midispatcher: React.FunctionComponent = () => {
}}>
Load
</WorkspaceButton>,
<WorkspaceButton key={"resetViewButtonKey"} onClick={() => { engine.zoomToFit(); model.realignGrid(); }}>
Reset View
</WorkspaceButton>,
<WorkspaceButton key={"helpButtonKey"} onClick={() => dispatch({ type: MidispatcherActionType.ToggleModal, result: { modalContent: helpModalContent } })}>
Help
</WorkspaceButton>,
Expand Down Expand Up @@ -389,6 +422,18 @@ const Midispatcher: React.FunctionComponent = () => {
<Disqus.DiscussionEmbed shortname={disqusShortname} config={disqusConfig} />
</S.Disqus>
</S.Body>

<Snackbar open={toastOpened != undefined}
autoHideDuration={toastOpened?.severity === "error" || toastOpened?.severity === "warning" ? undefined : 6000}
onClose={handleCloseToast}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}>
<Alert onClose={handleCloseToast}
severity={toastOpened?.severity}
variant="filled"
sx={{ width: '100%' }}>
{toastOpened?.content}
</Alert>
</Snackbar>
</Workspace>
)
}
Expand Down
99 changes: 56 additions & 43 deletions src/layout/Engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import { BaseModel, BaseEntityEvent, BaseEvent, BaseEntity, BaseEntityGenerics }
import { MachineSource, MachineTarget } from "../machines/Machines";
import { MachineNodeModel } from "./Node";
import { MidiLinkModel } from "./Link";
import { MachineRoutings } from "../machines/MachineRoutings";
import { MachineRoutings, ON_CYCLE_ClEARED, ON_CYCLE_DETECTED } from "../machines/MachineRoutings";
import { MachinePortModel } from "./Port";

const routings = new MachineRoutings();
export const engine = createEngine();
const state = engine.getStateMachine().getCurrentState();
if (state instanceof DefaultDiagramState) {
Expand Down Expand Up @@ -79,21 +78,35 @@ export class CommandManager {
export class MidispatcherDiagramModel extends DiagramModel {

readonly commandManager: CommandManager;
private routings: MachineRoutings;

onCycleDetected: () => void;
onCycleCleared: () => void;

deserializeModel(data: ReturnType<this["serialize"]>, engine: DiagramEngine) {

super.deserializeModel(data, engine);

this.routings = new MachineRoutings()
this.routings.addEventListener(ON_CYCLE_DETECTED, () => { this.onCycleDetected(); });
this.routings.addEventListener(ON_CYCLE_ClEARED, () => { this.onCycleCleared(); });

// running this forEach directly does not work (I guess engine not fully loaded?) so we go through setTimeout:
window.setTimeout(() =>
this.getLinks().forEach(link => applyLink(link as MidiLinkModel)), 0);
this.getLinks().forEach(link => this.applyLink(link as MidiLinkModel)), 0);

this.realignGrid();
}

constructor(commandManager: CommandManager) {
constructor(commandManager: CommandManager, onCycleDetected: () => void, onCycleCleared: () => void) {

super();

this.routings = new MachineRoutings();
this.onCycleDetected = onCycleDetected;
this.onCycleCleared = onCycleCleared;
this.routings.addEventListener(ON_CYCLE_DETECTED, () => { this.onCycleDetected(); });
this.routings.addEventListener(ON_CYCLE_ClEARED, () => { this.onCycleCleared(); });
this.commandManager = commandManager;

this.registerListener({
Expand All @@ -113,13 +126,13 @@ export class MidispatcherDiagramModel extends DiagramModel {
targetPortChanged: (_: BaseEvent) => {

linksUpdatedEvent.link.deregisterListener(registered);
applyLink(linksUpdatedEvent.link);
this.applyLink(linksUpdatedEvent.link);
}
});
}
else {

routings.disconnect(linksUpdatedEvent.link);
this.routings.disconnect(linksUpdatedEvent.link);
}
},
eventDidFire: (edf: BaseEvent) => {
Expand Down Expand Up @@ -179,7 +192,7 @@ export class MidispatcherDiagramModel extends DiagramModel {
() => {

super.addLink(link);
applyLink(link as MidiLinkModel);
this.applyLink(link as MidiLinkModel);
}
);

Expand All @@ -194,7 +207,7 @@ export class MidispatcherDiagramModel extends DiagramModel {
() => {

super.addLink(link);
applyLink(link as MidiLinkModel);
this.applyLink(link as MidiLinkModel);
},
() => super.removeLink(link),
);
Expand Down Expand Up @@ -232,54 +245,54 @@ export class MidispatcherDiagramModel extends DiagramModel {

return super.addAll(...models);
}
}

// "magic" constant to create a link used to connect all other links at the same time
export const AllLinkCode: string = "All";
applyLink(link: MidiLinkModel) {

function applyLink(link: MidiLinkModel) {
let portSource = link.getSourcePort() as MachinePortModel;
let portTarget = link.getTargetPort() as MachinePortModel;

let portSource = link.getSourcePort() as MachinePortModel;
let portTarget = link.getTargetPort() as MachinePortModel;
if (!portTarget.isIn) {

if (!portTarget.isIn) {
const temp = portTarget;
portTarget = portSource;
portSource = temp;
}

const temp = portTarget;
portTarget = portSource;
portSource = temp;
}
const sourceNode = portSource.getNode() as MachineNodeModel;
const machineSource = sourceNode.machine as MachineSource;

const sourceNode = portSource.getNode() as MachineNodeModel;
const machineSource = sourceNode.machine as MachineSource;
const targetNode = portTarget.getNode() as MachineNodeModel;
const machineTarget = targetNode.machine as MachineTarget;

const targetNode = portTarget.getNode() as MachineNodeModel;
const machineTarget = targetNode.machine as MachineTarget;
if (link.getSourcePort().getName() === AllLinkCode) {

if (link.getSourcePort().getName() === AllLinkCode) {
Object.keys(sourceNode.getMachinePorts()).forEach(key => {

Object.keys(sourceNode.getMachinePorts()).forEach(key => {
const port = sourceNode.getMachinePorts()[key];
if (port.getName() !== AllLinkCode && !port.isIn) {

const port = sourceNode.getMachinePorts()[key];
if (port.getName() !== AllLinkCode && !port.isIn) {
const newLink = (link.getTargetPort() as MachinePortModel).link<MidiLinkModel>(port);
engine.getModel().addAll(newLink);
const sourceNode = port.getNode() as MachineNodeModel;
const machineSource = sourceNode.machine as MachineSource;
this.routings.connect(machineSource, machineTarget, port.channel, portTarget.channel, newLink);
}
});

const newLink = (link.getTargetPort() as MachinePortModel).link<MidiLinkModel>(port);
engine.getModel().addAll(newLink);
const sourceNode = port.getNode() as MachineNodeModel;
const machineSource = sourceNode.machine as MachineSource;
routings.connect(machineSource, machineTarget, port.channel, portTarget.channel, newLink);
}
});
engine.getModel().removeLink(link);
}
else if (portSource.getLinks()[portTarget.getName()] !== undefined) {

engine.getModel().removeLink(link);
}
else if (portSource.getLinks()[portTarget.getName()] !== undefined) {
engine.getModel().removeLink(link);
}
else {

engine.getModel().removeLink(link);
}
else {
this.routings.connect(machineSource, machineTarget, portSource.channel, portTarget.channel, link);
}

routings.connect(machineSource, machineTarget, portSource.channel, portTarget.channel, link);
return true;
}
}

return true;
}
// "magic" constant to create a link used to connect all other links at the same time
export const AllLinkCode: string = "All";
Loading

0 comments on commit 4739c4b

Please sign in to comment.