Re-position member nodes when dragging/copying to another group

How can we re-position member nodes when dragging/copying to another group so that the member nodes stay inside the group.
Currently they hold their position, can they be re-positioned so that as soon as I release my mouse, they forget their original positions and take up space inside the new group.

drag

That is the responsibility of the Group.layout.

OK, so I saw the GoJS SubGraphs -- Northwoods Software page,
I dont want my groups to have organized nodes since position of nodes hold value to my application, I only want to organize them inside the group when dragged or copied to another group.

Presumably you are calling Group | GoJS API in your code, probably in a GraphObject | GoJS API event handler.

At that time you can get the area that the group has, either the Group.actualBounds or by calling GraphObject.getDocumentBounds on the Group.placeholder. And you can arrange the node in the Diagram.selection accordingly. Remember that there might not be room if there is no Placeholder, so the nodes might need to overlap each other.

coming a little late on this, I implemented it like below, but I have a couple of queries here ( TODO-1, TODO-2 below ), but I do not have a placeholder in place.

mouseDrop(e, grp) {
          const isGroup = grp.diagram.selection.iterator.any(
            x => x instanceof go.Group
          );

          if (isGroup) {
            grp.diagram.currentTool.doCancel();
          }
          else {
            grp["addMembers"](grp.diagram.selection, true);
            grp.diagram.selection.each(a => {
              //**TODO-1**: do this only if node fall inside the boundaries of the group{
                a.data.groupId = grp["data"].groupId;
                a.data.loc = go.Point.stringify(grp.getDocumentPoint(go.Spot.TopLeft));
                //**TODO-2**: find available space starting from TopLeft, but do not overlap nodes.
                a.updateTargetBindings();
              //TODO-1: }

            });
          }
        }

Calling Group.addMembers will modify the node data object to make them members of the group data object. So there is no need to set groupId if GraphLinksModel.nodeGroupKeyProperty == “groupId”. In fact if you want to be selective about it, you should not call Group.addMembers. You can set Part.containingGroup on each Node if desired.

So there is no Group.layout that is automatically laying out its members? OK, so you need to do it yourself. You can call Diagram.findPartsIn to see if a particular area in document coordinates is empty of Nodes. Remember to ignore the Group itself.

I recall that findPartsIn returns the gojs entities inside the area, how do I return location coordinates of the empty area in the group ?

Secondly, what check can i put to know if the node falls outside the group bounds ?

What “empty area in the group”? There might be none, or there might be many, some of which are too small for your purposes.

