@meta2d/core
Version:
@meta2d/core: Powerful, Beautiful, Simple, Open - Web-Based 2D At Its Best .
1,307 lines • 340 kB
JavaScript
import { KeydownType } from '../options';
import { addLineAnchor, calcIconRect, calcTextRect, calcWorldAnchors, calcWorldRects, LockState, nearestAnchor, PenType, pushPenAnchor, removePenAnchor, renderPen, scalePen, translateLine, deleteTempAnchor, connectLine, disconnectLine, getAnchor, calcAnchorDock, calcMoveDock, calcTextLines, setNodeAnimate, setLineAnimate, calcPenRect, setChildrenActive, getParent, setHover, randomId, getPensLock, getToAnchor, getFromAnchor, calcPadding, getPensDisableRotate, getPensDisableResize, needCalcTextRectProps, calcResizeDock, needPatchFlagsPenRectProps, needCalcIconRectProps, isDomShapes, renderPenRaw, needSetPenProps, getAllChildren, calcInView, isShowChild, getTextColor, clearLifeCycle, rotatePen, calcTextAutoWidth, getGradientAnimatePath, CanvasLayer, ctxFlip, ctxRotate, setGlobalAlpha, drawImage, setElemPosition, getAllFollowers, calcChildrenInitRect, needImgCanvasPatchFlagsProps, } from '../pen';
import { calcRotate, distance, getDistance, hitPoint, PointType, PrevNextType, rotatePoint, samePoint, scalePoint, translatePoint, TwoWay, } from '../point';
import { calcCenter, calcRightBottom, calcRelativePoint, getRect, getRectOfPoints, pointInRect, pointInSimpleRect, rectInRect, rectToPoints, resizeRect, translateRect, pointInPolygon } from '../rect';
import { EditType, globalStore, } from '../store';
import { deepClone, fileToBase64, uploadFile, formatPadding, rgba, s8, toNumber, } from '../utils';
import { inheritanceProps, defaultCursors, defaultDrawLineFns, HotkeyType, HoverType, MouseRight, rotatedCursors, } from '../data';
import { createOffscreen } from './offscreen';
import { curve, mind, getLineLength, getLineRect, pointInLine, simplify, smoothLine, lineSegment, getLineR, lineInRect, } from '../diagrams';
import { polyline, translatePolylineAnchor } from '../diagrams/line/polyline';
import { Tooltip } from '../tooltip';
import { Scroll } from '../scroll';
import { CanvasImage } from './canvasImage';
import { MagnifierCanvas } from './magnifierCanvas';
import { lockedError } from '../utils/error';
import { Dialog } from '../dialog';
import { setter } from '../utils/object';
import { isNumber } from '../utils/tool';
import { Title } from '../title';
import { CanvasTemplate } from './canvasTemplate';
import { getLinePoints } from '../diagrams/line';
import { Popconfirm } from '../popconfirm';
import { le5leTheme, themeKeys } from '../theme';
export const movingSuffix = '-moving';
export class Canvas {
parent;
parentElement;
store;
canvas = document.createElement('canvas');
offscreen = createOffscreen();
width;
height;
externalElements = document.createElement('div');
clientRect;
canvasRect;
activeRect;
initActiveRect;
dragRect;
lastRotate = 0;
sizeCPs;
activeInitPos;
hoverType = HoverType.None;
resizeIndex = 0;
mouseDown;
hotkeyType;
mouseRight;
addCaches;
touchCenter;
initTouchDis;
initScale;
touchScaling;
touchMoving;
startTouches;
lastTouchY;
startDistance;
startCenter;
currentCenter;
lastOffsetX = 0;
lastOffsetY = 0;
drawingLineName;
drawLineFns = [...defaultDrawLineFns];
drawingLine;
pencil;
pencilLine;
movingPens;
patchFlagsLines = new Set();
dock;
prevAnchor;
nextAnchor;
lastMouseTime = 0;
hoverTimer = 0;
fitTimer = 0;
// 即将取消活动状态的画笔,用于Ctrl选中/取消选中画笔
willInactivePen;
patchFlags = false;
lastRender = 0;
touchStart = 0;
touchStartTimer;
timer;
lastTapTime = 0;
lastAnimateRender = 0;
animateRendering = false;
renderTimer;
initPens;
pointSize = 8;
pasteOffset = true;
opening = false;
maxZindex = 5;
canMoveLine = false; //moveConnectedLine=false
randomIdObj; //记录拖拽前后id变化
keyOptions;
isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
/**
* @deprecated 改用 beforeAddPens
*/
beforeAddPen;
beforeAddPens;
beforeAddAnchor;
beforeRemovePens;
beforeRemoveAnchor;
customResizeDock;
customMoveDock;
inputParent = document.createElement('div');
// input = document.createElement('textarea');
inputDiv = document.createElement('div');
// inputRight = document.createElement('div');
dropdown = document.createElement('ul');
tooltip;
popconfirm;
title;
mousePos = { x: 0, y: 0 };
scroll;
movingAnchor; // 正在移动中的瞄点
canvasTemplate;
canvasImage;
canvasImageBottom;
magnifierCanvas;
dialog;
autoPolylineFlag = false; //标记open不自动计算
stopPropagation = (e) => {
e.stopPropagation();
};
constructor(parent, parentElement, store) {
this.parent = parent;
this.parentElement = parentElement;
this.store = store;
this.canvasTemplate = new CanvasTemplate(parentElement, store);
this.canvasTemplate.canvas.style.zIndex = '1';
this.canvasImageBottom = new CanvasImage(parentElement, store, true);
this.canvasImageBottom.canvas.style.zIndex = '2';
parentElement.appendChild(this.canvas);
this.canvas.style.position = 'absolute';
this.canvas.style.backgroundRepeat = 'no-repeat';
this.canvas.style.backgroundSize = '100% 100%';
this.canvas.style.zIndex = '3';
this.canvasImage = new CanvasImage(parentElement, store);
this.canvasImage.canvas.style.zIndex = '4';
this.magnifierCanvas = new MagnifierCanvas(this, parentElement, store);
this.magnifierCanvas.canvas.style.zIndex = '5';
this.externalElements.style.position = 'absolute';
this.externalElements.style.left = '0';
this.externalElements.style.top = '0';
this.externalElements.style.outline = 'none';
this.externalElements.style.background = 'transparent';
this.externalElements.style.zIndex = '5';
parentElement.style.position = 'relative';
parentElement.style['-webkit-tap-highlight-color'] = 'transparent';
parentElement.appendChild(this.externalElements);
this.createInput();
this.tooltip = new Tooltip(parentElement, store);
this.tooltip.box.onmouseleave = (e) => {
this.patchFlags = true;
this.store.lastHover && (this.store.lastHover.calculative.hover = false);
let hover = this.store.data.pens.find((item) => item.calculative.hover === true);
setHover(hover, false);
};
this.popconfirm = new Popconfirm(parentElement, store);
this.dialog = new Dialog(parentElement, store);
this.title = new Title(parentElement);
if (this.store.options.scroll) {
this.scroll = new Scroll(this);
}
this.store.dpiRatio = globalThis.devicePixelRatio || 1;
if (this.store.dpiRatio < 1) {
this.store.dpiRatio = 1;
}
else if (this.store.dpiRatio > 1 && this.store.dpiRatio < 1.5) {
this.store.dpiRatio = 1.5;
}
this.clientRect = this.externalElements.getBoundingClientRect();
this.listen();
window?.addEventListener('resize', this.onResize);
window?.addEventListener('scroll', this.onScroll);
window?.addEventListener('message', this.onMessage);
}
curve = curve;
polyline = polyline;
mind = mind;
line = lineSegment;
listen() {
// ios
this.externalElements.addEventListener('gesturestart', this.onGesturestart);
this.externalElements.ondragover = (e) => e.preventDefault();
this.externalElements.ondrop = this.ondrop;
this.externalElements.oncontextmenu = (e) => e.preventDefault();
this.store.options.interval = 50;
if (this.store.options.parentTouch) {
this.parentElement.ontouchstart = this.ontouchstart;
this.parentElement.ontouchmove = this.ontouchmove;
this.parentElement.ontouchend = this.ontouchend;
}
else {
this.externalElements.ontouchstart = this.ontouchstart;
this.externalElements.ontouchmove = this.ontouchmove;
this.externalElements.ontouchend = this.ontouchend;
}
this.externalElements.onmousedown = (e) => {
if (this.isMobile) {
return;
}
this.onMouseDown({
x: e.offsetX,
y: e.offsetY,
clientX: e.clientX,
clientY: e.clientY,
pageX: e.pageX,
pageY: e.pageY,
ctrlKey: e.ctrlKey || e.metaKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
buttons: e.buttons,
});
};
this.externalElements.onmousemove = (e) => {
if (this.isMobile) {
return;
}
if (e.target !== this.externalElements) {
return;
}
this.onMouseMove({
x: e.offsetX,
y: e.offsetY,
clientX: e.clientX,
clientY: e.clientY,
pageX: e.pageX,
pageY: e.pageY,
ctrlKey: e.ctrlKey || e.metaKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
buttons: e.buttons,
});
};
this.externalElements.onmouseup = (e) => {
if (this.isMobile) {
return;
}
this.onMouseUp({
x: e.offsetX,
y: e.offsetY,
clientX: e.clientX,
clientY: e.clientY,
pageX: e.pageX,
pageY: e.pageY,
ctrlKey: e.ctrlKey || e.metaKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
buttons: e.buttons,
button: e.button,
});
};
this.externalElements.onmouseleave = (e) => {
//离开画布取消所有选中
this.store.data.pens.forEach((pen) => {
if (pen.calculative.hover) {
pen.calculative.hover = false;
}
});
if (this.store.hover) {
this.store.hover.calculative.hover = false;
this.store.hover = undefined;
}
this.render();
if (e.toElement !== this.tooltip.box &&
e.toElement !== this.tooltip.arrowUp &&
e.toElement !== this.tooltip.arrowDown) {
this.tooltip.hide();
this.store.lastHover = undefined;
}
};
this.externalElements.ondblclick = (e) => {
if (this.isMobile) {
return;
}
this.ondblclick(e);
};
this.externalElements.tabIndex = 0;
this.externalElements.onblur = () => {
this.mouseDown = undefined;
};
this.externalElements.onwheel = this.onwheel;
document.addEventListener('copy', this.onCopy);
document.addEventListener('cut', this.onCut);
document.addEventListener('paste', this.onPaste);
switch (this.store.options.keydown) {
case KeydownType.Document:
document.addEventListener('keydown', this.onkeydown);
document.addEventListener('keyup', this.onkeyup);
break;
case KeydownType.Canvas:
this.externalElements.addEventListener('keydown', this.onkeydown);
this.externalElements.addEventListener('keyup', this.onkeyup);
break;
}
}
onCopy = (event) => {
if (this.store.options.disableClipboard) {
return;
}
if (event.target !== this.externalElements &&
event.target !== document.body &&
event.target.offsetParent !== this.externalElements) {
return;
}
this.copy();
};
onCut = (event) => {
if (this.store.options.disableClipboard) {
return;
}
if (event.target !== this.externalElements &&
event.target !== document.body &&
event.target.offsetParent !== this.externalElements) {
return;
}
this.cut();
};
onPaste = async (event) => {
if (this.store.data.locked || this.store.options.disableClipboard) {
return;
}
if (event.target !== this.externalElements &&
event.target !== document.body &&
event.target.offsetParent !== this.externalElements) {
return;
}
// 是否粘贴图片
let hasImages;
if (navigator.clipboard && event.clipboardData) {
const items = event.clipboardData.items;
if (items) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1 && items[i].getAsFile()) {
hasImages = true;
break;
}
}
}
}
if (hasImages) {
const items = event.clipboardData.items;
if (items) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1 && items[i].getAsFile()) {
const { x, y } = this.mousePos;
const blob = items[i].getAsFile();
let name = items[i].type.slice(6) === 'gif' ? 'gif' : 'image';
if (blob !== null) {
const isGif = name === 'gif';
const pen = await this.fileToPen(blob, isGif);
pen.height = (pen.height / pen.width) * 100,
pen.width = 100;
pen.x = x - 50 / 2,
pen.y = y - (pen.height / pen.width) * 50,
pen.externElement = isGif,
this.addPens([pen]);
this.active([pen]);
this.copy([pen]);
// let base64_str: any;
// const reader = new FileReader();
// reader.onload = (e) => {
// base64_str = e.target.result;
// const image = new Image();
// image.src = base64_str;
// image.onload = () => {
// const { width, height } = image;
// const pen = {
// name,
// x: x - 50 / 2,
// y: y - (height / width) * 50,
// externElement: name === 'gif',
// width: 100,
// height: (height / width) * 100,
// image: base64_str as any,
// };
// this.addPens([pen]);
// this.active([pen]);
// this.copy([pen]);
// };
// };
// reader.readAsDataURL(blob);
}
}
}
}
}
else {
this.paste();
}
};
onMessage = (e) => {
if (typeof e.data !== 'string' ||
!e.data ||
e.data.startsWith('setImmediate') ||
e.data.startsWith('webpackHotUpdate') // 处理vue2 webpack4 热更新消息冲突问题
) {
return;
}
let data = JSON.parse(e.data);
if (typeof data === 'object') {
if (data.name === 'onload') {
this.dialog.iframe.contentWindow.postMessage(JSON.stringify({
name: 'dialog',
data: this.dialog.data
}), '*');
}
if (data.name === 'closeDialog') {
this.dialog.hide();
}
this.parent.doMessageEvent(data.name, JSON.stringify(data.data));
}
else {
this.parent.doMessageEvent(data);
}
};
onwheel = (e) => {
//输入模式不允许滚动
if (this.inputDiv.contentEditable === 'true') {
return;
}
//画线过程中不允许缩放
if (this.drawingLine) {
return;
}
if (this.pencil) {
return;
}
if (this.store.hover) {
if (this.store.hover.onWheel) {
this.store.hover.onWheel(this.store.hover, e);
return;
}
}
if (this.store.data.disableScale || this.store.options.disableScale) {
return;
}
e.preventDefault();
e.stopPropagation();
//移动画笔过程中不允许缩放
if (this.mouseDown &&
(this.hoverType === HoverType.Node || this.hoverType === HoverType.Line))
return;
if (this.store.data.locked === LockState.Disable)
return;
if (this.store.data.locked === LockState.DisableScale)
return;
if (this.store.data.locked === LockState.DisableMoveScale)
return;
// e.ctrlKey: false - 平移; true - 缩放。老windows触摸板不支持
if (!e.ctrlKey &&
Math.abs(e.wheelDelta) < 100 &&
e.deltaY.toString().indexOf('.') === -1) {
if (this.store.options.scroll && !e.metaKey && this.scroll) {
this.scroll.wheel(e.deltaY < 0);
return;
}
const scale = this.store.data.scale || 1;
this.translate(-e.deltaX / scale, -e.deltaY / scale);
return;
}
if (Math.abs(e.wheelDelta) > 100) {
//鼠标滚轮滚动 scroll模式下是上下滚动而不是缩放 ctrl可以控制缩放
if (this.store.options.scroll && this.scroll && !this.store.options.scrollButScale && !(e.ctrlKey || e.metaKey)) {
this.scroll.wheel(e.deltaY < 0);
return;
}
}
//禁止触摸屏双指缩放操作
if (this.store.options.disableTouchPadScale) {
return;
}
let scaleOff = 0.015;
if (this.store.options.scaleOff) {
scaleOff = this.store.options.scaleOff;
if (e.deltaY > 0) {
scaleOff = -this.store.options.scaleOff;
}
}
else {
let isMac = /mac os /i.test(navigator.userAgent);
if (isMac) {
if (!e.ctrlKey) {
scaleOff *= e.wheelDeltaY / 240;
}
else if (e.deltaY > 0) {
scaleOff *= -1;
}
}
else {
let offset = 0.2;
if (e.deltaY.toString().indexOf('.') !== -1) {
offset = 0.01;
}
if (e.deltaY > 0) {
scaleOff = -offset;
}
else {
scaleOff = offset;
}
}
}
let { offsetX: x, offsetY: y } = e;
// if (this.parent.map && e.target === this.parent.map?.box) {
// //放大镜缩放
// const width = this.store.data.width || this.store.options.width;
// const height = this.store.data.height || this.store.options.height;
// if (width && height) {
// //大屏
// x =
// (x / this.parent.map.boxWidth) * width * this.store.data.scale +
// this.store.data.origin.x;
// y =
// (y / this.parent.map.boxHeight) * height * this.store.data.scale +
// this.store.data.origin.y;
// const rect = this.canvas.getBoundingClientRect();
// x = x + rect.left;
// y = y + rect.top;
// } else {
// const rect = this.parent.getRect();
// x =
// (x / this.parent.map.boxWidth) * rect.width +
// rect.x +
// this.store.data.x;
// y =
// (y / this.parent.map.boxHeight) * rect.height +
// rect.y +
// this.store.data.y;
// }
// }
this.scale(this.store.data.scale + scaleOff, { x, y });
this.externalElements.focus(); // 聚焦
};
onkeydown = (e) => {
if (this.store.data.locked >= LockState.DisableEdit &&
e.target.tagName !== 'INPUT' &&
e.target.tagName !== 'TEXTAREA' &&
!e.target.dataset.meta2dIgnore) {
this.store.active.forEach((pen) => {
pen.onKeyDown?.(pen, e.key);
});
}
if (this.store.data.locked >= LockState.DisableEdit ||
e.target.tagName === 'INPUT' ||
e.target.tagName === 'TEXTAREA' ||
e.target.dataset.meta2dIgnore) {
return;
}
if (this.store.options.unavailableKeys.includes(e.key)) {
return;
}
if (!this.keyOptions) {
this.keyOptions = {};
}
this.keyOptions.altKey = e.altKey;
this.keyOptions.shiftKey = e.shiftKey;
this.keyOptions.ctrlKey = e.ctrlKey;
this.keyOptions.metaKey = e.metaKey;
this.keyOptions.F = false;
if (e.key === 'F' || e.key === 'f') {
this.keyOptions.F = true;
}
let x = 10;
let y = 10;
let vRect = null;
if (this.store.options.strictScope) {
const width = this.store.data.width || this.store.options.width;
const height = this.store.data.height || this.store.options.height;
if (width && height) {
vRect = {
x: this.store.data.origin.x,
y: this.store.data.origin.y,
width: width * this.store.data.scale,
height: height * this.store.data.scale,
};
}
}
switch (e.key) {
case ' ':
this.hotkeyType = HotkeyType.Translate;
break;
case 'Control':
if (this.drawingLine) {
this.drawingLine.calculative.drawlineH =
!this.drawingLine.calculative.drawlineH;
}
else if (!this.hotkeyType) {
this.patchFlags = true;
this.hotkeyType = HotkeyType.Select;
}
break;
case 'Meta':
break;
case 'Shift':
if (this.store.active.length === 1 &&
this.store.active[0].type &&
this.store.activeAnchor) {
this.toggleAnchorHand();
}
else if (!this.hotkeyType) {
this.patchFlags = true;
if (!this.store.options.resizeMode) {
this.hotkeyType = HotkeyType.Resize;
}
}
break;
case 'Alt':
if (!e.ctrlKey && !e.shiftKey && this.drawingLine) {
const to = getToAnchor(this.drawingLine);
if (to !== this.drawingLine.calculative.activeAnchor) {
deleteTempAnchor(this.drawingLine);
this.drawingLine.calculative.worldAnchors.push(to);
}
else {
this.drawingLine.calculative.worldAnchors.push({
x: to.x,
y: to.y,
});
}
const index = this.drawLineFns.indexOf(this.drawingLineName);
this.drawingLineName =
this.drawLineFns[(index + 1) % this.drawLineFns.length];
this.drawingLine.lineName = this.drawingLineName;
this.drawline();
this.patchFlags = true;
}
e.preventDefault();
break;
case 'a':
case 'A':
if (e.ctrlKey || e.metaKey) {
// TODO: ctrl + A 会选中 visible == false 的元素
this.active(this.store.data.pens.filter((pen) => !pen.parentId && pen.locked !== LockState.Disable));
e.preventDefault();
}
else {
this.toggleAnchorMode();
}
break;
case 'Delete':
case 'Backspace':
if (this.canvasImage.fitFlag && this.canvasImage.activeFit) {
this.deleteFit();
break;
}
!this.store.data.locked && this.delete();
break;
case 'ArrowLeft':
if (this.movingAnchor) {
this.translateAnchor(-1, 0);
break;
}
x = -1;
if (e.shiftKey) {
x = -5;
}
if (e.ctrlKey || e.metaKey) {
x = -10;
}
x = x * this.store.data.scale;
if (this.store.activeAnchor &&
this.store.active &&
this.store.active.length === 1 &&
this.store.active[0].type) {
this.moveLineAnchor({ x: this.store.activeAnchor.x + x, y: this.store.activeAnchor.y }, {});
break;
}
if (vRect && this.activeRect.x + x < vRect.x) {
x = vRect.x - this.activeRect.x;
}
this.translatePens(this.store.active, x, 0);
break;
case 'ArrowUp':
if (this.movingAnchor) {
this.translateAnchor(0, -1);
break;
}
y = -1;
if (e.shiftKey) {
y = -5;
}
if (e.ctrlKey || e.metaKey) {
y = -10;
}
y = y * this.store.data.scale;
if (vRect && this.activeRect.y + y < vRect.y) {
y = vRect.y - this.activeRect.y;
}
if (this.store.activeAnchor &&
this.store.active &&
this.store.active.length === 1 &&
this.store.active[0].type) {
this.moveLineAnchor({ x: this.store.activeAnchor.x, y: this.store.activeAnchor.y + y }, {});
break;
}
this.translatePens(this.store.active, 0, y);
break;
case 'ArrowRight':
if (this.movingAnchor) {
this.translateAnchor(1, 0);
break;
}
x = 1;
if (e.shiftKey) {
x = 5;
}
if (e.ctrlKey || e.metaKey) {
x = 10;
}
x = x * this.store.data.scale;
if (this.store.activeAnchor &&
this.store.active &&
this.store.active.length === 1 &&
this.store.active[0].type) {
this.moveLineAnchor({ x: this.store.activeAnchor.x + x, y: this.store.activeAnchor.y }, {});
break;
}
if (vRect &&
this.activeRect.x + this.activeRect.width + x > vRect.x + vRect.width) {
x =
vRect.x + vRect.width - (this.activeRect.x + this.activeRect.width);
}
this.translatePens(this.store.active, x, 0);
break;
case 'ArrowDown':
if (this.movingAnchor) {
this.translateAnchor(0, 1);
break;
}
y = 1;
if (e.shiftKey) {
y = 5;
}
if (e.ctrlKey || e.metaKey) {
y = 10;
}
y = y * this.store.data.scale;
if (vRect &&
this.activeRect.y + this.activeRect.height + y >
vRect.y + vRect.height) {
y =
vRect.y +
vRect.height -
(this.activeRect.y + this.activeRect.height);
}
if (this.store.activeAnchor &&
this.store.active &&
this.store.active.length === 1 &&
this.store.active[0].type) {
this.moveLineAnchor({ x: this.store.activeAnchor.x, y: this.store.activeAnchor.y + y }, {});
break;
}
this.translatePens(this.store.active, 0, y);
break;
case 'd':
case 'D':
if (!this.store.active[0]?.locked) {
this.removeAnchorHand();
}
break;
case 'h':
case 'H':
if (!this.store.active[0]?.locked) {
this.addAnchorHand();
}
break;
case 'm':
case 'M':
this.toggleMagnifier();
break;
case 'g':
case 'G':
//组合/解组
if (e.ctrlKey || e.metaKey) {
if (e.shiftKey) {
this.parent.uncombine();
}
else {
if (this.store.active.length > 1) {
this.parent.combine(this.store.active);
}
}
e.preventDefault();
break;
}
// 进入移动瞄点状态
if (this.hoverType === HoverType.NodeAnchor) {
this.movingAnchor = this.store.hoverAnchor;
this.externalElements.style.cursor = 'move';
}
break;
case 's':
case 'S':
// 分割线
if (!this.store.data.locked &&
this.hoverType === HoverType.LineAnchor &&
this.store.hover === this.store.active[0]) {
this.splitLine(this.store.active[0], this.store.hoverAnchor);
}
// 保存
(e.ctrlKey || e.metaKey) &&
this.store.emitter.emit('save', { event: e });
break;
case 'c':
case 'C':
if ((e.ctrlKey || e.metaKey) && this.store.options.disableClipboard) {
this.copy();
}
break;
case 'x':
case 'X':
if ((e.ctrlKey || e.metaKey) && this.store.options.disableClipboard) {
this.cut();
}
break;
case '√': //MAC OPTION + V
case 'v':
case 'V':
if (!e.ctrlKey && !e.metaKey) {
if (this.pencil) {
this.stopPencil();
}
if (this.drawingLineName) {
this.finishDrawline();
this.drawingLineName = '';
}
else {
this.drawingLineName = this.store.options.drawingLineName;
}
}
if (!this.store.data.locked && (e.ctrlKey || e.metaKey) && (this.store.options.disableClipboard ||
(!this.store.options.disableClipboard && e.altKey)) //alt按下,paste事件无效
) {
this.paste();
}
break;
case 'b':
case 'B':
if (this.drawingLineName) {
this.finishDrawline();
this.drawingLineName = '';
}
if (this.pencil) {
this.stopPencil();
}
else {
this.drawingPencil();
}
break;
case 'y':
case 'Y':
if (e.ctrlKey || e.metaKey) {
this.redo();
}
break;
case 'z':
case 'Z':
if (e.ctrlKey || e.metaKey) {
if (e.shiftKey) {
this.redo();
}
else {
this.undo();
}
}
else if (e.shiftKey) {
this.redo();
}
break;
case 'Enter':
if (this.drawingLineName) {
this.finishDrawline(true);
if (this.store.active[0].anchors[0].connectTo) {
this.drawingLineName = '';
}
else {
this.drawingLineName = this.store.options.drawingLineName;
}
}
if (this.store.active) {
this.store.active.forEach((pen) => {
if (pen.type) {
pen.close = !pen.close;
if (pen.close) {
getLinePoints(pen);
}
this.store.path2dMap.set(pen, globalStore.path2dDraws.line(pen));
getLineLength(pen);
}
else {
//图元进入编辑模式
pen.calculative.focus = true;
}
});
this.render();
}
break;
case 'Escape':
if (this.drawingLineName) {
this.finishDrawline();
}
this.drawingLineName = undefined;
this.stopPencil();
if (this.store.active) {
this.store.active.forEach((pen) => {
if (pen.type) {
}
else {
//图元退出编辑模式
pen.calculative.focus = false;
}
});
}
if (this.movingPens) {
this.getAllByPens(this.movingPens).forEach((pen) => {
this.store.pens[pen.id] = undefined;
});
this.movingPens = undefined;
this.mouseDown = undefined;
this.clearDock();
this.store.active?.forEach((pen) => {
this.updateLines(pen);
});
this.calcActiveRect();
this.patchFlags = true;
}
this.hotkeyType = HotkeyType.None;
this.movingAnchor = undefined;
if (this.magnifierCanvas.magnifier) {
this.magnifierCanvas.magnifier = false;
this.patchFlags = true;
}
break;
case 'E':
case 'e':
this.store.options.disableAnchor = !this.store.options.disableAnchor;
this.store.emitter.emit('disableAnchor', this.store.options.disableAnchor);
break;
case '=':
if (e.ctrlKey || e.metaKey) {
this.scale(this.store.data.scale + 0.1);
e.preventDefault();
e.stopPropagation();
}
break;
case '-':
if (e.ctrlKey || e.metaKey) {
this.scale(this.store.data.scale - 0.1);
e.preventDefault();
e.stopPropagation();
}
break;
case 'l':
case 'L':
this.canMoveLine = true;
break;
case '[':
//下一层
this.parent.down();
break;
case ']':
//上一层
this.parent.up();
break;
case '{':
// 置底
this.parent.bottom();
break;
case '}':
//置顶
this.parent.top();
break;
case 'F':
case 'f':
if (!this.store.data.locked && (e.ctrlKey || e.metaKey) && !this.store.options.disableClipboard) {
//粘贴到被复制图元上一层
this.paste();
}
this.setFollowers();
break;
}
this.render(false);
};
/**
* 分割连线的锚点,变成两条线
* @param line 连线
* @param anchor 锚点,连线的某个锚点,引用相同
*/
splitLine(line, anchor) {
const worldAnchors = line.calculative.worldAnchors;
const index = worldAnchors.findIndex((a) => a === anchor);
if ([-1, 0, worldAnchors.length - 1].includes(index)) {
// 没找到,起终点不处理
return;
}
const initLine = deepClone(line, true);
const newLine = deepClone(line, true);
const id = s8();
newLine.id = id;
newLine.calculative.canvas = this;
newLine.calculative.active = false;
newLine.calculative.hover = false;
// index 作为公共点
const preAnchors = deepClone(worldAnchors.slice(0, index + 1));
const laterAnchors = deepClone(worldAnchors.slice(index)).map((a) => {
a.penId = id;
return a;
});
line.calculative.worldAnchors = preAnchors;
newLine.calculative.worldAnchors = laterAnchors;
this.initLineRect(line);
this.initLineRect(newLine);
this.store.data.pens.push(newLine);
this.store.pens[id] = newLine;
this.pushHistory({
type: EditType.Add,
pens: [deepClone(newLine, true)],
step: 2,
});
this.pushHistory({
type: EditType.Update,
initPens: [initLine],
pens: [deepClone(line, true)],
step: 2,
});
}
translateAnchor(x, y) {
this.movingAnchor.x += x;
this.movingAnchor.y += y;
// 点不在范围内,移动到范围内
const penId = this.movingAnchor.penId;
if (penId) {
const pen = this.store.pens[penId];
const rect = pen.calculative.worldRect;
if (this.movingAnchor.x < rect.x) {
this.movingAnchor.x = rect.x;
}
else if (this.movingAnchor.x > rect.ex) {
this.movingAnchor.x = rect.ex;
}
if (this.movingAnchor.y < rect.y) {
this.movingAnchor.y = rect.y;
}
else if (this.movingAnchor.y > rect.ey) {
this.movingAnchor.y = rect.ey;
}
const anchor = calcRelativePoint(this.movingAnchor, rect);
// 更改 pen 的 anchors 属性
const index = pen.anchors.findIndex((anchor) => anchor.id === this.movingAnchor.id);
pen.anchors[index] = anchor;
this.patchFlags = true;
}
}
onkeyup = (e) => {
switch (e.key) {
case 'l':
case 'L':
this.canMoveLine = false;
break;
// case 'Alt':
// if (this.drawingLine) {
// this.store.options.autoAnchor = !this.store.options.autoAnchor;
// }
// break;
}
if (this.hotkeyType) {
this.render();
}
if (this.hotkeyType < HotkeyType.AddAnchor) {
this.hotkeyType = HotkeyType.None;
}
};
async fileToPen(file, isGif) {
let url = '';
if (this.store.options.uploadFn) {
url = await this.store.options.uploadFn(file);
}
else if (this.store.options.uploadUrl) {
url = await uploadFile(file, this.store.options.uploadUrl, this.store.options.uploadParams, this.store.options.uploadHeaders);
}
else {
url = await fileToBase64(file);
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
globalStore.htmlElements[url] = img;
resolve({
width: img.width,
height: img.height,
name: isGif ? 'gif' : 'image',
image: url,
});
};
img.onerror = (e) => {
reject(e);
};
img.crossOrigin = this.store.options.crossOrigin || 'anonymous';
img.src = url;
});
}
ondrop = async (event) => {
if (this.store.data.locked) {
console.warn('canvas is locked, can not drop');
return;
}
// Fix bug: 在 firefox 上拖拽图片会打开新页
event.preventDefault();
event.stopPropagation();
const json = event.dataTransfer.getData('Meta2d') ||
event.dataTransfer.getData('Text');
let obj = null;
try {
if (json) {
obj = JSON.parse(json);
}
}
catch (e) { }
if (!obj) {
const { files } = event.dataTransfer;
if (files.length &&
files[0].type.match('image.*') &&
!(this.addCaches && this.addCaches.length)) {
// 必须是图片类型
const isGif = files[0].type === 'image/gif';
obj = await this.fileToPen(files[0], isGif);
}
else if (this.addCaches && this.addCaches.length) {
obj = this.addCaches;
this.addCaches = [];
}
else {
this.store.emitter.emit('drop', undefined);
return;
}
}
obj = Array.isArray(obj) ? obj : [obj];
if (obj[0] && obj[0].draggable !== false) {
const pt = { x: event.offsetX, y: event.offsetY };
this.calibrateMouse(pt);
await this.dropPens(obj, pt);
this.addCaches = [];
// 拖拽新增图元判断是否是在容器上
this.getContainerHover(pt);
this.mousePos.x = pt.x;
this.mousePos.y = pt.y;
this.store.emitter.emit('mouseup', {
x: pt.x,
y: pt.y,
pen: this.store.hoverContainer,
});
}
this.store.emitter.emit('drop', obj || json);
};
async dropPens(pens, e) {
this.randomIdObj = {};
if (this.parent.store.options.textPresetStyle) {
if (pens.length === 1 && !pens[0].temPreset && pens[0].name === 'text') {
Object.assign(pens[0], this.parent.store.options.textPresetStyle);
}
}
for (const pen of pens) {
// 只修改 树根处的 祖先节点, randomCombineId 会递归更改子节点
!pen.parentId && this.randomCombineId(pen, pens);
}
if (Object.keys(this.randomIdObj).length !== 0) {
const keys = Object.keys(this.randomIdObj).join('|');
const regex = new RegExp(`(${keys})`, "g");
for (const pen of pens) {
if (pen.type) {
pen.anchors[0].connectTo = this.randomIdObj[pen.anchors[0].connectTo];
pen.anchors[pen.anchors.length - 1].connectTo =
this.randomIdObj[pen.anchors[pen.anchors.length - 1].connectTo];
}
else {
pen.connectedLines?.forEach((item) => {
item.lineAnchor = this.randomIdObj[item.lineAnchor];
item.lineId = this.randomIdObj[item.lineId];
});
}
//动画、事件和状态
if (pen.animations?.length) {
const str = JSON.stringify(pen.animations).replace(regex, match => this.randomIdObj[match]);
pen.animations = JSON.parse(str);
}
if (pen.triggers?.length) {
const str = JSON.stringify(pen.triggers).replace(regex, match => this.randomIdObj[match]);
pen.triggers = JSON.parse(str);
}
if (pen.events?.length) {
const str = JSON.stringify(pen.events).replace(regex, match => this.randomIdObj[match]);
pen.events = JSON.parse(str);
}
}
}
for (const pen of pens) {
// TODO: randomCombineId 会更改 id, 此处应该不存在空 id
if (!pen.id) {
pen.id = s8();
}
!pen.calculative && (pen.calculative = { canvas: this });
this.store.pens[pen.id] = pen;
}
// // 计算区域
// for (const pen of pens) {
// // 组合节点才需要提前计算
// Array.isArray(pen.children) && pen.children.length > 0 && this.updatePenRect(pen);
// }
let num = 0;
let lastH = 0;
let lastW = 0;
for (const pen of pens) {
if (!pen.parentId) {
pen.width *= this.store.data.scale;
pen.height *= this.store.data.scale;
pen.x = e.x - pen.width / 2 + lastW;
pen.y = e.y - pen.height / 2 + lastH;
if (pen.tags && pen.tags.includes('meta3d')) {
pen.x = this.store.data.origin.x;
pen.y = this.store.data.origin.y;
}
if (pen.dataset) {
if (num % 2 === 0) {
lastW = pen.width - 40 * this.store.data.scale;
}
else {
lastW = 0;
}
num++;
if (num % 2 === 0) {
lastH += pen.height + 10 * this.store.data.scale;
}
delete pen.dataset;
}
if (pen.temOffsetX) {
pen.x += pen.temOffsetX * this.store.data.scale;
delete pen.temOffsetX;
}
if (pen.temOffsetY) {
pen.y += pen.temOffsetY * this.store.data.scale;
delete pen.temOffsetY;
}
}
}
//大屏区域
const width = this.store.data.width || this.store.options.width;
const height = this.store.data.height || this.store.options.height;
if (width && height) {
let rect = {
x: this.store.data.origin.x,
y: this.store.data.origin.y,
width: width * this.store.data.scale,
height: height * this.store.data.scale,
};
let flag = true;
for (const pen of pens) {
if (!pen.parentId) {
let points = [
{ x: pen.x, y: pen.y },
{ x: pen.x + pen.width, y: pen.y },
{ x: pen.x, y: pen.y + pen.height },
{ x: pen.x + pen.width, y: pen.y + pen.height },
{ x: pen.x + pen.width / 2, y: pen.y + pen.height / 2 },
];
if ((pen.x === rect.x &&
pen.y === rect.y &&
pen.width === rect.width &&
pen.height === rect.height) ||
points.some((point) => pointInRect(point, rect))) {
flag = false;
//严格范围模式下对齐大屏边界
if (this.store.options.strictScope) {
if (pen.x < rect.x) {
pen.x = rect.x;
}
if (pen.y < rect.y) {
pen.y = rect.y;
}
if (pen.x + pen.width > rect.x + rect.width) {
pen.x = rect.x + rect.width - pen.width;
}
if (pen.y + pen.height > rect.y + rect.height) {
pen.y = rect.y + rect.height - pen.height;
}
}
break;
}
}
}
if (flag) {
console.info('画笔在大屏范围外');
return;
}
}
await this.addPens(pens, true);
this.active(pens.filter((pen) => !pen.parentId));
this.render();
this.externalElements.focus(); // 聚焦
}
randomCombineId(pen, pens, parentId) {
let beforeIds = null;
if (pen.type) {
if (pen.anchors[0].connectTo ||
pen.anchors[pen.anchors.length - 1].connectTo) {
beforeIds = [
pen.id,
pen.anchors[0].id,
pen.anchors[pen.a