1function mxFreehand(graph)
2{
3	// Graph must have a container
4	var svgElement = (graph.view != null && graph.view.canvas != null) ? graph.view.canvas.ownerSVGElement : null;
5
6	if (graph.container == null || svgElement == null)
7	{
8		return;
9	}
10
11	// Stops drawing on escape
12	graph.addListener(mxEvent.ESCAPE, mxUtils.bind(this, function()
13	{
14		this.stopDrawing();
15	}));
16
17	//Code inspired by https://stackoverflow.com/questions/40324313/svg-smooth-freehand-drawing
18	var bufferSize = mxFreehand.prototype.NORMAL_SMOOTHING;
19	var path = null;
20	var partPathes = [];
21	var strPath;
22	var drawPoints = [];
23	var lastPart;
24	var closedPath = false;
25	var autoClose = true;
26	var autoInsert = true;
27	var autoScroll = true;
28	var openFill = true;
29	var buffer = []; // Contains the last positions of the mouse cursor
30	var enabled = false;
31	var stopClickEnabled = true
32
33	this.setClosedPath = function(isClosed)//TODO add closed settings
34	{
35		closedPath = isClosed;
36	};
37
38	this.setAutoClose = function(isAutoClose)//TODO add auto closed settings
39	{
40		autoClose = isAutoClose;
41	};
42
43	this.setAutoInsert = function(value)
44	{
45		autoInsert = value;
46	};
47
48	this.setAutoScroll = function(value)
49	{
50		autoScroll = value;
51	};
52
53	this.setOpenFill = function(value)
54	{
55		openFill = value;
56	};
57
58	this.setStopClickEnabled = function(enabled)
59	{
60		stopClickEnabled = enabled;
61	};
62
63	this.setSmoothing = function(smoothing)//TODO add smoothing settings
64	{
65		bufferSize = smoothing;
66	};
67
68	var setEnabled = function(isEnabled)
69	{
70		enabled = isEnabled;
71		graph.getRubberband().setEnabled(!isEnabled);
72		graph.graphHandler.setSelectEnabled(!isEnabled);
73		graph.graphHandler.setMoveEnabled(!isEnabled);
74		graph.container.style.cursor = (isEnabled) ? 'crosshair' : '';
75		graph.fireEvent(new mxEventObject('freehandStateChanged'));
76	};
77
78	this.startDrawing = function()
79	{
80		setEnabled(true);
81	}
82
83	this.isDrawing = function()
84	{
85		return enabled;
86	};
87
88	var endPath = mxUtils.bind(this, function(e)
89	{
90	    if (path)
91	    {
92			var lastLength = lastPart.length;
93
94	    	// Click stops drawing
95	    	var doStop = stopClickEnabled && drawPoints.length > 0 &&
96	    		lastPart != null && lastPart.length < 2;
97
98			if (!doStop)
99			{
100				drawPoints.push.apply(drawPoints, lastPart);
101			}
102
103	        lastPart = [];
104			drawPoints.push(null);
105	        partPathes.push(path);
106	        path = null;
107
108			if (doStop || autoInsert)
109			{
110				this.stopDrawing();
111			}
112
113			if (autoInsert && lastLength >= 2)
114			{
115				this.startDrawing();
116			}
117
118	        mxEvent.consume(e);
119	    }
120	});
121
122	this.createStyle = function(stencil)
123	{
124		return mxConstants.STYLE_SHAPE + '=' + stencil + ';fillColor=none;';
125	};
126
127	this.stopDrawing = function()
128	{
129	    if (partPathes.length > 0)
130	    {
131	        var maxX = drawPoints[0].x, minX = drawPoints[0].x, maxY = drawPoints[0].y, minY = drawPoints[0].y;
132
133	        for (var i = 1; i < drawPoints.length; i++)
134	        {
135	        	if (drawPoints[i] == null) continue;
136
137	        	maxX = Math.max(maxX, drawPoints[i].x);
138	        	minX = Math.min(minX, drawPoints[i].x);
139	        	maxY = Math.max(maxY, drawPoints[i].y);
140	        	minY = Math.min(minY, drawPoints[i].y);
141	        }
142
143	        var w = maxX - minX, h = maxY - minY;
144
145	        if (w > 0 && h > 0)
146	        {
147		        var xScale = 100 / w;
148		        var yScale = 100 / h;
149
150		        drawPoints.map(function(p)
151		        {
152		        	if (p == null) return p;
153
154		        	p.x = (p.x - minX) * xScale;
155		        	p.y = (p.y - minY) * yScale;
156		        	return p;
157		        });
158
159		        //toFixed(2) to reduce size of output
160		        var drawShape = '<shape strokewidth="inherit"><foreground>';
161
162		        var start = 0;
163
164		        for (var i = 0; i < drawPoints.length; i++)
165		        {
166		        	var p = drawPoints[i];
167
168		        	if (p == null)
169	        		{
170		        		var tmpClosedPath = false;
171				        var startP =  drawPoints[start], endP = drawPoints[i - 1];
172
173				        if (!closedPath && autoClose)
174				        {
175					        var xdiff = startP.x - endP.x, ydiff = startP.y - endP.y;
176					        var startEndDist = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
177
178					        tmpClosedPath = startEndDist <= graph.tolerance;
179				        }
180
181				        if (closedPath || tmpClosedPath)
182			        	{
183				        	drawShape += '<line x="'+ startP.x.toFixed(2) + '" y="' + startP.y.toFixed(2) + '"/>';
184			        	}
185
186		        		drawShape += '</path>' + ((openFill || closedPath || tmpClosedPath)? '<fillstroke/>' : '<stroke/>');
187		        		start = i + 1;
188	        		}
189		        	else if (i == start)
190	        		{
191		        		drawShape += '<path><move x="'+ p.x.toFixed(2) + '" y="' + p.y.toFixed(2) + '"/>'
192	        		}
193		        	else
194		        	{
195		        		drawShape += '<line x="'+ p.x.toFixed(2) + '" y="' + p.y.toFixed(2) + '"/>';
196		        	}
197		        }
198
199		        drawShape += '</foreground></shape>';
200
201				if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
202				{
203	                var style = this.createStyle('stencil(' + Graph.compress(drawShape) + ')');
204	                var s = graph.view.scale;
205	            	var tr = graph.view.translate;
206
207	                var cell = new mxCell('', new mxGeometry(minX / s - tr.x, minY / s - tr.y, w / s, h / s), style);
208	                cell.vertex = 1;
209
210	                graph.model.beginUpdate();
211	                try
212					{
213	                	cell = graph.addCell(cell);
214
215		                graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [cell]));
216		                graph.fireEvent(new mxEventObject('freehandInserted', 'cell', cell));
217					}
218	                finally
219					{
220	                	graph.model.endUpdate();
221					}
222
223					graph.setSelectionCells([cell]);
224				}
225	        }
226
227	        for (var i = 0; i < partPathes.length; i++)
228	        {
229	        	partPathes[i].parentNode.removeChild(partPathes[i]);
230	        }
231
232	        path = null;
233	        partPathes = [];
234	        drawPoints = [];
235	    }
236
237        setEnabled(false);
238	};
239
240	// Stops all interactions if freehand is enabled
241	graph.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt)
242	{
243		var evtName = evt.getProperty('eventName');
244		var me = evt.getProperty('event');
245
246		if (evtName == mxEvent.MOUSE_MOVE && enabled)
247		{
248			if (me.sourceState != null)
249			{
250				me.sourceState.setCursor('crosshair');
251			}
252
253			me.consume();
254		}
255	}));
256
257	// Used to retrieve default styles
258	var edge = new mxCell();
259	edge.edge = true;
260
261	// Implements a listener for hover and click handling
262	graph.addMouseListener(
263	{
264	    mouseDown: mxUtils.bind(this, function(sender, me)
265	    {
266			if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
267		    {
268				var e = me.getEvent();
269
270				if (!enabled || mxEvent.isPopupTrigger(e) || mxEvent.isMultiTouchEvent(e))
271				{
272					return;
273				}
274
275				var defaultStyle = graph.getCurrentCellStyle(edge);
276				var strokeWidth = parseFloat(graph.currentVertexStyle[mxConstants.STYLE_STROKEWIDTH] || 1);
277				strokeWidth = Math.max(1, strokeWidth * graph.view.scale);
278				var strokeColor = mxUtils.getValue(graph.currentVertexStyle, mxConstants.STYLE_STROKECOLOR,
279					mxUtils.getValue(defaultStyle, mxConstants.STYLE_STROKECOLOR, '#000'))
280
281				if (strokeColor == 'default')
282				{
283					strokeColor = graph.shapeForegroundColor;
284				}
285
286			    path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
287			    path.setAttribute('fill', 'none');
288			    path.setAttribute('stroke', strokeColor);
289			    path.setAttribute('stroke-width', strokeWidth);
290
291			    if (graph.currentVertexStyle[mxConstants.STYLE_DASHED] == '1')
292			    {
293			    	var dashPattern = graph.currentVertexStyle[mxConstants.STYLE_DASH_PATTERN] || '3 3';
294
295			    	dashPattern = dashPattern.split(' ').map(function(p)
296					{
297			    		return parseFloat(p) * strokeWidth;
298					}).join(' ');
299			    	path.setAttribute('stroke-dasharray', dashPattern);
300			    }
301
302			    buffer = [];
303			    var pt = getMousePosition(e);
304			    appendToBuffer(pt);
305			    strPath = 'M' + pt.x + ' ' + pt.y;
306			    drawPoints.push(pt);
307			    lastPart = [];
308			    path.setAttribute('d', strPath);
309			    svgElement.appendChild(path);
310
311				me.consume();
312			}
313	    }),
314	    mouseMove: mxUtils.bind(this, function(sender, me)
315	    {
316		    if (path && graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
317		    {
318	    		var e = me.getEvent();
319				var pt = getMousePosition(e);
320		        appendToBuffer(pt);
321		        updateSvgPath();
322
323				if (autoScroll)
324				{
325					var tr = graph.view.translate;
326					graph.scrollRectToVisible(new mxRectangle(pt.x - tr.x, pt.y - tr.y).grow(20));
327				}
328
329				me.consume();
330		    }
331	    }),
332	    mouseUp: mxUtils.bind(this, function(sender, me)
333	    {
334			if (path && graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
335			{
336				endPath(me.getEvent());
337				me.consume();
338			}
339	    })
340	});
341
342	var getMousePosition = function (e)
343	{
344	    return mxUtils.convertPoint(graph.container, mxEvent.getClientX(e), mxEvent.getClientY(e));
345	};
346
347	var appendToBuffer = function (pt)
348	{
349	    buffer.push(pt);
350
351	    while (buffer.length > bufferSize)
352	    {
353	        buffer.shift();
354	    }
355	};
356
357	// Calculate the average point, starting at offset in the buffer
358	var getAveragePoint = function (offset)
359	{
360	    var len = buffer.length;
361
362	    if (len % 2 === 1 || len >= bufferSize)
363	    {
364	        var totalX = 0;
365	        var totalY = 0;
366	        var pt, i;
367	        var count = 0;
368
369	        for (i = offset; i < len; i++)
370	        {
371	            count++;
372	            pt = buffer[i];
373	            totalX += pt.x;
374	            totalY += pt.y;
375	        }
376
377	        return {
378	            x: totalX / count,
379	            y: totalY / count
380	        }
381	    }
382
383	    return null;
384	};
385
386	var updateSvgPath = function ()
387	{
388	    var pt = getAveragePoint(0);
389
390	    if (pt)
391	    {
392	        // Get the smoothed part of the path that will not change
393	        strPath += ' L' + pt.x + ' ' + pt.y;
394	        drawPoints.push(pt);
395	        // Get the last part of the path (close to the current mouse position)
396	        // This part will change if the mouse moves again
397	        var tmpPath = '';
398	        lastPart = [];
399
400	        for (var offset = 2; offset < buffer.length; offset += 2)
401	        {
402	            pt = getAveragePoint(offset);
403	            tmpPath += ' L' + pt.x + ' ' + pt.y;
404	            lastPart.push(pt);
405	        }
406
407	        // Set the complete current path coordinates
408	        path.setAttribute('d', strPath + tmpPath);
409	    }
410	};
411};
412
413mxFreehand.prototype.NO_SMOOTHING = 1;
414mxFreehand.prototype.MILD_SMOOTHING = 4;
415mxFreehand.prototype.NORMAL_SMOOTHING = 8;
416mxFreehand.prototype.VERY_SMOOTH_SMOOTHING = 12;
417mxFreehand.prototype.SUPER_SMOOTH_SMOOTHING = 16;
418mxFreehand.prototype.HYPER_SMOOTH_SMOOTHING = 20;
419