Skip to content

Explorations/port proximity attachment #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,24 @@ The backlog is organized by epic, with each task having a unique ID, description
- **Utilize `react-dnd` for Drag-and-Drop Functionality**: `react-dnd` will be used to handle the drag-and-drop operations, providing a flexible and intuitive user experience for adding nodes to the canvas.
- **Visual Feedback and User Experience**: Implement visual cues during the drag-and-drop operation, such as changing the cursor, highlighting potential drop zones, and showing a "ghost" image of the node being dragged to provide clear feedback to the user.
- **Responsive Design Considerations**: Ensure that the drag-and-drop functionality is fully responsive and provides a consistent experience across different devices and screen sizes.
- **FB-04** (Priority: 4): Develop node connection functionality.
- **Objective**: Allow users to create connections between nodes on the canvas, forming logical flows.
- **FB-04** (Priority: 4): Verify and Enhance Node Connection Functionality.
- **Objective**: Ensure the existing node connection functionality is working as expected and introduce enhancements for a more intuitive user experience.
- **Technical Requirements**:
- Implement a method for users to draw connections between nodes, possibly by dragging from one node's output port to another node's input port.
- Utilize `@projectstorm/react-diagrams` for managing the rendering and logic of connections, ensuring compatibility with the library's way of handling links.
- Connections should be visually distinct and should support different styles (straight lines, curves) to enhance readability.
- Include validation to ensure that connections between incompatible node types or ports are not allowed.
- Provide visual feedback during the connection process, such as highlighting compatible ports when drawing a connection.
- **FB-05** (Priority: 5): Implement editor UI for nodes.
- **Objective**: Provide a user-friendly interface for configuring and editing node properties.
- **Technical Requirements**:
- Implement UI components for editing node properties, including individual node attributes and dialog boxes for configuration.
- Verify that users can draw connections between nodes by dragging from one node's output port to another node's input port, utilizing `@projectstorm/react-diagrams` for rendering and logic. Ensure connections are visually distinct, support different styles for enhanced readability, include validation for incompatible node types or ports, and provide visual feedback during the connection process.
- Implement state management for the flow canvas that streams changes into our application state, ensuring the state matches the spec for a Node-RED `flows.yaml` file. This will involve capturing the state of nodes, their connections, and any other relevant flow information in a format that is compatible with Node-RED, facilitating seamless integration and future features such as exporting flows.
- Explore the feasibility of enhancing the connection drawing process to allow for auto-attachment of connections to the nearest appropriate port (input or output) when a user draws a connection over a node. This feature aims to simplify the process of creating connections by reducing the precision required to attach a wire to a specific port, thereby improving the user experience.
- **Implementation Details**:
- **State Management for Flow Canvas**:
- To manage the state of the flow canvas effectively, including nodes, their connections, and other relevant flow information, new files dedicated to flow management will be introduced:
1. **Flow Slice (`flow/flow.slice.ts`)**: Manages the state of the flow canvas, including nodes, connections, and flow configurations.
2. **Flow Logic (`flow/flow.logic.ts`)**: Encapsulates the business logic for managing flows, including the creation, update, and deletion of nodes and connections.
3. **Flow Slice Tests (`flow/flow.slice.spec.ts`)**: Ensures the flow slice correctly manages the state of the flow canvas.
4. **Flow Logic Tests (`flow/flow.logic.spec.ts`)**: Validates the business logic for flow management.
- These files will work together to ensure a robust state management system for the flow canvas, enhancing node connection functionality and ensuring a seamless user experience.
- **Enhancements to Connection Drawing Process**:
- **User Experience (UX) Improvements**: Simplify the process of creating connections with intuitive auto-attachment to the nearest valid port and provide visual feedback during the process.
- **Technical Feasibility**: Implement port proximity detection and valid port identification to support auto-attachment features.
- **Implementation Strategies**: Explore extending or customizing `@projectstorm/react-diagrams` for auto-attachment functionality and develop custom drag-and-drop logic as needed.

