segmentOffset binding gives different values in version 1.8.37 and 2.x.x

Hi
I am upgrading gojs version (from 1.8.x to 2.x.x). I observed an issue with segmentOffset of links.
Below code is used for binding segmentOffset.

panel.bind(new Binding("segmentOffset", "", ((link: SequenceFlow) => {
            return link.edge? new Point(link.edge.xConditionOffset, link.edge.yConditionOffset) : new Point(0, 0);
        })).makeTwoWay((value: Point, link: SequenceFlow) => {
            console.log(`${value.x} and ${value.y}`)
            link.edge.xConditionOffset = value.x;
            link.edge.yConditionOffset = value.y;
        }));

Moving this panel in same direction in 1.8.x and 2.x.x version results in different values,
in 1.8.x version we get point with x= -X, y=-Y values and in 2.x.x version these values are x=+X, y=-Y.
Need to understand the why change in behavior.
Can anyone help here?

I’m not seeing a difference between 1.8.37 and 2.1.1 when testing with this example. Try moving around the circles near the links in 1.8.37 and then comment the 1.8.37 script tag and uncomment the 2.1.1 script tag and do the same thing. I’m getting the same results in both cases.

Do note that in 2.0 we made some incompatible changes to the offsetX and offsetY of alignmentFocus in a Spot panel, noted in the changelog under “Incompatible 2.0 changes and removals.” It’s probably worth going through that section to see all the incompatible changes that were made.

I have created a sample here.
Move the text - “LABEL” to bottom-right direction and you can see the difference with versions.

I see. This is also related to another breaking change in the 2.0 change log:

“Improved computation of Link.midPoint and Link.midAngle to be faster and changed some cases where labels fell on very small segments of a link. Orthogonal bezier links will also have more accurate label placement with the new methods.”

In your example, if you change the label to use segmentOrientation: go.Link.OrientUpright, you’ll notice that the label is perpendicular to the link in 1.8 due to the way the angle is calculated. This different angle is why the offset values aren’t the same. The new computation is smarter.

In my application, users are allowed to move ‘link label’ and segmentOffset value is persisted. Now, with this upgrade the position of link labels will get affected. Can you please suggest a way to handle this?

We’re thinking about a good way of doing this. Are all your labels at the midpoint of your links, meaning you haven’t set segmentIndex/segmentFraction? What is your link template?

No segmentIndex/segmentFraction set. Default position is midpoint-Point(0,0).

Link Template
let link = new go.Link();
link.routing = go.Link.AvoidsNodes;
link.curve = go.Link.JumpOver;
link.curviness = 10;
link.corner = 0;
link.relinkableFrom = true;
link.relinkableTo = true;
link.selectionAdorned = false;
link.add(cleanShape);
link.add(arrow);
return link;

And label is added in a panel and this panel is added to this link. This panel is movable and Binding is added for segmentOffset of this panel.

Note: This info may be useful - We are using LinkLabelDraggingTool from extensions.

Ok, here’s a script that should work to convert old segmentOffset values to the same corresponding position in 2.1:

function convertSegmentOffsets(links) {
  links.each(function(l) {
    var oldPoint = midPointOneEight(l);
    var newPoint = l.midPoint;
    var oldAngle = normalizeAngle(midAngleOneEight(l));
    var newAngle = normalizeAngle(l.midAngle);
    console.log("Midpoints - old: " + oldPoint + ", new: " + newPoint);
    console.log("Midangles - old: " + oldAngle + ", new: " + newAngle);
    if (l.data.xOff !== undefined && l.data.yOff !== undefined) {
      l.diagram.model.commit(function(m) {
        var x = l.data.xOff;
        var y = l.data.yOff;
        var t = (newAngle - oldAngle) * Math.PI / 180 * -1;
        var newXOffset = x * Math.cos(t) - y * Math.sin(t) + (newPoint.x - oldPoint.x);
        var newYOffset = x * Math.sin(t) + y * Math.cos(t) + (newPoint.y - oldPoint.y);
        m.set(l.data, "xOff", newXOffset);
        m.set(l.data, "yOff", newYOffset);
      }, null);
    }
  });
}

function normalizeAngle(a) {
  a = a % 360;
  if (a < 0) a += 360;
  return a;
}

