mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 00:20:07 +00:00
# Canvas Rotation and Mirroring ## Overview Adds rotation (90° left/right) and mirroring (horizontal/vertical) capabilities to the mask editor canvas. All three layers (image, mask, RGB) transform together. Redo and Undo respect transformations as new states. Keyboard shortcuts also added for all four functions in Keybinding settings. Additionally, fixed the issue of ctrl+z and ctrl+y keyboard commands not restricting to the mask editor canvas while opened. https://github.com/user-attachments/assets/fb8d5347-b357-4a3a-840a-721cdf8a6125 ## What Changed ### New Files - **`src/composables/maskeditor/useCanvasTransform.ts`** - Core transformation logic for rotation and mirroring - GPU texture recreation after transformations ### Modified Files #### **`src/composables/useCoreCommands.ts`** - Added check to see if Mask Editor is opened for undo and redo commands #### **`src/stores/maskEditorStore.ts`** - Added GPU texture recreation signals #### **`src/composables/maskeditor/useBrushDrawing.ts`** - Added watcher for `gpuTexturesNeedRecreation` signal - Handles GPU texture recreation when canvas dimensions change - Recreates textures with new dimensions after rotation - Updates preview canvas and readback buffers accordingly - Ensures proper ArrayBuffer backing for WebGPU compatibility #### **`src/components/maskeditor/TopBarHeader.vue`** - Added 4 new transform buttons with icons: - Rotate Left (counter-clockwise) - Rotate Right (clockwise) - Mirror Horizontal - Mirror Vertical - Added visual separators between button groups #### **`src/extensions/core/maskEditor.ts`** - Added keyboard shortcut settings for rotate and mirror #### **Translation Files** (e.g., `src/locales/en.json`) - Added i18n keys: ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7841-Added-MaskEditor-Rotate-and-Mirror-Functions-2de6d73d365081bc9b84ea4919a3c6a1) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
161 lines
4.8 KiB
TypeScript
161 lines
4.8 KiB
TypeScript
import _ from 'es-toolkit/compat'
|
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
|
|
import { app, ComfyApp } from '@/scripts/app'
|
|
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
|
import { useDialogStore } from '@/stores/dialogStore'
|
|
import { useMaskEditor } from '@/composables/maskeditor/useMaskEditor'
|
|
import { useCanvasTransform } from '@/composables/maskeditor/useCanvasTransform'
|
|
|
|
function openMaskEditor(node: LGraphNode): void {
|
|
if (!node) {
|
|
console.error('[MaskEditor] No node provided')
|
|
return
|
|
}
|
|
|
|
if (!node.imgs?.length && node.previewMediaType !== 'image') {
|
|
console.error('[MaskEditor] Node has no images')
|
|
return
|
|
}
|
|
|
|
useMaskEditor().openMaskEditor(node)
|
|
}
|
|
|
|
// Open mask editor from clipspace (for plugin compatibility)
|
|
// This is called when ComfyApp.open_maskeditor() is invoked without arguments
|
|
function openMaskEditorFromClipspace(): void {
|
|
const node = ComfyApp.clipspace_return_node as LGraphNode | null
|
|
if (!node) {
|
|
console.error('[MaskEditor] No clipspace_return_node found')
|
|
return
|
|
}
|
|
|
|
openMaskEditor(node)
|
|
}
|
|
|
|
// Check if the dialog is already opened
|
|
function isOpened(): boolean {
|
|
return useDialogStore().isDialogOpen('global-mask-editor')
|
|
}
|
|
|
|
const changeBrushSize = async (sizeChanger: (oldSize: number) => number) => {
|
|
if (!isOpened()) return
|
|
|
|
const store = useMaskEditorStore()
|
|
const oldBrushSize = store.brushSettings.size
|
|
const newBrushSize = sizeChanger(oldBrushSize)
|
|
store.setBrushSize(newBrushSize)
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: 'Comfy.MaskEditor',
|
|
settings: [
|
|
{
|
|
id: 'Comfy.MaskEditor.BrushAdjustmentSpeed',
|
|
category: ['Mask Editor', 'BrushAdjustment', 'Sensitivity'],
|
|
name: 'Brush adjustment speed multiplier',
|
|
tooltip:
|
|
'Controls how quickly the brush size and hardness change when adjusting. Higher values mean faster changes.',
|
|
type: 'slider',
|
|
attrs: {
|
|
min: 0.1,
|
|
max: 2.0,
|
|
step: 0.1
|
|
},
|
|
defaultValue: 1.0,
|
|
versionAdded: '1.0.0'
|
|
},
|
|
{
|
|
id: 'Comfy.MaskEditor.UseDominantAxis',
|
|
category: ['Mask Editor', 'BrushAdjustment', 'UseDominantAxis'],
|
|
name: 'Lock brush adjustment to dominant axis',
|
|
tooltip:
|
|
'When enabled, brush adjustments will only affect size OR hardness based on which direction you move more',
|
|
type: 'boolean',
|
|
defaultValue: true
|
|
}
|
|
],
|
|
commands: [
|
|
{
|
|
id: 'Comfy.MaskEditor.OpenMaskEditor',
|
|
icon: 'pi pi-pencil',
|
|
label: 'Open Mask Editor for Selected Node',
|
|
function: () => {
|
|
const selectedNodes = app.canvas.selected_nodes
|
|
if (!selectedNodes || Object.keys(selectedNodes).length !== 1) return
|
|
|
|
const selectedNode = selectedNodes[Object.keys(selectedNodes)[0]]
|
|
openMaskEditor(selectedNode)
|
|
}
|
|
},
|
|
{
|
|
id: 'Comfy.MaskEditor.BrushSize.Increase',
|
|
icon: 'pi pi-plus-circle',
|
|
label: 'Increase Brush Size in MaskEditor',
|
|
function: () => changeBrushSize((old) => _.clamp(old + 2, 1, 250))
|
|
},
|
|
{
|
|
id: 'Comfy.MaskEditor.BrushSize.Decrease',
|
|
icon: 'pi pi-minus-circle',
|
|
label: 'Decrease Brush Size in MaskEditor',
|
|
function: () => changeBrushSize((old) => _.clamp(old - 2, 1, 250))
|
|
},
|
|
{
|
|
id: 'Comfy.MaskEditor.ColorPicker',
|
|
icon: 'pi pi-palette',
|
|
label: 'Open Color Picker in MaskEditor',
|
|
function: () => {
|
|
if (!isOpened()) return
|
|
|
|
const store = useMaskEditorStore()
|
|
store.colorInput?.click()
|
|
}
|
|
},
|
|
{
|
|
id: 'Comfy.MaskEditor.Rotate.Right',
|
|
icon: 'pi pi-refresh',
|
|
label: 'Rotate Right in MaskEditor',
|
|
function: async () => {
|
|
if (!isOpened()) return
|
|
await useCanvasTransform().rotateClockwise()
|
|
}
|
|
},
|
|
{
|
|
id: 'Comfy.MaskEditor.Rotate.Left',
|
|
icon: 'pi pi-undo',
|
|
label: 'Rotate Left in MaskEditor',
|
|
function: async () => {
|
|
if (!isOpened()) return
|
|
await useCanvasTransform().rotateCounterclockwise()
|
|
}
|
|
},
|
|
{
|
|
id: 'Comfy.MaskEditor.Mirror.Horizontal',
|
|
icon: 'pi pi-arrows-h',
|
|
label: 'Mirror Horizontal in MaskEditor',
|
|
function: async () => {
|
|
if (!isOpened()) return
|
|
await useCanvasTransform().mirrorHorizontal()
|
|
}
|
|
},
|
|
{
|
|
id: 'Comfy.MaskEditor.Mirror.Vertical',
|
|
icon: 'pi pi-arrows-v',
|
|
label: 'Mirror Vertical in MaskEditor',
|
|
function: async () => {
|
|
if (!isOpened()) return
|
|
await useCanvasTransform().mirrorVertical()
|
|
}
|
|
}
|
|
],
|
|
init() {
|
|
// Set up ComfyApp static methods for plugin compatibility (deprecated)
|
|
ComfyApp.open_maskeditor = openMaskEditorFromClipspace
|
|
|
|
console.warn(
|
|
'[MaskEditor] ComfyApp.open_maskeditor is deprecated. ' +
|
|
'Plugins should migrate to using the command system or direct node context menu integration.'
|
|
)
|
|
}
|
|
})
|