Position panel positioning children, items and shapes

Hi,

I am struggling with getting the right group template. My app has a Scene graph data model with entities having relative poses to parent entities. Also these entities can have additional poses relative to the entity; example:

[
  { id: "A", pose: {x: 0, y: 0, yaw: 0}, additionalPoses: [] },
  { id: "A.0", parentId: "A", pose: {x: 0, y: 0, yaw: 0}, additionalPoses: [] },
  { id: "A.1", parentId: "A", pose: {x: 100, y: 0, yaw: 0}, additionalPoses: [] },
  { id: "B", pose: {x: 200, y: 0, yaw: 0}, additionalPoses: [] },
  { id: "B.1", parentId: "B", pose: {x: 100, y: 0, yaw: 0}, additionalPoses: [
    { name: "B-i1", pose: {x: 0, y: 100, yaw: 0} },
    { name: "B-i2", position: {x: 0, y: 200, yaw: 0} }
  ]},
  { id: "C", pose: {x: 200, y: 200, yaw: 1.57}, additionalPoses: [
    { name: "C-i1", pose: {x: 100, y: 100, yaw: 0} },
    { name: "C-i2", pose: {x: 0, y: 100, yaw: 0} }
  ]}
]

Now I’m mapping this to GoJS as follows:

  • Every node is a group (since it can have children)
  • We pre-calculate the absolute location for every node since GoJS does not support relative positions for nodes w.r.t. their parent group
  • We treat the additionalPoses as an itemArray with relative position w.r.t. the entity that holds the poses

This is what I came up with:

<!DOCTYPE html>
<html>

<head>
  <title>Positions</title>
  <meta charset="UTF-8">
</head>

