483 lines
12 KiB
JavaScript
483 lines
12 KiB
JavaScript
|
import Rectangle from './elements/Rectangle'
|
||
|
import Circle from './elements/Circle'
|
||
|
import Diamond from './elements/Diamond'
|
||
|
import Triangle from './elements/Triangle'
|
||
|
import Freedraw from './elements/Freedraw'
|
||
|
import Arrow from './elements/Arrow'
|
||
|
import Image from './elements/Image'
|
||
|
import Line from './elements/Line'
|
||
|
import Text from './elements/Text'
|
||
|
import {
|
||
|
getTowPointDistance,
|
||
|
throttle,
|
||
|
computedLineWidthBySpeed,
|
||
|
createImageObj
|
||
|
} from './utils'
|
||
|
import { DRAG_ELEMENT_PARTS } from './constants'
|
||
|
|
||
|
// 元素管理类
|
||
|
export default class Elements {
|
||
|
constructor(app) {
|
||
|
this.app = app
|
||
|
// 所有元素
|
||
|
this.elementList = []
|
||
|
// 当前激活元素
|
||
|
this.activeElement = null
|
||
|
// 当前正在创建新元素
|
||
|
this.isCreatingElement = false
|
||
|
// 当前正在调整元素
|
||
|
this.isResizing = false
|
||
|
// 当前正在调整的元素
|
||
|
this.resizingElement = null
|
||
|
// 稍微缓解一下卡顿
|
||
|
this.handleResize = throttle(this.handleResize, this, 16)
|
||
|
}
|
||
|
|
||
|
// 序列化当前画布上的所有元素
|
||
|
serialize(stringify = false) {
|
||
|
let data = this.elementList.map(element => {
|
||
|
return element.serialize()
|
||
|
})
|
||
|
return stringify ? JSON.stringify(data) : data
|
||
|
}
|
||
|
|
||
|
// 获取当前画布上的元素数量
|
||
|
getElementsNum() {
|
||
|
return this.elementList.length
|
||
|
}
|
||
|
|
||
|
// 当前画布上是否有元素
|
||
|
hasElements() {
|
||
|
return this.elementList.length > 0
|
||
|
}
|
||
|
|
||
|
// 添加元素
|
||
|
addElement(element) {
|
||
|
this.elementList.push(element)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 向前添加元素
|
||
|
unshiftElement(element) {
|
||
|
this.elementList.unshift(element)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 添加元素到指定位置
|
||
|
insertElement(element, index) {
|
||
|
this.elementList.splice(index, 0, element)
|
||
|
}
|
||
|
|
||
|
// 删除元素
|
||
|
deleteElement(element) {
|
||
|
let index = this.getElementIndex(element)
|
||
|
if (index !== -1) {
|
||
|
this.elementList.splice(index, 1)
|
||
|
if (element.isActive) {
|
||
|
this.cancelActiveElement(element)
|
||
|
}
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 删除全部元素
|
||
|
deleteAllElements() {
|
||
|
this.activeElement = null
|
||
|
if(this.app.isMobile){
|
||
|
this.elementList = this.elementList.filter(item => item.style.elReadonly)
|
||
|
}
|
||
|
else{
|
||
|
this.elementList = []
|
||
|
}
|
||
|
this.isCreatingElement = false
|
||
|
this.isResizing = false
|
||
|
this.resizingElement = null
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 获取元素在元素列表里的索引
|
||
|
getElementIndex(element) {
|
||
|
return this.elementList.findIndex(item => {
|
||
|
return item === element
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 根据元素数据创建元素
|
||
|
createElementsFromData(data) {
|
||
|
data.forEach(item => {
|
||
|
let element = this.pureCreateElement(item)
|
||
|
element.isActive = false
|
||
|
element.isCreating = false
|
||
|
this.addElement(element)
|
||
|
})
|
||
|
this.app.group.initIdToElementList(this.elementList)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 是否存在激活元素
|
||
|
hasActiveElement() {
|
||
|
return !!this.activeElement
|
||
|
}
|
||
|
|
||
|
// 设置激活元素
|
||
|
setActiveElement(element) {
|
||
|
this.cancelActiveElement()
|
||
|
this.activeElement = element
|
||
|
if (element) {
|
||
|
element.isActive = true
|
||
|
}
|
||
|
this.app.emit('activeElementChange', this.activeElement)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 取消当前激活元素
|
||
|
cancelActiveElement() {
|
||
|
if (!this.hasActiveElement()) {
|
||
|
return this
|
||
|
}
|
||
|
this.activeElement.isActive = false
|
||
|
this.activeElement = null
|
||
|
this.app.emit('activeElementChange', this.activeElement)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 检测是否点击选中元素
|
||
|
checkIsHitElement(e) {
|
||
|
// 判断是否选中元素
|
||
|
let x = e.unGridClientX
|
||
|
let y = e.unGridClientY
|
||
|
// 从后往前遍历元素,默认认为新创建的元素在上一层
|
||
|
for (let i = this.elementList.length - 1; i >= 0; i--) {
|
||
|
let element = this.elementList[i]
|
||
|
if (element.isHit(x, y)) {
|
||
|
return element
|
||
|
}
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
|
||
|
// 纯创建元素
|
||
|
pureCreateElement(opts = {}) {
|
||
|
switch (opts.type) {
|
||
|
case 'rectangle':
|
||
|
return new Rectangle(opts, this.app)
|
||
|
case 'diamond':
|
||
|
return new Diamond(opts, this.app)
|
||
|
case 'triangle':
|
||
|
return new Triangle(opts, this.app)
|
||
|
case 'circle':
|
||
|
return new Circle(opts, this.app)
|
||
|
case 'freedraw':
|
||
|
return new Freedraw(opts, this.app)
|
||
|
case 'image':
|
||
|
return new Image(opts, this.app)
|
||
|
case 'arrow':
|
||
|
return new Arrow(opts, this.app)
|
||
|
case 'line':
|
||
|
return new Line(opts, this.app)
|
||
|
case 'text':
|
||
|
return new Text(opts, this.app)
|
||
|
default:
|
||
|
return null
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 创建元素
|
||
|
createElement(opts = {}, callback = () => {}, ctx = null, notActive) {
|
||
|
if (this.hasActiveElement() || this.isCreatingElement) {
|
||
|
return this
|
||
|
}
|
||
|
let element = this.pureCreateElement(opts)
|
||
|
if (!element) {
|
||
|
return this
|
||
|
}
|
||
|
this.addElement(element)
|
||
|
if (!notActive) {
|
||
|
this.setActiveElement(element)
|
||
|
}
|
||
|
this.isCreatingElement = true
|
||
|
callback.call(ctx, element)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 复制元素
|
||
|
copyElement(element, notActive = false, pos) {
|
||
|
return new Promise(async resolve => {
|
||
|
if (!element) {
|
||
|
return resolve()
|
||
|
}
|
||
|
let data = this.app.group.handleCopyElementData(element.serialize())
|
||
|
// 图片元素需要先加载图片
|
||
|
if (data.type === 'image') {
|
||
|
data.imageObj = await createImageObj(data.url)
|
||
|
}
|
||
|
this.createElement(
|
||
|
data,
|
||
|
element => {
|
||
|
this.app.group.handleCopyElement(element)
|
||
|
element.startResize(DRAG_ELEMENT_PARTS.BODY)
|
||
|
// 默认偏移原图形20像素
|
||
|
let ox = 20
|
||
|
let oy = 20
|
||
|
// 指定了具体坐标则使用具体坐标
|
||
|
if (pos) {
|
||
|
ox = pos.x - element.x - element.width / 2
|
||
|
oy = pos.y - element.y - element.height / 2
|
||
|
}
|
||
|
// 如果开启了网格,那么要坐标要吸附到网格
|
||
|
let gridAdsorbentPos = this.app.coordinate.gridAdsorbent(ox, oy)
|
||
|
element.resize(
|
||
|
null,
|
||
|
null,
|
||
|
null,
|
||
|
gridAdsorbentPos.x,
|
||
|
gridAdsorbentPos.y
|
||
|
)
|
||
|
element.isCreating = false
|
||
|
if (notActive) {
|
||
|
element.isActive = false
|
||
|
}
|
||
|
this.isCreatingElement = false
|
||
|
resolve(element)
|
||
|
},
|
||
|
this,
|
||
|
notActive
|
||
|
)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 正在创建类矩形元素
|
||
|
creatingRectangleLikeElement(type, x, y, offsetX, offsetY) {
|
||
|
this.createElement({
|
||
|
type,
|
||
|
x: x,
|
||
|
y: y,
|
||
|
width: offsetX,
|
||
|
height: offsetY
|
||
|
})
|
||
|
this.activeElement.updateSize(offsetX, offsetY)
|
||
|
}
|
||
|
|
||
|
// 正在创建圆形元素
|
||
|
creatingCircle(x, y, e) {
|
||
|
this.createElement({
|
||
|
type: 'circle',
|
||
|
x: x,
|
||
|
y: y
|
||
|
})
|
||
|
let radius = getTowPointDistance(e.clientX, e.clientY, x, y)
|
||
|
this.activeElement.updateSize(radius, radius)
|
||
|
}
|
||
|
|
||
|
// 正在创建自由画笔元素
|
||
|
creatingFreedraw(e, event) {
|
||
|
this.createElement({
|
||
|
type: 'freedraw'
|
||
|
})
|
||
|
let element = this.activeElement
|
||
|
// 计算画笔粗细
|
||
|
let lineWidth = computedLineWidthBySpeed(
|
||
|
event.mouseSpeed,
|
||
|
element.lastLineWidth
|
||
|
)
|
||
|
element.lastLineWidth = lineWidth
|
||
|
element.addPoint(e.clientX, e.clientY, lineWidth)
|
||
|
// 绘制自由线不重绘,采用增量绘制,否则会卡顿
|
||
|
let { coordinate, ctx, state } = this.app
|
||
|
// 事件对象的坐标默认是加上了画布偏移量的,临时绘制的时候不需要,所以需要减去
|
||
|
let tfp = coordinate.transformToCanvasCoordinate(
|
||
|
coordinate.subScrollX(event.lastMousePos.x),
|
||
|
coordinate.subScrollY(event.lastMousePos.y)
|
||
|
)
|
||
|
let ttp = coordinate.transformToCanvasCoordinate(
|
||
|
coordinate.subScrollX(e.clientX),
|
||
|
coordinate.subScrollY(e.clientY)
|
||
|
)
|
||
|
ctx.save()
|
||
|
ctx.scale(state.scale, state.scale)
|
||
|
element.singleRender(tfp.x, tfp.y, ttp.x, ttp.y, lineWidth)
|
||
|
ctx.restore()
|
||
|
}
|
||
|
|
||
|
// 正在创建图片元素
|
||
|
creatingImage(e, { width, height, imageObj, url, ratio }) {
|
||
|
// 吸附到网格,如果网格开启的话
|
||
|
let gp = this.app.coordinate.gridAdsorbent(
|
||
|
e.unGridClientX - width / 2,
|
||
|
e.unGridClientY - height / 2
|
||
|
)
|
||
|
this.createElement({
|
||
|
type: 'image',
|
||
|
x: gp.x,
|
||
|
y: gp.y,
|
||
|
url: url,
|
||
|
imageObj: imageObj,
|
||
|
width: width,
|
||
|
height: height,
|
||
|
ratio: ratio
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 正在编辑文本元素
|
||
|
editingText(element) {
|
||
|
if (element.type !== 'text') {
|
||
|
return
|
||
|
}
|
||
|
element.noRender = true
|
||
|
this.setActiveElement(element)
|
||
|
}
|
||
|
|
||
|
// 完成文本元素的编辑
|
||
|
completeEditingText() {
|
||
|
let element = this.activeElement
|
||
|
if (!element || element.type !== 'text') {
|
||
|
return
|
||
|
}
|
||
|
if (!element.text.trim()) {
|
||
|
// 没有输入则删除该文字元素
|
||
|
this.deleteElement(element)
|
||
|
this.setActiveElement(null)
|
||
|
return
|
||
|
}
|
||
|
element.noRender = false
|
||
|
}
|
||
|
|
||
|
// 完成箭头元素的创建
|
||
|
completeCreateArrow(e) {
|
||
|
this.activeElement.addPoint(e.clientX, e.clientY)
|
||
|
}
|
||
|
|
||
|
// 正在创建箭头元素
|
||
|
creatingArrow(x, y, e) {
|
||
|
this.createElement(
|
||
|
{
|
||
|
type: 'arrow',
|
||
|
x,
|
||
|
y
|
||
|
},
|
||
|
element => {
|
||
|
element.addPoint(x, y)
|
||
|
}
|
||
|
)
|
||
|
this.activeElement.updateFictitiousPoint(e.clientX, e.clientY)
|
||
|
}
|
||
|
|
||
|
// 正在创建线段/折线元素
|
||
|
creatingLine(x, y, e, isSingle = false, notCreate = false) {
|
||
|
if (!notCreate) {
|
||
|
this.createElement(
|
||
|
{
|
||
|
type: 'line',
|
||
|
x,
|
||
|
y,
|
||
|
isSingle
|
||
|
},
|
||
|
element => {
|
||
|
element.addPoint(x, y)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
let element = this.activeElement
|
||
|
if (element) {
|
||
|
element.updateFictitiousPoint(e.clientX, e.clientY)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 完成线段/折线元素的创建
|
||
|
completeCreateLine(e, completeCallback = () => {}) {
|
||
|
let element = this.activeElement
|
||
|
let x = e.clientX
|
||
|
let y = e.clientY
|
||
|
if (element && element.isSingle) {
|
||
|
// 单根线段模式,鼠标松开则代表绘制完成
|
||
|
element.addPoint(x, y)
|
||
|
completeCallback()
|
||
|
} else {
|
||
|
// 绘制折线模式,鼠标松开代表固定一个端点
|
||
|
this.createElement({
|
||
|
type: 'line',
|
||
|
isSingle: false
|
||
|
})
|
||
|
element = this.activeElement
|
||
|
element.addPoint(x, y)
|
||
|
element.updateFictitiousPoint(x, y)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 创建元素完成
|
||
|
completeCreateElement() {
|
||
|
this.isCreatingElement = false
|
||
|
let element = this.activeElement
|
||
|
if (!element) {
|
||
|
return this
|
||
|
}
|
||
|
// 由多个端点构成的元素需要根据端点计算外包围框
|
||
|
if (['freedraw', 'arrow', 'line'].includes(element.type)) {
|
||
|
element.updateMultiPointBoundingRect()
|
||
|
}
|
||
|
element.isCreating = false
|
||
|
this.app.emitChange()
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 为激活元素设置样式
|
||
|
setActiveElementStyle(style = {}) {
|
||
|
if (!this.hasActiveElement()) {
|
||
|
return this
|
||
|
}
|
||
|
Object.keys(style).forEach(key => {
|
||
|
this.activeElement.style[key] = style[key]
|
||
|
if (key === 'fontSize' && this.activeElement.type === 'text') {
|
||
|
this.activeElement.updateTextSize()
|
||
|
}
|
||
|
})
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// 检测指定位置是否在元素调整手柄上
|
||
|
checkInResizeHand(x, y) {
|
||
|
// 按住了拖拽元素的某个部分
|
||
|
let element = this.activeElement
|
||
|
let hand = element.dragElement.checkPointInDragElementWhere(x, y)
|
||
|
if (hand) {
|
||
|
return {
|
||
|
element,
|
||
|
hand
|
||
|
}
|
||
|
}
|
||
|
return null
|
||
|
}
|
||
|
|
||
|
// 检查是否需要进行元素调整操作
|
||
|
checkIsResize(x, y, e) {
|
||
|
if (!this.hasActiveElement()) {
|
||
|
return false
|
||
|
}
|
||
|
let res = this.checkInResizeHand(x, y)
|
||
|
if (res) {
|
||
|
this.isResizing = true
|
||
|
this.resizingElement = res.element
|
||
|
this.resizingElement.startResize(res.hand, e)
|
||
|
this.app.cursor.setResize(res.hand)
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// 进行元素调整操作
|
||
|
handleResize(...args) {
|
||
|
if (!this.isResizing) {
|
||
|
return
|
||
|
}
|
||
|
this.resizingElement.resize(...args)
|
||
|
this.app.render.render()
|
||
|
}
|
||
|
|
||
|
// 结束元素调整操作
|
||
|
endResize() {
|
||
|
this.isResizing = false
|
||
|
this.resizingElement.endResize()
|
||
|
this.resizingElement = null
|
||
|
}
|
||
|
}
|