useDragElement.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { Ref, computed } from 'vue'
  2. import { useStore } from 'vuex'
  3. import { State, MutationTypes } from '@/store'
  4. import { ElementTypes, PPTElement } from '@/types/slides'
  5. import { AlignmentLineProps } from '@/types/edit'
  6. import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
  7. import { getRectRotatedRange, AlignLine, uniqAlignLines } from '@/utils/element'
  8. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  9. export default (
  10. elementList: Ref<PPTElement[]>,
  11. activeGroupElementId: Ref<string>,
  12. alignmentLines: Ref<AlignmentLineProps[]>,
  13. ) => {
  14. const store = useStore<State>()
  15. const activeElementIdList = computed(() => store.state.activeElementIdList)
  16. const canvasScale = computed(() => store.state.canvasScale)
  17. const { addHistorySnapshot } = useHistorySnapshot()
  18. const dragElement = (e: MouseEvent, element: PPTElement) => {
  19. if(!activeElementIdList.value.includes(element.id)) return
  20. let isMouseDown = true
  21. // 可视范围宽高,用于边缘对齐吸附
  22. const edgeWidth = VIEWPORT_SIZE
  23. const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
  24. const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
  25. const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.id))
  26. const sorptionRange = 3
  27. const elOriginLeft = element.left
  28. const elOriginTop = element.top
  29. const elOriginWidth = element.width
  30. const elOriginHeight = ('height' in element && element.height) ? element.height : 0
  31. const elOriginRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
  32. const startPageX = e.pageX
  33. const startPageY = e.pageY
  34. let isMisoperation: boolean | null = null
  35. const isActiveGroupElement = element.id === activeGroupElementId.value
  36. // 收集对齐参考线
  37. // 包括页面内出被操作元素以外的所有元素在页面内水平和垂直方向的范围和中心位置、页面边界和水平和垂直的中心位置
  38. let horizontalLines: AlignLine[] = []
  39. let verticalLines: AlignLine[] = []
  40. // 元素在页面内水平和垂直方向的范围和中心位置(需要特殊计算线条和被旋转的元素)
  41. for(const el of elementList.value) {
  42. if(el.type === ElementTypes.LINE) continue
  43. if(isActiveGroupElement && el.id === element.id) continue
  44. if(!isActiveGroupElement && activeElementIdList.value.includes(el.id)) continue
  45. let left, top, width, height
  46. if('rotate' in el && el.rotate) {
  47. const { xRange, yRange } = getRectRotatedRange({
  48. left: el.left,
  49. top: el.top,
  50. width: el.width,
  51. height: el.height,
  52. rotate: el.rotate,
  53. })
  54. left = xRange[0]
  55. top = yRange[0]
  56. width = xRange[1] - xRange[0]
  57. height = yRange[1] - yRange[0]
  58. }
  59. else {
  60. left = el.left
  61. top = el.top
  62. width = el.width
  63. height = el.height
  64. }
  65. const right = left + width
  66. const bottom = top + height
  67. const centerX = top + height / 2
  68. const centerY = left + width / 2
  69. const topLine: AlignLine = { value: top, range: [left, right] }
  70. const bottomLine: AlignLine = { value: bottom, range: [left, right] }
  71. const horizontalCenterLine: AlignLine = { value: centerX, range: [left, right] }
  72. const leftLine: AlignLine = { value: left, range: [top, bottom] }
  73. const rightLine: AlignLine = { value: right, range: [top, bottom] }
  74. const verticalCenterLine: AlignLine = { value: centerY, range: [top, bottom] }
  75. horizontalLines.push(topLine, bottomLine, horizontalCenterLine)
  76. verticalLines.push(leftLine, rightLine, verticalCenterLine)
  77. }
  78. // 页面边界、水平和垂直的中心位置
  79. const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] }
  80. const edgeBottomLine: AlignLine = { value: edgeHeight, range: [0, edgeWidth] }
  81. const edgeHorizontalCenterLine: AlignLine = { value: edgeHeight / 2, range: [0, edgeWidth] }
  82. const edgeLeftLine: AlignLine = { value: 0, range: [0, edgeHeight] }
  83. const edgeRightLine: AlignLine = { value: edgeWidth, range: [0, edgeHeight] }
  84. const edgeVerticalCenterLine: AlignLine = { value: edgeWidth / 2, range: [0, edgeHeight] }
  85. horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine)
  86. verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine)
  87. // 参考线去重
  88. horizontalLines = uniqAlignLines(horizontalLines)
  89. verticalLines = uniqAlignLines(verticalLines)
  90. document.onmousemove = e => {
  91. const currentPageX = e.pageX
  92. const currentPageY = e.pageY
  93. // 对于鼠标第一次滑动距离过小的操作判定为误操作
  94. // 这里仅在误操作标记未被赋值(null,第一次触发移动),以及被标记为误操作时(true,当前处于误操作范围,但可能会脱离该范围转变成正常操作),才会去计算
  95. // 已经被标记为非误操作时(false),不需要再次计算(因为不可能从非误操作转变成误操作)
  96. if(isMisoperation !== false) {
  97. isMisoperation = Math.abs(startPageX - currentPageX) < sorptionRange &&
  98. Math.abs(startPageY - currentPageY) < sorptionRange
  99. }
  100. if(!isMouseDown || isMisoperation) return
  101. // 鼠标按下后移动的距离
  102. const moveX = (currentPageX - startPageX) / canvasScale.value
  103. const moveY = (currentPageY - startPageY) / canvasScale.value
  104. // 被操作元素需要移动到的位置
  105. let targetLeft = elOriginLeft + moveX
  106. let targetTop = elOriginTop + moveY
  107. // 计算被操作元素在页面中的范围(用于吸附对齐)
  108. // 需要区分计算:多选状态、线条、被旋转的元素
  109. // 注意这里需要用元素的原始信息结合移动信息来计算
  110. let targetMinX: number, targetMaxX: number, targetMinY: number, targetMaxY: number
  111. if(activeElementIdList.value.length === 1 || isActiveGroupElement) {
  112. if(elOriginRotate) {
  113. const { xRange, yRange } = getRectRotatedRange({
  114. left: targetLeft,
  115. top: targetTop,
  116. width: elOriginWidth,
  117. height: elOriginHeight,
  118. rotate: elOriginRotate,
  119. })
  120. targetMinX = xRange[0]
  121. targetMaxX = xRange[1]
  122. targetMinY = yRange[0]
  123. targetMaxY = yRange[1]
  124. }
  125. else if(element.type === ElementTypes.LINE) {
  126. targetMinX = targetLeft
  127. targetMaxX = targetLeft + Math.max(element.start[0], element.end[0])
  128. targetMinY = targetTop
  129. targetMaxY = targetTop + Math.max(element.start[1], element.end[1])
  130. }
  131. else {
  132. targetMinX = targetLeft
  133. targetMaxX = targetLeft + elOriginWidth
  134. targetMinY = targetTop
  135. targetMaxY = targetTop + elOriginHeight
  136. }
  137. }
  138. else {
  139. const leftValues = []
  140. const topValues = []
  141. const rightValues = []
  142. const bottomValues = []
  143. for(let i = 0; i < originActiveElementList.length; i++) {
  144. const element = originActiveElementList[i]
  145. const left = element.left + moveX
  146. const top = element.top + moveY
  147. const width = element.width
  148. const height = ('height' in element && element.height) ? element.height : 0
  149. const rotate = ('rotate' in element && element.rotate) ? element.rotate : 0
  150. if('rotate' in element && element.rotate) {
  151. const { xRange, yRange } = getRectRotatedRange({ left, top, width, height, rotate })
  152. leftValues.push(xRange[0])
  153. topValues.push(yRange[0])
  154. rightValues.push(xRange[1])
  155. bottomValues.push(yRange[1])
  156. }
  157. else if(element.type === ElementTypes.LINE) {
  158. leftValues.push(left)
  159. topValues.push(top)
  160. rightValues.push(left + Math.max(element.start[0], element.end[0]))
  161. bottomValues.push(top + Math.max(element.start[1], element.end[1]))
  162. }
  163. else {
  164. leftValues.push(left)
  165. topValues.push(top)
  166. rightValues.push(left + width)
  167. bottomValues.push(top + height)
  168. }
  169. }
  170. targetMinX = Math.min(...leftValues)
  171. targetMaxX = Math.max(...rightValues)
  172. targetMinY = Math.min(...topValues)
  173. targetMaxY = Math.max(...bottomValues)
  174. }
  175. const targetCenterX = targetMinX + (targetMaxX - targetMinX) / 2
  176. const targetCenterY = targetMinY + (targetMaxY - targetMinY) / 2
  177. // 根据收集到的参考线,分别执行垂直和水平两个方向的对齐吸附
  178. const _alignmentLines: AlignmentLineProps[] = []
  179. let isVerticalAdsorbed = false
  180. let isHorizontalAdsorbed = false
  181. for(let i = 0; i < horizontalLines.length; i++) {
  182. const { value, range } = horizontalLines[i]
  183. const min = Math.min(...range, targetMinX, targetMaxX)
  184. const max = Math.max(...range, targetMinX, targetMaxX)
  185. if(Math.abs(targetMinY - value) < sorptionRange) {
  186. if(!isHorizontalAdsorbed) {
  187. targetTop = targetTop - (targetMinY - value)
  188. isHorizontalAdsorbed = true
  189. }
  190. _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
  191. }
  192. if(Math.abs(targetMaxY - value) < sorptionRange) {
  193. if(!isHorizontalAdsorbed) {
  194. targetTop = targetTop - (targetMaxY - value)
  195. isHorizontalAdsorbed = true
  196. }
  197. _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
  198. }
  199. if(Math.abs(targetCenterY - value) < sorptionRange) {
  200. if(!isHorizontalAdsorbed) {
  201. targetTop = targetTop - (targetCenterY - value)
  202. isHorizontalAdsorbed = true
  203. }
  204. _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
  205. }
  206. }
  207. for(let i = 0; i < verticalLines.length; i++) {
  208. const { value, range } = verticalLines[i]
  209. const min = Math.min(...range, targetMinY, targetMaxY)
  210. const max = Math.max(...range, targetMinY, targetMaxY)
  211. if(Math.abs(targetMinX - value) < sorptionRange) {
  212. if(!isVerticalAdsorbed) {
  213. targetLeft = targetLeft - (targetMinX - value)
  214. isVerticalAdsorbed = true
  215. }
  216. _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
  217. }
  218. if(Math.abs(targetMaxX - value) < sorptionRange) {
  219. if(!isVerticalAdsorbed) {
  220. targetLeft = targetLeft - (targetMaxX - value)
  221. isVerticalAdsorbed = true
  222. }
  223. _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
  224. }
  225. if(Math.abs(targetCenterX - value) < sorptionRange) {
  226. if(!isVerticalAdsorbed) {
  227. targetLeft = targetLeft - (targetCenterX - value)
  228. isVerticalAdsorbed = true
  229. }
  230. _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
  231. }
  232. }
  233. alignmentLines.value = _alignmentLines
  234. // 非多选,或者当前操作的元素时激活的组合元素
  235. if(activeElementIdList.value.length === 1 || isActiveGroupElement) {
  236. elementList.value = elementList.value.map(el => {
  237. return el.id === element.id ? { ...el, left: targetLeft, top: targetTop } : el
  238. })
  239. }
  240. // 修改元素位置,如果需要修改位置的元素不是被操作的元素(例如多选下的操作)
  241. // 那么其他非操作元素要移动的位置通过操作元素的移动偏移量计算
  242. else {
  243. const handleElement = elementList.value.find(el => el.id === element.id)
  244. if(!handleElement) return
  245. elementList.value = elementList.value.map(el => {
  246. if(activeElementIdList.value.includes(el.id)) {
  247. if(el.id === element.id) {
  248. return {
  249. ...el,
  250. left: targetLeft,
  251. top: targetTop,
  252. }
  253. }
  254. return {
  255. ...el,
  256. left: el.left + (targetLeft - handleElement.left),
  257. top: el.top + (targetTop - handleElement.top),
  258. }
  259. }
  260. return el
  261. })
  262. }
  263. }
  264. document.onmouseup = e => {
  265. isMouseDown = false
  266. document.onmousemove = null
  267. document.onmouseup = null
  268. alignmentLines.value = []
  269. const currentPageX = e.pageX
  270. const currentPageY = e.pageY
  271. // 对比初始位置,没有实际的位移不更新数据
  272. if(startPageX === currentPageX && startPageY === currentPageY) return
  273. store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
  274. addHistorySnapshot()
  275. }
  276. }
  277. return {
  278. dragElement,
  279. }
  280. }