1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | import * as go from '../release/go-module.js';
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | export class DrawCommandHandler extends go.CommandHandler {
|
38 | private _arrowKeyBehavior: string = 'move';
|
39 | private _pasteOffset: go.Point = new go.Point(10, 10);
|
40 | private _lastPasteOffset: go.Point = new go.Point(0, 0);
|
41 |
|
42 | |
43 |
|
44 |
|
45 |
|
46 |
|
47 | get arrowKeyBehavior(): string { return this._arrowKeyBehavior; }
|
48 | set arrowKeyBehavior(val: string) {
|
49 | if (val !== 'move' && val !== 'select' && val !== 'scroll' && val !== 'none') {
|
50 | throw new Error('DrawCommandHandler.arrowKeyBehavior must be either "move", "select", "scroll", or "none", not: ' + val);
|
51 | }
|
52 | this._arrowKeyBehavior = val;
|
53 | }
|
54 |
|
55 | |
56 |
|
57 |
|
58 | get pasteOffset(): go.Point { return this._pasteOffset; }
|
59 | set pasteOffset(val: go.Point) {
|
60 | if (!(val instanceof go.Point)) throw new Error('DrawCommandHandler.pasteOffset must be a Point, not: ' + val);
|
61 | this._pasteOffset.set(val);
|
62 | }
|
63 |
|
64 | |
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | public canAlignSelection(): boolean {
|
73 | const diagram = this.diagram;
|
74 | if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
|
75 | if (diagram.selection.count < 2) return false;
|
76 | return true;
|
77 | }
|
78 |
|
79 | |
80 |
|
81 |
|
82 | public alignLeft(): void {
|
83 | const diagram = this.diagram;
|
84 | diagram.startTransaction('aligning left');
|
85 | let minPosition = Infinity;
|
86 | diagram.selection.each((current) => {
|
87 | if (current instanceof go.Link) return;
|
88 | minPosition = Math.min(current.position.x, minPosition);
|
89 | });
|
90 | diagram.selection.each((current) => {
|
91 | if (current instanceof go.Link) return;
|
92 | current.move(new go.Point(minPosition, current.position.y));
|
93 | });
|
94 | diagram.commitTransaction('aligning left');
|
95 | }
|
96 |
|
97 | |
98 |
|
99 |
|
100 | public alignRight(): void {
|
101 | const diagram = this.diagram;
|
102 | diagram.startTransaction('aligning right');
|
103 | let maxPosition = -Infinity;
|
104 | diagram.selection.each((current) => {
|
105 | if (current instanceof go.Link) return;
|
106 | const rightSideLoc = current.actualBounds.x + current.actualBounds.width;
|
107 | maxPosition = Math.max(rightSideLoc, maxPosition);
|
108 | });
|
109 | diagram.selection.each((current) => {
|
110 | if (current instanceof go.Link) return;
|
111 | current.move(new go.Point(maxPosition - current.actualBounds.width, current.position.y));
|
112 | });
|
113 | diagram.commitTransaction('aligning right');
|
114 | }
|
115 |
|
116 | |
117 |
|
118 |
|
119 | public alignTop(): void {
|
120 | const diagram = this.diagram;
|
121 | diagram.startTransaction('alignTop');
|
122 | let minPosition = Infinity;
|
123 | diagram.selection.each((current) => {
|
124 | if (current instanceof go.Link) return;
|
125 | minPosition = Math.min(current.position.y, minPosition);
|
126 | });
|
127 | diagram.selection.each((current) => {
|
128 | if (current instanceof go.Link) return;
|
129 | current.move(new go.Point(current.position.x, minPosition));
|
130 | });
|
131 | diagram.commitTransaction('alignTop');
|
132 | }
|
133 |
|
134 | |
135 |
|
136 |
|
137 | public alignBottom(): void {
|
138 | const diagram = this.diagram;
|
139 | diagram.startTransaction('aligning bottom');
|
140 | let maxPosition = -Infinity;
|
141 | diagram.selection.each((current) => {
|
142 | if (current instanceof go.Link) return;
|
143 | const bottomSideLoc = current.actualBounds.y + current.actualBounds.height;
|
144 | maxPosition = Math.max(bottomSideLoc, maxPosition);
|
145 | });
|
146 | diagram.selection.each((current) => {
|
147 | if (current instanceof go.Link) return;
|
148 | current.move(new go.Point(current.actualBounds.x, maxPosition - current.actualBounds.height));
|
149 | });
|
150 | diagram.commitTransaction('aligning bottom');
|
151 | }
|
152 |
|
153 | |
154 |
|
155 |
|
156 | public alignCenterX(): void {
|
157 | const diagram = this.diagram;
|
158 | const firstSelection = diagram.selection.first();
|
159 | if (!firstSelection) return;
|
160 | diagram.startTransaction('aligning Center X');
|
161 | const centerX = firstSelection.actualBounds.x + firstSelection.actualBounds.width / 2;
|
162 | diagram.selection.each((current) => {
|
163 | if (current instanceof go.Link) return;
|
164 | current.move(new go.Point(centerX - current.actualBounds.width / 2, current.actualBounds.y));
|
165 | });
|
166 | diagram.commitTransaction('aligning Center X');
|
167 | }
|
168 |
|
169 |
|
170 | |
171 |
|
172 |
|
173 | public alignCenterY(): void {
|
174 | const diagram = this.diagram;
|
175 | const firstSelection = diagram.selection.first();
|
176 | if (!firstSelection) return;
|
177 | diagram.startTransaction('aligning Center Y');
|
178 | const centerY = firstSelection.actualBounds.y + firstSelection.actualBounds.height / 2;
|
179 | diagram.selection.each((current) => {
|
180 | if (current instanceof go.Link) return;
|
181 | current.move(new go.Point(current.actualBounds.x, centerY - current.actualBounds.height / 2));
|
182 | });
|
183 | diagram.commitTransaction('aligning Center Y');
|
184 | }
|
185 |
|
186 |
|
187 | |
188 |
|
189 |
|
190 |
|
191 | public alignColumn(distance: number): void {
|
192 | const diagram = this.diagram;
|
193 | diagram.startTransaction('align Column');
|
194 | if (distance === undefined) distance = 0;
|
195 | distance = parseFloat(distance.toString());
|
196 | const selectedParts = new Array();
|
197 | diagram.selection.each((current) => {
|
198 | if (current instanceof go.Link) return;
|
199 | selectedParts.push(current);
|
200 | });
|
201 | for (let i = 0; i < selectedParts.length - 1; i++) {
|
202 | const current = selectedParts[i];
|
203 |
|
204 | const curBottomSideLoc = current.actualBounds.y + current.actualBounds.height + distance;
|
205 | const next = selectedParts[i + 1];
|
206 | next.move(new go.Point(current.actualBounds.x, curBottomSideLoc));
|
207 | }
|
208 | diagram.commitTransaction('align Column');
|
209 | }
|
210 |
|
211 | |
212 |
|
213 |
|
214 |
|
215 | public alignRow(distance: number): void {
|
216 | if (distance === undefined) distance = 0;
|
217 | distance = parseFloat(distance.toString());
|
218 | const diagram = this.diagram;
|
219 | diagram.startTransaction('align Row');
|
220 | const selectedParts = new Array();
|
221 | diagram.selection.each((current) => {
|
222 | if (current instanceof go.Link) return;
|
223 | selectedParts.push(current);
|
224 | });
|
225 | for (let i = 0; i < selectedParts.length - 1; i++) {
|
226 | const current = selectedParts[i];
|
227 |
|
228 | const curRightSideLoc = current.actualBounds.x + current.actualBounds.width + distance;
|
229 | const next = selectedParts[i + 1];
|
230 | next.move(new go.Point(curRightSideLoc, current.actualBounds.y));
|
231 | }
|
232 | diagram.commitTransaction('align Row');
|
233 | }
|
234 |
|
235 | |
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 | public canRotate(): boolean {
|
243 | const diagram = this.diagram;
|
244 | if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
|
245 | if (diagram.selection.count < 1) return false;
|
246 | return true;
|
247 | }
|
248 |
|
249 | |
250 |
|
251 |
|
252 |
|
253 |
|
254 | public rotate(angle: number): void {
|
255 | if (angle === undefined) angle = 90;
|
256 | const diagram = this.diagram;
|
257 | diagram.startTransaction('rotate ' + angle.toString());
|
258 | diagram.selection.each((current) => {
|
259 | if (current instanceof go.Link || current instanceof go.Group) return;
|
260 | current.angle += angle;
|
261 | });
|
262 | diagram.commitTransaction('rotate ' + angle.toString());
|
263 | }
|
264 |
|
265 |
|
266 | |
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 | public pullToFront(): void {
|
273 | const diagram = this.diagram;
|
274 | diagram.startTransaction("pullToFront");
|
275 |
|
276 | const layers = new go.Map<go.Layer, number>();
|
277 | diagram.selection.each(function(part) {
|
278 | if (part.layer !== null) layers.set(part.layer, 0);
|
279 | });
|
280 |
|
281 | layers.iteratorKeys.each(function(layer) {
|
282 | let max = 0;
|
283 | layer.parts.each(function(part) {
|
284 | if (part.isSelected) return;
|
285 | const z = part.zOrder;
|
286 | if (isNaN(z)) {
|
287 | part.zOrder = 0;
|
288 | } else {
|
289 | max = Math.max(max, z);
|
290 | }
|
291 | });
|
292 | layers.set(layer, max);
|
293 | });
|
294 |
|
295 | diagram.selection.each(function(part) {
|
296 | const z = layers.get(part.layer as go.Layer) || 0;
|
297 | DrawCommandHandler._assignZOrder(part, z + 1);
|
298 | });
|
299 | diagram.commitTransaction("pullToFront");
|
300 | }
|
301 |
|
302 | |
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 | public pushToBack(): void {
|
309 | const diagram = this.diagram;
|
310 | diagram.startTransaction("pushToBack");
|
311 |
|
312 | const layers = new go.Map<go.Layer, number>();
|
313 | diagram.selection.each(function(part) {
|
314 | if (part.layer !== null) layers.set(part.layer, 0);
|
315 | });
|
316 |
|
317 | layers.iteratorKeys.each(function(layer) {
|
318 | let min = 0;
|
319 | layer.parts.each(function(part) {
|
320 | if (part.isSelected) return;
|
321 | const z = part.zOrder;
|
322 | if (isNaN(z)) {
|
323 | part.zOrder = 0;
|
324 | } else {
|
325 | min = Math.min(min, z);
|
326 | }
|
327 | });
|
328 | layers.set(layer, min);
|
329 | });
|
330 |
|
331 | diagram.selection.each(function(part) {
|
332 | const z = layers.get(part.layer as go.Layer) || 0;
|
333 | DrawCommandHandler._assignZOrder(part,
|
334 |
|
335 | z - 1 - DrawCommandHandler._findGroupDepth(part));
|
336 | });
|
337 | diagram.commitTransaction("pushToBack");
|
338 | }
|
339 |
|
340 | private static _assignZOrder(part: go.Part, z: number, root?: go.Part): void {
|
341 | if (root === undefined) root = part;
|
342 | if (part.layer === root.layer) part.zOrder = z;
|
343 | if (part instanceof go.Group) {
|
344 | part.memberParts.each(function(m) {
|
345 | DrawCommandHandler._assignZOrder(m, z+1, root);
|
346 | });
|
347 | }
|
348 | }
|
349 |
|
350 | private static _findGroupDepth(part: go.Part): number {
|
351 | if (part instanceof go.Group) {
|
352 | let d = 0;
|
353 | part.memberParts.each(function(m) {
|
354 | d = Math.max(d, DrawCommandHandler._findGroupDepth(m));
|
355 | });
|
356 | return d+1;
|
357 | } else {
|
358 | return 0;
|
359 | }
|
360 | }
|
361 |
|
362 |
|
363 | |
364 |
|
365 |
|
366 |
|
367 |
|
368 | public doKeyDown(): void {
|
369 | const diagram = this.diagram;
|
370 | const e = diagram.lastInput;
|
371 |
|
372 |
|
373 | if (e.key === 'Up' || e.key === 'Down' || e.key === 'Left' || e.key === 'Right') {
|
374 | const behavior = this.arrowKeyBehavior;
|
375 | if (behavior === 'none') {
|
376 |
|
377 | return;
|
378 | } else if (behavior === 'select') {
|
379 | this._arrowKeySelect();
|
380 | return;
|
381 | } else if (behavior === 'move') {
|
382 | this._arrowKeyMove();
|
383 | return;
|
384 | }
|
385 |
|
386 | }
|
387 |
|
388 |
|
389 | super.doKeyDown();
|
390 | }
|
391 |
|
392 | |
393 |
|
394 |
|
395 | private _getAllParts(): Array<any> {
|
396 | const allParts = new Array();
|
397 | this.diagram.nodes.each((node) => { allParts.push(node); });
|
398 | this.diagram.parts.each((part) => { allParts.push(part); });
|
399 |
|
400 | return allParts;
|
401 | }
|
402 |
|
403 | |
404 |
|
405 |
|
406 | private _arrowKeyMove(): void {
|
407 | const diagram = this.diagram;
|
408 | const e = diagram.lastInput;
|
409 |
|
410 | let vdistance = 0;
|
411 | let hdistance = 0;
|
412 |
|
413 | if (e.control || e.meta) {
|
414 | vdistance = 1;
|
415 | hdistance = 1;
|
416 | } else if (diagram.grid !== null) {
|
417 | const cellsize = diagram.grid.gridCellSize;
|
418 | hdistance = cellsize.width;
|
419 | vdistance = cellsize.height;
|
420 | }
|
421 | diagram.startTransaction('arrowKeyMove');
|
422 | diagram.selection.each((part) => {
|
423 | if (e.key === 'Up') {
|
424 | part.move(new go.Point(part.actualBounds.x, part.actualBounds.y - vdistance));
|
425 | } else if (e.key === 'Down') {
|
426 | part.move(new go.Point(part.actualBounds.x, part.actualBounds.y + vdistance));
|
427 | } else if (e.key === 'Left') {
|
428 | part.move(new go.Point(part.actualBounds.x - hdistance, part.actualBounds.y));
|
429 | } else if (e.key === 'Right') {
|
430 | part.move(new go.Point(part.actualBounds.x + hdistance, part.actualBounds.y));
|
431 | }
|
432 | });
|
433 | diagram.commitTransaction('arrowKeyMove');
|
434 | }
|
435 |
|
436 | |
437 |
|
438 |
|
439 | private _arrowKeySelect(): void {
|
440 | const diagram = this.diagram;
|
441 | const e = diagram.lastInput;
|
442 |
|
443 |
|
444 |
|
445 | let nextPart = null;
|
446 | if (e.key === 'Up') {
|
447 | nextPart = this._findNearestPartTowards(270);
|
448 | } else if (e.key === 'Down') {
|
449 | nextPart = this._findNearestPartTowards(90);
|
450 | } else if (e.key === 'Left') {
|
451 | nextPart = this._findNearestPartTowards(180);
|
452 | } else if (e.key === 'Right') {
|
453 | nextPart = this._findNearestPartTowards(0);
|
454 | }
|
455 | if (nextPart !== null) {
|
456 | if (e.shift) {
|
457 | nextPart.isSelected = true;
|
458 | } else if (e.control || e.meta) {
|
459 | nextPart.isSelected = !nextPart.isSelected;
|
460 | } else {
|
461 | diagram.select(nextPart);
|
462 | }
|
463 | }
|
464 | }
|
465 |
|
466 | |
467 |
|
468 |
|
469 |
|
470 |
|
471 |
|
472 | private _findNearestPartTowards(dir: number): go.Part | null {
|
473 | const originalPart = this.diagram.selection.first();
|
474 | if (originalPart === null) return null;
|
475 | const originalPoint = originalPart.actualBounds.center;
|
476 | const allParts = this._getAllParts();
|
477 | let closestDistance = Infinity;
|
478 | let closest = originalPart;
|
479 |
|
480 | for (let i = 0; i < allParts.length; i++) {
|
481 | const nextPart = allParts[i];
|
482 | if (nextPart === originalPart) continue;
|
483 | const nextPoint = nextPart.actualBounds.center;
|
484 | const angle = originalPoint.directionPoint(nextPoint);
|
485 | const anglediff = this._angleCloseness(angle, dir);
|
486 | if (anglediff <= 45) {
|
487 | let distance = originalPoint.distanceSquaredPoint(nextPoint);
|
488 | distance *= 1 + Math.sin(anglediff * Math.PI / 180);
|
489 | if (distance < closestDistance) {
|
490 | closestDistance = distance;
|
491 | closest = nextPart;
|
492 | }
|
493 | }
|
494 | }
|
495 | return closest;
|
496 | }
|
497 |
|
498 | private _angleCloseness(a: number, dir: number): number {
|
499 | return Math.min(Math.abs(dir - a), Math.min(Math.abs(dir + 360 - a), Math.abs(dir - 360 - a)));
|
500 | }
|
501 |
|
502 |
|
503 | |
504 |
|
505 |
|
506 |
|
507 | public copyToClipboard(coll: go.Iterable<go.Part>): void {
|
508 | super.copyToClipboard(coll);
|
509 | this._lastPasteOffset.set(this.pasteOffset);
|
510 | }
|
511 |
|
512 | |
513 |
|
514 |
|
515 |
|
516 | public pasteFromClipboard(): go.Set<go.Part> {
|
517 | const coll = super.pasteFromClipboard();
|
518 | this.diagram.moveParts(coll, this._lastPasteOffset, false);
|
519 | this._lastPasteOffset.add(this.pasteOffset);
|
520 | return coll;
|
521 | }
|
522 | }
|
523 |
|