= phys_size.to_logical(s_factor);
6 |
7 | return logical_size;
8 | }
--------------------------------------------------------------------------------
/rust-impl/midianimator/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json",
3 | "build": {
4 | "beforeBuildCommand": "npm run build",
5 | "beforeDevCommand": "npm run dev",
6 | "devPath": "http://localhost:3000",
7 | "distDir": "../build",
8 | "withGlobalTauri": true
9 | },
10 | "package": {
11 | "productName": "MIDIAnimator",
12 | "version": "0.1.0"
13 | },
14 | "tauri": {
15 | "allowlist": {
16 | "all": true,
17 | "fs": {
18 | "readFile": true,
19 | "scope": ["**", "**/*", "/**/*"]
20 | }
21 | },
22 | "bundle": {
23 | "active": true,
24 | "category": "DeveloperTool",
25 | "copyright": "",
26 | "deb": {
27 | "depends": []
28 | },
29 | "externalBin": [],
30 | "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
31 | "identifier": "com.jamesa08.midianimator",
32 | "longDescription": "",
33 | "macOS": {
34 | "entitlements": null,
35 | "exceptionDomain": "",
36 | "frameworks": [],
37 | "providerShortName": null,
38 | "signingIdentity": null
39 | },
40 | "resources": ["src/configs/default_nodes.json"],
41 | "shortDescription": "",
42 | "targets": "all",
43 | "windows": {
44 | "certificateThumbprint": null,
45 | "digestAlgorithm": "sha256",
46 | "timestampUrl": ""
47 | }
48 | },
49 | "security": {
50 | "csp": null
51 | },
52 | "updater": {
53 | "active": false
54 | },
55 | "windows": [
56 | {
57 | "fullscreen": false,
58 | "height": 600,
59 | "resizable": true,
60 | "title": "MIDIAnimator",
61 | "width": 1100,
62 | "titleBarStyle": "Overlay",
63 | "hiddenTitle": true
64 | }
65 | ]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src-tauri/tests/ipc_test.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | // run from /src-tauri
3 | // cargo test
4 | mod tests {
5 | // use MIDIAnimator::structures::ipc;
6 | // TODO: add tests here
7 | }
--------------------------------------------------------------------------------
/rust-impl/midianimator/src-tauri/tests/midi_test.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | // run from /src-tauri
3 | // cargo test
4 | mod tests {
5 | use lazy_static::lazy_static;
6 | use MIDIAnimator::midi::MIDIFile;
7 |
8 | lazy_static! {
9 | static ref TYPE_0: &'static str = "./tests/test_midi_type_0_rs_4_14_24.mid";
10 | static ref TYPE_1: &'static str = "./tests/test_midi_type_1_rs_4_14_24.mid";
11 | }
12 |
13 | #[test]
14 | fn test_load_valid_midi_file() {
15 | let midi_file = MIDIFile::new(&TYPE_0).unwrap();
16 | assert!(midi_file.get_midi_tracks().len() > 0);
17 | }
18 |
19 | #[test]
20 | fn test_load_invalid_midi_file() {
21 | let midi_file = MIDIFile::new(&"./tests/FILE_DOES_NOT_EXIST.mid");
22 | assert!(midi_file.is_err());
23 | }
24 |
25 | #[test]
26 | fn test_get_midi_tracks_type_0() {
27 | let midi_file = MIDIFile::new(&TYPE_0).unwrap();
28 | let tracks = midi_file.get_midi_tracks();
29 | // assert_eq!(tracks.len(), 2); // Adjust the expected number of tracks
30 | println!("Number of tracks: {}", tracks.len());
31 | for track in tracks {
32 | println!("Track name: {}", track.name);
33 | }
34 | }
35 |
36 | #[test]
37 | fn test_get_midi_tracks_type_1() {
38 | let midi_file = MIDIFile::new(&TYPE_1).unwrap();
39 | let tracks = midi_file.get_midi_tracks();
40 | // assert_eq!(tracks.len(), 2); // Adjust the expected number of tracks
41 | println!("Number of tracks: {}", tracks.len());
42 | for track in tracks {
43 | println!("Track name: {}", track.name);
44 | }
45 | }
46 |
47 | #[test]
48 | fn test_find_track_by_name_type_0() {
49 | let midi_file = MIDIFile::new(&TYPE_0).unwrap();
50 | let track = midi_file.find_track("Classic Electric Piano");
51 | assert!(track.is_some());
52 | }
53 |
54 | #[test]
55 | fn test_find_track_by_name_type_1() {
56 | let midi_file = MIDIFile::new(&TYPE_1).unwrap();
57 | let track = midi_file.find_track("Classic Electric Piano");
58 | assert!(track.is_some());
59 | }
60 |
61 | #[test]
62 | fn test_merge_tracks() {
63 | let midi_file = MIDIFile::new(&TYPE_1).unwrap();
64 | let track1 = midi_file.find_track("Classic Electric Piano").unwrap();
65 | let track2 = midi_file.find_track("Classic Electric Piano").unwrap();
66 | let merged_track = midi_file.merge_tracks(track1, track2, Some("Merged Track"));
67 | assert_eq!(merged_track.name, "Merged Track");
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src-tauri/tests/test_midi_type_0_rs_4_14_24.mid:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesa08/MIDIAnimator/8463ea5b1931311f0d30803ccd992111a32ca9c7/rust-impl/midianimator/src-tauri/tests/test_midi_type_0_rs_4_14_24.mid
--------------------------------------------------------------------------------
/rust-impl/midianimator/src-tauri/tests/test_midi_type_1_rs_4_14_24.mid:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesa08/MIDIAnimator/8463ea5b1931311f0d30803ccd992111a32ca9c7/rust-impl/midianimator/src-tauri/tests/test_midi_type_1_rs_4_14_24.mid
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/App.tsx:
--------------------------------------------------------------------------------
1 | import MenuBar from "./components/MenuBar";
2 | import ToolBar from "./components/ToolBar";
3 | import Panel from "./components/Panel";
4 | import StatusBar from "./components/StatusBar";
5 | import { useEffect } from "react";
6 | import { listen } from "@tauri-apps/api/event";
7 | import { WebviewWindow } from "@tauri-apps/api/window";
8 | import { invoke } from "@tauri-apps/api/tauri";
9 |
10 | import { useStateContext } from "./contexts/StateContext";
11 | import NodeGraph from "./components/NodeGraph";
12 |
13 | function App() {
14 | const { backEndState: backEndState, setBackEndState: setBackEndState, frontEndState: frontEndState, setFrontEndState: setFrontEndState } = useStateContext();
15 |
16 | useEffect(() => {
17 | // listner for window creation
18 | const windowEventListener = listen(`open-window`, (event: any) => {
19 | const window = new WebviewWindow(`${event.payload["title"]}`, event.payload);
20 |
21 | window.show();
22 | });
23 |
24 | const stateListner = listen("update_state", (event: any) => {
25 | setBackEndState(event.payload);
26 | });
27 |
28 | const executionRunner = listen("execute_function", (event: any) => {
29 | invoke(event.payload["function"], event.payload["args"]).then((res: any) => {});
30 | });
31 |
32 | // tell the backend we're ready & get the initial state
33 | invoke("ready").then((res: any) => {
34 | if (res !== null) {
35 | setBackEndState(res);
36 | }
37 | });
38 |
39 | return () => {
40 | windowEventListener.then((f) => f());
41 | stateListner.then((f) => f());
42 | executionRunner.then((f) => f());
43 | };
44 | }, []);
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
68 | export default App;
69 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/blender.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesa08/MIDIAnimator/8463ea5b1931311f0d30803ccd992111a32ca9c7/rust-impl/midianimator/src/blender.png
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/collapse-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesa08/MIDIAnimator/8463ea5b1931311f0d30803ccd992111a32ca9c7/rust-impl/midianimator/src/collapse-left.png
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/collapse-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesa08/MIDIAnimator/8463ea5b1931311f0d30803ccd992111a32ca9c7/rust-impl/midianimator/src/collapse-right.png
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/ConnectionLine.tsx:
--------------------------------------------------------------------------------
1 | import { getSimpleBezierPath, Position, useConnection } from "@xyflow/react";
2 |
3 | export default ({ fromX, fromY, toX, toY }: { fromX: number; fromY: number; toX: number; toY: number }) => {
4 | const [d] = getSimpleBezierPath({
5 | sourceX: fromX,
6 | sourceY: fromY,
7 | sourcePosition: Position.Right,
8 | targetX: toX,
9 | targetY: toY,
10 | targetPosition: Position.Left,
11 | });
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/IPCLink.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useStateContext } from "../contexts/StateContext";
3 | import { invoke } from "@tauri-apps/api/tauri";
4 |
5 | declare global {
6 | interface String {
7 | toProperCase(): string;
8 | }
9 | }
10 |
11 | String.prototype.toProperCase = function () {
12 | return this.replace(/\w\S*/g, function (txt) {
13 | return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase();
14 | });
15 | };
16 |
17 | function IPCLink() {
18 | const { backEndState: state, setBackEndState: setState } = useStateContext();
19 |
20 | const [menuShown, setMenuShown] = useState(false);
21 |
22 | function openMenu() {
23 | setMenuShown(!menuShown);
24 | }
25 |
26 | function disconnect() {
27 | console.log("disconnect button pushed");
28 | }
29 |
30 | function showWhenConnected() {
31 | if (state.connected) {
32 | return (
33 | <>
34 | {`${state.connected_application.toProperCase()} version ${state.connected_version}`}
35 | {`${state.connected_file_name}`}
36 | {`Port: ${state.port}`}
37 |
38 | Disconnect
39 |
40 | >
41 | );
42 | }
43 | }
44 |
45 | const floatingPanel = (
46 |
47 |
{state.connected ? "" : "Disconnected. Please connect on the 3D application to start."}
48 | {showWhenConnected()}
49 |
50 | );
51 |
52 | return (
53 |
54 |
55 |
56 |
{state.connected ? "CONNECTED" : "DISCONNECTED"}
57 | {floatingPanel}
58 |
59 | );
60 | }
61 |
62 | export default IPCLink;
63 | function useEffect(arg0: () => void, arg1: any[]) {
64 | throw new Error("Function not implemented.");
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/MacTrafficLights.tsx:
--------------------------------------------------------------------------------
1 | function MacTrafficLights() {
2 | return (
3 |
8 | );
9 | }
10 |
11 | export default MacTrafficLights;
12 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/MenuBar.tsx:
--------------------------------------------------------------------------------
1 | import MacTrafficLights from "./MacTrafficLights";
2 | import Tab from "./Tab";
3 | import IPCLink from "./IPCLink";
4 |
5 | function MenuBar() {
6 | return (
7 |
8 | {navigator.userAgent.includes("Mac OS") && }
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default MenuBar;
16 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/Panel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import nodeTypes from "../nodes/NodeTypes";
4 | import { ReactFlowProvider } from "@xyflow/react";
5 | import { WebviewWindow } from "@tauri-apps/api/window";
6 | import { listen } from "@tauri-apps/api/event";
7 |
8 | interface PanelProps {
9 | id: string;
10 | name: string;
11 | }
12 |
13 | const Panel: React.FC = ({ id, name }) => {
14 | const navigate = useNavigate();
15 |
16 | useEffect(() => {
17 | const handleClick = (event: any) => {
18 | console.log(`Got ${JSON.stringify(event)} on window listener`);
19 | };
20 |
21 | const setupListener = async () => {
22 | try {
23 | const unlisten = await listen("clicked", handleClick);
24 | return () => {
25 | unlisten();
26 | };
27 | } catch (error) {
28 | console.error("Failed to setup event listener:", error);
29 | }
30 | };
31 |
32 | setupListener();
33 | }, []);
34 |
35 | const createWindow = (event: React.MouseEvent) => {
36 | const webview = new WebviewWindow(id, {
37 | url: `/#/panel/${id}`,
38 | title: name,
39 | width: 400,
40 | height: 300,
41 | resizable: true,
42 | x: event.screenX,
43 | y: event.screenY,
44 | });
45 |
46 | webview.once("tauri://created", () => {
47 | console.log("Created new window");
48 | });
49 |
50 | webview.once("tauri://error", (e: any) => {
51 | console.error(`Error creating new window ${e.payload}`);
52 | });
53 |
54 | navigate(`/#/panel/${id}`);
55 | };
56 |
57 | function windowIfNodes() {
58 | if (name == "Nodes") {
59 | return (
60 |
61 | {Object.entries(nodeTypes).map(([key, value]) => {
62 | const Node: any = value;
63 | return ;
64 | })}
65 |
66 | );
67 | }
68 | }
69 |
70 | return (
71 |
72 |
73 | {name}
74 |
75 | Popout
76 |
77 |
78 | {windowIfNodes()}
79 |
80 | );
81 | };
82 |
83 | export default Panel;
84 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/PanelContent.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useParams } from "react-router-dom";
3 |
4 | const PanelContent: React.FC = () => {
5 | const { id } = useParams<{ id: string }>();
6 |
7 | console.log("PanelContent component rendered");
8 |
9 | return (
10 |
11 |
Hello World {id}
12 |
13 | );
14 | };
15 |
16 | export default PanelContent;
17 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/StatusBar.tsx:
--------------------------------------------------------------------------------
1 | function StatusBar({ event }: { event: string }) {
2 | return (
3 |
6 | );
7 | }
8 |
9 | export default StatusBar;
10 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/Tab.tsx:
--------------------------------------------------------------------------------
1 | function Tab({ name }: { name: string }): JSX.Element {
2 | return (
3 |
11 | );
12 | }
13 |
14 | export default Tab;
15 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/Tool.tsx:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api/tauri";
2 |
3 | function Tool({ type }: { type: string }) {
4 | var icon;
5 | if (type == "run") {
6 | icon = (
7 |
8 |
9 |
10 | );
11 | } else if (type == "collapse-left") {
12 | icon = ;
13 | } else if (type == "collapse-right") {
14 | icon = ;
15 | }
16 |
17 | return (
18 | {
21 | if (type == "run") {
22 | invoke("execute_graph", { realtime: false });
23 | }
24 | }}
25 | >
26 | {icon}
27 |
28 | );
29 | }
30 |
31 | export default Tool;
32 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/components/ToolBar.tsx:
--------------------------------------------------------------------------------
1 | import Tool from "./Tool";
2 |
3 | function MenuBar() {
4 | return (
5 |
6 | {/* logo */}
7 |
8 |
9 |
10 |
11 |
12 |
13 | {/* left aligned items */}
14 |
15 |
16 |
17 |
18 | {/* other icons here */}
19 |
20 | {/* right aligned items */}
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default MenuBar;
30 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/contexts/StateContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState } from "react";
2 |
3 | export const StateContext = createContext(null);
4 |
5 | const defaultBackendState = { ready: false };
6 | const defaultFrontendState = { panelsShown: [1, 2] };
7 |
8 | type StateContextProviderProps = {
9 | children: React.ReactNode;
10 | };
11 | type StateContext = {
12 | backEndState: any;
13 | setBackEndState: React.Dispatch>;
14 | frontEndState: any;
15 | setFrontEndState: React.Dispatch>;
16 | };
17 |
18 | // create a context provider
19 | const StateContextProvider = ({ children }: StateContextProviderProps) => {
20 | const [backendState, setBackEndState] = useState(defaultBackendState);
21 | const [frontendState, setFrontEndState] = useState(defaultFrontendState);
22 |
23 | return {children} ;
24 | };
25 |
26 | // custom state hook
27 | export const useStateContext = () => {
28 | const contextObj = useContext(StateContext);
29 |
30 | if (!contextObj) {
31 | throw new Error("useStateContext must be used within a StateContextProvider");
32 | }
33 |
34 | return contextObj;
35 | };
36 |
37 | export default StateContextProvider;
38 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .mac-traffic-lights-container {
6 | padding-left: 10px;
7 | padding-right: 10px;
8 | align-items: center;
9 | display: flex;
10 | flex-direction: row;
11 | justify-content: left;
12 | gap: 8px;
13 | }
14 |
15 | .mac-traffic-light {
16 | width: 12px;
17 | height: 12px;
18 | border-radius: 50%;
19 | display: inline-block;
20 | }
21 |
22 | /* FIXME eventually remove mac traffic lights */
23 | .mac-traffic-lights-container div {
24 | background: transparent!important;
25 | }
26 |
27 | .red {
28 | background-color: #ff5f57;
29 | }
30 |
31 | .yellow {
32 | background-color: #febc2e;
33 | }
34 |
35 | .green {
36 | background-color: #28c840;
37 | }
38 |
39 | .helvetica {
40 | font-family: 'Helvetica', sans-serif;
41 | }
42 |
43 | .blender {
44 | background: url(blender.png);
45 | background-size: contain;
46 | background-repeat: no-repeat;
47 | }
48 |
49 | /* prevent overscroll bounce */
50 | body {
51 | overflow: hidden;
52 | }
53 |
54 | #gtx-trans {
55 | display: none;
56 | }
57 |
58 | img {
59 | user-select: none;
60 | pointer-events: none;
61 | }
62 |
63 | /* sorry */
64 | .react-flow__attribution {
65 | display: none;
66 | }
67 |
68 | .node {
69 | border-radius: 6px;
70 | box-shadow: 0 1px 4px rgba(0,0,0,0.2);
71 | /* background: #303030; */
72 | min-width: 200px;
73 | max-width: 1000px;
74 | background: #fcfcfc;
75 | border: 1px solid #0f1010;
76 | color: black;
77 | }
78 |
79 | .node.preview {
80 | /* enable dragging preview components */
81 | cursor: grab;
82 | position: relative;
83 |
84 | /* scale temporary for now, will be managed by JS later */
85 | transform: scale(0.5);
86 | transform-origin: top left;
87 | }
88 |
89 | /* all preview components inside should not be able to be selected. This is to "disable them" essentially */
90 | .node.preview * {
91 | user-select: none;
92 | pointer-events: none;
93 | }
94 |
95 | .node {
96 | font-size: 12px;
97 | }
98 |
99 | .node .node-inner {
100 | margin: 2px 0px 4px;
101 | }
102 |
103 | .node .node-field {
104 | position: relative;
105 | margin: 2px 12px;
106 | }
107 |
108 | .node-header {
109 | color: #fff;
110 | padding: 4px 8px;
111 | border-top-left-radius: 5px;
112 | border-top-right-radius: 5px;
113 | box-shadow: inset 0 -1 rgba(0,0,0,0.4);
114 | }
115 |
116 | .react-flow__node.selected .node {
117 | border: 1px solid rgb(29, 198, 255);
118 | -webkit-box-sizing: border-box!important;
119 | box-sizing: border-box!important;
120 | }
121 |
122 | .react-flow__pane.draggable {
123 | cursor: default;
124 | }
125 |
126 | .react-flow__resize-control.line.right {
127 | border-right-width: 8px;
128 | border-right-color: #ffffff00;
129 | }
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesa08/MIDIAnimator/8463ea5b1931311f0d30803ccd992111a32ca9c7/rust-impl/midianimator/src/logo.png
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App";
4 | import { HashRouter as Router, Route, Routes } from "react-router-dom";
5 | import reportWebVitals from "./reportWebVitals";
6 | import "./index.css";
7 | import PanelContent from "./components/PanelContent";
8 | import Settings from "./windows/Settings";
9 | import StateContextProvider from "./contexts/StateContext";
10 |
11 | const rootElement = document.getElementById("root");
12 |
13 | if (rootElement) {
14 | const root = ReactDOM.createRoot(rootElement);
15 | root.render(
16 |
17 |
18 |
19 |
20 | } />
21 | } />
22 | } />
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | // If you want to start measuring performance in your app, pass a function
31 | // to log results (for example: reportWebVitals(console.log))
32 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
33 | reportWebVitals(console.log);
34 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/BaseNode.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React, { ReactNode, useCallback, useState, useEffect } from "react";
3 | import { Handle, NodeResizeControl, Position } from "@xyflow/react";
4 | import "@xyflow/react/dist/base.css";
5 | import NodeHeader from "./NodeHeader";
6 | import { memo } from "react";
7 | import { useDimensions } from "../hooks/useDimensions";
8 |
9 | const handleStyle = {
10 | width: "16px",
11 | height: "16px",
12 | display: "flex",
13 | justifyContent: "center",
14 | alignItems: "center",
15 | position: "absolute",
16 | };
17 |
18 | /// base node for creating nodes
19 | /// @param nodeData: the data for the node (NOT reactflow data)
20 | /// @param inject: map of handles with ui elements to inject into the handle
21 | /// @param hidden: map of handles to hide, good for when you want to hide a handle but want to write data to it (ui element)
22 | /// @param executor: function to execute when the node is executed. only should be used for nodes that use JS execution
23 | /// @param dynamicHandles: map of handles to add to the node. looks exactly like handles found in `default_nodes.json`. good for when you want to add handles to a node that are not in the node data, dynamically as a UI feature
24 | /// @param data: reactflow data
25 | /// @param children: may be removed later
26 | function BaseNode({ nodeData, inject, hidden, executor, dynamicHandles, data, children }: { nodeData: any; inject?: any; executor?: any; hidden?: any; dynamicHandles?: any; data: any; children?: ReactNode }) {
27 | // iterate over handles
28 | let handleObjects = [];
29 |
30 | let preview = data != undefined && data == "preview" ? true : false;
31 |
32 | if (nodeData != null) {
33 | const handleTypes = ["outputs", "inputs"];
34 | for (let handleType of handleTypes) {
35 | let rfHandleType: boolean = false;
36 | if (handleType == "inputs") {
37 | rfHandleType = true;
38 | }
39 |
40 | let dynHandleArray = dynamicHandles == null || dynamicHandles[handleType] == undefined ? [] : dynamicHandles[handleType];
41 |
42 | for (let handle of [...nodeData["handles"][handleType], ...dynHandleArray]) {
43 | let uiInject = <>>;
44 | let uiHidden = false;
45 |
46 | if (inject != null && inject[handle["id"]] != null) {
47 | uiInject = inject[handle["id"]];
48 | }
49 |
50 | if (hidden != null && hidden[handle["id"]] != null) {
51 | uiHidden = hidden[handle["id"]];
52 | }
53 |
54 | const buildHandle = (
55 | <>
56 |
57 | {handle["name"]}
58 | {preview ? <>> : }
59 |
60 | {uiInject}
61 | >
62 | );
63 | handleObjects.push(buildHandle);
64 | }
65 | }
66 | }
67 |
68 | return (
69 |
70 |
71 |
72 |
{handleObjects.map((handle) => handle)}
73 |
74 | );
75 | }
76 |
77 | export default memo(BaseNode);
78 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/NodeHeader.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import * as st from "../styles.tsx";
3 |
4 | function NodeHeader({ label, type }: {label: any, type: any}) {
5 | return (
6 |
13 | {label}
14 |
15 | );
16 | }
17 |
18 | export default NodeHeader;
19 |
20 |
21 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/NodeTypes.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | type NodeComponentModule = {
3 | default: React.ComponentType;
4 | };
5 |
6 | const nodeComponents: Record = import.meta.glob("./*.tsx", { eager: true });
7 |
8 | // Function to convert file names to node names
9 | function convertFileNameToNodeName(fileName: string) {
10 | return fileName
11 | .replace("./", "") // remove relative path
12 | .replace(".tsx", "") // remove file extension
13 | .replace(/([A-Z])/g, "_$1") // convert camelcase to snake_case
14 | .toLowerCase(); // convert to lowercase
15 | }
16 |
17 | let nodeTypes: any = {};
18 | for (const [filePath, componentModule] of Object.entries(nodeComponents)) {
19 | const key = convertFileNameToNodeName(filePath); // convert file name to node name
20 | if (key[0] == "_") {
21 | continue;
22 | } // skip files that are not nodes
23 | nodeTypes[key] = componentModule.default; // get default export from module
24 | }
25 |
26 | export default nodeTypes;
27 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/animation_generator.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { message, open } from "@tauri-apps/api/dialog";
3 | import "@xyflow/react/dist/base.css";
4 | import BaseNode from "./BaseNode";
5 | import { useStateContext } from "../contexts/StateContext";
6 | import { getNodeData } from "../utils/node";
7 | import { useReactFlow } from "@xyflow/react";
8 | import { invoke } from "@tauri-apps/api/tauri";
9 |
10 | function animation_generator({ id, data, isConnectable }: { id: any; data: any; isConnectable: any }) {
11 | const { updateNodeData } = useReactFlow();
12 | const { backEndState: state, setBackEndState: setState } = useStateContext();
13 |
14 | const [nodeData, setNodeData] = useState(null);
15 | // const [file, setFile] = useState("");
16 | // const fileName = file.split("/").pop();
17 |
18 | // const [fileStatsState, setFileStatsState] = useState({ tracks: 0, minutes: "0:00" });
19 |
20 | // useEffect(() => {
21 | // var fileStats: any = {};
22 | // if (state != undefined && state.executed_results != undefined && id != undefined && id in state.executed_results) {
23 | // for (let line of state.executed_results[id]["stats"].split("\n")) {
24 | // let res = line.split(" ");
25 | // if (res[1] == "tracks") {
26 | // fileStats["tracks"] = res[0];
27 | // } else if (res[1] == "minutes" || res[1] == "seconds") {
28 | // fileStats["minutes"] = line;
29 | // }
30 | // }
31 | // setFileStatsState(fileStats);
32 | // }
33 | // }, [state.executed_results]);
34 |
35 | useEffect(() => {
36 | getNodeData("animation_generator").then(setNodeData);
37 | }, []);
38 |
39 | // const pick = useCallback(async () => {
40 | // let res = await onMIDIFilePick();
41 | // if (res != null) {
42 | // console.log("updating data{} object");
43 | // setFile(res.toString());
44 | // updateNodeData(id, { ...data, inputs: { ...data.inputs, file_path: res.toString() } });
45 | // }
46 | // }, []);
47 |
48 | // const filePathComponent = (
49 | // <>
50 | //
51 | // Pick MIDI File
52 | //
53 | // {fileName}
54 |
55 | // {Object.keys(fileStatsState).length != 0 && fileStatsState.tracks != 0 ? (
56 | // <>
57 | //
58 | // {fileStatsState.tracks} track{fileStatsState.tracks == 1 ? "" : "s"}
59 | //
60 | // {fileStatsState.minutes}
61 | // >
62 | // ) : (
63 | // <>>
64 | // )}
65 | // >
66 | // );
67 |
68 | const uiInject = {
69 | // file_path: filePathComponent,
70 | };
71 |
72 | const hiddenHandles = {
73 | // file_path: true,
74 | };
75 |
76 | return ;
77 | }
78 |
79 | export default animation_generator;
80 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/get_midi_file.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { message, open } from "@tauri-apps/api/dialog";
3 | import "@xyflow/react/dist/base.css";
4 | import BaseNode from "./BaseNode";
5 | import { useStateContext } from "../contexts/StateContext";
6 | import { getNodeData } from "../utils/node";
7 | import { useReactFlow } from "@xyflow/react";
8 | import { invoke } from "@tauri-apps/api/tauri";
9 |
10 | function get_midi_file({ id, data, isConnectable }: { id: any; data: any; isConnectable: any }) {
11 | const { updateNodeData } = useReactFlow();
12 | const { backEndState: state, setBackEndState: setState } = useStateContext();
13 |
14 | const [nodeData, setNodeData] = useState(null);
15 | const [file, setFile] = useState("");
16 | const fileName = file.split("/").pop();
17 |
18 | const [fileStatsState, setFileStatsState] = useState({ tracks: 0, minutes: "0:00" });
19 |
20 | useEffect(() => {
21 | var fileStats: any = {};
22 | if (state != undefined && state.executed_results != undefined && id != undefined && id in state.executed_results) {
23 | for (let line of state.executed_results[id]["stats"].split("\n")) {
24 | let res = line.split(" ");
25 | if (res[1] == "tracks") {
26 | fileStats["tracks"] = res[0];
27 | } else if (res[1] == "minutes" || res[1] == "seconds") {
28 | fileStats["minutes"] = line;
29 | }
30 | }
31 | setFileStatsState(fileStats);
32 | // state.executed_results[id]["stats"].split("\n").foreach((line: any) => {
33 | // invoke("log", { message: JSON.stringify(line) });
34 | // });
35 | }
36 | }, [state.executed_results]);
37 |
38 | useEffect(() => {
39 | getNodeData("get_midi_file").then(setNodeData);
40 | }, []);
41 |
42 | const pick = useCallback(async () => {
43 | let res = await onMIDIFilePick();
44 | if (res != null) {
45 | console.log("updating data{} object");
46 | setFile(res.toString());
47 | updateNodeData(id, { ...data, inputs: { ...data.inputs, file_path: res.toString() } });
48 | }
49 | }, []);
50 |
51 | const filePathComponent = (
52 | <>
53 |
54 | Pick MIDI File
55 |
56 | {fileName}
57 |
58 | {Object.keys(fileStatsState).length != 0 && fileStatsState.tracks != 0 ? (
59 | <>
60 |
61 | {fileStatsState.tracks} track{fileStatsState.tracks == 1 ? "" : "s"}
62 |
63 | {fileStatsState.minutes}
64 | >
65 | ) : (
66 | <>>
67 | )}
68 | >
69 | );
70 |
71 | const uiInject = {
72 | file_path: filePathComponent,
73 | };
74 |
75 | const hiddenHandles = {
76 | file_path: true,
77 | stats: true,
78 | };
79 |
80 | return ;
81 | }
82 |
83 | async function onMIDIFilePick() {
84 | const selected = await open({
85 | multiple: false,
86 | filters: [
87 | {
88 | name: "",
89 | extensions: ["mid", "midi"],
90 | },
91 | ],
92 | });
93 | return selected;
94 | }
95 |
96 | export default get_midi_file;
97 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/get_midi_track_data.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useReactFlow } from "@xyflow/react";
3 | import "@xyflow/react/dist/base.css";
4 | import BaseNode from "./BaseNode";
5 | import { getNodeData } from "../utils/node";
6 | import { useStateContext } from "../contexts/StateContext";
7 | import { invoke } from "@tauri-apps/api/tauri";
8 |
9 | function get_midi_track_data({ id, data, isConnectable }: { id: any; data: any; isConnectable: any }) {
10 | const { updateNodeData } = useReactFlow();
11 | const [nodeData, setNodeData] = useState(null);
12 | const { backEndState: state, setBackEndState: setState } = useStateContext();
13 |
14 | const [trackNamesState, setTrackNamesState] = useState([""]);
15 |
16 | useEffect(() => {
17 | getNodeData("get_midi_track_data").then(setNodeData);
18 | updateNodeData(id, { ...data, inputs: { ...data.inputs, track_name: "" } });
19 | }, []);
20 |
21 | function arraysEqual(arr1: any[], arr2: string | any[]) {
22 | if (arr1.length !== arr2.length) return false;
23 |
24 | return arr1.every((item, index) => {
25 | return item === arr2[index];
26 | });
27 | }
28 |
29 | useEffect(() => {
30 | var trackNames = [];
31 | if (state != undefined && state.executed_inputs != undefined && id != undefined && id in state.executed_inputs) {
32 | for (let key in state.executed_inputs[id]["tracks"]) {
33 | let trackName = state.executed_inputs[id]["tracks"][key]["name"];
34 | trackNames.push(trackName);
35 | }
36 |
37 | if (trackNames.length == 0) {
38 | trackNames = ["No track names found"];
39 | }
40 | if (!arraysEqual(trackNames, trackNamesState)) {
41 | setTrackNamesState(trackNames);
42 | updateNodeData(id, { ...data, inputs: { ...data.inputs, track_name: trackNames[0] } });
43 | }
44 | } else {
45 | setTrackNamesState(["No track names found"]);
46 | }
47 | }, [state.executed_inputs]);
48 |
49 | // this will need updated to reflect the the real data,
50 | // and how do we handle evaluating the data on change?
51 | // how do we handle sending the data to the backend?
52 | const trackNameComponent = (
53 | <>
54 | {
57 | updateNodeData(id, { ...data, inputs: { ...data.inputs, track_name: event.target.value } });
58 | }}
59 | >
60 | {trackNamesState.map((track: any) => {
61 | return {track} ;
62 | })}
63 |
64 | >
65 | );
66 |
67 | const uiInject = {
68 | track_name: trackNameComponent,
69 | };
70 |
71 | const hiddenHandles = {
72 | track_name: true,
73 | };
74 |
75 | return ;
76 | }
77 |
78 | // takes in a hashmap and returns a hashmap of the output keys filled
79 | function execute(input: any): any {
80 | console.log("executing");
81 | return {
82 | tracks: [],
83 | };
84 | }
85 |
86 | export default get_midi_track_data;
87 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/nodes_and_ui.md:
--------------------------------------------------------------------------------
1 | # Nodes and UI
2 |
3 | ## Overview
4 |
5 | How nodes are managed in backend
6 |
7 |
8 | JSON file defines basic node structure. This is shared between Rust and Typescript.
9 |
10 | ```json
11 | {
12 | "nodes": [
13 | {
14 | "id": "example_node",
15 | "name": "Example Node",
16 | "description": "An example",
17 | "executor": "js", // OR "rust
18 | "handles": {
19 | "inputs": [
20 | {
21 | "id": "example_input",
22 | "name": "Example input handle",
23 | "type": "i32", // still defining what this does
24 | "hidden": false // show the handle in the UI? (useful for having hidden data props) aside: should this be STRICTLY in the UI file, and not in the JSON config?
25 | }
26 | ],
27 | "outputs": [
28 |
29 | ]
30 | }
31 | }
32 | ]
33 | }
34 | ```
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/scene_link.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import "@xyflow/react/dist/base.css";
3 | import BaseNode from "./BaseNode";
4 | import { useStateContext } from "../contexts/StateContext";
5 | import { getNodeData } from "../utils/node";
6 | import { useReactFlow } from "@xyflow/react";
7 |
8 | function scene_link({ id, data, isConnectable }: { id: any; data: any; isConnectable: any }) {
9 | const { updateNodeData } = useReactFlow();
10 | const { backEndState: state, setBackEndState: setState } = useStateContext();
11 |
12 | const [nodeData, setNodeData] = useState(null);
13 |
14 | useEffect(() => {
15 | if (state != undefined && state.executed_results != undefined && id != undefined) {
16 | if (id in state.executed_inputs && id in state.executed_results) {
17 | // executed
18 | } else {
19 | // not executed
20 | }
21 | }
22 | }, [state.executed_results]);
23 |
24 | useEffect(() => {
25 | getNodeData("scene_link").then(setNodeData);
26 | }, []);
27 |
28 | const uiInject = {};
29 |
30 | const hiddenHandles = {};
31 |
32 | return ;
33 | }
34 |
35 | export default scene_link;
36 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/nodes/viewer.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import "@xyflow/react/dist/base.css";
3 | import BaseNode from "./BaseNode";
4 | import { useStateContext } from "../contexts/StateContext";
5 | import { getNodeData } from "../utils/node";
6 | import { useReactFlow } from "@xyflow/react";
7 |
8 | function customStringify(obj: any, depth = 0, maxDepth = 1, maxLength = 300): any {
9 | // Function to stringify objects or arrays without pretty-print formatting
10 | const compactStringify = (value: any) => {
11 | let str = JSON.stringify(value, (key, val) => {
12 | if (typeof val === "function") return val.toString();
13 | if (typeof val === "symbol") return val.toString();
14 | return val;
15 | });
16 |
17 | // Truncate if string exceeds maxLength
18 | if (str != undefined && str != null && str.length > maxLength) {
19 | str = str.slice(0, maxLength) + "...";
20 | }
21 | return str;
22 | };
23 |
24 | // Check if we are at the top-level and apply pretty-printing
25 | if (depth < maxDepth) {
26 | if (typeof obj === "object" && obj !== null) {
27 | let indent = " ".repeat(depth);
28 | let entries = Object.entries(obj).map(([key, value]) => `${indent}${key}: ${customStringify(value, depth + 1, maxDepth, maxLength)}`);
29 | return `{\n${entries.join(",\n")}\n${indent}}`;
30 | } else if (Array.isArray(obj)) {
31 | return `[${obj.map((value) => customStringify(value, depth + 1, maxDepth, maxLength)).join(", ")}]`;
32 | }
33 | }
34 |
35 | // Once below the top level, use the compact representation
36 | return compactStringify(obj);
37 | }
38 |
39 | function viewer({ id, data, isConnectable }: { id: any; data: any; isConnectable: any }) {
40 | const { updateNodeData } = useReactFlow();
41 | const { backEndState: state, setBackEndState: setState } = useStateContext();
42 |
43 | const [nodeData, setNodeData] = useState(null);
44 | const [viewerData, setViewerData] = useState(null);
45 |
46 | useEffect(() => {
47 | var tempViewerData: any = "";
48 | if (state != undefined && state.executed_results != undefined && id != undefined) {
49 | if (id in state.executed_inputs && id in state.executed_results) {
50 | tempViewerData = customStringify(state.executed_inputs[id]["data"]);
51 |
52 | if (tempViewerData != viewerData) {
53 | setViewerData(tempViewerData);
54 | }
55 | } else {
56 | setViewerData("");
57 | }
58 | }
59 | }, [state.executed_results]);
60 |
61 | useEffect(() => {
62 | getNodeData("viewer").then(setNodeData);
63 | }, []);
64 |
65 | const viewerComponent = (
66 | <>
67 |
68 | {viewerData?.split("\n").map((line: any, _: any) => (
69 | <>
70 | {line}
71 |
72 | >
73 | ))}
74 |
75 | >
76 | );
77 |
78 | const uiInject = {
79 | data: viewerComponent,
80 | };
81 |
82 | const hiddenHandles = {};
83 |
84 | return ;
85 | }
86 |
87 | export default viewer;
88 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/reportWebVitals.tsx:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry: (info: any) => void) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/setupTests.tsx:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom";
6 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/styles.tsx:
--------------------------------------------------------------------------------
1 | export const TEXT_SHADOW = "0 1px rgba(0,0,0,0.4)";
2 | export const INPUT_BORDER_RADIUS = 4;
3 | export const CHECKBOX_BORDER_RADIUS = 3;
4 | export const ACTIVE_BUTTON_BG = "#3873b8";
5 | export const INPUT_BG = "#545555";
6 |
7 | export const COLORS = {
8 | purple: "#8e294b",
9 | blue: "#006487",
10 | green: "#00745e",
11 | black: "#1d1d1d",
12 | darkPurple: "#411b26",
13 | lightPurple: "#3c3c88",
14 | };
15 |
16 | export const SOCKET_COLORS = {
17 | VALUE: "#a1a1a1",
18 | GEOMETRY: "#00daa0",
19 | VECTOR: "#6363ce",
20 | INT: "#488d57",
21 | BOOLEAN: "#d3a4d9",
22 | };
23 |
24 | export const HEADER_COLORS = {
25 | INDEX: COLORS.purple,
26 | VALUE: COLORS.purple,
27 | INPUT_VECTOR: COLORS.purple,
28 |
29 | MAP_RANGE: COLORS.blue,
30 | MATH: COLORS.blue,
31 | VECT_MATH: COLORS.lightPurple,
32 | COMBXYZ: COLORS.blue,
33 | SEPXYZ: COLORS.blue,
34 |
35 | POINTS: COLORS.green,
36 | FILLET_CURVE: COLORS.green,
37 | CURVE_PRIMITIVE_CIRCLE: COLORS.green,
38 | CURVE_PRIMITIVE_QUADRILATERAL: COLORS.green,
39 | BOUNDING_BOX: COLORS.green,
40 | MESH_PRIMITIVE_CUBE: COLORS.green,
41 | MESH_PRIMITIVE_CYLINDER: COLORS.green,
42 | MESH_PRIMITIVE_UV_SPHERE: COLORS.green,
43 | MESH_PRIMITIVE_GRID: COLORS.green,
44 | CURVE_TO_MESH: COLORS.green,
45 | INSTANCE_ON_POINTS: COLORS.green,
46 | SET_MATERIAL: COLORS.green,
47 | JOIN_GEOMETRY: COLORS.green,
48 | TRANSFORM: COLORS.green,
49 |
50 | GROUP_OUTPUT: COLORS.black,
51 | GROUP_INPUT: COLORS.black,
52 |
53 | VIEWER: COLORS.darkPurple,
54 | };
55 |
56 | export const SOCKET_SHAPES = {
57 | CIRCLE: {},
58 | DIAMOND: {
59 | borderRadius: 0,
60 | transform: "rotate(45deg)",
61 | top: 8,
62 | },
63 | DIAMOND_DOT: {
64 | borderRadius: 0,
65 | transform: "rotate(45deg)",
66 | top: 8,
67 | },
68 | };
69 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/utils/node.tsx:
--------------------------------------------------------------------------------
1 | import { resolveResource } from '@tauri-apps/api/path';
2 | import { BaseDirectory, readTextFile } from '@tauri-apps/api/fs';
3 |
4 |
5 |
6 | export async function getNodeData(nodeId: string) {
7 | let data: any = await readTextFile("src/configs/default_nodes.json", { dir: BaseDirectory.Resource });
8 | if (data == null) {
9 | console.log("error finding data for node ", nodeId);
10 | return {"id": "", "name": "error", handles: {}};
11 | }
12 | data = JSON.parse(data);
13 | return data["nodes"].find((node: any) => node["id"] === nodeId);
14 | }
--------------------------------------------------------------------------------
/rust-impl/midianimator/src/windows/Settings.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Settings() {
4 | return Settings
;
5 | }
6 |
7 | export default Settings;
8 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | mode: "jit",
4 | content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | experimental: {
10 | optimizeUniversalDefaults: true, // https://github.com/tailwindlabs/tailwindcss/discussions/7411
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/rust-impl/midianimator/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | build: {
7 | outDir: 'build',
8 | },
9 | server: {
10 | port: 3000,
11 | },
12 | });
--------------------------------------------------------------------------------