From 0c129a5f8d809bf97fe13110d3c42bb764d55df0 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 25 Sep 2026 12:58:58 +0200 Subject: [PATCH] Draw a loop's way back above its nodes, clear of how tall they grow Below the ports, the way back ran through any node taller than a fixed drop - most of them, once expanded - and its limit label sat hidden behind the node. It now runs above the tops of the nodes at both ends, which move only when a node is dragged, and follows them when one is. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../custom-connection/custom-connection.ts | 17 ++++++----- src/app/utilities/rete-editor.ts | 29 ++++++++++++++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/app/shared/custom-connection/custom-connection.ts b/src/app/shared/custom-connection/custom-connection.ts index 844c3f2..134164b 100644 --- a/src/app/shared/custom-connection/custom-connection.ts +++ b/src/app/shared/custom-connection/custom-connection.ts @@ -198,20 +198,21 @@ export class CustomConnectionComponent { /** * A connection leading back runs from a node's output to one drawn before it, so the usual curve - * would double back through both nodes. It goes round underneath them instead. + * would double back through both nodes. It goes round above them instead. */ get drawnPath(): string { if (!this.isLoopBack || !this.start || !this.end) return this.path; - const drop = this.loopDrop; const { x: sx, y: sy } = this.start; const { x: ex, y: ey } = this.end; - const bottom = Math.max(sy, ey) + drop; - return `M ${sx} ${sy} C ${sx + 140} ${sy}, ${sx + 140} ${bottom}, ${sx} ${bottom}` - + ` L ${ex} ${bottom} C ${ex - 140} ${bottom}, ${ex - 140} ${ey}, ${ex} ${ey}`; + const top = this.loopTop; + return `M ${sx} ${sy} C ${sx + 140} ${sy}, ${sx + 140} ${top}, ${sx} ${top}` + + ` L ${ex} ${top} C ${ex - 140} ${top}, ${ex - 140} ${ey}, ${ex} ${ey}`; } - private get loopDrop(): number { - return 150; + private get loopTop(): number { + const ports = Math.min(this.start?.y ?? 0, this.end?.y ?? 0) - 90; + const nodes = this.loopData?.__loopTop; + return typeof nodes === 'number' ? Math.min(nodes, ports) : ports; } get loopLabelX(): number { @@ -219,7 +220,7 @@ export class CustomConnectionComponent { } get loopLabelY(): number { - return Math.max(this.start?.y ?? 0, this.end?.y ?? 0) + this.loopDrop - 17; + return this.loopTop - 17; } changeLimit(event: Event) { diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index 6879d52..5d0c614 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -77,6 +77,8 @@ export type LoopAwareConnection = HFSchemes['Connection'] & { loop?: FlowLoopEdgeSettings; __loopBack?: boolean; __readonly?: boolean; + /** Canvas y above both ends' nodes, where the way back is drawn so it crosses neither. */ + __loopTop?: number; }; export async function createEditor( @@ -175,6 +177,18 @@ export async function createEditor( return context; }); + area.addPipe((context: any) => { + if (context?.type === 'nodetranslated') { + const nodeId = String(context.data?.id ?? ''); + for (const connection of editor.getConnections() as LoopAwareConnection[]) { + if (!connection.__loopBack || (connection.source !== nodeId && connection.target !== nodeId)) continue; + connection.__loopTop = loopTop(area, connection); + void area.update('connection', connection.id); + } + } + return context; + }); + connection.addPreset(ConnectionPresets.classic.setup()); AreaExtensions.simpleNodesOrder(area); @@ -765,13 +779,26 @@ export async function refreshLoopMarkers( for (const connection of data) { const loopBack = backEdges.has(connection.id); const readonly = resolvedRuntime?.readonly === true; - if (connection.__loopBack === loopBack && connection.__readonly === readonly) continue; + const top = loopBack ? loopTop(area, connection) : undefined; + if (connection.__loopBack === loopBack && connection.__readonly === readonly && connection.__loopTop === top) continue; connection.__loopBack = loopBack; connection.__readonly = readonly; + connection.__loopTop = top; await area.update('connection', connection.id); } } +/** + * Above the tops of the nodes at both ends. Their tops, not their bottoms: a node's height changes + * as it is expanded or edited, its position only when it is moved. + */ +function loopTop(area: AreaPlugin, connection: LoopAwareConnection): number | undefined { + const tops = [connection.source, connection.target] + .map((id) => area.nodeViews.get(id)?.position.y) + .filter((y): y is number => typeof y === 'number'); + return tops.length ? Math.min(...tops) - 48 : undefined; +} + /** * Sets a loop connection's iteration limit; null goes back to the default. The settings stay on the * connection either way, so it remains the one its author marked as leading back.