refactor: color handling in node customization and selection tools

This commit is contained in:
Rizumu Ayaka
2026-01-16 17:03:58 +08:00
parent 0288b02113
commit 39a35fd54a
7 changed files with 595 additions and 212 deletions

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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,

View 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)
})
})
})

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

View File

@@ -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,

View File

@@ -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,