useDragElement.ts 13 KB

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