UNPKG

z-polygon-editor

Version:

一个基于MarkJs二次开发的Canvas标注库, 用于电子围栏多边形标注

740 lines (701 loc) 20.3 kB
import utils from './utils' /* // 配置 { data: null,// 附加数据,可以添加你需要的任何数据 lineType: 'line',线段类型,line(普通线段)、borderLine(带边框的线段)、custom(自定义绘图方法) customRenderLine(this){},// 自定义绘制线段的方法 strokeStyle: {// 标注轮廓样式 lineWidth: 3, strokeColor: 'rgba(0, 136, 255, 1)', lineJoin: 'round', // 如果lineType为borderLine,需要配observer置一下三个属性 frontLineWidth: 3, frontStrokeColor: 'rgba(0, 136, 255, 1)', frontLineJoin: 'round' }, fillColor: 'rgba(0, 136, 255, 0.5)',// 标注区域填充颜色 pointStyle: {// 端点的样式 lineWidth: 3, strokeColor: 'rgba(0, 136, 255, 1)', fillColor: 'rgba(0, 136, 255, 0.5)' }, dashOffset: [5, 10], // 虚线设置 isDash: false, // 是否以虚线显示 pointArr: [// 回显的标注区域数据 { data: null,//附加数据 pointArr: [{// 点位数据 {"x":0,"y":0} }], strokeStyle: {}, fillColor: '', pointStyle:{} } ] } */ // 默认的线条样式 const defaultStrokeStyle = { lineWidth: 3, strokeColor: 'rgba(0, 136, 255, 1)', lineJoin: 'round', frontLineWidth: 3, frontStrokeColor: 'rgba(0, 136, 255, 1)', frontLineJoin: 'round', dashOffset: [5, 10] } // 默认填充样式 const defaultFillColor = 'rgba(0, 136, 255, 0.5)' // 默认的端点样式 const defaultPointStyle = { lineWidth: 3, strokeColor: 'rgba(0, 136, 255, 1)', fillColor: 'rgba(0, 136, 255, 0.5)' } // 默认配置 const defaultOpt = { showPoint: true, pointType: 'square', pointWidth: 3, lineType: 'line', isDash: false } /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 15:12:17 * @Desc: 单个标注对象 */ export default class MarkItem { /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 15:12:11 * @Desc: 构造函数 */ constructor(ctx = null, opt = {}) { // canvas上下文 this.ctx = ctx // 配置 this.opt = { ...defaultOpt, ...opt } // 附加数据 this.data = opt.data || null // 线段绘制样式 this.strokeStyle = opt.strokeStyle ? { ...defaultStrokeStyle, ...opt.strokeStyle } : defaultStrokeStyle // 填充样式 this.fillColor = opt.fillColor || defaultFillColor // 端点样式 this.pointStyle = opt.pointStyle ? { ...defaultPointStyle, ...opt.pointStyle } : defaultPointStyle // 点位数组{x,y} this.pointArr = opt.pointArr || [] // 自定义更新端点位置的方法 this.updatePointFn = opt.updatePoint // 路径是否已经闭合了 this.isClosePath = false // 是否是编辑状态 this.isEditing = false // 是否是拖动状态 this.isDragging = false // 拖动的端点索引,没有则代表拖拽整体 this.dragPointIndex = -1 // 点位数组缓存,用于整体拖动 this.dragCachePointArr = [] // 鼠标滑过显示可选择状态 this.hoverActive = false // 是否是闭合绘制模式 this.area = opt.area || false // 始终闭合绘制模式下的当前鼠标移动到的非固定点 this.areaToPointPos = null // 是否允许新增节点,仅在闭合情况下的编辑期间 this.enableAddPoint = opt.enableAddPoint || false } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 15:17:35 * @Desc: 开启编辑 */ enable() { this.isEditing = true // 插入虚拟顶点 this.insertFictitiousPoints() } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 15:17:53 * @Desc: 结束编辑 */ disable() { this.isEditing = false // 移除虚拟顶点 this.removeFictitiousPoints() } /** * javascript comment * @Author: 王林25 * @Date: 2021-01-25 14:45:32 * @Desc: 获取真实顶点数组 */ getTruePointArr() { return this.pointArr.filter((item) => { return !item.fictitious }) } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 14:24:50 * @Desc: 端点数量 */ getPointLength() { return this.getTruePointArr().length } /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 15:54:50 * @Desc: 添加端点 */ pushPoint(x, y) { if (!this.isEditing || this.isClosePath) { return } this.pointArr.push({ x, y }) } /** * javascript comment * @Author: 王林25 * @Date: 2020-11-05 10:41:36 * @Desc: 删除某个顶点 */ removePoint(index) { // 点击的是虚拟节点直接返回 if (this.pointArr[index].fictitious) { return } this.pointArr.splice(index, 1) // 删除后需要重新创建虚拟节点 this.removeFictitiousPoints() this.insertFictitiousPoints() } /** * javascript comment * @Author: 王林25 * @Date: 2021-01-22 09:32:47 * @Desc: 始终闭合模式下的当前鼠标移动到的非固定点 */ areaToPoint(x, y) { this.areaToPointPos = { x, y } } /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 15:57:47 * @Desc: 渲染 */ render() { // 填充区域 if (this.isClosePath || this.area) { this.renderArea() } // 绘制线段 if (this.opt.lineType === 'custom') { this.opt.customRenderLine && this.opt.customRenderLine(this) } else if (this.opt.lineType === 'borderLine') { this.renderLines(this.strokeStyle) this.renderLines({ ...this.strokeStyle, lineWidth: this.strokeStyle.frontLineWidth, strokeColor: this.strokeStyle.frontStrokeColor, lineJoin: this.strokeStyle.frontLineJoin }) } else { this.renderLines(this.strokeStyle) } // 绘制端点 this.renderPoints() } /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 17:04:16 * @Desc: 填充区域 */ renderArea() { this.ctx.save() this.ctx.fillStyle = this.fillColor this.ctx.beginPath() let _pointArr = this.pointArr.concat(this.area ? this.areaToPointPos ? [this.areaToPointPos] : [] : []) for (let i = 0; i < _pointArr.length; i++) { let x = _pointArr[i].x let y = _pointArr[i].y if (i === 0) { this.ctx.moveTo(x, y) } else { this.ctx.lineTo(x, y) } } this.ctx.closePath() this.ctx.fill() this.ctx.restore() } /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 15:16:37 * @Desc: 绘制线段 */ renderLines({ lineWidth, strokeColor, lineJoin, dashOffset }, onlyPath) { this.ctx.save() this.ctx.lineWidth = lineWidth this.ctx.strokeStyle = strokeColor this.ctx.lineJoin = lineJoin this.ctx.beginPath() this.opt.isDash ? this.ctx.setLineDash(dashOffset) : utils.noop() let _pointArr = this.pointArr.concat(this.area ? this.areaToPointPos ? [this.areaToPointPos] : [] : []) for (let i = 0; i < _pointArr.length; i++) { let x = _pointArr[i].x let y = _pointArr[i].y if (i === 0) { this.ctx.moveTo(x, y) } else { this.ctx.lineTo(x, y) } } // 闭合路径 if (this.isClosePath || this.area) { this.ctx.closePath() } // 不实际绘制出来 if (!onlyPath) { this.ctx.stroke() } this.ctx.restore() } /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 17:54:18 * @Desc: 绘制端点 */ renderPoints(onlyPath, callback) { for (let i = 0; i < this.pointArr.length; i++) { this.ctx.beginPath() let x = this.pointArr[i].x let y = this.pointArr[i].y if (this.isEditing || onlyPath || this.hoverActive) { this.drawPoint(x, y, onlyPath, false, this.pointArr[i].fictitious) callback && callback(i) } } } /** * javascript comment * @Author: 王林25 * @Date: 2021-01-25 10:48:01 * @Desc: 插入虚拟节点 */ insertFictitiousPoints() { if (!this.isEditing || !this.isClosePath || !this.enableAddPoint) { return } // 先去掉虚拟节点 this.removeFictitiousPoints() let points = [] let arr = this.pointArr let len = arr.length for (let i = 0; i < len - 1; i++) { let s = arr[i] let e = arr[i + 1] points.push({ x: (s.x + e.x) / 2, y: (s.y + e.y) / 2, fictitious: true }) } points.push({ x: (arr[len - 1].x + arr[0].x) / 2, y: (arr[len - 1].y + arr[0].y) / 2, fictitious: true }) // 插入 let newArr = [] for (let i = 0; i < this.pointArr.length; i++) { newArr.push(this.pointArr[i]) newArr.push(points.shift()) } this.pointArr = newArr } /** * javascript comment * @Author: 王林25 * @Date: 2021-01-25 14:07:12 * @Desc: 去掉虚拟节点 */ removeFictitiousPoints() { this.pointArr = this.getTruePointArr() } /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 17:57:19 * @Desc: 绘制点 */ drawPoint(x, y, onlyPath, beginPath, reverse) { let { customRenderPoint, showPoint, pointType, pointWidth } = this.opt if (beginPath) { this.ctx.beginPath() } // 用户自定义绘制端点方法 if (customRenderPoint) { customRenderPoint(this.ctx, x, y, onlyPath, this.pointStyle) } else { // 预定义绘制端点方法 this.ctx.save() this.ctx.lineWidth = this.pointStyle.lineWidth this.ctx.strokeStyle = this.pointStyle.strokeColor this.ctx.fillStyle = this.pointStyle.fillColor // 反向样式,边框和填充的颜色互换,用于虚拟节点的显示 if (reverse) { this.ctx.strokeStyle = this.pointStyle.fillColor this.ctx.fillStyle = this.pointStyle.strokeColor } switch (pointType) { case 'square': this.ctx.rect(x - pointWidth, y - pointWidth, pointWidth * 2, pointWidth * 2) break; case 'circle': this.ctx.arc(x, y, pointWidth * 2, 0, 2 * Math.PI) break; default: break; } // 不实际绘制出来 if (!onlyPath) { if (showPoint) { this.ctx.fill() this.ctx.stroke() } } this.ctx.restore() } } /** * javascript comment * @Author: 王林25 * @Date: 2020-09-27 15:14:29 * @Desc: 检测某个点是否在该路径内 */ checkInPath(x, y) { this.renderLines(this.strokeStyle, true) return this.ctx.isPointInPath(x, y) } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 15:48:51 * @Desc: 判断某个点是否在某个端点内 */ checkInPoints(x, y) { let index = -1 this.renderPoints(true, (i) => { if (this.ctx.isPointInPath(x, y)) { index = i } }) return index } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 14:23:34 * @Desc: 闭合路径 */ closePath() { this.areaToPointPos = null this.isClosePath = true } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 16:54:34 * @Desc: 允许拖动 */ enableDrag(pointIndex = -1) { this.isDragging = true this.dragPointIndex = pointIndex this.dragCachePointArr = JSON.parse(JSON.stringify(this.pointArr)) } /** * javascript comment * @Author: 王林25 * @Date: 2021-01-25 14:25:32 * @Desc: 获取某个顶点在没有虚拟顶点的情况下的真实索引 */ getTruePointIndex(index) { if (index === -1 || this.pointArr[index].fictitious) { return index } let count = 0 for (let i = 0; i < index; i++) { if (this.pointArr[i].fictitious) { count++ } } return index - count } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 18:33:02 * @Desc: 停止拖动 */ disableDrag() { this.isDragging = false this.dragPointIndex = -1 this.dragCachePointArr = [] // 拖动结束后恢复虚拟节点的创建 this.insertFictitiousPoints() } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 17:04:23 * @Desc: 拖动某个端点 */ dragPoint(x, y) { if (!this.isDragging || this.dragPointIndex === -1) { return } // 拖动的是虚拟点,则转换成真实点 if (this.pointArr[this.dragPointIndex].fictitious) { delete this.pointArr[this.dragPointIndex].fictitious } // 获取某个顶点的真实索引 this.dragPointIndex = this.getTruePointIndex(this.dragPointIndex) // 拖动时隐藏虚拟节点 this.removeFictitiousPoints() if (this.updatePointFn) { this.updatePointFn(this, x, y) } else { this.pointArr.splice(this.dragPointIndex, 1, { ...this.pointArr[this.dragPointIndex], x, y }) } } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 17:04:32 * @Desc: 拖动整体 */ dragAll(ox, oy) { if (!this.isDragging) { return } this.pointArr = this.dragCachePointArr.map((item) => { return { ...item, x: item.x + ox, y: item.y + oy } }) } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 19:13:34 * @Desc: 显示鼠标滑过时可选择状态 */ enableHoverActive() { this.hoverActive = true } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-15 19:14:02 * @Desc: 关闭鼠标滑过时可选择状态 */ disableHoverActive() { this.hoverActive = false } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-21 20:23:44 * @Desc: 遍历检查线段是否存在交叉情况 */ checkLineSegmentCross() { if (!this.checkCrossPrevCheck()) { return false } // 已存在的线段 let lineSegments = this.createLineSegments(true) let len = lineSegments.length let cross = false for (let i = 0; i < len; i++) { let item = lineSegments[i] if (this.checkCrossWithLineSegments(item[0], item[1], true)) { cross = true } } return cross } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-22 09:36:33 * @Desc: 检查即将形成的线段是否会存在交叉 */ checkNextLineSegmentCross(x, y) { if (!this.checkCrossPrevCheck()) { return false } let arr = this.getTruePointArr() let len = arr.length // 即将形成的线段 let c = { x, y } let d = arr[len - 1] return this.checkCrossWithLineSegments(c, d) } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-22 09:37:07 * @Desc: 检查最后的闭合线段是否会存在交叉 */ checkEndLineSegmentCross() { if (!this.checkCrossPrevCheck()) { return false } let arr = this.getTruePointArr() let len = arr.length let c = arr[len - 1] let d = arr[0] return this.checkCrossWithLineSegments(c, d) } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-21 20:27:00 * @Desc: 判断某条线段是否会与当前存在的线段交叉 */ checkCrossWithLineSegments(c, d, close) { // 已存在的线段 let lineSegments = this.createLineSegments(close) let cross = false for (let i = 0; i < lineSegments.length; i++) { let item = lineSegments[i] let a = item[0] let b = item[1] if (utils.checkLineSegmentCross(a, b, c, d)) { cross = true } } return cross } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-21 20:24:28 * @Desc: 创建已存在点位组成的线段 */ createLineSegments(close) { let arr = this.getTruePointArr() let len = arr.length // 已存在的线段 let lineSegments = [] for (let i = 0; i < len - 1; i++) { lineSegments.push([ arr[i], arr[i + 1] ]) } // 包含起点和终点组成的线段 if (close) { lineSegments.push([ arr[len - 1], arr[0] ]) } return lineSegments } /** * javascript comment * @Author: 王林25 * @Date: 2020-10-21 20:23:59 * @Desc: 三个端点以下不可能交叉 */ checkCrossPrevCheck() { let len = this.getTruePointArr().length // 三个端点以下不可能交叉 if (len <= 2) { return false } return true } /** * javascript comment * @Author: 王林25 * @Date: 2021-01-22 10:37:07 * @Desc: 获取距离某个点最近的线段 */ getPintNearestLine(x, y) { let lineSegments = this.createLineSegments(this.isClosePath) if (lineSegments.length <= 0) { return null } if (this.dragPointIndex !== -1) { if (this.dragPointIndex === 0) { lineSegments.splice(0, 1) lineSegments.splice(-1, 1) } else { lineSegments.splice(this.dragPointIndex - 1, 2) } } let minNum = Infinity let minLine for (let i = 0; i < lineSegments.length; i++) { let item = lineSegments[i] let a = item[0] let b = item[1] let d = utils.getLinePointDistance(a.x, a.y, b.x, b.y, x, y) if (d < minNum) { minNum = d minLine = item } } return [minNum, minLine] } }