coze-studio/frontend/packages/workflow/fabric-canvas/src/utils/controls.tsx

697 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable max-lines */
/* eslint-disable max-params */
import {
Control,
controlsUtils,
type FabricObject,
type Transform,
type TPointerEvent,
type ControlCursorCallback,
} from 'fabric';
import { Snap } from '../typings';
import { snap } from './snap/snap';
const getAngle = (a: number) => {
if (a >= 360) {
return getAngle(a - 360);
}
if (a < 0) {
return getAngle(a + 360);
}
return a;
};
const svg0 =
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><g filter="url(#a)"><mask id="b" width="16" height="16" x="8" y="8" fill="#000" maskUnits="userSpaceOnUse"><path fill="#fff" d="M8 8h16v16H8z"/><path fill-rule="evenodd" d="M12.87 22 10 19.13h1.99v-.659a6.483 6.483 0 0 1 6.482-6.482h.723V10l2.869 2.87-2.87 2.869v-1.99h-.722a4.722 4.722 0 0 0-4.722 4.722v.66h1.989L12.869 22" clip-rule="evenodd"/></mask><path fill="#000" fill-rule="evenodd" d="M12.87 22 10 19.13h1.99v-.659a6.483 6.483 0 0 1 6.482-6.482h.723V10l2.869 2.87-2.87 2.869v-1.99h-.722a4.722 4.722 0 0 0-4.722 4.722v.66h1.989L12.869 22" clip-rule="evenodd"/><path fill="#fff" d="M10 19.13v-.8H8.068l1.366 1.367.566-.566M12.87 22l-.567.566.566.566.566-.566L12.87 22m-.88-2.87v.801h.8v-.8h-.8m6.482-7.141v-.8.8m.723 0v.8h.8v-.8h-.8m0-1.989.565-.566-1.366-1.366V10h.8m2.869 2.87.566.565.566-.566-.566-.566-.566.566m-2.87 2.869h-.8v1.932l1.366-1.366-.566-.566m0-1.99h.8v-.8h-.8v.8m-.722 0v-.8.8m-4.722 5.382h-.8v.8h.8v-.8m1.989 0 .566.566 1.366-1.367h-1.932v.8m-6.305.566 2.87 2.869 1.131-1.132-2.87-2.87-1.13 1.133m2.555-1.367H10v1.601h1.99v-1.6m-.8.142v.659h1.6v-.66h-1.6m7.283-7.284a7.283 7.283 0 0 0-7.283 7.283h1.6a5.682 5.682 0 0 1 5.683-5.682v-1.6m.723 0h-.723v1.601h.723v-1.6m.8.8V10h-1.6v1.989h1.6m-1.366-1.422 2.869 2.87 1.132-1.133-2.87-2.869-1.131 1.132m2.869 1.737-2.87 2.87 1.132 1.132 2.87-2.87-1.132-1.132m-1.503 3.436v-1.99h-1.6v1.99h1.6m-1.523-1.19h.723v-1.6h-.723v1.6m-3.922 3.922a3.922 3.922 0 0 1 3.922-3.921v-1.601a5.522 5.522 0 0 0-5.523 5.522h1.601m0 .66v-.66h-1.6v.66h1.6m1.189-.8h-1.99v1.6h1.99v-1.6m-2.304 4.235 2.87-2.87-1.132-1.131-2.87 2.87 1.132 1.13" mask="url(#b)"/></g><defs><filter id="a" width="18.728" height="18.665" x="6.268" y="7.268" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".9"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_184_36430"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_184_36430" result="shape"/></filter></defs></svg>';
const svg90 =
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><g filter="url(#a)"><mask id="b" width="16" height="16" x="8" y="8" fill="#000" maskUnits="userSpaceOnUse"><path fill="#fff" d="M8 8h16v16H8z"/><path fill-rule="evenodd" d="M10 12.87 12.87 10v1.99h.658a6.482 6.482 0 0 1 6.482 6.482v.722H22l-2.87 2.87-2.868-2.87h1.989v-.722a4.722 4.722 0 0 0-4.722-4.722h-.659v1.988L10 12.87" clip-rule="evenodd"/></mask><path fill="#000" fill-rule="evenodd" d="M10 12.87 12.87 10v1.99h.658a6.482 6.482 0 0 1 6.482 6.482v.722H22l-2.87 2.87-2.868-2.87h1.989v-.722a4.722 4.722 0 0 0-4.722-4.722h-.659v1.988L10 12.87" clip-rule="evenodd"/><path fill="#fff" d="M12.87 10h.8V8.068l-1.367 1.366.566.566M10 12.87l-.566-.567-.566.566.566.566.566-.566m2.87-.88h-.801v.8h.8v-.8m7.14 7.204h-.8v.8h.8v-.8m1.989 0 .566.566 1.366-1.366H22v.8m-2.87 2.87-.565.565.566.566.566-.566-.566-.566m-2.868-2.87v-.8h-1.932l1.366 1.366.566-.566m1.989 0v.8h.8v-.8h-.8m-5.38-5.443v-.8h-.801v.8h.8m0 1.987-.567.566 1.366 1.366v-1.932h-.8m-.567-6.304-2.869 2.87 1.132 1.131 2.869-2.87-1.132-1.13m1.366 2.556V10h-1.6v1.99h1.6m-.141-.8h-.659v1.6h.659v-1.6m7.283 7.282a7.283 7.283 0 0 0-7.283-7.282v1.6a5.682 5.682 0 0 1 5.682 5.682h1.6m0 .722v-.722h-1.6v.722h1.6m-.8.8h1.988v-1.6H20.01v1.6m1.422-1.366-2.869 2.87 1.132 1.131 2.869-2.869-1.132-1.132m-1.737 2.87-2.87-2.87-1.131 1.132 2.869 2.87 1.132-1.132m-3.435-1.503h1.989v-1.6h-1.99v1.6m1.188-1.523v.722h1.601v-.722h-1.6m-3.921-3.921a3.921 3.921 0 0 1 3.921 3.921h1.601a5.522 5.522 0 0 0-5.522-5.522v1.6m-.659 0h.659v-1.6h-.659v1.6m.8 1.187v-1.987h-1.6v1.987h1.6m-4.235-2.303 2.87 2.87 1.131-1.133-2.87-2.869-1.13 1.132" mask="url(#b)"/></g><defs><filter id="a" width="18.663" height="18.727" x="7.068" y="7.268" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".9"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_184_36425"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_184_36425" result="shape"/></filter></defs></svg>';
const svg180 =
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><g filter="url(#a)"><mask id="b" width="16" height="16" x="8" y="8" fill="#000" maskUnits="userSpaceOnUse"><path fill="#fff" d="M8 8h16v16H8z"/><path fill-rule="evenodd" d="M19.13 10 22 12.87h-1.99v.659a6.483 6.483 0 0 1-6.482 6.482h-.723V22l-2.869-2.87 2.87-2.869v1.99h.722a4.722 4.722 0 0 0 4.722-4.723v-.659h-1.989L19.131 10" clip-rule="evenodd"/></mask><path fill="#000" fill-rule="evenodd" d="M19.13 10 22 12.87h-1.99v.659a6.483 6.483 0 0 1-6.482 6.482h-.723V22l-2.869-2.87 2.87-2.869v1.99h.722a4.722 4.722 0 0 0 4.722-4.723v-.659h-1.989L19.131 10" clip-rule="evenodd"/><path fill="#fff" d="M22 12.87v.8h1.932l-1.366-1.367-.566.566M19.13 10l.567-.566-.566-.566-.566.566.566.566m.88 2.87v-.801h-.8v.8h.8m0 .659h-.8.8m-7.205 6.482v-.8h-.8v.8h.8m0 1.989-.565.566 1.366 1.366V22h-.8m-2.869-2.87-.566-.565-.566.566.566.566.566-.566m2.87-2.869h.8V14.33l-1.366 1.366.566.566m0 1.99h-.8v.8h.8v-.8m5.444-4.723h-.8.8m0-.659h.8v-.8h-.8v.8m-1.989 0-.566-.566-1.366 1.367h1.932v-.8m6.305-.566-2.87-2.869-1.131 1.132 2.87 2.87 1.13-1.133M20.01 13.67H22v-1.601h-1.99v1.6m.8-.142v-.659h-1.6v.66h1.6m-7.283 7.284a7.283 7.283 0 0 0 7.283-7.284h-1.6a5.682 5.682 0 0 1-5.683 5.683v1.6m-.723 0h.723V19.21h-.723v1.6m-.8-.8V22h1.6V20.01h-1.6m1.366 1.422-2.869-2.87-1.132 1.133 2.87 2.869 1.131-1.132m-2.869-1.737 2.87-2.87-1.132-1.132-2.87 2.87 1.132 1.132m1.503-3.436v1.99h1.6v-1.99h-1.6m1.523 1.19h-.723v1.6h.723v-1.6m3.922-3.923a3.922 3.922 0 0 1-3.922 3.922v1.601a5.522 5.522 0 0 0 5.522-5.522h-1.6m0-.659v.66h1.6v-.66h-1.6m-1.189.8h1.99v-1.6h-1.99v1.6m2.304-4.235-2.87 2.87 1.132 1.131 2.87-2.87-1.132-1.13" mask="url(#b)"/></g><defs><filter id="a" width="18.728" height="18.665" x="7.004" y="8.067" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".9"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_184_36440"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_184_36440" result="shape"/></filter></defs></svg>';
const svg270 =
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><g filter="url(#a)"><mask id="b" width="16" height="16" x="8" y="8" fill="#000" maskUnits="userSpaceOnUse"><path fill="#fff" d="M8 8h16v16H8z"/><path fill-rule="evenodd" d="M22 19.13 19.13 22v-1.99h-.658a6.483 6.483 0 0 1-6.483-6.483v-.722H10l2.87-2.87 2.869 2.87h-1.99v.722a4.722 4.722 0 0 0 4.723 4.722h.659v-1.988L22 19.131" clip-rule="evenodd"/></mask><path fill="#000" fill-rule="evenodd" d="M22 19.13 19.13 22v-1.99h-.658a6.483 6.483 0 0 1-6.483-6.483v-.722H10l2.87-2.87 2.869 2.87h-1.99v.722a4.722 4.722 0 0 0 4.723 4.722h.659v-1.988L22 19.131" clip-rule="evenodd"/><path fill="#fff" d="M19.13 22h-.8v1.932l1.367-1.366L19.13 22M22 19.13l.566.567.566-.566-.566-.566-.566.566m-2.87.88h.801v-.8h-.8v.8m-7.141-6.483h.8-.8m0-.722h.8v-.8h-.8v.8m-1.989 0-.566-.566-1.366 1.366H10v-.8m2.87-2.87.565-.565-.566-.566-.566.566.566.566m2.869 2.87v.8h1.932l-1.366-1.366-.566.566m-1.99 0v-.8h-.8v.8h.8m0 .722h.801-.8m5.382 4.722v.8h.8v-.8h-.8m0-1.988.566-.566-1.367-1.366v1.932h.8m.566 6.305 2.869-2.87-1.132-1.131-2.87 2.87 1.133 1.13M18.33 20.01V22h1.601v-1.99h-1.6m.142.8h.659v-1.6h-.66v1.6m-7.284-7.283a7.283 7.283 0 0 0 7.284 7.283v-1.6a5.682 5.682 0 0 1-5.683-5.683h-1.6m0-.722v.722h1.601v-.722h-1.6m.8-.8H10v1.6h1.989v-1.6m-1.422 1.366 2.87-2.87-1.133-1.131-2.869 2.869 1.132 1.132m1.737-2.87 2.87 2.87 1.132-1.132-2.87-2.87-1.132 1.133m3.436 1.504h-1.99v1.6h1.99v-1.6m-1.189 1.522v-.722h-1.6v.722h1.6m3.922 3.922a3.922 3.922 0 0 1-3.922-3.922h-1.6a5.522 5.522 0 0 0 5.522 5.523v-1.601m.659 0h-.66v1.6h.66v-1.6m-.8-1.188v1.988h1.6v-1.988h-1.6m4.235 2.304-2.87-2.87-1.131 1.132 2.87 2.87 1.13-1.132" mask="url(#b)"/></g><defs><filter id="a" width="18.664" height="18.727" x="6.268" y="8.005" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".9"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_184_36435"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_184_36435" result="shape"/></filter></defs></svg>';
const svg2Base64 = (svg: string) => `data:image/svg+xml;base64,${btoa(svg)}`;
const cursorRotate0 = svg2Base64(svg0);
const cursorRotate90 = svg2Base64(svg90);
const cursorRotate180 = svg2Base64(svg180);
const cursorRotate270 = svg2Base64(svg270);
const getCursor = (angle: number) => {
const a = getAngle(angle);
if (a >= 225 && a < 315) {
return cursorRotate270;
} else if (a >= 135 && a < 225) {
return cursorRotate180;
} else if (a >= 45 && a < 135) {
return cursorRotate90;
} else {
return cursorRotate0;
}
};
const {
scalingEqually,
scaleCursorStyleHandler,
rotationWithSnapping,
scalingX,
scalingY,
} = controlsUtils;
type GetControls = (props?: {
x?: number;
y?: number;
callback?: (data: { element: FabricObject }) => void;
needResetScaleAndSnap?: boolean;
}) => Control;
/**
* 直线起点控制点
*/
export const getLineStartControl: GetControls = (props = {}) => {
const { x, y, callback } = props;
return new Control({
x,
y,
actionHandler: (e, transformData, _x, _y) => {
transformData.target.set({
x1: _x,
y1: _y,
x2:
transformData.lastX +
transformData.width * (transformData.corner === 'tl' ? 1 : -1),
y2: transformData.lastY + transformData.height,
});
callback?.({ element: transformData.target });
return true;
},
actionName: 'startControl', // 控制点的名称
});
};
/**
* 直线终点控制点
*/
export const getLineEndControl: GetControls = (props = {}) => {
const { x, y, callback } = props;
return new Control({
x,
y,
actionHandler: (e, transformData, _x, _y) => {
transformData.target.set({
x1:
transformData.lastX -
transformData.width * (transformData.corner === 'br' ? 1 : -1),
y1: transformData.lastY - transformData.height,
x2: _x,
y2: _y,
});
callback?.({ element: transformData.target });
return true;
},
actionName: 'endControl', // 控制点的名称
});
};
const originData = {
width: 0,
height: 0,
top: 0,
left: 0,
};
type LeftTopCalcFn = (originData: {
angle: number;
originTop: number;
originLeft: number;
originWidth: number;
originHeight: number;
newWidth: number;
newHeight: number;
}) => {
left: number;
top: number;
};
const scaleToSize = (
transformData: Transform,
options?: {
scaleEqual?: boolean;
leftTopCalcFn?: LeftTopCalcFn;
},
) => {
const { width, height, scaleX, scaleY, strokeWidth, angle } =
transformData.target;
let targetWidth = Math.max((width + strokeWidth) * scaleX - strokeWidth, 1);
let targetHeight = Math.max((height + strokeWidth) * scaleY - strokeWidth, 1);
if (options?.scaleEqual) {
if (targetWidth - originData.width > targetHeight - originData.height) {
targetHeight = (originData.height / originData.width) * targetWidth;
} else {
targetWidth = (originData.width / originData.height) * targetHeight;
}
}
let targetLeft = originData.left;
let targetTop = originData.top;
if (options?.leftTopCalcFn) {
const rs = options.leftTopCalcFn({
angle,
originTop: originData.top,
originLeft: originData.left,
originWidth: originData.width,
originHeight: originData.height,
newWidth: targetWidth,
newHeight: targetHeight,
});
targetLeft = rs.left;
targetTop = rs.top;
}
transformData.target.set({
width: targetWidth,
height: targetHeight,
// textBox 特化属性
customFixedHeight: targetHeight,
scaleX: 1,
scaleY: 1,
top: targetTop,
left: targetLeft,
});
};
/**
* 直接问 GPT
一个矩形,宽度 w ,高度 h
将矩形顺时针旋转角度 a左上角坐标为 x1 y1
拉伸矩形左上角,使矩形右下角保持不变,宽度增加到 w1高度增加到 h1
求左上角坐标
*/
const calcLeftTopByTopLeft: LeftTopCalcFn = ({
angle,
originTop,
originLeft,
originWidth,
originHeight,
newWidth,
newHeight,
}) => {
const anglePI = angle * (Math.PI / 180);
return {
left:
originLeft +
originWidth * Math.cos(anglePI) -
originHeight * Math.sin(anglePI) -
newWidth * Math.cos(anglePI) +
newHeight * Math.sin(anglePI),
top:
originTop +
originWidth * Math.sin(anglePI) +
originData.height * Math.cos(anglePI) -
newWidth * Math.sin(anglePI) -
newHeight * Math.cos(anglePI),
};
};
/**
* 直接问 GPT
一个矩形,宽度 w ,高度 h
将矩形顺时针旋转角度 a , 左上角坐标为 x1 y1
拉伸矩形右上角,使矩形左下角保持不变,宽度增加到 w1高度增加到 h1
求左上角坐标
GPT 给的答案不准确,需要稍微理解下,修改加减)
*/
const calcLeftTopByTopRight: LeftTopCalcFn = ({
angle,
originTop,
originLeft,
originHeight,
newHeight,
}) => {
const anglePI = angle * (Math.PI / 180);
return {
left: originLeft - (originHeight - newHeight) * Math.sin(anglePI),
top: originTop + (originHeight - newHeight) * Math.cos(anglePI),
};
};
/**
* 直接问 GPT
一个矩形,宽度 w ,高度 h
将矩形顺时针旋转角度 a , 左上角坐标为 x1 y1
拉伸矩形左下角,使矩形右上角保持不变,宽度增加到 w1高度增加到 h1
求左上角坐标
GPT 给的答案不准确,这个比较麻烦,所以写出了每一步的推导过程
*/
const calcLeftTopByBottomLeft: LeftTopCalcFn = ({
angle,
originTop,
originLeft,
originWidth,
newWidth,
newHeight,
}) => {
// 将角度转换为弧度
const aRad = (angle * Math.PI) / 180;
// 计算旋转后的右上角坐标
const x2 = originLeft + originWidth * Math.cos(aRad);
const y2 = originTop + originWidth * Math.sin(aRad);
// 计算拉伸后的左下角坐标
const x3 = x2 - newHeight * Math.sin(aRad) - newWidth * Math.cos(aRad);
const y3 = y2 + newHeight * Math.cos(aRad) - newWidth * Math.sin(aRad);
// 计算拉伸后的左上角坐标
const x1New = x3 + newHeight * Math.sin(aRad);
const y1New = y3 - newHeight * Math.cos(aRad);
return {
left: x1New,
top: y1New,
};
};
const _mouseDownHandler = (
e: TPointerEvent,
transformData: Transform,
): boolean => {
originData.width = transformData.target.width;
originData.height = transformData.target.height;
originData.top = transformData.target.top;
originData.left = transformData.target.left;
return false;
};
const cursorMap: Record<string, string> = {
'e-resize': 'ew-resize',
'w-resize': 'ew-resize',
'n-resize': 'ns-resize',
's-resize': 'ns-resize',
'nw-resize': 'nwse-resize',
'ne-resize': 'nesw-resize',
'sw-resize': 'nesw-resize',
'se-resize': 'nwse-resize',
};
const customCursorStyleHandler: ControlCursorCallback = (a, b, c) => {
const cursor = scaleCursorStyleHandler(a, b, c);
return cursorMap[cursor] ?? cursor;
};
const _actionHandler = ({
e,
transformData,
x,
y,
needResetScaleAndSnap,
fn,
callback,
snapPosition,
leftTopCalcFn,
}: {
e: TPointerEvent;
transformData: Transform;
x: number;
y: number;
needResetScaleAndSnap?: boolean;
callback: ((data: { element: FabricObject }) => void) | undefined;
fn: (
eventData: TPointerEvent,
transform: Transform,
x: number,
y: number,
) => boolean;
snapPosition: Snap.ControlType;
leftTopCalcFn?: LeftTopCalcFn;
}) => {
const rs = fn(
// 如果使用吸附则禁用默认缩放;否则取反
{ ...e, shiftKey: needResetScaleAndSnap ? true : !e.shiftKey },
transformData,
x,
y,
);
if (needResetScaleAndSnap) {
scaleToSize(transformData, {
scaleEqual: e.shiftKey,
leftTopCalcFn,
});
snap.resize(transformData.target, snapPosition);
}
callback?.({ element: transformData.target });
return rs;
};
/**
* 上左
*/
export const getResizeTLControl: GetControls = (props = {}) => {
const { callback, needResetScaleAndSnap = true } = props;
return new Control({
x: -0.5,
y: -0.5,
cursorStyleHandler: customCursorStyleHandler,
actionHandler: (e, transformData, _x, _y) => {
const rs = _actionHandler({
e,
transformData,
x: _x,
y: _y,
needResetScaleAndSnap,
callback,
fn: scalingEqually,
leftTopCalcFn: calcLeftTopByTopLeft,
snapPosition: Snap.ControlType.TopLeft,
});
return rs;
},
mouseDownHandler: _mouseDownHandler,
actionName: 'resizeTLControl', // 控制点的名称
});
};
/**
* 上中
*/
export const getResizeMTControl: GetControls = (props = {}) => {
const { callback, needResetScaleAndSnap = true } = props;
return new Control({
x: 0,
y: -0.5,
cursorStyleHandler: customCursorStyleHandler,
actionHandler: (e, transformData, _x, _y) => {
const rs = _actionHandler({
e,
transformData,
x: _x,
y: _y,
needResetScaleAndSnap,
callback,
fn: scalingY,
leftTopCalcFn: calcLeftTopByTopLeft,
snapPosition: Snap.ControlType.Top,
});
return rs;
},
mouseDownHandler: _mouseDownHandler,
actionName: 'resizeMTControl', // 控制点的名称
});
};
/**
* 上右
*/
export const getResizeTRControl: GetControls = (props = {}) => {
const { callback, needResetScaleAndSnap } = props;
return new Control({
x: 0.5,
y: -0.5,
cursorStyleHandler: customCursorStyleHandler,
actionHandler: (e, transformData, _x, _y) => {
const rs = _actionHandler({
e,
transformData,
x: _x,
y: _y,
needResetScaleAndSnap,
callback,
fn: scalingEqually,
leftTopCalcFn: calcLeftTopByTopRight,
snapPosition: Snap.ControlType.TopRight,
});
return rs;
},
mouseDownHandler: _mouseDownHandler,
actionName: 'resizeTRControl', // 控制点的名称
});
};
/**
* 中左
*/
export const getResizeMLControl: GetControls = (props = {}) => {
const { callback, needResetScaleAndSnap } = props;
return new Control({
x: -0.5,
y: 0,
cursorStyleHandler: customCursorStyleHandler,
actionHandler: (e, transformData, _x, _y) => {
const rs = _actionHandler({
e,
transformData,
x: _x,
y: _y,
needResetScaleAndSnap,
callback,
fn: scalingX,
leftTopCalcFn: calcLeftTopByBottomLeft,
snapPosition: Snap.ControlType.Left,
});
return rs;
},
mouseDownHandler: _mouseDownHandler,
actionName: 'resizeMLControl', // 控制点的名称
});
};
/**
* 中右
*/
export const getResizeMRControl: GetControls = (props = {}) => {
const { callback, needResetScaleAndSnap } = props;
return new Control({
x: 0.5,
y: 0,
cursorStyleHandler: customCursorStyleHandler,
actionHandler: (e, transformData, _x, _y) => {
const rs = _actionHandler({
e,
transformData,
x: _x,
y: _y,
needResetScaleAndSnap,
callback,
fn: scalingX,
snapPosition: Snap.ControlType.Right,
});
return rs;
},
mouseDownHandler: _mouseDownHandler,
actionName: 'resizeMRControl', // 控制点的名称
});
};
/**
* 下左
*/
export const getResizeBLControl: GetControls = (props = {}) => {
const { callback, needResetScaleAndSnap } = props;
return new Control({
x: -0.5,
y: 0.5,
cursorStyleHandler: customCursorStyleHandler,
actionHandler: (e, transformData, _x, _y) => {
const rs = _actionHandler({
e,
transformData,
x: _x,
y: _y,
needResetScaleAndSnap,
callback,
fn: scalingEqually,
leftTopCalcFn: calcLeftTopByBottomLeft,
snapPosition: Snap.ControlType.BottomLeft,
});
return rs;
},
mouseDownHandler: _mouseDownHandler,
actionName: 'resizeBLControl', // 控制点的名称
});
};
/**
* 下中
*/
export const getResizeMBControl: GetControls = (props = {}) => {
const { callback, needResetScaleAndSnap } = props;
return new Control({
x: 0,
y: 0.5,
cursorStyleHandler: customCursorStyleHandler,
actionHandler: (e, transformData, _x, _y) => {
const rs = _actionHandler({
e,
transformData,
x: _x,
y: _y,
needResetScaleAndSnap,
callback,
fn: scalingY,
snapPosition: Snap.ControlType.Bottom,
});
return rs;
},
mouseDownHandler: _mouseDownHandler,
actionName: 'resizeMBControl', // 控制点的名称
});
};
/**
* 下右
*/
export const getResizeBRControl: GetControls = (props = {}) => {
const { callback, needResetScaleAndSnap } = props;
return new Control({
x: 0.5,
y: 0.5,
cursorStyleHandler: customCursorStyleHandler,
actionHandler: (e, transformData, _x, _y) => {
const rs = _actionHandler({
e,
transformData,
x: _x,
y: _y,
needResetScaleAndSnap,
callback,
fn: scalingEqually,
snapPosition: Snap.ControlType.BottomRight,
});
return rs;
},
mouseDownHandler: _mouseDownHandler,
actionName: 'resizeBRControl', // 控制点的名称
});
};
const _getRotateControl =
({
x,
y,
offsetY,
offsetX,
actionName,
rotateStaff,
}: {
x: number;
y: number;
offsetY: -1 | 1;
offsetX: -1 | 1;
actionName: string;
rotateStaff: number;
}): GetControls =>
(props = {}) => {
const { callback } = props;
// 这个大小,取决于 resize 控制点的大小
const offset = 12;
return new Control({
x,
y,
sizeX: 20,
sizeY: 20,
offsetY: offsetY * offset,
offsetX: offsetX * offset,
// 覆盖旋转控制点渲染,预期不显示,所以啥都没写
// eslint-disable-next-line @typescript-eslint/no-empty-function
render: () => {},
// 只能做到 hover 上时的 cursor旋转过程中 cursor 无法修改
cursorStyleHandler: (eventData, control, object) =>
`url(${getCursor(object.angle + rotateStaff)}) 16 16, crosshair`,
actionHandler: (e, transformData, _x, _y) => {
// 旋转吸附,单位:角度 一圈 = 360度
if (e.shiftKey) {
transformData.target.set({
snapAngle: 15,
});
} else {
transformData.target.set({
snapAngle: undefined,
});
}
const rs = rotationWithSnapping(
e,
{ ...transformData, originX: 'center', originY: 'center' },
_x,
_y,
);
// scaleToSize(transformData);
// transformData.target.canvas?.requestRenderAll();
callback?.({ element: transformData.target });
return rs;
},
actionName, // 控制点的名称
});
};
// 上左旋转点
export const getRotateTLControl: GetControls = (props = {}) =>
_getRotateControl({
x: -0.5,
y: -0.5,
offsetY: -1,
offsetX: -1,
rotateStaff: 0,
actionName: 'rotateTLControl',
})(props);
// 上右旋转点
export const getRotateTRControl: GetControls = (props = {}) =>
_getRotateControl({
x: 0.5,
y: -0.5,
offsetY: -1,
offsetX: 1,
rotateStaff: 90,
actionName: 'rotateTRControl',
})(props);
// 下右旋转点
export const getRotateBRControl: GetControls = (props = {}) =>
_getRotateControl({
x: 0.5,
y: 0.5,
offsetY: 1,
offsetX: 1,
rotateStaff: 180,
actionName: 'rotateBRControl',
})(props);
// 下左旋转点
export const getRotateBLControl: GetControls = (props = {}) =>
_getRotateControl({
x: -0.5,
y: 0.5,
offsetY: 1,
offsetX: -1,
rotateStaff: 270,
actionName: 'rotateBLControl',
})(props);