diff --git a/docs/en-US/group.md b/docs/en-US/group.md index b22ae616..7f4c0bb4 100644 --- a/docs/en-US/group.md +++ b/docs/en-US/group.md @@ -17,6 +17,8 @@ canvas.draw({ top: 100, left: 100, Class: AGroup // after setting the base class, the canvas will render based on the custom class. + ... + //the attribute below }], nodes: ... edges: ... @@ -28,119 +30,221 @@ canvas.addGroup({ // the attribute below }); ``` +
+ +**`The returned dom of the node must be set to position: absolute;`** + +
+
+ +## attribute + +### id _``_ (Require) +  unique id of node +### top _``_ (Require) +  y coordinate +### left _``_ (Require) +  x coordinate +### width _``_ (Option) +  group width +### height _``_ (Option) +  group height +### endpoints _``_ (Option) +  system endpoints configuration: system endpoints will be added when this configuration is present +### Class _``_ (Option) +  extended class +### scope _``_ (Option) +  scope: When the scope of the node is the same as the scope of the group, it can be added to the group. You can join as you like without setting it by default -## attribute: +```js +// single scope +group.scope = 'xxx'; +// multiple scope, any one matched can be connected +group.scope = 'xxx1 xxx2 xxx3'; +``` + +### draggable _``_ (Option) +  the group is draggable. the default value is true +### resize _``_ (Option) +  the size of the group is resizable. the default value is true -| key | describe | type | default -| :------ | :------ | :------ | :------ -| id | unique id | string (Require) | - -| top | y coordinate | number (Require) | - -| left | x coordinate | number (Require) | - -| width | group width | number (Option) | - -| height | group height | number (Option) | - -| type | group type | string (Option) | normal (drag in and drag out), inner (can only be dragged in and not out) -| endpoints | endpoint data | array (Option) | - -| Class | extended class | Class (Option) | When the extended class is passed in, the node group will be rendered according to the draw method of the extended class, and the related methods of the extended class will also override the method of the parent class. -| scope | scope | boolean (Option) | When the scope of the node is consistent with the scope of the group, it can be added to the group. You can join as you like without setting it by default. + -`* The returned dom of the node must be set to position: absolute;` +### group _``_ (Option) +  the id of the parent group: For supporting group nesting, you need to set 'canvas.theme.group.includeGroups' open + + -## API: +
+
-### custom group: +## Extented Class API: + +```js +import {Group} from 'butterfly-dag'; + +Class YourGroup extends Group { + + /** + * callback after group mount + */ + mount() {} + + /** + * group draw function + * @param {obj} data - group data + * @return {dom} - group dom + */ + draw(obj) {} +} +``` + +
+
+ +## External Call API: + +### group.getWidth() + +*description*: get group width + +*return* + +* `number` the width of the group ```js -/** - * group draw function - * @param {obj} data - group data - * @return {dom} - group dom - */ -draw = (obj) => {} - -/** - * callback after group mount - */ -mounted = () => {} - -/** - * @return {number} - get group width - */ getWidth = () => {} +``` + +### group.getHeight () + +*description*: get group height + +*return* -/** - * @return {number} - get group height - */ +* `number` the height of the group + +```js getHeight = () => {} ``` -### add and delete members: +### group.addNode (node) + +*description*: add node to the group + +*param* + +* `{obj} node` node data ```js -/** - * add node to group - * @param {obj} node - node data - */ addNode = (node) => {} +``` + +### group.addNodes (nodes) + +*description*: add multiple nodes to the group -/** - * add multiple nodes to group - * @param {array} nodes - nodes array - */ +*param* + +* `{array} nodes`nodes array + +```js addNodes = (nodes) => {} +``` + +### group.removeNode (node) + +*description*: delete node from the group + +*param* + +* `{obj} node`node data -/** - * delete node from group - * @param {obj} node - node data - */ +```js removeNode = (node) => {} +``` + +### group.removeNodes (nodes) + +*description*: delete nodes from the group -/** - * group删除节点 - * @param {array} nodes - 节点数组 - */ +*param* + +* `{obj} nodes`nodes array + +```js removeNodes = (nodes) => {} ``` -### custom endpoint: +### group.addEndpoint (obj) + +*description*: add endpoint to the group + +*params* + +* `{obj} param` endpoint data (this method must be executed after the node is mounted) +* `{string} param.id` endpoint id +* `{string} param.orientation` endpoint direction (it can control the direction of the edge linkin or linkout) +* `{string} param.scope` scope +* `{string} param.type` 'source' / 'target' / undefined,ednpoint is both source and target when undefined +* `{string} param.dom` any sub DOM in the node can be used as a custom endpoint ```js -/** - * @param {obj} data - endpoint data (this method must be executed after the node is mounted) - * @param {string} param.id - endpoint id - * @param {string} param.orientation - endpoint direction (it can control the direction of the edge linkin or linkout) - * @param {string} param.scope - scope - * @param {string} param.type - 'source' / 'target' / undefined,ednpoint is both source and target when undefined - * @param {string} param.dom - any sub DOM in the node can be used as a custom endpoint - */ addEndpoint = (obj) => {} +``` + +### group.getEndpoint (id) + +*description*: get endpoint by id + +*param* + +* `{string} pointId` endpoint id + +*return* -/** - * @param {string} pointId - endpoint id - * @param {string(Option)} type - endpoint type (Optional) - * @return {Endpoint} - Endpoint object - */ -getEndpoint = (id, type) => {} +* `{Endpoint}`Endpoint Object + +```js +getEndpoint = (id) => {} ``` -### move: +### group.moveTo (obj) + +*description*: move coordinates of the group + +*params* + +* `{number} obj.x `move to x coordinate +* `{number} obj.y `move to y coordinate + ```js -/** - * @param {number} x - move to x - * @param {number} y - move to y - */ moveTo = (obj) => {} ``` -### event: +### group.emit (event, data) + +*description*: emit events,canvas or any elements can receive event from the group + +*params* + +* `{string} event `emit event name +* `{number} data `emit event data ```js -/** - * emit events - */ emit = (string, obj) => {} +``` + +### group.on (string, callback) + +*description*: receive events, the group can receive events from canvas or any elements -/** - * accept events - */ +*params* + +* `{string} event ` receive event name +* `{function} data `receive event callback + +```js on = (string, callback) => {} ``` + diff --git a/src/utils/link/edgeTypes/_utils.js b/src/utils/link/edgeTypes/_utils.js index b79d7c7e..5db17c9e 100644 --- a/src/utils/link/edgeTypes/_utils.js +++ b/src/utils/link/edgeTypes/_utils.js @@ -1,4 +1,5 @@ 'use strict'; +<<<<<<< HEAD import _ from 'lodash'; const MINDIST = 20; @@ -311,4 +312,7 @@ export function _findManhattanPoint (points, pos) { } } return result; -} \ No newline at end of file +} +======= + +>>>>>>> 44fc7b5... chore: optimize link code diff --git a/src/utils/link/edgeTypes/bezier.js b/src/utils/link/edgeTypes/bezier.js index b0da78ee..cba1070a 100644 --- a/src/utils/link/edgeTypes/bezier.js +++ b/src/utils/link/edgeTypes/bezier.js @@ -1,3 +1,4 @@ +<<<<<<< HEAD 'use strict'; import {_findControlPoint, _calcOrientation} from './_utils.js'; @@ -44,3 +45,6 @@ function drawBezier(sourcePoint, targetPoint) { } export default drawBezier; +======= +'use strict'; +>>>>>>> 44fc7b5... chore: optimize link code diff --git a/src/utils/link/index.js b/src/utils/link/index.js index 99ca4b1c..2c465bf6 100644 --- a/src/utils/link/index.js +++ b/src/utils/link/index.js @@ -1,5 +1,6 @@ 'use strict'; +<<<<<<< HEAD import {_findManhattanPoint} from './edgeTypes/_utils.js'; export {default as drawAdvancedBezier} from './edgeTypes/advancedBezier.js' @@ -8,4 +9,417 @@ export {default as drawStraight} from './edgeTypes/straight.js'; export {default as drawFlow} from './edgeTypes/flow.js'; export {default as drawManhattan} from './edgeTypes/manhattan.js'; -export {_findManhattanPoint as findManhattanPoint}; \ No newline at end of file +export {_findManhattanPoint as findManhattanPoint}; +======= +import _ from 'lodash'; +import {_calcOrientation} from './_utils.js'; + +function _drawFlowSegment(segments, offset) { + let current = null; + let next = null; + let cornerRadius = 0; // 拐角角度,按道理可以配置 + let lw = 1; // strokeWidth,线条的粗细 + let _segLength = (s) => { + return Math.sqrt(Math.pow(s[0] - s[2], 2) + Math.pow(s[1] - s[3], 2)); + }; + let result = []; + let _drawStraight = (d) => { + return ['M', d.x1 + offset.x, d.y1 + offset.y, 'L', d.x2 + offset.x, d.y2 + offset.y]; + }; + let _drawArc = (d) => { + + // 有重复,可抽象,计算象限 + let _quadrant = (p1, p2) => { + if (p2[0] > p1[0]) { + return (p2[1] > p1[1]) ? 2 : 1; + } else if (p2[0] === p1[0]) { + return p2[1] > p1[1] ? 2 : 1; + } else { + return (p2[1] > p1[1]) ? 3 : 4; + } + }; + // 计算两点之间的直线的梯度 + let _gradient = (p1, p2) => { + if (p2[0] === p1[0]) { + return p2[1] > p1[0] ? Infinity : -Infinity; + } else if (p2[1] === p1[1]) { + return p2[0] > p1[0] ? 0 : -0; + } else { + return (p2[1] - p1[1]) / (p2[0] - p1[0]); + } + }; + // Calculates the angle between the two points + let _theta = (p1, p2) => { + let m = _gradient(p1, p2); + let t = Math.atan(m); + let s = _quadrant(p1, p2); + if ((s === 4 || s === 3)) { + t += Math.PI; + } + if (t < 0) { + t += (2 * Math.PI); + } + return t; + }; + + let _calcAngle = function (x, y) { + return _theta([d.cx, d.cy], [x, y]); + }; + + let startAngle = _calcAngle(x1, y1); + let endAngle = _calcAngle(x2, y2); + let TWO_PI = 2 * Math.PI; + if (endAngle < 0) { + endAngle += TWO_PI; + } + if (startAngle < 0) { + startAngle += TWO_PI; + } + // we now have startAngle and endAngle as positive numbers, meaning the + // absolute difference (|d|) between them is the sweep (s) of this arc, unless the + // arc is 'anticlockwise' in which case 's' is given by 2PI - |d|. + let ea = endAngle < startAngle ? endAngle + TWO_PI : endAngle; + let sweep = Math.abs(ea - startAngle); + let anticlockwise = d.ac; + + if (anticlockwise) { + sweep = TWO_PI - sweep; + } + + return ['M', d.x1 + offset.x, d.y1 + offset.y, 'A', d.r, d.r, '0', ',', d.x2 + offset.x, d.y2 + offset.y]; + }; + + // let offsetX = sourcePoint.pos[0] < targetPoint.pos[0] ? sourcePoint.pos[0] : targetPoint.pos[0]; + // let offsetY = sourcePoint.pos[1] < targetPoint.pos[1] ? sourcePoint.pos[1] : targetPoint.pos[1]; + + for (let i = 0; i < segments.length - 1; i++) { + current = current || _.cloneDeep(segments[i]); + next = _.cloneDeep(segments[i + 1]); + if (cornerRadius > 0 && current[4] !== next[4]) { + let radiusToUse = Math.min(cornerRadius, _segLength(current), _segLength(next)); + // right angle. adjust current segment's end point, and next segment's start point. + current[2] -= current[5] * radiusToUse; + current[3] -= current[6] * radiusToUse; + next[0] += next[5] * radiusToUse; + next[1] += next[6] * radiusToUse; + let ac = (current[6] === next[5] && next[5] === 1) || + ((current[6] === next[5] && next[5] === 0) && current[5] !== next[6]) || + (current[6] === next[5] && next[5] === -1); + let sgny = next[1] > current[3] ? 1 : -1; + let sgnx = next[0] > current[2] ? 1 : -1; + let sgnEqual = sgny === sgnx; + let cx = (sgnEqual && ac || (!sgnEqual && !ac)) ? next[0] : current[2]; + let cy = (sgnEqual && ac || (!sgnEqual && !ac)) ? current[3] : next[1]; + + let _line1 = _drawStraight({ + x1: current[0], + y1: current[1], + x2: current[2], + y2: current[3] + }); + let _line2 = _drawArc({ + r: radiusToUse, + x1: current[2], + y1: current[3], + x2: next[0], + y2: next[1], + cx: cx, + cy: cy, + ac: ac + }); + result = result.concat(_line1); + result = result.concat(_line2); + } else { + // dx + dy are used to adjust for line width. + let dx = (current[2] === current[0]) ? 0 : (current[2] > current[0]) ? (lw / 2) : -(lw / 2); + let dy = (current[3] === current[1]) ? 0 : (current[3] > current[1]) ? (lw / 2) : -(lw / 2); + let _line = _drawStraight({ + x1: current[0] - dx, + y1: current[1] - dy, + x2: current[2] + dx, + y2: current[3] + dy + }); + result = result.concat(_line); + } + current = next; + } + if (next !== null) { + let _line = _drawStraight({ + x1: next[0], + y1: next[1], + x2: next[2], + y2: next[3] + }); + result = result.concat(_line); + } + return result.join(' '); +} + +function drawFlow(sourcePoint, targetPoint, orientationLimit) { + if (!sourcePoint.orientation) { + sourcePoint.orientation = _calcOrientation(targetPoint.pos[0], targetPoint.pos[1], sourcePoint.pos[0], sourcePoint.pos[1], orientationLimit); + } + + if (!targetPoint.orientation) { + targetPoint.orientation = _calcOrientation(sourcePoint.pos[0], sourcePoint.pos[1], targetPoint.pos[0], targetPoint.pos[1], orientationLimit); + } + + let stub = 30; // 每部分折线的最小长度 + let midpoint = 0.5; // 折线中点 + let w = Math.abs(sourcePoint.pos[0] - targetPoint.pos[0]); + let h = Math.abs(sourcePoint.pos[1] - targetPoint.pos[1]); + let sx = targetPoint.pos[0] < sourcePoint.pos[0] ? w : 0; + let sy = targetPoint.pos[1] < sourcePoint.pos[1] ? h : 0; + let tx = targetPoint.pos[0] < sourcePoint.pos[0] ? 0 : w; + let ty = targetPoint.pos[1] < sourcePoint.pos[1] ? 0 : h; + let offsetX = sourcePoint.pos[0] < targetPoint.pos[0] ? sourcePoint.pos[0] : targetPoint.pos[0]; + let offsetY = sourcePoint.pos[1] < targetPoint.pos[1] ? sourcePoint.pos[1] : targetPoint.pos[1]; + // 小心有可能so[0]和to[0]同时为0;或者是so[1]和to[1]同时为0 + let so = sourcePoint.orientation; + let to = targetPoint.orientation; + // 拿来判断是对面,垂直还是正交 + let oProduct = ((so[0] * to[0]) + (so[1] * to[1])); + + let sourceAxis = so[0] === 0 ? 'y' : 'x'; + + let startStubX = sx + (so[0] * stub); + let startStubY = sy + (so[1] * stub); + let endStubX = tx + (to[0] * stub); + let endStubY = ty + (to[1] * stub); + + let isXGreaterThanStubTimes2 = Math.abs(sx - tx) > (stub + stub); + let isYGreaterThanStubTimes2 = Math.abs(sy - ty) > (stub + stub); + + // 判断方向 + let anchorOrientation = null; + if (oProduct === -1) { + anchorOrientation = 'opposite'; + } else if (oProduct === 0) { + anchorOrientation = 'perpendicular'; + } else if (oProduct === 1) { + anchorOrientation = 'orthogonal'; + } + + // 计算折线的方法 + let _commonStubCalculator = () => { + return [startStubX, startStubY, endStubX, endStubY]; + }; + let stubCalculators = { + perpendicular: _commonStubCalculator, + orthogonal: _commonStubCalculator, + opposite: (axis) => { + let idx = axis === 'x' ? 0 : 1; + let areInProximity = { + x: () => { + return ( (so[idx] === 1 && ( + ( (startStubX > endStubX) && (tx > startStubX) ) || + ( (sx > endStubX) && (tx > sx))))) || + ( (so[idx] === -1 && ( + ( (startStubX < endStubX) && (tx < startStubX) ) || + ( (sx < endStubX) && (tx < sx))))); + }, + y: () => { + return ( (so[idx] === 1 && ( + ( (startStubY > endStubY) && (ty > startStubY) ) || + ( (sy > endStubY) && (ty > sy))))) || + + ( (so[idx] === -1 && ( + ( (startStubY < endStubY) && (ty < startStubY) ) || + ( (sy < endStubY) && (ty < sy))))); + } + }; + + // 判断是否需要折线 + if (areInProximity[axis]()) { // 这判断可以设置总是有折线 + return { + x: [(sx + tx) / 2, startStubY, (sx + tx) / 2, endStubY], + y: [startStubX, (sy + ty) / 2, endStubX, (sy + ty) / 2] + }[axis]; + } else { + return [startStubX, startStubY, endStubX, endStubY]; + } + } + }; + + // 加工线条处理的方法 + let lastx = null; + let lasty = null; + let segments = []; + let _sgn = (n) => { + return n < 0 ? -1 : n === 0 ? 0 : 1; + }; + let _addSegment = (x, y) => { + if (lastx === x && lasty === y) { + return; + } + let lx = lastx == null ? sx : lastx; + let ly = lasty == null ? sy : lasty; + let o = lx === x ? 'v': 'h'; + let sgnx = _sgn(x - lx); + let sgny = _sgn(y - ly); + + lastx = x; + lasty = y; + + segments.push([lx, ly, x, y, o, sgnx, sgny]); + }; + + // 开始实现 + let stubs = stubCalculators[anchorOrientation](sourceAxis); + let idx = sourceAxis === 'x' ? 0 : 1; + let oidx = sourceAxis === 'x' ? 0 : 1; + let ss = stubs[idx]; + let oss = stubs[oidx]; + let es = stubs[idx + 2]; + let oes = stubs[oidx + 2]; + + _addSegment(stubs[0], stubs[1]); + + let midx = startStubX + (endStubX - startStubX) * midpoint; + let midy = startStubY + (endStubY - startStubY) * midpoint; + + let orientations = { x: [ 0, 1 ], y: [ 1, 0 ] }; + let _lineCalculators = { + perpendicular: (axis) => { + let sis = { + x: [ + [[1, 2, 3, 4], null, [2, 1, 4, 3]], + null, + [[4, 3, 2, 1], null, [3, 4, 1, 2]] + ], + y: [ + [[3, 2, 1, 4], null, [2, 3, 4, 1]], + null, + [[4, 1, 2, 3], null, [1, 4, 3, 2]] + ] + }; + let stubs = { + x: [[startStubX, endStubX], null, [endStubX, startStubX]], + y: [[startStubY, endStubY], null, [endStubY, startStubY]] + }; + let midLines = { + x: [[midx, startStubY], [midx, endStubY]], + y: [[startStubX, midy], [endStubX, midy]] + }; + let linesToEnd = { + x: [[endStubX, startStubY]], + y: [[startStubX, endStubY]] + }; + let startToEnd = { + x: [[startStubX, endStubY], [endStubX, endStubY]], + y: [[endStubX, startStubY], [endStubX, endStubY]] + }; + let startToMidToEnd = { + x: [[startStubX, midy], [endStubX, midy], [endStubX, endStubY]], + y: [[midx, startStubY], [midx, endStubY], [endStubX, endStubY]] + }; + let otherStubs = { + x: [startStubY, endStubY], + y: [startStubX, endStubX] + }; + let soIdx = orientations[axis][0]; + let toIdx = orientations[axis][1]; + let _so = so[soIdx] + 1; + let _to = to[toIdx] + 1; + let otherFlipped = (to[toIdx] === -1 && (otherStubs[axis][1] < otherStubs[axis][0])) || (to[toIdx] === 1 && (otherStubs[axis][1] > otherStubs[axis][0])); + let stub1 = stubs[axis][_so][0]; + let stub2 = stubs[axis][_so][1]; + let segmentIndexes = sis[axis][_so][_to]; + + // 计算一下节点象限 + let _quadrant = (p1, p2) => { + if (p2.pos[0] > p1.pos[0]) { + return (p2.pos[1] > p1.pos[1]) ? 2 : 1; + } else if (p2.pos[0] == p1.pos[0]) { + return p2.pos[1] > p1.pos[1] ? 2 : 1; + } else { + return (p2.pos[1] > p1.pos[1]) ? 3 : 4; + } + }; + var segment = _quadrant(sourcePoint, targetPoint); + if (segment === segmentIndexes[3] || (segment === segmentIndexes[2] && otherFlipped)) { + return midLines[axis]; + } else if (segment === segmentIndexes[2] && stub2 < stub1) { + return linesToEnd[axis]; + } else if ((segment === segmentIndexes[2] && stub2 >= stub1) || (segment === segmentIndexes[1] && !otherFlipped)) { + return startToMidToEnd[axis]; + } else if (segment === segmentIndexes[0] || (segment === segmentIndexes[1] && otherFlipped)) { + return startToEnd[axis]; + } + }, + orthogonal: (axis, startStub, otherStartStub, endStub, otherEndStub) => { + let extent = { + x: so[0] === -1 ? Math.min(startStub, endStub) : Math.max(startStub, endStub), + y: so[1] === -1 ? Math.min(startStub, endStub) : Math.max(startStub, endStub) + }[axis]; + + return { + x: [ + [extent, otherStartStub], + [extent, otherEndStub], + [endStub, otherEndStub] + ], + y: [ + [otherStartStub, extent], + [otherEndStub, extent], + [otherEndStub, endStub] + ] + }[axis]; + }, + opposite: (axis, ss, oss, es) => { + let otherAxis = {x: 'y', y: 'x'}[axis]; + let dim = {x: 'height', y: 'width'}[axis]; + let comparator = axis === 'x' ? isXGreaterThanStubTimes2 : isYGreaterThanStubTimes2; + + // 考虑下自连的情况, 现在很不严禁 + if (sourcePoint.pos[0] === targetPoint.pos[0] && sourcePoint.pos[1] === targetPoint.pos[1]) { + // + } else if (!comparator || (so[idx] === 1 && ss > es) || (so[idx] === -1 && ss < es)) { + return { + x: [ + [ss, midy], + [es, midy] + ], + y: [ + [midx, ss], + [midx, es] + ] + }[axis]; + } else if ((so[idx] === 1 && ss < es) || (so[idx] === -1 && ss > es)) { + return { + x: [ + [midx, sy], + [midx, ty] + ], + y: [ + [sx, midy], + [tx, midy] + ] + }[axis]; + } + } + }; + + // 计算剩余线条 + var p = _lineCalculators[anchorOrientation](sourceAxis, ss, oss, es, oes); + if (p) { + for (let i = 0; i < p.length; p++) { + _addSegment(p[i][0], p[i][1]); + } + } + + // line to end stub + _addSegment(stubs[2], stubs[3]); + + // end stub to end (common) + _addSegment(tx, ty); + + // 实际操作svg + return _drawFlowSegment(segments, { + x: offsetX, + y: offsetY + }); +} + +export default drawFlow; +>>>>>>> 44fc7b5... chore: optimize link code