import {
  Braces,
  Check,
  CheckSquare,
  ChevronDown,
  ChevronRight,
  ClipboardList,
  Copy,
  Crop,
  Download,
  Folder,
  FolderPlus,
  Hand,
  ImageIcon,
  ImagePlus,
  Info,
  Layers,
  Map as MapIcon,
  MousePointer2,
  Pencil,
  PencilLine,
  RotateCcw,
  Shapes,
  SlidersHorizontal,
  Sparkles,
  Square,
  Trash2,
  Type,
  WandSparkles,
  X,
} from 'lucide-react';
import {
  type CSSProperties,
  type DragEvent as ReactDragEvent,
  type PointerEvent as ReactPointerEvent,
  type ReactNode,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type WheelEvent as ReactWheelEvent,
} from 'react';
import { createPortal } from 'react-dom';

import { ApiClientError } from '../../services/apiClient';
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
import {
  createEditorAsset,
  createEditorAssetFolder,
  createEditorProjectResource,
  deleteEditorAsset,
  deleteEditorAssetFolder,
  editEditorImage,
  type EditorAssetLibrarySnapshot,
  type EditorCharacterAnimationFrameCount,
  type EditorCharacterAnimationGenerationResult,
  type EditorCharacterAnimationRatio,
  type EditorCharacterAnimationResolution,
  type EditorIconSpritesheetGenerationResult,
  type EditorIconSpritesheetIconResult,
  type EditorImageGenerationResult,
  type EditorProjectLayerSnapshot,
  generateEditorCharacterAnimation,
  generateEditorIconSpritesheet,
  generateEditorImage,
  loadEditorAssetLibrary,
  loadEditorProject,
  loadOrCreateRecentEditorProject,
  saveEditorProjectLayout,
  updateEditorAsset,
  updateEditorAssetFolder,
} from '../../services/image-editor/editorProjectClient';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformBatchActionToolbar } from '../common/PlatformBatchActionToolbar';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import {
  PlatformFloatingMenu,
  PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import {
  PlatformSelectField,
  PlatformTextField,
} from '../common/PlatformTextField';
import { UnifiedModal } from '../common/UnifiedModal';
import {
  EditorIconButton,
  SidebarMediaItem,
} from './ImageCanvasEditorPrimitives';

type EditorAsset = {
  id: string;
  label: string;
  src: string;
  width: number;
  height: number;
  folderId: string;
  sourceKind: 'built-in' | 'uploaded';
  sourceType: CanvasLayer['sourceType'];
  persisted: boolean;
  prompt?: string;
  actualPrompt?: string;
  model?: string;
  provider?: string;
  taskId?: string;
  objectKey?: string;
  assetObjectId?: string;
};

type CanvasGenerationInputField = {
  title: string;
  value: string;
};

type CanvasGenerationInputReference = {
  title: string;
  label: string;
  src: string;
};

type CanvasGenerationInputs = {
  fields: CanvasGenerationInputField[];
  references: CanvasGenerationInputReference[];
};

type CanvasLayer = {
  id: string;
  resourceId: string;
  title: string;
  src: string;
  x: number;
  y: number;
  width: number;
  height: number;
  originalWidth: number;
  originalHeight: number;
  zIndex: number;
  sourceType: 'uploaded' | 'generated' | 'mock_generated';
  prompt?: string | null;
  actualPrompt?: string | null;
  model?: string | null;
  provider?: string | null;
  taskId?: string | null;
  objectKey?: string | null;
  assetObjectId?: string | null;
  sourceResourceId?: string | null;
  groupId?: string | null;
  assetKind?: 'spec' | 'character' | 'icon' | 'icon-spec' | null;
  generationInputs?: CanvasGenerationInputs | null;
};

type CanvasViewport = {
  x: number;
  y: number;
  scale: number;
};

type CanvasTool =
  | 'select'
  | 'hand'
  | 'upload'
  | 'generate'
  | 'spec'
  | 'character'
  | 'icon'
  | 'text'
  | 'shape'
  | 'export';

type SidebarPanel = 'assets' | 'layers';

type EditorAssetFolder = {
  id: string;
  label: string;
  collapsed: boolean;
  systemDefault: boolean;
  persisted: boolean;
};

type GenerateDialogState = {
  id?: string;
  mode: 'generate' | 'edit' | 'spec' | 'character' | 'icon';
  prompt: string;
  status: 'idle' | 'generating' | 'failed';
  composerOpen?: boolean;
  sourceLayerId?: string;
  generatedLayerId?: string;
  specType?: SpecGenerationType;
  specValues?: SpecFormValues;
  characterSpecReference?: CharacterReferenceImage | null;
  characterReferences?: CharacterReferenceImage[];
  iconSpecReference?: CharacterReferenceImage | null;
  iconDescriptions?: string[];
  errorMessage?: string;
  placeholder?: {
    x: number;
    y: number;
    width: number;
    height: number;
    originalWidth: number;
    originalHeight: number;
  };
};

type CanvasGenerationDialogMode = Exclude<GenerateDialogState['mode'], 'edit'>;

type CanvasGenerationDialogState = GenerateDialogState & {
  id: string;
  mode: CanvasGenerationDialogMode;
};

type SpecGenerationType = 'character' | 'ui' | 'icon' | 'custom';

type SpecFormValues = {
  playSetting: string;
  artStyle: string;
  bodyRatio: string;
  characterView: string;
  customPrompt: string;
};

type CharacterReferenceImage = {
  id: string;
  label: string;
  src: string;
};

type ImageContextMenuState = {
  layerId: string;
  x: number;
  y: number;
};

type QuickEditPanelState = {
  sourceLayerId: string;
  prompt: string;
  size: string;
  model: string;
  status: 'idle' | 'generating' | 'failed';
  errorMessage?: string;
};

type CharacterAnimationPanelState = {
  sourceLayerId: string;
  promptText: string;
  resolution: EditorCharacterAnimationResolution;
  ratio: EditorCharacterAnimationRatio;
  frameCount: EditorCharacterAnimationFrameCount;
  durationSeconds: 4 | 5 | 6;
  status: 'idle' | 'generating' | 'completed' | 'failed';
  errorMessage?: string;
  result?: EditorCharacterAnimationGenerationResult;
};

type UploadTarget =
  | 'asset'
  | 'character-spec'
  | 'character-reference'
  | 'icon-spec';

type SnapGuide = {
  vertical?: number;
  horizontal?: number;
};

type SnapCandidate = {
  position: number;
  guide: number;
  distance: number;
};

type AssetMarqueeState = {
  pointerId: number;
  startX: number;
  startY: number;
  currentX: number;
  currentY: number;
};

type CanvasMarqueeState = {
  pointerId: number;
  startX: number;
  startY: number;
  currentX: number;
  currentY: number;
};

type DragState =
  | {
      kind: 'pan';
      pointerId: number;
      startClientX: number;
      startClientY: number;
      startViewport: CanvasViewport;
    }
  | {
      kind: 'layer';
      pointerId: number;
      layerId: string;
      layerIds: string[];
      startClientX: number;
      startClientY: number;
      startLayerX: number;
      startLayerY: number;
      startLayers: Array<{ id: string; x: number; y: number }>;
      startScale: number;
    }
  | {
      kind: 'generation-frame';
      dialogId: string;
      pointerId: number;
      startClientX: number;
      startClientY: number;
      startFrameX: number;
      startFrameY: number;
      startScale: number;
    }
  | {
      kind: 'minimap';
      pointerId: number;
    };

const EDITOR_ASSETS: EditorAsset[] = [
  {
    id: 'puzzle',
    label: '拼图素材',
    src: '/creation-type-references/puzzle.webp',
    width: 640,
    height: 640,
    folderId: 'project',
    sourceKind: 'built-in',
    sourceType: 'uploaded',
    persisted: false,
  },
  {
    id: 'match3d',
    label: '抓大鹅素材',
    src: '/creation-type-references/match3d.webp',
    width: 640,
    height: 640,
    folderId: 'project',
    sourceKind: 'built-in',
    sourceType: 'uploaded',
    persisted: false,
  },
  {
    id: 'big-fish',
    label: '大鱼素材',
    src: '/creation-type-references/big-fish.webp',
    width: 720,
    height: 405,
    folderId: 'project',
    sourceKind: 'built-in',
    sourceType: 'uploaded',
    persisted: false,
  },
  {
    id: 'bark-battle',
    label: '声浪素材',
    src: '/creation-type-references/bark-battle.webp',
    width: 640,
    height: 900,
    folderId: 'project',
    sourceKind: 'built-in',
    sourceType: 'uploaded',
    persisted: false,
  },
  {
    id: 'visual-novel',
    label: '视觉小说素材',
    src: '/creation-type-references/visual-novel.webp',
    width: 720,
    height: 405,
    folderId: 'project',
    sourceKind: 'built-in',
    sourceType: 'uploaded',
    persisted: false,
  },
];

const EDITOR_ASSET_FOLDERS: EditorAssetFolder[] = [
  {
    id: 'project',
    label: '项目素材',
    collapsed: false,
    systemDefault: true,
    persisted: false,
  },
];

const INITIAL_LAYERS: CanvasLayer[] = [
  {
    id: 'layer-puzzle',
    resourceId: 'resource-puzzle',
    title: '拼图素材',
    src: '/creation-type-references/puzzle.webp',
    x: 470,
    y: 300,
    width: 640,
    height: 640,
    originalWidth: 640,
    originalHeight: 640,
    zIndex: 1,
    sourceType: 'uploaded',
  },
  {
    id: 'layer-big-fish',
    resourceId: 'resource-big-fish',
    title: '大鱼素材',
    src: '/creation-type-references/big-fish.webp',
    x: 930,
    y: 360,
    width: 720,
    height: 405,
    originalWidth: 720,
    originalHeight: 405,
    zIndex: 2,
    sourceType: 'uploaded',
  },
];

const CANVAS_WORLD_SIZE = 12000;
const CANVAS_WORLD_ORIGIN = CANVAS_WORLD_SIZE / 2;
const MIN_SCALE = 0.24;
const MAX_SCALE = 3.2;
const TOOLBAR_HALF_WIDTH = 132;
const DEFAULT_CANVAS_SIZE = { width: 900, height: 640 };
const SNAP_THRESHOLD_SCREEN_PX = 18;
const FIT_VIEW_PADDING = 10;
const MINIMAP_SIZE = { width: 132, height: 84 };
const MINIMAP_PADDING = 8;
const SPEC_GENERATION_COST = 5;
const SPEC_GENERATION_SIZE = '2048x1152';
const SPEC_FRAME_ORIGINAL_SIZE = { width: 2048, height: 1152 };
const SPEC_FRAME_DISPLAY_SIZE = { width: 560, height: 315 };
const CHARACTER_FRAME_ORIGINAL_SIZE = { width: 2048, height: 2048 };
const CHARACTER_FRAME_DISPLAY_SIZE = { width: 420, height: 420 };
const ICON_FRAME_ORIGINAL_SIZE = { width: 512, height: 512 };
const ICON_FRAME_DISPLAY_SIZE = { width: 360, height: 360 };
const DEFAULT_IMAGE_MODEL = 'gpt-image-2';
const ICON_DESCRIPTION_LIMIT = 100;
// 图标素材面板按描述项扩宽，避免在画布子面板里做滑动列表。
const ICON_DESCRIPTION_CARD_WIDTH_REM = 8.4;
const ICON_COMPOSER_MIN_WIDTH_REM = 28;
const ICON_COMPOSER_HORIZONTAL_CHROME_REM = 2.4;
const DEFAULT_ICON_DESCRIPTIONS = [
  '返回按钮',
  '设置按钮',
  '下一关按钮',
  '提示按钮',
  '原图按钮',
  '冻结按钮',
];
const QUICK_EDIT_SIZE_PRESETS = [
  '1024x1024',
  '1536x1024',
  '2048x1152',
  '1024x1536',
] as const;
const QUICK_EDIT_MODEL_OPTIONS = [
  { label: 'GPT Image', value: DEFAULT_IMAGE_MODEL },
] as const;
const CHARACTER_ANIMATION_MODEL = 'seedance2.0';
const CHARACTER_ANIMATION_ACTION_PROMPTS = [
  { label: '待机', text: '待机动作，轻微呼吸起伏。' },
  { label: '行走', text: '循环行走动作，步伐稳定。' },
  { label: '奔跑', text: '循环奔跑动作，动作清晰有力。' },
  { label: '跳跃', text: '起跳、滞空、落地动作。' },
  { label: '攻击', text: '攻击动作，前摇、出手、收招清晰。' },
  { label: '受击', text: '受击后短暂后仰并恢复站姿。' },
  { label: '倒下', text: '倒下动作，重心下落自然。' },
] as const;
const CHARACTER_ANIMATION_RATIO_OPTIONS: Array<{
  label: string;
  value: EditorCharacterAnimationRatio;
}> = [
  { label: '与角色图片保持同尺寸', value: 'same' },
  { label: '1:1', value: '1:1' },
  { label: '4:3', value: '4:3' },
  { label: '16:9', value: '16:9' },
  { label: '9:16', value: '9:16' },
  { label: '3:4', value: '3:4' },
];
const CHARACTER_ANIMATION_DURATION_OPTIONS = [
  { label: '32帧·4秒', frameCount: 32, durationSeconds: 4 },
  { label: '40帧·5秒', frameCount: 40, durationSeconds: 5 },
  { label: '48帧·6秒', frameCount: 48, durationSeconds: 6 },
] as const;
const CANVAS_BACKGROUND_OPTIONS = [
  { label: '白色', value: '#ffffff' },
  { label: '浅灰', value: '#f8fafc' },
  { label: '暖灰', value: '#f3f0ea' },
  { label: '冷蓝', value: '#eef6ff' },
];

const DEFAULT_SPEC_FORM_VALUES: Record<SpecGenerationType, SpecFormValues> = {
  character: {
    playSetting: '战棋类RPG玩法',
    artStyle: '像素风',
    bodyRatio: '3',
    characterView:
      '右向斜侧身站姿，保留少量正面信息，能读到面部轮廓与胸肩结构，禁止生成完全 90 度纯右视图，也禁止生成正面立绘。',
    customPrompt: '',
  },
  ui: {
    playSetting: '抓娃娃题材的抓大鹅玩法',
    artStyle: '毛茸茸',
    bodyRatio: '3',
    characterView:
      '右向斜侧身站姿，保留少量正面信息，能读到面部轮廓与胸肩结构，禁止生成完全 90 度纯右视图，也禁止生成正面立绘。',
    customPrompt: '',
  },
  icon: {
    playSetting: '休闲小游戏',
    artStyle: '清爽卡通',
    bodyRatio: '3',
    characterView:
      '右向斜侧身站姿，保留少量正面信息，能读到面部轮廓与胸肩结构，禁止生成完全 90 度纯右视图，也禁止生成正面立绘。',
    customPrompt: '',
  },
  custom: {
    playSetting: '',
    artStyle: '',
    bodyRatio: '3',
    characterView:
      '右向斜侧身站姿，保留少量正面信息，能读到面部轮廓与胸肩结构，禁止生成完全 90 度纯右视图，也禁止生成正面立绘。',
    customPrompt: '',
  },
};

const SPEC_TYPE_LABEL: Record<SpecGenerationType, string> = {
  character: '角色形象规范',
  ui: 'UI素材规范',
  icon: '图标素材规范',
  custom: '自定义规范',
};

const CHARACTER_SPEC_VIEW_OPTIONS = [
  DEFAULT_SPEC_FORM_VALUES.character.characterView,
  '左向三分之二侧身站姿',
  '左向三分之二侧身站姿，保留少量正面信息，能读到面部轮廓与胸肩结构，禁止生成完全 90 度纯左视图，也禁止生成正面立绘。',
  '右向三分之二侧身站姿，保留少量正面信息，强调面部轮廓、胸肩结构与主要装备层次。',
  '背向斜侧身站姿，保留少量侧脸信息，突出背部服饰层次、武器挂载与轮廓识别。',
];

function clamp(value: number, min: number, max: number) {
  return Math.min(max, Math.max(min, value));
}

function formatPercent(value: number) {
  return `${Math.round(value * 100)}%`;
}

function formatImageSizeValue(width: number, height: number) {
  const safeWidth = Math.max(1, Math.round(width || 1024));
  const safeHeight = Math.max(1, Math.round(height || 1024));
  return `${safeWidth}x${safeHeight}`;
}

function resolveLayerResolutionSize(
  originalWidth: number,
  originalHeight: number,
  fallback: { width: number; height: number },
) {
  // 中文注释：画布不再维护独立展示 Size，图片显示尺寸统一跟随图片原始 Resolution。
  return {
    width: Math.max(1, Math.round(originalWidth || fallback.width || 1)),
    height: Math.max(1, Math.round(originalHeight || fallback.height || 1)),
  };
}

function buildQuickEditSizeOptions(currentSize: string) {
  return Array.from(new Set([currentSize, ...QUICK_EDIT_SIZE_PRESETS]));
}

function buildQuickEditModelOptions(currentModel: string) {
  const options = [...QUICK_EDIT_MODEL_OPTIONS];
  return options.some((option) => option.value === currentModel)
    ? options
    : [{ label: currentModel, value: currentModel }, ...options];
}

function triggerPlaceholderAction(label: string) {
  window.alert(`${label}功能建设中`);
}

function createLayerFromAsset(
  asset: EditorAsset,
  index: number,
  viewport: CanvasViewport,
  screenCenter: { x: number; y: number },
): CanvasLayer {
  const { width, height } = resolveLayerResolutionSize(
    asset.width,
    asset.height,
    { width: 360, height: 360 },
  );
  const worldCenterX = (screenCenter.x - viewport.x) / viewport.scale;
  const worldCenterY = (screenCenter.y - viewport.y) / viewport.scale;
  const offset = index * 34;

  return {
    id: `layer-${asset.id}-${index}`,
    resourceId: `local-resource-${asset.id}-${index}`,
    title: asset.label,
    src: asset.src,
    x: worldCenterX - width / 2 + offset,
    y: worldCenterY - height / 2 + offset,
    width,
    height,
    originalWidth: asset.width,
    originalHeight: asset.height,
    zIndex: index + 10,
    sourceType: asset.sourceType,
    prompt: asset.prompt,
    actualPrompt: asset.actualPrompt,
    model: asset.model,
    provider: asset.provider,
    taskId: asset.taskId,
    objectKey: asset.objectKey,
    assetObjectId: asset.assetObjectId,
  } satisfies CanvasLayer;
}

function serializeLayer(layer: CanvasLayer): EditorProjectLayerSnapshot {
  return {
    layerId: layer.id,
    resourceId: layer.resourceId,
    title: layer.title,
    src: layer.src,
    x: layer.x,
    y: layer.y,
    width: layer.width,
    height: layer.height,
    originalWidth: layer.originalWidth,
    originalHeight: layer.originalHeight,
    zIndex: layer.zIndex,
    sourceType: layer.sourceType,
    prompt: layer.prompt,
    actualPrompt: layer.actualPrompt,
    model: layer.model,
    provider: layer.provider,
    taskId: layer.taskId,
    objectKey: layer.objectKey,
    assetObjectId: layer.assetObjectId,
    sourceResourceId: layer.sourceResourceId,
    groupId: layer.groupId,
    assetKind: layer.assetKind,
    generationInputs: layer.generationInputs,
  };
}

function hydrateLayer(
  snapshot: EditorProjectLayerSnapshot,
): CanvasLayer | null {
  const resourceId =
    typeof snapshot.resourceId === 'string' ? snapshot.resourceId : '';
  const layerId = typeof snapshot.layerId === 'string' ? snapshot.layerId : '';
  const src = typeof snapshot.src === 'string' ? snapshot.src : '';
  const title =
    typeof snapshot.title === 'string' ? snapshot.title : '画布图片';
  if (!resourceId || !layerId || !src) {
    return null;
  }

  return {
    id: layerId,
    resourceId,
    title,
    src,
    x: numberFromSnapshot(snapshot.x, 0),
    y: numberFromSnapshot(snapshot.y, 0),
    ...(() => {
      const originalWidth = numberFromSnapshot(snapshot.originalWidth, 320);
      const originalHeight = numberFromSnapshot(snapshot.originalHeight, 320);
      return {
        ...resolveLayerResolutionSize(originalWidth, originalHeight, {
          width: numberFromSnapshot(snapshot.width, 320),
          height: numberFromSnapshot(snapshot.height, 320),
        }),
        originalWidth,
        originalHeight,
      };
    })(),
    zIndex: numberFromSnapshot(snapshot.zIndex, 1),
    sourceType: isCanvasSourceType(snapshot.sourceType)
      ? snapshot.sourceType
      : 'uploaded',
    prompt: stringOrNull(snapshot.prompt),
    actualPrompt: stringOrNull(snapshot.actualPrompt),
    model: stringOrNull(snapshot.model),
    provider: stringOrNull(snapshot.provider),
    taskId: stringOrNull(snapshot.taskId),
    objectKey: stringOrNull(snapshot.objectKey),
    assetObjectId: stringOrNull(snapshot.assetObjectId),
    sourceResourceId: stringOrNull(snapshot.sourceResourceId),
    groupId: stringOrNull(snapshot.groupId),
    assetKind: canvasAssetKindOrNull(snapshot.assetKind),
    generationInputs: generationInputsOrNull(snapshot.generationInputs),
  };
}

function mapAssetLibrarySnapshot(library: EditorAssetLibrarySnapshot): {
  folders: EditorAssetFolder[];
  assets: EditorAsset[];
} {
  return {
    folders: library.folders.map((folder) => ({
      id: folder.folderId,
      label: folder.label,
      collapsed: folder.collapsed,
      systemDefault: folder.systemDefault,
      persisted: true,
    })),
    assets: library.assets.map((asset) => ({
      id: asset.assetId,
      label: asset.label,
      src: asset.imageSrc,
      width: asset.width,
      height: asset.height,
      folderId: asset.folderId,
      sourceKind: 'uploaded',
      sourceType: asset.sourceType,
      persisted: true,
      prompt: asset.prompt ?? undefined,
      actualPrompt: asset.actualPrompt ?? undefined,
      model: asset.model ?? undefined,
      provider: asset.provider ?? undefined,
      taskId: asset.taskId ?? undefined,
      objectKey: asset.objectKey ?? undefined,
      assetObjectId: asset.assetObjectId ?? undefined,
    })),
  };
}

function mergeAssetLibraryWithBuiltIns(library: EditorAssetLibrarySnapshot) {
  const mapped = mapAssetLibrarySnapshot(library);
  const persistedFolderIds = new Set(mapped.folders.map((folder) => folder.id));
  const builtInFolders = EDITOR_ASSET_FOLDERS.filter(
    (folder) => !persistedFolderIds.has(folder.id),
  );
  return {
    folders: [...mapped.folders, ...builtInFolders],
    assets: [...EDITOR_ASSETS, ...mapped.assets],
  };
}

function numberFromSnapshot(value: unknown, fallback: number) {
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}

function stringOrNull(value: unknown) {
  return typeof value === 'string' && value.trim() ? value : null;
}

function generationInputsOrNull(value: unknown): CanvasGenerationInputs | null {
  if (!value || typeof value !== 'object') {
    return null;
  }
  const snapshot = value as {
    fields?: unknown;
    references?: unknown;
  };
  const fields = Array.isArray(snapshot.fields)
    ? snapshot.fields.flatMap((field) => {
        if (!field || typeof field !== 'object') {
          return [];
        }
        const item = field as { title?: unknown; value?: unknown };
        const title = stringOrNull(item.title);
        const fieldValue = stringOrNull(item.value);
        return title && fieldValue ? [{ title, value: fieldValue }] : [];
      })
    : [];
  const references = Array.isArray(snapshot.references)
    ? snapshot.references.flatMap((reference) => {
        if (!reference || typeof reference !== 'object') {
          return [];
        }
        const item = reference as {
          title?: unknown;
          label?: unknown;
          src?: unknown;
        };
        const title = stringOrNull(item.title);
        const label = stringOrNull(item.label);
        const src = stringOrNull(item.src);
        return title && label && src ? [{ title, label, src }] : [];
      })
    : [];

  return fields.length || references.length ? { fields, references } : null;
}

function canvasAssetKindOrNull(value: unknown): CanvasLayer['assetKind'] {
  return value === 'spec' ||
    value === 'character' ||
    value === 'icon' ||
    value === 'icon-spec'
    ? value
    : null;
}

function isCanvasSourceType(
  value: unknown,
): value is CanvasLayer['sourceType'] {
  return (
    value === 'uploaded' || value === 'generated' || value === 'mock_generated'
  );
}

function isGeneratedLayer(layer: CanvasLayer) {
  return (
    layer.sourceType === 'generated' || layer.sourceType === 'mock_generated'
  );
}

function buildCharacterSpecPrompt(values: SpecFormValues) {
  return [
    '生成2D 角色美术视觉规范设定图，纯白底板，整齐排布全身标准立绘；固定统一头身比例、勾线粗细恒定；展示待机行走攻击基础动作帧样例，重心对齐不变位，服饰配饰分层结构示意，搭配专属角色色卡标注色号，无多余杂物，精准尺寸标注，高清矢量规范稿',
    '禁止模糊、笔触杂乱、光影方向混乱、比例畸形、3D 渲染、实景照片、水印、花纹堆砌、画面抖动错位效果、噪点，',
    `玩法设计：${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.character.playSetting}`,
    `美术风格：${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.character.artStyle}`,
    `头身比：${values.bodyRatio.trim() || DEFAULT_SPEC_FORM_VALUES.character.bodyRatio}`,
    `视角要求：${values.characterView.trim() || DEFAULT_SPEC_FORM_VALUES.character.characterView}`,
  ].join('\n');
}

function buildUiSpecPrompt(values: SpecFormValues) {
  return [
    '生成一张完整游戏UI规范汇总设定展板，纯白色干净背景，Figma专业设计稿质感，矢量锐利线条，页面划分九大区域：色彩规范、字体规范、图标规范、按钮规范、组件规范、布局规范、特效规范、IP规范、主视觉。主视觉居中较大显示，其他八个区域环绕主视觉',
    '',
    `玩法设定：${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.ui.playSetting}`,
    `美术风格：${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.ui.artStyle}`,
  ].join('\n');
}

function buildIconSpecPrompt(values: SpecFormValues) {
  return [
    '生成一张游戏图标素材视觉规范展板，纯白色干净背景，展示按钮图标的统一视角、线条粗细、填充风格、描边、阴影、圆角、材质、状态层级和色彩规范，图标样例需要成组排列且风格高度统一。',
    '',
    `玩法设定：${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.icon.playSetting}`,
    `美术风格：${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.icon.artStyle}`,
  ].join('\n');
}

function buildSpecPrompt(type: SpecGenerationType, values: SpecFormValues) {
  if (type === 'character') {
    return buildCharacterSpecPrompt(values);
  }
  if (type === 'ui') {
    return buildUiSpecPrompt(values);
  }
  if (type === 'icon') {
    return buildIconSpecPrompt(values);
  }
  return values.customPrompt.trim();
}

function getLayerKindLabel(layer: CanvasLayer) {
  if (layer.assetKind === 'spec') {
    return '规范';
  }
  if (layer.assetKind === 'character') {
    return '角色';
  }
  if (layer.assetKind === 'icon') {
    return '图标';
  }
  if (layer.assetKind === 'icon-spec') {
    return '图标规范';
  }
  return null;
}

function formatLayerImageType(layer: CanvasLayer) {
  if (layer.assetKind === 'spec') {
    return '规范图片';
  }
  if (layer.assetKind === 'character') {
    return '角色图片';
  }
  if (layer.assetKind === 'icon') {
    return '图标素材图片';
  }
  if (layer.assetKind === 'icon-spec') {
    return '图标素材规范图片';
  }
  return isGeneratedLayer(layer) ? '生成图片' : '上传图片';
}

function calculateCharacterAnimationPrice(
  resolution: EditorCharacterAnimationResolution,
  durationSeconds: number,
) {
  return (resolution === '720p' ? 20 : 10) * durationSeconds;
}

function resolveCharacterAnimationSourceImageSrc(layer: CanvasLayer) {
  // 中文注释：角色图已持久化到 OSS 时优先传 objectKey，避免把大 Data URL 塞进 JSON 请求体触发 body limit。
  return layer.objectKey?.trim() || layer.src;
}

function createCanvasLayerReference(
  layer: CanvasLayer,
): CharacterReferenceImage {
  return {
    id: `canvas-${layer.id}`,
    label: layer.title,
    src: layer.src,
  };
}

function createGenerationInputField(
  title: string,
  value: string | null | undefined,
): CanvasGenerationInputField[] {
  const normalizedValue = value?.trim();
  return normalizedValue ? [{ title, value: normalizedValue }] : [];
}

function buildImageGenerationInputs(prompt: string): CanvasGenerationInputs {
  return {
    fields: createGenerationInputField('生成提示词', prompt),
    references: [],
  };
}

function buildSpecGenerationInputs(
  specType: SpecGenerationType,
  values: SpecFormValues,
): CanvasGenerationInputs {
  if (specType === 'custom') {
    return {
      fields: createGenerationInputField('自定义规范提示词', values.customPrompt),
      references: [],
    };
  }

  const baseFields = [
    ...createGenerationInputField('玩法设定', values.playSetting),
    ...createGenerationInputField('美术风格', values.artStyle),
  ];
  if (specType === 'character') {
    baseFields.push(
      ...createGenerationInputField('头身比', values.bodyRatio),
      ...createGenerationInputField('角色视角', values.characterView),
    );
  }
  return {
    fields: baseFields,
    references: [],
  };
}

function buildCharacterGenerationInputs(
  prompt: string,
  specReference: CharacterReferenceImage | null | undefined,
  references: CharacterReferenceImage[] | undefined,
): CanvasGenerationInputs {
  return {
    fields: createGenerationInputField('角色设定', prompt),
    references: [
      ...(specReference
        ? [
            {
              title: '角色形象规范',
              label: specReference.label,
              src: specReference.src,
            },
          ]
        : []),
      ...(references ?? []).map((reference, index) => ({
        title: `常规参考图 ${index + 1}`,
        label: reference.label,
        src: reference.src,
      })),
    ],
  };
}

function buildIconGenerationInputs(
  iconDescriptions: string[],
  specReference: CharacterReferenceImage,
): CanvasGenerationInputs {
  return {
    fields: iconDescriptions.map((description, index) => ({
      title: `素材描述 ${index + 1}`,
      value: description,
    })),
    references: [
      {
        title: '图标素材规范',
        label: specReference.label,
        src: specReference.src,
      },
    ],
  };
}

function buildEditGenerationInputs(
  title: '修改要求' | '快速编辑提示词',
  prompt: string,
  sourceLayer: CanvasLayer,
): CanvasGenerationInputs {
  return {
    fields: createGenerationInputField(title, prompt),
    references: [
      {
        title: '参考图',
        label: sourceLayer.title,
        src: sourceLayer.src,
      },
    ],
  };
}

function buildPortalMenuStyle(
  anchor: HTMLElement | null,
  placement: 'above' | 'below',
): CSSProperties {
  const rect = anchor?.getBoundingClientRect();
  if (!rect) {
    return {
      position: 'fixed',
      left: 0,
      top: 0,
      right: 'auto',
      bottom: 'auto',
      zIndex: 70,
    };
  }

  return {
    position: 'fixed',
    left: Math.round(rect.left),
    top:
      placement === 'above'
        ? Math.round(rect.top)
        : Math.round(rect.bottom + 8),
    right: 'auto',
    bottom: 'auto',
    zIndex: 70,
    transform:
      placement === 'above' ? 'translateY(calc(-100% - 0.45rem))' : undefined,
  };
}

function renderEditorPortal(node: ReactNode) {
  if (typeof document === 'undefined') {
    return node;
  }
  return createPortal(node, document.body);
}

function isImageFile(file: File) {
  return file.type.startsWith('image/');
}

function getLayerBounds(targetLayers: CanvasLayer[]) {
  if (targetLayers.length === 0) {
    return null;
  }

  return targetLayers.reduce(
    (current, layer) => ({
      minX: Math.min(current.minX, layer.x),
      minY: Math.min(current.minY, layer.y),
      maxX: Math.max(current.maxX, layer.x + layer.width),
      maxY: Math.max(current.maxY, layer.y + layer.height),
    }),
    {
      minX: Number.POSITIVE_INFINITY,
      minY: Number.POSITIVE_INFINITY,
      maxX: Number.NEGATIVE_INFINITY,
      maxY: Number.NEGATIVE_INFINITY,
    },
  );
}

function isEditableTarget(event: KeyboardEvent) {
  const target = event.target as HTMLElement | null;
  if (!target) {
    return false;
  }
  return (
    target.tagName === 'INPUT' ||
    target.tagName === 'TEXTAREA' ||
    target.isContentEditable
  );
}

function getPointerButton(event: ReactPointerEvent<HTMLElement>) {
  const nativeEvent = event.nativeEvent as PointerEvent;
  const nativeButtons = Number(nativeEvent.buttons);
  if (Number.isFinite(nativeButtons) && (nativeButtons & 4) === 4) {
    return 1;
  }
  const syntheticButtons = Number(event.buttons);
  if (Number.isFinite(syntheticButtons) && (syntheticButtons & 4) === 4) {
    return 1;
  }
  const syntheticButton = Number(event.button);
  if (Number.isFinite(syntheticButton)) {
    return syntheticButton;
  }
  const nativeButton = Number(nativeEvent.button);
  if (Number.isFinite(nativeButton)) {
    return nativeButton;
  }
  return 0;
}

function getPointerClient(event: ReactPointerEvent<HTMLElement>) {
  const nativeEvent = event.nativeEvent as PointerEvent;
  return {
    x: Number.isFinite(event.clientX)
      ? event.clientX
      : Number.isFinite(nativeEvent.clientX)
        ? nativeEvent.clientX
        : 0,
    y: Number.isFinite(event.clientY)
      ? event.clientY
      : Number.isFinite(nativeEvent.clientY)
        ? nativeEvent.clientY
        : 0,
  };
}

function getPointerId(event: ReactPointerEvent<HTMLElement>) {
  const nativeId = (event.nativeEvent as PointerEvent).pointerId;
  if (Number.isFinite(event.pointerId)) {
    return event.pointerId;
  }
  return Number.isFinite(nativeId) ? nativeId : -1;
}

function isCanvasGenerationDialog(
  dialog: GenerateDialogState | null,
): dialog is CanvasGenerationDialogState {
  return Boolean(
    dialog?.id &&
      (dialog.mode === 'generate' ||
        dialog.mode === 'spec' ||
        dialog.mode === 'character' ||
        dialog.mode === 'icon'),
  );
}

function getGenerationFrameAriaLabel(dialog: CanvasGenerationDialogState) {
  if (dialog.mode === 'character') {
    return '角色生成占位图';
  }
  if (dialog.mode === 'spec') {
    return '规范生成占位图';
  }
  if (dialog.mode === 'icon') {
    return '图标素材生成占位图';
  }
  return '图像生成占位图';
}

function getGenerationFrameLabel(dialog: CanvasGenerationDialogState) {
  if (dialog.mode === 'character') {
    return 'Character Generator';
  }
  if (dialog.mode === 'spec') {
    return 'Spec Generator';
  }
  if (dialog.mode === 'icon') {
    return 'Icon Generator';
  }
  return 'Image Generator';
}

function resolveImageGenerationErrorMessage(error: unknown) {
  if (
    error instanceof ApiClientError &&
    (error.status === 401 || error.status === 403)
  ) {
    return '请先登录后再生成图片';
  }

  return error instanceof Error && error.message.trim()
    ? error.message
    : '生成图片失败';
}

export function ImageCanvasEditorView() {
  const editorRootRef = useRef<HTMLElement | null>(null);
  const canvasViewportRef = useRef<HTMLDivElement | null>(null);
  const uploadInputRef = useRef<HTMLInputElement | null>(null);
  const assetListRef = useRef<HTMLDivElement | null>(null);
  const dragStateRef = useRef<DragState | null>(null);
  const isShiftPressedRef = useRef(false);
  const layerCounterRef = useRef(INITIAL_LAYERS.length);
  const generationDialogCounterRef = useRef(0);
  const saveTimerRef = useRef<number | null>(null);
  const projectIdRef = useRef<string | null>(null);
  const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
  const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
  const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
  const pendingProjectResourceLayersRef = useRef<
    Array<{
      layer: CanvasLayer;
      options: { onCreated?: (resourceId: string) => void };
    }>
  >([]);
  const selectedLayerIdRef = useRef<string | null>(null);
  const generateDialogRef = useRef<GenerateDialogState | null>(null);
  const inactiveGenerateDialogsRef = useRef<CanvasGenerationDialogState[]>([]);
  const deleteLayerByIdRef = useRef<(targetLayerId: string | null) => void>(
    () => {},
  );
  const [projectId, setProjectId] = useState<string | null>(null);
  const [isProjectReady, setIsProjectReady] = useState(false);
  const [activeSidebarPanel, setActiveSidebarPanel] =
    useState<SidebarPanel | null>('assets');
  const [viewport, setViewport] = useState<CanvasViewport>({
    x: -260,
    y: 70,
    scale: 0.82,
  });
  const [canvasSize, setCanvasSize] = useState(DEFAULT_CANVAS_SIZE);
  const [assetFolders, setAssetFolders] =
    useState<EditorAssetFolder[]>(EDITOR_ASSET_FOLDERS);
  const [assets, setAssets] = useState<EditorAsset[]>(EDITOR_ASSETS);
  const [layers, setLayers] = useState<CanvasLayer[]>(INITIAL_LAYERS);
  const [renamingAsset, setRenamingAsset] = useState<{
    assetId: string;
    value: string;
  } | null>(null);
  const [renamingFolder, setRenamingFolder] = useState<{
    folderId: string;
    value: string;
  } | null>(null);
  const [creatingFolder, setCreatingFolder] = useState(false);
  const [newFolderName, setNewFolderName] = useState('');
  const [activeUploadFolderId, setActiveUploadFolderId] = useState('project');
  const [isAssetSelectionMode, setIsAssetSelectionMode] = useState(false);
  const [selectedAssetIds, setSelectedAssetIds] = useState<Set<string>>(
    () => new Set(),
  );
  const [assetMarquee, setAssetMarquee] = useState<AssetMarqueeState | null>(
    null,
  );
  const [canvasMarquee, setCanvasMarquee] = useState<CanvasMarqueeState | null>(
    null,
  );
  const [selectedLayerId, setSelectedLayerId] = useState<string | null>(
    INITIAL_LAYERS[0]?.id ?? null,
  );
  const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>(
    INITIAL_LAYERS[0]?.id ? [INITIAL_LAYERS[0].id] : [],
  );
  const [hoveredLayerId, setHoveredLayerId] = useState<string | null>(null);
  const [activeTool, setActiveTool] = useState<CanvasTool>('select');
  const [isSpacePanning, setIsSpacePanning] = useState(false);
  const [isPanning, setIsPanning] = useState(false);
  const [snapGuide, setSnapGuide] = useState<SnapGuide | null>(null);
  const [isZoomMenuOpen, setIsZoomMenuOpen] = useState(false);
  const [isBackgroundMenuOpen, setIsBackgroundMenuOpen] = useState(false);
  const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
  const [isMinimapOpen, setIsMinimapOpen] = useState(true);
  const [canvasBackgroundColor, setCanvasBackgroundColor] = useState(
    CANVAS_BACKGROUND_OPTIONS[1]?.value ?? '#f8fafc',
  );
  const [metadataLayer, setMetadataLayer] = useState<CanvasLayer | null>(null);
  const [generateDialog, setGenerateDialog] =
    useState<GenerateDialogState | null>(null);
  const [inactiveGenerateDialogs, setInactiveGenerateDialogs] = useState<
    CanvasGenerationDialogState[]
  >([]);
  const [uploadTarget, setUploadTarget] = useState<UploadTarget>('asset');
  const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(false);
  const [
    isPickingCharacterSpecFromCanvas,
    setIsPickingCharacterSpecFromCanvas,
  ] = useState(false);
  const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false);
  const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
    useState(false);
  const [imageContextMenu, setImageContextMenu] =
    useState<ImageContextMenuState | null>(null);
  const [quickEditPanel, setQuickEditPanel] =
    useState<QuickEditPanelState | null>(null);
  const [characterAnimationPanel, setCharacterAnimationPanel] =
    useState<CharacterAnimationPanelState | null>(null);

  selectedLayerIdRef.current = selectedLayerId;
  generateDialogRef.current = generateDialog;
  inactiveGenerateDialogsRef.current = inactiveGenerateDialogs;

  const effectiveTool: CanvasTool = isSpacePanning ? 'hand' : activeTool;
  const activeCanvasGenerationDialog = isCanvasGenerationDialog(generateDialog)
    ? generateDialog
    : null;
  const canvasGenerationDialogs = useMemo(
    () =>
      activeCanvasGenerationDialog
        ? [...inactiveGenerateDialogs, activeCanvasGenerationDialog]
        : inactiveGenerateDialogs,
    [activeCanvasGenerationDialog, inactiveGenerateDialogs],
  );
  const selectedLayer = useMemo(
    () => layers.find((layer) => layer.id === selectedLayerId) ?? null,
    [layers, selectedLayerId],
  );
  const activeGenerationLayer = useMemo(
    () =>
      activeCanvasGenerationDialog?.generatedLayerId
        ? (layers.find(
            (layer) =>
              layer.id === activeCanvasGenerationDialog.generatedLayerId,
          ) ?? null)
        : null,
    [activeCanvasGenerationDialog, layers],
  );
  const generationAnchor =
    activeCanvasGenerationDialog
      ? (activeGenerationLayer ??
        activeCanvasGenerationDialog.placeholder ??
        null)
      : null;
  const generationComposerStyle =
    activeCanvasGenerationDialog?.status !== 'generating' &&
    activeCanvasGenerationDialog?.composerOpen !== false &&
    generationAnchor
      ? {
          left:
            viewport.x +
            (generationAnchor.x + generationAnchor.width / 2) * viewport.scale,
          top:
            viewport.y +
            (generationAnchor.y + generationAnchor.height) * viewport.scale +
            10,
        }
      : null;
  const iconDescriptionValues =
    activeCanvasGenerationDialog?.mode === 'icon'
      ? (activeCanvasGenerationDialog.iconDescriptions ??
        DEFAULT_ICON_DESCRIPTIONS)
      : DEFAULT_ICON_DESCRIPTIONS;
  const iconComposerStyle: CSSProperties | null =
    activeCanvasGenerationDialog?.mode === 'icon' && generationComposerStyle
      ? {
          ...generationComposerStyle,
          width: `${Math.max(
            ICON_COMPOSER_MIN_WIDTH_REM,
            ICON_COMPOSER_HORIZONTAL_CHROME_REM +
              iconDescriptionValues.length * ICON_DESCRIPTION_CARD_WIDTH_REM,
          ).toFixed(1)}rem`,
        }
      : null;
  const selectedToolbarStyle = selectedLayer
    ? {
        left: clamp(
          viewport.x +
            selectedLayer.x * viewport.scale +
            (selectedLayer.width * viewport.scale) / 2,
          TOOLBAR_HALF_WIDTH,
          Math.max(TOOLBAR_HALF_WIDTH, canvasSize.width - TOOLBAR_HALF_WIDTH),
        ),
        top: Math.max(10, viewport.y + selectedLayer.y * viewport.scale - 12),
      }
    : null;
  const characterAnimationSourceLayer = characterAnimationPanel
    ? (layers.find(
        (layer) => layer.id === characterAnimationPanel.sourceLayerId,
      ) ?? null)
    : null;
  const quickEditSourceLayer = quickEditPanel
    ? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ??
      null)
    : null;
  const quickEditPanelStyle =
    quickEditPanel && quickEditSourceLayer
      ? {
          left: clamp(
            viewport.x +
              (quickEditSourceLayer.x + quickEditSourceLayer.width / 2) *
                viewport.scale,
            12,
            Math.max(12, canvasSize.width - 12),
          ),
          top: clamp(
            viewport.y +
              (quickEditSourceLayer.y + quickEditSourceLayer.height) *
                viewport.scale +
              12,
            12,
            Math.max(12, canvasSize.height - 360),
          ),
        }
      : null;
  const quickEditSizeOptions = quickEditPanel
    ? buildQuickEditSizeOptions(quickEditPanel.size)
    : [];
  const quickEditModelOptions = quickEditPanel
    ? buildQuickEditModelOptions(quickEditPanel.model)
    : [];
  const characterAnimationPrice = characterAnimationPanel
    ? calculateCharacterAnimationPrice(
        characterAnimationPanel.resolution,
        characterAnimationPanel.durationSeconds,
      )
    : 0;
  const characterAnimationPanelStyle =
    characterAnimationPanel && characterAnimationSourceLayer
      ? {
          left: clamp(
            viewport.x +
              (characterAnimationSourceLayer.x +
                characterAnimationSourceLayer.width) *
                viewport.scale +
              12,
            12,
            Math.max(12, canvasSize.width - 364),
          ),
          top: clamp(
            viewport.y + characterAnimationSourceLayer.y * viewport.scale,
            12,
            Math.max(12, canvasSize.height - 520),
          ),
        }
      : null;
  const imageContextMenuLayer = imageContextMenu
    ? (layers.find((layer) => layer.id === imageContextMenu.layerId) ?? null)
    : null;
  const groupedAssets = useMemo(
    () =>
      assetFolders.map((folder) => ({
        ...folder,
        assets: assets.filter((asset) => asset.folderId === folder.id),
      })),
    [assetFolders, assets],
  );
  const selectableAssets = useMemo(
    () => assets.filter((asset) => asset.sourceKind === 'uploaded'),
    [assets],
  );
  const allSelectableAssetsSelected =
    selectableAssets.length > 0 &&
    selectableAssets.every((asset) => selectedAssetIds.has(asset.id));

  const createGenerationDialogId = () => {
    generationDialogCounterRef.current += 1;
    return `generation-dialog-${generationDialogCounterRef.current}`;
  };

  const archiveActiveCanvasGenerationDialog = () => {
    const currentDialog = generateDialogRef.current;
    if (!isCanvasGenerationDialog(currentDialog)) {
      return;
    }
    setInactiveGenerateDialogs((currentDialogs) =>
      currentDialogs.some((dialog) => dialog.id === currentDialog.id)
        ? currentDialogs
        : [
            ...currentDialogs,
            {
              ...currentDialog,
              composerOpen: false,
            },
          ],
    );
  };

  const openCanvasGenerationDialog = (
    dialog: Omit<CanvasGenerationDialogState, 'id'>,
  ) => {
    archiveActiveCanvasGenerationDialog();
    setGenerateDialog({
      ...dialog,
      id: createGenerationDialogId(),
    });
  };

  const updateCanvasGenerationDialogById = (
    dialogId: string,
    updater: (
      dialog: CanvasGenerationDialogState,
    ) => CanvasGenerationDialogState | null,
  ) => {
    setGenerateDialog((currentDialog) =>
      isCanvasGenerationDialog(currentDialog) &&
      currentDialog.id === dialogId
        ? updater(currentDialog)
        : currentDialog,
    );
    setInactiveGenerateDialogs((currentDialogs) =>
      currentDialogs.flatMap((dialog) => {
        if (dialog.id !== dialogId) {
          return [dialog];
        }
        const nextDialog = updater(dialog);
        return nextDialog ? [nextDialog] : [];
      }),
    );
  };

  const removeCanvasGenerationDialogById = (dialogId: string) => {
    updateCanvasGenerationDialogById(dialogId, () => null);
  };

  const activateCanvasGenerationDialog = (
    targetDialog: CanvasGenerationDialogState,
  ) => {
    setInactiveGenerateDialogs((currentDialogs) => {
      const nextDialogs = currentDialogs.filter(
        (dialog) => dialog.id !== targetDialog.id,
      );
      const currentDialog = generateDialogRef.current;
      if (
        isCanvasGenerationDialog(currentDialog) &&
        currentDialog.id !== targetDialog.id
      ) {
        nextDialogs.push({
          ...currentDialog,
          composerOpen: false,
        });
      }
      return nextDialogs;
    });
    setGenerateDialog({
      ...targetDialog,
      composerOpen: true,
    });
    setSelectedLayerId(null);
    setSelectedLayerIds([]);
    setImageContextMenu(null);
  };

  const removeCanvasGenerationDialogsByLayerId = (targetLayerId: string) => {
    const keepDialog = (dialog: CanvasGenerationDialogState) =>
      dialog.sourceLayerId !== targetLayerId &&
      dialog.generatedLayerId !== targetLayerId;
    setGenerateDialog((currentDialog) =>
      isCanvasGenerationDialog(currentDialog) && !keepDialog(currentDialog)
        ? null
        : currentDialog,
    );
    setInactiveGenerateDialogs((currentDialogs) =>
      currentDialogs.filter(keepDialog),
    );
  };

  const selectSingleLayer = useCallback((layerId: string | null) => {
    setSelectedLayerId(layerId);
    setSelectedLayerIds(layerId ? [layerId] : []);
    if (layerId) {
      setGenerateDialog((currentDialog) =>
        currentDialog?.mode === 'generate' ||
        currentDialog?.mode === 'spec' ||
        currentDialog?.mode === 'character' ||
        currentDialog?.mode === 'icon'
          ? {
              ...currentDialog,
              composerOpen: false,
            }
          : currentDialog,
      );
    }
  }, []);

  const hideGeneratedLayerPanelAfterBlur = useCallback(() => {
    setGenerateDialog((currentDialog) =>
      (currentDialog?.mode === 'generate' ||
        currentDialog?.mode === 'spec' ||
        currentDialog?.mode === 'character' ||
        currentDialog?.mode === 'icon') &&
      currentDialog.status !== 'generating'
        ? {
            ...currentDialog,
            composerOpen: false,
          }
        : currentDialog,
    );
  }, []);

  const clearCanvasFocus = useCallback(() => {
    selectSingleLayer(null);
    hideGeneratedLayerPanelAfterBlur();
    setImageContextMenu(null);
  }, [hideGeneratedLayerPanelAfterBlur, selectSingleLayer]);

  const getGeneratingDialogPlaceholder = useCallback(
    (dialog: GenerateDialogState) => {
      const currentDialog = generateDialogRef.current;
      if (dialog.id) {
        const latestDialog = [
          ...(isCanvasGenerationDialog(currentDialog) ? [currentDialog] : []),
          ...inactiveGenerateDialogsRef.current,
        ].find((candidateDialog) => candidateDialog.id === dialog.id);
        if (latestDialog?.status === 'generating') {
          return latestDialog.placeholder ?? dialog.placeholder;
        }
      }
      if (
        currentDialog?.mode === dialog.mode &&
        (!dialog.id || currentDialog.id === dialog.id) &&
        currentDialog.status === 'generating'
      ) {
        return currentDialog.placeholder ?? dialog.placeholder;
      }
      return dialog.placeholder;
    },
    [],
  );

  const createProjectResourceForLayer = useCallback(
    (
      layer: CanvasLayer,
      options: { onCreated?: (resourceId: string) => void } = {},
    ) => {
      const readyProjectId = projectIdRef.current;
      if (!readyProjectId) {
        pendingProjectResourceLayersRef.current.push({ layer, options });
        return;
      }
      createEditorProjectResource(readyProjectId, {
        imageSrc: layer.src,
        objectKey: layer.objectKey,
        assetObjectId: layer.assetObjectId,
        width: layer.originalWidth,
        height: layer.originalHeight,
        sourceType: layer.sourceType,
        prompt: layer.prompt,
        actualPrompt: layer.actualPrompt,
        model: layer.model,
        provider: layer.provider,
        taskId: layer.taskId,
        sourceResourceId: layer.sourceResourceId,
      })
        .then((resource) => {
          options.onCreated?.(resource.resourceId);
          setLayers((currentLayers) =>
            currentLayers.map((currentLayer) =>
              currentLayer.id === layer.id
                ? {
                    ...currentLayer,
                    resourceId: resource.resourceId,
                  }
                : currentLayer,
            ),
          );
        })
        .catch(() => {});
    },
    [],
  );

  const minimapModel = useMemo(() => {
    const layerBounds = getLayerBounds(layers);
    if (!layerBounds) {
      return null;
    }

    const visibleBounds = {
      minX: (0 - viewport.x) / viewport.scale,
      minY: (0 - viewport.y) / viewport.scale,
      maxX: (canvasSize.width - viewport.x) / viewport.scale,
      maxY: (canvasSize.height - viewport.y) / viewport.scale,
    };
    const bounds = {
      minX: Math.min(layerBounds.minX, visibleBounds.minX),
      minY: Math.min(layerBounds.minY, visibleBounds.minY),
      maxX: Math.max(layerBounds.maxX, visibleBounds.maxX),
      maxY: Math.max(layerBounds.maxY, visibleBounds.maxY),
    };
    const boundsWidth = Math.max(1, bounds.maxX - bounds.minX);
    const boundsHeight = Math.max(1, bounds.maxY - bounds.minY);
    const scale = Math.min(
      (MINIMAP_SIZE.width - MINIMAP_PADDING * 2) / boundsWidth,
      (MINIMAP_SIZE.height - MINIMAP_PADDING * 2) / boundsHeight,
    );
    const projectRect = (rect: {
      minX: number;
      minY: number;
      maxX: number;
      maxY: number;
    }) => ({
      left: MINIMAP_PADDING + (rect.minX - bounds.minX) * scale,
      top: MINIMAP_PADDING + (rect.minY - bounds.minY) * scale,
      width: Math.max(2, (rect.maxX - rect.minX) * scale),
      height: Math.max(2, (rect.maxY - rect.minY) * scale),
    });

    return {
      bounds,
      scale,
      layers: layers.map((layer) => ({
        id: layer.id,
        title: layer.title,
        rect: projectRect({
          minX: layer.x,
          minY: layer.y,
          maxX: layer.x + layer.width,
          maxY: layer.y + layer.height,
        }),
      })),
      viewport: projectRect(visibleBounds),
    };
  }, [canvasSize.height, canvasSize.width, layers, viewport]);

  useEffect(() => {
    let cancelled = false;
    const projectIdFromQuery =
      typeof window === 'undefined'
        ? null
        : new URLSearchParams(window.location.search)
            .get('projectid')
            ?.trim() || null;
    const loadProject = projectIdFromQuery
      ? loadEditorProject(projectIdFromQuery)
      : loadOrCreateRecentEditorProject();

    loadProject
      .then((project) => {
        if (cancelled) {
          return;
        }
        projectIdRef.current = project.projectId;
        setProjectId(project.projectId);
        const pendingLayers = pendingProjectResourceLayersRef.current.splice(0);
        pendingLayers.forEach(({ layer, options }) => {
          createProjectResourceForLayer(layer, options);
        });
        setViewport(project.viewport);
        const hydratedLayers = project.layers
          .map(hydrateLayer)
          .filter((layer): layer is CanvasLayer => Boolean(layer));
        if (hydratedLayers.length > 0) {
          layerCounterRef.current = hydratedLayers.length;
          setLayers(hydratedLayers);
          selectSingleLayer(hydratedLayers[0]?.id ?? null);
        }
        setIsProjectReady(true);
      })
      .catch(() => {
        if (!cancelled) {
          setIsProjectReady(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [createProjectResourceForLayer, selectSingleLayer]);

  useEffect(() => {
    let cancelled = false;
    loadEditorAssetLibrary()
      .then((library) => {
        if (cancelled) {
          return;
        }
        const nextLibrary = mergeAssetLibraryWithBuiltIns(library);
        setAssetFolders(nextLibrary.folders);
        setAssets(nextLibrary.assets);
        const defaultFolder = nextLibrary.folders.find(
          (folder) => folder.systemDefault,
        );
        setActiveUploadFolderId(
          defaultFolder?.id ?? nextLibrary.folders[0]?.id ?? 'project',
        );
      })
      .catch(() => {});
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    const viewportElement = canvasViewportRef.current;
    if (!viewportElement) {
      return undefined;
    }

    const updateCanvasSize = () => {
      setCanvasSize({
        width: viewportElement.clientWidth || DEFAULT_CANVAS_SIZE.width,
        height: viewportElement.clientHeight || DEFAULT_CANVAS_SIZE.height,
      });
    };

    updateCanvasSize();

    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', updateCanvasSize);
      return () => window.removeEventListener('resize', updateCanvasSize);
    }

    const observer = new ResizeObserver(updateCanvasSize);
    observer.observe(viewportElement);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Shift') {
        isShiftPressedRef.current = true;
      }
      if (
        (event.key === 'Backspace' || event.key === 'Delete') &&
        !event.repeat &&
        !isEditableTarget(event)
      ) {
        const currentDialog = generateDialogRef.current;
        const currentSelectedLayerId = selectedLayerIdRef.current;
        if (currentSelectedLayerId) {
          event.preventDefault();
          deleteLayerByIdRef.current(currentSelectedLayerId);
          return;
        }
        if (
          currentDialog?.placeholder &&
          currentDialog.status !== 'generating' &&
          (currentDialog.mode === 'generate' ||
            currentDialog.mode === 'spec' ||
            currentDialog.mode === 'character' ||
            currentDialog.mode === 'icon')
        ) {
          event.preventDefault();
          setGenerateDialog(null);
          setActiveTool('select');
          setIsCharacterSpecMenuOpen(false);
          setIsPickingCharacterSpecFromCanvas(false);
          setIsIconSpecMenuOpen(false);
          setIsPickingIconSpecFromCanvas(false);
          return;
        }
      }
      if (event.key === 'Escape') {
        setActiveSidebarPanel(null);
        setIsZoomMenuOpen(false);
        setIsBackgroundMenuOpen(false);
        setIsSpecMenuOpen(false);
        setImageContextMenu(null);
        setQuickEditPanel((currentPanel) =>
          currentPanel?.status === 'generating' ? currentPanel : null,
        );
        setIsCharacterSpecMenuOpen(false);
        setIsPickingCharacterSpecFromCanvas(false);
        setIsIconSpecMenuOpen(false);
        setIsPickingIconSpecFromCanvas(false);
        setGenerateDialog((currentDialog) => {
          if (!currentDialog || currentDialog.status === 'generating') {
            return currentDialog;
          }
          if (
            currentDialog.mode === 'generate' ||
            currentDialog.mode === 'spec'
          ) {
            return {
              ...currentDialog,
              composerOpen: false,
            };
          }
          if (currentDialog.mode === 'character') {
            return currentDialog;
          }
          if (currentDialog.mode === 'icon') {
            return currentDialog;
          }
          return null;
        });
        return;
      }
      if (event.code !== 'Space' || event.repeat || isEditableTarget(event)) {
        return;
      }
      event.preventDefault();
      setIsSpacePanning(true);
    };
    const handleKeyUp = (event: KeyboardEvent) => {
      if (event.key === 'Shift') {
        isShiftPressedRef.current = false;
      }
      if (event.code !== 'Space') {
        return;
      }
      event.preventDefault();
      setIsSpacePanning(false);
    };

    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('keyup', handleKeyUp);
    };
  }, []);

  useEffect(() => {
    const blockBrowserZoom = (event: WheelEvent) => {
      const editorElement = editorRootRef.current;
      if (
        editorElement &&
        event.target instanceof Node &&
        editorElement.contains(event.target) &&
        (event.ctrlKey || event.metaKey)
      ) {
        event.preventDefault();
      }
    };
    window.addEventListener('wheel', blockBrowserZoom, {
      capture: true,
      passive: false,
    });
    return () => {
      window.removeEventListener('wheel', blockBrowserZoom, { capture: true });
    };
  }, []);

  useEffect(() => {
    if (!projectId || !isProjectReady) {
      return undefined;
    }
    if (saveTimerRef.current) {
      window.clearTimeout(saveTimerRef.current);
    }

    saveTimerRef.current = window.setTimeout(() => {
      saveEditorProjectLayout(projectId, {
        viewport,
        layers: layers.map(serializeLayer),
      }).catch(() => {});
    }, 450);

    return () => {
      if (saveTimerRef.current) {
        window.clearTimeout(saveTimerRef.current);
      }
    };
  }, [isProjectReady, layers, projectId, viewport]);

  const fitLayers = useCallback(
    (targetLayers: CanvasLayer[] = layers) => {
      if (targetLayers.length === 0) {
        return;
      }

      const bounds = getLayerBounds(targetLayers);
      if (!bounds) {
        return;
      }
      const boundsWidth = Math.max(1, bounds.maxX - bounds.minX);
      const boundsHeight = Math.max(1, bounds.maxY - bounds.minY);
      const availableWidth = Math.max(
        1,
        canvasSize.width - FIT_VIEW_PADDING * 2,
      );
      const availableHeight = Math.max(
        1,
        canvasSize.height - FIT_VIEW_PADDING * 2,
      );
      const scale = clamp(
        Math.min(
          1,
          availableWidth / boundsWidth,
          availableHeight / boundsHeight,
        ),
        MIN_SCALE,
        MAX_SCALE,
      );

      setViewport({
        x: canvasSize.width / 2 - (bounds.minX + boundsWidth / 2) * scale,
        y: canvasSize.height / 2 - (bounds.minY + boundsHeight / 2) * scale,
        scale,
      });
    },
    [canvasSize.height, canvasSize.width, layers],
  );

  const updateScaleFromCenter = (nextScale: number) => {
    const viewportElement = canvasViewportRef.current;
    if (!viewportElement) {
      setViewport((currentViewport) => ({
        ...currentViewport,
        scale: clamp(nextScale, MIN_SCALE, MAX_SCALE),
      }));
      return;
    }

    const rect = viewportElement.getBoundingClientRect();
    const centerX = rect.width > 0 ? rect.width / 2 : canvasSize.width / 2;
    const centerY = rect.height > 0 ? rect.height / 2 : canvasSize.height / 2;
    setViewport((currentViewport) => {
      const scale = clamp(nextScale, MIN_SCALE, MAX_SCALE);
      const worldX = (centerX - currentViewport.x) / currentViewport.scale;
      const worldY = (centerY - currentViewport.y) / currentViewport.scale;
      return {
        x: centerX - worldX * scale,
        y: centerY - worldY * scale,
        scale,
      };
    });
  };

  const addAssetLayer = (
    asset: EditorAsset,
    position?: { x: number; y: number },
  ) => {
    setActiveUploadFolderId(asset.folderId);
    layerCounterRef.current += 1;
    const nextLayer = createLayerFromAsset(
      asset,
      layerCounterRef.current,
      viewport,
      {
        x: position?.x ?? canvasSize.width / 2,
        y: position?.y ?? canvasSize.height / 2,
      },
    );
    setLayers((currentLayers) => [...currentLayers, nextLayer]);
    selectSingleLayer(nextLayer.id);
    setHoveredLayerId(null);
    createProjectResourceForLayer(nextLayer);
  };

  const startRenamingAsset = (asset: EditorAsset) => {
    setRenamingAsset({
      assetId: asset.id,
      value: asset.label,
    });
  };

  const commitAssetRename = (asset: EditorAsset) => {
    const nextLabel = renamingAsset?.value.trim();
    if (!nextLabel) {
      setRenamingAsset(null);
      return;
    }
    setAssets((currentAssets) =>
      currentAssets.map((currentAsset) =>
        currentAsset.id === asset.id
          ? {
              ...currentAsset,
              label: nextLabel,
            }
          : currentAsset,
      ),
    );
    if (asset.persisted) {
      updateEditorAsset(asset.id, { label: nextLabel }).catch(() => {});
    }
    setRenamingAsset(null);
  };

  const toggleAssetFolder = (folderId: string) => {
    const nextFolder = assetFolders.find((folder) => folder.id === folderId);
    const nextCollapsed = !(nextFolder?.collapsed ?? false);
    setAssetFolders((currentFolders) =>
      currentFolders.map((folder) =>
        folder.id === folderId
          ? {
              ...folder,
              collapsed: !folder.collapsed,
            }
          : folder,
      ),
    );
    if (nextFolder?.persisted) {
      updateEditorAssetFolder(folderId, { collapsed: nextCollapsed }).catch(
        () => {},
      );
    }
  };

  const commitNewAssetFolder = async () => {
    const label = newFolderName.trim();
    if (!label) {
      setCreatingFolder(false);
      setNewFolderName('');
      return;
    }
    const folderId = `folder-${Date.now()}`;
    setAssetFolders((currentFolders) => [
      ...currentFolders,
      {
        id: folderId,
        label,
        collapsed: false,
        systemDefault: false,
        persisted: false,
      },
    ]);
    setActiveUploadFolderId(folderId);
    setCreatingFolder(false);
    setNewFolderName('');
    try {
      const folder = await createEditorAssetFolder(
        label,
        assetFolders.length + 100,
      );
      setAssetFolders((currentFolders) =>
        currentFolders.map((currentFolder) =>
          currentFolder.id === folderId
            ? {
                id: folder.folderId,
                label: folder.label,
                collapsed: folder.collapsed,
                systemDefault: folder.systemDefault,
                persisted: true,
              }
            : currentFolder,
        ),
      );
      setAssets((currentAssets) =>
        currentAssets.map((asset) =>
          asset.folderId === folderId
            ? {
                ...asset,
                folderId: folder.folderId,
              }
            : asset,
        ),
      );
      setActiveUploadFolderId(folder.folderId);
    } catch {
      // 本地临时文件夹仍可继续使用；下次刷新以后端为准。
    }
  };

  const deleteUploadedAsset = (asset: EditorAsset) => {
    if (asset.sourceKind !== 'uploaded') {
      return;
    }
    setAssets((currentAssets) =>
      currentAssets.filter((currentAsset) => currentAsset.id !== asset.id),
    );
    setRenamingAsset((currentRename) =>
      currentRename?.assetId === asset.id ? null : currentRename,
    );
    if (asset.persisted) {
      deleteEditorAsset(asset.id).catch(() => {});
    }
  };

  const startRenamingFolder = (folder: EditorAssetFolder) => {
    setRenamingFolder({
      folderId: folder.id,
      value: folder.label,
    });
  };

  const commitFolderRename = (folder: EditorAssetFolder) => {
    const nextLabel = renamingFolder?.value.trim();
    if (!nextLabel) {
      setRenamingFolder(null);
      return;
    }
    setAssetFolders((currentFolders) =>
      currentFolders.map((currentFolder) =>
        currentFolder.id === folder.id
          ? {
              ...currentFolder,
              label: nextLabel,
            }
          : currentFolder,
      ),
    );
    if (folder.persisted) {
      updateEditorAssetFolder(folder.id, { label: nextLabel }).catch(() => {});
    }
    setRenamingFolder(null);
  };

  const deleteAssetFolder = (folder: EditorAssetFolder) => {
    if (folder.systemDefault) {
      return;
    }
    const defaultFolder =
      assetFolders.find((currentFolder) => currentFolder.systemDefault) ??
      assetFolders[0];
    if (!defaultFolder) {
      return;
    }
    setAssetFolders((currentFolders) =>
      currentFolders.filter((currentFolder) => currentFolder.id !== folder.id),
    );
    setAssets((currentAssets) =>
      currentAssets.map((asset) =>
        asset.folderId === folder.id
          ? {
              ...asset,
              folderId: defaultFolder.id,
            }
          : asset,
      ),
    );
    if (folder.persisted) {
      deleteEditorAssetFolder(folder.id)
        .then((library) => {
          const nextLibrary = mergeAssetLibraryWithBuiltIns(library);
          setAssetFolders(nextLibrary.folders);
          setAssets(nextLibrary.assets);
        })
        .catch(() => {});
    }
  };

  const toggleAssetSelected = (assetId: string) => {
    setSelectedAssetIds((currentIds) => {
      const nextIds = new Set(currentIds);
      if (nextIds.has(assetId)) {
        nextIds.delete(assetId);
      } else {
        nextIds.add(assetId);
      }
      return nextIds;
    });
  };

  const toggleAllAssetsSelected = () => {
    setSelectedAssetIds(
      allSelectableAssetsSelected
        ? new Set()
        : new Set(selectableAssets.map((asset) => asset.id)),
    );
  };

  const deleteSelectedAssets = () => {
    const ids = [...selectedAssetIds];
    setAssets((currentAssets) =>
      currentAssets.filter((asset) => !selectedAssetIds.has(asset.id)),
    );
    setSelectedAssetIds(new Set());
    ids.forEach((assetId) => {
      void deleteEditorAsset(assetId);
    });
  };

  const closeAssetSelectionMode = () => {
    setIsAssetSelectionMode(false);
    setSelectedAssetIds(new Set());
    setAssetMarquee(null);
  };

  const updateAssetSelectionFromMarquee = (selectionRect: {
    left: number;
    right: number;
    top: number;
    bottom: number;
  }) => {
    const nextSelectedIds = new Set<string>();
    assetListRef.current
      ?.querySelectorAll<HTMLElement>('[data-asset-id]')
      .forEach((element) => {
        const assetId = element.dataset.assetId;
        if (!assetId) {
          return;
        }
        const asset = assets.find(
          (currentAsset) => currentAsset.id === assetId,
        );
        if (!asset || asset.sourceKind !== 'uploaded') {
          return;
        }
        const rect = element.getBoundingClientRect();
        const intersects =
          rect.left <= selectionRect.right &&
          rect.right >= selectionRect.left &&
          rect.top <= selectionRect.bottom &&
          rect.bottom >= selectionRect.top;
        if (intersects) {
          nextSelectedIds.add(assetId);
        }
      });
    setSelectedAssetIds(nextSelectedIds);
  };

  const handleAssetMarqueePointerDown = (
    event: ReactPointerEvent<HTMLDivElement>,
  ) => {
    if (!isAssetSelectionMode || event.button !== 0) {
      return;
    }
    const target = event.target as HTMLElement;
    if (target.closest('button, input, textarea, select, [data-asset-id]')) {
      return;
    }
    event.preventDefault();
    assetListRef.current?.setPointerCapture?.(event.pointerId);
    const rect = assetListRef.current?.getBoundingClientRect();
    const startX = event.clientX - (rect?.left ?? 0);
    const startY = event.clientY - (rect?.top ?? 0);
    setAssetMarquee({
      pointerId: event.pointerId,
      startX,
      startY,
      currentX: startX,
      currentY: startY,
    });
    setSelectedAssetIds(new Set());
  };

  const handleAssetMarqueePointerMove = (
    event: ReactPointerEvent<HTMLDivElement>,
  ) => {
    if (!assetMarquee || assetMarquee.pointerId !== event.pointerId) {
      return;
    }
    event.preventDefault();
    const containerRect = assetListRef.current?.getBoundingClientRect();
    const currentX = event.clientX - (containerRect?.left ?? 0);
    const currentY = event.clientY - (containerRect?.top ?? 0);
    const startClientX = (containerRect?.left ?? 0) + assetMarquee.startX;
    const startClientY = (containerRect?.top ?? 0) + assetMarquee.startY;
    setAssetMarquee((currentMarquee) =>
      currentMarquee
        ? {
            ...currentMarquee,
            currentX,
            currentY,
          }
        : null,
    );
    updateAssetSelectionFromMarquee({
      left: Math.min(startClientX, event.clientX),
      right: Math.max(startClientX, event.clientX),
      top: Math.min(startClientY, event.clientY),
      bottom: Math.max(startClientY, event.clientY),
    });
  };

  const handleAssetMarqueePointerUp = (
    event: ReactPointerEvent<HTMLDivElement>,
  ) => {
    if (!assetMarquee || assetMarquee.pointerId !== event.pointerId) {
      return;
    }
    event.preventDefault();
    if (assetListRef.current?.hasPointerCapture?.(event.pointerId)) {
      assetListRef.current.releasePointerCapture?.(event.pointerId);
    }
    setAssetMarquee(null);
  };

  const readImageFileAsDataUrl = (file: File) =>
    new Promise<string>((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => {
        if (typeof reader.result === 'string') {
          resolve(reader.result);
          return;
        }
        reject(new Error('图片读取失败'));
      };
      reader.onerror = () => reject(reader.error ?? new Error('图片读取失败'));
      reader.readAsDataURL(file);
    });

  const setCharacterGenerationIdle = (dialog: GenerateDialogState) => ({
    ...dialog,
    status: dialog.status === 'failed' ? 'idle' : dialog.status,
    errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
  });

  const addCharacterSpecReferenceFiles = async (files: FileList | File[]) => {
    const imageFile = Array.from(files).find(isImageFile);
    if (!imageFile) {
      window.alert('请选择图片文件');
      return;
    }

    const imageSrc = await readImageFileAsDataUrl(imageFile);
    setGenerateDialog((currentDialog) =>
      currentDialog?.mode === 'character'
        ? {
            ...setCharacterGenerationIdle(currentDialog),
            characterSpecReference: {
              id: `upload-character-spec-${Date.now()}`,
              label: imageFile.name || '角色形象规范',
              src: imageSrc,
            },
          }
        : currentDialog,
    );
  };

  const addCharacterReferenceFiles = async (files: FileList | File[]) => {
    const imageFiles = Array.from(files).filter(isImageFile);
    if (!imageFiles.length) {
      window.alert('请选择图片文件');
      return;
    }

    const references = await Promise.all(
      imageFiles.map(async (file, index) => ({
        id: `upload-character-reference-${Date.now()}-${index}`,
        label: file.name || `参考图${index + 1}`,
        src: await readImageFileAsDataUrl(file),
      })),
    );
    setGenerateDialog((currentDialog) =>
      currentDialog?.mode === 'character'
        ? {
            ...setCharacterGenerationIdle(currentDialog),
            characterReferences: [
              ...(currentDialog.characterReferences ?? []),
              ...references,
            ],
          }
        : currentDialog,
    );
  };

  const pickCharacterSpecFromLayer = (layer: CanvasLayer) => {
    setGenerateDialog((currentDialog) =>
      currentDialog?.mode === 'character'
        ? {
            ...setCharacterGenerationIdle(currentDialog),
            characterSpecReference: createCanvasLayerReference(layer),
            composerOpen: true,
          }
        : currentDialog,
    );
    setIsPickingCharacterSpecFromCanvas(false);
    setIsCharacterSpecMenuOpen(false);
    setImageContextMenu(null);
  };

  const setIconGenerationIdle = (dialog: GenerateDialogState) => ({
    ...dialog,
    status: dialog.status === 'failed' ? 'idle' : dialog.status,
    errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
  });

  const addIconSpecReferenceFiles = async (files: FileList | File[]) => {
    const imageFile = Array.from(files).find(isImageFile);
    if (!imageFile) {
      window.alert('请选择图片文件');
      return;
    }

    const imageSrc = await readImageFileAsDataUrl(imageFile);
    setGenerateDialog((currentDialog) =>
      currentDialog?.mode === 'icon'
        ? {
            ...setIconGenerationIdle(currentDialog),
            iconSpecReference: {
              id: `upload-icon-spec-${Date.now()}`,
              label: imageFile.name || '图标素材规范',
              src: imageSrc,
            },
          }
        : currentDialog,
    );
  };

  const pickIconSpecFromLayer = (layer: CanvasLayer) => {
    if (layer.assetKind !== 'icon-spec') {
      return;
    }
    setGenerateDialog((currentDialog) =>
      currentDialog?.mode === 'icon'
        ? {
            ...setIconGenerationIdle(currentDialog),
            iconSpecReference: createCanvasLayerReference(layer),
            composerOpen: true,
          }
        : currentDialog,
    );
    setIsPickingIconSpecFromCanvas(false);
    setIsIconSpecMenuOpen(false);
    setImageContextMenu(null);
  };

  const addUploadedLayer = async (
    file: File,
    options: {
      folderId?: string;
      canvasPoint?: { x: number; y: number };
      uploadIndex?: number;
      addToCanvas?: boolean;
    } = {},
  ) => {
    if (!file.type.startsWith('image/')) {
      window.alert('请选择图片文件');
      return;
    }

    const uploadIndex = options.uploadIndex ?? layerCounterRef.current + 1;
    layerCounterRef.current = Math.max(layerCounterRef.current, uploadIndex);
    const imageSrc = await readImageFileAsDataUrl(file);
    const fallbackWidth = 420;
    const fallbackHeight = 315;
    const uploadFolderId = assetFolders.some(
      (folder) => folder.id === (options.folderId ?? activeUploadFolderId),
    )
      ? (options.folderId ?? activeUploadFolderId)
      : 'project';
    const screenPoint = options.canvasPoint ?? {
      x: canvasSize.width / 2,
      y: canvasSize.height / 2,
    };
    const fallbackScreenPoint = {
      x: canvasSize.width > 0 ? canvasSize.width / 2 : 640,
      y: canvasSize.height > 0 ? canvasSize.height / 2 : 360,
    };
    const normalizedScreenPoint = {
      x: Number.isFinite(screenPoint.x) ? screenPoint.x : fallbackScreenPoint.x,
      y: Number.isFinite(screenPoint.y) ? screenPoint.y : fallbackScreenPoint.y,
    };
    const safeScale = viewport.scale > 0 ? viewport.scale : 1;
    const worldCenterX = (normalizedScreenPoint.x - viewport.x) / safeScale;
    const worldCenterY = (normalizedScreenPoint.y - viewport.y) / safeScale;
    const nextLayer: CanvasLayer = {
      id: `layer-upload-${uploadIndex}`,
      resourceId: `local-resource-upload-${uploadIndex}`,
      title: file.name || '上传图片',
      src: imageSrc,
      x: worldCenterX - fallbackWidth / 2,
      y: worldCenterY - fallbackHeight / 2,
      width: fallbackWidth,
      height: fallbackHeight,
      originalWidth: fallbackWidth,
      originalHeight: fallbackHeight,
      zIndex: uploadIndex + 10,
      sourceType: 'uploaded',
    };
    const uploadedAsset: EditorAsset = {
      id: `upload-${uploadIndex}`,
      label: file.name || '上传图片',
      src: imageSrc,
      width: fallbackWidth,
      height: fallbackHeight,
      folderId: uploadFolderId,
      sourceKind: 'uploaded',
      sourceType: 'uploaded',
      persisted: false,
    };

    if (options.addToCanvas) {
      setLayers((currentLayers) => [...currentLayers, nextLayer]);
    }
    setAssets((currentAssets) => [...currentAssets, uploadedAsset]);
    setAssetFolders((currentFolders) =>
      currentFolders.map((folder) =>
        folder.id === uploadFolderId
          ? {
              ...folder,
              collapsed: false,
            }
          : folder,
      ),
    );
    if (options.addToCanvas) {
      selectSingleLayer(nextLayer.id);
      setActiveSidebarPanel('layers');
    }

    createEditorAsset({
      folderId: uploadFolderId,
      label: uploadedAsset.label,
      imageSrc,
      width: fallbackWidth,
      height: fallbackHeight,
      sourceType: 'uploaded',
    })
      .then((asset) => {
        setAssets((currentAssets) =>
          currentAssets.map((currentAsset) =>
            currentAsset.id === uploadedAsset.id
              ? {
                  ...currentAsset,
                  id: asset.assetId,
                  folderId: asset.folderId,
                  label: asset.label,
                  src: asset.imageSrc,
                  width: asset.width,
                  height: asset.height,
                  objectKey: asset.objectKey ?? undefined,
                  assetObjectId: asset.assetObjectId ?? undefined,
                  persisted: true,
                }
              : currentAsset,
          ),
        );
      })
      .catch(() => {});

    if (options.addToCanvas) {
      createProjectResourceForLayer(nextLayer);
    }

    if (imageSrc) {
      const uploadedImage = new Image();
      uploadedImage.onload = () => {
        const originalWidth = uploadedImage.naturalWidth || fallbackWidth;
        const originalHeight = uploadedImage.naturalHeight || fallbackHeight;
        const { width, height } = resolveLayerResolutionSize(
          originalWidth,
          originalHeight,
          { width: fallbackWidth, height: fallbackHeight },
        );
        if (options.addToCanvas) {
          setLayers((currentLayers) =>
            currentLayers.map((layer) =>
              layer.id === nextLayer.id
                ? {
                    ...layer,
                    width,
                    height,
                    originalWidth,
                    originalHeight,
                    x: worldCenterX - width / 2,
                    y: worldCenterY - height / 2,
                  }
                : layer,
            ),
          );
        }
        setAssets((currentAssets) =>
          currentAssets.map((asset) =>
            asset.id === uploadedAsset.id
              ? {
                  ...asset,
                  width: originalWidth,
                  height: originalHeight,
                }
              : asset,
          ),
        );
      };
      uploadedImage.src = imageSrc;
    }
  };

  const addUploadedFiles = (
    files: FileList | File[],
    options: {
      folderId?: string;
      canvasPoint?: { x: number; y: number };
      addToCanvas?: boolean;
    } = {},
  ) => {
    Array.from(files).forEach((file, index) => {
      layerCounterRef.current += 1;
      const uploadIndex = layerCounterRef.current;
      void addUploadedLayer(file, {
        ...options,
        addToCanvas: options.addToCanvas ?? false,
        uploadIndex,
        canvasPoint: options.canvasPoint
          ? {
              x: options.canvasPoint.x + index * 28,
              y: options.canvasPoint.y + index * 28,
            }
          : undefined,
      });
    });
  };

  const deleteLayerById = (targetLayerId: string | null) => {
    if (!targetLayerId) {
      return;
    }
    setImageContextMenu(null);
    setLayers((currentLayers) => {
      const nextLayers = currentLayers.filter(
        (layer) => layer.id !== targetLayerId,
      );
      const nextSelectedLayer = nextLayers
        .slice()
        .sort((left, right) => right.zIndex - left.zIndex)[0];
      selectSingleLayer(nextSelectedLayer?.id ?? null);
      return nextLayers;
    });
    setHoveredLayerId(null);
    setMetadataLayer((currentLayer) =>
      currentLayer?.id === targetLayerId ? null : currentLayer,
    );
    setQuickEditPanel((currentPanel) =>
      currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
    );
    setCharacterAnimationPanel((currentPanel) =>
      currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
    );
    setGenerateDialog((currentDialog) =>
      currentDialog?.mode === 'edit' &&
      currentDialog.sourceLayerId === targetLayerId
        ? null
        : currentDialog,
    );
    removeCanvasGenerationDialogsByLayerId(targetLayerId);
  };

  deleteLayerByIdRef.current = deleteLayerById;

  const deleteSelectedLayer = () => deleteLayerById(selectedLayerId);

  const openGenerateDialog = () => {
    const placeholderWidth = 420;
    const placeholderHeight = 420;
    const worldCenterX = (canvasSize.width / 2 - viewport.x) / viewport.scale;
    const worldCenterY = (canvasSize.height / 2 - viewport.y) / viewport.scale;
    openCanvasGenerationDialog({
      mode: 'generate',
      prompt: '',
      status: 'idle',
      composerOpen: true,
      placeholder: {
        x: worldCenterX - placeholderWidth / 2,
        y: worldCenterY - placeholderHeight / 2,
        width: placeholderWidth,
        height: placeholderHeight,
        originalWidth: 2048,
        originalHeight: 2048,
      },
    });
    setActiveTool('generate');
    selectSingleLayer(null);
    setQuickEditPanel(null);
  };

  const openSpecDialog = (specType: SpecGenerationType) => {
    const worldCenterX = (canvasSize.width / 2 - viewport.x) / viewport.scale;
    const worldCenterY = (canvasSize.height / 2 - viewport.y) / viewport.scale;
    openCanvasGenerationDialog({
      mode: 'spec',
      prompt: '',
      status: 'idle',
      composerOpen: true,
      specType,
      specValues: { ...DEFAULT_SPEC_FORM_VALUES[specType] },
      placeholder: {
        x: worldCenterX - SPEC_FRAME_DISPLAY_SIZE.width / 2,
        y: worldCenterY - SPEC_FRAME_DISPLAY_SIZE.height / 2,
        width: SPEC_FRAME_DISPLAY_SIZE.width,
        height: SPEC_FRAME_DISPLAY_SIZE.height,
        originalWidth: SPEC_FRAME_ORIGINAL_SIZE.width,
        originalHeight: SPEC_FRAME_ORIGINAL_SIZE.height,
      },
    });
    setIsSpecMenuOpen(false);
    setActiveTool('generate');
    selectSingleLayer(null);
    setQuickEditPanel(null);
  };

  const openCharacterAnimationPanel = (layer: CanvasLayer) => {
    if (layer.assetKind !== 'character') {
      return;
    }
    setImageContextMenu(null);
    setQuickEditPanel(null);
    setCharacterAnimationPanel({
      sourceLayerId: layer.id,
      promptText: '',
      resolution: '480p',
      ratio: 'same',
      frameCount: 32,
      durationSeconds: 4,
      status: 'idle',
    });
    selectSingleLayer(layer.id);
  };

  const openCharacterGenerationDialog = () => {
    const worldCenterX = (canvasSize.width / 2 - viewport.x) / viewport.scale;
    const worldCenterY = (canvasSize.height / 2 - viewport.y) / viewport.scale;
    setIsSpecMenuOpen(false);
    setIsPickingCharacterSpecFromCanvas(false);
    openCanvasGenerationDialog({
      mode: 'character',
      prompt: '',
      status: 'idle',
      composerOpen: true,
      characterSpecReference: null,
      characterReferences: [],
      placeholder: {
        x: worldCenterX - CHARACTER_FRAME_DISPLAY_SIZE.width / 2,
        y: worldCenterY - CHARACTER_FRAME_DISPLAY_SIZE.height / 2,
        width: CHARACTER_FRAME_DISPLAY_SIZE.width,
        height: CHARACTER_FRAME_DISPLAY_SIZE.height,
        originalWidth: CHARACTER_FRAME_ORIGINAL_SIZE.width,
        originalHeight: CHARACTER_FRAME_ORIGINAL_SIZE.height,
      },
    });
    setActiveTool('character');
    selectSingleLayer(null);
    setQuickEditPanel(null);
  };

  const openIconGenerationDialog = () => {
    const worldCenterX = (canvasSize.width / 2 - viewport.x) / viewport.scale;
    const worldCenterY = (canvasSize.height / 2 - viewport.y) / viewport.scale;
    setIsSpecMenuOpen(false);
    setIsPickingCharacterSpecFromCanvas(false);
    setIsPickingIconSpecFromCanvas(false);
    openCanvasGenerationDialog({
      mode: 'icon',
      prompt: '',
      status: 'idle',
      composerOpen: true,
      iconSpecReference: null,
      iconDescriptions: [...DEFAULT_ICON_DESCRIPTIONS],
      placeholder: {
        x: worldCenterX - ICON_FRAME_DISPLAY_SIZE.width / 2,
        y: worldCenterY - ICON_FRAME_DISPLAY_SIZE.height / 2,
        width: ICON_FRAME_DISPLAY_SIZE.width,
        height: ICON_FRAME_DISPLAY_SIZE.height,
        originalWidth: ICON_FRAME_ORIGINAL_SIZE.width,
        originalHeight: ICON_FRAME_ORIGINAL_SIZE.height,
      },
    });
    setActiveTool('icon');
    selectSingleLayer(null);
    setQuickEditPanel(null);
    setCharacterAnimationPanel(null);
  };

  const openEditDialog = (sourceLayer: CanvasLayer) => {
    setMetadataLayer(null);
    setImageContextMenu(null);
    setQuickEditPanel(null);
    setGenerateDialog({
      mode: 'edit',
      prompt: sourceLayer.prompt
        ? `${sourceLayer.prompt}，在保持主体结构的基础上优化画面细节`
        : '',
      status: 'idle',
      composerOpen: true,
      sourceLayerId: sourceLayer.id,
    });
    setActiveTool('generate');
  };

  const openQuickEditPanel = (sourceLayer: CanvasLayer) => {
    setImageContextMenu(null);
    setMetadataLayer(null);
    setGenerateDialog(null);
    setCharacterAnimationPanel(null);
    setQuickEditPanel({
      sourceLayerId: sourceLayer.id,
      prompt: '',
      size: formatImageSizeValue(
        sourceLayer.originalWidth,
        sourceLayer.originalHeight,
      ),
      model: sourceLayer.model?.trim() || DEFAULT_IMAGE_MODEL,
      status: 'idle',
    });
    selectSingleLayer(sourceLayer.id);
    setActiveTool('generate');
  };

  const addGeneratedResultLayer = (
    generated: EditorImageGenerationResult,
    options: {
      sourceLayer?: CanvasLayer;
      frame?: GenerateDialogState['placeholder'];
      assetKind?: CanvasLayer['assetKind'];
      title?: string;
      dialogId?: string;
      generationInputs?: CanvasGenerationInputs;
    } = {},
  ) => {
    layerCounterRef.current += 1;
    const generatedIndex = layerCounterRef.current;
    const originalWidth = generated.width || 1024;
    const originalHeight = generated.height || 1024;
    const { width, height } = resolveLayerResolutionSize(
      originalWidth,
      originalHeight,
      { width: 1024, height: 1024 },
    );
    const worldCenterX = (canvasSize.width / 2 - viewport.x) / viewport.scale;
    const worldCenterY = (canvasSize.height / 2 - viewport.y) / viewport.scale;
    const frameX =
      options.frame && options.frame.width > 0
        ? options.frame.x + options.frame.width / 2 - width / 2
        : undefined;
    const frameY =
      options.frame && options.frame.height > 0
        ? options.frame.y + options.frame.height / 2 - height / 2
        : undefined;
    const nextLayer: CanvasLayer = {
      id: options.sourceLayer
        ? `layer-edit-${generatedIndex}`
        : `layer-generated-${generatedIndex}`,
      resourceId: options.sourceLayer
        ? `local-resource-edit-${generatedIndex}`
        : `local-resource-generated-${generatedIndex}`,
      title: options.sourceLayer
        ? `${options.sourceLayer.title} 修改结果`
        : (options.title ?? `生成图片 ${generatedIndex}`),
      src: generated.imageSrc,
      x: options.sourceLayer
        ? options.sourceLayer.x + options.sourceLayer.width + 32
        : (frameX ?? worldCenterX - width / 2),
      y: options.sourceLayer
        ? options.sourceLayer.y
        : (frameY ?? worldCenterY - height / 2),
      width,
      height,
      originalWidth,
      originalHeight,
      zIndex: generatedIndex + 10,
      sourceType: generated.sourceType,
      assetKind: options.assetKind,
      prompt: generated.prompt,
      actualPrompt: generated.actualPrompt ?? generated.prompt,
      model: generated.model,
      provider: generated.provider,
      taskId: generated.taskId,
      objectKey: generated.objectKey,
      assetObjectId: generated.assetObjectId,
      sourceResourceId: options.sourceLayer?.resourceId,
      generationInputs: options.generationInputs,
    };

    setLayers((currentLayers) => [...currentLayers, nextLayer]);
    selectSingleLayer(nextLayer.id);
    setActiveSidebarPanel('layers');
    if (options.sourceLayer) {
      setGenerateDialog(null);
      setActiveTool('select');
    } else if (options.dialogId) {
      updateCanvasGenerationDialogById(options.dialogId, (currentDialog) =>
        currentDialog.mode === 'character' || currentDialog.mode === 'icon'
          ? null
          : {
              ...currentDialog,
              status: 'idle',
              composerOpen: true,
              generatedLayerId: nextLayer.id,
              placeholder: undefined,
              errorMessage: undefined,
            },
      );
    }
    if (options.sourceLayer) {
      fitLayers([options.sourceLayer, nextLayer]);
    }
    createProjectResourceForLayer(nextLayer);
  };

  const addQuickEditResultLayer = (
    generated: EditorImageGenerationResult,
    sourceLayer: CanvasLayer,
    generationInputs: CanvasGenerationInputs,
  ) => {
    layerCounterRef.current += 1;
    const generatedIndex = layerCounterRef.current;
    const originalWidth = generated.width || sourceLayer.originalWidth || 1024;
    const originalHeight = generated.height || sourceLayer.originalHeight || 1024;
    const { width, height } = resolveLayerResolutionSize(
      originalWidth,
      originalHeight,
      {
        width: sourceLayer.width,
        height: sourceLayer.height,
      },
    );
    const nextLayer: CanvasLayer = {
      id: `layer-quick-edit-${generatedIndex}`,
      resourceId: `local-resource-quick-edit-${generatedIndex}`,
      title: `${sourceLayer.title} 快速编辑`,
      src: generated.imageSrc,
      x: sourceLayer.x + sourceLayer.width + 32,
      y: sourceLayer.y,
      width,
      height,
      originalWidth,
      originalHeight,
      zIndex: generatedIndex + 10,
      sourceType: generated.sourceType,
      prompt: generated.prompt,
      actualPrompt: generated.actualPrompt ?? generated.prompt,
      model: generated.model,
      provider: generated.provider,
      taskId: generated.taskId,
      objectKey: generated.objectKey,
      assetObjectId: generated.assetObjectId,
      sourceResourceId: sourceLayer.resourceId,
      groupId: sourceLayer.groupId,
      assetKind: sourceLayer.assetKind,
      generationInputs,
    };

    setLayers((currentLayers) => [...currentLayers, nextLayer]);
    selectSingleLayer(nextLayer.id);
    setActiveSidebarPanel('layers');
    setQuickEditPanel(null);
    setActiveTool('select');
    fitLayers([sourceLayer, nextLayer]);
    createProjectResourceForLayer(nextLayer);
  };

  const addIconSpritesheetResultLayers = (
    generated: EditorIconSpritesheetGenerationResult,
    iconResults: EditorIconSpritesheetIconResult[],
    generationInputs: CanvasGenerationInputs,
    frame?: GenerateDialogState['placeholder'],
    dialogId?: string,
  ) => {
    const startX =
      frame?.x ??
      (canvasSize.width / 2 - viewport.x) / viewport.scale -
        ICON_FRAME_DISPLAY_SIZE.width / 2;
    const startY =
      frame?.y ??
      (canvasSize.height / 2 - viewport.y) / viewport.scale -
        ICON_FRAME_DISPLAY_SIZE.height / 2;
    const spacing = 24;
    const maxRowWidth = 560;
    let cursorX = startX;
    let cursorY = startY;
    let rowHeight = 0;
    const nextLayers: CanvasLayer[] = [];

    iconResults.forEach((icon) => {
      const originalWidth = icon.width || 128;
      const originalHeight = icon.height || 128;
      const { width, height } = resolveLayerResolutionSize(
        originalWidth,
        originalHeight,
        { width: 128, height: 128 },
      );
      if (cursorX > startX && cursorX + width - startX > maxRowWidth) {
        cursorX = startX;
        cursorY += rowHeight + spacing;
        rowHeight = 0;
      }

      layerCounterRef.current += 1;
      const generatedIndex = layerCounterRef.current;
      nextLayers.push({
        id: `layer-icon-${generatedIndex}`,
        resourceId: `local-resource-icon-${generatedIndex}`,
        title: icon.name,
        src: icon.imageSrc,
        x: cursorX,
        y: cursorY,
        width,
        height,
        originalWidth,
        originalHeight,
        zIndex: generatedIndex + 10,
        sourceType: 'generated',
        prompt: generated.prompt,
        actualPrompt: generated.actualPrompt ?? generated.prompt,
        model: generated.model,
        provider: generated.provider,
        taskId: generated.taskId,
        assetKind: 'icon',
        generationInputs,
      });

      cursorX += width + spacing;
      rowHeight = Math.max(rowHeight, height);
    });

    if (!nextLayers.length) {
      return;
    }
    setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
    selectSingleLayer(nextLayers[0]?.id ?? null);
    setActiveSidebarPanel('layers');
    if (dialogId) {
      removeCanvasGenerationDialogById(dialogId);
    }
    setActiveTool('select');
    nextLayers.forEach((layer) => createProjectResourceForLayer(layer));
  };

  const updateIconDescription = (index: number, value: string) => {
    setGenerateDialog((currentDialog) =>
      currentDialog?.mode === 'icon'
        ? {
            ...setIconGenerationIdle(currentDialog),
            iconDescriptions: (
              currentDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS
            ).map((description, descriptionIndex) =>
              descriptionIndex === index ? value : description,
            ),
          }
        : currentDialog,
    );
  };

  const addIconDescription = () => {
    setGenerateDialog((currentDialog) => {
      if (currentDialog?.mode !== 'icon') {
        return currentDialog;
      }
      const descriptions =
        currentDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS;
      if (descriptions.length >= ICON_DESCRIPTION_LIMIT) {
        return currentDialog;
      }
      return {
        ...setIconGenerationIdle(currentDialog),
        iconDescriptions: [...descriptions, ''],
      };
    });
  };

  const submitIconSpritesheetGeneration = async (
    dialog: GenerateDialogState,
  ) => {
    if (dialog.mode !== 'icon') {
      return;
    }
    const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
    const setSubmittingIconDialog = (
      nextDialog: CanvasGenerationDialogState,
    ) => {
      updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog);
    };
    const iconDescriptions = (
      dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS
    )
      .map((description) => description.trim())
      .filter(Boolean);
    if (!dialog.iconSpecReference) {
      if (canvasDialog) {
        setSubmittingIconDialog({
          ...canvasDialog,
          status: 'failed',
          composerOpen: true,
          errorMessage: '请选择图标素材规范',
        });
      }
      return;
    }
    if (!iconDescriptions.length) {
      if (canvasDialog) {
        setSubmittingIconDialog({
          ...canvasDialog,
          status: 'failed',
          composerOpen: true,
          errorMessage: '请填写素材描述',
        });
      }
      return;
    }

    if (!canvasDialog) {
      return;
    }

    setSubmittingIconDialog({
      ...canvasDialog,
      iconDescriptions,
      status: 'generating',
      composerOpen: false,
      errorMessage: undefined,
    });

    try {
      const generated = await generateEditorIconSpritesheet({
        referenceImageSrc: dialog.iconSpecReference.src,
        iconDescriptions,
      });
      addIconSpritesheetResultLayers(
        generated,
        generated.iconImageSrcs,
        buildIconGenerationInputs(iconDescriptions, dialog.iconSpecReference),
        getGeneratingDialogPlaceholder(dialog),
        canvasDialog.id,
      );
    } catch (error) {
      setSubmittingIconDialog({
        ...canvasDialog,
        iconDescriptions,
        status: 'failed',
        composerOpen: true,
        errorMessage: resolveImageGenerationErrorMessage(error),
      });
    }
  };

  const submitQuickEdit = async () => {
    if (!quickEditPanel || !quickEditSourceLayer) {
      return;
    }

    const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片';
    setQuickEditPanel({
      ...quickEditPanel,
      prompt: normalizedPrompt,
      status: 'generating',
      errorMessage: undefined,
    });

    try {
      const referenceImageSrc =
        await resolveEditorImageReferenceDataUrl(quickEditSourceLayer.src);
      const generated = await generateEditorImage({
        prompt: normalizedPrompt,
        size: quickEditPanel.size,
        kind: 'quick-edit',
        model: quickEditPanel.model,
        referenceImageSrcs: [referenceImageSrc],
      });
      addQuickEditResultLayer(
        generated,
        quickEditSourceLayer,
        buildEditGenerationInputs(
          '快速编辑提示词',
          normalizedPrompt,
          quickEditSourceLayer,
        ),
      );
    } catch (error) {
      setQuickEditPanel({
        ...quickEditPanel,
        prompt: normalizedPrompt,
        status: 'failed',
        errorMessage: resolveImageGenerationErrorMessage(error),
      });
    }
  };

  const submitImageGeneration = async (dialog: GenerateDialogState) => {
    const normalizedPrompt =
      dialog.prompt.trim() ||
      (dialog.mode === 'edit' ? '修改当前图片' : 'AI 生成图片');
    const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
    if (canvasDialog) {
      updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
        ...currentDialog,
        prompt: normalizedPrompt,
        status: 'generating',
        composerOpen: false,
      }));
    } else {
      setGenerateDialog({
        ...dialog,
        prompt: normalizedPrompt,
        status: 'generating',
        composerOpen: dialog.mode === 'edit',
      });
    }

    try {
      if (dialog.mode === 'edit') {
        const sourceLayer = layers.find(
          (layer) => layer.id === dialog.sourceLayerId,
        );
        if (!sourceLayer) {
          throw new Error('未找到要修改的图片');
        }
        const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
          sourceLayer.src,
        );
        const generated = await editEditorImage({
          prompt: normalizedPrompt,
          sourceImageSrc: referenceImageSrc,
        });
        addGeneratedResultLayer(generated, {
          sourceLayer,
          generationInputs: buildEditGenerationInputs(
            '修改要求',
            normalizedPrompt,
            sourceLayer,
          ),
        });
      } else if (dialog.mode === 'spec') {
        const specType = dialog.specType ?? 'custom';
        const specValues =
          dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType];
        const specPrompt = buildSpecPrompt(specType, specValues);
        const generated = await generateEditorImage({
          prompt: specPrompt,
          size: SPEC_GENERATION_SIZE,
          model: DEFAULT_IMAGE_MODEL,
          kind: 'spec',
        });
        addGeneratedResultLayer(generated, {
          frame: getGeneratingDialogPlaceholder(dialog),
          assetKind: specType === 'icon' ? 'icon-spec' : 'spec',
          title: `${SPEC_TYPE_LABEL[specType]} ${layerCounterRef.current + 1}`,
          dialogId: canvasDialog?.id,
          generationInputs: buildSpecGenerationInputs(specType, specValues),
        });
      } else if (dialog.mode === 'character') {
        const referenceImageSrcs = [
          dialog.characterSpecReference?.src,
          ...(dialog.characterReferences ?? []).map(
            (reference) => reference.src,
          ),
        ].filter((src): src is string => Boolean(src));
        const generated = await generateEditorImage({
          prompt: normalizedPrompt,
          kind: 'character',
          ...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
        });
        addGeneratedResultLayer(generated, {
          frame: getGeneratingDialogPlaceholder(dialog),
          assetKind: 'character',
          title: `角色形象 ${layerCounterRef.current + 1}`,
          dialogId: canvasDialog?.id,
          generationInputs: buildCharacterGenerationInputs(
            normalizedPrompt,
            dialog.characterSpecReference,
            dialog.characterReferences,
          ),
        });
      } else {
        const generated = await generateEditorImage({
          prompt: normalizedPrompt,
        });
        addGeneratedResultLayer(generated, {
          frame: getGeneratingDialogPlaceholder(dialog),
          dialogId: canvasDialog?.id,
          generationInputs: buildImageGenerationInputs(normalizedPrompt),
        });
      }
    } catch (error) {
      if (canvasDialog) {
        updateCanvasGenerationDialogById(canvasDialog.id, () => ({
          ...canvasDialog,
          prompt: normalizedPrompt,
          status: 'failed',
          composerOpen: true,
          errorMessage: resolveImageGenerationErrorMessage(error),
        }));
      } else {
        setGenerateDialog({
          ...dialog,
          prompt: normalizedPrompt,
          status: 'failed',
          composerOpen: true,
          errorMessage: resolveImageGenerationErrorMessage(error),
        });
      }
    }
  };

  const handleWheel = (event: ReactWheelEvent<HTMLDivElement>) => {
    event.preventDefault();
    const viewportElement = canvasViewportRef.current;
    if (!viewportElement) {
      return;
    }

    if (!event.ctrlKey && !event.metaKey) {
      setViewport((currentViewport) => ({
        ...currentViewport,
        y: currentViewport.y - event.deltaY,
      }));
      return;
    }

    const rect = viewportElement.getBoundingClientRect();
    const pointerX = event.clientX - rect.left;
    const pointerY = event.clientY - rect.top;
    const scaleMultiplier = event.deltaY > 0 ? 0.9 : 1.1;

    setViewport((currentViewport) => {
      const nextScale = clamp(
        currentViewport.scale * scaleMultiplier,
        MIN_SCALE,
        MAX_SCALE,
      );
      const worldX = (pointerX - currentViewport.x) / currentViewport.scale;
      const worldY = (pointerY - currentViewport.y) / currentViewport.scale;

      return {
        x: pointerX - worldX * nextScale,
        y: pointerY - worldY * nextScale,
        scale: nextScale,
      };
    });
  };

  const startPan = (event: ReactPointerEvent<HTMLDivElement>) => {
    event.preventDefault();
    const pointer = getPointerClient(event);
    canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
    setIsPanning(true);
    dragStateRef.current = {
      kind: 'pan',
      pointerId: getPointerId(event),
      startClientX: pointer.x,
      startClientY: pointer.y,
      startViewport: viewport,
    };
  };

  const handleCanvasPointerDown = (
    event: ReactPointerEvent<HTMLDivElement>,
  ) => {
    const button = getPointerButton(event);
    if (button !== 0 || effectiveTool === 'hand') {
      startPan(event);
      return;
    }

    if (button !== 0) {
      return;
    }
    const target = event.target as HTMLElement;
    if (
      effectiveTool === 'select' &&
      (event.target === event.currentTarget ||
        target.classList.contains('image-canvas-editor__world'))
    ) {
      event.preventDefault();
      const rect = canvasViewportRef.current?.getBoundingClientRect();
      const startX = event.clientX - (rect?.left ?? 0);
      const startY = event.clientY - (rect?.top ?? 0);
      canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
      setCanvasMarquee({
        pointerId: event.pointerId,
        startX,
        startY,
        currentX: startX,
        currentY: startY,
      });
      clearCanvasFocus();
      return;
    }
    clearCanvasFocus();
  };

  const handleCanvasDragOver = (event: ReactDragEvent<HTMLDivElement>) => {
    if (event.dataTransfer.types.includes('Files')) {
      event.preventDefault();
      event.dataTransfer.dropEffect = 'copy';
    }
  };

  const handleCanvasDrop = (event: ReactDragEvent<HTMLDivElement>) => {
    const files = event.dataTransfer.files;
    if (!files.length) {
      return;
    }
    event.preventDefault();
    const rect = canvasViewportRef.current?.getBoundingClientRect();
    const canvasPoint = rect
      ? {
          x: event.clientX - rect.left,
          y: event.clientY - rect.top,
        }
      : {
          x: canvasSize.width / 2,
          y: canvasSize.height / 2,
        };
    const defaultFolder =
      assetFolders.find((folder) => folder.systemDefault) ?? assetFolders[0];
    addUploadedFiles(files, {
      folderId: defaultFolder?.id,
      canvasPoint,
      addToCanvas: true,
    });
  };

  const handleLayerPointerDown = (
    event: ReactPointerEvent<HTMLButtonElement>,
    layer: CanvasLayer,
  ) => {
    const button = getPointerButton(event);
    if (button === 1 || effectiveTool === 'hand') {
      event.stopPropagation();
      startPan(event as unknown as ReactPointerEvent<HTMLDivElement>);
      return;
    }
    if (button !== 0) {
      return;
    }
    if (
      isPickingCharacterSpecFromCanvas &&
      generateDialog?.mode === 'character'
    ) {
      event.preventDefault();
      event.stopPropagation();
      pickCharacterSpecFromLayer(layer);
      return;
    }
    if (isPickingIconSpecFromCanvas && generateDialog?.mode === 'icon') {
      event.preventDefault();
      event.stopPropagation();
      pickIconSpecFromLayer(layer);
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    const pointer = getPointerClient(event);
    canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
    const isMultiSelectGesture = event.shiftKey || isShiftPressedRef.current;
    const nextSelectedIds = isMultiSelectGesture
      ? selectedLayerIds.includes(layer.id)
        ? selectedLayerIds.length > 1
          ? selectedLayerIds.filter((layerId) => layerId !== layer.id)
          : [layer.id]
        : [...selectedLayerIds, layer.id]
      : [layer.id];
    setSelectedLayerId(layer.id);
    setSelectedLayerIds(nextSelectedIds);
    setGenerateDialog((currentDialog) => {
      if (
        currentDialog?.mode !== 'generate' &&
        currentDialog?.mode !== 'spec' &&
        currentDialog?.mode !== 'character' &&
        currentDialog?.mode !== 'icon'
      ) {
        return currentDialog;
      }
      if (currentDialog.generatedLayerId === layer.id) {
        return {
          ...currentDialog,
          composerOpen: true,
        };
      }
      return {
        ...currentDialog,
        composerOpen: false,
      };
    });
    const dragLayerIds = nextSelectedIds.includes(layer.id)
      ? nextSelectedIds
      : [layer.id];
    const startLayers = layers
      .filter((currentLayer) => dragLayerIds.includes(currentLayer.id))
      .map((currentLayer) => ({
        id: currentLayer.id,
        x: currentLayer.x,
        y: currentLayer.y,
      }));
    dragStateRef.current = {
      kind: 'layer',
      pointerId: getPointerId(event),
      layerId: layer.id,
      layerIds: dragLayerIds,
      startClientX: pointer.x,
      startClientY: pointer.y,
      startLayerX: layer.x,
      startLayerY: layer.y,
      startLayers,
      startScale: viewport.scale,
    };
  };

  const handleGenerationFramePointerDown = (
    event: ReactPointerEvent<HTMLDivElement>,
    dialog: CanvasGenerationDialogState,
  ) => {
    if (!dialog.placeholder) {
      return;
    }
    const button = getPointerButton(event);
    if (button === 1 || effectiveTool === 'hand') {
      event.stopPropagation();
      startPan(event as unknown as ReactPointerEvent<HTMLDivElement>);
      return;
    }
    if (button !== 0) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    const pointer = getPointerClient(event);
    canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
    activateCanvasGenerationDialog(dialog);
    dragStateRef.current = {
      kind: 'generation-frame',
      dialogId: dialog.id,
      pointerId: getPointerId(event),
      startClientX: pointer.x,
      startClientY: pointer.y,
      startFrameX: dialog.placeholder.x,
      startFrameY: dialog.placeholder.y,
      startScale: viewport.scale,
    };
  };

  const moveViewportFromMinimapPointer = (clientX: number, clientY: number) => {
    if (!minimapModel) {
      return;
    }
    const minimapElement = document.querySelector(
      '.image-canvas-editor__minimap',
    ) as HTMLElement | null;
    const rect = minimapElement?.getBoundingClientRect();
    if (!rect) {
      return;
    }
    const localX = clamp(clientX - rect.left, 0, MINIMAP_SIZE.width);
    const localY = clamp(clientY - rect.top, 0, MINIMAP_SIZE.height);
    const worldX =
      minimapModel.bounds.minX +
      (localX - MINIMAP_PADDING) / minimapModel.scale;
    const worldY =
      minimapModel.bounds.minY +
      (localY - MINIMAP_PADDING) / minimapModel.scale;
    setViewport((currentViewport) => ({
      ...currentViewport,
      x: canvasSize.width / 2 - worldX * currentViewport.scale,
      y: canvasSize.height / 2 - worldY * currentViewport.scale,
    }));
  };

  const handleMinimapPointerDown = (
    event: ReactPointerEvent<HTMLButtonElement>,
  ) => {
    event.preventDefault();
    event.stopPropagation();
    const pointer = getPointerClient(event);
    canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
    dragStateRef.current = {
      kind: 'minimap',
      pointerId: getPointerId(event),
    };
    moveViewportFromMinimapPointer(pointer.x, pointer.y);
  };

  const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
    if (canvasMarquee && canvasMarquee.pointerId === event.pointerId) {
      event.preventDefault();
      const rect = canvasViewportRef.current?.getBoundingClientRect();
      const currentX = event.clientX - (rect?.left ?? 0);
      const currentY = event.clientY - (rect?.top ?? 0);
      setCanvasMarquee((currentMarquee) =>
        currentMarquee
          ? {
              ...currentMarquee,
              currentX,
              currentY,
            }
          : null,
      );
      const left = Math.min(canvasMarquee.startX, currentX);
      const right = Math.max(canvasMarquee.startX, currentX);
      const top = Math.min(canvasMarquee.startY, currentY);
      const bottom = Math.max(canvasMarquee.startY, currentY);
      const selectedIds = layers
        .filter((layer) => {
          const layerLeft = viewport.x + layer.x * viewport.scale;
          const layerTop = viewport.y + layer.y * viewport.scale;
          const layerRight = layerLeft + layer.width * viewport.scale;
          const layerBottom = layerTop + layer.height * viewport.scale;
          return (
            layerLeft <= right &&
            layerRight >= left &&
            layerTop <= bottom &&
            layerBottom >= top
          );
        })
        .map((layer) => layer.id);
      setSelectedLayerIds(selectedIds);
      setSelectedLayerId(selectedIds[0] ?? null);
      return;
    }

    const dragState = dragStateRef.current;
    const pointerId = getPointerId(event);
    if (
      !dragState ||
      (dragState.pointerId >= 0 &&
        pointerId >= 0 &&
        dragState.pointerId !== pointerId)
    ) {
      return;
    }

    if (dragState.kind === 'pan') {
      const pointer = getPointerClient(event);
      setViewport({
        ...dragState.startViewport,
        x: dragState.startViewport.x + pointer.x - dragState.startClientX,
        y: dragState.startViewport.y + pointer.y - dragState.startClientY,
      });
      return;
    }

    if (dragState.kind === 'generation-frame') {
      const pointer = getPointerClient(event);
      const deltaX =
        (pointer.x - dragState.startClientX) / dragState.startScale;
      const deltaY =
        (pointer.y - dragState.startClientY) / dragState.startScale;
      updateCanvasGenerationDialogById(dragState.dialogId, (currentDialog) =>
        currentDialog.placeholder
          ? {
              ...currentDialog,
              placeholder: {
                ...currentDialog.placeholder,
                x: dragState.startFrameX + deltaX,
                y: dragState.startFrameY + deltaY,
              },
            }
          : currentDialog,
      );
      return;
    }

    if (dragState.kind === 'minimap') {
      const pointer = getPointerClient(event);
      moveViewportFromMinimapPointer(pointer.x, pointer.y);
      return;
    }

    const movingLayer = layers.find((layer) => layer.id === dragState.layerId);
    if (!movingLayer) {
      return;
    }
    const pointer = getPointerClient(event);
    const deltaX = (pointer.x - dragState.startClientX) / dragState.startScale;
    const deltaY = (pointer.y - dragState.startClientY) / dragState.startScale;
    const snapped = resolveSnappedLayerPosition(
      movingLayer,
      dragState.startLayerX + deltaX,
      dragState.startLayerY + deltaY,
      layers,
      dragState.startScale,
    );
    setSnapGuide(snapped.guide);
    setLayers((currentLayers) =>
      currentLayers.map((layer) =>
        dragState.layerIds.includes(layer.id)
          ? (() => {
              const startLayer = dragState.startLayers.find(
                (item) => item.id === layer.id,
              );
              if (!startLayer) {
                return layer;
              }
              if (layer.id === dragState.layerId) {
                return {
                  ...layer,
                  x: snapped.x,
                  y: snapped.y,
                };
              }
              return {
                ...layer,
                x:
                  startLayer.x +
                  deltaX +
                  (snapped.x - (dragState.startLayerX + deltaX)),
                y:
                  startLayer.y +
                  deltaY +
                  (snapped.y - (dragState.startLayerY + deltaY)),
              };
            })()
          : layer,
      ),
    );
  };

  const finishDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
    if (canvasMarquee && canvasMarquee.pointerId === event.pointerId) {
      event.preventDefault();
      setCanvasMarquee(null);
      if (canvasViewportRef.current?.hasPointerCapture?.(event.pointerId)) {
        canvasViewportRef.current.releasePointerCapture?.(event.pointerId);
      }
      return;
    }

    const dragState = dragStateRef.current;
    const pointerId = getPointerId(event);
    if (
      dragState &&
      (dragState.pointerId < 0 ||
        pointerId < 0 ||
        dragState.pointerId === pointerId)
    ) {
      dragStateRef.current = null;
      setIsPanning(false);
      setSnapGuide(null);
      if (canvasViewportRef.current?.hasPointerCapture?.(event.pointerId)) {
        canvasViewportRef.current.releasePointerCapture?.(event.pointerId);
      }
    }
  };

  const switchTool = (tool: CanvasTool) => {
    dragStateRef.current = null;
    setIsPanning(false);
    setSnapGuide(null);
    if (tool === 'upload') {
      setUploadTarget('asset');
      uploadInputRef.current?.click();
      return;
    }
    if (tool === 'generate') {
      openGenerateDialog();
      return;
    }
    if (tool === 'spec') {
      setIsSpecMenuOpen((open) => !open);
      setActiveTool('spec');
      return;
    }
    if (tool === 'character') {
      openCharacterGenerationDialog();
      return;
    }
    if (tool === 'icon') {
      openIconGenerationDialog();
      return;
    }
    setActiveTool(tool);
  };

  const toggleSidebarPanel = (panel: SidebarPanel) => {
    setActiveSidebarPanel((currentPanel) =>
      currentPanel === panel ? null : panel,
    );
  };

  const groupSelectedLayers = () => {
    const targetIds = selectedLayerIds.length
      ? selectedLayerIds
      : selectedLayerId
        ? [selectedLayerId]
        : [];
    if (!targetIds.length) {
      return;
    }
    const groupId = `layer-group-${Date.now()}`;
    setLayers((currentLayers) =>
      currentLayers.map((layer) =>
        targetIds.includes(layer.id)
          ? {
              ...layer,
              groupId,
            }
          : layer,
      ),
    );
  };

  const toolButtons = [
    { label: '裁剪', icon: Crop },
    { label: '重绘', icon: Sparkles },
    { label: '调整', icon: SlidersHorizontal },
    { label: '复制', icon: Copy },
  ];

  const canvasTools: Array<{
    id: CanvasTool;
    label: string;
    icon: typeof MousePointer2;
  }> = [
    { id: 'select', label: '选择工具', icon: MousePointer2 },
    { id: 'hand', label: '抓手工具', icon: Hand },
    { id: 'upload', label: '上传工具', icon: ImagePlus },
    { id: 'generate', label: '生成工具', icon: WandSparkles },
    { id: 'spec', label: '生成规范', icon: ClipboardList },
    { id: 'character', label: '生成角色形象', icon: Sparkles },
    { id: 'icon', label: '生成图标素材', icon: ImageIcon },
    { id: 'text', label: '文字工具', icon: Type },
    { id: 'shape', label: '形状标注工具', icon: Shapes },
    { id: 'export', label: '导出工具', icon: Download },
  ];

  const updateSpecFormValue = (key: keyof SpecFormValues, value: string) => {
    setGenerateDialog((currentDialog) => {
      if (currentDialog?.mode !== 'spec') {
        return currentDialog;
      }
      const specType = currentDialog.specType ?? 'custom';
      return {
        ...currentDialog,
        specValues: {
          ...DEFAULT_SPEC_FORM_VALUES[specType],
          ...currentDialog.specValues,
          [key]: value,
        },
        status:
          currentDialog.status === 'failed' ? 'idle' : currentDialog.status,
        errorMessage:
          currentDialog.status === 'failed'
            ? undefined
            : currentDialog.errorMessage,
      };
    });
  };

  const updateCharacterAnimationDuration = (frameCountValue: string) => {
    const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find(
      (item) => String(item.frameCount) === frameCountValue,
    );
    if (!option) {
      return;
    }
    setCharacterAnimationPanel((currentPanel) =>
      currentPanel
        ? {
            ...currentPanel,
            frameCount: option.frameCount,
            durationSeconds: option.durationSeconds,
            status:
              currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
            errorMessage:
              currentPanel.status === 'failed'
                ? undefined
                : currentPanel.errorMessage,
          }
        : currentPanel,
    );
  };

  const submitCharacterAnimation = async () => {
    if (!characterAnimationPanel || !characterAnimationSourceLayer) {
      return;
    }
    const promptText = characterAnimationPanel.promptText.trim();
    const nextPanel = {
      ...characterAnimationPanel,
      promptText,
      status: 'generating' as const,
      errorMessage: undefined,
      result: undefined,
    };
    setCharacterAnimationPanel(nextPanel);

    try {
      const result = await generateEditorCharacterAnimation({
        sourceLayerId: characterAnimationSourceLayer.id,
        sourceImageSrc: resolveCharacterAnimationSourceImageSrc(
          characterAnimationSourceLayer,
        ),
        sourceWidth: characterAnimationSourceLayer.originalWidth,
        sourceHeight: characterAnimationSourceLayer.originalHeight,
        promptText,
        resolution: nextPanel.resolution,
        ratio: nextPanel.ratio,
        frameCount: nextPanel.frameCount,
        durationSeconds: nextPanel.durationSeconds,
        priceMudPoints: calculateCharacterAnimationPrice(
          nextPanel.resolution,
          nextPanel.durationSeconds,
        ),
        model: CHARACTER_ANIMATION_MODEL,
      });
      setCharacterAnimationPanel((currentPanel) =>
        currentPanel
          ? {
              ...currentPanel,
              status: 'completed',
              result,
            }
          : currentPanel,
      );
    } catch (error) {
      setCharacterAnimationPanel((currentPanel) =>
        currentPanel
          ? {
              ...currentPanel,
              status: 'failed',
              errorMessage:
                error instanceof Error && error.message.trim()
                  ? error.message
                  : '生成角色动画失败',
            }
          : currentPanel,
      );
    }
  };

  return (
    <section
      ref={editorRootRef}
      className="image-canvas-editor"
      aria-label="图片画布编辑器"
      onContextMenu={(event) => event.preventDefault()}
    >
      <input
        ref={uploadInputRef}
        type="file"
        accept="image/*"
        multiple
        aria-label="上传图片文件"
        hidden
        onChange={(event) => {
          const files = event.currentTarget.files;
          if (files?.length) {
            if (uploadTarget === 'character-spec') {
              void addCharacterSpecReferenceFiles(files);
            } else if (uploadTarget === 'character-reference') {
              void addCharacterReferenceFiles(files);
            } else if (uploadTarget === 'icon-spec') {
              void addIconSpecReferenceFiles(files);
            } else {
              addUploadedFiles(files, { addToCanvas: activeTool === 'upload' });
            }
          }
          setUploadTarget('asset');
          event.currentTarget.value = '';
        }}
      />
      {activeSidebarPanel ? (
        <aside className="image-canvas-editor__sidebar" aria-label="图片资源栏">
          <div className="image-canvas-editor__sidebar-header">
            <div className="min-w-0">
              <h2 className="image-canvas-editor__sidebar-title">
                {activeSidebarPanel === 'assets' ? '素材' : '图层'}
              </h2>
              <div className="image-canvas-editor__sidebar-count">
                {activeSidebarPanel === 'assets'
                  ? assets.length
                  : layers.length}
              </div>
            </div>
            {activeSidebarPanel === 'assets' ? (
              <div className="image-canvas-editor__sidebar-header-actions">
                <EditorIconButton
                  className="image-canvas-editor__icon-button"
                  label="素材选择模式"
                  title="选择"
                  icon={isAssetSelectionMode ? CheckSquare : Square}
                  pressed={isAssetSelectionMode}
                  onClick={() =>
                    setIsAssetSelectionMode((currentMode) => !currentMode)
                  }
                />
                <EditorIconButton
                  className="image-canvas-editor__icon-button"
                  label="新建素材文件夹"
                  title="新建文件夹"
                  icon={FolderPlus}
                  onClick={() => setCreatingFolder(true)}
                />
              </div>
            ) : (
              <EditorIconButton
                className="image-canvas-editor__icon-button"
                label="图层打组"
                title="打组"
                icon={FolderPlus}
                disabled={!selectedLayerId && selectedLayerIds.length === 0}
                onClick={groupSelectedLayers}
              />
            )}
          </div>

          {activeSidebarPanel === 'assets' ? (
            <div
              ref={assetListRef}
              className="image-canvas-editor__asset-list"
              onPointerDown={handleAssetMarqueePointerDown}
              onPointerMove={handleAssetMarqueePointerMove}
              onPointerUp={handleAssetMarqueePointerUp}
              onPointerCancel={handleAssetMarqueePointerUp}
            >
              {creatingFolder ? (
                <form
                  className="image-canvas-editor__folder-create"
                  onSubmit={(event) => {
                    event.preventDefault();
                    void commitNewAssetFolder();
                  }}
                >
                  <PlatformTextField
                    aria-label="素材文件夹名称"
                    value={newFolderName}
                    autoFocus
                    size="xs"
                    density="compact"
                    className="image-canvas-editor__folder-create-input"
                    onChange={(event) => setNewFolderName(event.target.value)}
                    onKeyDown={(event) => {
                      if (event.key === 'Escape') {
                        event.preventDefault();
                        setCreatingFolder(false);
                        setNewFolderName('');
                      }
                    }}
                  />
                  <EditorIconButton
                    type="submit"
                    label="保存素材文件夹"
                    icon={Check}
                  />
                  <EditorIconButton
                    label="取消新建素材文件夹"
                    icon={X}
                    onClick={() => {
                      setCreatingFolder(false);
                      setNewFolderName('');
                    }}
                  />
                </form>
              ) : null}
              {groupedAssets.map((folder) => (
                <section
                  key={folder.id}
                  className="image-canvas-editor__asset-folder"
                  aria-label={folder.label}
                  onDragOver={(event) => {
                    if (event.dataTransfer.types.includes('Files')) {
                      event.preventDefault();
                      event.stopPropagation();
                      event.dataTransfer.dropEffect = 'copy';
                    }
                  }}
                  onDrop={(event) => {
                    if (!event.dataTransfer.files.length) {
                      return;
                    }
                    event.preventDefault();
                    event.stopPropagation();
                    addUploadedFiles(event.dataTransfer.files, {
                      folderId: folder.id,
                    });
                  }}
                >
                  <div className="image-canvas-editor__asset-folder-header">
                    <EditorIconButton
                      label={`${folder.collapsed ? '展开' : '折叠'}${folder.label}`}
                      title={folder.collapsed ? '展开' : '折叠'}
                      icon={folder.collapsed ? ChevronRight : ChevronDown}
                      expanded={!folder.collapsed}
                      onClick={() => toggleAssetFolder(folder.id)}
                    />
                    <Folder className="h-4 w-4" />
                    {renamingFolder?.folderId === folder.id ? (
                      <PlatformTextField
                        aria-label={`重命名文件夹${folder.label}`}
                        value={renamingFolder.value}
                        autoFocus
                        size="xs"
                        density="compact"
                        className="image-canvas-editor__folder-rename-input"
                        onChange={(event) =>
                          setRenamingFolder({
                            folderId: folder.id,
                            value: event.target.value,
                          })
                        }
                        onKeyDown={(event) => {
                          if (event.key === 'Enter') {
                            event.preventDefault();
                            commitFolderRename(folder);
                          }
                          if (event.key === 'Escape') {
                            event.preventDefault();
                            setRenamingFolder(null);
                          }
                        }}
                      />
                    ) : (
                      <span>{folder.label}</span>
                    )}
                    <span>{folder.assets.length}</span>
                    {renamingFolder?.folderId === folder.id ? (
                      <>
                        <EditorIconButton
                          label={`保存文件夹${folder.label}名称`}
                          title="保存"
                          icon={Check}
                          onClick={() => commitFolderRename(folder)}
                        />
                        <EditorIconButton
                          label={`取消重命名文件夹${folder.label}`}
                          title="取消"
                          icon={X}
                          onClick={() => setRenamingFolder(null)}
                        />
                      </>
                    ) : (
                      <EditorIconButton
                        label={`重命名文件夹${folder.label}`}
                        title="重命名"
                        icon={PencilLine}
                        onClick={() => startRenamingFolder(folder)}
                      />
                    )}
                    {!folder.systemDefault ? (
                      <EditorIconButton
                        label={`删除文件夹${folder.label}`}
                        title="删除"
                        icon={Trash2}
                        onClick={() => deleteAssetFolder(folder)}
                      />
                    ) : null}
                    <EditorIconButton
                      label={`上传到${folder.label}`}
                      title="上传"
                      icon={ImagePlus}
                      onClick={() => {
                        setActiveUploadFolderId(folder.id);
                        setUploadTarget('asset');
                        uploadInputRef.current?.click();
                      }}
                    />
                  </div>
                  <div
                    className="image-canvas-editor__asset-folder-list"
                    hidden={folder.collapsed}
                  >
                    {folder.assets.map((asset) => {
                      const isRenaming = renamingAsset?.assetId === asset.id;
                      const titleNode = isRenaming ? (
                        <PlatformTextField
                          aria-label={`重命名素材${asset.label}`}
                          value={renamingAsset.value}
                          autoFocus
                          size="xs"
                          density="compact"
                          className="image-canvas-editor__asset-rename-input"
                          onChange={(event) =>
                            setRenamingAsset({
                              assetId: asset.id,
                              value: event.target.value,
                            })
                          }
                          onKeyDown={(event) => {
                            if (event.key === 'Enter') {
                              event.preventDefault();
                              commitAssetRename(asset);
                            }
                            if (event.key === 'Escape') {
                              event.preventDefault();
                              setRenamingAsset(null);
                            }
                          }}
                        />
                      ) : undefined;
                      const actions = isRenaming ? (
                        <div className="image-canvas-editor__asset-actions">
                          <EditorIconButton
                            label={`保存素材${asset.label}名称`}
                            title="保存"
                            icon={Check}
                            onClick={() => commitAssetRename(asset)}
                          />
                          <EditorIconButton
                            label={`取消重命名素材${asset.label}`}
                            title="取消"
                            icon={X}
                            onClick={() => setRenamingAsset(null)}
                          />
                        </div>
                      ) : (
                        <div className="image-canvas-editor__asset-actions">
                          <EditorIconButton
                            label={`重命名素材${asset.label}`}
                            title="重命名"
                            icon={Pencil}
                            onClick={() => startRenamingAsset(asset)}
                          />
                          {asset.sourceKind === 'uploaded' ? (
                            <EditorIconButton
                              label={`删除素材${asset.label}`}
                              title="删除"
                              icon={Trash2}
                              onClick={() => deleteUploadedAsset(asset)}
                            />
                          ) : null}
                        </div>
                      );
                      return (
                        <div key={asset.id} data-asset-id={asset.id}>
                          <SidebarMediaItem
                            title={asset.label}
                            detail={`${asset.width} x ${asset.height}`}
                            imageSrc={asset.src}
                            imageAlt={`素材：${asset.label}`}
                            primaryLabel={
                              isAssetSelectionMode
                                ? `选择素材${asset.label}`
                                : `添加${asset.label}`
                            }
                            onPrimaryClick={() => {
                              if (isAssetSelectionMode) {
                                toggleAssetSelected(asset.id);
                                return;
                              }
                              addAssetLayer(asset);
                            }}
                            selected={selectedAssetIds.has(asset.id)}
                            rowClassName="image-canvas-editor__asset-row"
                            primaryClassName="image-canvas-editor__asset-button"
                            thumbnailClassName="image-canvas-editor__asset-thumb"
                            metaClassName="image-canvas-editor__asset-meta"
                            titleNode={titleNode}
                            actions={actions}
                            onPointerEnter={(event) => {
                              if (isAssetSelectionMode && event.buttons === 1) {
                                setSelectedAssetIds((currentIds) => {
                                  const nextIds = new Set(currentIds);
                                  nextIds.add(asset.id);
                                  return nextIds;
                                });
                              }
                            }}
                            onDragOver={(event) => {
                              if (event.dataTransfer.types.includes('Files')) {
                                event.preventDefault();
                                event.stopPropagation();
                                event.dataTransfer.dropEffect = 'copy';
                              }
                            }}
                            onDrop={(event) => {
                              if (!event.dataTransfer.files.length) {
                                return;
                              }
                              event.preventDefault();
                              event.stopPropagation();
                              addUploadedFiles(event.dataTransfer.files, {
                                folderId: asset.folderId,
                              });
                            }}
                          />
                        </div>
                      );
                    })}
                  </div>
                </section>
              ))}
              {isAssetSelectionMode ? (
                <PlatformBatchActionToolbar
                  className="image-canvas-editor__asset-batch-toolbar"
                  label="素材批量操作"
                >
                  <PlatformActionButton
                    tone="secondary"
                    size="sm"
                    onClick={toggleAllAssetsSelected}
                  >
                    {allSelectableAssetsSelected ? (
                      <CheckSquare className="h-4 w-4" />
                    ) : (
                      <Square className="h-4 w-4" />
                    )}
                    {selectedAssetIds.size > 0
                      ? `${allSelectableAssetsSelected ? '取消全选' : '全选'} · 已选 ${selectedAssetIds.size}`
                      : '全选'}
                  </PlatformActionButton>
                  <PlatformActionButton
                    tone="warning"
                    size="sm"
                    disabled={selectedAssetIds.size === 0}
                    onClick={deleteSelectedAssets}
                  >
                    <Trash2 className="h-4 w-4" />
                    删除
                  </PlatformActionButton>
                  <PlatformActionButton
                    tone="secondary"
                    size="sm"
                    onClick={closeAssetSelectionMode}
                  >
                    取消
                  </PlatformActionButton>
                </PlatformBatchActionToolbar>
              ) : null}
              {assetMarquee ? (
                <div
                  className="image-canvas-editor__asset-marquee"
                  aria-hidden="true"
                  style={{
                    left: Math.min(assetMarquee.startX, assetMarquee.currentX),
                    top: Math.min(assetMarquee.startY, assetMarquee.currentY),
                    width: Math.abs(
                      assetMarquee.currentX - assetMarquee.startX,
                    ),
                    height: Math.abs(
                      assetMarquee.currentY - assetMarquee.startY,
                    ),
                  }}
                />
              ) : null}
            </div>
          ) : (
            <div className="image-canvas-editor__layers-list">
              {layers
                .slice()
                .sort((left, right) => right.zIndex - left.zIndex)
                .map((layer) => (
                  <SidebarMediaItem
                    key={layer.id}
                    title={layer.title}
                    detail={`${Math.round(layer.width)} x ${Math.round(layer.height)}${layer.groupId ? ' · 已打组' : ''}`}
                    imageSrc={layer.src}
                    imageAlt={`图层缩略图：${layer.title}`}
                    selected={selectedLayerId === layer.id}
                    primaryLabel={`选择图层${layer.title}`}
                    onPrimaryClick={() => selectSingleLayer(layer.id)}
                    rowClassName="image-canvas-editor__layer-row"
                    primaryClassName="image-canvas-editor__layer-row-button"
                    thumbnailClassName="image-canvas-editor__layer-row-thumb"
                    metaClassName="image-canvas-editor__layer-row-meta"
                  />
                ))}
            </div>
          )}
        </aside>
      ) : null}

      <div className="image-canvas-editor__main">
        <div className="image-canvas-editor__topbar">
          <div className="image-canvas-editor__title-block">
            <h1>图片编辑器</h1>
            <span>画布</span>
          </div>
        </div>

        <div
          ref={canvasViewportRef}
          className={`image-canvas-editor__viewport ${isPanning ? 'image-canvas-editor__viewport--panning' : ''} image-canvas-editor__viewport--tool-${effectiveTool}`}
          style={{ backgroundColor: canvasBackgroundColor }}
          aria-label="画布工作区"
          onPointerDown={handleCanvasPointerDown}
          onPointerMove={handlePointerMove}
          onPointerUp={finishDrag}
          onPointerCancel={finishDrag}
          onWheel={handleWheel}
          onDragOver={handleCanvasDragOver}
          onDrop={handleCanvasDrop}
        >
          <div
            className="image-canvas-editor__world"
            style={{
              width: CANVAS_WORLD_SIZE,
              height: CANVAS_WORLD_SIZE,
              transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.scale})`,
            }}
          >
            {snapGuide?.vertical !== undefined ? (
              <div
                className="image-canvas-editor__snap-guide image-canvas-editor__snap-guide--vertical"
                data-testid="image-canvas-editor-snap-guide-vertical"
                style={{ left: snapGuide.vertical }}
              />
            ) : null}
            {snapGuide?.horizontal !== undefined ? (
              <div
                className="image-canvas-editor__snap-guide image-canvas-editor__snap-guide--horizontal"
                data-testid="image-canvas-editor-snap-guide-horizontal"
                style={{ top: snapGuide.horizontal }}
              />
            ) : null}

            {layers
              .slice()
              .sort((left, right) => left.zIndex - right.zIndex)
              .map((layer) => {
                const isSelected = selectedLayerIds.includes(layer.id);
                const isHovered = hoveredLayerId === layer.id;
                const kindLabel = getLayerKindLabel(layer);
                const layerGeneratingLabel =
                  generateDialog?.mode === 'edit' &&
                  generateDialog.status === 'generating' &&
                  generateDialog.sourceLayerId === layer.id
                    ? '修改中'
                    : quickEditPanel?.status === 'generating' &&
                        quickEditPanel.sourceLayerId === layer.id
                      ? '生成中'
                      : null;
                return (
                  <button
                    key={layer.id}
                    type="button"
                    className={`image-canvas-editor__layer ${isSelected ? 'image-canvas-editor__layer--selected' : ''} ${isHovered ? 'image-canvas-editor__layer--hovered' : ''} ${layerGeneratingLabel ? 'image-canvas-editor__layer--generating' : ''}`}
                    style={{
                      left: layer.x,
                      top: layer.y,
                      width: layer.width,
                      height: layer.height,
                      zIndex: layer.zIndex,
                    }}
                    onPointerDown={(event) =>
                      handleLayerPointerDown(event, layer)
                    }
                    onClick={(event) => {
                      // 测试环境和辅助技术可能只触发 click；
                      // 用 click 兜底选中，真实拖拽仍由 pointerDown 负责。
                      event.stopPropagation();
                      if (isPickingCharacterSpecFromCanvas) {
                        return;
                      }
                      if (isPickingIconSpecFromCanvas) {
                        return;
                      }
                      if (event.shiftKey || isShiftPressedRef.current) {
                        return;
                      }
                      selectSingleLayer(layer.id);
                      setImageContextMenu(null);
                    }}
                    onContextMenu={(event) => {
                      event.preventDefault();
                      event.stopPropagation();
                      selectSingleLayer(layer.id);
                      setImageContextMenu({
                        layerId: layer.id,
                        x: event.clientX,
                        y: event.clientY,
                      });
                    }}
                    onMouseEnter={() => setHoveredLayerId(layer.id)}
                    onMouseLeave={() =>
                      setHoveredLayerId((currentId) =>
                        currentId === layer.id ? null : currentId,
                      )
                    }
                    aria-label={`选择${layer.title}`}
                  >
                    <img src={layer.src} alt={`画布图片：${layer.title}`} />
                    {kindLabel ? (
                      <span
                        className={`image-canvas-editor__kind-badge image-canvas-editor__kind-badge--${layer.assetKind}`}
                      >
                        {kindLabel}
                      </span>
                    ) : null}
                    <PlatformIconButton
                      asChild="spanButton"
                      variant="darkMini"
                      className={`image-canvas-editor__metadata-corner ${
                        kindLabel
                          ? 'image-canvas-editor__metadata-corner--beside-kind'
                          : ''
                      }`}
                      label={`查看${layer.title}图片信息`}
                      icon={<Braces className="h-3 w-3" />}
                      onClick={(event) => {
                        event.stopPropagation();
                        setMetadataLayer(layer);
                        selectSingleLayer(layer.id);
                      }}
                      onPointerDown={(event) => event.stopPropagation()}
                    />
                    {isHovered ? (
                      <PlatformPillBadge
                        tone="lightOverlay"
                        size="xs"
                        className="image-canvas-editor__size-badge"
                      >
                        {Math.round(layer.originalWidth)} x{' '}
                        {Math.round(layer.originalHeight)} px
                      </PlatformPillBadge>
                    ) : null}
                    {layerGeneratingLabel ? (
                      <span
                        className="image-canvas-editor__generation-frame-progress image-canvas-editor__layer-generating-progress"
                        role="status"
                      >
                        {layerGeneratingLabel}
                      </span>
                    ) : null}
                  </button>
                );
              })}
            {canvasMarquee ? (
              <div
                className="image-canvas-editor__canvas-marquee"
                aria-hidden="true"
                style={{
                  left:
                    (Math.min(canvasMarquee.startX, canvasMarquee.currentX) -
                      viewport.x) /
                    viewport.scale,
                  top:
                    (Math.min(canvasMarquee.startY, canvasMarquee.currentY) -
                      viewport.y) /
                    viewport.scale,
                  width:
                    Math.abs(canvasMarquee.currentX - canvasMarquee.startX) /
                    viewport.scale,
                  height:
                    Math.abs(canvasMarquee.currentY - canvasMarquee.startY) /
                    viewport.scale,
                }}
              />
            ) : null}
            {canvasGenerationDialogs.map((dialog) =>
              dialog.placeholder ? (
                <div
                  key={dialog.id}
                  className={`image-canvas-editor__generation-frame ${
                    dialog.mode === 'icon'
                      ? 'image-canvas-editor__generation-frame--icon'
                      : ''
                  } ${
                    dialog.status === 'generating'
                      ? 'image-canvas-editor__generation-frame--generating'
                      : ''
                  }`}
                  role="button"
                  tabIndex={0}
                  style={{
                    left: dialog.placeholder.x,
                    top: dialog.placeholder.y,
                    width: dialog.placeholder.width,
                    height: dialog.placeholder.height,
                  }}
                  aria-label={getGenerationFrameAriaLabel(dialog)}
                  onPointerDown={(event) =>
                    handleGenerationFramePointerDown(event, dialog)
                  }
                  onDoubleClick={() => activateCanvasGenerationDialog(dialog)}
                >
                  <span className="image-canvas-editor__generation-frame-label">
                    <ImageIcon className="h-4 w-4" />
                    {getGenerationFrameLabel(dialog)}
                  </span>
                  {dialog.mode === 'character' ? (
                    <span className="image-canvas-editor__kind-badge image-canvas-editor__kind-badge--character">
                      角色
                    </span>
                  ) : null}
                  {dialog.mode === 'spec' ? (
                    <span className="image-canvas-editor__kind-badge image-canvas-editor__kind-badge--spec">
                      规范
                    </span>
                  ) : null}
                  {dialog.mode === 'icon' ? (
                    <span className="image-canvas-editor__kind-badge image-canvas-editor__kind-badge--icon">
                      图标
                    </span>
                  ) : null}
                  <span className="image-canvas-editor__generation-frame-size">
                    {dialog.placeholder.originalWidth} x{' '}
                    {dialog.placeholder.originalHeight}
                  </span>
                  <span className="image-canvas-editor__generation-frame-icon">
                    <ImageIcon className="h-8 w-8" />
                  </span>
                  {dialog.status === 'generating' ? (
                    <span
                      className="image-canvas-editor__generation-frame-progress"
                      role="status"
                    >
                      生成中
                    </span>
                  ) : null}
                </div>
              ) : null,
            )}
            {(generateDialog?.mode === 'generate' ||
              generateDialog?.mode === 'spec' ||
              generateDialog?.mode === 'character' ||
              generateDialog?.mode === 'icon') &&
            generateDialog.status === 'generating' &&
            generationComposerStyle ? (
              <PlatformStatusMessage
                tone="info"
                surface="platform"
                size="xs"
                className="image-canvas-editor__generate-status image-canvas-editor__generate-status--floating"
                role="status"
                style={generationComposerStyle}
              >
                生成中
              </PlatformStatusMessage>
            ) : null}
          </div>

          {selectedLayer && selectedToolbarStyle ? (
            <div
              className="image-canvas-editor__floating-toolbar"
              style={selectedToolbarStyle}
              role="toolbar"
              aria-label="图片工具栏"
              onPointerDown={(event) => event.stopPropagation()}
            >
              {toolButtons.map(({ label, icon: Icon }) => (
                <EditorIconButton
                  key={label}
                  label={`${label}占位`}
                  title={`${label}占位`}
                  icon={Icon}
                  onClick={() => triggerPlaceholderAction(label)}
                />
              ))}
              <EditorIconButton
                label="删除图片"
                title="删除图片"
                icon={Trash2}
                onClick={deleteSelectedLayer}
              />
              <EditorIconButton
                label="快速编辑"
                title="快速编辑"
                icon={Sparkles}
                onClick={() => openQuickEditPanel(selectedLayer)}
              />
              {isGeneratedLayer(selectedLayer) ? (
                <>
                  <EditorIconButton
                    label={`查看${selectedLayer.title}图片信息`}
                    title={`查看${selectedLayer.title}图片信息`}
                    icon={Info}
                    onClick={() => setMetadataLayer(selectedLayer)}
                  />
                  <EditorIconButton
                    label="修改图片"
                    title="修改图片"
                    icon={WandSparkles}
                    onClick={() => openEditDialog(selectedLayer)}
                  />
                </>
              ) : null}
              {selectedLayer.assetKind === 'character' ? (
                <EditorIconButton
                  label="生成动画"
                  title="生成动画"
                  icon={Sparkles}
                  onClick={() => openCharacterAnimationPanel(selectedLayer)}
                />
              ) : null}
            </div>
          ) : null}

          <EditorIconButton
            className="image-canvas-editor__reset-button"
            label="重置画布视图"
            title="重置画布视图"
            icon={RotateCcw}
            onClick={() => fitLayers()}
          />

          <div
            className="image-canvas-editor__panel-dock"
            role="toolbar"
            aria-label="画布面板入口"
            onPointerDown={(event) => event.stopPropagation()}
          >
            <div className="image-canvas-editor__zoom-menu-wrap">
              <PlatformInlineOptionButton
                className="image-canvas-editor__zoom-trigger"
                aria-label={`当前缩放比例 ${formatPercent(viewport.scale)}`}
                aria-haspopup="menu"
                aria-expanded={isZoomMenuOpen}
                onClick={() => setIsZoomMenuOpen((open) => !open)}
              >
                {formatPercent(viewport.scale)}
              </PlatformInlineOptionButton>
              {isZoomMenuOpen ? (
                <PlatformFloatingMenu label="缩放菜单" placement="top-start">
                  <PlatformFloatingMenuItem
                    className="image-canvas-editor__zoom-menu-item"
                    onClick={() => {
                      updateScaleFromCenter(viewport.scale * 1.16);
                      setIsZoomMenuOpen(false);
                    }}
                  >
                    放大
                  </PlatformFloatingMenuItem>
                  <PlatformFloatingMenuItem
                    className="image-canvas-editor__zoom-menu-item"
                    onClick={() => {
                      updateScaleFromCenter(viewport.scale * 0.86);
                      setIsZoomMenuOpen(false);
                    }}
                  >
                    缩小
                  </PlatformFloatingMenuItem>
                  <PlatformFloatingMenuItem
                    className="image-canvas-editor__zoom-menu-item"
                    onClick={() => {
                      fitLayers();
                      setIsZoomMenuOpen(false);
                    }}
                  >
                    显示画布所有元素
                  </PlatformFloatingMenuItem>
                  {[0.5, 1, 2].map((scale) => (
                    <PlatformFloatingMenuItem
                      key={scale}
                      className="image-canvas-editor__zoom-menu-item"
                      onClick={() => {
                        updateScaleFromCenter(scale);
                        setIsZoomMenuOpen(false);
                      }}
                    >
                      缩放至{Math.round(scale * 100)}%
                    </PlatformFloatingMenuItem>
                  ))}
                </PlatformFloatingMenu>
              ) : null}
            </div>
            <div className="image-canvas-editor__background-control">
              <PlatformIconButton
                label="画布背景色"
                title="画布背景色"
                aria-expanded={isBackgroundMenuOpen}
                onClick={() => setIsBackgroundMenuOpen((open) => !open)}
                icon={
                  <span
                    className="image-canvas-editor__background-swatch-current"
                    style={{ backgroundColor: canvasBackgroundColor }}
                  />
                }
              />
              {isBackgroundMenuOpen ? (
                <PlatformFloatingMenu
                  className="image-canvas-editor__background-menu"
                  label="画布背景色菜单"
                  placement="top-start"
                >
                  {CANVAS_BACKGROUND_OPTIONS.map((option) => (
                    <PlatformFloatingMenuItem
                      key={option.value}
                      className="image-canvas-editor__background-menu-item"
                      aria-label={`切换画布背景色为${option.label}`}
                      aria-pressed={canvasBackgroundColor === option.value}
                      onClick={() => {
                        setCanvasBackgroundColor(option.value);
                        setIsBackgroundMenuOpen(false);
                      }}
                    >
                      <span
                        className="image-canvas-editor__background-swatch"
                        style={{ backgroundColor: option.value }}
                      />
                    </PlatformFloatingMenuItem>
                  ))}
                </PlatformFloatingMenu>
              ) : null}
            </div>
            <EditorIconButton
              label="打开素材"
              title="素材"
              icon={ImagePlus}
              pressed={activeSidebarPanel === 'assets'}
              onClick={() => toggleSidebarPanel('assets')}
            />
            <EditorIconButton
              label="打开图层"
              title="图层"
              icon={Layers}
              pressed={activeSidebarPanel === 'layers'}
              onClick={() => toggleSidebarPanel('layers')}
            />
            <EditorIconButton
              label="切换小地图"
              title="小地图"
              icon={MapIcon}
              pressed={isMinimapOpen}
              onClick={() => setIsMinimapOpen((open) => !open)}
            />
          </div>

          {isMinimapOpen && minimapModel ? (
            <button
              type="button"
              className="image-canvas-editor__minimap"
              aria-label="画布小地图"
              title="拖拽移动视图"
              onPointerDown={handleMinimapPointerDown}
            >
              <span className="image-canvas-editor__minimap-stage">
                {minimapModel.layers.map((layer) => (
                  <span
                    key={layer.id}
                    className="image-canvas-editor__minimap-layer"
                    title={layer.title}
                    style={layer.rect}
                  />
                ))}
                <span
                  className="image-canvas-editor__minimap-viewport"
                  style={minimapModel.viewport}
                />
              </span>
            </button>
          ) : null}

          <div
            className="image-canvas-editor__bottom-toolbar"
            role="toolbar"
            aria-label="AI画布工具栏"
            onPointerDown={(event) => event.stopPropagation()}
          >
            {canvasTools.map(({ id, label, icon: Icon }) =>
              id === 'spec' ? (
                <span
                  key={id}
                  ref={specToolWrapRef}
                  className="image-canvas-editor__spec-tool-wrap"
                >
                  <EditorIconButton
                    label={label}
                    title={label}
                    icon={Icon}
                    pressed={effectiveTool === id}
                    onClick={() => switchTool(id)}
                  />
                </span>
              ) : (
                <EditorIconButton
                  key={id}
                  label={label}
                  title={label}
                  icon={Icon}
                  pressed={effectiveTool === id}
                  onClick={() => switchTool(id)}
                />
              ),
            )}
          </div>

          {isSpecMenuOpen
            ? renderEditorPortal(
                <PlatformFloatingMenu
                  className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
                  label="生成规范类型"
                  placement="top-start"
                  style={buildPortalMenuStyle(specToolWrapRef.current, 'above')}
                >
                  {(['character', 'ui', 'custom'] as const).map((specType) => (
                    <PlatformFloatingMenuItem
                      key={specType}
                      className="image-canvas-editor__spec-menu-item"
                      onClick={() => openSpecDialog(specType)}
                    >
                      {SPEC_TYPE_LABEL[specType]}
                    </PlatformFloatingMenuItem>
                  ))}
                </PlatformFloatingMenu>,
              )
            : null}

          {generateDialog?.mode === 'generate' &&
          generateDialog.composerOpen !== false &&
          generationComposerStyle ? (
            <form
              className="image-canvas-editor__generation-composer"
              style={generationComposerStyle}
              role="dialog"
              aria-label="生成图片"
              onPointerDown={(event) => event.stopPropagation()}
              onSubmit={(event) => {
                event.preventDefault();
                if (generateDialog.status !== 'generating') {
                  void submitImageGeneration(generateDialog);
                }
              }}
            >
              <PlatformIconButton
                variant="surfaceFloating"
                className="image-canvas-editor__generation-ref"
                label="添加参考图"
                disabled={generateDialog.status === 'generating'}
                onClick={() => {
                  setUploadTarget('asset');
                  uploadInputRef.current?.click();
                }}
                icon={<ImageIcon className="h-4 w-4" />}
              >
                <span>参考图</span>
              </PlatformIconButton>
              <PlatformTextField
                variant="textarea"
                aria-label="生成提示词"
                value={generateDialog.prompt}
                disabled={generateDialog.status === 'generating'}
                placeholder="今天我们要创作什么"
                size="sm"
                density="compact"
                className="image-canvas-editor__generation-prompt"
                onChange={(event) =>
                  setGenerateDialog((currentDialog) =>
                    currentDialog
                      ? {
                          ...currentDialog,
                          prompt: event.target.value,
                          status:
                            currentDialog.status === 'failed'
                              ? 'idle'
                              : currentDialog.status,
                          errorMessage:
                            currentDialog.status === 'failed'
                              ? undefined
                              : currentDialog.errorMessage,
                        }
                      : currentDialog,
                  )
                }
              />
              <div className="image-canvas-editor__generation-composer-footer">
                <PlatformInlineOptionButton
                  className="image-canvas-editor__generation-ratio"
                  aria-label="生成比例 1:1 2k 1张"
                  disabled={generateDialog.status === 'generating'}
                  onClick={() => triggerPlaceholderAction('生成参数')}
                  trailingIcon={<ChevronDown className="h-3 w-3" />}
                >
                  中 · 1:1(2k) · 1张
                </PlatformInlineOptionButton>
                <PlatformInlineOptionButton
                  className="image-canvas-editor__generation-model"
                  aria-label="生成模型 GPT Image"
                  disabled={generateDialog.status === 'generating'}
                  onClick={() => triggerPlaceholderAction('模型选择')}
                  trailingIcon={<ChevronDown className="h-3 w-3" />}
                >
                  GPT Im...
                </PlatformInlineOptionButton>
                <PlatformActionButton
                  type="submit"
                  tone="secondary"
                  size="xs"
                  shape="pill"
                  className="image-canvas-editor__generation-submit"
                  disabled={generateDialog.status === 'generating'}
                  aria-label="生成"
                >
                  {generateDialog.status === 'generating' ? '生成中' : '12'}
                </PlatformActionButton>
              </div>
              {generateDialog.status === 'generating' ? (
                <PlatformStatusMessage
                  tone="info"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="status"
                >
                  生成中
                </PlatformStatusMessage>
              ) : null}
              {generateDialog.status === 'failed' ? (
                <PlatformStatusMessage
                  tone="error"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="alert"
                >
                  {generateDialog.errorMessage}
                </PlatformStatusMessage>
              ) : null}
              <EditorIconButton
                className="image-canvas-editor__generation-close"
                label="关闭生成图片"
                icon={X}
                variant="surfaceFloating"
                disabled={generateDialog.status === 'generating'}
                onClick={() => {
                  setGenerateDialog((currentDialog) =>
                    currentDialog?.mode === 'generate'
                      ? {
                          ...currentDialog,
                          composerOpen: false,
                        }
                      : currentDialog,
                  );
                  setActiveTool('select');
                }}
              />
            </form>
          ) : null}

          {generateDialog?.mode === 'spec' &&
          generateDialog.composerOpen !== false &&
          generationComposerStyle ? (
            <form
              className="image-canvas-editor__generation-composer image-canvas-editor__spec-composer"
              style={generationComposerStyle}
              role="dialog"
              aria-label="生成规范"
              onPointerDown={(event) => event.stopPropagation()}
              onSubmit={(event) => {
                event.preventDefault();
                if (generateDialog.status !== 'generating') {
                  void submitImageGeneration(generateDialog);
                }
              }}
            >
              <div className="image-canvas-editor__spec-fields">
                {generateDialog.specType === 'custom' ? (
                  <label className="image-canvas-editor__field-block">
                    <PlatformFieldLabel
                      variant="form"
                      className="image-canvas-editor__field-title"
                    >
                      自定义规范提示词
                    </PlatformFieldLabel>
                    <PlatformTextField
                      variant="textarea"
                      aria-label="自定义规范提示词"
                      value={generateDialog.specValues?.customPrompt ?? ''}
                      disabled={generateDialog.status === 'generating'}
                      size="sm"
                      density="compact"
                      className="image-canvas-editor__spec-textarea"
                      onChange={(event) =>
                        updateSpecFormValue('customPrompt', event.target.value)
                      }
                    />
                  </label>
                ) : (
                  <>
                    <label className="image-canvas-editor__field-block">
                      <PlatformFieldLabel
                        variant="form"
                        className="image-canvas-editor__field-title"
                      >
                        玩法设定
                      </PlatformFieldLabel>
                      <PlatformTextField
                        aria-label="玩法设定"
                        value={generateDialog.specValues?.playSetting ?? ''}
                        disabled={generateDialog.status === 'generating'}
                        size="sm"
                        density="compact"
                        className="image-canvas-editor__spec-input"
                        onChange={(event) =>
                          updateSpecFormValue('playSetting', event.target.value)
                        }
                      />
                    </label>
                    <label className="image-canvas-editor__field-block">
                      <PlatformFieldLabel
                        variant="form"
                        className="image-canvas-editor__field-title"
                      >
                        美术风格
                      </PlatformFieldLabel>
                      <PlatformTextField
                        aria-label="美术风格"
                        value={generateDialog.specValues?.artStyle ?? ''}
                        disabled={generateDialog.status === 'generating'}
                        size="sm"
                        density="compact"
                        className="image-canvas-editor__spec-input"
                        onChange={(event) =>
                          updateSpecFormValue('artStyle', event.target.value)
                        }
                      />
                    </label>
                    {generateDialog.specType === 'character' ? (
                      <>
                        <label className="image-canvas-editor__field-block">
                          <PlatformFieldLabel
                            variant="form"
                            className="image-canvas-editor__field-title"
                          >
                            头身比
                          </PlatformFieldLabel>
                          <PlatformSelectField
                            aria-label="头身比"
                            value={generateDialog.specValues?.bodyRatio ?? '3'}
                            disabled={generateDialog.status === 'generating'}
                            size="sm"
                            density="compact"
                            className="image-canvas-editor__spec-input"
                            onChange={(event) =>
                              updateSpecFormValue(
                                'bodyRatio',
                                event.target.value,
                              )
                            }
                          >
                            {['2', '3', '4', '5', '6'].map((value) => (
                              <option key={value} value={value}>
                                {value}
                              </option>
                            ))}
                          </PlatformSelectField>
                        </label>
                        <label className="image-canvas-editor__field-block">
                          <PlatformFieldLabel
                            variant="form"
                            className="image-canvas-editor__field-title"
                          >
                            角色视角
                          </PlatformFieldLabel>
                          <PlatformSelectField
                            aria-label="角色视角"
                            value={
                              generateDialog.specValues?.characterView ?? ''
                            }
                            disabled={generateDialog.status === 'generating'}
                            size="sm"
                            density="compact"
                            className="image-canvas-editor__spec-input"
                            onChange={(event) =>
                              updateSpecFormValue(
                                'characterView',
                                event.target.value,
                              )
                            }
                          >
                            {CHARACTER_SPEC_VIEW_OPTIONS.map((value) => (
                              <option key={value} value={value}>
                                {value}
                              </option>
                            ))}
                          </PlatformSelectField>
                        </label>
                      </>
                    ) : null}
                  </>
                )}
              </div>
              {generateDialog.status === 'failed' ? (
                <PlatformStatusMessage
                  tone="error"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="alert"
                >
                  {generateDialog.errorMessage}
                </PlatformStatusMessage>
              ) : null}
              <div className="image-canvas-editor__spec-footer">
                <PlatformActionButton
                  type="submit"
                  tone="secondary"
                  size="sm"
                  className="image-canvas-editor__spec-submit"
                  disabled={generateDialog.status === 'generating'}
                  aria-label="提交生成规范"
                >
                  {generateDialog.status === 'generating'
                    ? '生成中'
                    : `消耗${SPEC_GENERATION_COST}泥点 · 生成`}
                </PlatformActionButton>
              </div>
            </form>
          ) : null}

          {generateDialog?.mode === 'character' && generationComposerStyle ? (
            <form
              className="image-canvas-editor__character-composer"
              style={generationComposerStyle}
              role="dialog"
              aria-label="生成角色形象"
              onPointerDown={(event) => event.stopPropagation()}
              onSubmit={(event) => {
                event.preventDefault();
                if (generateDialog.status !== 'generating') {
                  void submitImageGeneration(generateDialog);
                }
              }}
            >
              <div className="image-canvas-editor__character-reference-row">
                <div className="image-canvas-editor__field-block image-canvas-editor__character-reference-field image-canvas-editor__character-reference-field--spec">
                  <PlatformFieldLabel
                    variant="field"
                    className="image-canvas-editor__field-title"
                  >
                    角色形象规范
                  </PlatformFieldLabel>
                  <span className="image-canvas-editor__character-spec-wrap">
                    <button
                      ref={characterSpecButtonRef}
                      type="button"
                      className="image-canvas-editor__character-spec-ref image-canvas-editor__reference-tile image-canvas-editor__reference-tile--spec"
                      disabled={generateDialog.status === 'generating'}
                      onClick={() =>
                        setIsCharacterSpecMenuOpen((open) => !open)
                      }
                    >
                      <span className="image-canvas-editor__reference-tile-visual">
                        {generateDialog.characterSpecReference ? (
                          <img
                            src={generateDialog.characterSpecReference.src}
                            alt=""
                            aria-hidden="true"
                          />
                        ) : (
                          <ClipboardList
                            className="h-4 w-4"
                            aria-hidden="true"
                          />
                        )}
                      </span>
                      <span className="image-canvas-editor__reference-tile-copy">
                        {generateDialog.characterSpecReference?.label ??
                          '角色形象规范'}
                      </span>
                    </button>
                  </span>
                </div>
                {isCharacterSpecMenuOpen
                  ? renderEditorPortal(
                      <PlatformFloatingMenu
                        className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
                        label="角色形象规范来源"
                        placement="bottom-start"
                        style={buildPortalMenuStyle(
                          characterSpecButtonRef.current,
                          'below',
                        )}
                      >
                        <PlatformFloatingMenuItem
                          className="image-canvas-editor__context-menu-item"
                          onClick={() => {
                            setIsPickingCharacterSpecFromCanvas(true);
                            setIsCharacterSpecMenuOpen(false);
                          }}
                        >
                          从画布中选择
                        </PlatformFloatingMenuItem>
                        <PlatformFloatingMenuItem
                          className="image-canvas-editor__context-menu-item"
                          onClick={() => {
                            setIsCharacterSpecMenuOpen(false);
                            openSpecDialog('character');
                          }}
                        >
                          新建角色形象规范
                        </PlatformFloatingMenuItem>
                        <PlatformFloatingMenuItem
                          className="image-canvas-editor__context-menu-item"
                          onClick={() => {
                            setUploadTarget('character-spec');
                            setIsCharacterSpecMenuOpen(false);
                            uploadInputRef.current?.click();
                          }}
                        >
                          上传图片
                        </PlatformFloatingMenuItem>
                      </PlatformFloatingMenu>,
                    )
                  : null}
                <div className="image-canvas-editor__field-block image-canvas-editor__character-reference-field image-canvas-editor__character-reference-field--regular">
                  <PlatformFieldLabel
                    variant="field"
                    className="image-canvas-editor__field-title"
                  >
                    常规参考图
                  </PlatformFieldLabel>
                  <div className="image-canvas-editor__character-reference-list">
                    {(generateDialog.characterReferences ?? []).map(
                      (reference, index) => (
                        <span
                          key={reference.id}
                          className="image-canvas-editor__character-ref-thumb"
                          title={reference.label}
                        >
                          <img src={reference.src} alt={reference.label} />
                          <span className="image-canvas-editor__character-ref-index">
                            {index + 1}
                          </span>
                        </span>
                      ),
                    )}
                    <button
                      type="button"
                      className="image-canvas-editor__character-reference-add image-canvas-editor__reference-tile image-canvas-editor__reference-tile--upload"
                      disabled={generateDialog.status === 'generating'}
                      onClick={() => {
                        setUploadTarget('character-reference');
                        uploadInputRef.current?.click();
                      }}
                    >
                      <span className="image-canvas-editor__reference-tile-visual">
                        <ImagePlus className="h-4 w-4" aria-hidden="true" />
                      </span>
                      <span className="image-canvas-editor__reference-tile-copy">
                        上传常规参考图
                      </span>
                    </button>
                  </div>
                </div>
              </div>
              <label className="image-canvas-editor__field-block">
                <PlatformFieldLabel
                  variant="field"
                  className="image-canvas-editor__field-title"
                >
                  角色设定
                </PlatformFieldLabel>
                <PlatformTextField
                  variant="textarea"
                  aria-label="角色设定"
                  value={generateDialog.prompt}
                  disabled={generateDialog.status === 'generating'}
                  size="sm"
                  density="compact"
                  className="image-canvas-editor__generation-prompt"
                  onChange={(event) =>
                    setGenerateDialog((currentDialog) =>
                      currentDialog?.mode === 'character'
                        ? {
                            ...currentDialog,
                            prompt: event.target.value,
                            status:
                              currentDialog.status === 'failed'
                                ? 'idle'
                                : currentDialog.status,
                            errorMessage:
                              currentDialog.status === 'failed'
                                ? undefined
                                : currentDialog.errorMessage,
                          }
                        : currentDialog,
                    )
                  }
                />
              </label>
              {generateDialog.status === 'failed' ? (
                <PlatformStatusMessage
                  tone="error"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="alert"
                >
                  {generateDialog.errorMessage}
                </PlatformStatusMessage>
              ) : null}
              <div className="image-canvas-editor__generation-composer-footer">
                <div className="image-canvas-editor__option-field">
                  <PlatformFieldLabel
                    variant="field"
                    className="image-canvas-editor__field-title"
                  >
                    画面比例
                  </PlatformFieldLabel>
                  <PlatformInlineOptionButton
                    className="image-canvas-editor__generation-ratio"
                    disabled={generateDialog.status === 'generating'}
                    onClick={() => triggerPlaceholderAction('角色比例')}
                  >
                    1:1
                  </PlatformInlineOptionButton>
                </div>
                <div className="image-canvas-editor__option-field">
                  <PlatformFieldLabel
                    variant="field"
                    className="image-canvas-editor__field-title"
                  >
                    模型
                  </PlatformFieldLabel>
                  <PlatformInlineOptionButton
                    className="image-canvas-editor__generation-model"
                    disabled={generateDialog.status === 'generating'}
                    onClick={() => triggerPlaceholderAction('角色模型')}
                  >
                    GPT Image
                  </PlatformInlineOptionButton>
                </div>
                <PlatformActionButton
                  type="submit"
                  tone="secondary"
                  size="xs"
                  shape="pill"
                  className="image-canvas-editor__generation-submit"
                  disabled={generateDialog.status === 'generating'}
                >
                  {generateDialog.status === 'generating' ? '生成中' : '生成'}
                </PlatformActionButton>
              </div>
            </form>
          ) : null}

          {generateDialog?.mode === 'icon' &&
          generateDialog.composerOpen !== false &&
          iconComposerStyle ? (
            <form
              className="image-canvas-editor__icon-composer"
              style={iconComposerStyle}
              role="dialog"
              aria-label="生成图标素材"
              onPointerDown={(event) => event.stopPropagation()}
              onSubmit={(event) => {
                event.preventDefault();
                if (generateDialog.status !== 'generating') {
                  void submitIconSpritesheetGeneration(generateDialog);
                }
              }}
            >
              <div className="image-canvas-editor__field-block">
                <PlatformFieldLabel
                  variant="field"
                  className="image-canvas-editor__field-title"
                >
                  图标素材规范
                </PlatformFieldLabel>
                <div className="image-canvas-editor__icon-spec-row">
                  <span className="image-canvas-editor__character-spec-wrap">
                    <button
                      ref={iconSpecButtonRef}
                      type="button"
                      className="image-canvas-editor__icon-spec-card"
                      disabled={generateDialog.status === 'generating'}
                      aria-label={
                        generateDialog.iconSpecReference?.label ??
                        '图标素材规范'
                      }
                      onClick={() => setIsIconSpecMenuOpen((open) => !open)}
                    >
                      <span
                        className="image-canvas-editor__icon-spec-preview"
                        aria-hidden="true"
                      >
                        {generateDialog.iconSpecReference?.src ? (
                          <img
                            src={generateDialog.iconSpecReference.src}
                            alt=""
                          />
                        ) : (
                          <ImageIcon className="h-5 w-5" />
                        )}
                      </span>
                      <span className="image-canvas-editor__icon-spec-copy">
                        <span className="image-canvas-editor__icon-spec-eyebrow">
                          图标素材规范
                        </span>
                        <span className="image-canvas-editor__icon-spec-title">
                          {generateDialog.iconSpecReference?.label ?? '待选择'}
                        </span>
                      </span>
                      <span className="image-canvas-editor__icon-spec-state">
                        {generateDialog.iconSpecReference ? '已绑定' : '待绑定'}
                      </span>
                    </button>
                  </span>
                  {isIconSpecMenuOpen
                  ? renderEditorPortal(
                      <PlatformFloatingMenu
                        className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
                        label="图标素材规范来源"
                        placement="bottom-start"
                        style={buildPortalMenuStyle(
                          iconSpecButtonRef.current,
                          'below',
                        )}
                      >
                        <PlatformFloatingMenuItem
                          className="image-canvas-editor__context-menu-item"
                          onClick={() => {
                            setIsPickingIconSpecFromCanvas(true);
                            setIsIconSpecMenuOpen(false);
                          }}
                        >
                          从画布中选择
                        </PlatformFloatingMenuItem>
                        <PlatformFloatingMenuItem
                          className="image-canvas-editor__context-menu-item"
                          onClick={() => {
                            setIsIconSpecMenuOpen(false);
                            openSpecDialog('icon');
                          }}
                        >
                          新建图标素材规范
                        </PlatformFloatingMenuItem>
                        <PlatformFloatingMenuItem
                          className="image-canvas-editor__context-menu-item"
                          onClick={() => {
                            setUploadTarget('icon-spec');
                            setIsIconSpecMenuOpen(false);
                            uploadInputRef.current?.click();
                          }}
                        >
                          上传图片
                        </PlatformFloatingMenuItem>
                      </PlatformFloatingMenu>,
                    )
                  : null}
                  <div
                    className="image-canvas-editor__icon-spec-actions"
                    aria-label="图标素材规范操作"
                  >
                    <button
                      type="button"
                      disabled={generateDialog.status === 'generating'}
                      onClick={() => {
                        setIsPickingIconSpecFromCanvas(true);
                        setIsIconSpecMenuOpen(false);
                      }}
                    >
                      画布
                    </button>
                    <button
                      type="button"
                      disabled={generateDialog.status === 'generating'}
                      onClick={() => {
                        setIsIconSpecMenuOpen(false);
                        openSpecDialog('icon');
                      }}
                    >
                      新建
                    </button>
                    <button
                      type="button"
                      disabled={generateDialog.status === 'generating'}
                      onClick={() => {
                        setUploadTarget('icon-spec');
                        setIsIconSpecMenuOpen(false);
                        uploadInputRef.current?.click();
                      }}
                    >
                      上传
                    </button>
                  </div>
                </div>
              </div>
              <div className="image-canvas-editor__field-block">
                <PlatformFieldLabel
                  variant="field"
                  className="image-canvas-editor__field-title"
                >
                  素材描述
                </PlatformFieldLabel>
                <div className="image-canvas-editor__icon-description-list">
                  {iconDescriptionValues.map((description, index) => (
                    <label
                      key={index}
                      className="image-canvas-editor__icon-description-card"
                    >
                      <span className="image-canvas-editor__icon-description-index">
                        {String(index + 1).padStart(2, '0')}
                      </span>
                      <span className="image-canvas-editor__icon-description-title">
                        素材描述 {index + 1}
                      </span>
                      <PlatformTextField
                        aria-label={`素材描述${index + 1}`}
                        value={description}
                        disabled={generateDialog.status === 'generating'}
                        size="sm"
                        density="compact"
                        className="image-canvas-editor__icon-description-input"
                        onChange={(event) =>
                          updateIconDescription(index, event.target.value)
                        }
                      />
                    </label>
                  ))}
                </div>
              </div>
              {generateDialog.status === 'failed' ? (
                <PlatformStatusMessage
                  tone="error"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="alert"
                >
                  {generateDialog.errorMessage}
                </PlatformStatusMessage>
              ) : null}
              <div className="image-canvas-editor__icon-footer">
                <button
                  type="button"
                  className="image-canvas-editor__character-reference-add"
                  disabled={
                    generateDialog.status === 'generating' ||
                    (
                      generateDialog.iconDescriptions ??
                      DEFAULT_ICON_DESCRIPTIONS
                    ).length >= ICON_DESCRIPTION_LIMIT
                  }
                  onClick={addIconDescription}
                >
                  添加素材描述
                </button>
                <div className="image-canvas-editor__option-field">
                  <PlatformFieldLabel
                    variant="field"
                    className="image-canvas-editor__field-title"
                  >
                    模型
                  </PlatformFieldLabel>
                  <PlatformInlineOptionButton
                    className="image-canvas-editor__generation-model"
                    disabled={generateDialog.status === 'generating'}
                    onClick={() => triggerPlaceholderAction('图标模型')}
                  >
                    nanobanana2
                  </PlatformInlineOptionButton>
                </div>
                <PlatformActionButton
                  type="submit"
                  tone="secondary"
                  size="xs"
                  shape="pill"
                  className="image-canvas-editor__generation-submit"
                  disabled={generateDialog.status === 'generating'}
                  aria-label="生成"
                >
                  {generateDialog.status === 'generating' ? '生成中' : '生成'}
                </PlatformActionButton>
              </div>
            </form>
          ) : null}

          {isPickingCharacterSpecFromCanvas ? (
            <div className="image-canvas-editor__canvas-pick-hint">
              请选择画布中的图片作为角色形象规范，按 Esc 退出
            </div>
          ) : null}
          {isPickingIconSpecFromCanvas ? (
            <div className="image-canvas-editor__canvas-pick-hint">
              请选择画布中的图标素材规范，按 Esc 退出
            </div>
          ) : null}

          {quickEditPanel &&
          quickEditPanel.status !== 'generating' &&
          quickEditSourceLayer &&
          quickEditPanelStyle ? (
            <form
              className="image-canvas-editor__quick-edit-panel"
              style={quickEditPanelStyle}
              role="dialog"
              aria-label="快速编辑图片"
              onPointerDown={(event) => event.stopPropagation()}
              onSubmit={(event) => {
                event.preventDefault();
                void submitQuickEdit();
              }}
            >
              <div className="image-canvas-editor__quick-edit-head">
                <div className="image-canvas-editor__quick-edit-reference">
                  <img
                    src={quickEditSourceLayer.src}
                    alt={`${quickEditSourceLayer.title}参考图`}
                  />
                  <span>{quickEditSourceLayer.title}</span>
                </div>
                <EditorIconButton
                  label="关闭快速编辑图片"
                  title="关闭"
                  icon={X}
                  onClick={() => setQuickEditPanel(null)}
                />
              </div>
              <PlatformTextField
                variant="textarea"
                aria-label="快速编辑提示词"
                value={quickEditPanel.prompt}
                size="sm"
                density="compact"
                className="image-canvas-editor__quick-edit-prompt"
                onChange={(event) =>
                  setQuickEditPanel((currentPanel) =>
                    currentPanel
                      ? {
                          ...currentPanel,
                          prompt: event.target.value,
                          status:
                            currentPanel.status === 'failed'
                              ? 'idle'
                              : currentPanel.status,
                          errorMessage:
                            currentPanel.status === 'failed'
                              ? undefined
                              : currentPanel.errorMessage,
                        }
                      : currentPanel,
                  )
                }
              />
              <div className="image-canvas-editor__quick-edit-controls">
                <PlatformSelectField
                  aria-label="快速编辑尺寸"
                  value={quickEditPanel.size}
                  size="xs"
                  density="compact"
                  onChange={(event) =>
                    setQuickEditPanel((currentPanel) =>
                      currentPanel
                        ? { ...currentPanel, size: event.target.value }
                        : currentPanel,
                    )
                  }
                >
                  {quickEditSizeOptions.map((size) => (
                    <option key={size} value={size}>
                      {size}
                    </option>
                  ))}
                </PlatformSelectField>
                <PlatformSelectField
                  aria-label="快速编辑模型"
                  value={quickEditPanel.model}
                  size="xs"
                  density="compact"
                  onChange={(event) =>
                    setQuickEditPanel((currentPanel) =>
                      currentPanel
                        ? { ...currentPanel, model: event.target.value }
                        : currentPanel,
                    )
                  }
                >
                  {quickEditModelOptions.map((option) => (
                    <option key={option.value} value={option.value}>
                      {option.label}
                    </option>
                  ))}
                </PlatformSelectField>
              </div>
              {quickEditPanel.status === 'failed' ? (
                <PlatformStatusMessage
                  tone="error"
                  surface="platform"
                  size="xs"
                  role="alert"
                >
                  {quickEditPanel.errorMessage}
                </PlatformStatusMessage>
              ) : null}
              <PlatformActionButton
                type="submit"
                tone="secondary"
                size="sm"
                className="image-canvas-editor__quick-edit-submit"
              >
                生成
              </PlatformActionButton>
            </form>
          ) : null}

          {imageContextMenu && imageContextMenuLayer ? (
            <div
              className="image-canvas-editor__context-menu"
              style={{
                left: imageContextMenu.x,
                top: imageContextMenu.y,
              }}
              onPointerDown={(event) => event.stopPropagation()}
            >
              <PlatformFloatingMenu
                label="图片功能面板"
                placement="bottom-start"
              >
                <PlatformFloatingMenuItem
                  className="image-canvas-editor__context-menu-item"
                  onClick={() => openQuickEditPanel(imageContextMenuLayer)}
                >
                  快速编辑
                </PlatformFloatingMenuItem>
                <PlatformFloatingMenuItem
                  className="image-canvas-editor__context-menu-item"
                  onClick={() => {
                    setMetadataLayer(imageContextMenuLayer);
                    setImageContextMenu(null);
                  }}
                >
                  查看图片信息
                </PlatformFloatingMenuItem>
                {imageContextMenuLayer.assetKind === 'character' ? (
                  <PlatformFloatingMenuItem
                    className="image-canvas-editor__context-menu-item"
                    onClick={() =>
                      openCharacterAnimationPanel(imageContextMenuLayer)
                    }
                  >
                    生成动画
                  </PlatformFloatingMenuItem>
                ) : null}
                <PlatformFloatingMenuItem
                  className="image-canvas-editor__context-menu-item"
                  onClick={() => deleteLayerById(imageContextMenuLayer.id)}
                >
                  删除图片
                </PlatformFloatingMenuItem>
              </PlatformFloatingMenu>
            </div>
          ) : null}

          {characterAnimationPanel &&
          characterAnimationSourceLayer &&
          characterAnimationPanelStyle ? (
            <form
              className="image-canvas-editor__character-animation-panel"
              style={characterAnimationPanelStyle}
              role="dialog"
              aria-label="角色动画生成面板"
              onPointerDown={(event) => event.stopPropagation()}
              onSubmit={(event) => {
                event.preventDefault();
                if (characterAnimationPanel.status !== 'generating') {
                  void submitCharacterAnimation();
                }
              }}
            >
              <div className="image-canvas-editor__character-animation-head">
                <strong>角色动画</strong>
                <EditorIconButton
                  label="关闭角色动画生成面板"
                  title="关闭"
                  icon={X}
                  onClick={() => setCharacterAnimationPanel(null)}
                />
              </div>
              <PlatformTextField
                variant="textarea"
                aria-label="动画描述"
                value={characterAnimationPanel.promptText}
                maxLength={4000}
                disabled={characterAnimationPanel.status === 'generating'}
                size="sm"
                density="compact"
                className="image-canvas-editor__character-animation-textarea"
                onChange={(event) =>
                  setCharacterAnimationPanel((currentPanel) =>
                    currentPanel
                      ? {
                          ...currentPanel,
                          promptText: event.target.value.slice(0, 4000),
                          status:
                            currentPanel.status === 'failed'
                              ? 'idle'
                              : currentPanel.status,
                          errorMessage:
                            currentPanel.status === 'failed'
                              ? undefined
                              : currentPanel.errorMessage,
                        }
                      : currentPanel,
                  )
                }
              />
              <div className="image-canvas-editor__character-animation-presets">
                {CHARACTER_ANIMATION_ACTION_PROMPTS.map((preset) => (
                  <button
                    key={preset.label}
                    type="button"
                    className="image-canvas-editor__character-animation-preset"
                    disabled={characterAnimationPanel.status === 'generating'}
                    onClick={() =>
                      setCharacterAnimationPanel((currentPanel) =>
                        currentPanel
                          ? {
                              ...currentPanel,
                              promptText: preset.text,
                              status: 'idle',
                              errorMessage: undefined,
                            }
                          : currentPanel,
                      )
                    }
                  >
                    {preset.label}
                  </button>
                ))}
              </div>
              <div className="image-canvas-editor__character-animation-grid">
                <PlatformSelectField
                  aria-label="分辨率"
                  value={characterAnimationPanel.resolution}
                  disabled={characterAnimationPanel.status === 'generating'}
                  size="xs"
                  density="compact"
                  onChange={(event) =>
                    setCharacterAnimationPanel((currentPanel) =>
                      currentPanel
                        ? {
                            ...currentPanel,
                            resolution:
                              event.target.value === '720p' ? '720p' : '480p',
                            status:
                              currentPanel.status === 'failed'
                                ? 'idle'
                                : currentPanel.status,
                            errorMessage:
                              currentPanel.status === 'failed'
                                ? undefined
                                : currentPanel.errorMessage,
                          }
                        : currentPanel,
                    )
                  }
                >
                  <option value="480p">480p</option>
                  <option value="720p">720p</option>
                </PlatformSelectField>
                <PlatformSelectField
                  aria-label="画面比例"
                  value={characterAnimationPanel.ratio}
                  disabled={characterAnimationPanel.status === 'generating'}
                  size="xs"
                  density="compact"
                  onChange={(event) =>
                    setCharacterAnimationPanel((currentPanel) =>
                      currentPanel
                        ? {
                            ...currentPanel,
                            ratio:
                              CHARACTER_ANIMATION_RATIO_OPTIONS.find(
                                (item) => item.value === event.target.value,
                              )?.value ?? 'same',
                            status:
                              currentPanel.status === 'failed'
                                ? 'idle'
                                : currentPanel.status,
                            errorMessage:
                              currentPanel.status === 'failed'
                                ? undefined
                                : currentPanel.errorMessage,
                          }
                        : currentPanel,
                    )
                  }
                >
                  {CHARACTER_ANIMATION_RATIO_OPTIONS.map((option) => (
                    <option key={option.value} value={option.value}>
                      {option.label}
                    </option>
                  ))}
                </PlatformSelectField>
                <PlatformSelectField
                  aria-label="时长"
                  value={String(characterAnimationPanel.frameCount)}
                  disabled={characterAnimationPanel.status === 'generating'}
                  size="xs"
                  density="compact"
                  onChange={(event) =>
                    updateCharacterAnimationDuration(event.target.value)
                  }
                >
                  {CHARACTER_ANIMATION_DURATION_OPTIONS.map((option) => (
                    <option
                      key={option.frameCount}
                      value={String(option.frameCount)}
                    >
                      {option.label}
                    </option>
                  ))}
                </PlatformSelectField>
              </div>
              <div className="image-canvas-editor__character-animation-summary">
                <span
                  className="image-canvas-editor__character-animation-summary-text"
                  title={characterAnimationPanel.promptText.trim() || undefined}
                  aria-label={`生成文本：${
                    characterAnimationPanel.promptText.trim() || '动画描述'
                  }`}
                >
                  {characterAnimationPanel.promptText.trim()
                    ? characterAnimationPanel.promptText.trim()
                    : '动画描述'}
                </span>
                <strong>{characterAnimationPrice}泥点</strong>
              </div>
              {characterAnimationPanel.status === 'completed' &&
              characterAnimationPanel.result ? (
                <PlatformStatusMessage
                  tone="success"
                  surface="platform"
                  size="xs"
                  role="status"
                >
                  已生成 {characterAnimationPanel.result.frameCount} 帧
                </PlatformStatusMessage>
              ) : null}
              {characterAnimationPanel.status === 'failed' ? (
                <PlatformStatusMessage
                  tone="error"
                  surface="platform"
                  size="xs"
                  role="alert"
                >
                  {characterAnimationPanel.errorMessage}
                </PlatformStatusMessage>
              ) : null}
              <PlatformActionButton
                type="submit"
                tone="secondary"
                size="sm"
                className="image-canvas-editor__character-animation-submit"
                disabled={characterAnimationPanel.status === 'generating'}
              >
                {characterAnimationPanel.status === 'generating'
                  ? '生成中'
                  : '生成'}
              </PlatformActionButton>
            </form>
          ) : null}
        </div>
      </div>

      <UnifiedModal
        open={Boolean(metadataLayer)}
        title={metadataLayer ? `${metadataLayer.title}图片信息` : '图片信息'}
        size="sm"
        closeLabel="关闭图片信息"
        onClose={() => setMetadataLayer(null)}
        panelClassName="image-canvas-editor__metadata-dialog"
        bodyClassName="image-canvas-editor__metadata-body"
      >
        {metadataLayer ? (
          <dl className="image-canvas-editor__metadata-grid">
            <dt>图片类型</dt>
            <dd>{formatLayerImageType(metadataLayer)}</dd>
            <dt>生成输入</dt>
            <dd className="image-canvas-editor__metadata-inputs">
              {metadataLayer.generationInputs?.fields.length ||
              metadataLayer.generationInputs?.references.length ? (
                <>
                  {metadataLayer.generationInputs.fields.map((field) => (
                    <div
                      key={`${field.title}-${field.value}`}
                      className="image-canvas-editor__metadata-input-field"
                    >
                      <span className="image-canvas-editor__metadata-input-title">
                        {field.title}
                      </span>
                      <span>{field.value}</span>
                    </div>
                  ))}
                  {metadataLayer.generationInputs.references.length ? (
                    <div className="image-canvas-editor__metadata-reference-list">
                      {metadataLayer.generationInputs.references.map(
                        (reference) => (
                          <div
                            key={`${reference.title}-${reference.label}-${reference.src}`}
                            className="image-canvas-editor__metadata-reference-card"
                          >
                            <img src={reference.src} alt="" aria-hidden="true" />
                            <span className="image-canvas-editor__metadata-reference-copy">
                              <span className="image-canvas-editor__metadata-input-title">
                                {reference.title}
                              </span>
                              <span>{reference.label}</span>
                            </span>
                          </div>
                        ),
                      )}
                    </div>
                  ) : null}
                </>
              ) : (
                '-'
              )}
            </dd>
            <dt>Model</dt>
            <dd>{metadataLayer.model ?? '-'}</dd>
            <dt>Resolution</dt>
            <dd>
              {metadataLayer.originalWidth} x {metadataLayer.originalHeight} px
            </dd>
            <dt>Provider</dt>
            <dd>{metadataLayer.provider ?? '-'}</dd>
            <dt>Task</dt>
            <dd>{metadataLayer.taskId ?? '-'}</dd>
            <dt>Object</dt>
            <dd>
              {metadataLayer.objectKey ?? metadataLayer.assetObjectId ?? '-'}
            </dd>
          </dl>
        ) : null}
      </UnifiedModal>

      <UnifiedModal
        open={
          generateDialog?.mode === 'edit' &&
          generateDialog.status !== 'generating'
        }
        title={generateDialog?.mode === 'edit' ? '修改图片' : '生成图片'}
        size="sm"
        closeLabel={
          generateDialog?.mode === 'edit' ? '关闭修改图片' : '关闭生成图片'
        }
        closeDisabled={generateDialog?.status === 'generating'}
        onClose={() => setGenerateDialog(null)}
        panelClassName="image-canvas-editor__generate-dialog"
        bodyClassName="image-canvas-editor__generate-dialog-body"
      >
        {generateDialog?.mode === 'edit' ? (
          <form
            className="image-canvas-editor__generate-form"
            onSubmit={(event) => {
              event.preventDefault();
              if (generateDialog.status !== 'generating') {
                void submitImageGeneration(generateDialog);
              }
            }}
          >
            <div className="image-canvas-editor__generate-body">
              <PlatformTextField
                variant="textarea"
                aria-label="生成提示词"
                value={generateDialog.prompt}
                disabled={generateDialog.status === 'generating'}
                size="sm"
                density="roomy"
                className="image-canvas-editor__generate-prompt"
                placeholder={
                  generateDialog.mode === 'edit'
                    ? '描述你想如何修改这张图片'
                    : '描述你想生成的图片'
                }
                onChange={(event) =>
                  setGenerateDialog((currentDialog) =>
                    currentDialog
                      ? {
                          ...currentDialog,
                          prompt: event.target.value,
                        }
                      : currentDialog,
                  )
                }
              />
              {generateDialog.status === 'generating' ? (
                <PlatformStatusMessage
                  tone="info"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="status"
                >
                  {generateDialog.mode === 'edit' ? '修改中' : '生成中'}
                </PlatformStatusMessage>
              ) : null}
              {generateDialog.status === 'failed' ? (
                <PlatformStatusMessage
                  tone="error"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="alert"
                >
                  {generateDialog.errorMessage}
                </PlatformStatusMessage>
              ) : null}
              <PlatformActionButton
                type="submit"
                size="sm"
                className="image-canvas-editor__generate-submit"
                disabled={generateDialog.status === 'generating'}
              >
                {generateDialog.status === 'generating'
                  ? generateDialog.mode === 'edit'
                    ? '修改中'
                    : '生成中'
                  : generateDialog.mode === 'edit'
                    ? '修改'
                    : '生成'}
              </PlatformActionButton>
            </div>
          </form>
        ) : null}
      </UnifiedModal>
    </section>
  );
}

function resolveSnappedLayerPosition(
  movingLayer: CanvasLayer,
  proposedX: number,
  proposedY: number,
  layers: CanvasLayer[],
  scale: number,
) {
  const threshold = SNAP_THRESHOLD_SCREEN_PX / Math.max(scale, MIN_SCALE);
  const verticalTargets = [
    0,
    CANVAS_WORLD_ORIGIN,
    ...layers
      .filter((layer) => layer.id !== movingLayer.id)
      .flatMap((layer) => [
        layer.x,
        layer.x + layer.width / 2,
        layer.x + layer.width,
      ]),
  ];
  const horizontalTargets = [
    0,
    CANVAS_WORLD_ORIGIN,
    ...layers
      .filter((layer) => layer.id !== movingLayer.id)
      .flatMap((layer) => [
        layer.y,
        layer.y + layer.height / 2,
        layer.y + layer.height,
      ]),
  ];

  const xSnap = findNearestSnap(
    proposedX,
    [0, movingLayer.width / 2, movingLayer.width],
    verticalTargets,
    threshold,
  );
  const ySnap = findNearestSnap(
    proposedY,
    [0, movingLayer.height / 2, movingLayer.height],
    horizontalTargets,
    threshold,
  );

  return {
    x: xSnap ? xSnap.position : proposedX,
    y: ySnap ? ySnap.position : proposedY,
    guide:
      xSnap || ySnap
        ? {
            vertical: xSnap?.guide,
            horizontal: ySnap?.guide,
          }
        : null,
  };
}

function findNearestSnap(
  origin: number,
  offsets: number[],
  targets: number[],
  threshold: number,
): SnapCandidate | null {
  let nearest: SnapCandidate | null = null;
  for (const offset of offsets) {
    for (const target of targets) {
      const distance = Math.abs(target - (origin + offset));
      if (distance > threshold) {
        continue;
      }
      if (!nearest || distance < nearest.distance) {
        nearest = {
          position: target - offset,
          guide: target,
          distance,
        };
      }
    }
  }
  return nearest;
}

export default ImageCanvasEditorView;
