CurvedLinkReshapingTool doesn't work

I created a simple example to test the CurvedLinkReshapingTool but I can’t figure out what mistake I did. The handler is there but it won’t move when I try to drag it. What I’m missing? And how can I enable the second handler like in the following example? Various Visual Relationships | GoJS Diagramming Library

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://gojs.net/latest/release/go.js"></script>
    <script src="https://gojs.net/latest/extensions/CurvedLinkReshapingTool.js"></script>
    <div id="sample" style="display: flex">
      <div
        id="diagramDiv"
        style="border: solid 1px black; width: 600px; height: 600px"
      ></div>
    </div>

    <script id="code">
      const diagram = new go.Diagram("diagramDiv", {
        linkReshapingTool: new CurvedLinkReshapingTool(),
      });

      const nodeTemplate = new go.Node("Auto", {
        draggable: true,
      })
        .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
        .add(
          new go.Shape("RoundedRectangle", {
            strokeWidth: 0,
            fill: "white",
            fromSpot: go.Spot.AllSides,
            toSpot: go.Spot.AllSides,
            portId: "",
            fromLinkable: true,
            toLinkable: true,
          }).bind("fill", "color"),
          new go.TextBlock({
            margin: 8,
            font: "bold 14px sans-serif",
            stroke: "#333",
          }).bind("text")
        );

      const linkTemplate = new go.Link({
        curve: go.Curve.Bezier,
        reshapable: true,
      })
        .bindTwoWay("curviness", "curviness")
        .add(
          new go.Shape({ strokeWidth: 1.5 }),
          new go.Shape({ toArrow: "standard", stroke: null })
        );

      diagram.nodeTemplate = nodeTemplate;
      diagram.linkTemplate = linkTemplate;

      const nodes = [
        {
          key: "N1",
          text: "Alpha",
          color: "lightblue",
          location: "-12.51953125 117.89453125",
        },
        {
          key: "N2",
          text: "Beta",
          color: "orange",
          location: "270.5625 39.98828125",
        },
        {
          key: "N3",
          text: "Gamma",
          color: "lightgreen",
          location: "192.12538656080795 345.15234375",
        },
      ];

      const links = [
        {
          from: "N1",
          to: "N3",
        },
        {
          from: "N1",
          to: "N2",
        },
      ];

      diagram.model = new go.GraphLinksModel(nodes, links);
    </script>
  </body>
</html>

Your Link.fromSpot and toSpot get their effective values from the ports’ fromSpot and toSpot, which are Spot.AllSides. Specifying a Spot will cause there to be a short end segment that is normally perpendicular to the side of the port. So there’s no freedom for the the inner points to be used to control the curve of the Bezier link. Which means the CurvedLinkReshapingTool can’t do anything.

Just remove those spot settings.

How to make it work with from/to spots specified? I changed it to Spot.Top, Spot.Bottom and it also doesn’t work. And how can I add the second handler? Could you provide a working example?

What would you want that tool to do? Please sketch some of the possibilities for what the user might do using that tool.

What second handler are you asking about?

In the first post I asked about the second handler from Various Visual Relationships | GoJS Diagramming Library .

I want to achieve the same behavior as in the example, but also limit from/to spots.

Please, provide a working example.

I don’t see any event handlers or listeners defined in that sample, other than “DOMContentLoaded”, which is unlikely to be what you intend.

I’d be happy to provide a working example if I understood what it was that you wanted. Please sketch what happens as the user drags the CurvedLinkReshapingTool’s handle.

This is what I want to achieve:

I was pretty sure it will work the same way as Orthogonal links but if I add more segments it breaks completely:

Ah, I see now – and it’s not what I was expecting. Sorry, neither the LinkReshapingTool nor its Curved… subclass were designed to handle multi-Bezier-curve links.

That would be a nice feature for a future release.

One could adapt the code in Curved Multi-Node Path Link Routes | GoJS Diagramming Library

[EDIT: improved]

This doesn’t have support for adding or removing segments (i.e. individual Bezier curves), but I think it has some of the things you are looking for:

<!DOCTYPE html>
<html>

