Jump Over Link Template property loading times

We are trying to load some complex logic circuits schematics using gojs. For clarity in the visual representation of this diagrams, we wanted to use the JumpOver property for the LinkTemplate:

import * as go from "gojs";
import CustomLink from "./CustomLinkTemplate"

const $ = go.GraphObject.make;
const LinkTemplate = $(
    go.Link,
    {
        // Configuring behavior and properties
        routing: go.Routing.AvoidsNodes,
        curve: go.Curve.JumpOver,
        corner: 0,
        relinkableFrom: true,
        relinkableTo: true,
        resegmentable: true,
        reshapable: true,
        shadowOffset: new go.Point(0, 0),
        shadowBlur: 5,
    },
    new go.Binding("points").makeTwoWay(),
    new go.Binding("isShadowed", "isSelected").ofObject(), // Configuring behavior when link is selected
    $(go.Shape, { name: "SHAPE", strokeWidth: 2, stroke: "#000000" }) // Configuring Link shape
);

export default LinkTemplate;

The problem we are having is that if we use this type of curve property, the loading time for these complex schematics adds an overhead of 10 seconds, freezing up the page in the middle of the processing.

When we just use:

curve: go.Routing.Orthogonal,

The schematic loads under 3 seconds.

We tried different things but we have come to the conclusion that just changing that property changes the loading times.

This is just a fraction of the whole schematic:

Yes, it’s expensive to compute all of those intersections and figure out how to draw those JumpOvers.

I assume there’s an implicit question about how to improve the behavior. One thing you could try is to set Link.curve incrementally. I’ll see if I can come up with an example.

This is just for asynchronously computing JumpOver or JumpGap curves for Links. I adapted this from code that asynchronously computes AvoidsNodes routing.

<!DOCTYPE html>
<html>
<head>
  <title>Asynchronous JumpOver Curves</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
  <meta name="description"
    content="Asynchronously compute and render JumpOver curves, to speed up initial rendering of large diagrams">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:900px"></div>
  <button onclick="test()">Generate new model</button>
  <span id="myInfo"></span>

  <script src="https://cdn.jsdelivr.net/npm/gojs"></script>
  <script id="code">
// Support for asychronously implementing JumpOver or JumpGap curve

// To set Link.curve = go.Curve.JumpOver (or JumpGap) after "InitialLayoutCompleted":
//   1: set Link.curve = go.Curve.None in your link templates
//   2: create an instance of this class for your diagram:
//       const diag = ...;
//       const delay = new DelayLinkCurve(diag, go.Curve.JumpOver /* or JumpGap */);

// If you no longer need a DelayLinkCurve, set its .diagram = null.
// This will remove the listeners that it had registered on the Diagram.

class DelayLinkCurve {
  constructor(diagram, curve) {
    if (!curve) curve = go.Curve.JumpOver;
    this._diagram = null;
    this._curve = curve;
    this._batch = 30;

    this._AllOrthos = new go.Set();  // all links whose path geometry needs to be updated
    this._ilc = this.startJumping.bind(this);
    this._jm = this.jumpMore.bind(this);

    // now set up listeners
    if (diagram) this.diagram = diagram;
  }

  // default value: null
  get diagram() { return this._diagram; }
  set diagram(val) {
    const old = this._diagram;
    if (val !== old) {
      if (old) {
        old.removeDiagramListener("InitialLayoutCompleted", this._ilc);
        this._AllOrthos.clear();
      }
      this._diagram = val;
      if (val) {
        val.addDiagramListener("InitialLayoutCompleted", this._ilc);
      }
    }
  }

  // default value: go.Curve.JumpOver
  get curve() { return this._curve; }
  set curve(val) { this._curve = val; }

  // default value: 30
  get batch() { return this._batch; }
  set batch(val) { this._batch = val; }

  startJumping() {
    const delay = this;
    const diag = delay.diagram;
    if (!diag) return;
    let remaining = 0;
    if (delay.curve === go.Curve.JumpGap || delay.curve === go.Curve.JumpOver) {
      delay._AllOrthos.clear();
      diag.links.each(l => {
        if (l.curve === go.Curve.None && l.isOrthogonal) delay._AllOrthos.add(l);
      });
      remaining = delay._AllOrthos.count;
      if (remaining > 0) setTimeout(delay._jm, 10);
    }
    delay.onUpdate(remaining);
  }

  jumpMore() {
    const delay = this;
    const diag = delay.diagram;
    if (!diag) return;
    if (diag.currentTool !== diag.defaultTool) {
      setTimeout(delay._jm, 1000);  // process events for tools
    } else {
      // jump one or more links, if any need it
      if (delay._AllOrthos.count > 0) {
        diag.commit(diag => {
          for (let i = 0; i < delay._batch; i++) {  // could be smarter and do more in each batch if they are quick enough
            const link = delay._AllOrthos.first();  // otherwise pick another link to route
            if (!link) break;  // all done?
            link.curve = delay.curve;
            delay._AllOrthos.remove(link);
          }
        }, null);  // skipsUndoManager
      }
      // if there are any remaining, do them later
      let remaining = delay._AllOrthos.count;
      if (remaining > 0) {
        setTimeout(delay._jm, 10);
      }
      delay.onUpdate(remaining);
    }
  }

