|
@@ -0,0 +1,336 @@
|
|
|
+import { useMouseShapeStatus } from "./use-mouse-status.ts";
|
|
|
+import { Ref, ref, watch } from "vue";
|
|
|
+import { DC, EntityShape } from "../../helper/deconstruction";
|
|
|
+import { Shape } from "konva/lib/Shape";
|
|
|
+import { installGlobalVar, useMode, useStage, useTransformIngShapes } from "./use-global-vars.ts";
|
|
|
+import { Mode } from "../../constant/mode.ts";
|
|
|
+import { Transform, Util } from "konva/lib/Util";
|
|
|
+import { Pos } from "@/utils/math.ts";
|
|
|
+import { useConversionPosition } from "./use-coversion-position.ts";
|
|
|
+import { getOffset, listener } from "@/utils/event.ts";
|
|
|
+import { flatPositions, mergeFuns } from "@/utils/shared.ts";
|
|
|
+import { Line } from "konva/lib/shapes/Line";
|
|
|
+import { setShapeTransform } from "@/utils/shape.ts";
|
|
|
+import { TransformerConfig, Transformer } from "konva/lib/shapes/Transformer";
|
|
|
+import { themeMouseColors } from "@/constant/help-style.ts";
|
|
|
+import { useAutomaticData } from "./use-automatic-data.ts";
|
|
|
+
|
|
|
+export type TransformerExtends = Transformer & { queueShapes: Ref<EntityShape[]> }
|
|
|
+export const transformRectPadding = 10;
|
|
|
+export const useTransformer = installGlobalVar(() => {
|
|
|
+ const anchorCornerRadius = 5
|
|
|
+ const transformer = new Transformer({
|
|
|
+ borderStrokeWidth: 2,
|
|
|
+ borderStroke: themeMouseColors.pub,
|
|
|
+ anchorCornerRadius,
|
|
|
+ anchorSize: anchorCornerRadius * 2,
|
|
|
+ anchorStrokeWidth: 2,
|
|
|
+ anchorStroke: themeMouseColors.pub,
|
|
|
+ anchorFill: themeMouseColors.press,
|
|
|
+ padding: 10,
|
|
|
+ useSingleNodeRotation: true
|
|
|
+ }) as TransformerExtends;
|
|
|
+ transformer.queueShapes = ref([])
|
|
|
+ return transformer;
|
|
|
+}, Symbol("transformer"));
|
|
|
+
|
|
|
+export const usePointerIsTransformerInner = () => {
|
|
|
+ const transformer = useTransformer()
|
|
|
+ const stage = useStage()
|
|
|
+ return () => {
|
|
|
+ const $stage = stage.value!.getStage()
|
|
|
+ const tfRect = transformer.getClientRect()
|
|
|
+ tfRect.x -= transformRectPadding
|
|
|
+ tfRect.y -= transformRectPadding
|
|
|
+ tfRect.width += transformRectPadding
|
|
|
+ tfRect.height += transformRectPadding
|
|
|
+ const pointRect = { ...$stage.pointerPos!, width: 1, height: 1 }
|
|
|
+
|
|
|
+ return Util.haveIntersection(tfRect, pointRect);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+export const useShapeDrag = (
|
|
|
+ shape: Ref<DC<EntityShape> | undefined>) => {
|
|
|
+ const offset = ref<Pos>()
|
|
|
+ const mode = useMode();
|
|
|
+ const conversion = useConversionPosition(true);
|
|
|
+ const transformIngShapes = useTransformIngShapes()
|
|
|
+
|
|
|
+ const init = (shape: EntityShape) => {
|
|
|
+ const dom = shape.getStage()!.container();
|
|
|
+
|
|
|
+ let start: Pos | undefined;
|
|
|
+ const enter = (position: Pos) => {
|
|
|
+ if (!start) {
|
|
|
+ start = position
|
|
|
+ mode.push(Mode.update);
|
|
|
+ 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 {
|
|
|
+ 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-status");
|
|
|
+ start && leave()
|
|
|
+ },
|
|
|
+ listener(document.documentElement, "pointerup", () => {
|
|
|
+ start && leave()
|
|
|
+ }),
|
|
|
+ ]);
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => !!shape.value?.getStage(),
|
|
|
+ (loaded, _, onCleanup) => {
|
|
|
+ loaded && onCleanup(init(shape.value!.getNode()));
|
|
|
+ }
|
|
|
+ );
|
|
|
+ return offset;
|
|
|
+};
|
|
|
+
|
|
|
+type Rep<T> = { tempShape: T, update: () => void, destory: () => void }
|
|
|
+const emptyFn = () => {}
|
|
|
+export const useShapeTransformer = <T extends EntityShape>(
|
|
|
+ shape: Ref<DC<T> | undefined>,
|
|
|
+ transformerConfig: TransformerConfig = {},
|
|
|
+ replaceShape?: (transformer: TransformerExtends, shape: T) => Rep<T>
|
|
|
+) => {
|
|
|
+ const offset = useShapeDrag(shape)
|
|
|
+ const transform = ref<Transform>()
|
|
|
+ const status = useMouseShapeStatus(shape);
|
|
|
+ const mode = useMode();
|
|
|
+ const transformer = useTransformer();
|
|
|
+ const transformIngShapes = useTransformIngShapes()
|
|
|
+
|
|
|
+ const init = ($shape: T) => mergeFuns(
|
|
|
+ watch(
|
|
|
+ () => status.value.hover,
|
|
|
+ (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]);
|
|
|
+ }
|
|
|
+
|
|
|
+ let rep: Rep<T>
|
|
|
+ if (replaceShape) {
|
|
|
+ rep = replaceShape(transformer, $shape)
|
|
|
+ } else {
|
|
|
+ rep = {
|
|
|
+ tempShape: $shape,
|
|
|
+ destory: emptyFn,
|
|
|
+ update: emptyFn
|
|
|
+ }
|
|
|
+ transformer.nodes([$shape])
|
|
|
+ transformer.queueShapes.value = [$shape]
|
|
|
+ }
|
|
|
+ parent.add(transformer);
|
|
|
+
|
|
|
+ const updateTransform = () => {
|
|
|
+ transform.value = rep.tempShape.getTransform().copy()
|
|
|
+ }
|
|
|
+
|
|
|
+ const downHandler = () => {
|
|
|
+ isEnter && mode.pop()
|
|
|
+ isEnter = true
|
|
|
+ rep.update()
|
|
|
+ mode.push(Mode.update);
|
|
|
+ transformIngShapes.value.push($shape)
|
|
|
+ }
|
|
|
+
|
|
|
+ let isEnter = false
|
|
|
+ transformer.on("pointerdown.shapemer", downHandler);
|
|
|
+ transformer.on("transform.shapemer", updateTransform);
|
|
|
+ const stop = 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)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 拖拽时要更新矩阵
|
|
|
+ let prevMoveTf: Transform | null = null
|
|
|
+ const stopDragWatch = watch(offset, (translate, oldTranslate) => {
|
|
|
+ if (translate) {
|
|
|
+ if (!oldTranslate) {
|
|
|
+ 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);
|
|
|
+ updateTransform()
|
|
|
+ } else {
|
|
|
+ prevMoveTf = null
|
|
|
+ transform.value = void 0
|
|
|
+ }
|
|
|
+ }, { immediate: true })
|
|
|
+
|
|
|
+ onCleanup(() => {
|
|
|
+ for (const key in oldConfig) {
|
|
|
+ ;(transformer as any)[key](oldConfig[key]);
|
|
|
+ }
|
|
|
+ stop()
|
|
|
+ stopDragWatch()
|
|
|
+ parent.add($shape);
|
|
|
+ // TODO: 有可能transformer已经转移
|
|
|
+ if (transformer.queueShapes.value.includes($shape)) {
|
|
|
+ transformer.nodes([]);
|
|
|
+ transformer.queueShapes.value = []
|
|
|
+ }
|
|
|
+ transform.value = void 0;
|
|
|
+ rep.destory()
|
|
|
+ if (isEnter) {
|
|
|
+ mode.pop()
|
|
|
+ const ndx = transformIngShapes.value.indexOf($shape)
|
|
|
+ ~ndx && transformIngShapes.value.splice(ndx, 1)
|
|
|
+ }
|
|
|
+ transformer.off("pointerdown.shapemer", downHandler);
|
|
|
+ transformer.off("transform.shapemer", updateTransform);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ watch(shape, (shape, _, onCleanup) => {
|
|
|
+ if (shape) {
|
|
|
+ onCleanup(init(shape.getStage()));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return transform;
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+export const genTransformerRepShape = <T extends Shape>(
|
|
|
+ shape: T,
|
|
|
+ transformer: TransformerExtends,
|
|
|
+ getAttitudeMat: () => number[],
|
|
|
+ resumeData: (repShape: T, inverAttitude: Transform) => void
|
|
|
+) => {
|
|
|
+ const repShape = shape.clone({
|
|
|
+ fill: 'rgb(0, 255, 0)',
|
|
|
+ visible: false,
|
|
|
+ strokeWidth: 0
|
|
|
+ })
|
|
|
+
|
|
|
+ const update = () => {
|
|
|
+ const attitude = new Transform(getAttitudeMat());
|
|
|
+ const inverAttitude = attitude.copy().invert();
|
|
|
+ setShapeTransform(repShape, attitude)
|
|
|
+ resumeData(repShape, inverAttitude)
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (import.meta.env.DEV) {
|
|
|
+ repShape.visible(true)
|
|
|
+ shape.opacity(0.9)
|
|
|
+ repShape.opacity(0.1)
|
|
|
+ }
|
|
|
+
|
|
|
+ update()
|
|
|
+ shape.parent!.add(repShape)
|
|
|
+ repShape.zIndex(shape.getZIndex() - 1)
|
|
|
+ transformer.nodes([repShape]);
|
|
|
+ transformer.queueShapes.value = [shape]
|
|
|
+
|
|
|
+ return {
|
|
|
+ tempShape: repShape,
|
|
|
+ update,
|
|
|
+ destory: () => {
|
|
|
+ repShape.remove()
|
|
|
+ shape.opacity(1)
|
|
|
+ }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+export type LineTransformerData = {
|
|
|
+ points: Pos[],
|
|
|
+ attitude: number[]
|
|
|
+}
|
|
|
+export const useLineTransformer = (
|
|
|
+ shape: Ref<DC<Line> | undefined>,
|
|
|
+ init: () => LineTransformerData,
|
|
|
+ callback: (data: LineTransformerData) => void,
|
|
|
+) => {
|
|
|
+ const data = useAutomaticData(() => init())
|
|
|
+ let tempShape: Line
|
|
|
+ let inverAttitude: Transform
|
|
|
+ let stableVs = data.value.points
|
|
|
+ let tempVs = data.value.points
|
|
|
+
|
|
|
+ const transform = useShapeTransformer(shape, undefined, (transformer, $shape) => {
|
|
|
+ const result = genTransformerRepShape(
|
|
|
+ $shape,
|
|
|
+ transformer,
|
|
|
+ () => data.value.attitude, (repShape, inverMat) => {
|
|
|
+ const initVs = stableVs.map(v => inverMat.point(v))
|
|
|
+ repShape.points(flatPositions(initVs) )
|
|
|
+ repShape.closed(true)
|
|
|
+ inverAttitude = inverMat
|
|
|
+ }
|
|
|
+ )
|
|
|
+ tempShape = result.tempShape
|
|
|
+ return result
|
|
|
+ })
|
|
|
+
|
|
|
+ watch(() => shape.value?.getNode(), $shape => {
|
|
|
+ if ($shape) {
|
|
|
+ $shape.points(flatPositions(tempVs))
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ watch(transform, (current, prev) => {
|
|
|
+ if (current) {
|
|
|
+ // 顶点更新
|
|
|
+ const $shape = shape.value!.getNode()
|
|
|
+ const transfrom = current.copy().multiply(inverAttitude)
|
|
|
+ tempVs = stableVs.map(v => transfrom.point(v))
|
|
|
+ $shape.points(flatPositions(tempVs))
|
|
|
+ } else if (prev && tempShape) {
|
|
|
+ data.value.attitude = tempShape.getTransform().m;
|
|
|
+ data.value.points = stableVs = tempVs
|
|
|
+ callback(data.value)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return [transform, data] as const
|
|
|
+}
|