Skip to content

Commit 655fa58

Browse files
committed
feat: add bidirectional arrow support and enhance arrow creation logic
1 parent 9899a7c commit 655fa58

File tree

6 files changed

+61
-36
lines changed

6 files changed

+61
-36
lines changed

src/arrow.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import { createSvgGroup, editSvgText } from './utils/svg'
55
import type { CustomSvg, Topic } from './types/dom'
66
import type { MindElixirInstance, Uid } from './index'
77

8-
// p1: starting point
9-
// p2: control point of starting point
10-
// p3: control point of ending point
11-
// p4: ending point
8+
/**
9+
* FYI
10+
* p1: starting point
11+
* p2: control point of starting point
12+
* p3: control point of ending point
13+
* p4: ending point
14+
*/
1215

1316
export type Arrow = {
1417
id: string
@@ -23,6 +26,7 @@ export type Arrow = {
2326
x: number
2427
y: number
2528
}
29+
bidirectional?: boolean
2630
}
2731
export type DivData = {
2832
cx: number // center x
@@ -32,7 +36,13 @@ export type DivData = {
3236
ctrlX: number // control point x
3337
ctrlY: number // control point y
3438
}
39+
export type ArrowOptions = {
40+
bidirectional?: boolean
41+
}
3542

43+
/**
44+
* calc control point, center point and div size
45+
*/
3646
function calcCtrlP(mei: MindElixirInstance, tpc: Topic, delta: { x: number; y: number }) {
3747
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
3848
const w = tpc.offsetWidth
@@ -51,7 +61,9 @@ function calcCtrlP(mei: MindElixirInstance, tpc: Topic, delta: { x: number; y: n
5161
}
5262
}
5363

54-
// calc starting and ending point using control point and div status
64+
/**
65+
* calc starting and ending point using control point and div status
66+
*/
5567
function calcP(data: DivData) {
5668
let x, y
5769
const k = (data.cy - data.ctrlY) / (data.ctrlX - data.cx)
@@ -104,12 +116,15 @@ const drawArrow = function (mei: MindElixirInstance, from: Topic, to: Topic, obj
104116
const { ctrlX: p3x, ctrlY: p3y } = toData
105117
const { x: p4x, y: p4y } = calcP(toData)
106118

107-
const arrowPoint = getArrowPoints(p3x, p3y, p4x, p4y)
119+
const arrowT = getArrowPoints(p3x, p3y, p4x, p4y)
108120

109-
const newSvgGroup = createSvgGroup(
110-
`M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`,
111-
`M ${arrowPoint.x1} ${arrowPoint.y1} L ${p4x} ${p4y} L ${arrowPoint.x2} ${arrowPoint.y2}`
112-
)
121+
const toArrow = `M ${arrowT.x1} ${arrowT.y1} L ${p4x} ${p4y} L ${arrowT.x2} ${arrowT.y2}`
122+
let fromArrow = ''
123+
if (obj.bidirectional) {
124+
const arrowF = getArrowPoints(p2x, p2y, p1x, p1y)
125+
fromArrow = `M ${arrowF.x1} ${arrowF.y1} L ${p1x} ${p1y} L ${arrowF.x2} ${arrowF.y2}`
126+
}
127+
const newSvgGroup = createSvgGroup(`M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`, toArrow, fromArrow)
113128

114129
const halfx = p1x / 8 + (p2x * 3) / 8 + (p3x * 3) / 8 + p4x / 8
115130
const halfy = p1y / 8 + (p2y * 3) / 8 + (p3y * 3) / 8 + p4y / 8
@@ -128,7 +143,7 @@ const drawArrow = function (mei: MindElixirInstance, from: Topic, to: Topic, obj
128143
console.log(`DrawArrow Execution time: ${end - start} ms`)
129144
}
130145

131-
export const createArrow = function (this: MindElixirInstance, from: Topic, to: Topic) {
146+
export const createArrow = function (this: MindElixirInstance, from: Topic, to: Topic, options: ArrowOptions = {}) {
132147
const arrowObj = {
133148
id: generateUUID(),
134149
label: 'Custom Link',
@@ -142,6 +157,7 @@ export const createArrow = function (this: MindElixirInstance, from: Topic, to:
142157
x: 0,
143158
y: -200,
144159
},
160+
...options,
145161
}
146162
drawArrow(this, from, to, arrowObj)
147163

@@ -229,8 +245,8 @@ const showLinkController = function (mei: MindElixirInstance, linkItem: Arrow, f
229245
mei.helper1 = LinkDragMoveHelper.create(mei.P2)
230246
mei.helper2 = LinkDragMoveHelper.create(mei.P3)
231247

232-
// TODO: generate cb function
233248
mei.helper1.init(mei.map, (deltaX, deltaY) => {
249+
if (!mei.currentArrow) return
234250
// recalc key points
235251
p2x = p2x + deltaX / mei.scaleVal
236252
p2y = p2y + deltaY / mei.scaleVal
@@ -242,8 +258,12 @@ const showLinkController = function (mei: MindElixirInstance, linkItem: Arrow, f
242258
// update dom position
243259
mei.P2.style.top = p2y + 'px'
244260
mei.P2.style.left = p2x + 'px'
245-
mei.currentArrow?.children[0].setAttribute('d', `M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`)
246-
setAttributes(mei.currentArrow!.children[2], {
261+
mei.currentArrow.children[0].setAttribute('d', `M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`)
262+
if (linkItem.bidirectional) {
263+
const arrowPoint = getArrowPoints(p2x, p2y, p1x, p1y)
264+
mei.currentArrow.children[2].setAttribute('d', `M ${arrowPoint.x1} ${arrowPoint.y1} L ${p1x} ${p1y} L ${arrowPoint.x2} ${arrowPoint.y2}`)
265+
}
266+
setAttributes(mei.currentArrow.children[3], {
247267
x: halfx + '',
248268
y: halfy + '',
249269
})
@@ -259,6 +279,7 @@ const showLinkController = function (mei: MindElixirInstance, linkItem: Arrow, f
259279
})
260280

261281
mei.helper2.init(mei.map, (deltaX, deltaY) => {
282+
if (!mei.currentArrow) return
262283
p3x = p3x + deltaX / mei.scaleVal
263284
p3y = p3y + deltaY / mei.scaleVal
264285
const p4 = calcP({ ...toData, ctrlX: p3x, ctrlY: p3y })
@@ -270,9 +291,9 @@ const showLinkController = function (mei: MindElixirInstance, linkItem: Arrow, f
270291

271292
mei.P3.style.top = p3y + 'px'
272293
mei.P3.style.left = p3x + 'px'
273-
mei.currentArrow?.children[0].setAttribute('d', `M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`)
274-
mei.currentArrow?.children[1].setAttribute('d', `M ${arrowPoint.x1} ${arrowPoint.y1} L ${p4x} ${p4y} L ${arrowPoint.x2} ${arrowPoint.y2}`)
275-
setAttributes(mei.currentArrow!.children[2], {
294+
mei.currentArrow.children[0].setAttribute('d', `M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`)
295+
mei.currentArrow.children[1].setAttribute('d', `M ${arrowPoint.x1} ${arrowPoint.y1} L ${p4x} ${p4y} L ${arrowPoint.x2} ${arrowPoint.y2}`)
296+
setAttributes(mei.currentArrow.children[3], {
276297
x: halfx + '',
277298
y: halfy + '',
278299
})
@@ -304,7 +325,7 @@ export function renderArrow(this: MindElixirInstance) {
304325
export function editArrowLabel(this: MindElixirInstance, el: CustomSvg) {
305326
console.time('editSummary')
306327
if (!el) return
307-
const textEl = el.children[2]
328+
const textEl = el.children[3]
308329
editSvgText(this, textEl, div => {
309330
const node = el.arrowObj
310331
const text = div.textContent?.trim() || ''

src/exampleData/1.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ const aboutMindElixir: MindElixirData = {
354354
x: 146.1171875,
355355
y: 45,
356356
},
357+
bidirectional: false,
357358
},
358359
],
359360
summaries: [

src/plugin/contextMenu.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { MindElixirInstance } from '../types/index'
44
import { encodeHTML, isTopic } from '../utils/index'
55
import dragMoveHelper from '../utils/dragMoveHelper'
66
import './contextMenu.less'
7+
import type { ArrowOptions } from '../arrow'
78

89
export type ContextMenuOption = {
910
focus?: boolean
@@ -39,6 +40,7 @@ export default function (mind: MindElixirInstance, option?: ContextMenuOption) {
3940
const up = createLi('cm-up', lang.moveUp, 'PgUp')
4041
const down = createLi('cm-down', lang.moveDown, 'Pgdn')
4142
const link = createLi('cm-link', lang.link, '')
43+
const linkBidirectional = createLi('cm-link-bidirectional', 'Bidreactional Link', '')
4244
const summary = createLi('cm-summary', lang.summary, '')
4345

4446
const menuUl = document.createElement('ul')
@@ -56,6 +58,7 @@ export default function (mind: MindElixirInstance, option?: ContextMenuOption) {
5658
menuUl.appendChild(summary)
5759
if (!option || option.link) {
5860
menuUl.appendChild(link)
61+
menuUl.appendChild(linkBidirectional)
5962
}
6063
if (option && option.extend) {
6164
for (let i = 0; i < option.extend.length; i++) {
@@ -178,7 +181,7 @@ export default function (mind: MindElixirInstance, option?: ContextMenuOption) {
178181
mind.moveDownNode()
179182
menuContainer.hidden = true
180183
}
181-
link.onclick = () => {
184+
const linkFunc = (options?: ArrowOptions) => {
182185
menuContainer.hidden = true
183186
const from = mind.currentNode as Topic
184187
const tips = createTips(lang.clickTips)
@@ -190,7 +193,7 @@ export default function (mind: MindElixirInstance, option?: ContextMenuOption) {
190193
tips.remove()
191194
const target = e.target as Topic
192195
if (target.parentElement.tagName === 'ME-PARENT' || target.parentElement.tagName === 'ME-ROOT') {
193-
mind.createArrow(from, target)
196+
mind.createArrow(from, target, options)
194197
} else {
195198
console.log('link cancel')
196199
}
@@ -200,6 +203,8 @@ export default function (mind: MindElixirInstance, option?: ContextMenuOption) {
200203
}
201204
)
202205
}
206+
link.onclick = () => linkFunc()
207+
linkBidirectional.onclick = () => linkFunc({ bidirectional: true })
203208
summary.onclick = () => {
204209
menuContainer.hidden = true
205210
mind.createSummary()

src/types/dom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ export type CustomLine = SVGPathElement
5454
export type CustomArrow = SVGPathElement
5555
export interface CustomSvg extends SVGGElement {
5656
arrowObj: Arrow
57-
children: HTMLCollection & [CustomLine, CustomArrow, SVGTextElement]
57+
children: HTMLCollection & [CustomLine, CustomArrow, CustomArrow, SVGTextElement]
5858
}

src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function getArrowPoints(p3x: number, p3y: number, p4x: number, p4y: numbe
6565
if (deltax > 0 && deltay < 0) {
6666
angle = 360 - angle
6767
}
68-
const arrowLength = 15
68+
const arrowLength = 12
6969
const arrowAngle = 30
7070
const a1 = angle + arrowAngle
7171
const a2 = angle - arrowAngle

src/utils/svg.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,26 @@ export const createLine = function () {
3232
return line
3333
}
3434

35-
export const createSvgGroup = function (d: string, arrowd: string): CustomSvg {
35+
export const createSvgGroup = function (d: string, arrowd1: string, arrowd2: string): CustomSvg {
3636
const pathAttrs = {
3737
stroke: 'rgb(235, 95, 82)',
3838
fill: 'none',
3939
'stroke-linecap': 'cap',
4040
'stroke-width': '2',
4141
}
4242
const g = $d.createElementNS(svgNS, 'g') as CustomSvg
43-
const path = $d.createElementNS(svgNS, 'path')
44-
const arrow = $d.createElementNS(svgNS, 'path')
45-
setAttributes(arrow, {
46-
d: arrowd,
47-
...pathAttrs,
48-
})
49-
setAttributes(path, {
50-
d,
51-
...pathAttrs,
52-
'stroke-dasharray': '8,2',
43+
;[d, arrowd1, arrowd2].forEach((d, i) => {
44+
const path = $d.createElementNS(svgNS, 'path')
45+
const attrs = {
46+
d,
47+
...pathAttrs,
48+
}
49+
setAttributes(path, attrs)
50+
if (i === 0) {
51+
path.setAttribute('stroke-dasharray', '8,2')
52+
}
53+
g.appendChild(path)
5354
})
54-
55-
g.appendChild(path)
56-
g.appendChild(arrow)
5755
return g
5856
}
5957

0 commit comments

Comments
 (0)