📜 ⬆️ ⬇️

Writing mnemonic editor for SCADA-system on Fabric.js

Good day everyone. Today I will tell you how I wrote a mnemoscheme editor for a SCADA system on fabric.js. The share of dextop SCADA-systems is slowly but surely decreasing. Everything translates to the Web, and the process control system is no exception.

So, what we want to get in the end: SCADA server runs as a Windows service. Technological data obtained from OPC servers. To service http requests on the server, we use the idhttpServer component. On the client side, the mnemonic will be displayed in the browser. The graphics are only SVG, so that the scheme itself changes to the user's screen resolution.

Accordingly, what we want from the editor:


All this could be done by writing an editor under Windows. But the difficulty was in rendering small SVG images. And I thought, if we display the mnemonic diagram in the browser, then why not draw it in the browser? After all, the browser is the best SVG and render.
This is where fabric.js was found, which almost came up for this purpose.
')
Adding, copying, pasting simple elements is trivial, everything is as written in the documentation. Here I will not bring it.

But with the insertion and copying of SVG-images is not all smooth. Next, I will describe how I circumvented the fabric.js bugs.

There are 2 ways to insert a finished SVG image on canvas:


The main problem is that when the SVG element <rect x="0" y="0" has the property transform="translate(168 202)" , the SVG is drawn on the canvas in coordinates 168 202 , and the pointers for resizing appear in another place, in the coordinates x="0" y="0" .

Judging by this such a sore from his birth. This means that we insert the SVG image already grouped.

 var addShape = function(shapeName) { fabric.loadSVGFromURL('./assets/' + shapeName + '.svg', function(objects, options) { var loadedObject = fabric.util.groupSVGElements(objects, options); loadedObject.set({ left: 0, top: 0, angle: 0 }) .setCoords(); canvas.add(loadedObject); }); }; 

The same bug (or feature?) Appears when copying / pasting. The standard clone method with SVG does not work. When inserting, the pointers for resizing do not overlap with the inserted image, so first we bring the coordinates of the original image to 0:

 canvas.getActiveGroup().setTop(0); canvas.getActiveGroup().setLeft(0); 

Then, we present the image in the form of text, create a new image from the text, restore the coordinates of the original image.

