chore: replace all cn comments of fe to en version by volc api (#320)
This commit is contained in:
@@ -58,7 +58,7 @@ describe('useAlign', () => {
|
||||
act(() => {
|
||||
result.current.alignLeft();
|
||||
});
|
||||
// 由于没有 canvas,不应该有任何操作发生
|
||||
// Since there is no canvas, no operation should occur
|
||||
});
|
||||
|
||||
it('应该在选中对象少于 2 个时不执行任何操作', () => {
|
||||
|
||||
@@ -88,11 +88,11 @@ describe('useBackground', () => {
|
||||
},
|
||||
);
|
||||
|
||||
// 更新 schema
|
||||
// Update schema
|
||||
const newSchema = createMockSchema('#000000');
|
||||
rerender({ currentSchema: newSchema });
|
||||
|
||||
// 等待 debounce
|
||||
// Waiting to debounce
|
||||
await vi.runAllTimers();
|
||||
|
||||
expect(result.current.backgroundColor).toBe('#000000');
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('useCanvasChange', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
on: vi.fn((event: string, callback: (event: any) => void) =>
|
||||
// 返回一个清理函数
|
||||
// Returns a cleaning function
|
||||
() => {
|
||||
mockCanvas.off(event, callback);
|
||||
},
|
||||
@@ -121,7 +121,7 @@ describe('useCanvasChange', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// 验证是否监听了所有默认事件
|
||||
// Verify that all default events are being listened for
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'object:modified',
|
||||
expect.any(Function),
|
||||
@@ -157,12 +157,12 @@ describe('useCanvasChange', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 object:modified 事件的回调函数
|
||||
// Get the callback function for the object: modified event
|
||||
const modifiedCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'object:modified',
|
||||
)?.[1];
|
||||
|
||||
// 模拟事件触发
|
||||
// simulated event firing
|
||||
modifiedCallback?.();
|
||||
|
||||
expect(mockCanvas.toObject).toHaveBeenCalledWith(saveProps);
|
||||
@@ -194,15 +194,15 @@ describe('useCanvasChange', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 object:removed 事件的回调函数
|
||||
// Get the callback function for the object: removed event
|
||||
const removedCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'object:removed',
|
||||
)?.[1];
|
||||
|
||||
// 模拟删除事件
|
||||
// mock delete event
|
||||
removedCallback?.();
|
||||
|
||||
// 验证只保留了存在对象的引用
|
||||
// Validation only keeps references to existing objects
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
...mockSchema,
|
||||
customVariableRefs: [
|
||||
@@ -296,7 +296,7 @@ describe('useCanvasChange', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// 更新已存在的引用
|
||||
// Update existing references
|
||||
act(() => {
|
||||
result.current.updateRefByObjectId({
|
||||
objectId: 'obj1',
|
||||
@@ -309,7 +309,7 @@ describe('useCanvasChange', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 添加新的引用
|
||||
// Add a new reference
|
||||
act(() => {
|
||||
result.current.updateRefByObjectId({
|
||||
objectId: 'obj2',
|
||||
@@ -322,14 +322,14 @@ describe('useCanvasChange', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 删除引用
|
||||
// delete reference
|
||||
act(() => {
|
||||
result.current.updateRefByObjectId({
|
||||
objectId: 'obj1',
|
||||
});
|
||||
});
|
||||
|
||||
// 验证最终的引用关系
|
||||
// Verify the final reference relationship
|
||||
expect(mockCanvas.toObject().customVariableRefs).toEqual([
|
||||
{ objectId: 'obj2', variableId: 'var3', variableName: 'var3' },
|
||||
]);
|
||||
|
||||
@@ -110,12 +110,12 @@ describe('useCanvasClip', () => {
|
||||
useCanvasClip({ canvas: mockCanvas, schema }),
|
||||
);
|
||||
|
||||
// 先添加裁剪区域
|
||||
// Add the clipping area first
|
||||
act(() => {
|
||||
result.current.addClip();
|
||||
});
|
||||
|
||||
// 移除裁剪区域
|
||||
// Remove clipping area
|
||||
act(() => {
|
||||
result.current.removeClip();
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('useGroup', () => {
|
||||
add: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
getObjects: vi.fn(),
|
||||
// 添加必要的 fabric.Object 属性
|
||||
// Add the necessary fabric. Object properties
|
||||
noScaleCache: false,
|
||||
lockMovementX: false,
|
||||
lockMovementY: false,
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('useInitCanvas', () => {
|
||||
const mockRequestRenderAll = vi.fn();
|
||||
const mockDispose = vi.fn();
|
||||
const mockOn = vi.fn(() => () => {
|
||||
// 清理函数
|
||||
// cleanup function
|
||||
});
|
||||
let mockCanvas: any;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('useMousePosition', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
on: vi.fn((event: string, callback: (event: any) => void) =>
|
||||
// 返回一个清理函数
|
||||
// Returns a cleaning function
|
||||
() => {
|
||||
mockCanvas.off(event, callback);
|
||||
},
|
||||
@@ -85,10 +85,10 @@ describe('useMousePosition', () => {
|
||||
useMousePosition({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
// 初始位置
|
||||
// initial position
|
||||
expect(result.current.mousePosition).toEqual({ left: 0, top: 0 });
|
||||
|
||||
// 模拟鼠标移动
|
||||
// Simulate mouse movement
|
||||
act(() => {
|
||||
moveCallback({
|
||||
e: { clientX: 100, clientY: 200 },
|
||||
@@ -135,12 +135,12 @@ describe('useMousePosition', () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// 更新 canvas
|
||||
// Update canvas
|
||||
rerender({ canvas: mockCanvas2 });
|
||||
|
||||
// 应该清理旧的事件监听
|
||||
// Old event listeners should be cleaned up
|
||||
expect(cleanupSpy).toHaveBeenCalled();
|
||||
// 应该设置新的事件监听
|
||||
// New event listeners should be set up
|
||||
expect(mockCanvas2.on).toHaveBeenCalledWith(
|
||||
'mouse:move',
|
||||
expect.any(Function),
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('useSnapMove', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
on: vi.fn((event: string, callback: (event: any) => void) =>
|
||||
// 返回一个清理函数
|
||||
// Returns a cleaning function
|
||||
() => {
|
||||
mockCanvas.off(event, callback);
|
||||
},
|
||||
@@ -114,12 +114,12 @@ describe('useSnapMove', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 mouse:down 事件的回调函数
|
||||
// Get the callback function for the mouse: down event
|
||||
const mouseDownCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'mouse:down',
|
||||
)?.[1];
|
||||
|
||||
// 模拟事件触发
|
||||
// simulated event firing
|
||||
mouseDownCallback?.({ target: { id: 'test-object' } });
|
||||
|
||||
expect(snap.resetAllObjectsPosition).toHaveBeenCalledWith({
|
||||
@@ -157,12 +157,12 @@ describe('useSnapMove', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 mouse:up 事件的回调函数
|
||||
// Get the callback function for the mouse: up event
|
||||
const mouseUpCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'mouse:up',
|
||||
)?.[1];
|
||||
|
||||
// 模拟事件触发
|
||||
// simulated event firing
|
||||
mouseUpCallback?.({ target: { id: 'test-object' } });
|
||||
|
||||
expect(mockSnap.reset).toHaveBeenCalled();
|
||||
@@ -198,12 +198,12 @@ describe('useSnapMove', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 object:moving 事件的回调函数
|
||||
// Get the callback function for the object: moving event
|
||||
const movingCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'object:moving',
|
||||
)?.[1];
|
||||
|
||||
// 模拟事件触发
|
||||
// simulated event firing
|
||||
movingCallback?.({ target: { id: 'test-object' } });
|
||||
|
||||
expect(mockSnap.move).toHaveBeenCalledWith({ id: 'test-object' });
|
||||
@@ -259,7 +259,7 @@ describe('useSnapMove', () => {
|
||||
},
|
||||
);
|
||||
|
||||
// 更新 scale
|
||||
// Update scale
|
||||
rerender({ scale: 2 });
|
||||
|
||||
expect(snap.helpline.resetScale).toHaveBeenCalledWith(2);
|
||||
|
||||
@@ -37,14 +37,14 @@ describe('typings', () => {
|
||||
|
||||
describe('AlignMode', () => {
|
||||
it('应该定义正确的对齐模式枚举值', () => {
|
||||
// 注意:这里的具体值需要根据实际的 AlignMode 枚举定义来填写
|
||||
// Note: The specific values here need to be filled in according to the actual AlignMode enumeration definition
|
||||
expect(AlignMode).toBeDefined();
|
||||
expect(typeof AlignMode).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
// 由于其他导出主要是类型定义,在运行时无法直接测试
|
||||
// 但我们可以通过 TypeScript 的类型检查来验证它们的正确性
|
||||
// Since other exports are mainly type definitions, they cannot be tested directly at runtime
|
||||
// But we can verify their correctness through TypeScript's type checking
|
||||
it('应该正确定义 FormMetaItem 接口', () => {
|
||||
const formMetaItem = {
|
||||
name: 'test',
|
||||
@@ -59,7 +59,7 @@ describe('typings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// 这个测试主要是确保类型定义正确,实际运行时不会失败
|
||||
// The main purpose of this test is to ensure that the type definition is correct and the actual execution does not fail
|
||||
expect(formMetaItem).toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const fontsFormat: {
|
||||
const group = dArr[2];
|
||||
|
||||
return {
|
||||
// 原本的名称
|
||||
// Original name
|
||||
value: dArr[1],
|
||||
label: (
|
||||
<img
|
||||
@@ -57,11 +57,11 @@ const fontsFormat: {
|
||||
src={`${cdnPrefix}/image-canvas-fonts-preview-svg/${d}`}
|
||||
/>
|
||||
),
|
||||
// 顺序
|
||||
// order
|
||||
order: Number(dArr[0]),
|
||||
// 一级分组名称
|
||||
// first-level grouping name
|
||||
name,
|
||||
// 属于哪个分组
|
||||
// Which group does it belong to?
|
||||
groupName: group,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -15,31 +15,31 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 画布最大缩放
|
||||
* Canvas Max Zoom
|
||||
*/
|
||||
export const MAX_ZOOM = 3;
|
||||
/**
|
||||
* 画布最小缩放
|
||||
* Canvas minimum zoom
|
||||
*/
|
||||
export const MIN_ZOOM = 1;
|
||||
/**
|
||||
* 画布最大宽度
|
||||
* Canvas maximum width
|
||||
*/
|
||||
export const MAX_WIDTH = 10000;
|
||||
/**
|
||||
* 画布最小宽度
|
||||
* Canvas minimum width
|
||||
*/
|
||||
export const MIN_WIDTH = 1;
|
||||
/**
|
||||
* 画布最大高度
|
||||
* Canvas maximum height
|
||||
*/
|
||||
export const MAX_HEIGHT = 10000;
|
||||
/**
|
||||
* 画布最小高度
|
||||
* minimum height of canvas
|
||||
*/
|
||||
export const MIN_HEIGHT = 1;
|
||||
|
||||
/**
|
||||
* 画布最大面积
|
||||
* Canvas maximum area
|
||||
*/
|
||||
export const MAX_AREA = 3840 * 2160;
|
||||
|
||||
@@ -72,8 +72,8 @@ interface IProps {
|
||||
onChange: (schema: FabricSchema) => void;
|
||||
className?: string;
|
||||
/**
|
||||
* 不强制,用来当做 redo/undo 操作栈保存到内存的 key
|
||||
* 不传的话,不会保存操作栈到内存,表现:关闭侧拉窗,丢失操作栈
|
||||
* Unforced, used as the key to save the redo/undo operation stack to memory
|
||||
* If it is not passed, the operation stack will not be saved to memory. Performance: close the side pull window and lose the operation stack.
|
||||
*/
|
||||
id?: string;
|
||||
}
|
||||
@@ -98,10 +98,10 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
}, [_variables]);
|
||||
|
||||
/**
|
||||
* props.onChange 是异步,这个异步导致 schema 的状态很难管理。
|
||||
* 因此此处用 state 来管理 schema,后续消费 onChange 的地方可以当同步处理
|
||||
* Props.onChange is asynchronous, which makes the state of the schema difficult to manage.
|
||||
* Therefore, state is used to manage the schema here, and subsequent consumption of onChange can be handled synchronously
|
||||
*
|
||||
* 副作用:外界引发的 schema 变化,不会同步到画布(暂时没这个场景)
|
||||
* Side effect: Schema changes caused by the outside world will not be synchronized to the canvas (this scene is not available for now)
|
||||
*/
|
||||
const [schema, setSchema] = useState<FabricSchema>(_schema);
|
||||
const onChange = useCallback(
|
||||
@@ -115,26 +115,26 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
const [id] = useState<string>(props.id ?? nanoid());
|
||||
const helpLineLayerId = `help-line-${id}`;
|
||||
|
||||
// 快捷点监听区域
|
||||
// Shortcut the listening area
|
||||
const shortcutRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Popover 渲染至 dom
|
||||
// Popover rendering to dom
|
||||
const popRef = useRef(null);
|
||||
// Popover 渲染至 dom,作用于 select ,dropdown 右对齐
|
||||
// Popover render to dom, act on select, dropdown right align
|
||||
const popRefAlignRight = useRef<HTMLDivElement>(null);
|
||||
|
||||
// canvas 可渲染区域 dom
|
||||
// Canvas renderable domain
|
||||
const sizeRef = useRef<HTMLDivElement>(null);
|
||||
const size = useSize(sizeRef);
|
||||
|
||||
// popover 渲染至 dom
|
||||
// Popover render to dom
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const popoverSize = useSize(popoverRef);
|
||||
|
||||
// fabric canvas 渲染 dom
|
||||
// Fabric canvas rendering dom
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// 模式
|
||||
// pattern
|
||||
const [drawMode, setDrawMode] = useState<Mode | undefined>();
|
||||
const latestDrawMode = useLatest(drawMode);
|
||||
|
||||
@@ -146,7 +146,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
| undefined
|
||||
>();
|
||||
|
||||
// 监听鼠标是否处于按下状态,松手时才显示属性设置面板
|
||||
// Monitor whether the mouse is pressed, and display the property settings panel when you let go
|
||||
const [isMousePressing, setIsMousePressing] = useState(false);
|
||||
|
||||
const cancelContentMenu = useCallback(() => {
|
||||
@@ -298,7 +298,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
},
|
||||
};
|
||||
|
||||
// 针对画笔模式,达到上限后,主动退出绘画模式
|
||||
// For brush mode, after reaching the upper limit, actively exit the painting mode
|
||||
useEffect(() => {
|
||||
if (drawMode && !couldAddNewObject) {
|
||||
modeSetting[drawMode]?.exitFn();
|
||||
@@ -307,7 +307,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
}, [couldAddNewObject, drawMode]);
|
||||
|
||||
const zoomStartPointer = useRef<Point>();
|
||||
// 鼠标滚轮缩放
|
||||
// mouse wheel zoom
|
||||
const onWheelZoom = (e: WheelEvent, isFirst: boolean) => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
@@ -321,7 +321,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
zoomStartPointer.current = pointer;
|
||||
}
|
||||
|
||||
// 根据滚轮方向确定是放大还是缩小
|
||||
// Determine whether to zoom in or out according to the direction of the roller
|
||||
if (delta < 0) {
|
||||
zoomLevel += zoomStep;
|
||||
} else {
|
||||
@@ -333,7 +333,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
// 鼠标位移
|
||||
// mouse displacement
|
||||
const onWheelTransform = (deltaX: number, deltaY: number) => {
|
||||
const vpt: TMat2D = [...viewport];
|
||||
vpt[4] -= deltaX;
|
||||
@@ -341,7 +341,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
setViewport(vpt);
|
||||
};
|
||||
|
||||
// 触摸板手势缩放、位移
|
||||
// Touchpad gesture zoom, shift
|
||||
const gestureBind = useGesture(
|
||||
{
|
||||
onPinch: state => {
|
||||
@@ -380,7 +380,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
},
|
||||
);
|
||||
|
||||
// 当用户编辑文本时,按删除键不应该执行删除元素操作
|
||||
// When a user edits text, pressing the delete key should not perform a delete element operation
|
||||
const [isTextEditing, setIsTextEditing] = useState(false);
|
||||
useEffect(() => {
|
||||
let disposers: (() => void)[] = [];
|
||||
@@ -426,7 +426,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
};
|
||||
}, [sizeRef]);
|
||||
|
||||
// 点击画布外侧,取消选中
|
||||
// Click on the outside of the canvas and uncheck it.
|
||||
useEffect(() => {
|
||||
const clickOutside = (e: MouseEvent) => {
|
||||
setContentMenuPosition(undefined);
|
||||
@@ -438,7 +438,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
};
|
||||
}, [discardActiveObject]);
|
||||
|
||||
// 注册快捷键
|
||||
// Registration shortcut
|
||||
useShortcut({
|
||||
ref: shortcutRef,
|
||||
state: {
|
||||
@@ -471,7 +471,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
|
||||
const isContentMenuShow = !readonly && contentMenuPosition;
|
||||
|
||||
// 选中元素是否为同一类型(包含框选)
|
||||
// Whether the selected elements are of the same type (including box selection)
|
||||
const isSameActiveObjects =
|
||||
Array.from(
|
||||
new Set(
|
||||
@@ -482,14 +482,14 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
).length === 1;
|
||||
|
||||
/**
|
||||
* 属性菜单没有展示 &&
|
||||
* 鼠标右键没有按下(拖拽 ing)&&
|
||||
* Properties menu not displayed & &
|
||||
* The right mouse button is not pressed (drag and drop ing) & &
|
||||
* isSameActiveObjects &&
|
||||
*/
|
||||
const isFormShow =
|
||||
!isContentMenuShow && !isMousePressing && isSameActiveObjects;
|
||||
|
||||
// 最大宽高有两层限制 1. 面积 2. 固定最大值
|
||||
// There are two restrictions on the maximum width and height: 1. Area 2. Fixed maximum
|
||||
const { canvasMaxWidth, canvasMaxHeight } = useMemo(
|
||||
() => ({
|
||||
canvasMaxWidth: Math.min(MAX_AREA / schema.height, MAX_WIDTH),
|
||||
@@ -708,7 +708,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{/* 引用 tag */}
|
||||
{/* Reference tag */}
|
||||
<RefTitle visible={!isMousePressing} />
|
||||
<div className="w-fit h-fit overflow-hidden">
|
||||
<div
|
||||
@@ -719,7 +719,7 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 右键菜单 */}
|
||||
{/* right-click menu */}
|
||||
{isContentMenuShow ? (
|
||||
<ContentMenu
|
||||
limitRect={popoverSize}
|
||||
@@ -745,10 +745,10 @@ export const FabricEditor: FC<IProps> = props => {
|
||||
<></>
|
||||
)}
|
||||
|
||||
{/* 属性面板 */}
|
||||
{/* properties panel */}
|
||||
{isFormShow ? (
|
||||
<Form
|
||||
// 文本切换时,涉及字号变化,需要 rerender form 同步状态
|
||||
// Text switching, involving font size changes, need to rerender form synchronization state
|
||||
key={
|
||||
(activeObjects as FabricObjectWithCustomProps[])?.[0]
|
||||
?.customType
|
||||
|
||||
@@ -74,7 +74,7 @@ export const useShortcut = ({
|
||||
horizontalAverage: () => void;
|
||||
};
|
||||
}) => {
|
||||
// 上下左右微调元素位置
|
||||
// Fine-tune element positions up, down, left, right
|
||||
useKeyPress(
|
||||
['uparrow', 'downarrow', 'leftarrow', 'rightarrow'],
|
||||
e => {
|
||||
@@ -101,7 +101,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 删除元素
|
||||
// Delete element
|
||||
useKeyPress(
|
||||
['backspace', 'delete'],
|
||||
e => {
|
||||
@@ -118,7 +118,7 @@ export const useShortcut = ({
|
||||
useKeyPress(
|
||||
['ctrl.z', 'meta.z'],
|
||||
e => {
|
||||
// 一定要加,否则会命中浏览器乱七八糟的默认行为
|
||||
// Be sure to add it, otherwise it will hit the browser's messy default behavior.
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
redo();
|
||||
@@ -133,7 +133,7 @@ export const useShortcut = ({
|
||||
);
|
||||
|
||||
/**
|
||||
* 功能开发暂停了,原因详见 packages/workflow/fabric-canvas/src/hooks/use-group.tsx
|
||||
* Functional development has been suspended. For the reasons, see packages/workflow/fabricate-canvas/src/hooks/use-group.tsx
|
||||
*/
|
||||
// useKeyPress(
|
||||
// ['ctrl.g', 'meta.g'],
|
||||
@@ -181,11 +181,11 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 生成副本
|
||||
// make a copy
|
||||
useKeyPress(
|
||||
['ctrl.d', 'meta.d'],
|
||||
async e => {
|
||||
// 必须阻止默认行为,否则会触发添加标签
|
||||
// The default behavior must be blocked or the add label will be triggered
|
||||
e.preventDefault();
|
||||
await copy(CopyMode.CtrlD);
|
||||
paste({
|
||||
@@ -199,7 +199,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// [ 下移一层
|
||||
// [Move down one floor
|
||||
useKeyPress(
|
||||
['openbracket'],
|
||||
e => {
|
||||
@@ -214,7 +214,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// ] 上移一层
|
||||
// Move up one layer
|
||||
useKeyPress(
|
||||
['closebracket'],
|
||||
e => {
|
||||
@@ -228,7 +228,7 @@ export const useShortcut = ({
|
||||
target: ref,
|
||||
},
|
||||
);
|
||||
// ⌘ + [、⌘ + ] 禁止浏览器默认行为 前进、后退
|
||||
// ⌘ + [、⌘ + ] disable browser default behavior, forward and backward
|
||||
useKeyPress(
|
||||
['meta.openbracket', 'meta.closebracket'],
|
||||
e => {
|
||||
@@ -243,7 +243,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// ⌘ + [ 置底
|
||||
// < unk > +
|
||||
useKeyPress(
|
||||
['meta.openbracket'],
|
||||
e => {
|
||||
@@ -258,7 +258,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// ⌘ + ] 置顶
|
||||
// 🥰 +] top
|
||||
useKeyPress(
|
||||
['meta.closebracket'],
|
||||
e => {
|
||||
@@ -273,7 +273,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 水平居左
|
||||
// Horizontal left
|
||||
useKeyPress(
|
||||
['alt.a'],
|
||||
e => {
|
||||
@@ -287,7 +287,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 水平居右
|
||||
// Horizontal right
|
||||
useKeyPress(
|
||||
['alt.d'],
|
||||
e => {
|
||||
@@ -301,7 +301,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 水平居中
|
||||
// centered text
|
||||
useKeyPress(
|
||||
['alt.h'],
|
||||
e => {
|
||||
@@ -315,7 +315,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 垂直居上
|
||||
// vertical top
|
||||
useKeyPress(
|
||||
['alt.w'],
|
||||
e => {
|
||||
@@ -329,7 +329,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 垂直居下
|
||||
// vertical
|
||||
useKeyPress(
|
||||
['alt.s'],
|
||||
e => {
|
||||
@@ -343,7 +343,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 垂直居中
|
||||
// Vertically centered
|
||||
useKeyPress(
|
||||
['alt.v'],
|
||||
e => {
|
||||
@@ -357,7 +357,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 水平均分
|
||||
// horizontal average fraction
|
||||
useKeyPress(
|
||||
['alt.ctrl.h'],
|
||||
e => {
|
||||
@@ -371,7 +371,7 @@ export const useShortcut = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 垂直均分
|
||||
// vertical equipartition
|
||||
useKeyPress(
|
||||
['alt.ctrl.v'],
|
||||
e => {
|
||||
|
||||
@@ -40,7 +40,7 @@ export const FabricPreview: FC<IFabricPreview> = props => {
|
||||
oldWidth.current = size?.width || 0;
|
||||
}
|
||||
|
||||
// 防止抖动,当宽度变化 > 20 时才更新宽度
|
||||
// To prevent jitter, update the width when the width changes > 20
|
||||
if (size?.width && size.width - oldWidth.current > 20) {
|
||||
oldWidth.current = size?.width || 0;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ const FormItem = memo(
|
||||
metaItem: FormMetaItem;
|
||||
isLast: boolean;
|
||||
isRow: boolean;
|
||||
// 给图片上传组件特化的,需要根据是否为引用元素,设置不同的 label
|
||||
// Specialized for the image upload component, you need to set different labels according to whether it is a reference element.
|
||||
isRefElement: boolean;
|
||||
formValue: Partial<FabricObjectSchema>;
|
||||
onChange: (v: Partial<FabricObjectSchema>, cacheSave?: boolean) => void;
|
||||
@@ -236,7 +236,7 @@ export const Form: FC<IProps> = props => {
|
||||
[activeObjects],
|
||||
);
|
||||
|
||||
// 临时保存不需要保存到 schema 中的表单值
|
||||
// Temporary saving of form values that do not need to be saved to the schema
|
||||
const [cacheFormValue, setCacheFormValue] = useState<
|
||||
Partial<FabricObjectSchema>
|
||||
>({});
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import styles from './index.module.less';
|
||||
|
||||
/**
|
||||
* 在 size:small 的基础上,覆盖了 padding ,5px -> 4px
|
||||
* On the basis of size: small, overlay padding, 5px - > 4px
|
||||
*/
|
||||
export const MyIconButton = forwardRef<
|
||||
SemiButton,
|
||||
|
||||
@@ -66,8 +66,8 @@ export const PopInScreen: FC<IProps> = props => {
|
||||
}
|
||||
|
||||
/**
|
||||
* ahooks useSize 初次执行会返回 undefined,导致组件位置计算错误
|
||||
* 这里监听 childrenSize ,如果为 undefined 则延迟 100ms 再渲染,以修正组件位置
|
||||
* ahooks useSize returns undefined on first execution, resulting in an error in component location evaluation
|
||||
* This listens to childrenSize. If it is undefined, delay rendering by 100ms to correct the component position.
|
||||
*/
|
||||
const [id, setId] = useState('');
|
||||
const timer = useRef<NodeJS.Timeout>();
|
||||
@@ -109,7 +109,7 @@ export const PopInScreen: FC<IProps> = props => {
|
||||
transform,
|
||||
}}
|
||||
>
|
||||
{/* 为了触发二次渲染 */}
|
||||
{/* To trigger secondary rendering */}
|
||||
<div className="hidden" id={id} />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -138,13 +138,13 @@ export const ColorPicker: FC<IProps> = props => {
|
||||
})}
|
||||
</div>
|
||||
<Input
|
||||
// 因为是不受控模式,当点击色块时,需要重置 input.value。所以这里以 color 为 key
|
||||
// Because it is in uncontrolled mode, when clicking on the color block, you need to reset the input.value. So here color is the key
|
||||
key={`input-${color}`}
|
||||
disabled={readonly}
|
||||
prefix={<ColorRect color={color as string} size={16} />}
|
||||
type="text"
|
||||
className="w-[110px]"
|
||||
// 为什么不使用受控模式?使用受控模式,用户输入过程中触发的格式校验处理起来比较麻烦
|
||||
// Why not use controlled mode? With controlled mode, format checking triggered during user input is cumbersome to handle
|
||||
defaultValue={color}
|
||||
onChange={v => {
|
||||
if (isHexColor(v)) {
|
||||
|
||||
@@ -64,8 +64,8 @@ export const FontSize: FC<IProps> = props => {
|
||||
<IconCozFontSize className="text-[16px] coz-fg-secondary m-[8px]" />
|
||||
}
|
||||
/**
|
||||
* 因为开启了 allowCreate,所以 optionList 不会再响应动态变化
|
||||
* 这里给个 key ,重新渲染 select,保证 optionList 符合预期
|
||||
* Since allowCreate is enabled, the optionList will no longer respond to dynamic changes
|
||||
* Give a key here, re-render select, and ensure that the optionList meets expectations
|
||||
*/
|
||||
key={_optionsList.map(d => d.label).join()}
|
||||
value={value}
|
||||
|
||||
@@ -30,8 +30,8 @@ export const InputNumber = forwardRef<InputNumberProps, InputNumberProps>(
|
||||
min={min}
|
||||
max={max}
|
||||
value={value}
|
||||
// InputNumber 长按 + - 时,会一直触发变化。这里有 bug,有时定时器清不掉,会鬼畜(一直增加/减小)。
|
||||
// 把 pressInterval 设置成 24h ,变相禁用长按增减
|
||||
// InputNumber When long pressing + -, it will keep triggering changes. There are bugs here, and sometimes the timer can't be cleared, and it will be ghost (keep increasing/decreasing).
|
||||
// Set pressInterval to 24h, and disable long press increase or decrease in disguise
|
||||
pressInterval={1000 * 60 * 60 * 24}
|
||||
onNumberChange={v => {
|
||||
if (Number.isFinite(v)) {
|
||||
|
||||
@@ -71,8 +71,8 @@ export const LineHeight: FC<IProps> = props => {
|
||||
}
|
||||
{...rest}
|
||||
/**
|
||||
* 因为开启了 allowCreate,所以 optionList 不会再响应动态变化
|
||||
* 这里给个 key ,重新渲染 select,保证 optionList 符合预期
|
||||
* Since allowCreate is enabled, the optionList will no longer respond to dynamic changes
|
||||
* Give a key here, re-render select, and ensure that the optionList meets expectations
|
||||
*/
|
||||
key={_optionsList.map(d => d.label).join()}
|
||||
filter
|
||||
|
||||
@@ -61,7 +61,7 @@ export const Align: FC<IProps> = props => {
|
||||
</Select.Option>
|
||||
);
|
||||
return (
|
||||
// 禁止冒泡,防止点击对齐时,canvas 的选中状态被清空
|
||||
// Prohibit bubbling to prevent the selected state of canvas from being cleared when clicking align
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -193,7 +193,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
aligns,
|
||||
} = props;
|
||||
|
||||
// 点击已选中的,则取消选中
|
||||
// Click on the selected one to unselect it.
|
||||
const onModeChange = useCallback(
|
||||
(m: Mode | undefined) => {
|
||||
if (m === mode) {
|
||||
@@ -233,7 +233,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
'flex justify-center items-center gap-[12px]',
|
||||
])}
|
||||
>
|
||||
{/* 引用变量 */}
|
||||
{/* reference variable */}
|
||||
<Tooltip
|
||||
key="ref-variable"
|
||||
content={I18n.t('workflow_detail_condition_reference')}
|
||||
@@ -317,7 +317,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
</Tooltip>
|
||||
<SplitLine />
|
||||
|
||||
{/* 画布基础设置 */}
|
||||
{/* canvas base settings */}
|
||||
<Tooltip
|
||||
key="canvas-setting"
|
||||
position="bottom"
|
||||
@@ -407,7 +407,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 重置视图 */}
|
||||
{/* Reset view */}
|
||||
<Tooltip
|
||||
key="reset-view"
|
||||
content={I18n.t('imageflow_canvas_restart')}
|
||||
@@ -489,7 +489,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
</Tooltip>
|
||||
<SplitLine />
|
||||
|
||||
{/* 置底 置顶 */}
|
||||
{/* Bottom, top */}
|
||||
<Tooltip
|
||||
key="move-to-bottom"
|
||||
content={I18n.t('card_builder_move_to_bottom')}
|
||||
@@ -512,12 +512,12 @@ export const TopBar: FC<IProps> = props => {
|
||||
icon={<IconCozMoveToTopFill className="text-[16px]" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/* 对齐 */}
|
||||
{/* align */}
|
||||
<div className="flex">
|
||||
<MyIconButton
|
||||
disabled={readonly}
|
||||
onClick={e => {
|
||||
// 禁止冒泡,防止点击对齐时,canvas 的选中状态被清空
|
||||
// Prohibit bubbling to prevent the selected state of canvas from being cleared when clicking align
|
||||
e.stopPropagation();
|
||||
aligns[alignType]();
|
||||
}}
|
||||
@@ -534,7 +534,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 文本 */}
|
||||
{/* Text */}
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
key="text"
|
||||
@@ -583,7 +583,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* 图片 */}
|
||||
{/* picture */}
|
||||
|
||||
<ImageUpload
|
||||
onChange={onAddImg}
|
||||
@@ -595,7 +595,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
/>
|
||||
</ImageUpload>
|
||||
|
||||
{/* 形状 */}
|
||||
{/* shape */}
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
key="shape"
|
||||
@@ -656,7 +656,7 @@ export const TopBar: FC<IProps> = props => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 自由画笔 */}
|
||||
{/* Free brush */}
|
||||
<Tooltip
|
||||
key="pencil"
|
||||
content={I18n.t('imageflow_canvas_draw')}
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
import { setImageFixed } from '../share';
|
||||
import { useCanvasChange } from './use-canvas-change';
|
||||
|
||||
// 设置元素属性
|
||||
// Set element properties
|
||||
const setElementProps = async ({
|
||||
element,
|
||||
props,
|
||||
@@ -52,7 +52,7 @@ const setElementProps = async ({
|
||||
props: Partial<FabricObjectSchema>;
|
||||
canvas?: Canvas;
|
||||
}): Promise<void> => {
|
||||
// 特化一:img 的属性设置需要设置到 img 元素上,而不是外层包裹的 group
|
||||
// Specialization 1: The attribute settings of img need to be set to the img element, not the outer wrapped group
|
||||
if (
|
||||
element?.isType('group') &&
|
||||
(element as Group)?.getObjects()?.[0]?.isType('image')
|
||||
@@ -62,21 +62,21 @@ const setElementProps = async ({
|
||||
const img = group.getObjects()[0] as FabricImage;
|
||||
const borderRect = group.getObjects()[1] as Rect;
|
||||
|
||||
// 边框颜色设置到 borderRect 上
|
||||
// Set the border color to borderRect
|
||||
if (stroke) {
|
||||
borderRect.set({
|
||||
stroke,
|
||||
});
|
||||
}
|
||||
|
||||
// 边框粗细设置到 borderRect 上
|
||||
// The border thickness is set to borderRect
|
||||
if (typeof strokeWidth === 'number') {
|
||||
borderRect.set({
|
||||
strokeWidth,
|
||||
});
|
||||
}
|
||||
|
||||
// 替换图片
|
||||
// Replace image
|
||||
if (src) {
|
||||
const newImg = document.createElement('img');
|
||||
await new Promise((done, reject) => {
|
||||
@@ -94,7 +94,7 @@ const setElementProps = async ({
|
||||
|
||||
setImageFixed({ element: group });
|
||||
} else {
|
||||
// 特化二:文本与段落切换,需要特化处理
|
||||
// Specialization 2: Text and paragraph switching requires specialized processing
|
||||
const { customType, ...rest } = props;
|
||||
if (
|
||||
customType &&
|
||||
@@ -143,11 +143,11 @@ const setElementProps = async ({
|
||||
elementProps: {
|
||||
...extendsProps,
|
||||
...(props.customType === Mode.INLINE_TEXT
|
||||
? // 块状 -> 单行
|
||||
? // Block - > single row
|
||||
{}
|
||||
: // 单行 -> 块状
|
||||
: // Single Row - > Block
|
||||
{
|
||||
// 单行切块状,尽量保持字体大小不变化
|
||||
// Cut the block in a single line, and try to keep the font size unchanged.
|
||||
fontSize: newFontSize,
|
||||
padding: newFontSize / 4,
|
||||
width: 200,
|
||||
@@ -156,30 +156,30 @@ const setElementProps = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// 如果还有别的属性,设置到新 element 上
|
||||
// If there are other properties, set them to the new element
|
||||
if (Object.keys(rest).length > 0) {
|
||||
newElement?.set(rest);
|
||||
}
|
||||
|
||||
// 添加新的,顺序不能错,否则删除时引用关系会被判定为无用关系而被删除掉
|
||||
// Add new ones in the correct order, otherwise the reference relationship will be determined to be useless and deleted when deleting.
|
||||
canvas?.add(newElement as FabricObject);
|
||||
// 删掉老的
|
||||
// Delete the old one
|
||||
canvas?.remove(oldElement);
|
||||
|
||||
canvas?.discardActiveObject();
|
||||
canvas?.setActiveObject(newElement as FabricObject);
|
||||
canvas?.requestRenderAll();
|
||||
|
||||
// 普通的属性设置
|
||||
// Normal property settings
|
||||
} else {
|
||||
const { fontFamily } = props;
|
||||
// 特化三: 字体需要异步加载
|
||||
// Specialization 3: Fonts need to be loaded asynchronously
|
||||
if (fontFamily) {
|
||||
await loadFont(fontFamily);
|
||||
}
|
||||
/**
|
||||
* textBox 比较恶心,不知道什么时机会给每个字都生成样式文件(对应 styles)
|
||||
* 这里主动清除下,否则字体相关的设置(fontSize、fontFamily...)不生效
|
||||
* textBox is disgusting. I don't know when to generate a style file for each word (corresponding styles).
|
||||
* Take the initiative to clear it here, otherwise the font-related settings (fontSize, fontFamily...) will not take effect
|
||||
*/
|
||||
if (element?.isType('textbox')) {
|
||||
element?.set({
|
||||
@@ -187,7 +187,7 @@ const setElementProps = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// 特化四:padding = fontSize/2 , 避免文本上下被截断
|
||||
// Specialization 4: padding = fontSize/2, to avoid text being truncated up and down
|
||||
if (element?.isType('textbox') && typeof props.fontSize === 'number') {
|
||||
element?.set({
|
||||
padding: props.fontSize / 4,
|
||||
@@ -285,9 +285,9 @@ export const useActiveObjectChange = ({
|
||||
if (selected) {
|
||||
selected.set(selectedBorderProps);
|
||||
/**
|
||||
* 为什么禁用选中多元素的控制点?
|
||||
* 因为直线不期望有旋转,旋转会影响控制点的计算逻辑。
|
||||
* 想要放开这个限制,需要在直线的控制点内考虑旋转 & 缩放因素
|
||||
* Why disable control points with multiple elements selected?
|
||||
* Since a straight line does not expect rotation, rotation affects the computational logic of the control points.
|
||||
* To remove this restriction, you need to consider the rotation & scaling factor within the control points of the line
|
||||
*/
|
||||
if (selected.isType('activeselection')) {
|
||||
selected.setControlsVisibility({
|
||||
@@ -318,7 +318,7 @@ export const useActiveObjectChange = ({
|
||||
};
|
||||
}, [canvas]);
|
||||
|
||||
// 窗口大小变化时,修正下位置
|
||||
// When the window size changes, correct the position
|
||||
useEffect(() => {
|
||||
_setActiveObjectsPopPosition();
|
||||
}, [scale]);
|
||||
@@ -361,7 +361,7 @@ export const useActiveObjectChange = ({
|
||||
canvas?.fire('object:modified');
|
||||
};
|
||||
|
||||
// 实现 shift 水平/垂直移动
|
||||
// To shift horizontally/vertically
|
||||
useEffect(() => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
@@ -369,39 +369,39 @@ export const useActiveObjectChange = ({
|
||||
let originalPos = { left: 0, top: 0 };
|
||||
|
||||
const disposers = [
|
||||
// 监听对象移动开始事件
|
||||
// Listening object movement start event
|
||||
canvas.on('object:moving', function (e) {
|
||||
const obj = e.target;
|
||||
// 手动 canvas.fire('object:moving') 获取不到 obj
|
||||
// Manual canvas.fire ('object: moving') cannot get obj
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是第一次移动,记录对象的原始位置
|
||||
// If it is the first move, record the original position of the object
|
||||
if (originalPos.left === 0 && originalPos.top === 0) {
|
||||
originalPos = { left: obj.left, top: obj.top };
|
||||
}
|
||||
|
||||
// 检查是否按下了Shift键
|
||||
// Check if the Shift key is pressed
|
||||
if (e?.e?.shiftKey) {
|
||||
// 计算从开始移动以来的水平和垂直距离
|
||||
// Calculate the horizontal and vertical distance since the start of the movement
|
||||
const distanceX = obj.left - originalPos.left;
|
||||
const distanceY = obj.top - originalPos.top;
|
||||
|
||||
// 根据移动距离的绝对值判断是水平移动还是垂直移动
|
||||
// Determine whether to move horizontally or vertically according to the absolute value of the moving distance
|
||||
if (Math.abs(distanceX) > Math.abs(distanceY)) {
|
||||
// 水平移动:保持垂直位置不变
|
||||
// Horizontal movement: maintain the same vertical position
|
||||
obj.set('top', originalPos.top);
|
||||
} else {
|
||||
// 垂直移动:保持水平位置不变
|
||||
// Vertical movement: maintain the same horizontal position
|
||||
obj.set('left', originalPos.left);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// 监听对象移动结束事件
|
||||
// Listening Object Move End Event
|
||||
canvas.on('object:modified', function (e) {
|
||||
// 移动结束后重置原始位置
|
||||
// Reset original position after move
|
||||
originalPos = { left: 0, top: 0 };
|
||||
}),
|
||||
];
|
||||
@@ -418,7 +418,7 @@ export const useActiveObjectChange = ({
|
||||
| undefined
|
||||
>();
|
||||
|
||||
// 元素移动过程中,隐藏控制点
|
||||
// Hide control points during element movement
|
||||
useEffect(() => {
|
||||
const disposers: (() => void)[] = [];
|
||||
if (activeObjects?.length === 1) {
|
||||
@@ -427,29 +427,29 @@ export const useActiveObjectChange = ({
|
||||
element.on('moving', () => {
|
||||
if (!controlsVisibility.current) {
|
||||
controlsVisibility.current = Object.assign(
|
||||
// fabric 规则: undefined 认为是 true
|
||||
// Fabric rule: undefined is considered true
|
||||
{
|
||||
ml: true, // 中点左
|
||||
mr: true, // 中点右
|
||||
mt: true, // 中点上
|
||||
mb: true, // 中点下
|
||||
bl: true, // 底部左
|
||||
br: true, // 底部右
|
||||
tl: true, // 顶部左
|
||||
tr: true, // 顶部右
|
||||
ml: true, // Midpoint left
|
||||
mr: true, // Midpoint right
|
||||
mt: true, // midpoint
|
||||
mb: true, // midpoint
|
||||
bl: true, // Bottom left
|
||||
br: true, // Bottom right
|
||||
tl: true, // Top Left
|
||||
tr: true, // Top right
|
||||
},
|
||||
element._controlsVisibility,
|
||||
);
|
||||
}
|
||||
element.setControlsVisibility({
|
||||
ml: false, // 中点左
|
||||
mr: false, // 中点右
|
||||
mt: false, // 中点上
|
||||
mb: false, // 中点下
|
||||
bl: false, // 底部左
|
||||
br: false, // 底部右
|
||||
tl: false, // 顶部左
|
||||
tr: false, // 顶部右
|
||||
ml: false, // Midpoint left
|
||||
mr: false, // Midpoint right
|
||||
mt: false, // midpoint
|
||||
mb: false, // midpoint
|
||||
bl: false, // Bottom left
|
||||
br: false, // Bottom right
|
||||
tl: false, // Top Left
|
||||
tr: false, // Top right
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useAlign = ({
|
||||
canvas?: Canvas;
|
||||
selectObjects?: FabricObject[];
|
||||
}) => {
|
||||
// 水平居左
|
||||
// Horizontal left
|
||||
const alignLeft = useCallback(() => {
|
||||
if (!canvas || selectObjects.length < 2) {
|
||||
return;
|
||||
@@ -48,7 +48,7 @@ export const useAlign = ({
|
||||
canvas.requestRenderAll();
|
||||
}, [canvas, selectObjects]);
|
||||
|
||||
// 水平居右
|
||||
// Horizontal right
|
||||
const alignRight = useCallback(() => {
|
||||
if (!canvas || selectObjects.length < 2) {
|
||||
return;
|
||||
@@ -66,7 +66,7 @@ export const useAlign = ({
|
||||
canvas.requestRenderAll();
|
||||
}, [canvas, selectObjects]);
|
||||
|
||||
// 水平居中
|
||||
// centered text
|
||||
const alignCenter = useCallback(() => {
|
||||
if (!canvas || selectObjects.length < 2) {
|
||||
return;
|
||||
@@ -84,7 +84,7 @@ export const useAlign = ({
|
||||
canvas.requestRenderAll();
|
||||
}, [canvas, selectObjects]);
|
||||
|
||||
// 垂直居上
|
||||
// vertical top
|
||||
const alignTop = useCallback(() => {
|
||||
if (!canvas || selectObjects.length < 2) {
|
||||
return;
|
||||
@@ -102,7 +102,7 @@ export const useAlign = ({
|
||||
canvas.requestRenderAll();
|
||||
}, [canvas, selectObjects]);
|
||||
|
||||
// 垂直居中
|
||||
// Vertically centered
|
||||
const alignMiddle = useCallback(() => {
|
||||
if (!canvas || selectObjects.length < 2) {
|
||||
return;
|
||||
@@ -120,7 +120,7 @@ export const useAlign = ({
|
||||
canvas.requestRenderAll();
|
||||
}, [canvas, selectObjects]);
|
||||
|
||||
// 垂直居下
|
||||
// vertical
|
||||
const alignBottom = useCallback(() => {
|
||||
if (!canvas || selectObjects.length < 2) {
|
||||
return;
|
||||
@@ -138,7 +138,7 @@ export const useAlign = ({
|
||||
canvas.requestRenderAll();
|
||||
}, [canvas, selectObjects]);
|
||||
|
||||
// 水平均分
|
||||
// horizontal average fraction
|
||||
const verticalAverage = useCallback(() => {
|
||||
if (!canvas || selectObjects.length < 2) {
|
||||
return;
|
||||
@@ -155,7 +155,7 @@ export const useAlign = ({
|
||||
const spacing =
|
||||
(activeObject.width - totalWidth) / (selectObjects.length - 1);
|
||||
|
||||
let currentLeft = -activeObject.width / 2; // 初始位置
|
||||
let currentLeft = -activeObject.width / 2; // initial position
|
||||
|
||||
selectObjects
|
||||
.sort((a, b) => a.getBoundingRect().left - b.getBoundingRect().left)
|
||||
@@ -169,7 +169,7 @@ export const useAlign = ({
|
||||
canvas.requestRenderAll();
|
||||
}, [canvas, selectObjects]);
|
||||
|
||||
// 垂直均分
|
||||
// vertical equipartition
|
||||
const horizontalAverage = useCallback(() => {
|
||||
if (!canvas || selectObjects.length < 2) {
|
||||
return;
|
||||
@@ -186,7 +186,7 @@ export const useAlign = ({
|
||||
const spacing =
|
||||
(activeObject.height - totalHeight) / (selectObjects.length - 1);
|
||||
|
||||
let currentTop = -activeObject.height / 2; // 初始位置
|
||||
let currentTop = -activeObject.height / 2; // initial position
|
||||
|
||||
selectObjects
|
||||
.sort((a, b) => a.getBoundingRect().top - b.getBoundingRect().top)
|
||||
|
||||
@@ -40,7 +40,7 @@ export const useBackground = ({
|
||||
);
|
||||
}, [canvas]);
|
||||
|
||||
// 防抖的作用在于,form.schema.backgroundColor 的变化是异步的,setBackgroundColor 是同步的,两者可能会打架
|
||||
// The effect of stabilization is that the change of form.schema.backgroundColor is asynchronous, and the change of setBackgroundColor is synchronous, and the two may fight
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
setBackgroundColor(schema.backgroundColor as string);
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
|
||||
const ImagePlaceholder = `${getUploadCDNAsset('')}/workflow/fabric-canvas/img-placeholder.png`;
|
||||
|
||||
// 需要额外保存的属性
|
||||
// Properties that require additional saving
|
||||
export const saveProps = [
|
||||
'width',
|
||||
'height',
|
||||
@@ -46,17 +46,17 @@ export const saveProps = [
|
||||
'text',
|
||||
'backgroundColor',
|
||||
'padding',
|
||||
// 自定义参数
|
||||
// textBox 的真实高度
|
||||
// custom parameters
|
||||
// The true height of the textBox
|
||||
'customFixedHeight',
|
||||
// 元素 id
|
||||
// Element ID
|
||||
'customId',
|
||||
// 元素类型
|
||||
// element type
|
||||
'customType',
|
||||
// image 的适应模式
|
||||
// Adaptive mode of image
|
||||
'customFixedType',
|
||||
// // 由变量生成元素的 title
|
||||
// 引用关系
|
||||
// The title of the element generated by the variable
|
||||
// reference relationship
|
||||
'customVariableRefs',
|
||||
];
|
||||
|
||||
@@ -94,7 +94,7 @@ export const useCanvasChange = ({
|
||||
schema?.customVariableRefs ?? [],
|
||||
);
|
||||
|
||||
// 删除画布中不存在的引用关系
|
||||
// Delete reference relationships that do not exist in the canvas
|
||||
const resetCustomVariableRefs = useCallback(
|
||||
({ schema: _schema }: { schema: FabricSchema }) => {
|
||||
let newCustomVariableRefs = cacheCustomVariableRefs.current;
|
||||
@@ -110,12 +110,12 @@ export const useCanvasChange = ({
|
||||
[],
|
||||
);
|
||||
|
||||
// 监听画布变化
|
||||
// Monitor canvas changes
|
||||
useEffect(() => {
|
||||
if (canvas && onChangeLatest.current && isListen) {
|
||||
const _onChange = ({ isRemove }: { isRemove: boolean }) => {
|
||||
const json = canvas.toObject(saveProps) as FabricSchema;
|
||||
// 删除时,顺便删掉无效 ref
|
||||
// When deleting, delete the invalid ref by the way.
|
||||
if (isRemove) {
|
||||
json.customVariableRefs = resetCustomVariableRefs({
|
||||
schema: json,
|
||||
@@ -146,7 +146,7 @@ export const useCanvasChange = ({
|
||||
}, [canvas, isListen]);
|
||||
|
||||
/**
|
||||
* 生成带引用的新元素
|
||||
* Generate new elements with references
|
||||
*/
|
||||
const addRefObjectByVariable = useCallback(
|
||||
async (variable: InputVariable, element?: FabricObject) => {
|
||||
@@ -167,7 +167,7 @@ export const useCanvasChange = ({
|
||||
|
||||
let _element: FabricObject | undefined = element;
|
||||
|
||||
// 如果没有传入现有元素,则创建新元素
|
||||
// If no existing element is passed in, a new element is created
|
||||
if (!_element) {
|
||||
if (type === ViewVariableType.Image) {
|
||||
_element = await createElement({
|
||||
@@ -205,7 +205,7 @@ export const useCanvasChange = ({
|
||||
}
|
||||
|
||||
if (_element) {
|
||||
// 更新引用关系
|
||||
// Update reference relationship
|
||||
cacheCustomVariableRefs.current.push({
|
||||
variableId: id as string,
|
||||
objectId: (_element as FabricObjectWithCustomProps)
|
||||
@@ -213,7 +213,7 @@ export const useCanvasChange = ({
|
||||
variableName: name,
|
||||
});
|
||||
|
||||
// 添加到画布并激活
|
||||
// Add to canvas and activate
|
||||
canvas.add(_element);
|
||||
canvas.setActiveObject(_element);
|
||||
}
|
||||
@@ -222,10 +222,10 @@ export const useCanvasChange = ({
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新指定 objectId 的元素的引用关系
|
||||
* 如果 variable 为空,则删除引用
|
||||
* 如果 variable 不为空 && customVariableRefs 已存在对应关系,则更新引用
|
||||
* 如果 variable 不为空 && customVariableRefs 不存在对应关系,则新增引用
|
||||
* Update the reference relationship of the element specifying objectId
|
||||
* If the variable is empty, remove the reference
|
||||
* If variable is not empty & & customVariableRefs already has a correspondence, update the reference
|
||||
* If variable is not empty & & customVariableRefs does not have a correspondence, add a reference
|
||||
*
|
||||
*/
|
||||
const updateRefByObjectId = useCallback(
|
||||
@@ -239,12 +239,12 @@ export const useCanvasChange = ({
|
||||
const customVariableRefs = cacheCustomVariableRefs.current;
|
||||
const targetRef = customVariableRefs.find(d => d.objectId === objectId);
|
||||
let newCustomVariableRefs = [];
|
||||
// 如果 variable 为空,则删除引用
|
||||
// If the variable is empty, remove the reference
|
||||
if (!variable) {
|
||||
newCustomVariableRefs = customVariableRefs.filter(
|
||||
d => d.objectId !== objectId,
|
||||
);
|
||||
// 如果 variable 不为空 && customVariableRefs 不存在对应关系,则新增引用
|
||||
// If variable is not empty & & customVariableRefs does not have a correspondence, add a reference
|
||||
} else if (!targetRef) {
|
||||
newCustomVariableRefs = [
|
||||
...customVariableRefs,
|
||||
@@ -254,7 +254,7 @@ export const useCanvasChange = ({
|
||||
variableName: variable.name,
|
||||
},
|
||||
];
|
||||
// 如果 variable 不为空 && customVariableRefs 已存在对应关系,则更新引用
|
||||
// If variable is not empty & & customVariableRefs already has a correspondence, update the reference
|
||||
} else {
|
||||
newCustomVariableRefs = customVariableRefs.map(d => {
|
||||
if (d.objectId === objectId) {
|
||||
@@ -278,7 +278,7 @@ export const useCanvasChange = ({
|
||||
);
|
||||
|
||||
/**
|
||||
* variables 变化时,更新引用关系中的变量名
|
||||
* When variables change, update the variable names in the reference relationship
|
||||
*/
|
||||
useEffect(() => {
|
||||
const { customVariableRefs = [] } = schemaLatest.current ?? {};
|
||||
@@ -310,7 +310,7 @@ export const useCanvasChange = ({
|
||||
|
||||
const startListen = useCallback(() => {
|
||||
setIsListener(true);
|
||||
// redo undo 完成后,更新引用关系
|
||||
// After redo undo, update the reference relationship
|
||||
cacheCustomVariableRefs.current =
|
||||
schemaLatest.current?.customVariableRefs ?? [];
|
||||
}, []);
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useCommonOperation = ({ canvas }: { canvas?: Canvas }) => {
|
||||
direct: 'left' | 'right' | 'up' | 'down',
|
||||
offsetValue = 1,
|
||||
) => {
|
||||
// 这里不用额外考虑框选 case ,框选时会形成一个临时的组,对组做位移,会影响到组内的每一个元素
|
||||
// There is no need to consider the box selection case here. The box selection will form a temporary group, and the displacement of the group will affect every element in the group
|
||||
const activeSelection = canvas?.getActiveObject();
|
||||
|
||||
switch (direct) {
|
||||
@@ -59,12 +59,12 @@ export const useCommonOperation = ({ canvas }: { canvas?: Canvas }) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 键盘上下左右触发的图形位移,需要主动触发
|
||||
* The graphic displacement triggered by the keyboard up, down, left and right needs to be triggered actively.
|
||||
* 1. moving
|
||||
* if (activeSelection) canvas.fire('object:moving')
|
||||
* else activeSelection.fire('moving')
|
||||
*
|
||||
* 2. object:modified ,用来触发保存
|
||||
* 2. object: modified, used to trigger save
|
||||
*/
|
||||
const isActiveSelection = activeSelection?.isType('activeselection');
|
||||
const fabricObject = (
|
||||
@@ -116,7 +116,7 @@ export const useCommonOperation = ({ canvas }: { canvas?: Canvas }) => {
|
||||
canvas.sendObjectBackwards(obj);
|
||||
});
|
||||
}
|
||||
// 主动触发一次自定义事件:zIndex 变化
|
||||
// Actively trigger a custom event: zIndex change
|
||||
canvas.fire('object:modified-zIndex' as keyof CanvasEvents);
|
||||
canvas.requestRenderAll();
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { saveProps } from './use-canvas-change';
|
||||
|
||||
/**
|
||||
* 粘贴后的默认偏移
|
||||
* Default offset after pasting
|
||||
*/
|
||||
const staff = 16;
|
||||
export const useCopyPaste = ({
|
||||
@@ -59,11 +59,11 @@ export const useCopyPaste = ({
|
||||
element?: FabricObject,
|
||||
) => void;
|
||||
}) => {
|
||||
// ctrlCV 复制的元素
|
||||
// CtrlCV copied elements
|
||||
const copiedObject1 = useRef<FabricObject>();
|
||||
// ctrlD 复制的元素
|
||||
// CtrlD copied elements
|
||||
const copiedObject2 = useRef<FabricObject>();
|
||||
// dragCopy 拖拽复制的元素
|
||||
// dragCopy Drag and copy elements
|
||||
const copiedObject3 = useRef<FabricObject>();
|
||||
|
||||
const latestCustomVariableRefs = useLatest(customVariableRefs);
|
||||
@@ -88,9 +88,9 @@ export const useCopyPaste = ({
|
||||
});
|
||||
const latestIgnoreMousePosition = useLatest(ignoreMousePosition);
|
||||
|
||||
// 如果鼠标动了,就以鼠标位置为准。仅影响 CopyMode.CtrlD 的粘贴
|
||||
// If the mouse is moved, the mouse position shall prevail. Only the paste of CopyMode. CtrlD is affected.
|
||||
useEffect(() => {
|
||||
// 默认 left top 对应元素的左上角。需要实现元素中点对齐鼠标位置,因此做偏移
|
||||
// Default left top corresponds to the upper left corner of the element. You need to align the mouse position in the middle of the element, so offset
|
||||
setPosition({
|
||||
left: mousePosition.left - (copiedObject1.current?.width ?? 0) / 2,
|
||||
top: mousePosition.top - (copiedObject1.current?.height ?? 0) / 2,
|
||||
@@ -100,12 +100,12 @@ export const useCopyPaste = ({
|
||||
const handleElement = async (element: FabricObject): Promise<void> => {
|
||||
const oldObjectId = (element as FabricObjectWithCustomProps).customId;
|
||||
const newObjectId = nanoid();
|
||||
// 设置新的 id
|
||||
// Set a new ID.
|
||||
element.set({
|
||||
customId: newObjectId,
|
||||
});
|
||||
|
||||
// 走统一的创建元素逻辑
|
||||
// Take a unified approach to creating element logic
|
||||
const rs = await createElement({
|
||||
element: element as FabricObjectWithCustomProps,
|
||||
canvas,
|
||||
@@ -125,8 +125,8 @@ export const useCopyPaste = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* mode 分为三种:'ctrlCV' | 'ctrlD' | 'dragCopy'
|
||||
* 行为一致,区别就是三种行为的复制源隔离,互不影响
|
||||
* There are three modes: 'ctrlCV' | 'ctrlD' | 'dragCopy'
|
||||
* The behavior is consistent, the difference is that the replication sources of the three behaviors are isolated and do not affect each other
|
||||
*/
|
||||
const copy = useCallback(
|
||||
async (mode: CopyMode = CopyMode.CtrlCV) => {
|
||||
@@ -195,14 +195,14 @@ export const useCopyPaste = ({
|
||||
}
|
||||
const cloneObj = await copiedObject.clone(saveProps);
|
||||
|
||||
// ctrlCV 需要考虑鼠标位置,其他的不用
|
||||
// CtrlCV needs to consider the mouse position, others do not need to be
|
||||
const isIgnoreMousePosition = mode !== CopyMode.CtrlCV;
|
||||
|
||||
const { left, top } = isIgnoreMousePosition
|
||||
? latestIgnoreMousePosition.current
|
||||
: latestPosition.current;
|
||||
|
||||
// 计算下次粘贴位置,向 left top 各偏移 staff
|
||||
// Calculate the next paste position and offset the staff to the left top
|
||||
if (isIgnoreMousePosition) {
|
||||
setIgnoreMousePosition({
|
||||
left: left + staff,
|
||||
@@ -228,7 +228,7 @@ export const useCopyPaste = ({
|
||||
}),
|
||||
});
|
||||
|
||||
// 把需要复制的元素都拿出来,多选
|
||||
// Take out all the elements that need to be copied and select more
|
||||
const allPasteObjects: FabricObject[] = [];
|
||||
const originXY = {
|
||||
left: cloneObj.left + cloneObj.width / 2,
|
||||
@@ -242,19 +242,19 @@ export const useCopyPaste = ({
|
||||
});
|
||||
allPasteObjects.push(o);
|
||||
});
|
||||
// 把需要复制的元素都拿出来,单选
|
||||
// Take out all the elements that need to be copied, radio select
|
||||
} else {
|
||||
allPasteObjects.push(cloneObj);
|
||||
}
|
||||
|
||||
// 挨着调用 handleElement 处理元素
|
||||
// Calling handleElement next to handle elements
|
||||
await Promise.all(allPasteObjects.map(async o => handleElement(o)));
|
||||
|
||||
// 如果是多选,需要创新新的多选框,并激活
|
||||
// If it is multiple selection, you need to innovate a new multi-checkbox and activate it.
|
||||
let allPasteObjectsActiveSelection: ActiveSelection | undefined;
|
||||
if (cloneObj.isType('activeselection')) {
|
||||
allPasteObjectsActiveSelection = new ActiveSelection(
|
||||
// 很恶心,这里激活选框,并不会自动转换坐标,需要手动转一下
|
||||
// It's disgusting. Activating the check box here will not automatically convert the coordinates. You need to turn it manually.
|
||||
allPasteObjects.map(o => {
|
||||
o.set({
|
||||
left: o.left - originXY.left,
|
||||
@@ -279,23 +279,23 @@ export const useCopyPaste = ({
|
||||
const keyCodes = ['Alt'];
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (keyCodes.includes(e.key)) {
|
||||
e.preventDefault(); // 阻止默认行为
|
||||
isAltPressing = true; // 标记 alt 已按下
|
||||
e.preventDefault(); // Block default behavior
|
||||
isAltPressing = true; // Mark alt pressed
|
||||
}
|
||||
};
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (keyCodes.includes(e.key)) {
|
||||
e.preventDefault(); // 阻止默认行为
|
||||
isAltPressing = false; // 标记 alt 已松开
|
||||
e.preventDefault(); // Block default behavior
|
||||
isAltPressing = false; // The alt tag has been released
|
||||
}
|
||||
};
|
||||
|
||||
const onWindowBlur = () => {
|
||||
isAltPressing = false; // 标记 alt 已松开
|
||||
isAltPressing = false; // The alt tag has been released
|
||||
};
|
||||
|
||||
const onContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault(); // 阻止默认行为
|
||||
e.preventDefault(); // Block default behavior
|
||||
};
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
document.addEventListener('keyup', onKeyUp);
|
||||
@@ -307,7 +307,7 @@ export const useCopyPaste = ({
|
||||
let originalPos = { left: 0, top: 0 };
|
||||
|
||||
const disposers = [
|
||||
// 复制时机:按下 alt 键 & 鼠标按下激活元素
|
||||
// Copy timing: Alt key & mouse down to activate element
|
||||
canvas?.on('mouse:down', async e => {
|
||||
if (isAltPressing) {
|
||||
if (!latestCouldAddNewObject.current) {
|
||||
@@ -320,7 +320,7 @@ export const useCopyPaste = ({
|
||||
|
||||
isDragCopying = true;
|
||||
const activeObject = canvas.getActiveObject();
|
||||
// 创建元素副本期间,锁定 xy 方向的移动
|
||||
// Lock movement in the xy direction during element copy creation
|
||||
activeObject?.set({
|
||||
lockMovementX: true,
|
||||
lockMovementY: true,
|
||||
@@ -331,7 +331,7 @@ export const useCopyPaste = ({
|
||||
mode: CopyMode.DragCV,
|
||||
});
|
||||
|
||||
// 记录对象的原始位置,实现 shift 垂直、水平移动
|
||||
// Record the original position of the object and realize vertical and horizontal shift
|
||||
originalPos = {
|
||||
left: pasteObj?.left ?? 0,
|
||||
top: pasteObj?.top ?? 0,
|
||||
@@ -345,26 +345,26 @@ export const useCopyPaste = ({
|
||||
}
|
||||
}),
|
||||
|
||||
// 因为 copy 是异步的,所以这里会有一些延迟(大图片比较明显),没啥好办法
|
||||
// Because the copy is asynchronous, there will be some delay here (the big picture is more obvious), there is no good way
|
||||
canvas?.on('mouse:move', event => {
|
||||
if (isAltPressing && isDragCopying && pasteObj) {
|
||||
const pointer = canvas.getScenePoint(event.e);
|
||||
|
||||
// 检查是否按下了Shift键
|
||||
// Check if the Shift key is pressed
|
||||
if (event.e.shiftKey) {
|
||||
// 计算从开始移动以来的水平和垂直距离
|
||||
// Calculate the horizontal and vertical distance since the start of the movement
|
||||
const distanceX = pointer.x - originalPos.left;
|
||||
const distanceY = pointer.y - originalPos.top;
|
||||
|
||||
// 根据移动距离的绝对值判断是水平移动还是垂直移动
|
||||
// Determine whether to move horizontally or vertically according to the absolute value of the moving distance
|
||||
if (Math.abs(distanceX) > Math.abs(distanceY)) {
|
||||
// 水平移动:保持垂直位置不变
|
||||
// Horizontal movement: maintain the same vertical position
|
||||
pasteObj?.set({
|
||||
left: pointer.x - (pasteObj?.width ?? 0) / 2,
|
||||
top: originalPos.top,
|
||||
});
|
||||
} else {
|
||||
// 垂直移动:保持水平位置不变
|
||||
// Vertical movement: maintain the same horizontal position
|
||||
pasteObj?.set({
|
||||
left: originalPos.left,
|
||||
top: pointer.y - (pasteObj?.height ?? 0) / 2,
|
||||
@@ -386,7 +386,7 @@ export const useCopyPaste = ({
|
||||
canvas?.on('mouse:up', () => {
|
||||
isDragCopying = false;
|
||||
pasteObj = undefined;
|
||||
// 释放拖拽复制对象,避免对下次拖拽(按着 alt 不松手)造成干扰
|
||||
// Release the drag and drop to copy the object to avoid disturbing the next drag (press alt without letting go)
|
||||
copiedObject3.current = undefined;
|
||||
}),
|
||||
];
|
||||
@@ -399,7 +399,7 @@ export const useCopyPaste = ({
|
||||
};
|
||||
}, [canvas, copy, paste]);
|
||||
|
||||
// 拖拽复制
|
||||
// Drag and drop to copy
|
||||
return {
|
||||
copy,
|
||||
paste,
|
||||
|
||||
@@ -118,7 +118,7 @@ const modeElementMap: Partial<
|
||||
y2: dy + (element as Line).y1,
|
||||
});
|
||||
|
||||
// 创建直线时的终点位置修改,需要主动 fire 影响控制点的显示
|
||||
// The end position modification when creating a straight line requires active fire to affect the display of the control point
|
||||
element.fire('start-end:modified' as keyof ObjectEvents);
|
||||
},
|
||||
},
|
||||
@@ -193,7 +193,7 @@ export const useDragAdd = ({
|
||||
moved: false,
|
||||
};
|
||||
|
||||
// 隐藏控制点,否则 onmouseup 可能被控制点截胡
|
||||
// Hide the control point, otherwise onmouseup may be truncated by the control point
|
||||
element.set('hasControls', false);
|
||||
}
|
||||
});
|
||||
@@ -212,7 +212,7 @@ export const useDragAdd = ({
|
||||
dy,
|
||||
});
|
||||
|
||||
// 修正元素坐标信息
|
||||
// Correct element coordinate information
|
||||
element.setCoords();
|
||||
|
||||
newElement.current.moved = true;
|
||||
@@ -233,7 +233,7 @@ export const useDragAdd = ({
|
||||
|
||||
onShapeAdded?.({ element: element as FabricObjectWithCustomProps });
|
||||
|
||||
// 恢复控制点
|
||||
// Restore Control Point
|
||||
element.set('hasControls', true);
|
||||
newElement.current = undefined;
|
||||
canvas.requestRenderAll();
|
||||
|
||||
@@ -78,8 +78,8 @@ export const useFabricEditor = ({
|
||||
}) => {
|
||||
const schema: FabricSchema = useMemo(() => {
|
||||
/**
|
||||
* 兼容历史数据
|
||||
* 删除时机,见 apps/fabric-canvas-node-render/utils/replace-ref-value.ts 注释
|
||||
* Compatible with historical data
|
||||
* Delete timing, see apps/fabric-canvas-node-render/utils/replace-ref-value.ts comment
|
||||
*/
|
||||
if (
|
||||
!_schema?.customVariableRefs &&
|
||||
@@ -106,7 +106,7 @@ export const useFabricEditor = ({
|
||||
const objectLength = useMemo(() => schema.objects.length, [schema]);
|
||||
|
||||
/**
|
||||
* 最大可添加元素数量限制
|
||||
* Maximum number of elements that can be added
|
||||
*/
|
||||
const MAX_OBJECT_LENGTH = 50;
|
||||
const couldAddNewObject = useMemo(
|
||||
@@ -121,7 +121,7 @@ export const useFabricEditor = ({
|
||||
height: schema.height,
|
||||
});
|
||||
|
||||
// 初始化 fabric canvas
|
||||
// Initialize fabric canvas
|
||||
const { canvas, loadFromJSON } = useInitCanvas({
|
||||
startInit,
|
||||
ref: ref.current,
|
||||
|
||||
@@ -27,18 +27,18 @@ export const useFreePencil = ({ canvas }: { canvas?: Canvas }) => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
// 启用自由绘图模式
|
||||
// Enable free drawing mode
|
||||
canvas.isDrawingMode = true;
|
||||
|
||||
// 设置 PencilBrush 为当前的画笔
|
||||
// Set PencilBrush to the current brush
|
||||
canvas.freeDrawingBrush = new PencilBrush(canvas);
|
||||
|
||||
// 设置画笔的一些属性
|
||||
canvas.freeDrawingBrush.color = defaultProps[Mode.PENCIL].stroke as string; // 画笔颜色
|
||||
// Set some properties of the brush
|
||||
canvas.freeDrawingBrush.color = defaultProps[Mode.PENCIL].stroke as string; // Brush color
|
||||
canvas.freeDrawingBrush.width = defaultProps[Mode.PENCIL]
|
||||
.strokeWidth as number; // 画笔宽度
|
||||
.strokeWidth as number; // Brush Width
|
||||
|
||||
// 你也可以设置其他属性,比如 opacity (不透明度)
|
||||
// You can also set other properties, such as opacity.
|
||||
// canvas.freeDrawingBrush.opacity = 0.6;
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export const useFreePencil = ({ canvas }: { canvas?: Canvas }) => {
|
||||
element: path,
|
||||
});
|
||||
|
||||
// 得触发一次 object:added ,以触发 onSave,否则 schema 里并不会包含 commonOptions
|
||||
// You must fire object: added once to trigger onSave, otherwise the schema will not contain commonOptions.
|
||||
canvas.fire('object:modified');
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ export const useFreePencil = ({ canvas }: { canvas?: Canvas }) => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
// 禁用自由绘图模式
|
||||
// Disable free drawing mode
|
||||
canvas.isDrawingMode = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,20 +15,20 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 这是个半成品,暂时不做了,后面再考虑
|
||||
* 1. 成组后要支持下钻继续选择
|
||||
* 实现思路:
|
||||
* a.双击解组,并记录组关系;
|
||||
* b.下钻选择子组,继续解组,并记录组关系;
|
||||
* c.点击画布(没有任何选中元素时),恢复组(要注意 z-index)。
|
||||
* This is a work in progress, I won't do it for the time being, I'll think about it later.
|
||||
* 1. After forming a group, support drilling down and continue to select.
|
||||
* Realization idea:
|
||||
* A. Double-click to ungroup and record the group relationship;
|
||||
* B. Drill down to select subgroups, continue to ungroup, and record group relationships;
|
||||
* Click on the canvas (when no elements are selected) and restore the group (note the z-index).
|
||||
*
|
||||
* 2. 复制粘贴组时,需要排除掉引用元素
|
||||
* 3. 删除组是,也需要排除引用元素
|
||||
* 4. 因为组的引入,打破了所有元素都是拍平的原则,要注意这个改动的破坏性。
|
||||
* 2. When copying and pasting groups, you need to exclude reference elements
|
||||
* 3. Delete group Yes, also need to exclude reference elements
|
||||
* 4. Due to the introduction of the group, the principle that all elements are flattened is broken. Be aware of the destructive nature of this change.
|
||||
* eg:
|
||||
* a. 获取所有元素
|
||||
* b. 元素的位置计算是由每层父元素叠加来的
|
||||
* c. 服务端渲染:遍历找所有的图片元素。完成图片下载后恢复组
|
||||
* A. Get all elements
|
||||
* B. The position calculation of the element is superimposed by each layer of parent elements
|
||||
* C.server-side rendering: Iterate to find all image elements. Restore group after finishing image download
|
||||
*/
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -47,7 +47,7 @@ export const useGroup = ({ canvas }: { canvas?: Canvas }) => {
|
||||
const group = useCallback(async () => {
|
||||
const activeObject = canvas?.getActiveObject();
|
||||
const objects = (activeObject as ActiveSelection)?.getObjects();
|
||||
// 选中了多个元素时,才可以 group
|
||||
// You can only group when multiple elements are selected.
|
||||
if ((objects?.length ?? 0) > 1) {
|
||||
const _group = await createElement({
|
||||
mode: Mode.GROUP,
|
||||
@@ -69,7 +69,7 @@ export const useGroup = ({ canvas }: { canvas?: Canvas }) => {
|
||||
const unGroup = useCallback(async () => {
|
||||
const activeObject = canvas?.getActiveObject();
|
||||
|
||||
// 仅选中了一个 group 元素时,才可以 ungroup
|
||||
// Ungroup can only be done if a group element is selected
|
||||
if (isGroupElement(activeObject)) {
|
||||
const _group = activeObject as Group;
|
||||
const objects = _group.getObjects();
|
||||
|
||||
@@ -46,7 +46,7 @@ export const useInitCanvas = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 按比例给个初始化高度,随后会通过 resize 修正为真正的宽高
|
||||
// Give an initial height proportionally, and then correct it to the true width and height by resizing.
|
||||
const _canvas = new Canvas(ref, {
|
||||
width: schema.width * scale,
|
||||
height: schema.height * scale,
|
||||
@@ -86,7 +86,7 @@ export const useInitCanvas = ({
|
||||
await fabricCanvas?.loadFromJSON(
|
||||
JSON.stringify(_schema),
|
||||
async (elementSchema, element) => {
|
||||
// 每个元素被加载后的回调
|
||||
// Callback after each element is loaded
|
||||
await setElementAfterLoad({
|
||||
element: element as FabricObject,
|
||||
options: { readonly },
|
||||
|
||||
@@ -49,7 +49,7 @@ const getElementTitlePosition = ({
|
||||
let left = targetElementTopLeft.x * scale;
|
||||
let top = targetElementTopLeft.y * scale;
|
||||
|
||||
// 图片特化,需要考虑比例拉伸,位置限定在 group 范围内
|
||||
// Image specialization, proportional stretching needs to be considered, and the position is limited to the group range
|
||||
if (isImg) {
|
||||
const strokeWidth =
|
||||
(element as unknown as Group).getObjects()?.[1]?.strokeWidth ?? 0;
|
||||
@@ -84,7 +84,7 @@ export const usePosition = ({
|
||||
const [screenPositions, setScreenPositions] = useState<IRefPosition[]>([]);
|
||||
|
||||
const _setPositions = useCallback(() => {
|
||||
// 为什么要 setTimeout?批量时,需要延迟才能拿到正确的坐标
|
||||
// Why setTimeout? When batching, you need to delay to get the correct coordinates.
|
||||
setTimeout(() => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
|
||||
@@ -60,7 +60,7 @@ export const useRedoUndo = ({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 保存多少步
|
||||
// How many steps to save
|
||||
const max = 20;
|
||||
const end = stepLatest.current + 1;
|
||||
const start = Math.max(0, end - max);
|
||||
@@ -81,25 +81,25 @@ export const useRedoUndo = ({
|
||||
const newStep = stepLatest.current - 1;
|
||||
const _schema = historyLatest.current[newStep];
|
||||
|
||||
// 开始执行 undo
|
||||
// Start executing undo
|
||||
setRedoUndoing(true);
|
||||
|
||||
// 停止监听画布变化
|
||||
// Stop listening for canvas changes
|
||||
stopListen();
|
||||
|
||||
// 保存 schema
|
||||
// Save schema
|
||||
onChange?.(_schema);
|
||||
|
||||
// 画布重新加载
|
||||
// Canvas reload
|
||||
await loadFromJSON?.(_schema);
|
||||
|
||||
// 同步 step
|
||||
// Synchronization steps
|
||||
setStep(newStep);
|
||||
|
||||
// 恢复画布监听
|
||||
// Restore canvas monitor
|
||||
startListen();
|
||||
|
||||
// undo 执行完成
|
||||
// Undo execution complete
|
||||
setRedoUndoing(false);
|
||||
}, [loadFromJSON]);
|
||||
|
||||
@@ -115,25 +115,25 @@ export const useRedoUndo = ({
|
||||
const newStep = stepLatest.current + 1;
|
||||
const _schema = historyLatest.current[newStep];
|
||||
|
||||
// 开始执行 redo
|
||||
// Start redo
|
||||
setRedoUndoing(true);
|
||||
|
||||
// 停止监听画布变化
|
||||
// Stop listening for canvas changes
|
||||
stopListen();
|
||||
|
||||
// 保存 schema
|
||||
// Save schema
|
||||
onChange?.(_schema);
|
||||
|
||||
// 画布重新加载
|
||||
// Canvas reload
|
||||
await loadFromJSON?.(_schema);
|
||||
|
||||
// 同步 step
|
||||
// Synchronization steps
|
||||
setStep(newStep);
|
||||
|
||||
// 恢复画布监听
|
||||
// Restore canvas monitor
|
||||
startListen();
|
||||
|
||||
// redo 执行完成
|
||||
// Redo execution completed
|
||||
setRedoUndoing(false);
|
||||
}, [loadFromJSON]);
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ import { setElementAfterLoad } from '../utils';
|
||||
import { type FabricSchema } from '../typings';
|
||||
|
||||
/**
|
||||
* 监听 schema 变化,reload canvas
|
||||
* 仅只读态需要
|
||||
* Listen for schema changes, reload canvas
|
||||
* Read-only state required
|
||||
*/
|
||||
export const useSchemaChange = ({
|
||||
canvas,
|
||||
@@ -39,7 +39,7 @@ export const useSchemaChange = ({
|
||||
setLoading(true);
|
||||
canvas
|
||||
?.loadFromJSON(JSON.stringify(schema), (elementSchema, element) => {
|
||||
// 这里是 schema 中每个元素被加载后的回调
|
||||
// Here is the callback for each element in the schema after it has been loaded
|
||||
setElementAfterLoad({
|
||||
element: element as FabricObject,
|
||||
options: { readonly },
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface Options<T> {
|
||||
const storage: Record<string, unknown> = {};
|
||||
|
||||
/**
|
||||
* 持久化保存到内存
|
||||
* Persistent save to memory
|
||||
*/
|
||||
export function useStorageState<T>(key: string, options: Options<T> = {}) {
|
||||
function getStoredValue() {
|
||||
|
||||
@@ -40,7 +40,7 @@ export const useViewport = ({
|
||||
return;
|
||||
}
|
||||
const _vpt: TMat2D = [...vpt];
|
||||
// 限制 viewport 移动区域:不能移出画布
|
||||
// Limit viewport movement area: Cannot move out of canvas
|
||||
if (_vpt[4] > 0) {
|
||||
_vpt[4] = 0;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 这个文件仅实现 setImageFixed 一个函数就好
|
||||
* nodejs 图片渲染同样需要计算位置。要在 packages/workflow/nodejs/fabric-render 实现一份功能完全一致的 js 版
|
||||
* This file only implements one function setImageFixed
|
||||
* Nodejs image rendering also needs to calculate the position. To implement a fully functional js version in packages/workflow/nodejs/fabricate-render
|
||||
*/
|
||||
import {
|
||||
type FabricImage,
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { ImageFixedType, type FabricObjectWithCustomProps } from './typings';
|
||||
|
||||
/**
|
||||
* 调整 img 位置
|
||||
* Adjust img position
|
||||
*/
|
||||
export const setImageFixed = ({ element }: { element: FabricObject }) => {
|
||||
const { width, height } = element;
|
||||
@@ -38,7 +38,7 @@ export const setImageFixed = ({ element }: { element: FabricObject }) => {
|
||||
const borderRect = (element as Group).getObjects()[1] as Rect;
|
||||
const { strokeWidth = 0 } = borderRect;
|
||||
|
||||
// 填充/拉伸时,框适配 group 大小即可
|
||||
// When filling/stretching, the box fits the group size
|
||||
const borderRectWidth = width - strokeWidth;
|
||||
const borderRectHeight = height - strokeWidth;
|
||||
borderRect.set({
|
||||
@@ -51,11 +51,11 @@ export const setImageFixed = ({ element }: { element: FabricObject }) => {
|
||||
const { width: originWidth, height: originHeight } = img.getOriginalSize();
|
||||
|
||||
/**
|
||||
* 为什么 +1?
|
||||
* 经过计算后,存储位数有限,不管是 scaleX/Y width/height top/left,都会丢失一点点精度
|
||||
* 这点精度反馈到图片上,就是图片与边框有一点点间隙
|
||||
* 这里 +1 让图片显示的稍微大一点,弥补精度带来的间隙。
|
||||
* 弊端:边框会覆盖一点点图片(覆盖多少看缩放比),用户基本无感
|
||||
* Why + 1?
|
||||
* After calculation, the number of storage bits is limited, whether it is scaleX/Y width/height top/left, a little precision will be lost
|
||||
* This accuracy is fed back to the picture, that is, there is a little gap between the picture and the border.
|
||||
* Here + 1 makes the image appear slightly larger to make up for the gap in accuracy.
|
||||
* Disadvantages: The border will cover a little bit of the picture (how much to cover depends on the zoom ratio), and the user basically has no feeling
|
||||
*/
|
||||
const realScaleX = (width - strokeWidth * 2 + 1) / originWidth;
|
||||
const realScaleY = (height - strokeWidth * 2 + 1) / originHeight;
|
||||
@@ -74,7 +74,7 @@ export const setImageFixed = ({ element }: { element: FabricObject }) => {
|
||||
const imgLeft = -(originWidth * scaleX) / 2;
|
||||
const imgTop = -(originHeight * scaleY) / 2;
|
||||
|
||||
// 自适应时需要对图片描边
|
||||
// When adapting, you need to stroke the picture
|
||||
if (customFixedType === ImageFixedType.AUTO) {
|
||||
borderRect.set({
|
||||
width: Math.min(borderRectWidth, originWidth * scaleX + strokeWidth),
|
||||
|
||||
@@ -212,8 +212,8 @@ export const fontSvg = [
|
||||
'47-Bolderslant.svg',
|
||||
'48-PinyonScript.svg',
|
||||
'49-ZYLAADeepblue.svg',
|
||||
// 站酷庆科黄油体 加载总是失败 ,找不到原因,暂时屏蔽
|
||||
// '5-站酷庆科黄油体.svg',
|
||||
// Station Kuqingke butter body, the loading always fails, the reason cannot be found, and it is temporarily blocked.
|
||||
// '5-Station Kuqingke Butter Body.svg ',
|
||||
'50-ZYLAASylph.svg',
|
||||
'51-ZYENAFetching.svg',
|
||||
'52-ZYLAACosy.svg',
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface FabricObjectSchema extends CustomFabricProps {
|
||||
lineHeight?: number;
|
||||
text?: string;
|
||||
/**
|
||||
* 图片链接
|
||||
* image link
|
||||
*/
|
||||
src?: string;
|
||||
objects?: FabricObjectSchema[];
|
||||
@@ -50,9 +50,9 @@ export interface FabricSchema extends FabricObjectSchema {
|
||||
}
|
||||
|
||||
/**
|
||||
* 为什么不用 FabricObject.type?
|
||||
* 因为 fabricSchema.type 跟 fabricObject.type 对不上
|
||||
* eg: Textbox 在 schema 里是 textbox,实例化后是 Textbox
|
||||
* Why not use FabricObject.type?
|
||||
* Because fabricSchema.type does not match fabricObject.type
|
||||
* Eg: Textbox is a textbox in the schema, and a Textbox after instantiation
|
||||
*/
|
||||
export enum Mode {
|
||||
INLINE_TEXT = 'inline_text',
|
||||
@@ -67,7 +67,7 @@ export enum Mode {
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充和描边
|
||||
* Fill and Stroke
|
||||
*/
|
||||
export enum ColorMode {
|
||||
FILL = 'fill',
|
||||
@@ -75,7 +75,7 @@ export enum ColorMode {
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本对齐方式
|
||||
* text alignment
|
||||
*/
|
||||
export enum TextAlign {
|
||||
LEFT = 'left',
|
||||
@@ -84,7 +84,7 @@ export enum TextAlign {
|
||||
JUSTIFY = 'justify',
|
||||
}
|
||||
/**
|
||||
* 图片填充方式
|
||||
* Image filling method
|
||||
*/
|
||||
export enum ImageFixedType {
|
||||
AUTO = 'auto',
|
||||
@@ -97,7 +97,7 @@ export interface CustomFabricProps {
|
||||
customId: string;
|
||||
customFixedHeight?: number;
|
||||
customFixedType?: ImageFixedType;
|
||||
/** @deprecated 兼容历史,不可新增消费 */
|
||||
/** @deprecated compatible history, no new consumption */
|
||||
customVariableName?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { type FabricObjectSchema } from './share/typings';
|
||||
export interface FormMetaItem {
|
||||
name?: string;
|
||||
title?: string;
|
||||
// 临时存储,不保存到后端
|
||||
// Temporary storage, not saved to the backend
|
||||
cacheSave?: boolean;
|
||||
visible?: (formValue: Partial<FabricObjectSchema>) => boolean;
|
||||
setter:
|
||||
|
||||
@@ -82,7 +82,7 @@ type GetControls = (props?: {
|
||||
needResetScaleAndSnap?: boolean;
|
||||
}) => Control;
|
||||
/**
|
||||
* 直线起点控制点
|
||||
* Straight Start Control Point
|
||||
*/
|
||||
export const getLineStartControl: GetControls = (props = {}) => {
|
||||
const { x, y, callback } = props;
|
||||
@@ -102,12 +102,12 @@ export const getLineStartControl: GetControls = (props = {}) => {
|
||||
callback?.({ element: transformData.target });
|
||||
return true;
|
||||
},
|
||||
actionName: 'startControl', // 控制点的名称
|
||||
actionName: 'startControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 直线终点控制点
|
||||
* straight endpoint control point
|
||||
*/
|
||||
export const getLineEndControl: GetControls = (props = {}) => {
|
||||
const { x, y, callback } = props;
|
||||
@@ -127,7 +127,7 @@ export const getLineEndControl: GetControls = (props = {}) => {
|
||||
callback?.({ element: transformData.target });
|
||||
return true;
|
||||
},
|
||||
actionName: 'endControl', // 控制点的名称
|
||||
actionName: 'endControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
@@ -191,7 +191,7 @@ const scaleToSize = (
|
||||
transformData.target.set({
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
// textBox 特化属性
|
||||
// textBox specialization property
|
||||
customFixedHeight: targetHeight,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
@@ -200,11 +200,11 @@ const scaleToSize = (
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 直接问 GPT:
|
||||
一个矩形,宽度 w ,高度 h
|
||||
将矩形顺时针旋转角度 a,左上角坐标为 x1 y1
|
||||
拉伸矩形左上角,使矩形右下角保持不变,宽度增加到 w1,高度增加到 h1
|
||||
求左上角坐标
|
||||
* Ask the GPT directly:
|
||||
A rectangle, width w, height h
|
||||
Rotate the rectangle clockwise by angle a with the upper left coordinate x1 y1.
|
||||
Stretch the upper left corner of the rectangle so that the lower right corner of the rectangle remains unchanged, increasing the width to w1 and the height to h1.
|
||||
Find the coordinates of the upper left corner
|
||||
*/
|
||||
const calcLeftTopByTopLeft: LeftTopCalcFn = ({
|
||||
angle,
|
||||
@@ -233,12 +233,12 @@ const calcLeftTopByTopLeft: LeftTopCalcFn = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 直接问 GPT:
|
||||
一个矩形,宽度 w ,高度 h
|
||||
将矩形顺时针旋转角度 a , 左上角坐标为 x1 y1
|
||||
拉伸矩形右上角,使矩形左下角保持不变,宽度增加到 w1,高度增加到 h1
|
||||
求左上角坐标
|
||||
(GPT 给的答案不准确,需要稍微理解下,修改加减)
|
||||
* Ask the GPT directly:
|
||||
A rectangle, width w, height h
|
||||
Rotate the rectangle clockwise by angle a, with the upper left coordinate x1 y1.
|
||||
Stretch the upper right corner of the rectangle so that the lower left corner of the rectangle remains unchanged, increasing the width to w1 and the height to h1.
|
||||
Find the coordinates of the upper left corner
|
||||
(The answer given by GPT is inaccurate, you need to understand it a little, modify it, add and subtract)
|
||||
*/
|
||||
const calcLeftTopByTopRight: LeftTopCalcFn = ({
|
||||
angle,
|
||||
@@ -255,13 +255,13 @@ const calcLeftTopByTopRight: LeftTopCalcFn = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 直接问 GPT:
|
||||
一个矩形,宽度 w ,高度 h
|
||||
将矩形顺时针旋转角度 a , 左上角坐标为 x1 y1
|
||||
拉伸矩形左下角,使矩形右上角保持不变,宽度增加到 w1,高度增加到 h1
|
||||
求左上角坐标
|
||||
* Ask the GPT directly:
|
||||
A rectangle, width w, height h
|
||||
Rotate the rectangle clockwise by angle a, with the upper left coordinate x1 y1.
|
||||
Stretch the lower left corner of the rectangle so that the upper right corner of the rectangle remains unchanged, increasing the width to w1 and the height to h1.
|
||||
Find the coordinates of the upper left corner
|
||||
|
||||
GPT 给的答案不准确,这个比较麻烦,所以写出了每一步的推导过程
|
||||
The answer given by GPT is inaccurate, which is more troublesome, so I wrote out the derivation process of each step
|
||||
*/
|
||||
const calcLeftTopByBottomLeft: LeftTopCalcFn = ({
|
||||
angle,
|
||||
@@ -271,18 +271,18 @@ const calcLeftTopByBottomLeft: LeftTopCalcFn = ({
|
||||
newWidth,
|
||||
newHeight,
|
||||
}) => {
|
||||
// 将角度转换为弧度
|
||||
// Convert angle to radians
|
||||
const aRad = (angle * Math.PI) / 180;
|
||||
|
||||
// 计算旋转后的右上角坐标
|
||||
// Calculate the coordinates of the upper right corner after rotation
|
||||
const x2 = originLeft + originWidth * Math.cos(aRad);
|
||||
const y2 = originTop + originWidth * Math.sin(aRad);
|
||||
|
||||
// 计算拉伸后的左下角坐标
|
||||
// Calculate the lower left corner coordinates after stretching
|
||||
const x3 = x2 - newHeight * Math.sin(aRad) - newWidth * Math.cos(aRad);
|
||||
const y3 = y2 + newHeight * Math.cos(aRad) - newWidth * Math.sin(aRad);
|
||||
|
||||
// 计算拉伸后的左上角坐标
|
||||
// Calculate the coordinates of the upper left corner after stretching
|
||||
const x1New = x3 + newHeight * Math.sin(aRad);
|
||||
const y1New = y3 - newHeight * Math.cos(aRad);
|
||||
return {
|
||||
@@ -344,7 +344,7 @@ const _actionHandler = ({
|
||||
leftTopCalcFn?: LeftTopCalcFn;
|
||||
}) => {
|
||||
const rs = fn(
|
||||
// 如果使用吸附则禁用默认缩放;否则取反
|
||||
// Disable default scaling if adsorption is used; otherwise reverse
|
||||
{ ...e, shiftKey: needResetScaleAndSnap ? true : !e.shiftKey },
|
||||
transformData,
|
||||
x,
|
||||
@@ -363,7 +363,7 @@ const _actionHandler = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 上左
|
||||
* upper left
|
||||
*/
|
||||
export const getResizeTLControl: GetControls = (props = {}) => {
|
||||
const { callback, needResetScaleAndSnap = true } = props;
|
||||
@@ -386,11 +386,11 @@ export const getResizeTLControl: GetControls = (props = {}) => {
|
||||
return rs;
|
||||
},
|
||||
mouseDownHandler: _mouseDownHandler,
|
||||
actionName: 'resizeTLControl', // 控制点的名称
|
||||
actionName: 'resizeTLControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 上中
|
||||
* upper middle school
|
||||
*/
|
||||
export const getResizeMTControl: GetControls = (props = {}) => {
|
||||
const { callback, needResetScaleAndSnap = true } = props;
|
||||
@@ -413,12 +413,12 @@ export const getResizeMTControl: GetControls = (props = {}) => {
|
||||
return rs;
|
||||
},
|
||||
mouseDownHandler: _mouseDownHandler,
|
||||
actionName: 'resizeMTControl', // 控制点的名称
|
||||
actionName: 'resizeMTControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 上右
|
||||
* upper right
|
||||
*/
|
||||
export const getResizeTRControl: GetControls = (props = {}) => {
|
||||
const { callback, needResetScaleAndSnap } = props;
|
||||
@@ -441,12 +441,12 @@ export const getResizeTRControl: GetControls = (props = {}) => {
|
||||
return rs;
|
||||
},
|
||||
mouseDownHandler: _mouseDownHandler,
|
||||
actionName: 'resizeTRControl', // 控制点的名称
|
||||
actionName: 'resizeTRControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 中左
|
||||
* center left
|
||||
*/
|
||||
export const getResizeMLControl: GetControls = (props = {}) => {
|
||||
const { callback, needResetScaleAndSnap } = props;
|
||||
@@ -470,12 +470,12 @@ export const getResizeMLControl: GetControls = (props = {}) => {
|
||||
return rs;
|
||||
},
|
||||
mouseDownHandler: _mouseDownHandler,
|
||||
actionName: 'resizeMLControl', // 控制点的名称
|
||||
actionName: 'resizeMLControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 中右
|
||||
* center right
|
||||
*/
|
||||
export const getResizeMRControl: GetControls = (props = {}) => {
|
||||
const { callback, needResetScaleAndSnap } = props;
|
||||
@@ -498,12 +498,12 @@ export const getResizeMRControl: GetControls = (props = {}) => {
|
||||
return rs;
|
||||
},
|
||||
mouseDownHandler: _mouseDownHandler,
|
||||
actionName: 'resizeMRControl', // 控制点的名称
|
||||
actionName: 'resizeMRControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 下左
|
||||
* Lower left
|
||||
*/
|
||||
export const getResizeBLControl: GetControls = (props = {}) => {
|
||||
const { callback, needResetScaleAndSnap } = props;
|
||||
@@ -527,12 +527,12 @@ export const getResizeBLControl: GetControls = (props = {}) => {
|
||||
return rs;
|
||||
},
|
||||
mouseDownHandler: _mouseDownHandler,
|
||||
actionName: 'resizeBLControl', // 控制点的名称
|
||||
actionName: 'resizeBLControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 下中
|
||||
* lower middle
|
||||
*/
|
||||
export const getResizeMBControl: GetControls = (props = {}) => {
|
||||
const { callback, needResetScaleAndSnap } = props;
|
||||
@@ -555,12 +555,12 @@ export const getResizeMBControl: GetControls = (props = {}) => {
|
||||
return rs;
|
||||
},
|
||||
mouseDownHandler: _mouseDownHandler,
|
||||
actionName: 'resizeMBControl', // 控制点的名称
|
||||
actionName: 'resizeMBControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 下右
|
||||
* Lower right
|
||||
*/
|
||||
export const getResizeBRControl: GetControls = (props = {}) => {
|
||||
const { callback, needResetScaleAndSnap } = props;
|
||||
@@ -583,7 +583,7 @@ export const getResizeBRControl: GetControls = (props = {}) => {
|
||||
return rs;
|
||||
},
|
||||
mouseDownHandler: _mouseDownHandler,
|
||||
actionName: 'resizeBRControl', // 控制点的名称
|
||||
actionName: 'resizeBRControl', // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
@@ -605,7 +605,7 @@ const _getRotateControl =
|
||||
}): GetControls =>
|
||||
(props = {}) => {
|
||||
const { callback } = props;
|
||||
// 这个大小,取决于 resize 控制点的大小
|
||||
// This size depends on the size of the resize control point
|
||||
const offset = 12;
|
||||
return new Control({
|
||||
x,
|
||||
@@ -614,15 +614,15 @@ const _getRotateControl =
|
||||
sizeY: 20,
|
||||
offsetY: offsetY * offset,
|
||||
offsetX: offsetX * offset,
|
||||
// 覆盖旋转控制点渲染,预期不显示,所以啥都没写
|
||||
// Override the rotation control point rendering, it is not expected to be displayed, so nothing is written.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
render: () => {},
|
||||
// 只能做到 hover 上时的 cursor,旋转过程中 cursor 无法修改
|
||||
// You can only do the cursor when hovering, and the cursor cannot be modified during rotation.
|
||||
cursorStyleHandler: (eventData, control, object) =>
|
||||
`url(${getCursor(object.angle + rotateStaff)}) 16 16, crosshair`,
|
||||
|
||||
actionHandler: (e, transformData, _x, _y) => {
|
||||
// 旋转吸附,单位:角度 一圈 = 360度
|
||||
// Rotational adsorption, unit: angle, one turn = 360 degrees
|
||||
if (e.shiftKey) {
|
||||
transformData.target.set({
|
||||
snapAngle: 15,
|
||||
@@ -647,11 +647,11 @@ const _getRotateControl =
|
||||
|
||||
return rs;
|
||||
},
|
||||
actionName, // 控制点的名称
|
||||
actionName, // The name of the control point
|
||||
});
|
||||
};
|
||||
|
||||
// 上左旋转点
|
||||
// Top Left Rotation Point
|
||||
export const getRotateTLControl: GetControls = (props = {}) =>
|
||||
_getRotateControl({
|
||||
x: -0.5,
|
||||
@@ -662,7 +662,7 @@ export const getRotateTLControl: GetControls = (props = {}) =>
|
||||
actionName: 'rotateTLControl',
|
||||
})(props);
|
||||
|
||||
// 上右旋转点
|
||||
// Top right rotation point
|
||||
export const getRotateTRControl: GetControls = (props = {}) =>
|
||||
_getRotateControl({
|
||||
x: 0.5,
|
||||
@@ -673,7 +673,7 @@ export const getRotateTRControl: GetControls = (props = {}) =>
|
||||
actionName: 'rotateTRControl',
|
||||
})(props);
|
||||
|
||||
// 下右旋转点
|
||||
// Lower right rotation point
|
||||
export const getRotateBRControl: GetControls = (props = {}) =>
|
||||
_getRotateControl({
|
||||
x: 0.5,
|
||||
@@ -684,7 +684,7 @@ export const getRotateBRControl: GetControls = (props = {}) =>
|
||||
actionName: 'rotateBRControl',
|
||||
})(props);
|
||||
|
||||
// 下左旋转点
|
||||
// Lower left rotation point
|
||||
export const getRotateBLControl: GetControls = (props = {}) =>
|
||||
_getRotateControl({
|
||||
x: -0.5,
|
||||
|
||||
@@ -44,27 +44,27 @@ export const setLineControlVisible = ({
|
||||
const { x1, x2, y1, y2 } = element as Line;
|
||||
if ((x1 < x2 && y1 < y2) || (x1 > x2 && y1 > y2)) {
|
||||
element.setControlsVisibility({
|
||||
ml: false, // 中点左
|
||||
mr: false, // 中点右
|
||||
mt: false, // 中点上
|
||||
mb: false, // 中点下
|
||||
bl: false, // 底部左
|
||||
br: true, // 底部右
|
||||
tl: true, // 顶部左
|
||||
tr: false, // 顶部右
|
||||
mtr: false, // 旋转控制点
|
||||
ml: false, // Midpoint left
|
||||
mr: false, // Midpoint right
|
||||
mt: false, // midpoint
|
||||
mb: false, // midpoint
|
||||
bl: false, // Bottom left
|
||||
br: true, // Bottom right
|
||||
tl: true, // Top Left
|
||||
tr: false, // Top right
|
||||
mtr: false, // Rotation Control Point
|
||||
});
|
||||
} else {
|
||||
element.setControlsVisibility({
|
||||
ml: false, // 中点左
|
||||
mr: false, // 中点右
|
||||
mt: false, // 中点上
|
||||
mb: false, // 中点下
|
||||
bl: true, // 底部左
|
||||
br: false, // 底部右
|
||||
tl: false, // 顶部左
|
||||
tr: true, // 顶部右
|
||||
mtr: false, // 旋转控制点
|
||||
ml: false, // Midpoint left
|
||||
mr: false, // Midpoint right
|
||||
mt: false, // midpoint
|
||||
mb: false, // midpoint
|
||||
bl: true, // Bottom left
|
||||
br: false, // Bottom right
|
||||
tl: false, // Top Left
|
||||
tr: true, // Top right
|
||||
mtr: false, // Rotation Control Point
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -82,50 +82,50 @@ const getCommonControl = ({
|
||||
needResetScaleAndSnap?: boolean;
|
||||
}) => {
|
||||
element.setControlsVisibility({
|
||||
mtr: false, // 旋转控制点
|
||||
mtr: false, // Rotation Control Point
|
||||
});
|
||||
// resize
|
||||
// 上左
|
||||
// upper left
|
||||
element.controls.tl = getResizeTLControl({
|
||||
needResetScaleAndSnap,
|
||||
});
|
||||
// 上中
|
||||
// upper middle school
|
||||
element.controls.mt = getResizeMTControl({
|
||||
needResetScaleAndSnap,
|
||||
});
|
||||
// 上右
|
||||
// upper right
|
||||
element.controls.tr = getResizeTRControl({
|
||||
needResetScaleAndSnap,
|
||||
});
|
||||
// 中左
|
||||
// center left
|
||||
element.controls.ml = getResizeMLControl({
|
||||
needResetScaleAndSnap,
|
||||
});
|
||||
// 中右
|
||||
// center right
|
||||
element.controls.mr = getResizeMRControl({
|
||||
needResetScaleAndSnap,
|
||||
});
|
||||
// 下左
|
||||
// Lower left
|
||||
element.controls.bl = getResizeBLControl({
|
||||
needResetScaleAndSnap,
|
||||
});
|
||||
// 下中
|
||||
// lower middle
|
||||
element.controls.mb = getResizeMBControl({
|
||||
needResetScaleAndSnap,
|
||||
});
|
||||
// 下右
|
||||
// Lower right
|
||||
element.controls.br = getResizeBRControl({
|
||||
needResetScaleAndSnap,
|
||||
});
|
||||
|
||||
// rotate
|
||||
// 上左
|
||||
// upper left
|
||||
element.controls.tlr = getRotateTLControl();
|
||||
// 上右
|
||||
// upper right
|
||||
element.controls.trr = getRotateTRControl();
|
||||
// 下左
|
||||
// Lower left
|
||||
element.controls.blr = getRotateBLControl();
|
||||
// 下右
|
||||
// Lower right
|
||||
element.controls.brr = getRotateBRControl();
|
||||
};
|
||||
|
||||
@@ -135,27 +135,27 @@ export const createControls: Partial<
|
||||
[Mode.STRAIGHT_LINE]: ({ element }) => {
|
||||
setLineControlVisible({ element });
|
||||
|
||||
// 左上
|
||||
// top left
|
||||
element.controls.tl = getLineStartControl({
|
||||
x: -0.5,
|
||||
y: -0.5,
|
||||
callback: setLineControlVisible,
|
||||
});
|
||||
// 右上
|
||||
// top right
|
||||
element.controls.tr = getLineStartControl({
|
||||
x: 0.5,
|
||||
y: -0.5,
|
||||
callback: setLineControlVisible,
|
||||
});
|
||||
|
||||
// 左下
|
||||
// lower left
|
||||
element.controls.bl = getLineEndControl({
|
||||
x: -0.5,
|
||||
y: 0.5,
|
||||
callback: setLineControlVisible,
|
||||
});
|
||||
|
||||
// 右下
|
||||
// lower right
|
||||
element.controls.br = getLineEndControl({
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
@@ -173,7 +173,7 @@ export const createControls: Partial<
|
||||
},
|
||||
[Mode.CIRCLE]: ({ element }) => {
|
||||
element.setControlsVisibility({
|
||||
mtr: false, // 旋转控制点
|
||||
mtr: false, // Rotation Control Point
|
||||
});
|
||||
|
||||
const controlProps = {
|
||||
@@ -195,7 +195,7 @@ export const createControls: Partial<
|
||||
},
|
||||
[Mode.BLOCK_TEXT]: ({ element }) => {
|
||||
element.setControlsVisibility({
|
||||
mtr: false, // 旋转控制点
|
||||
mtr: false, // Rotation Control Point
|
||||
});
|
||||
|
||||
const controlProps = {
|
||||
@@ -220,7 +220,7 @@ export const createControls: Partial<
|
||||
},
|
||||
[Mode.INLINE_TEXT]: ({ element }) => {
|
||||
element.setControlsVisibility({
|
||||
mtr: false, // 旋转控制点
|
||||
mtr: false, // Rotation Control Point
|
||||
});
|
||||
|
||||
element.controls.tlr = getRotateTLControl();
|
||||
@@ -230,7 +230,7 @@ export const createControls: Partial<
|
||||
},
|
||||
[Mode.IMAGE]: ({ element }) => {
|
||||
element.setControlsVisibility({
|
||||
mtr: false, // 旋转控制点
|
||||
mtr: false, // Rotation Control Point
|
||||
});
|
||||
|
||||
const controlProps = {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '../typings';
|
||||
|
||||
/**
|
||||
* 选中态边框及控制点样式
|
||||
* Selected border and control point styles
|
||||
*/
|
||||
export const selectedBorderProps = {
|
||||
borderColor: '#4D53E8',
|
||||
@@ -60,7 +60,7 @@ export const defaultProps: Record<Mode, Partial<FabricObjectSchema>> = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
padding: defaultFontSize / 2,
|
||||
// 必须拆分(true),否则中文不会换行。splitByGrapheme:true 约等于 wordBreak: break-all
|
||||
// It must be split (true), otherwise Chinese will not wrap lines. splitByGrapheme: true is approximately equal to wordBreak: break-all
|
||||
splitByGrapheme: true,
|
||||
},
|
||||
[Mode.RECT]: shapeProps,
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
/* eslint-disable complexity */
|
||||
/**
|
||||
* 托管所有的图形创建,赋予业务改造
|
||||
* 调用时机
|
||||
* 1. 初次创建
|
||||
* Hosting all graphics creation, empowering business transformation
|
||||
* call timing
|
||||
* 1. Initial creation
|
||||
* 2. loadFromSchema
|
||||
*/
|
||||
import { nanoid } from 'nanoid';
|
||||
@@ -49,9 +49,9 @@ import { defaultProps } from './default-props';
|
||||
import { createControls, setLineControlVisible } from './create-controls';
|
||||
|
||||
/**
|
||||
* 覆盖默认的 Textbox height 计算逻辑
|
||||
* 默认:根据内容,撑起 Textbox
|
||||
* 预期:严格按照给定高度渲染,溢出隐藏
|
||||
* Override default Textbox height calculation logic
|
||||
* Default: Hold up Textbox according to content
|
||||
* Expected: Render strictly according to the given height, overflow hidden
|
||||
*/
|
||||
const _calcTextHeight = Textbox.prototype.calcTextHeight;
|
||||
Textbox.prototype.calcTextHeight = function () {
|
||||
@@ -60,8 +60,8 @@ Textbox.prototype.calcTextHeight = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* 修复 fabric bug:使用某些自定义字体后,Text 宽度计算异常
|
||||
* 修复方案 from https://github.com/fabricjs/fabric.js/issues/9852
|
||||
* Fix fabric bug: Text width calculation is abnormal after using some custom fonts
|
||||
* Repair plan from https://github.com/fabricjs/fabric.js/issues/9852
|
||||
*/
|
||||
IText.getDefaults = () => ({});
|
||||
// for each class in the chain that has a ownDefaults object:
|
||||
@@ -80,7 +80,7 @@ export const createCommonObjectOptions = (
|
||||
});
|
||||
|
||||
/**
|
||||
* 元素创建入口,所有的元素创建逻辑都走这里
|
||||
* Element creation portal, where all element creation logic goes
|
||||
*/
|
||||
export const createElement = async ({
|
||||
mode,
|
||||
@@ -170,7 +170,7 @@ export const createElement = async ({
|
||||
...elementProps,
|
||||
});
|
||||
|
||||
// 创建直线时的起始点不是通过控制点触发的,需要额外监听
|
||||
// The starting point when creating a line is not triggered by a control point and requires additional listening
|
||||
_element.on('start-end:modified' as keyof ObjectEvents, () => {
|
||||
setLineControlVisible({
|
||||
element: _element as Line,
|
||||
@@ -242,14 +242,14 @@ export const createElement = async ({
|
||||
let _element: FabricObject | undefined = element;
|
||||
|
||||
/**
|
||||
* FabricImage 不支持拉伸/自适应
|
||||
* 需要用 Group 包一下,根据 Group 大小,计算 image 的位置
|
||||
* 1. customId customType 要给到 Group。(根其他元素保持一致,从 element 上能直接取到)
|
||||
* 2. 边框的相关设置要给到图片
|
||||
* FabricImage does not support stretching/adaptive
|
||||
* You need to wrap it in Group, and calculate the position of the image according to the size of the Group.
|
||||
* 1. customId customType should be given to the Group. (The root of other elements is consistent and can be directly retrieved from element)
|
||||
* 2. The relevant settings of the border should be given to the picture.
|
||||
*
|
||||
* 因此而产生的 Hack:
|
||||
* 1. 属性表单根据 schema 解析成 formValue ,需要取 groupSchema.objects[0]
|
||||
* 2. 属性表单设置元素属性(边框),需要调用 group.getObjects()[0].set
|
||||
* The resulting hack:
|
||||
* 1. The attribute form is parsed into formValue according to the schema, and you need to take groupSchema.objects [0]
|
||||
* 2. The property form sets the element properties (borders), you need to call group.getObjects () [0] .set
|
||||
*/
|
||||
if (!_element) {
|
||||
if (elementProps?.src) {
|
||||
@@ -264,7 +264,7 @@ export const createElement = async ({
|
||||
top ?? ((elementProps?.top ?? defaultProps[_mode].top) as number);
|
||||
|
||||
/**
|
||||
* stroke, strokeWidth 设置给 borderRect objects[1]
|
||||
* Stroke, strokeWidth set for borderRect objects [1]
|
||||
*/
|
||||
const { stroke, strokeWidth, ...rest } = {
|
||||
...defaultProps[_mode],
|
||||
@@ -294,7 +294,7 @@ export const createElement = async ({
|
||||
(_element as Group).add(borderRect);
|
||||
_element.set(groupProps);
|
||||
|
||||
// 比例填充时,图片会溢出,所以加了裁剪
|
||||
// When filling in proportions, the image will overflow, so cropping is added.
|
||||
const clipRect = new Rect();
|
||||
_element.set({
|
||||
clipPath: clipRect,
|
||||
@@ -304,7 +304,7 @@ export const createElement = async ({
|
||||
|
||||
resetElementClip({ element: _element as FabricObject });
|
||||
|
||||
// 计算 image 的渲染位置
|
||||
// Calculate the render position of the image
|
||||
setImageFixed({
|
||||
element: _element as Group,
|
||||
});
|
||||
@@ -342,7 +342,7 @@ export const createElement = async ({
|
||||
}
|
||||
};
|
||||
|
||||
// hook element 加载到画布
|
||||
// Load hook elements to canvas
|
||||
export const setElementAfterLoad = async ({
|
||||
element,
|
||||
options: { readonly },
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { Mode, type FabricObjectWithCustomProps } from '../typings';
|
||||
|
||||
/**
|
||||
* 缩放到指定点
|
||||
* Zoom to the specified point
|
||||
*/
|
||||
export const zoomToPoint = ({
|
||||
canvas,
|
||||
@@ -40,17 +40,17 @@ export const zoomToPoint = ({
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
}): TMat2D => {
|
||||
// 设置缩放级别的限制
|
||||
zoomLevel = Math.max(zoomLevel, minZoom); // 最小缩放级别
|
||||
zoomLevel = Math.min(zoomLevel, maxZoom); // 最大缩放级别
|
||||
// Set limits on zoom levels
|
||||
zoomLevel = Math.max(zoomLevel, minZoom); // minimum zoom level
|
||||
zoomLevel = Math.min(zoomLevel, maxZoom); // Maximum zoom level
|
||||
|
||||
// 以鼠标位置为中心进行缩放
|
||||
// Zoom centered on mouse position
|
||||
canvas?.zoomToPoint(point, zoomLevel);
|
||||
return [...(canvas?.viewportTransform as TMat2D)];
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置 canvas 视图
|
||||
* Set canvas view
|
||||
*/
|
||||
export const setViewport = ({
|
||||
canvas,
|
||||
@@ -65,7 +65,7 @@ export const setViewport = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 画布坐标点距离画布左上角距离(单位:px)
|
||||
* Canvas coordinate point distance from the upper left corner of the canvas (unit: px)
|
||||
*/
|
||||
export const canvasXYToScreen = ({
|
||||
canvas,
|
||||
@@ -76,10 +76,10 @@ export const canvasXYToScreen = ({
|
||||
scale: number;
|
||||
point: { x: number; y: number };
|
||||
}) => {
|
||||
// 获取画布的变换矩阵
|
||||
// Get the transformation matrix of the canvas
|
||||
const transform = canvas.viewportTransform;
|
||||
|
||||
// 应用缩放和平移
|
||||
// Apply scaling and panning
|
||||
const zoomX = transform[0];
|
||||
const zoomY = transform[3];
|
||||
const translateX = transform[4];
|
||||
@@ -88,11 +88,11 @@ export const canvasXYToScreen = ({
|
||||
const screenX = (point.x * zoomX + translateX) * scale;
|
||||
const screenY = (point.y * zoomY + translateY) * scale;
|
||||
|
||||
// 获取画布在屏幕上的位置
|
||||
// Get the position of the canvas on the screen
|
||||
const x = screenX;
|
||||
const y = screenY;
|
||||
|
||||
// 不做限制
|
||||
// No restrictions
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
@@ -100,7 +100,7 @@ export const canvasXYToScreen = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 得到选中元素的屏幕坐标(左上 tl、右下 br)
|
||||
* Get the screen coordinates of the selected element (upper left tl, lower right br)
|
||||
*/
|
||||
export const getPopPosition = ({
|
||||
canvas,
|
||||
@@ -113,13 +113,13 @@ export const getPopPosition = ({
|
||||
if (canvas && selection) {
|
||||
const boundingRect = selection.getBoundingRect();
|
||||
|
||||
// 左上角坐标
|
||||
// upper left coordinate
|
||||
const tl = {
|
||||
x: boundingRect.left,
|
||||
y: boundingRect.top,
|
||||
};
|
||||
|
||||
// 右下角坐标
|
||||
// Lower right corner coordinates
|
||||
const br = {
|
||||
x: boundingRect.left + boundingRect.width,
|
||||
y: boundingRect.top + boundingRect.height,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '../typings';
|
||||
|
||||
/**
|
||||
* fabric schema 转 formValue
|
||||
* Fabric schema to formValue
|
||||
*/
|
||||
export const schemaToFormValue = ({
|
||||
schema,
|
||||
@@ -35,12 +35,12 @@ export const schemaToFormValue = ({
|
||||
}): Partial<FabricObjectSchema> => {
|
||||
let s = schema.objects.find(o => o.customId === activeObjectId);
|
||||
|
||||
// 图片是 Group 复合元素,要把需要的元素取出来
|
||||
// The picture is a Group composite element, and the required elements should be taken out.
|
||||
if (s?.customType === Mode.IMAGE) {
|
||||
s = {
|
||||
...s,
|
||||
...s.objects?.[0],
|
||||
// 描边颜色和粗细从 borderRect 上取
|
||||
// Stroke color and thickness are taken from borderRect
|
||||
stroke: s.objects?.[1].stroke,
|
||||
strokeWidth: s.objects?.[1].strokeWidth,
|
||||
} as unknown as FabricObjectSchema;
|
||||
|
||||
@@ -53,7 +53,7 @@ const getLineHtml = (startXY: Snap.Point, endXY: Snap.Point) => {
|
||||
"
|
||||
></div>`;
|
||||
} else {
|
||||
// 横线
|
||||
// horizontal line
|
||||
innerHTML += `<div
|
||||
class="absolute bg-[#00B2B2]"
|
||||
style="
|
||||
|
||||
@@ -28,20 +28,20 @@ type Attribute =
|
||||
|
||||
interface Config {
|
||||
/**
|
||||
* 影响到的属性
|
||||
* Affected properties
|
||||
*/
|
||||
key: Attribute;
|
||||
/**
|
||||
* 吸附方向
|
||||
* adsorption direction
|
||||
*/
|
||||
direction: 'x' | 'y';
|
||||
/**
|
||||
* 吸附到的值
|
||||
* Adsorbed value
|
||||
*/
|
||||
snapValue: number[];
|
||||
}
|
||||
|
||||
// 计算目标元素的未来位置
|
||||
// Calculate the future position of the target element
|
||||
const getNextHelplinePoint = ({
|
||||
targetPoint,
|
||||
latestDistance,
|
||||
@@ -94,7 +94,7 @@ const getNextHelplinePoint = ({
|
||||
}));
|
||||
};
|
||||
|
||||
// 计算吸附结果
|
||||
// Calculate adsorption results
|
||||
const getNextRs = ({
|
||||
latestDistance,
|
||||
latestDistanceAbs,
|
||||
@@ -176,7 +176,7 @@ const getNextRs = ({
|
||||
return {};
|
||||
};
|
||||
|
||||
// 对齐规则
|
||||
// alignment rule
|
||||
export const alignRule: Snap.Rule = ({
|
||||
otherPoints,
|
||||
targetPoint,
|
||||
@@ -275,27 +275,27 @@ export const alignRule: Snap.Rule = ({
|
||||
const config = configMap[controlType];
|
||||
|
||||
config.forEach(item => {
|
||||
// 需要判断吸附的点位集合
|
||||
// It is necessary to determine the set of adsorption points
|
||||
const points = item.snapValue;
|
||||
|
||||
// 找到距离最近的吸附点集合
|
||||
// Find the closest collection of adsorption points
|
||||
const {
|
||||
snapPoints,
|
||||
distance: latestDistance,
|
||||
distanceAbs: latestDistanceAbs,
|
||||
} = findLatestObject(otherPoints, points, item.direction);
|
||||
|
||||
// 如果距离小于阈值,则进行吸附
|
||||
// If the distance is less than the threshold, adsorption is performed
|
||||
if (latestDistanceAbs <= threshold) {
|
||||
const helplines: Snap.Line[] = [];
|
||||
const allPoints: (Snap.Point & { isTarget?: boolean })[] = [];
|
||||
|
||||
// 将所有其他对象的点添加到 allPoints 中
|
||||
// Add the points of all other objects to allPoints
|
||||
otherPoints.forEach(a => {
|
||||
allPoints.push(...Object.values(a));
|
||||
});
|
||||
|
||||
// 将目标对象的点添加到 allPoints 中
|
||||
// Add the target object's points to allPoints
|
||||
allPoints.push(
|
||||
...getNextHelplinePoint({
|
||||
targetPoint,
|
||||
@@ -305,10 +305,10 @@ export const alignRule: Snap.Rule = ({
|
||||
}),
|
||||
);
|
||||
|
||||
// 根据吸附方向对 allPoints 进行排序
|
||||
// Sort allPoints by adsorption direction
|
||||
const sortKey = item.direction === 'x' ? 'y' : 'x';
|
||||
|
||||
// 根据吸附结果,从所有点中挑选出需要绘制辅助线的点
|
||||
// According to the adsorption results, select the points where the auxiliary line needs to be drawn from all the points
|
||||
snapPoints.forEach(sp => {
|
||||
const _helpline = allPoints
|
||||
.filter(p => numberEqual(p[item.direction], sp[item.direction]))
|
||||
@@ -328,7 +328,7 @@ export const alignRule: Snap.Rule = ({
|
||||
}
|
||||
});
|
||||
|
||||
// x,y 一起吸附,需要对 x 吸附线的 y 坐标进行修正
|
||||
// X and y are adsorbed together, and the y coordinate of the x adsorption line needs to be corrected.
|
||||
if (
|
||||
controlType === Snap.ControlType.Center &&
|
||||
rs.top?.isSnap &&
|
||||
@@ -347,7 +347,7 @@ export const alignRule: Snap.Rule = ({
|
||||
);
|
||||
}
|
||||
|
||||
// x,y 一起吸附,需要对 y 吸附线的 x 坐标进行修正
|
||||
// X and y are adsorbed together, and the x coordinate of the y adsorption line needs to be corrected.
|
||||
if (
|
||||
controlType === Snap.ControlType.Center &&
|
||||
rs.left?.isSnap &&
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import { numberEqual } from '../util';
|
||||
import { Snap } from '../../../typings';
|
||||
|
||||
// 获取对象的底部点
|
||||
// Get the bottom point of the object
|
||||
const getBottomPoint = (p: Snap.ObjectPointsWithMiddle) => {
|
||||
let point = p.tl;
|
||||
Object.values(p).forEach(d => {
|
||||
@@ -31,7 +31,7 @@ const getBottomPoint = (p: Snap.ObjectPointsWithMiddle) => {
|
||||
});
|
||||
return point;
|
||||
};
|
||||
// 获取对象的顶部点
|
||||
// Get the top point of the object
|
||||
const getTopPoint = (p: Snap.ObjectPointsWithMiddle) => {
|
||||
let point = p.tl;
|
||||
Object.values(p).forEach(d => {
|
||||
@@ -42,7 +42,7 @@ const getTopPoint = (p: Snap.ObjectPointsWithMiddle) => {
|
||||
return point;
|
||||
};
|
||||
|
||||
// 获取对象的左侧点
|
||||
// Get the left point of the object
|
||||
const getLeftPoint = (p: Snap.ObjectPointsWithMiddle) => {
|
||||
let point = p.tl;
|
||||
Object.values(p).forEach(d => {
|
||||
@@ -53,7 +53,7 @@ const getLeftPoint = (p: Snap.ObjectPointsWithMiddle) => {
|
||||
return point;
|
||||
};
|
||||
|
||||
// 获取对象的右侧点
|
||||
// Get the right point of the object
|
||||
const getRightPoint = (p: Snap.ObjectPointsWithMiddle) => {
|
||||
let point = p.tl;
|
||||
Object.values(p).forEach(d => {
|
||||
@@ -64,7 +64,7 @@ const getRightPoint = (p: Snap.ObjectPointsWithMiddle) => {
|
||||
return point;
|
||||
};
|
||||
|
||||
// 判断两段范围是否重叠
|
||||
// Determine whether the two ranges overlap
|
||||
const isInDuration = (
|
||||
target: {
|
||||
min: number;
|
||||
@@ -93,7 +93,7 @@ const getMiddle = (
|
||||
return target.max - (target.max - duration.min) / 2;
|
||||
};
|
||||
|
||||
// 找到指定方向(横/纵)距离最近的左右两个对象的相关信息
|
||||
// Find the relevant information about the two objects closest to the specified direction (horizontal/vertical)
|
||||
const findLatestObject = ({
|
||||
otherPoints: _otherPoints,
|
||||
targetPoint,
|
||||
@@ -204,7 +204,7 @@ const findLatestObject = ({
|
||||
return rs;
|
||||
};
|
||||
|
||||
// 等距吸附规则
|
||||
// isometric adsorption rule
|
||||
export const paddingRule: Snap.Rule = ({
|
||||
otherPoints,
|
||||
targetPoint,
|
||||
@@ -224,19 +224,19 @@ export const paddingRule: Snap.Rule = ({
|
||||
},
|
||||
};
|
||||
|
||||
// 如果没有其他对象,则直接返回结果
|
||||
// If there are no other objects, the result is returned directly
|
||||
if (!otherPoints || otherPoints.length === 0) {
|
||||
return rs;
|
||||
}
|
||||
|
||||
// 横向 padding 判断
|
||||
// Horizontal padding judgment
|
||||
const latestXObj = findLatestObject({
|
||||
otherPoints,
|
||||
targetPoint,
|
||||
direction: 'x',
|
||||
});
|
||||
|
||||
// 遍历得到横向的右边对象
|
||||
// Traverse to get the horizontal right object
|
||||
let next = [];
|
||||
let i = latestXObj.durationObjects.findIndex(
|
||||
d => d === latestXObj.next.point,
|
||||
@@ -256,7 +256,7 @@ export const paddingRule: Snap.Rule = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历得到横向的左边对象
|
||||
// Traverse to get the horizontal left object
|
||||
let prev = [];
|
||||
i = latestXObj.durationObjects.findIndex(d => d === latestXObj.prev.point);
|
||||
if (i !== -1) {
|
||||
@@ -409,7 +409,7 @@ export const paddingRule: Snap.Rule = ({
|
||||
let latestDistance = 999;
|
||||
let latestItem = next[0];
|
||||
|
||||
// 从左侧所有 padding 中,找到最接近的吸附距离
|
||||
// Find the closest adsorption distance from all the padding on the left
|
||||
next.forEach(n => {
|
||||
const distance = n.distance - latestXObj.next.distance;
|
||||
if (Math.abs(distance) < Math.abs(latestDistance)) {
|
||||
@@ -418,20 +418,20 @@ export const paddingRule: Snap.Rule = ({
|
||||
}
|
||||
});
|
||||
|
||||
// 如果距离小于阈值,则进行吸附
|
||||
// If the distance is less than the threshold, adsorption is performed
|
||||
if (Math.abs(latestDistance) <= threshold) {
|
||||
// 如果找到的 所有 padding 距离 = 最接近的距离,则将这些 padding 添加到 mins 中
|
||||
// If found, all padding distances = closest distance, add those padding to mins
|
||||
next.forEach(n => {
|
||||
if (numberEqual(n.distance, latestItem.distance)) {
|
||||
mins.push(n);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果 mins 不为空,则进行吸附
|
||||
// If mins is not empty, perform adsorption
|
||||
if (mins.length > 0) {
|
||||
// 计算吸附距离
|
||||
// Calculate the adsorption distance
|
||||
let staff = latestXObj.next.distance - mins[0].distance;
|
||||
// 是拖拽右侧 resize 控制点
|
||||
// Yes drag right resize control point
|
||||
if (
|
||||
[
|
||||
Snap.ControlType.TopRight,
|
||||
@@ -442,7 +442,7 @@ export const paddingRule: Snap.Rule = ({
|
||||
staff = -(latestXObj.next.distance - mins[0].distance);
|
||||
}
|
||||
|
||||
// 计算目标对象的中间点
|
||||
// Calculate the midpoint of the target object
|
||||
const nextMiddle = getMiddle(
|
||||
{
|
||||
min: getTopPoint(latestXObj.next.point).y,
|
||||
@@ -454,10 +454,10 @@ export const paddingRule: Snap.Rule = ({
|
||||
},
|
||||
);
|
||||
|
||||
// 计算辅助线 x 坐标
|
||||
// Calculate auxiliary line x coordinates
|
||||
let nextX = staff + targetPoint.tr.x;
|
||||
|
||||
// 如果是拖拽右侧 resize 控制点,计算方式不同
|
||||
// If you drag and drop the right to resize the control point, the calculation method is different.
|
||||
if (
|
||||
[
|
||||
Snap.ControlType.TopRight,
|
||||
@@ -941,7 +941,7 @@ export const paddingRule: Snap.Rule = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 根据控制点,处理属性变化值
|
||||
// According to the control point, process the property change value
|
||||
if (
|
||||
[
|
||||
Snap.ControlType.TopLeft,
|
||||
|
||||
@@ -27,20 +27,20 @@ type Attribute =
|
||||
|
||||
interface Config {
|
||||
/**
|
||||
* 影响到的属性
|
||||
* Affected properties
|
||||
*/
|
||||
key: Attribute;
|
||||
/**
|
||||
* 吸附方向
|
||||
* adsorption direction
|
||||
*/
|
||||
direction: 'x' | 'y';
|
||||
/**
|
||||
* 吸附到的值
|
||||
* Adsorbed value
|
||||
*/
|
||||
snapValue: number[];
|
||||
}
|
||||
|
||||
// 计算目标元素的未来位置
|
||||
// Calculate the future position of the target element
|
||||
const getNextHelplinePoint = ({
|
||||
targetPoint,
|
||||
latestDistance,
|
||||
|
||||
@@ -47,10 +47,10 @@ class SnapService {
|
||||
testPoints: Snap.Point[] = [];
|
||||
|
||||
snapOpen = true;
|
||||
// 开发模式,开启后,按下 shift 会显示激活元素的 5 个点位
|
||||
// Development mode, when turned on, pressing shift will display 5 points of the active element
|
||||
devMode = false;
|
||||
onKeyDown = (event: KeyboardEvent) => {
|
||||
// 按下 cmd 键,关闭吸附(与截图冲突,暂时隐藏)
|
||||
// Press the cmd key to turn off the adsorption (conflicts with screenshots, temporarily hidden)
|
||||
if (event.key.toLowerCase() === 'meta') {
|
||||
// this.snapOpen = false;
|
||||
} else if (event.key.toLowerCase() === 'shift' && this.devMode) {
|
||||
@@ -64,7 +64,7 @@ class SnapService {
|
||||
};
|
||||
|
||||
onKeyUp = (event: KeyboardEvent) => {
|
||||
// 松手 cmd 键,打开吸附(与截图冲突,暂时隐藏)
|
||||
// Let go of the cmd button and turn on the adsorption (conflicts with screenshots, temporarily hidden)
|
||||
if (event.key.toLowerCase() === 'meta') {
|
||||
// this.snapOpen = true;
|
||||
// this.helpline.hide();
|
||||
@@ -152,14 +152,14 @@ class SnapService {
|
||||
return newAttrs;
|
||||
};
|
||||
|
||||
// move 和 resize 影响到的属性不同,所以分开。move 仅影响 left top
|
||||
// Move and resize affect different properties, so they are separated. move only affects left top
|
||||
move = (target: FabricObject) =>
|
||||
this._move({
|
||||
target,
|
||||
controlType: Snap.ControlType.Center,
|
||||
});
|
||||
|
||||
// resize 根据控制点的不同,可能影响到多个属性
|
||||
// Resize may affect multiple properties depending on the control point
|
||||
resize = (target: FabricObject, controlType: Snap.ControlType) => {
|
||||
if (target.angle !== 0) {
|
||||
return;
|
||||
|
||||
@@ -103,7 +103,7 @@ export const fixedMiddlePoint = (
|
||||
};
|
||||
};
|
||||
|
||||
// 寻找距离指定点,最近元素及点位
|
||||
// Find the specified point, the nearest element, and the point
|
||||
export const findLatestObject = (
|
||||
otherPoints: Snap.ObjectPointsWithMiddle[],
|
||||
targets: number[],
|
||||
@@ -146,10 +146,10 @@ export const getLatestSnapRs = (
|
||||
const sortedSnapRs = snapRsFilterEmpty.sort(
|
||||
(a, b) => a.snapDistance - b.snapDistance,
|
||||
);
|
||||
// 找到最近的距离
|
||||
// Find the nearest distance
|
||||
const latestSnapRs = sortedSnapRs[0];
|
||||
|
||||
// 找到最近的距离的 helplines,最近的距离可能有多个,要把 helplines 合并
|
||||
// Find the helplines with the closest distance, there may be multiple closest distances, and merge the helplines.
|
||||
const helplinesRs = snapRsFilterEmpty
|
||||
.filter(rs => numberEqual(rs.snapDistance, latestSnapRs.snapDistance))
|
||||
.map(rs => rs.helplines)
|
||||
|
||||
@@ -5,7 +5,7 @@ module.exports = {
|
||||
important: '',
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
corePlugins: {
|
||||
preflight: false, // 关闭@tailwind base默认样式,避免对现有样式影响
|
||||
preflight: false, // Turn off @tailwind base default styles to avoid affecting existing styles
|
||||
},
|
||||
plugins: [require('@coze-arch/tailwind-config/coze')],
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
'./node_modules/@coze-arch/coze-design/dist/**/*.{js,jsx,css}',
|
||||
],
|
||||
corePlugins: {
|
||||
preflight: false, // 关闭@tailwind base默认样式,避免对现有样式影响
|
||||
preflight: false, // Turn off @tailwind base default styles to avoid affecting existing styles
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
plugins: [require('@coze-arch/tailwind-config/coze')],
|
||||
|
||||
Reference in New Issue
Block a user