предисловие
Эта статья поможет вам создать собственный набор инструментов для рисования на основе React.
Адрес проекта, который я разместил по адресуgithubДобро пожаловать на критику и советы!Выполненные функции будут синхронно обновляться на мой гитхаб в будущемблогначальство.
Строительство окружающей среды
- Инициализировать проект
create-react-app project
-
Установить зависимости
"@topology/activity-diagram": "^0.2.24", "@topology/chart-diagram": "^0.3.0", "@topology/class-diagram": "^0.2.24", "@topology/core": "^0.3.1", "@topology/flow-diagram": "^0.2.24", "@topology/layout": "^0.3.0", "@topology/sequence-diagram": "^0.2.24", "antd": "^3.26.7",
Что касается кода базовой раскладки, то вы можете играть свободно, и в этой статье мы не будем вдаваться в подробности. Если вы хотите сразу приступить к работе с основными функциями, вы можете напрямую клонироватьсуществующий склад~
Хорошо!После завершения построения базовой среды проекта, вы можете приступить к выполнению следующих функциональных пунктов один за другим.
Функции
Пример пользовательского изображения
Область рендеринга графики в левой части главной страницы, вы можете настроить рендеринг.
const Layout = ({ Tools, onDrag }) => {
return Tools.map((item, index) => (
<div key={index}>
<div className="title">{item.group}</div>
<div className="button">
{item.children.map((item, idx) => {
// eslint-disable-next-line jsx-a11y/anchor-is-valid
return (
<a
key={idx}
title={item.name}
draggable
href="/#"
onDragStart={(ev) => onDrag(ev, item)}
>
<i className={'iconfont ' + item.icon} style={{ fontSize: 13 }}></i>
</a>
);
})}
</div>
</div>
));
};
пользовательский источник данных CI,icon: 'icon-image'
Относится к небольшому икону, показанному слева. Значение атрибута имени данных данных представляетimage
, топология использует это свойство, чтобы определить, является ли отрисовка изображением.
Значением атрибута изображения является адрес изображения.
{
group: '自定义图片',
children: [
{
name: 'image',
icon: 'icon-image',
data: {
text: '',
rect: {
width: 100,
height: 100
},
name: 'image',
image: require('./machine.jpg')
}
},
]
}
Поддержка функции добавления онлайн-изображений
Если вам неудобно использовать локальные изображения, мы можем переключиться на онлайн-изображения.
Сначала мы получаем base64 на основе URL-адреса изображения.
function getBase64(url, callback) {
var Img = new Image(),
dataURL = '';
Img.src = url + '?v=' + Math.random();
Img.setAttribute('crossOrigin', 'Anonymous');
Img.onload = function () {
var canvas = document.createElement('canvas'),
width = Img.width,
height = Img.height;
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(Img, 0, 0, width, height);
dataURL = canvas.toDataURL('image/jpeg');
return callback ? callback(dataURL) : null;
};
}
наконец прошлоonDrag
Метод может быть онлайн-изображение на холсте.
const onDrag = (event, image) => {
event.dataTransfer.setData(
'Text',
JSON.stringify({
name: 'image',
rect: {
width: 100,
height: 100
},
image
})
);
};
Поддержка нового файла, открытие файла, экспорт json, сохранение png и svg
-
Создайте пустой артборд
canvas.open({ nodes: [], lines: [] });
-
Открытие файла с существующим чертежом
const onHandleImportJson = () => { const input = document.createElement('input'); input.type = 'file'; input.onchange = event => { const elem = event.srcElement || event.target; if (elem.files && elem.files[0]) { const reader = new FileReader(); reader.onload = e => { const text = e.target.result + ''; try { const data = JSON.parse(text); canvas.open(data); } catch (e) { return false; } finally { } }; reader.readAsText(elem.files[0]); } }; input.click(); }
-
Сохраните нарисованное изображение в виде файла json.
import * as FileSaver from 'file-saver'; FileSaver.saveAs( new Blob([JSON.stringify(canvas.data)], { type: 'text/plain;charset=utf-8' }), `le5le.topology.json` );
-
сохранить как файл png
canvas.saveAsImage('le5le.topology.png');
-
Сохранить как файл SVG
const onHandleSaveToSvg = () => { const C2S = window.C2S; const ctx = new C2S(canvas.canvas.width + 200, canvas.canvas.height + 200); if (canvas.data.pens) { for (const item of canvas.data.pens) { item.render(ctx); } } let mySerializedSVG = ctx.getSerializedSvg(); mySerializedSVG = mySerializedSVG.replace( '<defs/>', `<defs> <style type="text/css"> @font-face { font-family: 'topology'; src: url('http://at.alicdn.com/t/font_1331132_h688rvffmbc.ttf?t=1569311680797') format('truetype'); } </style> </defs>` ); mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, '&#x'); const urlObject = window.URL || window; const export_blob = new Blob([mySerializedSVG]); const url = urlObject.createObjectURL(export_blob); const a = document.createElement('a'); a.setAttribute('download', 'le5le.topology.svg'); a.setAttribute('href', url); const evt = document.createEvent('MouseEvents'); evt.initEvent('click', true, true); a.dispatchEvent(evt); }
-
Отменить, повторить, скопировать, вырезать, вставить
canvas.undo(); // 撤销 canvas.redo(); // 恢复 canvas.copy(); // 复制 canvas.cut(); // 剪切 canvas.paste(); // 粘贴
Поддержка настройки свойств внешнего вида узла (поле размера позиции, стиль границы, стиль шрифта)
- Расположение и размер узла
const renderForm = useMemo(() => {
return <Form>
<Row>
<Col span={12}>
<Form.Item label="X(px)">
{getFieldDecorator('x', {
initialValue: x
})(<InputNumber />)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Y(px)" name="y">
{getFieldDecorator('y', {
initialValue: y
})(<InputNumber />)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="宽(px)" name="width">
{getFieldDecorator('width', {
initialValue: width
})(<InputNumber />)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="高(px)" name="height">
{getFieldDecorator('height', {
initialValue: height
})(<InputNumber />)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="角度(deg)" name="rotate">
{getFieldDecorator('rotate', {
initialValue: rotate
})(<InputNumber />)}
</Form.Item>
</Col>
</Row>
</Form>
}, [x, y, width, height, rotate, getFieldDecorator]);
- стиль границы
const renderStyleForm = useMemo(() => {
return <Form>
<Row>
<Col span={24}>
<Form.Item label="线条颜色">
{getFieldDecorator('strokeStyle', {
initialValue: strokeStyle
})(<Input type="color" />)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="线条样式">
{getFieldDecorator('dash', {
initialValue: dash
})(
<Select style={{ width: '95%' }}>
<Option value={0}>_________</Option>
<Option value={1}>---------</Option>
<Option value={2}>_ _ _ _ _</Option>
<Option value={3}>- . - . - .</Option>
</Select>
)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="线条宽度">
{getFieldDecorator('lineWidth', {
initialValue: lineWidth
})(<InputNumber style={{ width: '100%' }} />)}
</Form.Item>
</Col>
</Row>
</Form>
}, [lineWidth, strokeStyle, dash, getFieldDecorator]);
- настройки шрифта
const renderFontForm = useMemo(() => {
return <Form>
<Col span={24}>
<Form.Item label="字体颜色">
{getFieldDecorator('color', {
initialValue: color
})(<Input type="color" />)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="字体类型">
{getFieldDecorator('fontFamily', {
initialValue: fontFamily
})(<Input />)}
</Form.Item>
</Col>
<Col span={11} offset={1}>
<Form.Item label="字体大小">
{getFieldDecorator('fontSize', {
initialValue: fontSize
})(<InputNumber />)}
</Form.Item>
</Col>
<Col span={24}>
<Form.Item label="内容">
{getFieldDecorator('text', {
initialValue: text
})(<TextArea />)}
</Form.Item>
</Col>
</Form>
}, [color, fontFamily, fontSize, text, getFieldDecorator])
Когда мы изменим каждый элемент в форме, он будет называтьсяonFormValueChange
Способ для изменения свойств соответствующего узла. Наконец, обновите модифицированный узел в холст.
if (changedValues.node) {
// 遍历查找修改的属性,赋值给原始Node
for (const key in changedValues.node) {
if (Array.isArray(changedValues.node[key])) {
} else if (typeof changedValues.node[key] === 'object') {
for (const k in changedValues.node[key]) {
selected.node[key][k] = changedValues.node[key][k];
}
} else {
selected.node[key] = changedValues.node[key];
}
}
}
canvas.updateProps(selected.node);
Узел поддержки атрибутов данных
При реальном развитии бизнеса неизбежно, что атрибутов на узле Node по умолчанию недостаточно или на узле есть определенные бизнес-данные. Затем в это время мы можем сохранить эти специальные данные в пользовательском поле данных узла.
const renderExtraDataForm = useMemo(() => {
return <Form >
<Col>
<Form.Item label="自定义数据字段">
{getFieldDecorator('data', {
initialValue: JSON.stringify(extraFields)
})(<TextArea rows={10} />)}
</Form.Item>
</Col>
</Form>
}, [extraFields, getFieldDecorator])
Примечание. По умолчанию узел Le5leTopology.Node не имеет атрибута данных.
Функция для поддержки пользовательских событий узла
Как видно из рисунка выше, события узла делятся на две части: тип события и поведение события. Типы событий можно разделить на: 1. Событие щелчка 2. Событие двойного щелчка 3. Событие websocket 4. Событие mqtt. Поведение событий можно разделить на: 1. Перейти по ссылке 2. Выполнить анимацию 3. Выполнить функцию 4. Выполнить глобальную функцию под окном 5. Обновить данные атрибута.
Атрибут пользовательских событий в узле Node, поэтому в соответствии сДокументацияОбеспечение стоимости этого свойства, мы можем нарисовать очень простую переписку между различными типами событий и поведением событий. Из-за конкретного кода космических ограничений оно не вставлено. Заинтересованные студенты могут читатьСоответствующий исходный код, Затем позвольте мне продемонстрировать, как привязать событие щелчка узла к событию веб-сокета.
- Нажмите, чтобы выполнить пользовательскую функцию
Глядя на анимацию выше, мы можем обнаружить, что каждый раз, когда мы нажимаем на график, он выводит我是自定义函数
. Итак, на нашем редакторе, как его настроить?
- Получает значение WebSocket
Мы отправляем сигнал на сервер через websocket, и при этом получаем соответствующее значение. Во-первых, нам нужно заранее подключиться к серверу ws.
canvas.openSocket('ws://123.207.136.134:9010/ajaxchattest');
Этот шаг является критическим, иначе все последующие процессы будут сообщать об ошибках.
Затем мы добавляем узел с событием щелчка, чтобы имитировать инициацию сигнала.
Наконец, мы определяем узел для получения значения, возвращаемого websocket.
Затем мы можем нажать кнопку предварительного просмотра, чтобы проверить правильность настроенного нами кода.
Поддержка изменения стиля линии
В настоящее время поддерживаются только настройки нескольких свойств, показанных на рисунке выше. Обновление строки аналогично обновлению узла, мы напрямую модифицируем свойства строки, а затем передаемupdateProps
Обновите стиль соответствующей строки.
const onHandleLineFormValueChange = useCallback(
(value) => {
const { dash, lineWidth, strokeStyle, name, fromArrow, toArrow, ...other } = value;
const changedValues = {
line: { rect: other, lineWidth, dash, strokeStyle, name, fromArrow, toArrow }
};
if (changedValues.line) {
// 遍历查找修改的属性,赋值给原始line
for (const key in changedValues.line) {
if (Array.isArray(changedValues.line[key])) {
} else if (typeof changedValues.line[key] === 'object') {
for (const k in changedValues.line[key]) {
selected.line[key][k] = changedValues.line[key][k];
}
} else {
selected.line[key] = changedValues.line[key];
}
}
}
canvas.updateProps(selected.line);
},
[selected]
);
Поддержка функции предварительного просмотра
Когда мы закончим редактирование графика, нам нужно просмотреть его. Затем мы можем передать данные с холста на новую страницу через параметры маршрутизации (состояние) и, наконец, регенерировать холст с помощью новой топологии для рендеринга графики.
let reader = new FileReader();
const result = new Blob([JSON.stringify(canvas.data)], { type: 'text/plain;charset=utf-8' });
reader.readAsText(result, 'text/plain;charset=utf-8');
reader.onload = (e) => {
history.push({ pathname: '/preview', state: { data: JSON.parse(reader.result) } });
}
Поддерживает блокировку, установку начальной и конечной стрелок глобальной линии
- запирание
canvas.lock(2)
- разблокировать
canvas.lock(0)
- Установите тип подключения по умолчанию
const onHandleSelectMenu = data => {
setLineStyle(data.item.props.children);
canvas.data.lineName = data.key;
canvas.render();
}
- Установите стрелку начала соединения по умолчанию
const onHandleSelectMenu1 = data => {
setFromArrowType(data.item.props.children);
canvas.data.fromArrowType = data.key;
canvas.render();
}
- Установите стрелку завершения провода по умолчанию
const onHandleSelectMenu2 = data => {
setToArrowType(data.item.props.children);
canvas.data.toArrowType = data.key;
canvas.render();
}
Поддержка функции автоматического набора текста
Если нарисованный график беспорядочный, вы можете использовать функцию автоматического центрирования. Сначала мы проходимrect.calcCenter();
Получаем центральную точку текущего графика, а затем вычисляем разницу между центральной точкой холста и центральной точкой текущего графика,
Наконец, позвонивcanvas.translate(x, y)
способ перевода графика.
const onHandleFit = () => {
const rect = canvas.getRect();
rect.calcCenter();
x = document.body.clientWidth / 2 - rect.center.x;
y = (document.body.clientHeight - 66) / 2 - rect.center.y;
canvas.translate(x, y);
};
конец
несмотря на то чтоTopologyНа официальном сайте есть подробные описания каждого API, но переход от API к реальному бизнесу занимает много времени. Во-вторых, поток данных официальной версии React более сложен, а стоимость начала работы для новичков относительно высока, поэтому родилась идея написать простую версию.topology-react
Помогите всем начать быстро.
Наконец, спасибоAlsmileДвижок рисования с открытым исходным кодом. Если это поможет вам, не забудьте дать небольшойstarО, спасибо~