mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 18:10:08 +00:00
merge main into rh-test
This commit is contained in:
52
src/utils/categoryIcons.ts
Normal file
52
src/utils/categoryIcons.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Maps category IDs to their corresponding Lucide icon classes
|
||||
*/
|
||||
export const getCategoryIcon = (categoryId: string): string => {
|
||||
const iconMap: Record<string, string> = {
|
||||
// Main categories
|
||||
all: 'icon-[lucide--list]',
|
||||
'getting-started': 'icon-[lucide--graduation-cap]',
|
||||
|
||||
// Generation types
|
||||
'generation-image': 'icon-[lucide--image]',
|
||||
image: 'icon-[lucide--image]',
|
||||
'generation-video': 'icon-[lucide--film]',
|
||||
video: 'icon-[lucide--film]',
|
||||
'generation-3d': 'icon-[lucide--box]',
|
||||
'3d': 'icon-[lucide--box]',
|
||||
'generation-audio': 'icon-[lucide--volume-2]',
|
||||
audio: 'icon-[lucide--volume-2]',
|
||||
'generation-llm': 'icon-[lucide--message-square-text]',
|
||||
|
||||
// API and models
|
||||
'api-nodes': 'icon-[lucide--hand-coins]',
|
||||
'closed-models': 'icon-[lucide--hand-coins]',
|
||||
|
||||
// LLMs and AI
|
||||
llm: 'icon-[lucide--message-square-text]',
|
||||
llms: 'icon-[lucide--message-square-text]',
|
||||
'llm-api': 'icon-[lucide--message-square-text]',
|
||||
|
||||
// Performance and hardware
|
||||
'small-models': 'icon-[lucide--zap]',
|
||||
performance: 'icon-[lucide--zap]',
|
||||
'mac-compatible': 'icon-[lucide--command]',
|
||||
'runs-on-mac': 'icon-[lucide--command]',
|
||||
|
||||
// Training
|
||||
'lora-training': 'icon-[lucide--dumbbell]',
|
||||
training: 'icon-[lucide--dumbbell]',
|
||||
|
||||
// Extensions and tools
|
||||
extensions: 'icon-[lucide--puzzle]',
|
||||
tools: 'icon-[lucide--wrench]',
|
||||
|
||||
// Fallbacks for common patterns
|
||||
upscaling: 'icon-[lucide--maximize-2]',
|
||||
controlnet: 'icon-[lucide--sliders-horizontal]',
|
||||
'area-composition': 'icon-[lucide--layout-grid]'
|
||||
}
|
||||
|
||||
// Return mapped icon or fallback to folder
|
||||
return iconMap[categoryId.toLowerCase()] || 'icon-[lucide--folder]'
|
||||
}
|
||||
@@ -1,9 +1,20 @@
|
||||
import { memoize } from 'es-toolkit/compat'
|
||||
|
||||
type RGB = { r: number; g: number; b: number }
|
||||
export interface HSB {
|
||||
h: number
|
||||
s: number
|
||||
b: number
|
||||
}
|
||||
type HSL = { h: number; s: number; l: number }
|
||||
type HSLA = { h: number; s: number; l: number; a: number }
|
||||
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
|
||||
type ColorFormatInternal = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
|
||||
export type ColorFormat = 'hex' | 'rgb' | 'hsb'
|
||||
interface HSV {
|
||||
h: number
|
||||
s: number
|
||||
v: number
|
||||
}
|
||||
|
||||
export interface ColorAdjustOptions {
|
||||
lightness?: number
|
||||
@@ -59,7 +70,119 @@ export function hexToRgb(hex: string): RGB {
|
||||
return { r, g, b }
|
||||
}
|
||||
|
||||
const identifyColorFormat = (color: string): ColorFormat | null => {
|
||||
export function rgbToHex({ r, g, b }: RGB): string {
|
||||
const toHex = (n: number) =>
|
||||
Math.max(0, Math.min(255, Math.round(n)))
|
||||
.toString(16)
|
||||
.padStart(2, '0')
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
export function hsbToRgb({ h, s, b }: HSB): RGB {
|
||||
// Normalize
|
||||
const hh = ((h % 360) + 360) % 360
|
||||
const ss = Math.max(0, Math.min(100, s)) / 100
|
||||
const vv = Math.max(0, Math.min(100, b)) / 100
|
||||
|
||||
const c = vv * ss
|
||||
const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
|
||||
const m = vv - c
|
||||
|
||||
let rp = 0,
|
||||
gp = 0,
|
||||
bp = 0
|
||||
|
||||
if (hh < 60) {
|
||||
rp = c
|
||||
gp = x
|
||||
bp = 0
|
||||
} else if (hh < 120) {
|
||||
rp = x
|
||||
gp = c
|
||||
bp = 0
|
||||
} else if (hh < 180) {
|
||||
rp = 0
|
||||
gp = c
|
||||
bp = x
|
||||
} else if (hh < 240) {
|
||||
rp = 0
|
||||
gp = x
|
||||
bp = c
|
||||
} else if (hh < 300) {
|
||||
rp = x
|
||||
gp = 0
|
||||
bp = c
|
||||
} else {
|
||||
rp = c
|
||||
gp = 0
|
||||
bp = x
|
||||
}
|
||||
|
||||
return {
|
||||
r: Math.floor((rp + m) * 255),
|
||||
g: Math.floor((gp + m) * 255),
|
||||
b: Math.floor((bp + m) * 255)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize various color inputs (hex, rgb/rgba, hsl/hsla, hsb string/object)
|
||||
* into lowercase #rrggbb. Falls back to #000000 on invalid inputs.
|
||||
*/
|
||||
export function parseToRgb(color: string): RGB {
|
||||
const format = identifyColorFormat(color)
|
||||
if (!format) return { r: 0, g: 0, b: 0 }
|
||||
|
||||
const hsla = parseToHSLA(color, format)
|
||||
if (!isHSLA(hsla)) return { r: 0, g: 0, b: 0 }
|
||||
|
||||
// Convert HSL to RGB
|
||||
const h = hsla.h / 360
|
||||
const s = hsla.s / 100
|
||||
const l = hsla.l / 100
|
||||
|
||||
const c = (1 - Math.abs(2 * l - 1)) * s
|
||||
const x = c * (1 - Math.abs(((h * 6) % 2) - 1))
|
||||
const m = l - c / 2
|
||||
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0
|
||||
|
||||
if (h < 1 / 6) {
|
||||
r = c
|
||||
g = x
|
||||
b = 0
|
||||
} else if (h < 2 / 6) {
|
||||
r = x
|
||||
g = c
|
||||
b = 0
|
||||
} else if (h < 3 / 6) {
|
||||
r = 0
|
||||
g = c
|
||||
b = x
|
||||
} else if (h < 4 / 6) {
|
||||
r = 0
|
||||
g = x
|
||||
b = c
|
||||
} else if (h < 5 / 6) {
|
||||
r = x
|
||||
g = 0
|
||||
b = c
|
||||
} else {
|
||||
r = c
|
||||
g = 0
|
||||
b = x
|
||||
}
|
||||
|
||||
return {
|
||||
r: Math.round((r + m) * 255),
|
||||
g: Math.round((g + m) * 255),
|
||||
b: Math.round((b + m) * 255)
|
||||
}
|
||||
}
|
||||
|
||||
const identifyColorFormat = (color: string): ColorFormatInternal | null => {
|
||||
if (!color) return null
|
||||
if (color.startsWith('#') && (color.length === 4 || color.length === 7))
|
||||
return 'hex'
|
||||
@@ -80,7 +203,73 @@ const isHSLA = (color: unknown): color is HSLA => {
|
||||
)
|
||||
}
|
||||
|
||||
function parseToHSLA(color: string, format: ColorFormat): HSLA | null {
|
||||
export function isColorFormat(v: unknown): v is ColorFormat {
|
||||
return v === 'hex' || v === 'rgb' || v === 'hsb'
|
||||
}
|
||||
|
||||
function isHSBObject(v: unknown): v is HSB {
|
||||
if (!v || typeof v !== 'object') return false
|
||||
const rec = v as Record<string, unknown>
|
||||
return (
|
||||
typeof rec.h === 'number' &&
|
||||
Number.isFinite(rec.h) &&
|
||||
typeof rec.s === 'number' &&
|
||||
Number.isFinite(rec.s) &&
|
||||
typeof (rec as Record<string, unknown>).b === 'number' &&
|
||||
Number.isFinite((rec as Record<string, number>).b!)
|
||||
)
|
||||
}
|
||||
|
||||
function isHSVObject(v: unknown): v is HSV {
|
||||
if (!v || typeof v !== 'object') return false
|
||||
const rec = v as Record<string, unknown>
|
||||
return (
|
||||
typeof rec.h === 'number' &&
|
||||
Number.isFinite(rec.h) &&
|
||||
typeof rec.s === 'number' &&
|
||||
Number.isFinite(rec.s) &&
|
||||
typeof (rec as Record<string, unknown>).v === 'number' &&
|
||||
Number.isFinite((rec as Record<string, number>).v!)
|
||||
)
|
||||
}
|
||||
|
||||
export function toHexFromFormat(val: unknown, format: ColorFormat): string {
|
||||
if (format === 'hex' && typeof val === 'string') {
|
||||
const raw = val.trim().toLowerCase()
|
||||
if (!raw) return '#000000'
|
||||
if (/^[0-9a-f]{3}$/.test(raw)) return `#${raw}`
|
||||
if (/^#[0-9a-f]{3}$/.test(raw)) return raw
|
||||
if (/^[0-9a-f]{6}$/.test(raw)) return `#${raw}`
|
||||
if (/^#[0-9a-f]{6}$/.test(raw)) return raw
|
||||
return '#000000'
|
||||
}
|
||||
|
||||
if (format === 'rgb' && typeof val === 'string') {
|
||||
const rgb = parseToRgb(val)
|
||||
return rgbToHex(rgb).toLowerCase()
|
||||
}
|
||||
|
||||
if (format === 'hsb') {
|
||||
if (isHSBObject(val)) {
|
||||
return rgbToHex(hsbToRgb(val)).toLowerCase()
|
||||
}
|
||||
if (isHSVObject(val)) {
|
||||
const { h, s, v } = val
|
||||
return rgbToHex(hsbToRgb({ h, s, b: v })).toLowerCase()
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
const nums = val.match(/\d+(?:\.\d+)?/g)?.map(Number) || []
|
||||
if (nums.length >= 3) {
|
||||
return rgbToHex(
|
||||
hsbToRgb({ h: nums[0], s: nums[1], b: nums[2] })
|
||||
).toLowerCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
return '#000000'
|
||||
}
|
||||
|
||||
function parseToHSLA(color: string, format: ColorFormatInternal): HSLA | null {
|
||||
let match: RegExpMatchArray | null
|
||||
|
||||
switch (format) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
export function isElectron() {
|
||||
return 'electronAPI' in window && window.electronAPI !== undefined
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ISerialisedGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
|
||||
export interface ErrorReportData {
|
||||
exceptionType: string
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type {
|
||||
ComfyApiWorkflow,
|
||||
ComfyWorkflowJSON
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
import { ExecutableGroupNodeDTO, isGroupNode } from './executableGroupNodeDto'
|
||||
import { compressWidgetInputSlots } from './litegraphUtil'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResultItem } from '@/schemas/apiSchema'
|
||||
import type { ResultItem } from '@/schemas/apiSchema'
|
||||
import type { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
export function formatCamelCase(str: string): string {
|
||||
@@ -33,10 +33,6 @@ export function appendJsonExt(path: string) {
|
||||
return path
|
||||
}
|
||||
|
||||
export function trimJsonExt(path?: string) {
|
||||
return path?.replace(/\.json$/, '')
|
||||
}
|
||||
|
||||
export function highlightQuery(text: string, query: string) {
|
||||
if (!query) return text
|
||||
|
||||
@@ -80,28 +76,6 @@ export function formatSize(value?: number) {
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the common directory prefix between two paths
|
||||
* @example
|
||||
* findCommonPrefix('a/b/c', 'a/b/d') // returns 'a/b'
|
||||
* findCommonPrefix('x/y/z', 'a/b/c') // returns ''
|
||||
* findCommonPrefix('a/b/c', 'a/b/c/d') // returns 'a/b/c'
|
||||
*/
|
||||
export function findCommonPrefix(path1: string, path2: string): string {
|
||||
const parts1 = path1.split('/')
|
||||
const parts2 = path2.split('/')
|
||||
|
||||
const commonParts: string[] = []
|
||||
for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) {
|
||||
if (parts1[i] === parts2[i]) {
|
||||
commonParts.push(parts1[i])
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return commonParts.join('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns various filename components.
|
||||
* Example:
|
||||
@@ -109,7 +83,7 @@ export function findCommonPrefix(path1: string, path2: string): string {
|
||||
* - filename: 'file'
|
||||
* - suffix: 'txt'
|
||||
*/
|
||||
export function getFilenameDetails(fullFilename: string) {
|
||||
function getFilenameDetails(fullFilename: string) {
|
||||
if (fullFilename.includes('.')) {
|
||||
return {
|
||||
filename: fullFilename.split('.').slice(0, -1).join('.'),
|
||||
@@ -390,59 +364,6 @@ export const downloadUrlToHfRepoUrl = (url: string): string => {
|
||||
}
|
||||
}
|
||||
|
||||
export const isSemVer = (
|
||||
version: string
|
||||
): version is `${number}.${number}.${number}` => {
|
||||
const regex = /^\d+\.\d+\.\d+$/
|
||||
return regex.test(version)
|
||||
}
|
||||
|
||||
const normalizeVersion = (version: string) =>
|
||||
version
|
||||
.split(/[+.-]/)
|
||||
.map(Number)
|
||||
.filter((part) => !Number.isNaN(part))
|
||||
|
||||
export function compareVersions(
|
||||
versionA: string | undefined,
|
||||
versionB: string | undefined
|
||||
): number {
|
||||
versionA ??= '0.0.0'
|
||||
versionB ??= '0.0.0'
|
||||
|
||||
const aParts = normalizeVersion(versionA)
|
||||
const bParts = normalizeVersion(versionB)
|
||||
|
||||
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
||||
const aPart = aParts[i] ?? 0
|
||||
const bPart = bParts[i] ?? 0
|
||||
if (aPart < bPart) return -1
|
||||
if (aPart > bPart) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a currency amount to Metronome's integer representation.
|
||||
* For USD, converts to cents (multiplied by 100).
|
||||
* For all other currencies (including custom pricing units), returns the amount as is.
|
||||
* This is specific to Metronome's API requirements.
|
||||
*
|
||||
* @param amount - The amount in currency to convert
|
||||
* @param currency - The currency to convert
|
||||
* @returns The amount in Metronome's integer format (cents for USD, base units for others)
|
||||
* @example
|
||||
* toMetronomeCurrency(1.23, 'usd') // returns 123 (cents)
|
||||
* toMetronomeCurrency(1000, 'jpy') // returns 1000 (yen)
|
||||
*/
|
||||
export function toMetronomeCurrency(amount: number, currency: string): number {
|
||||
if (currency === 'usd') {
|
||||
return Math.round(amount * 100)
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Metronome's integer amount back to a formatted currency string.
|
||||
* For USD, converts from cents to dollars.
|
||||
@@ -525,7 +446,7 @@ export function formatVersionAnchor(version: string): string {
|
||||
/**
|
||||
* Supported locale types for the application (from OpenAPI schema)
|
||||
*/
|
||||
export type SupportedLocale = NonNullable<
|
||||
type SupportedLocale = NonNullable<
|
||||
operations['getReleaseNotes']['parameters']['query']['locale']
|
||||
>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Fuse, { FuseOptionKey, FuseSearchOptions, IFuseOptions } from 'fuse.js'
|
||||
import type { FuseOptionKey, FuseSearchOptions, IFuseOptions } from 'fuse.js'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
export type SearchAuxScore = number[]
|
||||
|
||||
|
||||
@@ -8,6 +8,23 @@ import { parseNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
import { isSubgraphIoNode } from './typeGuardUtil'
|
||||
|
||||
interface NodeWithId {
|
||||
id: string | number
|
||||
subgraphId?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a locator ID from node data with optional subgraph context.
|
||||
*
|
||||
* @param nodeData - Node data containing id and optional subgraphId
|
||||
* @returns The locator ID string
|
||||
*/
|
||||
export function getLocatorIdFromNodeData(nodeData: NodeWithId): string {
|
||||
return nodeData.subgraphId
|
||||
? `${nodeData.subgraphId}:${String(nodeData.id)}`
|
||||
: String(nodeData.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an execution ID into its component parts.
|
||||
*
|
||||
@@ -394,7 +411,7 @@ export function getAllNonIoNodesInSubgraph(subgraph: Subgraph): LGraphNode[] {
|
||||
/**
|
||||
* Options for traverseNodesDepthFirst function
|
||||
*/
|
||||
export interface TraverseNodesOptions<T> {
|
||||
interface TraverseNodesOptions<T> {
|
||||
/** Function called for each node during traversal */
|
||||
visitor?: (node: LGraphNode, context: T) => T
|
||||
/** Initial context value */
|
||||
@@ -449,7 +466,7 @@ export function traverseNodesDepthFirst<T = void>(
|
||||
/**
|
||||
* Options for collectFromNodes function
|
||||
*/
|
||||
export interface CollectFromNodesOptions<T, C> {
|
||||
interface CollectFromNodesOptions<T, C> {
|
||||
/** Function that returns data to collect for each node */
|
||||
collector?: (node: LGraphNode, context: C) => T | null
|
||||
/** Function that builds context for child nodes */
|
||||
|
||||
45
src/utils/gridUtil.ts
Normal file
45
src/utils/gridUtil.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
interface GridOptions {
|
||||
/** Minimum width for each grid item (default: 15rem) */
|
||||
minWidth?: string
|
||||
/** Maximum width for each grid item (default: 1fr) */
|
||||
maxWidth?: string
|
||||
/** Padding around the grid (default: 0) */
|
||||
padding?: string
|
||||
/** Gap between grid items (default: 1rem) */
|
||||
gap?: string
|
||||
/** Fixed number of columns (overrides auto-fill with minmax) */
|
||||
columns?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates CSS grid styles for responsive grid layouts
|
||||
* @param options Grid configuration options
|
||||
* @returns CSS properties object for grid styling
|
||||
*/
|
||||
export function createGridStyle(options: GridOptions = {}): CSSProperties {
|
||||
const {
|
||||
minWidth = '15rem',
|
||||
maxWidth = '1fr',
|
||||
padding = '0',
|
||||
gap = '1rem',
|
||||
columns
|
||||
} = options
|
||||
|
||||
// Runtime validation for columns
|
||||
if (columns !== undefined && columns < 1) {
|
||||
console.warn('createGridStyle: columns must be >= 1, defaulting to 1')
|
||||
}
|
||||
|
||||
const gridTemplateColumns = columns
|
||||
? `repeat(${Math.max(1, columns ?? 1)}, 1fr)`
|
||||
: `repeat(auto-fill, minmax(${minWidth}, ${maxWidth}))`
|
||||
|
||||
return {
|
||||
display: 'grid',
|
||||
gridTemplateColumns,
|
||||
padding,
|
||||
gap
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ import type {
|
||||
ISerialisedNode
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
|
||||
export interface BadLinksData<T = ISerialisedGraph | LGraph> {
|
||||
interface BadLinksData<T = ISerialisedGraph | LGraph> {
|
||||
hasBadLinks: boolean
|
||||
fixed: boolean
|
||||
graph: T
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import { ColorOption, LGraph, Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ColorOption, LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import {
|
||||
import type {
|
||||
ComfyNodeDef as ComfyNodeDefV2,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const registryToFrontendV2NodeOutputs = (
|
||||
registryDef: components['schemas']['ComfyNode']
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
|
||||
/**
|
||||
* Finds the greatest common divisor (GCD) for two numbers.
|
||||
*
|
||||
@@ -19,3 +22,48 @@ export const gcd = (a: number, b: number): number => {
|
||||
export const lcm = (a: number, b: number): number => {
|
||||
return Math.abs(a * b) / gcd(a, b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the union (bounding box) of multiple rectangles using a single-pass algorithm.
|
||||
*
|
||||
* Finds the minimum and maximum x/y coordinates across all rectangles to create
|
||||
* a single bounding rectangle that contains all input rectangles. Optimized for
|
||||
* performance with V8-friendly tuple access patterns.
|
||||
*
|
||||
* @param rectangles - Array of rectangle tuples in [x, y, width, height] format
|
||||
* @returns Bounds object with union rectangle, or null if no rectangles provided
|
||||
*/
|
||||
export function computeUnionBounds(
|
||||
rectangles: readonly ReadOnlyRect[]
|
||||
): Bounds | null {
|
||||
const n = rectangles.length
|
||||
if (n === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const r0 = rectangles[0]
|
||||
let minX = r0[0]
|
||||
let minY = r0[1]
|
||||
let maxX = minX + r0[2]
|
||||
let maxY = minY + r0[3]
|
||||
|
||||
for (let i = 1; i < n; i++) {
|
||||
const r = rectangles[i]
|
||||
const x1 = r[0]
|
||||
const y1 = r[1]
|
||||
const x2 = x1 + r[2]
|
||||
const y2 = y1 + r[3]
|
||||
|
||||
if (x1 < minX) minX = x1
|
||||
if (y1 < minY) minY = y1
|
||||
if (x2 > maxX) maxX = x2
|
||||
if (y2 > maxY) maxY = y2
|
||||
}
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
NodeId,
|
||||
Reroute,
|
||||
WorkflowJSON04
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
type RerouteNode = ComfyNode & {
|
||||
type: 'Reroute'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ModelFile } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ModelFile } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
/**
|
||||
* Gets models from the node's `properties.models` field, excluding those
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TWidgetValue } from '@/lib/litegraph/src/litegraph'
|
||||
import type { TWidgetValue } from '@/lib/litegraph/src/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
isFloatInputSpec,
|
||||
isIntInputSpec
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
|
||||
import { lcm } from './mathUtil'
|
||||
|
||||
@@ -139,11 +138,3 @@ export const mergeInputSpec = (
|
||||
|
||||
return mergeCommonInputSpec(spec1, spec2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node definition represents a subgraph node.
|
||||
* Subgraph nodes are created with category='subgraph' and python_module='nodes'.
|
||||
*/
|
||||
export const isSubgraphNode = (nodeDef: ComfyNodeDefImpl): boolean => {
|
||||
return nodeDef.category === 'subgraph' && nodeDef.python_module === 'nodes'
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
|
||||
import { normalizePackId } from '@/utils/packUtils'
|
||||
|
||||
export function extractCustomNodeName(
|
||||
pythonModule: string | undefined
|
||||
): string | null {
|
||||
const modules = pythonModule?.split('.') || []
|
||||
if (modules.length >= 2 && modules[0] === 'custom_nodes') {
|
||||
return modules[1].split('@')[0]
|
||||
// Use normalizePackId to remove version suffix
|
||||
return normalizePackId(modules[1])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
35
src/utils/packUtils.ts
Normal file
35
src/utils/packUtils.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { mapKeys } from 'es-toolkit/compat'
|
||||
|
||||
/**
|
||||
* Normalizes a pack ID by removing the version suffix.
|
||||
*
|
||||
* ComfyUI-Manager returns pack IDs in different formats:
|
||||
* - Enabled packs: "packname" (without version)
|
||||
* - Disabled packs: "packname@1_0_3" (with version suffix)
|
||||
* - Latest versions from registry: "packname" (without version)
|
||||
*
|
||||
* Since the pack object itself contains the version info (ver field),
|
||||
* we normalize all pack IDs to just the base name for consistent access.
|
||||
* This ensures we can always find a pack by its base name (nodePack.id)
|
||||
* regardless of its enabled/disabled state.
|
||||
*
|
||||
* @param packId - The pack ID that may contain a version suffix
|
||||
* @returns The normalized pack ID without version suffix
|
||||
*/
|
||||
export function normalizePackId(packId: string): string {
|
||||
return packId.split('@')[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes all keys in a pack record by removing version suffixes.
|
||||
* This is used when receiving pack data from the server to ensure
|
||||
* consistent key format across the application.
|
||||
*
|
||||
* @param packs - Record of packs with potentially versioned keys
|
||||
* @returns Record with normalized keys
|
||||
*/
|
||||
export function normalizePackKeys<T>(
|
||||
packs: Record<string, T>
|
||||
): Record<string, T> {
|
||||
return mapKeys(packs, (_value, key) => normalizePackId(key))
|
||||
}
|
||||
1
src/utils/tailwindUtil.ts
Normal file
1
src/utils/tailwindUtil.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { cn, type ClassValue } from '@comfyorg/tailwind-utils'
|
||||
@@ -129,7 +129,7 @@ export const findNodeByKey = <T extends TreeNode>(
|
||||
* @param node - The node to clone.
|
||||
* @returns A deep clone of the node.
|
||||
*/
|
||||
export function cloneTree<T extends TreeNode>(node: T): T {
|
||||
function cloneTree<T extends TreeNode>(node: T): T {
|
||||
const clone = { ...node }
|
||||
|
||||
// Clone children recursively
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PrimitiveNode } from '@/extensions/core/widgetInputs'
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
export function isPrimitiveNode(
|
||||
node: LGraphNode
|
||||
@@ -39,3 +40,16 @@ export const isSubgraphIoNode = (
|
||||
const nodeClass = node.constructor?.comfyClass
|
||||
return nodeClass === 'SubgraphInputNode' || nodeClass === 'SubgraphOutputNode'
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for slot objects (inputs/outputs)
|
||||
*/
|
||||
export const isSlotObject = (obj: unknown): obj is INodeSlot => {
|
||||
return (
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'name' in obj &&
|
||||
'type' in obj &&
|
||||
'boundingRect' in obj
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,16 +4,3 @@ export enum ValidationState {
|
||||
VALID = 'VALID',
|
||||
INVALID = 'INVALID'
|
||||
}
|
||||
|
||||
export const mergeValidationStates = (states: ValidationState[]) => {
|
||||
if (states.some((state) => state === ValidationState.INVALID)) {
|
||||
return ValidationState.INVALID
|
||||
}
|
||||
if (states.some((state) => state === ValidationState.LOADING)) {
|
||||
return ValidationState.LOADING
|
||||
}
|
||||
if (states.every((state) => state === ValidationState.VALID)) {
|
||||
return ValidationState.VALID
|
||||
}
|
||||
return ValidationState.IDLE
|
||||
}
|
||||
|
||||
71
src/utils/widgetPropFilter.ts
Normal file
71
src/utils/widgetPropFilter.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Widget prop filtering utilities
|
||||
* Filters out style-related and customization props from PrimeVue components
|
||||
* to maintain consistent widget appearance across the application
|
||||
*/
|
||||
|
||||
// Props to exclude based on the widget interface specifications
|
||||
export const STANDARD_EXCLUDED_PROPS = [
|
||||
'style',
|
||||
'class',
|
||||
'dt',
|
||||
'pt',
|
||||
'ptOptions',
|
||||
'unstyled'
|
||||
] as const
|
||||
|
||||
export const INPUT_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'inputClass',
|
||||
'inputStyle'
|
||||
] as const
|
||||
|
||||
export const PANEL_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'panelClass',
|
||||
'panelStyle',
|
||||
'overlayClass'
|
||||
] as const
|
||||
|
||||
// export const IMAGE_EXCLUDED_PROPS = [
|
||||
// ...STANDARD_EXCLUDED_PROPS,
|
||||
// 'imageClass',
|
||||
// 'imageStyle'
|
||||
// ] as const
|
||||
|
||||
export const GALLERIA_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'thumbnailsPosition',
|
||||
'verticalThumbnailViewPortHeight',
|
||||
'indicatorsPosition',
|
||||
'maskClass',
|
||||
'containerStyle',
|
||||
'containerClass',
|
||||
'galleriaClass'
|
||||
] as const
|
||||
|
||||
export const BADGE_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'badgeClass'
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Filters widget props by excluding specified properties
|
||||
* @param props - The props object to filter
|
||||
* @param excludeList - List of property names to exclude
|
||||
* @returns Filtered props object
|
||||
*/
|
||||
export function filterWidgetProps<T extends Record<string, any>>(
|
||||
props: T | undefined,
|
||||
excludeList: readonly string[]
|
||||
): Partial<T> {
|
||||
if (!props) return {}
|
||||
|
||||
const filtered: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (!excludeList.includes(key)) {
|
||||
filtered[key] = value
|
||||
}
|
||||
}
|
||||
return filtered as Partial<T>
|
||||
}
|
||||
Reference in New Issue
Block a user