<body onload="init()">
  <div id="diagram" style="border: solid 1px black; width:100%; height:600px"></div>

  <script src="go.js"></script>
  <script id="code">
    function init() {
      var $ = go.GraphObject.make;

      // start group-rotating-tool.js
      /*
      *  Copyright (C) 1998-2017 by Northwoods Software Corporation. All Rights Reserved.
      */

      /**
      * @constructor
      * @class
      */
      function GroupRotatingTool() {
        go.RotatingTool.call(this);

        // Internal state
        // ._initialInfo holds references to all selected non-Link Parts and
        // their initial relative points and angles
        this._initialInfo = null;
        this._rotatePoint = new go.Point();
      }
      go.Diagram.inherit(GroupRotatingTool, go.RotatingTool);

      GroupRotatingTool.prototype.doActivate = function () {
        go.RotatingTool.prototype.doActivate.call(this);

        var group = this.adornedObject.part;
        if (group instanceof go.Group) {
          if (group.placeholder !== null) throw new Error("GroupRotatingTool can't handle Placeholder in Group");
          // assume rotation about the location point
          this._rotatePoint = group.location;
          // remember initial points for each Part
          var infos = new go.Map(go.Part, MultiplePartInfo);
          this.walkTree(group, infos);
          this._initialInfo = infos;
        }
      }

      GroupRotatingTool.prototype.walkTree = function (part, infos) {
        if (part === null || part instanceof go.Link) return;
        // saves original relative position and original angle
        var loc = part.locationObject.getDocumentPoint(go.Spot.Center);
        var locoffset = loc.copy().subtract(part.location);
        var relloc = loc.subtract(this._rotatePoint);
        infos.add(part, new MultiplePartInfo(relloc, locoffset, part.rotateObject.angle));
        // recurse into Groups
        if (part instanceof go.Group) {
          var it = part.memberParts.iterator;
          while (it.next()) this.walkTree(it.value, infos);
        }
      };

      function MultiplePartInfo(relativeloc, locoffset, rotationAngle) {
        this.relativeLocation = relativeloc;
        this.centerLocationOffset = locoffset;
        this.rotationAngle = rotationAngle;  // in degrees
      }

      /**
      * Rotate all members of a selected Group about the rotatePoint.
      * @this {GroupRotatingTool}
      * @param {number} newangle
      */
      GroupRotatingTool.prototype.rotate = function (newangle) {
        go.RotatingTool.prototype.rotate.call(this, newangle);

        var group = this.adornedObject.part;
        if (group instanceof go.Group) {
          var ang = newangle - this.originalAngle;
          var cp = this._rotatePoint;
          var it = this._initialInfo.iterator;
          while (it.next()) {
            var part = it.key;
            if (part instanceof go.Link) return; // only Nodes and simple Parts
            var info = it.value;

            part.rotateObject.angle = info.rotationAngle + ang;

            var loc = cp.copy().add(info.relativeLocation);
            var dir = cp.directionPoint(loc);
            var newrad = (ang + dir) * (Math.PI / 180);
            var locoffset = info.centerLocationOffset.copy();
            locoffset.rotate(ang);
            var dist = Math.sqrt(cp.distanceSquaredPoint(loc));
            part.location = new go.Point(cp.x + dist * Math.cos(newrad),
              cp.y + dist * Math.sin(newrad)).subtract(locoffset);
          }
        }
      }
      // end group-rotating-tool.js

      diagram = $(go.Diagram, "diagram",  // create a Diagram for the DIV HTML element
        {
          "undoManager.isEnabled": true,
          "grid.visible": true
        });

      diagram.toolManager.rotatingTool = new GroupRotatingTool();

      diagram.groupTemplate =
        $(go.Group, "Position",
          { locationSpot: go.Spot.Center, locationObjectName: "ORIGIN", rotatable: true },
          new go.Binding("location", "location"),
          new go.Binding("angle", "angle").makeTwoWay(),
          // $(go.Placeholder, { padding: 5}),
          $(go.Shape, "RoundedRectangle", {width: 100, height: 30, position: new go.Point(-50, -15), fill: "white", stroke: "black"}),
          $(go.Shape, "LineH", {width: 50, height: 5, position: new go.Point(2.5, 0)}, new go.Binding("stroke", "color")),
          $(go.Shape, "Circle", {name: "ORIGIN", width: 5, height: 5}, new go.Binding("fill", "color")),
          $(go.TextBlock, {margin: 5}, new go.Binding("text", "key"), new go.Binding("angle", "angle", (a) => -a)),
          $(go.Panel, "Position", new go.Binding("itemArray", "itemArray"),
          {
            itemTemplate: $(
              go.Panel,
              {
                fromLinkable: true,
                toLinkable: true,
                cursor: "pointer",
              },
              new go.Binding("position", "position"),
              $(go.Shape, "LineH", {width: 50, height: 5, position: new go.Point(2.5, 0), stroke: "cyan"}),
              $(go.Shape, "Circle", {width: 5, height: 5, fill: "cyan"}),
              $(go.TextBlock, new go.Binding("text", "name"), { margin: 5 })
            ), // end of itemTemplate
          })
        );

      diagram.linkTemplate =
        $(go.Link,
          { curve: go.Link.Bezier, adjusting: go.Link.Stretch, reshapable: true },
          $(go.Shape),
          $(go.Shape, { toArrow: "Standard" })
        );

        diagram.model = new go.GraphLinksModel([
          { key: "A", isGroup: true, color: "red", location: new go.Point(0, 0), angle: 0, itemArray: [] },
          { key: "A.0", group: "A", isGroup: true, color: "red", location: new go.Point(0, 0), angle: 0, itemArray: [] },
          { key: "A.1", group: "A", isGroup: true, color: "red", location: new go.Point(100, 0), angle: 0, itemArray: [] },
          { key: "B", isGroup: true, color: "blue", location: new go.Point(200, 0), angle: 0, itemArray: [] },
          { key: "B.1", group: "B", isGroup: true, color: "blue", location: new go.Point(300, 0), angle: 0, itemArray: [
            { name: "B-i1", position: new go.Point(0, 100) },
            { name: "B-i2", position: new go.Point(0, 200) }
          ]},
          { key: "C", isGroup: true, color: "lightgreen", location: new go.Point(200, 200), angle: 90, itemArray: [
            { name: "C-i1", position: new go.Point(100, 100) },
            { name: "C-i2", position: new go.Point(0, 100) },
            { name: "C-i3", position: new go.Point(-100, -100) }
          ]},
        ]);
    }
  </script>
