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 }"> <template #option="{ option }">
<i <i
v-tooltip.top="option.localizedName" v-tooltip.top="
typeof option.localizedName === 'function'
? option.localizedName()
: option.localizedName
"
class="pi pi-circle-fill" class="pi pi-circle-fill"
:style="{ :style="{
color: isLightTheme ? option.value.light : option.value.dark color: isLightTheme ? option.value.light : option.value.dark
@@ -48,78 +52,38 @@ import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue' 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 { import type {
ColorOption as CanvasColorOption, ColorOption as CanvasColorOption,
Positionable Positionable
} from '@/lib/litegraph/src/litegraph' } from '@/lib/litegraph/src/litegraph'
import { import { isColorable } from '@/lib/litegraph/src/litegraph'
LGraphCanvas,
LiteGraph,
isColorable
} from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
import { getItemsColorOption } from '@/utils/litegraphUtil' import { getItemsColorOption } from '@/utils/litegraphUtil'
const { t } = useI18n() const { t } = useI18n()
const canvasStore = useCanvasStore() const canvasStore = useCanvasStore()
const colorPaletteStore = useColorPaletteStore()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const isLightTheme = computed(
() => colorPaletteStore.completedActivePalette.light_theme const { colorOptions, NO_COLOR_OPTION, applyColorToItems, isLightTheme } =
) useNodeColorOptions()
const toLightThemeColor = (color: string) =>
adjustColor(color, { lightness: 0.5 })
const showColorPicker = ref(false) const showColorPicker = ref(false)
type ColorOption = { const selectedColorOption = ref<NodeColorOption | null>(null)
name: string const applyColor = (colorOption: NodeColorOption | null) => {
localizedName: string const colorName = colorOption?.name ?? NO_COLOR_OPTION.value.name
value: {
dark: string
light: string
}
}
const NO_COLOR_OPTION: ColorOption = { const colorableItems = canvasStore.selectedItems
name: 'noColor', .filter(isColorable)
localizedName: t('color.noColor'), .map((item) => item as unknown as IColorable)
value: { applyColorToItems(colorableItems, colorName)
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)
}
}
canvasStore.canvas?.setDirty(true, true) canvasStore.canvas?.setDirty(true, true)
currentColorOption.value = canvasColorOption currentColorOption.value = getItemsColorOption(canvasStore.selectedItems)
showColorPicker.value = false showColorPicker.value = false
workflowStore.activeWorkflow?.changeTracker.checkState() workflowStore.activeWorkflow?.changeTracker.checkState()
} }
@@ -128,20 +92,24 @@ const currentColorOption = ref<CanvasColorOption | null>(null)
const currentColor = computed(() => const currentColor = computed(() =>
currentColorOption.value currentColorOption.value
? isLightTheme.value ? isLightTheme.value
? toLightThemeColor(currentColorOption.value?.bgcolor) ? colorOptions.value.find(
(option) => option.value.dark === currentColorOption.value?.bgcolor
)?.value.light
: currentColorOption.value?.bgcolor : currentColorOption.value?.bgcolor
: null : null
) )
const localizedCurrentColorName = computed(() => { const localizedCurrentColorName = computed(() => {
if (!currentColorOption.value?.bgcolor) return null if (!currentColorOption.value?.bgcolor) return null
const colorOption = colorOptions.find( const colorOption = colorOptions.value.find(
(option) => (option) =>
option.value.dark === currentColorOption.value?.bgcolor || option.value.dark === currentColorOption.value?.bgcolor ||
option.value.light === 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 = ( const updateColorSelectionFromNode = (
newSelectedItems: Raw<Positionable[]> newSelectedItems: Raw<Positionable[]>
) => { ) => {
@@ -149,6 +117,7 @@ const updateColorSelectionFromNode = (
selectedColorOption.value = null selectedColorOption.value = null
currentColorOption.value = getItemsColorOption(newSelectedItems) currentColorOption.value = getItemsColorOption(newSelectedItems)
} }
watch( watch(
() => canvasStore.selectedItems, () => canvasStore.selectedItems,
(newSelectedItems) => { (newSelectedItems) => {

View File

@@ -2,103 +2,37 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { useNodeColorOptions } from '@/composables/graph/useNodeColorOptions'
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph' import type { IColorable } from '@/lib/litegraph/src/interfaces'
import type { ColorOption } from '@/lib/litegraph/src/litegraph'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
import LayoutField from './LayoutField.vue' import LayoutField from './LayoutField.vue'
/** const { nodes } = defineProps<{ nodes: IColorable[] }>()
* 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 emit = defineEmits<{ (e: 'changed'): void }>() const emit = defineEmits<{ (e: 'changed'): void }>()
const { t } = useI18n() 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 = { const nodeColor = computed<string | null>({
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>({
get() { get() {
if (nodes.length === 0) return null return getCurrentColorName(nodes)
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
)
}, },
set(colorName) { set(colorName) {
if (colorName === null) return if (colorName === null) return
const canvasColorOption = applyColorToItems(nodes, colorName)
colorName === NO_COLOR_OPTION.name
? null
: LGraphCanvas.node_colors[colorName]
for (const item of nodes) {
item.setColorOption(canvasColorOption)
}
emit('changed') emit('changed')
} }
}) })
@@ -123,7 +57,11 @@ const nodeColor = computed<NodeColorOption['name'] | null>({
@click="nodeColor = option.name" @click="nodeColor = option.name"
> >
<div <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')" :class="cn('size-4 rounded-full ring-2 ring-gray-500/10')"
:style="{ :style="{
backgroundColor: isLightTheme backgroundColor: isLightTheme

View File

@@ -65,8 +65,11 @@ export function useGroupMenuOptions() {
label: t('contextMenu.Color'), label: t('contextMenu.Color'),
icon: 'icon-[lucide--palette]', icon: 'icon-[lucide--palette]',
hasSubmenu: true, hasSubmenu: true,
submenu: colorOptions.map((colorOption) => ({ submenu: colorOptions.value.map((colorOption) => ({
label: colorOption.localizedName, label:
typeof colorOption.localizedName === 'function'
? colorOption.localizedName()
: colorOption.localizedName,
color: isLightTheme.value color: isLightTheme.value
? colorOption.value.light ? colorOption.value.light
: colorOption.value.dark, : 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 { computed } from 'vue'
import { useI18n } from 'vue-i18n' 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 { import {
LGraphCanvas,
LGraphNode, LGraphNode,
LiteGraph,
RenderShape, RenderShape,
isColorable isColorable
} from '@/lib/litegraph/src/litegraph' } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
import { useCanvasRefresh } from './useCanvasRefresh' import { useCanvasRefresh } from './useCanvasRefresh'
interface ColorOption {
name: string
localizedName: string
value: {
dark: string
light: string
}
}
interface ShapeOption { interface ShapeOption {
name: string name: string
localizedName: string localizedName: string
@@ -35,36 +25,16 @@ interface ShapeOption {
export function useNodeCustomization() { export function useNodeCustomization() {
const { t } = useI18n() const { t } = useI18n()
const canvasStore = useCanvasStore() const canvasStore = useCanvasStore()
const colorPaletteStore = useColorPaletteStore()
const canvasRefresh = useCanvasRefresh() const canvasRefresh = useCanvasRefresh()
const isLightTheme = computed(
() => colorPaletteStore.completedActivePalette.light_theme
)
const toLightThemeColor = (color: string) => // Use shared color options logic
adjustColor(color, { lightness: 0.5 }) const {
colorOptions,
// 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[] = [
NO_COLOR_OPTION, NO_COLOR_OPTION,
...Object.entries(LGraphCanvas.node_colors).map(([name, color]) => ({ applyColorToItems,
name, getCurrentColorName,
localizedName: t(`color.${name}`), isLightTheme
value: { } = useNodeColorOptions()
dark: color.bgcolor,
light: toLightThemeColor(color.bgcolor)
}
}))
]
// Shape options // Shape options
const shapeOptions: ShapeOption[] = [ const shapeOptions: ShapeOption[] = [
@@ -85,18 +55,13 @@ export function useNodeCustomization() {
} }
] ]
const applyColor = (colorOption: ColorOption | null) => { const applyColor = (colorOption: NodeColorOption | null) => {
const colorName = colorOption?.name ?? NO_COLOR_OPTION.name const colorName = colorOption?.name ?? NO_COLOR_OPTION.value.name
const canvasColorOption =
colorName === NO_COLOR_OPTION.name
? null
: LGraphCanvas.node_colors[colorName]
for (const item of canvasStore.selectedItems) { const colorableItems = Array.from(canvasStore.selectedItems)
if (isColorable(item)) { .filter(isColorable)
item.setColorOption(canvasColorOption) .map((item) => item as unknown as IColorable)
} applyColorToItems(colorableItems, colorName)
}
canvasRefresh.refreshCanvas() canvasRefresh.refreshCanvas()
} }
@@ -117,25 +82,21 @@ export function useNodeCustomization() {
canvasRefresh.refreshCanvas() canvasRefresh.refreshCanvas()
} }
const getCurrentColor = (): ColorOption | null => { const getCurrentColor = (): NodeColorOption | null => {
const selectedItems = Array.from(canvasStore.selectedItems) const selectedItems = Array.from(canvasStore.selectedItems)
if (selectedItems.length === 0) return null if (selectedItems.length === 0) return null
// Get color from first colorable item const colorableItems = selectedItems
const firstColorableItem = selectedItems.find((item) => isColorable(item)) .filter(isColorable)
if (!firstColorableItem || !isColorable(firstColorableItem)) return null .map((item) => item as unknown as IColorable)
if (colorableItems.length === 0) return null
// Get the current color option from the colorable item const currentColorName = getCurrentColorName(colorableItems)
const currentColorOption = firstColorableItem.getColorOption() if (!currentColorName) return null
const currentBgColor = currentColorOption?.bgcolor ?? null
// Find matching color option
return ( return (
colorOptions.find( colorOptions.value.find((option) => option.name === currentColorName) ??
(option) => NO_COLOR_OPTION.value
option.value.dark === currentBgColor ||
option.value.light === currentBgColor
) ?? NO_COLOR_OPTION
) )
} }
@@ -156,7 +117,7 @@ export function useNodeCustomization() {
} }
return { return {
colorOptions, colorOptions: computed(() => colorOptions.value),
shapeOptions, shapeOptions,
applyColor, applyColor,
applyShape, applyShape,

View File

@@ -29,8 +29,11 @@ export function useNodeMenuOptions() {
) )
const colorSubmenu = computed(() => { const colorSubmenu = computed(() => {
return colorOptions.map((colorOption) => ({ return colorOptions.value.map((colorOption) => ({
label: colorOption.localizedName, label:
typeof colorOption.localizedName === 'function'
? colorOption.localizedName()
: colorOption.localizedName,
color: isLightTheme.value color: isLightTheme.value
? colorOption.value.light ? colorOption.value.light
: colorOption.value.dark, : colorOption.value.dark,