<head>
  <title>Double Bezier Link</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
  <meta name="description" content="a custom routed Link that always has two Bezier curve segments in it">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <button id="loadModel" onclick="load()">Load</button>
  <button id="saveModel" onclick="save()">Save</button>
  <textarea id="mySavedModel" style="width:100%;height:200px">
{ "class": "GraphLinksModel",
  "nodeDataArray": [
{"key":1,"text":"Alpha","color":"lightgreen","loc":"0 0"},
{"key":2,"text":"Beta","color":"yellow","loc":"-100 189"},
{"key":3,"text":"Gamma","color":"lightgreen","loc":"100 0"},
{"key":4,"text":"Delta","color":"yellow","loc":"100 189"},
{"key":5,"text":"Epsilon","color":"lightgreen","loc":"200 0"},
{"key":6,"text":"Zeta","color":"yellow","loc":"300 189"}
],
  "linkDataArray": [
{"from":1,"to":2,"points":[58.28947448730469,19.547368192672728,87.29297828674316,48.6140338420868,20.824562072753906,83.3649121284485,-2.9078941345214844,109.94035076300304,-26.640350341796875,136.51578939755757,-74.10526275634766,179,-74.10526275634766,189],"adjusting":17},
{"from":3,"to":4,"points":[171.2894744873047,19.547368192672728,181.2894744873047,19.547368192672728,214.59299659729004,82.81403079032899,158.22629737854004,97.38071169853211,82.85965919494629,114.48071780204774,79.69297218322754,154.3807116985321,127.69736862182617,189],"adjusting":17},
{"from":5,"to":6,"points":[267.68421173095703,19.547368192672728,277.68421173095703,19.547368192672728,293.6315797170003,83.3649121284485,301.605263710022,109.94035076300304,309.57894770304364,136.51578939755757,325.5263156890869,179,325.5263156890869,189]}
]}
  </textarea>

  <script src="../latest/release/go.js"></script>
  <script id="code">
// A Link whose route includes two Bezier curve segments (7 points).
// At the current time there is no support for Link.resegmentable
class DoubleBezierLink extends go.Link {
  constructor(init) {
    super();
    this.curve = go.Curve.Bezier;
    if (init) Object.assign(this, init);
  }

  computeEndSegmentLength(node, port, spot, from) {
    let result = super.computeEndSegmentLength(node, port, spot, from);
    if (this.curve === go.Curve.Bezier && !this.isOrthogonal && !spot.equals(go.Spot.None)) {
      result += 20;
    }
    return result;
  }

  computePoints() {
    const result = super.computePoints();  // do normal routing
    const fromport = this.fromPort;
    const toport = this.toPort;
    if (result && this.curve === go.Curve.Bezier && !this.isOrthogonal && fromport && toport && fromport !== toport && this.pointsCount === 4) {
      const fromspot = this.computeSpot(true, fromport);  // link's Spot takes precedence, if defined
      const tospot = this.computeSpot(false, toport);  // link's Spot takes precedence, if defined
      if (!fromspot.equals(go.Spot.None) && !tospot.equals(go.Spot.None)) {
        const p0 = this.getPoint(0);  // the normal end points,
        const p1 = this.getPoint(3);  // determined by getLinkPoint
        const c0 = this.getPoint(1);  // the normal control points,
        const c1 = this.getPoint(2);  // determined by getLinkDirection & computeEndSegmentLength
        const fromdir = this.getLinkDirection(fromport.part, fromport, p0, fromspot, true, false, toport.part, toport);
        const todir = this.getLinkDirection(toport.part, toport, p1, tospot, false, false, fromport.part, fromport);

        const horiz = (fromdir === 0 && todir === 180) || (fromdir === 180 && todir === 0);
        const vert = (fromdir === 90 && todir === 270) || (fromdir === 270 && todir === 90);
        const doublesback =
          (horiz && ((fromdir === 0 && c0.x > c1.x) || (fromdir === 180 && c0.x < c1.x))) ||
          (vert && ((fromdir === 90 && c0.y > c1.y) || (fromdir === 270 && c0.y < c1.y)));
        let mx = (c0.x + c1.x) / 2;  // midpoint
        let my = (c0.y + c1.y) / 2;

        const curvi = this.computeCurviness();
        if (horiz) {
          if (doublesback) my += curvi; else mx += curvi;
        } else {
          if (doublesback) mx += curvi; else my += curvi;
        }

        const c2x = (mx * 2 + c0.x) / 3;
        const c2y = (my * 2 + c0.y) / 3;
        const c3x = (mx * 2 + c1.x) / 3;
        const c3y = (my * 2 + c1.y) / 3;

        this.insertPointAt(2, c2x, c2y);
        this.insertPointAt(3, (c2x + c3x) / 2, (c2y + c3y) / 2);  // make sure the three points are in a line
        this.insertPointAt(4, c3x, c3y);
      }
    }
    return result;
  }

  // When moving the middle point that is common between both Bezier curves,
  // also move the adjacent points so as to maintain the smoothness of the joint at that middle point.
  reshape(newPoint) {
    const link = this.adornedLink;
    if (link === null) return;
    const hnd = this.handle;
    if (hnd === null) return;
    const index = hnd.segmentIndex;
    if (index !== 0 && index !== this.PointsCount-1 && index % 3 === 0) {
      const prevPoint = link.getPoint(index);
      super.reshape(newPoint);
      link.startRoute();
      link.setPoint(index-1, link.getPoint(index-1).copy().add(newPoint).subtract(prevPoint));
      link.setPoint(index+1, link.getPoint(index+1).copy().add(newPoint).subtract(prevPoint));
      link.commitRoute();
    } else {
      super.reshape(newPoint);
    }
  }
}  // end of DoubleBezierLink


