@meta2d/core
Version:
@meta2d/core: Powerful, Beautiful, Simple, Open - Web-Based 2D At Its Best .
1,268 lines • 246 kB
JavaScript
import { clearIframes, commonAnchors, commonPens, cube, updateIframes, reset, updateFormData } from './diagrams';
import { Canvas } from './canvas';
import { calcInView, calcTextDrawRect, calcTextLines, calcTextRect, facePen, formatAttrs, getAllChildren, getFromAnchor, getParent, getToAnchor, getWords, LockState, PenType, renderPenRaw, setElemPosition, connectLine, nearestAnchor, setChildValue, isAncestor, isShowChild, CanvasLayer, validationPlugin, setLifeCycleFunc, getAllFollowers, isInteraction, calcWorldAnchors, isDomShapes, defaultFormat, findOutliersByZScore, } from './pen';
import { rotatePoint } from './point';
import { clearStore, EditType, globalStore, register, registerAnchors, registerCanvasDraw, registerGridDrawer, unregisterGridDrawer, useStore, } from './store';
import { formatPadding, loadCss, s8, valueInArray, valueInRange, } from './utils';
import { calcCenter, calcRelativeRect, calcRightBottom, getRect, rectInRect, } from './rect';
import { deepClone } from './utils/clone';
import { EventAction } from './event';
import { ViewMap } from './map';
import * as mqtt from 'mqtt/dist/mqtt.min.js';
import pkg from '../package.json';
import { lockedError } from './utils/error';
import { Scroll } from './scroll';
import { getter } from './utils/object';
import { d, getCookie, getMeta2dData, getToken, queryURLParams } from './utils/url';
import { HotkeyType } from './data';
import { Message, messageList } from './message';
import { closeJetLinks, connectJetLinks, getSendData, sendJetLinksData } from './utils/jetLinks';
import { le5leTheme } from './theme';
const echartReg = /^echarts/;
export class Meta2d {
store;
canvas;
websocket;
mqttClient;
websockets;
mqttClients;
eventSources;
penPluginMap = new Map();
socketFn;
events = {};
map;
mapTimer;
/**
* fillView 首次填充前各 fit 顶层图元的原始几何快照(设计坐标系,与 scale/origin 无关)。
* 用于保证多次调用 fillView 的幂等性:每次填充前先还原到原始几何,再执行拉伸,
* 避免在已拉伸的结果上重复拉伸导致超出屏幕。open 新图纸时置空。
*/
fillViewSnapshot = null;
constructor(parent, opts = {}) {
this.store = useStore(s8());
this.setOptions(opts);
this.setDatabyOptions(opts);
this.init(parent);
this.register(commonPens());
this.registerCanvasDraw({ cube });
this.registerAnchors(commonAnchors());
globalThis.meta2d = this;
this.initEventFns();
this.store.emitter.on('*', this.onEvent);
}
facePen = facePen;
getWords = getWords;
calcTextLines = calcTextLines;
calcTextRect = calcTextRect;
calcTextDrawRect = calcTextDrawRect;
/**
* @deprecated 改用 beforeAddPens
*/
get beforeAddPen() {
return this.canvas.beforeAddPen;
}
/**
* @deprecated 改用 beforeAddPens
*/
set beforeAddPen(fn) {
this.canvas.beforeAddPen = fn;
}
get beforeAddPens() {
return this.canvas.beforeAddPens;
}
set beforeAddPens(fn) {
this.canvas.beforeAddPens = fn;
}
get beforeAddAnchor() {
return this.canvas.beforeAddAnchor;
}
set beforeAddAnchor(fn) {
this.canvas.beforeAddAnchor = fn;
}
get beforeRemovePens() {
return this.canvas.beforeRemovePens;
}
set beforeRemovePens(fn) {
this.canvas.beforeRemovePens = fn;
}
get beforeRemoveAnchor() {
return this.canvas.beforeRemoveAnchor;
}
set beforeRemoveAnchor(fn) {
this.canvas.beforeRemoveAnchor = fn;
}
setOptions(opts = {}) {
if (opts.grid !== undefined ||
opts.gridColor !== undefined ||
opts.gridSize !== undefined) {
// this.setGrid({
// grid: opts.grid,
// gridColor: opts.gridColor,
// gridSize: opts.gridSize,
// });
this.canvas && (this.canvas.canvasTemplate.bgPatchFlags = true);
}
if (opts.rule !== undefined ||
opts.ruleColor !== undefined ||
opts.ruleOptions !== undefined) {
// this.setRule({
// rule: opts.rule,
// ruleColor: opts.ruleColor,
// });
this.store.patchFlagsTop = true;
if (opts.ruleOptions) {
if (this.store.options?.ruleOptions) {
Object.assign(this.store.options.ruleOptions, opts.ruleOptions);
opts.ruleOptions = this.store.options.ruleOptions;
}
}
}
if (opts.background !== undefined) {
this.canvas && (this.canvas.canvasTemplate.bgPatchFlags = true);
}
if (opts.resizeMode !== undefined) {
if (!opts.resizeMode) {
this.canvas.hotkeyType = HotkeyType.None;
}
}
if (opts.width !== undefined || opts.height !== undefined) {
this.canvas && (this.canvas.canvasTemplate.bgPatchFlags = true);
if (this.canvas &&
this.canvas.canvasTemplate.canvas.style.backgroundImage) {
this.canvas.canvasTemplate.canvas.style.backgroundImage = '';
}
}
this.store.options = Object.assign(this.store.options, opts);
if (opts.roles && this.store.data.pens?.length) {
for (const pen of this.store.data.pens) {
calcInView(pen);
}
}
if (this.canvas && opts.scroll !== undefined) {
if (opts.scroll) {
!this.canvas.scroll && (this.canvas.scroll = new Scroll(this.canvas));
this.canvas.scroll.show();
}
else {
this.canvas.scroll && this.canvas.scroll.hide();
}
}
this.canvas?.initGlobalStyle();
}
getOptions() {
return this.store.options;
}
/**
* @description
* @author Joseph Ho
* @date 21/02/2025
* @param {string} themeName 主题名
* @param {object} theme 主题变量字符串数组
* @returns {*}
* @memberof Meta2d
*/
registerTheme(themeName, theme) {
// 校验数据
if (!Array.isArray(theme)) {
return;
}
// 写一个正则,中间必须有且仅有1个冒号,结尾不能有分号,符合"A:B"的形式,冒号两边必须非空,允许有空格
const regex = /^\s*\S+\s*:\s*\S+\s*$/;
const ret = theme.every(el => regex.test(el));
if (!ret) {
return;
}
const obj = {}, newTheme = [];
for (let i = 0; i < theme.length; i++) {
const item = theme[i];
const kvs = item.split(":");
const kvs0 = kvs[0].trim();
const kvs1 = kvs[1].trim();
newTheme.push([kvs0, kvs1].join(":"));
obj[kvs0] = kvs1;
}
le5leTheme.addTheme(themeName, newTheme);
this.store.theme[themeName] = obj;
}
setTheme(theme) {
this.store.data.theme = theme;
this.setBackgroundColor(this.store.theme[theme].background);
this.canvas.parentElement.style.background =
this.store.theme[theme].parentBackground;
this.setOptions({
ruleColor: this.store.theme[theme].ruleColor,
ruleOptions: this.store.theme[theme].ruleOptions,
});
// 更新全局的主题css变量
if (!(this.store.options.themeOnlyCanvas || this.store.data.themeOnlyCanvas)) {
this.store.data.color = this.store.theme[theme].color;
le5leTheme.updateCssRule(this.store.id, theme);
this.canvas.initGlobalStyle();
for (let i = 0; i < this.store.data.pens.length; i++) {
const pen = this.store.data.pens[i];
// 调用pen的主题设置函数,如果单个pen有主题的自定义设置的话
pen.setTheme && pen.setTheme(pen, this.store.styles);
}
}
this.render();
}
setDatabyOptions(options = {}) {
const { color, activeColor, activeBackground, grid, gridColor, gridSize, gridType, gridScope, fromArrow, toArrow, rule, ruleColor, textColor, x = 0, y = 0, } = options;
this.setRule({ rule, ruleColor });
this.setGrid({
grid,
gridColor,
gridSize,
gridType,
gridScope,
});
this.store.data = Object.assign(this.store.data, {
textColor,
color,
activeColor,
activeBackground,
fromArrow,
toArrow,
x,
y,
});
}
init(parent) {
if (typeof parent === 'string') {
this.canvas = new Canvas(this, document.getElementById(parent), this.store);
}
else {
this.canvas = new Canvas(this, parent, this.store);
}
this.canvas.initGlobalStyle();
this.resize();
this.canvas.listen();
// 创建主题样式表
// if(this.store.data.theme){
le5leTheme.createThemeSheet(this.store.data.theme, this.store.id);
// }
}
initEventFns() {
this.events[EventAction.Link] = (pen, e) => {
if (window && e.value && typeof e.value === 'string') {
let url = e.value;
if (url.includes('${')) {
let keys = url.match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys) {
keys?.forEach((key) => {
url = url.replace(`\${${key}}`, getter(pen, key) || this.getDynamicParam(key));
});
}
}
window.open(url, e.params ?? '_blank');
return;
}
console.warn('[meta2d] Link param is not a string');
};
this.events[EventAction.SetProps] = (pen, e) => {
// TODO: 若频繁地触发,重复 render 可能带来性能问题,待考虑
const value = e.value;
if (value && typeof value === 'object') {
const pens = e.params ? this.find(e.params) : this.find(pen.id);
const _value = {};
for (let key in value) {
if (value[key]?.id) {
_value[key] = this.store.pens[value[key].id]?.[value[key].key];
}
else {
if (typeof value[key] === 'string' && value[key].includes('${')) {
let __value = value[key];
let keys = __value.match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys) {
keys.forEach((key) => {
__value = __value.replace(`\${${key}}`, getter(pen, key) || this.getDynamicParam(key));
});
}
_value[key] = __value;
}
else if (typeof value[key] === 'string' &&
((value[key].startsWith('{') && value[key].endsWith('}')) ||
(value[key].startsWith('[') && value[key].endsWith(']')))) {
try {
_value[key] = JSON.parse(value[key]);
}
catch (e) {
_value[key] = value[key];
}
}
else {
_value[key] = value[key];
}
}
}
pens.forEach((pen) => {
if (_value.hasOwnProperty('visible')) {
if (pen.visible !== _value.visible) {
this.setVisible(pen, _value.visible);
}
}
this.setValue({ id: pen.id, ..._value }, { render: false, doEvent: false });
});
this.render();
return;
}
console.warn('[meta2d] SetProps value is not an object');
};
this.events[EventAction.StartAnimate] = (pen, e) => {
let _pen = pen;
if (e.value) {
_pen = this.findOne(e.value);
}
if (this.store.animates.has(_pen) &&
!_pen.calculative.pause &&
_pen.animateName === e.params) {
return;
}
if (e.targetType && e.params) {
this.startAnimate(e.value || [pen], e.params);
return;
}
if (!e.value || typeof e.value === 'string') {
this.startAnimate(e.value || [pen]);
return;
}
console.warn('[meta2d] StartAnimate value is not a string');
};
this.events[EventAction.PauseAnimate] = (pen, e) => {
if (!e.value || typeof e.value === 'string') {
this.pauseAnimate(e.value || [pen]);
return;
}
console.warn('[meta2d] PauseAnimate value is not a string');
};
this.events[EventAction.StopAnimate] = (pen, e) => {
if (!e.value || typeof e.value === 'string') {
if (e.value) {
let _pen = this.findOne(e.value);
if (!this.store.animates.has(_pen)) {
return;
}
}
else {
if (!this.store.animates.has(pen)) {
return;
}
}
this.stopAnimate(e.value || [pen]);
return;
}
console.warn('[meta2d] StopAnimate event value is not a string');
};
this.events[EventAction.StartVideo] = (pen, e) => {
if (!e.value || typeof e.value === 'string') {
this.startVideo(e.value || [pen]);
return;
}
console.warn('[meta2d] StartVideo value is not a string');
};
this.events[EventAction.PauseVideo] = (pen, e) => {
if (!e.value || typeof e.value === 'string') {
this.pauseVideo(e.value || [pen]);
return;
}
console.warn('[meta2d] PauseVideo value is not a string');
};
this.events[EventAction.StopVideo] = (pen, e) => {
if (!e.value || typeof e.value === 'string') {
this.stopVideo(e.value || [pen]);
return;
}
console.warn('[meta2d] StopVideo event value is not a string');
};
this.events[EventAction.JS] = (pen, e, params) => {
if (e.value && !e.fn) {
try {
if (typeof e.value !== 'string') {
throw new Error('[meta2d] Function value must be string');
}
const fnJs = e.value;
e.fn = new Function('pen', 'params', 'context', fnJs);
}
catch (err) {
console.error('[meta2d]: Error on make a function:', err, 'code:', e.value);
}
}
e.fn?.(pen, params || e.params, { meta2d: this, eventName: e.name });
};
this.events[EventAction.GlobalFn] = (pen, e) => {
if (typeof e.value !== 'string') {
console.warn('[meta2d] GlobalFn value must be a string');
return;
}
if (globalThis[e.value]) {
globalThis[e.value](pen, e.params);
}
};
this.events[EventAction.Emit] = (pen, e) => {
if (typeof e.value !== 'string') {
console.warn('[meta2d] Emit value must be a string');
return;
}
this.store.emitter.emit(e.value, {
pen,
params: e.params,
eventName: e.name,
});
};
this.events[EventAction.SendPropData] = (pen, e) => {
const value = deepClone(e.value);
if (value && typeof value === 'object') {
const _pen = e.params ? this.findOne(e.params) : pen;
for (let key in value) {
if (value[key] === undefined || value[key] === '') {
value[key] = _pen[key];
}
}
value.id = _pen.id;
this.doSendDataEvent(value, e.extend);
return;
}
console.warn('[meta2d] SendPropData value is not an object');
};
this.events[EventAction.SendVarData] = (pen, e) => {
const value = deepClone(e.value);
if (value && typeof value === 'object') {
const _pen = e.params ? this.findOne(e.params) : pen;
let array = [];
for (let key in value) {
let obj = {
dataId: key,
value: value[key],
};
if (!obj.value) {
let oneForm = _pen.form.find((_item) => _item.dataIds &&
_item.dataIds.dataId === obj.dataId);
if (oneForm) {
obj.value = _pen[oneForm.key];
}
}
array.push(obj);
}
this.doSendDataEvent(array, e.extend);
return;
}
console.warn('[meta2d] SendVarData value is not an object');
};
this.events[EventAction.Navigator] = (pen, e) => {
if (e.value && typeof e.value === 'string') {
this.navigatorTo(e.value);
}
};
this.events[EventAction.Dialog] = (pen, e) => {
if (e.params && typeof e.params === 'string') {
let url = e.params;
if (e.params.includes('${')) {
let keys = e.params.match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys) {
keys?.forEach((key) => {
url = url.replace(`\${${key}}`, getter(pen, key) || this.getDynamicParam(key));
});
}
}
Object.keys(e.extend).forEach((key) => {
if (!['x', 'y', 'width', 'height'].includes(key)) {
if (url.indexOf('?') !== -1) {
url += `&${key}=${e.extend[key]}`;
}
else {
url += `?${key}=${e.extend[key]}`;
}
}
});
let data = this.getEventData(e.list, pen);
if (Object.keys(data).length) {
data = null;
}
this.canvas.dialog.show(e.value, url, e.extend, data);
}
};
this.events[EventAction.SendData] = (pen, e) => {
if (e.data?.length) {
const value = this.getSendData(e.data, pen);
if (pen.formId && pen.formData) {
//表单数据
Object.assign(value, pen.formData);
}
this.sendDataToNetWork(value, pen, e);
return;
}
if (e.list?.length) {
// if (e.targetType === 'id') {
if (e.network && e.network.protocol === 'ADIIOT') {
const list = getSendData(this, pen, e);
if (list.length) {
sendJetLinksData(this, list);
}
return;
}
const value = this.getEventData(e.list, pen);
if (pen.deviceId) {
value.deviceId = pen.deviceId;
}
if (pen.formId && pen.formData) {
//表单数据
Object.assign(value, pen.formData);
}
this.sendDataToNetWork(value, pen, e);
return;
// }
}
const value = deepClone(e.value);
if (value && typeof value === 'object') {
if (e.targetType === 'id') {
const _pen = e.params ? this.findOne(e.params) : pen;
for (let key in value) {
if (value[key] === undefined || value[key] === '') {
value[key] = getter(_pen, key);
}
else if (typeof value[key] === 'string' &&
value[key]?.indexOf('${') > -1) {
let keys = value[key].match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys?.length) {
value[key] = getter(_pen, key[0]) ?? this.getDynamicParam(keys[0]);
}
}
}
// value.id = _pen.id;
if (_pen.deviceId) {
value.deviceId = _pen.deviceId;
}
this.sendDataToNetWork(value, pen, e);
return;
}
}
};
this.events[EventAction.PostMessage] = (pen, e) => {
if (typeof e.value !== 'string') {
console.warn('[meta2d] Emit value must be a string');
return;
}
const _pen = e.params ? this.findOne(e.params) : pen;
if (_pen.name !== 'iframe' || !_pen.iframe) {
console.warn('不是嵌入页面');
return;
}
let params = queryURLParams(_pen.iframe.split('?')[1]);
let value = this.getSendData(e.data, pen);
if (e.list) {
value = this.getEventData(e.list, pen);
}
_pen.calculative.singleton.div.children[0].contentWindow.postMessage(JSON.stringify({
name: e.value,
id: params.id,
data: value,
}), '*');
return;
};
this.events[EventAction.PostMessageToParent] = (pen, e) => {
if (typeof e.value !== 'string') {
console.warn('[meta2d] Emit value must be a string');
return;
}
let value = this.getSendData(e.data, pen);
if (e.list) {
value = this.getEventData(e.list, pen);
}
window.parent.postMessage(JSON.stringify({ name: e.value, data: value }), '*');
return;
};
this.events[EventAction.Message] = (pen, e, params) => {
let theme = e.params;
let content = e.value;
if (!content && params && params.type === 'http') {
content = params.error.statusText;
if (!theme) {
theme = 'error';
}
}
this.message({
theme,
content,
...e.extend,
});
};
}
getSendData(data, cpen) {
const value = {};
data?.forEach((item) => {
if (item.prop) {
if (item.id && item.id !== '固定值') {
const pen = this.findOne(item.id);
value[item.prop] = getter(pen, item.key); // pen[item.key];
}
else {
if (typeof item.value === 'string' && item.value.includes('${')) {
let _value = item.value;
let keys = _value.match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys) {
keys.forEach((key) => {
_value = _value.replace(`\${${key}}`, getter(cpen, key) || this.getDynamicParam(key));
});
}
value[item.prop] = _value;
}
else {
value[item.prop] = this.convertType(item.value, item.type);
}
}
}
});
return value;
}
convertType(value, type) {
if (typeof value === 'string') {
if (['switch', 'bool', 'boolean'].includes(type)) {
if (value === 'false') {
return false;
}
else if (value === 'true') {
return true;
}
}
else if (['integer', 'number', 'int', 'enum', 'double', 'float'].includes(type)) {
if (!isNaN(Number(value))) {
return Number(value);
}
}
}
else if (typeof value === 'object') {
let bodyStr = JSON.stringify(value);
let keys = bodyStr.match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys?.length) {
for (let i = 0; i < keys.length; i++) {
bodyStr = bodyStr.replace(`\${${keys[i]}}`, this.getDynamicParam(keys[i]));
}
}
return JSON.parse(bodyStr);
}
return value;
}
getEventData(list, pen) {
const value = {};
if (list?.length) {
list.forEach((item) => {
const _pen = item.params ? this.findOne(item.params) : pen;
for (let key in item.value) {
if (item.value[key] === undefined || item.value[key] === '') {
value[key] = _pen[key];
}
else if (typeof item.value[key] === 'string' &&
item.value[key]?.indexOf('${') > -1) {
let keys = item.value[key].match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys?.length) {
value[key] = _pen[keys[0]] ?? this.getDynamicParam(keys[0]);
}
}
else {
value[key] = item.value[key];
}
}
});
}
if (Object.keys(value).length) {
return value;
}
else
return {};
}
message(options) {
const message = new Message(this.canvas.parentElement, options);
message.init();
}
closeAll() {
for (let key in messageList) {
messageList[key].close();
}
}
async navigatorTo(id) {
if (!id) {
return;
}
// let href = window.location.href;
// let arr: string[] = href.split('id=');
// if (arr.length > 1) {
// let idx = arr[1].indexOf('&');
// if (idx === -1) {
// window.location.href = arr[0] + 'id=' + id;
// } else {
// window.location.href = arr[0] + 'id=' + id + arr[1].slice(idx);
// }
// }
//路径参数更新
let hasId = queryURLParams()?.id;
if (hasId) {
const url = new URL(window.location);
url.searchParams.set('id', id);
history.pushState({}, '', url);
}
//图纸更新
const data = await getMeta2dData(this.store, id);
if (data) {
data.locked = 1;
data.fits?.length && (globalThis.meta2dData = JSON.stringify(data));
this.canvas.opening = true;
this.open(data);
this.lock(1);
const width = this.store.data.width || this.store.options.width;
const height = this.store.data.height || this.store.options.height;
if (width && height) {
this.fitSizeView(true, 0);
}
else {
this.fitView(true, 10);
}
this.render(true);
// document.title = data.name + "-" + window.name;
}
}
doSendDataEvent(value, topics) {
let data = JSON.stringify(value);
if (this.mqttClient && this.mqttClient.connected) {
if (topics) {
topics.split(',').forEach((topic) => {
this.mqttClient.publish(topic, data);
});
}
else {
this.store.data.mqttTopics &&
this.store.data.mqttTopics.split(',').forEach((topic) => {
this.mqttClient.publish(topic, data);
});
}
}
if (this.websocket && this.websocket.readyState === 1) {
this.websocket.send(data);
}
if (this.store.data.https || this.store.data.http) {
this.sendDatabyHttp(data);
}
this.store.emitter.emit('sendData', data);
}
async sendDataToNetWork(value, pen, e) {
const network = deepClone(e.network);
if (network.data) {
Object.assign(network, network.data);
delete network.data;
}
if (network.protocol === 'iot') {
if (this.store.data.iot.room) {
let payload = {
token: this.store.data.iot.token,
data: value,
};
if (value.name) {
const keys = Object.keys(value).filter((key) => value[key] === 'aggregate');
if (keys.length) {
const _value = deepClone(value);
keys.forEach((key) => {
delete _value[key];
});
delete _value.name;
if (_value.start && typeof _value.start === 'string') {
_value.start = Math.floor(new Date(_value.start).getTime() / 1000);
}
if (_value.end && typeof _value.end === 'string') {
_value.end = Math.floor(new Date(_value.end).getTime() / 1000);
}
let payload = [];
keys.forEach((key) => {
let arr = key.split('#');
payload.push({
token: this.store.data.iot.token,
deviceId: arr[0],
key: arr[1],
name: arr[1] + '_' + value.name,
..._value,
});
});
this.iotMqttClient &&
this.iotMqttClient.publish(`le5le-iot/${this.store.data.iot.room}/property/aggregate`, JSON.stringify(payload));
return;
}
}
this.iotMqttClient &&
this.iotMqttClient.publish(`le5le-iot/${this.store.data.iot.room}/properties/set`, JSON.stringify(payload));
}
else {
this.iotMqttClient &&
this.iotMqttClient.publish(`le5le-iot/property/set/${this.store.data.iot?.token}`, JSON.stringify(value));
}
return;
}
if (!network.url) {
return;
}
if (network.protocol === 'http') {
if (typeof network.headers === 'object') {
/*for (let i in network.headers) {
if (typeof network.headers[i] === 'string') {
let keys = network.headers[i].match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys) {
network.headers[i] = network.headers[i].replace(
`\${${keys[0]}}`,
this.getDynamicParam(keys[0])
);
}
}
}*/
let headersStr = JSON.stringify(network.headers);
let keys = headersStr.match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys?.length) {
for (let i = 0; i < keys.length; i++) {
headersStr = headersStr.replace(`\${${keys[i]}}`, this.getDynamicParam(keys[i]));
}
}
network.headers = JSON.parse(headersStr);
}
let params = undefined;
let url = network.url;
if (network.method === 'GET') {
if (Object.keys(value).length !== 0) {
if (url.includes('?')) {
params =
'&' +
Object.keys(value)
.map((key) => key + '=' + value[key])
.join('&');
}
else {
params =
'?' +
Object.keys(value)
.map((key) => key + '=' + value[key])
.join('&');
}
}
}
// if (network.method === 'POST') {
if (url.indexOf('${') > -1) {
let keys = url.match(/\$\{([^}]+)\}/g)?.map(m => m.slice(2, -1));
if (keys) {
keys.forEach((key) => {
url = url.replace(`\${${key}}`, getter(pen, key) || this.getDynamicParam(key));
});
}
}
// }
const res = await fetch(url + (params ? params : ''), {
headers: network.headers || {},
method: network.method,
body: network.method === 'POST' ? JSON.stringify(value) : undefined,
});
if (res.ok) {
if (e.callback) {
const data = await res.text();
if (!e.fn) {
try {
if (typeof e.callback !== 'string') {
throw new Error('[meta2d] Function callback must be string');
}
const fnJs = e.callback;
e.fn = new Function('pen', 'data', 'context', fnJs);
}
catch (err) {
console.error('[meta2d]: Error on make a function:', err, 'code:', e.callback);
}
}
e.fn?.(pen, data, { meta2d: this, e });
}
console.info('http消息发送成功');
}
else {
this.store.emitter.emit('error', { type: 'http', error: res });
}
}
else if (network.protocol === 'mqtt') {
const clients = this.mqttClients?.filter((client) => client.options.href === network.url);
if (clients && clients.length) {
if (clients[0].connected) {
network.topics.split(',').forEach((topic) => {
clients[0].publish(topic, JSON.stringify(value));
});
}
}
else {
//临时建立连接
let mqttClient = mqtt.connect(network.url, network.options);
mqttClient.on('connect', () => {
console.info('mqtt连接成功');
network.topics.split(',').forEach((topic) => {
mqttClient.publish(topic, JSON.stringify(value));
setTimeout(() => {
mqttClient?.end();
}, 1000);
});
});
}
}
else if (network.protocol === 'websocket') {
const websockets = this.websockets?.filter((socket) => socket.url === network.url);
if (websockets && websockets.length) {
if (websockets[0].readyState === 1) {
websockets[0].send(JSON.stringify(value));
}
}
else {
//临时建立连接
let websocket = new WebSocket(network.url, network.protocols || undefined);
websocket.onopen = function () {
console.info('websocket连接成功');
websocket.send(JSON.stringify(value));
setTimeout(() => {
websocket.close();
}, 100);
};
}
}
}
resize(width, height) {
this.canvas.resize(width, height);
this.render();
this.store.emitter.emit('resize', { width, height });
if (this.canvas.scroll && this.canvas.scroll.isShow) {
this.canvas.scroll.init();
}
}
/**
*
* @param emit 是否发送消息
*/
async addPen(pen, history, emit = true, abs = false) {
return await this.canvas.addPen(pen, history, emit, abs);
}
addPenSync(pen, history, emit = true, abs = true) {
return this.canvas.addPenSync(pen, history, emit, abs);
}
addPensSync(pens, history, abs = true) {
return this.canvas.addPensSync(pens, history, abs);
}
async addPens(pens, history, abs = false) {
return await this.canvas.addPens(pens, history, abs);
}
render(patchFlags) {
this.canvas?.render(patchFlags);
}
async setBackgroundImage(url, data) {
let that = this;
async function loadImage(url) {
return new Promise((resolve) => {
const img = new Image();
img.src = url;
if (that.store.options.cdn &&
!(url.startsWith('http') ||
url.startsWith('//') ||
url.startsWith('data:image'))) {
img.src = that.store.options.cdn + url;
}
img.crossOrigin = that.store.options.crossOrigin || 'anonymous';
img.onload = () => {
resolve(img);
};
});
}
this.store.data.bkImage = url;
const width = data?.width || this.store.data?.width || this.store.options?.width;
const height = data?.height || this.store.data?.height || this.store.options?.height;
if (width && height) {
this.canvas.canvasTemplate.canvas.style.backgroundImage = null;
this.canvas && (this.canvas.canvasTemplate.bgPatchFlags = true);
}
else {
this.canvas.canvasTemplate.canvas.style.backgroundImage = url
? `url('${url}')`
: '';
}
if (url) {
const img = await loadImage(url);
// 用作 toPng 的绘制
this.store.bkImg = img;
if (width && height) {
if (this.canvas) {
this.canvas.canvasTemplate.init();
this.render();
}
}
}
else {
this.store.bkImg = null;
}
}
setBackgroundColor(color = this.store.data.background) {
this.store.data.background = color;
// this.store.patchFlagsBackground = true;
this.canvas && (this.canvas.canvasTemplate.bgPatchFlags = true);
}
setGrid({ grid = this.store.data.grid, gridColor = this.store.data.gridColor, gridSize = this.store.data.gridSize, gridRotate = this.store.data.gridRotate, gridType = this.store.data.gridType, gridScope = this.store.data.gridScope, } = {}) {
this.store.data.grid = grid;
this.store.data.gridColor = gridColor;
this.store.data.gridSize = gridSize < 0 ? 0 : gridSize;
this.store.data.gridRotate = gridRotate;
this.store.data.gridType = gridType;
this.store.data.gridScope = gridScope;
// this.store.patchFlagsBackground = true;
this.canvas && (this.canvas.canvasTemplate.bgPatchFlags = true);
}
setRule({ rule = this.store.data.rule, ruleColor = this.store.data.ruleColor, } = {}) {
this.store.data.rule = rule;
this.store.data.ruleColor = ruleColor;
this.store.patchFlagsTop = true;
}
open(data, render = true) {
this.clear(false, data?.template);
// 打开新图纸,原始几何快照失效,下次 fillView 时重新采集
this.fillViewSnapshot = null;
this.canvas.autoPolylineFlag = true;
if (data) {
// 根据图纸的主题设置主题
if (data.theme) {
this.setTheme(data.theme);
}
updateIframes(data.pens);
this.setBackgroundImage(data.bkImage, data);
Object.assign(this.store.data, data);
this.store.data.pens = [];
// 第一遍赋初值
for (const pen of data.pens) {
if (!pen.id) {
pen.id = s8();
}
!pen.calculative && (pen.calculative = { canvas: this.canvas });
this.store.pens[pen.id] = pen;
}
for (const pen of data.pens) {
this.canvas.makePen(pen);
}
//首次计算连线bug
// for (const pen of data.pens) {
// this.canvas.updateLines(pen);
// }
}
this.canvas.patchFlagsLines.forEach((pen) => {
if (pen.type) {
this.canvas.initLineRect(pen);
}
});
if (!this.store.data.template) {
this.store.data.template = s8();
}
if (!render) {
this.canvas.opening = true;
}
this.doInitJS();
this.initBindDatas();
this.initBinds();
this.doInitFn();
this.loadLineAnimateDraws();
this.initMessageEvents();
this.initGlobalTriggers();
this.startAnimate();
this.startVideo();
this.listenSocket();
this.connectSocket();
this.connectNetwork();
this.startDataMock();
this.canvas.initGlobalStyle();
this.render();
setTimeout(() => {
const pen = this.store.data.pens.find((pen) => pen.autofocus);
if (pen) {
this.focus(pen.id);
}
}, 100);
if (this.store.data.iconUrls) {
for (const item of this.store.data.iconUrls) {
loadCss(item, () => {
this.render();
});
}
}
this.canvas.autoPolylineFlag = false;
this.store.emitter.emit('opened');
if (this.canvas.scroll && this.canvas.scroll.isShow) {
this.canvas.scroll.init();
}
}
dirtyData(active) {
//获取画布脏数据
const pens = this.store.data.pens;
const width = this.store.data.width || this.store.options.width;
const height = this.store.data.height || this.store.options.height;
const dirtyPens = [];
for (let i = pens.length - 1; i >= 0; i--) {
let pen = pens[i];
if (pen.parentId) {
const parent = this.store.pens[pen.parentId];
if (pen.x > 10 || pen.y > 10 || pen.width > 10 || pen.height > 10) {
// 子图元坐标值很大
dirtyPens.push(pen);
}
else if (!parent.children || !parent.children.includes(pen.id)) {
//已经解组但子图元还有父图元id
dirtyPens.push(pen);
}
}
if (width && height) {
//大屏区域外
let rect = this.getPenRect(pen);
if (rect.x < -10 || rect.y < -10 || rect.x + rect.width > width || rect.y + rect.height > height) {
dirtyPens.push(pen);
}
}
//无效连线 单个锚点连线
if (pen.name === 'line') {
if (pen.anchors.length < 2) {
dirtyPens.push(pen);
}
}
}
if (!width || !height) {
//2d 偏移量很大
let outpens = findOutliersByZScore(pens);
outpens.forEach((item) => {
let repeat = dirtyPens.filter((_item) => _item.id === item.id);
if (!repeat.length) {
dirtyPens.push(item);
}
});
}
if (active) {
this.active(dirtyPens);
}
return dirtyPens;
}
clearDirtyData() {
let dirtyPens = this.dirtyData();
this.delete(dirtyPens, true);
}
cacheData(id) {
if (id && this.store.options.cacheLength) {
let index = this.store.cacheDatas.findIndex((item) => item.data && item.data._id === id);
if (index === -1) {
this.store.cacheDatas.push({
data: deepClone(this.store.data, true),
// offscreen: new Array(2),
// flag: new Array(2)
});
if (this.store.cacheDatas.length > this.store.options.cacheLength) {
this.store.cacheDatas.shift();
}
}
else {
let cacheDatas = this.store.cacheDatas.splice(index, 1)[0];
this.store.cacheDatas.push(cacheDatas);
}
}
}
loadCacheData(id) {
let index = this.store.cacheDatas.findIndex((item) => item.data && item.data._id === id);
if (index === -1) {
return;
}
// const ctx = this.canvas.canvas.getContext('2d');
// ctx.clearRect(0, 0, this.canvas.canvas.width, this.canvas.canvas.height);
// for (let offs of this.store.cacheDatas[index].offscreen) {
// if (offs) {
// ctx.drawImage(offs, 0, 0, this.canvas.width, this.canvas.height);
// }
// }
// ctx.clearRect(0, 0, this.canvas.canvas.width, this.canvas.canvas.height);
this.store.data = this.store.cacheDatas[index].data;
this.setBackgroundImage(this.store.data.bkImage);
this.store.pens = {};
this.store.data.pens.forEach((pen) => {
pen.calculative.canvas = this.canvas;
this.store.pens[pen.id] = pen;
globalStore.path2dDraws[pen.name] &&
this.store.path2dMap.set(pen, globalStore.path2dDraws[pen.name](pen));
pen.type &&
this.store.path2dMap.set(pen, globalStore.path2dDraws[pen.name](pen));
if (pen.image) {
pen.calculative.imageDrawed = false;
this.canvas.loadImage(pen);
}
});
this.render();
}
loadLineAnimateDraws() {
globalStore.lineAnimateDraws = {};
Object.entries(this.store.data.lineAnimateDraws).forEach(([key, drawFunc]) => {
// @ts-ignore
globalStore.lineAnimateDraws[key] = new Function('ctx', 'pen', 'state', 'index', drawFunc);
});
}
statistics() {
const num = this.store.data.pens.length;
const imgNum = this.store.data.pens.filter((pen) => pen.image).length;
const imgDrawNum = this.store.data.pens.filter((pen) => pen.image && pen.calculative.inView).length;
const domNum = this.store.data.pens.filter((pen) => pen.name.endsWith('Dom') ||
isDomShapes.includes(pen.name) ||
this.store.options.domShapes.includes(pen.name) ||
pen.externElement || pen.isDom).length;
const aningNum = this.store.animates.size;
let dataPointsNum = 0;
Object.keys(this.store.bind).forEach((key) => {
dataPointsNum += this.store.bind[key].length;
});
Object.keys(this.store.bindDatas).forEach((key) => {
dataPointsNum += this.store.bindDatas[key].length;
});
return {
"图元总数量": num,
"图片图元数量": imgNum,
"图片图元绘制数量": imgDrawNum,
"dom图元数量": domNum,
"正在执行的动画数量": aningNum,
"数据点数量": dataPointsNum,
};
}
initBindDatas() {
this.store.bindDatas = {};
this.store.data.pens.forEach((pen) => {
pen.form?.forEach((formItem) => {
let dataIds;
if (formItem.dataIds) {
if (Array.isArray(formItem.dataIds)) {
dataIds = formItem.dataIds;
}
else {
dataIds = [formItem.dataIds];
}
}
dataIds?.forEach((item) => {
if (!this.store.bindDatas[item.dataId]) {
this.store.bindDatas[item.dataId] = [];
}
this.store.bindDatas[item.dataId].push({
id: pen.id,
formItem,
});
});
});
});
}
jetLinksList = [];
jetLinksClient;
initBinds() {
this.jetLinksList = [];
this.store.bind = {};
const devices = [];
const properties = [];
const computes = [];
this.store.data.pens.forEach((pen) => {
pen.realTimes?.forEach((realTime) => {
if (realTime.bind && realTime.bind.id) {
// if (!this.store.bind[realTime.bind.id]) {
// this.store.bind[realTime.bind.id] = [];
// }
// this.store.bind[realTime.bind.id].push({
// id: pen.id,
// key: realTime.key,
// });
//JetLinks
const Jet = this.store.data.networks?.some((item) => item.protocol === 'ADIIOT');
let productId = realTime.produc