import { installGlobalVar, useStage } from "./use-global-vars.ts"; import { useCan, useMode } from "./use-status"; import { DrawItem, ShapeType } from "../components"; import { nextTick, reactive, Ref, ref, watch, watchEffect } from "vue"; import { eqPoint, Pos } from "../../utils/math.ts"; import { clickListener, getOffset, listener } from "../../utils/event.ts"; import { mergeFuns } from "../../utils/shared.ts"; import { Mode } from "@/constant/mode.ts"; import { useMouseShapesStatus } from "./use-mouse-status.ts"; import { EntityShape } from "@/deconstruction.js"; export type InteractivePreset = { key?: string; type: T; callback?: () => void; preset?: Partial> & { getMessages?: () => Pos[] | Area[] }; operate?: { immediate?: boolean; single?: boolean; data?: any; preSelectIds?: string[]; }; }; export const useInteractiveProps = installGlobalVar(() => { const props = ref(); return props; }, Symbol("interactiveProps")); export type Area = [Pos, Pos]; export type InteractiveHook = | typeof useInteractiveAreas | typeof useInteractiveDots; export type InteractiveAreas = ReturnType; export type InteractiveDots = ReturnType; export type Interactive = InteractiveAreas | InteractiveDots; const useInteractiveExpose = ( messages: Ref, init: (dom: HTMLDivElement) => () => void, singleDone: Ref, isRunning: Ref, quit: () => void, autoConsumed?: boolean, attachInfos?: WeakMap, ) => { const consumedMessages = reactive(new WeakSet()) as WeakSet; const stage = useStage(); const interactiveProps = useInteractiveProps(); watch( isRunning, (can, _, onCleanup) => { if (can) { const props = interactiveProps.value!; const cleanups = [] as Array<() => void>; if (props.operate?.single) { // 如果指定单次则消息中有信息,并且确定完成则马上退出 cleanups.push( watchEffect( () => { if (messages.value.length > 0 && singleDone.value) { quit(); props.callback && props.callback(); } }, { flush: "post" }, ), ); } // 单纯添加 if (props.operate?.immediate) { messages.value.push(props.operate.data as T); singleDone.value = true; } else { const $stage = stage.value!.getStage(); const dom = $stage.container(); cleanups.push(init(dom)); cleanups.push(() => { quit(); props.callback && props.callback(); }); } onCleanup(mergeFuns(cleanups)); } else { messages.value = []; } }, { immediate: true }, ); return { attachInfos, isRunning, get preset() { return interactiveProps.value?.preset; }, get messages() { const items = messages.value; const result = items.filter((item) => !consumedMessages.has(item)); autoConsumed && result.forEach((item) => consumedMessages.add(item)); return result as T[]; }, getNdx(item: T) { return messages.value.indexOf(item); }, get consumedMessage() { const items = messages.value; return items.filter((item) => consumedMessages.has(item)) as T[]; }, consume(items: T[]) { items.forEach((item) => consumedMessages.add(item)); }, singleDone, }; }; type UseInteractiveProps = { isRuning: Ref; quit: () => void; beforeHandler?: (p: Pos) => Pos; enter?: () => void; shapeType?: ShapeType; autoConsumed?: boolean; }; export const useInteractiveAreas = ({ isRuning, autoConsumed, beforeHandler, quit, enter, }: UseInteractiveProps) => { const mode = useMode(); const can = useCan(); const singleDone = ref(true); const messages = ref([]); const init = (dom: HTMLDivElement) => { let pushed = false; let pushNdx = -1; let downed = false; let tempArea: Area; let dragging = false; enter && enter(); const upHandler = (ev: MouseEvent) => { if (downed) { mode.del(Mode.draging); } downed = false; if (!dragging) return; if (can.dragMode) { const position = getOffset(ev, dom); messages.value[pushNdx]![1] = beforeHandler ? beforeHandler(position) : position; } prevEv = null; pushNdx = -1; pushed = false; downed = false; dragging = false; singleDone.value = true; }; let prevEv: any; return mergeFuns( listener(dom, "pointerdown", (ev) => { if (!can.dragMode) return; const position = getOffset(ev, dom); if (ev.button === 0) { tempArea = [ beforeHandler ? beforeHandler(position) : position, ] as unknown as Area; downed = true; singleDone.value = false; dragging = false; mode.add(Mode.draging); } }), listener(document.documentElement, "pointermove", (ev) => { if (!can.dragMode) return; if (ev.buttons <= 0) { prevEv && upHandler(prevEv); return; } const end = getOffset(ev, dom); const point = beforeHandler ? beforeHandler(end) : end; prevEv = ev; if (downed) { if (pushed) { messages.value[pushNdx]![1] = point; } else { tempArea[1] = point; pushed = true; pushNdx = messages.value.length; messages.value[pushNdx] = tempArea; } dragging = true; } else { tempArea = [point] as unknown as Area; } }), listener(document.documentElement, "pointerup", upHandler), ); }; return useInteractiveExpose( messages, init, singleDone, isRuning, quit, autoConsumed, ); }; export const useInteractiveDots = ({ autoConsumed, isRuning, quit, enter, beforeHandler, }: UseInteractiveProps) => { if (autoConsumed === void 0) autoConsumed = false; const interactiveProps = useInteractiveProps(); const attachInfos = new WeakMap(); const mode = useMode(); const can = useCan(); const singleDone = ref(true); const messages = ref([]); const stage = useStage(); const shapesStatus = useMouseShapesStatus(); const init = (dom: HTMLDivElement) => { if (!can.dragMode) return () => {}; let moveIng = false; let pushed = false; const empty = { x: -9999, y: -9999 }; const pointer = ref(empty); const beforePointer = ref(empty); enter && enter(); mode.add(Mode.draging); const posMove = (position: Pos) => { if (!can.dragMode) return; if (!pushed) { messages.value.push(pointer.value); singleDone.value = false; pushed = true; } moveIng = true; const current = beforeHandler ? beforeHandler(position) : position; pointer.value.x = current.x; pointer.value.y = current.y; beforePointer.value = { ...position }; }; const move = (ev: MouseEvent) => { posMove(getOffset(ev)); }; const preSelectIds = interactiveProps.value?.operate?.preSelectIds; let prevPoint: Pos = { ...empty }; const $stage = stage.value!.getNode(); if (preSelectIds) { shapesStatus.actives = preSelectIds .map((id) => $stage.findOne(`#${id}`)) .filter(Boolean) as EntityShape[]; } return mergeFuns( () => { mode.del(Mode.draging); shapesStatus.actives = [] }, watch(singleDone, () => { if (singleDone.value) { prevPoint = pointer.value; const prevBeforePoint = beforePointer.value; pointer.value = { ...empty }; beforePointer.value = { ...empty }; singleDone.value = true; moveIng = false; pushed = false; nextTick(() => posMove(prevBeforePoint)); } }), clickListener(dom, (_, ev) => { if (!moveIng || !can.dragMode || eqPoint(prevPoint, pointer.value)) return; if (preSelectIds?.length && $stage) { const joinIds: string[] = []; for (const id of preSelectIds) { const $shape = $stage.findOne(`#${id}`); if (!$shape) continue; const rect = $shape.getClientRect(); let x = ev.offsetX, y = ev.offsetY; if ( x > rect.x && x < rect.x + rect.width && y > rect.y && y < rect.y + rect.height ) { joinIds.push(id); } } if (joinIds.length) { attachInfos.set(pointer.value, joinIds); return; } } singleDone.value = true; }), listener(dom, "pointermove", move), ); }; return useInteractiveExpose( messages, init, singleDone, isRuning, quit, autoConsumed, attachInfos, ); };