diff --git a/docs/how-to/define-nodes-edges.md b/docs/how-to/define-nodes-edges.md index 6ab4882..ff329c7 100644 --- a/docs/how-to/define-nodes-edges.md +++ b/docs/how-to/define-nodes-edges.md @@ -155,11 +155,34 @@ edges = [ | `source` | yes | ID of the source node. | | `target` | yes | ID of the target node. | | `label` | no | Text rendered on the edge. | -| `type` | no | Edge type name (for styling / editors). | +| `type` | no | Edge type (see built-in types below, or a custom type name). | | `data` | no | Arbitrary dict of payload data. | | `sourceHandle` | no | Specific output handle on the source node. | | `targetHandle` | no | Specific input handle on the target node. | +### Built-in edge types + +| Type | Description | +|------------------|-------------| +| `"bezier"` | Smooth bezier curve (default). | +| `"straight"` | Straight line between nodes. | +| `"step"` | Orthogonal path with right angles. | +| `"smoothstep"` | Step path with rounded corners. | +| `"smart_bezier"` | Bezier curve that automatically routes around nodes. | +| `"smart_straight"`| Straight segments that automatically route around nodes. | +| `"smart_step"` | Step path that automatically routes around nodes. | + +Smart edge types use pathfinding to avoid overlapping with other nodes in +the graph. They are useful when edges would otherwise pass through +intermediate nodes. + +```python +edges = [ + {"id": "e1", "source": "n1", "target": "n2", "type": "smoothstep"}, + {"id": "e2", "source": "n1", "target": "n3", "type": "smart_bezier"}, +] +``` + --- ## Define edges as classes diff --git a/examples/edge_types_comparison.py b/examples/edge_types_comparison.py new file mode 100644 index 0000000..1cc4f92 --- /dev/null +++ b/examples/edge_types_comparison.py @@ -0,0 +1,102 @@ +""" +Comparison of standard edges vs smart edges. + +This example demonstrates the difference between standard React Flow edges +and smart edges that automatically route around obstacles. +""" + +import panel as pn +from panel_reactflow import EdgeSpec, NodeSpec, ReactFlow + +pn.extension() + +# Create nodes arranged to show edge routing +nodes = [ + # Left column - sources + NodeSpec(id="s1", position={"x": 0, "y": 0}, label="Source 1").to_dict(), + NodeSpec(id="s2", position={"x": 0, "y": 100}, label="Source 2").to_dict(), + NodeSpec(id="s3", position={"x": 0, "y": 200}, label="Source 3").to_dict(), + NodeSpec(id="s4", position={"x": 0, "y": 300}, label="Source 4").to_dict(), + # Middle obstacles + NodeSpec(id="obs1", position={"x": 150, "y": 50}, label="Obstacle 1").to_dict(), + NodeSpec(id="obs2", position={"x": 150, "y": 150}, label="Obstacle 2").to_dict(), + NodeSpec(id="obs3", position={"x": 150, "y": 250}, label="Obstacle 3").to_dict(), + # Right column - targets + NodeSpec(id="t1", position={"x": 300, "y": 0}, label="Target 1").to_dict(), + NodeSpec(id="t2", position={"x": 300, "y": 100}, label="Target 2").to_dict(), + NodeSpec(id="t3", position={"x": 300, "y": 200}, label="Target 3").to_dict(), + NodeSpec(id="t4", position={"x": 300, "y": 300}, label="Target 4").to_dict(), +] + +# Create edges with different types +edges = [ + # Default edge (goes through obstacle) + EdgeSpec( + id="e1", + source="s1", + target="t2", + label="default", + type=None, + style={"stroke": "#999"}, + ).to_dict(), + # Smart bezier (routes around obstacle) + EdgeSpec( + id="e2", + source="s2", + target="t3", + label="smart_bezier", + type="smart_bezier", + style={"stroke": "#3b82f6"}, + ).to_dict(), + # Smart straight (routes around obstacle) + EdgeSpec( + id="e3", + source="s3", + target="t1", + label="smart_straight", + type="smart_straight", + style={"stroke": "#10b981"}, + ).to_dict(), + # Smart step (routes around obstacle) + EdgeSpec( + id="e4", + source="s4", + target="t4", + label="smart_step", + type="smart_step", + style={"stroke": "#f59e0b"}, + ).to_dict(), +] + +# Create the flow +flow = ReactFlow( + nodes=nodes, + edges=edges, + height=600, + width="100%", +) + +# Layout +pn.template.FastListTemplate( + title="Edge Types Comparison", + sidebar=[ + pn.pane.Markdown( + """ + ## Edge Types + + This example compares different edge types: + + - **Gray (default)**: Standard edge that goes straight through obstacles + - **Blue (smart_bezier)**: Curved edge that routes around obstacles + - **Green (smart_straight)**: Straight segments that avoid obstacles + - **Orange (smart_step)**: Step-style edge that avoids obstacles + + ### Try it: + - Drag the obstacle nodes around + - Watch how smart edges automatically reroute + - Notice the default edge doesn't avoid obstacles + """, + ), + ], + main=[flow], +).servable() diff --git a/examples/smart_edges_example.py b/examples/smart_edges_example.py new file mode 100644 index 0000000..0952e48 --- /dev/null +++ b/examples/smart_edges_example.py @@ -0,0 +1,99 @@ +""" +Example demonstrating smart edges that automatically route around nodes. + +Smart edges automatically find paths around nodes to avoid overlaps. +Available edge types: +- 'bezier' (default): Smooth bezier curve +- 'straight': Straight line between nodes +- 'step': Orthogonal step path (right angles) +- 'smoothstep': Step path with rounded corners +- 'smart_bezier': Smart bezier curve that routes around nodes +- 'smart_straight': Smart straight segments that route around nodes +- 'smart_step': Smart step path that routes around nodes +""" + +import panel as pn +import panel_material_ui as pmui +from panel_reactflow import EdgeSpec, NodeSpec, ReactFlow + +pn.extension() + +# Create a simple graph with nodes that would normally cause edge overlaps +nodes = [ + NodeSpec(id="1", position={"x": 0, "y": 100}, label="Start").to_dict(), + NodeSpec(id="2", position={"x": 200, "y": 0}, label="Top").to_dict(), + NodeSpec(id="3", position={"x": 200, "y": 200}, label="Bottom").to_dict(), + NodeSpec(id="4", position={"x": 400, "y": 100}, label="End").to_dict(), + NodeSpec(id="5", position={"x": 200, "y": 100}, label="Middle Obstacle").to_dict(), +] + +# Create edges with different types +edges = [ + EdgeSpec(id="e1", source="1", target="4", label="Regular", type=None).to_dict(), + EdgeSpec(id="e2", source="2", target="3", label="Smart Bezier", type="smart_bezier").to_dict(), + EdgeSpec(id="e3", source="1", target="2", label="Smart Straight", type="smart_straight").to_dict(), + EdgeSpec(id="e4", source="1", target="3", label="Smart Step", type="smart_step").to_dict(), +] + +# Create the flow with smart edges +flow = ReactFlow( + nodes=nodes, + edges=edges, + sizing_mode="stretch_both" +) + +# Create edge type selector +edge_type_selector = pmui.Select( + label="Edge Type for e1", + options={ + "Bezier (default)": "bezier", + "Straight": "straight", + "Step": "step", + "Smooth Step": "smoothstep", + "Smart Bezier": "smart_bezier", + "Smart Straight": "smart_straight", + "Smart Step": "smart_step", + }, + value=None, +) + + +def update_edge_type(event): + """Update the edge type when selector changes.""" + for edge in flow.edges: + edge["type"] = event.new + flow.edges = flow.edges # Trigger update + +edge_type_selector.param.watch(update_edge_type, "value") + +# Layout +pmui.Page( + title="Smart Edges Example", + sidebar=[ + pn.pane.Markdown( + """ + ## Smart Edges Demo + + Smart edges automatically route around nodes to avoid overlaps. + + **Standard Edge Types:** + - **Bezier**: Smooth bezier curve (default) + - **Straight**: Direct straight line + - **Step**: Orthogonal path (right angles) + - **Smooth Step**: Step path with rounded corners + + **Smart Edge Types:** + - **Smart Bezier**: Curved path that avoids nodes + - **Smart Straight**: Straight segments that route around nodes + - **Smart Step**: Step-style path that avoids nodes + + **Try it:** + 1. Change the edge type for the "Start → End" connection + 2. Drag nodes around to see smart edges automatically reroute + 3. Notice how smart edges avoid the "Middle Obstacle" node + """, + ), + edge_type_selector, + ], + main=[flow], +).servable() diff --git a/src/panel_reactflow/base.py b/src/panel_reactflow/base.py index 7ec105c..f28da76 100644 --- a/src/panel_reactflow/base.py +++ b/src/panel_reactflow/base.py @@ -687,7 +687,17 @@ class EdgeSpec: label : str, optional Display label shown on the edge. If ``None``, no label is displayed. type : str, optional - Edge type identifier. Reference a custom type defined in + Edge type identifier. Built-in types are: + + - ``'bezier'`` (default): Smooth bezier curve + - ``'straight'``: Straight line between nodes + - ``'step'``: Orthogonal step path (right angles) + - ``'smoothstep'``: Step path with rounded corners + - ``'smart_bezier'``: Bezier curve that routes around nodes + - ``'smart_straight'``: Straight segments that route around nodes + - ``'smart_step'``: Step path that routes around nodes + + You can also reference a custom type defined in ``ReactFlow.edge_types`` for schema validation and custom rendering. selected : bool, default False Whether the edge is currently selected in the UI. @@ -1411,7 +1421,12 @@ class ReactFlow(ReactComponent): _bundle = DIST_PATH / "panel-reactflow.bundle.js" _esm = Path(__file__).parent / "models" / "reactflow.jsx" - _importmap = {"imports": {"@xyflow/react": "https://esm.sh/@xyflow/react@12.8.3"}} + _importmap = { + "imports": { + "@xyflow/react": "https://esm.sh/@xyflow/react@12.8.3", + "@tisoap/react-flow-smart-edge": "https://esm.sh/@tisoap/react-flow-smart-edge@4.0.3", + } + } _stylesheets = [DIST_PATH / "panel-reactflow.bundle.css", DIST_PATH / "css" / "reactflow.css"] _render_policy = "manual" diff --git a/src/panel_reactflow/models/reactflow.jsx b/src/panel_reactflow/models/reactflow.jsx index dd2bc07..eef77eb 100644 --- a/src/panel_reactflow/models/reactflow.jsx +++ b/src/panel_reactflow/models/reactflow.jsx @@ -1,6 +1,7 @@ import React from "react"; -import { Background, Controls, Handle, MiniMap, NodeToolbar, Panel, Position, ReactFlow, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow, useStore } from "@xyflow/react"; +import { Background, BezierEdge, Controls, Handle, MiniMap, NodeToolbar, Panel, Position, ReactFlow, ReactFlowProvider, SmoothStepEdge, StraightEdge, StepEdge, addEdge, useEdgesState, useNodes, useNodesState, useReactFlow, useStore, BaseEdge, getBezierPath, getSmoothStepPath, getStraightPath } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; +import { getSmartEdge, svgDrawStraightLinePath, pathfindingAStarNoDiagonal } from "@tisoap/react-flow-smart-edge"; const { useCallback, useEffect, useMemo, useRef, useState } = React; @@ -179,6 +180,188 @@ function makeNodeComponent(typeName, typeSpec, editorMode) { }; } +function SmartBezierEdge(props) { + const { + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style = {}, + markerEnd, + label, + } = props; + + const nodes = useNodes(); + + const getSmartEdgeResponse = getSmartEdge({ + sourcePosition, + targetPosition, + sourceX, + sourceY, + targetX, + targetY, + nodes, + }); + + if (getSmartEdgeResponse instanceof Error) { + const [path, labelX, labelY] = getBezierPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }); + return ( + <> + + {label && ( + + {label} + + )} + + ); + } + + const { edgeCenterX, edgeCenterY, svgPathString } = getSmartEdgeResponse; + + return ( + <> + + {label && ( + + {label} + + )} + + ); +} + +function SmartStraightEdge(props) { + const { + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style = {}, + markerEnd, + label, + } = props; + + const nodes = useNodes(); + + const getSmartEdgeResponse = getSmartEdge({ + sourcePosition, + targetPosition, + sourceX, + sourceY, + targetX, + targetY, + nodes, + options: { drawEdge: svgDrawStraightLinePath }, + }); + + if (getSmartEdgeResponse instanceof Error) { + const [path, labelX, labelY] = getStraightPath({ sourceX, sourceY, targetX, targetY }); + return ( + <> + + {label && ( + + {label} + + )} + + ); + } + + const { edgeCenterX, edgeCenterY, svgPathString } = getSmartEdgeResponse; + + return ( + <> + + {label && ( + + {label} + + )} + + ); +} + +function SmartStepEdge(props) { + const { + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style = {}, + markerEnd, + label, + } = props; + + const nodes = useNodes(); + + const getSmartEdgeResponse = getSmartEdge({ + sourcePosition, + targetPosition, + sourceX, + sourceY, + targetX, + targetY, + nodes, + options: { drawEdge: svgDrawStraightLinePath, generatePath: pathfindingAStarNoDiagonal }, + }); + + if (getSmartEdgeResponse instanceof Error) { + const [path, labelX, labelY] = getSmoothStepPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }); + return ( + <> + + {label && ( + + {label} + + )} + + ); + } + + const { edgeCenterX, edgeCenterY, svgPathString } = getSmartEdgeResponse; + + return ( + <> + + {label && ( + + {label} + + )} + + ); +} + function useDebouncedSync(syncMode, debounceMs, syncFn) { const timeoutRef = useRef(null); @@ -227,6 +410,7 @@ function FlowInner({ onPaneClick, defaultEdgeOptions, nodeTypes, + edgeTypes, nodeEditors, edgeEditors, colorMode, @@ -470,6 +654,7 @@ function FlowInner({ nodes={nodes} edges={edges} nodeTypes={nodeTypes} + edgeTypes={edgeTypes} defaultEdgeOptions={defaultEdgeOptions} colorMode={colorMode} onNodesChange={handleNodesChange} @@ -656,6 +841,16 @@ export function render({ model, view }) { return mapping; }, [editorMode, pyNodeTypes]); + const hydratedEdgeTypes = useMemo(() => ({ + bezier: BezierEdge, + straight: StraightEdge, + step: StepEdge, + smoothstep: SmoothStepEdge, + smart_bezier: SmartBezierEdge, + smart_straight: SmartStraightEdge, + smart_step: SmartStepEdge, + }), []); + return (
@@ -672,6 +867,7 @@ export function render({ model, view }) { defaultEdgeOptions={defaultEdgeOptions} colorMode={colorMode} nodeTypes={hydratedNodeTypes} + edgeTypes={hydratedEdgeTypes} nodeEditors={nodeEditors} edgeEditors={edgeEditors} editable={editable}