#### Epic: Node Management Interface

Expand Down Expand Up @@ -220,6 +226,19 @@ The backlog is organized by epic, with each task having a unique ID, description
- **UX-03**: Implement responsive design.
- **Objective**: Ensure the frontend client is accessible and usable across various devices.
- **Technical Requirements**: Adopt a responsive design approach that allows the frontend client to adapt to different screen sizes and resolutions, ensuring a consistent user experience.
- **UX-04**: Implement Visual Indicators for Node Connection Compatibility.
- **Objective**: Enhance the user experience by introducing visual indicators that provide immediate feedback on the compatibility of connections between nodes during the drag-and-drop operation.
- **Technical Requirements**:
- Develop a system to visually indicate when a connection being dragged is compatible or incompatible with a potential target port.
- Customize port and link models to include compatibility information, allowing for dynamic styling based on the context of the drag-and-drop operation.
- Implement custom widgets for ports and links that change appearance (e.g., color, icons) to reflect compatibility status.
- Utilize the event system in `@projectstorm/react-diagrams` to update the appearance of ports and links in real-time during drag-and-drop actions.
- **Justification**: This feature aims to simplify the process of creating connections by reducing the need for trial and error, thereby improving the overall user experience. By providing clear visual cues, users can easily identify valid connection paths, leading to more efficient flow construction.
- **Implementation Notes**:
- Consider the development effort and complexity involved in customizing the underlying library. This task may require extensive testing to ensure a seamless integration with existing functionalities.
- Prioritize user feedback on the current version of the flow builder to determine the necessity and priority of this enhancement.
- **Future Considerations**:
- Gather user feedback on the implementation to assess its effectiveness and explore further enhancements based on real-world usage.

#### Epic: Debugging and Testing Tools

Expand Down Expand Up @@ -270,8 +289,8 @@ The backlog is organized by epic, with each task having a unique ID, description

| To Do | In Progress | In Review | Done |
| ----- | ----------- | --------- | ----- |
| FB-04 | | | FB-01 |
| FB-05 | | | FB-02 |
| FB-05 | FB-04 | | FB-01 |
| | | | FB-02 |
| | | | FB-03 |

### Progress Tracking
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
AbstractReactFactory,
DefaultDiagramState,
DefaultLabelFactory,
DefaultLinkFactory,
DefaultPortFactory,
DiagramEngine,
LayerModel,
Expand All @@ -13,6 +12,7 @@ import {
SelectionBoxLayerFactory,
} from '@projectstorm/react-diagrams';

import { CustomLinkFactory } from './link';
import { CustomNodeFactory } from './node';

