123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699 |
- 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";
- export const rendererName = "renderer";
- export const rendererMap = new WeakMap<any, { unmounteds: (() => 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 = <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;
- 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;
- };
|