</body>

</html>

Now I have the following questions:

  • Are the assumptions in my approach correct or would you approach the problem differently?
  • How can I deal with group placeholders? When I comment out the placeholder line in the group template, the locations of the nodes seem incorrect.
  • Not all shapes are correctly positioned; I would like to position my shapes always w.r.t. the specified location, how can I achieve this? I also don’t get the additional locations offset for all the shapes.
  • How to position items with a negative position? Now it seems that only positive positions are allowed, see C-i3
  • For the text labels of the nodes I have a binding that ensures that the text is always places horizontally; is this also possible for the text in the item array?

Hmmm, there’s a lot of possibilities and ambiguity in what you are presenting, but I’ll guess that you want to do something like:

<!DOCTYPE html>
<html>

<head>
  <title>Positions</title>
  <meta charset="UTF-8">
</head>

<body onload="init()">
  <div id="diagram" style="border: solid 1px black; width:100%; height:600px"></div>

  <script src="go.js"></script>
  <script id="code">
function init() {
  var $ = go.GraphObject.make;

  // start group-rotating-tool.js
  /*
  *  Copyright (C) 1998-2017 by Northwoods Software Corporation. All Rights Reserved.
  */

  /**
  * @constructor
  * @class
  */
  function GroupRotatingTool() {
    go.RotatingTool.call(this);

    // Internal state
    // ._initialInfo holds references to all selected non-Link Parts and
    // their initial relative points and angles
    this._initialInfo = null;
    this._rotatePoint = new go.Point();
  }
  go.Diagram.inherit(GroupRotatingTool, go.RotatingTool);

  GroupRotatingTool.prototype.doActivate = function () {
    go.RotatingTool.prototype.doActivate.call(this);

    var group = this.adornedObject.part;
    if (group instanceof go.Group) {
      if (group.placeholder !== null) throw new Error("GroupRotatingTool can't handle Placeholder in Group");
      // assume rotation about the location point
      this._rotatePoint = group.location;
      // remember initial points for each Part
      var infos = new go.Map(go.Part, MultiplePartInfo);
      this.walkTree(group, infos);
      this._initialInfo = infos;
    }
  }

  GroupRotatingTool.prototype.walkTree = function (part, infos) {
    if (part === null || part instanceof go.Link) return;
    // saves original relative position and original angle
    var loc = part.locationObject.getDocumentPoint(go.Spot.Center);
    var locoffset = loc.copy().subtract(part.location);
    var relloc = loc.subtract(this._rotatePoint);
    infos.add(part, new MultiplePartInfo(relloc, locoffset, part.rotateObject.angle));
    // recurse into Groups
    if (part instanceof go.Group) {
      var it = part.memberParts.iterator;
      while (it.next()) this.walkTree(it.value, infos);
    }
  };

  function MultiplePartInfo(relativeloc, locoffset, rotationAngle) {
    this.relativeLocation = relativeloc;
    this.centerLocationOffset = locoffset;
    this.rotationAngle = rotationAngle;  // in degrees
  }

  /**
  * Rotate all members of a selected Group about the rotatePoint.
  * @this {GroupRotatingTool}
  * @param {number} newangle
  */
  GroupRotatingTool.prototype.rotate = function (newangle) {
    go.RotatingTool.prototype.rotate.call(this, newangle);

    var group = this.adornedObject.part;
    if (group instanceof go.Group) {
      var ang = newangle - this.originalAngle;
      var cp = this._rotatePoint;
      var it = this._initialInfo.iterator;
      while (it.next()) {
        var part = it.key;
        if (part instanceof go.Link) return; // only Nodes and simple Parts
        var info = it.value;

        part.rotateObject.angle = info.rotationAngle + ang;

        var loc = cp.copy().add(info.relativeLocation);
        var dir = cp.directionPoint(loc);
        var newrad = (ang + dir) * (Math.PI / 180);
        var locoffset = info.centerLocationOffset.copy();
        locoffset.rotate(ang);
        var dist = Math.sqrt(cp.distanceSquaredPoint(loc));
        part.location = new go.Point(cp.x + dist * Math.cos(newrad),
          cp.y + dist * Math.sin(newrad)).subtract(locoffset);
      }
    }
  }
  // end group-rotating-tool.js

  diagram = $(go.Diagram, "diagram",  // create a Diagram for the DIV HTML element
    {
      "undoManager.isEnabled": true,
      "grid.visible": true
    });

  diagram.toolManager.rotatingTool = new GroupRotatingTool();

  diagram.groupTemplate =
    $(go.Group, "Position",
      { locationSpot: go.Spot.Center, locationObjectName: "ORIGIN", rotatable: true },
      new go.Binding("location", "location").makeTwoWay(),
      new go.Binding("angle", "angle").makeTwoWay(),
      $(go.Panel, "Position",
        $(go.Shape, "RoundedRectangle", { width: 100, height: 30, fill: "white", stroke: "black" }),
        $(go.Shape, "LineH", { width: 50, height: 5, position: new go.Point(52.5, 15) },
          new go.Binding("stroke", "color")),
        $(go.Shape, "Circle", { name: "ORIGIN", width: 5, height: 5, position: new go.Point(50, 15) },
          new go.Binding("fill", "color")),
        $(go.TextBlock, { position: new go.Point(3, 3) },
          new go.Binding("text", "key"),
          new go.Binding("angle", "angle", (a) => -a))
      ),
      $(go.Panel, "Position",
        new go.Binding("itemArray", "itemArray"),
        new go.Binding("position", "itemArray", a => {
          let x = 0; let y = 0;
          a.forEach(d => { x = Math.min(x, d.position.x); y = Math.min(y, d.position.y); });
          return new go.Point(x, y);
        }),
        {
          itemTemplate: $(go.Panel, "Position",
            { fromLinkable: true, toLinkable: true, cursor: "pointer" },
            new go.Binding("position", "position"),
            $(go.Shape, "LineH", { width: 50, height: 5, position: new go.Point(2.5, 0), stroke: "cyan" }),
            $(go.Shape, "Circle", { width: 5, height: 5, fill: "cyan" }),
            $(go.TextBlock, { position: new go.Point(5, 5) },
              new go.Binding("text", "name"),
              new go.Binding("angle", "angle", a => -a).ofObject("/"))
          ), // end of itemTemplate
        })
    );

  diagram.linkTemplate =
    $(go.Link,
      { curve: go.Link.Bezier, adjusting: go.Link.Stretch, reshapable: true },
      $(go.Shape),
      $(go.Shape, { toArrow: "Standard" })
    );

    diagram.model = new go.GraphLinksModel([
      { key: "A", isGroup: true, color: "red", location: new go.Point(0, 0), angle: 0, itemArray: [] },
      { key: "A.0", group: "A", isGroup: true, color: "red", location: new go.Point(0, 0), angle: 0, itemArray: [] },
      { key: "A.1", group: "A", isGroup: true, color: "red", location: new go.Point(100, 0), angle: 0, itemArray: [] },
      { key: "B", isGroup: true, color: "blue", location: new go.Point(200, 0), angle: 0, itemArray: [] },
      { key: "B.1", group: "B", isGroup: true, color: "blue", location: new go.Point(300, 0), angle: 0, itemArray: [
        { name: "B-i1", position: new go.Point(0, 100) },
        { name: "B-i2", position: new go.Point(0, 200) }
      ]},
      { key: "C", isGroup: true, color: "lightgreen", location: new go.Point(200, 200), angle: 90, itemArray: [
        { name: "C-i1", position: new go.Point(100, 100) },
        { name: "C-i2", position: new go.Point(0, 100) },
        { name: "C-i3", position: new go.Point(-100, -100) }
      ]},
    ]);
}
  </script>
