UNPKG

@plait/draw

Version:

Implementation of the core logic of the flowchart drawing tool plugin.

1,242 lines (1,225 loc) 428 kB
import { ACTIVE_STROKE_WIDTH, DEFAULT_COLOR, ThemeColorMode, PlaitElement, RectangleClient, getSelectedElements, idCreator, createDebugGenerator, Point, createG, arrowPoints, createPath, distanceBetweenPointAndPoint, drawLinearPath, rotate, catmullRomFitting, PlaitBoard, setStrokeLinecap, findElements, createMask, createRect, getNearestPointBetweenPointAndArc, setPathStrokeLinecap, distanceBetweenPointAndSegments, HIT_DISTANCE_BUFFER, isPointInPolygon, isLineHitRectangle, rotatePointsByAngle, rotateAntiPointsByElement, getEllipseArcCenter, Transforms, clearSelectedElement, addSelectedElement, BoardTransforms, PlaitPointerType, depthFirstRecursion, getIsRecursionFunc, SNAPPING_STROKE_WIDTH, SELECTION_BORDER_COLOR, SELECTION_FILL_COLOR, drawCircle, getI18nValue, SELECTION_RECTANGLE_CLASS_NAME, toActivePointFromViewBoxPoint, toActiveRectangleFromViewBoxRectangle, drawRectangle, isSelectionMoving, rgbaToHEX, getElementById, rotatePointsByElement, PlaitNode, hasValidAngle, toViewBoxPoint, toHostPoint, Direction, rotatePoints, getSelectionAngle, rotatedDataPoints, isAxisChangedByAngle, getRectangleByElements, getRectangleByAngle, getSnapRectangles, getTripleAxis, getMinPointDelta, SNAP_TOLERANCE, drawPointSnapLines, drawSolidLines, getNearestPointBetweenPointAndSegments, getEllipseTangentSlope, getVectorFromPointAndSlope, getNearestPointBetweenPointAndEllipse, isPointInEllipse, isPointInRoundRectangle, drawRoundRectangle, getCrossingPointsBetweenEllipseAndSegment, drawLine, getNearestPointBetweenPointAndDiscreteSegments, getNearestPointBetweenPointAndSegment, Path, getHitElementByPoint, WritableClipboardOperationType, WritableClipboardType, addOrCreateClipboardContext, setAngleForG, toActivePoint, temporaryDisableSelection, toScreenPointFromActivePoint, PRESS_AND_MOVE_BUFFER, CursorClass, isHorizontalDirection, isMainPointer, throttleRAF, getAngleBetweenPoints, normalizeAngle, degreesToRadians, rotateElements, MERGING, ROTATE_HANDLE_CLASS_NAME, isSelectedElement, isDragging } from '@plait/core'; import { DEFAULT_FILL, Alignment, WithTextPluginKey, TextManage, getMemorizedLatest, memorizeLatest, removeDuplicatePoints, generateElbowLineRoute, simplifyOrthogonalPoints, getExtendPoint, getUnitVectorByPointAndPoint, Generator, getElementSize, DEFAULT_FONT_FAMILY, getPointByVectorComponent, getStrokeLineDash, StrokeStyle, getPointOnPolyline, buildText, getCrossingPointsBetweenPointAndSegment, isPointOnSegment, getFirstTextEditor, sortElementsByArea, isFilled, getTextEditorsByElement, RESIZE_HANDLE_DIAMETER, drawPrimaryHandle, drawFillPrimaryHandle, PRIMARY_COLOR, measureElement, getFirstTextManage, isSourceAndTargetIntersect, getPoints, DEFAULT_ROUTE_MARGIN, normalizeShapePoints, resetPointsAfterResize, getDirectionByVector, getRectangleResizeHandleRefs, getRotatedResizeCursorClassByAngle, ROTATE_HANDLE_SIZE, ROTATE_HANDLE_DISTANCE_TO_ELEMENT, isCornerHandle, getIndexByResizeHandle, withResize, getSymmetricHandleIndex, getResizeHandlePointByIndex, drawHandle, getDirectionFactorByDirectionComponent, buildClipboardData as buildClipboardData$1, insertClipboardData as insertClipboardData$1, getDirectionByPointOfRectangle, getDirectionFactor, rotateVector, getOppositeDirection, rotateVectorAnti90, getSourceAndTargetOuterRectangle, getNextPoint, CommonElementFlavour, createActiveGenerator, hasResizeHandle, ActiveGenerator, isVirtualKey, isDelete, isSpaceHotkey, isDndMode, isDrawingMode, getElementsText, acceptImageTypes, getElementOfFocusedImage, buildImage, isResizingByCondition, getRatioByPoint, getTextManages, ImageGenerator, getDirectionByIndex, moveXOfPoint, getXDistanceBetweenPoint, ResizeHandle, addRotating, removeRotating, drawRotateHandle } from '@plait/common'; import { pointsOnBezierCurves } from 'points-on-curve'; import { DEFAULT_FONT_SIZE, AlignEditor } from '@plait/text-plugins'; import { Editor, Node } from 'slate'; import { isKeyHotkey } from 'is-hotkey'; var BasicShapes; (function (BasicShapes) { BasicShapes["rectangle"] = "rectangle"; BasicShapes["ellipse"] = "ellipse"; BasicShapes["diamond"] = "diamond"; BasicShapes["roundRectangle"] = "roundRectangle"; BasicShapes["parallelogram"] = "parallelogram"; BasicShapes["text"] = "text"; BasicShapes["triangle"] = "triangle"; BasicShapes["leftArrow"] = "leftArrow"; BasicShapes["trapezoid"] = "trapezoid"; BasicShapes["rightArrow"] = "rightArrow"; BasicShapes["cross"] = "cross"; BasicShapes["star"] = "star"; BasicShapes["pentagon"] = "pentagon"; BasicShapes["hexagon"] = "hexagon"; BasicShapes["octagon"] = "octagon"; BasicShapes["pentagonArrow"] = "pentagonArrow"; BasicShapes["processArrow"] = "processArrow"; BasicShapes["twoWayArrow"] = "twoWayArrow"; BasicShapes["comment"] = "comment"; BasicShapes["roundComment"] = "roundComment"; BasicShapes["cloud"] = "cloud"; })(BasicShapes || (BasicShapes = {})); var FlowchartSymbols; (function (FlowchartSymbols) { FlowchartSymbols["process"] = "process"; FlowchartSymbols["decision"] = "decision"; FlowchartSymbols["data"] = "data"; FlowchartSymbols["connector"] = "connector"; FlowchartSymbols["terminal"] = "terminal"; FlowchartSymbols["manualInput"] = "manualInput"; FlowchartSymbols["preparation"] = "preparation"; FlowchartSymbols["manualLoop"] = "manualLoop"; FlowchartSymbols["merge"] = "merge"; FlowchartSymbols["delay"] = "delay"; FlowchartSymbols["storedData"] = "storedData"; FlowchartSymbols["or"] = "or"; FlowchartSymbols["summingJunction"] = "summingJunction"; FlowchartSymbols["predefinedProcess"] = "predefinedProcess"; FlowchartSymbols["offPage"] = "offPage"; FlowchartSymbols["document"] = "document"; FlowchartSymbols["multiDocument"] = "multiDocument"; FlowchartSymbols["database"] = "database"; FlowchartSymbols["hardDisk"] = "hardDisk"; FlowchartSymbols["internalStorage"] = "internalStorage"; FlowchartSymbols["noteCurlyRight"] = "noteCurlyRight"; FlowchartSymbols["noteCurlyLeft"] = "noteCurlyLeft"; FlowchartSymbols["noteSquare"] = "noteSquare"; FlowchartSymbols["display"] = "display"; })(FlowchartSymbols || (FlowchartSymbols = {})); var UMLSymbols; (function (UMLSymbols) { UMLSymbols["actor"] = "actor"; UMLSymbols["useCase"] = "useCase"; UMLSymbols["container"] = "container"; UMLSymbols["note"] = "note"; UMLSymbols["simpleClass"] = "simpleClass"; UMLSymbols["activityClass"] = "activityClass"; UMLSymbols["branchMerge"] = "branchMerge"; UMLSymbols["port"] = "port"; UMLSymbols["package"] = "package"; UMLSymbols["combinedFragment"] = "combinedFragment"; UMLSymbols["class"] = "class"; UMLSymbols["interface"] = "interface"; UMLSymbols["object"] = "object"; UMLSymbols["component"] = "component"; UMLSymbols["componentBox"] = "componentBox"; UMLSymbols["template"] = "template"; UMLSymbols["activation"] = "activation"; UMLSymbols["deletion"] = "deletion"; UMLSymbols["assembly"] = "assembly"; UMLSymbols["providedInterface"] = "providedInterface"; UMLSymbols["requiredInterface"] = "requiredInterface"; })(UMLSymbols || (UMLSymbols = {})); var GeometryCommonTextKeys; (function (GeometryCommonTextKeys) { GeometryCommonTextKeys["name"] = "name"; GeometryCommonTextKeys["content"] = "content"; })(GeometryCommonTextKeys || (GeometryCommonTextKeys = {})); const PlaitGeometry = {}; var SwimlaneSymbols; (function (SwimlaneSymbols) { SwimlaneSymbols["swimlaneVertical"] = "swimlaneVertical"; SwimlaneSymbols["swimlaneHorizontal"] = "swimlaneHorizontal"; })(SwimlaneSymbols || (SwimlaneSymbols = {})); var SwimlaneDrawSymbols; (function (SwimlaneDrawSymbols) { SwimlaneDrawSymbols["swimlaneVertical"] = "swimlaneVertical"; SwimlaneDrawSymbols["swimlaneHorizontal"] = "swimlaneHorizontal"; SwimlaneDrawSymbols["swimlaneVerticalWithHeader"] = "swimlaneVerticalWithHeader"; SwimlaneDrawSymbols["swimlaneHorizontalWithHeader"] = "swimlaneHorizontalWithHeader"; })(SwimlaneDrawSymbols || (SwimlaneDrawSymbols = {})); var TableSymbols; (function (TableSymbols) { TableSymbols["table"] = "table"; })(TableSymbols || (TableSymbols = {})); const PlaitTableElement = { isTable: (value) => { return value.type === 'table'; }, isVerticalText: (value) => { return value.text?.direction === 'vertical'; } }; const WithDrawPluginKey = 'plait-draw-plugin-key'; var DrawI18nKey; (function (DrawI18nKey) { DrawI18nKey["lineText"] = "line-text"; DrawI18nKey["geometryText"] = "geometry-text"; })(DrawI18nKey || (DrawI18nKey = {})); const ShapeDefaultSpace = { rectangleAndText: 4 }; const DefaultDrawStyle = { strokeWidth: 2, defaultRadius: 4, strokeColor: '#000', fill: DEFAULT_FILL }; const DefaultDrawActiveStyle = { strokeWidth: ACTIVE_STROKE_WIDTH, selectionStrokeWidth: ACTIVE_STROKE_WIDTH }; const DefaultBasicShapeProperty = { width: 100, height: 100, strokeColor: DEFAULT_COLOR, strokeWidth: 2 }; const DefaultPentagonArrowProperty = { width: 120, height: 50 }; const DefaultTwoWayArrowProperty = { width: 138, height: 80 }; const DefaultArrowProperty = { width: 100, height: 80 }; const DefaultCloudProperty = { width: 120, height: 100 }; const DefaultTextProperty = { width: 36, height: 20, text: '文本' }; const GeometryThreshold = { defaultTextMaxWidth: 34 * 14 }; const DefaultConnectorProperty = { width: 44, height: 44 }; const DefaultFlowchartProperty = { width: 120, height: 60 }; const DefaultDataBaseProperty = { width: 70, height: 80 }; const DefaultInternalStorageProperty = { width: 80, height: 80 }; const DefaultDecisionProperty = { width: 140, height: 70 }; const DefaultDataProperty = { width: 124, height: 60 }; const DefaultDocumentProperty = { width: 120, height: 70 }; const DefaultNoteProperty = { width: 160, height: 100 }; const DefaultMultiDocumentProperty = { width: 120, height: 80 }; const DefaultManualInputProperty = { width: 117, height: 59 }; const DefaultMergeProperty = { width: 47, height: 33 }; const DefaultActorProperty = { width: 68, height: 100 }; const DefaultContainerProperty = { width: 300, height: 240 }; const DefaultPackageProperty = { width: 210, height: 150, texts: [ { id: GeometryCommonTextKeys.name, text: '包名', align: Alignment.left }, { id: GeometryCommonTextKeys.content, text: '', align: Alignment.left } ] }; const DefaultActivationProperty = { width: 18, height: 80 }; const DefaultObjectProperty = { width: 120, height: 60 }; const DefaultComponentBoxProperty = { width: 200, height: 150 }; const DefaultDeletionProperty = { width: 40, height: 40 }; const DefaultPortProperty = { width: 20, height: 20 }; const DefaultRequiredInterfaceProperty = { width: 70, height: 56 }; const DefaultAssemblyProperty = { width: 120, height: 56 }; const DefaultProvidedInterfaceProperty = { width: 70, height: 34 }; const DefaultCombinedFragmentProperty = { width: 400, height: 280, texts: [ { id: GeometryCommonTextKeys.name, text: 'Opt | Alt | Loop', align: Alignment.left }, { id: GeometryCommonTextKeys.content, text: '[Condition]', align: Alignment.left } ] }; const DefaultClassProperty = { width: 230, height: 180, texts: [ { text: 'Class', align: Alignment.center }, { text: '+ attribute1:type defaultValue\n+ attribute2:type\n- attribute3:type', align: Alignment.left }, { text: '+ operation1(params):returnType\n- operation2(params)\n- operation3()', align: Alignment.left } ] }; const DefaultInterfaceProperty = { width: 230, height: 140, texts: [ { text: '<<interface>>\nInterface', align: Alignment.center }, { text: '+ operation1(params):returnType\n- operation2(params)\n- operation3()', align: Alignment.left } ] }; const DefaultBasicShapePropertyMap = { [BasicShapes.pentagonArrow]: DefaultPentagonArrowProperty, [BasicShapes.processArrow]: DefaultPentagonArrowProperty, [BasicShapes.cloud]: DefaultCloudProperty, [BasicShapes.twoWayArrow]: DefaultTwoWayArrowProperty, [BasicShapes.leftArrow]: DefaultArrowProperty, [BasicShapes.rightArrow]: DefaultArrowProperty }; const DefaultFlowchartPropertyMap = { [FlowchartSymbols.connector]: DefaultConnectorProperty, [FlowchartSymbols.process]: DefaultFlowchartProperty, [FlowchartSymbols.decision]: DefaultDecisionProperty, [FlowchartSymbols.data]: DefaultDataProperty, [FlowchartSymbols.terminal]: DefaultFlowchartProperty, [FlowchartSymbols.manualInput]: DefaultManualInputProperty, [FlowchartSymbols.preparation]: DefaultFlowchartProperty, [FlowchartSymbols.manualLoop]: DefaultFlowchartProperty, [FlowchartSymbols.merge]: DefaultMergeProperty, [FlowchartSymbols.delay]: DefaultFlowchartProperty, [FlowchartSymbols.storedData]: DefaultFlowchartProperty, [FlowchartSymbols.or]: DefaultConnectorProperty, [FlowchartSymbols.summingJunction]: DefaultConnectorProperty, [FlowchartSymbols.predefinedProcess]: DefaultFlowchartProperty, [FlowchartSymbols.offPage]: DefaultFlowchartProperty, [FlowchartSymbols.document]: DefaultDocumentProperty, [FlowchartSymbols.multiDocument]: DefaultMultiDocumentProperty, [FlowchartSymbols.database]: DefaultDataBaseProperty, [FlowchartSymbols.hardDisk]: DefaultFlowchartProperty, [FlowchartSymbols.internalStorage]: DefaultInternalStorageProperty, [FlowchartSymbols.noteCurlyLeft]: DefaultNoteProperty, [FlowchartSymbols.noteCurlyRight]: DefaultNoteProperty, [FlowchartSymbols.noteSquare]: DefaultNoteProperty, [FlowchartSymbols.display]: DefaultFlowchartProperty }; const DefaultUMLPropertyMap = { [UMLSymbols.actor]: DefaultActorProperty, [UMLSymbols.useCase]: DefaultDocumentProperty, [UMLSymbols.container]: DefaultContainerProperty, [UMLSymbols.note]: DefaultObjectProperty, [UMLSymbols.package]: DefaultPackageProperty, [UMLSymbols.combinedFragment]: DefaultCombinedFragmentProperty, [UMLSymbols.class]: DefaultClassProperty, [UMLSymbols.interface]: DefaultInterfaceProperty, [UMLSymbols.activation]: DefaultActivationProperty, [UMLSymbols.object]: DefaultObjectProperty, [UMLSymbols.deletion]: DefaultDeletionProperty, [UMLSymbols.activityClass]: DefaultObjectProperty, [UMLSymbols.simpleClass]: DefaultObjectProperty, [UMLSymbols.component]: DefaultMultiDocumentProperty, [UMLSymbols.template]: DefaultMultiDocumentProperty, [UMLSymbols.componentBox]: DefaultComponentBoxProperty, [UMLSymbols.port]: DefaultPortProperty, [UMLSymbols.branchMerge]: DefaultDeletionProperty, [UMLSymbols.assembly]: DefaultAssemblyProperty, [UMLSymbols.providedInterface]: DefaultProvidedInterfaceProperty, [UMLSymbols.requiredInterface]: DefaultRequiredInterfaceProperty }; const MultipleTextGeometryTextKeys = { [UMLSymbols.package]: Object.keys(GeometryCommonTextKeys), [UMLSymbols.combinedFragment]: Object.keys(GeometryCommonTextKeys) }; const LINE_HIT_GEOMETRY_BUFFER = 4; const LINE_SNAPPING_BUFFER = 4; const LINE_SNAPPING_CONNECTOR_BUFFER = 4; const LINE_ALIGN_TOLERANCE = 4; const GEOMETRY_WITHOUT_TEXT = [ FlowchartSymbols.or, FlowchartSymbols.summingJunction, UMLSymbols.activation, UMLSymbols.deletion, UMLSymbols.port, UMLSymbols.branchMerge, UMLSymbols.assembly, UMLSymbols.providedInterface, UMLSymbols.requiredInterface ]; const GEOMETRY_WITH_MULTIPLE_TEXT = [UMLSymbols.package, UMLSymbols.combinedFragment]; const GEOMETRY_NOT_CLOSED = [ FlowchartSymbols.noteCurlyLeft, FlowchartSymbols.noteCurlyRight, FlowchartSymbols.noteSquare, UMLSymbols.requiredInterface, UMLSymbols.deletion ]; const getGeometryPointers = () => { return [...Object.keys(BasicShapes), ...Object.keys(FlowchartSymbols), ...Object.keys(UMLSymbols)]; }; const getSwimlanePointers = () => { return Object.keys(SwimlaneDrawSymbols); }; const getSwimlaneShapes = () => { return Object.keys(SwimlaneSymbols); }; const getBasicPointers = () => { return Object.keys(BasicShapes); }; const getFlowchartPointers = () => { return Object.keys(FlowchartSymbols); }; const getUMLPointers = () => { return Object.keys(UMLSymbols); }; const getArrowLinePointers = () => { return Object.keys(ArrowLineShape); }; const getVectorLinePointers = () => { return Object.keys(VectorLinePointerType); }; const DEFAULT_IMAGE_WIDTH = 1000; const DrawThemeColors = { [ThemeColorMode.default]: { strokeColor: DEFAULT_COLOR, fill: '#FFFFFF' }, [ThemeColorMode.colorful]: { strokeColor: '#06ADBF', fill: '#CDEFF2' }, [ThemeColorMode.soft]: { strokeColor: '#6D89C1', fill: '#DADFEB' }, [ThemeColorMode.retro]: { strokeColor: '#E9C358', fill: '#F6EDCF' }, [ThemeColorMode.dark]: { strokeColor: '#FFFFFF', fill: '#434343' }, [ThemeColorMode.starry]: { strokeColor: '#42ABE5', fill: '#163F5A' } }; const SWIMLANE_HEADER_SIZE = 42; const DefaultSwimlaneVerticalWithHeaderProperty = { width: 580, height: 524 }; const DefaultSwimlaneHorizontalWithHeaderProperty = { width: 524, height: 580 }; const DefaultSwimlaneVerticalProperty = { width: 580, height: 524 }; const DefaultSwimlaneHorizontalProperty = { width: 524, height: 580 }; const DefaultSwimlanePropertyMap = { [SwimlaneDrawSymbols.swimlaneHorizontal]: DefaultSwimlaneHorizontalProperty, [SwimlaneDrawSymbols.swimlaneVertical]: DefaultSwimlaneVerticalProperty, [SwimlaneDrawSymbols.swimlaneHorizontalWithHeader]: DefaultSwimlaneHorizontalWithHeaderProperty, [SwimlaneDrawSymbols.swimlaneVerticalWithHeader]: DefaultSwimlaneVerticalWithHeaderProperty }; const MIN_TEXT_WIDTH = 5; const DefaultLineStyle = { strokeWidth: 2, strokeColor: '#000' }; const LINE_TEXT_SPACE = 4; const LINE_AUTO_COMPLETE_DIAMETER = 6; const LINE_AUTO_COMPLETE_OPACITY = 1; const LINE_AUTO_COMPLETE_HOVERED_OPACITY = 1; const LINE_AUTO_COMPLETE_HOVERED_DIAMETER = 12; const LINE_TEXT = '文本'; // TODO: 是否可以完全基于位置定位 TextManager,实现 line 和 多文本 geometry 统一 // 一个元素有多个文本时,单纯通过位置无法获取 TextManage,因此这里单独通过 Map 保存关键字 key 和 TextManage 的对应关系 // 1. 单文本元素 key 就是元素的 id // 2. 表格元素 key 是单元格的 id // 3. 符合 isMultipleTextGeometry 的元素,key 是元素 id + text.id (通常不是 id 而是文本位置的常量) // 4. arrow-line 和 vector-line 文本不依赖于 text.generator,基于 text 可以直接找到 TextManage const KEY_TO_TEXT_MANAGE = new WeakMap(); const setTextManage = (board, element, text, textManage) => { const textManages = KEY_TO_TEXT_MANAGE.get(board); return KEY_TO_TEXT_MANAGE.set(board, { ...textManages, [getTextKey(element, text)]: textManage }); }; const getTextManage = (board, element, text) => { const textManages = KEY_TO_TEXT_MANAGE.get(board); return textManages[getTextKey(element, text)]; }; const deleteTextManage = (board, key) => { const textManages = KEY_TO_TEXT_MANAGE.get(board); delete textManages[key]; KEY_TO_TEXT_MANAGE.set(board, textManages); }; class TextGenerator { get shape() { return this.element.shape || this.element.type; } constructor(board, element, texts, options) { this.board = board; this.texts = texts; this.element = element; this.options = options; } initialize() { const textPlugins = (this.board.getPluginOptions(WithTextPluginKey) || {}) .textPlugins; this.textManages = this.texts.map(text => { const textManage = this.createTextManage(text, textPlugins); setTextManage(this.board, this.element, text, textManage); return textManage; }); const ref = PlaitElement.getElementRef(this.element); ref.initializeTextManage(this.textManages); } draw(elementG) { const centerPoint = RectangleClient.getCenterPoint(this.board.getRectangle(this.element)); this.texts.forEach(drawShapeText => { const textManage = getTextManage(this.board, this.element, drawShapeText); if (drawShapeText.text && textManage) { textManage.draw(drawShapeText.text); elementG.append(textManage.g); (this.element.angle || this.element.angle === 0) && textManage.updateAngle(centerPoint, this.element.angle); } }); } update(element, previousDrawShapeTexts, currentDrawShapeTexts, elementG) { this.element = element; const centerPoint = RectangleClient.getCenterPoint(this.board.getRectangle(this.element)); const textPlugins = (this.board.getPluginOptions(WithTextPluginKey) || {}) .textPlugins; const removedTexts = previousDrawShapeTexts.filter(value => { return !currentDrawShapeTexts.find(item => item.id === value.id); }); if (removedTexts.length) { removedTexts.forEach(item => { const textManage = getTextManage(this.board, element, item); const index = this.textManages.findIndex(value => value === textManage); if (index > -1 && item.text) { this.textManages.splice(index, 1); } textManage?.destroy(); deleteTextManage(this.board, item.id); }); } currentDrawShapeTexts.forEach(drawShapeText => { if (drawShapeText.text) { let textManage = getTextManage(this.board, this.element, drawShapeText); if (!textManage) { textManage = this.createTextManage(drawShapeText, textPlugins); setTextManage(this.board, element, drawShapeText, textManage); textManage.draw(drawShapeText.text); elementG.append(textManage.g); this.textManages.push(textManage); } else { textManage.updateText(drawShapeText.text); textManage.updateRectangle(); } (this.element.angle || this.element.angle === 0) && textManage.updateAngle(centerPoint, this.element.angle); } }); } createTextManage(text, textPlugins) { const textManage = new TextManage(this.board, { getRectangle: () => { return this.getRectangle(text); }, onChange: (data) => { return this.options.onChange(this.element, data, text); }, getMaxWidth: () => { return this.getMaxWidth(text); }, getRenderRectangle: () => { return this.options.getRenderRectangle ? this.options.getRenderRectangle(this.element, text) : this.getRectangle(text); }, textPlugins }); return textManage; } getRectangle(text) { const getRectangle = getEngine(this.shape).getTextRectangle; if (getRectangle) { return getRectangle(this.board, this.element, text); } return getTextRectangle$1(this.board, this.element); } getMaxWidth(text) { return this.options.getMaxWidth ? this.options.getMaxWidth() : this.getRectangle(text).width; } destroy() { const ref = PlaitElement.getElementRef(this.element); ref.destroyTextManage(); this.textManages = []; this.texts.forEach(item => { deleteTextManage(this.board, item.id); }); } } const isSingleSelectTable = (board) => { const selectedElements = getSelectedElements(board); return selectedElements && selectedElements.length === 1 && PlaitDrawElement.isElementByTable(selectedElements[0]); }; const getSelectedTableElements = (board, elements) => { const selectedElements = elements?.length ? elements : getSelectedElements(board); return selectedElements.filter(value => PlaitDrawElement.isElementByTable(value)); }; const SELECTED_CELLS = new WeakMap(); function getSelectedCells(element) { return SELECTED_CELLS.get(element); } function setSelectedCells(element, cells) { return SELECTED_CELLS.set(element, cells); } function clearSelectedCells(element) { return SELECTED_CELLS.delete(element); } function getCellsWithPoints(board, element) { const table = board?.buildTable(element); if (!table || !table.points || !table.columns || !table.rows) { throw new Error('can not get table cells points'); } const rectangle = RectangleClient.getRectangleByPoints(table.points); const columnsCount = table.columns.length; const rowsCount = table.rows.length; const cellWidths = calculateCellsSize(table.columns, rectangle.width, columnsCount, true); const cellHeights = calculateCellsSize(table.rows, rectangle.height, rowsCount, false); const cells = table.cells.map(cell => { const rowIdx = table.rows.findIndex(row => row.id === cell.rowId); const columnIdx = table.columns.findIndex(column => column.id === cell.columnId); let cellTopLeftX = rectangle.x; for (let i = 0; i < columnIdx; i++) { cellTopLeftX += cellWidths[i]; } let cellTopLeftY = rectangle.y; for (let i = 0; i < rowIdx; i++) { cellTopLeftY += cellHeights[i]; } const cellWidth = calculateCellSize(cell, cellWidths, columnIdx, true); const cellBottomRightX = cellTopLeftX + cellWidth; const cellHeight = calculateCellSize(cell, cellHeights, rowIdx, false); const cellBottomRightY = cellTopLeftY + cellHeight; return { ...cell, points: [ [cellTopLeftX, cellTopLeftY], [cellBottomRightX, cellBottomRightY] ] }; }); return cells; } function getCellWithPoints(board, table, cellId) { try { const cells = getCellsWithPoints(board, table); const cellIndex = cells && table.cells.findIndex(item => item.id === cellId); return cells[cellIndex]; } catch (error) { throw new Error('can not get table cell points'); } } function calculateCellsSize(items, tableSize, count, isWidth) { const cellSizes = []; const sizeType = isWidth ? 'width' : 'height'; // The remaining size of the table excluding cells with already set sizes. let totalSizeRemaining = tableSize; items.forEach((item, index) => { if (item[sizeType]) { cellSizes[index] = item[sizeType]; totalSizeRemaining -= item[sizeType]; } }); // Divide the remaining size equally. const remainingItemCount = count - cellSizes.filter(item => !!item).length; const remainingCellSize = remainingItemCount > 0 ? totalSizeRemaining / remainingItemCount : 0; for (let i = 0; i < count; i++) { if (!cellSizes[i]) { cellSizes[i] = remainingCellSize; } } return cellSizes; } function calculateCellSize(cell, sizes, index, isWidth) { const span = isWidth ? cell.colspan || 1 : cell.rowspan || 1; let size = 0; for (let i = 0; i < span; i++) { const cellIndex = index + i; size += sizes[cellIndex]; } return size; } function getHitCell(board, element, point) { const table = board.buildTable(element); const cells = getCellsWithPoints(board, table); const rectangle = RectangleClient.getRectangleByPoints([point, point]); const cell = cells.find(item => { const cellRectangle = RectangleClient.getRectangleByPoints(item.points); return RectangleClient.isHit(rectangle, cellRectangle); }); if (cell) { return table.cells.find(item => item.id === cell.id); } return null; } function editCell(board, cell) { const textManage = getTextManageByCell(board, cell); textManage && textManage.edit(); } function getTextManageByCell(board, cell) { return getTextManage(board, undefined, cell); } const updateColumns = (table, columnId, width, offset) => { const columns = table.columns.map(item => (item.id === columnId ? { ...item, width } : item)); const points = [table.points[0], [table.points[1][0] + offset, table.points[1][1]]]; return { columns, points }; }; const updateRows = (table, rowId, height, offset) => { const rows = table.rows.map(item => (item.id === rowId ? { ...item, height } : item)); const points = [table.points[0], [table.points[1][0], table.points[1][1] + offset]]; return { rows, points }; }; function updateCellIdsByRowOrColumn(cells, oldId, newId, type) { const id = `${type}Id`; cells.forEach(item => { if (item[id] === oldId) { item[id] = newId; } }); } function updateRowOrColumnIds(element, type) { element[`${type}s`].forEach(item => { const newId = idCreator(); updateCellIdsByRowOrColumn(element.cells, item.id, newId, type); item.id = newId; }); } function updateCellIds(cells) { cells.forEach(item => { const newId = idCreator(); item.id = newId; }); } function isCellIncludeText(cell) { return cell.text; } function getCellsRectangle(board, element, cells) { const cellsWithPoints = getCellsWithPoints(board, element); const points = cells.map(cell => { const cellWithPoints = cellsWithPoints.find(item => item.id === cell.id); return cellWithPoints.points; }); return RectangleClient.getRectangleByPoints(points); } const createCell = (rowId, columnId, text = null) => { const cell = { id: idCreator(), rowId, columnId }; if (text !== null) { cell['text'] = { children: [{ text }], align: Alignment.center }; } return cell; }; const getSelectedTableCellsEditor = (board) => { if (isSingleSelectTable(board)) { const elements = getSelectedTableElements(board); const selectedCells = getSelectedCells(elements[0]); const selectedCellsEditor = selectedCells?.map(cell => { const textManage = getTextManageByCell(board, cell); return textManage?.editor; }); if (selectedCellsEditor?.length) { return selectedCellsEditor; } } return undefined; }; const SHAPE_MAX_LENGTH = 6; const memorizedShape = new WeakMap(); const getMemorizeKey = (element) => { let key = ''; switch (true) { case PlaitDrawElement.isText(element): { key = MemorizeKey.text; break; } case PlaitDrawElement.isBasicShape(element): { key = MemorizeKey.basicShape; break; } case PlaitDrawElement.isFlowchart(element): { key = MemorizeKey.flowchart; break; } case PlaitDrawElement.isArrowLine(element): { key = MemorizeKey.arrowLine; break; } case PlaitDrawElement.isUML(element): { key = MemorizeKey.UML; } } return key; }; const getLineMemorizedLatest = () => { const properties = getMemorizedLatest(MemorizeKey.arrowLine); return { ...properties }; }; const getMemorizedLatestByPointer = (pointer) => { let memorizeKey = ''; if (PlaitDrawElement.isBasicShape({ shape: pointer })) { memorizeKey = pointer === BasicShapes.text ? MemorizeKey.text : MemorizeKey.basicShape; } else if (PlaitDrawElement.isUML({ shape: pointer })) { memorizeKey = MemorizeKey.UML; } else { memorizeKey = MemorizeKey.flowchart; } const properties = { ...getMemorizedLatest(memorizeKey) }; const textProperties = { ...properties.text }; delete properties.text; return { textProperties, geometryProperties: properties }; }; const memorizeLatestText = (element, operations) => { const memorizeKey = getMemorizeKey(element); let textMemory = getMemorizedLatest(memorizeKey)?.text || {}; const setNodeOperation = operations.find((operation) => operation.type === 'set_node'); if (setNodeOperation) { const { properties, newProperties } = setNodeOperation; for (const key in newProperties) { const value = newProperties[key]; if (value == null) { delete textMemory[key]; } else { textMemory[key] = value; } } for (const key in properties) { if (!newProperties.hasOwnProperty(key)) { delete textMemory[key]; } } memorizeLatest(memorizeKey, 'text', textMemory); } }; const memorizeLatestShape = (board, shape) => { const shapes = memorizedShape.has(board) ? memorizedShape.get(board) : []; const shapeIndex = shapes.indexOf(shape); if (shape === BasicShapes.text || shapeIndex === 0) { return; } if (shapeIndex !== -1) { shapes.splice(shapeIndex, 1); } else { if (shapes.length === SHAPE_MAX_LENGTH) { shapes.pop(); } } shapes.unshift(shape); memorizedShape.set(board, shapes); }; const getMemorizedLatestShape = (board) => { return memorizedShape.get(board); }; const debugKey$4 = 'debug:plait:line-mirror'; const debugGenerator$6 = createDebugGenerator(debugKey$4); const alignPoint = (basePoint, movingPoint) => { const newPoint = [...movingPoint]; if (Point.isVertical(newPoint, basePoint, LINE_ALIGN_TOLERANCE)) { newPoint[0] = basePoint[0]; } if (Point.isHorizontal(newPoint, basePoint, LINE_ALIGN_TOLERANCE)) { newPoint[1] = basePoint[1]; } return newPoint; }; const alignPoints = (basePoints, movingPoint, targetIndex) => { let newMovingPoint = [...movingPoint]; basePoints.forEach((basePoint, index) => { if (index === targetIndex) { return; } newMovingPoint = alignPoint(basePoint, newMovingPoint); }); return newMovingPoint; }; function getResizedPreviousAndNextPoint(nextRenderPoints, sourcePoint, targetPoint, handleIndex) { const referencePoint = { previous: null, next: null }; const startPoint = nextRenderPoints[handleIndex]; const endPoint = nextRenderPoints[handleIndex + 1]; const isHorizontal = Point.isHorizontal(startPoint, endPoint); const isVertical = Point.isVertical(startPoint, endPoint); const previousPoint = nextRenderPoints[handleIndex - 1] ?? nextRenderPoints[0]; const beforePreviousPoint = nextRenderPoints[handleIndex - 2] ?? sourcePoint; if ((isHorizontal && Point.isHorizontal(beforePreviousPoint, previousPoint)) || (isVertical && Point.isVertical(beforePreviousPoint, previousPoint))) { referencePoint.previous = previousPoint; } const nextPoint = nextRenderPoints[handleIndex + 2] ?? nextRenderPoints[nextRenderPoints.length - 1]; const afterNextPoint = nextRenderPoints[handleIndex + 3] ?? targetPoint; if ((isHorizontal && Point.isHorizontal(nextPoint, afterNextPoint)) || (isVertical && Point.isVertical(nextPoint, afterNextPoint))) { referencePoint.next = nextPoint; } return referencePoint; } function alignElbowSegment(startKeyPoint, endKeyPoint, resizeState, resizedPreviousAndNextPoint) { let newStartPoint = startKeyPoint; let newEndPoint = endKeyPoint; if (Point.isHorizontal(startKeyPoint, endKeyPoint)) { const offsetY = Point.getOffsetY(resizeState.startPoint, resizeState.endPoint); let pointY = startKeyPoint[1] + offsetY; if (resizedPreviousAndNextPoint.previous && Math.abs(resizedPreviousAndNextPoint.previous[1] - pointY) < LINE_ALIGN_TOLERANCE) { pointY = resizedPreviousAndNextPoint.previous[1]; } else if (resizedPreviousAndNextPoint.next && Math.abs(resizedPreviousAndNextPoint.next[1] - pointY) < LINE_ALIGN_TOLERANCE) { pointY = resizedPreviousAndNextPoint.next[1]; } newStartPoint = [startKeyPoint[0], pointY]; newEndPoint = [endKeyPoint[0], pointY]; } if (Point.isVertical(startKeyPoint, endKeyPoint)) { const offsetX = Point.getOffsetX(resizeState.startPoint, resizeState.endPoint); let pointX = startKeyPoint[0] + offsetX; if (resizedPreviousAndNextPoint.previous && Math.abs(resizedPreviousAndNextPoint.previous[0] - pointX) < LINE_ALIGN_TOLERANCE) { pointX = resizedPreviousAndNextPoint.previous[0]; } else if (resizedPreviousAndNextPoint.next && Math.abs(resizedPreviousAndNextPoint.next[0] - pointX) < LINE_ALIGN_TOLERANCE) { pointX = resizedPreviousAndNextPoint.next[0]; } newStartPoint = [pointX, startKeyPoint[1]]; newEndPoint = [pointX, endKeyPoint[1]]; } return [newStartPoint, newEndPoint]; } function getIndexAndDeleteCountByKeyPoint(board, element, dataPoints, nextRenderPoints, handleIndex) { let index = null; let deleteCount = null; const startKeyPoint = nextRenderPoints[handleIndex]; const endKeyPoint = nextRenderPoints[handleIndex + 1]; if (!startKeyPoint || !endKeyPoint) { return { index, deleteCount }; } const midDataPoints = dataPoints.slice(1, -1); const startIndex = midDataPoints.findIndex((item) => Point.isEquals(item, startKeyPoint)); const endIndex = midDataPoints.findIndex((item) => Point.isEquals(item, endKeyPoint)); if (Math.max(startIndex, endIndex) > -1) { if (startIndex > -1 && endIndex > -1) { return { index: startIndex, deleteCount: 2 }; } if (startIndex > -1 && endIndex === -1) { const isReplace = startIndex < midDataPoints.length - 1 && Point.isAlign([midDataPoints[startIndex], midDataPoints[startIndex + 1], startKeyPoint, endKeyPoint]); if (isReplace) { return { index: startIndex, deleteCount: 2 }; } return { index: startIndex, deleteCount: 1 }; } if (startIndex === -1 && endIndex > -1) { const isReplace = endIndex > 0 && Point.isAlign([midDataPoints[endIndex], midDataPoints[endIndex - 1], startKeyPoint, endKeyPoint]); if (isReplace) { return { index: endIndex - 1, deleteCount: 2 }; } return { index: endIndex, deleteCount: 1 }; } } else { for (let i = 0; i < midDataPoints.length - 1; i++) { const currentPoint = midDataPoints[i]; const nextPoint = midDataPoints[i + 1]; if (Point.isAlign([currentPoint, nextPoint, startKeyPoint, endKeyPoint])) { index = i; deleteCount = 2; break; } if (Point.isAlign([currentPoint, nextPoint, startKeyPoint])) { index = Math.min(i + 1, midDataPoints.length - 1); deleteCount = 1; break; } if (Point.isAlign([currentPoint, nextPoint, endKeyPoint])) { index = Math.max(i - 1, 0); deleteCount = 1; break; } } } if (index === null) { deleteCount = 0; if (midDataPoints.length > 0) { const handleRefPair = getArrowLineHandleRefPair(board, element); const params = getElbowLineRouteOptions(board, element, handleRefPair); const keyPoints = removeDuplicatePoints(generateElbowLineRoute(params, board)); const nextKeyPoints = simplifyOrthogonalPoints(keyPoints.slice(1, keyPoints.length - 1)); const nextDataPoints = [nextRenderPoints[0], ...midDataPoints, nextRenderPoints[nextRenderPoints.length - 1]]; const mirrorDataPoints = getMirrorDataPoints(board, nextDataPoints, nextKeyPoints, params); for (let i = handleIndex - 1; i >= 0; i--) { const previousIndex = mirrorDataPoints.slice(1, -1).findIndex((item) => Point.isEquals(item, nextRenderPoints[i])); if (previousIndex > -1) { index = previousIndex + 1; break; } } if (index === null) { index = 0; // When renderPoints is a straight line and dataPoints are not on the line, // the default 'deleteCount' is set to midDataPoints.length. if (Point.isAlign(nextRenderPoints)) { deleteCount = midDataPoints.length; } } } else { index = 0; } } return { index, deleteCount }; } function getMirrorDataPoints(board, nextDataPoints, nextKeyPoints, params) { for (let index = 1; index < nextDataPoints.length - 2; index++) { adjustByCustomPointStartIndex(board, index, nextDataPoints, nextKeyPoints, params); } return nextDataPoints; } /** * adjust based parallel segment */ const adjustByCustomPointStartIndex = (board, customPointStartIndex, nextDataPoints, nextKeyPoints, params) => { const beforePoint = nextDataPoints[customPointStartIndex - 1]; const startPoint = nextDataPoints[customPointStartIndex]; const endPoint = nextDataPoints[customPointStartIndex + 1]; const afterPoint = nextDataPoints[customPointStartIndex + 2]; const beforeSegment = [beforePoint, startPoint]; const afterSegment = [endPoint, afterPoint]; const isStraightWithBefore = Point.isAlign(beforeSegment); const isStraightWithAfter = Point.isAlign(afterSegment); let isAdjustStart = false; let isAdjustEnd = false; if (!isStraightWithBefore || !isStraightWithAfter) { const midKeyPointsWithBefore = getMidKeyPoints(nextKeyPoints, beforeSegment[0], beforeSegment[1]); const midKeyPointsWithAfter = getMidKeyPoints(nextKeyPoints, afterSegment[0], afterSegment[1]); const hasMidKeyPoints = midKeyPointsWithBefore.length > 0 && midKeyPointsWithAfter.length > 0; isAdjustStart = !isStraightWithBefore && !hasMidKeyPoints; isAdjustEnd = !isStraightWithAfter && !hasMidKeyPoints; } if (isAdjustStart || isAdjustEnd) { const parallelSegment = [startPoint, endPoint]; const parallelSegments = findOrthogonalParallelSegments(parallelSegment, nextKeyPoints); const mirrorSegments = findMirrorSegments(board, parallelSegment, parallelSegments, params.sourceRectangle, params.targetRectangle); if (mirrorSegments.length === 1) { const mirrorSegment = mirrorSegments[0]; if (isAdjustStart) { nextDataPoints.splice(customPointStartIndex, 1, mirrorSegment[0]); } if (isAdjustEnd) { nextDataPoints.splice(customPointStartIndex + 1, 1, mirrorSegment[1]); } } else { const isHorizontal = Point.isHorizontal(startPoint, endPoint); const adjustIndex = isHorizontal ? 0 : 1; if (isAdjustStart) { const newStartPoint = [startPoint[0], startPoint[1]]; newStartPoint[adjustIndex] = beforePoint[adjustIndex]; nextDataPoints.splice(customPointStartIndex, 1, newStartPoint); } if (isAdjustEnd) { const newEndPoint = [endPoint[0], endPoint[1]]; newEndPoint[adjustIndex] = afterPoint[adjustIndex]; nextDataPoints.splice(customPointStartIndex + 1, 1, newEndPoint); } } } }; function isUpdatedHandleIndex(board, element, dataPoints, nextRenderPoints, handleIndex) { const { deleteCount } = getIndexAndDeleteCountByKeyPoint(board, element, dataPoints, nextRenderPoints, handleIndex); if (deleteCount !== null && deleteCount > 1) { return true; } return false; } function getMidKeyPoints(simplifiedNextKeyPoints, startPoint, endPoint) { let midElbowPoints = []; let startPointIndex = -1; let endPointIndex = -1; for (let i = 0; i < simplifiedNextKeyPoints.length; i++) { if (Point.isAlign([simplifiedNextKeyPoints[i], startPoint])) { startPointIndex = i; } if (startPointIndex > -1 && Point.isAlign([simplifiedNextKeyPoints[i], endPoint])) { endPointIndex = i; break; } } if (startPointIndex > -1 && endPointIndex > -1) { midElbowPoints = simplifiedNextKeyPoints.slice(startPointIndex, endPointIndex + 1); } return midElbowPoints; } function findOrthogonalParallelSegments(segment, keyPoints) { const isHorizontalSegment = Point.isHorizontal(segment[0], segment[1]); const parallelSegments = []; for (let i = 0; i < keyPoints.length - 1; i++) { const current = keyPoints[i]; const next = keyPoints[i + 1]; const isHorizontal = Point.isHorizontal(current, next, 0.1); if (isHorizontalSegment && isHorizontal) { parallelSegments.push([current, next]); } if (!isHorizontalSegment && !isHorizontal) { parallelSegments.push([current, next]); } } return parallelSegments; } function findMirrorSegments(board, segment, parallelSegments, sourceRectangle, targetRectangle) { debugGenerator$6.isDebug() && debugGenerator$6.clear(); const mirrorSegments = []; for (let index = 0; index < parallelSegments.length; index++) { const parallelPath = parallelSegments[index]; const startPoint = [segment[0][0], segment[0][1]]; const endPoint = [segment[1][0], segment[1][1]]; const isHorizontal = Point.isHorizontal(startPoint, endPoint); const adjustDataIndex = isHorizontal ? 0 : 1; startPoint[adjustDataIndex] = parallelPath[0][adjustDataIndex]; endPoint[adjustDataIndex] = parallelPath[1][adjustDataIndex]; const fakeRectangle = RectangleClient.getRectangleByPoints([startPoint, endPoint, ...parallelPath]); const isValid = !RectangleClient.isHit(fakeRectangle, sourceRectangle) && !RectangleClient.isHit(fakeRectangle, targetRectangle); if (isValid) { mirrorSegments.push([startPoint, endPoint]); debugGenerator$6.isDebug() && debugGenerator$6.drawPolygon(board, RectangleClient.getCornerPoints(fakeRectangle)); } } return mirrorSegments; } const hasIllegalElbowPoint = (midDataPoints) => { if (midDataPoints.length === 1) { return true; } return midDataPoints.some((item, index) => { const beforePoint = midDataPoints[index - 1]; const afterPoint = midDataPoints[index + 1]; const beforeSegment = beforePoint && [beforePoint, item]; const afterSegment = afterPoint && [item, afterPoint]; const isStraightWithBefore = beforeSegment && Point.isAlign(beforeSegment); const isStraightWithAfter = afterSegment && Point.isAlign(afterSegment); if (index === 0) { return !isStraightWithAfter; } if (index === midDataPoints.length - 1) { return !isStraightWithBefore; } return !isStraightWithBefore && !isStraightWithAfter; }); }; const ARROW_LENGTH = 20; const drawArrowLineArrow = (element, points, options) => { const arrowG = createG(); if (PlaitArrowLine.isSourceMark(element, ArrowLineMarkerType.none) && PlaitArrowLine.isTargetMark(element, ArrowLineMarkerType.none)) { return null; } const strokeWidth = getStrokeWidthByElement(element); const offset = (strokeWidth * strokeWidth) / 3; if (points.length === 1) { points = [points[0], [points[0][0] + 0.1, points[0][1]]]; } if (!PlaitArrowLine.isSourceMark(element, ArrowLineMarkerType.none)) { const source = getExtendPoint(points[0], points[1], ARROW_LENGTH + offset); const sourceArrow = getArrow(element, { marker: element.source.marker, source, target: points[0], isSource: true }, options); sourceArrow && arrowG.appendChild(sourceArrow); } if (!PlaitArrowLine.isTargetMark(element, ArrowLineMarkerType.none)) { const source = getExtendPoint(points[points.length - 1], points[points.length - 2], ARROW_LENGTH + offset); const arrow = getArrow(element, { marker: element.target.marker, source, target: points[points.length - 1], isSource: false }, options); arrow && arrowG.appendChild(arrow); } return arrowG; }; const getArrow = (element, arrowOptions, options) => { const { marker, target, source, isSource } = arrowOptions; let targetArrow; switch (marker) { case ArrowLineMarkerType.openTriangle: { targetArrow = drawOpenTriangle(element, source, target, options); break; } case ArrowLineMarkerType.solidTriangle: { targetArrow = drawSolidTriangle(source, target, options); break; } case ArrowLineMarkerType.arrow: { targetArrow = drawArrow(element, source, target, options); break; } case ArrowLineMarkerType.sharpArrow: { targetArrow = d