export class CustomEngine extends DiagramEngine {
Expand Down Expand Up @@ -67,7 +67,7 @@ export const createEngine = (options = {}) => {
engine.getLayerFactories().registerFactory(new SelectionBoxLayerFactory());
engine.getLabelFactories().registerFactory(new DefaultLabelFactory());
engine.getNodeFactories().registerFactory(new CustomNodeFactory()); // i cant figure out why
engine.getLinkFactories().registerFactory(new DefaultLinkFactory());
engine.getLinkFactories().registerFactory(new CustomLinkFactory());
engine.getLinkFactories().registerFactory(new PathFindingLinkFactory());
engine.getPortFactories().registerFactory(new DefaultPortFactory());
// register the default interaction behaviours
Expand Down
83 changes: 83 additions & 0 deletions packages/flow-client/src/app/components/flow-canvas/diagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
DiagramModel,
LinkModel,
LinkModelGenerics,
} from '@projectstorm/react-diagrams';

type Point = {
x: number;
y: number;
};

export class CustomDiagramModel extends DiagramModel {
addLink(link: LinkModel<LinkModelGenerics>): LinkModel<LinkModelGenerics> {
const addedLink = super.addLink(link);
// After adding the link, attach event listeners
this.attachLinkListeners(addedLink);
return addedLink;
}

private attachLinkListeners(link: LinkModel<LinkModelGenerics>) {
// Logic to attach event listeners goes here
// Since the actual DOM element might not yet be available immediately after adding the link,
// you might need to defer this operation or use a MutationObserver as discussed in Option 3.
setTimeout(() => this.setupLinkDragEvents(link), 50); // Example delay
}

private setupLinkDragEvents(link: LinkModel<LinkModelGenerics>) {
// Assuming you have a way to identify the DOM element for the link
const linkElement = document.querySelector(
`[data-linkid="${link.getID()}"]`
);
if (linkElement) {
linkElement.addEventListener('dragstart', () => {
// Your drag start logic here
});
linkElement.addEventListener('dragend', e => {
const dropPosition = this.getDropPosition(e);
// Now you have the drop position, you can proceed to find the nearest node or port
this.attachLinkToNearestNode(link, dropPosition);
});
}
}

private getDropPosition(event: DragEvent): Point {
const engine = this.engine;
// Assuming `event` is the native drop event
const { clientX, clientY } = event;

// Translate screen coordinates to diagram coordinates
// This step is crucial because the diagram might be zoomed or scrolled
const relativePoint = engine.getRelativeMousePoint({
clientX,
clientY,
});

return relativePoint;
}

private attachLinkToNearestNode(
link: LinkModel<LinkModelGenerics>,
dropPosition: Point
) {
// Example pseudocode
const dropPosition = this.getDropPosition(); // Implement this based on your app's logic
let nearestNode = null;
let minDistance = Infinity;

this.getNodes().forEach(node => {
const nodePosition = this.getNodePosition(node); // Implement this
const distance = this.calculateDistance(dropPosition, nodePosition); // Implement this

if (distance < minDistance) {
nearestNode = node;
minDistance = distance;
}
});

if (nearestNode) {
// Logic to attach the link to the nearestNode's port
// This might involve finding a specific port on the node and setting the link's target port
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
DiagramModel,
PortModelAlignment,
} from '@projectstorm/react-diagrams';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useDrop } from 'react-dnd';
import styled from 'styled-components';

Expand All @@ -15,6 +15,7 @@ import { NodeEntity } from '../../redux/modules/node/node.slice';
import { createEngine } from './custom-engine';
import { CustomNodeModel } from './node';
import { useAppLogic } from '../../redux/hooks';
import { CustomDiagramModel } from './diagram';

const StyledCanvasWidget = styled(CanvasWidget)`
background-color: #f0f0f0; /* Light grey background */
Expand Down Expand Up @@ -67,31 +68,21 @@ export const FlowCanvasContainer: React.FC<FlowCanvasContainerProps> = ({
}) => {
const nodeLogic = useAppLogic().node;

const model = new DiagramModel();
model.setGridSize(20);

// Your existing setup code for adding nodes and links to the model

engine.setModel(model);
const [model, setModel] = useState<DiagramModel>(new CustomDiagramModel());

useEffect(() => {
const canvas = document.querySelector('.flow-canvas');
const handleZoom = (event: Event) =>
engine.increaseZoomLevel(event as WheelEvent);

canvas?.addEventListener('wheel', handleZoom);

return () => {
canvas?.removeEventListener('wheel', handleZoom);
};
}, []);
engine.setModel(model);

// Add initial nodes and links to the model if any
initialDiagram.nodes?.forEach(node => model.addNode(node));
initialDiagram.links?.forEach(link => model.addLink(link));
model.setGridSize(20);
// Add initial nodes and links to the model if any
initialDiagram.nodes?.forEach(node => model.addNode(node));
initialDiagram.links?.forEach(link => model.addLink(link));

// Configure engine and model as needed
engine.setModel(model);
const links = model.getLinks();
links.forEach(link => {
setupLinkDragEvents(link);
});
}, [initialDiagram.links, initialDiagram.nodes, model]);

const [, drop] = useDrop(() => ({
accept: ItemTypes.NODE,
Expand Down Expand Up @@ -176,6 +167,65 @@ export const FlowCanvasContainer: React.FC<FlowCanvasContainerProps> = ({
},
}));

useEffect(() => {
const canvas = document.querySelector('.flow-canvas');
const handleZoom = (event: Event) =>
engine.increaseZoomLevel(event as WheelEvent);
const disableContextMenu = (event: Event) => event.preventDefault();

canvas?.addEventListener('wheel', handleZoom);
canvas?.addEventListener('contextmenu', disableContextMenu);

return () => {
canvas?.removeEventListener('wheel', handleZoom);
canvas?.removeEventListener('contextmenu', disableContextMenu);
};
}, []);

useEffect(() => {
const handleDrop = (event: DragEvent) => {
event.preventDefault();
// Assuming you have a way to get the currently dragged link
const draggedLink = getCurrentDraggedLink();
if (!draggedLink) {
return;
}

const mousePoint = engine.getRelativeMousePoint(event);
const nodes = engine.getModel().getNodes();
let closestPort = null;
let minDistance = Infinity;

nodes.forEach(node => {
node.getPorts().forEach(port => {
const portPosition = engine.getPortCoords(port);
const distance = Math.hypot(
portPosition.x - mousePoint.x,
portPosition.y - mousePoint.y
);

if (distance < minDistance) {
closestPort = port;
minDistance = distance;
}
});
});

if (closestPort && minDistance < YOUR_DEFINED_THRESHOLD) {
// Check if the port is compatible with the link
if (isPortCompatibleWithLink(closestPort, draggedLink)) {
// Attach the link to the port
draggedLink.setTargetPort(closestPort);
engine.repaintCanvas();
}
}
};

const canvas = document.querySelector('.flow-canvas');
canvas?.addEventListener('drop', handleDrop);
return () => canvas?.removeEventListener('drop', handleDrop);
}, []); // Add dependencies as needed

// The CanvasWidget component is used to render the flow canvas within the UI.
// The "canvas-widget" className can be targeted for custom styling.
return (
Expand Down
47 changes: 47 additions & 0 deletions packages/flow-client/src/app/components/flow-canvas/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
DefaultLinkFactory,
DefaultLinkModel,
DefaultLinkProps,
DefaultLinkWidget,
GenerateWidgetEvent,
} from '@projectstorm/react-diagrams';

export class CustomLinkModel extends DefaultLinkModel {
constructor() {
super({
type: 'custom',
// Additional custom properties
});
}

// Custom methods for handling proximity or compatibility checks
}

export interface CustomLinkProps extends DefaultLinkProps {
link: CustomLinkModel;
// Other props
}

export const CustomLinkWidget: React.FC<CustomLinkProps> = props => {
// Custom rendering logic here
return <DefaultLinkWidget {...props} />;
};

export class CustomLinkFactory extends DefaultLinkFactory {
constructor() {
super('custom'); // This type should match the type in your CustomLinkModel
}

generateModel(): CustomLinkModel {
return new CustomLinkModel();
}

// If you have a custom widget, override generateReactWidget method
generateReactWidget(
event: GenerateWidgetEvent<CustomLinkModel>
): JSX.Element {
return (
<CustomLinkWidget link={event.model} diagramEngine={this.engine} />
);
}
}
8 changes: 8 additions & 0 deletions packages/flow-client/src/app/components/flow-canvas/node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ export class CustomNodeModel extends DefaultNodeModel {
type: 'custom-node',
});
}

// Method to calculate distance from the port to a given point
calculateDistanceToPoint(x: number, y: number): number {
const portPosition = this.getPosition();
return Math.sqrt(
Math.pow(portPosition.x - x, 2) + Math.pow(portPosition.y - y, 2)
);
}
}

// Factory for the custom node, if you're using TypeScript, you might need to extend the appropriate factory class
Expand Down