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