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



Остановились мы на том, что при загрузке 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-системы с редактором здесь.

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