1 | /*
|
2 | * Copyright (C) 1998-2021 by Northwoods Software Corporation. All Rights Reserved.
|
3 | */
|
4 | import * as go from '../release/go-module.js';
|
5 | /**
|
6 | * The RotateMultipleTool class lets the user rotate multiple objects at a time.
|
7 | * When more than one part is selected, rotates all parts, revolving them about their collective center.
|
8 | * If the control key is held down during rotation, rotates all parts individually.
|
9 | *
|
10 | * Caution: this only works for Groups that do *not* have a Placeholder.
|
11 | *
|
12 | * If you want to experiment with this extension, try the <a href="../../extensionsJSM/RotateMultiple.html">Rotate Multiple</a> sample.
|
13 | * @category Tool Extension
|
14 | */
|
15 | export class RotateMultipleTool extends go.RotatingTool {
|
16 | /**
|
17 | * Constructs a RotateMultipleTool and sets the name for the tool.
|
18 | */
|
19 | constructor() {
|
20 | super();
|
21 | /**
|
22 | * Holds references to all selected non-Link Parts and their offset & angles
|
23 | */
|
24 | this._initialInfo = null;
|
25 | /**
|
26 | * Initial angle when rotating as a whole
|
27 | */
|
28 | this._initialAngle = 0;
|
29 | /**
|
30 | * Rotation point of selection
|
31 | */
|
32 | this._centerPoint = new go.Point();
|
33 | this.name = 'RotateMultiple';
|
34 | }
|
35 | /**
|
36 | * Calls {@link RotatingTool#doActivate}, and then remembers the center point of the collection,
|
37 | * and the initial distances and angles of selected parts to the center.
|
38 | */
|
39 | doActivate() {
|
40 | super.doActivate();
|
41 | const diagram = this.diagram;
|
42 | // center point of the collection
|
43 | this._centerPoint = diagram.computePartsBounds(diagram.selection).center;
|
44 | // remember the angle relative to the center point when rotating the whole collection
|
45 | this._initialAngle = this._centerPoint.directionPoint(diagram.lastInput.documentPoint);
|
46 | // remember initial angle and distance for each Part
|
47 | const infos = new go.Map();
|
48 | const tool = this;
|
49 | diagram.selection.each(function (part) {
|
50 | tool.walkTree(part, infos);
|
51 | });
|
52 | this._initialInfo = infos;
|
53 | }
|
54 | /**
|
55 | * @hidden @internal
|
56 | */
|
57 | walkTree(part, infos) {
|
58 | if (part === null || part instanceof go.Link)
|
59 | return;
|
60 | // distance from _centerPoint to locationSpot of part
|
61 | const dist = Math.sqrt(this._centerPoint.distanceSquaredPoint(part.location));
|
62 | // calculate initial relative angle
|
63 | const dir = this._centerPoint.directionPoint(part.location);
|
64 | // saves part-angle combination in array
|
65 | infos.add(part, new PartInfo(dir, dist, part.rotateObject.angle));
|
66 | // recurse into Groups
|
67 | if (part instanceof go.Group) {
|
68 | const it = part.memberParts.iterator;
|
69 | while (it.next())
|
70 | this.walkTree(it.value, infos);
|
71 | }
|
72 | }
|
73 | /**
|
74 | * Clean up any references to Parts.
|
75 | */
|
76 | doDeactivate() {
|
77 | this._initialInfo = null;
|
78 | super.doDeactivate();
|
79 | }
|
80 | /**
|
81 | * Rotate all selected objects about their collective center.
|
82 | * When the control key is held down while rotating, all selected objects are rotated individually.
|
83 | */
|
84 | rotate(newangle) {
|
85 | const diagram = this.diagram;
|
86 | if (this._initialInfo === null)
|
87 | return;
|
88 | const node = this.adornedObject !== null ? this.adornedObject.part : null;
|
89 | if (node === null)
|
90 | return;
|
91 | const e = diagram.lastInput;
|
92 | // when rotating individual parts, remember the original angle difference
|
93 | const angleDiff = newangle - node.rotateObject.angle;
|
94 | const tool = this;
|
95 | this._initialInfo.each(function (kvp) {
|
96 | const part = kvp.key;
|
97 | if (part instanceof go.Link)
|
98 | return; // only Nodes and simple Parts
|
99 | const partInfo = kvp.value;
|
100 | // rotate every selected non-Link Part
|
101 | // find information about the part set in RotateMultipleTool._initialInformation
|
102 | if (e.control || e.meta) {
|
103 | if (node === part) {
|
104 | part.rotateObject.angle = newangle;
|
105 | }
|
106 | else {
|
107 | part.rotateObject.angle += angleDiff;
|
108 | }
|
109 | }
|
110 | else {
|
111 | const radAngle = newangle * (Math.PI / 180); // converts the angle traveled from degrees to radians
|
112 | // calculate the part's x-y location relative to the central rotation point
|
113 | const offsetX = partInfo.distance * Math.cos(radAngle + partInfo.placementAngle);
|
114 | const offsetY = partInfo.distance * Math.sin(radAngle + partInfo.placementAngle);
|
115 | // move part
|
116 | part.location = new go.Point(tool._centerPoint.x + offsetX, tool._centerPoint.y + offsetY);
|
117 | // rotate part
|
118 | part.rotateObject.angle = partInfo.rotationAngle + newangle;
|
119 | }
|
120 | });
|
121 | }
|
122 | /**
|
123 | * Calculate the desired angle with different rotation points,
|
124 | * depending on whether we are rotating the whole selection as one, or Parts individually.
|
125 | * @param {Point} newPoint in document coordinates
|
126 | */
|
127 | computeRotate(newPoint) {
|
128 | const diagram = this.diagram;
|
129 | if (this.adornedObject === null)
|
130 | return 0.0;
|
131 | let angle = 0.0;
|
132 | const e = diagram.lastInput;
|
133 | if (e.control || e.meta) { // relative to the center of the Node whose handle we are rotating
|
134 | const part = this.adornedObject.part;
|
135 | if (part !== null) {
|
136 | const rotationPoint = part.getDocumentPoint(part.locationSpot);
|
137 | angle = rotationPoint.directionPoint(newPoint);
|
138 | }
|
139 | }
|
140 | else { // relative to the center of the whole selection
|
141 | angle = this._centerPoint.directionPoint(newPoint) - this._initialAngle;
|
142 | }
|
143 | if (angle >= 360)
|
144 | angle -= 360;
|
145 | else if (angle < 0)
|
146 | angle += 360;
|
147 | const interval = Math.min(Math.abs(this.snapAngleMultiple), 180);
|
148 | const epsilon = Math.min(Math.abs(this.snapAngleEpsilon), interval / 2);
|
149 | // if it's close to a multiple of INTERVAL degrees, make it exactly so
|
150 | if (!diagram.lastInput.shift && interval > 0 && epsilon > 0) {
|
151 | if (angle % interval < epsilon) {
|
152 | angle = Math.floor(angle / interval) * interval;
|
153 | }
|
154 | else if (angle % interval > interval - epsilon) {
|
155 | angle = (Math.floor(angle / interval) + 1) * interval;
|
156 | }
|
157 | if (angle >= 360)
|
158 | angle -= 360;
|
159 | else if (angle < 0)
|
160 | angle += 360;
|
161 | }
|
162 | return angle;
|
163 | }
|
164 | }
|
165 | /**
|
166 | * Internal class to remember a Part's offset and angle.
|
167 | */
|
168 | class PartInfo {
|
169 | constructor(placementAngle, distance, rotationAngle) {
|
170 | this.placementAngle = placementAngle * (Math.PI / 180); // in radians
|
171 | this.distance = distance;
|
172 | this.rotationAngle = rotationAngle; // in degrees
|
173 | }
|
174 | }
|