Skip to content

Commit b70bdb6

Browse files
Merge pull request #226 from vitruv-tools/UML-issue
Refactor UML relationship rendering and merge point logic
2 parents d7eb524 + d4ebc6f commit b70bdb6

File tree

2 files changed

+68
-28
lines changed

2 files changed

+68
-28
lines changed

src/components/flow/FlowCanvas.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1841,56 +1841,77 @@ execute actions in ${targetPackageName}
18411841
const calculateMergePoint = useCallback((avgSource: { x: number; y: number }, targetNode: Node) => {
18421842
const targetCenterX = targetNode.position.x + NODE_DIMENSIONS.width / 2;
18431843
const targetCenterY = targetNode.position.y + NODE_DIMENSIONS.height / 2;
1844+
1845+
// Place merge point vertically under/above the target center so
1846+
// the last segment into the class box is a straight line into
1847+
// the middle (no diagonal hit on the left/right side).
18441848
return {
1845-
x: avgSource.x + (targetCenterX - avgSource.x) * 0.4,
1846-
y: avgSource.y + (targetCenterY - avgSource.y) * 0.4
1849+
x: targetCenterX,
1850+
y: avgSource.y + (targetCenterY - avgSource.y) * 0.4,
18471851
};
18481852
}, []);
18491853

