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

import {
  createEditorProjectResource,
  createEditorAsset,
  createEditorAssetFolder,
  deleteEditorAsset,
  deleteEditorAssetFolder,
  editEditorImage,
  type EditorAssetLibrarySnapshot,
  type EditorImageGenerationResult,
  type EditorProjectLayerSnapshot,
  generateEditorImage,
  loadEditorAssetLibrary,
  loadEditorProject,
  loadOrCreateRecentEditorProject,
  saveEditorProjectLayout,
  updateEditorAsset,
  updateEditorAssetFolder,
} from '../../services/image-editor/editorProjectClient';
import { ApiClientError } from '../../services/apiClient';
import {
  EditorIconButton,
  SidebarMediaItem,
} from './ImageCanvasEditorPrimitives';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformBatchActionToolbar } from '../common/PlatformBatchActionToolbar';
import {
  PlatformFloatingMenu,
  PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { UnifiedModal } from '../common/UnifiedModal';

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

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

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

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

type SidebarPanel = 'assets' | 'layers';

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

type GenerateDialogState = {
  mode: 'generate' | 'edit';
  prompt: string;
  status: 'idle' | 'generating' | 'failed';
  sourceLayerId?: string;
  generatedLayerId?: string;
  errorMessage?: string;
  placeholder?: {
    x: number;
    y: number;
    width: number;
    height: number;
    originalWidth: number;
    originalHeight: number;
  };
};

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

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

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

type DragState =
  | {
      kind: 'pan';
      pointerId: number;
      startClientX: number;
      startClientY: number;
      startViewport: CanvasViewport;
    }
  | {
      kind: 'layer';
      pointerId: number;
      layerId: string;
      startClientX: number;
      startClientY: number;
      startLayerX: number;
      startLayerY: number;
      startScale: number;
    }
  | {
      kind: 'generation-frame';
      pointerId: number;
      startClientX: number;
      startClientY: number;
      startFrameX: number;
      startFrameY: number;
      startScale: number;
    }
  | {
      kind: 'minimap';
      pointerId: number;
    };

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

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

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

const CANVAS_WORLD_SIZE = 12000;
const CANVAS_WORLD_ORIGIN = CANVAS_WORLD_SIZE / 2;
const MIN_SCALE = 0.24;
const MAX_SCALE = 3.2;
const TOOLBAR_HALF_WIDTH = 132;
const DEFAULT_CANVAS_SIZE = { width: 900, height: 640 };
const SNAP_THRESHOLD_SCREEN_PX = 18;
const FIT_VIEW_PADDING = 10;
const MINIMAP_SIZE = { width: 132, height: 84 };
const MINIMAP_PADDING = 8;
const CANVAS_BACKGROUND_OPTIONS = [
  { label: '白色', value: '#ffffff' },
  { label: '浅灰', value: '#f8fafc' },
  { label: '暖灰', value: '#f3f0ea' },
  { label: '冷蓝', value: '#eef6ff' },
];

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

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

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

function createLayerFromAsset(
  asset: EditorAsset,
  index: number,
  viewport: CanvasViewport,
  screenCenter: { x: number; y: number },
): CanvasLayer {
  const longestSide = Math.max(asset.width, asset.height);
  const sizeRatio = longestSide > 0 ? 360 / longestSide : 1;
  const width = Math.round(asset.width * sizeRatio);
  const height = Math.round(asset.height * sizeRatio);
  const worldCenterX = (screenCenter.x - viewport.x) / viewport.scale;
  const worldCenterY = (screenCenter.y - viewport.y) / viewport.scale;
  const offset = index * 34;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

export function ImageCanvasEditorView() {
  const canvasViewportRef = useRef<HTMLDivElement | null>(null);
  const uploadInputRef = useRef<HTMLInputElement | null>(null);
  const assetListRef = useRef<HTMLDivElement | null>(null);
  const dragStateRef = useRef<DragState | null>(null);
  const layerCounterRef = useRef(INITIAL_LAYERS.length);
  const saveTimerRef = useRef<number | null>(null);
  const [projectId, setProjectId] = useState<string | null>(null);
  const [isProjectReady, setIsProjectReady] = useState(false);
  const [activeSidebarPanel, setActiveSidebarPanel] =
    useState<SidebarPanel | null>('assets');
  const [viewport, setViewport] = useState<CanvasViewport>({
    x: -260,
    y: 70,
    scale: 0.82,
  });
  const [canvasSize, setCanvasSize] = useState(DEFAULT_CANVAS_SIZE);
  const [assetFolders, setAssetFolders] =
    useState<EditorAssetFolder[]>(EDITOR_ASSET_FOLDERS);
  const [assets, setAssets] = useState<EditorAsset[]>(EDITOR_ASSETS);
  const [layers, setLayers] = useState<CanvasLayer[]>(INITIAL_LAYERS);
  const [renamingAsset, setRenamingAsset] = useState<{
    assetId: string;
    value: string;
  } | null>(null);
  const [renamingFolder, setRenamingFolder] = useState<{
    folderId: string;
    value: string;
  } | null>(null);
  const [creatingFolder, setCreatingFolder] = useState(false);
  const [newFolderName, setNewFolderName] = useState('');
  const [activeUploadFolderId, setActiveUploadFolderId] = useState('uploads');
  const [isAssetSelectionMode, setIsAssetSelectionMode] = useState(false);
  const [selectedAssetIds, setSelectedAssetIds] = useState<Set<string>>(
    () => new Set(),
  );
  const [assetMarquee, setAssetMarquee] = useState<AssetMarqueeState | null>(
    null,
  );
  const [selectedLayerId, setSelectedLayerId] = useState<string | null>(
    INITIAL_LAYERS[0]?.id ?? null,
  );
  const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>(
    INITIAL_LAYERS[0]?.id ? [INITIAL_LAYERS[0].id] : [],
  );
  const [hoveredLayerId, setHoveredLayerId] = useState<string | null>(null);
  const [activeTool, setActiveTool] = useState<CanvasTool>('select');
  const [isSpacePanning, setIsSpacePanning] = useState(false);
  const [isPanning, setIsPanning] = useState(false);
  const [snapGuide, setSnapGuide] = useState<SnapGuide | null>(null);
  const [isZoomMenuOpen, setIsZoomMenuOpen] = useState(false);
  const [isBackgroundMenuOpen, setIsBackgroundMenuOpen] = useState(false);
  const [isMinimapOpen, setIsMinimapOpen] = useState(true);
  const [canvasBackgroundColor, setCanvasBackgroundColor] = useState(
    CANVAS_BACKGROUND_OPTIONS[1]?.value ?? '#f8fafc',
  );
  const [metadataLayer, setMetadataLayer] = useState<CanvasLayer | null>(null);
  const [generateDialog, setGenerateDialog] =
    useState<GenerateDialogState | null>(null);

  const effectiveTool: CanvasTool = isSpacePanning ? 'hand' : activeTool;
  const selectedLayer = useMemo(
    () => layers.find((layer) => layer.id === selectedLayerId) ?? null,
    [layers, selectedLayerId],
  );
  const activeGenerationLayer = useMemo(
    () =>
      generateDialog?.mode === 'generate' && generateDialog.generatedLayerId
        ? layers.find((layer) => layer.id === generateDialog.generatedLayerId) ?? null
        : null,
    [generateDialog, layers],
  );
  const generationAnchor =
    generateDialog?.mode === 'generate'
      ? (activeGenerationLayer ?? generateDialog.placeholder ?? null)
      : null;
  const generationComposerStyle = generationAnchor
    ? {
        left:
          viewport.x +
          (generationAnchor.x + generationAnchor.width / 2) * viewport.scale,
        top:
          viewport.y +
          (generationAnchor.y + generationAnchor.height) * viewport.scale +
          10,
      }
    : 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 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 selectSingleLayer = (layerId: string | null) => {
    setSelectedLayerId(layerId);
    setSelectedLayerIds(layerId ? [layerId] : []);
  };
  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;
        }
        setProjectId(project.projectId);
        setViewport(project.viewport);
        const hydratedLayers = project.layers
          .map(hydrateLayer)
          .filter((layer): layer is CanvasLayer => Boolean(layer));
        if (hydratedLayers.length > 0) {
          layerCounterRef.current = hydratedLayers.length;
          setLayers(hydratedLayers);
          selectSingleLayer(hydratedLayers[0]?.id ?? null);
        }
        setIsProjectReady(true);
      })
      .catch(() => {
        if (!cancelled) {
          setIsProjectReady(false);
        }
      });

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

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

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

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

    updateCanvasSize();

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

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

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        setActiveSidebarPanel(null);
        setIsZoomMenuOpen(false);
        setIsBackgroundMenuOpen(false);
        setGenerateDialog((currentDialog) =>
          currentDialog?.status === 'generating' ? currentDialog : null,
        );
        return;
      }
      if (event.code !== 'Space' || event.repeat || isEditableTarget(event)) {
        return;
      }
      event.preventDefault();
      setIsSpacePanning(true);
    };
    const handleKeyUp = (event: KeyboardEvent) => {
      if (event.code !== 'Space') {
        return;
      }
      event.preventDefault();
      setIsSpacePanning(false);
    };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    createProjectResourceForLayer(nextLayer);

    if (imageSrc) {
      const uploadedImage = new Image();
      uploadedImage.onload = () => {
        const originalWidth = uploadedImage.naturalWidth || fallbackWidth;
        const originalHeight = uploadedImage.naturalHeight || fallbackHeight;
        const longestSide = Math.max(originalWidth, originalHeight);
        const sizeRatio = longestSide > 0 ? Math.min(1, 420 / longestSide) : 1;
        const width = Math.round(originalWidth * sizeRatio);
        const height = Math.round(originalHeight * sizeRatio);
        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 } } = {},
  ) => {
    Array.from(files).forEach((file, index) => {
      layerCounterRef.current += 1;
      const uploadIndex = layerCounterRef.current;
      void addUploadedLayer(file, {
        ...options,
        uploadIndex,
        canvasPoint: options.canvasPoint
          ? {
              x: options.canvasPoint.x + index * 28,
              y: options.canvasPoint.y + index * 28,
            }
          : undefined,
      });
    });
  };

  const deleteSelectedLayer = () => {
    if (!selectedLayerId) {
      return;
    }
    setLayers((currentLayers) => {
      const nextLayers = currentLayers.filter((layer) => layer.id !== selectedLayerId);
      const nextSelectedLayer = nextLayers
        .slice()
        .sort((left, right) => right.zIndex - left.zIndex)[0];
      selectSingleLayer(nextSelectedLayer?.id ?? null);
      return nextLayers;
    });
    setHoveredLayerId(null);
    setMetadataLayer((currentLayer) =>
      currentLayer?.id === selectedLayerId ? null : currentLayer,
    );
  };

  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;
    setGenerateDialog({
      mode: 'generate',
      prompt: '',
      status: 'idle',
      placeholder: {
        x: worldCenterX - placeholderWidth / 2,
        y: worldCenterY - placeholderHeight / 2,
        width: placeholderWidth,
        height: placeholderHeight,
        originalWidth: 2048,
        originalHeight: 2048,
      },
    });
    setActiveTool('generate');
    selectSingleLayer(null);
  };

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

  const addGeneratedResultLayer = (
    generated: EditorImageGenerationResult,
    options: { sourceLayer?: CanvasLayer; frame?: GenerateDialogState['placeholder'] } = {},
  ) => {
    layerCounterRef.current += 1;
    const generatedIndex = layerCounterRef.current;
    const originalWidth = generated.width || 1024;
    const originalHeight = generated.height || 1024;
    const longestSide = Math.max(originalWidth, originalHeight);
    const sizeRatio = longestSide > 0 ? Math.min(1, 420 / longestSide) : 1;
    const width = options.frame?.width ?? Math.round(originalWidth * sizeRatio);
    const height = options.frame?.height ?? Math.round(originalHeight * sizeRatio);
    const worldCenterX = (canvasSize.width / 2 - viewport.x) / viewport.scale;
    const worldCenterY = (canvasSize.height / 2 - viewport.y) / viewport.scale;
    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} 修改结果`
        : `生成图片 ${generatedIndex}`,
      src: generated.imageSrc,
      x: options.sourceLayer
        ? options.sourceLayer.x + options.sourceLayer.width + 32
        : options.frame?.x ?? worldCenterX - width / 2,
      y: options.sourceLayer ? options.sourceLayer.y : options.frame?.y ?? worldCenterY - height / 2,
      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,
      sourceResourceId: options.sourceLayer?.resourceId,
    };

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

  const submitImageGeneration = async (dialog: GenerateDialogState) => {
    const normalizedPrompt =
      dialog.prompt.trim() ||
      (dialog.mode === 'edit' ? '修改当前图片' : 'AI 生成图片');
    setGenerateDialog({
      ...dialog,
      prompt: normalizedPrompt,
      status: 'generating',
    });

    try {
      if (dialog.mode === 'edit') {
        const sourceLayer = layers.find((layer) => layer.id === dialog.sourceLayerId);
        if (!sourceLayer) {
          throw new Error('未找到要修改的图片');
        }
        if (!sourceLayer.src.startsWith('data:image/')) {
          throw new Error('当前图片缺少可提交的原图数据，请先使用生成图片结果进行修改');
        }
        const generated = await editEditorImage({
          prompt: normalizedPrompt,
          sourceImageSrc: sourceLayer.src,
        });
        addGeneratedResultLayer(generated, { sourceLayer });
      } else {
        const generated = await generateEditorImage({ prompt: normalizedPrompt });
        addGeneratedResultLayer(generated, { frame: dialog.placeholder });
      }
    } catch (error) {
      setGenerateDialog({
        ...dialog,
        prompt: normalizedPrompt,
        status: 'failed',
        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;
    }
    selectSingleLayer(null);
  };

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

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

  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;
    }

    event.preventDefault();
    event.stopPropagation();
    const pointer = getPointerClient(event);
    canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
    selectSingleLayer(layer.id);
    dragStateRef.current = {
      kind: 'layer',
      pointerId: getPointerId(event),
      layerId: layer.id,
      startClientX: pointer.x,
      startClientY: pointer.y,
      startLayerX: layer.x,
      startLayerY: layer.y,
      startScale: viewport.scale,
    };
  };

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

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

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

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

  const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
    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;
      setGenerateDialog((currentDialog) =>
        currentDialog?.mode === 'generate' && currentDialog.placeholder
          ? {
              ...currentDialog,
              placeholder: {
                ...currentDialog.placeholder,
                x: dragState.startFrameX + deltaX,
                y: dragState.startFrameY + deltaY,
              },
            }
          : currentDialog,
      );
      return;
    }

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

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

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

  const switchTool = (tool: CanvasTool) => {
    dragStateRef.current = null;
    setIsPanning(false);
    setSnapGuide(null);
    if (tool === 'upload') {
      uploadInputRef.current?.click();
      return;
    }
    if (tool === 'generate') {
      openGenerateDialog();
      return;
    }
    setActiveTool(tool);
  };

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

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

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

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

  return (
    <section
      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) {
            addUploadedFiles(files);
          }
          event.currentTarget.value = '';
        }}
      />
      {activeSidebarPanel ? (
        <aside
          className="image-canvas-editor__sidebar"
          aria-label="图片资源栏"
        >
          <div className="image-canvas-editor__sidebar-header">
            <div className="min-w-0">
              <h2 className="image-canvas-editor__sidebar-title">
                {activeSidebarPanel === 'assets' ? '素材' : '图层'}
              </h2>
              <div className="image-canvas-editor__sidebar-count">
                {activeSidebarPanel === 'assets' ? assets.length : layers.length}
              </div>
            </div>
            {activeSidebarPanel === 'assets' ? (
              <div className="image-canvas-editor__sidebar-header-actions">
                <EditorIconButton
                  className="image-canvas-editor__icon-button"
                  label="素材选择模式"
                  title="选择"
                  icon={isAssetSelectionMode ? CheckSquare : Square}
                  pressed={isAssetSelectionMode}
                  onClick={() =>
                    setIsAssetSelectionMode((currentMode) => !currentMode)
                  }
                />
                <EditorIconButton
                  className="image-canvas-editor__icon-button"
                  label="新建素材文件夹"
                  title="新建文件夹"
                  icon={FolderPlus}
                  onClick={() => setCreatingFolder(true)}
                />
              </div>
            ) : (
              <EditorIconButton
                className="image-canvas-editor__icon-button"
                label="图层打组"
                title="打组"
                icon={FolderPlus}
                disabled={!selectedLayerId && selectedLayerIds.length === 0}
                onClick={groupSelectedLayers}
              />
            )}
          </div>

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

      <div className="image-canvas-editor__main">
        <div className="image-canvas-editor__topbar">
          <div className="image-canvas-editor__title-block">
            <h1>图片编辑器</h1>
            <span>画布</span>
          </div>
          <div className="image-canvas-editor__zoom-menu-wrap">
            <button
              type="button"
              className="image-canvas-editor__zoom-trigger"
              aria-label={`当前缩放比例 ${formatPercent(viewport.scale)}`}
              aria-haspopup="menu"
              aria-expanded={isZoomMenuOpen}
              onClick={() => setIsZoomMenuOpen((open) => !open)}
            >
              <span>{formatPercent(viewport.scale)}</span>
            </button>
            {isZoomMenuOpen ? (
              <PlatformFloatingMenu label="缩放菜单" placement="bottom-end">
                <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>

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

            {layers
              .slice()
              .sort((left, right) => left.zIndex - right.zIndex)
              .map((layer) => {
                const isSelected = selectedLayerId === layer.id;
                const isHovered = hoveredLayerId === layer.id;
                return (
                  <button
                    key={layer.id}
                    type="button"
                    className={`image-canvas-editor__layer ${isSelected ? 'image-canvas-editor__layer--selected' : ''} ${isHovered ? 'image-canvas-editor__layer--hovered' : ''}`}
                    style={{
                      left: layer.x,
                      top: layer.y,
                      width: layer.width,
                      height: layer.height,
                      zIndex: layer.zIndex,
                    }}
                    onPointerDown={(event) => handleLayerPointerDown(event, layer)}
                    onMouseEnter={() => setHoveredLayerId(layer.id)}
                    onMouseLeave={() =>
                      setHoveredLayerId((currentId) =>
                        currentId === layer.id ? null : currentId,
                      )
                    }
                    aria-label={`选择${layer.title}`}
                  >
                    <img src={layer.src} alt={`画布图片：${layer.title}`} />
                    {isGeneratedLayer(layer) ? (
                      <PlatformIconButton
                        asChild="spanButton"
                        variant="darkMini"
                        className="image-canvas-editor__metadata-corner"
                        label={`查看${layer.title}元数据`}
                        icon={<Braces className="h-3 w-3" />}
                        onClick={(event) => {
                          event.stopPropagation();
                          setMetadataLayer(layer);
                          selectSingleLayer(layer.id);
                        }}
                        onPointerDown={(event) => event.stopPropagation()}
                      />
                    ) : null}
                    {isHovered ? (
                      <span className="image-canvas-editor__size-badge">
                        {Math.round(layer.width)} x {Math.round(layer.height)} px
                      </span>
                    ) : null}
                  </button>
                );
              })}
            {generateDialog?.mode === 'generate' && generateDialog.placeholder ? (
              <div
                className="image-canvas-editor__generation-frame"
                style={{
                  left: generateDialog.placeholder.x,
                  top: generateDialog.placeholder.y,
                  width: generateDialog.placeholder.width,
                  height: generateDialog.placeholder.height,
                }}
                aria-label="图像生成占位图"
                onPointerDown={handleGenerationFramePointerDown}
              >
                <span className="image-canvas-editor__generation-frame-label">
                  <ImageIcon className="h-4 w-4" />
                  Image Generator
                </span>
                <span className="image-canvas-editor__generation-frame-size">
                  {generateDialog.placeholder.originalWidth} x{' '}
                  {generateDialog.placeholder.originalHeight}
                </span>
                <span className="image-canvas-editor__generation-frame-icon">
                  <ImageIcon className="h-8 w-8" />
                </span>
              </div>
            ) : 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}
              />
              {isGeneratedLayer(selectedLayer) ? (
                <>
                  <EditorIconButton
                    label={`查看${selectedLayer.title}元数据`}
                    title={`查看${selectedLayer.title}元数据`}
                    icon={Info}
                    onClick={() => setMetadataLayer(selectedLayer)}
                  />
                  <EditorIconButton
                    label="修改图片"
                    title="修改图片"
                    icon={WandSparkles}
                    onClick={() => openEditDialog(selectedLayer)}
                  />
                </>
              ) : null}
            </div>
          ) : null}

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

          <div
            className="image-canvas-editor__panel-dock"
            role="toolbar"
            aria-label="画布面板入口"
            onPointerDown={(event) => event.stopPropagation()}
          >
            <div className="image-canvas-editor__background-control">
              <button
                type="button"
                aria-label="画布背景色"
                title="画布背景色"
                aria-expanded={isBackgroundMenuOpen}
                onClick={() => setIsBackgroundMenuOpen((open) => !open)}
              >
                <span
                  className="image-canvas-editor__background-swatch-current"
                  style={{ backgroundColor: canvasBackgroundColor }}
                />
              </button>
              {isBackgroundMenuOpen ? (
                <PlatformFloatingMenu
                  className="image-canvas-editor__background-menu"
                  label="画布背景色菜单"
                  placement="top-start"
                >
                  {CANVAS_BACKGROUND_OPTIONS.map((option) => (
                    <PlatformFloatingMenuItem
                      key={option.value}
                      className="image-canvas-editor__background-menu-item"
                      aria-label={`切换画布背景色为${option.label}`}
                      aria-pressed={canvasBackgroundColor === option.value}
                      onClick={() => {
                        setCanvasBackgroundColor(option.value);
                        setIsBackgroundMenuOpen(false);
                      }}
                    >
                      <span
                        className="image-canvas-editor__background-swatch"
                        style={{ backgroundColor: option.value }}
                      />
                    </PlatformFloatingMenuItem>
                  ))}
                </PlatformFloatingMenu>
              ) : null}
            </div>
            <EditorIconButton
              label="打开素材"
              title="素材"
              icon={ImagePlus}
              pressed={activeSidebarPanel === 'assets'}
              onClick={() => toggleSidebarPanel('assets')}
            />
            <EditorIconButton
              label="打开图层"
              title="图层"
              icon={Layers}
              pressed={activeSidebarPanel === 'layers'}
              onClick={() => toggleSidebarPanel('layers')}
            />
            <EditorIconButton
              label="切换小地图"
              title="小地图"
              icon={MapIcon}
              pressed={isMinimapOpen}
              onClick={() => setIsMinimapOpen((open) => !open)}
            />
          </div>

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

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

          {generateDialog?.mode === 'generate' && 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={() => uploadInputRef.current?.click()}
                icon={<ImageIcon className="h-4 w-4" />}
              >
                <span>参考图</span>
              </PlatformIconButton>
              <textarea
                aria-label="生成提示词"
                value={generateDialog.prompt}
                disabled={generateDialog.status === 'generating'}
                placeholder="今天我们要创作什么"
                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">
                <button
                  type="button"
                  className="image-canvas-editor__generation-ratio"
                  aria-label="生成比例 1:1 2k 1张"
                  disabled={generateDialog.status === 'generating'}
                  onClick={() => triggerPlaceholderAction('生成参数')}
                >
                  中 · 1:1(2k) · 1张
                  <ChevronDown className="h-3 w-3" />
                </button>
                <button
                  type="button"
                  className="image-canvas-editor__generation-model"
                  aria-label="生成模型 GPT Image"
                  disabled={generateDialog.status === 'generating'}
                  onClick={() => triggerPlaceholderAction('模型选择')}
                >
                  GPT Im...
                  <ChevronDown className="h-3 w-3" />
                </button>
                <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}
                disabled={generateDialog.status === 'generating'}
                onClick={() => {
                  setGenerateDialog(null);
                  setActiveTool('select');
                }}
              />
            </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>{metadataLayer.sourceType}</dd>
            <dt>尺寸</dt>
            <dd>
              {metadataLayer.originalWidth} x {metadataLayer.originalHeight}
            </dd>
            <dt>模型</dt>
            <dd>{metadataLayer.model ?? '-'}</dd>
            <dt>服务</dt>
            <dd>{metadataLayer.provider ?? '-'}</dd>
            <dt>任务</dt>
            <dd>{metadataLayer.taskId ?? '-'}</dd>
            <dt>对象</dt>
            <dd>{metadataLayer.objectKey ?? metadataLayer.assetObjectId ?? '-'}</dd>
            <dt>Prompt</dt>
            <dd>{metadataLayer.prompt ?? '-'}</dd>
          </dl>
        ) : null}
      </UnifiedModal>

      <UnifiedModal
        open={generateDialog?.mode === 'edit'}
        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">
              <textarea
                aria-label="生成提示词"
                value={generateDialog.prompt}
                disabled={generateDialog.status === 'generating'}
                placeholder={
                  generateDialog.mode === 'edit'
                    ? '描述你想如何修改这张图片'
                    : '描述你想生成的图片'
                }
                onChange={(event) =>
                  setGenerateDialog((currentDialog) =>
                    currentDialog
                      ? {
                          ...currentDialog,
                          prompt: event.target.value,
                        }
                      : currentDialog,
                  )
                }
              />
              {generateDialog.status === 'generating' ? (
                <PlatformStatusMessage
                  tone="info"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="status"
                >
                  {generateDialog.mode === 'edit' ? '修改中' : '生成中'}
                </PlatformStatusMessage>
              ) : null}
              {generateDialog.status === 'failed' ? (
                <PlatformStatusMessage
                  tone="error"
                  surface="platform"
                  size="xs"
                  className="image-canvas-editor__generate-status"
                  role="alert"
                >
                  {generateDialog.errorMessage}
                </PlatformStatusMessage>
              ) : null}
              <PlatformActionButton
                type="submit"
                size="sm"
                className="image-canvas-editor__generate-submit"
                disabled={generateDialog.status === 'generating'}
              >
                {generateDialog.status === 'generating'
                  ? generateDialog.mode === 'edit'
                    ? '修改中'
                    : '生成中'
                  : generateDialog.mode === 'edit'
                    ? '修改'
                    : '生成'}
              </PlatformActionButton>
            </div>
          </form>
        ) : null}
      </UnifiedModal>
    </section>
  );
}

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

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

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

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

export default ImageCanvasEditorView;
