UNPKG

16.5 kBPlain TextView Raw
1/*
2* Copyright (C) 1998-2021 by Northwoods Software Corporation. All Rights Reserved.
3*/
4
5/*
6* This is an extension and not part of the main GoJS library.
7* Note that the API for this class may change with any version, even point releases.
8* If you intend to use an extension in production, you should copy the code to your own source directory.
9* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
10* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
11*/
12
13import * as go from '../release/go-module.js';
14
15/**
16 * The PolygonDrawingTool class lets the user draw a new polygon or polyline shape by clicking where the corners should go.
17 * Right click or type ENTER to finish the operation.
18 *
19 * Set {@link #isPolygon} to false if you want this tool to draw open unfilled polyline shapes.
20 * Set {@link #archetypePartData} to customize the node data object that is added to the model.
21 * Data-bind to those properties in your node template to customize the appearance and behavior of the part.
22 *
23 * This tool uses a temporary {@link Shape}, {@link #temporaryShape}, held by a {@link Part} in the "Tool" layer,
24 * to show interactively what the user is drawing.
25 *
26 * If you want to experiment with this extension, try the <a href="../../extensionsJSM/PolygonDrawing.html">Polygon Drawing</a> sample.
27 * @category Tool Extension
28 */
29export class PolygonDrawingTool extends go.Tool {
30 private _isPolygon: boolean = true;
31 private _hasArcs: boolean = false;
32 private _isOrthoOnly: boolean = false;
33 private _isGridSnapEnabled: boolean = false;
34 private _archetypePartData: go.ObjectData= {}; // the data to copy for a new polygon Part
35
36 // this is the Shape that is shown during a drawing operation
37 private _temporaryShape: go.Shape = go.GraphObject.make(go.Shape, { name: 'SHAPE', fill: 'lightgray', strokeWidth: 1.5 });
38 // the Shape has to be inside a temporary Part that is used during the drawing operation
39 private temp: go.Part = go.GraphObject.make(go.Part, { layerName: 'Tool' }, this._temporaryShape);
40
41 /**
42 * Constructs an PolygonDrawingTool and sets the name for the tool.
43 */
44 constructor() {
45 super();
46 this.name = 'PolygonDrawing';
47 }
48
49 /**
50 * Gets or sets whether this tools draws a filled polygon or an unfilled open polyline.
51 *
52 * The default value is true.
53 */
54 get isPolygon(): boolean { return this._isPolygon; }
55 set isPolygon(val: boolean) { this._isPolygon = val; }
56
57
58 /**
59 * Gets or sets whether this tool draws shapes with quadratic bezier curves for each segment, or just straight lines.
60 *
61 * The default value is false -- only use straight lines.
62 */
63 get hasArcs(): boolean { return this._hasArcs; }
64 set hasArcs(val: boolean) { this._hasArcs = val; }
65
66 /**
67 * Gets or sets whether this tool draws shapes with only orthogonal segments, or segments in any direction.
68 * The default value is false -- draw segments in any direction. This does not restrict the closing segment, which may not be orthogonal.
69 */
70 get isOrthoOnly(): boolean { return this._isOrthoOnly; }
71 set isOrthoOnly(val: boolean) { this._isOrthoOnly = val; }
72
73 /**
74 * Gets or sets whether this tool only places the shape's corners on the Diagram's visible grid.
75 * The default value is false
76 */
77 get isGridSnapEnabled(): boolean { return this._isGridSnapEnabled; }
78 set isGridSnapEnabled(val: boolean) { this._isGridSnapEnabled = val; }
79
80 /**
81 * Gets or sets the node data object that is copied and added to the model
82 * when the drawing operation completes.
83 */
84 get archetypePartData(): go.ObjectData { return this._archetypePartData; }
85 set archetypePartData(val: go.ObjectData) { this._archetypePartData = val; }
86
87 /**
88 * Gets or sets the Shape that is used to hold the line as it is being drawn.
89 *
90 * The default value is a simple Shape drawing an unfilled open thin black line.
91 */
92 get temporaryShape(): go.Shape { return this._temporaryShape; }
93 set temporaryShape(val: go.Shape) {
94 if (this._temporaryShape !== val && val !== null) {
95 val.name = 'SHAPE';
96 const panel = this._temporaryShape.panel;
97 if (panel !== null) {
98 if (panel !== null) panel.remove(this._temporaryShape);
99 this._temporaryShape = val;
100 if (panel !== null) panel.add(this._temporaryShape);
101 }
102 }
103 }
104
105 /**
106 * Don't start this tool in a mode-less fashion when the user's mouse-down is on an existing Part.
107 * When this tool is a mouse-down tool, it requires using the left mouse button in the background of a modifiable Diagram.
108 * Modal uses of this tool will not call this canStart predicate.
109 */
110 public canStart(): boolean {
111 if (!this.isEnabled) return false;
112 const diagram = this.diagram;
113 if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
114 const model = diagram.model;
115 if (model === null) return false;
116 // require left button
117 if (!diagram.firstInput.left) return false;
118 // can't start when mouse-down on an existing Part
119 const obj = diagram.findObjectAt(diagram.firstInput.documentPoint, null, null);
120 return (obj === null);
121 }
122
123 /**
124 * Start a transaction, capture the mouse, use a "crosshair" cursor,
125 * and start accumulating points in the geometry of the {@link #temporaryShape}.
126 * @this {PolygonDrawingTool}
127 */
128 public doStart() {
129 super.doStart();
130 var diagram = this.diagram;
131 if (!diagram) return;
132 this.startTransaction(this.name);
133 diagram.currentCursor = diagram.defaultCursor = "crosshair";
134 if (!diagram.lastInput.isTouchEvent) diagram.isMouseCaptured = true;
135 }
136
137 /**
138 * Start a transaction, capture the mouse, use a "crosshair" cursor,
139 * and start accumulating points in the geometry of the {@link #temporaryShape}.
140 */
141 public doActivate(): void {
142 super.doActivate();
143 var diagram = this.diagram;
144 if (!diagram) return;
145 // the first point
146 if (!diagram.lastInput.isTouchEvent) this.addPoint(diagram.lastInput.documentPoint);
147 }
148
149 /**
150 * Stop the transaction and clean up.
151 */
152 public doStop(): void {
153 super.doStop();
154 var diagram = this.diagram;
155 if (!diagram) return;
156 diagram.currentCursor = diagram.defaultCursor = "auto";
157 if (this.temporaryShape !== null && this.temporaryShape.part !== null) {
158 diagram.remove(this.temporaryShape.part);
159 }
160 if (diagram.isMouseCaptured) diagram.isMouseCaptured = false;
161 this.stopTransaction();
162 }
163
164 /**
165 * @hidden @internal
166 * Given a potential Point for the next segment, return a Point it to snap to the grid, and remain orthogonal, if either is applicable.
167 */
168 public modifyPointForGrid(p: go.Point): go.Point {
169 const pregrid = p.copy();
170 const grid = this.diagram.grid;
171 if (grid !== null && grid.visible && this.isGridSnapEnabled) {
172 const cell = grid.gridCellSize;
173 const orig = grid.gridOrigin;
174 p = p.copy();
175 p.snapToGrid(orig.x, orig.y, cell.width, cell.height); // compute the closest grid point (modifies p)
176 }
177 if (this.temporaryShape.geometry === null) return p;
178 const geometry = this.temporaryShape.geometry;
179 if (geometry === null) return p;
180 const fig = geometry.figures.first();
181 if (fig === null) return p;
182 const segments = fig.segments;
183 if (this.isOrthoOnly && segments.count > 0) {
184 let lastPt = new go.Point(fig.startX, fig.startY); // assuming segments.count === 1
185 if (segments.count > 1) {
186 // the last segment is the current temporary segment, which we might be altering. We want the segment before
187 const secondLastSegment = (segments.elt(segments.count - 2));
188 lastPt = new go.Point(secondLastSegment.endX, secondLastSegment.endY);
189 }
190 if (pregrid.distanceSquared(lastPt.x, pregrid.y) < pregrid.distanceSquared(pregrid.x, lastPt.y)) { // closer to X coord
191 return new go.Point(lastPt.x, p.y);
192 } else { // closer to Y coord
193 return new go.Point(p.x, lastPt.y);
194 }
195 }
196 return p;
197 }
198
199
200 /**
201 * @hidden @internal
202 * This internal method adds a segment to the geometry of the {@link #temporaryShape}.
203 */
204 public addPoint(p: go.Point): void {
205 const diagram = this.diagram;
206 const shape = this.temporaryShape;
207 if (shape === null) return;
208
209 // for the temporary Shape, normalize the geometry to be in the viewport
210 const viewpt = diagram.viewportBounds.position;
211 const q = this.modifyPointForGrid(new go.Point(p.x - viewpt.x, p.y - viewpt.y));
212
213 const part = shape.part;
214 let geo: go.Geometry | null = null;
215 // if it's not in the Diagram, re-initialize the Shape's geometry and add the Part to the Diagram
216 if (part !== null && part.diagram === null) {
217 const fig = new go.PathFigure(q.x, q.y, true); // possibly filled, depending on Shape.fill
218 geo = new go.Geometry().add(fig); // the Shape.geometry consists of a single PathFigure
219 this.temporaryShape.geometry = geo;
220 // position the Shape's Part, accounting for the stroke width
221 part.position = viewpt.copy().offset(-shape.strokeWidth / 2, -shape.strokeWidth / 2);
222 diagram.add(part);
223 } else if (shape.geometry !== null) {
224 // must copy whole Geometry in order to add a PathSegment
225 geo = shape.geometry.copy();
226 const fig = geo.figures.first();
227 if (fig !== null) {
228 if (this.hasArcs) {
229 const lastseg = fig.segments.last();
230 if (lastseg === null) {
231 fig.add(new go.PathSegment(go.PathSegment.QuadraticBezier, q.x, q.y, (fig.startX + q.x) / 2, (fig.startY + q.y) / 2));
232 } else {
233 fig.add(new go.PathSegment(go.PathSegment.QuadraticBezier, q.x, q.y, (lastseg.endX + q.x) / 2, (lastseg.endY + q.y) / 2));
234 }
235 } else {
236 fig.add(new go.PathSegment(go.PathSegment.Line, q.x, q.y));
237 }
238 }
239 }
240 shape.geometry = geo;
241 }
242
243 /**
244 * @hidden @internal
245 * This internal method changes the last segment of the geometry of the {@link #temporaryShape} to end at the given point.
246 */
247 public moveLastPoint(p: go.Point): void {
248 p = this.modifyPointForGrid(p);
249 const diagram = this.diagram;
250 // must copy whole Geometry in order to change a PathSegment
251 const shape = this.temporaryShape;
252 if (shape.geometry === null) return;
253 const geo = shape.geometry.copy();
254 const fig = geo.figures.first();
255 if (fig === null) return;
256 const segs = fig.segments;
257 if (segs.count > 0) {
258 // for the temporary Shape, normalize the geometry to be in the viewport
259 const viewpt = diagram.viewportBounds.position;
260 const seg = segs.elt(segs.count - 1);
261 // modify the last PathSegment to be the given Point p
262 seg.endX = p.x - viewpt.x;
263 seg.endY = p.y - viewpt.y;
264 if (seg.type === go.PathSegment.QuadraticBezier) {
265 let prevx = 0.0;
266 let prevy = 0.0;
267 if (segs.count > 1) {
268 const prevseg = segs.elt(segs.count - 2);
269 prevx = prevseg.endX;
270 prevy = prevseg.endY;
271 } else {
272 prevx = fig.startX;
273 prevy = fig.startY;
274 }
275 seg.point1X = (seg.endX + prevx) / 2;
276 seg.point1Y = (seg.endY + prevy) / 2;
277 }
278 shape.geometry = geo;
279 }
280 }
281
282 /**
283 * @hidden @internal
284 * This internal method removes the last segment of the geometry of the {@link #temporaryShape}.
285 */
286 public removeLastPoint(): void {
287 // must copy whole Geometry in order to remove a PathSegment
288 const shape = this.temporaryShape;
289 if (shape.geometry === null) return;
290 const geo = shape.geometry.copy();
291 const fig = geo.figures.first();
292 if (fig === null) return;
293 const segs = fig.segments;
294 if (segs.count > 0) {
295 segs.removeAt(segs.count - 1);
296 shape.geometry = geo;
297 }
298 }
299
300 /**
301 * Add a new node data JavaScript object to the model and initialize the Part's
302 * position and its Shape's geometry by copying the {@link #temporaryShape}'s {@link Shape#geometry}.
303 */
304 public finishShape(): void {
305 const diagram = this.diagram;
306 const shape: go.Shape = this.temporaryShape;
307 if (shape !== null && this.archetypePartData !== null) {
308 // remove the temporary point, which is last, except on touch devices
309 if (!diagram.lastInput.isTouchEvent) this.removeLastPoint();
310 const tempgeo = shape.geometry;
311 // require 3 points (2 segments) if polygon; 2 points (1 segment) if polyline
312 if (tempgeo !== null) {
313 const tempfig = tempgeo.figures.first();
314 if (tempfig !== null && tempfig.segments.count >= (this.isPolygon ? 2 : 1)) {
315 // normalize geometry and node position
316 const viewpt = diagram.viewportBounds.position;
317 const copygeo = tempgeo.copy();
318 const copyfig = copygeo.figures.first();
319 if (this.isPolygon && copyfig !== null) {
320 // if polygon, close the last segment
321 const segs = copyfig.segments;
322 const seg = segs.elt(segs.count - 1);
323 seg.isClosed = true;
324 }
325 // create the node data for the model
326 const d = diagram.model.copyNodeData(this.archetypePartData);
327 if (d !== null) {
328 // adding data to model creates the actual Part
329 diagram.model.addNodeData(d);
330 const part = diagram.findPartForData(d);
331 if (part !== null) {
332 // assign the position for the whole Part
333 const pos = copygeo.normalize();
334 pos.x = viewpt.x - pos.x - shape.strokeWidth / 2;
335 pos.y = viewpt.y - pos.y - shape.strokeWidth / 2;
336 part.position = pos;
337 // assign the Shape.geometry
338 const pShape: go.Shape = part.findObject('SHAPE') as go.Shape;
339 if (pShape !== null) pShape.geometry = copygeo;
340 this.transactionResult = this.name;
341 }
342 }
343 }
344 }
345 }
346 this.stopTool();
347 }
348
349 /**
350 * Add another point to the geometry of the {@link #temporaryShape}.
351 */
352 public doMouseDown(): void {
353 const diagram = this.diagram;
354 if (!this.isActive) {
355 this.doActivate();
356 }
357 // a new temporary end point, the previous one is now "accepted"
358 this.addPoint(diagram.lastInput.documentPoint);
359 if (!diagram.lastInput.left) { // e.g. right mouse down
360 this.finishShape();
361 } else if (diagram.lastInput.clickCount > 1) { // e.g. double-click
362 this.removeLastPoint();
363 this.finishShape();
364 }
365 }
366
367 /**
368 * Move the last point of the {@link #temporaryShape}'s geometry to follow the mouse point.
369 */
370 public doMouseMove(): void {
371 const diagram = this.diagram;
372 if (this.isActive) {
373 this.moveLastPoint(diagram.lastInput.documentPoint);
374 }
375 }
376
377 /**
378 * Do not stop this tool, but continue to accumulate Points via mouse-down events.
379 */
380 public doMouseUp(): void {
381 // don't stop this tool (the default behavior is to call stopTool)
382 }
383
384 /**
385 * Typing the "ENTER" key accepts the current geometry (excluding the current mouse point)
386 * and creates a new part in the model by calling {@link #finishShape}.
387 *
388 * Typing the "Z" key causes the previous point to be discarded.
389 *
390 * Typing the "ESCAPE" key causes the temporary Shape and its geometry to be discarded and this tool to be stopped.
391 */
392 public doKeyDown(): void {
393 const diagram = this.diagram;
394 if (!this.isActive) return;
395 const e = diagram.lastInput;
396 if (e.key === '\r') { // accept
397 this.finishShape(); // all done!
398 } else if (e.key === 'Z') { // undo
399 this.undo();
400 } else {
401 super.doKeyDown();
402 }
403 }
404
405 /**
406 * Undo: remove the last point and continue the drawing of new points.
407 */
408 public undo(): void {
409 const diagram = this.diagram;
410 // remove a point, and then treat the last one as a temporary one
411 this.removeLastPoint();
412 const lastInput = diagram.lastInput;
413 if (lastInput.event instanceof MouseEvent) this.moveLastPoint(lastInput.documentPoint);
414 }
415}