前端使用 Konva 实现可视化设计器(15)- 自定义连接点、连接优化
前面,本示例实现了折线连接线,简述了实现的思路和原理,也已知了一些缺陷。本章将处理一些缺陷的同时,实现支持连接点的自定义,一个节点可以定义多个连接点,最终可以满足类似图元接线的效果。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
一些调整
- 把示例素材从 src 转移至 public 目录,拖入画布的素材改为异步加载
- 移除部分示例素材
- 一些开发过程中的测试用例可以在线加载
此前有些朋友说导入、导出有异常,估计是线上版本和线下版本的构建示例素材的文件 hash 后缀不一样,跨环境导入、导出无法加载图片导致的。现在调整后就应该正常了。
自定义连接点
先说明一下定义:
// src/Render/types.ts
export interface AssetInfoPoint {
x: number
y: number
direction?: 'top' | 'bottom' | 'left' | 'right' // 人为定义连接点属于元素的什么方向
}
export interface AssetInfo {
url: string
points?: Array<AssetInfoPoint>
}
// src/Render/draws/LinkDraw.ts
// 连接点
export interface LinkDrawPoint {
id: string
groupId: string
visible: boolean
pairs: LinkDrawPair[]
x: number
y: number
direction?: 'top' | 'bottom' | 'left' | 'right' // 人为定义连接点属于元素的什么方向
}
一个素材除了原来的 url 信息外,增加了一个 points 的连接点数组,每个 point 除了记录了它的相对于素材的位置 x、y,还有方向的定义,目的是说明该连接点出入口方向,例如:

做这个定义的原因是,连接方向不可以预知,是与图元的含义有关。
不设定 direction 的话,就代表连接线可以从上下左右4个方向进出,如:

最佳实践应该另外实现一个连接点定义工具(也许后面有机会实现一个),多多支持~
// src/App.vue
// 从 public 加载静态资源 + 自定义连接点
const assetsModules: Array<Types.AssetInfo> = [
{ "url": "./img/svg/ARRESTER_1.svg", points: [{ x: 101, y: 1, direction: 'top' }, { x: 101, y: 199, direction: 'bottom' }] },
{ "url": "./img/svg/ARRESTER_2.svg", points: [{ x: 101, y: 1, direction: 'top' }, { x: 101, y: 199, direction: 'bottom' }] },
{ "url": "./img/svg/ARRESTER_2_1.svg", points: [{ x: 101, y: 1, direction: 'top' }, { x: 101, y: 199, direction: 'bottom' }] },
{ "url": "./img/svg/BREAKER_CLOSE.svg", points: [{ x: 100, y: 1, direction: 'top' }, { x: 100, y: 199, direction: 'bottom' }] },
{ "url": "./img/svg/BREAKER_OPEN.svg", points: [{ x: 100, y: 1, direction: 'top' }, { x: 100, y: 199, direction: 'bottom' }] },
// 略
]
素材拖入之前,需要携带 points 信息:
// src/App.vue
function onDragstart(e: GlobalEventHandlersEventMap['dragstart'], item: Types.AssetInfo) {
if (e.dataTransfer) {
e.dataTransfer.setData('src', item.url)
e.dataTransfer.setData('points', JSON.stringify(item.points)) // 传递连接点信息
e.dataTransfer.setData('type', item.url.match(/([^./]+)\.([^./]+)$/)?.[2] ?? '')
}
}
拖入之后,需要解析 points 信息:
// src/Render/handlers/DragOutsideHandlers.ts
drop: (e: GlobalEventHandlersEventMap['drop']) => {
const src = e.dataTransfer?.getData('src')
// 接收连接点信息
let morePoints: Types.AssetInfoPoint[] = []
const morePointsTxt = e.dataTransfer?.getData('points') ?? '[]'
try {
morePoints = JSON.parse(morePointsTxt)
} catch (e) {
console.error(e)
}
// 略
// 默认连接点
let points: Types.AssetInfoPoint[] = [
// 左
{ x: 0, y: group.height() / 2, direction: 'left' },
// 右
{
x: group.width(),
y: group.height() / 2,
direction: 'right'
},
// 上
{ x: group.width() / 2, y: 0, direction: 'top' },
// 下
{
x: group.width() / 2,
y: group.height(),
direction: 'bottom'
}
]
// 自定义连接点 覆盖 默认连接点
if (Array.isArray(morePoints) && morePoints.length > 0) {
points = morePoints
}
// 连接点信息
group.setAttrs({
points: points.map(
(o) =>
({
...o,
id: nanoid(),
groupId: group.id(),
visible: false,
pairs: [],
direction: o.direction // 补充信息
}) as LinkDrawPoint
)
})
// 连接点(锚点)
for (const point of group.getAttr('points') ?? []) {
group.add(
new Konva.Circle({
name: 'link-anchor',
id: point.id,
x: point.x,
y: point.y,
radius: this.render.toStageValue(1),
stroke: 'rgba(0,0,255,1)',
strokeWidth: this.render.toStageValue(2),
visible: false,
direction: point.direction // 补充信息
})
)
}
// 略
}
如果没有自定义连接点,这里会给予之前一样的 4 个默认连接点。
出入口修改
原来的逻辑就不能用了,需要重写一个。目标是计算出:沿着当前连接点的方向 与 不可通过区域其中一边的相交点,上图:

