mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
Adds full support for directed, bidirectional, and undirected relationships between actors with visual arrow markers and intuitive controls. **Type System:** - Add EdgeDirectionality type (directed | bidirectional | undirected) - Add directionality field to RelationData - Add defaultDirectionality field to EdgeTypeConfig **Visual Representation:** - Directed edges: single arrow marker at target (→) - Bidirectional edges: arrow markers at both ends (↔) - Undirected edges: no arrow markers (—) - Separate marker definitions for start/end to ensure correct orientation **Property Panel Controls:** - MUI ToggleButtonGroup for selecting directionality - Visual connection indicator with directional symbols - Reverse direction button (swaps source/target, only for directed edges) - Live updates with 500ms debounce **Edge Type Configuration:** - Default directionality selector in edge type form - Dropdown with helpful descriptions (→, ↔, —) - Applied to both create and edit workflows **Edge Creation:** - New edges inherit defaultDirectionality from edge type config - Falls back to 'directed' for backwards compatibility **Reverse Direction:** - Swaps source/target and sourceHandle/targetHandle - Maintains edge ID and selection state - Tracked in undo/redo history Includes comprehensive UX specification document with wireframes, interaction patterns, accessibility guidelines, and implementation phases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
141 lines
3.7 KiB
TypeScript
141 lines
3.7 KiB
TypeScript
import { memo } from 'react';
|
|
import {
|
|
EdgeProps,
|
|
getBezierPath,
|
|
EdgeLabelRenderer,
|
|
BaseEdge,
|
|
} from 'reactflow';
|
|
import { useGraphStore } from '../../stores/graphStore';
|
|
import type { RelationData } from '../../types';
|
|
|
|
/**
|
|
* CustomEdge - Represents a relation between actors in the constellation graph
|
|
*
|
|
* Features:
|
|
* - Bezier curve path
|
|
* - Type-based coloring and styling
|
|
* - Optional label display
|
|
* - Edge type badge
|
|
* - Directional arrow markers (directed, bidirectional, undirected)
|
|
*
|
|
* Usage: Automatically rendered by React Flow for edges with type='custom'
|
|
*/
|
|
const CustomEdge = ({
|
|
id,
|
|
sourceX,
|
|
sourceY,
|
|
targetX,
|
|
targetY,
|
|
sourcePosition,
|
|
targetPosition,
|
|
data,
|
|
selected,
|
|
}: EdgeProps<RelationData>) => {
|
|
const edgeTypes = useGraphStore((state) => state.edgeTypes);
|
|
|
|
// Calculate the bezier path
|
|
const [edgePath, labelX, labelY] = getBezierPath({
|
|
sourceX,
|
|
sourceY,
|
|
sourcePosition,
|
|
targetX,
|
|
targetY,
|
|
targetPosition,
|
|
});
|
|
|
|
// Find the edge type configuration
|
|
const edgeTypeConfig = edgeTypes.find((et) => et.id === data?.type);
|
|
const edgeColor = edgeTypeConfig?.color || '#6b7280';
|
|
const edgeStyle = edgeTypeConfig?.style || 'solid';
|
|
|
|
// Use custom label if provided, otherwise use type's default label
|
|
const displayLabel = data?.label || edgeTypeConfig?.label;
|
|
|
|
// Convert style to stroke-dasharray
|
|
const strokeDasharray = {
|
|
solid: '0',
|
|
dashed: '5,5',
|
|
dotted: '1,5',
|
|
}[edgeStyle];
|
|
|
|
// Get directionality (default to 'directed' for backwards compatibility)
|
|
const directionality = data?.directionality || edgeTypeConfig?.defaultDirectionality || 'directed';
|
|
|
|
// Create unique marker IDs based on color (for reusability)
|
|
const safeColor = edgeColor.replace('#', '');
|
|
const markerEndId = `arrow-end-${safeColor}`;
|
|
const markerStartId = `arrow-start-${safeColor}`;
|
|
|
|
// Determine marker start/end based on directionality
|
|
const markerEnd = (directionality === 'directed' || directionality === 'bidirectional')
|
|
? `url(#${markerEndId})`
|
|
: undefined;
|
|
const markerStart = directionality === 'bidirectional'
|
|
? `url(#${markerStartId})`
|
|
: undefined;
|
|
|
|
return (
|
|
<>
|
|
{/* Arrow marker definitions */}
|
|
<defs>
|
|
{/* Arrow pointing right (for marker-end) */}
|
|
<marker
|
|
id={markerEndId}
|
|
viewBox="0 0 10 10"
|
|
refX="8"
|
|
refY="5"
|
|
markerWidth="8"
|
|
markerHeight="8"
|
|
orient="auto"
|
|
fill={edgeColor}
|
|
>
|
|
<path d="M 0 0 L 10 5 L 0 10 z" />
|
|
</marker>
|
|
|
|
{/* Arrow pointing left (for marker-start) */}
|
|
<marker
|
|
id={markerStartId}
|
|
viewBox="0 0 10 10"
|
|
refX="2"
|
|
refY="5"
|
|
markerWidth="8"
|
|
markerHeight="8"
|
|
orient="auto"
|
|
fill={edgeColor}
|
|
>
|
|
<path d="M 10 0 L 0 5 L 10 10 z" />
|
|
</marker>
|
|
</defs>
|
|
|
|
<BaseEdge
|
|
id={id}
|
|
path={edgePath}
|
|
style={{
|
|
stroke: edgeColor,
|
|
strokeWidth: selected ? 3 : 2,
|
|
strokeDasharray,
|
|
}}
|
|
markerEnd={markerEnd}
|
|
markerStart={markerStart}
|
|
/>
|
|
|
|
{/* Edge label - show custom or type default */}
|
|
{displayLabel && (
|
|
<EdgeLabelRenderer>
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
|
pointerEvents: 'all',
|
|
}}
|
|
className="bg-white px-2 py-1 rounded border border-gray-300 text-xs font-medium shadow-sm"
|
|
>
|
|
<div style={{ color: edgeColor }}>{displayLabel}</div>
|
|
</div>
|
|
</EdgeLabelRenderer>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default memo(CustomEdge);
|