From f1fe597d68686dd2141c8bd2068513575483851b Mon Sep 17 00:00:00 2001 From: Sean Sun <1194458432@qq.com> Date: Thu, 18 Jun 2026 21:51:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(image-editor):=20wire=20prototype-placehol?= =?UTF-8?q?der=20layer=20ops=20=E2=80=94=20rasterize,=20copy/paste/clear?= =?UTF-8?q?=20style,=20rename,=20color=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PixelForge prototype exposes several layer operations as placeholders (modal "this would…" descriptions) or disabled items. Toolbox's editor already implemented most of them (merge, flatten, smart object, masks). These four were the genuine gaps, now implemented: - Rasterize Layer: bakes the selected layer (post effects/clip/opacity) into a plain image-shape layer at the same id/bbox. Reuses the existing composite-ops.rasterizeLayer helper (was internal-only). Gated for adjustment/filter/mask layers. - Copy / Paste / Clear Layer Style: moves the fx stack between layers via a useRef clipboard (deep-cloned so gradients/ids don't alias). No render change. - Rename Layer: double-click a layer row (or context-menu Rename) → inline input → patchLayer({ name }). The row shows the custom name when it differs from the dynamic kind label. - Layer Color Tag: optional PS-style color label (new colorTag on LayerCommon, round-trips through serialize for free); a dot on the row opens a swatch picker. Tag palette in lib/image-editor/layer-color-tags.ts. Wired into both the Layer menu (MenuBar) and the layer-row context menu; en + zh-CN i18n added (keys identical across locales). Verified: typecheck/lint/build clean, 388 tests green; headless-Chrome created a group layer and confirmed all four ops render + function (rename typed and applied, context menu has all items, color dot present), zero page errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/image-editor/LayersPanel.tsx | 114 +++++++++++- src/components/image-editor/MenuBar.tsx | 29 +++ src/components/image-editor/RightSidebar.tsx | 15 +- src/i18n/en.json | 23 ++- src/i18n/zh-CN.json | 23 ++- src/lib/image-editor/layer-color-tags.ts | 27 +++ src/lib/image-editor/types.ts | 10 ++ src/pages/ImageEditor.tsx | 180 ++++++++++++++++++- 8 files changed, 414 insertions(+), 7 deletions(-) create mode 100644 src/lib/image-editor/layer-color-tags.ts diff --git a/src/components/image-editor/LayersPanel.tsx b/src/components/image-editor/LayersPanel.tsx index 0591f08..c5a0be8 100644 --- a/src/components/image-editor/LayersPanel.tsx +++ b/src/components/image-editor/LayersPanel.tsx @@ -1,4 +1,4 @@ -import { type DragEvent } from 'react' +import { type DragEvent, useState } from 'react' import { useTranslation } from 'react-i18next' import { ChevronDown, @@ -20,7 +20,8 @@ import { removeAtPath, } from '@/lib/image-editor/layer-tree' import { hasEffects } from '@/lib/image-editor/layer-effects' -import type { EditorState, GroupLayer, Layer } from '@/lib/image-editor/types' +import type { EditorState, GroupLayer, Layer, LayerColorTag } from '@/lib/image-editor/types' +import { LAYER_TAG_COLORS, LAYER_TAGS } from '@/lib/image-editor/layer-color-tags' type Props = { state: EditorState @@ -37,6 +38,14 @@ type Props = { /** Add a mask to the given layer. For annotation / SO / group: insert a * new raster MaskLayer above. For adjustment / filter: add maskDataUrl. */ onAddMask?: (id: string) => void + /** Which row is in inline-rename mode (double-click / context menu Rename). */ + renamingId?: string | null + /** Begin inline rename for a row (double-click). */ + onStartRename?: (id: string) => void + /** Commit a rename (Enter / blur) and exit edit mode. */ + onCommitRename?: (id: string, name: string) => void + /** Set / clear a layer's organisational color tag. */ + onSetColorTag?: (id: string, tag: LayerColorTag | undefined) => void } type DropMode = 'into' | 'sibling' | 'top' @@ -66,6 +75,10 @@ export function LayersPanel({ onOpenStyle, onLayerContextMenu, onAddMask, + renamingId, + onStartRename, + onCommitRename, + onSetColorTag, }: Props) { const { t } = useTranslation() @@ -156,6 +169,10 @@ export function LayersPanel({ onOpenStyle={onOpenStyle} onLayerContextMenu={onLayerContextMenu} onAddMask={onAddMask} + renamingId={renamingId} + onStartRename={onStartRename} + onCommitRename={onCommitRename} + onSetColorTag={onSetColorTag} /> ))} @@ -225,6 +242,10 @@ function LayerSubtree({ onOpenStyle, onLayerContextMenu, onAddMask, + renamingId, + onStartRename, + onCommitRename, + onSetColorTag, }: { layer: Layer depth: number @@ -238,6 +259,10 @@ function LayerSubtree({ onOpenStyle: (id: string) => void onLayerContextMenu?: (id: string, x: number, y: number) => void onAddMask?: (id: string) => void + renamingId?: string | null + onStartRename?: (id: string) => void + onCommitRename?: (id: string, name: string) => void + onSetColorTag?: (id: string, tag: LayerColorTag | undefined) => void }) { const group = isGroup(layer) ? layer : null return ( @@ -257,6 +282,10 @@ function LayerSubtree({ onOpenStyle={() => onOpenStyle(layer.id)} onContextMenu={(x, y) => onLayerContextMenu?.(layer.id, x, y)} onAddMask={onAddMask} + renaming={renamingId === layer.id} + onStartRename={() => onStartRename?.(layer.id)} + onCommitRename={(name) => onCommitRename?.(layer.id, name)} + onSetColorTag={(tag) => onSetColorTag?.(layer.id, tag)} /> {group && group.expanded && [...group.children].reverse().map((c) => ( @@ -274,6 +303,10 @@ function LayerSubtree({ onOpenStyle={onOpenStyle} onLayerContextMenu={onLayerContextMenu} onAddMask={onAddMask} + renamingId={renamingId} + onStartRename={onStartRename} + onCommitRename={onCommitRename} + onSetColorTag={onSetColorTag} /> ))} @@ -304,6 +337,10 @@ function LayerRow({ onOpenStyle, onContextMenu, onAddMask, + renaming, + onStartRename, + onCommitRename, + onSetColorTag, }: { layer: Layer depth: number @@ -319,10 +356,15 @@ function LayerRow({ onOpenStyle: () => void onContextMenu?: (x: number, y: number) => void onAddMask?: (layerId: string) => void + renaming?: boolean + onStartRename?: () => void + onCommitRename?: (name: string) => void + onSetColorTag?: (tag: LayerColorTag | undefined) => void }) { const { t } = useTranslation() const showFx = hasEffects(layer) const labelKey = layerLabelKey(layer) + const [colorMenuOpen, setColorMenuOpen] = useState(false) const labelArgs = layer.kind === 'annotation' && layer.shape.kind === 'text' ? { @@ -414,7 +456,73 @@ function LayerRow({ style={{ background: '#000' }} /> )} - {t(labelKey, labelArgs)} + {/* Color tag dot — click cycles open a tiny swatch row. */} + {onSetColorTag && ( + + + + )} + + )} + {renaming ? ( + e.stopPropagation()} + onKeyDown={(e) => { + e.stopPropagation() + if (e.key === 'Enter') onCommitRename?.((e.target as HTMLInputElement).value) + else if (e.key === 'Escape') onCommitRename?.(layer.name) + }} + onBlur={(e) => onCommitRename?.(e.target.value)} + /> + ) : ( + { + e.stopPropagation() + onStartRename?.() + }} + > + {layer.name && layer.name !== t(labelKey, labelArgs) ? layer.name : t(labelKey, labelArgs)} + + )} {showFx && (