From a15930c57a569131ad1458d6b256f8d88f5c0fae Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 17 Jun 2026 02:44:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E5=9B=BE=E7=89=87=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E8=88=9E=E5=8F=B0=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽出画布工作区视觉树为 ImageCanvasStageView 保留拖拽缩放历史上传生成等状态机在主视图 更新图片画布拆分计划和 TRACKING 回归记录 --- TRACKING.md | 2 + ...构】图片画布编辑器前端拆分计划-2026-06-17.md | 10 +- .../image-editor/ImageCanvasEditorView.tsx | 1047 +++------------- .../image-editor/ImageCanvasStageView.tsx | 1091 +++++++++++++++++ 4 files changed, 1270 insertions(+), 880 deletions(-) create mode 100644 src/components/image-editor/ImageCanvasStageView.tsx diff --git a/TRACKING.md b/TRACKING.md index 09adb9bd..b3698a2b 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -114,3 +114,5 @@ - 2026-06-17 浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录打开工程和未登录上传均弹出 `账号入口`;关闭登录后点击 `画布背景色` 打开 `画布背景设置` 面板,点击 `暖灰` 后画布背景为 `rgb(243, 240, 234)`;登录开发账号后上传图片成功进入 `项目素材`,`AI画布工具栏` 保持可见。 - 2026-06-17 前端拆分第二阶段:新增 `ImageCanvasSidebarView`,把素材 / 图层共用左侧整合面板从主视图抽出;上传链路、登录弹窗、素材拖到画布、持久化、图层历史和右键菜单状态机仍保留在主视图,避免过度拆分。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`。 - 2026-06-17 侧栏拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`;`画布背景色` 打开 `画布背景设置` dialog,包含预设、自定义颜色、HEX 和恢复默认;使用临时开发账号登录后上传图片成功进入 `项目素材`,点击素材可添加到画布,切换 `图层` 侧栏后能看到同一图片图层,`AI画布工具栏` 保持可见。 +- 2026-06-17 前端拆分第三阶段:新增 `ImageCanvasStageView`,把画布工作区视觉树、图层渲染、生成占位框、右键菜单、左下 dock、小地图和底部 AI 工具栏从主视图抽出;拖拽 / 缩放、历史、上传、登录、生成提交、素材持久化和右键命令仍保留在主视图,避免拆散状态机。 +- 2026-06-17 舞台拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`;关闭登录后点击 `画布背景色` 打开完整 `画布背景设置` dialog,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`;使用临时开发账号密码登录后上传 `smoke.png` 成功进入 `项目素材`,点击素材添加到画布,切换 `图层` 后显示同一图层,图片浮动工具栏、小地图和 `AI画布工具栏` 保持可见。 diff --git a/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md b/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md index 9acf73d7..b246398d 100644 --- a/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md +++ b/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md @@ -41,11 +41,17 @@ - 继续通过 props 调用主视图状态机,不接管上传、登录弹窗、持久化、拖到画布的坐标换算和图层历史记录。 - 保持“素材”和“图层”同一侧栏切换的 Lovart 式布局,不恢复右侧独立图层栏或左侧竖向工具栏。 -第二阶段以后,主视图仍是画布编排入口。继续拆分前应优先选择能形成稳定边界的深模块,避免把上传链路、DataTransfer、画布坐标和历史快照拆成互相回调的小碎片。 +## 第三阶段模块 + +- `ImageCanvasStageView.tsx` + - 承载中央画布工作区的视觉树:viewport / world DOM、图层渲染、生成占位框、选中图片浮动工具栏、空白和图片右键菜单、左下 dock、缩放菜单、背景设置面板、小地图和底部 AI 工具栏。 + - 继续通过 props 调用主视图状态机,不接管拖拽 / 平移 / 缩放、画布坐标换算、历史 undo / redo、上传、登录、生成提交、素材持久化和右键命令实现。 + - 保持 `canvasViewportRef` 由主视图传入,确保 pointer capture、drop 坐标、滚轮缩放和小地图拖拽仍使用同一套坐标源。 + +第三阶段以后,主视图仍是画布编排入口。继续拆分前应优先选择能形成稳定边界的深模块,避免把上传链路、DataTransfer、画布坐标和历史快照拆成互相回调的小碎片。 ## 后续阶段 -- `ImageCanvasStageView`:画布 viewport、图层渲染、右键菜单和生成占位框,等交互回归覆盖更强后再拆。 - `ImageCanvasGenerationDock`:底部 AI 工具栏和生成面板族,等生成对象状态机进一步收口后再拆。 ## 验证计划 diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index 75f6d1ad..1ce17e8a 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -1,37 +1,19 @@ import { - Braces, Check, ChevronDown, ChevronLeft, - ChevronRight, ClipboardList, - Copy, - Crop, Download, - Folder, - Hand, ImageIcon, ImagePlus, - Info, - Layers, - Map as MapIcon, - MousePointer2, Pencil, - Redo2, - RotateCcw, - Shapes, - SlidersHorizontal, - Sparkles, - Trash2, - Type, - Undo2, - WandSparkles, X, } from 'lucide-react'; import JSZip from 'jszip'; import { type CSSProperties, type DragEvent as ReactDragEvent, + type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode, useCallback, @@ -74,7 +56,6 @@ import { } from '../common/PlatformFloatingMenu'; import { PlatformIconButton } from '../common/PlatformIconButton'; import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; -import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformSelectField, @@ -84,10 +65,9 @@ import { UnifiedModal } from '../common/UnifiedModal'; import { useAuthUi } from '../auth/AuthUiContext'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; import { ImageCanvasSidebarView } from './ImageCanvasSidebarView'; +import { ImageCanvasStageView } from './ImageCanvasStageView'; import { ASSET_DRAG_MIME_TYPE, - CANVAS_BACKGROUND_OPTIONS, - CANVAS_WORLD_SIZE, DEFAULT_CANVAS_BACKGROUND_COLOR, DEFAULT_CANVAS_SIZE, EDITOR_ASSET_FOLDERS, @@ -103,7 +83,6 @@ import { createLayerFromAsset, escapeCssIdentifier, formatImageSizeValue, - formatPercent, getDraggedAssetId, getLayerBounds, hasDataTransferType, @@ -3570,6 +3549,24 @@ export function ImageCanvasEditorView() { }); }; + const handleCanvasContextMenu = ( + event: ReactMouseEvent, + ) => { + event.preventDefault(); + event.stopPropagation(); + const position = resolveContextMenuPosition( + event.clientX, + event.clientY, + 'blank', + ); + setImageContextMenu(null); + setContextMenu({ + kind: 'blank', + ...position, + canvasPoint: getCanvasPointFromClient(event.clientX, event.clientY), + }); + }; + const handleLayerPointerDown = ( event: ReactPointerEvent, layer: CanvasLayer, @@ -3657,6 +3654,61 @@ export function ImageCanvasEditorView() { }; }; + const handleLayerClick = ( + event: ReactMouseEvent, + layer: CanvasLayer, + ) => { + // 测试环境和辅助技术可能只触发 click; + // 用 click 兜底选中,真实拖拽仍由 pointerDown 负责。 + event.stopPropagation(); + if (isPickingCharacterSpecFromCanvas) { + return; + } + if (isPickingIconSpecFromCanvas) { + return; + } + if (event.shiftKey || isShiftPressedRef.current) { + return; + } + selectSingleLayer(layer.id); + setImageContextMenu(null); + }; + + const handleLayerContextMenu = ( + event: ReactMouseEvent, + layer: CanvasLayer, + ) => { + event.preventDefault(); + event.stopPropagation(); + if (!selectedLayerIds.includes(layer.id)) { + selectSingleLayer(layer.id); + } + const position = resolveContextMenuPosition( + event.clientX, + event.clientY, + 'layer', + ); + setContextMenu({ + kind: 'layer', + layerId: layer.id, + ...position, + canvasPoint: getCanvasPointFromClient(event.clientX, event.clientY), + }); + setImageContextMenu({ + layerId: layer.id, + ...position, + }); + }; + + const handleCanvasBackgroundHexChange = (nextValue: string) => { + setCanvasBackgroundHexValue(nextValue); + const normalizedColor = normalizeCanvasBackgroundHex(nextValue); + if (normalizedColor) { + setCanvasBackgroundColor(normalizedColor); + setCanvasBackgroundHexValue(normalizedColor); + } + }; + const handleGenerationFramePointerDown = ( event: ReactPointerEvent, dialog: CanvasGenerationDialogState, @@ -3988,30 +4040,6 @@ export function ImageCanvasEditorView() { ); }; - const toolButtons = [ - { label: '裁剪', icon: Crop }, - { label: '重绘', icon: Sparkles }, - { label: '调整', icon: SlidersHorizontal }, - { label: '复制', icon: Copy }, - ]; - - const canvasTools: Array<{ - id: CanvasTool; - label: string; - icon: typeof MousePointer2; - }> = [ - { id: 'select', label: '选择工具', icon: MousePointer2 }, - { id: 'hand', label: '抓手工具', icon: Hand }, - { id: 'upload', label: '上传工具', icon: ImagePlus }, - { id: 'generate', label: '生成工具', icon: WandSparkles }, - { id: 'spec', label: '生成规范', icon: ClipboardList }, - { id: 'character', label: '生成角色形象', icon: Sparkles }, - { id: 'icon', label: '生成图标素材', icon: ImageIcon }, - { id: 'text', label: '文字工具', icon: Type }, - { id: 'shape', label: '形状标注工具', icon: Shapes }, - { id: 'export', label: '导出工具', icon: Download }, - ]; - const updateSpecFormValue = (key: keyof SpecFormValues, value: string) => { setGenerateDialog((currentDialog) => { if (currentDialog?.mode !== 'spec') { @@ -4301,9 +4329,9 @@ export function ImageCanvasEditorView() { onClick={startProjectRename} /> - )} - 画布 - + )} + 画布 +
) : null}
- + -
{ - event.preventDefault(); - event.stopPropagation(); - const position = resolveContextMenuPosition( - event.clientX, - event.clientY, - 'blank', - ); - setImageContextMenu(null); - setContextMenu({ - kind: 'blank', - ...position, - canvasPoint: getCanvasPointFromClient(event.clientX, event.clientY), - }); + + setHoveredLayerId((currentId) => + currentId === layerId ? null : currentId, + ) + } + onOpenLayerMetadata={(layer) => { + setMetadataLayer(layer); + selectSingleLayer(layer.id); }} + onGenerationFramePointerDown={handleGenerationFramePointerDown} + onActivateGenerationDialog={activateCanvasGenerationDialog} + onDeleteSelectedLayer={deleteSelectedLayer} + onOpenQuickEditPanel={openQuickEditPanel} + onOpenEditDialog={openEditDialog} + onOpenCharacterAnimationPanel={openCharacterAnimationPanel} + onPasteCanvasClipboard={pasteCanvasClipboard} + onCopyContextLayers={copyContextLayers} + onDuplicateContextLayers={duplicateContextLayers} + onMoveContextLayers={moveContextLayers} + onGroupContextLayers={groupContextLayers} + onUngroupContextLayers={ungroupContextLayers} + onToggleContextLayerVisibility={toggleContextLayerVisibility} + onToggleContextLayerLock={toggleContextLayerLock} + onFlipContextLayers={flipContextLayers} + onExportContextLayer={exportContextLayer} + onDeleteContextLayers={deleteContextLayers} + onDeleteLayerById={deleteLayerById} + onCloseContextMenu={() => setContextMenu(null)} + onCloseImageContextMenu={() => setImageContextMenu(null)} + onUpdateScaleFromCenter={updateScaleFromCenter} + onFitLayers={fitLayers} + onUndoCanvasChange={undoCanvasChange} + onRedoCanvasChange={redoCanvasChange} + onToggleZoomMenu={() => setIsZoomMenuOpen((open) => !open)} + onCloseZoomMenu={() => setIsZoomMenuOpen(false)} + onToggleBackgroundSettings={() => + setIsBackgroundSettingsOpen((isOpen) => !isOpen) + } + onApplyCanvasBackgroundColor={applyCanvasBackgroundColor} + onCanvasBackgroundHexChange={handleCanvasBackgroundHexChange} + onToggleSidebarPanel={toggleSidebarPanel} + onToggleMinimap={() => setIsMinimapOpen((open) => !open)} + onMinimapPointerDown={handleMinimapPointerDown} + onSwitchTool={switchTool} > - {uploadDropTarget === 'canvas' ? ( -
- 添加到画布 - 松开即可添加 -
- ) : null} -
- {snapGuide?.vertical !== undefined ? ( -
- ) : null} - {snapGuide?.horizontal !== undefined ? ( -
- ) : null} - - {layers - .slice() - .filter((layer) => !layer.hidden) - .sort((left, right) => left.zIndex - right.zIndex) - .map((layer) => { - const isSelected = selectedLayerIds.includes(layer.id); - const isHovered = hoveredLayerId === layer.id; - const kindLabel = getLayerKindLabel(layer); - const layerGeneratingLabel = - generateDialog?.mode === 'edit' && - generateDialog.status === 'generating' && - generateDialog.sourceLayerId === layer.id - ? '修改中' - : quickEditPanel?.status === 'generating' && - quickEditPanel.sourceLayerId === layer.id - ? '生成中' - : null; - return ( - - ); - })} - {canvasMarquee ? ( - - - {selectedLayer && selectedToolbarStyle ? ( -
event.stopPropagation()} - > - {toolButtons.map(({ label, icon: Icon }) => ( - triggerPlaceholderAction(label)} - /> - ))} - - openQuickEditPanel(selectedLayer)} - /> - {isGeneratedLayer(selectedLayer) ? ( - <> - setMetadataLayer(selectedLayer)} - /> - openEditDialog(selectedLayer)} - /> - - ) : null} - {selectedLayer.assetKind === 'character' ? ( - openCharacterAnimationPanel(selectedLayer)} - /> - ) : null} -
- ) : null} - - {contextMenu ? ( -
event.preventDefault()} - onPointerDown={(event) => event.stopPropagation()} - > - {contextMenu.kind === 'blank' ? ( - <> - - - - - - ) : ( - <> - - - - -
- - - - -
- - - - -
- - - -
- {imageContextMenuLayer ? ( - <> - - - {imageContextMenuLayer.assetKind === 'character' ? ( - - ) : null} -
- - ) : null} - - - )} -
- ) : null} - - fitLayers()} - /> - -
event.stopPropagation()} - > - - -
- setIsZoomMenuOpen((open) => !open)} - > - {formatPercent(viewport.scale)} - - {isZoomMenuOpen ? ( - - { - updateScaleFromCenter(viewport.scale * 1.16); - setIsZoomMenuOpen(false); - }} - > - 放大 - - { - updateScaleFromCenter(viewport.scale * 0.86); - setIsZoomMenuOpen(false); - }} - > - 缩小 - - { - fitLayers(); - setIsZoomMenuOpen(false); - }} - > - 显示画布所有元素 - - {[0.5, 1, 2].map((scale) => ( - { - updateScaleFromCenter(scale); - setIsZoomMenuOpen(false); - }} - > - 缩放至{Math.round(scale * 100)}% - - ))} - - ) : null} -
-
- - setIsBackgroundSettingsOpen((isOpen) => !isOpen) - } - icon={ - - } - /> - {isBackgroundSettingsOpen ? ( -
-
- 画布背景 - -
-
- {CANVAS_BACKGROUND_OPTIONS.map((option) => ( - - ))} -
- - - - applyCanvasBackgroundColor(DEFAULT_CANVAS_BACKGROUND_COLOR) - } - > - -
- ) : null} -
- toggleSidebarPanel('assets')} - /> - toggleSidebarPanel('layers')} - /> - setIsMinimapOpen((open) => !open)} - /> -
- - {isMinimapOpen && minimapModel ? ( - - ) : null} - -
event.stopPropagation()} - > - {canvasTools.map(({ id, label, icon: Icon }) => - id === 'spec' ? ( - - switchTool(id)} - /> - - ) : ( - switchTool(id)} - /> - ), - )} -
{isSpecMenuOpen ? renderEditorPortal( @@ -6020,53 +5357,7 @@ export function ImageCanvasEditorView() { ) : null} - {imageContextMenu && imageContextMenuLayer && !contextMenu ? ( -
event.stopPropagation()} - > - - openQuickEditPanel(imageContextMenuLayer)} - > - 快速编辑 - - { - setMetadataLayer(imageContextMenuLayer); - setImageContextMenu(null); - }} - > - 查看图片信息 - - {imageContextMenuLayer.assetKind === 'character' ? ( - - openCharacterAnimationPanel(imageContextMenuLayer) - } - > - 生成动画 - - ) : null} - deleteLayerById(imageContextMenuLayer.id)} - > - 删除图片 - - -
- ) : null} + {characterAnimationPanel && characterAnimationSourceLayer && @@ -6277,7 +5568,7 @@ export function ImageCanvasEditorView() { ) : null} -
+
; + viewport: CSSProperties; +}; + +type ImageCanvasStageViewProps = { + canvasViewportRef: RefObject; + specToolWrapRef: RefObject; + isPanning: boolean; + effectiveTool: CanvasTool; + canvasBackgroundColor: string; + canvasBackgroundHexValue: string; + viewport: CanvasViewport; + snapGuide: SnapGuide | null; + layers: CanvasLayer[]; + selectedLayer: CanvasLayer | null; + selectedLayerIds: string[]; + hoveredLayerId: string | null; + canvasMarquee: CanvasMarqueeState | null; + canvasGenerationDialogs: CanvasGenerationDialogState[]; + generateDialog: GenerateDialogState | null; + quickEditPanel: QuickEditPanelState | null; + generationComposerStyle: CSSProperties | null; + selectedToolbarStyle: CSSProperties | null; + uploadDropTarget: 'canvas' | 'assets' | null; + contextMenu: CanvasContextMenuState | null; + canvasClipboard: CanvasClipboard | null; + imageContextMenu: ImageContextMenuState | null; + imageContextMenuLayer: CanvasLayer | null; + contextShouldShowLayer: boolean; + contextShouldUnlockLayer: boolean; + canUndo: boolean; + canRedo: boolean; + isZoomMenuOpen: boolean; + isBackgroundSettingsOpen: boolean; + activeSidebarPanel: SidebarPanel | null; + isMinimapOpen: boolean; + minimapModel: StageMinimapModel | null; + children?: ReactNode; + onCanvasPointerDown: (event: ReactPointerEvent) => void; + onCanvasPointerMove: (event: ReactPointerEvent) => void; + onCanvasPointerUp: (event: ReactPointerEvent) => void; + onCanvasWheel: (event: ReactWheelEvent) => void; + onCanvasDragOver: (event: ReactDragEvent) => void; + onCanvasDragLeave: (event: ReactDragEvent) => void; + onCanvasDrop: (event: ReactDragEvent) => void; + onCanvasContextMenu: (event: ReactMouseEvent) => void; + onLayerPointerDown: ( + event: ReactPointerEvent, + layer: CanvasLayer, + ) => void; + onLayerClick: ( + event: ReactMouseEvent, + layer: CanvasLayer, + ) => void; + onLayerContextMenu: ( + event: ReactMouseEvent, + layer: CanvasLayer, + ) => void; + onLayerMouseEnter: (layerId: string) => void; + onLayerMouseLeave: (layerId: string) => void; + onOpenLayerMetadata: (layer: CanvasLayer) => void; + onGenerationFramePointerDown: ( + event: ReactPointerEvent, + dialog: CanvasGenerationDialogState, + ) => void; + onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void; + onDeleteSelectedLayer: () => void; + onOpenQuickEditPanel: (layer: CanvasLayer) => void; + onOpenEditDialog: (layer: CanvasLayer) => void; + onOpenCharacterAnimationPanel: (layer: CanvasLayer) => void; + onPasteCanvasClipboard: (canvasPoint?: { x: number; y: number }) => void; + onCopyContextLayers: (options?: { cut?: boolean }) => void; + onDuplicateContextLayers: () => void; + onMoveContextLayers: (mode: 'up' | 'down' | 'top' | 'bottom') => void; + onGroupContextLayers: () => void; + onUngroupContextLayers: () => void; + onToggleContextLayerVisibility: () => void; + onToggleContextLayerLock: () => void; + onFlipContextLayers: (axis: 'x' | 'y') => void; + onExportContextLayer: () => void; + onDeleteContextLayers: () => void; + onDeleteLayerById: (layerId: string | null) => void; + onCloseContextMenu: () => void; + onCloseImageContextMenu: () => void; + onUpdateScaleFromCenter: (nextScale: number) => void; + onFitLayers: () => void; + onUndoCanvasChange: () => void; + onRedoCanvasChange: () => void; + onToggleZoomMenu: () => void; + onCloseZoomMenu: () => void; + onToggleBackgroundSettings: () => void; + onApplyCanvasBackgroundColor: (color: string) => void; + onCanvasBackgroundHexChange: (value: string) => void; + onToggleSidebarPanel: (panel: SidebarPanel) => void; + onToggleMinimap: () => void; + onMinimapPointerDown: (event: ReactPointerEvent) => void; + onSwitchTool: (tool: CanvasTool) => void; +}; + +const layerToolButtons = [ + { label: '裁剪', icon: Crop }, + { label: '重绘', icon: Sparkles }, + { label: '调整', icon: SlidersHorizontal }, + { label: '复制', icon: Copy }, +]; + +const canvasTools: Array<{ + id: CanvasTool; + label: string; + icon: typeof MousePointer2; +}> = [ + { id: 'select', label: '选择工具', icon: MousePointer2 }, + { id: 'hand', label: '抓手工具', icon: Hand }, + { id: 'upload', label: '上传工具', icon: ImagePlus }, + { id: 'generate', label: '生成工具', icon: WandSparkles }, + { id: 'spec', label: '生成规范', icon: ClipboardList }, + { id: 'character', label: '生成角色形象', icon: Sparkles }, + { id: 'icon', label: '生成图标素材', icon: ImageIcon }, + { id: 'text', label: '文字工具', icon: Type }, + { id: 'shape', label: '形状标注工具', icon: Shapes }, + { id: 'export', label: '导出工具', icon: Download }, +]; + +function triggerPlaceholderAction(label: string) { + window.alert(`${label}功能建设中`); +} + +export function ImageCanvasStageView({ + canvasViewportRef, + specToolWrapRef, + isPanning, + effectiveTool, + canvasBackgroundColor, + canvasBackgroundHexValue, + viewport, + snapGuide, + layers, + selectedLayer, + selectedLayerIds, + hoveredLayerId, + canvasMarquee, + canvasGenerationDialogs, + generateDialog, + quickEditPanel, + generationComposerStyle, + selectedToolbarStyle, + uploadDropTarget, + contextMenu, + canvasClipboard, + imageContextMenu, + imageContextMenuLayer, + contextShouldShowLayer, + contextShouldUnlockLayer, + canUndo, + canRedo, + isZoomMenuOpen, + isBackgroundSettingsOpen, + activeSidebarPanel, + isMinimapOpen, + minimapModel, + children, + onCanvasPointerDown, + onCanvasPointerMove, + onCanvasPointerUp, + onCanvasWheel, + onCanvasDragOver, + onCanvasDragLeave, + onCanvasDrop, + onCanvasContextMenu, + onLayerPointerDown, + onLayerClick, + onLayerContextMenu, + onLayerMouseEnter, + onLayerMouseLeave, + onOpenLayerMetadata, + onGenerationFramePointerDown, + onActivateGenerationDialog, + onDeleteSelectedLayer, + onOpenQuickEditPanel, + onOpenEditDialog, + onOpenCharacterAnimationPanel, + onPasteCanvasClipboard, + onCopyContextLayers, + onDuplicateContextLayers, + onMoveContextLayers, + onGroupContextLayers, + onUngroupContextLayers, + onToggleContextLayerVisibility, + onToggleContextLayerLock, + onFlipContextLayers, + onExportContextLayer, + onDeleteContextLayers, + onDeleteLayerById, + onCloseContextMenu, + onCloseImageContextMenu, + onUpdateScaleFromCenter, + onFitLayers, + onUndoCanvasChange, + onRedoCanvasChange, + onToggleZoomMenu, + onCloseZoomMenu, + onToggleBackgroundSettings, + onApplyCanvasBackgroundColor, + onCanvasBackgroundHexChange, + onToggleSidebarPanel, + onToggleMinimap, + onMinimapPointerDown, + onSwitchTool, +}: ImageCanvasStageViewProps) { + return ( +
+ {uploadDropTarget === 'canvas' ? ( +
+ 添加到画布 + 松开即可添加 +
+ ) : null} +
+ {snapGuide?.vertical !== undefined ? ( +
+ ) : null} + {snapGuide?.horizontal !== undefined ? ( +
+ ) : null} + + {layers + .slice() + .filter((layer) => !layer.hidden) + .sort((left, right) => left.zIndex - right.zIndex) + .map((layer) => { + const isSelected = selectedLayerIds.includes(layer.id); + const isHovered = hoveredLayerId === layer.id; + const kindLabel = getLayerKindLabel(layer); + const layerGeneratingLabel = + generateDialog?.mode === 'edit' && + generateDialog.status === 'generating' && + generateDialog.sourceLayerId === layer.id + ? '修改中' + : quickEditPanel?.status === 'generating' && + quickEditPanel.sourceLayerId === layer.id + ? '生成中' + : null; + return ( + + ); + })} + {canvasMarquee ? ( + + + {selectedLayer && selectedToolbarStyle ? ( +
event.stopPropagation()} + > + {layerToolButtons.map(({ label, icon: Icon }) => ( + triggerPlaceholderAction(label)} + /> + ))} + + onOpenQuickEditPanel(selectedLayer)} + /> + {isGeneratedLayer(selectedLayer) ? ( + <> + onOpenLayerMetadata(selectedLayer)} + /> + onOpenEditDialog(selectedLayer)} + /> + + ) : null} + {selectedLayer.assetKind === 'character' ? ( + onOpenCharacterAnimationPanel(selectedLayer)} + /> + ) : null} +
+ ) : null} + + {contextMenu ? ( +
event.preventDefault()} + onPointerDown={(event) => event.stopPropagation()} + > + {contextMenu.kind === 'blank' ? ( + <> + + + + + + ) : ( + <> + + + + +
+ + + + +
+ + + + +
+ + + +
+ {imageContextMenuLayer ? ( + <> + + + {imageContextMenuLayer.assetKind === 'character' ? ( + + ) : null} +
+ + ) : null} + + + )} +
+ ) : null} + + + +
event.stopPropagation()} + > + + +
+ + {formatPercent(viewport.scale)} + + {isZoomMenuOpen ? ( + + { + onUpdateScaleFromCenter(viewport.scale * 1.16); + onCloseZoomMenu(); + }} + > + 放大 + + { + onUpdateScaleFromCenter(viewport.scale * 0.86); + onCloseZoomMenu(); + }} + > + 缩小 + + { + onFitLayers(); + onCloseZoomMenu(); + }} + > + 显示画布所有元素 + + {[0.5, 1, 2].map((scale) => ( + { + onUpdateScaleFromCenter(scale); + onCloseZoomMenu(); + }} + > + 缩放至{Math.round(scale * 100)}% + + ))} + + ) : null} +
+
+ + } + /> + {isBackgroundSettingsOpen ? ( +
+
+ 画布背景 + +
+
+ {CANVAS_BACKGROUND_OPTIONS.map((option) => ( + + ))} +
+ + + + onApplyCanvasBackgroundColor(DEFAULT_CANVAS_BACKGROUND_COLOR) + } + > + +
+ ) : null} +
+ onToggleSidebarPanel('assets')} + /> + onToggleSidebarPanel('layers')} + /> + +
+ + {isMinimapOpen && minimapModel ? ( + + ) : null} + +
event.stopPropagation()} + > + {canvasTools.map(({ id, label, icon: Icon }) => + id === 'spec' ? ( + + onSwitchTool(id)} + /> + + ) : ( + onSwitchTool(id)} + /> + ), + )} +
+ + {imageContextMenu && imageContextMenuLayer && !contextMenu ? ( +
event.stopPropagation()} + > + + onOpenQuickEditPanel(imageContextMenuLayer)} + > + 快速编辑 + + { + onOpenLayerMetadata(imageContextMenuLayer); + onCloseImageContextMenu(); + }} + > + 查看图片信息 + + {imageContextMenuLayer.assetKind === 'character' ? ( + onOpenCharacterAnimationPanel(imageContextMenuLayer)} + > + 生成动画 + + ) : null} + onDeleteLayerById(imageContextMenuLayer.id)} + > + 删除图片 + + +
+ ) : null} + + {children} +
+ ); +}