Undo after switching Bezier → Orthogonal causes reshaped link to lose endpoints

Hi GoJS team,

I’m experiencing a strange issue with links when combining reshaping, style switching, and Undo.

Setup

I have a diagram with:

  • Links initially configured as Bezier (curve: go.Curve.Bezier)

  • reshapable: true

  • Ability to toggle links between:

    • Bezier (Normal routing)

    • Orthogonal routing

  • Undo/Redo enabled

Minimal setup:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>GoJS Curved Link Demo</title>
  <script src="https://unpkg.com/gojs/release/go.js"></script>
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 20px;
    }
    #myDiagramDiv {
      width: 800px;
      height: 500px;
      border: 1px solid #ccc;
      margin-bottom: 10px;
    }
    .controls {
      display: flex;
      gap: 10px;
    }
    button {
      padding: 10px 20px;
      font-size: 14px;
      cursor: pointer;
    }
    .info {
      margin-top: 10px;
      color: #666;
    }
  </style>
</head>
<body>
  <h1>GoJS Curved Link Demo</h1>
  <div id="myDiagramDiv"></div>
  <div class="controls">
    <button id="toggleBtn">Switch to Orthogonal</button>
    <button id="undoBtn">Undo (Ctrl+Z)</button>
    <button id="redoBtn">Redo (Ctrl+Y)</button>
  </div>
  <div class="info">
    <p>Current mode: <strong id="modeLabel">Curved</strong></p>
  </div>

  <script>
    const $ = go.GraphObject.make;
    let isCurved = true;

    const myDiagram = $(go.Diagram, "myDiagramDiv", {
      "undoManager.isEnabled": true,
      initialContentAlignment: go.Spot.Center
    });

    myDiagram.nodeTemplate = $(go.Node, "Auto",
      { locationSpot: go.Spot.Center },
      $(go.Shape, "RoundedRectangle", {
        fill: "white",
        stroke: "#707989",
        strokeWidth: 1,
        portId: "",
        fromLinkable: true,
        toLinkable: true,
        cursor: "pointer"
      }),
      $(go.TextBlock, {
        margin: 20,
        font: "16px sans-serif"
      }, new go.Binding("text", "name"))
    );

    myDiagram.linkTemplate = $(go.Link, {
        curve: go.Curve.Bezier,
        curviness: 50,
        reshapable: true,
        relinkableFrom: true,
        relinkableTo: true,
        toShortLength: 4
      },
      new go.Binding("curve", "curve"),
      new go.Binding("curviness", "curviness"),
      new go.Binding("routing", "routing"),
      $(go.Shape, {
        strokeWidth: 2,
        stroke: "#4a90d9"
      }),
      $(go.Shape, {
        toArrow: "Standard",
        stroke: "#4a90d9",
        fill: "#4a90d9"
      })
    );

    myDiagram.model = new go.GraphLinksModel(
      [
        { key: 1, name: "Node A", loc: "0 0" },
        { key: 2, name: "Node B", loc: "250 100" }
      ],
      [
        { from: 1, to: 2, curve: go.Curve.Bezier, curviness: 50, routing: go.Routing.Normal }
      ]
    );

    // Parse locations
    myDiagram.nodes.each(node => {
      const loc = node.data.loc;
      if (loc) {
        const parts = loc.split(" ");
        node.location = new go.Point(parseFloat(parts[0]), parseFloat(parts[1]));
      }
    });

    // Toggle button
    document.getElementById("toggleBtn").addEventListener("click", () => {
      myDiagram.startTransaction("toggle link style");
      myDiagram.links.each(link => {
        if (isCurved) {
          myDiagram.model.setDataProperty(link.data, "curve", go.Curve.None);
          myDiagram.model.setDataProperty(link.data, "routing", go.Routing.Orthogonal);
        } else {
          myDiagram.model.setDataProperty(link.data, "curve", go.Curve.Bezier);
          myDiagram.model.setDataProperty(link.data, "routing", go.Routing.Normal);
        }
      });
      myDiagram.commitTransaction("toggle link style");

      isCurved = !isCurved;
      document.getElementById("toggleBtn").textContent = isCurved ? "Switch to Orthogonal" : "Switch to Curved";
      document.getElementById("modeLabel").textContent = isCurved ? "Curved" : "Orthogonal";
    });

    // Undo/Redo buttons
    document.getElementById("undoBtn").addEventListener("click", () => {
      myDiagram.commandHandler.undo();
    });

    document.getElementById("redoBtn").addEventListener("click", () => {
      myDiagram.commandHandler.redo();
    });

  </script>
</body>
</html>

Steps to reproduce

  1. Start with a Bezier link

  2. Reshape it enough (very slight reshaping might not work) - (drag handles to significantly change its shape)

  3. Switch to Orthogonal

  4. Press Undo

Actual result

After Undo:

  • The link no longer connects properly to its start and/or end node

  • It looks detached or visually broken

  • The geometry becomes inconsistent / weird

Expected result

Undo should fully restore the previous link state, including:

  • Proper connection to nodes

  • Original Bezier shape

  • Correct geometry

Screenshot

(Attach your screenshot here — the one you shared showing the broken link)

Question

Is this:

  • A known limitation when switching between Bezier and Orthogonal after reshaping?

  • Related to how reshaping data is stored (points) and restored?

  • Something that requires manually clearing or preserving link points when switching routing/curve types?

What would be the recommended way to:

  • Safely switch between Bezier and Orthogonal links

  • While preserving Undo behavior and avoiding corrupted link geometry?

Thanks in advance for any insights!

That’s interesting. We’ll look into it.

I wondered whether you were saving the route (i.e. the Link.points list) and that that might cause problems because normal Bezier curve link routes have four points whereas normal Orthogonal link routes have six points. Even that ought not to be a problem, unless maybe you had set Link.adjusting. But you haven’t set or bound either of those properties.

Thanks again for the great demo app.

BTW, you can just use the static function go.Point.parse instead of the function you defined.

1 Like

Thanks for reporting this original bug. I believe we have fixed it in v3.1.5, which should be released next week.

1 Like
1 Like