Custom Shape migration from 2.3 to 3.0

Hi

During migration to 3.0 i got s strange error while trying to replace a shape with a custom shape,

I get the following error :

My deafult Shape (RoundedRectangle) in my nodeTemplate has a width of 90.

and the node lokks like this :

But when i replace the RoundedRectangle, with my custom “CustomSquareArrowRight” shape, it crashes. In GOJS 2.3 it didnt crash and looked like that:

The width of the shape is the node templae is set to 90,

but when trying to replace the shape with the custom shape, it grows to 120, and GOJS crashes.

The problem is that the same code was running very good in 2.3 and crashes in 3.0

My node template looks like this

export class PathNodeTemplate extends GoJSAbstractTemplate {

override initTemplate() {
var GO = this.GO;
// let that = this;
this.template =
GO(go.Node, “Spot”,{ width: 150 },
new go.Binding(“visible”, “showPP”, function (showPP, obj) {
if (obj[“diagram”].model.modelData.diagramType != GoJSDiagramComponent.CONST_DIAGRAM_TYPE_DIRECTED_PATH)
return true;
return !obj[“part”].data.isPP || (obj[“part”].data.isPP && showPP);
}).ofModel(),
{
toolTip: this.nodeTooltip(GO),
groupable: true,
// background: “transparent”,
selectionObjectName: “BODY”,
mouseDragEnter: this.mouseDragEnterHandler,
mouseDragLeave: this.mouseDragLeaveHandler,
mouseDrop: null, // implemented by each componenet
shadowOffset: new go.Point(5, 5),
shadowBlur: 25,
shadowColor: “#a0e6d5”,
shadowVisible: false,
},
{
selectionAdornmentTemplate:
GO(go.Adornment, “Auto”,
{

        },
        GO(go.Shape, "RoundedRectangle",
            { 
               fill: null, 
               stroke: "transparent", 
               strokeWidth: 1,
            },
        ),
        GO(go.Placeholder)
        ) // end Adornment
    },
    new go.Binding("position", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding("key"),
    new go.Binding("group"),
    new go.Binding("isShadowed", "isSelected").ofObject(),

    GO(go.Panel, "Vertical", 
    // Panel to hold device id in front of RoundedRectangle
    GO(go.Panel, "Auto", { margin: new go.Margin(0,0,10,0) },
        GO(go.Shape, "RoundedRectangle",
          { 
            fill: "#F8F8F8",                
            strokeWidth: 0,
          }
        ),

    // Vertical Panel to hold deviceid shelf/Rack/CardId right under each other
    GO(go.Panel, "Vertical",  

    // Shelf Id 
   GO(go.TextBlock,  
     {
          margin: new go.Margin(5,5,0,5), 
          isMultiline: false, editable: false,
          stroke: GO(go.Brush, { color: GoJSAbstractTemplate.CONST_GREY_COLOR }),
          font: GoJSAbstractTemplate.CONST_MEDIUM_FONT,
          textAlign: "center",
        },
          new go.Binding("text"),
          new go.Binding("visible", "isFullId").ofModel(),
          new go.Binding("font")            
   ),
   // Shelf Id : rack + shelf no 
   GO(go.TextBlock,  
      {
                isMultiline: false, editable: false,
                 stroke: GO(go.Brush, { color: GoJSAbstractTemplate.CONST_GREY_COLOR }),
                 font: GoJSAbstractTemplate.CONST_MEDIUM_FONT,
                 textAlign: "center",
               },
              new go.Binding("text"),
              new go.Binding("font")            
    ),       
   // Card Id
   GO(go.TextBlock,  
   {
                 margin: new go.Margin(0,5,5,5), 
                 isMultiline: false, editable: false,
                 stroke: GO(go.Brush, { color: GoJSAbstractTemplate.CONST_GREY_COLOR }),
                 font: GoJSAbstractTemplate.CONST_MEDIUM_FONT,
                 textAlign: "center",
               },
                 new go.Binding("text", ""),
                 new go.Binding("font")                   
    )
    )) ,       
    GO(go.Panel, "Spot",  // Panel for holding picture and deleted picture
    {
      alignment: go.Spot.Center,
      cursor: "cell",

    },
    new go.Binding("alignment", "nodeAlignment"),
           
    GO(go.Shape, "RoundedRectangle",
        { 
          width: 90, 
          height: 130, 
          fill: "white", 
          stroke: "rgb(95,95,95)",
          portId: "",
          fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides,
          name: "BODY",
        },
        new go.Binding("height", "nodeHeight"),
        new go.Binding("fill"),
        new go.Binding("stroke"),
        new go.Binding("strokeWidth"),
        new go.Binding("figure"),

        ),
    GO(go.Shape, "LineH",
    { 
      width: 90, 
      stroke: "rgb(95,95,95)",
      alignment: new go.Spot(0.5,0.24),         
    },
    new go.Binding("visible", "visibleLineH"),
    new go.Binding("stroke","strokeLineH"),
    ),
   // Device Type 
      GO(go.TextBlock,  
        {
          alignment: new go.Spot(0.5,0.13),
          textAlign: "center", 
          editable: false,
          stroke: GO(go.Brush, { color: GoJSAbstractTemplate.CONST_DEFAULT_COLOR }),
          font: GoJSAbstractTemplate.CONST_SMALL_FONT,
          width: 50
        },
        new go.Binding("text", "deviceType"),
      ),
  // Picture Panel
  GO(go.Panel, "Vertical",
        {
          alignment: new go.Spot(0.5,0.5),
          // scale : 0.7,
        },
        // the node icon
        new go.Binding("alignment","picAlignment"), 
        GO(go.Picture,
            new go.Binding("source"),
          ), // Picture
      ), // Picture Panel

    // Monitor State
    GO(go.TextBlock,  
       {
        stroke: GO(go.Brush, { color: GoJSAbstractTemplate.CONST_DEFAULT_COLOR }),
        font: GoJSAbstractTemplate.CONST_SMALL_FONT,
        alignment: new go.Spot(0.5,0.95),
        },
        new go.Binding("alignment","monitorAlignment"), 
        new go.Binding("text", "monitorStatus"),
        new go.Binding("stroke", "monitorColor"),
        new go.Binding("visible", "monitorStatus", function (s) { return s != null && s != "" }),
        )      
   ), // Vertical Panel
   GO(go.TextBlock,  // Node alarmMessage
    {
      isMultiline: false, editable: false,
      stroke: GO(go.Brush, { color: GoJSAbstractTemplate.CONST_DEFAULT_COLOR }),
      font: AbstractLinkTemplate.CONST_MEDIUM_FONT,
      name: "nodeAlarmMessage",
      //background: "white"
    },
    new go.Binding("text", "alarmMessage"),
    new go.Binding("stroke", "statusColor"),
    new go.Binding("visible", "alarmMessage", function (s) { return s != null && s != "" })        
  )
  ),
    // Port Array for UNconnected ports
                 new go.Binding("itemArray", "ports"),
                 {
                   name: "PortPanel",
                   itemTemplate:
                     GO(go.Panel, "Vertical",
                       new go.Binding("portId"),
                       new go.Binding("alignment", "spot", go.Spot.parse).makeTwoWay(go.Spot.stringify),
                       {
                        // fromLinkable: true, toLinkable: true,
                         toolTip: this.portTooltip(GO,"fullId",""),
                         cursor: "pointer",
                         name: "PortName",
                       },
                       GO(go.Panel, // panel to place the textbox (portId) and the port shape (circle) side by side horizontaly
                       new go.Binding("visible", "isConnected", function (c) { return !c }),
                       this.createPortName(),   // port Name, which also construct the port shape
                       ), // end port Template
                       this.createPortAlarm(GO)   // port Alarm
                     )
                 },
  
)

}
/**
*
*/
nodeTooltip(GO) {
// var GO = this.GO;
var x =
GO(go.Adornment, “Auto”, GoJSAbstractTemplate.CONST_TOOLTIP_BASIC_ATTRIBUTES,
new go.Binding(“visible”, “showTT”).ofModel(),
GO(go.Shape, “RoundedRectangle”, GoJSAbstractTemplate.CONST_TOOLTIP_BACKGROUND_ATTRIBUTES),
GO(go.Panel, “Vertical”, { defaultStretch: go.GraphObject.Horizontal },
GO(go.TextBlock, " נתוני מכשיר", GoJSAbstractTemplate.CONST_TOOLTIP_HEADER_ATTRIBUTES ),
GO(go.TextBlock, “”, GoJSAbstractTemplate.CONST_TOOLTIP_TEXTBOX_ATTRIBUTES,
new go.Binding(“text”, “fullId”, function (txt) { return txt + " :זיהוי מלא" }),
),
GO(go.TextBlock, “”, GoJSAbstractTemplate.CONST_TOOLTIP_TEXTBOX_ATTRIBUTES_RIGHT,
new go.Binding(“text”, “deviceType”, function (txt) { return txt + " :סוג מכשיר"}),
),
GO(go.TextBlock, “”, GoJSAbstractTemplate.CONST_TOOLTIP_TEXTBOX_ATTRIBUTES_RIGHT,
new go.Binding(“text”, “sla”, function (txt) { return txt + " :SLA" }),
),
GO(go.TextBlock, “”, GoJSAbstractTemplate.CONST_TOOLTIP_TEXTBOX_ATTRIBUTES,
new go.Binding(“margin”, “alarmMessage”, function (txt) { return txt != null && txt != “” ? new go.Margin(4, 4, 4, 4) : 0 }),
new go.Binding(“visible”, “alarmMessage”, function (txt) { return txt != null && txt != “”; }),
new go.Binding(“text”, “alarmMessage”, function (txt) { return "אירוע: " + txt }),
),
GO(go.TextBlock, “”, GoJSAbstractTemplate.CONST_TOOLTIP_TEXTBOX_ATTRIBUTES_RIGHT,
new go.Binding(“margin”, “monitorStatus”, function (txt) { return txt != null && txt != “” ? new go.Margin(4, 4, 4, 4) : new go.Margin(0, 0, 0, 0) }),
new go.Binding(“visible”, “monitorStatus”, function (txt) { return txt != null && txt != “”; }),
new go.Binding(“text”, “monitorStatus”, function (txt) { return "סטטוס ניטור: " + txt }),
),

    )
  );  // end of Adornment
return x;

}

/**
*
/
private createPortName() {
// let that = this;
var GO = this.GO;
var x =
GO(go.Panel, “Auto”,
GO(go.TextBlock,
{
font: GoJSAbstractTemplate.CONST_SMALL_FONT,
editable: false,
visible: true,
stroke: “black”,
background: “transparent”,
name: “portNameTextBox”,
},
new go.Binding(“stroke”, “portStatusColor”),
new go.Binding(“text”, “portName”), //.makeTwoWay(),
)
);
return x;
}
/
*
*
*/
private createPortAlarm(GO) {
var x =
GO(go.TextBlock, {
font: GoJSAbstractTemplate.CONST_MEDIUM_FONT,
editable: false,
visible: false
},
new go.Binding(“text”, “alarmMessage”),
new go.Binding(“stroke”, “statusColor”),
new go.Binding(“visible”, “alarmMessage”, function (s) { return s != null && s != “”; }),
new go.Binding(“visible”, “isConnected”, function (c) { return !c }),
);
return x;
}

/**
*
*/
override setData(data): Object {
var device = data;
var deviceId = device.deviceId;
var icon = typeof (device.icon) !== “undefined” ? device.icon + “.svg” : device.source;
let lastIndex = icon.lastIndexOf(‘/’);
icon = GoJSAbstractTemplate.CONST_ICON_RELATIVE_PATH + icon.substring(lastIndex + 1);

var nodeData = {
  key: device.key,
  alarmMessage: null,
  isDeleted: false,
  source: icon,
  deviceId: deviceId,
  fullId: deviceId.fullId,
  // used by netmap report
  name: deviceId.fullId + ' | ' + deviceId.shortId + ' | ' + this.htmlDecode(device.site.name) + ' | ' + this.htmlDecode(device.networkType)
    + ' | ' + device.site.area
    + ' | ' + device.site.region,
  site: device.site,
  siteId: device.site.siteId,
  siteName: this.htmlDecode(device.site.name),
  shelfId: this.getShelfId(device),
  shortId: deviceId.shortId,
  ipAddress: this.getIpAddress(device),
  ipAddressesInfo: device.ipAddressesInfo,
  deviceLevel: device.deviceLevel,
  deviceType: device.deviceType,
  toolTip: device.toolTip,
  connectionType: device.connectionType !== undefined ? device.connectionType : null,
  ports: [],
  picHeight: 50,
  picWidth: 50,
  lastPortPanel: -1,
  rectangleWidth: 85,
  statusColor: InventoryConfigurationConstants.CONST_DEFAULT_PORT_COLOR,
  loc: null,
  isTreeExpanded: null,
  parent: null,
  serviceId: null,
  networkType: null,
  networkTypeKey: null,
  nt: this.htmlDecode(device.networkType),
  area: device.site.area,
  region: device.site.region,
  rowNum: 0 // rowNum,
};

if (device.loc !== undefined && device.loc !== null)
  nodeData.loc = device.loc; // device.loc.x + " " + device.loc.y;
if (typeof (device.isTreeExpanded) !== "undefined")
  nodeData.isTreeExpanded = device.isTreeExpanded;
if (typeof (device.serviceId) !== "undefined")
  nodeData.serviceId = device.serviceId;
if (typeof (device.group) !== "undefined")
  nodeData['group'] = device.group;
if (device.deviceLevel === InventoryConfigurationConstants.CONST_DL_PHYSICALDEVICE) {
  nodeData.networkType = device.networkType;
  nodeData.networkTypeKey = device.networkTypeKey;
}

return nodeData;

};

getShelfId(device) {
return device.deviceType !== undefined ? device.deviceId.shelfId.substr(3 + device.deviceType.length + 1) : device.deviceId.shelfId.substr(3);
}

getIpAddress(device) {
return device.ipAddressesInfo !== undefined && device.ipAddressesInfo.length > 0 ? device.ipAddressesInfo[0].ipAddress : null;
}

override setContextMenu() {
let nodeContext = document.getElementById(‘nodeContextMenu’);
let that = this;
nodeContext.addEventListener(‘contextmenu’, (e) => {
e.preventDefault();
return false;
}, false)

let myNodeContextMenu = this.GO(go.HTMLInfo, {
  show: showNodeContextMenu,
  hide: hideNodeContextMenu,
  mainElement: nodeContext
})

function hideCX() {
  if (that.diagram.currentTool instanceof go.ContextMenuTool) {
    setTimeout(() => {
      that.diagram.currentTool.doCancel();
    }, 0);
  }
}
function showNodeContextMenu(obj, diagram, tool) {
  diagram.scrollsPageOnFocus = true;
  nodeContext.style.display = 'block';
  // Ticket #654
  GoJSAbstractTemplate.setContextMenuLocation(obj, nodeContext, 228, 520);
  window.addEventListener("click", hideCX, true);
}
function hideNodeContextMenu(obj, diagram, tool) {
  nodeContext.style.display = 'none';
  window.removeEventListener("click", hideCX, true);
}
this.template.contextMenu = myNodeContextMenu;
this.setPortContextMenu("PortPanel");

}

setPortContextMenu(portPanelName) {

let portContext = document.getElementById('portContextMenu');
portContext.addEventListener('contextmenu', (e) => {
  e.preventDefault();
  return false;
}, false)

let myPortContextMenu = this.GO(go.HTMLInfo, {
  show: showPortContextMenu,
  mainElement: portContext
})

function showPortContextMenu(obj, diagram, tool) {
  
  // enable port connection on ports on a node
  let connectPortContextMenuItem = document.getElementById('connectPortContextMenuItem');
  if (connectPortContextMenuItem != null)
     connectPortContextMenuItem.style.display = 'block';
  // disable port disconnection on none connected ports on a node
  let disconnectPortContextMenuItem = document.getElementById('disconnectPortContextMenuItem');
     if (disconnectPortContextMenuItem != null)
        disconnectPortContextMenuItem.style.display = 'none';

  let mousePt = diagram.lastInput.viewPoint;
  portContext.style.display = 'block';

  jsHelpers.setContextPosition(mousePt,portContext);

  
}

var portPanel = this.template.findObject(portPanelName);
if (portPanel.itemTemplate != null)  // Node template has itemTemplate attribute, LinkTemplate does not
  portPanel.itemTemplate.contextMenu = myPortContextMenu;
else
  portPanel.contextMenu = myPortContextMenu;

}
}

The custom shape code is :

go.Shape.defineFigureGenerator(“CustomSquareArrowRight”, function (shape, w, h) {
var param1 = shape ? shape.parameter1 : NaN;
if (isNaN(param1) || param1 < 0) param1 = 5; // default corner
param1 = Math.min(param1, w / 3);
param1 = Math.min(param1, h / 3);
var rightLineHeight = h * 0.23;
var arrowHeight = rightLineHeight / 2;
var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
var cpOffset = param1 * KAPPA;
var geo = new go.Geometry()
.add(new go.PathFigure(param1, 0, true)
.add(new go.PathSegment(go.SegmentType.Line, w, 0))
.add(new go.PathSegment(go.SegmentType.Line, w, arrowHeight))
.add(new go.PathSegment(go.SegmentType.Line, w + 30, rightLineHeight))
.add(new go.PathSegment(go.SegmentType.Line, w, rightLineHeight))
.add(new go.PathSegment(go.SegmentType.Line, w, rightLineHeight))
.add(new go.PathSegment(go.SegmentType.Line, w, h - param1))
.add(new go.PathSegment(go.SegmentType.Bezier, w - param1, h, w, h - cpOffset, w - cpOffset, h))
.add(new go.PathSegment(go.SegmentType.Line, param1, h))
.add(new go.PathSegment(go.SegmentType.Bezier, 0, h - param1, cpOffset, h, 0, h - cpOffset))
.add(new go.PathSegment(go.SegmentType.Line, 0, param1))
.add(new go.PathSegment(go.SegmentType.Bezier, param1, 0, 0, cpOffset, cpOffset, 0).close()));
if (cpOffset > 1) {
geo.spot1 = new go.Spot(0, 0, cpOffset, cpOffset);
geo.spot2 = new go.Spot(1, 1, -cpOffset, -cpOffset);
}
return geo;
});

Please advice

Tany

The problem is this call:

