mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 06:19:58 +00:00
refactor: color handling in node customization and selection tools
This commit is contained in:
@@ -28,7 +28,11 @@
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<i
|
||||
v-tooltip.top="option.localizedName"
|
||||
v-tooltip.top="
|
||||
typeof option.localizedName === 'function'
|
||||
? option.localizedName()
|
||||
: option.localizedName
|
||||
"
|
||||
class="pi pi-circle-fill"
|
||||
:style="{
|
||||
color: isLightTheme ? option.value.light : option.value.dark
|
||||
@@ -48,78 +52,38 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { NodeColorOption } from '@/composables/graph/useNodeColorOptions'
|
||||
import { useNodeColorOptions } from '@/composables/graph/useNodeColorOptions'
|
||||
import type { IColorable } from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
ColorOption as CanvasColorOption,
|
||||
Positionable
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LiteGraph,
|
||||
isColorable
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { isColorable } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import { getItemsColorOption } from '@/utils/litegraphUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
const toLightThemeColor = (color: string) =>
|
||||
adjustColor(color, { lightness: 0.5 })
|
||||
|
||||
const { colorOptions, NO_COLOR_OPTION, applyColorToItems, isLightTheme } =
|
||||
useNodeColorOptions()
|
||||
|
||||
const showColorPicker = ref(false)
|
||||
|
||||
type ColorOption = {
|
||||
name: string
|
||||
localizedName: string
|
||||
value: {
|
||||
dark: string
|
||||
light: string
|
||||
}
|
||||
}
|
||||
const selectedColorOption = ref<NodeColorOption | null>(null)
|
||||
const applyColor = (colorOption: NodeColorOption | null) => {
|
||||
const colorName = colorOption?.name ?? NO_COLOR_OPTION.value.name
|
||||
|
||||
const NO_COLOR_OPTION: ColorOption = {
|
||||
name: 'noColor',
|
||||
localizedName: t('color.noColor'),
|
||||
value: {
|
||||
dark: LiteGraph.NODE_DEFAULT_BGCOLOR,
|
||||
light: toLightThemeColor(LiteGraph.NODE_DEFAULT_BGCOLOR)
|
||||
}
|
||||
}
|
||||
const colorOptions: ColorOption[] = [
|
||||
NO_COLOR_OPTION,
|
||||
...Object.entries(LGraphCanvas.node_colors).map(([name, color]) => ({
|
||||
name,
|
||||
localizedName: t(`color.${name}`),
|
||||
value: {
|
||||
dark: color.bgcolor,
|
||||
light: toLightThemeColor(color.bgcolor)
|
||||
}
|
||||
}))
|
||||
]
|
||||
|
||||
const selectedColorOption = ref<ColorOption | null>(null)
|
||||
const applyColor = (colorOption: ColorOption | null) => {
|
||||
const colorName = colorOption?.name ?? NO_COLOR_OPTION.name
|
||||
const canvasColorOption =
|
||||
colorName === NO_COLOR_OPTION.name
|
||||
? null
|
||||
: LGraphCanvas.node_colors[colorName]
|
||||
|
||||
for (const item of canvasStore.selectedItems) {
|
||||
if (isColorable(item)) {
|
||||
item.setColorOption(canvasColorOption)
|
||||
}
|
||||
}
|
||||
const colorableItems = canvasStore.selectedItems
|
||||
.filter(isColorable)
|
||||
.map((item) => item as unknown as IColorable)
|
||||
applyColorToItems(colorableItems, colorName)
|
||||
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
currentColorOption.value = canvasColorOption
|
||||
currentColorOption.value = getItemsColorOption(canvasStore.selectedItems)
|
||||
showColorPicker.value = false
|
||||
workflowStore.activeWorkflow?.changeTracker.checkState()
|
||||
}
|
||||
@@ -128,20 +92,24 @@ const currentColorOption = ref<CanvasColorOption | null>(null)
|
||||
const currentColor = computed(() =>
|
||||
currentColorOption.value
|
||||
? isLightTheme.value
|
||||
? toLightThemeColor(currentColorOption.value?.bgcolor)
|
||||
? colorOptions.value.find(
|
||||
(option) => option.value.dark === currentColorOption.value?.bgcolor
|
||||
)?.value.light
|
||||
: currentColorOption.value?.bgcolor
|
||||
: null
|
||||
)
|
||||
|
||||
const localizedCurrentColorName = computed(() => {
|
||||
if (!currentColorOption.value?.bgcolor) return null
|
||||
const colorOption = colorOptions.find(
|
||||
const colorOption = colorOptions.value.find(
|
||||
(option) =>
|
||||
option.value.dark === currentColorOption.value?.bgcolor ||
|
||||
option.value.light === currentColorOption.value?.bgcolor
|
||||
)
|
||||
return colorOption?.localizedName ?? NO_COLOR_OPTION.localizedName
|
||||
const name = colorOption?.localizedName ?? NO_COLOR_OPTION.value.localizedName
|
||||
return typeof name === 'function' ? name() : name
|
||||
})
|
||||
|
||||
const updateColorSelectionFromNode = (
|
||||
newSelectedItems: Raw<Positionable[]>
|
||||
) => {
|
||||
@@ -149,6 +117,7 @@ const updateColorSelectionFromNode = (
|
||||
selectedColorOption.value = null
|
||||
currentColorOption.value = getItemsColorOption(newSelectedItems)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => canvasStore.selectedItems,
|
||||
(newSelectedItems) => {
|
||||
|
||||
@@ -2,103 +2,37 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ColorOption } from '@/lib/litegraph/src/litegraph'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import { useNodeColorOptions } from '@/composables/graph/useNodeColorOptions'
|
||||
import type { IColorable } from '@/lib/litegraph/src/interfaces'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import LayoutField from './LayoutField.vue'
|
||||
|
||||
/**
|
||||
* Good design limits dependencies and simplifies the interface of the abstraction layer.
|
||||
* Here, we only care about the getColorOption and setColorOption methods,
|
||||
* and do not concern ourselves with other methods.
|
||||
*/
|
||||
type PickedNode = Pick<LGraphNode, 'getColorOption' | 'setColorOption'>
|
||||
|
||||
const { nodes } = defineProps<{ nodes: PickedNode[] }>()
|
||||
const { nodes } = defineProps<{ nodes: IColorable[] }>()
|
||||
const emit = defineEmits<{ (e: 'changed'): void }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const { colorOptions, applyColorToItems, getCurrentColorName, isLightTheme } =
|
||||
useNodeColorOptions({
|
||||
includeRingColors: true,
|
||||
lightnessAdjustments: {
|
||||
dark: 0.3,
|
||||
light: 0.4,
|
||||
ringDark: 0.5,
|
||||
ringLight: 0.1
|
||||
},
|
||||
localizedNameAsFunction: true
|
||||
})
|
||||
|
||||
type NodeColorOption = {
|
||||
name: string
|
||||
localizedName: () => string
|
||||
value: {
|
||||
dark: string
|
||||
light: string
|
||||
ringDark: string
|
||||
ringLight: string
|
||||
}
|
||||
}
|
||||
|
||||
const nodeColorEntries = Object.entries(LGraphCanvas.node_colors)
|
||||
|
||||
function getColorValue(color: string): NodeColorOption['value'] {
|
||||
return {
|
||||
dark: adjustColor(color, { lightness: 0.3 }),
|
||||
light: adjustColor(color, { lightness: 0.4 }),
|
||||
ringDark: adjustColor(color, { lightness: 0.5 }),
|
||||
ringLight: adjustColor(color, { lightness: 0.1 })
|
||||
}
|
||||
}
|
||||
|
||||
const NO_COLOR_OPTION: NodeColorOption = {
|
||||
name: 'noColor',
|
||||
localizedName: () => t('color.noColor'),
|
||||
value: getColorValue(LiteGraph.NODE_DEFAULT_BGCOLOR)
|
||||
}
|
||||
|
||||
const colorOptions: NodeColorOption[] = [
|
||||
NO_COLOR_OPTION,
|
||||
...nodeColorEntries.map(([name, color]) => ({
|
||||
name,
|
||||
localizedName: () => t(`color.${name}`),
|
||||
value: getColorValue(color.bgcolor)
|
||||
}))
|
||||
]
|
||||
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
|
||||
const nodeColor = computed<NodeColorOption['name'] | null>({
|
||||
const nodeColor = computed<string | null>({
|
||||
get() {
|
||||
if (nodes.length === 0) return null
|
||||
const theColorOptions = nodes.map((item) => item.getColorOption())
|
||||
|
||||
let colorOption: ColorOption | null | false = theColorOptions[0]
|
||||
if (!theColorOptions.every((option) => option === colorOption)) {
|
||||
colorOption = false
|
||||
}
|
||||
|
||||
if (colorOption === false) return null
|
||||
if (colorOption == null || (!colorOption.bgcolor && !colorOption.color))
|
||||
return NO_COLOR_OPTION.name
|
||||
return (
|
||||
nodeColorEntries.find(
|
||||
([_, color]) =>
|
||||
color.bgcolor === colorOption.bgcolor &&
|
||||
color.color === colorOption.color
|
||||
)?.[0] ?? null
|
||||
)
|
||||
return getCurrentColorName(nodes)
|
||||
},
|
||||
set(colorName) {
|
||||
if (colorName === null) return
|
||||
|
||||
const canvasColorOption =
|
||||
colorName === NO_COLOR_OPTION.name
|
||||
? null
|
||||
: LGraphCanvas.node_colors[colorName]
|
||||
|
||||
for (const item of nodes) {
|
||||
item.setColorOption(canvasColorOption)
|
||||
}
|
||||
|
||||
applyColorToItems(nodes, colorName)
|
||||
emit('changed')
|
||||
}
|
||||
})
|
||||
@@ -123,7 +57,11 @@ const nodeColor = computed<NodeColorOption['name'] | null>({
|
||||
@click="nodeColor = option.name"
|
||||
>
|
||||
<div
|
||||
v-tooltip.top="option.localizedName()"
|
||||
v-tooltip.top="
|
||||
typeof option.localizedName === 'function'
|
||||
? option.localizedName()
|
||||
: option.localizedName
|
||||
"
|
||||
:class="cn('size-4 rounded-full ring-2 ring-gray-500/10')"
|
||||
:style="{
|
||||
backgroundColor: isLightTheme
|
||||
|
||||
@@ -65,8 +65,11 @@ export function useGroupMenuOptions() {
|
||||
label: t('contextMenu.Color'),
|
||||
icon: 'icon-[lucide--palette]',
|
||||
hasSubmenu: true,
|
||||
submenu: colorOptions.map((colorOption) => ({
|
||||
label: colorOption.localizedName,
|
||||
submenu: colorOptions.value.map((colorOption) => ({
|
||||
label:
|
||||
typeof colorOption.localizedName === 'function'
|
||||
? colorOption.localizedName()
|
||||
: colorOption.localizedName,
|
||||
color: isLightTheme.value
|
||||
? colorOption.value.light
|
||||
: colorOption.value.dark,
|
||||
|
||||
269
src/composables/graph/useNodeColorOptions.test.ts
Normal file
269
src/composables/graph/useNodeColorOptions.test.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import type * as VueI18n from 'vue-i18n'
|
||||
|
||||
import { useNodeColorOptions } from '@/composables/graph/useNodeColorOptions'
|
||||
import type { IColorable } from '@/lib/litegraph/src/interfaces'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof VueI18n>()
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
useColorPaletteStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/colorUtil', () => ({
|
||||
adjustColor: (color: string, options: { lightness?: number }) => {
|
||||
const lightness = options.lightness ?? 0
|
||||
return `adjusted(${color}, ${lightness})`
|
||||
}
|
||||
}))
|
||||
|
||||
const mockColorPaletteStore = {
|
||||
completedActivePalette: {
|
||||
light_theme: false
|
||||
},
|
||||
$id: 'colorPalette',
|
||||
$state: {} as any,
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$onAction: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
_customProperties: new Set(),
|
||||
_p: {} as any
|
||||
}
|
||||
|
||||
describe('useNodeColorOptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue(
|
||||
mockColorPaletteStore as any
|
||||
)
|
||||
|
||||
// Mock LGraphCanvas.node_colors
|
||||
vi.spyOn(LGraphCanvas, 'node_colors', 'get').mockReturnValue({
|
||||
red: { bgcolor: '#ff0000', color: '#ffffff' },
|
||||
blue: { bgcolor: '#0000ff', color: '#ffffff' }
|
||||
} as any)
|
||||
|
||||
// Mock LiteGraph.NODE_DEFAULT_BGCOLOR
|
||||
vi.spyOn(LiteGraph, 'NODE_DEFAULT_BGCOLOR', 'get').mockReturnValue(
|
||||
'#999999'
|
||||
)
|
||||
})
|
||||
|
||||
describe('Basic Configuration', () => {
|
||||
test('should generate color options with default config', () => {
|
||||
const { colorOptions } = useNodeColorOptions()
|
||||
|
||||
expect(colorOptions.value).toHaveLength(3) // NO_COLOR + red + blue
|
||||
expect(colorOptions.value[0].name).toBe('noColor')
|
||||
expect(colorOptions.value[1].name).toBe('red')
|
||||
expect(colorOptions.value[2].name).toBe('blue')
|
||||
})
|
||||
|
||||
test('should include ring colors when configured', () => {
|
||||
const { colorOptions } = useNodeColorOptions({
|
||||
includeRingColors: true
|
||||
})
|
||||
|
||||
const firstOption = colorOptions.value[0]
|
||||
expect(firstOption.value).toHaveProperty('dark')
|
||||
expect(firstOption.value).toHaveProperty('light')
|
||||
expect(firstOption.value).toHaveProperty('ringDark')
|
||||
expect(firstOption.value).toHaveProperty('ringLight')
|
||||
})
|
||||
|
||||
test('should not include ring colors by default', () => {
|
||||
const { colorOptions } = useNodeColorOptions()
|
||||
|
||||
const firstOption = colorOptions.value[0]
|
||||
expect(firstOption.value).toHaveProperty('dark')
|
||||
expect(firstOption.value).toHaveProperty('light')
|
||||
expect(firstOption.value).not.toHaveProperty('ringDark')
|
||||
expect(firstOption.value).not.toHaveProperty('ringLight')
|
||||
})
|
||||
|
||||
test('should use localizedName as function when configured', () => {
|
||||
const { colorOptions } = useNodeColorOptions({
|
||||
localizedNameAsFunction: true
|
||||
})
|
||||
|
||||
const firstOption = colorOptions.value[0]
|
||||
expect(typeof firstOption.localizedName).toBe('function')
|
||||
if (typeof firstOption.localizedName === 'function') {
|
||||
expect(firstOption.localizedName()).toBe('color.noColor')
|
||||
}
|
||||
})
|
||||
|
||||
test('should use localizedName as string by default', () => {
|
||||
const { colorOptions } = useNodeColorOptions()
|
||||
|
||||
const firstOption = colorOptions.value[0]
|
||||
expect(typeof firstOption.localizedName).toBe('string')
|
||||
expect(firstOption.localizedName).toBe('color.noColor')
|
||||
})
|
||||
})
|
||||
|
||||
describe('NO_COLOR_OPTION', () => {
|
||||
test('should provide NO_COLOR_OPTION', () => {
|
||||
const { NO_COLOR_OPTION } = useNodeColorOptions()
|
||||
|
||||
expect(NO_COLOR_OPTION.value.name).toBe('noColor')
|
||||
expect(NO_COLOR_OPTION.value.localizedName).toBe('color.noColor')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getColorValue', () => {
|
||||
test('should generate color variants with default adjustments', () => {
|
||||
const { getColorValue } = useNodeColorOptions()
|
||||
|
||||
const variants = getColorValue('#ff0000')
|
||||
|
||||
expect(variants.dark).toBe('adjusted(#ff0000, 0)')
|
||||
expect(variants.light).toBe('adjusted(#ff0000, 0.5)')
|
||||
})
|
||||
|
||||
test('should generate color variants with custom adjustments', () => {
|
||||
const { getColorValue } = useNodeColorOptions({
|
||||
lightnessAdjustments: {
|
||||
dark: 0.3,
|
||||
light: 0.4
|
||||
}
|
||||
})
|
||||
|
||||
const variants = getColorValue('#ff0000')
|
||||
|
||||
expect(variants.dark).toBe('adjusted(#ff0000, 0.3)')
|
||||
expect(variants.light).toBe('adjusted(#ff0000, 0.4)')
|
||||
})
|
||||
|
||||
test('should include ring colors when configured', () => {
|
||||
const { getColorValue } = useNodeColorOptions({
|
||||
includeRingColors: true,
|
||||
lightnessAdjustments: {
|
||||
ringDark: 0.5,
|
||||
ringLight: 0.1
|
||||
}
|
||||
})
|
||||
|
||||
const variants = getColorValue('#ff0000')
|
||||
|
||||
expect(variants.ringDark).toBe('adjusted(#ff0000, 0.5)')
|
||||
expect(variants.ringLight).toBe('adjusted(#ff0000, 0.1)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyColorToItems', () => {
|
||||
test('should apply color to items', () => {
|
||||
const { applyColorToItems } = useNodeColorOptions()
|
||||
|
||||
const mockItem1 = {
|
||||
setColorOption: vi.fn()
|
||||
} as unknown as IColorable
|
||||
|
||||
const mockItem2 = {
|
||||
setColorOption: vi.fn()
|
||||
} as unknown as IColorable
|
||||
|
||||
applyColorToItems([mockItem1, mockItem2], 'red')
|
||||
|
||||
expect(mockItem1.setColorOption).toHaveBeenCalledWith({
|
||||
bgcolor: '#ff0000',
|
||||
color: '#ffffff'
|
||||
})
|
||||
expect(mockItem2.setColorOption).toHaveBeenCalledWith({
|
||||
bgcolor: '#ff0000',
|
||||
color: '#ffffff'
|
||||
})
|
||||
})
|
||||
|
||||
test('should reset color when noColor is selected', () => {
|
||||
const { applyColorToItems } = useNodeColorOptions()
|
||||
|
||||
const mockItem = {
|
||||
setColorOption: vi.fn()
|
||||
} as unknown as IColorable
|
||||
|
||||
applyColorToItems([mockItem], 'noColor')
|
||||
|
||||
expect(mockItem.setColorOption).toHaveBeenCalledWith(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCurrentColorName', () => {
|
||||
test('should return null for empty items', () => {
|
||||
const { getCurrentColorName } = useNodeColorOptions()
|
||||
|
||||
expect(getCurrentColorName([])).toBe(null)
|
||||
})
|
||||
|
||||
test('should return noColor for items with no color', () => {
|
||||
const { getCurrentColorName } = useNodeColorOptions()
|
||||
|
||||
const mockItem = {
|
||||
getColorOption: vi.fn(() => null)
|
||||
} as unknown as IColorable
|
||||
|
||||
expect(getCurrentColorName([mockItem])).toBe('noColor')
|
||||
})
|
||||
|
||||
test('should return color name for items with matching color', () => {
|
||||
const { getCurrentColorName } = useNodeColorOptions()
|
||||
|
||||
const mockItem = {
|
||||
getColorOption: vi.fn(() => ({
|
||||
bgcolor: '#ff0000',
|
||||
color: '#ffffff'
|
||||
}))
|
||||
} as unknown as IColorable
|
||||
|
||||
expect(getCurrentColorName([mockItem])).toBe('red')
|
||||
})
|
||||
|
||||
test('should return null when items have different colors', () => {
|
||||
const { getCurrentColorName } = useNodeColorOptions()
|
||||
|
||||
const mockItem1 = {
|
||||
getColorOption: vi.fn(() => ({
|
||||
bgcolor: '#ff0000',
|
||||
color: '#ffffff'
|
||||
}))
|
||||
} as unknown as IColorable
|
||||
|
||||
const mockItem2 = {
|
||||
getColorOption: vi.fn(() => ({
|
||||
bgcolor: '#0000ff',
|
||||
color: '#ffffff'
|
||||
}))
|
||||
} as unknown as IColorable
|
||||
|
||||
expect(getCurrentColorName([mockItem1, mockItem2])).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isLightTheme', () => {
|
||||
test('should reflect color palette store light theme setting', () => {
|
||||
mockColorPaletteStore.completedActivePalette.light_theme = false
|
||||
const { isLightTheme } = useNodeColorOptions()
|
||||
|
||||
expect(isLightTheme.value).toBe(false)
|
||||
|
||||
mockColorPaletteStore.completedActivePalette.light_theme = true
|
||||
const { isLightTheme: isLightTheme2 } = useNodeColorOptions()
|
||||
|
||||
expect(isLightTheme2.value).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
240
src/composables/graph/useNodeColorOptions.ts
Normal file
240
src/composables/graph/useNodeColorOptions.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ColorOption as CanvasColorOption } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IColorable } from '@/lib/litegraph/src/interfaces'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
/**
|
||||
* Color variants for different themes and display purposes
|
||||
*/
|
||||
interface ColorVariants {
|
||||
dark: string
|
||||
light: string
|
||||
ringDark?: string
|
||||
ringLight?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Node color option with localized name and theme variants
|
||||
*/
|
||||
export interface NodeColorOption {
|
||||
name: string
|
||||
localizedName: string | (() => string)
|
||||
value: ColorVariants
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for useNodeColorOptions composable
|
||||
*/
|
||||
export interface UseNodeColorOptionsConfig {
|
||||
/**
|
||||
* Whether to include ring color variants for UI elements like borders
|
||||
* @default false
|
||||
*/
|
||||
includeRingColors?: boolean
|
||||
|
||||
/**
|
||||
* Lightness adjustments for color variants
|
||||
* @default { dark: 0, light: 0.5, ringDark: 0.5, ringLight: 0.1 }
|
||||
*/
|
||||
lightnessAdjustments?: {
|
||||
dark?: number
|
||||
light?: number
|
||||
ringDark?: number
|
||||
ringLight?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether localizedName should be a function instead of a string
|
||||
* Useful when you need reactive i18n updates
|
||||
* @default false
|
||||
*/
|
||||
localizedNameAsFunction?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type for useNodeColorOptions composable
|
||||
*/
|
||||
export interface UseNodeColorOptionsReturn {
|
||||
colorOptions: ComputedRef<NodeColorOption[]>
|
||||
NO_COLOR_OPTION: ComputedRef<NodeColorOption>
|
||||
getColorValue: (color: string) => ColorVariants
|
||||
applyColorToItems: (items: IColorable[], colorName: string) => void
|
||||
getCurrentColorName: (items: IColorable[]) => string | null
|
||||
isLightTheme: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing node color options with flexible configuration.
|
||||
* Consolidates color picker logic across SetNodeColor, ColorPickerButton, and useNodeCustomization.
|
||||
*
|
||||
* @param config - Configuration options for color variants and localization
|
||||
* @returns Color options, helper functions, and theme information
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage (2 color variants)
|
||||
* const { colorOptions, applyColorToItems } = useNodeColorOptions()
|
||||
*
|
||||
* // With ring colors (4 color variants)
|
||||
* const { colorOptions, NO_COLOR_OPTION } = useNodeColorOptions({
|
||||
* includeRingColors: true,
|
||||
* lightnessAdjustments: { dark: 0.3, light: 0.4, ringDark: 0.5, ringLight: 0.1 }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function useNodeColorOptions(
|
||||
config: UseNodeColorOptionsConfig = {}
|
||||
): UseNodeColorOptionsReturn {
|
||||
const {
|
||||
includeRingColors = false,
|
||||
lightnessAdjustments = {},
|
||||
localizedNameAsFunction = false
|
||||
} = config
|
||||
|
||||
const { t } = useI18n()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const isLightTheme = computed<boolean>(() =>
|
||||
Boolean(colorPaletteStore.completedActivePalette.light_theme)
|
||||
)
|
||||
|
||||
// Default lightness adjustments
|
||||
const defaultAdjustments = {
|
||||
dark: 0,
|
||||
light: 0.5,
|
||||
ringDark: 0.5,
|
||||
ringLight: 0.1
|
||||
}
|
||||
|
||||
const adjustments = {
|
||||
...defaultAdjustments,
|
||||
...lightnessAdjustments
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate color variants for a given base color
|
||||
*/
|
||||
const getColorValue = (color: string): ColorVariants => {
|
||||
const variants: ColorVariants = {
|
||||
dark: adjustColor(color, { lightness: adjustments.dark }),
|
||||
light: adjustColor(color, { lightness: adjustments.light })
|
||||
}
|
||||
|
||||
if (includeRingColors) {
|
||||
variants.ringDark = adjustColor(color, {
|
||||
lightness: adjustments.ringDark
|
||||
})
|
||||
variants.ringLight = adjustColor(color, {
|
||||
lightness: adjustments.ringLight
|
||||
})
|
||||
}
|
||||
|
||||
return variants
|
||||
}
|
||||
|
||||
const nodeColorEntries = Object.entries(LGraphCanvas.node_colors)
|
||||
|
||||
/**
|
||||
* The "no color" option that resets nodes to default color
|
||||
*/
|
||||
const NO_COLOR_OPTION = computed<NodeColorOption>(() => {
|
||||
const localizedName = localizedNameAsFunction
|
||||
? () => t('color.noColor')
|
||||
: t('color.noColor')
|
||||
|
||||
return {
|
||||
name: 'noColor',
|
||||
localizedName,
|
||||
value: getColorValue(LiteGraph.NODE_DEFAULT_BGCOLOR)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* All available color options including the "no color" option
|
||||
*/
|
||||
const colorOptions = computed<NodeColorOption[]>(() => {
|
||||
const options: NodeColorOption[] = [
|
||||
NO_COLOR_OPTION.value,
|
||||
...nodeColorEntries.map(([name, color]) => {
|
||||
const localizedName = localizedNameAsFunction
|
||||
? () => t(`color.${name}`)
|
||||
: t(`color.${name}`)
|
||||
|
||||
return {
|
||||
name,
|
||||
localizedName,
|
||||
value: getColorValue(color.bgcolor)
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
/**
|
||||
* Apply a color to multiple colorable items
|
||||
*
|
||||
* @param items - Items that implement IColorable interface
|
||||
* @param colorName - Name of the color to apply (or 'noColor' to reset)
|
||||
*/
|
||||
const applyColorToItems = (items: IColorable[], colorName: string): void => {
|
||||
const canvasColorOption =
|
||||
colorName === NO_COLOR_OPTION.value.name
|
||||
? null
|
||||
: LGraphCanvas.node_colors[colorName]
|
||||
|
||||
for (const item of items) {
|
||||
item.setColorOption(canvasColorOption)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current color name from a list of items
|
||||
* Returns null if items have different colors or no color is set
|
||||
*
|
||||
* @param items - Items to check color from
|
||||
* @returns Color name or null
|
||||
*/
|
||||
const getCurrentColorName = (items: IColorable[]): string | null => {
|
||||
if (items.length === 0) return null
|
||||
|
||||
const colorOptions = items.map((item) => item.getColorOption())
|
||||
|
||||
// Check if all items have the same color
|
||||
let colorOption: CanvasColorOption | null | false = colorOptions[0]
|
||||
if (!colorOptions.every((option) => option === colorOption)) {
|
||||
colorOption = false
|
||||
}
|
||||
|
||||
// Different colors
|
||||
if (colorOption === false) return null
|
||||
|
||||
// No color or default color
|
||||
if (colorOption == null || (!colorOption.bgcolor && !colorOption.color)) {
|
||||
return NO_COLOR_OPTION.value.name
|
||||
}
|
||||
|
||||
// Find matching color name
|
||||
return (
|
||||
nodeColorEntries.find(
|
||||
([_, color]) =>
|
||||
color.bgcolor === colorOption.bgcolor &&
|
||||
color.color === colorOption.color
|
||||
)?.[0] ?? null
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
colorOptions,
|
||||
NO_COLOR_OPTION,
|
||||
getColorValue,
|
||||
applyColorToItems,
|
||||
getCurrentColorName,
|
||||
isLightTheme
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,18 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useNodeColorOptions } from '@/composables/graph/useNodeColorOptions'
|
||||
import type { NodeColorOption } from '@/composables/graph/useNodeColorOptions'
|
||||
import type { IColorable } from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
RenderShape,
|
||||
isColorable
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
import { useCanvasRefresh } from './useCanvasRefresh'
|
||||
|
||||
interface ColorOption {
|
||||
name: string
|
||||
localizedName: string
|
||||
value: {
|
||||
dark: string
|
||||
light: string
|
||||
}
|
||||
}
|
||||
|
||||
interface ShapeOption {
|
||||
name: string
|
||||
localizedName: string
|
||||
@@ -35,36 +25,16 @@ interface ShapeOption {
|
||||
export function useNodeCustomization() {
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const canvasRefresh = useCanvasRefresh()
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
|
||||
const toLightThemeColor = (color: string) =>
|
||||
adjustColor(color, { lightness: 0.5 })
|
||||
|
||||
// Color options
|
||||
const NO_COLOR_OPTION: ColorOption = {
|
||||
name: 'noColor',
|
||||
localizedName: t('color.noColor'),
|
||||
value: {
|
||||
dark: LiteGraph.NODE_DEFAULT_BGCOLOR,
|
||||
light: toLightThemeColor(LiteGraph.NODE_DEFAULT_BGCOLOR)
|
||||
}
|
||||
}
|
||||
|
||||
const colorOptions: ColorOption[] = [
|
||||
// Use shared color options logic
|
||||
const {
|
||||
colorOptions,
|
||||
NO_COLOR_OPTION,
|
||||
...Object.entries(LGraphCanvas.node_colors).map(([name, color]) => ({
|
||||
name,
|
||||
localizedName: t(`color.${name}`),
|
||||
value: {
|
||||
dark: color.bgcolor,
|
||||
light: toLightThemeColor(color.bgcolor)
|
||||
}
|
||||
}))
|
||||
]
|
||||
applyColorToItems,
|
||||
getCurrentColorName,
|
||||
isLightTheme
|
||||
} = useNodeColorOptions()
|
||||
|
||||
// Shape options
|
||||
const shapeOptions: ShapeOption[] = [
|
||||
@@ -85,18 +55,13 @@ export function useNodeCustomization() {
|
||||
}
|
||||
]
|
||||
|
||||
const applyColor = (colorOption: ColorOption | null) => {
|
||||
const colorName = colorOption?.name ?? NO_COLOR_OPTION.name
|
||||
const canvasColorOption =
|
||||
colorName === NO_COLOR_OPTION.name
|
||||
? null
|
||||
: LGraphCanvas.node_colors[colorName]
|
||||
const applyColor = (colorOption: NodeColorOption | null) => {
|
||||
const colorName = colorOption?.name ?? NO_COLOR_OPTION.value.name
|
||||
|
||||
for (const item of canvasStore.selectedItems) {
|
||||
if (isColorable(item)) {
|
||||
item.setColorOption(canvasColorOption)
|
||||
}
|
||||
}
|
||||
const colorableItems = Array.from(canvasStore.selectedItems)
|
||||
.filter(isColorable)
|
||||
.map((item) => item as unknown as IColorable)
|
||||
applyColorToItems(colorableItems, colorName)
|
||||
|
||||
canvasRefresh.refreshCanvas()
|
||||
}
|
||||
@@ -117,25 +82,21 @@ export function useNodeCustomization() {
|
||||
canvasRefresh.refreshCanvas()
|
||||
}
|
||||
|
||||
const getCurrentColor = (): ColorOption | null => {
|
||||
const getCurrentColor = (): NodeColorOption | null => {
|
||||
const selectedItems = Array.from(canvasStore.selectedItems)
|
||||
if (selectedItems.length === 0) return null
|
||||
|
||||
// Get color from first colorable item
|
||||
const firstColorableItem = selectedItems.find((item) => isColorable(item))
|
||||
if (!firstColorableItem || !isColorable(firstColorableItem)) return null
|
||||
const colorableItems = selectedItems
|
||||
.filter(isColorable)
|
||||
.map((item) => item as unknown as IColorable)
|
||||
if (colorableItems.length === 0) return null
|
||||
|
||||
// Get the current color option from the colorable item
|
||||
const currentColorOption = firstColorableItem.getColorOption()
|
||||
const currentBgColor = currentColorOption?.bgcolor ?? null
|
||||
const currentColorName = getCurrentColorName(colorableItems)
|
||||
if (!currentColorName) return null
|
||||
|
||||
// Find matching color option
|
||||
return (
|
||||
colorOptions.find(
|
||||
(option) =>
|
||||
option.value.dark === currentBgColor ||
|
||||
option.value.light === currentBgColor
|
||||
) ?? NO_COLOR_OPTION
|
||||
colorOptions.value.find((option) => option.name === currentColorName) ??
|
||||
NO_COLOR_OPTION.value
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,7 +117,7 @@ export function useNodeCustomization() {
|
||||
}
|
||||
|
||||
return {
|
||||
colorOptions,
|
||||
colorOptions: computed(() => colorOptions.value),
|
||||
shapeOptions,
|
||||
applyColor,
|
||||
applyShape,
|
||||
|
||||
@@ -29,8 +29,11 @@ export function useNodeMenuOptions() {
|
||||
)
|
||||
|
||||
const colorSubmenu = computed(() => {
|
||||
return colorOptions.map((colorOption) => ({
|
||||
label: colorOption.localizedName,
|
||||
return colorOptions.value.map((colorOption) => ({
|
||||
label:
|
||||
typeof colorOption.localizedName === 'function'
|
||||
? colorOption.localizedName()
|
||||
: colorOption.localizedName,
|
||||
color: isLightTheme.value
|
||||
? colorOption.value.light
|
||||
: colorOption.value.dark,
|
||||
|
||||
Reference in New Issue
Block a user