javascript

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

  • среда, 5 апреля 2017 г. в 03:14:20
https://habrahabr.ru/post/325640/
  • Промышленное программирование
  • Программирование
  • SCADA
  • JavaScript


В прошлой статье мы начали писать редактор мнемосхем. Сегодня статья о готовом редакторе. Вот, что получилось:



Остановились мы на том, что при загрузке SVG из файла разгруппированным у элементов со свойством transform=«translate(X Y)» указатели для изменения размера оказываются в левом верхнем углу, тогда как само изображение в координатах X Y. Обойти этот баг оказалось не так просто. Опытным путем было замечено, что если установить координаты в 0 0, запомнив перед этим их transformMatrix, а затем восстановить их до X Y указатели для изменения размера совпадут с изображением. И вот для этого и была написана следующая функция:

Перемещаем указатели для изменения размера куда надо
  function After_load() {   
      var kol = 0;   
canvas.forEachObject(function(obj){ 
var transformMatrix1 = [1,0,0,1,0,0];
    var str_x; 
    var str_y; 
    var tr_y; 
        var tx_sg = obj.toSVG();
            if ((tx_sg.indexOf('transform="translate')>=0) && (tx_sg.indexOf('<g ')>=0))
  {
     obj.setOriginX('left');
      obj.set('lockScalingX','false');
     obj.set('lockScalingY','false');
  }
        var transformMatrix2 = obj.get('transformMatrix');
        var strokeWidth1 = obj.getStrokeWidth();  
        var m_x = obj.getLeft();
 var m_y = obj.getTop();
 var calcTransformMatrix2 = obj.calcTransformMatrix();  
        transformMatrix1[1] = transformMatrix2[1];
        transformMatrix1[2] = transformMatrix2[2];
  if (!((tx_sg.indexOf('transform="translate')>=0) && (tx_sg.indexOf('<g ')>=0)))
  {
    obj.setTransformMatrix(transformMatrix1);
  }   
    obj.setTop(transformMatrix2[5]+m_y*transformMatrix2[3]);*/
if (tx_sg.indexOf('<line x1="0"')>=0){
      obj.setLeft(transformMatrix2[4]+(m_x)*transformMatrix2[0]-(strokeWidth1/2)*transformMatrix2[0]);//(m_x-0.5)*transformMatrix2[0]
       obj.setTop(transformMatrix2[5]+(m_y)*transformMatrix2[3]-(strokeWidth1/2)*transformMatrix2[3]); //(m_y-0.5)*transformMatrix2[3]     
}
else  
  if ((tx_sg.indexOf('<rect ')>=0) || (tx_sg.indexOf('<polygon ')>=0) || (tx_sg.indexOf('<line x1="-')>=0)|| (tx_sg.indexOf('<circle cx=')>=0))
  {
        obj.setLeft(transformMatrix2[4]+(m_x)*transformMatrix2[0]-(strokeWidth1/2)*transformMatrix2[0]);//(m_x-0.5)*transformMatrix2[0]
       obj.setTop(transformMatrix2[5]+(m_y)*transformMatrix2[3]-(strokeWidth1/2)*transformMatrix2[3]); //(m_y-0.5)*transformMatrix2[3]
}
else
  if (tx_sg.indexOf('<path ')>=0)
  {
       obj.setLeft(transformMatrix2[4]+(m_x-strokeWidth1/2)*transformMatrix2[0]);
       obj.setTop(transformMatrix2[5]+(m_y-strokeWidth1/2)*transformMatrix2[3]); 
}
else
    if ((tx_sg.indexOf('transform="translate')>=0) && (tx_sg.indexOf('<g ')>=0))//if (tx_sg.indexOf('<g transform="translate')>=0)
  {
   var  poz2 = tx_sg.indexOf("</tspan>");
   var  poz1 = tx_sg.indexOf("<tspan ");
   if ((poz2>0) && (poz1>0))
   {
    var  poz3 = tx_sg.indexOf(">",poz1+1);
    if (poz3>0){
      var str = tx_sg.substring(poz3+1,poz2);    
    }
  }//  if ((poz2>0) && (poz1>0)) 
tx_sg = obj.toSVG();
poz2 = tx_sg.indexOf("</tspan>");
poz1 = tx_sg.indexOf("<tspan ");
   if ((poz2>0) && (poz1>0))
   {
    var  poz3 = tx_sg.indexOf('x="',poz1+1);
    if (poz3>0){
      var  poz4 = tx_sg.indexOf('"',poz3+4);
      str_x = tx_sg.substring(poz3+3,poz4);      
    }
      poz3 = tx_sg.indexOf('y="',poz1+1);
    if (poz3>0){
      var  poz4 = tx_sg.indexOf('"',poz3+4);
      str_y = tx_sg.substring(poz3+3,poz4);      
    }
   }//  if ((poz2>0) && (poz1>0)) 
 poz1 = tx_sg.indexOf('transform="translate(');
 if (poz1>0)
    {
         poz2 = tx_sg.indexOf(" ",poz1+21);
         var  poz3 = tx_sg.indexOf(')',poz2+1);
          tr_y = tx_sg.substring(poz2,poz3);   
    }
             m_x = obj.getLeft();
             m_y = obj.getTop();
             transformMatrix2 = obj.get('transformMatrix');
   	      obj.setTransformMatrix(transformMatrix1);
             obj.setTop(transformMatrix2[5]-parseFloat(tr_y)-parseFloat(str_y)-strokeWidth1*0.58);
       obj.setLeft(transformMatrix2[4] + parseFloat(str_x) - (strokeWidth1/2) );
}
else
  {
             obj.setLeft(transformMatrix2[4]+(m_x)*transformMatrix2[0]-(strokeWidth1/2)*transformMatrix2[0]);//(m_x-0.5)*transformMatrix2[0]
       obj.setTop(transformMatrix2[5]+(m_y)*transformMatrix2[3]-(strokeWidth1/2)*transformMatrix2[3]); //(m_y-0.5)*transformMatrix2[3]    
}
       obj.setScaleX(transformMatrix2[0]);
       obj.setScaleY(transformMatrix2[3]);         
       obj.set('transparentCorners','true');
       obj.setCoords();
       kol = kol + 1;
});
if (kol === 0)
{
 $.post("After_load", kol); 
}
else
  {
    Is_After_load = true;
}
    };


