UNPKG

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