Всем добрый день. Сегодня я расскажу как на 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