coze-studio/frontend/packages/workflow/fabric-canvas/src/hooks/use-drag-add.tsx

266 lines
6.6 KiB
TypeScript

/*
* 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.
*/
import { useRef } from 'react';
import {
type ObjectEvents,
type Canvas,
type FabricObject,
type Line,
} from 'fabric';
import { snap } from '../utils/snap/snap';
import { resetElementClip } from '../utils/fabric-utils';
import { createElement, defaultProps } from '../utils';
import { type FabricObjectWithCustomProps, Mode, Snap } from '../typings';
const modeElementMap: Partial<
Record<
Mode,
{
down: (props: {
left: number;
top: number;
canvas?: Canvas;
}) => Promise<FabricObject | undefined>;
move: (e: { element: FabricObject; dx: number; dy: number }) => void;
up?: (e: { element: FabricObject }) => void;
}
>
> = {
[Mode.RECT]: {
down: ({ left, top, canvas }) =>
createElement({
mode: Mode.RECT,
position: [left, top],
canvas,
}),
move: ({ element, dx, dy }) => {
element.set({
width: dx,
height: dy,
});
snap.resize(element, Snap.ControlType.BottomRight);
},
up: ({ element }) => {
element.set({
width: defaultProps[Mode.RECT].width,
height: defaultProps[Mode.RECT].height,
});
},
},
[Mode.CIRCLE]: {
down: ({ left, top, canvas }) =>
createElement({
mode: Mode.CIRCLE,
position: [left, top],
canvas,
}),
move: ({ element, dx, dy }) => {
element.set({
rx: Math.max(dx / 2, 0),
ry: Math.max(dy / 2, 0),
});
snap.resize(element, Snap.ControlType.BottomRight);
},
up: ({ element }) => {
element.set({
rx: defaultProps[Mode.CIRCLE].rx,
ry: defaultProps[Mode.CIRCLE].ry,
});
},
},
[Mode.TRIANGLE]: {
down: ({ left, top, canvas }) =>
createElement({
mode: Mode.TRIANGLE,
position: [left, top],
canvas,
}),
move: ({ element, dx, dy }) => {
element.set({
width: dx,
height: dy,
});
snap.resize(element, Snap.ControlType.BottomRight);
},
up: ({ element }) => {
element.set({
width: defaultProps[Mode.TRIANGLE].width,
height: defaultProps[Mode.TRIANGLE].height,
});
},
},
[Mode.STRAIGHT_LINE]: {
down: ({ left, top, canvas }) =>
createElement({
mode: Mode.STRAIGHT_LINE,
position: [left, top],
canvas,
}),
move: ({ element, dx, dy }) => {
element.set({
x2: dx + (element as Line).x1,
y2: dy + (element as Line).y1,
});
// 创建直线时的终点位置修改,需要主动 fire 影响控制点的显示
element.fire('start-end:modified' as keyof ObjectEvents);
},
},
[Mode.BLOCK_TEXT]: {
down: ({ left, top, canvas }) =>
createElement({
mode: Mode.BLOCK_TEXT,
position: [left, top],
canvas,
}),
move: ({ element, dx, dy }) => {
element.set({
customFixedHeight: dy,
width: dx,
height: dy,
});
snap.resize(element, Snap.ControlType.BottomRight);
resetElementClip({ element });
},
up: ({ element }) => {
element.set({
width: defaultProps[Mode.BLOCK_TEXT].width,
height: defaultProps[Mode.BLOCK_TEXT].height,
customFixedHeight: defaultProps[Mode.BLOCK_TEXT].height,
});
resetElementClip({ element });
},
},
};
export const useDragAdd = ({
canvas,
onShapeAdded,
}: {
canvas?: Canvas;
onShapeAdded?: (data: { element: FabricObjectWithCustomProps }) => void;
}): {
enterDragAddElement: (mode: Mode) => void;
exitDragAddElement: () => void;
} => {
const newElement = useRef<
{ element: FabricObject; x: number; y: number; moved: boolean } | undefined
>();
const disposers = useRef<(() => void)[]>([]);
const enterDragAddElement = (mode: Mode) => {
if (!canvas) {
return;
}
const mouseDownDisposer = canvas.on('mouse:down', async function ({ e }) {
canvas.selection = false;
const pointer = canvas.getScenePoint(e);
e.preventDefault();
const element = await modeElementMap[mode]?.down({
left: pointer.x,
top: pointer.y,
canvas,
});
if (element) {
canvas.add(element);
canvas.setActiveObject(element);
newElement.current = {
element,
x: pointer.x,
y: pointer.y,
moved: false,
};
// 隐藏控制点,否则 onmouseup 可能被控制点截胡
element.set('hasControls', false);
}
});
const mouseMoveDisposer = canvas.on('mouse:move', function ({ e }) {
e.preventDefault();
if (newElement.current) {
const { element, x, y } = newElement.current;
const pointer = canvas.getScenePoint(e);
const dx = pointer.x - x;
const dy = pointer.y - y;
modeElementMap[mode]?.move({
element,
dx,
dy,
});
// 修正元素坐标信息
element.setCoords();
newElement.current.moved = true;
canvas.fire('object:modified');
canvas.requestRenderAll();
}
});
const mouseUpDisposer = canvas.on('mouse:up', function ({ e }) {
e.preventDefault();
if (newElement.current) {
const { element } = newElement.current;
if (!newElement.current.moved) {
modeElementMap[mode]?.up?.({
element,
});
}
onShapeAdded?.({ element: element as FabricObjectWithCustomProps });
// 恢复控制点
element.set('hasControls', true);
newElement.current = undefined;
canvas.requestRenderAll();
}
});
disposers.current.push(
mouseDownDisposer,
mouseMoveDisposer,
mouseUpDisposer,
);
};
const exitDragAddElement = () => {
if (canvas) {
canvas.selection = true;
}
if (disposers.current.length > 0) {
disposers.current.forEach(disposer => disposer());
disposers.current = [];
}
};
return {
enterDragAddElement,
exitDragAddElement,
};
};