1850-
// Calculate merge points for UML edges with same target
1854+
// Calculate merge points for UML inheritance edges with same target.
1855+
// We keep merge visualization ONLY for inheritance (like multiple
1856+
// subclasses pointing to the same superclass), not for other UML
1857+
// relationships. This gives a clean "fan-in" into the superclass
1858+
// while keeping compositions and associations as simple lines.
18511859
const umlMergeData = useMemo(() => {
18521860
const mergePointsMap = new Map<string, { x: number; y: number; mergeGroupId: string }>();
18531861
const firstInGroupMap = new Map<string, string>();
18541862
const mergeGroupSourceNodesMap = new Map<string, string[]>();
18551863

1856-
// Count UML edges per source node
1864+
// Consider only UML inheritance edges for merging
1865+
const umlInheritanceEdges = uniqueEdges.filter(
1866+
(e) => e.type === 'uml' && (e.data as any)?.relationshipType === 'inheritance'
1867+
);
1868+
1869+
if (umlInheritanceEdges.length === 0) {
1870+
return { mergePointsMap, firstInGroupMap, mergeGroupSourceNodesMap };
1871+
}
1872+
1873+
// Count inheritance edges per source node
18571874
const edgesPerSource = new Map<string, number>();
1858-
uniqueEdges.filter(e => e.type === 'uml').forEach(edge => {
1875+
umlInheritanceEdges.forEach((edge) => {
18591876
edgesPerSource.set(edge.source, (edgesPerSource.get(edge.source) || 0) + 1);
18601877
});
18611878

1862-
// Group UML edges by target
1879+
// Group inheritance edges by target (superclass)
18631880
const edgesByTarget = new Map<string, Edge[]>();
1864-
uniqueEdges.filter(e => e.type === 'uml').forEach(edge => {
1881+
umlInheritanceEdges.forEach((edge) => {
18651882
const existing = edgesByTarget.get(edge.target) || [];
18661883
existing.push(edge);
18671884
edgesByTarget.set(edge.target, existing);
18681885
});
18691886

1870-
// Process each target group
1887+
// For each superclass, create one merge point for eligible subclasses
18711888
edgesByTarget.forEach((edgesGroup, targetId) => {
18721889
if (edgesGroup.length < 2) return;
18731890

1874-
const eligibleEdges = edgesGroup.filter(edge => (edgesPerSource.get(edge.source) || 0) === 1);
1891+
// Only merge subclasses that connect to this superclass once
1892+
const eligibleEdges = edgesGroup.filter(
1893+
(edge) => (edgesPerSource.get(edge.source) || 0) === 1
1894+
);
18751895
if (eligibleEdges.length < 2) return;
18761896

18771897
eligibleEdges.sort((a, b) => a.source.localeCompare(b.source));
18781898

1879-
const targetNode = nodes.find(n => n.id === targetId);
1899+
const targetNode = nodes.find((n) => n.id === targetId);
18801900
if (!targetNode) return;
18811901

18821902
const avgSourcePos = calculateAverageSourcePosition(eligibleEdges);
18831903
const mergePoint = calculateMergePoint(avgSourcePos, targetNode);
18841904
const mergeGroupId = `merge-${targetId}`;
18851905

1886-
mergeGroupSourceNodesMap.set(mergeGroupId, eligibleEdges.map(e => e.source));
1887-
eligibleEdges.forEach((edge, index) => {
1906+
mergeGroupSourceNodesMap.set(
1907+
mergeGroupId,
1908+
eligibleEdges.map((e) => e.source)
1909+
);
1910+
eligibleEdges.forEach((edge) => {
18881911
mergePointsMap.set(edge.id, { ...mergePoint, mergeGroupId });
1889-
console.log(` Edge ${index}: ${edge.id.slice(-8)} (source: ${edge.source.slice(-6)})`);
18901912
});
18911913

18921914
firstInGroupMap.set(mergeGroupId, eligibleEdges[0].id);
1893-
console.log(`✨ Merge group ${mergeGroupId}: First edge = ${eligibleEdges[0].id.slice(-8)}`);
18941915
});
18951916

18961917
return { mergePointsMap, firstInGroupMap, mergeGroupSourceNodesMap };

src/components/flow/UMLRelationship.tsx

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -314,22 +314,21 @@ export function UMLRelationship({
314314
const isMergeGroupHovered = !!(mergeGroupId && data?.hoveredMergeGroup === mergeGroupId);
315315

316316
// Logic for highlighting:
317-
// 1. Individual edge (source to merge point) is red if:
318-
// - edge selected OR source node selected OR target node selected OR merge group hovered
319-
const isIndividualEdgeHighlighted: boolean = !!(selected || isSourceSelected || isTargetSelected || isMergeGroupHovered);
317+
// 1. Individual edge (source to merge point) is red ONLY if:
318+
// - this edge is selected OR this edge is hovered.
319+
// Node selection alone no longer turns every incident edge red.
320+
const isIndividualEdgeHighlighted: boolean = !!(selected || isHovered);
320321

321-
// 2. MERGE DOT LOGIC: Merge dot is red if ANY node that has a connection through it is selected
322-
// OR if the merge group is being hovered
323-
// This includes: current source, target, any other source node in the merge group, OR hover
324-
const isMergeDotRed: boolean = data?.hasMerge
325-
? !!(isSourceSelected || isTargetSelected || isMergeGroupNodeSelected || isMergeGroupHovered)
322+
// 2. MERGE DOT LOGIC: Merge dot is red when any edge in the merge group
323+
// is highlighted (selected/hovered) OR when the merge group itself is hovered.
324+
const isMergeDotRed: boolean = data?.hasMerge
325+
? !!(isIndividualEdgeHighlighted || isMergeGroupHovered)
326326
: isIndividualEdgeHighlighted;
327327

328-
// 3. SHARED SEGMENT LOGIC: Shared segment is red if merge dot is red
329-
// When merge dot is red, everything after it (to target) must be red
328+
// 3. SHARED SEGMENT LOGIC: Shared segment is red only when merge dot is red.
330329
const isSharedSegmentHighlighted: boolean = isMergeDotRed;
331330

332-
// For the main edge path (source to merge), use individual edge highlighting
331+
// For the main edge path (source to merge), use individual edge highlighting.
333332
const isHighlighted: boolean = isIndividualEdgeHighlighted;
334333

335334
// Debug logging for merged edges
@@ -551,8 +550,16 @@ export function UMLRelationship({
551550
...getRelationshipStyle(),
552551
strokeLinecap: 'butt',
553552
strokeLinejoin: 'miter',
554-
// Remove markers from individual branches when merged
555-
...(hasMerge && { markerEnd: 'none', markerStart: 'none' })
553+
// For merged edges we suppress markers only for relationships
554+
// that use arrowheads at the target side (inheritance/realization/dependency).
555+
// For compositions/aggregations we KEEP the diamond on the owning
556+
// side (source class) to preserve correct UML semantics.
557+
...(hasMerge &&
558+
data?.relationshipType !== 'composition' &&
559+
data?.relationshipType !== 'aggregation' && {
560+
markerEnd: 'none',
561+
markerStart: 'none',
562+
})
556563
}}
557564
d={edgePath}
558565
onClick={handleClick}
@@ -600,7 +607,19 @@ export function UMLRelationship({
600607
id={`${id}-shared`}
601608
d={sharedSegmentPath}
602609
style={{
603-
...getRelationshipStyle(isSharedSegmentHighlighted),
610+
// On the shared segment we never want the composition/
611+
// aggregation diamond to appear at the merge point – it
612+
// must stay attached to the owning class at the start of
613+
// each branch. Therefore we explicitly clear markerStart
614+
// for these relationship types while still allowing
615+
// arrowheads for inheritance/realization/dependency.
616+
...(data?.relationshipType === 'composition' ||
617+
data?.relationshipType === 'aggregation'
618+
? {
619+
...getRelationshipStyle(isSharedSegmentHighlighted),
620+
markerStart: 'none',
621+
}
622+
: getRelationshipStyle(isSharedSegmentHighlighted)),
604623
strokeLinecap: 'butt',
605624
strokeLinejoin: 'miter',
606625
}}

0 commit comments

Comments
 (0)