import {
  Braces,
  Check,
  ChevronDown,
  ChevronLeft,
  ChevronRight,
  ClipboardList,
  Copy,
  Crop,
  Download,
  Folder,
  Hand,
  ImageIcon,
  ImagePlus,
  Info,
  Layers,
  Map as MapIcon,
  MousePointer2,
  Pencil,
  Redo2,
  RotateCcw,
  Shapes,
  SlidersHorizontal,
  Sparkles,
  Trash2,
  Type,
  Undo2,
  WandSparkles,
  X,
} from 'lucide-react';
import JSZip from 'jszip';
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 EditorIconSpritesheetGenerationResult,
  type EditorIconSpritesheetIconResult,
  type EditorImageGenerationResult,
  generateEditorCharacterAnimation,
  generateEditorIconSpritesheet,
  generateEditorImage,
  loadEditorAssetLibrary,
  loadEditorProject,
  loadOrCreateRecentEditorProject,
  renameEditorProject,
  saveEditorProjectLayout,
  updateEditorAsset,
  updateEditorAssetFolder,
} from '../../services/image-editor/editorProjectClient';
import { PlatformActionButton } from '../common/PlatformActionButton';
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 { useAuthUi } from '../auth/AuthUiContext';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import { ImageCanvasSidebarView } from './ImageCanvasSidebarView';
import {
  ASSET_DRAG_MIME_TYPE,
  CANVAS_BACKGROUND_OPTIONS,
  CANVAS_WORLD_SIZE,
  DEFAULT_CANVAS_BACKGROUND_COLOR,
  DEFAULT_CANVAS_SIZE,
  EDITOR_ASSET_FOLDERS,
  FIT_VIEW_PADDING,
  MAX_HISTORY_STEPS,
  MAX_SCALE,
  MIN_SCALE,
  MINIMAP_DRAG_SENSITIVITY,
  MINIMAP_PADDING,
  MINIMAP_SIZE,
  TOOLBAR_HALF_WIDTH,
  clamp,
  createLayerFromAsset,
  escapeCssIdentifier,
  formatImageSizeValue,
  formatPercent,
  getDraggedAssetId,
  getLayerBounds,
  hasDataTransferType,
  hydrateLayer,
  isGeneratedLayer,
  isLayerLinkedToAsset,
  normalizeAssetLibrary,
  normalizeCanvasBackgroundHex,
  resolveContextMenuPosition,
  resolveLayerResolutionSize,
  resolveSnappedLayerPosition,
  serializeLayer,
} from './ImageCanvasEditorModel';
import {
  blobToUint8Array,
  buildLayerExportMetadata,
  formatExportDate,
  getImageExtensionFromTypeOrSrc,
  getLayerExportKey,
  readLayerImageBlob,
  sanitizeExportFilePart,
} from './ImageCanvasExportModel';
import {
  CHARACTER_ANIMATION_ACTION_PROMPTS,
  CHARACTER_ANIMATION_DURATION_OPTIONS,
  CHARACTER_ANIMATION_MODEL,
  CHARACTER_ANIMATION_RATIO_OPTIONS,
  CHARACTER_FRAME_DISPLAY_SIZE,
  CHARACTER_FRAME_ORIGINAL_SIZE,
  CHARACTER_SPEC_VIEW_OPTIONS,
  DEFAULT_ICON_DESCRIPTIONS,
  DEFAULT_IMAGE_MODEL,
  DEFAULT_SPEC_FORM_VALUES,
  ICON_COMPOSER_HORIZONTAL_CHROME_REM,
  ICON_COMPOSER_MIN_WIDTH_REM,
  ICON_DESCRIPTION_CARD_WIDTH_REM,
  ICON_DESCRIPTION_LIMIT,
  ICON_FRAME_DISPLAY_SIZE,
  ICON_FRAME_ORIGINAL_SIZE,
  SPEC_FRAME_DISPLAY_SIZE,
  SPEC_FRAME_ORIGINAL_SIZE,
  SPEC_GENERATION_COST,
  SPEC_GENERATION_SIZE,
  SPEC_TYPE_LABEL,
  buildCharacterGenerationInputs,
  buildEditGenerationInputs,
  buildIconGenerationInputs,
  buildImageGenerationInputs,
  buildQuickEditModelOptions,
  buildQuickEditSizeOptions,
  buildSpecGenerationInputs,
  buildSpecPrompt,
  calculateCharacterAnimationPrice,
  createCanvasLayerReference,
  formatLayerImageType,
  getGenerationFrameAriaLabel,
  getGenerationFrameLabel,
  getLayerKindLabel,
  isCanvasGenerationDialog,
  resolveCharacterAnimationSourceImageSrc,
  resolveImageGenerationErrorMessage,
} from './ImageCanvasGenerationModel';
import type {
  AssetMarqueeState,
  AssetPointerDragState,
  CanvasAssetExportImage,
  CanvasAssetExportMetadata,
  CanvasClipboard,
  CanvasContextMenuState,
  CanvasGenerationDialogState,
  CanvasGenerationInputs,
  CanvasHistorySnapshot,
  CanvasLayer,
  CanvasMarqueeState,
  CanvasTool,
  CanvasViewport,
  CharacterAnimationPanelState,
  DragState,
  EditorAsset,
  EditorAssetFolder,
  GenerateDialogState,
  ImageContextMenuState,
  QuickEditPanelState,
  SidebarPanel,
  SnapGuide,
  SpecFormValues,
  SpecGenerationType,
  UploadTarget,
} from './ImageCanvasEditorTypes';

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

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 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 isEditorAuthError(error: unknown) {
  return (
    error instanceof ApiClientError &&
    (error.status === 401 || error.status === 403)
  );
}

