Added Edge Styling & Labeling
This commit is contained in:
parent
5f4125b197
commit
e5e5b26a4c
3
.gitignore
vendored
3
.gitignore
vendored
@ -16,4 +16,5 @@ Borealis-Server.exe
|
||||
/Agent/
|
||||
|
||||
# Misc Files/Folders
|
||||
.vs/s
|
||||
.vs/s
|
||||
AI_Model_Custom_Instructions.md
|
@ -12,6 +12,7 @@
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.0",
|
||||
"react-resizable": "3.0.5",
|
||||
"react-color": "2.19.3",
|
||||
"reactflow": "11.11.4",
|
||||
"socket.io-client": "4.8.1",
|
||||
"react-simple-keyboard": "3.8.62",
|
||||
|
@ -1,4 +1,4 @@
|
||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Flow_Editor.jsx
|
||||
// //////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Flow_Editor.jsx
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import ReactFlow, {
|
||||
@ -8,25 +8,20 @@ import ReactFlow, {
|
||||
applyEdgeChanges,
|
||||
useReactFlow
|
||||
} from "reactflow";
|
||||
import { Menu, MenuItem } from "@mui/material";
|
||||
import { Menu, MenuItem, MenuList, Slider, Box } from "@mui/material";
|
||||
import {
|
||||
Polyline as PolylineIcon,
|
||||
DeleteForever as DeleteForeverIcon
|
||||
DeleteForever as DeleteForeverIcon,
|
||||
DoubleArrow as DoubleArrowIcon,
|
||||
LinearScale as LinearScaleIcon,
|
||||
Timeline as TimelineIcon,
|
||||
FormatColorFill as FormatColorFillIcon,
|
||||
ArrowRight as ArrowRightIcon,
|
||||
Edit as EditIcon
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import { SketchPicker } from "react-color";
|
||||
import "reactflow/dist/style.css";
|
||||
|
||||
/**
|
||||
* Single flow editor component.
|
||||
*
|
||||
* Props:
|
||||
* - nodes
|
||||
* - edges
|
||||
* - setNodes
|
||||
* - setEdges
|
||||
* - nodeTypes
|
||||
* - categorizedNodes (used to find node meta info on drop)
|
||||
*/
|
||||
export default function FlowEditor({
|
||||
nodes,
|
||||
edges,
|
||||
@ -38,36 +33,38 @@ export default function FlowEditor({
|
||||
const wrapperRef = useRef(null);
|
||||
const { project } = useReactFlow();
|
||||
const [contextMenu, setContextMenu] = useState(null);
|
||||
const [edgeContextMenu, setEdgeContextMenu] = useState(null);
|
||||
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||
const [colorPickerMode, setColorPickerMode] = useState(null);
|
||||
const [labelPadding, setLabelPadding] = useState([8, 4]);
|
||||
const [labelBorderRadius, setLabelBorderRadius] = useState(4);
|
||||
const [labelOpacity, setLabelOpacity] = useState(0.8);
|
||||
const [tempColor, setTempColor] = useState({ hex: "#58a6ff" });
|
||||
const [pickerPos, setPickerPos] = useState({ x: 0, y: 0 });
|
||||
|
||||
const edgeStyles = {
|
||||
step: "step",
|
||||
curved: "bezier",
|
||||
straight: "straight"
|
||||
};
|
||||
|
||||
const animationStyles = {
|
||||
dashes: { animated: true, style: { strokeDasharray: "6 3" } },
|
||||
dots: { animated: true, style: { strokeDasharray: "2 4" } },
|
||||
none: { animated: false, style: {} }
|
||||
};
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const type = event.dataTransfer.getData("application/reactflow");
|
||||
if (!type) return;
|
||||
|
||||
const bounds = wrapperRef.current.getBoundingClientRect();
|
||||
const position = project({
|
||||
x: event.clientX - bounds.left,
|
||||
y: event.clientY - bounds.top
|
||||
});
|
||||
|
||||
const position = project({ x: event.clientX - bounds.left, y: event.clientY - bounds.top });
|
||||
const id = "node-" + Date.now();
|
||||
|
||||
// Find node definition in the categorizedNodes
|
||||
const nodeMeta = Object.values(categorizedNodes)
|
||||
.flat()
|
||||
.find((n) => n.type === type);
|
||||
|
||||
const newNode = {
|
||||
id: id,
|
||||
type: type,
|
||||
position: position,
|
||||
data: {
|
||||
label: nodeMeta?.label || type,
|
||||
content: nodeMeta?.content
|
||||
}
|
||||
};
|
||||
|
||||
const nodeMeta = Object.values(categorizedNodes).flat().find((n) => n.type === type);
|
||||
const newNode = { id, type, position, data: { label: nodeMeta?.label || type, content: nodeMeta?.content } };
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
},
|
||||
[project, setNodes, categorizedNodes]
|
||||
@ -79,21 +76,19 @@ export default function FlowEditor({
|
||||
}, []);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params) =>
|
||||
(params) => {
|
||||
setEdges((eds) =>
|
||||
addEdge(
|
||||
{
|
||||
...params,
|
||||
type: "smoothstep",
|
||||
type: "bezier",
|
||||
animated: true,
|
||||
style: {
|
||||
strokeDasharray: "6 3",
|
||||
stroke: "#58a6ff"
|
||||
}
|
||||
style: { strokeDasharray: "6 3", stroke: "#58a6ff" }
|
||||
},
|
||||
eds
|
||||
)
|
||||
),
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
@ -101,7 +96,6 @@ export default function FlowEditor({
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[setEdges]
|
||||
@ -109,45 +103,119 @@ export default function FlowEditor({
|
||||
|
||||
const handleRightClick = (e, node) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({
|
||||
mouseX: e.clientX + 2,
|
||||
mouseY: e.clientY - 6,
|
||||
nodeId: node.id
|
||||
});
|
||||
setContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6, nodeId: node.id });
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
if (contextMenu?.nodeId) {
|
||||
const handleEdgeRightClick = (e, edge) => {
|
||||
e.preventDefault();
|
||||
setEdgeContextMenu({ edgeId: edge.id, mouseX: e.clientX + 2, mouseY: e.clientY - 6 });
|
||||
setSelectedEdgeId(edge.id);
|
||||
};
|
||||
|
||||
const changeEdgeType = (newType) => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) =>
|
||||
e.id === selectedEdgeId ? { ...e, type: edgeStyles[newType] } : e
|
||||
)
|
||||
);
|
||||
setEdgeContextMenu(null);
|
||||
};
|
||||
|
||||
const changeEdgeAnimation = (newAnim) => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) => {
|
||||
if (e.id !== selectedEdgeId) return e;
|
||||
const strokeColor = e.style?.stroke || "#58a6ff";
|
||||
const anim = animationStyles[newAnim] || {};
|
||||
return {
|
||||
...e,
|
||||
animated: anim.animated,
|
||||
style: { ...anim.style, stroke: strokeColor },
|
||||
markerEnd: e.markerEnd ? { ...e.markerEnd, color: strokeColor } : undefined
|
||||
};
|
||||
})
|
||||
);
|
||||
setEdgeContextMenu(null);
|
||||
};
|
||||
|
||||
const handleColorChange = (color) => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) => {
|
||||
if (e.id !== selectedEdgeId) return e;
|
||||
const updated = { ...e };
|
||||
if (colorPickerMode === "stroke") {
|
||||
updated.style = { ...e.style, stroke: color.hex };
|
||||
if (e.markerEnd) updated.markerEnd = { ...e.markerEnd, color: color.hex };
|
||||
} else if (colorPickerMode === "labelText") {
|
||||
updated.labelStyle = { ...e.labelStyle, fill: color.hex };
|
||||
} else if (colorPickerMode === "labelBg") {
|
||||
updated.labelBgStyle = { ...e.labelBgStyle, fill: color.hex, fillOpacity: labelOpacity };
|
||||
}
|
||||
return updated;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddLabel = () => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) =>
|
||||
e.id === selectedEdgeId ? { ...e, label: "New Label" } : e
|
||||
)
|
||||
);
|
||||
setEdgeContextMenu(null);
|
||||
};
|
||||
|
||||
const handleEditLabel = () => {
|
||||
const newText = prompt("Enter label text:");
|
||||
if (newText !== null) {
|
||||
setEdges((eds) =>
|
||||
eds.filter(
|
||||
(e) =>
|
||||
e.source !== contextMenu.nodeId &&
|
||||
e.target !== contextMenu.nodeId
|
||||
eds.map((e) =>
|
||||
e.id === selectedEdgeId ? { ...e, label: newText } : e
|
||||
)
|
||||
);
|
||||
}
|
||||
setContextMenu(null);
|
||||
setEdgeContextMenu(null);
|
||||
};
|
||||
|
||||
const handleRemoveNode = () => {
|
||||
if (contextMenu?.nodeId) {
|
||||
setNodes((nds) => nds.filter((n) => n.id !== contextMenu.nodeId));
|
||||
setEdges((eds) =>
|
||||
eds.filter(
|
||||
(e) =>
|
||||
e.source !== contextMenu.nodeId &&
|
||||
e.target !== contextMenu.nodeId
|
||||
)
|
||||
);
|
||||
}
|
||||
setContextMenu(null);
|
||||
const handleRemoveLabel = () => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) =>
|
||||
e.id === selectedEdgeId ? { ...e, label: undefined } : e
|
||||
)
|
||||
);
|
||||
setEdgeContextMenu(null);
|
||||
};
|
||||
|
||||
const handlePickColor = (mode) => {
|
||||
setColorPickerMode(mode);
|
||||
setTempColor({ hex: "#58a6ff" });
|
||||
setPickerPos({ x: edgeContextMenu?.mouseX || 0, y: edgeContextMenu?.mouseY || 0 });
|
||||
setShowColorPicker(true);
|
||||
};
|
||||
|
||||
const applyLabelStyleExtras = () => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) =>
|
||||
e.id === selectedEdgeId
|
||||
? {
|
||||
...e,
|
||||
labelBgPadding: labelPadding,
|
||||
labelBgStyle: {
|
||||
...e.labelBgStyle,
|
||||
fillOpacity: labelOpacity,
|
||||
rx: labelBorderRadius,
|
||||
ry: labelBorderRadius
|
||||
}
|
||||
}
|
||||
: e
|
||||
)
|
||||
);
|
||||
setEdgeContextMenu(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const nodeCountEl = document.getElementById("nodeCount");
|
||||
if (nodeCountEl) {
|
||||
nodeCountEl.innerText = nodes.length;
|
||||
}
|
||||
if (nodeCountEl) nodeCountEl.innerText = nodes.length;
|
||||
}, [nodes]);
|
||||
|
||||
return (
|
||||
@ -162,26 +230,18 @@ export default function FlowEditor({
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeContextMenu={handleRightClick}
|
||||
onEdgeContextMenu={handleEdgeRightClick}
|
||||
defaultViewport={{ x: 0, y: 0, zoom: 1.5 }}
|
||||
edgeOptions={{
|
||||
type: "smoothstep",
|
||||
type: "bezier",
|
||||
animated: true,
|
||||
style: {
|
||||
strokeDasharray: "6 3",
|
||||
stroke: "#58a6ff"
|
||||
}
|
||||
style: { strokeDasharray: "6 3", stroke: "#58a6ff" }
|
||||
}}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background
|
||||
variant="lines"
|
||||
gap={65}
|
||||
size={1}
|
||||
color="rgba(255, 255, 255, 0.2)"
|
||||
/>
|
||||
<Background variant="lines" gap={65} size={1} color="rgba(255,255,255,0.2)" />
|
||||
</ReactFlow>
|
||||
|
||||
{/* Right-click node menu */}
|
||||
<Menu
|
||||
open={Boolean(contextMenu)}
|
||||
onClose={() => setContextMenu(null)}
|
||||
@ -191,23 +251,196 @@ export default function FlowEditor({
|
||||
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
||||
: undefined
|
||||
}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: "#1e1e1e",
|
||||
color: "#fff",
|
||||
fontSize: "13px"
|
||||
}
|
||||
}}
|
||||
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
|
||||
>
|
||||
<MenuItem onClick={handleDisconnect}>
|
||||
<PolylineIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (contextMenu?.nodeId) {
|
||||
setEdges((eds) =>
|
||||
eds.filter(
|
||||
(e) =>
|
||||
e.source !== contextMenu.nodeId &&
|
||||
e.target !== contextMenu.nodeId
|
||||
)
|
||||
);
|
||||
}
|
||||
setContextMenu(null);
|
||||
}}
|
||||
>
|
||||
<PolylineIcon
|
||||
sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }}
|
||||
/>
|
||||
Disconnect All Edges
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleRemoveNode}>
|
||||
<DeleteForeverIcon sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }} />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (contextMenu?.nodeId) {
|
||||
setNodes((nds) =>
|
||||
nds.filter((n) => n.id !== contextMenu.nodeId)
|
||||
);
|
||||
setEdges((eds) =>
|
||||
eds.filter(
|
||||
(e) =>
|
||||
e.source !== contextMenu.nodeId &&
|
||||
e.target !== contextMenu.nodeId
|
||||
)
|
||||
);
|
||||
}
|
||||
setContextMenu(null);
|
||||
}}
|
||||
>
|
||||
<DeleteForeverIcon
|
||||
sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }}
|
||||
/>
|
||||
Remove Node
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<Menu
|
||||
open={Boolean(edgeContextMenu)}
|
||||
onClose={() => setEdgeContextMenu(null)}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
edgeContextMenu
|
||||
? { top: edgeContextMenu.mouseY, left: edgeContextMenu.mouseX }
|
||||
: undefined
|
||||
}
|
||||
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
setEdges((eds) =>
|
||||
eds.filter((e) => e.id !== selectedEdgeId)
|
||||
)
|
||||
}
|
||||
>
|
||||
<DeleteForeverIcon
|
||||
sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }}
|
||||
/>
|
||||
Unlink Edge
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
Edge Styles
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => changeEdgeType("step")}>Step</MenuItem>
|
||||
<MenuItem onClick={() => changeEdgeType("curved")}>Curved</MenuItem>
|
||||
<MenuItem onClick={() => changeEdgeType("straight")}>Straight</MenuItem>
|
||||
</MenuList>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
Animations
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => changeEdgeAnimation("dashes")}>Dashes</MenuItem>
|
||||
<MenuItem onClick={() => changeEdgeAnimation("dots")}>Dots</MenuItem>
|
||||
<MenuItem onClick={() => changeEdgeAnimation("none")}>Solid Line</MenuItem>
|
||||
</MenuList>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
Label
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleAddLabel}>Add</MenuItem>
|
||||
<MenuItem onClick={handleRemoveLabel}>Remove</MenuItem>
|
||||
<MenuItem onClick={handleEditLabel}>
|
||||
<EditIcon sx={{ fontSize: 16, mr: 1 }} />
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handlePickColor("labelText")}>Text Color</MenuItem>
|
||||
<MenuItem onClick={() => handlePickColor("labelBg")}>Background Color</MenuItem>
|
||||
<MenuItem>
|
||||
Padding:
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={`${labelPadding[0]},${labelPadding[1]}`}
|
||||
style={{ width: 80, marginLeft: 8 }}
|
||||
onBlur={(e) => {
|
||||
const parts = e.target.value.split(",").map((v) => parseInt(v.trim()));
|
||||
if (parts.length === 2 && parts.every(Number.isFinite)) {
|
||||
setLabelPadding(parts);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
Radius:
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="20"
|
||||
defaultValue={labelBorderRadius}
|
||||
style={{ width: 60, marginLeft: 8 }}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value);
|
||||
if (!isNaN(val)) setLabelBorderRadius(val);
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
Opacity:
|
||||
<Box display="flex" alignItems="center" ml={1}>
|
||||
<Slider
|
||||
value={labelOpacity}
|
||||
onChange={(_, v) => setLabelOpacity(v)}
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
style={{ width: 100 }}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
step="0.05"
|
||||
min="0"
|
||||
max="1"
|
||||
value={labelOpacity}
|
||||
style={{ width: 60, marginLeft: 8 }}
|
||||
onChange={(e) => {
|
||||
const v = parseFloat(e.target.value);
|
||||
if (!isNaN(v)) setLabelOpacity(v);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={applyLabelStyleExtras}>
|
||||
Apply Label Style Changes
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handlePickColor("stroke")}>Color</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{showColorPicker && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: pickerPos.y,
|
||||
left: pickerPos.x,
|
||||
zIndex: 9999,
|
||||
background: "#1e1e1e",
|
||||
padding: "10px",
|
||||
borderRadius: "8px"
|
||||
}}
|
||||
>
|
||||
<SketchPicker color={tempColor.hex} onChange={(c) => setTempColor(c)} />
|
||||
<div style={{ marginTop: "10px", textAlign: "center" }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleColorChange(tempColor);
|
||||
setShowColorPicker(false);
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: "#58a6ff",
|
||||
color: "#121212",
|
||||
border: "none",
|
||||
padding: "6px 12px",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
fontWeight: "bold"
|
||||
}}
|
||||
>
|
||||
Set Color
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
69
project_directory_tree.txt
Normal file
69
project_directory_tree.txt
Normal file
@ -0,0 +1,69 @@
|
||||
Folder Structure:
|
||||
├── Dependencies
|
||||
├── Launch-Borealis.ps1
|
||||
├── Launch-Borealis.sh
|
||||
└── Data
|
||||
├── Agent
|
||||
│ ├── agent-requirements.txt
|
||||
│ ├── Borealis.ico
|
||||
│ ├── borealis-agent.py
|
||||
│ └── Package_Borealis-Agent.ps1
|
||||
└── Server
|
||||
├── Borealis.ico
|
||||
├── Package-Borealis-Server.ps1
|
||||
├── server.py
|
||||
├── server-requirements.txt
|
||||
├── Python_API_Endpoints
|
||||
│ ├── ocr_engines.py
|
||||
│ └── Tesseract-OCR
|
||||
├── Sounds
|
||||
│ └── Short_Beep.wav
|
||||
├── WebUI
|
||||
│ ├── package.json
|
||||
│ ├── public
|
||||
│ │ ├── Borealis_Logo.png
|
||||
│ │ ├── Borealis_Logo_Full.png
|
||||
│ │ ├── favicon.ico
|
||||
│ │ └── index.html
|
||||
│ └── src
|
||||
│ ├── App.jsx
|
||||
│ ├── Borealis.css
|
||||
│ ├── Dialogs.jsx
|
||||
│ ├── Flow_Editor.jsx
|
||||
│ ├── Flow_Tabs.jsx
|
||||
│ ├── index.js
|
||||
│ ├── Node_Sidebar.jsx
|
||||
│ ├── Status_Bar.jsx
|
||||
│ └── nodes
|
||||
│ ├── Agents
|
||||
│ │ ├── Node_Agent.jsx
|
||||
│ │ └── Node_Agent_Role_Screenshot.jsx
|
||||
│ ├── Alerting
|
||||
│ │ └── Node_Alert_Sound.jsx
|
||||
│ ├── Data Analysis
|
||||
│ │ └── Node_OCR_Text_Extraction.jsx
|
||||
│ ├── Data Manipulation
|
||||
│ │ └── Node_Array_Index_Extractor.jsx
|
||||
│ ├── Flyff Universe
|
||||
│ ├── General Purpose
|
||||
│ │ ├── Node_Data.jsx
|
||||
│ │ ├── Node_Logical_Operators.jsx
|
||||
│ │ └── Node_Math_Operation.jsx
|
||||
│ ├── Image Processing
|
||||
│ │ ├── Node_Adjust_Contrast.jsx
|
||||
│ │ ├── Node_BW_Threshold.jsx
|
||||
│ │ ├── Node_Convert_to_Grayscale.jsx
|
||||
│ │ ├── Node_Export_Image.jsx
|
||||
│ │ ├── Node_Image_Viewer.jsx
|
||||
│ │ └── Node_Upload_Image.jsx
|
||||
│ ├── Macro Automation
|
||||
│ │ └── Node_Macro_KeyPress.jsx
|
||||
│ ├── Organization
|
||||
│ │ └── Node_Backdrop_Group_Box.jsx
|
||||
│ └── Reporting
|
||||
│ └── Node_Export_to_CSV.jsx
|
||||
└── Workflows
|
||||
├── Examples
|
||||
│ └── Logic-Comparison-Example.json
|
||||
└── Flyff Universe
|
||||
└── Flyff_OCR_Workflow.json
|
Loading…
x
Reference in New Issue
Block a user