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) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-25 12:58:58 +02:00
parent f2378216ee
commit 0c129a5f8d
2 changed files with 37 additions and 9 deletions

View File

@ -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) {

View File

@ -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<HFSchemes, AreaExtra>, 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.