Files
Genarrative/src/components/image-editor/ImageCanvasAssetLibraryModel.ts
T
lhk229 72acd0ec05
Project CI / Frontend tests (push) Successful in 3m40s
Project CI / Backend tests (push) Successful in 4m11s
Project CI / Repository checks (push) Successful in 1m19s
Project CI / Native shell tests (push) Successful in 12m36s
停止向用户泄露抠图模型 (#120)
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/120
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-31 16:40:43 +08:00

580 lines
13 KiB
TypeScript

import { isEditorInternalProcessingModel } from './ImageCanvasEditorModel';
import type {
AssetMarqueeState,
CanvasLayer,
EditorAsset,
EditorAssetFolder,
} from './ImageCanvasEditorTypes';
type ClientRectLike = Pick<DOMRect, 'bottom' | 'left' | 'right' | 'top'>;
type ClientPoint = {
clientX: number;
clientY: number;
};
type AssetFolderHitTarget = {
folderId: string;
rect: ClientRectLike;
};
type AssetHitTarget = {
assetId: string;
rect: ClientRectLike;
};
export type GroupedAssetFolder = EditorAssetFolder & {
assets: EditorAsset[];
};
export const EDITOR_UPLOAD_ASSET_FOLDER_LABEL = '上传素材';
function isPointInRect(point: ClientPoint, rect: ClientRectLike) {
return (
point.clientX >= rect.left &&
point.clientX <= rect.right &&
point.clientY >= rect.top &&
point.clientY <= rect.bottom
);
}
function doRectsIntersect(rect: ClientRectLike, selectionRect: ClientRectLike) {
return (
rect.left <= selectionRect.right &&
rect.right >= selectionRect.left &&
rect.top <= selectionRect.bottom &&
rect.bottom >= selectionRect.top
);
}
export function resolveAssetFolderIdFromPoint({
point,
listRect,
folders,
}: {
point: ClientPoint;
listRect: ClientRectLike | null | undefined;
folders: AssetFolderHitTarget[];
}) {
if (!listRect || !isPointInRect(point, listRect)) {
return null;
}
const matchedFolder = folders.find((folder) =>
isPointInRect(point, folder.rect),
);
return matchedFolder?.folderId ?? null;
}
export function resolvePinnedAssetMoveFolderId({
folderId,
listRect,
headerRect,
}: {
folderId: string | null;
listRect: ClientRectLike | null | undefined;
headerRect: ClientRectLike | null | undefined;
}) {
if (
!folderId ||
!listRect ||
!headerRect ||
(headerRect.bottom >= listRect.top && headerRect.top <= listRect.bottom)
) {
return null;
}
return folderId;
}
export function shouldStartAssetMarquee({
isAssetSelectionMode,
button,
isBlockedTarget,
}: {
isAssetSelectionMode: boolean;
button: number;
isBlockedTarget: boolean;
}) {
return isAssetSelectionMode && button === 0 && !isBlockedTarget;
}
export function createAssetMarqueeFromPointer({
pointerId,
point,
containerRect,
}: {
pointerId: number;
point: ClientPoint;
containerRect: Pick<DOMRect, 'left' | 'top'> | null | undefined;
}): AssetMarqueeState {
const startX = point.clientX - (containerRect?.left ?? 0);
const startY = point.clientY - (containerRect?.top ?? 0);
return {
pointerId,
startX,
startY,
currentX: startX,
currentY: startY,
};
}
export function moveAssetMarqueeToPointer({
marquee,
point,
containerRect,
}: {
marquee: AssetMarqueeState;
point: ClientPoint;
containerRect: Pick<DOMRect, 'left' | 'top'> | null | undefined;
}) {
return {
...marquee,
currentX: point.clientX - (containerRect?.left ?? 0),
currentY: point.clientY - (containerRect?.top ?? 0),
};
}
export function createAssetMarqueeSelectionRect({
marquee,
point,
containerRect,
}: {
marquee: AssetMarqueeState;
point: ClientPoint;
containerRect: Pick<DOMRect, 'left' | 'top'> | null | undefined;
}): ClientRectLike {
const startClientX = (containerRect?.left ?? 0) + marquee.startX;
const startClientY = (containerRect?.top ?? 0) + marquee.startY;
return {
left: Math.min(startClientX, point.clientX),
right: Math.max(startClientX, point.clientX),
top: Math.min(startClientY, point.clientY),
bottom: Math.max(startClientY, point.clientY),
};
}
export function selectUploadedAssetsInRect({
assets,
assetTargets,
selectionRect,
}: {
assets: EditorAsset[];
assetTargets: AssetHitTarget[];
selectionRect: ClientRectLike;
}) {
const uploadedAssetIds = new Set(
assets
.filter((asset) => asset.sourceKind === 'uploaded')
.map((asset) => asset.id),
);
return new Set(
assetTargets
.filter(
(target) =>
uploadedAssetIds.has(target.assetId) &&
doRectsIntersect(target.rect, selectionRect),
)
.map((target) => target.assetId),
);
}
export function groupAssetsByFolder(
assetFolders: EditorAssetFolder[],
assets: EditorAsset[],
): GroupedAssetFolder[] {
return assetFolders.map((folder) => ({
...folder,
assets: assets.filter((asset) => asset.folderId === folder.id),
}));
}
function normalizeSearchQuery(query: string) {
return query.trim().toLowerCase();
}
function matchesSearchText(query: string, values: Array<string | null | undefined>) {
if (!query) {
return true;
}
return values.some((value) => value?.toLowerCase().includes(query));
}
function startsWithSearchText(query: string, values: Array<string | null | undefined>) {
if (!query) {
return true;
}
return values.some((value) => value?.trim().toLowerCase().startsWith(query));
}
function resolveSearchableEditorModel(model: string | null | undefined) {
const normalizedModel = model?.trim();
return normalizedModel && !isEditorInternalProcessingModel(normalizedModel)
? normalizedModel
: undefined;
}
function buildAssetSearchValues(asset: EditorAsset) {
return [
asset.label,
asset.prompt,
asset.actualPrompt,
resolveSearchableEditorModel(asset.model),
asset.taskId,
asset.assetKind,
asset.mediaType,
];
}
function buildLayerSearchValues(layer: CanvasLayer) {
return [
layer.title,
layer.prompt,
layer.actualPrompt,
resolveSearchableEditorModel(layer.model),
layer.taskId,
layer.assetKind,
layer.mediaType,
];
}
function assetMatchesSearch(asset: EditorAsset, query: string) {
return matchesSearchText(query, buildAssetSearchValues(asset));
}
function assetStartsWithSearch(asset: EditorAsset, query: string) {
return startsWithSearchText(query, buildAssetSearchValues(asset));
}
export function layerMatchesSearch(layer: CanvasLayer, query: string) {
return matchesSearchText(query, buildLayerSearchValues(layer));
}
export function layerStartsWithSearch(layer: CanvasLayer, query: string) {
return startsWithSearchText(query, buildLayerSearchValues(layer));
}
export function filterGroupedAssetsBySearch(
groupedAssets: GroupedAssetFolder[],
query: string,
) {
const normalizedQuery = normalizeSearchQuery(query);
if (!normalizedQuery) {
return groupedAssets;
}
return groupedAssets
.map((folder) => {
const folderMatches =
startsWithSearchText(normalizedQuery, [folder.label]) ||
matchesSearchText(normalizedQuery, [folder.label]);
const prefixAssets = folder.assets.filter((asset) =>
assetStartsWithSearch(asset, normalizedQuery),
);
const matchingAssets = folder.assets.filter((asset) =>
assetMatchesSearch(asset, normalizedQuery),
);
return {
...folder,
assets: folderMatches
? folder.assets
: [
...prefixAssets,
...matchingAssets.filter(
(asset) =>
!prefixAssets.some(
(prefixAsset) => prefixAsset.id === asset.id,
),
),
],
};
})
.filter((folder) => folder.assets.length > 0);
}
export function getSelectableAssets(assets: EditorAsset[]) {
return assets.filter((asset) => asset.sourceKind === 'uploaded');
}
export type AssetShowcaseSubmissionState = {
canSubmit: boolean;
label: string;
reason: string | null;
};
function normalizeAssetShowcaseReason(value: string | null | undefined) {
return value?.trim() || null;
}
export function resolveAssetShowcaseSubmissionState(
asset: EditorAsset,
): AssetShowcaseSubmissionState {
const reviewStatus = asset.showcaseReviewStatus ?? null;
if (reviewStatus === 'pending') {
return {
canSubmit: false,
label: '精选审核中',
reason: '素材正在审核中,请等待审核结果',
};
}
if (reviewStatus === 'approved') {
return {
canSubmit: false,
label:
asset.showcaseDisplayEnabled === false
? '已入选精选,暂未展示'
: '已入选精选',
reason:
asset.showcaseDisplayEnabled === false
? '素材已通过审核,运营后台开启展示后才会出现在前台'
: '素材已经入选精选',
};
}
if (reviewStatus === 'rejected') {
return {
canSubmit: false,
label: '精选未通过',
reason: '该素材审核未通过,不能重复提交',
};
}
if (asset.sourceType !== 'generated') {
return {
canSubmit: false,
label: '提交精选审核',
reason: '只有生成素材可以提交精选审核',
};
}
if (!asset.persisted) {
return {
canSubmit: false,
label: '提交精选审核',
reason: '素材保存完成后才能提交精选审核',
};
}
if (!asset.src.trim()) {
return {
canSubmit: false,
label: '提交精选审核',
reason: '没有媒体内容的素材不能提交精选审核',
};
}
return {
canSubmit: true,
label: '提交精选审核',
reason: normalizeAssetShowcaseReason(asset.showcaseSubmitError),
};
}
export function areAllSelectableAssetsSelected(
selectableAssets: EditorAsset[],
selectedAssetIds: Set<string>,
) {
return (
selectableAssets.length > 0 &&
selectableAssets.every((asset) => selectedAssetIds.has(asset.id))
);
}
export function renameAssetById(
assets: EditorAsset[],
assetId: string,
label: string,
) {
return assets.map((asset) =>
asset.id === assetId
? {
...asset,
label,
}
: asset,
);
}
export function toggleAssetFolderCollapsed(
assetFolders: EditorAssetFolder[],
folderId: string,
) {
return assetFolders.map((folder) =>
folder.id === folderId
? {
...folder,
collapsed: !folder.collapsed,
}
: folder,
);
}
export function createLocalAssetFolder({
folderId,
label,
}: {
folderId: string;
label: string;
}): EditorAssetFolder {
return {
id: folderId,
label,
collapsed: false,
systemDefault: false,
persisted: false,
};
}
export function replaceLocalAssetFolder({
folders,
assets,
localFolderId,
persistedFolder,
}: {
folders: EditorAssetFolder[];
assets: EditorAsset[];
localFolderId: string;
persistedFolder: {
folderId: string;
label: string;
collapsed: boolean;
systemDefault: boolean;
};
}) {
return {
folders: folders.map((folder) =>
folder.id === localFolderId
? {
id: persistedFolder.folderId,
label: persistedFolder.label,
collapsed: persistedFolder.collapsed,
systemDefault: persistedFolder.systemDefault,
persisted: true,
}
: folder,
),
assets: assets.map((asset) =>
asset.folderId === localFolderId
? {
...asset,
folderId: persistedFolder.folderId,
}
: asset,
),
};
}
export function removeAssetById(assets: EditorAsset[], assetId: string) {
return assets.filter((asset) => asset.id !== assetId);
}
export function renameAssetFolderById(
assetFolders: EditorAssetFolder[],
folderId: string,
label: string,
) {
return assetFolders.map((folder) =>
folder.id === folderId
? {
...folder,
label,
}
: folder,
);
}
export function resolveDefaultAssetFolder(assetFolders: EditorAssetFolder[]) {
return (
assetFolders.find((folder) => folder.systemDefault) ?? assetFolders[0] ?? null
);
}
export function resolveUploadAssetFolder(assetFolders: EditorAssetFolder[]) {
return (
assetFolders.find(
(folder) => folder.label.trim() === EDITOR_UPLOAD_ASSET_FOLDER_LABEL,
) ?? null
);
}
export function resolvePreferredAssetUploadFolder(
assetFolders: EditorAssetFolder[],
) {
return resolveUploadAssetFolder(assetFolders) ?? resolveDefaultAssetFolder(assetFolders);
}
export function deleteAssetFolderLocally({
folders,
assets,
folderId,
defaultFolderId,
}: {
folders: EditorAssetFolder[];
assets: EditorAsset[];
folderId: string;
defaultFolderId: string;
}) {
return {
folders: folders.filter((folder) => folder.id !== folderId),
assets: assets.map((asset) =>
asset.folderId === folderId
? {
...asset,
folderId: defaultFolderId,
}
: asset,
),
};
}
export function toggleAssetSelection(
selectedAssetIds: Set<string>,
assetId: string,
) {
const nextIds = new Set(selectedAssetIds);
if (nextIds.has(assetId)) {
nextIds.delete(assetId);
} else {
nextIds.add(assetId);
}
return nextIds;
}
export function resolveAllAssetSelection({
allSelectableAssetsSelected,
selectableAssets,
}: {
allSelectableAssetsSelected: boolean;
selectableAssets: EditorAsset[];
}) {
return allSelectableAssetsSelected
? new Set<string>()
: new Set(selectableAssets.map((asset) => asset.id));
}
export function removeSelectedAssets(
assets: EditorAsset[],
selectedAssetIds: Set<string>,
) {
const deletedAssets = assets.filter((asset) => selectedAssetIds.has(asset.id));
return {
assets: assets.filter((asset) => !selectedAssetIds.has(asset.id)),
deletedAssets,
};
}
export function moveAssetToFolderLocally(
assets: EditorAsset[],
assetId: string,
folderId: string,
) {
return assets.map((asset) =>
asset.id === assetId
? {
...asset,
folderId,
}
: asset,
);
}