import { useMouseShapeStatus } from "./use-mouse-status.ts"; import { Ref, ref, watch } from "vue"; import { DC, EntityShape } from "../../deconstruction"; import { installGlobalVar, useCan, useMode, useStage, useTransformIngShapes, } from "./use-global-vars.ts"; import { Mode } from "../../constant/mode.ts"; import { Transform, Util } from "konva/lib/Util"; import { Pos, vector } from "@/utils/math.ts"; import { useConversionPosition } from "./use-coversion-position.ts"; import { getOffset, listener } from "@/utils/event.ts"; import { flatPositions, mergeFuns, round } from "@/utils/shared.ts"; import { Line } from "konva/lib/shapes/Line"; import { setShapeTransform } from "@/utils/shape.ts"; import { Transformer } from "../transformer.ts"; import { TransformerConfig } from "konva/lib/shapes/Transformer"; import { themeColor, themeMouseColors } from "@/constant/help-style.ts"; import { useComponentSnap } from "./use-snap.ts"; import { useViewerInvertTransform, useViewerTransform } from "./use-viewer.ts"; import { Rect } from "konva/lib/shapes/Rect"; import { Text } from "konva/lib/shapes/Text"; import { Group } from "konva/lib/Group"; import { BaseItem } from "../components/util.ts"; import { useGetComponentData } from "./use-component.ts"; export type TransformerExtends = Transformer & { queueShapes: Ref; }; export const useTransformer = installGlobalVar(() => { const anchorCornerRadius = 5; const transformer = new Transformer({ borderStrokeWidth: 2, borderStroke: themeMouseColors.pub, anchorCornerRadius, anchorSize: anchorCornerRadius * 2, rotationSnaps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 360], rotationSnapTolerance: 3, anchorStrokeWidth: 2, anchorStroke: themeMouseColors.pub, anchorFill: themeMouseColors.press, flipEnabled: false, padding: 10, useSingleNodeRotation: true, }) as TransformerExtends; transformer.queueShapes = ref([]); transformer.on("transformstart.attachText", () => { const operateType = transformer.getActiveAnchor() as TransformerVectorType; if (operateType !== "rotater") return; const $node = transformer.findOne(".rotater")!; const $g = new Group(); const $text = new Text({ listening: false, fill: themeColor, fontSize: 12, width: 100, align: "center", offset: { x: 50, y: 0 }, }); $g.add($text); $node.parent!.add($g); setShapeTransform($g, $node.getTransform()); $g.y($g.y() - 2 * $text.fontSize()); const updateText = () => { const rotation = transformer.rotation(); $text.rotation(-rotation).text(` ${round(rotation, 1)}°`); }; updateText(); transformer.on("transform.attachText", updateText); transformer.on("transformend.attachText", () => { transformer.off("transform.attachText transformend.attachText"); $g.destroy(); }); }); return transformer; }, Symbol("transformer")); export const usePointerIsTransformerInner = () => { const transformer = useTransformer(); const stage = useStage(); return () => { const $stage = stage.value!.getStage(); const tfRect = transformer.getClientRect(); const padding = transformer.padding(); tfRect.x -= padding; tfRect.y -= padding; tfRect.width += padding; tfRect.height += padding; const pointRect = { ...$stage.pointerPos!, width: 1, height: 1 }; return Util.haveIntersection(tfRect, pointRect); }; }; export type ScaleVectorType = | "middle-left" | "middle-right" | "top-center" | "bottom-center" | "top-right" | "top-left" | "bottom-right" | "bottom-left"; export type TransformerVectorType = ScaleVectorType | "rotater"; export const useGetTransformerOperType = () => { const transformer = useTransformer(); return () => { if (!transformer.nodes().length) return null; return transformer.getActiveAnchor() as TransformerVectorType; }; }; export const useGetTransformerVectors = () => { const viewerInvertTransform = useViewerInvertTransform(); const transformer = useTransformer(); return (type: TransformerVectorType): Pos | null => { if (!transformer.nodes().length) return null; const merTransform = viewerInvertTransform.value .copy() .multiply(transformer.getTransform()); const getVector = (operateType: TransformerVectorType): Pos => { if (operateType === "rotater") { return vector(getVector("bottom-left")) .add(getVector("bottom-right")) .add(getVector("top-left")) .add(getVector("top-right")) .divideScalar(4); } else { const centerNode = transformer.findOne(`.${operateType}`)!; return { x: centerNode.x(), y: centerNode.y(), }; } }; return merTransform.point(getVector(type)); }; }; export const useGetTransformerOperDirection = () => { const getTransformerOperType = useGetTransformerOperType(); const getTransformerVectors = useGetTransformerVectors(); const originTypeMap = { "middle-left": "middle-right", "middle-right": "middle-left", "top-center": "bottom-center", "bottom-center": "top-center", "top-right": "bottom-left", "top-left": "bottom-right", "bottom-right": "top-left", "bottom-left": "top-right", rotater: "rotater", } as const; return () => { const operateType = getTransformerOperType(); if (!operateType || !originTypeMap[operateType]) return null; const origin = getTransformerVectors(originTypeMap[operateType]); const operTarget = getTransformerVectors(operateType); return origin && operTarget && ([origin, operTarget] as const); }; }; export const useShapeDrag = (shape: Ref | undefined>) => { const offset = ref(); const mode = useMode(); const can = useCan(); const conversion = useConversionPosition(true); const transformIngShapes = useTransformIngShapes(); const status = useMouseShapeStatus(shape) const init = (shape: EntityShape) => { const dom = shape.getStage()!.container(); let start: Pos | undefined; const enter = (position: Pos) => { mode.push(Mode.update); if (!start) { start = position; if (!can.dragMode) return; mode.add(Mode.draging); transformIngShapes.value.push(shape); } }; const leave = () => { if (start) { offset.value = void 0; mode.pop(); start = void 0; const ndx = transformIngShapes.value.indexOf(shape); ~ndx && transformIngShapes.value.splice(ndx, 1); } }; shape.draggable(true); shape.dragBoundFunc((_, ev) => { if (!start) { enter(ev); } else if (can.dragMode) { const end = conversion(getOffset(ev, dom)); offset.value = { x: end.x - start.x, y: end.y - start.y, }; } return shape.absolutePosition(); }); shape.on("pointerdown.mouse-drag", (ev) => { enter(conversion(getOffset(ev.evt))); }); return mergeFuns([ () => { shape.draggable(false); shape.off("pointerdown.mouse-drag"); start && leave(); }, listener(document.documentElement, "pointerup", () => { start && leave(); }), ]); }; watch( () => (can.editMode || mode.include(Mode.update)) && (status.value.active || status.value.hover), (canEdit, _, onCleanup) => { canEdit && onCleanup(init(shape.value!.getNode())); } ); return offset; }; type Rep = { tempShape: T; init?: () => void; update?: () => void; destory: () => void; }; const emptyFn = () => {}; export const useShapeTransformer = ( shape: Ref | undefined>, transformerConfig: TransformerConfig = {}, replaceShape?: (shape: T) => Rep, handlerTransform?: (transform: Transform) => Transform ) => { const offset = useShapeDrag(shape); const transform = ref(); const status = useMouseShapeStatus(shape); const mode = useMode(); const transformer = useTransformer(); const transformIngShapes = useTransformIngShapes(); const viewTransform = useViewerTransform(); const getComponentData = useGetComponentData(); const can = useCan(); const init = ($shape: T) => { let rep: Rep; if (replaceShape) { rep = replaceShape($shape); } else { rep = { tempShape: $shape, destory: emptyFn, update: emptyFn, }; } const updateTransform = () => { if (!can.dragMode) return; let appleTransform = rep.tempShape.getTransform().copy(); if (handlerTransform) { appleTransform = handlerTransform(appleTransform); setShapeTransform(rep.tempShape, appleTransform); } transform.value = appleTransform; }; rep.tempShape.on("transform.shapemer", updateTransform); const boundHandler = () => rep.update && rep.update(); $shape.on("bound-change", boundHandler); // 拖拽时要更新矩阵 let prevMoveTf: Transform | null = null; const stopDragWatch = watch( offset, (translate, oldTranslate) => { if (translate) { if (!oldTranslate) { rep.update && rep.update(); } const moveTf = new Transform().translate(translate.x, translate.y); const finalTf = moveTf.copy(); prevMoveTf && finalTf.multiply(prevMoveTf.invert()); finalTf.multiply(rep.tempShape.getTransform()); prevMoveTf = moveTf; setShapeTransform(rep.tempShape, finalTf); rep.tempShape.fire("transform"); } else { prevMoveTf = null; transform.value = void 0; } }, { immediate: true } ); const stopTransformerWatch = watch( () => status.value.active, (active, _, onCleanup) => { const parent = $shape.parent; if (!(active && parent)) return; const oldConfig: TransformerConfig = {}; for (const key in transformerConfig) { oldConfig[key] = (transformer as any)[key](); (transformer as any)[key](transformerConfig[key]); } transformer.nodes([rep.tempShape]); transformer.queueShapes.value = [$shape]; parent.add(transformer); let isEnter = false; const downHandler = () => { if (isEnter) { mode.pop(); } mode.push(Mode.update); isEnter = true; if (!can.dragMode) return; rep.update && rep.update(); mode.add(Mode.draging); transformIngShapes.value.push($shape); }; transformer.on("pointerdown.shapemer", downHandler); const stopPointupListener = listener( $shape.getStage()!.container(), "pointerup", () => { if (isEnter) { mode.pop(); transform.value = void 0; isEnter = false; const ndx = transformIngShapes.value.indexOf($shape); ~ndx && transformIngShapes.value.splice(ndx, 1); } } ); const stopTransformerForceUpdate = watch( viewTransform, () => transformer.forceUpdate(), { flush: "post" } ); const stopLeaveUpdate = watch( () => getComponentData($shape).value, (val) => { rep.update && rep.update(); }, { flush: "post", deep: true } ); onCleanup(() => { for (const key in oldConfig) { (transformer as any)[key](oldConfig[key]); } stopTransformerForceUpdate(); stopPointupListener(); stopLeaveUpdate(); // TODO: 有可能transformer已经转移 if (transformer.queueShapes.value.includes($shape)) { transformer.nodes([]); transformer.queueShapes.value = []; } transform.value = void 0; if (isEnter) { mode.pop(); const ndx = transformIngShapes.value.indexOf($shape); ~ndx && transformIngShapes.value.splice(ndx, 1); } transformer.off("pointerdown.shapemer", downHandler); }); } ); return () => { $shape.off("bound-change", boundHandler); rep.tempShape.off("transform.shapemer", updateTransform); stopDragWatch(); stopTransformerWatch(); rep.destory(); }; }; watch( () => shape.value, (shape, _) => { if (!shape) return; watch( () => (can.editMode || mode.include(Mode.update)) && (status.value.active || status.value.hover), (canEdit, _, onCleanup) => { if (canEdit) { const stop = init(shape.getStage()); onCleanup(stop); } else { onCleanup(() => {}); } }, { immediate: true } ); } ); return transform; }; export const cloneRepShape = ( shape: T ): ReturnType> => { shape = ((shape as Group)?.findOne && ((shape as Group)?.findOne(".repShape") as T)) || shape; return { shape: shape.clone({ fill: "rgb(0, 255, 0)", visible: false, strokeWidth: 0, }), update: (_, rep) => { setShapeTransform(rep, shape.getTransform()); }, }; }; export const transformerRepShapeHandler = ( shape: T, repShape: T ) => { if (import.meta.env.DEV) { repShape.visible(true); repShape.opacity(0.1); } shape.parent!.add(repShape); repShape.zIndex(shape.getZIndex()); repShape.listening(false); shape.repShape = repShape; return [ repShape, () => { shape.repShape = undefined; repShape.remove(); }, ] as const; }; type GetRepShape = (shape: T) => { shape: T; update?: (data: K, shape: T) => void; }; export type CustomTransformerProps< T extends BaseItem, S extends EntityShape > = { openSnap?: boolean; getRepShape?: GetRepShape; beforeHandler?: (data: T, mat: Transform) => T; handler?: (data: T, mat: Transform) => Transform | void | true; callback?: (data: T, mat: Transform) => void; transformerConfig?: TransformerConfig; }; export const useCustomTransformer = ( shape: Ref | undefined>, data: Ref, props: CustomTransformerProps ) => { const { getRepShape, handler, callback, openSnap, transformerConfig } = props; const needSnap = openSnap && useComponentSnap(data.value.id); const transformer = useTransformer(); let repResult: ReturnType>; const transform = useShapeTransformer( shape, transformerConfig, getRepShape && ((shape) => { repResult = getRepShape(shape); const [_, destory] = transformerRepShapeHandler(shape, repResult.shape); return { tempShape: repResult.shape, update: () => { repResult.update && repResult.update(data.value, repResult.shape); }, destory, }; }) ); let callMat: Transform; watch(transform, (current, oldTransform) => { if (current) { if (!handler) return; const snapData = props.beforeHandler ? props.beforeHandler(data.value, current) : data.value; let nTransform; if (needSnap && (nTransform = needSnap[0](snapData))) { current = nTransform.multiply(current); } callMat = current; const mat = handler(data.value, current); if (mat) { if (repResult.update) { repResult.update(data.value, repResult.shape); } else if (mat !== true) { setShapeTransform(repResult.shape, mat); callMat = mat; } transformer.forceUpdate(); } } else if (oldTransform) { needSnap && needSnap[1](); callback && callback(data.value, callMat); } }); return transform; }; export type LineTransformerData = BaseItem & { points: Pos[]; attitude: number[]; }; export const useLineTransformer = ( shape: Ref | undefined>, data: Ref, callback: (data: T) => void, genRepShape?: ($shape: Line) => Line ) => { let tempShape: Line; let inverAttitude: Transform; let stableVs = data.value.points; let tempVs = data.value.points; useCustomTransformer(shape, data, { openSnap: true, beforeHandler(data, mat) { const transfrom = mat.copy().multiply(inverAttitude); return { ...data, points: stableVs.map((v) => transfrom.point(v)), }; }, handler(data, mat) { // 顶点更新 const transfrom = mat.copy().multiply(inverAttitude); data.points = tempVs = stableVs.map((v) => transfrom.point(v)); }, callback(data, mat) { data.attitude = mat.m; data.points = stableVs = tempVs; callback(data); }, getRepShape($shape) { let repShape: Line; if (genRepShape) { repShape = genRepShape($shape); } else { repShape = cloneRepShape($shape).shape; } repShape = (repShape as any).points ? repShape : (repShape as unknown as Group).findOne(".line")!; tempShape = repShape; const update = (data: T) => { const attitude = new Transform(data.attitude); const inverMat = attitude.copy().invert(); setShapeTransform(repShape, attitude); const initVs = data.points.map((v) => inverMat.point(v)); stableVs = tempVs = data.points; repShape.points(flatPositions(initVs)); repShape.closed(true); inverAttitude = inverMat; }; update(data.value); return { update, shape: repShape, }; }, }); }; export const useMatCompTransformer = ( shape: Ref | undefined>, data: Ref, callback: (data: T) => void ) => { return useCustomTransformer(shape, data, { beforeHandler(data, mat) { return { ...data, mat: mat.m }; }, handler(data, mat) { data.mat = mat.m; }, getRepShape: cloneRepShape, callback, openSnap: true, }); };