После загрузки документа canvas не всегда содержал элементы. Они появлялись на нем несколько позже. Пришлось делать вот так:

Вызываем эту функцию через 5 секунд после загрузки страницы
setTimeout(function() {
  if (!Is_After_load){
   After_load(); 
  }
}, 5000); 


Теперь от запуска в браузере перейдем к собственному редактору. Html-страницы будем отдавать через компонент idhttpserver. Для отображения будем использовать Chromium, в точнее компонент DcefBrowser. Idhttpserver открывает порт 15500, Chromium открывает страницу 127.0.0.1:15500/. Можно, например, открыть 127.0.0.1:15500/ любым другим браузером и вести редактирование с помощью него. Подсовывая свой Chromium пользователю, а не заставляя его использовать свой браузер, мы даём ему браузер в котором гарантированно будут работать java-скрипты так как нам нужно.

Index.html через idhttpserver мы отдаём немного модифицированным:

  1. Canvas становится таких размеров, какие задал пользователь в настройках.
  2. Загружается тот SVG-файл, который сейчас редактирует пользователь.
  3. Подгружаются пользовательские SVG-файлы из библиотеки изображений.
  4. Заполняется список переменных, получаемых от ОРС-серверов.

С помощью методов $.post и toSVG можно сделать следующие вещи:

Сохранение схемы в SVG:

function Post_sheme() {    
$.post("save.php", canvas.toSVG());
};

В http-сервере ловим post-запрос и сохраняем в SVG ARequestInfo.UnparsedParams.
Добавление SVG-изображения в библиотеку. Выделяем несколько элементов, нажимаем на кнопку «Добавить выделенные в библиотеку», выполняется $.post("new.php?"+$("input[name=namenewsvg]").val(), Buff_clipb);
Где $("input[name=namenewsvg]").val() – это имя SVG-изображения.
Копирование в буфер обмена операционной системы. Выделяем несколько элементов, нажимаем на кнопку «копировать», выполняется следующий скрипт:

Заголовок спойлера
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>';
$.post("copy.php", tx);


В http-сервере ловим post-запрос и копируем в буфер обмена

Clipboard.AsText := ARequestInfo.UnparsedParams;

Теперь можно открыть другую схему и вставить в неё SVG. Для этого перейти на вкладку «Загрузить SVG» — вставить в текстовое поле содержимое буфера обмена, нажать на кнопку «Загрузить».

Теперь перейдем к установлению связей между переменной, получающей своё значение от ОРС-сервера и SVG-изображением.

Для привязки аналоговых переменных используется элемент текст. Надо создать текст на вкладке «текст». Выделить его. На вкладке «Привязка» выбрать имя переменной. При этом текст станет {{val.Имя_переменной}}. Т.е. привязка текста осуществляется через содержимое text.

Для отображения дискретной переменной надо использовать 2 элемента. Один будет использоваться для отображения включённого состояния, другой для отключенного. Т.е. когда элемент включен, будет видим элемент, отображающий включенное состояние. Элемент, отображающий отключенное состояние будет невидим.

Чтобы привязать элемент, отображающий включенное состояние надо выделить его, в выпадающем списке выберать Имя_переменной_on. Чтобы привязать элемент, отображающий отключенное состояние нужно выделить его, в выпадающем списке выбрать Имя_переменной_off. Стрелками на клавиатуре переместить элементы так, чтобы один оказался под другим.

Привязка дискретных переменных осуществляется через id.
function setIDObj() {    
     var activeObject = canvas.getActiveObject();
     if (activeObject) {
    activeObject.set({
     id : $("input[name=nameobj]").val()
     });
     var tx_sg = activeObject.toSVG(); 
     var poz2 = tx_sg.indexOf("</tspan>");
     var poz1 = tx_sg.indexOf("<tspan ");  
      if ((poz2>0) && (poz1>0))
   {
    var ttx = $("input[name=nameobj]").val();
    if (ttx.indexOf("{{")<=0) 
    {
      ttx = "{{val."+ttx+"}}";
    }
     activeObject.set({
        text: ttx
   });
    canvas.renderAll();
   }
   }
  };


Теперь про рисование. Многие графические редакторы позволяют сохранять в формате SVG. Пока я использую следующую схему.



Уже имеющуюся схему открываю в Visio, копирую её в Inkscape, сохраняю в SVG, открываю в блокноте, копирую, вставляю в редакторе через вкладку «Загрузить SVG» — текстовое поле – кнопка «загрузить».

Из Visio в Inkscape приходится копировать, т.к. Visio создает SVG, который FabricJS не может нормально рендерить.

Посмотреть редактор online без возможности сохранения можно здесь.

Скачать прототип SCADA-системы с редактором здесь.

А в следующей статье мы будем оживлять нарисованную схему.