326 lines
9.5 KiB
TypeScript
326 lines
9.5 KiB
TypeScript
/*
|
|
* Copyright 2025 coze-dev Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/* eslint-disable @coze-arch/max-line-per-function */
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
import { cloneDeep } from 'lodash-es';
|
|
import { type Canvas, type CanvasEvents, type FabricObject } from 'fabric';
|
|
import { useLatest } from 'ahooks';
|
|
import {
|
|
ViewVariableType,
|
|
type InputVariable,
|
|
} from '@coze-workflow/base/types';
|
|
import { I18n } from '@coze-arch/i18n';
|
|
import { getUploadCDNAsset } from '@coze-workflow/base-adapter';
|
|
|
|
import { createElement, defaultProps } from '../utils';
|
|
import {
|
|
Mode,
|
|
UNKNOWN_VARIABLE_NAME,
|
|
type FabricObjectWithCustomProps,
|
|
type FabricSchema,
|
|
type VariableRef,
|
|
} from '../typings';
|
|
|
|
const ImagePlaceholder = `${getUploadCDNAsset('')}/workflow/fabric-canvas/img-placeholder.png`;
|
|
|
|
// 需要额外保存的属性
|
|
export const saveProps = [
|
|
'width',
|
|
'height',
|
|
'editable',
|
|
'text',
|
|
'backgroundColor',
|
|
'padding',
|
|
// 自定义参数
|
|
// textBox 的真实高度
|
|
'customFixedHeight',
|
|
// 元素 id
|
|
'customId',
|
|
// 元素类型
|
|
'customType',
|
|
// image 的适应模式
|
|
'customFixedType',
|
|
// // 由变量生成元素的 title
|
|
// 引用关系
|
|
'customVariableRefs',
|
|
];
|
|
|
|
export const useCanvasChange = ({
|
|
variables,
|
|
canvas,
|
|
onChange,
|
|
schema,
|
|
listenerEvents = [
|
|
'object:modified',
|
|
'object:added',
|
|
'object:removed',
|
|
'object:moving',
|
|
'object:modified-zIndex',
|
|
],
|
|
}: {
|
|
variables?: InputVariable[];
|
|
canvas?: Canvas;
|
|
onChange?: (schema: FabricSchema) => void;
|
|
schema?: FabricSchema;
|
|
listenerEvents?: (
|
|
| 'object:modified'
|
|
| 'object:added'
|
|
| 'object:removed'
|
|
| 'object:moving'
|
|
| 'object:modified-zIndex'
|
|
)[];
|
|
}) => {
|
|
const eventDisposers = useRef<(() => void)[]>([]);
|
|
|
|
const [isListen, setIsListener] = useState(true);
|
|
const onChangeLatest = useLatest(onChange);
|
|
const schemaLatest = useLatest(schema);
|
|
const cacheCustomVariableRefs = useRef<VariableRef[]>(
|
|
schema?.customVariableRefs ?? [],
|
|
);
|
|
|
|
// 删除画布中不存在的引用关系
|
|
const resetCustomVariableRefs = useCallback(
|
|
({ schema: _schema }: { schema: FabricSchema }) => {
|
|
let newCustomVariableRefs = cacheCustomVariableRefs.current;
|
|
|
|
const allObjectIds = _schema.objects.map(d => d.customId);
|
|
newCustomVariableRefs = newCustomVariableRefs?.filter(d =>
|
|
allObjectIds.includes(d.objectId),
|
|
);
|
|
cacheCustomVariableRefs.current = newCustomVariableRefs;
|
|
|
|
return newCustomVariableRefs;
|
|
},
|
|
[],
|
|
);
|
|
|
|
// 监听画布变化
|
|
useEffect(() => {
|
|
if (canvas && onChangeLatest.current && isListen) {
|
|
const _onChange = ({ isRemove }: { isRemove: boolean }) => {
|
|
const json = canvas.toObject(saveProps) as FabricSchema;
|
|
// 删除时,顺便删掉无效 ref
|
|
if (isRemove) {
|
|
json.customVariableRefs = resetCustomVariableRefs({
|
|
schema: json,
|
|
});
|
|
} else {
|
|
json.customVariableRefs = cloneDeep(cacheCustomVariableRefs.current);
|
|
}
|
|
|
|
onChangeLatest.current?.(json);
|
|
};
|
|
|
|
eventDisposers.current.forEach(disposer => disposer());
|
|
eventDisposers.current = [];
|
|
|
|
listenerEvents.forEach(event => {
|
|
const disposer = canvas.on(event as keyof CanvasEvents, function (e) {
|
|
_onChange({
|
|
isRemove: event === 'object:removed',
|
|
});
|
|
});
|
|
eventDisposers.current.push(disposer);
|
|
});
|
|
}
|
|
return () => {
|
|
eventDisposers.current.forEach(disposer => disposer?.());
|
|
eventDisposers.current = [];
|
|
};
|
|
}, [canvas, isListen]);
|
|
|
|
/**
|
|
* 生成带引用的新元素
|
|
*/
|
|
const addRefObjectByVariable = useCallback(
|
|
async (variable: InputVariable, element?: FabricObject) => {
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
const {
|
|
customVariableRefs = [],
|
|
width = 0,
|
|
height = 0,
|
|
} = schemaLatest.current ?? {};
|
|
|
|
const { id, name, type } = variable;
|
|
const centerXY = [
|
|
width / 2 + customVariableRefs.length * 16,
|
|
height / 2 + customVariableRefs.length * 16,
|
|
];
|
|
|
|
let _element: FabricObject | undefined = element;
|
|
|
|
// 如果没有传入现有元素,则创建新元素
|
|
if (!_element) {
|
|
if (type === ViewVariableType.Image) {
|
|
_element = await createElement({
|
|
mode: Mode.IMAGE,
|
|
position: [
|
|
centerXY[0] - (defaultProps[Mode.IMAGE].width as number) / 2,
|
|
centerXY[1] - (defaultProps[Mode.IMAGE].height as number) / 2,
|
|
],
|
|
elementProps: {
|
|
width: defaultProps[Mode.IMAGE].width,
|
|
height: defaultProps[Mode.IMAGE].width,
|
|
editable: false,
|
|
src: ImagePlaceholder,
|
|
},
|
|
});
|
|
} else if (type === ViewVariableType.String) {
|
|
_element = await createElement({
|
|
mode: Mode.BLOCK_TEXT,
|
|
position: [
|
|
centerXY[0] - (defaultProps[Mode.BLOCK_TEXT].width as number) / 2,
|
|
centerXY[1] -
|
|
(defaultProps[Mode.BLOCK_TEXT].height as number) / 2,
|
|
],
|
|
elementProps: {
|
|
text: I18n.t(
|
|
'imageflow_canvas_change_text',
|
|
{},
|
|
'点击编辑文本预览',
|
|
),
|
|
width: defaultProps[Mode.BLOCK_TEXT].width,
|
|
height: defaultProps[Mode.BLOCK_TEXT].height,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
if (_element) {
|
|
// 更新引用关系
|
|
cacheCustomVariableRefs.current.push({
|
|
variableId: id as string,
|
|
objectId: (_element as FabricObjectWithCustomProps)
|
|
.customId as string,
|
|
variableName: name,
|
|
});
|
|
|
|
// 添加到画布并激活
|
|
canvas.add(_element);
|
|
canvas.setActiveObject(_element);
|
|
}
|
|
},
|
|
[canvas],
|
|
);
|
|
|
|
/**
|
|
* 更新指定 objectId 的元素的引用关系
|
|
* 如果 variable 为空,则删除引用
|
|
* 如果 variable 不为空 && customVariableRefs 已存在对应关系,则更新引用
|
|
* 如果 variable 不为空 && customVariableRefs 不存在对应关系,则新增引用
|
|
*
|
|
*/
|
|
const updateRefByObjectId = useCallback(
|
|
({
|
|
objectId,
|
|
variable,
|
|
}: {
|
|
objectId: string;
|
|
variable?: InputVariable;
|
|
}) => {
|
|
const customVariableRefs = cacheCustomVariableRefs.current;
|
|
const targetRef = customVariableRefs.find(d => d.objectId === objectId);
|
|
let newCustomVariableRefs = [];
|
|
// 如果 variable 为空,则删除引用
|
|
if (!variable) {
|
|
newCustomVariableRefs = customVariableRefs.filter(
|
|
d => d.objectId !== objectId,
|
|
);
|
|
// 如果 variable 不为空 && customVariableRefs 不存在对应关系,则新增引用
|
|
} else if (!targetRef) {
|
|
newCustomVariableRefs = [
|
|
...customVariableRefs,
|
|
{
|
|
variableId: variable.id as string,
|
|
objectId,
|
|
variableName: variable.name,
|
|
},
|
|
];
|
|
// 如果 variable 不为空 && customVariableRefs 已存在对应关系,则更新引用
|
|
} else {
|
|
newCustomVariableRefs = customVariableRefs.map(d => {
|
|
if (d.objectId === objectId) {
|
|
return {
|
|
...d,
|
|
variableId: variable.id as string,
|
|
variableName: variable.name,
|
|
};
|
|
}
|
|
return d;
|
|
});
|
|
}
|
|
|
|
cacheCustomVariableRefs.current = newCustomVariableRefs;
|
|
onChangeLatest.current?.({
|
|
...(schemaLatest.current as FabricSchema),
|
|
customVariableRefs: newCustomVariableRefs,
|
|
});
|
|
},
|
|
[onChangeLatest, schemaLatest],
|
|
);
|
|
|
|
/**
|
|
* variables 变化时,更新引用关系中的变量名
|
|
*/
|
|
useEffect(() => {
|
|
const { customVariableRefs = [] } = schemaLatest.current ?? {};
|
|
const needsUpdate = customVariableRefs.some(ref => {
|
|
const variable = variables?.find(v => v.id === ref.variableId);
|
|
return ref.variableName !== (variable?.name ?? UNKNOWN_VARIABLE_NAME);
|
|
});
|
|
|
|
if (needsUpdate) {
|
|
const newCustomVariableRefs = customVariableRefs.map(ref => {
|
|
const variable = variables?.find(v => v.id === ref.variableId);
|
|
return {
|
|
...ref,
|
|
variableName: variable?.name ?? UNKNOWN_VARIABLE_NAME,
|
|
};
|
|
});
|
|
|
|
cacheCustomVariableRefs.current = newCustomVariableRefs;
|
|
onChangeLatest.current?.({
|
|
...(schemaLatest.current as FabricSchema),
|
|
customVariableRefs: newCustomVariableRefs,
|
|
});
|
|
}
|
|
}, [variables]);
|
|
|
|
const stopListen = useCallback(() => {
|
|
setIsListener(false);
|
|
}, []);
|
|
|
|
const startListen = useCallback(() => {
|
|
setIsListener(true);
|
|
// redo undo 完成后,更新引用关系
|
|
cacheCustomVariableRefs.current =
|
|
schemaLatest.current?.customVariableRefs ?? [];
|
|
}, []);
|
|
|
|
return {
|
|
customVariableRefs: cacheCustomVariableRefs.current,
|
|
addRefObjectByVariable,
|
|
updateRefByObjectId,
|
|
stopListen,
|
|
startListen,
|
|
};
|
|
};
|