diff --git a/src/hooks/useCreateElement.ts b/src/hooks/useCreateElement.ts index 58ecdc5c..8cf648c4 100644 --- a/src/hooks/useCreateElement.ts +++ b/src/hooks/useCreateElement.ts @@ -124,7 +124,7 @@ export default () => { }) } - const createLineElement = (position: LineElementPosition, points: [string, string], lineType: string) => { + const createLineElement = (position: LineElementPosition, points: [string, string]) => { const { left, top, start, end } = position createElement({ ...DEFAULT_LINE, @@ -135,7 +135,6 @@ export default () => { start, end, points, - lineType, }) } diff --git a/src/mocks/index.ts b/src/mocks/index.ts index 4461eb83..306bb751 100644 --- a/src/mocks/index.ts +++ b/src/mocks/index.ts @@ -88,6 +88,18 @@ export const slides: Slide[] = [ lock: false, content: '
๐Ÿ˜€ ๐Ÿ˜ ๐Ÿ˜ถ ๐Ÿ˜œ ๐Ÿ”” โญ โšก ๐Ÿ”ฅ ๐Ÿ‘ ๐Ÿ’ก ๐Ÿ”ฐ ๐ŸŽ€ ๐ŸŽ ๐Ÿฅ‡ ๐Ÿ… ๐Ÿ† ๐ŸŽˆ ๐ŸŽ‰ ๐Ÿ’Ž ๐Ÿšง โ›” ๐Ÿ“ข โŒ› โฐ ๐Ÿ•’ ๐Ÿงฉ ๐ŸŽต ๐Ÿ“Ž ๐Ÿ”’ ๐Ÿ”‘ โ›ณ ๐Ÿ“Œ ๐Ÿ“ ๐Ÿ’ฌ ๐Ÿ“… ๐Ÿ“ˆ ๐Ÿ“‹ ๐Ÿ“œ ๐Ÿ“ ๐Ÿ“ฑ ๐Ÿ’ป ๐Ÿ’พ ๐ŸŒ ๐Ÿšš ๐Ÿšก ๐Ÿšข๐Ÿ’ง ๐ŸŒ ๐Ÿงญ ๐Ÿ’ฐ ๐Ÿ’ณ ๐Ÿ›’
', }, + { + id: 'xsfdas', + type: 'line', + width: 2, + left: 100, + top: 400, + end: [0, 0], + start: [300, 120], + style: 'solid', + color: '#888', + points: ['', 'arrow'], + }, { id: 'xxx7', type: 'shape', diff --git a/src/types/edit.ts b/src/types/edit.ts index 46f09068..90197ec1 100644 --- a/src/types/edit.ts +++ b/src/types/edit.ts @@ -27,7 +27,7 @@ export enum OperateBorderLines { R = 'right', } -export type OperateResizeHandler = 'left-top' | 'top' | 'right-top' | 'left' | 'right' | 'left-bottom' | 'bottom' | 'right-bottom' +export type OperateResizeHandler = '' | 'left-top' | 'top' | 'right-top' | 'left' | 'right' | 'left-bottom' | 'bottom' | 'right-bottom' export enum OperateResizeHandlers { LEFT_TOP = 'left-top', @@ -40,6 +40,13 @@ export enum OperateResizeHandlers { RIGHT_BOTTOM = 'right-bottom', } +export type OperateLineHandler = 'start' | 'end' + +export enum OperateLineHandlers { + START = 'start', + END = 'end,' +} + export interface AlignmentLineAxis { x: number; y: number; diff --git a/src/types/slides.ts b/src/types/slides.ts index 206f3c0d..470c0c6a 100644 --- a/src/types/slides.ts +++ b/src/types/slides.ts @@ -14,22 +14,19 @@ export enum ElementTypes { TABLE = 'table', } -export interface PPTElementBaseProps { - id: string; - left: number; - top: number; - lock?: boolean; - groupId?: string; -} - export interface PPTElementOutline { style?: 'dashed' | 'solid'; width?: number; color?: string; } -export interface PPTTextElement extends PPTElementBaseProps { +export interface PPTTextElement { type: 'text'; + id: string; + left: number; + top: number; + lock?: boolean; + groupId?: string; width: number; height: number; content: string; @@ -49,8 +46,13 @@ export interface ImageElementFilters { 'hue-rotate': string; 'opacity': string; } -export interface PPTImageElement extends PPTElementBaseProps { +export interface PPTImageElement { type: 'image'; + id: string; + left: number; + top: number; + lock?: boolean; + groupId?: string; width: number; height: number; fixedRatio: boolean; @@ -66,8 +68,13 @@ export interface PPTImageElement extends PPTElementBaseProps { shadow?: PPTElementShadow; } -export interface PPTShapeElement extends PPTElementBaseProps { +export interface PPTShapeElement { type: 'shape'; + id: string; + left: number; + top: number; + lock?: boolean; + groupId?: string; width: number; height: number; viewBox: number; @@ -80,19 +87,29 @@ export interface PPTShapeElement extends PPTElementBaseProps { shadow?: PPTElementShadow; } -export interface PPTLineElement extends PPTElementBaseProps { +export interface PPTLineElement { type: 'line'; + id: string; + left: number; + top: number; + lock?: boolean; + groupId?: string; start: [number, number]; end: [number, number]; width: number; style: string; color: string; points: [string, string]; - lineType: string; + shadow?: PPTElementShadow; } -export interface PPTChartElement extends PPTElementBaseProps { +export interface PPTChartElement { type: 'chart'; + id: string; + left: number; + top: number; + lock?: boolean; + groupId?: string; width: number; height: number; chartType: string; @@ -107,8 +124,13 @@ export interface TableElementCell { content: string; bgColor: string; } -export interface PPTTableElement extends PPTElementBaseProps { +export interface PPTTableElement { type: 'table'; + id: string; + left: number; + top: number; + lock?: boolean; + groupId?: string; width: number; height: number; borderTheme?: string; diff --git a/src/views/Editor/Canvas/EditableElement.vue b/src/views/Editor/Canvas/EditableElement.vue index a18ee526..0e09ada2 100644 --- a/src/views/Editor/Canvas/EditableElement.vue +++ b/src/views/Editor/Canvas/EditableElement.vue @@ -31,6 +31,7 @@ import { ElementOrderCommands, ElementAlignCommands } from '@/types/edit' import ImageElement from '@/views/components/element/ImageElement/index.vue' import TextElement from '@/views/components/element/TextElement/index.vue' import ShapeElement from '@/views/components/element/ShapeElement/index.vue' +import LineElement from '@/views/components/element/LineElement/index.vue' export default defineComponent({ name: 'editable-element', @@ -58,6 +59,7 @@ export default defineComponent({ 'image': ImageElement, 'text': TextElement, 'shape': ShapeElement, + 'line': LineElement, } return elementTypeMap[props.elementInfo.type] || null }) diff --git a/src/views/Editor/Canvas/Operate/LineElementOperate.vue b/src/views/Editor/Canvas/Operate/LineElementOperate.vue new file mode 100644 index 00000000..d477c307 --- /dev/null +++ b/src/views/Editor/Canvas/Operate/LineElementOperate.vue @@ -0,0 +1,77 @@ + + + \ No newline at end of file diff --git a/src/views/Editor/Canvas/Operate/ResizeHandler.vue b/src/views/Editor/Canvas/Operate/ResizeHandler.vue index b3b9f99e..d665df34 100644 --- a/src/views/Editor/Canvas/Operate/ResizeHandler.vue +++ b/src/views/Editor/Canvas/Operate/ResizeHandler.vue @@ -11,7 +11,7 @@ export default { props: { type: { type: String as PropType, - required: true, + default: '', }, }, } diff --git a/src/views/Editor/Canvas/Operate/index.vue b/src/views/Editor/Canvas/Operate/index.vue index 5bef9e08..9179763d 100644 --- a/src/views/Editor/Canvas/Operate/index.vue +++ b/src/views/Editor/Canvas/Operate/index.vue @@ -17,6 +17,7 @@ :isMultiSelect="isMultiSelect" :rotateElement="rotateElement" :scaleElement="scaleElement" + :dragLineElement="dragLineElement" >
void>, required: true, }, + dragLineElement: { + type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateLineHandler) => void>, + required: true, + }, }, setup(props) { const store = useStore() @@ -82,6 +88,7 @@ export default defineComponent({ 'image': ImageElementOperate, 'text': TextElementOperate, 'shape': ShapeElementOperate, + 'line': LineElementOperate, } return elementTypeMap[props.elementInfo.type] || null }) diff --git a/src/views/Editor/Canvas/hooks/useDragLineElement.ts b/src/views/Editor/Canvas/hooks/useDragLineElement.ts new file mode 100644 index 00000000..7d4bb51f --- /dev/null +++ b/src/views/Editor/Canvas/hooks/useDragLineElement.ts @@ -0,0 +1,169 @@ +import { Ref, computed } from 'vue' +import { useStore } from 'vuex' +import { State, MutationTypes } from '@/store' +import { PPTElement, PPTLineElement } from '@/types/slides' +import { OperateLineHandler, OperateLineHandlers } from '@/types/edit' +import useHistorySnapshot from '@/hooks/useHistorySnapshot' + +interface AdsorptionPoint { + x: number; + y: number; +} + +export default (elementList: Ref) => { + const store = useStore() + const canvasScale = computed(() => store.state.canvasScale) + + const { addHistorySnapshot } = useHistorySnapshot() + + const dragLineElement = (e: MouseEvent, element: PPTLineElement, command: OperateLineHandler) => { + let isMouseDown = true + + const sorptionRange = 10 + + const startPageX = e.pageX + const startPageY = e.pageY + + const adsorptionPoints: AdsorptionPoint[] = [] + + // ่Žทๅ–ๅ…จ้ƒจ้ž็บฟๆกไธ”ๆœชๆ—‹่ฝฌๅ…ƒ็ด ็š„8ไธช็‚นไฝœไธบๅธ้™„็‚น + for(let i = 0; i < elementList.value.length; i++) { + const _element = elementList.value[i] + if(_element.type === 'line' || ('rotate' in _element && _element.rotate)) continue + + const left = _element.left + const top = _element.top + const width = _element.width + const height = _element.height + + const right = left + width + const bottom = top + height + const centerX = top + height / 2 + const centerY = left + width / 2 + + const topPoint = { x: centerY, y: top } + const bottomPoint = { x: centerY, y: bottom } + const leftPoint = { x: left, y: centerX } + const rightPoint = { x: right, y: centerX } + + const leftTopPoint = { x: left, y: top } + const rightTopPoint = { x: right, y: top } + const leftBottomPoint = { x: left, y: bottom } + const rightBottomPoint = { x: right, y: bottom } + + adsorptionPoints.push( + topPoint, + bottomPoint, + leftPoint, + rightPoint, + leftTopPoint, + rightTopPoint, + leftBottomPoint, + rightBottomPoint, + ) + } + + document.onmousemove = e => { + if(!isMouseDown) return + + const currentPageX = e.pageX + const currentPageY = e.pageY + + // ้ผ ๆ ‡ๆŒ‰ไธ‹ๅŽ็งปๅŠจ็š„่ท็ฆป + const moveX = (currentPageX - startPageX) / canvasScale.value + const moveY = (currentPageY - startPageY) / canvasScale.value + + // ็บฟๆกไธคไธช็ซฏ็‚น๏ผˆ่ตท็‚นๅ’Œ็ปˆ็‚น๏ผ‰ๅŸบไบŽ็ผ–่พ‘ๅŒบๅŸŸ็š„ไฝ็ฝฎ + let startX = element.left + element.start[0] + let startY = element.top + element.start[1] + let endX = element.left + element.end[0] + let endY = element.top + element.end[1] + + // ๆ นๆฎๆ‹–ๆ‹ฝ็š„็‚น๏ผŒ้€‰ๆ‹ฉไฟฎๆ”น่ตท็‚นๆˆ–็ปˆ็‚น็š„ไฝ็ฝฎ + // ไธค็‚นๅœจๆฐดๅนณๅ’Œๅž‚็›ดๆ–นๅ‘ไธŠๆœ‰ๅฏน้ฝๅธ้™„ + // ้ ่ฟ‘ๅ…ถไป–ๅ…ƒ็ด ็š„ๅธ้™„็‚นๆœ‰ๅฏน้ฝๅธ้™„ + if(command === OperateLineHandlers.START) { + startX = startX + moveX + startY = startY + moveY + + if(Math.abs(startX - endX) < sorptionRange) startX = endX + if(Math.abs(startY - endY) < sorptionRange) startY = endY + + for(const adsorptionPoint of adsorptionPoints) { + const { x, y } = adsorptionPoint + if(Math.abs(x - startX) < sorptionRange && Math.abs(y - startY) < sorptionRange) { + startX = x + startY = y + break + } + } + } + else { + endX = endX + moveX + endY = endY + moveY + + if(Math.abs(startX - endX) < sorptionRange) endX = startX + if(Math.abs(startY - endY) < sorptionRange) endY = startY + + for(const adsorptionPoint of adsorptionPoints) { + const { x, y } = adsorptionPoint + if(Math.abs(x - endX) < sorptionRange && Math.abs(y - endY) < sorptionRange) { + endX = x + endY = y + break + } + } + } + + // ่ฎก็ฎ—ไธคไธช็ซฏ็‚นๅŸบไบŽ่‡ช่บซๅ…ƒ็ด ไฝ็ฝฎ็š„ๅๆ ‡ + const minX = Math.min(startX, endX) + const minY = Math.min(startY, endY) + const maxX = Math.max(startX, endX) + const maxY = Math.max(startY, endY) + + const start: [number, number] = [0, 0] + const end: [number, number] = [maxX - minX, maxY - minY] + if(startX > endX) { + start[0] = maxX - minX + end[0] = 0 + } + if(startY > endY) { + start[1] = maxY - minY + end[1] = 0 + } + + // ไฟฎๆ”น็บฟๆก็š„ไฝ็ฝฎๅ’Œไธค็‚น็š„ๅๆ ‡ + elementList.value = elementList.value.map(el => { + if(el.id === element.id) { + return { + ...el, + left: minX, + top: minY, + start: start, + end: end, + } + } + return el + }) + } + + document.onmouseup = e => { + isMouseDown = false + document.onmousemove = null + document.onmouseup = null + + const currentPageX = e.pageX + const currentPageY = e.pageY + + // ๅฏนๆฏ”ๅŽŸๅง‹้ผ ๆ ‡ไฝ็ฝฎ๏ผŒๆฒกๆœ‰ๅฎž้™…็š„ไฝ็งปไธๆ›ดๆ–ฐๆ•ฐๆฎ + if(startPageX === currentPageX && startPageY === currentPageY) return + + store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value }) + addHistorySnapshot() + } + } + + return { + dragLineElement, + } +} \ No newline at end of file diff --git a/src/views/Editor/Canvas/index.vue b/src/views/Editor/Canvas/index.vue index 67eedfb0..72625561 100644 --- a/src/views/Editor/Canvas/index.vue +++ b/src/views/Editor/Canvas/index.vue @@ -39,6 +39,7 @@ :isMultiSelect="activeElementIdList.length > 1" :rotateElement="rotateElement" :scaleElement="scaleElement" + :dragLineElement="dragLineElement" />
@@ -85,6 +86,7 @@ import useRotateElement from './hooks/useRotateElement' import useScaleElement from './hooks/useScaleElement' import useSelectElement from './hooks/useSelectElement' import useDragElement from './hooks/useDragElement' +import useDragLineElement from './hooks/useDragLineElement' import useDeleteElement from '@/hooks/useDeleteElement' import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement' @@ -140,6 +142,7 @@ export default defineComponent({ const { mouseSelectionState, updateMouseSelection } = useMouseSelection(elementList, viewportRef) const { dragElement } = useDragElement(elementList, activeGroupElementId, alignmentLines) + const { dragLineElement } = useDragLineElement(elementList) const { selectElement } = useSelectElement(elementList, activeGroupElementId, dragElement) const { scaleElement, scaleMultiElement } = useScaleElement(elementList, activeGroupElementId, alignmentLines) const { rotateElement } = useRotateElement(elementList, viewportRef) @@ -218,6 +221,7 @@ export default defineComponent({ selectElement, rotateElement, scaleElement, + dragLineElement, scaleMultiElement, mousewheelScaleCanvas, contextmenus, diff --git a/src/views/Screen/ScreenElement.vue b/src/views/Screen/ScreenElement.vue index 2b1c315a..700ccaf9 100644 --- a/src/views/Screen/ScreenElement.vue +++ b/src/views/Screen/ScreenElement.vue @@ -21,6 +21,8 @@ import { PPTElement, Slide } from '@/types/slides' import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue' import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue' +import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue' +import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue' export default defineComponent({ name: 'screen-element', @@ -43,6 +45,8 @@ export default defineComponent({ const elementTypeMap = { 'image': BaseImageElement, 'text': BaseTextElement, + 'shape': BaseShapeElement, + 'line': BaseLineElement, } return elementTypeMap[props.elementInfo.type] || null }) diff --git a/src/views/components/ThumbnailSlide/ThumbnailElement.vue b/src/views/components/ThumbnailSlide/ThumbnailElement.vue index a0fe516c..4c345ded 100644 --- a/src/views/components/ThumbnailSlide/ThumbnailElement.vue +++ b/src/views/components/ThumbnailSlide/ThumbnailElement.vue @@ -17,6 +17,7 @@ import { PPTElement } from '@/types/slides' import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue' import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue' import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue' +import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue' export default defineComponent({ name: 'base-element', @@ -36,6 +37,7 @@ export default defineComponent({ 'image': BaseImageElement, 'text': BaseTextElement, 'shape': BaseShapeElement, + 'line': BaseLineElement, } return elementTypeMap[props.elementInfo.type] || null }) diff --git a/src/views/components/element/LineElement/BaseLineElement.vue b/src/views/components/element/LineElement/BaseLineElement.vue new file mode 100644 index 00000000..1d75e260 --- /dev/null +++ b/src/views/components/element/LineElement/BaseLineElement.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/views/components/element/LineElement/LinePointMarker.vue b/src/views/components/element/LineElement/LinePointMarker.vue new file mode 100644 index 00000000..1a74fea9 --- /dev/null +++ b/src/views/components/element/LineElement/LinePointMarker.vue @@ -0,0 +1,66 @@ + + + \ No newline at end of file diff --git a/src/views/components/element/LineElement/index.vue b/src/views/components/element/LineElement/index.vue new file mode 100644 index 00000000..cd44eaae --- /dev/null +++ b/src/views/components/element/LineElement/index.vue @@ -0,0 +1,142 @@ + + + + +