|
@@ -0,0 +1,700 @@
|
|
|
+import {
|
|
|
+ computed,
|
|
|
+ getCurrentInstance,
|
|
|
+ nextTick,
|
|
|
+ reactive,
|
|
|
+ Ref,
|
|
|
+ ref,
|
|
|
+ shallowRef,
|
|
|
+ toRaw,
|
|
|
+ watch,
|
|
|
+ WatchCallback,
|
|
|
+ watchEffect,
|
|
|
+ WatchOptions,
|
|
|
+ WatchSource,
|
|
|
+} from "vue";
|
|
|
+import { v4 as uuid } 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";
|
|
|
+
|
|
|
+export const rendererName = "renderer";
|
|
|
+export const rendererMap = new WeakMap<any, { unmounteds: (() => void)[] }>();
|
|
|
+
|
|
|
+export const useRendererInstance = () => {
|
|
|
+ let instance = getCurrentInstance()!;
|
|
|
+ while (instance.type.__name !== rendererName) {
|
|
|
+ if (instance.parent) {
|
|
|
+ instance = instance.parent;
|
|
|
+ } else {
|
|
|
+ throw "未发现渲染实例";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return instance;
|
|
|
+};
|
|
|
+
|
|
|
+export const installGlobalVar = <T>(
|
|
|
+ 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) {
|
|
|
+ val.onDestory && unmounteds.push(val.onDestory);
|
|
|
+ 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 = <T>(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<R>(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<DC<Stage> | 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<Size>();
|
|
|
+ const setSize = () => {
|
|
|
+ if (fix.value) return;
|
|
|
+ console.error(stage.value);
|
|
|
+ const container = stage.value?.getStage().container();
|
|
|
+ if (container) {
|
|
|
+ container.style.setProperty("display", "none");
|
|
|
+ }
|
|
|
+
|
|
|
+ const dom = stage.value!.getNode().container().parentElement!;
|
|
|
+ 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: () => {
|
|
|
+ fix || unResize();
|
|
|
+ unWatch && unWatch();
|
|
|
+ },
|
|
|
+ };
|
|
|
+}, Symbol("resize"));
|
|
|
+
|
|
|
+export const globalWatch = <T>(
|
|
|
+ source: WatchSource<T>,
|
|
|
+ cb: WatchCallback<T, T>,
|
|
|
+ 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;
|
|
|
+ const downHandler = (ev: PointerEvent) => {
|
|
|
+ const start = getOffset(ev, dom);
|
|
|
+ let prev = start;
|
|
|
+ down && down(start, ev);
|
|
|
+ props.notPrevent || ev.preventDefault();
|
|
|
+
|
|
|
+ moveHandler = (ev: PointerEvent) => {
|
|
|
+ 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 }) => {
|
|
|
+ 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), 3);
|
|
|
+ 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 extends object> = T & {
|
|
|
+ pause: () => void;
|
|
|
+ resume: () => void;
|
|
|
+ isPause: boolean;
|
|
|
+};
|
|
|
+export const usePause = <T extends object>(api?: T): PausePack<T> => {
|
|
|
+ const isPause = ref(false);
|
|
|
+ const result = (api || {}) as PausePack<T>;
|
|
|
+
|
|
|
+ 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<typeof useCursor>) => {
|
|
|
+ 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<DC<EntityShape> | 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<EntityShape> | DC<EntityShape>[] | undefined>,
|
|
|
+) => {
|
|
|
+ const cursor = useCursor();
|
|
|
+ const stage = useStage();
|
|
|
+ const drag = ref<Pos & { ndx: number }>();
|
|
|
+ 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) {
|
|
|
+ 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();
|
|
|
+ });
|
|
|
+
|
|
|
+ return mergeFuns(
|
|
|
+ listener(dom, "pointerup", () => {
|
|
|
+ pop && pop();
|
|
|
+ pop = null;
|
|
|
+ drag.value = undefined;
|
|
|
+ }),
|
|
|
+ () => {
|
|
|
+ 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<EntityShape | undefined>; 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<EntityShape>();
|
|
|
+ const enterHandler = (ev: KonvaEventObject<any, Stage>) => {
|
|
|
+ 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<DC<EntityShape> | 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;
|
|
|
+};
|