@@ -178,7 +178,13 @@ def update_position(self) -> None:
178178
179179 Recalculates the cubic Bezier curve path between nodes, including
180180 offset for multiple connections, arrow head position, and label placement.
181+ For self-loops, draws a loop arc above the node.
181182 """
183+ # Check if this is a self-loop
184+ if self .from_node == self .to_node :
185+ self ._update_self_loop_position ()
186+ return
187+
182188 from_center : QPointF = self .from_node .get_connection_point ()
183189 to_center : QPointF = self .to_node .get_connection_point ()
184190 from_pos : QPointF = self .from_node .get_edge_point (to_center )
@@ -238,3 +244,93 @@ def update_position(self) -> None:
238244 mid_point .x () - label_rect .width () / 2 ,
239245 mid_point .y () - label_rect .height () / 2 ,
240246 )
247+
248+ def _update_self_loop_position (self ) -> None :
249+ """Update position for a self-loop (transition from a state to itself).
250+
251+ Draws a loop arc above the node with an arrow pointing back into the node.
252+ Multiple self-loops on the same node are offset to avoid overlap.
253+ """
254+ node = self .from_node
255+ center : QPointF = node .get_connection_point ()
256+
257+ # Calculate offset for multiple self-loops on the same node
258+ self_loops = [
259+ conn
260+ for conn in node .connections
261+ if conn .from_node == conn .to_node and conn .from_node == node
262+ ]
263+ self_loops .sort (key = lambda c : c .outcome )
264+
265+ try :
266+ loop_index : int = self_loops .index (self )
267+ except ValueError :
268+ loop_index = 0
269+
270+ num_loops : int = len (self_loops )
271+
272+ # Base loop parameters
273+ loop_height : float = 80 # How far the loop extends above the node
274+ loop_width : float = 60 # Width of the loop
275+
276+ # Offset multiple loops horizontally
277+ horizontal_spacing : float = 50
278+ if num_loops > 1 :
279+ center_offset = (num_loops - 1 ) / 2
280+ horizontal_offset = (loop_index - center_offset ) * horizontal_spacing
281+ else :
282+ horizontal_offset = 0
283+
284+ # Calculate start and end points on the top edge of the node
285+ # Start point is slightly to the left, end point slightly to the right
286+ start_x = center .x () + horizontal_offset - 20
287+ end_x = center .x () + horizontal_offset + 20
288+
289+ # Get the top of the node (approximate)
290+ node_top = center .y () - 40 # Approximate top of node
291+
292+ start_pos = QPointF (start_x , node_top )
293+ end_pos = QPointF (end_x , node_top )
294+
295+ # Control points for the loop curve (above the node)
296+ ctrl1 = QPointF (start_x - loop_width / 2 , node_top - loop_height )
297+ ctrl2 = QPointF (end_x + loop_width / 2 , node_top - loop_height )
298+
299+ # Create the path
300+ path : QPainterPath = QPainterPath ()
301+ path .moveTo (start_pos )
302+ path .cubicTo (ctrl1 , ctrl2 , end_pos )
303+ self .setPath (path )
304+
305+ # Arrow head pointing down into the node
306+ arrow_size : float = 12
307+ # Arrow points downward
308+ angle = math .pi / 2 # 90 degrees (pointing down)
309+
310+ arrow_p1 : QPointF = end_pos - QPointF (
311+ math .cos (angle - math .pi / 6 ) * arrow_size ,
312+ math .sin (angle - math .pi / 6 ) * arrow_size ,
313+ )
314+ arrow_p2 : QPointF = end_pos - QPointF (
315+ math .cos (angle + math .pi / 6 ) * arrow_size ,
316+ math .sin (angle + math .pi / 6 ) * arrow_size ,
317+ )
318+
319+ arrow_polygon : QPolygonF = QPolygonF ([end_pos , arrow_p1 , arrow_p2 ])
320+ self .arrow_head .setPolygon (arrow_polygon )
321+
322+ # Position label at the top of the loop
323+ mid_point = QPointF (center .x () + horizontal_offset , node_top - loop_height + 10 )
324+ label_rect = self .label .boundingRect ()
325+ padding : float = 4
326+
327+ self .label_bg .setRect (
328+ mid_point .x () - label_rect .width () / 2 - padding ,
329+ mid_point .y () - label_rect .height () / 2 - padding ,
330+ label_rect .width () + padding * 2 ,
331+ label_rect .height () + padding * 2 ,
332+ )
333+ self .label .setPos (
334+ mid_point .x () - label_rect .width () / 2 ,
335+ mid_point .y () - label_rect .height () / 2 ,
336+ )
0 commit comments