Rotate a Group

Hi there-

We have a requirement to be able to rotate a group (and all of its nodes). I have searched the forums and documents and noticed in the Groups intro “At the current time groups are not rotatable”. Does that mean it is not really possible or just that I would need to implement it myself? Do you have any advice regarding this?

thank you!

The user can rotate a collection of nodes if you use the RotateMultipleTool extension: http://gojs.net/latest/extensions/RotateMultipleTool.js

The problem is if the Group has a Placeholder, changing the angle of member nodes and revolving their locations about some central point will cause the union of their bounds to change, thereby resizing the Group.

But there’s no reason it shouldn’t work when there’s no Placeholder. We’ll put this in the next release:

"use strict";
/*
*  Copyright (C) 1998-2016 by Northwoods Software Corporation. All Rights Reserved.
*/

/**
* @constructor
* @extends RotatingTool
* @class
* A custom tool for rotating multiple objects at a time. When more than one
* part is selected, rotates all parts, revolving them about their collective center.
* If the control key is held down during rotation, rotates all parts individually.
* <p>
* Caution: this only works for Groups that do *not* have a Placeholder.
*/
function RotateMultipleTool() {
  go.RotatingTool.call(this);
  this.name = "RotateMultiple";
  // holds references to all selected non-Link Parts and their offset & angles
  this.initialInfo = null;
  // initial angle when rotating as a whole
  this.initialAngle = 0;
  // rotation point of selection
  this.centerPoint = null;
 }
go.Diagram.inherit(RotateMultipleTool, go.RotatingTool);

/**
* Calls RotatingTool.doActivate, and then remembers the center point of the collection,
* and the initial distances and angles of selected parts to the center.
* @this {RotateMultipleTool}
*/
RotateMultipleTool.prototype.doActivate = function() {
  go.RotatingTool.prototype.doActivate.call(this);
  var diagram = this.diagram;
  // center point of the collection
  this.centerPoint = diagram.computePartsBounds(diagram.selection).center;

  // remember the angle relative to the center point when rotating the whole collection
  this.initialAngle = this.centerPoint.directionPoint(diagram.lastInput.documentPoint);

  // remember initial angle and distance for each Part
  var infos = new go.Map(go.Part, PartInfo);
  var tool = this;
  diagram.selection.each(function(part) {
    tool.walkTree(part, infos);
  });
  this.initialInfo = infos;
}

/**
* @ignore
* @param {Part} part
* @param {Map} infos
*/
RotateMultipleTool.prototype.walkTree = function(part, infos) {
  if (part === null || part instanceof go.Link) return;
  // distance from centerPoint to locationSpot of part
  var dist = Math.sqrt(this.centerPoint.distanceSquaredPoint(part.location));
  // calculate initial relative angle
  var dir = this.centerPoint.directionPoint(part.location);
  // saves part-angle combination in array
  infos.add(part, new PartInfo(dir, dist, part.rotateObject.angle));
  // recurse into Groups
  if (part instanceof go.Group) {
    var it = part.memberParts.iterator;
    while (it.next()) this.walkTree(it.value, infos);
  }
};

/**
* @ignore
* Internal class that remembers a Part's offset & angle.
*/
function PartInfo(placementAngle, distance, rotationAngle) {
  this.placementAngle = placementAngle * (Math.PI / 180);  // in radians
  this.distance = distance;
  this.rotationAngle = rotationAngle;  // in degrees
}

/**
* Clean up any references to Parts.
* @this {RotateMultipleTool}
*/
RotateMultipleTool.prototype.doDeactivate = function() {
  this.initialInfo = null;
  go.RotatingTool.prototype.doDeactivate.call(this);
};

/**
* Overrides rotatingTool.rotate to rotate all selected objects about their collective center.
* When the control key is held down while rotating, all selected objects are rotated individually.
* @this {RotateMultipleTool}
* @param {number} newangle
*/
RotateMultipleTool.prototype.rotate = function(newangle) {
  var diagram = this.diagram;
  var e = diagram.lastInput;
  // when rotating individual parts, remember the original angle difference
  var angleDiff = newangle - this.adornedObject.part.rotateObject.angle;
  var tool = this;
  this.initialInfo.each(function(kvp) {
    var part = kvp.key;
    if (part instanceof go.Link) return; // only Nodes and simple Parts
    var partInfo = kvp.value;
    // rotate every selected non-Link Part
    // find information about the part set in RotateMultipleTool.initialInformation
    if (e.control || e.meta) {
      if (tool.adornedObject.part === part) {
        part.rotateObject.angle = newangle;
      } else {
        part.rotateObject.angle += angleDiff;
      }
    } else {
      var radAngle = newangle * (Math.PI / 180); // converts the angle traveled from degrees to radians
      // calculate the part's x-y location relative to the central rotation point
      var offsetX = partInfo.distance * Math.cos(radAngle + partInfo.placementAngle);
      var offsetY = partInfo.distance * Math.sin(radAngle + partInfo.placementAngle);
      // move part
      part.location = new go.Point(tool.centerPoint.x + offsetX, tool.centerPoint.y + offsetY);
      // rotate part
      part.rotateObject.angle = partInfo.rotationAngle + newangle;
    }
  });
}

/**
* This override needs to calculate the desired angle with different rotation points,
* depending on whether we are rotating the whole selection as one, or Parts individually.
* @this {RotateMultipleTool}
* @param {Point} newPoint in document coordinates
*/
RotateMultipleTool.prototype.computeRotate = function(newPoint) {
  var diagram = this.diagram;
  var angle;
  var e = diagram.lastInput;
  if (e.control || e.meta) {  // relative to the center of the Node whose handle we are rotating
    var part = this.adornedObject.part;
    var rotationPoint = part.getDocumentPoint(part.locationSpot);
    angle = rotationPoint.directionPoint(newPoint);
  } else {  // relative to the center of the whole selection
    angle = this.centerPoint.directionPoint(newPoint) - this.initialAngle;
  }
  if (angle >= 360) angle -= 360;
  else if (angle < 0) angle += 360;
  var interval = Math.min(Math.abs(this.snapAngleMultiple), 180);
  var epsilon = Math.min(Math.abs(this.snapAngleEpsilon), interval / 2);
  // if it's close to a multiple of INTERVAL degrees, make it exactly so
  if (!diagram.lastInput.shift && interval > 0 && epsilon > 0) {
    if (angle % interval < epsilon) {
      angle = Math.floor(angle / interval) * interval;
    } else if (angle % interval > interval - epsilon) {
      angle = (Math.floor(angle / interval) + 1) * interval;
    }
    if (angle >= 360) angle -= 360;
    else if (angle < 0) angle += 360;
  }
  return angle;
};

Hi Walter,
just wanted to know if this issue of placeholder is fixed in latest release or not?

We did not add such a new feature in v1.7.