export function ImageCanvasEditorView() {
  const authUi = useAuthUi();
  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 assetPointerDragRef = useRef<AssetPointerDragState | null>(null);
  const authUiRef = useRef(authUi);
  const isShiftPressedRef = useRef(false);
  const layerCounterRef = useRef(0);
  const generationDialogCounterRef = useRef(0);
  const saveTimerRef = useRef<number | null>(null);
  const undoStackRef = useRef<CanvasHistorySnapshot[]>([]);
  const redoStackRef = useRef<CanvasHistorySnapshot[]>([]);
  const layersRef = useRef<CanvasLayer[]>([]);
  const viewportRef = useRef<CanvasViewport>({
    x: -260,
    y: 70,
    scale: 0.82,
  });
  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 selectedLayerIdsRef = useRef<string[]>([]);
  const generateDialogRef = useRef<GenerateDialogState | null>(null);
  const inactiveGenerateDialogsRef = useRef<CanvasGenerationDialogState[]>([]);
  const deleteLayerByIdRef = useRef<(targetLayerId: string | null) => void>(
    () => {},
  );
  const suppressAssetClickRef = useRef(false);
  const [projectId, setProjectId] = useState<string | null>(null);
  const [projectTitle, setProjectTitle] = useState('未命名画布');
  const [projectRenameValue, setProjectRenameValue] = useState('未命名画布');
  const [isRenamingProject, setIsRenamingProject] = useState(false);
  const [isProjectRenameSaving, setIsProjectRenameSaving] = useState(false);
  const [projectRenameError, setProjectRenameError] = useState<string | null>(
    null,
  );
  const [isProjectReady, setIsProjectReady] = useState(false);
  const [assetExportStatus, setAssetExportStatus] = useState<{
    tone: 'info' | 'success' | 'error';
    message: string;
  } | null>(null);
  const [isExportingAssets, setIsExportingAssets] = 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[]>([]);
  const [layers, setLayers] = useState<CanvasLayer[]>([]);
  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 [assetPointerDrag, setAssetPointerDrag] =
    useState<AssetPointerDragState | null>(null);
  const [assetMoveDropFolderId, setAssetMoveDropFolderId] = useState<
    string | null
  >(null);
  const [pinnedAssetMoveFolderId, setPinnedAssetMoveFolderId] = useState<
    string | null
  >(null);
  const [canvasMarquee, setCanvasMarquee] = useState<CanvasMarqueeState | null>(
    null,
  );
  const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
  const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
  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 [isBackgroundSettingsOpen, setIsBackgroundSettingsOpen] =
    useState(false);
  const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
  const [isMinimapOpen, setIsMinimapOpen] = useState(true);
  const [canvasBackgroundColor, setCanvasBackgroundColor] = useState(
    DEFAULT_CANVAS_BACKGROUND_COLOR,
  );
  const [canvasBackgroundHexValue, setCanvasBackgroundHexValue] = useState(
    DEFAULT_CANVAS_BACKGROUND_COLOR,
  );
  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 [contextMenu, setContextMenu] =
    useState<CanvasContextMenuState | null>(null);
  const [canvasClipboard, setCanvasClipboard] =
    useState<CanvasClipboard | null>(null);
  const [historyVersion, setHistoryVersion] = useState(0);
  const [quickEditPanel, setQuickEditPanel] =
    useState<QuickEditPanelState | null>(null);
  const [characterAnimationPanel, setCharacterAnimationPanel] =
    useState<CharacterAnimationPanelState | null>(null);
  const [uploadDropTarget, setUploadDropTarget] = useState<
    'canvas' | 'assets' | null
  >(null);

  selectedLayerIdRef.current = selectedLayerId;
  selectedLayerIdsRef.current = selectedLayerIds;
  layersRef.current = layers;
  viewportRef.current = viewport;
  generateDialogRef.current = generateDialog;
  inactiveGenerateDialogsRef.current = inactiveGenerateDialogs;
  const assetsRef = useRef(assets);
  const addAssetLayerRef = useRef<
    (asset: EditorAsset, screenCenter?: { x: number; y: number }) => void
  >(() => {});
  const moveAssetToFolderRef = useRef<
    (assetId: string, folderId: string) => void
  >(() => {});
  authUiRef.current = authUi;
  const openEditorLoginModal = useCallback(
    (postLoginAction?: (() => void) | null) => {
      authUiRef.current?.openLoginModal(postLoginAction);
    },
    [],
  );
  const applyCanvasBackgroundColor = useCallback((color: string) => {
    const normalizedColor = normalizeCanvasBackgroundHex(color);
    if (!normalizedColor) {
      return false;
    }
    setCanvasBackgroundColor(normalizedColor);
    setCanvasBackgroundHexValue(normalizedColor);
    return true;
  }, []);

  useEffect(() => {
    assetsRef.current = assets;
  }, [assets]);

  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 selectedLayerCount = selectedLayerIds.length;
  const hasMultipleSelectedLayers = selectedLayerCount > 1;
  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 getContextTargetLayerIds = useCallback(
    (menu: CanvasContextMenuState | null = contextMenu) => {
      if (menu?.kind !== 'layer') {
        return [];
      }
      return selectedLayerIdsRef.current.includes(menu.layerId)
        ? selectedLayerIdsRef.current
        : [menu.layerId];
    },
    [contextMenu],
  );
  const contextTargetIds = getContextTargetLayerIds(contextMenu);
  const contextTargetLayers = layers.filter((layer) =>
    contextTargetIds.includes(layer.id),
  );
  const contextShouldShowLayer = contextTargetLayers.some(
    (layer) => layer.hidden,
  );
  const contextShouldUnlockLayer = contextTargetLayers.some(
    (layer) => layer.locked,
  );
  const canUndo = undoStackRef.current.length > 0;
  const canRedo = redoStackRef.current.length > 0;
  void historyVersion;
  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 getCanvasHistorySnapshot = useCallback(
    (): CanvasHistorySnapshot => ({
      layers: layersRef.current.map((layer) => ({ ...layer })),
      viewport: { ...viewportRef.current },
      generateDialog: generateDialogRef.current
        ? {
            ...generateDialogRef.current,
            placeholder: generateDialogRef.current.placeholder
              ? { ...generateDialogRef.current.placeholder }
              : undefined,
          }
        : null,
      inactiveGenerateDialogs: inactiveGenerateDialogsRef.current.map(
        (dialog) => ({
          ...dialog,
          placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
        }),
      ),
      selectedLayerId: selectedLayerIdRef.current,
      selectedLayerIds: [...selectedLayerIdsRef.current],
    }),
    [],
  );

  const restoreCanvasHistorySnapshot = useCallback(
    (snapshot: CanvasHistorySnapshot) => {
      setLayers(snapshot.layers.map((layer) => ({ ...layer })));
      setViewport({ ...snapshot.viewport });
      setGenerateDialog(
        snapshot.generateDialog
          ? {
              ...snapshot.generateDialog,
              placeholder: snapshot.generateDialog.placeholder
                ? { ...snapshot.generateDialog.placeholder }
                : undefined,
            }
          : null,
      );
      setInactiveGenerateDialogs(
        snapshot.inactiveGenerateDialogs.map((dialog) => ({
          ...dialog,
          placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
        })),
      );
      setSelectedLayerId(snapshot.selectedLayerId);
      setSelectedLayerIds([...snapshot.selectedLayerIds]);
      setHoveredLayerId(null);
      setMetadataLayer(null);
      setCanvasMarquee(null);
      setSnapGuide(null);
      setImageContextMenu(null);
      setContextMenu(null);
      setIsPanning(false);
      dragStateRef.current = null;
    },
    [],
  );

  const captureCanvasHistory = useCallback(
    (options: { clearRedo?: boolean } = {}) => {
      undoStackRef.current = [
        ...undoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
        getCanvasHistorySnapshot(),
      ];
      if (options.clearRedo !== false) {
        redoStackRef.current = [];
      }
      setHistoryVersion((version) => version + 1);
    },
    [getCanvasHistorySnapshot],
  );

  const undoCanvasChange = useCallback(() => {
    const previousSnapshot = undoStackRef.current.at(-1);
    if (!previousSnapshot) {
      return;
    }
    undoStackRef.current = undoStackRef.current.slice(0, -1);
    redoStackRef.current = [
      ...redoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
      getCanvasHistorySnapshot(),
    ];
    restoreCanvasHistorySnapshot(previousSnapshot);
    setHistoryVersion((version) => version + 1);
  }, [getCanvasHistorySnapshot, restoreCanvasHistorySnapshot]);

  const redoCanvasChange = useCallback(() => {
    const nextSnapshot = redoStackRef.current.at(-1);
    if (!nextSnapshot) {
      return;
    }
    redoStackRef.current = redoStackRef.current.slice(0, -1);
    undoStackRef.current = [
      ...undoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
      getCanvasHistorySnapshot(),
    ];
    restoreCanvasHistorySnapshot(nextSnapshot);
    setHistoryVersion((version) => version + 1);
  }, [getCanvasHistorySnapshot, restoreCanvasHistorySnapshot]);

  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);
    setContextMenu(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((error: unknown) => {
          if (isEditorAuthError(error)) {
            openEditorLoginModal();
          }
        });
    },
    [openEditorLoginModal],
  );

  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 nextProjectTitle = project.title?.trim() || '未命名画布';
        setProjectTitle(nextProjectTitle);
        setProjectRenameValue(nextProjectTitle);
        const pendingLayers = pendingProjectResourceLayersRef.current.splice(0);
        pendingLayers.forEach(({ layer, options }) => {
          createProjectResourceForLayer(layer, options);
        });
        setViewport(project.viewport);
        const resourcesById = new Map(
          project.resources.map((resource) => [
            resource.resourceId,
            { imageSrc: resource.imageSrc },
          ]),
        );
        const hydratedLayers = project.layers
          .map((layer) => hydrateLayer(layer, resourcesById))
          .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((error: unknown) => {
        if (cancelled) {
          return;
        }
        setIsProjectReady(false);
        if (isEditorAuthError(error)) {
          openEditorLoginModal(() => {
            window.location.reload();
          });
        }
      });

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

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

  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.ctrlKey || event.metaKey) &&
        event.code === 'KeyZ' &&
        !isEditableTarget(event)
      ) {
        event.preventDefault();
        if (event.shiftKey) {
          redoCanvasChange();
        } else {
          undoCanvasChange();
        }
        return;
      }
	      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);
	        setIsBackgroundSettingsOpen(false);
	        setIsSpecMenuOpen(false);
	        setImageContextMenu(null);
        setContextMenu(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);
    };
  }, [redoCanvasChange, undoCanvasChange]);

  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(() => {
    const updatePointerDrag = (event: PointerEvent) => {
      const currentDrag = assetPointerDragRef.current;
      if (!currentDrag || currentDrag.pointerId !== event.pointerId) {
        return;
      }
      const distance = Math.hypot(
        event.clientX - currentDrag.startClientX,
        event.clientY - currentDrag.startClientY,
      );
      const dropFolderId = resolveAssetFolderId(event.clientX, event.clientY);
      const nextDrag: AssetPointerDragState = {
        ...currentDrag,
        currentClientX: event.clientX,
        currentClientY: event.clientY,
        active: currentDrag.active || distance > 4,
        dropFolderId,
      };
      assetPointerDragRef.current = nextDrag;
      setAssetPointerDrag(nextDrag);
      setUploadDropTarget(
        resolveCanvasPoint(event.clientX, event.clientY) ? 'canvas' : null,
      );
      updateAssetMoveDropFolder(dropFolderId);
    };

    const finishPointerDrag = (event: PointerEvent) => {
      const currentDrag = assetPointerDragRef.current;
      if (!currentDrag || currentDrag.pointerId !== event.pointerId) {
        return;
      }
      const canvasPoint = resolveCanvasPoint(event.clientX, event.clientY);
      const dropFolderId =
        resolveAssetFolderId(event.clientX, event.clientY) ??
        currentDrag.dropFolderId;
      const draggedAsset = assetsRef.current.find(
        (asset) => asset.id === currentDrag.assetId,
      );
      assetPointerDragRef.current = null;
      setAssetPointerDrag(null);
      setUploadDropTarget(null);
      updateAssetMoveDropFolder(null);
      if (!currentDrag.active || !draggedAsset) {
        return;
      }
      suppressAssetClickRef.current = true;
      window.setTimeout(() => {
        suppressAssetClickRef.current = false;
      }, 0);
      if (dropFolderId && dropFolderId !== draggedAsset.folderId) {
        moveAssetToFolderRef.current(draggedAsset.id, dropFolderId);
        return;
      }
      if (canvasPoint) {
        addAssetLayerRef.current(draggedAsset, canvasPoint);
      }
    };

    window.addEventListener('pointermove', updatePointerDrag);
    window.addEventListener('pointerup', finishPointerDrag);
    window.addEventListener('pointercancel', finishPointerDrag);
    return () => {
      window.removeEventListener('pointermove', updatePointerDrag);
      window.removeEventListener('pointerup', finishPointerDrag);
      window.removeEventListener('pointercancel', finishPointerDrag);
    };
  }, []);

  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((error: unknown) => {
        if (isEditorAuthError(error)) {
          openEditorLoginModal();
        }
      });
    }, 450);

    return () => {
      if (saveTimerRef.current) {
        window.clearTimeout(saveTimerRef.current);
      }
    };
  }, [isProjectReady, layers, openEditorLoginModal, 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,
	      );

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

	  const updateScaleFromCenter = (nextScale: number) => {
	    const viewportElement = canvasViewportRef.current;
	    if (!viewportElement) {
      captureCanvasHistory();
	      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;
    captureCanvasHistory();
	    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 resolveCanvasPoint = (clientX: number, clientY: number) => {
    const rect = canvasViewportRef.current?.getBoundingClientRect();
    if (!rect) {
      return null;
    }
    if (
      clientX < rect.left ||
      clientX > rect.right ||
      clientY < rect.top ||
      clientY > rect.bottom
    ) {
      return null;
    }
    return {
      x: clientX - rect.left,
      y: clientY - rect.top,
    };
  };

  const getCanvasDropPoint = (event: ReactDragEvent<HTMLDivElement>) =>
    resolveCanvasPoint(event.clientX, event.clientY) ?? {
      x: Number.isFinite(canvasSize.width) ? canvasSize.width / 2 : 0,
      y: Number.isFinite(canvasSize.height) ? canvasSize.height / 2 : 0,
    };

  const getCanvasPointFromClient = (clientX: number, clientY: number) => {
    const rect = canvasViewportRef.current?.getBoundingClientRect();
    const screenX = clientX - (rect?.left ?? 0);
    const screenY = clientY - (rect?.top ?? 0);
    return {
      x: (screenX - viewport.x) / viewport.scale,
      y: (screenY - viewport.y) / viewport.scale,
    };
  };

  const duplicateLayersToPoint = (
    sourceLayers: CanvasLayer[],
    canvasPoint?: { x: number; y: number },
    options: { renameCopies?: boolean } = {},
  ) => {
    if (!sourceLayers.length) {
      return [];
    }
    const minX = Math.min(...sourceLayers.map((layer) => layer.x));
    const minY = Math.min(...sourceLayers.map((layer) => layer.y));
    const maxZIndex = layersRef.current.reduce(
      (maxZ, layer) => Math.max(maxZ, layer.zIndex),
      0,
    );
    const stamp = Date.now();
    return sourceLayers.map((layer, index) => ({
      ...layer,
      id: `layer-copy-${stamp}-${index}`,
      resourceId: `local-resource-copy-${stamp}-${index}`,
      title: options.renameCopies === false ? layer.title : `${layer.title} 副本`,
      x: canvasPoint ? canvasPoint.x + (layer.x - minX) : layer.x + 32,
      y: canvasPoint ? canvasPoint.y + (layer.y - minY) : layer.y + 32,
      zIndex: maxZIndex + index + 1,
      groupId: null,
    }));
  };

  const pasteCanvasClipboard = (canvasPoint?: { x: number; y: number }) => {
    if (!canvasClipboard?.layers.length) {
      return;
    }
    const nextLayers = duplicateLayersToPoint(canvasClipboard.layers, canvasPoint, {
      renameCopies: canvasClipboard.mode !== 'cut',
    });
    if (!nextLayers.length) {
      return;
    }
    captureCanvasHistory();
    setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
    setSelectedLayerIds(nextLayers.map((layer) => layer.id));
    setSelectedLayerId(nextLayers[0]?.id ?? null);
    setActiveTool('select');
    setContextMenu(null);
  };

  const copyContextLayers = (options: { cut?: boolean } = {}) => {
    const targetIds = getContextTargetLayerIds();
    const targetLayers = layers.filter((layer) => targetIds.includes(layer.id));
    if (!targetLayers.length) {
      return;
    }
    setCanvasClipboard({
      layers: targetLayers.map((layer) => ({ ...layer })),
      mode: options.cut ? 'cut' : 'copy',
    });
    if (options.cut) {
      captureCanvasHistory();
      setLayers((currentLayers) =>
        currentLayers.filter((layer) => !targetIds.includes(layer.id)),
      );
      selectSingleLayer(null);
      setMetadataLayer((currentLayer) =>
        currentLayer && targetIds.includes(currentLayer.id)
          ? null
          : currentLayer,
      );
    }
    setContextMenu(null);
  };

  const duplicateContextLayers = () => {
    const targetIds = getContextTargetLayerIds();
    const targetLayers = layers.filter((layer) => targetIds.includes(layer.id));
    const nextLayers = duplicateLayersToPoint(targetLayers);
    if (!nextLayers.length) {
      return;
    }
    captureCanvasHistory();
    setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
    setSelectedLayerIds(nextLayers.map((layer) => layer.id));
    setSelectedLayerId(nextLayers[0]?.id ?? null);
    setContextMenu(null);
  };

  const updateContextLayers = (
    updater: (layer: CanvasLayer, targetIds: string[]) => CanvasLayer,
  ) => {
    const targetIds = getContextTargetLayerIds();
    if (!targetIds.length) {
      return;
    }
    captureCanvasHistory();
    setLayers((currentLayers) =>
      currentLayers.map((layer) =>
        targetIds.includes(layer.id) ? updater(layer, targetIds) : layer,
      ),
    );
    setContextMenu(null);
  };

  const moveContextLayers = (mode: 'up' | 'down' | 'top' | 'bottom') => {
    const targetIds = getContextTargetLayerIds();
    if (!targetIds.length) {
      return;
    }
    const maxZIndex = layers.reduce(
      (maxZ, layer) => Math.max(maxZ, layer.zIndex),
      0,
    );
    const minZIndex = layers.reduce(
      (minZ, layer) => Math.min(minZ, layer.zIndex),
      0,
    );
    let offsetIndex = 0;
    updateContextLayers((layer) => {
      if (mode === 'up') {
        return { ...layer, zIndex: layer.zIndex + 1 };
      }
      if (mode === 'down') {
        return { ...layer, zIndex: layer.zIndex - 1 };
      }
      offsetIndex += 1;
      if (mode === 'top') {
        return { ...layer, zIndex: maxZIndex + offsetIndex };
      }
      return { ...layer, zIndex: minZIndex - (targetIds.length - offsetIndex + 1) };
    });
  };

  const groupContextLayers = () => {
    const targetIds = getContextTargetLayerIds();
    if (!targetIds.length) {
      return;
    }
    const groupId = `layer-group-${Date.now()}`;
    updateContextLayers((layer) => ({
      ...layer,
      groupId,
    }));
  };

  const ungroupContextLayers = () => {
    updateContextLayers((layer) => ({
      ...layer,
      groupId: null,
    }));
  };

  const toggleContextLayerVisibility = () => {
    const targetIds = getContextTargetLayerIds();
    const shouldHide = layers
      .filter((layer) => targetIds.includes(layer.id))
      .some((layer) => !layer.hidden);
    updateContextLayers((layer) => ({
      ...layer,
      hidden: shouldHide,
    }));
  };

  const toggleContextLayerLock = () => {
    const targetIds = getContextTargetLayerIds();
    const shouldLock = layers
      .filter((layer) => targetIds.includes(layer.id))
      .some((layer) => !layer.locked);
    updateContextLayers((layer) => ({
      ...layer,
      locked: shouldLock,
    }));
  };

  const flipContextLayers = (axis: 'x' | 'y') => {
    updateContextLayers((layer) =>
      axis === 'x'
        ? {
            ...layer,
            flipX: !layer.flipX,
          }
        : {
            ...layer,
            flipY: !layer.flipY,
          },
    );
  };

  const deleteContextLayers = () => {
    const targetIds = getContextTargetLayerIds();
    if (!targetIds.length) {
      return;
    }
    captureCanvasHistory();
    setLayers((currentLayers) =>
      currentLayers.filter((layer) => !targetIds.includes(layer.id)),
    );
    selectSingleLayer(null);
    setHoveredLayerId(null);
    setMetadataLayer((currentLayer) =>
      currentLayer && targetIds.includes(currentLayer.id) ? null : currentLayer,
    );
    setContextMenu(null);
  };

  const exportContextLayer = () => {
    const targetIds = getContextTargetLayerIds();
    const targetLayer = layers.find((layer) => targetIds.includes(layer.id));
    if (!targetLayer) {
      return;
    }
    const link = document.createElement('a');
    link.href = targetLayer.src;
    link.download = `${sanitizeExportFilePart(targetLayer.title, 'canvas-layer')}.png`;
    document.body.appendChild(link);
    link.click();
    link.remove();
    setContextMenu(null);
  };

  const resolveAssetFolderId = (clientX: number, clientY: number) => {
    const listElement = assetListRef.current;
    if (!listElement) {
      return null;
    }
    const listRect = listElement.getBoundingClientRect();
    if (
      clientX < listRect.left ||
      clientX > listRect.right ||
      clientY < listRect.top ||
      clientY > listRect.bottom
    ) {
      return null;
    }
    const folderElements = [
      ...listElement.querySelectorAll<HTMLElement>('[data-asset-folder-id]'),
    ];
    const matchedFolder = folderElements.find((element) => {
      const rect = element.getBoundingClientRect();
      return (
        clientX >= rect.left &&
        clientX <= rect.right &&
        clientY >= rect.top &&
        clientY <= rect.bottom
      );
    });
    return matchedFolder?.dataset.assetFolderId ?? null;
  };

  const updateAssetMoveDropFolder = (folderId: string | null) => {
    setAssetMoveDropFolderId(folderId);
    if (!folderId) {
      setPinnedAssetMoveFolderId(null);
      return;
    }
    const listElement = assetListRef.current;
    const header = listElement?.querySelector<HTMLElement>(
      `[data-asset-folder-header-id="${escapeCssIdentifier(folderId)}"]`,
    );
    const listRect = listElement?.getBoundingClientRect();
    const headerRect = header?.getBoundingClientRect();
    setPinnedAssetMoveFolderId(
      listRect && headerRect &&
        (headerRect.bottom < listRect.top || headerRect.top > listRect.bottom)
        ? folderId
        : null,
    );
  };

  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,
      },
    );
    captureCanvasHistory();
    setLayers((currentLayers) => [...currentLayers, nextLayer]);
    selectSingleLayer(nextLayer.id);
    setHoveredLayerId(null);
    createProjectResourceForLayer(nextLayer);
  };
  addAssetLayerRef.current = addAssetLayer;

  const exportCanvasAssets = async () => {
    if (isExportingAssets) {
      return;
    }
    const exportableLayers = layers
      .filter((layer) => layer.src.trim().length > 0)
      .sort((left, right) => left.zIndex - right.zIndex);
    if (!exportableLayers.length) {
      setAssetExportStatus({
        tone: 'info',
        message: '当前画布没有可导出的素材',
      });
      return;
    }

    setIsExportingAssets(true);
    setAssetExportStatus(null);

    try {
      const exportedAt = new Date();
      const projectName = sanitizeExportFilePart(projectTitle, '未命名画布');
      const rootFolderName = `${projectName}-画布素材`;
      const zip = new JSZip();
      const rootFolder = zip.folder(rootFolderName) ?? zip;
      const imagesFolder = rootFolder.folder('images') ?? rootFolder;
      const imageByKey = new Map<string, CanvasAssetExportImage>();
      const usedFileNames = new Map<string, number>();

      for (const layer of exportableLayers) {
        const key = getLayerExportKey(layer);
        if (imageByKey.has(key)) {
          continue;
        }
        const index = imageByKey.size + 1;
        const safeTitle = sanitizeExportFilePart(layer.title, '画布素材');
        const baseFileName = `${String(index).padStart(3, '0')}-${safeTitle}`;
        const duplicateCount = usedFileNames.get(baseFileName) ?? 0;
        usedFileNames.set(baseFileName, duplicateCount + 1);
        const indexedFileName =
          duplicateCount > 0
            ? `${baseFileName}-${duplicateCount + 1}`
            : baseFileName;

        try {
          const blob = await readLayerImageBlob(layer);
          const extension = getImageExtensionFromTypeOrSrc(blob.type, layer.src);
          const file = `images/${indexedFileName}.${extension}`;
          imageByKey.set(key, {
            key,
            file,
            layer,
            blob,
          });
          imagesFolder.file(
            `${indexedFileName}.${extension}`,
            await blobToUint8Array(blob),
          );
        } catch (error) {
          imageByKey.set(key, {
            key,
            file: `images/${indexedFileName}.png`,
            layer,
            error: error instanceof Error ? error.message : '图片读取失败',
          });
        }
      }

      const failedImages = [...imageByKey.values()].filter(
        (image) => image.error,
      );
      const successfulImages = [...imageByKey.values()].filter(
        (image) => image.blob,
      );
      if (!successfulImages.length) {
        setAssetExportStatus({
          tone: 'error',
          message: '素材导出失败',
        });
        return;
      }

      const metadata: CanvasAssetExportMetadata = {
        projectId,
        projectTitle,
        exportedAt: exportedAt.toISOString(),
        layers: exportableLayers.map((layer) => {
          const image = imageByKey.get(getLayerExportKey(layer));
          return buildLayerExportMetadata(
            layer,
            image?.blob ? image.file : null,
            image?.error,
          );
        }),
        failedImages: failedImages.map((image) => ({
          key: image.key,
          title: image.layer.title,
          src: image.layer.src,
          error: image.error ?? '图片读取失败',
        })),
      };
      const manifest = [
        `项目：${projectTitle}`,
        `导出时间：${metadata.exportedAt}`,
        `素材数量：${successfulImages.length}`,
        `图层数量：${exportableLayers.length}`,
        failedImages.length ? `失败素材数量：${failedImages.length}` : null,
      ]
        .filter(Boolean)
        .join('\n');

      rootFolder.file('metadata.json', JSON.stringify(metadata, null, 2));
      rootFolder.file('manifest.txt', manifest);

      const zipBlob = await zip.generateAsync({ type: 'blob' });
      if (
        typeof URL.createObjectURL !== 'function' ||
        typeof URL.revokeObjectURL !== 'function'
      ) {
        setAssetExportStatus({
          tone: 'error',
          message: '当前浏览器不支持素材下载',
        });
        return;
      }

      const downloadUrl = URL.createObjectURL(zipBlob);
      const link = document.createElement('a');
      link.href = downloadUrl;
      link.download = `${rootFolderName}-${formatExportDate(exportedAt)}.zip`;
      document.body.appendChild(link);
      link.click();
      link.remove();
      URL.revokeObjectURL(downloadUrl);
      setAssetExportStatus({
        tone: failedImages.length ? 'error' : 'success',
        message: failedImages.length ? '部分素材未能导出' : '画布素材已导出',
      });
    } catch {
      setAssetExportStatus({
        tone: 'error',
        message: '素材导出失败',
      });
    } finally {
      setIsExportingAssets(false);
    }
  };

  const startProjectRename = () => {
    setProjectRenameValue(projectTitle);
    setProjectRenameError(null);
    setIsRenamingProject(true);
  };

  const cancelProjectRename = () => {
    setProjectRenameValue(projectTitle);
    setProjectRenameError(null);
    setIsRenamingProject(false);
  };

  const submitProjectRename = () => {
    const nextTitle = projectRenameValue.trim();
    if (!nextTitle) {
      setProjectRenameError('项目名称不能为空');
      return;
    }
    if (!projectId || nextTitle === projectTitle) {
      setProjectRenameValue(projectTitle);
      setProjectRenameError(null);
      setIsRenamingProject(false);
      return;
    }
    setIsProjectRenameSaving(true);
    setProjectRenameError(null);
    renameEditorProject(projectId, nextTitle)
      .then((project) => {
        const savedTitle = project.title?.trim() || nextTitle;
        setProjectTitle(savedTitle);
        setProjectRenameValue(savedTitle);
        setIsRenamingProject(false);
      })
      .catch((error: unknown) => {
        if (isEditorAuthError(error)) {
          openEditorLoginModal();
        }
        setProjectRenameError(
          error instanceof Error ? error.message : '重命名项目失败',
        );
      })
      .finally(() => setIsProjectRenameSaving(false));
  };

  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),
    );
    setLayers((currentLayers) =>
      currentLayers.filter((layer) => !isLayerLinkedToAsset(layer, asset)),
    );
    setSelectedLayerIds((currentIds) =>
      currentIds.filter((layerId) =>
        layers.every(
          (layer) => layer.id !== layerId || !isLayerLinkedToAsset(layer, asset),
        ),
      ),
    );
    setSelectedLayerId((currentId) => {
      if (!currentId) {
        return currentId;
      }
      const currentLayer = layers.find((layer) => layer.id === currentId);
      return currentLayer && isLayerLinkedToAsset(currentLayer, asset)
        ? null
        : currentId;
    });
    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 = normalizeAssetLibrary(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];
    const deletedAssets = assets.filter((asset) => selectedAssetIds.has(asset.id));
    setAssets((currentAssets) =>
      currentAssets.filter((asset) => !selectedAssetIds.has(asset.id)),
    );
    setLayers((currentLayers) =>
      currentLayers.filter(
        (layer) =>
          !deletedAssets.some((asset) => isLayerLinkedToAsset(layer, asset)),
      ),
    );
    setSelectedAssetIds(new Set());
    ids.forEach((assetId) => {
      void deleteEditorAsset(assetId);
    });
  };

  const moveAssetToFolder = (assetId: string, folderId: string) => {
    const asset = assets.find((currentAsset) => currentAsset.id === assetId);
    if (!asset || asset.folderId === folderId) {
      return;
    }
    setAssets((currentAssets) =>
      currentAssets.map((currentAsset) =>
        currentAsset.id === assetId
          ? {
              ...currentAsset,
              folderId,
            }
          : currentAsset,
      ),
    );
    if (asset.persisted) {
      updateEditorAsset(asset.id, { folderId }).catch(() => {});
    }
  };

  moveAssetToFolderRef.current = moveAssetToFolder;

  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 fallbackWidth = 420;
    const fallbackHeight = 315;
    const uploadFolderId = assetFolders.some(
      (folder) => folder.id === (options.folderId ?? activeUploadFolderId),
    )
      ? (options.folderId ?? activeUploadFolderId)
      : 'project';
    const uploadIndex = options.uploadIndex ?? layerCounterRef.current + 1;
    layerCounterRef.current = Math.max(layerCounterRef.current, uploadIndex);
    const uploadedAsset: EditorAsset = {
      id: `upload-${uploadIndex}`,
      label: file.name || '上传图片',
      src: '',
      width: fallbackWidth,
      height: fallbackHeight,
      folderId: uploadFolderId,
      sourceKind: 'uploaded',
      sourceType: 'uploaded',
      persisted: false,
      uploadStatus: 'uploading',
      uploadProgress: 8,
      uploadMessage: '准备上传',
    };
    setAssets((currentAssets) => [...currentAssets, uploadedAsset]);
    setAssetFolders((currentFolders) =>
      currentFolders.map((folder) =>
        folder.id === uploadFolderId
          ? {
              ...folder,
              collapsed: false,
            }
          : folder,
      ),
    );

    let imageSrc = '';
    try {
      imageSrc = await readImageFileAsDataUrl(file);
      setAssets((currentAssets) =>
        currentAssets.map((asset) =>
          asset.id === uploadedAsset.id
            ? {
                ...asset,
                src: imageSrc,
                uploadProgress: 42,
                uploadMessage: '读取图片',
              }
            : asset,
        ),
      );
    } catch {
      setAssets((currentAssets) =>
        currentAssets.map((asset) =>
          asset.id === uploadedAsset.id
            ? {
                ...asset,
                uploadStatus: 'failed',
                uploadProgress: 100,
                uploadMessage: '读取失败',
              }
            : asset,
        ),
      );
      return;
    }
    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',
      sourceAssetId: `upload-${uploadIndex}`,
    };

    if (options.addToCanvas) {
      setLayers((currentLayers) => [...currentLayers, nextLayer]);
    }
    if (options.addToCanvas) {
      selectSingleLayer(nextLayer.id);
      setActiveSidebarPanel('layers');
    }

    setAssets((currentAssets) =>
      currentAssets.map((asset) =>
        asset.id === uploadedAsset.id
          ? {
              ...asset,
              uploadProgress: 68,
              uploadMessage: '上传中',
            }
          : asset,
      ),
    );
    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,
                  uploadStatus: undefined,
                  uploadProgress: undefined,
                  uploadMessage: undefined,
                }
              : currentAsset,
          ),
        );
        if (options.addToCanvas) {
          setLayers((currentLayers) =>
            currentLayers.map((currentLayer) =>
              currentLayer.id === nextLayer.id
                ? {
                    ...currentLayer,
                    sourceAssetId: asset.assetId,
                    objectKey: asset.objectKey ?? currentLayer.objectKey,
                    assetObjectId:
                      asset.assetObjectId ?? currentLayer.assetObjectId,
                  }
                : currentLayer,
            ),
          );
        }
      })
      .catch((error: unknown) => {
        const isAuthError = isEditorAuthError(error);
        if (isAuthError) {
          openEditorLoginModal();
        }
        setAssets((currentAssets) =>
          currentAssets.map((asset) =>
            asset.id === uploadedAsset.id
              ? {
                  ...asset,
                  uploadStatus: 'failed',
                  uploadProgress: 100,
                  uploadMessage: isAuthError ? '请先登录' : '上传失败',
                }
              : asset,
          ),
        );
      });

    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;
    } = {},
  ) => {
    const imageFiles = Array.from(files);
    const currentAuthUi = authUiRef.current;
    if (currentAuthUi && !currentAuthUi.canAccessProtectedData) {
      openEditorLoginModal(() => {
        addUploadedFiles(imageFiles, options);
      });
      return;
    }
    imageFiles.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);
    setContextMenu(null);
    captureCanvasHistory();
    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 = () => {
    const targetIds = selectedLayerIds.length
      ? selectedLayerIds
      : selectedLayerId
        ? [selectedLayerId]
        : [];
    if (targetIds.length <= 1) {
      deleteLayerById(targetIds[0] ?? null);
      return;
    }
    captureCanvasHistory();
    setImageContextMenu(null);
    setContextMenu(null);
    setLayers((currentLayers) => {
      const nextLayers = currentLayers.filter(
        (layer) => !targetIds.includes(layer.id),
      );
      const nextSelectedLayer = nextLayers
        .slice()
        .sort((left, right) => right.zIndex - left.zIndex)[0];
      selectSingleLayer(nextSelectedLayer?.id ?? null);
      return nextLayers;
    });
    setHoveredLayerId(null);
    setMetadataLayer((currentLayer) =>
      currentLayer && targetIds.includes(currentLayer.id) ? null : currentLayer,
    );
    setQuickEditPanel((currentPanel) =>
      currentPanel && targetIds.includes(currentPanel.sourceLayerId)
        ? null
        : currentPanel,
    );
    setCharacterAnimationPanel((currentPanel) =>
      currentPanel && targetIds.includes(currentPanel.sourceLayerId)
        ? null
        : currentPanel,
    );
    targetIds.forEach(removeCanvasGenerationDialogsByLayerId);
  };

  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 (hasDataTransferType(event.dataTransfer, ASSET_DRAG_MIME_TYPE)) {
      event.preventDefault();
      setUploadDropTarget('canvas');
      event.dataTransfer.dropEffect = 'copy';
      return;
    }
    if (hasDataTransferType(event.dataTransfer, 'Files')) {
      event.preventDefault();
      setUploadDropTarget('canvas');
      event.dataTransfer.dropEffect = 'copy';
    }
  };

  const handleCanvasDragLeave = (event: ReactDragEvent<HTMLDivElement>) => {
    if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
      setUploadDropTarget((currentTarget) =>
        currentTarget === 'canvas' ? null : currentTarget,
      );
    }
  };

  const handleCanvasDrop = (event: ReactDragEvent<HTMLDivElement>) => {
    const draggedAssetId = getDraggedAssetId(event.dataTransfer);
    if (draggedAssetId) {
      const draggedAsset = assets.find((asset) => asset.id === draggedAssetId);
      if (!draggedAsset) {
        return;
      }
      event.preventDefault();
      setUploadDropTarget(null);
      updateAssetMoveDropFolder(null);
      addAssetLayer(draggedAsset, getCanvasDropPoint(event));
      return;
    }
    const files = event.dataTransfer.files;
    if (!files.length) {
      return;
    }
    event.preventDefault();
    setUploadDropTarget(null);
    updateAssetMoveDropFolder(null);
    const canvasPoint = getCanvasDropPoint(event);
    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 moveViewportFromMinimapDrag = (
    dragState: Extract<DragState, { kind: 'minimap' }>,
    clientX: number,
    clientY: number,
  ) => {
    const deltaWorldX =
      ((clientX - dragState.startClientX) / dragState.minimapScale) *
      MINIMAP_DRAG_SENSITIVITY;
    const deltaWorldY =
      ((clientY - dragState.startClientY) / dragState.minimapScale) *
      MINIMAP_DRAG_SENSITIVITY;
    setViewport({
      ...dragState.startViewport,
      x:
        dragState.startViewport.x -
        deltaWorldX * dragState.startViewport.scale,
      y:
        dragState.startViewport.y -
        deltaWorldY * dragState.startViewport.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),
      startClientX: pointer.x,
      startClientY: pointer.y,
      startViewport: { ...viewport },
      minimapScale: minimapModel?.scale ?? 1,
      moved: false,
    };
  };

  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);
      const deltaX = pointer.x - dragState.startClientX;
      const deltaY = pointer.y - dragState.startClientY;
      if (!dragState.moved && Math.hypot(deltaX, deltaY) >= 2) {
        dragState.moved = true;
      }
      if (dragState.moved) {
        moveViewportFromMinimapDrag(dragState, 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)
    ) {
      if (dragState.kind === 'minimap' && !dragState.moved) {
        const pointer = getPointerClient(event);
        moveViewportFromMinimapPointer(pointer.x, pointer.y);
      }
      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()}`;
    captureCanvasHistory();
    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 = '';
        }}
      />
      {assetPointerDrag?.active ? (
        <div
          className="image-canvas-editor__asset-drag-preview"
          style={{
            left: Number.isFinite(assetPointerDrag.currentClientX)
              ? assetPointerDrag.currentClientX
              : 0,
            top: Number.isFinite(assetPointerDrag.currentClientY)
              ? assetPointerDrag.currentClientY
              : 0,
          }}
          aria-hidden="true"
        >
          {assets.find((asset) => asset.id === assetPointerDrag.assetId)
            ?.label ?? '素材'}
        </div>
      ) : null}
      <ImageCanvasSidebarView
        activeSidebarPanel={activeSidebarPanel}
        assetListRef={assetListRef}
        uploadInputRef={uploadInputRef}
        assetPointerDragRef={assetPointerDragRef}
        suppressAssetClickRef={suppressAssetClickRef}
        assets={assets}
        groupedAssets={groupedAssets}
        assetFolders={assetFolders}
        layers={layers}
        selectedLayerId={selectedLayerId}
        selectedLayerIds={selectedLayerIds}
        isAssetSelectionMode={isAssetSelectionMode}
        selectedAssetIds={selectedAssetIds}
        assetMoveDropFolderId={assetMoveDropFolderId}
        pinnedAssetMoveFolderId={pinnedAssetMoveFolderId}
        creatingFolder={creatingFolder}
        newFolderName={newFolderName}
        renamingFolder={renamingFolder}
        renamingAsset={renamingAsset}
        allSelectableAssetsSelected={allSelectableAssetsSelected}
        assetMarquee={assetMarquee}
        setIsAssetSelectionMode={setIsAssetSelectionMode}
        setCreatingFolder={setCreatingFolder}
        setNewFolderName={setNewFolderName}
        setRenamingFolder={setRenamingFolder}
        setRenamingAsset={setRenamingAsset}
        setActiveUploadFolderId={setActiveUploadFolderId}
        setUploadTarget={setUploadTarget}
        setUploadDropTarget={setUploadDropTarget}
        setAssetPointerDrag={setAssetPointerDrag}
        setSelectedAssetIds={setSelectedAssetIds}
        setImageContextMenu={setImageContextMenu}
        setContextMenu={setContextMenu}
        onAssetMarqueePointerDown={handleAssetMarqueePointerDown}
        onAssetMarqueePointerMove={handleAssetMarqueePointerMove}
        onAssetMarqueePointerUp={handleAssetMarqueePointerUp}
        updateAssetMoveDropFolder={updateAssetMoveDropFolder}
        addUploadedFiles={addUploadedFiles}
        moveAssetToFolder={moveAssetToFolder}
        commitNewAssetFolder={commitNewAssetFolder}
        toggleAssetFolder={toggleAssetFolder}
        startRenamingFolder={startRenamingFolder}
        commitFolderRename={commitFolderRename}
        deleteAssetFolder={deleteAssetFolder}
        startRenamingAsset={startRenamingAsset}
        commitAssetRename={commitAssetRename}
        deleteUploadedAsset={deleteUploadedAsset}
        toggleAssetSelected={toggleAssetSelected}
        addAssetLayer={addAssetLayer}
        toggleAllAssetsSelected={toggleAllAssetsSelected}
        deleteSelectedAssets={deleteSelectedAssets}
        closeAssetSelectionMode={closeAssetSelectionMode}
        groupSelectedLayers={groupSelectedLayers}
        selectSingleLayer={selectSingleLayer}
        resolveContextMenuPosition={resolveContextMenuPosition}
        getCanvasPointFromClient={getCanvasPointFromClient}
      />

      <div className="image-canvas-editor__main">
        <div className="image-canvas-editor__topbar">
          <a
            className="image-canvas-editor__project-back-button"
            href="/project"
            aria-label="返回项目页面"
            title="返回项目"
          >
            <ChevronLeft className="h-4 w-4" aria-hidden="true" />
          </a>
          <div className="image-canvas-editor__title-block">
            {isRenamingProject ? (
              <form
                className="image-canvas-editor__project-title-form"
                onSubmit={(event) => {
                  event.preventDefault();
                  submitProjectRename();
                }}
              >
                <PlatformTextField
                  aria-label="项目名称"
                  value={projectRenameValue}
                  autoFocus
                  disabled={isProjectRenameSaving}
                  className="image-canvas-editor__project-title-input"
                  onChange={(event) => {
                    setProjectRenameValue(event.target.value);
                    setProjectRenameError(null);
                  }}
                  onKeyDown={(event) => {
                    if (event.key === 'Escape') {
                      event.preventDefault();
                      cancelProjectRename();
                    }
                  }}
                />
                <EditorIconButton
                  type="submit"
                  label="保存项目名称"
                  title="保存"
                  icon={Check}
                  disabled={isProjectRenameSaving}
                />
                <EditorIconButton
                  label="取消修改项目名称"
                  title="取消"
                  icon={X}
                  disabled={isProjectRenameSaving}
                  onClick={cancelProjectRename}
                />
                {projectRenameError ? (
                  <span
                    className="image-canvas-editor__project-title-error"
                    role="alert"
                  >
                    {projectRenameError}
                  </span>
                ) : null}
              </form>
            ) : (
              <div className="image-canvas-editor__project-title-row">
                <button
                  type="button"
                  className="image-canvas-editor__project-title-button"
                  onDoubleClick={startProjectRename}
                  aria-label={`编辑项目名称${projectTitle}`}
                >
                  <h1>{projectTitle}</h1>
                </button>
                <EditorIconButton
                  className="image-canvas-editor__project-rename-button"
                  label="编辑项目名称"
                  title="编辑项目名称"
                  icon={Pencil}
                  onClick={startProjectRename}
                />
              </div>
	            )}
	            <span>画布</span>
	          </div>
          <div className="image-canvas-editor__topbar-actions">
            <EditorIconButton
              label="下载画布素材"
              title="下载画布素材"
              icon={Download}
              disabled={
                isExportingAssets ||
                !layers.some((layer) => layer.src.trim().length > 0)
              }
              onClick={() => void exportCanvasAssets()}
            />
            {assetExportStatus ? (
              <PlatformStatusMessage
                tone={assetExportStatus.tone}
                surface="platform"
                size="xs"
                role={assetExportStatus.tone === 'error' ? 'alert' : 'status'}
              >
                {assetExportStatus.message}
              </PlatformStatusMessage>
            ) : null}
          </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}
          onDragLeave={handleCanvasDragLeave}
          onDrop={handleCanvasDrop}
          onContextMenu={(event) => {
            event.preventDefault();
            event.stopPropagation();
            const position = resolveContextMenuPosition(
              event.clientX,
              event.clientY,
              'blank',
            );
            setImageContextMenu(null);
            setContextMenu({
              kind: 'blank',
              ...position,
              canvasPoint: getCanvasPointFromClient(event.clientX, event.clientY),
            });
          }}
        >
          {uploadDropTarget === 'canvas' ? (
            <div
              className="image-canvas-editor__upload-drop-overlay image-canvas-editor__upload-drop-overlay--canvas"
              role="status"
            >
              <span>添加到画布</span>
              <strong>松开即可添加</strong>
            </div>
          ) : null}
          <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()
              .filter((layer) => !layer.hidden)
              .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' : ''} ${layer.locked ? 'image-canvas-editor__layer--locked' : ''}`}
                    style={{
                      left: layer.x,
                      top: layer.y,
                      width: layer.width,
                      height: layer.height,
                      zIndex: layer.zIndex,
                      display: layer.hidden ? 'none' : undefined,
                    }}
                    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();
                      if (!selectedLayerIds.includes(layer.id)) {
                        selectSingleLayer(layer.id);
                      }
                      const position = resolveContextMenuPosition(
                        event.clientX,
                        event.clientY,
                        'layer',
                      );
                      setContextMenu({
                        kind: 'layer',
                        layerId: layer.id,
                        ...position,
                        canvasPoint: getCanvasPointFromClient(
                          event.clientX,
                          event.clientY,
                        ),
                      });
                      setImageContextMenu({
                        layerId: layer.id,
                        ...position,
                      });
                    }}
                    onMouseEnter={() => setHoveredLayerId(layer.id)}
                    onMouseLeave={() =>
                      setHoveredLayerId((currentId) =>
                        currentId === layer.id ? null : currentId,
                      )
                    }
                    aria-label={`选择${layer.title}`}
                  >
                    <img
                      src={layer.src}
                      alt={`画布图片：${layer.title}`}
                      style={{
                        transform:
                          layer.flipX || layer.flipY
                            ? `scale(${layer.flipX ? -1 : 1}, ${
                                layer.flipY ? -1 : 1
                              })`
                            : undefined,
                      }}
                    />
                    {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}

          {contextMenu ? (
            <div
              className="image-canvas-editor__context-menu"
              role="menu"
              aria-label={
                contextMenu.kind === 'blank' ? '画布右键菜单' : '图片功能面板'
              }
              style={{
                left: contextMenu.x,
                top: contextMenu.y,
              }}
              onContextMenu={(event) => event.preventDefault()}
              onPointerDown={(event) => event.stopPropagation()}
            >
              {contextMenu.kind === 'blank' ? (
                <>
                  <button
                    type="button"
                    role="menuitem"
                    disabled={!canvasClipboard?.layers.length}
                    onClick={() => pasteCanvasClipboard(contextMenu.canvasPoint)}
                  >
                    粘贴
                  </button>
                  <button
                    type="button"
                    role="menuitem"
                    onClick={() => {
                      updateScaleFromCenter(viewport.scale * 1.16);
                      setContextMenu(null);
                    }}
                  >
                    放大
                  </button>
                  <button
                    type="button"
                    role="menuitem"
                    onClick={() => {
                      updateScaleFromCenter(viewport.scale * 0.86);
                      setContextMenu(null);
                    }}
                  >
                    缩小
                  </button>
                  <button
                    type="button"
                    role="menuitem"
                    onClick={() => {
                      fitLayers();
                      setContextMenu(null);
                    }}
                  >
                    显示画布所有元素
                  </button>
                </>
              ) : (
                <>
                  <button type="button" role="menuitem" onClick={() => copyContextLayers()}>
                    复制
                  </button>
                  <button
                    type="button"
                    role="menuitem"
                    onClick={() => copyContextLayers({ cut: true })}
                  >
                    剪切
                  </button>
                  <button
                    type="button"
                    role="menuitem"
                    disabled={!canvasClipboard?.layers.length}
                    onClick={() => pasteCanvasClipboard(contextMenu.canvasPoint)}
                  >
                    粘贴
                  </button>
                  <button type="button" role="menuitem" onClick={duplicateContextLayers}>
                    创建副本
                  </button>
                  <hr />
                  <button type="button" role="menuitem" onClick={() => moveContextLayers('up')}>
                    上移一层
                  </button>
                  <button type="button" role="menuitem" onClick={() => moveContextLayers('down')}>
                    下移一层
                  </button>
                  <button type="button" role="menuitem" onClick={() => moveContextLayers('top')}>
                    置于顶层
                  </button>
                  <button type="button" role="menuitem" onClick={() => moveContextLayers('bottom')}>
                    移动至底层
                  </button>
                  <hr />
                  <button type="button" role="menuitem" onClick={groupContextLayers}>
                    创建组
                  </button>
                  <button type="button" role="menuitem" onClick={ungroupContextLayers}>
                    解除组
                  </button>
                  <button type="button" role="menuitem" onClick={toggleContextLayerVisibility}>
                    {contextShouldShowLayer ? '显示' : '隐藏'}
                  </button>
                  <button type="button" role="menuitem" onClick={toggleContextLayerLock}>
                    {contextShouldUnlockLayer ? '解锁' : '锁定'}
                  </button>
                  <hr />
                  <button type="button" role="menuitem" onClick={() => flipContextLayers('x')}>
                    水平翻转
                  </button>
                  <button type="button" role="menuitem" onClick={() => flipContextLayers('y')}>
                    垂直翻转
                  </button>
                  <button type="button" role="menuitem" onClick={exportContextLayer}>
                    导出为
                  </button>
                  <hr />
                  {imageContextMenuLayer ? (
                    <>
                      <button
                        type="button"
                        role="menuitem"
                        onClick={() => openQuickEditPanel(imageContextMenuLayer)}
                      >
                        快速编辑
                      </button>
                      <button
                        type="button"
                        role="menuitem"
                        onClick={() => {
                          setMetadataLayer(imageContextMenuLayer);
                          setContextMenu(null);
                          setImageContextMenu(null);
                        }}
                      >
                        查看图片信息
                      </button>
                      {imageContextMenuLayer.assetKind === 'character' ? (
                        <button
                          type="button"
                          role="menuitem"
                          onClick={() =>
                            openCharacterAnimationPanel(imageContextMenuLayer)
                          }
                        >
                          生成动画
                        </button>
                      ) : null}
                      <hr />
                    </>
                  ) : null}
                  <button
                    type="button"
                    role="menuitem"
                    className="image-canvas-editor__context-menu-danger"
                    onClick={deleteContextLayers}
                  >
                    删除
                  </button>
                </>
              )}
            </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()}
          >
            <EditorIconButton
              label="撤销"
              title="撤销"
              icon={Undo2}
              disabled={!canUndo}
              onClick={undoCanvasChange}
            />
            <EditorIconButton
              label="重做"
              title="重做"
              icon={Redo2}
              disabled={!canRedo}
              onClick={redoCanvasChange}
            />
            <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={isBackgroundSettingsOpen}
                onClick={() =>
                  setIsBackgroundSettingsOpen((isOpen) => !isOpen)
                }
                icon={
                  <span
                    className="image-canvas-editor__background-swatch-current"
                    style={{ backgroundColor: canvasBackgroundColor }}
                  />
                }
              />
              {isBackgroundSettingsOpen ? (
                <div
                  className="image-canvas-editor__background-panel"
                  role="dialog"
                  aria-label="画布背景设置"
                >
                  <div className="image-canvas-editor__background-panel-head">
                    <span>画布背景</span>
                    <span
                      className="image-canvas-editor__background-preview"
                      aria-label={`当前画布背景色 ${canvasBackgroundColor}`}
                      style={{ backgroundColor: canvasBackgroundColor }}
                    />
                  </div>
                  <div
                    className="image-canvas-editor__background-presets"
                    aria-label="画布背景预设色"
                  >
                    {CANVAS_BACKGROUND_OPTIONS.map((option) => (
                      <button
                        key={option.value}
                        type="button"
                        className="image-canvas-editor__background-preset"
                        aria-label={option.label}
                        aria-pressed={canvasBackgroundColor === option.value}
                        onClick={() => applyCanvasBackgroundColor(option.value)}
                      >
                        <span
                          className="image-canvas-editor__background-swatch"
                          style={{ backgroundColor: option.value }}
                        />
                        <span>{option.label}</span>
                      </button>
                    ))}
                  </div>
                  <label className="image-canvas-editor__background-field">
                    <span>自定义</span>
                    <input
                      type="color"
                      aria-label="自定义画布背景色"
                      value={canvasBackgroundColor}
                      onChange={(event) =>
                        applyCanvasBackgroundColor(event.currentTarget.value)
                      }
                    />
                  </label>
                  <label className="image-canvas-editor__background-field image-canvas-editor__background-field--hex">
                    <span>HEX</span>
                    <PlatformTextField
                      aria-label="画布背景十六进制颜色"
                      value={canvasBackgroundHexValue}
                      density="compact"
                      size="xs"
                      spellCheck={false}
                      onChange={(event) => {
                        const nextValue = event.currentTarget.value;
                        setCanvasBackgroundHexValue(nextValue);
                        const normalizedColor =
                          normalizeCanvasBackgroundHex(nextValue);
                        if (normalizedColor) {
                          setCanvasBackgroundColor(normalizedColor);
                          setCanvasBackgroundHexValue(normalizedColor);
                        }
                      }}
                    />
                  </label>
                  <PlatformActionButton
                    tone="secondary"
                    size="xs"
                    className="image-canvas-editor__background-reset"
                    onClick={() =>
                      applyCanvasBackgroundColor(DEFAULT_CANVAS_BACKGROUND_COLOR)
                    }
                  >
                    <RotateCcw className="h-3.5 w-3.5" aria-hidden="true" />
                    恢复默认
                  </PlatformActionButton>
                </div>
              ) : 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 && !contextMenu ? (
            <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>
  );
}

export default ImageCanvasEditorView;