function midPointOneEight(l) {
  var result = new go.Point();
  var numpts = l.pointsCount;
  if (numpts === 0) {  // no point means call UpdatePoints/ComputeMidPoint again
    result.setTo(NaN, NaN);
    return result;
  } else if (numpts === 1) {
    result.set(l.getPoint(0));
    return result;
  } else if (numpts === 2) {
    var a = l.getPoint(0);
    var b = l.getPoint(1);
    result.setTo((a.x + b.x) / 2, (a.y + b.y) / 2);
    return result;
  }

  if (l.computeCurve() === go.Link.Bezier && numpts >= 3 && !l.isOrthogonal) {
    if (numpts === 3) return l.getPoint(1);
    var numsegs = ((numpts - 1) / 3) | 0;
    var idx = ((numsegs / 2) | 0) * 3;
    if (numsegs % 2 === 1) {
      var a = l.getPoint(idx);
      var b = l.getPoint(idx + 1);
      var c = l.getPoint(idx + 2);
      var d = l.getPoint(idx + 3);
      bezierMidPoint(a.x, a.y, b.x, b.y, c.x, c.y, d.x, d.y, result);
      return result;
    } else {
      result.set(l.getPoint(idx));
      return result;
    }
  }

  var distanceAlongRoute = 0.0;
  var segmentDistanceQueue = [];
  for (var i = 0; i < numpts - 1; i++) {
    var segmentDistance = 0.0;
    var a = l.getPoint(i);
    var b = l.getPoint(i + 1);

    //Handle computationally simple Orthogonal cases
    if (isApprox(a.x, b.x)) {
      segmentDistance = b.y - a.y;
      if (segmentDistance < 0) segmentDistance = -segmentDistance;
      segmentDistanceQueue.push(segmentDistance);
      distanceAlongRoute += segmentDistance;
    } else if (isApprox(a.y, b.y)) {
      segmentDistance = b.x - a.x;
      if (segmentDistance < 0) segmentDistance = -segmentDistance;
      segmentDistanceQueue.push(segmentDistance);
      distanceAlongRoute += segmentDistance;
    } else {
      //Handle non-Orthogonal lines
      segmentDistance = Math.sqrt(a.distanceSquaredPoint(b));
      segmentDistanceQueue.push(segmentDistance);
      distanceAlongRoute += segmentDistance;
    }
  }

  var currentDistance = 0.0;
  var currentPointIndex = 0;
  var nextSegmentLength = 0.0;
  while (currentDistance < distanceAlongRoute / 2 && currentPointIndex < numpts) {
    nextSegmentLength = segmentDistanceQueue[currentPointIndex];
    if (currentDistance + nextSegmentLength > distanceAlongRoute / 2) break;
    currentDistance += nextSegmentLength;
    currentPointIndex++;
  }
  var currentPoint = l.getPoint(currentPointIndex);
  var nextPoint = l.getPoint(currentPointIndex + 1);

  // Handle Orthogonal cases first
  if (currentPoint.x === nextPoint.x) {
    if (currentPoint.y > nextPoint.y)
      result.setTo(currentPoint.x, currentPoint.y - (distanceAlongRoute / 2 - currentDistance));
    else
      result.setTo(currentPoint.x, currentPoint.y + (distanceAlongRoute / 2 - currentDistance));
  } else if (currentPoint.y === nextPoint.y) {
    if (currentPoint.x > nextPoint.x)
      result.setTo(currentPoint.x - (distanceAlongRoute / 2 - currentDistance), currentPoint.y);
    else
      result.setTo(currentPoint.x + (distanceAlongRoute / 2 - currentDistance), currentPoint.y);
  } else { // Handle non-Orthogonal lines
    var similarTriangleRatio = (distanceAlongRoute / 2 - currentDistance) / nextSegmentLength;
    var dx = similarTriangleRatio * (nextPoint.x - currentPoint.x);
    var dy = similarTriangleRatio * (nextPoint.y - currentPoint.y);

    result.setTo(currentPoint.x + dx, currentPoint.y + dy);
  }
  return result;
}

