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 | */
|
11 | import * 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 | */
|
26 | export 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 | }
|