      .add(new go.PathSegment(go.SegmentType.Line, w + 30, rightLineHeight))

That causes the Geometry to have a width that is wider than the given width w. The requirements for the Geometry generator are that it produces a Geometry with the given width and height.

Those requirements haven’t changed since before 1.0, but maybe in 3.0 we made the testing more strict in order to avoid certain kinds of problems managing the bounds of objects.

You’ll need to change that geometry generator so that the resulting Geometry doesn’t have a width of w+30 but just w. Be careful to generate a reasonable Geometry when the given width is less than 30.

I figured that also,

But if I dont do the w+30 i wont get “bird’s beak” in the shape

By the way, i ran the code in a simple JS and it works fine

<meta charset="UTF-8">
<script src="script/go-3.0.28.js"></script>
<script id="code">
    var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);



    function init() {

        go.Shape.defineFigureGenerator("CustomSquareArrowLeft", function(shape, w, h) {  // predefined in 2.0
            var param1 = shape ? shape.parameter1 : NaN;
            if (isNaN(param1) || param1 < 0) param1 = 5;  // default corner
            param1 = Math.min(param1, w / 3);
            param1 = Math.min(param1, h / 3);
            var leftLineHeight = h*0.23;
            var arrowHeight = leftLineHeight / 2;
            var cpOffset = param1 * KAPPA;
            var geo = new go.Geometry()
                    .add(new go.PathFigure(param1, 0, true)
                            .add(new go.PathSegment(go.SegmentType.Line, w - param1, 0))
                            .add(new go.PathSegment(go.SegmentType.Bezier, w, param1, w - cpOffset, 0, w, cpOffset))
                            .add(new go.PathSegment(go.SegmentType.Line, w, h - param1))
                            .add(new go.PathSegment(go.SegmentType.Bezier, w - param1, h, w, h - cpOffset, w - cpOffset, h))
                            .add(new go.PathSegment(go.SegmentType.Line, param1, h))
                            .add(new go.PathSegment(go.SegmentType.Bezier, 0, h - param1, cpOffset, h, 0, h - cpOffset))
                            .add(new go.PathSegment(go.SegmentType.Line, 0, leftLineHeight))
                            .add(new go.PathSegment(go.SegmentType.Line, w, leftLineHeight))
                            .add(new go.PathSegment(go.SegmentType.Line, 0, leftLineHeight))
                            .add(new go.PathSegment(go.SegmentType.Line, -26.5, arrowHeight))
                            .add(new go.PathSegment(go.SegmentType.Line, 0, 0))
                            .add(new go.PathSegment(go.SegmentType.Line, 10, 0)));
            if (cpOffset > 1) {
                geo.spot1 = new go.Spot(0, 0, cpOffset, cpOffset);
                geo.spot2 = new go.Spot(1, 1, -cpOffset, -cpOffset);
            }
            return geo;
        });

        go.Shape.defineFigureGenerator("CustomSquareArrowRight", function(shape, w, h) {  // predefined in 2.0
            var param1 = shape ? shape.parameter1 : NaN;
            if (isNaN(param1) || param1 < 0) param1 = 5;  // default corner
            param1 = Math.min(param1, w / 3);
            param1 = Math.min(param1, h / 3);
            var rightLineHeight = h*0.23;
            var arrowHeight = rightLineHeight / 2;
            var cpOffset = param1 * KAPPA;
            var geo = new go.Geometry()
                    .add(new go.PathFigure(param1, 0, true)
                            .add(new go.PathSegment(go.SegmentType.Line, w, 0))                // ------
                            .add(new go.PathSegment(go.SegmentType.Line, w+30, arrowHeight)) //       \
                            .add(new go.PathSegment(go.SegmentType.Line, w, rightLineHeight))  //       /
                            .add(new go.PathSegment(go.SegmentType.Line, 0, rightLineHeight))  // ------
                            .add(new go.PathSegment(go.SegmentType.Line, w, rightLineHeight))  // ------
                            .add(new go.PathSegment(go.SegmentType.Line, w, h - param1))       //       |
                            .add(new go.PathSegment(go.SegmentType.Bezier, w - param1, h, w, h - cpOffset, w - cpOffset, h))  //
                            .add(new go.PathSegment(go.SegmentType.Line, 0, h))                // -------
                            .add(new go.PathSegment(go.SegmentType.Bezier, 0, h - param1, cpOffset, h, 0, h - cpOffset))  //
                            .add(new go.PathSegment(go.SegmentType.Line, 0, param1))           //  |
                            .add(new go.PathSegment(go.SegmentType.Bezier, param1, 0, 0, cpOffset, cpOffset, 0).close()));
            if (cpOffset > 1) {
                geo.spot1 = new go.Spot(0, 0, cpOffset, cpOffset);
                geo.spot2 = new go.Spot(1, 1, -cpOffset, -cpOffset);
            }
            return geo;
        });
        go.Shape.defineFigureGenerator("CustomRoundedRectangle", function(shape, w, h) {  // predefined in 2.0
            var param1 = shape ? shape.parameter1 : NaN;
            if (isNaN(param1) || param1 < 0) param1 = 5;  // default corner
            param1 = Math.min(param1, w / 3);
            param1 = Math.min(param1, h / 3);

            var cpOffset = param1 * KAPPA;
            var geo = new go.Geometry()
                    .add(new go.PathFigure(param1, 0, true)
                            .add(new go.PathSegment(go.PathSegment.Line, w - param1, 0))
                            .add(new go.PathSegment(go.PathSegment.Bezier, w, param1, w - cpOffset, 0, w, cpOffset))
                            .add(new go.PathSegment(go.PathSegment.Line, w, h*0.23))
                            .add(new go.PathSegment(go.PathSegment.Line, 0, h*0.23))
                            .add(new go.PathSegment(go.PathSegment.Line, w, h*0.23))
                            .add(new go.PathSegment(go.PathSegment.Line, w, h - param1))
                            .add(new go.PathSegment(go.PathSegment.Bezier, w - param1, h, w, h - cpOffset, w - cpOffset, h))
                            .add(new go.PathSegment(go.PathSegment.Line, param1, h))
                            .add(new go.PathSegment(go.PathSegment.Bezier, 0, h - param1, cpOffset, h, 0, h - cpOffset))
                            .add(new go.PathSegment(go.PathSegment.Line, 0, param1))
                            .add(new go.PathSegment(go.PathSegment.Bezier, param1, 0, 0, cpOffset, cpOffset, 0).close()));
            if (cpOffset > 1) {
                geo.spot1 = new go.Spot(0, 0, cpOffset, cpOffset);
                geo.spot2 = new go.Spot(1, 1, -cpOffset, -cpOffset);
            }
            return geo;
        });



        var nodeDataArray = [
            { key: 1, nodeName:"שם האמא",   source:"icons/Router.svg", figure: "CustomSquareArrowRight", loc:"0 500",   },
            { key: 2, nodeName:"סימה", source:"icons/Router.svg", figure:  "CustomSquareArrowLeft",loc:"500 500",  },
            { key: 3, nodeName:"שם היחידה", source:"icons/Router.svg", figure:  "CustomRoundedRectangle",loc:"700 500" },
        ];

        var linkDataArray = [ {
                                from : 1, to: 2, portA:"portA1", alarmA: "alarmA", portZ: "portZ1", alarmZ: "alarmZ",
                               // fromSpot: go.Spot.Right, toSpot: go.Spot.Left
                               } ,
                              {

// from : 2, to: 1, portA:“portA2”, alarmA: “system going down”, portZ: “portZ2”, alarmZ:“8888888888”,
// fromSpot: go.Spot.Left, toSpot: go.Spot.Right
}
];

        var $ = go.GraphObject.make;  // for conciseness in defining templates
        myDiagram =
            $(go.Diagram, "myDiagramDiv",  // Diagram refers to its DIV HTML element by id
                {
                    // start everything in the middle of the viewport
                    initialContentAlignment: go.Spot.Center,
                }
            );
        // define the node template
        myDiagram.nodeTemplate =
                $(go.Node, "Auto",
                        // {  fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides },
                        new go.Binding("position", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
                        //new go.Binding("fromSpot"),
                        //new go.Binding("toSpot"),
                        $(go.Panel, "Auto",
                                $(go.Shape, "CustomSquareArrowLeft",
                                        {
                                            fill: "white",
                                            stroke: "black",
                                            strokeWidth: 1,
                                            width: 90,
                                            height: 130,
                                            margin : 20
                                        },
                                        new go.Binding("figure")
                                ),
                                $(go.Panel, "Vertical",
                                        {
                                            height: 142,
                                        },
                                        $(go.TextBlock,
                                                {
                                                    //alignment: new go.Spot(0.5,0.5),
                                                    textAlign: "center",
                                                    editable: false,
                                                    margin : new go.Margin(10, 0, 0, 0),
                                                },
                                                new go.Binding("text","nodeName")
                                        ),
                                        $(go.Picture, new go.Binding("source"), { margin : new go.Margin(20, 0, 0, 0), scale: 0.2 })
                                )
                        )
                );  // end Node

        myDiagram.linkTemplate =
                $(go.Link,
                        $(go.Shape),  // the line
                        $(go.Shape,
                                { toArrow: "Standard" }  // end Arrow
                        ),
                        new go.Binding("fromSpot"),
                        new go.Binding("toSpot"),

        // {  fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides },
                        // Port A name
                        $(go.TextBlock,
                                {
                                    segmentIndex: 0,
                                    //segmentOffset: new go.Point(NaN,NaN),
                                    //segmentOrientation : go.Link.OrientUpright,
                                    segmentFraction: 0.5,
                                    background: "white",
                                    textAlign: "center",
                                    // name: "portZtext",
                                    // height: 15,
                                   // textAlign: "center",
                                    width: 50,
                                    stroke:"black",
                                    // cursor: "pointer",
                                    // font: "10pt arial",
                                    // stroke:"black",
                                    //text: "portZ"
                                    // margin : new go.Margin(0,130,0,130)
                                },
                                new go.Binding("text", "portA")
                        ),

// // Port A Alarm
// $(go.TextBlock,
// {
// segmentIndex: 0,
// //segmentOffset: new go.Point(NaN,10),
// segmentOrientation : go.Link.OrientUpright,
// // segmentOffset: new go.Point(0, 10),
// // segmentFraction: 0.2,
// // name: “portZtext”,
// height: 15,
// cursor: “pointer”,
// font: “10pt arial”,
// stroke:“blue”,
// //text: “alarmZ”,
// },
// new go.Binding(“text”, “alarmA”),
// new go.Binding(“segmentOffset”, “direction”, function (dir, obj) {
// return dir == “tx” ? new go.Point(NaN, 10) : new go.Point(NaN, -10)
// })
// ),
///////////////////////////////////////////////////////////////////////////////////
// Port Z name
$(go.TextBlock,
{
segmentIndex: -1,
// segmentOrientation : go.Link.OrientUpright,
//segmentOffset: new go.Point(0, -10),
segmentFraction: 0.2,
background: “white”,
// name: “portZtext”,
// height: 15,
textAlign: “center”,
width: 50,
stroke:“red”,
},
new go.Binding(“text”, “portZ”)
)
// // Port Z Alarm
// $(go.TextBlock,
// {
// segmentIndex: -1,
// //segmentOrientation : go.Link.OrientUpright,
// //segmentOffset: new go.Point(30, 10),
// // segmentFraction: 0.2,
// // name: “portZtext”,
// height: 15,
// cursor: “pointer”,
// font: “10pt arial”,
// stroke:“blue”,
// //text: “alarmZ”,
// },
// new go.Binding(“text”, “alarmZ”),
// new go.Binding(“segmentOffset”, “direction”, function (dir, obj) {
// return dir == “tx” ? new go.Point(NaN, 10) : new go.Point(NaN, -10)
// })
// )
);
// generate the initial model
myDiagram.model = new go.GraphLinksModel(nodeDataArray,linkDataArray);
}

Why is your node template an “Auto” Panel? Remember that the purpose of the “Auto” Panel is to size its main (or first) element to be big enough to enclose all of its other elements. But your first element is another “Auto” Panel holding a “CustomSquareArrowLeft” figure Shape and a Panel with other stuff in it.

I would think you should not use an “Auto” Panel, Maybe a “Vertical” or “Table” Panel.

You are right, in the simple JS sample the node is “Auto” but in the previous TS node template code it is “Spot”, so what is wrong with the TS templare that does not work ?

Here are my node template and figure definitions that produce this example:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2026 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">

go.Shape.defineFigureGenerator("CustomSquareArrowLeft", function(shape, w, h) {  // predefined in 2.0
  const x1 = 30;
  const x2 = w-30;
  return new go.Geometry()
    .add(new go.PathFigure(x1, h, true)
      .add(new go.PathSegment(go.SegmentType.Line, 0, h/2))
      .add(new go.PathSegment(go.SegmentType.Line, x1, 0))
      .add(new go.PathSegment(go.SegmentType.Line, x2-8, 0))
      .add(new go.PathSegment(go.SegmentType.Bezier, x2, 8, x2-8/2, 0, x2, 8/2))
      .add(new go.PathSegment(go.SegmentType.Line, x2, h))
      .add(new go.PathSegment(go.SegmentType.Move, w, 0))  // include a point at (w,0)
    );
});

go.Shape.defineFigureGenerator("CustomSquareArrowRight", function(shape, w, h) {  // predefined in 2.0
  const x1 = 30;
  const x2 = w-30;
  return new go.Geometry()
    .add(new go.PathFigure(30, h, true)
      .add(new go.PathSegment(go.SegmentType.Line, x1, 8))
      .add(new go.PathSegment(go.SegmentType.Bezier, x1+8, 0, x1, 8/2, x1+8/2, 0))
      .add(new go.PathSegment(go.SegmentType.Line, x2, 0))
      .add(new go.PathSegment(go.SegmentType.Line, w, h/2))
      .add(new go.PathSegment(go.SegmentType.Line, x2, h))
      .add(new go.PathSegment(go.SegmentType.Move, 0, 0))  // include a point at (0, 0)
    );
});

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  new go.Node("Vertical")
    .bind("location", "loc", go.Point.parse)
    .add(
      new go.Panel("Auto", { height: 40, width: 180, margin: new go.Margin(0, 0, -1, 0) })
        .add(
          new go.Shape("CustomSquareArrowLeft", { fill: "white" })
            .bind("figure", "right", r => r ? "CustomSquareArrowRight" : "CustomSquareArrowLeft"),
          new go.TextBlock({ font: "bold 10pt sans-serif" })
            .bind("text")
        ),
      new go.Panel("Auto", {
          height: 130, width: 120,
          portId: "", fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides
        })
        .add(
          new go.Shape("RoundedRectangle", { parameter1: 8, parameter2: 4|8, fill: "white" }),
          new go.Picture("???", { width: 40, height: 60, background: "whitesmoke" })
        )
    );

myDiagram.linkTemplate =
  new go.Link({ routing: go.Routing.Orthogonal })
    .add(
      new go.Shape(),
      new go.Shape({ toArrow: "Standard" }),
      new go.TextBlock({
          segmentIndex: 0, segmentFraction: 1,
          segmentOrientation: go.Orientation.Upright,
          segmentOffset: new go.Point(NaN, 0),
          font: "bold 9pt sans-serif",
          background: "#FFFFFF"
        })
        .bind("text", "flab"),
      new go.TextBlock({
          segmentIndex: -1, segmentFraction: 1,
          segmentOrientation: go.Orientation.Upright,
          segmentOffset: new go.Point(NaN, 0),
          font: "bold 9pt sans-serif",
          background: "#FFFFFF"
        })
        .bind("text", "tlab")
    )

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "CRG10", loc: "0 0", right: true },
  { key: 2, text: "LEFT", loc: "-250 0" },
  { key: 3, text: "RIGHT", loc: "250 0", right: true }
],
[
  { from: 1, to: 2, flab: "L17Rx" },
  { from: 1, to: 2, flab: "L17Tx" },
  { from: 1, to: 3, flab: "H8Tx" },
  { from: 3, to: 1, tlab: "H8Rx" },
]);
  </script>