I suppose you could check whether group.actualBounds.containsRect(n.actualBounds is false.

Yes, there might be none, or many, some of which may be too small for my purposes.

How do I find place for my dragged node in the empty area after checking for findPartsIn ?

If you are not going to depend on an automatic layout to position nodes to avoid overlaps, then you need to figure out where to put the nodes. I was merely suggesting that if you have a proposed location for a node you could call Diagram.findPartsIn to see if there are already any other Parts (partially) occupying that area.

Maybe we could implement a function that would return all of the empty areas larger than a certain size within a rectangular area. That would make it easier to decide where to locate a node.

“Maybe we could implement a function that would return all of the empty areas larger than a certain size within a rectangular area. That would make it easier to decide where to locate a node.”
– This is exactly what I was expecting.
is there something I could start with ?

No, there isn’t. Most people have an idea of where the node should go, so they keep trying until they find an empty area. In your case you should be aware that there might not be any such place in a fixed area group. In that event I hope you will expand the size of your group, in which case you will know exactly where the extra empty space is.

Try this:

<!DOCTYPE html>
<html>
<head>
<title>Finding an empty spot within an area</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script id="code">
  function init() {
    var $ = go.GraphObject.make;

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

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        $(go.Shape, { fill: "white" },
          new go.Binding("fill", "color")),
        $(go.TextBlock, { margin: 8 },
          new go.Binding("text"))
      );

    myDiagram.model = new go.GraphLinksModel(
    [
      { key: 1, text: "Alpha", color: "lightblue" },
      { key: 2, text: "Beta", color: "orange" },
      { key: 3, text: "Gamma", color: "lightgreen" },
      { key: 4, text: "Delta", color: "pink" }
    ],
    [
      { from: 1, to: 2 },
      { from: 1, to: 3 },
      { from: 2, to: 2 },
      { from: 3, to: 4 },
      { from: 4, to: 1 }
    ]);
  }

  function test() {
    myDiagram.commit(function(diag) {
      var data = { text: "NEW " + diag.nodes.count };

      // search in a band starting with the current documentBounds up to 1000
      // in the direction of ANGLE
      var bounds = diag.documentBounds.copy().subtractMargin(diag.padding);
      var angle = 0;

      if (angle === 0) bounds.width = 1000;
      else if (angle === 90) bounds.height = 1000;
      else if (angle === 180) { bounds.width = 1000; bounds.x -= 1000; }
      else if (angle === 270) { bounds.height = 1000; bounds.y -= 1000; }

      // temporarily add the data/node just so we can find out how big it really is
      var oldskips = myDiagram.skipsUndoManager;
      diag.skipsUndoManager = true;  // ignore all side-effects from adding the node temporarily
      diag.model.addNodeData(data);  // add the node data temporarily
      var node = diag.findNodeForData(data);  // find the corresponding Node
      node.position = bounds.position;  // temporarily put it within the document bounds
      node.ensureBounds();  // make sure the node has been measured and arranged
      var nodesize = node.actualBounds.size;  // and remember its size
      diag.model.removeNodeData(data);  // remove the temporary node
      diag.skipsUndoManager = oldskips;

      var found = findUnoccupiedRect(myDiagram, bounds, angle, nodesize);
      if (found) {  // found an empty area?
        // now add the node for real, and position it in the empty area
        diag.model.addNodeData(data);
        node = diag.findNodeForData(data);
        node.position = found.position;
      }
    });
  }

  // BOUNDS specifies the area to search for empty space (no Avoidable Nodes) of at least size SIZE.
  // ANGLE specifies the direction (0, 90, 180, 270) in which to search the area.
  // SKIP is an optional Node that should be ignored when checking for existing Avoidable Nodes in the area.
  function findUnoccupiedRect(diag, bounds, angle, size, skip) {
    if (!diag) return null;
    if (skip === undefined) skip = null;
    var r = new go.Rect(bounds.position, size);
    var a, b, ma, mb, da, db;
    if (angle === 0) {
      a = bounds.left; ma = bounds.right - size.width; da = 8;
      b = bounds.top; mb = bounds.bottom - size.height; db = 8;
    } else if (angle === 90) {
      a = bounds.top; ma = bounds.bottom - size.height; da = 8;
      b = bounds.left; ma = bounds.right - size.width; db = 8;
    } else if (angle === 180) {
      a = bounds.right - size.width; ma = bounds.left; da = -8;
      b = bounds.top; mb = bounds.bottom - size.height; db = 8;
    } else if (angle === 270) {
      a = bounds.bottom - size.height; ma = bounds.top; da = -8;
      b = bounds.left; ma = bounds.right - size.width; db = 8;
    } else {
      throw new Error("unknown angle for findUnoccupiedRect: " + angle);
    }
    var s = b;
    for (; (da > 0) ? a < ma : a > ma; a += da) {
      if (angle === 0 || angle === 180) {
        r.x = a;
      } else {
        r.y = a;
      }
      for (; (db > 0) ? b < mb : b > mb; b += db) {
        if (angle === 0 || angle === 180) {
          r.y = b;
        } else {
          r.x = b;
        }
        var empty = diag.isUnoccupied(r, skip);
        if (empty) return r;
      }
      b = s;
    }
    return null;
  }
</script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:500px"></div>
  <button onclick="test()">Test</button>
</body>
</html>