Пишем редактор мнемосхем для SCADA-системы на Fabric.js

в 11:48, , рубрики: Fabric.js, javascript, scada, Промышленное программирование

Всем добрый день. Сегодня я расскажу как на fabric.js я написал редактор мнемосхем для SCADA-системы. Доля декстопных SCADA-систем медленно но верно уменьшается. Всё переводится на Web, и АСУ ТП тут не исключение.

Итак, что хотим получить в конечном итоге: Сервер SCADA выполняется в качестве службы Windows. Технологические данные получаем от ОРС-серверов. Для обслуживания http-запросов на сервере используем компонент idhttpServer. На клиентской стороне мнемосхему будем отображать в браузере. Графика только SVG, чтобы схема сама изменялась под разрешение экрана пользователя.

Соответственно, что хотим от редактора:

  • Рисование простейших фигур: прямоугольники, круги, линии, текст.
  • Вставка готовых SVG-изображений, созданных в других более продвинутых редакторах.
  • Изменение размеров фигур, перемещение мышкой по экрану, перемещение стрелками клавиатуры.
  • Копирование/вставка/удаление.
  • Привязка объекта к данным, получаемым от оборудования (по ID).
  • Сохранение в файл, открытие из файла.

Всё это можно было реализовать, написав редактор под Windows. Но сложность была в рендеринге мелких SVG-изображений. И я подумал, если отображать мнемосхему будем в браузере, то почему бы в браузере её и не нарисовать? Ведь браузер лучше всех SVG и рендерит.
Тут и нашелся fabric.js, который для этих целей почти подошел.

Добавление, копирование, вставка простых элементов делается тривиально, всё как написано в документации. Тут приводить это не буду.

А вот со вставкой и копированием SVG-изображений не всё гладко. Далее я буду описывать, как я обходил баги fabric.js.

Есть 2 способа вставить готовое SVG-изображение на canvas:

  • всё SVG-изображение вставляется как единое целое (сгруппированное)
  • изображение вставляется сразу разгруппированным. Каждый элемент можно по отдельности двигать.

Основная проблема в том, что когда у SVG-элемента <rect x="0" y="0" есть свойство transform="translate(168 202)", то SVG рисуется на канве в координатах 168 202, а указатели для изменения размера оказываются в другом месте, в координатах x="0" y="0".

Судя по этому такая болячка у него с самого рождения. Значит, SVG-изображение вставляем уже сгруппированным.

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);
    });
  };

Тот же баг (или фича?) проявляется при копировании/вставке. Стандартный метод clone c SVG не работает. При вставке указатели для изменения размера не совмещаются со вставленным изображением, поэтому сначала приводим координаты исходного изображения к 0:


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

Затем приведенное изображение представляем в виде текста, создаём новое изображение из текста, восстанавливаем координаты исходного изображения.

Копирование во внутренний буфер:

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);       
  };            
   };

Вставка из внутреннего буфера:

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);
  };

С рисованием линий тоже не все гладко. Толщина линии в fabric.js зависит от длины линии, что довольно-таки странно. Поэтому, линию вставляем как SVG.

Вставка линии

  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); 
  };

Двигать стрелками клавиатуры элементы очень удобно.

Делается тривиально:

$(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(); 
          }
         }
     	}
      });

Привязка к тэгам SCADA-системы осуществляется через id элемента.

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

В SCADA для отображения аналоговых величин будем использовать текстовое поле text, для дискретных любое изображение. У изображения будем менять либо цвет, либо прозрачность. Т.е. создадим 2 изображения, одно привяжем ко включенному состоянию, другое к отключенному. Когда состояние включенное, установим у первого изображения прозрачность 1, у второго 0.

Сохранение. Сохранять будем в формате SVG. Для этого воспользуемся canvas.toSVG().

Открытие мнемосхемы в новой вкладке:

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

Открытие из файла:

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

Загружаем SVG из файла полностью разгруппированным. И видим, что у элементов со свойством transform="translate(X Y)" а указатели для изменения размера оказались в левом верхнем углу, тогда как само изображение в координатах X Y.

Придется для Web-сервера писать костыль, который бы обнулял координаты translate и переводил их в x="X" y="Y".

В Web-сервер редактора мнемосхему будем сохранять методом POST.

Автор: Opc-server

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js