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 | export class GeometryReshapingTool extends go.Tool {
|
29 |
|
30 | private _handleArchetype: go.GraphObject;
|
31 | private _midHandleArchetype: go.GraphObject;
|
32 | private _isResegmenting: boolean;
|
33 | private _resegmentingDistance: number;
|
34 | private _reshapeObjectName: string;
|
35 |
|
36 |
|
37 |
|
38 | private _handle: go.GraphObject | null = null;
|
39 | private _adornedShape: go.Shape | null = null;
|
40 | private _originalGeometry: go.Geometry | null = null;
|
41 |
|
42 | |
43 |
|
44 |
|
45 | constructor() {
|
46 | super();
|
47 | this.name = 'GeometryReshaping';
|
48 |
|
49 | let h: go.Shape = new go.Shape();
|
50 | h.figure = 'Diamond';
|
51 | h.desiredSize = new go.Size(8, 8);
|
52 | h.fill = 'lightblue';
|
53 | h.stroke = 'dodgerblue';
|
54 | h.cursor = 'move';
|
55 | this._handleArchetype = h;
|
56 |
|
57 | h = new go.Shape();
|
58 | h.figure = 'Circle';
|
59 | h.desiredSize = new go.Size(7, 7);
|
60 | h.fill = 'lightblue';
|
61 | h.stroke = 'dodgerblue';
|
62 | h.cursor = 'move';
|
63 | this._midHandleArchetype = h;
|
64 |
|
65 | this._isResegmenting = false;
|
66 | this._resegmentingDistance = 3;
|
67 |
|
68 | this._reshapeObjectName = 'SHAPE';
|
69 | }
|
70 |
|
71 | |
72 |
|
73 |
|
74 |
|
75 | get handleArchetype(): go.GraphObject { return this._handleArchetype; }
|
76 | set handleArchetype(value: go.GraphObject) { this._handleArchetype = value; }
|
77 |
|
78 | |
79 |
|
80 |
|
81 |
|
82 | get midHandleArchetype(): go.GraphObject { return this._midHandleArchetype; }
|
83 | set midHandleArchetype(value: go.GraphObject) { this._midHandleArchetype = value; }
|
84 |
|
85 | |
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | get isResegmenting(): boolean { return this._isResegmenting; }
|
92 | set isResegmenting(val: boolean) { this._isResegmenting = val; }
|
93 |
|
94 | |
95 |
|
96 |
|
97 |
|
98 |
|
99 | get resegmentingDistance(): number { return this._resegmentingDistance; }
|
100 | set resegmentingDistance(val: number) { this._resegmentingDistance = val; }
|
101 |
|
102 | |
103 |
|
104 |
|
105 |
|
106 | get reshapeObjectName(): string { return this._reshapeObjectName; }
|
107 | set reshapeObjectName(value: string) { this._reshapeObjectName = value; }
|
108 |
|
109 | |
110 |
|
111 |
|
112 |
|
113 |
|
114 | get handle(): go.GraphObject | null { return this._handle; }
|
115 | set handle(val: go.GraphObject | null) { this._handle = val; }
|
116 |
|
117 | |
118 |
|
119 |
|
120 |
|
121 | get adornedShape(): go.Shape | null { return this._adornedShape; }
|
122 |
|
123 | |
124 |
|
125 |
|
126 |
|
127 | get originalGeometry(): go.Geometry | null { return this._originalGeometry; }
|
128 |
|
129 | |
130 |
|
131 |
|
132 |
|
133 |
|
134 | public updateAdornments(part: go.Part): void {
|
135 | if (part === null || part instanceof go.Link) return;
|
136 | if (part.isSelected && !this.diagram.isReadOnly) {
|
137 | const selelt = part.findObject(this.reshapeObjectName);
|
138 | if (selelt instanceof go.Shape && selelt.geometry !== null &&
|
139 | selelt.actualBounds.isReal() && selelt.isVisibleObject() &&
|
140 | part.canReshape() && part.actualBounds.isReal() && part.isVisible() &&
|
141 | selelt.geometry.type === go.Geometry.Path) {
|
142 | const geo = selelt.geometry;
|
143 | let adornment = part.findAdornment(this.name);
|
144 | if (adornment === null || (this._countHandles(geo) !== adornment.elements.count - 1)) {
|
145 | adornment = this.makeAdornment(selelt);
|
146 | }
|
147 | if (adornment !== null) {
|
148 |
|
149 | const b = geo.bounds;
|
150 |
|
151 | const body = adornment.findObject('BODY');
|
152 | if (body !== null) body.desiredSize = b.size;
|
153 | let unneeded = null;
|
154 | const elts = adornment.elements;
|
155 | for (let i = 0; i < elts.count; i++) {
|
156 | const h = adornment.elt(i);
|
157 | if (typeof (h as any)._typ !== "number") continue;
|
158 | const typ = (h as any)._typ as number;
|
159 | if (typeof (h as any)._fig !== "number") continue;
|
160 | const figi = (h as any)._fig as number;
|
161 | if (figi >= geo.figures.count) {
|
162 | if (unneeded === null) unneeded = [];
|
163 | unneeded.push(h);
|
164 | continue;
|
165 | }
|
166 | var fig = geo.figures.elt(figi);
|
167 | if (typeof (h as any)._seg !== "number") continue;
|
168 | const segi = (h as any)._seg as number;
|
169 | if (segi >= fig.segments.count) {
|
170 | if (unneeded === null) unneeded = [];
|
171 | unneeded.push(h);
|
172 | continue;
|
173 | }
|
174 | var seg = fig.segments.elt(segi);
|
175 | var x = 0;
|
176 | var y = 0;
|
177 | switch (typ) {
|
178 | case 0: x = fig.startX; y = fig.startY; break;
|
179 | case 1: x = seg.endX; y = seg.endY; break;
|
180 | case 2: x = seg.point1X; y = seg.point1Y; break;
|
181 | case 3: x = seg.point2X; y = seg.point2Y; break;
|
182 | case 4: x = (fig.startX + seg.endX) / 2; y = (fig.startY + seg.endY) / 2; break;
|
183 | case 5: x = (fig.segments.elt(segi-1).endX + seg.endX) / 2; y = (fig.segments.elt(segi-1).endY + seg.endY) / 2; break;
|
184 | case 6: x = (fig.startX + seg.endX) / 2; y = (fig.startY + seg.endY) / 2; break;
|
185 | default: throw new Error('unexpected handle type')
|
186 | }
|
187 | h.alignment = new go.Spot(0, 0, x - b.x, y - b.y);
|
188 | }
|
189 | if (unneeded !== null) {
|
190 | unneeded.forEach(function(h) { if (adornment) adornment.remove(h); });
|
191 | }
|
192 |
|
193 | part.addAdornment(this.name, adornment);
|
194 | adornment.location = selelt.getDocumentPoint(go.Spot.TopLeft);
|
195 | adornment.angle = selelt.getDocumentAngle();
|
196 | return;
|
197 | }
|
198 | }
|
199 | }
|
200 | part.removeAdornment(this.name);
|
201 | }
|
202 |
|
203 | |
204 |
|
205 |
|
206 | private _countHandles(geo: go.Geometry): number {
|
207 | var reseg = this.isResegmenting;
|
208 | var c = 0;
|
209 | geo.figures.each(function(fig) {
|
210 | c++;
|
211 | fig.segments.each(function(seg) {
|
212 | if (reseg) {
|
213 | if (seg.type === go.PathSegment.Line) c++;
|
214 | if (seg.isClosed) c++;
|
215 | }
|
216 | c++;
|
217 | if (seg.type === go.PathSegment.QuadraticBezier) c++;
|
218 | else if (seg.type === go.PathSegment.Bezier) c += 2;
|
219 | })
|
220 | });
|
221 | return c;
|
222 | };
|
223 |
|
224 | |
225 |
|
226 |
|
227 | public makeAdornment(selelt: go.Shape): go.Adornment {
|
228 | const adornment = new go.Adornment();
|
229 | adornment.type = go.Panel.Spot;
|
230 | adornment.locationObjectName = 'BODY';
|
231 | adornment.locationSpot = new go.Spot(0, 0, -selelt.strokeWidth / 2, -selelt.strokeWidth / 2);
|
232 | let h: any = new go.Shape();
|
233 | h.name = 'BODY';
|
234 | h.fill = null;
|
235 | h.stroke = null;
|
236 | h.strokeWidth = 0;
|
237 | adornment.add(h);
|
238 |
|
239 | const geo = selelt.geometry;
|
240 | if (geo !== null) {
|
241 | if (this.isResegmenting) {
|
242 | for (let f = 0; f < geo.figures.count; f++) {
|
243 | const fig = geo.figures.elt(f);
|
244 | for (let g = 0; g < fig.segments.count; g++) {
|
245 | const seg = fig.segments.elt(g);
|
246 | let h: go.GraphObject | null;
|
247 | if (seg.type === go.PathSegment.Line) {
|
248 | h = this.makeResegmentHandle(selelt, fig, seg);
|
249 | if (h !== null) {
|
250 | (h as any)._typ = (g === 0) ? 4 : 5;
|
251 | (h as any)._fig = f;
|
252 | (h as any)._seg = g;
|
253 | adornment.add(h);
|
254 | }
|
255 | }
|
256 | if (seg.isClosed) {
|
257 | h = this.makeResegmentHandle(selelt, fig, seg);
|
258 | if (h !== null) {
|
259 | (h as any)._typ = 6;
|
260 | (h as any)._fig = f;
|
261 | (h as any)._seg = g;
|
262 | adornment.add(h);
|
263 | }
|
264 | }
|
265 | }
|
266 | }
|
267 | }
|
268 |
|
269 |
|
270 | for (let f = 0; f < geo.figures.count; f++) {
|
271 | const fig = geo.figures.elt(f);
|
272 | for (let g = 0; g < fig.segments.count; g++) {
|
273 | const seg = fig.segments.elt(g);
|
274 | if (g === 0) {
|
275 | h = this.makeHandle(selelt, fig, seg);
|
276 | if (h !== null) {
|
277 | h._typ = 0;
|
278 | h._fig = f;
|
279 | h._seg = g;
|
280 | adornment.add(h);
|
281 | }
|
282 | }
|
283 | h = this.makeHandle(selelt, fig, seg);
|
284 | if (h !== null) {
|
285 | h._typ = 1;
|
286 | h._fig = f;
|
287 | h._seg = g;
|
288 | adornment.add(h);
|
289 | }
|
290 | if (seg.type === go.PathSegment.QuadraticBezier || seg.type === go.PathSegment.Bezier) {
|
291 | h = this.makeHandle(selelt, fig, seg);
|
292 | if (h !== null) {
|
293 | h._typ = 2;
|
294 | h._fig = f;
|
295 | h._seg = g;
|
296 | adornment.add(h);
|
297 | }
|
298 | if (seg.type === go.PathSegment.Bezier) {
|
299 | h = this.makeHandle(selelt, fig, seg);
|
300 | if (h !== null) {
|
301 | h._typ = 3;
|
302 | h._fig = f;
|
303 | h._seg = g;
|
304 | adornment.add(h);
|
305 | }
|
306 | }
|
307 | }
|
308 | }
|
309 | }
|
310 | }
|
311 | adornment.category = this.name;
|
312 | adornment.adornedObject = selelt;
|
313 | return adornment;
|
314 | }
|
315 |
|
316 | |
317 |
|
318 |
|
319 | public makeHandle(selelt: go.Shape, fig: go.PathFigure, seg: go.PathSegment): go.GraphObject | null {
|
320 | const h = this.handleArchetype;
|
321 | if (h === null) return null;
|
322 | return h.copy();
|
323 | }
|
324 |
|
325 | |
326 |
|
327 |
|
328 | public makeResegmentHandle(pathshape: go.Shape, fig: go.PathFigure, seg: go.PathSegment) {
|
329 | var h = this.midHandleArchetype;
|
330 | if (h === null) return null;
|
331 | return h.copy();
|
332 | }
|
333 |
|
334 |
|
335 | |
336 |
|
337 |
|
338 | public canStart(): boolean {
|
339 | if (!this.isEnabled) return false;
|
340 |
|
341 | const diagram = this.diagram;
|
342 | if (diagram.isReadOnly) return false;
|
343 | if (!diagram.allowReshape) return false;
|
344 | if (!diagram.lastInput.left) return false;
|
345 | const h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
346 | return (h !== null);
|
347 | }
|
348 |
|
349 | |
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 | public doActivate(): void {
|
358 | const diagram = this.diagram;
|
359 | if (diagram === null) return;
|
360 | this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
361 | const h = this._handle;
|
362 | if (h === null) return;
|
363 | const shape = (h.part as go.Adornment).adornedObject as go.Shape;
|
364 | if (!shape || !shape.part) return;
|
365 | this._adornedShape = shape;
|
366 | diagram.isMouseCaptured = true;
|
367 | this.startTransaction(this.name);
|
368 |
|
369 | const typ = (h as any)._typ as number;
|
370 | const figi = (h as any)._fig as number;
|
371 | const segi = (h as any)._seg as number;
|
372 | if (this.isResegmenting && typ >= 4 && shape.geometry !== null) {
|
373 | const locpt = shape.getLocalPoint(diagram.firstInput.documentPoint);
|
374 | const geo = shape.geometry.copy();
|
375 | const fig = geo.figures.elt(figi);
|
376 | const seg = fig.segments.elt(segi);
|
377 | const newseg = seg.copy();
|
378 | switch (typ) {
|
379 | case 4: {
|
380 | newseg.endX = (fig.startX + seg.endX) / 2;
|
381 | newseg.endY = (fig.startY + seg.endY) / 2;
|
382 | newseg.isClosed = false;
|
383 | fig.segments.insertAt(segi, newseg);
|
384 | break;
|
385 | }
|
386 | case 5: {
|
387 | const prevseg = fig.segments.elt(segi - 1);
|
388 | newseg.endX = (prevseg.endX + seg.endX) / 2;
|
389 | newseg.endY = (prevseg.endY + seg.endY) / 2;
|
390 | newseg.isClosed = false;
|
391 | fig.segments.insertAt(segi, newseg);
|
392 | break;
|
393 | }
|
394 | case 6: {
|
395 | newseg.endX = (fig.startX + seg.endX) / 2;
|
396 | newseg.endY = (fig.startY + seg.endY) / 2;
|
397 | newseg.isClosed = seg.isClosed;
|
398 | seg.isClosed = false;
|
399 | fig.add(newseg);
|
400 | break;
|
401 | }
|
402 | }
|
403 | shape.geometry = geo;
|
404 | var part = shape.part;
|
405 | part.ensureBounds();
|
406 | this.updateAdornments(part);
|
407 | this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
408 | if (this._handle === null) {
|
409 | this.doDeactivate();
|
410 | return;
|
411 | }
|
412 | }
|
413 |
|
414 | this._originalGeometry = shape.geometry;
|
415 | this.isActive = true;
|
416 | }
|
417 |
|
418 | |
419 |
|
420 |
|
421 | public doDeactivate(): void {
|
422 | this.stopTransaction();
|
423 |
|
424 | this._handle = null;
|
425 | this._adornedShape = null;
|
426 | const diagram = this.diagram;
|
427 | if (diagram !== null) diagram.isMouseCaptured = false;
|
428 | this.isActive = false;
|
429 | }
|
430 |
|
431 | |
432 |
|
433 |
|
434 | public doCancel(): void {
|
435 | const shape = this._adornedShape;
|
436 | if (shape !== null) {
|
437 |
|
438 | shape.geometry = this._originalGeometry;
|
439 | }
|
440 | this.stopTool();
|
441 | }
|
442 |
|
443 | |
444 |
|
445 |
|
446 |
|
447 | public doMouseMove(): void {
|
448 | const diagram = this.diagram;
|
449 | if (this.isActive && diagram !== null) {
|
450 | const newpt = this.computeReshape(diagram.lastInput.documentPoint);
|
451 | this.reshape(newpt);
|
452 | }
|
453 | }
|
454 |
|
455 | |
456 |
|
457 |
|
458 |
|
459 | public doMouseUp(): void {
|
460 | const diagram = this.diagram;
|
461 | if (this.isActive && diagram !== null) {
|
462 | const newpt = this.computeReshape(diagram.lastInput.documentPoint);
|
463 | this.reshape(newpt);
|
464 | const shape = this.adornedShape;
|
465 | if (this.isResegmenting && shape && shape.geometry && shape.part) {
|
466 | const typ = (this.handle as any)._typ as number;
|
467 | const figi = (this.handle as any)._fig as number;
|
468 | const segi = (this.handle as any)._seg as number;
|
469 | const fig = shape.geometry.figures.elt(figi);
|
470 | if (fig && fig.segments.count > 2) {
|
471 | let ax, ay, bx, by, cx, cy;
|
472 | if (typ === 0) {
|
473 | const lastseg = fig.segments.length-1;
|
474 | ax = fig.segments.elt(lastseg).endX; ay = fig.segments.elt(lastseg).endY;
|
475 | bx = fig.startX; by = fig.startY;
|
476 | cx = fig.segments.elt(0).endX; cy = fig.segments.elt(0).endY;
|
477 | } else {
|
478 | if (segi <= 0) {
|
479 | ax = fig.startX; ay = fig.startY;
|
480 | } else {
|
481 | ax = fig.segments.elt(segi - 1).endX; ay = fig.segments.elt(segi - 1).endY;
|
482 | }
|
483 | bx = fig.segments.elt(segi).endX; by = fig.segments.elt(segi).endY;
|
484 | if (segi >= fig.segments.length-1) {
|
485 | cx = fig.startX; cy = fig.startY;
|
486 | } else {
|
487 | cx = fig.segments.elt(segi + 1).endX; cy = fig.segments.elt(segi + 1).endY;
|
488 | }
|
489 | }
|
490 | const q = new go.Point(bx, by);
|
491 | q.projectOntoLineSegment(ax, ay, cx, cy);
|
492 |
|
493 |
|
494 | const dist = q.distanceSquaredPoint(new go.Point(bx, by));
|
495 | if (dist < this.resegmentingDistance * this.resegmentingDistance) {
|
496 | const geo = shape.geometry.copy();
|
497 | const fig = geo.figures.elt(figi);
|
498 | if (typ === 0) {
|
499 | const first = fig.segments.first();
|
500 | if (first) { fig.startX = first.endX; fig.startY = first.endY; }
|
501 | }
|
502 | if (segi > 0) {
|
503 | const prev = fig.segments.elt(segi - 1);
|
504 | const seg = fig.segments.elt(segi);
|
505 | prev.isClosed = seg.isClosed;
|
506 | }
|
507 | fig.segments.removeAt(segi);
|
508 | shape.geometry = geo;
|
509 | shape.part.removeAdornment(this.name);
|
510 | this.updateAdornments(shape.part);
|
511 | }
|
512 | }
|
513 | }
|
514 | this.transactionResult = this.name;
|
515 | }
|
516 | this.stopTool();
|
517 | }
|
518 |
|
519 | |
520 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 | public reshape(newPoint: go.Point): void {
|
527 | const shape = this.adornedShape;
|
528 | if (shape === null || shape.geometry === null) return;
|
529 | const locpt = shape.getLocalPoint(newPoint);
|
530 | const geo = shape.geometry.copy();
|
531 | const h = this.handle;
|
532 | if (!h) return;
|
533 | const type = (h as any)._typ;
|
534 | if (type === undefined) return;
|
535 | if ((h as any)._fig >= geo.figures.count) return;
|
536 | const fig = geo.figures.elt((h as any)._fig);
|
537 | if ((h as any)._seg >= fig.segments.count) return;
|
538 | const seg = fig.segments.elt((h as any)._seg);
|
539 | switch (type) {
|
540 | case 0: fig.startX = locpt.x; fig.startY = locpt.y; break;
|
541 | case 1: seg.endX = locpt.x; seg.endY = locpt.y; break;
|
542 | case 2: seg.point1X = locpt.x; seg.point1Y = locpt.y; break;
|
543 | case 3: seg.point2X = locpt.x; seg.point2Y = locpt.y; break;
|
544 | }
|
545 | const offset = geo.normalize();
|
546 | shape.desiredSize = new go.Size(NaN, NaN);
|
547 | shape.geometry = geo;
|
548 | const part = shape.part;
|
549 | if (part === null) return;
|
550 | part.ensureBounds();
|
551 | if (part.locationObject !== shape && !part.locationSpot.equals(go.Spot.Center)) {
|
552 |
|
553 | part.move(part.position.copy().subtract(offset.rotate(part.angle)));
|
554 | }
|
555 | this.updateAdornments(part);
|
556 | this.diagram.maybeUpdate();
|
557 | }
|
558 |
|
559 | |
560 |
|
561 |
|
562 |
|
563 |
|
564 |
|
565 |
|
566 | public computeReshape(p: go.Point): go.Point {
|
567 | return p;
|
568 | }
|
569 | }
|