Make link connect to a specific shape/panel?

We have a node that looks roughly like this:

  • The 4 dark gray rectangles are ports with toLikable: false, fromLinkable: true
  • The big lighter gray rectangle is a port with toLinkable: true, fromLinkable: false
  • The yellow rectangle is just a shape that shows an icon and it’s not a port at all

We are also using the routing: go.Link.AvoidsNodes on the Link template so the link is connected to only top center, left center, right center or bottom center location of the node.

What we want is for the link with the arrow to always connect to the lighter gray big rectangle. As you see there is this gap, marked by the green stroke, which I assume is because the yellow shape has pushed a boundary of the node a little further.

Is there any way that I can tell the link to only connect to the big rectangle? I think fromSpot and toSpot are just relative to the whole node and not individual panels.

If you think a small repro snippet helps, let me know and I will strip down our template and paste it here.

The fromSpot and toSpot are relative to the bounds of the port.

Which port element is that Link connected with, and what is its Link.toPortId?

I suppose you could have the Link.toPortId be the default empty string and declare the lighter gray rectangle to be a port with portId: "".

Here’s the snippet:

import * as go from 'gojs';

var diagram = go.GraphObject.make(go.Diagram, 'box');

const linkTemplate = new go.Link({
  routing: go.Link.AvoidsNodes,
  corner: 6,
})
  .add(new go.Shape({ strokeWidth: 2, stroke: 'black' }))
  .add(new go.Shape({ toArrow: 'Standard', alignment: go.Spot.Center }))
  .bind(new go.Binding('points').makeTwoWay());

const portToAlignment = {
  TopPort: go.Spot.MiddleTop,
  RightPort: go.Spot.MiddleRight,
  BottomPort: go.Spot.MiddleBottom,
  LeftPort: go.Spot.MiddleLeft,
};

function createPort(name) {
  return new go.Shape('Circle', {
    name: name,
    alignment: portToAlignment[name],
    alignmentFocus: go.Spot.Center,
    fill: 'gray',
    strokeWidth: 0,
    opacity: 0,
    desiredSize: new go.Size(16, 16),
    portId: name,
    fromSpot: go.Spot.Center,
    toSpot: go.Spot.Center,
    fromEndSegmentLength: 0,
    toEndSegmentLength: 0,
    cursor: 'pointer',
    fromLinkable: true,
    shadowVisible: true,
  });
}

const availablePortNames = new Set([
  'TopPort',
  'RightPort',
  'BottomPort',
  'LeftPort',
]);

export const nodeTemplate = new go.Node(go.Panel.Spot, {
  alignment: go.Spot.TopLeft,
  stretch: go.GraphObject.Fill,
  background: 'transparent',
  resizeObjectName: 'box',
  selectionObjectName: 'box',
  fromLinkableDuplicates: false,
  toLinkableDuplicates: false,
  selectionAdorned: false,
  mouseEnter: (_e, obj) => {
    if (obj instanceof go.Panel) {
      availablePortNames.forEach((n) => {
        const panel = obj?.findObject(n);
        if (panel != null) panel.opacity = 1;
      });
    }
  },
  mouseLeave: (_e, obj) => {
    if (obj instanceof go.Panel) {
      availablePortNames.forEach((n) => {
        const panel = obj?.findObject(n);
        if (panel != null) panel.opacity = 0;
      });
    }
  },
})
  .bind(
    new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(
      go.Point.stringify
    )
  )
  .add(
    new go.Panel('Auto', {
      alignment: go.Spot.TopLeft,
      portId: 'RectPort',
      toLinkable: true,
      alignmentFocus: go.Spot.Center,
      fromLinkableDuplicates: false,
      toLinkableDuplicates: false,
      toSpot: go.Spot.Center,
    }).add(
      new go.Shape('RoundedRectangle', {
        isPanelMain: true,
        fill: 'whitesmoke',
        stroke: 'gray',
        strokeWidth: 2,
        strokeDashArray: [],
        stretch: go.GraphObject.Fill,
        toLinkable: false,
        name: 'box',
        desiredSize: new go.Size(230, NaN),
        minSize: new go.Size(170, 100),
        maxSize: new go.Size(300, NaN),
      })
    )
  )
  .add(createPort('LeftPort'))
  .add(createPort('TopPort'))
  .add(createPort('RightPort'))
  .add(createPort('BottomPort'))
  .add(
    new go.Panel(go.Panel.Horizontal, {
      toLinkable: false,
      alignment: new go.Spot(0, 0, -2, 0),
      alignmentFocus: go.Spot.Center,
    }).add(
      new go.Shape('Circle', {
        fill: 'gold',
        width: 24,
        height: 24,
      })
    )
  );

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

diagram.model = new go.GraphLinksModel(
  [
    { key: 'A', name: 'A', loc: '0 0' },
    { key: 'B', name: 'B', loc: '200 200' },
    { key: 'C', name: 'C', loc: '600 250' },
  ],
  [
    { from: 'A', to: 'B' },
    { from: 'B', to: 'C' },
  ]
);

Just note that link being connected to the closest location is necessary and desired:

gojs_link

Also, the link creation starts from the circle ports. top/right/bottom/left ones, but connects to the rounded rectangle.

Instead of setting portId to “RectPort” on the “Auto” Panel, if you set it on the “RoundedRectangle” Shape, do you get what you want?

Tried that before but the problem is the link doesn’t connect anymore. This gif shows before and after:

gojs_rect_port

Tried it one more time cause I noticed the toLinkable was set to false on the rectangle. Now linking works but the original problem persists and also I have a new problem. See this:

gojs_to_linkable

If in the last case the problem is that you can’t move nodes, it’s due to the whole “RoundedRectangle” Shape being a linkable port. Starting to draw a new link from a port is not supposed to move the node.

But you can avoid that by putting a non-port (or a non-linkable port) in front of that Shape that you want to be the port. Then a mouse-down on that element (presumably a transparent Shape) would not start drawing a new link.

Typically you’d have it be a similar shape that’s slightly smaller than the actual port shape, so that there would be a narrow edge or border area all around where starting a link would be possible. You could do that by adding to the “Auto” Panel a Shape with a margin that also does stretch: go.GraphObject.Fill.

As I pointed out, that does not fix the original problem of having a gap between the arrow and the rectangle. Also the Auto panel around the RoundedRectangle fits it perfectly, so I still have no idea why the link connects to the entire node layout rather than just this box.

Is the Link actually connected with that “RoundedRectangle” Shape? What is the Link.toPort? Or, what is the link data object?

I haven’t set the port id for links. You can see the actual data in the snippet I posted. Just scroll to the bottom.

Here’s the link to stackblitz if you wanna try it for yourself.

If you didn’t specify “RectPort” as the port identifier, of course the link will not be connected with that port.

OK, instead of setting portId to “RectPort”, set it to “”, the default port identifier name.

Thank you very much. That sorted it out but I also had to change the toSpot to go.Spot.AllSides. The go.Spot.Center was making the arrow heads look angled in a weird way.