@zhangqingcq/vgce
Version:
Vector graphics configure editor. svg组态编辑器。基于vue3.3+ts+element-plus+vite
589 lines (571 loc) • 17.2 kB
text/typescript
import type { ISystemStraightLine } from '@/components/config/types'
import { ELineBindAnchors } from '@/components/config/types'
import type { IConfigItem } from '@/config/types'
import { EDoneJsonType } from '@/config/types'
import type { IDoneJson, IPointCoordinate } from '@/stores/global/types'
import { EGlobalStoreIntention, EMouseInfoState } from '@/stores/global/types'
import { useGlobalStore } from '@/stores/global'
import { pinia } from '@/hooks'
import { useConfigStore } from '@/stores/config'
import { useSvgEditLayoutStore } from '@/stores/svg-edit-layout'
import { kebabCase } from 'lodash-es'
import { vueComp } from '@/config'
export const stopEvent = (e: any) => {
e.stopPropagation()
}
export const preventDefault = (e: any) => {
e.preventDefault()
}
export const myFixed = (d: number, n: number) => {
return Number(d.toFixed(n))
}
export function componentsRegister(data?: Record<string, any>) {
//注册所有组件
const instance = getCurrentInstance()
const t = data ? Object.assign(vueComp, data) : vueComp
for (let key in t) {
if (!instance?.appContext?.components.hasOwnProperty(key) && vueComp.hasOwnProperty(key)) {
instance?.appContext?.app.component(key, vueComp[key])
}
}
}
export function setEditorLoadTime() {
//记录编辑器加载到浏览器的时间,有些组件需要用这个时间
window.sessionStorage.setItem('editorLoadTime', new Date().getTime().toString())
}
/**
* 生成随机字符串
* @param len 生成个数
*/
export const randomString = (len?: number) => {
len = len || 10
const str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const maxPos = str.length
let random_str = ''
for (let i = 0; i < len; i++) {
let t = maxPos
if (i === 0) {
t = maxPos - 10
}
random_str += str.charAt(Math.floor(Math.random() * t))
}
return random_str
}
// 通过泛型定义通用类型保护函数
export const isOfType = <T>(target: unknown, prop: keyof T): target is T => {
return (target as T)[prop] !== undefined
}
/**
* 获取坐标偏移量
* @param length 真实宽/高
* @param scale 缩放倍数
* @returns 坐标偏移量
*/
export const getCoordinateOffset = (length: number, scale: number) => {
return (length / 2) * (scale - 1)
}
// 角度转弧度
// Math.PI = 180 度
export const angleToRadian = (angle: number) => {
return (angle * Math.PI) / 180
}
/**
* 计算根据圆心旋转后的点的坐标
* @param point 旋转前的点坐标
* @param center 旋转中心
* @param rotate 旋转的角度
* @return 旋转后的坐标
* https://www.zhihu.com/question/67425734/answer/252724399 旋转矩阵公式
*/
export const calculateRotatedPointCoordinate = (
point: { x: number; y: number },
center: { x: number; y: number },
rotate: number
) => {
/**
* 旋转公式:
* 点a(x, y)
* 旋转中心c(x, y)
* 旋转后点n(x, y)
* 旋转角度θ tan ??
* nx = cosθ * (ax - cx) - sinθ * (ay - cy) + cx
* ny = sinθ * (ax - cx) + cosθ * (ay - cy) + cy
*/
return {
x: myFixed(
(point.x - center.x) * Math.cos(angleToRadian(rotate)) -
(point.y - center.y) * Math.sin(angleToRadian(rotate)) +
center.x,
1
),
y: myFixed(
(point.x - center.x) * Math.sin(angleToRadian(rotate)) +
(point.y - center.y) * Math.cos(angleToRadian(rotate)) +
center.y,
1
)
}
}
// 求两点之间的中点坐标
export const getCenterPoint = (p1: { x: number; y: number }, p2: { x: number; y: number }) => {
return {
x: p1.x + (p2.x - p1.x) / 2,
y: p1.y + (p2.y - p1.y) / 2
}
}
/**
* 坐标数组转换成path路径
* @param position_arr
* @returns
*/
export const positionArrToPath = (position_arr: { x: number; y: number }[]) => {
let path_str = ''
for (let index = 0; index < position_arr.length; index++) {
if (index === 0) {
path_str += `M ${position_arr[index].x} ${position_arr[index].y}`
} else {
path_str += ` L ${position_arr[index].x} ${position_arr[index].y}`
}
}
return path_str
}
/**
* 获取相对于svg最新的坐标
* @param init_pos 原中心坐标
* @param finally_pos 新中心坐标
* @param svg_init_pos 在画布中的的原坐标
* @returns svg最新的坐标
*/
export const getSvgNowPosition = (init_pos: number, finally_pos: number, svg_init_pos: number) => {
return svg_init_pos + (finally_pos - init_pos)
}
/**
* 对象深拷贝
* @param object
* @param default_val
* @returns
*/
export const objectDeepClone = <T>(object: Record<keyof any, any>, default_val: any = {}) => {
if (!object) {
return default_val as T
}
return JSON.parse(JSON.stringify(object)) as T
}
/**
* 设置实际的属性
* @param done_json
* @param resize
*/
export const setSvgActualInfo = (done_json: IDoneJson, resize?: boolean) => {
const queryBbox = document.querySelector(`#${done_json.id}`)
const rectBBox = document.querySelector(`#rect${done_json.id}`)
if (queryBbox) {
let x: number = 0,
y: number = 0,
width: number = 0,
height: number = 0
if (done_json.type === EDoneJsonType.Vue) {
width = done_json.props.width?.val || (queryBbox as HTMLElement).offsetWidth || 100
height = done_json.props.height?.val || (queryBbox as HTMLElement).offsetHeight || 100
x = 50 - width / 2
y = 50 - height / 2
const foreignObjectBox = document.querySelector(`#foreign-object${done_json.id}`)
if (
foreignObjectBox &&
((foreignObjectBox.getAttribute('x') === '0' &&
foreignObjectBox.getAttribute('y') === '0' &&
foreignObjectBox.getAttribute('width') === '0' &&
foreignObjectBox.getAttribute('height') === '0') ||
resize)
) {
foreignObjectBox.setAttribute('x', x.toString())
foreignObjectBox.setAttribute('y', y.toString())
foreignObjectBox.setAttribute('width', width.toString())
foreignObjectBox.setAttribute('height', height.toString())
}
} else {
const BBox = (queryBbox as SVGGraphicsElement).getBBox()
x = myFixed(BBox.x, 0)
y = myFixed(BBox.y, 0)
width = myFixed(BBox.width, 0)
height = myFixed(BBox.height, 0)
}
if (
rectBBox &&
((rectBBox.getAttribute('x') === '0' &&
rectBBox.getAttribute('y') === '0' &&
rectBBox.getAttribute('width') === '0' &&
rectBBox.getAttribute('height') === '0') ||
resize)
) {
rectBBox.setAttribute('x', x.toString())
rectBBox.setAttribute('y', y.toString())
rectBBox.setAttribute('width', width.toString())
rectBBox.setAttribute('height', height.toString())
}
//实际大小和坐标理论上不会变 但是如果子组件设置了100% 还是会变 所以要做下判断
if (
(done_json.actual_bound.x === 0 &&
done_json.actual_bound.y === 0 &&
done_json.actual_bound.width === 0 &&
done_json.actual_bound.height === 0) ||
resize
) {
done_json.actual_bound = {
x,
y,
width,
height
}
}
done_json.center_position = {
x: done_json.x + done_json.actual_bound.x + width / 2,
y: done_json.y + done_json.actual_bound.y + height / 2
}
done_json.point_coordinate.tl = {
x: done_json.center_position.x - (width * done_json.scale_x) / 2,
y: done_json.center_position.y - (height * done_json.scale_y) / 2
}
done_json.point_coordinate.tc = {
x: done_json.center_position.x,
y: done_json.center_position.y - (height * done_json.scale_y) / 2
}
done_json.point_coordinate.tr = {
x: done_json.center_position.x + (width * done_json.scale_x) / 2,
y: done_json.center_position.y - (height * done_json.scale_y) / 2
}
done_json.point_coordinate.l = {
x: done_json.center_position.x - (width * done_json.scale_x) / 2,
y: done_json.center_position.y
}
done_json.point_coordinate.r = {
x: done_json.center_position.x + (width * done_json.scale_x) / 2,
y: done_json.center_position.y
}
done_json.point_coordinate.bl = {
x: done_json.center_position.x - (width * done_json.scale_x) / 2,
y: done_json.center_position.y + (height * done_json.scale_y) / 2
}
done_json.point_coordinate.bc = {
x: done_json.center_position.x,
y: done_json.center_position.y + (height * done_json.scale_y) / 2
}
done_json.point_coordinate.br = {
x: done_json.center_position.x + (width * done_json.scale_x) / 2,
y: done_json.center_position.y + (height * done_json.scale_y) / 2
}
if (done_json.rotate !== 0) {
setAfterRotationPointCoordinate(done_json)
}
moveAnchors(done_json)
}
}
/**
* 重置旧八点坐标
* @param done_json
*/
export const resetHandlePointOld = (done_json: IDoneJson) => {
for (const k of Object.keys(done_json.point_coordinate)) {
if (done_json.point_coordinate_old) {
done_json.point_coordinate_old[k as keyof IPointCoordinate].x = 0
done_json.point_coordinate_old[k as keyof IPointCoordinate].y = 0
}
}
}
/**
* 移动八点坐标
* @param done_json 当前组件
* @param x x轴移动量
* @param y y轴移动量
*/
export const moveHandlePoint = (done_json: IDoneJson, x?: number, y?: number) => {
const globalStore = useGlobalStore(pinia)
const _x = x ?? globalStore.mouse_info.new_position_x - globalStore.mouse_info.position_x
const _y = y ?? globalStore.mouse_info.new_position_y - globalStore.mouse_info.position_y
for (const k of Object.keys(done_json.point_coordinate)) {
if (x !== undefined && y !== undefined) {
done_json.point_coordinate[k as keyof IPointCoordinate].x += _x
done_json.point_coordinate[k as keyof IPointCoordinate].y += _y
} else if (done_json.point_coordinate_old) {
done_json.point_coordinate[k as keyof IPointCoordinate].x =
done_json.point_coordinate_old[k as keyof IPointCoordinate].x + _x
done_json.point_coordinate[k as keyof IPointCoordinate].y =
done_json.point_coordinate_old[k as keyof IPointCoordinate].y + _y
}
}
}
/**
* 移动绑定锚点的线
* @param done_json
*/
export const moveAnchors = (done_json: IDoneJson) => {
const globalStore = useGlobalStore(pinia)
for (let d of globalStore.done_json) {
if (d.type === EDoneJsonType.ConnectionLine) {
if (d.bind_anchors?.start?.target_id === done_json.id) {
const a = getAnchorPosByAnchorType(d.bind_anchors.start.type, done_json)
d.props.point_position.val[0] = {
x: a.x - d.x,
y: a.y - d.y
}
}
if (d.bind_anchors?.end?.target_id === done_json.id) {
const a = getAnchorPosByAnchorType(d.bind_anchors.end.type, done_json)
d.props.point_position.val[d.props.point_position.val.length - 1] = {
x: a.x - d.x,
y: a.y - d.y
}
}
}
}
}
/**
* 解绑连线的锚点
* @param id 被绑定的组件的id
*/
export const unbindAnchors = (id: string) => {
const globalStore = useGlobalStore(pinia)
for (let d of globalStore.done_json) {
if (d.type === EDoneJsonType.ConnectionLine) {
if (d.bind_anchors?.start?.target_id === id) {
d.bind_anchors.start = null
}
if (d.bind_anchors?.end?.target_id === id) {
d.bind_anchors.end = null
}
}
}
}
/**
* 根据锚点类型获取锚点坐标
* @param anchor_type
* @param done_json
* @returns
*/
export const getAnchorPosByAnchorType = (anchor_type: ELineBindAnchors, done_json: IDoneJson) => {
if (anchor_type === ELineBindAnchors.BottomCenter) {
return done_json.point_coordinate.bc
} else if (anchor_type === ELineBindAnchors.Left) {
return done_json.point_coordinate.l
} else if (anchor_type === ELineBindAnchors.Right) {
return done_json.point_coordinate.r
} else {
return done_json.point_coordinate.tc
}
}
/**
* 旋转之后重新设置组件八点坐标
* @param item
*/
export const setAfterRotationPointCoordinate = (item: IDoneJson) => {
item.point_coordinate = {
tl: calculateRotatedPointCoordinate(item.point_coordinate.tl, item.center_position, item.rotate),
tc: calculateRotatedPointCoordinate(item.point_coordinate.tc, item.center_position, item.rotate),
tr: calculateRotatedPointCoordinate(item.point_coordinate.tr, item.center_position, item.rotate),
l: calculateRotatedPointCoordinate(item.point_coordinate.l, item.center_position, item.rotate),
r: calculateRotatedPointCoordinate(item.point_coordinate.r, item.center_position, item.rotate),
bl: calculateRotatedPointCoordinate(item.point_coordinate.bl, item.center_position, item.rotate),
bc: calculateRotatedPointCoordinate(item.point_coordinate.bc, item.center_position, item.rotate),
br: calculateRotatedPointCoordinate(item.point_coordinate.br, item.center_position, item.rotate)
}
}
export const prosToVBind = (item: IConfigItem, ignore: any[] = []) => {
let temp: Record<string, any> = {}
if (item.state) {
for (const key in item.state) {
if (item.state.hasOwnProperty(key) && key === item.defaultState) {
for (let _p in item.state[key]) {
if (_p !== 'label') {
temp[kebabCase(_p)] = item.state[key][_p]
}
}
break
}
}
}
for (const key in item.props) {
if (ignore.indexOf(key) < 0) {
temp[kebabCase(key)] = item.props[key].val
}
}
return temp
}
export const setArrItemByID = (id: string, key: string, val: any, json_arr: IDoneJson[]) => {
return new Promise((res) => {
const find_item = json_arr.find((f) => f.id === id)
if (!find_item) {
res({
status: false,
msg: '要设置的id不存在'
})
}
eval(`find_item.${key} = val;`)
res({
status: true,
msg: '操作成功'
})
})
}
export const getCommonClass = (item: IDoneJson) => {
if (!item.common_animations || !item.common_animations.val) {
return ``
}
return `common-ani animate__animated animate__${item.common_animations.val} animate__${item.common_animations.speed} animate__${item.common_animations.repeat} animate__${item.common_animations.delay}`
}
export const numberArray = (l: number) => {
let t: number[] = []
for (let i = 0; i < l; i++) {
t.push(i)
}
return t
}
/*获取字符串width*/
export const getStringWidth = (str: string, fontSize = 12) => {
if (str.length > 0) {
let nodesH = document.createElement('span')
nodesH.style.fontSize = fontSize + 'px'
nodesH.style.fontFamily = 'inherit'
nodesH.innerHTML = str
nodesH.style.opacity = '0'
nodesH.style.position = 'fixed'
nodesH.style.top = '3000px'
document.body.append(nodesH)
const width = nodesH.clientWidth
document.body.removeChild(nodesH)
return width
}
return 0
}
export const valFormat = (v: any) => {
if (/false|true/.test(v)) {
return v !== 'false'
}
if (/^\d+(\.\d+)?$/.test(v)) {
return Number(v)
}
return v
}
/**
* 创建连线
* @param e 事件对象
* @param type 锚点位置
* @param itemInfo 被绑定组件
*/
export const createLine = (e: MouseEvent, type?: ELineBindAnchors, itemInfo?: IDoneJson) => {
e.preventDefault()
const globalStore = useGlobalStore(pinia)
const configStore = useConfigStore(pinia)
const svgEditLayoutStore = useSvgEditLayoutStore(pinia)
const { clientX, clientY } = e
let create_line_info = objectDeepClone<ISystemStraightLine>(configStore.connection_line)
//以后顶部可以选择连线是哪种 直线先不做
/*if (false) {
create_line_info = straight_line_system
}*/
let x: number = 0
let y: number = 0
if (type && itemInfo) {
create_line_info.bind_anchors.start = {
type: type,
target_id: itemInfo.id
}
let t = getAnchorPosByAnchorType(type, itemInfo)
x = t.x
y = t.y
} else {
x = Math.round(
(clientX - svgEditLayoutStore.canvasInfo.left) / configStore.svg.scale - svgEditLayoutStore.center_offset.x
)
y = Math.round(
(clientY - svgEditLayoutStore.canvasInfo.top) / configStore.svg.scale - svgEditLayoutStore.center_offset.y
)
}
const done_item_json = {
id: randomString(),
x: x,
y: y,
client: {
x: x,
y: y
},
scale_x: 1,
scale_y: 1,
rotate: 0,
actual_bound: {
x: 0,
y: 0,
width: 0,
height: 0
},
center_position: {
x: 0,
y: 0
},
point_coordinate: {
tl: {
x: 0,
y: 0
},
tc: {
x: 0,
y: 0
},
tr: {
x: 0,
y: 0
},
l: {
x: 0,
y: 0
},
r: {
x: 0,
y: 0
},
bl: {
x: 0,
y: 0
},
bc: {
x: 0,
y: 0
},
br: {
x: 0,
y: 0
}
},
...create_line_info
}
done_item_json.props.point_position.val.push({
x: 0,
y: 0
})
globalStore.setDoneJson(done_item_json)
globalStore.setHandleSvgInfo(done_item_json, globalStore.done_json.length - 1)
globalStore.intention = EGlobalStoreIntention.Connection
globalStore.mouse_info = {
state: EMouseInfoState.Down,
position_x: x,
position_y: y,
now_position_x: x,
now_position_y: y,
new_position_x: 0,
new_position_y: 0
}
}
export const getZoomPosition = (e: Record<string, any>, scale: any, center: Record<string, any>, isAdd: boolean) => {
/*鼠标位置*/
const offsetX = e.layerX
const offsetY = e.layerY
/*上次的画布位置*/
const tx = center.x
const ty = center.y
/*上次的scale*/
const ts = myFixed(isAdd ? scale - 0.1 : scale + 0.1, 1)
/*画布在没有缩放的时候移动位置*/
const lx = myFixed(tx + (offsetX * (ts - 1)) / ts, 1)
const ly = myFixed(ty + (offsetY * (ts - 1)) / ts, 1)
center.x = myFixed(lx / scale - ((offsetX - lx) * (scale - 1)) / scale, 2)
center.y = myFixed(ly / scale - ((offsetY - ly) * (scale - 1)) / scale, 2)
}