</body>
</html>

[EDIT: I had forgotten to address your fourth point – I have updated the code.]

Thanks for the example Walter; I do have some comments though:

  • I would expect B-i1 to render at the red dot if I multiply the poses correctly: B.1 * B-i1 = (x=300, y=0, angle=0) * (x=0, y=100, angle=0) = (x=300, y=100, angle=0)

  • For some reason the RoundedRectangle shape is not aligned with the origin of the node:
    image
    image

  • Missing group placeholder
    image

Sidenote: correct absolute locations for the nodes and ports (rendered by the circles and the LineH) are really important for our application since it these locations are used as a real world reference for a robotic system.

I still do not understand exactly what the coordinate systems should be, so you will need to fix the “position” binding conversion functions.

I tried to address this in the first illustration I gave with the red dot. As I understand, all nodes coordinates are represented in the global frame with use of the location property but the port coordinates of the items are represented in the local node frame with the position property; is this correct?

If this is correct, let’s go back to my first illustration:

I added the global document coordinates here as a text overlay. So B.1 is positioned using the location property on global coordinates (300, 0): this seems correct according to the render. However, B-i1 should be positioned on the red since we have: global coordinates B.1 * local coordinates B-i1 = (300, 100). However, its position is somewhat around (250, 82.5) in document coordinates. Does this clear things up?

