Всем добрый день. Сегодня я расскажу как на fabric.js я написал редактор мнемосхем для SCADA-системы. Доля декстопных SCADA-систем медленно но верно уменьшается. Всё переводится на Web, и АСУ ТП тут не исключение.
Итак, что хотим получить в конечном итоге: Сервер SCADA выполняется в качестве службы Windows. Технологические данные получаем от ОРС-серверов. Для обслуживания http-запросов на сервере используем компонент idhttpServer. На клиентской стороне мнемосхему будем отображать в браузере. Графика только SVG, чтобы схема сама изменялась под разрешение экрана пользователя.
Соответственно, что хотим от редактора:
Всё это можно было реализовать, написав редактор под Windows. Но сложность была в рендеринге мелких SVG-изображений. И я подумал, если отображать мнемосхему будем в браузере, то почему бы в браузере её и не нарисовать? Ведь браузер лучше всех SVG и рендерит.
Тут и нашелся fabric.js, который для этих целей почти подошел.
Добавление, копирование, вставка простых элементов делается тривиально, всё как написано в документации. Тут приводить это не буду.
А вот со вставкой и копированием SVG-изображений не всё гладко. Далее я буду описывать, как я обходил баги fabric.js.
Есть 2 способа вставить готовое SVG-изображение на canvas:
Основная проблема в том, что когда у SVG-элемента
Судя по этому такая болячка у него с самого рождения. Значит, SVG-изображение вставляем уже сгруппированным.
Тот же баг (или фича?) проявляется при копировании/вставке. Стандартный метод clone c SVG не работает. При вставке указатели для изменения размера не совмещаются со вставленным изображением, поэтому сначала приводим координаты исходного изображения к 0:
Затем приведенное изображение представляем в виде текста, создаём новое изображение из текста, восстанавливаем координаты исходного изображения.
С рисованием линий тоже не все гладко. Толщина линии в fabric.js зависит от длины линии, что довольно-таки странно. Поэтому, линию вставляем как SVG.
Двигать стрелками клавиатуры элементы очень удобно.
Привязка к тэгам SCADA-системы осуществляется через id элемента.
В SCADA для отображения аналоговых величин будем использовать текстовое поле text, для дискретных любое изображение. У изображения будем менять либо цвет, либо прозрачность. Т.е. создадим 2 изображения, одно привяжем ко включенному состоянию, другое к отключенному. Когда состояние включенное, установим у первого изображения прозрачность 1, у второго 0.
Сохранение. Сохранять будем в формате SVG. Для этого воспользуемся
Загружаем SVG из файла полностью разгруппированным. И видим, что у элементов со свойством
Придется для Web-сервера писать костыль, который бы обнулял координаты
В Web-сервер редактора мнемосхему будем сохранять методом POST.
Продолжение во 2-й части.
Итак, что хотим получить в конечном итоге: Сервер 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.
Продолжение во 2-й части.