</body>
</html>

I had to make some guesses about the ports, and of course I don’t have the icon that you have for the node, so I used an empty Picture.

Also I made the assumption that those two custom figures will never be used for widths near or less than 60, and heights much below 20.

Great,

But what if my default shape is a Rectangle with width LESS than the custom shape.

Will the links end on the custom shape or since the custom shape is wider than the dafault shape, they will not touch the shape ?

That actually is the case in the node template I just provided you. See how the Panel whose width is 120 has portId set on it, and how the links connect with that panel rather than with the whole node, whose width is 180.

If you want to use separate distinct ports, instead of treating the lower rectangular panel as the sole port for the node, that’s OK too – just arrange them to be in or near that lower panel.

Ahhh, now i see (speaking like ChatGPT….),

Works perfect,

Thanks

OK,

I did some progress.

I defined 2 orthogonal shapes in my template and one is replacing eah other according to boolean value, “isAggShape”.

The first is the “vertical” shape you sent me, with the two vertical shapes sitting one above each other and another is a panel with a regular “RoundedRectange”.

The node shows either the “vertical” shape XOR the “RoundedRectange” shape according to the boolean value '“isAggShape”.

The problme is that i had to define portId on two shapes and the result is :

namely, whenever there are links between shapes with the same portId (shapes), the links are parallel.