关注的就是这个绿色点(出入口):

就算这个点,用的是三角函数:

这里边长称为 offset,角度为 rotate,计算大概如下:
const offset = gap * Math.atan(((90 - rotate) * Math.PI) / 180)
不同角度范围,计算略有不同,是根据多次测试得出的,有兴趣的朋友可以在优化精简一下。
完整方法有点长,四个角直接赋值,其余按不同角度范围计算:
// 连接出入口(原来第二个参数是 最小区域,先改为 不可通过区域)
getEntry(anchor: Konva.Node, groupForbiddenArea: Area, gap: number): Konva.Vector2d {
// stage 状态
const stageState = this.render.getStageState()
const fromPos = anchor.absolutePosition()
// 默认为 起点/终点 位置(无 direction 时的值)
let x = fromPos.x - stageState.x,
y = fromPos.y - stageState.y
const direction = anchor.attrs.direction
// 定义了 direction 的时候
if (direction) {
// 取整 连接点 锚点 旋转角度(保留 1 位小数点)
const rotate = Math.round(anchor.getAbsoluteRotation() * 10) / 10
// 利用三角函数,计算按 direction 方向与 不可通过区域 的相交点位置(即出/入口 entry)
if (rotate === -45) {
if (direction === 'top') {
x = groupForbiddenArea.x1
y = groupForbiddenArea.y1
} else if (direction === 'bottom') {
x = groupForbiddenArea.x2
y = groupForbiddenArea.y2
} else if (direction === 'left') {
x = groupForbiddenArea.x1
y = groupForbiddenArea.y2
} else if (direction === 'right') {
x = groupForbiddenArea.x2
y = groupForbiddenArea.y1
}
} else if (rotate === 45) {
if (direction === 'top') {
x = groupForbiddenArea.x2
y = groupForbiddenArea.y1
} else if (direction === 'bottom') {
x = groupForbiddenArea.x1
y = groupForbiddenArea.y2
} else if (direction === 'left') {
x = groupForbiddenArea.x1
y = groupForbiddenArea.y1
} else if (direction === 'right') {
x = groupForbiddenArea.x2
y = groupForbiddenArea.y2
}
} else if (rotate === 135) {
if (direction === 'top') {
x = groupForbiddenArea.x2
y = groupForbiddenArea.y2
} else if (direction === 'bottom') {
x = groupForbiddenArea.x1
y = groupForbiddenArea.y1
} else if (direction === 'left') {
x = groupForbiddenArea.x2
y = groupForbiddenArea.y1
} else if (direction === 'right') {
x = groupForbiddenArea.x1
y = groupForbiddenArea.y2
}
} else if (rotate === -135) {
if (direction === 'top') {
x = groupForbiddenArea.x1
y = groupForbiddenArea.y2
} else if (direction === 'bottom') {
x = groupForbiddenArea.x2
y = groupForbiddenArea.y1
} else if (direction === 'left') {
x = groupForbiddenArea.x2
y = groupForbiddenArea.y2
} else if (direction === 'right') {
x = groupForbiddenArea.x1
y = groupForbiddenArea.y1
}
} else if (rotate > -45 && rotate < 45) {
const offset = gap * Math.tan((rotate * Math.PI) / 180)
if (direction === 'top') {
x = fromPos.x - stageState.x + offset
y = groupForbiddenArea.y1
} else if (direction === 'bottom') {
x = fromPos.x - stageState.x - offset
y = groupForbiddenArea.y2
} else if (direction === 'left') {
x = groupForbiddenArea.x1
y = fromPos.y - stageState.y - offset
} else if (direction === 'right') {
x = groupForbiddenArea.x2
y = fromPos.y - stageState.y + offset
}
} else if (rotate > 45 && rotate < 135) {
const offset = gap * Math.atan(((90 - rotate) * Math.PI) / 180)
if (direction === 'top') {
x = groupForbiddenArea.x2
y = fromPos.y - stageState.y - offset
} else if (direction === 'bottom') {
x = groupForbiddenArea.x1
y = fromPos.y - stageState.y + offset
} else if (direction === 'left') {
x = fromPos.x - stageState.x - offset
y = groupForbiddenArea.y1
} else if (direction === 'right') {
x = fromPos.x - stageState.x + offset
y = groupForbiddenArea.y2
}
} else if ((rotate > 135 && rotate <= 180) || (rotate >= -180 && rotate < -135)) {
const offset = gap * Math.tan((rotate * Math.PI) / 180)
if (direction === 'top') {
x = fromPos.x - stageState.x - offset
y = groupForbiddenArea.y2
} else if (direction === 'bottom') {
x = fromPos.x - stageState.x + offset
y = groupForbiddenArea.y1
} else if (direction === 'left') {
x = groupForbiddenArea.x2
y = fromPos.y - stageState.y + offset
} else if (direction === 'right') {
x = groupForbiddenArea.x1
y = fromPos.y - stageState.y - offset
}
} else if (rotate > -135 && rotate < -45) {
const offset = gap * Math.atan(((90 + rotate) * Math.PI) / 180)
if (direction === 'top') {
x = groupForbiddenArea.x1
y = fromPos.y - stageState.y - offset
} else if (direction === 'bottom') {
x = groupForbiddenArea.x2
y = fromPos.y - stageState.y + offset
} else if (direction === 'left') {
x = fromPos.x - stageState.x - offset
y = groupForbiddenArea.y2
} else if (direction === 'right') {
x = fromPos.x - stageState.x + offset
y = groupForbiddenArea.y1
}
}
}
return { x, y } as Konva.Vector2d
}
原来的算法起点、终点 与 连接点一一对应,科室现在新的计算方法得出的出入口x、y坐标与连接点不再总是存在同一方向一致(因为被旋转),所以现在把算法的起点、终点改为出入口对应:
// 出口、入口 -> 算法 起点、终点
if (columns[x] === fromEntry.x && rows[y] === fromEntry.y) {
matrix[y][x] = 1
matrixStart = { x, y }
}
if (columns[x] === toEntry.x && rows[y] === toEntry.y) {
matrix[y][x] = 1
matrixEnd = { x, y }
}
上面提到没有定义 direction 的连接点可以从不同方向出入,所以会进行下面处理:
// 没有定义方向(给于十字可通过区域)
// 如,从:
// 1 1 1
// 1 0 1
// 1 1 1
// 变成:
// 1 0 1
// 0 0 0
// 1 0 1
if (!fromAnchor.attrs.direction) {
if (columns[x] === fromEntry.x || rows[y] === fromEntry.y) {
if (
x >= columnFromStart &&
x <= columnFromEnd &&
y >= rowFromStart &&
y <= rowFromEnd
) {
matrix[y][x] = 1
}
}
}
if (!toAnchor.attrs.direction) {
if (columns[x] === toEntry.x || rows[y] === toEntry.y) {
if (x >= columnToStart && x <= columnToEnd && y >= rowToStart && y <= rowToEnd) {
matrix[y][x] = 1
}
}
}
最后在绘制连线的时候,补上连接点(起点、终点)即可:
this.group.add(
new Konva.Line({
name: 'link-line',
// 用于删除连接线
groupId: fromGroup.id(),
pointId: fromPoint.id,
pairId: pair.id,
//
points: _.flatten([
[
this.render.toStageValue(fromAnchorPos.x),
this.render.toStageValue(fromAnchorPos.y)
], // 补充 起点
...way.map((o) => [
this.render.toStageValue(columns[o.x]),
this.render.toStageValue(rows[o.y])
]),
[this.render.toStageValue(toAnchorPos.x), this.render.toStageValue(toAnchorPos.y)] // 补充 终点
]),
stroke: 'red',
strokeWidth: 2
})
)
测试一下