function midAngleOneEight(l) {
  var numpts = l.pointsCount;
  if (numpts < 2) return NaN;  // no angle means call ComputeMidAngle again

  if (l.computeCurve() === go.Link.Bezier && numpts >= 4 && !l.isOrthogonal) {
    var numsegs = ((numpts - 1) / 3) | 0;
    var idx = ((numsegs / 2) | 0) * 3;
    if (numsegs % 2 === 1) {
      idx = Math.floor(idx);
      var a = l.getPoint(idx);
      var b = l.getPoint(idx + 1);
      var c = l.getPoint(idx + 2);
      var d = l.getPoint(idx + 3);
      return bezierMidAngle(a.x, a.y, b.x, b.y, c.x, c.y, d.x, d.y);
    } else {
      if (idx > 0 && idx + 1 < numpts) {
        var a = l.getPoint(idx - 1);
        var b = l.getPoint(idx + 1);
        return a.directionPoint(b);
      }
      // else drop through and treat as a non-Bezier-curve
    }
  }

  var midEnd = (numpts / 2) | 0;
  if (numpts % 2 === 0) {  // even number of points means odd number of segments
    // get the middle segment (perhaps the only one)
    var a = l.getPoint(midEnd - 1);
    var b = l.getPoint(midEnd);
    // if both of these segment points are the same, treat both as a single point and check either side
    if (numpts >= 4 && pointEqualsApprox(a, b)) {
      a = l.getPoint(midEnd - 2);
      var c = l.getPoint(midEnd + 1);
      var d1 = a.distanceSquaredPoint(b);
      var d2 = b.distanceSquaredPoint(c);
      if (d1 > d2+10)
        return a.directionPoint(b);
      else if (d2 > d1+10)
        return b.directionPoint(c);
      else
        return a.directionPoint(c);
    }
    if (l.geometry !== null && !l.isOrthogonal)
      return getAngleAlongPath(l.geometry, 0.5);
    return a.directionPoint(b);
  } else {
    if (l.geometry !== null && !l.isOrthogonal)
      return getAngleAlongPath(l.geometry, 0.5);
    // also find the points on either side of the middle point
    // then figure out if one segment is much longer than the other
    var a = l.getPoint(midEnd - 1);
    var b = l.getPoint(midEnd);
    var c = l.getPoint(midEnd + 1);
    var d1 = a.distanceSquaredPoint(b);
    var d2 = b.distanceSquaredPoint(c);
    if (d1 > d2+10)
      return a.directionPoint(b);
    else if (d2 > d1+10)
      return b.directionPoint(c);
    else
      return a.directionPoint(c);
  }
}

function bezierMidPoint(sx, sy, c1x, c1y, c2x, c2y, ex, ey, midp) {
  var m1x = (sx + c1x) / 2;
  var m1y = (sy + c1y) / 2;
  var m2x = (c1x + c2x) / 2;
  var m2y = (c1y + c2y) / 2;
  var m3x = (c2x + ex) / 2;
  var m3y = (c2y + ey) / 2;

  var vx = (m1x + m2x) / 2;
  var vy = (m1y + m2y) / 2;
  var wx = (m2x + m3x) / 2;
  var wy = (m2y + m3y) / 2;

  midp.x = (vx + wx) / 2;
  midp.y = (vy + wy) / 2;
  return midp;
}

function bezierMidAngle(sx, sy, c1x, c1y, c2x, c2y, ex, ey) {
  var m1x = (sx + c1x) / 2;
  var m1y = (sy + c1y) / 2;
  var m2x = (c1x + c2x) / 2;
  var m2y = (c1y + c2y) / 2;
  var m3x = (c2x + ex) / 2;
  var m3y = (c2y + ey) / 2;

  var vx = (m1x + m2x) / 2;
  var vy = (m1y + m2y) / 2;
  var wx = (m2x + m3x) / 2;
  var wy = (m2y + m3y) / 2;

  return go.Point.direction(vx, vy, wx, wy);
}

function pointEqualsApprox(a, b) {
  return isApprox(a.x, b.x) && isApprox(a.y, b.y);
}

function isApprox(x, y) {
  var d = x - y;
  return d < 0.5 && d > -0.5;
}

function getAngleAlongPath(geo, fraction) {
  if (fraction < 0) fraction = 0.0;
  else if (fraction > 1) fraction = 1.0;
  var result = 0;
  if (geo.type === go.Geometry.Line) {
    result = Math.atan2(geo.endY - geo.startY, geo.endX - geo.startX) * 180 / Math.PI;
    return result;
  }

  // build allPaths array if needed
  var allPaths = geo.flattenedSegments;
  var allDists = geo.flattenedLengths;
  var totalDist = geo.flattenedTotalLength;

  var l = allPaths.length;
  var fractionalDist = totalDist * fraction;
  var currentDist = 0.0;

  for (var i = 0; i < l; i++) {
    var dists = allDists[i];
    var ll = dists.length;
    for (var j = 0; j < ll; j++) {
      var dist = dists[j];

      if (currentDist + dist >= fractionalDist) {
        var paths = allPaths[i];
        var lastptx = paths[j * 2];
        var lastpty = paths[j * 2 + 1];
        var ptx = paths[j * 2 + 2];
        var pty = paths[j * 2 + 3];
        result = Math.atan2(pty - lastpty, ptx - lastptx) * 180 / Math.PI;
        return result;
      }
      currentDist += dist;
    }
  }

  // only if there was no path on the Geometry, shouldn't be hit
  return NaN;
}

To use the script, you should call convertSegmentOffsets with the full set of links in the diagram. The midPointOneEight and midAngleOneEight functions give the values that get computed in 1.8, so you can use those if for some reason the script doesn’t work as expected.