import { computed, h, nextTick, reactive, ref, watch, watchEffect } from "vue"; import { installGlobalVar, useCursor, useStage } from "./use-global-vars"; import { useCan, useMode, useOperMode } from "./use-status"; import { Area, InteractiveHook, InteractivePreset, useInteractiveAreas, useInteractiveDots, useInteractiveProps, } from "./use-interactive"; import { Mode } from "@/constant/mode"; import { copy, mergeFuns } from "@/utils/shared"; import { Components, components, ComponentSnapInfo, ComponentValue, DrawItem, ShapeType, SnapPoint, } from "../components"; import { useConversionPosition } from "./use-coversion-position"; import { eqPoint, lineInner, Pos } 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 { useCurrentZIndex } from "./use-layer"; import { useViewerTransform } from "./use-viewer"; 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 useInteractiveDrawShapeAPI = installGlobalVar(() => { const mode = useMode(); const can = useCan(); const interactiveProps = useInteractiveProps(); const conversion = useConversionPosition(true); const currentZIndex = useCurrentZIndex(); const store = useStore(); let addCount = 0; let isEnter = false; let modePop: (() => void) | undefined = void 0; const enter = () => { if (!isEnter) { isEnter = true; addCount = 0; modePop = mode.push(Mode.draw); } }; const leave = () => { if (isEnter) { isEnter = false; modePop && modePop(); addCount = 0; } }; store.bus.on("addItemBefore", () => addCount++); store.bus.on("setItemBefore", () => addCount++); return { delShape(id: string) { const type = store.getType(id); type && store.delItem(type, id); }, addShape: ( shapeType: T, preset: Partial> = {}, data?: PayData, pixel = false ) => { if (!can.drawMode) { throw "当前状态不允许添加"; } enter(); data = (data || {}) as PayData; if (pixel) { data = ( Array.isArray(data) ? data.map(conversion) : conversion(data) ) as PayData; } if (!preset.zIndex) { preset.zIndex = currentZIndex.max + 1; } interactiveProps.value = { type: shapeType, preset, callback: leave, operate: { single: true, immediate: true, data }, }; }, enterDrawShape: async ( shapeType: T, preset: InteractivePreset["preset"] = {}, single = false ) => { if (isEnter) { leave(); await new Promise((resolve) => setTimeout(resolve, 16)); } if (!can.drawMode || mode.include(Mode.draw)) { throw "当前状态不允许添加"; } if (!preset.zIndex) { preset.zIndex = currentZIndex.max + 1; } interactiveProps.value = { type: shapeType, preset, operate: { single }, callback: leave, }; enter(); }, quitDrawShape: () => { const currentAddCount = addCount; leave(); interactiveProps.value = void 0; return currentAddCount; }, drawing: computed(() => mode.include(Mode.draw)), drawType: computed(() => { return interactiveProps.value?.type && components[interactiveProps.value?.type].addMode }) }; }); export const useDrawRunning = (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.draw) && 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; }; export const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => { const operMode = useOperMode(); const conversionPosition = useConversionPosition(enableTransform); const snap = enableSnap && useSnap(); const infos = useCustomSnapInfos(); const addedInfos: ComponentSnapInfo[] = []; return { transform: (p: SnapPoint, geo = [p], geoNeedConversion = true) => { p = conversionPosition(p); if (operMode.value.freeDraw) { return p; } snap && snap.clear(); if (geoNeedConversion) { geo = geo.map(conversionPosition); } 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 useInteractiveDrawTemp = ({ type, useIA, refSelf, enter, quit, getSnapGeo, }: { type: T; useIA: InteractiveHook; refSelf?: boolean; enter?: () => void; quit?: () => void; getSnapGeo?: (data: DrawItem) => SnapPoint[]; }) => { const { quitDrawShape } = useInteractiveDrawShapeAPI(); const isRuning = useDrawRunning(type); const items = reactive([]) as DrawItem[]; const obj = components[type] as Components[T]; const beforeHandler = usePointBeforeHandler(true, true); const processors = useStoreRenderProcessors(); const viewTransform = useViewerTransform(); const conversionPosition = useConversionPosition(true); const history = useHistory(); 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; quitDrawShape(); clear(); quit && quit(); }, enter, beforeHandler: (p) => { beforeHandler.clear(); let geo: SnapPoint[] | undefined; if (items.length && getSnapGeo) { const item = obj.interactiveFixData({ info: { cur: conversionPosition(p), consumed: ia.consumedMessage, action: MessageAction.update, }, data: copy(items[0]), history, viewTransform: viewTransform.value, store, } as any); geo = getSnapGeo(item as any); } return beforeHandler.transform(p, geo, !geo); }, }); const addItem = (cur: PayData) => { let item: any = obj.interactiveToData({ info: { cur, consumed: ia.consumedMessage, action: MessageAction.add }, preset: ia.preset, history, viewTransform: viewTransform.value, store, } as any); if (!item) return; item = reactive(item); const storeAddItem = (cItem: any) => { const items = store.getTypeItems(type); if (items.some((item) => item.id === cItem.id)) { store.setItem(type, { id: cItem.id, value: cItem }); } else { store.addItem(type, cItem); } }; if (ia.singleDone.value) { storeAddItem(item); return; } items.push(item); // 箭头参考自身位置 if (refSelf && Array.isArray(cur)) { beforeHandler.addRef(cur[0]); } const stop = mergeFuns( // 监听位置变化 watch( cur, () => { obj.interactiveFixData({ info: { cur, consumed: ia.consumedMessage, action: MessageAction.update, }, data: item, history, viewTransform: viewTransform.value, store, } as any); }, { deep: true } ), // 监听是否消费完毕 watch(ia.singleDone, () => { processorIds.push(item.id); storeAddItem(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 useInteractiveDrawAreas = (type: T) => { const cursor = useCursor(); let cursorPop: () => void; return useInteractiveDrawTemp({ type, useIA: useInteractiveAreas, refSelf: type === "arrow", enter() { cursorPop = cursor.push("./icons/m_draw.png"); }, quit() { cursorPop && cursorPop(); }, }); }; export const useInteractiveDrawDots = (type: T) => { const cursor = useCursor(); let cursorPop: () => void; return useInteractiveDrawTemp({ type, useIA: useInteractiveDots, enter() { cursorPop = cursor.push("./icons/m_add.png"); }, quit() { cursorPop && cursorPop(); }, getSnapGeo(item) { return components[type].getSnapPoints(item as any); }, }); }; export const penUpdatePoints = ( transfromPoints: T[], cur: T, needClose = false ) => { 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 = needClose && i === 0; if (!isStart && !isLast) { points.splice(i--, 1); oper = "del"; repeatStart = false; } 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 useInteractiveDrawPen = (type: T) => { const { quitDrawShape } = useInteractiveDrawShapeAPI(); const isRuning = useDrawRunning(type); const obj = components[type] as Components[T]; const beforeHandler = usePointBeforeHandler(true, true); const history = useHistory(); const processors = useStoreRenderProcessors(); const store = useStore(); const viewTransform = useViewerTransform(); const operMode = useOperMode(); const processorIds = processors.register(() => { return (props: any) => h(DrawShape, { ...props, show: false }); }); // 可能历史空间会撤销 重做更改到正在绘制的组件 const currentCursor = ref("./icons/m_add.png"); const cursor = useCursor(); let cursorPop: ReturnType | null = null; let stopWatch: (() => void) | null = null; const ia = useInteractiveDots({ shapeType: type, isRuning, enter() { cursorPop = cursor.push(currentCursor.value); watch(currentCursor, () => { cursorPop?.set(currentCursor.value); }); }, quit: () => { items.length = 0; processorIds.length = 0; quitDrawShape(); beforeHandler.clear(); cursorPop && cursorPop(); stopWatch && stopWatch(); }, beforeHandler: (p) => { beforeHandler.clear(); const pa = beforeHandler.transform(p, prev && [prev, p]); currentIsDel && beforeHandler.clear(); return pa; }, }); const shape = computed( () => ia.isRunning.value && typeof ia.preset?.id === "string" && ia.preset?.id && ia.preset.getMessages && store.getItemById(ia.preset.id) ); const items = reactive([]) as DrawItem[]; const messages = useHistoryAttach( `${type}-pen`, isRuning, shape.value ? (ia.preset!.getMessages! as any) : () => [], true ); let prev: SnapPoint; let firstEntry = true; let currentIsDel = false; if (shape.value) { processorIds.push(shape.value.id); items[0] = copy(shape.value) as DrawItem; firstEntry = false; } const getAddMessage = (cur: Pos) => { let consumed = messages.value; currentCursor.value = "./icons/m_add.png"; let pen: null | ReturnType = null; if (!operMode.value.freeDraw) { // pen = penUpdatePoints(messages.value, cur, type !== "polygon"); // consumed = pen.points; // cur = pen.cur; } return { pen, consumed, cur, action: firstEntry ? MessageAction.add : MessageAction.replace, } as any; }; const setMessage = (cur: Pos) => { const { pen, ...msg } = getAddMessage(cur); if ((currentIsDel = pen?.oper === "del")) { currentCursor.value = "./icons/m_reduce.png"; beforeHandler.clear(); } return msg; }; 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({ preset: ia.preset as any, info: setMessage(dot), viewTransform: viewTransform.value, history, store, }); if (!item) return; items[0] = item = reactive(item); } const storeAddItem = (cItem: any) => { const items = store.getTypeItems(type); if (items.some((item) => item.id === cItem.id)) { store.setItem(type, { id: cItem.id, value: cItem }); } else { store.addItem(type, cItem); } }; if (ia.singleDone.value) { storeAddItem(item); return; } const update = () => { obj.interactiveFixData({ data: item, info: setMessage(dot), viewTransform: viewTransform.value, history, store, }); }; stopWatch = mergeFuns( watch(() => operMode.value.freeDraw, update), watch(dot, update, { immediate: true, deep: true }), watch( messages, () => { if (!messages.value) return; if (messages.value.length === 0) { quitDrawShape(); } else { update(); } }, { deep: true } ), // 监听是否消费完毕 watch(ia.singleDone, () => { prev = { ...dot, view: true }; const cItem = JSON.parse(JSON.stringify(item)); const isChange = pushMessages(dot); if (isChange) { if (firstEntry) { processorIds.push(item.id); history.preventTrack(() => { storeAddItem(cItem); }); } else { store.setItem(type, { id: item.id, value: cItem }); } } beforeHandler.clear(); stopWatch && stopWatch(); stopWatch = null; 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 useInteractiveDrawPen(type); } else if (obj.addMode === "area") { return useInteractiveDrawAreas(type); } else { return useInteractiveDrawDots(type); } };