已知缺陷
从 Issue 中得知,当节点进行说 transform rotate 旋转的时候,对齐就会出问题。大家多多支持,后面抽空研究处理一下(-_-)。。。
More Stars please!勾勾手指~
前端使用 Konva 实现可视化设计器(15)- 自定义连接点、连接优化的更多相关文章
- 惊闻企业Web应用生成平台 活字格 V4.0 免费了,不单可视化设计器免费,服务器也免费!
官网消息: 针对活字格开发者,新版本完全免费!您可下载活字格 Web 应用生成平台 V4.0 Updated 1,方便的创建各类 Web 应用系统,任意部署,永不过期. 我之前学习过活字格,也曾经向用 ...
- (原创)【B4A】一步一步入门02:可视化界面设计器、控件的使用
一.前言 上篇 (原创)[B4A]一步一步入门01:简介.开发环境搭建.HelloWorld 中我们创建了默认的项目,现在我们来看一下B4A项目的构成,以及如何所见即所得的设计界面,并添加和使用自带的 ...
- Windows Phone 十二、设计器同步
在设计阶段为页面添加数据源 Blend或者VS的可视化设计器会跑我们的代码,然后来显示出来,当我们Build之后,设计器会进入页面的构造函数,调用InitializeComponent();方法来将U ...
- WinForms项目升级.Net Core 3.0之后,没有WinForm设计器?
目录 .NET Conf 2019 Window Forms 设计器 .NET Conf 2019 2019 9.23-9.25召开了 .NET Conf 2019 大会,大会宣布了 .Net Cor ...
- ActiveReports 9 新功能:可视化查询设计器(VQD)介绍
在最新发布的ActiveReports 9报表控件中添加了多项新功能,以帮助你在更短的时间里创建外观绚丽.功能强大的报表系统,本文将重点介绍可视化数据查询设计器,无需手动编写任何SQL语句,主要内容如 ...
- VS2015 android 设计器不能可视化问题解决。
近期安装了VS2015,体验了一下android 的开发,按模板创建执行了个,试下效果非常不错.也能够可视化设计.但昨天再次打开或创建一个android程序后,设计界面直接不能显示,显示错误:(可能是 ...
- 可视化流程设计——流程设计器演示(基于Silverlight)
上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ...
- F2工作流引擎之-纯JS Web在线可拖拽的流程设计器(八)
Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回.传阅.转交,都可以非常方便快捷地实现,管理员 ...
- 纯JS Web在线可拖拽的流程设计器
F2工作流引擎之-纯JS Web在线可拖拽的流程设计器 Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回. ...
- Type Script 在流程设计器的落地实践
流程设计器项目介绍 从事过BPM行业的大佬必然对流程建模工具非常熟悉,做为WFMC三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...
随机推荐
- Java Map中那些巧妙的设计
简介: 他山之石可以攻玉,这些巧妙的设计思想非常有借鉴价值,可谓是最佳实践.然而,大多数有关Java Map原理的科普类文章都是专注于"点",并没有连成"线", ...
- Flink 1.13,面向流批一体的运行时与 DataStream API 优化
简介: 在 1.13 中,针对流批一体的目标,Flink 优化了大规模作业调度以及批执行模式下网络 Shuffle 的性能,以及在 DataStream API 方面完善有限流作业的退出语义. 本文由 ...
- 阿里云GanosBase重磅升级,发布首个云孪生时空数据库
简介: GanosBase是李飞飞带领的达摩院数据库与存储实验室联合阿里云共同研发的新一代位置智能引擎:本次重磅升级为V4.0版本,推出首个云孪生时空数据库. 作者 | 谢炯 来源 | 阿里技术 ...
- 本周ddl(4.1-4.5)
本周ddl(4.1-4.5) cs61a首先完成2.23之前的任务 cs61a完成3.1之前的学习 cs61a完成3.8任务并且ants完成阶段1 csapp的bomblab 记录南京5个景点及其周边 ...
- VisualStudio 在 DebuggerDisplay 的属性更改业务逻辑将会让调试和非调试下逻辑不同
本文记录我写的逗比代码,我在 DebuggerDisplay 对应的属性的 get 方法上,在这个方法里面修改了业务逻辑,如修改界面元素,此时我在 VisualStudio 断点调试下和非断点调试下的 ...
- 10.prometheus监控--监控进程process
一.进程监控 如果想要对主机的进程进行监控,例如chronyd,sshd等服务进程以及自定义脚本程序运行状态监控.我们使用node exporter就不能实现需求了,此时就需要使用process ex ...
- Servlet注解的使用,简化配置 以及,使用模板方法设计模式优化oa项目
Servlet注解的使用,简化配置 以及,使用模板方法设计模式优化oa项目 每博一文案 有句谚语说:"一怒之下踢石头,只有痛着脚趾头." 比一件糟糕的事情更可拍的,是你用糟糕的态度 ...
- Unity 热更--AssetBundle学习笔记 1.0【AB包资源加载工具类的实现】
工具类封装 通过上文中对AB包加载API的了解和简单使用,对AB包资源加载的几种方法进行封装,将其写入单例类中,如代码展示. 确保每个AB资源包只加载一次: 在LoadAssetBundleManag ...
- JDK源码阅读-------自学笔记(十八)(java.lang.Enum枚举类)
枚举类简介 如果有必要定义一组常量的时候使用 所有的枚举类型隐性地继承自 java.lang.Enum,枚举实质上还是类. 每一个枚举中的成员,就相当于枚举的一个对象,默认都是public stati ...
- Dapr 与 .NET Aspire 结合使用获得无与伦比的本地开发体验
Dapr 提供了一组构建块,用于抽象分布式系统中常用的概念.这包括服务.缓存.工作流.复原能力.机密管理等之间的安全同步和异步通信.不必自己实现这些功能,可以消除样板,降低复杂性,并允许您专注于开发业 ...