mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
Merge branch 'main' into sno-lint-issue-write
This commit is contained in:
@@ -49,7 +49,7 @@ DO NOT use deprecated PrimeVue components. Use these replacements instead:
|
||||
|
||||
## Development Guidelines
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use lodash for utility functions
|
||||
2. Use es-toolkit for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
4. Implement proper props and emits definitions
|
||||
5. Utilize Vue 3's Teleport component when needed
|
||||
|
||||
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -18,7 +18,7 @@ Use Tailwind CSS for styling
|
||||
|
||||
Leverage VueUse functions for performance-enhancing styles
|
||||
|
||||
Use lodash for utility functions
|
||||
Use es-toolkit for utility functions
|
||||
|
||||
Use TypeScript for type safety
|
||||
|
||||
|
||||
5
.github/workflows/update-manager-types.yaml
vendored
5
.github/workflows/update-manager-types.yaml
vendored
@@ -61,6 +61,11 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Lint generated types
|
||||
run: |
|
||||
echo "Linting generated ComfyUI-Manager API types..."
|
||||
npm run lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
|
||||
5
.github/workflows/update-registry-types.yaml
vendored
5
.github/workflows/update-registry-types.yaml
vendored
@@ -61,6 +61,11 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Lint generated types
|
||||
run: |
|
||||
echo "Linting generated Comfy Registry API types..."
|
||||
npm run lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,6 +7,9 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import fs from 'fs'
|
||||
import _ from 'lodash'
|
||||
import path from 'path'
|
||||
import type { Request, Route } from 'playwright'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@@ -75,7 +75,9 @@ export default class TaskHistory {
|
||||
|
||||
private async handleGetView(route: Route) {
|
||||
const fileName = getFilenameParam(route.request())
|
||||
if (!this.outputContentTypes.has(fileName)) route.continue()
|
||||
if (!this.outputContentTypes.has(fileName)) {
|
||||
return route.continue()
|
||||
}
|
||||
|
||||
const asset = this.loadAsset(fileName)
|
||||
return route.fulfill({
|
||||
|
||||
@@ -317,6 +317,25 @@ test.describe('Workflows sidebar', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('Can duplicate workflow from context menu', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'default.json'
|
||||
})
|
||||
|
||||
const { workflowsTab } = comfyPage.menu
|
||||
await workflowsTab.open()
|
||||
|
||||
await workflowsTab
|
||||
.getPersistedItem('workflow1.json')
|
||||
.click({ button: 'right' })
|
||||
await comfyPage.clickContextMenuItem('Duplicate')
|
||||
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'default.json'
|
||||
|
||||
@@ -14,7 +14,10 @@ export default [
|
||||
ignores: [
|
||||
'src/scripts/*',
|
||||
'src/extensions/core/*',
|
||||
'src/types/vue-shim.d.ts'
|
||||
'src/types/vue-shim.d.ts',
|
||||
// Generated files that don't need linting
|
||||
'src/types/comfyRegistryTypes.ts',
|
||||
'src/types/generatedManagerTypes.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -30,12 +30,12 @@
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.2",
|
||||
"marked": "^15.0.11",
|
||||
"pinia": "^2.1.7",
|
||||
@@ -62,7 +62,6 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/three": "^0.169.0",
|
||||
@@ -4887,12 +4886,6 @@
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz",
|
||||
"integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
@@ -7998,6 +7991,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.9",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.9.tgz",
|
||||
"integrity": "sha512-9OtbkZmTA2Qc9groyA1PUNeb6knVTkvB2RSdr/LcJXDL8IdEakaxwXLHXa7VX/Wj0GmdMJPR3WhnPGhiP3E+qg==",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
@@ -11450,7 +11452,8 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
|
||||
11
package.json
11
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -21,8 +21,10 @@
|
||||
"test:component": "vitest run src/components/",
|
||||
"prepare": "husky || true",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint": "eslint src --cache",
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"knip": "knip",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
|
||||
@@ -39,7 +41,6 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/three": "^0.169.0",
|
||||
@@ -98,12 +99,12 @@
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.2",
|
||||
"marked": "^15.0.11",
|
||||
"pinia": "^2.1.7",
|
||||
|
||||
@@ -51,7 +51,7 @@ const template = await fetch('/templates/default.json')
|
||||
|
||||
## General Guidelines
|
||||
|
||||
- Use lodash for utility functions
|
||||
- Use es-toolkit for utility functions
|
||||
- Implement proper TypeScript types
|
||||
- Follow Vue 3 composition API style guide
|
||||
- Use vue-i18n for ALL user-facing strings in `src/locales/en/main.json`
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
useLocalStorage,
|
||||
watchDebounced
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import Panel from 'primevue/panel'
|
||||
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="TFilter extends SearchFilter">
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import { useElementSize, useScroll, whenever } from '@vueuse/core'
|
||||
import { clamp, debounce } from 'lodash'
|
||||
import { clamp, debounce } from 'es-toolkit/compat'
|
||||
import { type CSSProperties, computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
type GridState = {
|
||||
|
||||
@@ -169,8 +169,8 @@ import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
import _ from 'lodash'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { cloneDeep } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { merge } from 'lodash'
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import {
|
||||
computed,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('lodash', () => ({
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
}))
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { stubTrue } from 'lodash'
|
||||
import { stubTrue } from 'es-toolkit/compat'
|
||||
import AutoComplete, {
|
||||
AutoCompleteOptionSelectEvent
|
||||
} from 'primevue/autocomplete'
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="isVisible"
|
||||
v-if="isUnpackVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_UnpackSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.Graph.UnpackSubgraph')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:expand />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="isConvertVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_ConvertToSubgraph.label'),
|
||||
showDelay: 1000
|
||||
@@ -20,6 +34,7 @@ import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
@@ -27,7 +42,13 @@ const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const isVisible = computed(() => {
|
||||
const isUnpackVisible = computed(() => {
|
||||
return (
|
||||
canvasStore.selectedItems?.length === 1 &&
|
||||
canvasStore.selectedItems[0] instanceof SubgraphNode
|
||||
)
|
||||
})
|
||||
const isConvertVisible = computed(() => {
|
||||
return (
|
||||
canvasStore.groupSelected ||
|
||||
canvasStore.rerouteSelected ||
|
||||
|
||||
@@ -82,7 +82,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
@@ -265,6 +265,14 @@ const renderTreeNode = (
|
||||
const workflow = node.data
|
||||
await workflowService.insertWorkflow(workflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('g.duplicate'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: async () => {
|
||||
const workflow = node.data
|
||||
await workflowService.duplicateWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { Ref, markRaw, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useTerminal(element: Ref<HTMLElement | undefined>) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutationObserver, useResizeObserver } from '@vueuse/core'
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { readonly, ref } from 'vue'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
|
||||
@@ -418,7 +418,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const modeValue = String(modeWidget.value)
|
||||
|
||||
// Pricing matrix from CSV data based on mode string content
|
||||
if (modeValue.includes('v2-master')) {
|
||||
if (modeValue.includes('v2-1-master')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$2.80/Run' // price is the same as for v2-master model
|
||||
}
|
||||
return '$1.40/Run' // price is the same as for v2-master model
|
||||
} else if (modeValue.includes('v2-master')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$2.80/Run'
|
||||
}
|
||||
@@ -558,6 +563,32 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
MinimaxTextToVideoNode: {
|
||||
displayPrice: '$0.43/Run'
|
||||
},
|
||||
MinimaxHailuoVideoNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const resolutionWidget = node.widgets?.find(
|
||||
(w) => w.name === 'resolution'
|
||||
) as IComboWidget
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
|
||||
if (!resolutionWidget || !durationWidget) {
|
||||
return '$0.28-0.56/Run (varies with resolution & duration)'
|
||||
}
|
||||
|
||||
const resolution = String(resolutionWidget.value)
|
||||
const duration = String(durationWidget.value)
|
||||
|
||||
if (resolution.includes('768P')) {
|
||||
if (duration.includes('6')) return '$0.28/Run'
|
||||
if (duration.includes('10')) return '$0.56/Run'
|
||||
} else if (resolution.includes('1080P')) {
|
||||
if (duration.includes('6')) return '$0.49/Run'
|
||||
}
|
||||
|
||||
return '$0.43/Run' // default median
|
||||
}
|
||||
},
|
||||
OpenAIDalle2: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const sizeWidget = node.widgets?.find(
|
||||
@@ -1278,9 +1309,13 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
// Google Veo video generation
|
||||
if (model.includes('veo-2.0')) {
|
||||
return '$0.5/second'
|
||||
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
||||
return '$0.00016/$0.0006 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-flash-preview-04-17')) {
|
||||
return '$0.0003/$0.0025 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-flash')) {
|
||||
return '$0.0003/$0.0025 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
||||
return '$0.00125/$0.01 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-pro')) {
|
||||
return '$0.00125/$0.01 per 1K tokens'
|
||||
}
|
||||
// For other Gemini models, show token-based pricing info
|
||||
@@ -1358,6 +1393,7 @@ export const useNodePricing = () => {
|
||||
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
|
||||
KlingSingleImageVideoEffectNode: ['effect_scene'],
|
||||
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
|
||||
MinimaxHailuoVideoNode: ['resolution', 'duration'],
|
||||
OpenAIDalle3: ['size', 'quality'],
|
||||
OpenAIDalle2: ['size', 'n'],
|
||||
OpenAIGPTImage1: ['quality', 'n'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { groupBy } from 'lodash'
|
||||
import { groupBy } from 'es-toolkit/compat'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
@@ -795,6 +796,23 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
const { node } = res
|
||||
canvas.select(node)
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.UnpackSubgraph',
|
||||
icon: 'pi pi-sitemap',
|
||||
label: 'Unpack the selected Subgraph',
|
||||
versionAdded: '1.20.1',
|
||||
category: 'essentials' as const,
|
||||
function: () => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const graph = canvas.subgraph ?? canvas.graph
|
||||
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
|
||||
|
||||
const subgraphNode = app.canvas.selectedItems.values().next().value
|
||||
useNodeOutputStore().revokeSubgraphPreviews(subgraphNode)
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { orderBy } from 'lodash'
|
||||
import { orderBy } from 'es-toolkit/compat'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { DEFAULT_PAGE_SIZE } from '@/constants/searchConstants'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tryOnScopeDispose } from '@vueuse/core'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
@@ -88,6 +89,11 @@ export function useWorkflowPersistence() {
|
||||
)
|
||||
api.addEventListener('graphChanged', persistCurrentWorkflow)
|
||||
|
||||
// Clean up event listener when component unmounts
|
||||
tryOnScopeDispose(() => {
|
||||
api.removeEventListener('graphChanged', persistCurrentWorkflow)
|
||||
})
|
||||
|
||||
// Restore workflow tabs states
|
||||
const openWorkflows = computed(() => workflowStore.openWorkflows)
|
||||
const activeWorkflow = computed(() => workflowStore.activeWorkflow)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { debounce } from 'lodash'
|
||||
import _ from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
|
||||
@@ -43,6 +43,19 @@ export class CanvasPointer {
|
||||
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
|
||||
static #maxClickDrift2 = this.#maxClickDrift ** 2
|
||||
|
||||
/** Assume that "wheel" events with both deltaX and deltaY less than this value are trackpad gestures. */
|
||||
static trackpadThreshold = 60
|
||||
|
||||
/**
|
||||
* The minimum time between "wheel" events to allow switching between trackpad
|
||||
* and mouse modes.
|
||||
*
|
||||
* This prevents trackpad "flick" panning from registering as regular mouse wheel.
|
||||
* After a flick gesture is complete, the automatic wheel events are sent with
|
||||
* reduced frequency, but much higher deltaX and deltaY values.
|
||||
*/
|
||||
static trackpadMaxGap = 200
|
||||
|
||||
/** The element this PointerState should capture input against when dragging. */
|
||||
element: Element
|
||||
/** Pointer ID used by drag capture. */
|
||||
@@ -77,6 +90,9 @@ export class CanvasPointer {
|
||||
/** The last pointerup event for the primary button */
|
||||
eUp?: CanvasPointerEvent
|
||||
|
||||
/** The last pointermove event that was treated as a trackpad gesture. */
|
||||
lastTrackpadEvent?: WheelEvent
|
||||
|
||||
/**
|
||||
* If set, as soon as the mouse moves outside the click drift threshold, this action is run once.
|
||||
* @param pointer [DEPRECATED] This parameter will be removed in a future release.
|
||||
@@ -257,6 +273,35 @@ export class CanvasPointer {
|
||||
delete this.onDragStart
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given wheel event is part of a continued trackpad gesture.
|
||||
* @param e The wheel event to check
|
||||
* @returns `true` if the event is part of a continued trackpad gesture, otherwise `false`
|
||||
*/
|
||||
#isContinuationOfGesture(e: WheelEvent): boolean {
|
||||
const { lastTrackpadEvent } = this
|
||||
if (!lastTrackpadEvent) return false
|
||||
|
||||
return (
|
||||
e.timeStamp - lastTrackpadEvent.timeStamp < CanvasPointer.trackpadMaxGap
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given wheel event is part of a trackpad gesture.
|
||||
* @param e The wheel event to check
|
||||
* @returns `true` if the event is part of a trackpad gesture, otherwise `false`
|
||||
*/
|
||||
isTrackpadGesture(e: WheelEvent): boolean {
|
||||
if (this.#isContinuationOfGesture(e)) {
|
||||
this.lastTrackpadEvent = e
|
||||
return true
|
||||
}
|
||||
|
||||
const threshold = CanvasPointer.trackpadThreshold
|
||||
return Math.abs(e.deltaX) < threshold && Math.abs(e.deltaY) < threshold
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the state of this {@link CanvasPointer} instance.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type { Point, Rect } from './interfaces'
|
||||
import { LGraphCanvas } from './litegraph'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { toString } from 'lodash'
|
||||
import { toString } from 'es-toolkit/compat'
|
||||
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
@@ -1664,6 +1664,254 @@ export class LGraph
|
||||
return { subgraph, node: subgraphNode as SubgraphNode }
|
||||
}
|
||||
|
||||
unpackSubgraph(subgraphNode: SubgraphNode) {
|
||||
if (!(subgraphNode instanceof SubgraphNode))
|
||||
throw new Error('Can only unpack Subgraph Nodes')
|
||||
this.beforeChange()
|
||||
const center = [0, 0]
|
||||
for (const node of subgraphNode.subgraph.nodes) {
|
||||
center[0] += node.pos[0] + node.size[0] / 2
|
||||
center[1] += node.pos[1] + node.size[1] / 2
|
||||
}
|
||||
center[0] /= subgraphNode.subgraph.nodes.length
|
||||
center[1] /= subgraphNode.subgraph.nodes.length
|
||||
|
||||
const offsetX = subgraphNode.pos[0] - center[0] + subgraphNode.size[0] / 2
|
||||
const offsetY = subgraphNode.pos[1] - center[1] + subgraphNode.size[1] / 2
|
||||
const movedNodes = multiClone(subgraphNode.subgraph.nodes)
|
||||
const nodeIdMap = new Map<NodeId, NodeId>()
|
||||
for (const n_info of movedNodes) {
|
||||
const node = LiteGraph.createNode(String(n_info.type), n_info.title)
|
||||
if (!node) {
|
||||
throw new Error('Node not found')
|
||||
}
|
||||
|
||||
nodeIdMap.set(n_info.id, ++this.last_node_id)
|
||||
node.id = this.last_node_id
|
||||
n_info.id = this.last_node_id
|
||||
|
||||
this.add(node, true)
|
||||
node.configure(n_info)
|
||||
node.pos[0] += offsetX
|
||||
node.pos[1] += offsetY
|
||||
for (const input of node.inputs) {
|
||||
input.link = null
|
||||
}
|
||||
}
|
||||
//cleanup reoute.linkIds now, but leave link.parentIds dangling
|
||||
for (const islot of subgraphNode.inputs) {
|
||||
if (!islot.link) continue
|
||||
const link = this.links.get(islot.link)
|
||||
if (!link) {
|
||||
console.warn('Broken link', islot, islot.link)
|
||||
continue
|
||||
}
|
||||
for (const reroute of LLink.getReroutes(this, link)) {
|
||||
reroute.linkIds.delete(link.id)
|
||||
}
|
||||
}
|
||||
for (const oslot of subgraphNode.outputs) {
|
||||
for (const linkId of oslot.links ?? []) {
|
||||
const link = this.links.get(linkId)
|
||||
if (!link) {
|
||||
console.warn('Broken link', oslot, linkId)
|
||||
continue
|
||||
}
|
||||
for (const reroute of LLink.getReroutes(this, link)) {
|
||||
reroute.linkIds.delete(link.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newLinks: [
|
||||
NodeId,
|
||||
number,
|
||||
NodeId,
|
||||
number,
|
||||
LinkId,
|
||||
RerouteId | undefined,
|
||||
RerouteId | undefined,
|
||||
boolean
|
||||
][] = []
|
||||
for (const [, link] of subgraphNode.subgraph._links) {
|
||||
let externalParentId: RerouteId | undefined
|
||||
if (link.origin_id === SUBGRAPH_INPUT_ID) {
|
||||
const outerLinkId = subgraphNode.inputs[link.origin_slot].link
|
||||
if (!outerLinkId) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
continue
|
||||
}
|
||||
const outerLink = this.links[outerLinkId]
|
||||
link.origin_id = outerLink.origin_id
|
||||
link.origin_slot = outerLink.origin_slot
|
||||
externalParentId = outerLink.parentId
|
||||
} else {
|
||||
const origin_id = nodeIdMap.get(link.origin_id)
|
||||
if (!origin_id) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
continue
|
||||
}
|
||||
link.origin_id = origin_id
|
||||
}
|
||||
if (link.target_id === SUBGRAPH_OUTPUT_ID) {
|
||||
for (const linkId of subgraphNode.outputs[link.target_slot].links ??
|
||||
[]) {
|
||||
const sublink = this.links[linkId]
|
||||
newLinks.push([
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
sublink.target_id,
|
||||
sublink.target_slot,
|
||||
link.id,
|
||||
link.parentId,
|
||||
sublink.parentId,
|
||||
true
|
||||
])
|
||||
sublink.parentId = undefined
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
const target_id = nodeIdMap.get(link.target_id)
|
||||
if (!target_id) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
continue
|
||||
}
|
||||
link.target_id = target_id
|
||||
}
|
||||
newLinks.push([
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
link.target_id,
|
||||
link.target_slot,
|
||||
link.id,
|
||||
link.parentId,
|
||||
externalParentId,
|
||||
false
|
||||
])
|
||||
}
|
||||
this.remove(subgraphNode)
|
||||
this.subgraphs.delete(subgraphNode.subgraph.id)
|
||||
const linkIdMap = new Map<LinkId, LinkId[]>()
|
||||
for (const newLink of newLinks) {
|
||||
let created: LLink | null | undefined
|
||||
if (newLink[0] == SUBGRAPH_INPUT_ID) {
|
||||
if (!(this instanceof Subgraph)) {
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
}
|
||||
const tnode = this._nodes_by_id[newLink[2]]
|
||||
created = this.inputNode.slots[newLink[1]].connect(
|
||||
tnode.inputs[newLink[3]],
|
||||
tnode
|
||||
)
|
||||
} else if (newLink[2] == SUBGRAPH_OUTPUT_ID) {
|
||||
if (!(this instanceof Subgraph)) {
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
}
|
||||
const tnode = this._nodes_by_id[newLink[0]]
|
||||
created = this.outputNode.slots[newLink[3]].connect(
|
||||
tnode.outputs[newLink[1]],
|
||||
tnode
|
||||
)
|
||||
} else {
|
||||
created = this._nodes_by_id[newLink[0]].connect(
|
||||
newLink[1],
|
||||
this._nodes_by_id[newLink[2]],
|
||||
newLink[3]
|
||||
)
|
||||
}
|
||||
if (!created) {
|
||||
console.error('Failed to create link')
|
||||
continue
|
||||
}
|
||||
//This is a little unwieldy since Map.has isn't a type guard
|
||||
const linkIds = linkIdMap.get(newLink[4]) ?? []
|
||||
linkIds.push(created.id)
|
||||
if (!linkIdMap.has(newLink[4])) {
|
||||
linkIdMap.set(newLink[4], linkIds)
|
||||
}
|
||||
newLink[4] = created.id
|
||||
}
|
||||
const rerouteIdMap = new Map<RerouteId, RerouteId>()
|
||||
for (const reroute of subgraphNode.subgraph.reroutes.values()) {
|
||||
if (
|
||||
reroute.parentId !== undefined &&
|
||||
rerouteIdMap.get(reroute.parentId) === undefined
|
||||
) {
|
||||
console.error('Missing Parent ID')
|
||||
}
|
||||
const migratedReroute = new Reroute(++this.state.lastRerouteId, this, [
|
||||
reroute.pos[0] + offsetX,
|
||||
reroute.pos[1] + offsetY
|
||||
])
|
||||
rerouteIdMap.set(reroute.id, migratedReroute.id)
|
||||
this.reroutes.set(migratedReroute.id, migratedReroute)
|
||||
}
|
||||
//iterate over newly created links to update reroute parentIds
|
||||
for (const newLink of newLinks) {
|
||||
const linkInstance = this.links.get(newLink[4])
|
||||
if (!linkInstance) {
|
||||
continue
|
||||
}
|
||||
let instance: Reroute | LLink | undefined = linkInstance
|
||||
let parentId: RerouteId | undefined = newLink[6]
|
||||
if (newLink[7]) {
|
||||
parentId = newLink[6]
|
||||
//TODO: recursion check/helper method? Probably exists, but wouldn't mesh with the reference tracking used by this implementation
|
||||
while (parentId) {
|
||||
instance.parentId = parentId
|
||||
instance = this.reroutes.get(parentId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
parentId = instance.parentId
|
||||
}
|
||||
}
|
||||
parentId = newLink[5]
|
||||
while (parentId) {
|
||||
const migratedId = rerouteIdMap.get(parentId)
|
||||
if (!migratedId) throw new Error('Broken Id link when unpacking')
|
||||
instance.parentId = migratedId
|
||||
instance = this.reroutes.get(migratedId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
const oldReroute = subgraphNode.subgraph.reroutes.get(parentId)
|
||||
if (!oldReroute) throw new Error('Broken Id link when unpacking')
|
||||
parentId = oldReroute.parentId
|
||||
}
|
||||
if (!newLink[7]) {
|
||||
parentId = newLink[6]
|
||||
while (parentId) {
|
||||
instance.parentId = parentId
|
||||
instance = this.reroutes.get(parentId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
parentId = instance.parentId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nodes: LGraphNode[] = []
|
||||
for (const nodeId of nodeIdMap.values()) {
|
||||
const node = this._nodes_by_id[nodeId]
|
||||
nodes.push(node)
|
||||
node._setConcreteSlots()
|
||||
node.arrange()
|
||||
}
|
||||
const reroutes = [...rerouteIdMap.values()]
|
||||
.map((i) => this.reroutes.get(i))
|
||||
.filter((x): x is Reroute => !!x)
|
||||
|
||||
this.canvasAction((c) => c.selectItems([...nodes, ...reroutes]))
|
||||
this.afterChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path of subgraph node IDs into a list of subgraph nodes.
|
||||
* Not intended to be run from subgraphs.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { toString } from 'lodash'
|
||||
import { toString } from 'es-toolkit/compat'
|
||||
|
||||
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
|
||||
@@ -3456,10 +3456,6 @@ export class LGraphCanvas
|
||||
processMouseWheel(e: WheelEvent): void {
|
||||
if (!this.graph || !this.allow_dragcanvas) return
|
||||
|
||||
// TODO: Mouse wheel zoom rewrite
|
||||
// @ts-expect-error wheelDeltaY is non-standard property on WheelEvent
|
||||
const delta = e.wheelDeltaY ?? e.detail * -60
|
||||
|
||||
this.adjustMouseEvent(e)
|
||||
|
||||
const pos: Point = [e.clientX, e.clientY]
|
||||
@@ -3467,35 +3463,34 @@ export class LGraphCanvas
|
||||
|
||||
let { scale } = this.ds
|
||||
|
||||
if (
|
||||
LiteGraph.canvasNavigationMode === 'legacy' ||
|
||||
(LiteGraph.canvasNavigationMode === 'standard' && e.ctrlKey)
|
||||
) {
|
||||
if (delta > 0) {
|
||||
scale *= this.zoom_speed
|
||||
} else if (delta < 0) {
|
||||
scale *= 1 / this.zoom_speed
|
||||
}
|
||||
this.ds.changeScale(scale, [e.clientX, e.clientY])
|
||||
} else if (
|
||||
LiteGraph.macTrackpadGestures &&
|
||||
(!LiteGraph.macGesturesRequireMac || navigator.userAgent.includes('Mac'))
|
||||
) {
|
||||
if (e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {
|
||||
if (e.deltaY > 0) {
|
||||
scale *= 1 / this.zoom_speed
|
||||
} else if (e.deltaY < 0) {
|
||||
scale *= this.zoom_speed
|
||||
}
|
||||
this.ds.changeScale(scale, [e.clientX, e.clientY])
|
||||
} else if (e.ctrlKey) {
|
||||
// Detect if this is a trackpad gesture or mouse wheel
|
||||
const isTrackpad = this.pointer.isTrackpadGesture(e)
|
||||
|
||||
if (e.ctrlKey || LiteGraph.canvasNavigationMode === 'legacy') {
|
||||
// Legacy mode or standard mode with ctrl - use wheel for zoom
|
||||
if (isTrackpad) {
|
||||
// Trackpad gesture - use smooth scaling
|
||||
scale *= 1 + e.deltaY * (1 - this.zoom_speed) * 0.18
|
||||
this.ds.changeScale(scale, [e.clientX, e.clientY], false)
|
||||
} else if (e.shiftKey) {
|
||||
this.ds.offset[0] -= e.deltaY * 1.18 * (1 / scale)
|
||||
} else {
|
||||
this.ds.offset[0] -= e.deltaX * 1.18 * (1 / scale)
|
||||
this.ds.offset[1] -= e.deltaY * 1.18 * (1 / scale)
|
||||
// Mouse wheel - use stepped scaling
|
||||
if (e.deltaY < 0) {
|
||||
scale *= this.zoom_speed
|
||||
} else if (e.deltaY > 0) {
|
||||
scale *= 1 / this.zoom_speed
|
||||
}
|
||||
this.ds.changeScale(scale, [e.clientX, e.clientY])
|
||||
}
|
||||
} else {
|
||||
// Standard mode without ctrl - use wheel / gestures to pan
|
||||
// Trackpads and mice work on significantly different scales
|
||||
const factor = isTrackpad ? 0.18 : 0.008_333
|
||||
|
||||
if (!isTrackpad && e.shiftKey && e.deltaX === 0) {
|
||||
this.ds.offset[0] -= e.deltaY * (1 + factor) * (1 / scale)
|
||||
} else {
|
||||
this.ds.offset[0] -= e.deltaX * (1 + factor) * (1 / scale)
|
||||
this.ds.offset[1] -= e.deltaY * (1 + factor) * (1 / scale)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ export class LiteGraphGlobal {
|
||||
]
|
||||
|
||||
/**
|
||||
* @deprecated Removed; has no effect.
|
||||
* If `true`, mouse wheel events will be interpreted as trackpad gestures.
|
||||
* Tested on MacBook M4 Pro.
|
||||
* @default false
|
||||
@@ -292,6 +293,7 @@ export class LiteGraphGlobal {
|
||||
macTrackpadGestures: boolean = false
|
||||
|
||||
/**
|
||||
* @deprecated Removed; has no effect.
|
||||
* If both this setting and {@link macTrackpadGestures} are `true`, trackpad gestures will
|
||||
* only be enabled when the browser user agent includes "Mac".
|
||||
* @default true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type {
|
||||
ReadOnlyRect,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pull } from 'lodash'
|
||||
import { pull } from 'es-toolkit/compat'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type { IKnobWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { getWidgetStep } from '@/lib/litegraph/src/utils/widget'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type { ISliderWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import { beforeEach, describe, expect, vi } from 'vitest'
|
||||
|
||||
import { LiteGraphGlobal } from '@/lib/litegraph/src/LiteGraphGlobal'
|
||||
|
||||
197
src/lib/litegraph/test/subgraph/SubgraphConversion.test.ts
Normal file
197
src/lib/litegraph/test/subgraph/SubgraphConversion.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ISlotType,
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
function createNode(
|
||||
graph: LGraph,
|
||||
inputs: ISlotType[] = [],
|
||||
outputs: ISlotType[] = [],
|
||||
title?: string
|
||||
) {
|
||||
const type = JSON.stringify({ inputs, outputs })
|
||||
if (!LiteGraph.registered_node_types[type]) {
|
||||
class testnode extends LGraphNode {
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
let i_count = 0
|
||||
for (const input of inputs) this.addInput('input_' + i_count++, input)
|
||||
let o_count = 0
|
||||
for (const output of outputs)
|
||||
this.addOutput('output_' + o_count++, output)
|
||||
}
|
||||
}
|
||||
LiteGraph.registered_node_types[type] = testnode
|
||||
}
|
||||
const node = LiteGraph.createNode(type, title)
|
||||
if (!node) {
|
||||
throw new Error('Failed to create node')
|
||||
}
|
||||
graph.add(node)
|
||||
return node
|
||||
}
|
||||
describe('SubgraphConversion', () => {
|
||||
describe('Subgraph Unpacking Functionality', () => {
|
||||
it('Should keep interior nodes and links', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const node1 = createNode(subgraph, [], ['number'])
|
||||
const node2 = createNode(subgraph, ['number'])
|
||||
node1.connect(0, node2, 0)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.nodes.length).toBe(2)
|
||||
expect(graph.links.size).toBe(1)
|
||||
})
|
||||
it('Should merge boundry links', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'value', type: 'number' }],
|
||||
outputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const innerNode1 = createNode(subgraph, [], ['number'])
|
||||
const innerNode2 = createNode(subgraph, ['number'], [])
|
||||
subgraph.inputNode.slots[0].connect(innerNode2.inputs[0], innerNode2)
|
||||
subgraph.outputNode.slots[0].connect(innerNode1.outputs[0], innerNode1)
|
||||
|
||||
const outerNode1 = createNode(graph, [], ['number'])
|
||||
const outerNode2 = createNode(graph, ['number'])
|
||||
outerNode1.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, outerNode2, 0)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.nodes.length).toBe(4)
|
||||
expect(graph.links.size).toBe(2)
|
||||
})
|
||||
it('Should keep reroutes', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner = createNode(subgraph, [], ['number'])
|
||||
const innerLink = subgraph.outputNode.slots[0].connect(
|
||||
inner.outputs[0],
|
||||
inner
|
||||
)
|
||||
assert(innerLink)
|
||||
|
||||
const outer = createNode(graph, ['number'])
|
||||
const outerLink = subgraphNode.connect(0, outer, 0)
|
||||
assert(outerLink)
|
||||
|
||||
subgraph.createReroute([10, 10], innerLink)
|
||||
graph.createReroute([10, 10], outerLink)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.reroutes.size).toBe(2)
|
||||
})
|
||||
it('Should map reroutes onto split outputs', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [
|
||||
{ name: 'value1', type: 'number' },
|
||||
{ name: 'value2', type: 'number' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner = createNode(subgraph, [], ['number', 'number'])
|
||||
const innerLink1 = subgraph.outputNode.slots[0].connect(
|
||||
inner.outputs[0],
|
||||
inner
|
||||
)
|
||||
const innerLink2 = subgraph.outputNode.slots[1].connect(
|
||||
inner.outputs[1],
|
||||
inner
|
||||
)
|
||||
const outer1 = createNode(graph, ['number'])
|
||||
const outer2 = createNode(graph, ['number'])
|
||||
const outer3 = createNode(graph, ['number'])
|
||||
const outerLink1 = subgraphNode.connect(0, outer1, 0)
|
||||
assert(innerLink1 && innerLink2 && outerLink1)
|
||||
subgraphNode.connect(0, outer2, 0)
|
||||
subgraphNode.connect(1, outer3, 0)
|
||||
|
||||
subgraph.createReroute([10, 10], innerLink1)
|
||||
subgraph.createReroute([10, 20], innerLink2)
|
||||
graph.createReroute([10, 10], outerLink1)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.reroutes.size).toBe(3)
|
||||
expect(graph.links.size).toBe(3)
|
||||
let linkRefCount = 0
|
||||
for (const reroute of graph.reroutes.values()) {
|
||||
linkRefCount += reroute.linkIds.size
|
||||
}
|
||||
expect(linkRefCount).toBe(4)
|
||||
})
|
||||
it('Should map reroutes onto split inputs', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: 'value1', type: 'number' },
|
||||
{ name: 'value2', type: 'number' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner1 = createNode(subgraph, ['number', 'number'])
|
||||
const inner2 = createNode(subgraph, ['number'])
|
||||
const innerLink1 = subgraph.inputNode.slots[0].connect(
|
||||
inner1.inputs[0],
|
||||
inner1
|
||||
)
|
||||
const innerLink2 = subgraph.inputNode.slots[1].connect(
|
||||
inner1.inputs[1],
|
||||
inner1
|
||||
)
|
||||
const innerLink3 = subgraph.inputNode.slots[1].connect(
|
||||
inner2.inputs[0],
|
||||
inner2
|
||||
)
|
||||
assert(innerLink1 && innerLink2 && innerLink3)
|
||||
const outer = createNode(graph, [], ['number'])
|
||||
const outerLink1 = outer.connect(0, subgraphNode, 0)
|
||||
const outerLink2 = outer.connect(0, subgraphNode, 1)
|
||||
assert(outerLink1 && outerLink2)
|
||||
|
||||
graph.createReroute([10, 10], outerLink1)
|
||||
graph.createReroute([10, 20], outerLink2)
|
||||
subgraph.createReroute([10, 10], innerLink1)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.reroutes.size).toBe(3)
|
||||
expect(graph.links.size).toBe(3)
|
||||
let linkRefCount = 0
|
||||
for (const reroute of graph.reroutes.values()) {
|
||||
linkRefCount += reroute.linkIds.size
|
||||
}
|
||||
expect(linkRefCount).toBe(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -77,9 +77,6 @@
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "تبديل الخريطة المصغرة في اللوحة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "تثبيت/إلغاء تثبيت العناصر المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "تجاوز/إلغاء تجاوز العقد المحددة"
|
||||
},
|
||||
@@ -92,6 +89,9 @@
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
|
||||
"label": "تثبيت/إلغاء تثبيت العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "تثبيت/إلغاء تثبيت العناصر المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ZoomIn": {
|
||||
"label": "تكبير"
|
||||
},
|
||||
@@ -122,6 +122,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "تحويل التحديد إلى رسم فرعي"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "الخروج من الرسم البياني الفرعي"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "ضبط المجموعة على المحتويات"
|
||||
},
|
||||
@@ -233,9 +236,6 @@
|
||||
"Workspace_ToggleBottomPanel": {
|
||||
"label": "تبديل اللوحة السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "عرض مربع حوار اختصارات لوحة المفاتيح"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_command-terminal": {
|
||||
"label": "تبديل لوحة الطرفية السفلية"
|
||||
},
|
||||
@@ -248,6 +248,9 @@
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
|
||||
"label": "تبديل لوحة تحكم العرض السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "عرض مربع حوار اختصارات لوحة المفاتيح"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "تبديل وضع التركيز"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -19,11 +19,11 @@
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "نمط النافذة",
|
||||
"tooltip": "مخصص: استبدال شريط عنوان النظام بالقائمة العلوية لـ ComfyUI",
|
||||
"options": {
|
||||
"default": "افتراضي",
|
||||
"custom": "مخصص"
|
||||
}
|
||||
"custom": "مخصص",
|
||||
"default": "افتراضي"
|
||||
},
|
||||
"tooltip": "مخصص: استبدال شريط عنوان النظام بالقائمة العلوية لـ ComfyUI"
|
||||
},
|
||||
"Comfy_Canvas_BackgroundImage": {
|
||||
"name": "صورة خلفية اللوحة",
|
||||
@@ -32,8 +32,8 @@
|
||||
"Comfy_Canvas_NavigationMode": {
|
||||
"name": "وضع تنقل اللوحة",
|
||||
"options": {
|
||||
"Standard (New)": "قياسي (جديد)",
|
||||
"Left-Click Pan (Legacy)": "سحب بالنقر الأيسر (قديم)"
|
||||
"Left-Click Pan (Legacy)": "سحب بالنقر الأيسر (قديم)",
|
||||
"Standard (New)": "قياسي (جديد)"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
@@ -42,6 +42,9 @@
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "طلب التأكيد عند مسح سير العمل"
|
||||
},
|
||||
"Comfy_DOMClippingEnabled": {
|
||||
"name": "تمكين قص عناصر DOM (قد يقلل التمكين من الأداء)"
|
||||
},
|
||||
"Comfy_DevMode": {
|
||||
"name": "تمكين خيارات وضع المطور (حفظ API، إلخ)"
|
||||
},
|
||||
@@ -52,9 +55,6 @@
|
||||
"Comfy_DisableSliders": {
|
||||
"name": "تعطيل منزلقات أدوات العقد"
|
||||
},
|
||||
"Comfy_DOMClippingEnabled": {
|
||||
"name": "تمكين قص عناصر DOM (قد يقلل التمكين من الأداء)"
|
||||
},
|
||||
"Comfy_EditAttention_Delta": {
|
||||
"name": "دقة تحكم +Ctrl فوق/تحت"
|
||||
},
|
||||
@@ -80,43 +80,43 @@
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "علامات منتصف الروابط",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Arrow": "سهم",
|
||||
"Circle": "دائرة",
|
||||
"Arrow": "سهم"
|
||||
"None": "لا شيء"
|
||||
}
|
||||
},
|
||||
"Comfy_Graph_ZoomSpeed": {
|
||||
"name": "سرعة تكبير اللوحة"
|
||||
},
|
||||
"Comfy_Group_DoubleClickTitleToEdit": {
|
||||
"name": "انقر مزدوج على عنوان المجموعة للتحرير"
|
||||
},
|
||||
"Comfy_GroupSelectedNodes_Padding": {
|
||||
"name": "تباعد حول العقد المحددة في المجموعة"
|
||||
},
|
||||
"Comfy_Group_DoubleClickTitleToEdit": {
|
||||
"name": "انقر مزدوج على عنوان المجموعة للتحرير"
|
||||
},
|
||||
"Comfy_LinkRelease_Action": {
|
||||
"name": "الإجراء عند تحرير الرابط (بدون مفتاح تعديل)",
|
||||
"options": {
|
||||
"context menu": "قائمة السياق",
|
||||
"search box": "صندوق البحث",
|
||||
"no action": "لا إجراء"
|
||||
"no action": "لا إجراء",
|
||||
"search box": "صندوق البحث"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRelease_ActionShift": {
|
||||
"name": "الإجراء عند تحرير الرابط (Shift)",
|
||||
"options": {
|
||||
"context menu": "قائمة السياق",
|
||||
"search box": "صندوق البحث",
|
||||
"no action": "لا إجراء"
|
||||
"no action": "لا إجراء",
|
||||
"search box": "صندوق البحث"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRenderMode": {
|
||||
"name": "وضع عرض الروابط",
|
||||
"options": {
|
||||
"Straight": "مستقيم",
|
||||
"Hidden": "مخفي",
|
||||
"Linear": "خطي",
|
||||
"Spline": "منحنى",
|
||||
"Hidden": "مخفي"
|
||||
"Straight": "مستقيم"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
@@ -129,11 +129,11 @@
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "نوع الكاميرا الابتدائي",
|
||||
"tooltip": "يحدد ما إذا كانت الكاميرا منظور أو متعامدة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد. يمكن تعديل هذا الإعداد لكل عنصر بعد الإنشاء.",
|
||||
"options": {
|
||||
"perspective": "منظور",
|
||||
"orthographic": "متعامد"
|
||||
}
|
||||
"orthographic": "متعامد",
|
||||
"perspective": "منظور"
|
||||
},
|
||||
"tooltip": "يحدد ما إذا كانت الكاميرا منظور أو متعامدة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد. يمكن تعديل هذا الإعداد لكل عنصر بعد الإنشاء."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "زيادة تعديل الضوء",
|
||||
@@ -180,12 +180,64 @@
|
||||
},
|
||||
"Comfy_ModelLibrary_NameFormat": {
|
||||
"name": "اسم العرض في شجرة مكتبة النماذج",
|
||||
"tooltip": "اختر \"اسم الملف\" لعرض اسم الملف المبسط بدون المجلد أو الامتداد \".safetensors\" في قائمة النماذج. اختر \"العنوان\" لعرض عنوان بيانات النموذج القابل للتكوين.",
|
||||
"options": {
|
||||
"filename": "اسم الملف",
|
||||
"title": "العنوان"
|
||||
},
|
||||
"tooltip": "اختر \"اسم الملف\" لعرض اسم الملف المبسط بدون المجلد أو الامتداد \".safetensors\" في قائمة النماذج. اختر \"العنوان\" لعرض عنوان بيانات النموذج القابل للتكوين."
|
||||
},
|
||||
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||
"name": "وضع شارة معرف العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
|
||||
"name": "وضع شارة دورة حياة العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeSourceBadgeMode": {
|
||||
"name": "وضع شارة مصدر العقدة",
|
||||
"options": {
|
||||
"Hide built-in": "إخفاء المدمج",
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_ShowApiPricing": {
|
||||
"name": "عرض شارة تسعير عقدة API"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "تنفيذ مربع بحث العقدة",
|
||||
"options": {
|
||||
"default": "افتراضي",
|
||||
"litegraph (legacy)": "لايت جراف (قديم)"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_NodePreview": {
|
||||
"name": "معاينة العقدة",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowCategory": {
|
||||
"name": "عرض فئة العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowIdName": {
|
||||
"name": "عرض اسم معرف العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
|
||||
"name": "عرض تكرار العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "عدد اقتراحات العقد",
|
||||
"tooltip": "خاص بمربع بحث / قائمة السياق في لايت جراف فقط"
|
||||
},
|
||||
"Comfy_Node_AllowImageSizeDraw": {
|
||||
"name": "عرض العرض × الارتفاع تحت معاينة الصورة"
|
||||
},
|
||||
@@ -218,58 +270,6 @@
|
||||
"name": "تثبيت يبرز العقدة",
|
||||
"tooltip": "عند سحب رابط فوق عقدة تحتوي على فتحة إدخال صالحة، يتم تمييز العقدة"
|
||||
},
|
||||
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||
"name": "وضع شارة معرف العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
|
||||
"name": "وضع شارة دورة حياة العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeSourceBadgeMode": {
|
||||
"name": "وضع شارة مصدر العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل",
|
||||
"Hide built-in": "إخفاء المدمج"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_ShowApiPricing": {
|
||||
"name": "عرض شارة تسعير عقدة API"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "تنفيذ مربع بحث العقدة",
|
||||
"options": {
|
||||
"default": "افتراضي",
|
||||
"litegraph (legacy)": "لايت جراف (قديم)"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_NodePreview": {
|
||||
"name": "معاينة العقدة",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowCategory": {
|
||||
"name": "عرض فئة العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowIdName": {
|
||||
"name": "عرض اسم معرف العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
|
||||
"name": "عرض تكرار العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "عدد اقتراحات العقد",
|
||||
"tooltip": "خاص بمربع بحث / قائمة السياق في لايت جراف فقط"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "عرض تحديثات الإصدار",
|
||||
"tooltip": "عرض التحديثات للنماذج الجديدة والميزات الرئيسية."
|
||||
@@ -293,14 +293,14 @@
|
||||
"Comfy_PromptFilename": {
|
||||
"name": "طلب اسم الملف عند حفظ سير العمل"
|
||||
},
|
||||
"Comfy_Queue_MaxHistoryItems": {
|
||||
"name": "حجم تاريخ قائمة الانتظار",
|
||||
"tooltip": "العدد الأقصى للمهام المعروضة في تاريخ قائمة الانتظار."
|
||||
},
|
||||
"Comfy_QueueButton_BatchCountLimit": {
|
||||
"name": "حد عدد الدُفعات",
|
||||
"tooltip": "العدد الأقصى للمهام التي تضاف إلى القائمة بنقرة زر واحدة"
|
||||
},
|
||||
"Comfy_Queue_MaxHistoryItems": {
|
||||
"name": "حجم تاريخ قائمة الانتظار",
|
||||
"tooltip": "العدد الأقصى للمهام المعروضة في تاريخ قائمة الانتظار."
|
||||
},
|
||||
"Comfy_Sidebar_Location": {
|
||||
"name": "موقع الشريط الجانبي",
|
||||
"options": {
|
||||
@@ -333,23 +333,23 @@
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
"name": "استخدام القائمة الجديدة",
|
||||
"tooltip": "موقع شريط القائمة. على الأجهزة المحمولة، تُعرض القائمة دائمًا في الأعلى.",
|
||||
"options": {
|
||||
"Bottom": "أسفل",
|
||||
"Disabled": "معطل",
|
||||
"Top": "أعلى",
|
||||
"Bottom": "أسفل"
|
||||
}
|
||||
"Top": "أعلى"
|
||||
},
|
||||
"tooltip": "موقع شريط القائمة. على الأجهزة المحمولة، تُعرض القائمة دائمًا في الأعلى."
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "التحقق من صحة سير العمل"
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "وضع التحكم في الودجت",
|
||||
"tooltip": "يتحكم في متى يتم تحديث قيم الودجت (توليد عشوائي/زيادة/نقصان)، إما قبل إدراج الطلب في الطابور أو بعده.",
|
||||
"options": {
|
||||
"before": "قبل",
|
||||
"after": "بعد"
|
||||
}
|
||||
"after": "بعد",
|
||||
"before": "قبل"
|
||||
},
|
||||
"tooltip": "يتحكم في متى يتم تحديث قيم الودجت (توليد عشوائي/زيادة/نقصان)، إما قبل إدراج الطلب في الطابور أو بعده."
|
||||
},
|
||||
"Comfy_Window_UnloadConfirmation": {
|
||||
"name": "عرض تأكيد عند إغلاق النافذة"
|
||||
@@ -357,8 +357,8 @@
|
||||
"Comfy_Workflow_AutoSave": {
|
||||
"name": "الحفظ التلقائي",
|
||||
"options": {
|
||||
"off": "إيقاف",
|
||||
"after delay": "بعد تأخير"
|
||||
"after delay": "بعد تأخير",
|
||||
"off": "إيقاف"
|
||||
}
|
||||
},
|
||||
"Comfy_Workflow_AutoSaveDelay": {
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Exit Subgraph"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Unpack the selected Subgraph"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Fit Group To Contents"
|
||||
},
|
||||
|
||||
@@ -145,7 +145,8 @@
|
||||
"stopRecording": "Stop Recording",
|
||||
"micPermissionDenied": "Microphone permission denied",
|
||||
"noAudioRecorded": "No audio recorded",
|
||||
"nodesRunning": "nodes running"
|
||||
"nodesRunning": "nodes running",
|
||||
"duplicate": "Duplicate"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "Deshabilitando",
|
||||
"dismiss": "Descartar",
|
||||
"download": "Descargar",
|
||||
"duplicate": "Duplicar",
|
||||
"edit": "Editar",
|
||||
"empty": "Vacío",
|
||||
"enableAll": "Habilitar todo",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "Désactivation",
|
||||
"dismiss": "Fermer",
|
||||
"download": "Télécharger",
|
||||
"duplicate": "Dupliquer",
|
||||
"edit": "Modifier",
|
||||
"empty": "Vide",
|
||||
"enableAll": "Activer tout",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "無効化",
|
||||
"dismiss": "閉じる",
|
||||
"download": "ダウンロード",
|
||||
"duplicate": "複製",
|
||||
"edit": "編集",
|
||||
"empty": "空",
|
||||
"enableAll": "すべて有効にする",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "비활성화 중",
|
||||
"dismiss": "닫기",
|
||||
"download": "다운로드",
|
||||
"duplicate": "복제",
|
||||
"edit": "편집",
|
||||
"empty": "비어 있음",
|
||||
"enableAll": "모두 활성화",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "Отключение",
|
||||
"dismiss": "Закрыть",
|
||||
"download": "Скачать",
|
||||
"duplicate": "Дублировать",
|
||||
"edit": "Редактировать",
|
||||
"empty": "Пусто",
|
||||
"enableAll": "Включить все",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "停用中",
|
||||
"dismiss": "關閉",
|
||||
"download": "下載",
|
||||
"duplicate": "複製",
|
||||
"edit": "編輯",
|
||||
"empty": "空",
|
||||
"enableAll": "全部啟用",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "禁用中",
|
||||
"dismiss": "關閉",
|
||||
"download": "下载",
|
||||
"duplicate": "复制",
|
||||
"edit": "编辑",
|
||||
"empty": "空",
|
||||
"enableAll": "启用全部",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import * as jsondiffpatch from 'jsondiffpatch'
|
||||
import _ from 'lodash'
|
||||
import log from 'loglevel'
|
||||
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { type Component, toRaw } from 'vue'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import {
|
||||
type INodeOutputSlot,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
|
||||
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
|
||||
@@ -805,6 +805,15 @@ export const useLitegraphService = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (this instanceof SubgraphNode) {
|
||||
options.unshift({
|
||||
content: 'Unpack Subgraph',
|
||||
callback: () => {
|
||||
useNodeOutputStore().revokeSubgraphPreviews(this)
|
||||
this.graph.unpackSubgraph(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
SearchResponse
|
||||
} from 'algoliasearch/dist/lite/browser'
|
||||
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
|
||||
import { memoize, omit } from 'lodash'
|
||||
import { memoize, omit } from 'es-toolkit/compat'
|
||||
|
||||
import {
|
||||
MIN_CHARS_FOR_SUGGESTIONS_ALGOLIA,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import QuickLRU from '@alloc/quick-lru'
|
||||
import { partition } from 'lodash'
|
||||
import { partition } from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// We should consider moving to https://primevue.org/dynamicdialog/ once everything is in Vue.
|
||||
// Currently we need to bridge between legacy app code and Vue app with a Pinia store.
|
||||
import { merge } from 'lodash'
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { DialogPassThroughOptions } from 'primevue/dialog'
|
||||
import { type Component, markRaw, ref } from 'vue'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
ExecutedWsMessage,
|
||||
ResultItem,
|
||||
@@ -268,6 +268,20 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
app.nodePreviewImages = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke all preview of a subgraph node and the graph it contains.
|
||||
* Does not recurse to contents of nested subgraphs.
|
||||
*/
|
||||
function revokeSubgraphPreviews(subgraphNode: SubgraphNode) {
|
||||
const graphId = subgraphNode.graph.isRootGraph
|
||||
? ''
|
||||
: subgraphNode.graph.id + ':'
|
||||
revokePreviewsByLocatorId(graphId + subgraphNode.id)
|
||||
for (const node of subgraphNode.subgraph.nodes) {
|
||||
revokePreviewsByLocatorId(subgraphNode.subgraph.id + node.id)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getNodeOutputs,
|
||||
getNodeImageUrls,
|
||||
@@ -279,6 +293,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
setNodePreviewsByNodeId,
|
||||
revokePreviewsByExecutionId,
|
||||
revokeAllPreviews,
|
||||
revokeSubgraphPreviews,
|
||||
getPreviewParam
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Ref, computed, ref, toRaw } from 'vue'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, toRaw } from 'vue'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { groupBy } from 'lodash'
|
||||
import { groupBy } from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memoize } from 'lodash'
|
||||
import { memoize } from 'es-toolkit/compat'
|
||||
|
||||
type RGB = { r: number; g: number; b: number }
|
||||
type HSL = { h: number; s: number; l: number }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import { ColorOption, LGraph, Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import type {
|
||||
ComfyLinkObject,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import type {
|
||||
ComboInputSpec,
|
||||
|
||||
@@ -15,7 +15,7 @@ interface ColorTestCase {
|
||||
|
||||
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
|
||||
|
||||
vi.mock('lodash', () => ({
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
memoize: (fn: any) => fn
|
||||
}))
|
||||
|
||||
|
||||
@@ -64,6 +64,16 @@ describe('useNodePricing', () => {
|
||||
})
|
||||
|
||||
describe('dynamic pricing - KlingTextToVideoNode', () => {
|
||||
it('should return high price for kling-v2-1-master model', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('KlingTextToVideoNode', [
|
||||
{ name: 'mode', value: 'standard / 5s / v2-1-master' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.40/Run')
|
||||
})
|
||||
|
||||
it('should return high price for kling-v2-master model', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('KlingTextToVideoNode', [
|
||||
@@ -219,6 +229,49 @@ describe('useNodePricing', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
|
||||
it('should return $0.28 for 6s duration and 768P resolution', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('MinimaxHailuoVideoNode', [
|
||||
{ name: 'duration', value: '6' },
|
||||
{ name: 'resolution', value: '768P' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.28/Run')
|
||||
})
|
||||
|
||||
it('should return $0.60 for 10s duration and 768P resolution', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('MinimaxHailuoVideoNode', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'resolution', value: '768P' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.56/Run')
|
||||
})
|
||||
|
||||
it('should return $0.49 for 6s duration and 1080P resolution', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('MinimaxHailuoVideoNode', [
|
||||
{ name: 'duration', value: '6' },
|
||||
{ name: 'resolution', value: '1080P' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.49/Run')
|
||||
})
|
||||
|
||||
it('should return range when duration widget is missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('MinimaxHailuoVideoNode', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.28-0.56/Run (varies with resolution & duration)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - OpenAIDalle2', () => {
|
||||
it('should return $0.02 for 1024x1024 size', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
@@ -1384,11 +1437,19 @@ describe('useNodePricing', () => {
|
||||
const testCases = [
|
||||
{
|
||||
model: 'gemini-2.5-pro-preview-05-06',
|
||||
expected: '$0.00016/$0.0006 per 1K tokens'
|
||||
expected: '$0.00125/$0.01 per 1K tokens'
|
||||
},
|
||||
{
|
||||
model: 'gemini-2.5-pro',
|
||||
expected: '$0.00125/$0.01 per 1K tokens'
|
||||
},
|
||||
{
|
||||
model: 'gemini-2.5-flash-preview-04-17',
|
||||
expected: '$0.00125/$0.01 per 1K tokens'
|
||||
expected: '$0.0003/$0.0025 per 1K tokens'
|
||||
},
|
||||
{
|
||||
model: 'gemini-2.5-flash',
|
||||
expected: '$0.0003/$0.0025 per 1K tokens'
|
||||
},
|
||||
{ model: 'unknown-gemini-model', expected: 'Token-based' }
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@ This guide covers patterns and examples for unit testing utilities, composables,
|
||||
2. [Working with LiteGraph and Nodes](#working-with-litegraph-and-nodes)
|
||||
3. [Working with Workflow JSON Files](#working-with-workflow-json-files)
|
||||
4. [Mocking the API Object](#mocking-the-api-object)
|
||||
5. [Mocking Lodash Functions](#mocking-lodash-functions)
|
||||
5. [Mocking Utility Functions](#mocking-utility-functions)
|
||||
6. [Testing with Debounce and Throttle](#testing-with-debounce-and-throttle)
|
||||
7. [Mocking Node Definitions](#mocking-node-definitions)
|
||||
|
||||
@@ -147,17 +147,17 @@ it('should subscribe to logs API', () => {
|
||||
|
||||
## Mocking Lodash Functions
|
||||
|
||||
Mocking lodash functions like debounce:
|
||||
Mocking utility functions like debounce:
|
||||
|
||||
```typescript
|
||||
// Mock debounce to execute immediately
|
||||
import { debounce } from 'lodash-es'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
|
||||
vi.mock('lodash-es', () => ({
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: vi.fn((fn) => {
|
||||
// Return function that calls the input function immediately
|
||||
const mockDebounced = (...args: any[]) => fn(...args)
|
||||
// Add cancel method that lodash debounced functions have
|
||||
// Add cancel method that debounced functions have
|
||||
mockDebounced.cancel = vi.fn()
|
||||
return mockDebounced
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user