移除误提交的临时诊断文件
cf397644c 的 git add -A 把一个文件名被路径转义搞坏的临时文件扫进了仓库,
其内容带行尾空白,导致 CI 的 git diff --check base...HEAD 以 exit 2 失败。
该文件是会话中一次工具调用把 Windows 绝对路径当成文件名产生的产物,与本分支
的改动无关。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
-399
@@ -1,399 +0,0 @@
|
||||
diff --git a/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx b/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx
|
||||
index b1a423324..d8434e0e1 100644
|
||||
--- a/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx
|
||||
+++ b/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Crop,
|
||||
Download,
|
||||
+ Grid2X2,
|
||||
ImageOff,
|
||||
Loader2,
|
||||
PersonStanding,
|
||||
@@ -22,6 +23,8 @@ type ImageCanvasSelectedLayerToolbarViewProps = {
|
||||
onOpenRedrawPanel: (layer: CanvasLayer) => void;
|
||||
onOpenCropExpandPanel: (layer: CanvasLayer) => void;
|
||||
onRemoveBackground: (layer: CanvasLayer) => void;
|
||||
+ onPerfectPixel: (layer: CanvasLayer) => void;
|
||||
+ isPerfectPixelProcessing: boolean;
|
||||
isSplittingIconSpritesheet: boolean;
|
||||
isPersistingAssetKind: boolean;
|
||||
onSplitIconSpritesheet: (layer: CanvasLayer) => void;
|
||||
@@ -37,6 +40,8 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
onOpenRedrawPanel,
|
||||
onOpenCropExpandPanel,
|
||||
onRemoveBackground,
|
||||
+ onPerfectPixel,
|
||||
+ isPerfectPixelProcessing,
|
||||
isSplittingIconSpritesheet,
|
||||
isPersistingAssetKind,
|
||||
onSplitIconSpritesheet,
|
||||
@@ -120,6 +125,47 @@ export function ImageCanvasSelectedLayerToolbarView({
|
||||
icon={ImageOff}
|
||||
onClick={() => onRemoveBackground(selectedLayer)}
|
||||
/>
|
||||
+ <PlatformIconButton
|
||||
+ className="image-canvas-editor__floating-toolbar-text-button"
|
||||
+ // 中文注释:保存态名称不能直接用「素材类型保存中」——icon-spritesheet 图层上
|
||||
+ // 拆分图集按钮同时显示该文案,两个控件会撞同一个无障碍名称。
|
||||
+ label={
|
||||
+ isPersistingAssetKind
|
||||
+ ? '完美像素等待素材类型保存'
|
||||
+ : isPerfectPixelProcessing
|
||||
+ ? '完美像素处理中'
|
||||
+ : '完美像素'
|
||||
+ }
|
||||
+ title={
|
||||
+ isPersistingAssetKind
|
||||
+ ? '完美像素等待素材类型保存'
|
||||
+ : isPerfectPixelProcessing
|
||||
+ ? '完美像素处理中'
|
||||
+ : '自动识别并规整像素网格'
|
||||
+ }
|
||||
+ icon={
|
||||
+ isPersistingAssetKind || isPerfectPixelProcessing ? (
|
||||
+ <Loader2 className="h-4 w-4 animate-spin" />
|
||||
+ ) : (
|
||||
+ <Grid2X2 className="h-4 w-4" />
|
||||
+ )
|
||||
+ }
|
||||
+ // 中文注释:素材类型保存在途时必须一并禁用。请求同时带 assetKind 和
|
||||
+ // sourceResourceId,本地类型已改但资源尚未落库时两者不一致,后端
|
||||
+ // resolve_editor_pixel_art_snap_asset_kind 会直接 400,只留下失败占位。
|
||||
+ // 与相邻的拆分图集按钮保持同一套门禁。
|
||||
+ disabled={isPersistingAssetKind || isPerfectPixelProcessing}
|
||||
+ aria-busy={isPersistingAssetKind || isPerfectPixelProcessing}
|
||||
+ onClick={() => onPerfectPixel(selectedLayer)}
|
||||
+ >
|
||||
+ <span>
|
||||
+ {isPersistingAssetKind
|
||||
+ ? '保存中'
|
||||
+ : isPerfectPixelProcessing
|
||||
+ ? '处理中'
|
||||
+ : '完美像素'}
|
||||
+ </span>
|
||||
+ </PlatformIconButton>
|
||||
</>
|
||||
) : null}
|
||||
{selectedLayer.assetKind === 'icon-spritesheet' ? (
|
||||
diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts
|
||||
index e40b1e752..43b404216 100644
|
||||
--- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts
|
||||
+++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
+import { ApiClientError } from '../../services/apiClient';
|
||||
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
|
||||
import { uploadEditorMediaAssetFile } from '../../services/image-editor/editorMediaAssetUploadClient';
|
||||
import {
|
||||
@@ -18,9 +19,11 @@ import {
|
||||
type EditorProjectLayerSnapshot,
|
||||
type EditorProjectResourceSnapshot,
|
||||
type EditorProjectSnapshot,
|
||||
+ loadEditorProject,
|
||||
splitEditorIconSpritesheet,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
|
||||
+import { isCanvasGenerationDialogLayoutItem } from './ImageCanvasEditorModel';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasHistoryAction,
|
||||
@@ -93,6 +96,7 @@ import {
|
||||
buildCropExpandInsetsFromFrame,
|
||||
removeImageBackground,
|
||||
renderCropExpandImage,
|
||||
+ snapImageToPerfectPixels,
|
||||
} from './ImageCanvasRasterEditModel';
|
||||
import {
|
||||
createUiAssetExtractionDraftMark,
|
||||
@@ -628,6 +632,8 @@ type GenerationWorkflowOptions = {
|
||||
project: EditorProjectSnapshot,
|
||||
action?: CanvasHistoryAction,
|
||||
) => void;
|
||||
+ flushProjectPersistence?: () => Promise<void>;
|
||||
+ refreshAssetLibrary?: () => Promise<unknown> | void;
|
||||
onWalletBalanceMayHaveChanged?: () => void;
|
||||
};
|
||||
|
||||
@@ -662,6 +668,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
assetFolderId,
|
||||
upsertGeneratedAsset,
|
||||
applyProjectSnapshot,
|
||||
+ flushProjectPersistence,
|
||||
+ refreshAssetLibrary,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
}: GenerationWorkflowOptions) {
|
||||
const [isTaskSidebarOpen, setIsTaskSidebarOpen] = useState(false);
|
||||
@@ -677,6 +685,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 +1746,241 @@ 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;
|
||||
+ let perfectPixelPostAttempted = false;
|
||||
+ try {
|
||||
+ const assetLabel = `${sourceLayer.title} · 完美像素`;
|
||||
+ const placement = openPlacedCanvasGenerationDialog({
|
||||
+ ...createQuickEditGenerationDialogDraft({
|
||||
+ sourceLayer,
|
||||
+ prompt: '完美像素',
|
||||
+ assetLabel,
|
||||
+ status: 'generating',
|
||||
+ frame: {
|
||||
+ width: sourceLayer.width,
|
||||
+ height: sourceLayer.height,
|
||||
+ },
|
||||
+ }),
|
||||
+ composerOpen: false,
|
||||
+ // 中文注释:完美像素是同步 HTTP,服务端不建 durable job(见 snap_editor_image_to_pixel_art
|
||||
+ // 无 enqueue / spawn)。刷新掉本页会话后,没有任何东西会把这个占位推向终态,而
|
||||
+ // 服务端 409 门禁又要求占位必须先落库,所以只能在读取侧收口:置位后由项目首次
|
||||
+ // 加载时的 dropDeadInlineGenerationPlaceholders 清掉。
|
||||
+ requiresLiveSession: true,
|
||||
+ });
|
||||
+ perfectPixelDialogId = placement.dialogId;
|
||||
+ if (!placement.placeholder) {
|
||||
+ throw new Error('无法创建完美像素处理占位');
|
||||
+ }
|
||||
+
|
||||
+ const sourceImageSrc = await resolveEditorGenerationMediaReference(
|
||||
+ sourceLayer,
|
||||
+ 'image',
|
||||
+ normalizedProjectId,
|
||||
+ );
|
||||
+ await flushProjectPersistence();
|
||||
+ const sourceResourceId = sourceLayer.resourceId.trim();
|
||||
+ perfectPixelPostAttempted = true;
|
||||
+ 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) {
|
||||
+ // 中文注释:只有真正发出过 POST 才谈得上「结果可能已落库」。占位创建、源图解析
|
||||
+ // 和 flush 都在 POST 之前,它们失败时请求根本没发出去,此时提示核对素材库是反向
|
||||
+ // 谎报。
|
||||
+ //
|
||||
+ // 一旦发出过 POST,就不能只用「服务端有没有响应」判定结果是否已知。服务端持久化
|
||||
+ // 是非事务的(OSS + asset object → project resource → editor asset → canvas
|
||||
+ // completion),而 completion 失败走 map_editor_project_error 会被映射成
|
||||
+ // 403 / 404 / 409 / 400,带响应的 4xx 同样可能发生在对象、资源和账号素材全部落库
|
||||
+ // 之后。状态码分不出持久化阶段,所以服务端在第一次 OSS PUT 之后的失败路径上置
|
||||
+ // `resultPersistenceStarted`,客户端只对这类失败和完全无响应的失败做对账——常见的
|
||||
+ // 纯校验 400 / 排队 503 / 预算 504 不会白白多两次读取,也不会被附上不适用的提示。
|
||||
+ const persistenceMayHaveStarted =
|
||||
+ error instanceof ApiClientError &&
|
||||
+ (error.details as { resultPersistenceStarted?: unknown } | null)
|
||||
+ ?.resultPersistenceStarted === true;
|
||||
+ const outcomeMayBePersisted =
|
||||
+ perfectPixelPostAttempted &&
|
||||
+ Boolean(perfectPixelDialogId) &&
|
||||
+ (!(error instanceof ApiClientError) || persistenceMayHaveStarted);
|
||||
+ let reconciledMessage: string | undefined;
|
||||
+ if (outcomeMayBePersisted && perfectPixelDialogId) {
|
||||
+ // 中文注释:契约要求核对「项目 / 素材」两份快照。素材库是独立记录,只 GET
|
||||
+ // 项目却让用户去核对素材库,他看到的仍是旧列表。
|
||||
+ //
|
||||
+ // 但不能因为调了它就在文案里断言「素材库已刷新」:refreshAssetLibrary 在
|
||||
+ // `canAccessProtectedData` 为 false 时直接 return,读取失败也只在鉴权错误时
|
||||
+ // 弹登录框、其余一律吞掉,返回 `Promise<void>` 不带成败信号;它本身还是可选
|
||||
+ // prop。刷新成功时用户白赚一份新列表,失败时文案必须仍然成立,所以下面只给
|
||||
+ // 「请确认」的指令,不给「已刷新」的事实断言。
|
||||
+ const [reconciled] = await Promise.all([
|
||||
+ loadEditorProject(normalizedProjectId).catch(() => null),
|
||||
+ Promise.resolve(refreshAssetLibrary?.()).catch(() => undefined),
|
||||
+ ]);
|
||||
+ if (reconciled) {
|
||||
+ const placeholderSurvived = reconciled.layers.some(
|
||||
+ (item) =>
|
||||
+ isCanvasGenerationDialogLayoutItem(item) &&
|
||||
+ (item as { dialog?: { id?: unknown } }).dialog?.id ===
|
||||
+ perfectPixelDialogId,
|
||||
+ );
|
||||
+ if (!placeholderSurvived) {
|
||||
+ // 中文注释:权威快照里占位不在了有两种原因——服务端 completion 消费掉,
|
||||
+ // 或者用户在请求期间主动删除。必须再查本地:本地占位也没了就是用户删的,
|
||||
+ // 契约要求此时不应用完成快照、不写历史,删除意图胜出。成功路径同一处有
|
||||
+ // 这道检查,对账路径不能漏。
|
||||
+ if (!hasCanvasGenerationDialogById(perfectPixelDialogId)) {
|
||||
+ return;
|
||||
+ }
|
||||
+ applyProjectSnapshot(reconciled, {
|
||||
+ type: 'perfect-pixel',
|
||||
+ count: 1,
|
||||
+ });
|
||||
+ setActiveTool('select');
|
||||
+ setActiveSidebarPanel('layers');
|
||||
+ return;
|
||||
+ }
|
||||
+ // 中文注释:占位仍在,画布没收到结果。这里刻意不调 applyProjectSnapshot:
|
||||
+ // 传给本 hook 的是 ImageCanvasEditorView 的 applyGeneratedProjectSnapshot,
|
||||
+ // 它的 action 默认值是 `generate-image`,不传 action 会写一条类型错误且受
|
||||
+ // 撤销保护的历史;而权威快照此刻和本地状态一致(占位都在),套用它只会白白
|
||||
+ // 覆盖用户在请求期间的未保存编辑。这次 GET 的用途是判定而不是同步。
|
||||
+ reconciledMessage = '画布未收到完美像素结果。';
|
||||
+ } else {
|
||||
+ reconciledMessage = '权威项目快照读取失败,无法确认完美像素结果。';
|
||||
+ }
|
||||
+ }
|
||||
+ const serverMessage =
|
||||
+ error instanceof Error && error.message.trim()
|
||||
+ ? error.message
|
||||
+ : undefined;
|
||||
+ // 中文注释:保留服务端原文(例如 assetKind 校验失败)便于定位,同时附上对账结论。
|
||||
+ // 持久化非事务,对象与账号素材可能已落库而只有画布回填没完成,所以必须让用户先核对
|
||||
+ // 素材库再决定,而不是直接重试。尾句只下指令、不断言素材库已经刷新——上面那次刷新
|
||||
+ // 可能是 no-op 或被静默吞掉,断言会让用户对着旧列表判定「没有派生图,可以重试」。
|
||||
+ const errorMessage = reconciledMessage
|
||||
+ ? `${serverMessage ? `${serverMessage} ` : ''}${reconciledMessage}请确认素材库是否已生成派生图,再决定是否重试。`
|
||||
+ : (serverMessage ?? '完美像素处理失败');
|
||||
+ 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,
|
||||
+ refreshAssetLibrary,
|
||||
+ projectId,
|
||||
+ setActiveSidebarPanel,
|
||||
+ setActiveTool,
|
||||
+ setCharacterAnimationPanel,
|
||||
+ setCropExpandPanel,
|
||||
+ setImageContextMenu,
|
||||
+ setMetadataLayer,
|
||||
+ setQuickEditPanel,
|
||||
+ showGenerationWarning,
|
||||
+ updateCanvasGenerationDialogById,
|
||||
+ upsertGeneratedAsset,
|
||||
+ ],
|
||||
+ );
|
||||
+
|
||||
const splitSelectedIconSpritesheet = useCallback(
|
||||
async (sourceLayer: CanvasLayer) => {
|
||||
if (
|
||||
@@ -2576,6 +2823,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
openCropExpandPanel,
|
||||
startCropExpandFrameResize,
|
||||
removeSelectedLayerBackground,
|
||||
+ snapSelectedLayerToPerfectPixels,
|
||||
+ perfectPixelLayerIds,
|
||||
splitSelectedIconSpritesheet,
|
||||
splittingIconSpritesheetLayerIds,
|
||||
taskListRefreshKey,
|
||||
@@ -2679,6 +2928,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
quickEditPanel,
|
||||
quickEditSourceLayer,
|
||||
removeSelectedLayerBackground,
|
||||
+ snapSelectedLayerToPerfectPixels,
|
||||
+ perfectPixelLayerIds,
|
||||
splitSelectedIconSpritesheet,
|
||||
splittingIconSpritesheetLayerIds,
|
||||
submitCharacterAnimation,
|
||||
Reference in New Issue
Block a user