/* * 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 = ''; const svg90 = ''; const svg180 = ''; const svg270 = ''; 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 = { '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);