新增画布完美像素功能

在图片工具栏接入完美像素交互、占位流程与历史保护

新增严格像素网格检测和登录态同步处理接口

补齐素材类型校验、持久化边界、并发限制及技术文档

修复 Windows rlib 门禁长文件名解析
This commit is contained in:
2026-07-30 11:42:45 +00:00
parent b512430e53
commit ee2cad377f
25 changed files with 2469 additions and 64 deletions
@@ -93,6 +93,7 @@ import {
buildCropExpandInsetsFromFrame,
removeImageBackground,
renderCropExpandImage,
snapImageToPerfectPixels,
} from './ImageCanvasRasterEditModel';
import {
createUiAssetExtractionDraftMark,
@@ -628,6 +629,7 @@ type GenerationWorkflowOptions = {
project: EditorProjectSnapshot,
action?: CanvasHistoryAction,
) => void;
flushProjectPersistence?: () => Promise<void>;
onWalletBalanceMayHaveChanged?: () => void;
};
@@ -662,6 +664,7 @@ export function useImageCanvasGenerationWorkflow({
assetFolderId,
upsertGeneratedAsset,
applyProjectSnapshot,
flushProjectPersistence,
onWalletBalanceMayHaveChanged,
}: GenerationWorkflowOptions) {
const [isTaskSidebarOpen, setIsTaskSidebarOpen] = useState(false);
@@ -677,6 +680,10 @@ export function useImageCanvasGenerationWorkflow({
const splittingIconSpritesheetLayerIdsRef = useRef(new Set<string>());
const [splittingIconSpritesheetLayerIds, setSplittingIconSpritesheetLayerIds] =
useState<Set<string>>(() => new Set());
const perfectPixelLayerIdsRef = useRef(new Set<string>());
const [perfectPixelLayerIds, setPerfectPixelLayerIds] = useState<Set<string>>(
() => new Set(),
);
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
const [isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen] =
useState(false);
@@ -1734,6 +1741,160 @@ export function useImageCanvasGenerationWorkflow({
],
);
const snapSelectedLayerToPerfectPixels = useCallback(
async (sourceLayer: CanvasLayer) => {
const normalizedProjectId = projectId?.trim();
const isStaticRasterLayer =
(sourceLayer.mediaType === undefined ||
sourceLayer.mediaType === 'image') &&
sourceLayer.assetKind !== 'character-animation';
if (
!isStaticRasterLayer ||
!normalizedProjectId ||
!applyProjectSnapshot ||
!flushProjectPersistence ||
perfectPixelLayerIdsRef.current.has(sourceLayer.id)
) {
if (
isStaticRasterLayer &&
(!normalizedProjectId ||
!applyProjectSnapshot ||
!flushProjectPersistence)
) {
showGenerationWarning('项目尚未准备好,暂时无法执行完美像素。');
}
return;
}
perfectPixelLayerIdsRef.current.add(sourceLayer.id);
setPerfectPixelLayerIds((currentLayerIds) => {
const nextLayerIds = new Set(currentLayerIds);
nextLayerIds.add(sourceLayer.id);
return nextLayerIds;
});
closeGenerationTransientState();
setImageContextMenu(null);
setMetadataLayer(null);
setCropExpandPanel(null);
setQuickEditPanel(null);
setCharacterAnimationPanel(null);
let perfectPixelDialogId: string | undefined;
try {
const assetLabel = `${sourceLayer.title} · 完美像素`;
const placement = openPlacedCanvasGenerationDialog({
...createQuickEditGenerationDialogDraft({
sourceLayer,
prompt: '完美像素',
assetLabel,
status: 'generating',
frame: {
width: sourceLayer.width,
height: sourceLayer.height,
},
}),
composerOpen: false,
});
perfectPixelDialogId = placement.dialogId;
if (!placement.placeholder) {
throw new Error('无法创建完美像素处理占位');
}
const sourceImageSrc = await resolveEditorGenerationMediaReference(
sourceLayer,
'image',
normalizedProjectId,
);
await flushProjectPersistence();
const sourceResourceId = sourceLayer.resourceId.trim();
const result = await snapImageToPerfectPixels({
sourceImageSrc,
projectId: normalizedProjectId,
sourceResourceId:
sourceResourceId &&
!sourceResourceId.startsWith('local-resource-') &&
!sourceResourceId.startsWith('generation-dialog:')
? sourceResourceId
: undefined,
assetKind: sourceLayer.assetKind,
generationInputs: sourceLayer.generationInputs,
assetFolderId,
assetLabel,
canvasCompletion: {
dialogId: perfectPixelDialogId,
title: assetLabel,
placeholder: placement.placeholder,
},
});
upsertGeneratedAsset?.(result.asset);
if (!result.project) {
if (hasCanvasGenerationDialogById(perfectPixelDialogId)) {
updateCanvasGenerationDialogById(perfectPixelDialogId, () => null);
showGenerationWarning(
'完美像素结果已保存到素材库,画布占位已不存在。',
);
}
return;
}
if (!hasCanvasGenerationDialogById(perfectPixelDialogId)) {
return;
}
applyProjectSnapshot(result.project, {
type: 'perfect-pixel',
count: 1,
});
setActiveTool('select');
setActiveSidebarPanel('layers');
} catch (error) {
const errorMessage =
error instanceof Error && error.message.trim()
? error.message
: '完美像素处理失败';
if (
perfectPixelDialogId &&
hasCanvasGenerationDialogById(perfectPixelDialogId)
) {
updateCanvasGenerationDialogById(perfectPixelDialogId, (dialog) => ({
...dialog,
status: 'failed',
errorMessage,
}));
} else if (!perfectPixelDialogId) {
showGenerationWarning(errorMessage);
}
} finally {
perfectPixelLayerIdsRef.current.delete(sourceLayer.id);
setPerfectPixelLayerIds((currentLayerIds) => {
if (!currentLayerIds.has(sourceLayer.id)) {
return currentLayerIds;
}
const nextLayerIds = new Set(currentLayerIds);
nextLayerIds.delete(sourceLayer.id);
return nextLayerIds;
});
}
},
[
applyProjectSnapshot,
assetFolderId,
closeGenerationTransientState,
flushProjectPersistence,
hasCanvasGenerationDialogById,
openPlacedCanvasGenerationDialog,
projectId,
setActiveSidebarPanel,
setActiveTool,
setCharacterAnimationPanel,
setCropExpandPanel,
setImageContextMenu,
setMetadataLayer,
setQuickEditPanel,
showGenerationWarning,
updateCanvasGenerationDialogById,
upsertGeneratedAsset,
],
);
const splitSelectedIconSpritesheet = useCallback(
async (sourceLayer: CanvasLayer) => {
if (
@@ -2576,6 +2737,8 @@ export function useImageCanvasGenerationWorkflow({
openCropExpandPanel,
startCropExpandFrameResize,
removeSelectedLayerBackground,
snapSelectedLayerToPerfectPixels,
perfectPixelLayerIds,
splitSelectedIconSpritesheet,
splittingIconSpritesheetLayerIds,
taskListRefreshKey,
@@ -2679,6 +2842,8 @@ export function useImageCanvasGenerationWorkflow({
quickEditPanel,
quickEditSourceLayer,
removeSelectedLayerBackground,
snapSelectedLayerToPerfectPixels,
perfectPixelLayerIds,
splitSelectedIconSpritesheet,
splittingIconSpritesheetLayerIds,
submitCharacterAnimation,