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 | export class GuidedDraggingTool extends go.DraggingTool {
|
23 |
|
24 | private guidelineHtop: go.Part;
|
25 | private guidelineHbottom: go.Part;
|
26 | private guidelineHcenter: go.Part;
|
27 |
|
28 | private guidelineVleft: go.Part;
|
29 | private guidelineVright: go.Part;
|
30 | private guidelineVcenter: go.Part;
|
31 |
|
32 |
|
33 | private _guidelineSnapDistance: number = 6;
|
34 | private _isGuidelineEnabled: boolean = true;
|
35 | private _horizontalGuidelineColor: string = 'gray';
|
36 | private _verticalGuidelineColor: string = 'gray';
|
37 | private _centerGuidelineColor: string = 'gray';
|
38 | private _guidelineWidth: number = 1;
|
39 | private _searchDistance: number = 1000;
|
40 | private _isGuidelineSnapEnabled: boolean = true;
|
41 |
|
42 | |
43 |
|
44 |
|
45 | constructor() {
|
46 | super();
|
47 |
|
48 | const partProperties = { layerName: 'Tool', isInDocumentBounds: false };
|
49 | const shapeProperties = { stroke: 'gray', isGeometryPositioned: true };
|
50 |
|
51 | const $ = go.GraphObject.make;
|
52 |
|
53 | this.guidelineHtop =
|
54 | $(go.Part, partProperties,
|
55 | $(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
56 | this.guidelineHbottom =
|
57 | $(go.Part, partProperties,
|
58 | $(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
59 | this.guidelineHcenter =
|
60 | $(go.Part, partProperties,
|
61 | $(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
62 |
|
63 | this.guidelineVleft =
|
64 | $(go.Part, partProperties,
|
65 | $(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
66 | this.guidelineVright =
|
67 | $(go.Part, partProperties,
|
68 | $(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
69 | this.guidelineVcenter =
|
70 | $(go.Part, partProperties,
|
71 | $(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
72 | }
|
73 |
|
74 | |
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | get guidelineSnapDistance(): number { return this._guidelineSnapDistance; }
|
81 | set guidelineSnapDistance(val: number) {
|
82 | if (typeof val !== 'number' || isNaN(val) || val < 0) throw new Error('new value for GuideddraggingTool.guidelineSnapDistance must be a non-negative number');
|
83 | if (this._guidelineSnapDistance !== val) {
|
84 | this._guidelineSnapDistance = val;
|
85 | }
|
86 | }
|
87 |
|
88 | |
89 |
|
90 |
|
91 |
|
92 |
|
93 | get isGuidelineEnabled(): boolean { return this._isGuidelineEnabled; }
|
94 | set isGuidelineEnabled(val: boolean) {
|
95 | if (typeof val !== 'boolean') throw new Error('new value for GuidedDraggingTool.isGuidelineEnabled must be a boolean value.');
|
96 | if (this._isGuidelineEnabled !== val) {
|
97 | this._isGuidelineEnabled = val;
|
98 | }
|
99 | }
|
100 |
|
101 | |
102 |
|
103 |
|
104 |
|
105 |
|
106 | get horizontalGuidelineColor(): string { return this._horizontalGuidelineColor; }
|
107 | set horizontalGuidelineColor(val: string) {
|
108 | if (this._horizontalGuidelineColor !== val) {
|
109 | this._horizontalGuidelineColor = val;
|
110 | (this.guidelineHbottom.elements.first() as go.Shape).stroke = this._horizontalGuidelineColor;
|
111 | (this.guidelineHtop.elements.first() as go.Shape).stroke = this._horizontalGuidelineColor;
|
112 | }
|
113 | }
|
114 |
|
115 | |
116 |
|
117 |
|
118 |
|
119 |
|
120 | get verticalGuidelineColor(): string { return this._verticalGuidelineColor; }
|
121 | set verticalGuidelineColor(val: string) {
|
122 | if (this._verticalGuidelineColor !== val) {
|
123 | this._verticalGuidelineColor = val;
|
124 | (this.guidelineVleft.elements.first() as go.Shape).stroke = this._verticalGuidelineColor;
|
125 | (this.guidelineVright.elements.first() as go.Shape).stroke = this._verticalGuidelineColor;
|
126 | }
|
127 | }
|
128 |
|
129 | |
130 |
|
131 |
|
132 |
|
133 |
|
134 | get centerGuidelineColor(): string { return this._centerGuidelineColor; }
|
135 | set centerGuidelineColor(val: string) {
|
136 | if (this._centerGuidelineColor !== val) {
|
137 | this._centerGuidelineColor = val;
|
138 | (this.guidelineVcenter.elements.first() as go.Shape).stroke = this._centerGuidelineColor;
|
139 | (this.guidelineHcenter.elements.first() as go.Shape).stroke = this._centerGuidelineColor;
|
140 | }
|
141 | }
|
142 |
|
143 | |
144 |
|
145 |
|
146 |
|
147 |
|
148 | get guidelineWidth(): number { return this._guidelineWidth; }
|
149 | set guidelineWidth(val: number) {
|
150 | if (typeof val !== 'number' || isNaN(val) || val < 0) throw new Error('New value for GuidedDraggingTool.guidelineWidth must be a non-negative number.');
|
151 | if (this._guidelineWidth !== val) {
|
152 | this._guidelineWidth = val;
|
153 | (this.guidelineVcenter.elements.first() as go.Shape).strokeWidth = val;
|
154 | (this.guidelineHcenter.elements.first() as go.Shape).strokeWidth = val;
|
155 | (this.guidelineVleft.elements.first() as go.Shape).strokeWidth = val;
|
156 | (this.guidelineVright.elements.first() as go.Shape).strokeWidth = val;
|
157 | (this.guidelineHbottom.elements.first() as go.Shape).strokeWidth = val;
|
158 | (this.guidelineHtop.elements.first() as go.Shape).strokeWidth = val;
|
159 | }
|
160 | }
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 | get searchDistance(): number { return this._searchDistance; }
|
169 | set searchDistance(val: number) {
|
170 | if (typeof val !== 'number' || isNaN(val) || val <= 0) throw new Error('new value for GuidedDraggingTool.searchDistance must be a positive number.');
|
171 | if (this._searchDistance !== val) {
|
172 | this._searchDistance = val;
|
173 | }
|
174 | }
|
175 |
|
176 | |
177 |
|
178 |
|
179 |
|
180 |
|
181 | get isGuidelineSnapEnabled(): boolean { return this._isGuidelineSnapEnabled; }
|
182 | set isGuidelineSnapEnabled(val: boolean) {
|
183 | if (typeof val !== 'boolean') throw new Error('new value for GuidedDraggingTool.isGuidelineSnapEnabled must be a boolean.');
|
184 | if (this._isGuidelineSnapEnabled !== val) {
|
185 | this._isGuidelineSnapEnabled = val;
|
186 | }
|
187 | }
|
188 |
|
189 | |
190 |
|
191 |
|
192 | public clearGuidelines(): void {
|
193 | this.diagram.remove(this.guidelineHbottom);
|
194 | this.diagram.remove(this.guidelineHcenter);
|
195 | this.diagram.remove(this.guidelineHtop);
|
196 | this.diagram.remove(this.guidelineVleft);
|
197 | this.diagram.remove(this.guidelineVright);
|
198 | this.diagram.remove(this.guidelineVcenter);
|
199 | }
|
200 |
|
201 | |
202 |
|
203 |
|
204 | public doDeactivate(): void {
|
205 | super.doDeactivate();
|
206 |
|
207 | this.clearGuidelines();
|
208 | }
|
209 |
|
210 | |
211 |
|
212 |
|
213 | public doDragOver(pt: go.Point, obj: go.GraphObject): void {
|
214 |
|
215 | this.clearGuidelines();
|
216 |
|
217 |
|
218 | const draggingParts = this.copiedParts || this.draggedParts;
|
219 | if (draggingParts === null) return;
|
220 | const partItr = draggingParts.iterator;
|
221 | if (partItr.next()) {
|
222 | const part = partItr.key;
|
223 |
|
224 | this.showHorizontalMatches(part, this.isGuidelineEnabled, false);
|
225 | this.showVerticalMatches(part, this.isGuidelineEnabled, false);
|
226 | }
|
227 | }
|
228 |
|
229 | |
230 |
|
231 |
|
232 |
|
233 | public doDropOnto(pt: go.Point, obj: go.GraphObject): void {
|
234 | this.clearGuidelines();
|
235 |
|
236 |
|
237 | const draggingParts = this.copiedParts || this.draggedParts;
|
238 | if (draggingParts === null) return;
|
239 | const partItr = draggingParts.iterator;
|
240 | if (partItr.next()) {
|
241 | const part = partItr.key;
|
242 |
|
243 |
|
244 | const e = this.diagram.lastInput;
|
245 | const snap = this.isGuidelineSnapEnabled && !e.shift;
|
246 |
|
247 | this.showHorizontalMatches(part, false, snap);
|
248 | this.showVerticalMatches(part, false, snap);
|
249 | }
|
250 | }
|
251 |
|
252 | |
253 |
|
254 |
|
255 |
|
256 |
|
257 | public invalidateLinks(node: go.Part): void {
|
258 | if (node instanceof go.Node) node.invalidateConnectedLinks();
|
259 | }
|
260 |
|
261 | |
262 |
|
263 |
|
264 |
|
265 |
|
266 | protected isGuiding(part: go.Part, guidedpart: go.Part): boolean {
|
267 | return part instanceof go.Part &&
|
268 | !part.isSelected &&
|
269 | !(part instanceof go.Link) &&
|
270 | guidedpart instanceof go.Part &&
|
271 | part.containingGroup === guidedpart.containingGroup &&
|
272 | part.layer !== null && !part.layer.isTemporary;
|
273 | }
|
274 |
|
275 | |
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 | public showHorizontalMatches(part: go.Part, guideline: boolean, snap: boolean): void {
|
284 | const objBounds = part.locationObject.getDocumentBounds();
|
285 | const p0 = objBounds.y;
|
286 | const p1 = objBounds.y + objBounds.height / 2;
|
287 | const p2 = objBounds.y + objBounds.height;
|
288 |
|
289 | const marginOfError = this.guidelineSnapDistance;
|
290 | const distance = this.searchDistance;
|
291 |
|
292 | const area = objBounds.copy();
|
293 | area.inflate(distance, marginOfError + 1);
|
294 | const otherObjs = this.diagram.findObjectsIn(area,
|
295 | (obj) => obj.part as go.Part,
|
296 | (p) => this.isGuiding(p as go.Part, part),
|
297 | true) as go.Set<go.Part>;
|
298 |
|
299 | let bestDiff: number = marginOfError;
|
300 | let bestObj: any = null;
|
301 | let bestSpot: go.Spot = go.Spot.Default;
|
302 | let bestOtherSpot: go.Spot = go.Spot.Default;
|
303 |
|
304 | otherObjs.each((other) => {
|
305 | if (other === part) return;
|
306 |
|
307 | const otherBounds = other.locationObject.getDocumentBounds();
|
308 | const q0 = otherBounds.y;
|
309 | const q1 = otherBounds.y + otherBounds.height / 2;
|
310 | const q2 = otherBounds.y + otherBounds.height;
|
311 |
|
312 |
|
313 | if (Math.abs(p1 - q1) < bestDiff) {
|
314 | bestDiff = Math.abs(p1 - q1);
|
315 | bestObj = other;
|
316 | bestSpot = go.Spot.Center;
|
317 | bestOtherSpot = go.Spot.Center;
|
318 | }
|
319 |
|
320 | if (Math.abs(p0 - q0) < bestDiff) {
|
321 | bestDiff = Math.abs(p0 - q0);
|
322 | bestObj = other;
|
323 | bestSpot = go.Spot.Top;
|
324 | bestOtherSpot = go.Spot.Top;
|
325 | } else if (Math.abs(p0 - q2) < bestDiff) {
|
326 | bestDiff = Math.abs(p0 - q2);
|
327 | bestObj = other;
|
328 | bestSpot = go.Spot.Top;
|
329 | bestOtherSpot = go.Spot.Bottom;
|
330 | }
|
331 |
|
332 | if (Math.abs(p2 - q0) < bestDiff) {
|
333 | bestDiff = Math.abs(p2 - q0);
|
334 | bestObj = other;
|
335 | bestSpot = go.Spot.Bottom;
|
336 | bestOtherSpot = go.Spot.Top;
|
337 | } else if (Math.abs(p2 - q2) < bestDiff) {
|
338 | bestDiff = Math.abs(p2 - q2);
|
339 | bestObj = other;
|
340 | bestSpot = go.Spot.Bottom;
|
341 | bestOtherSpot = go.Spot.Bottom;
|
342 | }
|
343 | });
|
344 |
|
345 | if (bestObj !== null) {
|
346 | const offsetX = objBounds.x - part.actualBounds.x;
|
347 | const offsetY = objBounds.y - part.actualBounds.y;
|
348 | const bestBounds = bestObj.locationObject.getDocumentBounds();
|
349 |
|
350 | const x0 = Math.min(objBounds.x, bestBounds.x) - 10;
|
351 | const x2 = Math.max(objBounds.x + objBounds.width, bestBounds.x + bestBounds.width) + 10;
|
352 |
|
353 | const bestPoint = new go.Point().setRectSpot(bestBounds, bestOtherSpot);
|
354 | if (bestSpot === go.Spot.Center) {
|
355 | if (snap) {
|
356 |
|
357 | part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - objBounds.height / 2 - offsetY));
|
358 | this.invalidateLinks(part);
|
359 | }
|
360 | if (guideline) {
|
361 | this.guidelineHcenter.position = new go.Point(x0, bestPoint.y);
|
362 | this.guidelineHcenter.elt(0).width = x2 - x0;
|
363 | this.diagram.add(this.guidelineHcenter);
|
364 | }
|
365 | } else if (bestSpot === go.Spot.Top) {
|
366 | if (snap) {
|
367 | part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - offsetY));
|
368 | this.invalidateLinks(part);
|
369 | }
|
370 | if (guideline) {
|
371 | this.guidelineHtop.position = new go.Point(x0, bestPoint.y);
|
372 | this.guidelineHtop.elt(0).width = x2 - x0;
|
373 | this.diagram.add(this.guidelineHtop);
|
374 | }
|
375 | } else if (bestSpot === go.Spot.Bottom) {
|
376 | if (snap) {
|
377 | part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - objBounds.height - offsetY));
|
378 | this.invalidateLinks(part);
|
379 | }
|
380 | if (guideline) {
|
381 | this.guidelineHbottom.position = new go.Point(x0, bestPoint.y);
|
382 | this.guidelineHbottom.elt(0).width = x2 - x0;
|
383 | this.diagram.add(this.guidelineHbottom);
|
384 | }
|
385 | }
|
386 | }
|
387 | }
|
388 |
|
389 | |
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 | public showVerticalMatches(part: go.Part, guideline: boolean, snap: boolean): void {
|
398 | const objBounds = part.locationObject.getDocumentBounds();
|
399 | const p0 = objBounds.x;
|
400 | const p1 = objBounds.x + objBounds.width / 2;
|
401 | const p2 = objBounds.x + objBounds.width;
|
402 |
|
403 | const marginOfError = this.guidelineSnapDistance;
|
404 | const distance = this.searchDistance;
|
405 |
|
406 | const area = objBounds.copy();
|
407 | area.inflate(marginOfError + 1, distance);
|
408 | const otherObjs = this.diagram.findObjectsIn(area,
|
409 | (obj) => obj.part as go.Part,
|
410 | (p) => this.isGuiding(p as go.Part, part),
|
411 | true) as go.Set<go.Part>;
|
412 |
|
413 | let bestDiff: number = marginOfError;
|
414 | let bestObj: any = null;
|
415 | let bestSpot: go.Spot = go.Spot.Default;
|
416 | let bestOtherSpot: go.Spot = go.Spot.Default;
|
417 |
|
418 | otherObjs.each((other) => {
|
419 | if (other === part) return;
|
420 |
|
421 | const otherBounds = other.locationObject.getDocumentBounds();
|
422 | const q0 = otherBounds.x;
|
423 | const q1 = otherBounds.x + otherBounds.width / 2;
|
424 | const q2 = otherBounds.x + otherBounds.width;
|
425 |
|
426 |
|
427 | if (Math.abs(p1 - q1) < bestDiff) {
|
428 | bestDiff = Math.abs(p1 - q1);
|
429 | bestObj = other;
|
430 | bestSpot = go.Spot.Center;
|
431 | bestOtherSpot = go.Spot.Center;
|
432 | }
|
433 |
|
434 | if (Math.abs(p0 - q0) < bestDiff) {
|
435 | bestDiff = Math.abs(p0 - q0);
|
436 | bestObj = other;
|
437 | bestSpot = go.Spot.Left;
|
438 | bestOtherSpot = go.Spot.Left;
|
439 | } else if (Math.abs(p0 - q2) < bestDiff) {
|
440 | bestDiff = Math.abs(p0 - q2);
|
441 | bestObj = other;
|
442 | bestSpot = go.Spot.Left;
|
443 | bestOtherSpot = go.Spot.Right;
|
444 | }
|
445 |
|
446 | if (Math.abs(p2 - q0) < bestDiff) {
|
447 | bestDiff = Math.abs(p2 - q0);
|
448 | bestObj = other;
|
449 | bestSpot = go.Spot.Right;
|
450 | bestOtherSpot = go.Spot.Left;
|
451 | } else if (Math.abs(p2 - q2) < bestDiff) {
|
452 | bestDiff = Math.abs(p2 - q2);
|
453 | bestObj = other;
|
454 | bestSpot = go.Spot.Right;
|
455 | bestOtherSpot = go.Spot.Right;
|
456 | }
|
457 | });
|
458 |
|
459 | if (bestObj !== null) {
|
460 | const offsetX = objBounds.x - part.actualBounds.x;
|
461 | const offsetY = objBounds.y - part.actualBounds.y;
|
462 | const bestBounds = bestObj.locationObject.getDocumentBounds();
|
463 |
|
464 | const y0 = Math.min(objBounds.y, bestBounds.y) - 10;
|
465 | const y2 = Math.max(objBounds.y + objBounds.height, bestBounds.y + bestBounds.height) + 10;
|
466 |
|
467 | const bestPoint = new go.Point().setRectSpot(bestBounds, bestOtherSpot);
|
468 | if (bestSpot === go.Spot.Center) {
|
469 | if (snap) {
|
470 |
|
471 | part.move(new go.Point(bestPoint.x - objBounds.width / 2 - offsetX, objBounds.y - offsetY));
|
472 | this.invalidateLinks(part);
|
473 | }
|
474 | if (guideline) {
|
475 | this.guidelineVcenter.position = new go.Point(bestPoint.x, y0);
|
476 | this.guidelineVcenter.elt(0).height = y2 - y0;
|
477 | this.diagram.add(this.guidelineVcenter);
|
478 | }
|
479 | } else if (bestSpot === go.Spot.Left) {
|
480 | if (snap) {
|
481 | part.move(new go.Point(bestPoint.x - offsetX, objBounds.y - offsetY));
|
482 | this.invalidateLinks(part);
|
483 | }
|
484 | if (guideline) {
|
485 | this.guidelineVleft.position = new go.Point(bestPoint.x, y0);
|
486 | this.guidelineVleft.elt(0).height = y2 - y0;
|
487 | this.diagram.add(this.guidelineVleft);
|
488 | }
|
489 | } else if (bestSpot === go.Spot.Right) {
|
490 | if (snap) {
|
491 | part.move(new go.Point(bestPoint.x - objBounds.width - offsetX, objBounds.y - offsetY));
|
492 | this.invalidateLinks(part);
|
493 | }
|
494 | if (guideline) {
|
495 | this.guidelineVright.position = new go.Point(bestPoint.x, y0);
|
496 | this.guidelineVright.elt(0).height = y2 - y0;
|
497 | this.diagram.add(this.guidelineVright);
|
498 | }
|
499 | }
|
500 | }
|
501 | }
|
502 | }
|