Maybe. In any case the positions of the Position Panels within the Group needs to be computed, and that can be done with the binding. So you will need to adapt the conversion function and/or add a binding to the first panel.

It would be easier if the things like “B-i1” were independent Parts, so they could just use location.

I understand but the thing I like is the relative coordinate systems of the items and I need to connect ports to the item. But I cannot wrap my head around how it is working exactly. For this example:

{ key: "B.1", group: "B", isGroup: true, color: "blue", location: new go.Point(300, 0), angle: 0, itemArray: [
        { name: "B-i1", position: new go.Point(0, 100) },
        { name: "B-i2", position: new go.Point(0, 200) }
      ]},

and this template:

$(go.Panel, "Position",
        new go.Binding("itemArray", "itemArray"),
        // new go.Binding("position", "itemArray", a => {
        //   let x = 0; let y = 0;
        //   a.forEach(d => { x = Math.min(x, d.position.x); y = Math.min(y, d.position.y); });
        //   return new go.Point(0, 0);
        // }),
        {
          itemTemplate: $(go.Panel, "Position",
            { fromLinkable: true, toLinkable: true, cursor: "pointer" },
            new go.Binding("position", "position"),
            $(go.Shape, "LineH", { width: 50, height: 5, position: new go.Point(2.5, 0), stroke: "cyan" }),
            $(go.Shape, "Circle", { width: 5, height: 5, fill: "cyan" }),
            $(go.TextBlock, { position: new go.Point(5, 5) },
              new go.Binding("text", "name"),
              new go.Binding("angle", "angle", a => -a).ofObject("/"))
          ), // end of itemTemplate
        })

image

Why do the B-i* items have different x coordinates than B.1?

They don’t – the “B1” RoundedRectangle Shape is at (0, 0) in the coordinate system of the Position Panel that is also a Group. Unless you have set/bound it differently, that is the default position.

Ok, but the location is set to (300,0) ; so why isn’t it rendering there? Due to the locationObjectName name setting? So this property is not influencing the position of the items?

The position and location of a Part is in document coordinates. So if the Group.location is (300, 0), that is where the group will be. Each Part is an atomic object as far as position or location are concerned.

The position of everything within the visual tree of a Part (in this case a Group) is determined by the Panel(s) and individual GraphObjects within the Panel(s). In my version of your group template, all of the panels are Position Panels. The Group Panel has two elements, each of which is a Position Panel. Each Panel establishes its own coordinate system. The GraphObject.position property controls the relative position of that object within its immediate container Panel, not within the Part – i.e. the grand-parent Panel, and not in document coordinates.

I suggested that you adapt the “position” Binding conversion function so that the relative positions of those two Position Panels are the way that you want.

This clears things up; I was expecting I was setting positions w.r.t. the parent part instead of the position panel. By updating the position property of the position panel, I seem to get the desired behavior:

<!DOCTYPE html>
<html>

