import { h, nextTick, reactive, ref, watch, watchEffect } from "vue"; import { installGlobalVar, useCan, useCursor, useDownKeys, useMode, useStage, } from "./use-global-vars"; import { Area, InteractiveHook, useInteractiveAreas, useInteractiveDots, useInteractiveProps, } from "./use-interactive"; import { Mode } from "@/constant/mode"; import { mergeFuns } from "@/utils/shared"; import { Components, components, ComponentSnapInfo, ComponentValue, DrawItem, ShapeType, SnapPoint, } from "../components"; import { useConversionPosition } from "./use-coversion-position"; import { eqPoint, lineInner, linePointLen, Pos, zeroEq } from "@/utils/math"; import { useCustomSnapInfos, useSnap } from "./use-snap"; import { generateSnapInfos } from "../components/util"; import { useStore, useStoreRenderProcessors } from "../store"; import DrawShape from "../renderer/draw-shape.vue"; import { useHistory, useHistoryAttach } from "./use-history"; import penA from "../assert/cursor/pic_pen_a.ico"; import penR from "../assert/cursor/pic_pen_r.ico"; type PayData = ComponentValue extends "area" ? Area : Pos; export enum MessageAction { add, delete, update, replace, } export type AddMessage = { consumed: PayData[]; cur?: PayData; ndx?: number; action: MessageAction; }; export const useInteractiveAddShapeAPI = installGlobalVar(() => { const mode = useMode(); const can = useCan(); const interactiveProps = useInteractiveProps(); const conversion = useConversionPosition(true); let isEnter = false; const enter = () => { if (!isEnter) { isEnter = true; mode.push(Mode.add); } }; const leave = () => { if (isEnter) { isEnter = false; mode.pop(); } }; return { addShape: ( shapeType: T, preset: Partial> = {}, data: PayData, pixel = false ) => { if (!can.addMode) { throw "当前状态不允许添加"; } enter(); if (pixel) { data = ( Array.isArray(data) ? data.map(conversion) : conversion(data) ) as PayData; } interactiveProps.value = { type: shapeType, preset, callback: leave, operate: { single: true, immediate: true, data }, }; }, enterMouseAddShape: async ( shapeType: T, preset: Partial> = {}, single = false ) => { if (isEnter) { leave(); await new Promise((resolve) => setTimeout(resolve, 16)); } if (!can.addMode || mode.include(Mode.add)) { throw "当前状态不允许添加"; } enter(); interactiveProps.value = { type: shapeType, preset, operate: { single }, callback: leave, }; }, quitMouseAddShape: () => { leave(); interactiveProps.value = void 0; }, }; }); export const useIsAddRunning = (shapeType?: ShapeType) => { const stage = useStage(); const mode = useMode(); const interactiveProps = useInteractiveProps(); const isRunning = ref(false); let currentPreset: any; const updateIsRunning = () => { const isRun = !!( stage.value && mode.include(Mode.add) && (!shapeType || shapeType === interactiveProps.value?.type) ); if (isRunning.value !== isRun) { isRunning.value = isRun; } else if (currentPreset !== interactiveProps.value?.preset) { isRunning.value = false; nextTick(() => { isRunning.value = isRun; }); } currentPreset = interactiveProps.value?.preset; }; watchEffect(updateIsRunning); return isRunning; }; const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => { const conversionPosition = useConversionPosition(enableTransform); const snap = enableSnap && useSnap(); const infos = useCustomSnapInfos(); const addedInfos: ComponentSnapInfo[] = []; return { transform: ( point: SnapPoint, prevPoint?: SnapPoint, nextPoint?: SnapPoint ) => { snap && snap.clear(); let p = conversionPosition(point); const geo = [p]; prevPoint && geo.unshift({ ...prevPoint, view: true }); nextPoint && geo.push({ ...nextPoint, view: true }); const selfInfos = generateSnapInfos(geo, true, true); const transform = snap && snap.move(selfInfos); p = transform ? transform.point(p) : p; return p; }, addRef(p: Pos | Pos[]) { const geo = Array.isArray(p) ? p : [p]; const snapInfos = generateSnapInfos(geo, true, true); snapInfos.forEach((info) => { infos.add(info); addedInfos.push(info); }); }, clear: () => { snap && snap.clear(); }, clearRef: () => { addedInfos.forEach((info) => { infos.remove(info); }); addedInfos.length = 0; }, }; }; const useInteractiveAddTemp = ({ type, useIA, refSelf, enter, quit, }: { type: T; useIA: InteractiveHook; refSelf?: boolean; enter?: () => void; quit?: () => void; }) => { const { quitMouseAddShape } = useInteractiveAddShapeAPI(); const isRuning = useIsAddRunning(type); const items = reactive([]) as DrawItem[]; const obj = components[type] as Components[T]; const beforeHandler = usePointBeforeHandler(true, true); const processors = useStoreRenderProcessors(); const store = useStore(); const processorIds = processors.register(() => DrawShape); const clear = () => { beforeHandler.clear(); beforeHandler.clearRef(); }; const ia = useIA({ shapeType: type, isRuning, quit: () => { items.length = 0; processorIds.length = 0; quitMouseAddShape(); clear(); quit && quit(); }, enter, beforeHandler: (p) => { beforeHandler.clear(); return beforeHandler.transform(p); }, }); const addItem = (cur: PayData) => { let item: any = obj.interactiveToData( { consumed: ia.consumedMessage, cur, action: MessageAction.add, } as any, ia.preset ); if (!item) return; item = reactive(item); if (ia.singleDone.value) { store.addItem(type, item); return; } items.push(item); // 箭头参考自身位置 if (refSelf && Array.isArray(cur)) { beforeHandler.addRef(cur[0]); } const stop = mergeFuns( // 监听位置变化 watch( cur, () => obj.interactiveFixData(item, { consumed: ia.consumedMessage, cur, action: MessageAction.update, } as any), { deep: true } ), // 监听是否消费完毕 watch(ia.singleDone, () => { processorIds.push(item.id); store.addItem(type, item); const ndx = items.indexOf(item); items.splice(ndx, 1); clear(); stop(); }) ); }; // 每次拽结束都加组件 watch( () => ia.messages, (datas: any) => { datas.forEach(addItem); ia.consume(datas); }, { immediate: true } ); return items; }; // 拖拽面积确定组件 export const useInteractiveAddAreas = (type: T) => { const cursor = useCursor(); return useInteractiveAddTemp({ type, useIA: useInteractiveAreas, refSelf: type === "arrow", enter() { cursor.push("crosshair"); }, quit() { cursor.pop(); }, }); }; export const useInteractiveAddDots = (type: T) => { const cursor = useCursor(); return useInteractiveAddTemp({ type, useIA: useInteractiveDots, enter() { cursor.push(penA); }, quit() { cursor.pop(); }, }); }; export const penUpdatePoints = ( transfromPoints: T[], cur: T ) => { const points = [...transfromPoints]; let oper: "del" | "add" | "set" | "no" = "add"; const resetCur = () => { if (points.length) { return (cur = points.pop()!); } else { return cur; } }; let repeatStart = false; for (let i = 0; i < points.length; i++) { if (eqPoint(points[i], cur)) { const isLast = i === points.length - 1; const isStart = i === 0; if (!isStart && !isLast) { points.splice(i--, 1); oper = "del"; } else if ((oper !== "del" && isLast) || isStart) { oper = "no"; if (isStart) { repeatStart = true; } } } } if (oper === "del" || oper === "no") { if (repeatStart) { const change = points.length > 2 return { points, oper, cur: change ? cur : resetCur(), unchanged: !change }; } return { points, oper, cur: repeatStart ? cur : resetCur() }; } for (let i = 0, ndx = 0; i < transfromPoints.length - 1; i++, ndx++) { const line = [transfromPoints[i], transfromPoints[i + 1]]; if (lineInner(line, cur)) { oper = "set"; points.splice(++ndx, 0, cur); resetCur(); } } return { points, oper, cur }; }; // 钢笔添加 export const useInteractiveAddPen = (type: T) => { const { quitMouseAddShape } = useInteractiveAddShapeAPI(); const isRuning = useIsAddRunning(type); const items = reactive([]) as DrawItem[]; const obj = components[type] as Components[T]; const beforeHandler = usePointBeforeHandler(true, true); const history = useHistory(); const processors = useStoreRenderProcessors(); const store = useStore(); const downKeys = useDownKeys(); const processorIds = processors.register(() => { return (props: any) => h(DrawShape, { ...props, show: false }); }); let prev: Pos; let firstEntry = true; // 可能历史空间会撤销 重做更改到正在绘制的组件 const messages = useHistoryAttach(`${type}-pen`, isRuning, []); const currentCursor = ref(penA); const cursor = useCursor(); const ia = useInteractiveDots({ shapeType: type, isRuning, enter() { cursor.push(currentCursor.value); watch(currentCursor, () => { cursor.value = currentCursor.value; }); }, quit: () => { items.length = 0; processorIds.length = 0; quitMouseAddShape(); beforeHandler.clear(); cursor.pop(); }, beforeHandler: (p) => { beforeHandler.clear(); return beforeHandler.transform(p, prev); }, }); const getAddMessage = (cur: Pos) => { let consumed = messages.value; currentCursor.value = penA; let pen: null | ReturnType = null; if (!downKeys.has("Control")) { pen = penUpdatePoints(messages.value, cur); if (pen.oper === "del") { currentCursor.value = penR; } consumed = pen.points; cur = pen.cur; } return { pen, consumed, cur, action: firstEntry ? MessageAction.add : MessageAction.replace, } as any; }; const pushMessages = (cur: Pos) => { const { pen } = getAddMessage(cur); if (pen) { if (!pen.unchanged) { messages.value = pen.points; cur = pen.cur; messages.value.push(cur); } } else { messages.value.push(cur); } return !pen?.unchanged; }; const addItem = (cur: PayData) => { const dot = cur as Pos; if (messages.value.length === 0) { firstEntry = true; items.length = 0; } let item: any = items.length === 0 ? null : items[0]; if (!item) { item = obj.interactiveToData(getAddMessage(dot), ia.preset); if (!item) return; items[0] = item = reactive(item); } if (ia.singleDone.value) { store.addItem(type, item); return; } const update = () => { obj.interactiveFixData(item, getAddMessage(dot)); }; const stop = mergeFuns( watch(dot, update, { immediate: true, deep: true }), watch( messages, () => { if (!messages.value) return; if (messages.value.length === 0) { quitMouseAddShape(); } else { update(); } }, { deep: true } ), // 监听是否消费完毕 watch(ia.singleDone, () => { prev = dot; const cItem = JSON.parse(JSON.stringify(item)); const isChange = pushMessages(dot); if (isChange) { if (firstEntry) { processorIds.push(item.id); history.preventTrack(() => store.addItem(type, cItem)); } else { store.setItem(type, { id: item.id, value: cItem }); } } beforeHandler.clear(); stop(); firstEntry = false; }) ); }; // 每次拽结束都加组件 watch( () => ia.messages, (datas: any) => { datas.forEach(addItem); ia.consume(datas); }, { immediate: true } ); return items; }; export const useInteractiveAdd = (type: T) => { const obj = components[type]; if (obj.addMode === "dots") { return useInteractiveAddPen(type); } else if (obj.addMode === "area") { return useInteractiveAddAreas(type); } else { return useInteractiveAddDots(type); } };