import { useMouseShapeStatus } from "./use-mouse-status.ts"; import { computed, nextTick, Ref, ref, toRaw, watch, watchEffect } from "vue"; import { DC, EntityShape } from "../../deconstruction"; import { installGlobalVar, usePointerIntersections, useStage, useTransformIngShapes, } from "./use-global-vars.ts"; import { useCan, useMode } from "./use-status"; import { Mode } from "../../constant/mode.ts"; import { Transform } 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 { debounce, 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 } from "@/constant"; import { useComponentSnap } from "./use-snap.ts"; import { useViewer, 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"; import { usePause } from "./use-pause.ts"; import { getSvgContent, parseSvgContent } from "@/utils/resource.ts"; import { Path } from "konva/lib/shapes/Path"; import { Circle } from "konva/lib/shapes/Circle"; export type TransformerExtends = Transformer & { queueShapes: Ref; }; export const useTransformer = installGlobalVar(() => { const anchorCornerRadius = 5; const transformer = new Transformer({ borderStrokeWidth: 2, borderStroke: themeColor, anchorCornerRadius, anchorSize: anchorCornerRadius * 2, borderEnabled: true, rotationSnaps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 360], rotationSnapTolerance: 3, anchorStrokeWidth: 2, anchorStroke: themeColor, anchorFill: "#fff", flipEnabled: false, padding: 0, useSingleNodeRotation: true, }) as TransformerExtends; transformer.queueShapes = ref([]); transformer.on("transformstart.attachText", (e) => { 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: 6, width: 100, align: "center", offset: { x: 50, y: 0 }, }); $g.add($text); $node.parent!.add($g); setShapeTransform($g, $node.getTransform()); $g.y($g.y() - 4 * $text.fontSize()); const updateText = () => { const rotation = (transformer.rotation() + 360) % 360; $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(); }); }); const cleanups: (() => void)[] = []; const getData = useGetComponentData(); const viewer = useViewer(); getSvgContent("./icons/m_rotate.svg").then((svgContent) => { const svg = parseSvgContent(svgContent); const group = new Group({ listening: false }); const rotateRect = transformer.findOne(".rotater")!; const size = 8; rotateRect.scale({ x: 2, y: 2 }); const bg = new Circle({ radius: 6, fill: themeColor, x: 0.5, y: 0.5 }); group.add(bg); svg.paths.forEach((path) => { const $path = new Path({ ...path, fill: "#ffffff" }); const scale = size / svg.width; $path.scale({ x: scale, y: scale }); $path.offset({ x: svg.width / 2, y: svg.height / 2 }); group.add($path); }); rotateRect.opacity(0); rotateRect.parent!.add(group); const update = async () => { await nextTick(); setShapeTransform(group, rotateRect.getTransform()); group.x(group.x() + 8); group.y(group.y() + 8); group.visible(rotateRect.visible()); }; viewer.viewer.bus.on("transformChange", update); const stopShapeJoin = watch( () => transformer.queueShapes.value[0], (shape, _b, onCleanup) => { if (shape) { const data = getData(computed(() => shape)); update(); shape.on("bound-change", update); onCleanup(() => shape.off("bound-change", update)); onCleanup(watch(data, update)); } }, { immediate: true } ); cleanups.push( watch(() => transformer.queueShapes.value, update, { flush: "sync" }), () => viewer.viewer.bus.off("transformChange", update), stopShapeJoin ); }); return { var: transformer, onDestroy: () => mergeFuns(cleanups)(), }; }, Symbol("transformer")); export const usePointerIsTransformerInner = () => { const transformer = useTransformer(); const stage = useStage(); const hitShapes = usePointerIntersections(); return () => { const $stage = stage.value!.getStage(); const pos = $stage.pointerPos; if (!pos) return false; if (!hitShapes.value.length) return false; const selfShapes = [ ...transformer.children, ...transformer.queueShapes.value.map(toRaw), ] as any; for (const shape of hitShapes.value) { if (selfShapes.includes(shape)) return true; } return false; }; }; 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 undefined; 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) { return shape.absolutePosition(); } 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) => { if (ev.evt.button !== 0) return; enter(conversion(getOffset(ev.evt))); }); return mergeFuns([ () => { shape.draggable(false); shape.off("pointerdown.mouse-drag"); start && leave(); }, listener(document.documentElement, "pointerup", (ev) => { if (ev.button !== 0) return; start && leave(); }), ]); }; const result = usePause(offset); watch( () => (can.editMode || mode.include(Mode.update)) && (status.value.active || status.value.press || status.value.hover) && !result.isPause, (canEdit, _, onCleanup) => { canEdit && onCleanup(init(shape.value!.getNode())); }, { immediate: true } ); return result; }; 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 getData = useGetComponentData(); const mode = useMode(); const transformer = useTransformer(); const transformIngShapes = useTransformIngShapes(); const viewTransform = useViewerTransform(); const can = useCan(); const init = ($shape: T) => { let isRun = false; let rep: Rep; if (replaceShape) { rep = replaceShape($shape); } else { rep = { tempShape: $shape, destory: emptyFn, update: emptyFn, }; } let selfFire = false; const set = (appleTransform: Transform | undefined) => { transform.value = appleTransform; selfFire = true; $shape.fire("bound-change"); selfFire = false; }; const updateTransform = () => { if (!can.dragMode) return; let appleTransform = rep.tempShape.getTransform().copy(); if (appleTransform.m.some((m) => m === null || Number.isNaN(m))) { return; } if (handlerTransform) { appleTransform = handlerTransform(appleTransform); setShapeTransform(rep.tempShape, appleTransform); } set(appleTransform); }; rep.tempShape.on("transform.shapemer", updateTransform); const rect = ref(rep.tempShape.getClientRect()); const boundHandler = () => { if (!selfFire) { rep.update && rep.update(); // transformer.forceUpdate() } rect.value = rep.tempShape.getClientRect(); }; $shape.on("bound-change", boundHandler); // 拖拽时要更新矩阵 let prevMoveTf: Transform | null = null; const stopDragWatch = watch( offset, (translate, oldTranslate) => { if (translate) { if (!oldTranslate) { isRun = true; rep.init && rep.init(); 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; isRun = false; } }, { immediate: true } ); const data = getData(computed(() => $shape)); const stopTransformerWatch = watch( () => status.value.active && !data.value?.disableTransformer && rect.value.width > 0.5 && rect.value.height > 0.5, (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); rep.init && rep.init(); let isEnter = false; const downHandler = () => { isRun = true; 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( document.documentElement, "pointerup", () => { isRun = false; 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( data, debounce(() => rep.update && rep.update(), 16), { flush: "post", deep: true } ); onCleanup(() => { try { for (const key in oldConfig) { (transformer as any)[key](oldConfig[key]); } } catch (e) { console.error("页面销毁?"); console.error(e); } 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); }); }, { immediate: true, flush: "post" } ); return () => { $shape.off("bound-change", boundHandler); rep.tempShape.off("transform.shapemer", updateTransform); stopDragWatch(); stopTransformerWatch(); rep.destory(); }; }; watch( () => shape.value, (shape, _, onCleanup) => { if (!shape) return; onCleanup( watch( () => (can.editMode || mode.include(Mode.update)) && (status.value.active || status.value.hover || status.value.press), (canEdit, _, onCleanup) => { if (canEdit) { const stop = init(shape.getNode()); 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: themeColor, 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; init?: (data: K, shape: T) => void; destory?: () => void; }; export type CustomTransformerProps< T extends BaseItem, S extends EntityShape > = { openSnap?: boolean; getRepShape?: GetRepShape; start?: () => void; beforeHandler?: (data: T, mat: Transform) => T; handler?: ( data: T, mat: Transform, raw?: Transform ) => Transform | void | boolean; 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); }, init: () => { repResult.init && repResult.init(data.value, repResult.shape); }, destory: () => { repResult.destory && repResult.destory(); destory(); }, }; }) ); let callMat: Transform; watch(transform, (current, oldTransform) => { if (!oldTransform) { props.start && props.start(); } if (current) { if (!handler) return; const snapData = props.beforeHandler ? props.beforeHandler(data.value, current) : data.value; let nTransform; const raw = current; if (needSnap && (nTransform = needSnap[0](snapData))) { current = nTransform.multiply(current); } callMat = current; const mat = handler(data.value, current, raw); 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 inverAttitude: Transform; let stableVs = data.value.points; let tempVs = data.value.points; const transformer = useTransformer(); 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)); data.attitude = mat.m; }, 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")!; 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; transformer.forceUpdate(); }; update(data.value); return { update, shape: repShape, }; }, }); }; export const useMatCompTransformer = ( shape: Ref | undefined>, data: Ref, callback: (data: T) => void, getRepShape = cloneRepShape ) => { return useCustomTransformer(shape, data, { beforeHandler(data, mat) { return { ...data, mat: mat.m }; }, handler(data, mat) { data.mat = mat.m; // return true }, getRepShape, callback, openSnap: false, }); };