<head>
  <title>Positions</title>
  <meta charset="UTF-8">
</head>

<body onload="init()">
  <div id="diagram" style="border: solid 1px black; width:100%; height:600px"></div>
  <span id="mousePosition" />

  <script src="go.js"></script>
  <script id="code">
function init() {
  var $ = go.GraphObject.make;

  // start group-rotating-tool.js
  /*
  *  Copyright (C) 1998-2017 by Northwoods Software Corporation. All Rights Reserved.
  */

  /**
  * @constructor
  * @class
  */
  function GroupRotatingTool() {
    go.RotatingTool.call(this);

    // Internal state
    // ._initialInfo holds references to all selected non-Link Parts and
    // their initial relative points and angles
    this._initialInfo = null;
    this._rotatePoint = new go.Point();
  }
  go.Diagram.inherit(GroupRotatingTool, go.RotatingTool);

  GroupRotatingTool.prototype.doActivate = function () {
    go.RotatingTool.prototype.doActivate.call(this);

    var group = this.adornedObject.part;
    if (group instanceof go.Group) {
      if (group.placeholder !== null) throw new Error("GroupRotatingTool can't handle Placeholder in Group");
      // assume rotation about the location point
      this._rotatePoint = group.location;
      // remember initial points for each Part
      var infos = new go.Map(go.Part, MultiplePartInfo);
      this.walkTree(group, infos);
      this._initialInfo = infos;
    }
  }

  GroupRotatingTool.prototype.walkTree = function (part, infos) {
    if (part === null || part instanceof go.Link) return;
    // saves original relative position and original angle
    var loc = part.locationObject.getDocumentPoint(go.Spot.Center);
    var locoffset = loc.copy().subtract(part.location);
    var relloc = loc.subtract(this._rotatePoint);
    infos.add(part, new MultiplePartInfo(relloc, locoffset, part.rotateObject.angle));
    // recurse into Groups
    if (part instanceof go.Group) {
      var it = part.memberParts.iterator;
      while (it.next()) this.walkTree(it.value, infos);
    }
  };

  function MultiplePartInfo(relativeloc, locoffset, rotationAngle) {
    this.relativeLocation = relativeloc;
    this.centerLocationOffset = locoffset;
    this.rotationAngle = rotationAngle;  // in degrees
  }

  /**
  * Rotate all members of a selected Group about the rotatePoint.
  * @this {GroupRotatingTool}
  * @param {number} newangle
  */
  GroupRotatingTool.prototype.rotate = function (newangle) {
    go.RotatingTool.prototype.rotate.call(this, newangle);

    var group = this.adornedObject.part;
    if (group instanceof go.Group) {
      var ang = newangle - this.originalAngle;
      var cp = this._rotatePoint;
      var it = this._initialInfo.iterator;
      while (it.next()) {
        var part = it.key;
        if (part instanceof go.Link) return; // only Nodes and simple Parts
        var info = it.value;

        part.rotateObject.angle = info.rotationAngle + ang;

        var loc = cp.copy().add(info.relativeLocation);
        var dir = cp.directionPoint(loc);
        var newrad = (ang + dir) * (Math.PI / 180);
        var locoffset = info.centerLocationOffset.copy();
        locoffset.rotate(ang);
        var dist = Math.sqrt(cp.distanceSquaredPoint(loc));
        part.location = new go.Point(cp.x + dist * Math.cos(newrad),
          cp.y + dist * Math.sin(newrad)).subtract(locoffset);
      }
    }
  }
  // end group-rotating-tool.js

  diagram = $(go.Diagram, "diagram",  // create a Diagram for the DIV HTML element
    {
      "undoManager.isEnabled": true,
      "grid.visible": true
    });

  diagram.toolManager.rotatingTool = new GroupRotatingTool();
  diagram.mouseOver = function(e) {
    document.getElementById("mousePosition").innerHTML = diagram.transformViewToDoc(e.viewPoint).toString()
  }

  diagram.groupTemplate =
    $(go.Group, "Position",
      { locationSpot: go.Spot.Center, locationObjectName: "ORIGIN", rotatable: true },
      new go.Binding("location", "location").makeTwoWay(),
      new go.Binding("angle", "angle").makeTwoWay(),
      // $(go.Placeholder, { padding: 5}),
        $(go.Shape, "RoundedRectangle", { width: 100, height: 30, fill: "white", stroke: "black", position: new go.Point(-47.5, -12.5)}),
        $(go.Shape, "LineH", { width: 50, height: 5, position: new go.Point(2.5, 0) },
          new go.Binding("stroke", "color")),
        $(go.Shape, "Circle", { name: "ORIGIN", width: 5, height: 5 },
          new go.Binding("fill", "color")),
        $(go.TextBlock, {margin: 5 },
          new go.Binding("text", "key"),
          new go.Binding("angle", "angle", (a) => -a)),
      $(go.Panel, "Position",
        new go.Binding("position", "itemArray", a => {
          let x = 0; let y = 0;
          a.forEach(d => { x = Math.min(x, d.position.x); y = Math.min(y, d.position.y); });
          return new go.Point(x, y);
        }),
        new go.Binding("itemArray", "itemArray"),
        {
          itemTemplate: $(go.Panel, "Position",
            { fromLinkable: true, toLinkable: true, cursor: "pointer" },
            new go.Binding("position", "position"),
            $(go.Shape, "LineH", { width: 50, height: 5, position: new go.Point(2.5, 0), stroke: "cyan" }),
            $(go.Shape, "Circle", { width: 5, height: 5, fill: "cyan" }),
            $(go.TextBlock, { position: new go.Point(5, 5) },
              new go.Binding("text", "name"),
              new go.Binding("angle", "angle", a => -a).ofObject("/"))
          ), // end of itemTemplate
        })
    );

  diagram.linkTemplate =
    $(go.Link,
      { curve: go.Link.Bezier, adjusting: go.Link.Stretch, reshapable: true },
      $(go.Shape),
      $(go.Shape, { toArrow: "Standard" })
    );

    diagram.model = new go.GraphLinksModel([
      { key: "A", isGroup: true, color: "red", location: new go.Point(0, 0), angle: 0, itemArray: [] },
      { key: "A.0", group: "A", isGroup: true, color: "red", location: new go.Point(0, 0), angle: 0, itemArray: [] },
      { key: "A.1", group: "A", isGroup: true, color: "red", location: new go.Point(100, 0), angle: 0, itemArray: [] },
      { key: "B", isGroup: true, color: "blue", location: new go.Point(200, 0), angle: 0, itemArray: [] },
      { key: "B.1", group: "B", isGroup: true, color: "blue", location: new go.Point(300, 0), angle: 0, itemArray: [
        { name: "B-i1", position: new go.Point(0, 100) },
        { name: "B-i2", position: new go.Point(0, 200) },
        { name: "B-i3", position: new go.Point(-100, 200) },
      ]},
      { key: "C", isGroup: true, color: "lightgreen", location: new go.Point(-200, -200), angle: 90, itemArray: [
        { name: "C-i1", position: new go.Point(100, 100) },
        { name: "C-i2", position: new go.Point(0, 100) },
        { name: "C-i3", position: new go.Point(-100, -100) }
      ]},
    ]);
}
  </script>
</body>
</html>

However, I am still struggling with the group placeholder. When I enable the placeholder, it seems that my coordinates are not respected anymore.

That is probably correct – by definition the Group.placeholder will have a size and position determined by the area covered by the Group.memberParts plus the Placeholder.padding.

And rotating a Group with a Placeholder isn’t likely to be what you want, since the area covered by the Placeholder is in document coordinates, not in any rotated coordinate system in the group’s visual tree.

Thanks, so can we conclude it is not possible to realize placeholder functionality if we care about individual component coordinates? Or can you think of another solution ?

The purpose of the Placeholder class is to provide particular functionality that is convenient for most kinds of Groups and Adornments. If you can’t use a Group.placeholder, you’ll have to implement the functionality for positioning and sizing yourself, including when resizing the group (if desired) and rotating the group (if desired).