  onUpdate(numremaining) { }
}
// end of DelayLinkCurve class

myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      layout: new go.TreeLayout(),
      "InitialLayoutCompleted": e => {
        // scroll to where there are some nodes
        e.diagram.select(e.diagram.nodes.first());
        e.diagram.commandHandler.scrollToPart(e.diagram.nodes.first());
      },
      "animationManager.isEnabled": false
    });

// using DelayLinkCurve:
const delay = new DelayLinkCurve(myDiagram /*, go.Curve.JumpGap*/);  // default is JumpOver
delay.onUpdate = numremaining => {
  // FOR DEBUGGING in this sample only: just to show how many links still need to be processed
  document.getElementById("myInfo").textContent = numremaining;
  if (numremaining <= 0) {
    // change link template(s) to be normal
    myDiagram.linkTemplate.curve = go.Curve.JumpOver;
  }
};

// the node template doesn't really matter for this sample
myDiagram.nodeTemplate =
  new go.Node("Auto")
    .add(
      new go.Shape({ fill: "white" })
        .bind("fill"),
      new go.TextBlock({ margin: new go.Margin(18, 28) })
        .bind("text")
    );

myDiagram.linkTemplate =
  new go.Link({
      routing: go.Routing.Orthogonal, corner: 10,
      curve: go.Curve.None,  // NOTE: Curve.None, not Curve.JumpOver or JumpGap
      mouseEnter: (e, link) => { link.path.stroke = "red"; link.path.strokeWidth = 3; },
      mouseLeave: (e, link) => { link.path.stroke = "black"; link.path.strokeWidth = 1.5; }
    })
    .add(
      new go.Shape({ strokeWidth: 1.5 })
    );

function test() {
  generate(2500, 2, 6);
}

// create a model that is overall somewhat tree-structured but includes many non-tree-structured links
function generate(numNodes, minChil, maxChil) {
  const nodeArray = [];
  const linkArray = [];
  for (let i = 0; i < numNodes; i++) {
    nodeArray.push({
      key: i,  // the unique identifier
      text: i.toString(),  // some text to be shown by the node template
      fill: go.Brush.randomColor()  // a color to be shown by the node template
    });
  }

  // Randomize the node data
  for (i = 0; i < nodeArray.length; i++) {
    const swap = Math.floor(Math.random() * nodeArray.length);
    const temp = nodeArray[swap];
    nodeArray[swap] = nodeArray[i];
    nodeArray[i] = temp;
  }

  // Takes the random collection of node data and creates a random tree with them.
  // Respects the minimum and maximum number of links from each node.
  // The minimum can be disregarded if we run out of nodes to link to.
  if (nodeArray.length > 1) {
    // keep the Set of node data that do not yet have a parent
    const available = new go.Set();
    available.addAll(nodeArray);
    for (let i = 0; i < nodeArray.length; i++) {
      const parent = nodeArray[i];
      available.remove(parent);

      // assign some number of node data as children of this parent node data
      const children = Math.floor(Math.random() * (maxChil - minChil + 1)) + minChil;
      for (let j = 0; j < children; j++) {
        const child = available.first();
        if (child === null) break;  // oops, ran out already
        available.remove(child);
        linkArray.push({ from: parent.key, to: child.key });
      }
      if (available.count === 0) break;  // nothing left?
    }

    // add some extra non-tree links
    for (let i = 0; i < numNodes / 2; i++) {
      linkArray.push({
        from: nodeArray[Math.floor(Math.random() * numNodes)].key,
        to: nodeArray[Math.floor(Math.random() * numNodes)].key
      });
    }
  }

  myDiagram.model = new go.GraphLinksModel(nodeArray, linkArray);
}

// set up initial model
test();
  </script>
</body>
</html>

Thanks so much for the reply!
Today I have tried using the DelayLinkCurve and it actually did work but it seems that the computation of the curves makes the whole diagram freeze up for at least 30 seconds. I will provide you with my Diagram code to see if there is something I am missing or just messing up:

async function createDiagram(diagramContainer) {
    interlockDiagram = new go.Diagram(diagramContainer, {
        // Diagram initialization options
        "draggingTool.isGridSnapEnabled": true,
        "commandHandler.archetypeGroupData": {
            text: "Group",
            isGroup: true,
            color: "blue",
        },
        "undoManager.isEnabled": true,
        "draggingTool.dragsLink": true,
        "draggingTool.isGridSnapEnabled": true,
        "linkingTool.isUnconnectedLinkValid": true,
        "linkingTool.portGravity": 20,
        "relinkingTool.isUnconnectedLinkValid": true,
        "relinkingTool.portGravity": 20,
        "relinkingTool.fromHandleArchetype": $(go.Shape, "Diamond", {
            segmentIndex: 0,
            cursor: "pointer",
            desiredSize: new go.Size(8, 8),
            fill: "tomato",
            stroke: "darkred",
        }),
        "relinkingTool.toHandleArchetype": $(go.Shape, "Diamond", {
            segmentIndex: -1,
            cursor: "pointer",
            desiredSize: new go.Size(8, 8),
            fill: "darkred",
            stroke: "tomato",
        }),
        "linkReshapingTool.handleArchetype": $(go.Shape, "Diamond", {
            desiredSize: new go.Size(7, 7),
            fill: "lightblue",
            stroke: "deepskyblue",
        }),
        "rotatingTool.handleAngle": 270,
        "rotatingTool.handleDistance": 30,
        "rotatingTool.snapAngleMultiple": 15,
        "rotatingTool.snapAngleEpsilon": 15,
        "undoManager.isEnabled": true,
        scrollMode: go.Diagram.DocumentScroll,
        maxScale: 2,
        minScale: 0.5
    });

    interlockDiagram.addDiagramListener("ObjectSingleClicked", (e) => {
        const part = e.subject.part;
        verifyCanEditPorts(part);

        // Only input & output gates can update signal label
        if (!canEditPorts.value) setPropertiesSection(part);
        else txtSignalLabel.value = "";
    });

    // Add visibility change event listener
    document.addEventListener("visibilitychange", handleVisibilityChange);

    interlockDiagram.addDiagramListener("BackgroundSingleClicked", () => {
        cleanPropertiesSection();
    });

    interlockDiagram.addDiagramListener("ChangedSelection", (event) => {
        const selection = event.diagram.selection;

        // Reset the red border for all nodes
        interlockDiagram.nodes.each((node) => {
            const shape = node.findObject("SHAPE");
            if (shape) {
                shape.stroke = "black";  // Reset stroke to default color
                shape.strokeWidth = 1;   // Reset stroke width
            }
        });

        // If exactly one node is selected, apply logic to highlight it and sync with the monitor view
        if (selection.count === 1 && selection.first().data.category) {
            const selectedGate = selection.first().data;
            projectStore.selectGate(selectedGate);

            // Highlight the selected node in red
            const selectedNode = selection.first();
            const shape = selectedNode.findObject("SHAPE");
            if (shape) {
                shape.stroke = "red";
                shape.strokeWidth = 3;
            }
        } else {
            projectStore.selectGate(null);
        }
    });
    
    // Log when the layout is completed
    interlockDiagram.addDiagramListener("InitialLayoutCompleted", () => {
      // Hide the loader once the diagram finishes rendering
      isLoading.value = false;
      delayLinkCurve = new DelayLinkCurve(interlockDiagram /*, go.Curve.JumpGap*/);
      delayLinkCurve.onUpdate = numremaining => {
        if (numremaining <= 0) {
          interlockDiagram.linkTemplate.curve = go.Curve.JumpOver;
        }
      };
    });

    // configuring Grid
    interlockDiagram.grid = $(
        go.Panel,
        "Grid",
        $(go.Shape, "LineH", {
            strokeWidth: 0.5,
            strokeDashArray: [0, 9.5, 0.5, 0],
        })
    );

    // enabling Grid Snap
    interlockDiagram.toolManager.draggingTool.isGridSnapEnabled = true;
    interlockDiagram.toolManager.resizingTool.isGridSnapEnabled = true;

    // setting node Template
    interlockDiagram.nodeTemplateMap = TemplateGatesMap.diagramTemplateGatesMap;

    // setting Link Template
    interlockDiagram.linkTemplate = LinkTemplate;

    // Define the appearance and behavior for Groups:
    // Groups consist of a title in the color given by the group node data
    // above a translucent gray rectangle surrounding the member parts
    interlockDiagram.groupTemplate = GroupTemplate;

    const dataModel = {
        linkFromPortIdProperty: "fromPort", // required information:
        linkToPortIdProperty: "toPort", // identifies data property names
        linkLabelKeysProperty: "labelKeys",
        nodeDataArray: [],
        linkDataArray: [],
        latchTermArray: [],
        channels: []
    };

    interlockDiagram.model = new go.GraphLinksModel(dataModel);

    ensureHasNotField(interlockDiagram.model.nodeDataArray);

    interlockDiagram.toolManager.linkingTool.archetypeLabelNodeData = {
        category: "LinkLabel",
    };
}

Try increasing the setTimeout time from 10 milliseconds.