Copy to internal buffer:
 CopyClip = function() { var activeObject = canvas.getActiveObject(), activeGroup = canvas.getActiveGroup(); if (activeGroup) { var tx_top = canvas.getActiveGroup().getTop(); var tx_left = canvas.getActiveGroup().getLeft(); var tx_Angle = canvas.getActiveGroup().getAngle(); canvas.getActiveGroup().setAngle(0); canvas.getActiveGroup().setTop(0); canvas.getActiveGroup().setLeft(0); var tx = canvas.getActiveGroup().toSVG(); tx = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">' +tx+ '</svg>'; canvas.getActiveGroup().setAngle(tx_Angle); canvas.getActiveGroup().setTop(tx_top); canvas.getActiveGroup().setLeft(tx_left); var _loadSVG = function(svg) { fabric.loadSVGFromString(svg, function(objects, options) { var obj = fabric.util.groupSVGElements(objects, options); canvas.add(obj).centerObject(obj).renderAll(); obj.setCoords(); }); } var _loadSVGWithoutGrouping = function(svg) { fabric.loadSVGFromString(svg, function(objects) { canvas.add.apply(canvas, objects); canvas.renderAll(); }); }; Buff_clipb = tx; canvas.getActiveGroup().setAngle(tx_Angle); } else if (activeObject) { var tx_top = canvas.getActiveObject().getTop(); var tx_left = canvas.getActiveObject().getLeft(); var tx_Angle = canvas.getActiveObject().getAngle(); canvas.getActiveObject().setAngle(0); canvas.getActiveObject().setTop(0); canvas.getActiveObject().setLeft(0); var tx = canvas.getActiveObject().toSVG(); tx = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">' +tx+ '</svg>'; canvas.getActiveObject().setAngle(tx_Angle); canvas.getActiveObject().setTop(tx_top); canvas.getActiveObject().setLeft(tx_left); var _loadSVG = function(svg) { fabric.loadSVGFromString(svg, function(objects, options) { var obj = fabric.util.groupSVGElements(objects, options); canvas.add(obj).centerObject(obj).renderAll(); obj.setCoords(); }); } Buff_clipb = tx; canvas.getActiveObject().setAngle(tx_Angle); }; }; 


Paste from internal buffer:
 PasteClip = function() { var _loadSVG = function(svg) { fabric.loadSVGFromString(svg, function(objects, options) { var obj = fabric.util.groupSVGElements(objects, options); canvas.add(obj).centerObject(obj).renderAll(); obj.setCoords(); }); } _loadSVG(Buff_clipb); }; 


With drawing lines, too, not everything is smooth. The thickness of the line in fabric.js depends on the length of the line, which is rather strange. Therefore, insert the line as SVG.

Line insert
 function addLineGoriz(wid) { var wid2; wid2 = $("#spinner[name=Line_widht_value]").spinner("value"); console.log('Line_widht_value ', wid2); var SVGValue_txt; if (tek_Stroke_color[0] != "#") { tek_Stroke_color = "#"+tek_Stroke_color}; var Stroke_col = tek_Stroke_color; SVGValue_txt = "<Line x1=\"370\" y1=\"90\" x2=\"570\" y2=\"90\" style=\"stroke: "+Stroke_col+"; stroke-width:"+wid2 +"px;\" />"; SVGValue_txt = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">' +SVGValue_txt+ '</svg>'; var _loadSVGWithoutGrouping = function(svg) { fabric.loadSVGFromString(svg, function(objects) { canvas.add.apply(canvas, objects); canvas.renderAll(); }); }; _loadSVGWithoutGrouping(SVGValue_txt); }; 


Move the keyboard arrows elements very convenient.

It is done trivially:
 $(document).keydown(function(eventObject){ var activeObject = canvas.getActiveObject(), activeGroup = canvas.getActiveGroup(); if ($("[name=Line_widht_value]").is(":focus")) { var val_width=$( "#spinner[name=Line_widht_value]" ).spinner("value"); if (activeObject) { activeObject.setStrokeWidth(val_width); } } if ($("[name=opacity_value]").is(":focus")) { var val_width=$( "[name=opacity_value]" ).spinner("value"); if (activeObject) { activeObject.set("opacity",val_width); } } if ($("[name=font_size_value]").is(":focus")) { var val_size=$( "#spinnerfont[name=font_size_value]" ).spinner("value"); if (activeObject) { activeObject.set('fontSize',val_size); } } if ((!($("[name=nameobj]").is(":focus")))&& (!($("[name=Line_widht_value]").is(":focus")))&& (!($("[name=opacity_value]").is(":focus")))&&(!($("[name=nametxt]").is(":focus")))&& (!($("[name=font_size_value]").is(":focus"))) ) { if (activeGroup) { if (eventObject.which == 37) { activeGroup.setLeft(activeGroup.getLeft()-1); } if (eventObject.which == 39) { activeGroup.setLeft(activeGroup.getLeft()+1); } if (eventObject.which == 38) { activeGroup.setTop(activeGroup.getTop()-1); } if (eventObject.which == 40) { activeGroup.setTop(activeGroup.getTop()+1); } if (eventObject.which == 46) { var objectsInGroup = activeGroup.getObjects(); canvas.discardActiveGroup(); objectsInGroup.forEach(function(object) { canvas.remove(object); }); } if (eventObject.which == 67) { CopyClip(); } if (eventObject.which == 86) { PasteClip(); } } else if (activeObject) { if (eventObject.which == 37) { activeObject.setLeft(activeObject.getLeft()-1); } if (eventObject.which == 39) { activeObject.setLeft(activeObject.getLeft()+1); } if (eventObject.which == 38) { activeObject.setTop(activeObject.getTop()-1); } if (eventObject.which == 40) { activeObject.setTop(activeObject.getTop()+1); } if (eventObject.which == 46) { canvas.remove(activeObject); } if (eventObject.which == 67) { CopyClip(); } if (eventObject.which == 86) { PasteClip(); } } } }); 


Binding to the tags of the SCADA-system is carried out through the element id.

 function setIDObj() { var activeObject = canvas.getActiveObject(); if (activeObject) { activeObject.set({ id : $("input[name=nameobj]").val() }); } }; 

In SCADA for displaying analog values, we will use the text text field, for discrete any image. The image will change either color or transparency. Those. let's create 2 images, we will tie one to the enabled state, the other to the disabled one. When the state is on, set the transparency of the first image to 1, the second to 0.

Preservation. We will save in SVG format. To do this, use canvas.toSVG().

Opening a mimic in a new tab:
  rasterizeSVG = function() { window.open( 'data:image/svg+xml;utf8,' + encodeURIComponent(canvas.toSVG())); }; 


Opening from file:
  var Loadfromfile = function(shapeName) { fabric.loadSVGFromURL(shapeName + '.svg', function(objects, options) { canvas.add.apply(canvas, objects); canvas.renderAll(); }); }; 


We load the SVG from the file completely ungrouped. And we see that the elements with the property transform="translate(XY)" and the pointers for resizing were in the upper left corner, whereas the image itself in the coordinates of X Y.

It is necessary for the Web server to write a crutch that would reset the translate coordinates and translate them to x="X" y="Y" .

In the web-server of the editor, the mnemonic will be saved using the POST method.
Continued in the 2nd part.

Source: https://habr.com/ru/post/322990/


All Articles