import { computed, getCurrentInstance, nextTick, reactive, Ref, ref, shallowRef, toRaw, watch, WatchCallback, watchEffect, WatchOptions, WatchSource, } from "vue"; import { v4 as uuidRaw } from "uuid"; import { DC, EntityShape, Pos, Size } from "./dec"; import { Stage } from "konva/lib/Stage"; import { Transform } from "konva/lib/Util"; import { lineLen } from "./math"; import { Viewer } from "./viewer"; import { KonvaEventObject } from "konva/lib/Node"; import { asyncTimeout } from "@/utils"; export const rendererName = "renderer"; export const rendererMap = new WeakMap void)[] }>(); export const uuid = uuidRaw export const useRendererInstance = () => { let instance = getCurrentInstance()!; while (instance.type.__name !== rendererName) { if (instance.parent) { instance = instance.parent; } else { throw "未发现渲染实例"; } } return instance; }; export const installGlobalVar = ( create: () => { var: T; onDestroy: () => void } | T, key = Symbol("globalVar") ) => { const useGlobalVar = (): T => { const instance = useRendererInstance() as any; const { unmounteds } = rendererMap.get(instance)!; if (!(key in instance)) { let val = create() as any; if (typeof val === "object" && "var" in val && "onDestroy" in val) { console.error('val.onDestory', val, key, val.onDestroy) val.onDestroy && unmounteds.push(val.onDestroy); if (import.meta.env.DEV) { unmounteds.push(() => { console.log("销毁变量", key); }); } val = val.var; } instance[key] = val; } return instance[key]; }; return useGlobalVar; }; export const useGlobalVar = installGlobalVar(() => { return { misPixel: 10, }; }); export const onlyId = () => uuid(); export const stackVar = (init?: T) => { const factory = (init: T) => ({ var: init, id: onlyId() }); const stack = reactive([]) as { var: T; id: string }[]; if (init) { stack.push(factory(init)); } const result = { get value() { return stack[stack.length - 1]?.var; }, set value(val) { stack[stack.length - 1].var = val; }, push(data: T) { stack.push(factory(data)); const item = stack[stack.length - 1]; const pop = (() => { const ndx = stack.findIndex(({ id }) => id === item.id); if (~ndx) { stack.splice(ndx, 1); } }) as (() => void) & { set: (data: T) => void }; pop.set = (data) => { item.var = data; }; return pop; }, pop() { if (stack.length - 1 > 0) { stack.pop(); } else { console.error("已到达栈顶"); } }, cycle(data: T, run: () => R): R { result.push(data); const r = run(); result.pop(); return r; }, }; return result; }; export const useCursor = installGlobalVar( () => stackVar("default"), Symbol("cursor") ); /** * 多个函数合并成一个函数 * @param fns * @returns */ export const mergeFuns = (...fns: (() => void)[] | (() => void)[][]) => { return () => { fns.forEach((fn) => { if (Array.isArray(fn)) { fn.forEach((f) => f()); } else { fn(); } }); }; }; export const useStage = installGlobalVar( () => shallowRef | undefined>(), Symbol("stage") ); export const listener = < T extends HTMLElement | Window, K extends keyof HTMLElementEventMap >( target: T, eventName: K, callback: (this: T, ev: HTMLElementEventMap[K]) => any ) => { target.addEventListener(eventName, callback as any); return () => { target.removeEventListener(eventName, callback as any); }; }; export const useGlobalResize = installGlobalVar(() => { const stage = useStage(); const size = ref(); const setSize = async () => { if (fix.value) return; const container = stage.value?.getStage().container(); if (container) { container.style.setProperty("display", "none"); } const dom = stage.value!.getNode().container().parentElement!; await asyncTimeout(16) size.value = { width: dom.offsetWidth, height: dom.offsetHeight, }; if (container) { container.style.removeProperty("display"); } }; const stopWatch = watchEffect(() => { if (stage.value) { setSize(); nextTick(() => stopWatch()); } }); let unResize = listener(window, "resize", setSize); const fix = ref(false); let unWatch: (() => void) | null = null; const setFixSize = (fixSize: { width: number; height: number } | null) => { if (fixSize) { size.value = { ...fixSize }; unWatch && unWatch(); unWatch = watchEffect(() => { const $stage = stage.value?.getStage(); if ($stage) { $stage.width(fixSize.width); $stage.height(fixSize.height); nextTick(() => unWatch && unWatch()); } }); } if (fix.value && !fixSize) { unResize = listener(window, "resize", setSize); fix.value = false; nextTick(setSize); } else if (!fix.value && fixSize) { fix.value = true; unResize(); } }; return { var: { setFixSize: setFixSize, updateSize: setSize, size, fix, }, onDestroy: () => { console.error('size onDest') unResize(); unWatch && unWatch(); }, }; }, Symbol("resize")); export const globalWatch = ( source: WatchSource, cb: WatchCallback, options?: WatchOptions ): (() => void) => { let stop: () => void; nextTick(() => { stop = watch(source, cb as any, options as any); }); return () => { stop && stop(); }; }; export const getOffset = ( ev: MouseEvent | TouchEvent, dom = ev.target! as HTMLElement, ndx = 0 ) => { const event = ev instanceof TouchEvent ? ev.changedTouches[ndx] : ev; const rect = dom.getBoundingClientRect(); const offsetX = event.clientX - rect.left; const offsetY = event.clientY - rect.top; return { x: offsetX, y: offsetY, }; }; type DragProps = { move?: ( info: Record<"start" | "prev" | "end", Pos> & { ev: PointerEvent } ) => void; down?: (pos: Pos, ev: PointerEvent) => void; up?: (pos: Pos, ev: PointerEvent) => void; notPrevent?: boolean; }; export const dragListener = ( dom: HTMLElement, props: DragProps | DragProps["move"] = {} ) => { if (typeof props === "function") { props = { move: props }; } const { move, up, down } = props; const mount = document.documentElement; if (!move && !up && !down) return () => {}; let moveHandler: any, endHandler: any; let button: number = -1 const downHandler = (ev: PointerEvent) => { button = ev.button const start = getOffset(ev, dom); let prev = start; down && down(start, ev); props.notPrevent || ev.preventDefault(); moveHandler = (ev: PointerEvent) => { if (ev.buttons <= 0) { endHandler() return; } const end = getOffset(ev, dom); move!({ start, end, prev, ev }); prev = end; props.notPrevent || ev.preventDefault(); }; endHandler = (ev: PointerEvent) => { up && up(getOffset(ev, dom), ev); mount.removeEventListener("pointermove", moveHandler); mount.removeEventListener("pointerup", endHandler); props.notPrevent || ev.preventDefault(); }; move && mount.addEventListener("pointermove", moveHandler, { passive: false }); mount.addEventListener("pointerup", endHandler, { passive: false }); }; dom.addEventListener("pointerdown", downHandler, { passive: false }); return () => { dom.removeEventListener("pointerdown", downHandler); moveHandler && mount.removeEventListener("pointermove", moveHandler); endHandler && mount.removeEventListener("pointerup", endHandler); }; }; export const getTouchScaleProps = ( ev: TouchEvent, dom = ev.target! as HTMLElement ) => { const start = getOffset(ev, dom, 0); const end = getOffset(ev, dom, 1); const center = { x: (end.x + start.x) / 2, y: (end.y + start.y) / 2, }; const initDist = lineLen(start, end); return { center, dist: initDist, }; }; export const touchScaleListener = ( dom: HTMLElement, cb: (props: { center: Pos; scale: number }) => void ) => { const mount = document.documentElement; let moveHandler: (ev: TouchEvent) => void; let endHandler: (ev: TouchEvent) => void; const startHandler = (ev: TouchEvent) => { if (ev.changedTouches.length <= 1) return; let prevScale = getTouchScaleProps(ev, dom); ev.preventDefault(); moveHandler = (ev: TouchEvent) => { if (ev.changedTouches.length <= 1) return; const curScale = getTouchScaleProps(ev, dom); cb({ center: prevScale.center, scale: curScale.dist / prevScale.dist }); prevScale = curScale; ev.preventDefault(); }; endHandler = (ev: TouchEvent) => { mount.removeEventListener("touchmove", moveHandler); mount.removeEventListener("touchend", endHandler); ev.preventDefault(); }; mount.addEventListener("touchmove", moveHandler, { passive: false, }); mount.addEventListener("touchend", endHandler, { passive: false, }); }; dom.addEventListener("touchstart", startHandler, { passive: false }); return () => { dom.removeEventListener("touchstart", startHandler); mount.removeEventListener("touchmove", moveHandler); mount.removeEventListener("touchend", endHandler); }; }; export const wheelListener = ( dom: HTMLElement, cb: (props: { center: Pos; scale: number }) => void ) => { const wheelHandler = (ev: WheelEvent) => { const scale = 1 - ev.deltaY / 1000; const center = { x: ev.offsetX, y: ev.offsetY }; cb({ center, scale }); ev.preventDefault(); }; dom.addEventListener("wheel", wheelHandler); return () => { dom.removeEventListener("wheel", wheelHandler); }; }; export const scaleListener = ( dom: HTMLElement, cb: (props: { center: Pos; scale: number }) => void ) => mergeFuns(touchScaleListener(dom, cb), wheelListener(dom, cb)); export const useViewer = installGlobalVar(() => { const stage = useStage(); const viewer = new Viewer(); const transform = ref(new Transform()); const cursor = useCursor(); const init = (dom: HTMLDivElement) => { const dragDestroy = dragListener(dom, { move: ({ end, prev, ev }) => { if (cursor.value !== "move") { viewer.movePixel({ x: end.x - prev.x, y: 0 }); } }, notPrevent: true, }); const scaleDestroy = scaleListener(dom, (info) => { const currentScalex = viewer.viewMat.decompose().scaleX; const finalScale = currentScalex * info.scale; const scale = Math.min(Math.max(finalScale, 0.5), 8); if (cursor.value !== "move") { viewer.scalePixel(info.center, { x: scale / currentScalex, y: 1 }); } }); viewer.bus.on("transformChange", (newTransform) => { // console.log(newTransform.m) transform.value = newTransform; }); transform.value = viewer.transform; return mergeFuns(dragDestroy, scaleDestroy); }; return { var: { transform: transform, viewer, }, onDestroy: globalWatch( () => stage.value?.getNode().container(), (dom, _, onCleanup) => { dom && onCleanup(init(dom)); }, { immediate: true } ), }; }, Symbol("viewer")); export const useViewerTransform = installGlobalVar(() => { const viewer = useViewer(); return viewer.transform; }, Symbol("viewTransform")); export const useViewerTransformConfig = () => { const transform = useViewerTransform(); return computed(() => transform.value.decompose()); }; export const useViewerInvertTransform = () => { const transform = useViewerTransform(); return computed(() => transform.value.copy().invert()); }; export const useViewerInvertTransformConfig = () => { const transform = useViewerInvertTransform(); return computed(() => transform.value.decompose()); }; export const flatPositions = (positions: Pos[]) => positions.flatMap((p) => [p.x, p.y]); export type PausePack = T & { pause: () => void; resume: () => void; isPause: boolean; }; export const usePause = (api?: T): PausePack => { const isPause = ref(false); const result = (api || {}) as PausePack; Object.defineProperty(result, "isPause", { get() { return isPause.value; }, set(v) { return true; }, }); result.pause = () => (isPause.value = true); result.resume = () => (isPause.value = false); return result; }; const hoverPointer = (shape: EntityShape, cursor: ReturnType) => { shape.on("pointerenter.hover", () => { const pop = cursor.push("pointer"); shape.on("pointerleave.hover", () => { pop(); shape.off("pointerleave.hover"); }); }); return () => { shape.off("pointerenter.hover pointerleave.hover"); } } export const useHoverPointer = (shape: Ref | undefined>) => { const cursor = useCursor() watchEffect((onCleanup) => { if (shape.value) { console.error('shape.value', shape.value) onCleanup(hoverPointer(shape.value.getNode(), cursor)) } }) return cursor } export const useDrag = ( shape: Ref | DC[] | undefined>, ) => { const cursor = useCursor(); const stage = useStage(); const drag = ref(); const invMat = useViewerInvertTransform(); const init = (shape: EntityShape, dom: HTMLDivElement, ndx: number) => { shape.on("pointerenter.drag", () => { const pop = cursor.push("pointer"); shape.on("pointerleave.drag", () => { pop(); shape.off("pointerleave.drag"); }); }); let pop: (() => void) | null = null; let start = { x: 0, y: 0 } shape.on("pointerdown.drag", (ev) => { pop = cursor.push("move") start = invMat.value.point(getOffset(ev.evt, stage.value!.getNode().container())); }); shape.draggable(true); shape.dragBoundFunc(function (this: any, _: any, ev: MouseEvent) { console.log(ev.buttons) if (ev.buttons <= 0) return upHandler() const current = invMat.value.point(getOffset(ev, stage.value!.getNode().container())); drag.value = { x: current.x - start.x, y: current.y - start.y, ndx, }; start = current return this.absolutePosition(); }); const upHandler = () => { pop && pop(); pop = null; drag.value = undefined; console.error('up') } return mergeFuns( listener(document.documentElement, "pointerup", upHandler), () => { shape.off("pointerenter.drag pointerleave.drag pointerdown.drag"); if (pop) { pop(); shape.draggable(false); } } ); }; const result = usePause({ drag, stop: () => { stopWatch(); }, }); const stopWatch = watch( () => { const shapes = shape.value ? Array.isArray(shape.value) ? [...shape.value] : [shape.value] : []; if (shapes.some((item) => !item)) { return []; } return shapes; }, (shapes, _, onCleanup) => { onCleanup( mergeFuns( shapes.map((shape, ndx) => watchEffect((onCleanup) => { if (!result.isPause && shape?.getNode() && stage.value?.getNode) { onCleanup( init( shape?.getNode(), stage.value?.getNode().container(), ndx ) ); } }) ) ) ); }, { immediate: true } ); return result; }; const stageHoverMap = new WeakMap< Stage, { result: Ref; count: number; des: () => void } >(); export const getHoverShape = (stage: Stage) => { let isStop = false; const stop = () => { if (isStop || !stageHoverMap.has(stage)) return; isStop = true; const data = stageHoverMap.get(stage)!; if (--data.count <= 0) { data.des(); } }; if (stageHoverMap.has(stage)) { const data = stageHoverMap.get(stage)!; ++data.count; return [data.result, stop] as const; } const hover = ref(); const enterHandler = (ev: KonvaEventObject) => { const target = ev.target; hover.value = target; target.off(`pointerleave`, leaveHandler); target.on(`pointerleave`, leaveHandler as any); }; const leaveHandler = () => { if (hover.value) { hover.value.off(`pointerleave`, leaveHandler); hover.value = undefined; } }; stage.on(`pointerenter`, enterHandler); stageHoverMap.set(stage, { result: hover, count: 1, des: () => { stage.off(`pointerenter`, enterHandler); leaveHandler(); stageHoverMap.delete(stage); }, }); return [hover, stop] as const; }; export const useShapeIsHover = (shape: Ref | undefined>) => { const stage = useStage(); const isHover = ref(false); const stop = watch( () => ({ stage: stage.value?.getNode(), shape: shape.value?.getNode() }), ({ stage, shape }, _, onCleanup) => { if (!stage || !shape || result.isPause) { isHover.value = false; return; } const [hoverShape, stopHoverListener] = getHoverShape(stage); watchEffect(() => { isHover.value = !!( hoverShape.value && shapeTreeContain([shape], toRaw(hoverShape.value)) ); }); onCleanup(stopHoverListener); }, { immediate: true } ); const result = usePause([isHover, stop] as const); return result; }; export const shapeTreeContain = ( parent: EntityShape | EntityShape[], target: EntityShape, checked: EntityShape[] = [] ) => { const eq = Array.isArray(parent) ? (shape: EntityShape) => parent.includes(shape) : (shape: EntityShape) => parent === shape; return shapeParentsEq(target, eq, checked); }; export const shapeParentsEq = ( target: EntityShape, eq: (shape: EntityShape) => boolean, checked: EntityShape[] = [] ) => { while (target) { if (checked.includes(target)) return null; if (eq(target)) { return target; } target = target.parent as any; } return null; };