If the link is between diffrernt shapes, it looks like GOJS confuses becuase of multiple portId defintions.

Any thoughts.

Why do you need two separate ports?

Some of the nodes should have a look with the “Right/Left” Arrow and some with regular rectangular.

I beleieve there is some way to do it woth one port, maybe to enlarge the recatnaglar and hide the right/left arrow (visible:false)

I think i managed,

I created anothe shape :

go.Shape.defineFigureGenerator(“CustomSquareTop”, function(shape, w, h) { // predefined in 2.0
const x1 = 30;
const x2 = w-30;
return new go.Geometry()
.add(new go.PathFigure(30, h, true)
.add(new go.PathSegment(go.SegmentType.Line, x1, 8))
.add(new go.PathSegment(go.SegmentType.Bezier, x1+8, 0, x1, 8/2, x1+8/2, 0))
.add(new go.PathSegment(go.SegmentType.Line, x2-8, 0))
.add(new go.PathSegment(go.SegmentType.Bezier, x2, 8, x2-8/2, 0, x2, 8/2))
.add(new go.PathSegment(go.SegmentType.Line, x2, h))
.add(new go.PathSegment(go.SegmentType.Move, 0, 0)) // include a point at (0, 0)
);
});

which replaces the “CustomSquareArrowRight” or the “CustomSquareArrowRight” according to some rule and now i work only with the “vertical” shapes with one portId.

The result looks like this :

Actually i think i have impleneted a “RoundedTopRectangle” shape.

I will use GOJS out of the box figure.

But note that, as soon as i replace RoundedTopRectangle shape with the Arrow shapes, i need to enlarge the containg panel to 150.

Is this code “valid” ? It works fine, but is it GOJS native coding ?

$(go.Shape, “RoundedTopRectangle”,
{ fill: “white”, stroke: “green” },
new go.Binding(“figure”,“figure”, function(newVal,shape) {
shape.figure = newVal; // set the figure to the new figure
var node = shape.part;
var panel = node.findObject(“topPanel”);
panel.width = 150 // extend the containing panel to 150 to hold Arrow type shapes
})
),