class BezierLinkReshapingTool extends go.LinkReshapingTool {
  constructor(init) {
    super();
    if (init) Object.assign(this, init);
  }

  makeAdornment(pathshape) {
    const adornment = super.makeAdornment(pathshape);
    if (adornment === null) return null;
    const link = pathshape.part;
    if (link instanceof go.Link && link.isSelected &&
      link.curve === go.Curve.Bezier && !link.isOrthogonal && link.pointsCount > 4) {
      const h1 = this.makeHandle(pathshape, 1);
      // needs to be a GraphObject so we can set its Cursor
      if (h1 !== null) {
        // identify this particular handle within the LinkPanel
        h1.segmentIndex = 1;
        this.setReshapingBehavior(h1, go.ReshapingBehavior.All);
        h1.cursor = "move";
        adornment.add(h1);
      }
      const hn = this.makeHandle(pathshape, link.pointsCount - 2);
      // needs to be a GraphObject so we can set its Cursor
      if (hn !== null) {
        // identify this particular handle within the LinkPanel
        hn.segmentIndex = link.pointsCount - 2;
        this.setReshapingBehavior(hn, go.ReshapingBehavior.All);
        hn.cursor = "move";
        adornment.add(hn);
      }
    }
    return adornment;
  }

  updateAdornments(link) {
    if (link === null || !(link instanceof go.Link)) return;  // tool only applies to Links
    let adornment = null;
    if (link.isSelected && !this.diagram.isReadOnly) {
      const pathshape = link.path;
      if (pathshape !== null && link.canReshape() && link.actualBounds.isReal() && link.isVisible() && pathshape.actualBounds.isReal() && pathshape.isVisibleObject()) {
        adornment = link.findAdornment(this.name);
        if (adornment === null || adornment._oldPointsCount !== link.pointsCount || adornment._wasResegmentable !== link.resegmentable) {
          adornment = this.makeAdornment(pathshape);
          if (adornment !== null) {
            adornment._oldPointsCount = link.pointsCount;
            adornment._wasResegmentable = link.resegmentable;
            link.addAdornment(this.name, adornment);
          }
        }
      }
    }
    if (adornment === null) link.removeAdornment(this.name);
  }
}  // end of CurvedLinkReshapingTool


myDiagram =
  new go.Diagram("myDiagramDiv", {
      linkReshapingTool: new BezierLinkReshapingTool(),
      // override Diagram.copyParts to prevent routing temporary Links by DraggingTool calls to moveParts
      copyParts: function(coll, diagram, check) {
        const map = go.Diagram.prototype.copyParts.call(this, coll, diagram, check);
        if (this.currentTool instanceof go.DraggingTool) {
          map.iteratorValues.each(part => {
            if (part instanceof go.Link) part.suspendsRouting = true;
          });
        }
        return map;
      },
      "LinkReshaped": e => e.subject.adjusting = go.LinkAdjusting.End,
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape({
          fill: "white", portId: "",
          fromLinkableDuplicates: true, toLinkableDuplicates: true,
          fromSpot: go.Spot.Right, toSpot: go.Spot.Top
        })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 12 })
        .bind("text")
    );

myDiagram.linkTemplate =
  new DoubleBezierLink({
      reshapable: true  // but there's no support for resegmenting
    })
    .bindTwoWay("adjusting")
    .bindTwoWay("points")
    .add(
      new go.Shape({ strokeWidth: 2 }),
      new go.Shape({ toArrow: "OpenTriangle" })
    );

myDiagram.model = new go.GraphLinksModel({
  nodeDataArray: [
    { "key": 1, "text": "Alpha", "color": "lightgreen", "loc": "0 0" },
    { "key": 2, "text": "Beta", "color": "yellow", "loc": "-100 189" },
    { "key": 3, "text": "Gamma", "color": "lightgreen", "loc": "100 0" },
    { "key": 4, "text": "Delta", "color": "yellow", "loc": "100 189" },
    { "key": 5, "text": "Epsilon", "color": "lightgreen", "loc": "200 0" },
    { "key": 6, "text": "Zeta", "color": "yellow", "loc": "300 189" },
  ],
  linkDataArray: [
    { from: 1, to: 2 },
    { from: 3, to: 4 },
    { from: 5, to: 6 },
  ]
});

myDiagram.selectCollection(myDiagram.links);

// save a model to and load a model from Json text, displayed below the Diagram
function save() {
  const str = myDiagram.model.toJson();
  document.getElementById("mySavedModel").value = str;
}
function load() {
  const str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
}

load();
  </script>
</body>

</html>

Thanks for the example.

I managed to fix the original issue by setting routing to Normal and setting from/to spots to None.

It would be great to have some docs that clearly explain possible combinations of Link properties and what are consequences of using them.