Compare commits

..

1 Commits

Author SHA1 Message Date
bymyself
ed9e8a2144 chore: enforce vue block order in pre-commit 2025-09-19 22:25:24 -07:00
141 changed files with 2739 additions and 3600 deletions

View File

@@ -32,10 +32,11 @@ jobs:
with:
repository: Comfy-Org/ComfyUI_frontend
path: ComfyUI_frontend
- name: Copy ComfyUI_devtools from frontend repo
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI_devtools
path: ComfyUI/custom_nodes/ComfyUI_devtools
- name: Checkout custom node repository
uses: actions/checkout@v4
with:

View File

@@ -27,10 +27,12 @@ jobs:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
- name: Copy ComfyUI_devtools from frontend repo
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
- name: Install pnpm
uses: pnpm/action-setup@v4

1
.gitignore vendored
View File

@@ -44,7 +44,6 @@ components.d.ts
tests-ui/data/*
tests-ui/ComfyUI_examples
tests-ui/workflows/examples
coverage/
# Browser tests
/test-results/

View File

@@ -1,61 +1,17 @@
# Desktop/Electron
/src/types/desktop/ @webfiltered
/src/constants/desktopDialogs.ts @webfiltered
/src/constants/desktopMaintenanceTasks.ts @webfiltered
/src/stores/electronDownloadStore.ts @webfiltered
/src/extensions/core/electronAdapter.ts @webfiltered
/src/views/DesktopDialogView.vue @webfiltered
/src/components/install/ @webfiltered
/src/components/maintenance/ @webfiltered
/vite.electron.config.mts @webfiltered
# Admins
* @Comfy-Org/comfy_frontend_devs
# Common UI Components
/src/components/chip/ @viva-jinyi
/src/components/card/ @viva-jinyi
/src/components/button/ @viva-jinyi
/src/components/input/ @viva-jinyi
# Maintainers
*.md @Comfy-Org/comfy_maintainer
/tests-ui/ @Comfy-Org/comfy_maintainer
/browser_tests/ @Comfy-Org/comfy_maintainer
/.env_example @Comfy-Org/comfy_maintainer
# Topbar
/src/components/topbar/ @pythongosssss
# Translations (AIGODLIKE team + shinshin86)
/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer
# Thumbnail
/src/renderer/core/thumbnail/ @pythongosssss
# Load 3D extension
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
# Legacy UI
/scripts/ui/ @pythongosssss
# Link rendering
/src/renderer/core/canvas/links/ @benceruleanlu
# Node help system
/src/utils/nodeHelpUtil.ts @benceruleanlu
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
/src/services/nodeHelpService.ts @benceruleanlu
# Selection toolbox
/src/components/graph/selectionToolbox/ @Myestery
# Minimap
/src/renderer/extensions/minimap/ @jtydhr88
# Assets
/src/platform/assets/ @arjansingh
# Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki
# Mask Editor
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp
# 3D
/src/extensions/core/load3d.ts @jtydhr88
/src/components/load3d/ @jtydhr88
# Manager
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
# Translations
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer
# Mask Editor extension
/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs

View File

@@ -16,14 +16,9 @@ Without this flag, parallel tests will conflict and fail randomly.
### ComfyUI devtools
ComfyUI_devtools is now included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory.
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
For local development, copy the devtools files to your ComfyUI installation:
```bash
cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
```
### Node.js & Playwright Prerequisites
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,44 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Vue Nodes - LOD', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.loadWorkflow('default')
})
test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => {
await comfyPage.vueNodes.waitForNodes()
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
expect(initialNodeCount).toBeGreaterThan(0)
await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-default.png')
const vueNodesContainer = comfyPage.vueNodes.nodes
const textboxesInNodes = vueNodesContainer.getByRole('textbox')
const buttonsInNodes = vueNodesContainer.getByRole('button')
await expect(textboxesInNodes.first()).toBeVisible()
await expect(buttonsInNodes.first()).toBeVisible()
await comfyPage.zoom(120, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-lod-active.png')
await expect(textboxesInNodes.first()).toBeHidden()
await expect(buttonsInNodes.first()).toBeHidden()
await comfyPage.zoom(-120, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-lod-inactive.png'
)
await expect(textboxesInNodes.first()).toBeVisible()
await expect(buttonsInNodes.first()).toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -1,47 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Selection', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
const modifiers = [
{ key: 'Control', name: 'ctrl' },
{ key: 'Shift', name: 'shift' }
] as const
for (const { key: modifier, name } of modifiers) {
test(`should allow selecting multiple nodes with ${name}+click`, async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Empty Latent Image').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
await comfyPage.page.getByText('KSampler').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(3)
})
test(`should allow de-selecting nodes with ${name}+click`, async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Load Checkpoint').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
})
}
})

View File

@@ -1,49 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const BYPASS_HOTKEY = 'Control+b'
const BYPASS_CLASS = /before:bg-bypass\/60/
test.describe('Vue Node Bypass', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should allow toggling bypass on a selected node with hotkey', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'Load Checkpoint'
})
await checkpointNode.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
})
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'Load Checkpoint'
})
const ksamplerNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'KSampler'
})
await checkpointNode.getByText('Load Checkpoint').click()
await ksamplerNode.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await expect(ksamplerNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
await expect(ksamplerNode).not.toHaveClass(BYPASS_CLASS)
})
})

View File

@@ -3,6 +3,7 @@
"compilerOptions": {
/* Test files should not be compiled */
"noEmit": true,
// "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true

View File

@@ -1,17 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
/* Build scripts configuration */
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true
},
"include": [
"**/*.ts"
]
}

View File

@@ -33,13 +33,7 @@ export default defineConfig([
},
parserOptions: {
parser: tseslint.parser,
projectService: {
allowDefaultProject: [
'vite.config.mts',
'vite.electron.config.mts',
'vite.types.config.mts'
]
},
projectService: true,
tsConfigRootDir: import.meta.dirname,
ecmaVersion: 2020,
sourceType: 'module',
@@ -83,25 +77,12 @@ export default defineConfig([
'@typescript-eslint/prefer-as-const': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/no-empty-object-type': [
'error',
{
allowInterfaces: 'always'
}
],
'unused-imports/no-unused-imports': 'error',
'vue/no-v-html': 'off',
// Enforce dark-theme: instead of dark: prefix
'vue/no-restricted-class': ['error', '/^dark:/'],
'vue/multi-word-component-names': 'off', // TODO: fix
'vue/no-template-shadow': 'off', // TODO: fix
/* Toggle on to do additional until we can clean up existing violations.
'vue/no-unused-emit-declarations': 'error',
'vue/no-unused-properties': 'error',
'vue/no-unused-refs': 'error',
'vue/no-use-v-else-with-v-for': 'error',
'vue/no-useless-v-bind': 'error',
// */
'vue/one-component-per-file': 'off', // TODO: fix
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
// Restrict deprecated PrimeVue components

View File

@@ -8,8 +8,8 @@
<link rel="stylesheet" type="text/css" href="user.css" />
<link rel="stylesheet" type="text/css" href="api/userdata/user.css" />
<!-- Fullscreen mode on mobile browsers -->
<meta name="mobile-web-app-capable" content="yes">
<!-- Fullscreen mode on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Status bar style (eg. black or transparent) -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">

View File

@@ -1,10 +1,19 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
const vueBlockOrderRule =
"vue/block-order: ['error', {'order': ['docs', 'script', 'template', 'i18n', 'style']}]"
export default {
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
'./**/*.{ts,tsx,mts}': (stagedFiles) => [
...formatAndEslint(stagedFiles),
'pnpm typecheck'
]
],
'./**/*.vue': (stagedFiles) => [
runVueBlockOrder(stagedFiles),
...formatAndEslint(stagedFiles)
].filter(Boolean)
}
function formatAndEslint(fileNames) {
@@ -13,3 +22,9 @@ function formatAndEslint(fileNames) {
`pnpm exec prettier --cache --write ${fileNames.join(' ')}`
]
}
function runVueBlockOrder(fileNames) {
if (fileNames.length === 0) return null
const quotedFiles = fileNames.map((file) => `"${file}"`).join(' ')
return `pnpm exec eslint --fix --rule "${vueBlockOrderRule}" ${quotedFiles}`
}

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.28.1",
"version": "1.28.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -27,8 +27,6 @@
"preview": "nx preview",
"lint": "eslint src --cache",
"lint:fix": "eslint src --cache --fix",
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
"lint:no-cache": "eslint src",
"lint:fix:no-cache": "eslint src --fix",
"knip": "knip --cache",
@@ -96,7 +94,6 @@
"vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^7.7.6",
"vitest": "^3.2.4",
"vue-component-type-helpers": "^3.0.7",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.7",
"zip-dir": "^2.0.0",

3
pnpm-lock.yaml generated
View File

@@ -339,9 +339,6 @@ importers:
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)
vue-component-type-helpers:
specifier: ^3.0.7
version: 3.0.7
vue-eslint-parser:
specifier: ^10.2.0
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))

View File

@@ -9,18 +9,9 @@ import { normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
const nodeDefsPath = './src/locales/en/nodeDefs.json'
interface WidgetInfo {
name?: string
label?: string
}
interface WidgetLabels {
[key: string]: Record<string, { name: string }>
}
test('collect-i18n-node-defs', async ({ comfyPage }) => {
// Mock view route
await comfyPage.page.route('**/view**', async (route) => {
comfyPage.page.route('**/view**', async (route) => {
await route.fulfill({
body: JSON.stringify({})
})
@@ -29,7 +20,6 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
const nodeDefs: ComfyNodeDefImpl[] = (
Object.values(
await comfyPage.page.evaluate(async () => {
// @ts-expect-error - app is dynamically added to window
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})
@@ -62,7 +52,7 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
)
async function extractWidgetLabels() {
const nodeLabels: WidgetLabels = {}
const nodeLabels = {}
for (const nodeDef of nodeDefs) {
const inputNames = Object.values(nodeDef.inputs).map(
@@ -75,15 +65,12 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
const widgetsMappings = await comfyPage.page.evaluate(
(args) => {
const [nodeName, displayName, inputNames] = args
// @ts-expect-error - LiteGraph is dynamically added to window
const node = window['LiteGraph'].createNode(nodeName, displayName)
if (!node.widgets?.length) return {}
return Object.fromEntries(
node.widgets
.filter(
(w: WidgetInfo) => w?.name && !inputNames.includes(w.name)
)
.map((w: WidgetInfo) => [w.name, w.label])
.filter((w) => w?.name && !inputNames.includes(w.name))
.map((w) => [w.name, w.label])
)
},
[nodeDef.name, nodeDef.display_name, inputNames]

View File

@@ -72,7 +72,7 @@ function capture(srcLocaleDir: string, tempBaseDir: string) {
const relativePath = file.replace(srcLocaleDir, '')
const targetPath = join(tempBaseDir, relativePath)
ensureDir(dirname(targetPath))
writeFileSync(targetPath, readFileSync(file, 'utf8'))
writeFileSync(targetPath, readFileSync(file))
}
console.log('Captured current locale files to temp/base/')
}

View File

@@ -1,14 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
/* Script files configuration */
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true
},
"include": [
"**/*.ts"
]
}

View File

@@ -929,6 +929,48 @@ audio.comfy-audio.empty-audio-widget {
}
/* End of [Desktop] Electron window specific styles */
/* Vue Node LOD (Level of Detail) System */
/* These classes control rendering detail based on zoom level */
/* Minimal LOD (zoom <= 0.4) - Title only for performance */
.lg-node--lod-minimal {
min-height: 32px;
transition: min-height 0.2s ease;
/* Performance optimizations */
text-shadow: none;
backdrop-filter: none;
}
.lg-node--lod-minimal .lg-node-body {
display: none !important;
}
/* Reduced LOD (0.4 < zoom <= 0.8) - Essential widgets, simplified styling */
.lg-node--lod-reduced {
transition: opacity 0.1s ease;
/* Performance optimizations */
text-shadow: none;
}
.lg-node--lod-reduced .lg-widget-label,
.lg-node--lod-reduced .lg-slot-label {
display: none;
}
.lg-node--lod-reduced .lg-slot {
opacity: 0.6;
font-size: 0.75rem;
}
.lg-node--lod-reduced .lg-widget {
margin: 2px 0;
font-size: 0.875rem;
}
/* Full LOD (zoom > 0.8) - Complete detail rendering */
.lg-node--lod-full {
/* Uses default styling - no overrides needed */
}
.lg-node {
/* Disable text selection on all nodes */
@@ -954,52 +996,23 @@ audio.comfy-audio.empty-audio-widget {
will-change: transform;
}
/* START LOD specific styles */
/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */
.isLOD .lg-node {
box-shadow: none;
filter: none;
backdrop-filter: none;
text-shadow: none;
-webkit-mask-image: none;
mask-image: none;
clip-path: none;
background-image: none;
text-rendering: optimizeSpeed;
border-radius: 0;
contain: layout style;
transition: none;
/* Global performance optimizations for LOD */
.lg-node--lod-minimal,
.lg-node--lod-reduced {
/* Remove ALL expensive paint effects */
box-shadow: none !important;
filter: none !important;
backdrop-filter: none !important;
text-shadow: none !important;
-webkit-mask-image: none !important;
mask-image: none !important;
clip-path: none !important;
}
.isLOD .lg-node > * {
pointer-events: none;
/* Reduce paint complexity for minimal LOD */
.lg-node--lod-minimal {
/* Skip complex borders */
border-radius: 0 !important;
/* Use solid colors only */
background-image: none !important;
}
.lod-toggle {
visibility: visible;
}
.isLOD .lod-toggle {
visibility: hidden;
}
.lod-fallback {
display: none;
}
.isLOD .lod-fallback {
display: block;
}
.isLOD .image-preview img {
image-rendering: pixelated;
}
.isLOD .slot-dot {
border-radius: 0;
}
/* END LOD specific styles */

View File

@@ -1,98 +0,0 @@
/**
* Cross-browser async utilities for scheduling tasks during browser idle time
* with proper fallbacks for browsers that don't support requestIdleCallback.
*
* Implementation based on:
* https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts
*/
interface IdleDeadline {
didTimeout: boolean
timeRemaining(): number
}
interface IDisposable {
dispose(): void
}
/**
* Internal implementation function that handles the actual scheduling logic.
* Uses feature detection to determine whether to use native requestIdleCallback
* or fall back to setTimeout-based implementation.
*/
let _runWhenIdle: (
targetWindow: any,
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
/**
* Execute the callback during the next browser idle period.
* Falls back to setTimeout-based scheduling in browsers without native support.
*/
export let runWhenGlobalIdle: (
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
// Self-invoking function to set up the idle callback implementation
;(function () {
const safeGlobal: any = globalThis
if (
typeof safeGlobal.requestIdleCallback !== 'function' ||
typeof safeGlobal.cancelIdleCallback !== 'function'
) {
// Fallback implementation for browsers without native support (e.g., Safari)
_runWhenIdle = (_targetWindow, runner, _timeout?) => {
setTimeout(() => {
if (disposed) {
return
}
// Simulate IdleDeadline - give 15ms window (one frame at ~64fps)
const end = Date.now() + 15
const deadline: IdleDeadline = {
didTimeout: true,
timeRemaining() {
return Math.max(0, end - Date.now())
}
}
runner(Object.freeze(deadline))
})
let disposed = false
return {
dispose() {
if (disposed) {
return
}
disposed = true
}
}
}
} else {
// Native requestIdleCallback implementation
_runWhenIdle = (targetWindow: typeof safeGlobal, runner, timeout?) => {
const handle: number = targetWindow.requestIdleCallback(
runner,
typeof timeout === 'number' ? { timeout } : undefined
)
let disposed = false
return {
dispose() {
if (disposed) {
return
}
disposed = true
targetWindow.cancelIdleCallback(handle)
}
}
}
}
runWhenGlobalIdle = (runner, timeout) =>
_runWhenIdle(globalThis, runner, timeout)
})()

View File

@@ -33,7 +33,7 @@
<!-- TransformPane for Vue node rendering -->
<TransformPane
v-if="shouldRenderVueNodes && comfyApp.canvas && comfyAppReady"
v-if="isVueNodesEnabled && comfyApp.canvas && comfyAppReady"
:canvas="comfyApp.canvas"
@transform-update="handleTransformUpdate"
@wheel.capture="canvasInteractions.forwardEventToCanvas"
@@ -43,6 +43,8 @@
v-for="nodeData in allNodes"
:key="nodeData.id"
:node-data="nodeData"
:position="nodePositions.get(nodeData.id)"
:size="nodeSizes.get(nodeData.id)"
:readonly="false"
:error="
executionStore.lastExecutionError?.node_id === nodeData.id
@@ -51,6 +53,9 @@
"
:zoom-level="canvasStore.canvas?.ds?.scale || 1"
:data-node-id="nodeData.id"
@node-click="handleNodeSelect"
@update:collapsed="handleNodeCollapse"
@update:title="handleNodeTitleUpdate"
/>
</TransformPane>
@@ -74,6 +79,7 @@ import {
nextTick,
onMounted,
onUnmounted,
provide,
ref,
shallowRef,
watch,
@@ -111,11 +117,14 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useExecutionStateProvider } from '@/renderer/extensions/vueNodes/execution/useExecutionStateProvider'
import { UnauthorizedError, api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
@@ -162,13 +171,20 @@ const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
// Feature flags
const { shouldRenderVueNodes } = useVueFeatureFlags()
const isVueNodesEnabled = computed(() => shouldRenderVueNodes.value)
// Vue node system
const vueNodeLifecycle = useVueNodeLifecycle()
const viewportCulling = useViewportCulling()
const vueNodeLifecycle = useVueNodeLifecycle(isVueNodesEnabled)
const viewportCulling = useViewportCulling(
isVueNodesEnabled,
vueNodeLifecycle.vueNodeData,
vueNodeLifecycle.nodeDataTrigger,
vueNodeLifecycle.nodeManager
)
const nodeEventHandlers = useNodeEventHandlers(vueNodeLifecycle.nodeManager)
const handleVueNodeLifecycleReset = async () => {
if (shouldRenderVueNodes.value) {
if (isVueNodesEnabled.value) {
vueNodeLifecycle.disposeNodeManagerAndSyncs()
await nextTick()
vueNodeLifecycle.initializeNodeManager()
@@ -187,8 +203,32 @@ watch(
}
)
const nodePositions = vueNodeLifecycle.nodePositions
const nodeSizes = vueNodeLifecycle.nodeSizes
const allNodes = viewportCulling.allNodes
const handleTransformUpdate = viewportCulling.handleTransformUpdate
const handleTransformUpdate = () => {
viewportCulling.handleTransformUpdate()
// TODO: Fix paste position sync in separate PR
vueNodeLifecycle.detectChangesInRAF.value()
}
const handleNodeSelect = nodeEventHandlers.handleNodeSelect
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
const handleNodeTitleUpdate = nodeEventHandlers.handleNodeTitleUpdate
// Provide selection state to all Vue nodes
const selectedNodeIds = computed(
() =>
new Set(
canvasStore.selectedItems
.filter((item) => item.id !== undefined)
.map((item) => String(item.id))
)
)
provide(SelectedNodeIdsKey, selectedNodeIds)
// Provide execution state to all Vue nodes
useExecutionStateProvider()
watchEffect(() => {
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')

View File

@@ -33,11 +33,9 @@ const tooltipText = ref('')
const left = ref<string>()
const top = ref<string>()
function hideTooltip() {
return (tooltipText.value = '')
}
const hideTooltip = () => (tooltipText.value = '')
async function showTooltip(tooltip: string | null | undefined) {
const showTooltip = async (tooltip: string | null | undefined) => {
if (!tooltip) return
left.value = comfyApp.canvas.mouse[0] + 'px'
@@ -58,9 +56,9 @@ async function showTooltip(tooltip: string | null | undefined) {
}
}
function onIdle() {
const onIdle = () => {
const { canvas } = comfyApp
const node = canvas?.node_over
const node = canvas.node_over
if (!node) return
const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }

View File

@@ -11,7 +11,7 @@
:style="`backgroundColor: ${containerStyles.backgroundColor};`"
:pt="{
header: 'hidden',
content: 'p-1 h-10 flex flex-row gap-1'
content: 'px-1 py-1 h-10 px-1 flex flex-row gap-1'
}"
@wheel="canvasInteractions.handleWheel"
>

View File

@@ -112,12 +112,16 @@
</div>
<div
class="transition-opacity flex gap-3 h-0 items-center"
class="transition-opacity flex gap-3 h-0"
:class="{
'opacity-40': selected && selected !== 'cpu'
}"
>
<ToggleSwitch v-model="cpuMode" input-id="cpu-mode" />
<ToggleSwitch
v-model="cpuMode"
input-id="cpu-mode"
class="-translate-y-40"
/>
<label for="cpu-mode" class="select-none">
{{ $t('install.gpuSelection.enableCpuMode') }}
</label>

View File

@@ -64,29 +64,31 @@ const litegraphService = useLitegraphService()
const { visible, newSearchBoxEnabled } = storeToRefs(searchBoxStore)
const dismissable = ref(true)
function getNewNodeLocation(): Point {
const getNewNodeLocation = (): Point => {
return triggerEvent
? [triggerEvent.canvasX, triggerEvent.canvasY]
: litegraphService.getCanvasCenter()
}
const nodeFilters = ref<FuseFilterWithValue<ComfyNodeDefImpl, string>[]>([])
function addFilter(filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) {
const addFilter = (filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) => {
nodeFilters.value.push(filter)
}
function removeFilter(filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) {
const removeFilter = (
filter: FuseFilterWithValue<ComfyNodeDefImpl, string>
) => {
nodeFilters.value = nodeFilters.value.filter(
(f) => toRaw(f) !== toRaw(filter)
)
}
function clearFilters() {
const clearFilters = () => {
nodeFilters.value = []
}
function closeDialog() {
const closeDialog = () => {
visible.value = false
}
const canvasStore = useCanvasStore()
function addNode(nodeDef: ComfyNodeDefImpl) {
const addNode = (nodeDef: ComfyNodeDefImpl) => {
const node = litegraphService.addNodeOnGraph(nodeDef, {
pos: getNewNodeLocation()
})
@@ -104,7 +106,7 @@ function addNode(nodeDef: ComfyNodeDefImpl) {
window.requestAnimationFrame(closeDialog)
}
function showSearchBox(e: CanvasPointerEvent | null) {
const showSearchBox = (e: CanvasPointerEvent | null) => {
if (newSearchBoxEnabled.value) {
if (e?.pointerType === 'touch') {
setTimeout(() => {
@@ -118,12 +120,11 @@ function showSearchBox(e: CanvasPointerEvent | null) {
}
}
function getFirstLink() {
return canvasStore.getCanvas().linkConnector.renderLinks.at(0)
}
const getFirstLink = () =>
canvasStore.getCanvas().linkConnector.renderLinks.at(0)
const nodeDefStore = useNodeDefStore()
function showNewSearchBox(e: CanvasPointerEvent | null) {
const showNewSearchBox = (e: CanvasPointerEvent | null) => {
const firstLink = getFirstLink()
if (firstLink) {
const filter =
@@ -148,7 +149,7 @@ function showNewSearchBox(e: CanvasPointerEvent | null) {
}, 300)
}
function showContextMenu(e: CanvasPointerEvent) {
const showContextMenu = (e: CanvasPointerEvent) => {
const firstLink = getFirstLink()
if (!firstLink) return
@@ -225,7 +226,7 @@ watchEffect(() => {
)
})
function canvasEventHandler(e: LiteGraphCanvasEvent) {
const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
if (e.detail.subType === 'empty-double-click') {
showSearchBox(e.detail.originalEvent)
} else if (e.detail.subType === 'group-double-click') {
@@ -248,10 +249,8 @@ const linkReleaseActionShift = computed(() =>
)
// Prevent normal LinkConnector reset (called by CanvasPointer.finally)
function preventDefault(e: Event) {
return e.preventDefault()
}
function cancelNextReset(e: CustomEvent<CanvasPointerEvent>) {
const preventDefault = (e: Event) => e.preventDefault()
const cancelNextReset = (e: CustomEvent<CanvasPointerEvent>) => {
e.preventDefault()
const canvas = canvasStore.getCanvas()
@@ -261,7 +260,7 @@ function cancelNextReset(e: CustomEvent<CanvasPointerEvent>) {
})
}
function handleDroppedOnCanvas(e: CustomEvent<CanvasPointerEvent>) {
const handleDroppedOnCanvas = (e: CustomEvent<CanvasPointerEvent>) => {
disconnectOnReset = true
const action = e.detail.shiftKey
? linkReleaseActionShift.value
@@ -282,7 +281,7 @@ function handleDroppedOnCanvas(e: CustomEvent<CanvasPointerEvent>) {
}
// Resets litegraph state
function reset() {
const reset = () => {
listenerController?.abort()
listenerController = null
triggerEvent = null

View File

@@ -1,7 +1,7 @@
<template>
<div
ref="workflowTabRef"
class="flex p-2 gap-2 workflow-tab group"
class="flex p-2 gap-2 workflow-tab"
v-bind="$attrs"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@@ -11,13 +11,9 @@
{{ workflowOption.workflow.filename }}
</span>
<div class="relative">
<span
v-if="shouldShowStatusIndicator"
class="group-hover:hidden absolute font-bold text-2xl top-1/2 left-1/2 -translate-1/2 z-10 bg-(--comfy-menu-secondary-bg) w-4"
></span
>
<span v-if="shouldShowStatusIndicator" class="status-indicator"></span>
<Button
class="close-button p-0 w-auto invisible"
class="close-button p-0 w-auto"
icon="pi pi-times"
text
severity="secondary"
@@ -178,6 +174,18 @@ onUnmounted(() => {
})
</script>
<style scoped>
@reference '../../assets/css/style.css';
.status-indicator {
@apply absolute font-bold;
font-size: 1.5rem;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<style>
.p-tooltip.workflow-tab-tooltip {
z-index: 1200 !important;

View File

@@ -358,6 +358,14 @@ onUpdated(() => {
@apply visible;
}
:deep(.p-togglebutton:hover) .status-indicator {
@apply hidden;
}
:deep(.p-togglebutton) .close-button {
@apply invisible;
}
:deep(.p-scrollpanel-content) {
@apply h-full;
}

View File

@@ -1,5 +1,4 @@
import { whenever } from '@vueuse/core'
import { computed } from 'vue'
import { computed, watch } from 'vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { t } from '@/i18n'
@@ -34,8 +33,19 @@ export const useCurrentUser = () => {
return null
})
const onUserResolved = (callback: (user: AuthUserInfo) => void) =>
whenever(resolvedUserInfo, callback, { immediate: true })
const onUserResolved = (callback: (user: AuthUserInfo) => void) => {
if (resolvedUserInfo.value) {
callback(resolvedUserInfo.value)
}
const stop = watch(resolvedUserInfo, (value) => {
if (value) {
callback(value)
}
})
return () => stop()
}
const userDisplayName = computed(() => {
if (isApiKeyLogin.value) {

View File

@@ -2,15 +2,42 @@
* Vue node lifecycle management for LiteGraph integration
* Provides event-driven reactivity with performance optimizations
*/
import { reactive } from 'vue'
import { nextTick, reactive } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types'
import { type Bounds, QuadTree } from '@/renderer/core/spatial/QuadTree'
import type { WidgetValue } from '@/types/simplifiedWidget'
import type { SpatialIndexDebugInfo } from '@/types/spatialIndex'
import type { LGraph, LGraphNode } from '../../lib/litegraph/src/litegraph'
export interface NodeState {
visible: boolean
dirty: boolean
lastUpdate: number
culled: boolean
}
interface NodeMetadata {
lastRenderTime: number
cachedBounds: DOMRect | null
lodLevel: 'high' | 'medium' | 'low'
spatialIndex?: QuadTree<string>
}
interface PerformanceMetrics {
fps: number
frameTime: number
updateTime: number
nodeCount: number
culledCount: number
callbackUpdateCount: number
rafUpdateCount: number
adaptiveQuality: boolean
}
export interface SafeWidgetData {
name: string
type: string
@@ -36,26 +63,109 @@ export interface VueNodeData {
}
}
export interface GraphNodeManager {
interface SpatialMetrics {
queryTime: number
nodesInIndex: number
}
interface GraphNodeManager {
// Reactive state - safe data extracted from LiteGraph nodes
vueNodeData: ReadonlyMap<string, VueNodeData>
nodeState: ReadonlyMap<string, NodeState>
nodePositions: ReadonlyMap<string, { x: number; y: number }>
nodeSizes: ReadonlyMap<string, { width: number; height: number }>
// Access to original LiteGraph nodes (non-reactive)
getNode(id: string): LGraphNode | undefined
// Lifecycle methods
setupEventListeners(): () => void
cleanup(): void
// Update methods
scheduleUpdate(
nodeId?: string,
priority?: 'critical' | 'normal' | 'low'
): void
forceSync(): void
detectChangesInRAF(): void
// Spatial queries
getVisibleNodeIds(viewportBounds: Bounds): Set<string>
// Performance
performanceMetrics: PerformanceMetrics
spatialMetrics: SpatialMetrics
// Debug
getSpatialIndexDebugInfo(): SpatialIndexDebugInfo | null
}
export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Get layout mutations composable
const { createNode, deleteNode, setSource } = useLayoutMutations()
const { moveNode, resizeNode, createNode, deleteNode, setSource } =
useLayoutMutations()
// Safe reactive data extracted from LiteGraph nodes
const vueNodeData = reactive(new Map<string, VueNodeData>())
const nodeState = reactive(new Map<string, NodeState>())
const nodePositions = reactive(new Map<string, { x: number; y: number }>())
const nodeSizes = reactive(
new Map<string, { width: number; height: number }>()
)
// Non-reactive storage for original LiteGraph nodes
const nodeRefs = new Map<string, LGraphNode>()
// WeakMap for heavy data that auto-GCs when nodes are removed
const nodeMetadata = new WeakMap<LGraphNode, NodeMetadata>()
// Performance tracking
const performanceMetrics = reactive<PerformanceMetrics>({
fps: 0,
frameTime: 0,
updateTime: 0,
nodeCount: 0,
culledCount: 0,
callbackUpdateCount: 0,
rafUpdateCount: 0,
adaptiveQuality: false
})
// Spatial indexing using QuadTree
const spatialIndex = new QuadTree<string>(
{ x: -10000, y: -10000, width: 20000, height: 20000 },
{ maxDepth: 6, maxItemsPerNode: 4 }
)
let lastSpatialQueryTime = 0
// Spatial metrics
const spatialMetrics = reactive<SpatialMetrics>({
queryTime: 0,
nodesInIndex: 0
})
// Update batching
const pendingUpdates = new Set<string>()
const criticalUpdates = new Set<string>()
const lowPriorityUpdates = new Set<string>()
let updateScheduled = false
let batchTimeoutId: number | null = null
// Change detection state
const lastNodesSnapshot = new Map<
string,
{ pos: [number, number]; size: [number, number] }
>()
const attachMetadata = (node: LGraphNode) => {
nodeMetadata.set(node, {
lastRenderTime: performance.now(),
cachedBounds: null,
lodLevel: 'high',
spatialIndex: undefined
})
}
// Extract safe data from LiteGraph node for Vue consumption
const extractVueNodeData = (node: LGraphNode): VueNodeData => {
// Determine subgraph ID - null for root graph, string for subgraphs
@@ -176,6 +286,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
...currentData,
widgets: updatedWidgets
})
performanceMetrics.callbackUpdateCount++
} catch (error) {
// Ignore widget update errors to prevent cascade failures
}
@@ -245,6 +356,71 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
})
}
// Uncomment when needed for future features
// const getNodeMetadata = (node: LGraphNode): NodeMetadata => {
// let metadata = nodeMetadata.get(node)
// if (!metadata) {
// attachMetadata(node)
// metadata = nodeMetadata.get(node)!
// }
// return metadata
// }
const scheduleUpdate = (
nodeId?: string,
priority: 'critical' | 'normal' | 'low' = 'normal'
) => {
if (nodeId) {
const state = nodeState.get(nodeId)
if (state) state.dirty = true
// Priority queuing
if (priority === 'critical') {
criticalUpdates.add(nodeId)
flush() // Immediate flush for critical updates
return
} else if (priority === 'low') {
lowPriorityUpdates.add(nodeId)
} else {
pendingUpdates.add(nodeId)
}
}
if (!updateScheduled) {
updateScheduled = true
// Adaptive batching strategy
if (pendingUpdates.size > 10) {
// Many updates - batch in nextTick
void nextTick(() => flush())
} else {
// Few updates - small delay for more batching
batchTimeoutId = window.setTimeout(() => flush(), 4)
}
}
}
const flush = () => {
const startTime = performance.now()
if (batchTimeoutId !== null) {
clearTimeout(batchTimeoutId)
batchTimeoutId = null
}
// Clear all pending updates
criticalUpdates.clear()
pendingUpdates.clear()
lowPriorityUpdates.clear()
updateScheduled = false
// Sync with graph state
syncWithGraph()
const endTime = performance.now()
performanceMetrics.updateTime = endTime - startTime
}
const syncWithGraph = () => {
if (!graph?._nodes) return
@@ -255,6 +431,11 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
if (!currentNodes.has(id)) {
nodeRefs.delete(id)
vueNodeData.delete(id)
nodeState.delete(id)
nodePositions.delete(id)
nodeSizes.delete(id)
lastNodesSnapshot.delete(id)
spatialIndex.remove(id)
}
}
@@ -270,7 +451,163 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Extract and store safe data for Vue
vueNodeData.set(id, extractVueNodeData(node))
if (!nodeState.has(id)) {
nodeState.set(id, {
visible: true,
dirty: false,
lastUpdate: performance.now(),
culled: false
})
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
attachMetadata(node)
// Add to spatial index
const bounds: Bounds = {
x: node.pos[0],
y: node.pos[1],
width: node.size[0],
height: node.size[1]
}
spatialIndex.insert(id, bounds, id)
}
})
// Update performance metrics
performanceMetrics.nodeCount = vueNodeData.size
performanceMetrics.culledCount = Array.from(nodeState.values()).filter(
(s) => s.culled
).length
}
// Most performant: Direct position sync without re-setting entire node
// Query visible nodes using QuadTree spatial index
const getVisibleNodeIds = (viewportBounds: Bounds): Set<string> => {
const startTime = performance.now()
// Use QuadTree for fast spatial query
const results: string[] = spatialIndex.query(viewportBounds)
const visibleIds = new Set(results)
lastSpatialQueryTime = performance.now() - startTime
spatialMetrics.queryTime = lastSpatialQueryTime
return visibleIds
}
/**
* Detects position changes for a single node and updates reactive state
*/
const detectPositionChanges = (node: LGraphNode, id: string): boolean => {
const currentPos = nodePositions.get(id)
if (
!currentPos ||
currentPos.x !== node.pos[0] ||
currentPos.y !== node.pos[1]
) {
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
// Push position change to layout store
// Source is already set to 'canvas' in detectChangesInRAF
void moveNode(id, { x: node.pos[0], y: node.pos[1] })
return true
}
return false
}
/**
* Detects size changes for a single node and updates reactive state
*/
const detectSizeChanges = (node: LGraphNode, id: string): boolean => {
const currentSize = nodeSizes.get(id)
if (
!currentSize ||
currentSize.width !== node.size[0] ||
currentSize.height !== node.size[1]
) {
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
// Push size change to layout store
// Source is already set to 'canvas' in detectChangesInRAF
void resizeNode(id, {
width: node.size[0],
height: node.size[1]
})
return true
}
return false
}
/**
* Updates spatial index for a node if bounds changed
*/
const updateSpatialIndex = (node: LGraphNode, id: string): void => {
const bounds: Bounds = {
x: node.pos[0],
y: node.pos[1],
width: node.size[0],
height: node.size[1]
}
spatialIndex.update(id, bounds)
}
/**
* Updates performance metrics after change detection
*/
const updatePerformanceMetrics = (
startTime: number,
positionUpdates: number,
sizeUpdates: number
): void => {
const endTime = performance.now()
performanceMetrics.updateTime = endTime - startTime
performanceMetrics.nodeCount = vueNodeData.size
performanceMetrics.culledCount = Array.from(nodeState.values()).filter(
(state) => state.culled
).length
spatialMetrics.nodesInIndex = spatialIndex.size
if (positionUpdates > 0 || sizeUpdates > 0) {
performanceMetrics.rafUpdateCount++
}
}
/**
* Main RAF change detection function
*/
const detectChangesInRAF = () => {
const startTime = performance.now()
if (!graph?._nodes) return
let positionUpdates = 0
let sizeUpdates = 0
// Set source for all canvas-driven updates
setSource(LayoutSource.Canvas)
// Process each node for changes
for (const node of graph._nodes) {
const id = String(node.id)
const posChanged = detectPositionChanges(node, id)
const sizeChanged = detectSizeChanges(node, id)
if (posChanged) positionUpdates++
if (sizeChanged) sizeUpdates++
// Update spatial index if geometry changed
if (posChanged || sizeChanged) {
updateSpatialIndex(node, id)
}
}
updatePerformanceMetrics(startTime, positionUpdates, sizeUpdates)
}
/**
@@ -292,11 +629,32 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Extract initial data for Vue (may be incomplete during graph configure)
vueNodeData.set(id, extractVueNodeData(node))
// Set up reactive tracking state
nodeState.set(id, {
visible: true,
dirty: false,
lastUpdate: performance.now(),
culled: false
})
const initializeVueNodeLayout = () => {
// Extract actual positions after configure() has potentially updated them
const nodePosition = { x: node.pos[0], y: node.pos[1] }
const nodeSize = { width: node.size[0], height: node.size[1] }
nodePositions.set(id, nodePosition)
nodeSizes.set(id, nodeSize)
attachMetadata(node)
// Add to spatial index for viewport culling with final positions
const nodeBounds: Bounds = {
x: nodePosition.x,
y: nodePosition.y,
width: nodeSize.width,
height: nodeSize.height
}
spatialIndex.insert(id, nodeBounds, id)
// Add node to layout store with final positions
setSource(LayoutSource.Canvas)
void createNode(id, {
@@ -340,6 +698,9 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
) => {
const id = String(node.id)
// Remove from spatial index
spatialIndex.remove(id)
// Remove node from layout store
setSource(LayoutSource.Canvas)
void deleteNode(id)
@@ -347,6 +708,10 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Clean up all tracking references
nodeRefs.delete(id)
vueNodeData.delete(id)
nodeState.delete(id)
nodePositions.delete(id)
nodeSizes.delete(id)
lastNodesSnapshot.delete(id)
// Call original callback if provided
if (originalCallback) {
@@ -368,9 +733,23 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
graph.onNodeRemoved = originalOnNodeRemoved || undefined
graph.onTrigger = originalOnTrigger || undefined
// Clear pending updates
if (batchTimeoutId !== null) {
clearTimeout(batchTimeoutId)
batchTimeoutId = null
}
// Clear all state maps
nodeRefs.clear()
vueNodeData.clear()
nodeState.clear()
nodePositions.clear()
nodeSizes.clear()
lastNodesSnapshot.clear()
pendingUpdates.clear()
criticalUpdates.clear()
lowPriorityUpdates.clear()
spatialIndex.clear()
}
}
@@ -466,7 +845,18 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
return {
vueNodeData,
nodeState,
nodePositions,
nodeSizes,
getNode,
cleanup
setupEventListeners,
cleanup,
scheduleUpdate,
forceSync: syncWithGraph,
detectChangesInRAF,
getVisibleNodeIds,
performanceMetrics,
spatialMetrics,
getSpatialIndexDebugInfo: () => spatialIndex.getDebugInfo()
}
}

View File

@@ -6,18 +6,27 @@
* 2. Set display none on element to avoid cascade resolution overhead
* 3. Only run when transform changes (event driven)
*/
import { useThrottleFn } from '@vueuse/core'
import { computed } from 'vue'
import { type Ref, computed } from 'vue'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app as comfyApp } from '@/scripts/app'
export function useViewportCulling() {
interface NodeManager {
getNode: (id: string) => any
}
export function useViewportCulling(
isVueNodesEnabled: Ref<boolean>,
vueNodeData: Ref<ReadonlyMap<string, VueNodeData>>,
nodeDataTrigger: Ref<number>,
nodeManager: Ref<NodeManager | null>
) {
const canvasStore = useCanvasStore()
const { vueNodeData, nodeManager } = useVueNodeLifecycle()
const allNodes = computed(() => {
if (!isVueNodesEnabled.value) return []
void nodeDataTrigger.value // Force re-evaluation when nodeManager initializes
return Array.from(vueNodeData.value.values())
})
@@ -25,7 +34,7 @@ export function useViewportCulling() {
* Update visibility of all nodes based on viewport
* Queries DOM directly - no cache maintenance needed
*/
function updateVisibility() {
const updateVisibility = () => {
if (!nodeManager.value || !canvasStore.canvas || !comfyApp.canvas) return
const canvas = canvasStore.canvas
@@ -67,17 +76,31 @@ export function useViewportCulling() {
}
}
const updateVisibilityDebounced = useThrottleFn(updateVisibility, 20)
// RAF throttling for smooth updates during continuous panning
function handleTransformUpdate() {
requestAnimationFrame(async () => {
await updateVisibilityDebounced()
let rafId: number | null = null
/**
* Handle transform update - called by TransformPane event
* Uses RAF to batch updates for smooth performance
*/
const handleTransformUpdate = () => {
if (!isVueNodesEnabled.value) return
// Cancel previous RAF if still pending
if (rafId !== null) {
cancelAnimationFrame(rafId)
}
// Schedule update in next animation frame
rafId = requestAnimationFrame(() => {
updateVisibility()
rafId = null
})
}
return {
allNodes,
handleTransformUpdate
handleTransformUpdate,
updateVisibility
}
}

View File

@@ -1,12 +1,20 @@
import { createSharedComposable } from '@vueuse/core'
import { readonly, ref, shallowRef, watch } from 'vue'
/**
* Vue Node Lifecycle Management Composable
*
* Handles the complete lifecycle of Vue node rendering system including:
* - Node manager initialization and cleanup
* - Layout store synchronization
* - Slot and link sync management
* - Reactive state management for node data, positions, and sizes
* - Memory management and proper cleanup
*/
import { type Ref, computed, readonly, ref, shallowRef, watch } from 'vue'
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import type {
GraphNodeManager,
NodeState,
VueNodeData
} from '@/composables/graph/useGraphNodeManager'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
@@ -16,12 +24,13 @@ import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync
import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync'
import { app as comfyApp } from '@/scripts/app'
function useVueNodeLifecycleIndividual() {
export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
const canvasStore = useCanvasStore()
const layoutMutations = useLayoutMutations()
const { shouldRenderVueNodes } = useVueFeatureFlags()
const nodeManager = shallowRef<GraphNodeManager | null>(null)
const nodeManager = shallowRef<ReturnType<typeof useGraphNodeManager> | null>(
null
)
const cleanupNodeManager = shallowRef<(() => void) | null>(null)
// Sync management
@@ -31,10 +40,22 @@ function useVueNodeLifecycleIndividual() {
// Vue node data state
const vueNodeData = ref<ReadonlyMap<string, VueNodeData>>(new Map())
const nodeState = ref<ReadonlyMap<string, NodeState>>(new Map())
const nodePositions = ref<ReadonlyMap<string, { x: number; y: number }>>(
new Map()
)
const nodeSizes = ref<ReadonlyMap<string, { width: number; height: number }>>(
new Map()
)
// Change detection function
const detectChangesInRAF = ref<() => void>(() => {})
// Trigger for forcing computed re-evaluation
const nodeDataTrigger = ref(0)
const isNodeManagerReady = computed(() => nodeManager.value !== null)
const initializeNodeManager = () => {
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
const activeGraph = comfyApp.canvas?.graph || comfyApp.graph
@@ -47,6 +68,10 @@ function useVueNodeLifecycleIndividual() {
// Use the manager's data maps
vueNodeData.value = manager.vueNodeData
nodeState.value = manager.nodeState
nodePositions.value = manager.nodePositions
nodeSizes.value = manager.nodeSizes
detectChangesInRAF.value = manager.detectChangesInRAF
// Initialize layout system with existing nodes from active graph
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
@@ -109,12 +134,18 @@ function useVueNodeLifecycleIndividual() {
// Reset reactive maps to clean state
vueNodeData.value = new Map()
nodeState.value = new Map()
nodePositions.value = new Map()
nodeSizes.value = new Map()
// Reset change detection function
detectChangesInRAF.value = () => {}
}
// Watch for Vue nodes enabled state changes
watch(
() =>
shouldRenderVueNodes.value &&
isVueNodesEnabled.value &&
Boolean(comfyApp.canvas?.graph || comfyApp.graph),
(enabled) => {
if (enabled) {
@@ -128,7 +159,7 @@ function useVueNodeLifecycleIndividual() {
// Consolidated watch for slot layout sync management
watch(
[() => canvasStore.canvas, () => shouldRenderVueNodes.value],
[() => canvasStore.canvas, () => isVueNodesEnabled.value],
([canvas, vueMode], [, oldVueMode]) => {
const modeChanged = vueMode !== oldVueMode
@@ -160,7 +191,7 @@ function useVueNodeLifecycleIndividual() {
// Handle case where Vue nodes are enabled but graph starts empty
const setupEmptyGraphListener = () => {
if (
shouldRenderVueNodes.value &&
isVueNodesEnabled.value &&
comfyApp.graph &&
!nodeManager.value &&
comfyApp.graph._nodes.length === 0
@@ -171,7 +202,7 @@ function useVueNodeLifecycleIndividual() {
comfyApp.graph.onNodeAdded = originalOnNodeAdded
// Initialize node manager if needed
if (shouldRenderVueNodes.value && !nodeManager.value) {
if (isVueNodesEnabled.value && !nodeManager.value) {
initializeNodeManager()
}
@@ -202,7 +233,13 @@ function useVueNodeLifecycleIndividual() {
return {
vueNodeData,
nodeState,
nodePositions,
nodeSizes,
nodeDataTrigger: readonly(nodeDataTrigger),
nodeManager: readonly(nodeManager),
detectChangesInRAF: readonly(detectChangesInRAF),
isNodeManagerReady,
// Lifecycle methods
initializeNodeManager,
@@ -211,7 +248,3 @@ function useVueNodeLifecycleIndividual() {
cleanup
}
}
export const useVueNodeLifecycle = createSharedComposable(
useVueNodeLifecycleIndividual
)

View File

@@ -1548,71 +1548,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
},
ByteDanceImageReferenceNode: {
displayPrice: byteDanceVideoPricingCalculator
},
WanTextToVideoApi: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
const resolutionWidget = node.widgets?.find(
(w) => w.name === 'size'
) as IComboWidget
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
const seconds = parseFloat(String(durationWidget.value))
const resolutionStr = String(resolutionWidget.value).toLowerCase()
const resKey = resolutionStr.includes('1080')
? '1080p'
: resolutionStr.includes('720')
? '720p'
: resolutionStr.includes('480')
? '480p'
: resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? ''
const pricePerSecond: Record<string, number> = {
'480p': 0.05,
'720p': 0.1,
'1080p': 0.15
}
const pps = pricePerSecond[resKey]
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
const cost = (pps * seconds).toFixed(2)
return `$${cost}/Run`
}
},
WanImageToVideoApi: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
const resolutionWidget = node.widgets?.find(
(w) => w.name === 'resolution'
) as IComboWidget
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
const seconds = parseFloat(String(durationWidget.value))
const resolution = String(resolutionWidget.value).trim().toLowerCase()
const pricePerSecond: Record<string, number> = {
'480p': 0.05,
'720p': 0.1,
'1080p': 0.15
}
const pps = pricePerSecond[resolution]
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
const cost = (pps * seconds).toFixed(2)
return `$${cost}/Run`
}
},
WanTextToImageApi: {
displayPrice: '$0.03/Run'
}
}
@@ -1712,9 +1647,7 @@ export const useNodePricing = () => {
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
WanTextToVideoApi: ['duration', 'size'],
WanImageToVideoApi: ['duration', 'resolution']
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution']
}
return widgetMap[nodeType] || []
}

View File

@@ -2,17 +2,16 @@
* Vue-related feature flags composable
* Manages local settings-driven flags and LiteGraph integration
*/
import { createSharedComposable } from '@vueuse/core'
import { computed, watch } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { LiteGraph } from '../lib/litegraph/src/litegraph'
function useVueFeatureFlagsIndividual() {
export const useVueFeatureFlags = () => {
const settingStore = useSettingStore()
const shouldRenderVueNodes = computed(() => {
const isVueNodesEnabled = computed(() => {
try {
return settingStore.get('Comfy.VueNodes.Enabled') ?? false
} catch {
@@ -20,20 +19,20 @@ function useVueFeatureFlagsIndividual() {
}
})
// Whether Vue nodes should render
const shouldRenderVueNodes = computed(() => isVueNodesEnabled.value)
// Sync the Vue nodes flag with LiteGraph global settings
const syncVueNodesFlag = () => {
LiteGraph.vueNodesMode = isVueNodesEnabled.value
}
// Watch for changes and update LiteGraph immediately
watch(
shouldRenderVueNodes,
() => {
LiteGraph.vueNodesMode = shouldRenderVueNodes.value
},
{ immediate: true }
)
watch(isVueNodesEnabled, syncVueNodesFlag, { immediate: true })
return {
shouldRenderVueNodes
isVueNodesEnabled,
shouldRenderVueNodes,
syncVueNodesFlag
}
}
export const useVueFeatureFlags = createSharedComposable(
useVueFeatureFlagsIndividual
)

View File

@@ -122,14 +122,14 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
key: '.'
},
commandId: 'Comfy.Canvas.FitView',
targetElementId: 'graph-canvas-container'
targetElementId: 'graph-canvas'
},
{
combo: {
key: 'p'
},
commandId: 'Comfy.Canvas.ToggleSelected.Pin',
targetElementId: 'graph-canvas-container'
targetElementId: 'graph-canvas'
},
{
combo: {
@@ -137,7 +137,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
alt: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
targetElementId: 'graph-canvas-container'
targetElementId: 'graph-canvas'
},
{
combo: {
@@ -145,7 +145,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
ctrl: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
targetElementId: 'graph-canvas-container'
targetElementId: 'graph-canvas'
},
{
combo: {
@@ -153,7 +153,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
ctrl: true
},
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
targetElementId: 'graph-canvas-container'
targetElementId: 'graph-canvas'
},
{
combo: {

View File

@@ -3901,19 +3901,6 @@ class UIManager {
this.paintBucketSettingsHTML.style.display = 'none'
}
}
if (tool === Tools.MaskColorFill) {
this.brushSettingsHTML.style.display = 'none'
this.colorSelectSettingsHTML.style.display = 'flex'
this.paintBucketSettingsHTML.style.display = 'none'
} else if (tool === Tools.MaskBucket) {
this.brushSettingsHTML.style.display = 'none'
this.colorSelectSettingsHTML.style.display = 'none'
this.paintBucketSettingsHTML.style.display = 'flex'
} else {
this.brushSettingsHTML.style.display = 'flex'
this.colorSelectSettingsHTML.style.display = 'none'
this.paintBucketSettingsHTML.style.display = 'none'
}
this.messageBroker.publish('setTool', tool)
this.onToolChange()
const newActiveLayer = this.toolSettings[tool].newActiveLayerOnSet

View File

@@ -76,7 +76,7 @@ export const i18n = createI18n({
})
/** Convenience shorthand: i18n.global */
export const { t, te, d } = i18n.global
export const { t, te } = i18n.global
/**
* Safe translation function that returns the fallback message if the key is not found.

View File

@@ -1,9 +1,9 @@
import { LiteGraphInternal } from './LiteGraphInternal'
import type {
ContextMenuDivElement,
IContextMenuOptions,
IContextMenuValue
} from './interfaces'
import { LiteGraph } from './litegraph'
// TODO: Replace this pattern with something more modern.
export interface ContextMenu<TValue = unknown> {
@@ -182,7 +182,7 @@ export class ContextMenu<TValue = unknown> {
root.style.left = `${left}px`
root.style.top = `${top}px`
if (LiteGraphInternal.context_menu_scaling && options.scale) {
if (LiteGraph.context_menu_scaling && options.scale) {
root.style.transform = `scale(${Math.round(options.scale * 4) * 0.25})`
}
}
@@ -355,7 +355,7 @@ export class ContextMenu<TValue = unknown> {
) {
ContextMenu.trigger(
this.parentMenu.root,
`${LiteGraphInternal.pointerevents_method}leave`,
`${LiteGraph.pointerevents_method}leave`,
e
)
}

View File

@@ -14,7 +14,6 @@ import { LGraphCanvas } from './LGraphCanvas'
import { LGraphGroup } from './LGraphGroup'
import { LGraphNode, type NodeId } from './LGraphNode'
import { LLink, type LinkId } from './LLink'
import { LiteGraphInternal } from './LiteGraphInternal'
import { MapProxyHandler } from './MapProxyHandler'
import { Reroute, type RerouteId } from './Reroute'
import { CustomEventTarget } from './infrastructure/CustomEventTarget'
@@ -35,7 +34,7 @@ import type {
Positionable,
Size
} from './interfaces'
import { SubgraphNode } from './litegraph'
import { LiteGraph, SubgraphNode } from './litegraph'
import {
alignOutsideContainer,
alignToContainer,
@@ -275,7 +274,7 @@ export class LGraph
* @param o data from previous serialization [optional]
*/
constructor(o?: ISerialisedGraph | SerialisableGraph) {
if (LiteGraphInternal.debug) console.log('Graph created')
if (LiteGraph.debug) console.log('Graph created')
/** @see MapProxyHandler */
const links = this._links
@@ -425,7 +424,7 @@ export class LGraph
this.sendEventToAllNodes('onStart')
// launch
this.starttime = LiteGraphInternal.getTime()
this.starttime = LiteGraph.getTime()
this.last_update_time = this.starttime
interval ||= 0
@@ -487,7 +486,7 @@ export class LGraph
runStep(num: number, do_not_catch_errors: boolean, limit?: number): void {
num = num || 1
const start = LiteGraphInternal.getTime()
const start = LiteGraph.getTime()
this.globaltime = 0.001 * (start - this.starttime)
const nodes = this._nodes_executable || this._nodes
@@ -531,15 +530,14 @@ export class LGraph
this.errors_in_execution = false
} catch (error) {
this.errors_in_execution = true
if (LiteGraphInternal.throw_errors) throw error
if (LiteGraph.throw_errors) throw error
if (LiteGraphInternal.debug)
console.log('Error during execution:', error)
if (LiteGraph.debug) console.log('Error during execution:', error)
this.stop()
}
}
const now = LiteGraphInternal.getTime()
const now = LiteGraph.getTime()
let elapsed = now - start
if (elapsed == 0) elapsed = 1
@@ -664,7 +662,7 @@ export class LGraph
L.push(M[i])
}
if (L.length != this._nodes.length && LiteGraphInternal.debug)
if (L.length != this._nodes.length && LiteGraph.debug)
console.warn('something went wrong, nodes missing')
/** Ensure type is set */
@@ -720,21 +718,16 @@ export class LGraph
if (!column) continue
let max_size = 100
let y = margin + LiteGraphInternal.NODE_TITLE_HEIGHT
let y = margin + LiteGraph.NODE_TITLE_HEIGHT
for (const node of column) {
node.pos[0] = layout == LiteGraphInternal.VERTICAL_LAYOUT ? y : x
node.pos[1] = layout == LiteGraphInternal.VERTICAL_LAYOUT ? x : y
const max_size_index =
layout == LiteGraphInternal.VERTICAL_LAYOUT ? 1 : 0
node.pos[0] = layout == LiteGraph.VERTICAL_LAYOUT ? y : x
node.pos[1] = layout == LiteGraph.VERTICAL_LAYOUT ? x : y
const max_size_index = layout == LiteGraph.VERTICAL_LAYOUT ? 1 : 0
if (node.size[max_size_index] > max_size) {
max_size = node.size[max_size_index]
}
const node_size_index =
layout == LiteGraphInternal.VERTICAL_LAYOUT ? 0 : 1
y +=
node.size[node_size_index] +
margin +
LiteGraphInternal.NODE_TITLE_HEIGHT
const node_size_index = layout == LiteGraph.VERTICAL_LAYOUT ? 0 : 1
y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT
}
x += max_size + margin
}
@@ -838,7 +831,7 @@ export class LGraph
const { state } = this
// Ensure created items are snapped
if (LiteGraphInternal.alwaysSnapToGrid) {
if (LiteGraph.alwaysSnapToGrid) {
const snapTo = this.getSnapToGridSize()
if (snapTo) node.snapToGrid(snapTo)
}
@@ -863,18 +856,16 @@ export class LGraph
console.warn(
'LiteGraph: there is already a node with this ID, changing it'
)
node.id = LiteGraphInternal.use_uuids
? LiteGraphInternal.uuidv4()
: ++state.lastNodeId
node.id = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++state.lastNodeId
}
if (this._nodes.length >= LiteGraphInternal.MAX_NUMBER_OF_NODES) {
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
throw 'LiteGraph: max number of nodes in a graph reached'
}
// give him an id
if (LiteGraphInternal.use_uuids) {
if (node.id == null || node.id == -1) node.id = LiteGraphInternal.uuidv4()
if (LiteGraph.use_uuids) {
if (node.id == null || node.id == -1) node.id = LiteGraph.uuidv4()
} else {
if (node.id == null || node.id == -1) {
node.id = ++state.lastNodeId
@@ -1137,9 +1128,9 @@ export class LGraph
/**
* Snaps the provided items to a grid.
*
* Item positions are reounded to the nearest multiple of {@link LiteGraphInternal.CANVAS_GRID_SIZE}.
* Item positions are reounded to the nearest multiple of {@link LiteGraph.CANVAS_GRID_SIZE}.
*
* When {@link LiteGraphInternal.alwaysSnapToGrid} is enabled
* When {@link LiteGraph.alwaysSnapToGrid} is enabled
* and the grid size is falsy, a default of 1 is used.
* @param items The items to be snapped to the grid
* @todo Currently only snaps nodes.
@@ -1159,9 +1150,9 @@ export class LGraph
*/
getSnapToGridSize(): number {
// Default to 1 when always snapping
return LiteGraphInternal.alwaysSnapToGrid
? LiteGraphInternal.CANVAS_GRID_SIZE || 1
: LiteGraphInternal.CANVAS_GRID_SIZE
return LiteGraph.alwaysSnapToGrid
? LiteGraph.CANVAS_GRID_SIZE || 1
: LiteGraph.CANVAS_GRID_SIZE
}
/**
@@ -1173,11 +1164,11 @@ export class LGraph
checkNodeTypes() {
const { _nodes } = this
for (const [i, node] of _nodes.entries()) {
const ctor = LiteGraphInternal.registered_node_types[node.type]
const ctor = LiteGraph.registered_node_types[node.type]
if (node.constructor == ctor) continue
console.log('node being replaced by newer version:', node.type)
const newnode = LiteGraphInternal.createNode(node.type)
const newnode = LiteGraph.createNode(node.type)
if (!newnode) continue
_nodes[i] = newnode
newnode.configure(node.serialize())
@@ -1238,7 +1229,7 @@ export class LGraph
/* Called when something visually changed (not the graph!) */
change(): void {
if (LiteGraphInternal.debug) {
if (LiteGraph.debug) {
console.log('Graph changed')
}
this.canvasAction((c) => c.setDirty(true, true))
@@ -1588,13 +1579,9 @@ export class LGraph
})
// Create subgraph node object
const subgraphNode = LiteGraphInternal.createNode(
subgraph.id,
subgraph.name,
{
outputs: structuredClone(outputs)
}
)
const subgraphNode = LiteGraph.createNode(subgraph.id, subgraph.name, {
outputs: structuredClone(outputs)
})
if (!subgraphNode) throw new Error('Failed to create subgraph node')
for (let i = 0; i < inputs.length; i++) {
Object.assign(subgraphNode.inputs[i], inputs[i])
@@ -1611,7 +1598,7 @@ export class LGraph
)
//Correct for title height. It's included in bounding box, but not _posSize
subgraphNode.pos[1] += LiteGraphInternal.NODE_TITLE_HEIGHT / 2
subgraphNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2
// Add the subgraph node to the graph
this.add(subgraphNode)
@@ -1732,10 +1719,7 @@ export class LGraph
const movedNodes = multiClone(subgraphNode.subgraph.nodes)
const nodeIdMap = new Map<NodeId, NodeId>()
for (const n_info of movedNodes) {
const node = LiteGraphInternal.createNode(
String(n_info.type),
n_info.title
)
const node = LiteGraph.createNode(String(n_info.type), n_info.title)
if (!node) {
throw new Error('Node not found')
}
@@ -2043,7 +2027,7 @@ export class LGraph
definitions,
config,
extra,
version: LiteGraphInternal.VERSION
version: LiteGraph.VERSION
}
}
@@ -2068,7 +2052,7 @@ export class LGraph
const { id, revision, config, state } = this
const nodeList =
!LiteGraphInternal.use_uuids && options?.sortNodes
!LiteGraph.use_uuids && options?.sortNodes
? // @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers.
[...this._nodes].sort((a, b) => a.id - b.id)
: this._nodes
@@ -2088,8 +2072,7 @@ export class LGraph
// Save scale and offset
const extra = { ...this.extra }
if (LiteGraphInternal.saveViewportWithGraph)
extra.ds = this.#getDragAndScale()
if (LiteGraph.saveViewportWithGraph) extra.ds = this.#getDragAndScale()
if (!extra.ds) delete extra.ds
const data: ReturnType<typeof this.asSerialisable> = {
@@ -2247,12 +2230,9 @@ export class LGraph
if (nodesData) {
for (const n_info of nodesData) {
// stored info
let node = LiteGraphInternal.createNode(
String(n_info.type),
n_info.title
)
let node = LiteGraph.createNode(String(n_info.type), n_info.title)
if (!node) {
if (LiteGraphInternal.debug)
if (LiteGraph.debug)
console.log('Node not found or has errors:', n_info.type)
// in case of error we create a replacement node to avoid losing info
@@ -2304,7 +2284,7 @@ export class LGraph
if (groupData) {
for (const data of groupData) {
// TODO: Search/remove these global object refs
const group = new LiteGraphInternal.LGraphGroup()
const group = new LiteGraph.LGraphGroup()
group.configure(data)
this.add(group)
}

View File

@@ -16,7 +16,6 @@ import type { LGraph } from './LGraph'
import { LGraphGroup } from './LGraphGroup'
import { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode'
import { LLink, type LinkId } from './LLink'
import { LiteGraphInternal } from './LiteGraphInternal'
import { Reroute, type RerouteId } from './Reroute'
import { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
import { strokeShape } from './draw'
@@ -53,7 +52,7 @@ import type {
Rect,
Size
} from './interfaces'
import { Rectangle, SubgraphNode, createUuidv4 } from './litegraph'
import { LiteGraph, Rectangle, SubgraphNode, createUuidv4 } from './litegraph'
import {
containsRect,
createBounds,
@@ -411,12 +410,12 @@ export class LGraphCanvas
* @deprecated Use {@link LGraphNode.titleFontStyle} instead.
*/
get title_text_font(): string {
return `${LiteGraphInternal.NODE_TEXT_SIZE}px ${LiteGraphInternal.NODE_FONT}`
return `${LiteGraph.NODE_TEXT_SIZE}px ${LiteGraph.NODE_FONT}`
}
// #endregion Legacy accessors
get inner_text_font(): string {
return `normal ${LiteGraphInternal.NODE_SUBTEXT_SIZE}px ${LiteGraphInternal.NODE_FONT}`
return `normal ${LiteGraph.NODE_SUBTEXT_SIZE}px ${LiteGraph.NODE_FONT}`
}
#maximumFrameGap = 0
@@ -435,14 +434,14 @@ export class LGraphCanvas
* @deprecated Use {@link LiteGraphGlobal.ROUND_RADIUS} instead.
*/
get round_radius() {
return LiteGraphInternal.ROUND_RADIUS
return LiteGraph.ROUND_RADIUS
}
/**
* @deprecated Use {@link LiteGraphGlobal.ROUND_RADIUS} instead.
*/
set round_radius(value: number) {
LiteGraphInternal.ROUND_RADIUS = value
LiteGraph.ROUND_RADIUS = value
}
// Cached LOD threshold values for performance
@@ -461,7 +460,7 @@ export class LGraphCanvas
return
}
const baseFontSize = LiteGraphInternal.NODE_TEXT_SIZE // 14px
const baseFontSize = LiteGraph.NODE_TEXT_SIZE // 14px
const dprAdjustment = Math.sqrt(window.devicePixelRatio || 1) //Using sqrt here because higher DPR monitors do not linearily scale the readability of the font, instead they increase the font by some heurisitc, and to approximate we use sqrt to say bascially a DPR of 2 increases the readibility by 40%, 3 by 70%
// Calculate the zoom level where text becomes unreadable
@@ -790,7 +789,7 @@ export class LGraphCanvas
// No longer in use
// add menu when releasing link in empty space
if (LiteGraphInternal.release_link_on_empty_shows_menu) {
if (LiteGraph.release_link_on_empty_shows_menu) {
const linkReleaseContext =
this.linkConnector.state.connectingTo === 'input'
? {
@@ -837,8 +836,8 @@ export class LGraphCanvas
// in range (1.01, 2.5). Less than 1 will invert the zoom direction
this.zoom_speed = 1.1
this.node_title_color = LiteGraphInternal.NODE_TITLE_COLOR
this.default_link_color = LiteGraphInternal.LINK_COLOR
this.node_title_color = LiteGraph.NODE_TITLE_COLOR
this.default_link_color = LiteGraph.LINK_COLOR
this.default_connection_color = {
input_off: '#778',
input_on: '#7F7',
@@ -961,7 +960,7 @@ export class LGraphCanvas
): void {
const canvas = LGraphCanvas.active_canvas
const group = new LiteGraphInternal.LGraphGroup()
const group = new LiteGraph.LGraphGroup()
group.pos = canvas.convertEventToCanvasOffset(mouse_event)
if (!canvas.graph) throw new NullGraphError()
canvas.graph.add(group)
@@ -1011,7 +1010,7 @@ export class LGraphCanvas
prev_menu: ContextMenu<string>,
node: LGraphNode
): void {
new LiteGraphInternal.ContextMenu(['Top', 'Bottom', 'Left', 'Right'], {
new LiteGraph.ContextMenu(['Top', 'Bottom', 'Left', 'Right'], {
event,
callback: inner_clicked,
parentMenu: prev_menu
@@ -1035,7 +1034,7 @@ export class LGraphCanvas
event: MouseEvent,
prev_menu: ContextMenu<string>
): void {
new LiteGraphInternal.ContextMenu(['Top', 'Bottom', 'Left', 'Right'], {
new LiteGraph.ContextMenu(['Top', 'Bottom', 'Left', 'Right'], {
event,
callback: inner_clicked,
parentMenu: prev_menu
@@ -1058,7 +1057,7 @@ export class LGraphCanvas
event: MouseEvent,
prev_menu: ContextMenu<string>
): void {
new LiteGraphInternal.ContextMenu(['Vertically', 'Horizontally'], {
new LiteGraph.ContextMenu(['Vertically', 'Horizontally'], {
event,
callback: inner_clicked,
parentMenu: prev_menu
@@ -1106,7 +1105,7 @@ export class LGraphCanvas
): void {
if (!graph) return
const categories = LiteGraphInternal.getNodeTypesCategories(
const categories = LiteGraph.getNodeTypesCategories(
canvas.filter || graph.filter
).filter((category) => category.startsWith(base_category))
const entries: AddNodeMenu[] = []
@@ -1149,7 +1148,7 @@ export class LGraphCanvas
}
}
const nodes = LiteGraphInternal.getNodeTypesInCategory(
const nodes = LiteGraph.getNodeTypesInCategory(
base_category.slice(0, -1),
canvas.filter || graph.filter
)
@@ -1173,7 +1172,7 @@ export class LGraphCanvas
const first_event = contextMenu.getFirstEvent()
canvas.graph.beforeChange()
const node = LiteGraphInternal.createNode(value.value)
const node = LiteGraph.createNode(value.value)
if (node) {
if (!first_event)
throw new TypeError(
@@ -1193,7 +1192,7 @@ export class LGraphCanvas
entries.push(entry)
}
new LiteGraphInternal.ContextMenu(
new LiteGraph.ContextMenu(
entries,
{ event: e, parentMenu: prev_menu },
// @ts-expect-error - extra parameter
@@ -1222,12 +1221,12 @@ export class LGraphCanvas
let entries: (IContextMenuValue<INodeSlotContextItem> | null)[] = []
if (
LiteGraphInternal.do_add_triggers_slots &&
LiteGraph.do_add_triggers_slots &&
node.findOutputSlot('onExecuted') == -1
) {
entries.push({
content: 'On Executed',
value: ['onExecuted', LiteGraphInternal.EVENT, { nameLocked: true }],
value: ['onExecuted', LiteGraph.EVENT, { nameLocked: true }],
className: 'event'
})
}
@@ -1237,7 +1236,7 @@ export class LGraphCanvas
if (!entries.length) return
new LiteGraphInternal.ContextMenu<INodeSlotContextItem>(entries, {
new LiteGraph.ContextMenu<INodeSlotContextItem>(entries, {
event: e,
callback: inner_clicked,
parentMenu: prev_menu,
@@ -1265,7 +1264,7 @@ export class LGraphCanvas
for (const i in value) {
entries.push({ content: i, value: value[i] })
}
new LiteGraphInternal.ContextMenu(entries, {
new LiteGraph.ContextMenu(entries, {
event: e,
callback: inner_clicked,
parentMenu: prev_menu,
@@ -1324,7 +1323,7 @@ export class LGraphCanvas
return
}
new LiteGraphInternal.ContextMenu<string>(
new LiteGraph.ContextMenu<string>(
entries,
{
event: e,
@@ -1467,21 +1466,18 @@ export class LGraphCanvas
let dialogCloseTimer: number
dialog.addEventListener('mouseleave', function () {
if (LiteGraphInternal.dialog_close_on_mouse_leave) {
if (
!dialog.is_modified &&
LiteGraphInternal.dialog_close_on_mouse_leave
) {
if (LiteGraph.dialog_close_on_mouse_leave) {
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) {
// @ts-expect-error - setTimeout type
dialogCloseTimer = setTimeout(
dialog.close,
LiteGraphInternal.dialog_close_on_mouse_leave_delay
LiteGraph.dialog_close_on_mouse_leave_delay
)
}
}
})
dialog.addEventListener('mouseenter', function () {
if (LiteGraphInternal.dialog_close_on_mouse_leave) {
if (LiteGraph.dialog_close_on_mouse_leave) {
if (dialogCloseTimer) clearTimeout(dialogCloseTimer)
}
})
@@ -1601,7 +1597,7 @@ export class LGraphCanvas
menu: ContextMenu,
node: LGraphNode
): boolean {
new LiteGraphInternal.ContextMenu(LiteGraphInternal.NODE_MODES, {
new LiteGraph.ContextMenu(LiteGraph.NODE_MODES, {
event: e,
callback: inner_clicked,
parentMenu: menu,
@@ -1611,9 +1607,9 @@ export class LGraphCanvas
function inner_clicked(v: string) {
if (!node) return
const kV = Object.values(LiteGraphInternal.NODE_MODES).indexOf(v)
const kV = Object.values(LiteGraph.NODE_MODES).indexOf(v)
const fApplyMultiNode = function (node: LGraphNode) {
if (kV !== -1 && LiteGraphInternal.NODE_MODES[kV]) {
if (kV !== -1 && LiteGraph.NODE_MODES[kV]) {
node.changeMode(kV)
} else {
console.warn(`unexpected mode: ${v}`)
@@ -1669,7 +1665,7 @@ export class LGraphCanvas
}
values.push(value)
}
new LiteGraphInternal.ContextMenu<string | null>(values, {
new LiteGraph.ContextMenu<string | null>(values, {
event: e,
callback: inner_clicked,
parentMenu: menu,
@@ -1703,27 +1699,26 @@ export class LGraphCanvas
static onMenuNodeShapes(
// @ts-expect-error - unused parameter
value: IContextMenuValue<(typeof LiteGraphInternal.VALID_SHAPES)[number]>,
value: IContextMenuValue<(typeof LiteGraph.VALID_SHAPES)[number]>,
// @ts-expect-error - unused parameter
options: IContextMenuOptions<
(typeof LiteGraphInternal.VALID_SHAPES)[number]
>,
options: IContextMenuOptions<(typeof LiteGraph.VALID_SHAPES)[number]>,
e: MouseEvent,
menu?: ContextMenu<(typeof LiteGraphInternal.VALID_SHAPES)[number]>,
menu?: ContextMenu<(typeof LiteGraph.VALID_SHAPES)[number]>,
node?: LGraphNode
): boolean {
if (!node) throw 'no node passed'
new LiteGraphInternal.ContextMenu<
(typeof LiteGraphInternal.VALID_SHAPES)[number]
>(LiteGraphInternal.VALID_SHAPES, {
event: e,
callback: inner_clicked,
parentMenu: menu,
node
})
new LiteGraph.ContextMenu<(typeof LiteGraph.VALID_SHAPES)[number]>(
LiteGraph.VALID_SHAPES,
{
event: e,
callback: inner_clicked,
parentMenu: menu,
node
}
)
function inner_clicked(v: (typeof LiteGraphInternal.VALID_SHAPES)[number]) {
function inner_clicked(v: (typeof LiteGraph.VALID_SHAPES)[number]) {
if (!node) return
if (!node.graph) throw new NullGraphError()
@@ -2105,8 +2100,7 @@ export class LGraphCanvas
if (this.#maximumFrameGap > 0) {
// Manual FPS limit
const gap =
this.#maximumFrameGap -
(LiteGraphInternal.getTime() - this.last_draw_time)
this.#maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time)
setTimeout(renderFrame.bind(this), Math.max(1, gap))
} else {
// FPS limited by refresh rate
@@ -2177,7 +2171,7 @@ export class LGraphCanvas
// Hover transitions
// TODO: Implement single lerp ease factor for current progress on hover in/out.
// In drawNode, multiply by ease factor and differential value (e.g. bg alpha +0.5).
otherNode.lostFocusAt = LiteGraphInternal.getTime()
otherNode.lostFocusAt = LiteGraph.getTime()
this.node_over?.onMouseLeave?.(e)
this.node_over = undefined
@@ -2230,7 +2224,7 @@ export class LGraphCanvas
this.canvas.focus()
LiteGraphInternal.closeAllContextMenus(ref_window)
LiteGraph.closeAllContextMenus(ref_window)
if (this.onMouse?.(e) == true) return
@@ -2301,7 +2295,7 @@ export class LGraphCanvas
}
this.last_mouse = [x, y]
this.last_mouseclick = LiteGraphInternal.getTime()
this.last_mouseclick = LiteGraph.getTime()
this.last_mouse_dragging = true
graph.change()
@@ -2355,7 +2349,7 @@ export class LGraphCanvas
if (
ctrlOrMeta &&
!e.altKey &&
LiteGraphInternal.leftMouseClickBehavior === 'panning'
LiteGraph.leftMouseClickBehavior === 'panning'
) {
this.#setupNodeSelectionDrag(e, pointer, node)
@@ -2370,7 +2364,7 @@ export class LGraphCanvas
// clone node ALT dragging
if (
LiteGraphInternal.alt_drag_do_clone_nodes &&
LiteGraph.alt_drag_do_clone_nodes &&
e.altKey &&
!e.ctrlKey &&
node &&
@@ -2390,7 +2384,7 @@ export class LGraphCanvas
if (node_data?.type != null) {
// Ensure the cloned node is configured against the correct type (especially for SubgraphNodes)
node_data.type = newType
const cloned = LiteGraphInternal.createNode(newType)
const cloned = LiteGraph.createNode(newType)
if (cloned) {
cloned.configure(node_data)
cloned.pos[0] += 5
@@ -2573,7 +2567,7 @@ export class LGraphCanvas
}
pointer.finally = () => (this.resizingGroup = null)
} else {
const f = group.font_size || LiteGraphInternal.DEFAULT_GROUP_FONT_SIZE
const f = group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
const headerHeight = f * 1.4
if (
isInRectangle(
@@ -2624,10 +2618,7 @@ export class LGraphCanvas
this.allow_dragcanvas
) {
// allow dragging canvas based on leftMouseClickBehavior or read-only mode
if (
LiteGraphInternal.leftMouseClickBehavior === 'panning' ||
this.read_only
) {
if (LiteGraph.leftMouseClickBehavior === 'panning' || this.read_only) {
pointer.onClick = () => this.processSelect(null, e)
pointer.finally = () => (this.dragging_canvas = false)
this.dragging_canvas = true
@@ -2724,11 +2715,11 @@ export class LGraphCanvas
linkConnector.dragNewFromOutput(graph, node, output)
this.#linkConnectorDrop()
if (LiteGraphInternal.shift_click_do_break_link_from) {
if (LiteGraph.shift_click_do_break_link_from) {
if (e.shiftKey) {
node.disconnectOutput(i)
}
} else if (LiteGraphInternal.ctrl_alt_click_do_break_link) {
} else if (LiteGraph.ctrl_alt_click_do_break_link) {
if (ctrlOrMeta && e.altKey && !e.shiftKey) {
node.disconnectOutput(i)
}
@@ -2757,13 +2748,13 @@ export class LGraphCanvas
pointer.onClick = () => node.onInputClick?.(i, e)
const shouldBreakLink =
LiteGraphInternal.ctrl_alt_click_do_break_link &&
LiteGraph.ctrl_alt_click_do_break_link &&
ctrlOrMeta &&
e.altKey &&
!e.shiftKey
if (input.link !== null || input._floatingLinks?.size) {
// Existing link
if (shouldBreakLink || LiteGraphInternal.click_do_break_link_to) {
if (shouldBreakLink || LiteGraph.click_do_break_link_to) {
node.disconnectInput(i, true)
} else if (e.shiftKey || this.allow_reconnect_links) {
linkConnector.moveInputLink(graph, input)
@@ -3038,7 +3029,7 @@ export class LGraphCanvas
const { pointer } = this
if (
LiteGraphInternal.middle_click_slot_add_default_node &&
LiteGraph.middle_click_slot_add_default_node &&
node &&
this.allow_interaction &&
!this.read_only &&
@@ -3333,7 +3324,7 @@ export class LGraphCanvas
} else if (
inputId != -1 &&
node.inputs[inputId] &&
LiteGraphInternal.isValidConnection(
LiteGraph.isValidConnection(
firstLink.fromSlot.type,
node.inputs[inputId].type
)
@@ -3360,7 +3351,7 @@ export class LGraphCanvas
if (
outputId != -1 &&
node.outputs[outputId] &&
LiteGraphInternal.isValidConnection(
LiteGraph.isValidConnection(
firstLink.fromSlot.type,
node.outputs[outputId].type
)
@@ -3513,7 +3504,7 @@ export class LGraphCanvas
*/
#processDraggedItems(e: CanvasPointerEvent): void {
const { graph } = this
if (e.shiftKey || LiteGraphInternal.alwaysSnapToGrid)
if (e.shiftKey || LiteGraph.alwaysSnapToGrid)
graph?.snapToGrid(this.selectedItems)
this.dirty_canvas = true
@@ -3537,7 +3528,7 @@ export class LGraphCanvas
this.adjustMouseEvent(e)
const now = LiteGraphInternal.getTime()
const now = LiteGraph.getTime()
e.click_time = now - this.last_mouseclick
/** The mouseup event occurred near the mousedown event. */
@@ -3639,7 +3630,7 @@ export class LGraphCanvas
e.ctrlKey || (e.metaKey && navigator.platform.includes('Mac'))
const isZoomModifier = isCtrlOrMacMeta && !e.altKey && !e.shiftKey
if (isZoomModifier || LiteGraphInternal.mouseWheelScroll === 'zoom') {
if (isZoomModifier || LiteGraph.mouseWheelScroll === 'zoom') {
// Zoom mode or modifier key pressed - use wheel for zoom
if (isTrackpad) {
// Trackpad gesture - use smooth scaling
@@ -3879,7 +3870,7 @@ export class LGraphCanvas
// if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior
if (
!LiteGraphInternal.ctrl_shift_v_paste_connect_unselected_outputs &&
!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs &&
connectInputs
)
return
@@ -3961,8 +3952,7 @@ export class LGraphCanvas
// Nodes
for (const info of parsed.nodes) {
const node =
info.type == null ? null : LiteGraphInternal.createNode(info.type)
const node = info.type == null ? null : LiteGraph.createNode(info.type)
if (!node) {
// failedNodes.push(info)
continue
@@ -4005,7 +3995,7 @@ export class LGraphCanvas
// If it wasn't copied, use the original graph value
if (
connectInputs &&
LiteGraphInternal.ctrl_shift_v_paste_connect_unselected_outputs
LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs
) {
outNode ??= graph.getNodeById(info.origin_id)
afterRerouteId ??= info.parentId
@@ -4042,18 +4032,6 @@ export class LGraphCanvas
// TODO: Report failures, i.e. `failedNodes`
const newPositions = created.map((node) => ({
nodeId: String(node.id),
bounds: {
x: node.pos[0],
y: node.pos[1],
width: node.size?.[0] ?? 100,
height: node.size?.[1] ?? 200
}
}))
layoutStore.batchUpdateNodeBounds(newPositions)
this.selectItems(created)
graph.afterChange()
@@ -4582,7 +4560,7 @@ export class LGraphCanvas
return
// fps counting
const now = LiteGraphInternal.getTime()
const now = LiteGraph.getTime()
this.render_time = (now - this.last_draw_time) * 0.001
this.last_draw_time = now
@@ -4647,7 +4625,7 @@ export class LGraphCanvas
// TODO: Set snapping value when changed instead of once per frame
this.#snapToGrid =
this.#shiftDown || LiteGraphInternal.alwaysSnapToGrid
this.#shiftDown || LiteGraph.alwaysSnapToGrid
? this.graph?.getSnapToGridSize()
: undefined
@@ -4759,10 +4737,7 @@ export class LGraphCanvas
ctx.fillStyle = colour
ctx.beginPath()
if (
connType === LiteGraphInternal.EVENT ||
connShape === RenderShape.BOX
) {
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
ctx.rect(
highlightPos[0] - 6 + 0.5,
@@ -4852,7 +4827,7 @@ export class LGraphCanvas
/** Get the target snap / highlight point in graph space */
#getHighlightPosition(): ReadOnlyPoint {
return LiteGraphInternal.snaps_for_comfy
return LiteGraph.snaps_for_comfy
? this.linkConnector.state.snapLinksPos ??
this._highlight_pos ??
this.graph_mouse
@@ -4888,7 +4863,7 @@ export class LGraphCanvas
const { linkConnector } = this
const { overReroute, overWidget } = linkConnector
if (
!LiteGraphInternal.snap_highlights_node ||
!LiteGraph.snap_highlights_node ||
!linkConnector.isConnecting ||
linkConnectorSnap
)
@@ -4905,7 +4880,7 @@ export class LGraphCanvas
const area = node.boundingRect
const gap = 3
const radius = LiteGraphInternal.ROUND_RADIUS + gap
const radius = LiteGraph.ROUND_RADIUS + gap
const x = area[0] - gap
const y = area[1] - gap
@@ -4953,7 +4928,7 @@ export class LGraphCanvas
const {
pos: [nodeX, nodeY]
} = node
const height = LiteGraphInternal.NODE_WIDGET_HEIGHT
const height = LiteGraph.NODE_WIDGET_HEIGHT
if (
overWidget.type.startsWith('custom') &&
computedHeight != null &&
@@ -4997,7 +4972,7 @@ export class LGraphCanvas
ctx.save()
ctx.translate(x, y)
ctx.font = `10px ${LiteGraphInternal.DEFAULT_FONT}`
ctx.font = `10px ${LiteGraph.DEFAULT_FONT}`
ctx.fillStyle = '#888'
ctx.textAlign = 'left'
if (this.graph) {
@@ -5176,7 +5151,7 @@ export class LGraphCanvas
// When Vue nodes mode is enabled, LiteGraph should not draw node chrome or widgets.
// We still need to keep slot metrics and layout in sync for hit-testing and links.
// Interaction system changes coming later, chances are vue nodes mode will be mostly broken on land
if (LiteGraphInternal.vueNodesMode) {
if (LiteGraph.vueNodesMode) {
// Prepare concrete slots and compute layout measures without rendering visuals.
node._setConcreteSlots()
if (!node.collapsed) {
@@ -5193,7 +5168,7 @@ export class LGraphCanvas
ctx.globalAlpha = editor_alpha
if (this.render_shadows && !low_quality) {
ctx.shadowColor = LiteGraphInternal.DEFAULT_SHADOW_COLOR
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR
ctx.shadowOffsetX = 2 * this.ds.scale
ctx.shadowOffsetY = 2 * this.ds.scale
ctx.shadowBlur = 3 * this.ds.scale
@@ -5233,7 +5208,7 @@ export class LGraphCanvas
// Render title buttons (if not collapsed)
if (node.title_buttons && !node.flags.collapsed) {
const title_height = LiteGraphInternal.NODE_TITLE_HEIGHT
const title_height = LiteGraph.NODE_TITLE_HEIGHT
let current_x = size[0] // Start flush with right edge
for (let i = 0; i < node.title_buttons.length; i++) {
@@ -5260,7 +5235,7 @@ export class LGraphCanvas
ctx.shadowColor = 'transparent'
// TODO: Legacy behaviour: onDrawForeground received ctx in this state
ctx.strokeStyle = LiteGraphInternal.NODE_BOX_OUTLINE_COLOR
ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR
// Draw Foreground
node.onDrawForeground?.(ctx, this, this.canvas)
@@ -5388,7 +5363,7 @@ export class LGraphCanvas
ctx.strokeStyle = fgcolor
ctx.fillStyle = bgcolor
const title_height = LiteGraphInternal.NODE_TITLE_HEIGHT
const title_height = LiteGraph.NODE_TITLE_HEIGHT
const { low_quality } = this
const { collapsed } = node.flags
@@ -5420,13 +5395,8 @@ export class LGraphCanvas
area[2],
area[3],
shape == RenderShape.CARD
? [
LiteGraphInternal.ROUND_RADIUS,
LiteGraphInternal.ROUND_RADIUS,
0,
0
]
: [LiteGraphInternal.ROUND_RADIUS]
? [LiteGraph.ROUND_RADIUS, LiteGraph.ROUND_RADIUS, 0, 0]
: [LiteGraph.ROUND_RADIUS]
)
} else if (shape == RenderShape.CIRCLE) {
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI * 2)
@@ -5554,7 +5524,7 @@ export class LGraphCanvas
const visibleReroutes: Reroute[] = []
const now = LiteGraphInternal.getTime()
const now = LiteGraph.getTime()
const { visible_area } = this
LGraphCanvas.#margin_area[0] = visible_area[0] - 20
LGraphCanvas.#margin_area[1] = visible_area[1] - 20
@@ -5581,7 +5551,7 @@ export class LGraphCanvas
const link = graph._links.get(link_id)
if (!link) continue
const endPos: Point = LiteGraphInternal.vueNodesMode // TODO: still use LG get pos if vue nodes is off until stable
const endPos: Point = LiteGraph.vueNodesMode // TODO: still use LG get pos if vue nodes is off until stable
? getSlotPosition(node, i, true)
: node.getInputPos(i)
@@ -5593,7 +5563,7 @@ export class LGraphCanvas
const startPos: Point =
outputId === -1
? [start_node.pos[0] + 10, start_node.pos[1] + 10]
: LiteGraphInternal.vueNodesMode // TODO: still use LG get pos if vue nodes is off until stable
: LiteGraph.vueNodesMode // TODO: still use LG get pos if vue nodes is off until stable
? getSlotPosition(start_node, outputId, false)
: start_node.getOutputPos(outputId)
@@ -6013,23 +5983,23 @@ export class LGraphCanvas
for (const node of visible_nodes) {
ctx.fillStyle = 'black'
ctx.fillRect(
node.pos[0] - LiteGraphInternal.NODE_TITLE_HEIGHT,
node.pos[1] - LiteGraphInternal.NODE_TITLE_HEIGHT,
LiteGraphInternal.NODE_TITLE_HEIGHT,
LiteGraphInternal.NODE_TITLE_HEIGHT
node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT,
node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT,
LiteGraph.NODE_TITLE_HEIGHT,
LiteGraph.NODE_TITLE_HEIGHT
)
if (node.order == 0) {
ctx.strokeRect(
node.pos[0] - LiteGraphInternal.NODE_TITLE_HEIGHT + 0.5,
node.pos[1] - LiteGraphInternal.NODE_TITLE_HEIGHT + 0.5,
LiteGraphInternal.NODE_TITLE_HEIGHT,
LiteGraphInternal.NODE_TITLE_HEIGHT
node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT + 0.5,
node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5,
LiteGraph.NODE_TITLE_HEIGHT,
LiteGraph.NODE_TITLE_HEIGHT
)
}
ctx.fillStyle = '#FFF'
ctx.fillText(
toString(node.order),
node.pos[0] + LiteGraphInternal.NODE_TITLE_HEIGHT * -0.5,
node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5,
node.pos[1] - 6
)
}
@@ -6128,7 +6098,7 @@ export class LGraphCanvas
const { origin_id, origin_slot } = segment
if (origin_id == null || origin_slot == null) {
new LiteGraphInternal.ContextMenu<string>(['Link has no origin'], {
new LiteGraph.ContextMenu<string>(['Link has no origin'], {
event: e,
title
})
@@ -6140,7 +6110,7 @@ export class LGraphCanvas
const options = ['Add Node', 'Add Reroute', null, 'Delete', null]
const menu = new LiteGraphInternal.ContextMenu<string>(options, {
const menu = new LiteGraph.ContextMenu<string>(options, {
event: e,
title,
callback: inner_clicked.bind(this)
@@ -6303,11 +6273,10 @@ export class LGraphCanvas
}
// check for defaults nodes for this slottype
const fromSlotType =
slotX.type == LiteGraphInternal.EVENT ? '_event_' : slotX.type
const fromSlotType = slotX.type == LiteGraph.EVENT ? '_event_' : slotX.type
const slotTypesDefault = isFrom
? LiteGraphInternal.slot_types_default_out
: LiteGraphInternal.slot_types_default_in
? LiteGraph.slot_types_default_out
: LiteGraph.slot_types_default_in
if (slotTypesDefault?.[fromSlotType]) {
// TODO: Remove "any" kludge
let nodeNewType: any = false
@@ -6336,18 +6305,14 @@ export class LGraphCanvas
}
// that.graph.beforeChange();
const xSizeFix = opts.posSizeFix[0] * LiteGraphInternal.NODE_WIDTH
const ySizeFix = opts.posSizeFix[1] * LiteGraphInternal.NODE_SLOT_HEIGHT
const xSizeFix = opts.posSizeFix[0] * LiteGraph.NODE_WIDTH
const ySizeFix = opts.posSizeFix[1] * LiteGraph.NODE_SLOT_HEIGHT
const nodeX = opts.position[0] + opts.posAdd[0] + xSizeFix
const nodeY = opts.position[1] + opts.posAdd[1] + ySizeFix
const pos = [nodeX, nodeY]
const newNode = LiteGraphInternal.createNode(
nodeNewType,
nodeNewOpts.title,
{
pos
}
)
const newNode = LiteGraph.createNode(nodeNewType, nodeNewOpts.title, {
pos
})
if (newNode) {
// if is object pass options
if (nodeNewOpts) {
@@ -6515,11 +6480,10 @@ export class LGraphCanvas
}
// get defaults nodes for this slottype
const fromSlotType =
slotX.type == LiteGraphInternal.EVENT ? '_event_' : slotX.type
const fromSlotType = slotX.type == LiteGraph.EVENT ? '_event_' : slotX.type
const slotTypesDefault = isFrom
? LiteGraphInternal.slot_types_default_out
: LiteGraphInternal.slot_types_default_in
? LiteGraph.slot_types_default_out
: LiteGraph.slot_types_default_in
if (slotTypesDefault?.[fromSlotType]) {
if (typeof slotTypesDefault[fromSlotType] == 'object') {
for (const typeX in slotTypesDefault[fromSlotType]) {
@@ -6531,7 +6495,7 @@ export class LGraphCanvas
}
// build menu
const menu = new LiteGraphInternal.ContextMenu<string>(options, {
const menu = new LiteGraph.ContextMenu<string>(options, {
event: opts.e,
extra: slotX,
title:
@@ -6680,23 +6644,20 @@ export class LGraphCanvas
let dialogCloseTimer: number
let prevent_timeout = 0
LiteGraphInternal.pointerListenerAdd(dialog, 'leave', function () {
LiteGraph.pointerListenerAdd(dialog, 'leave', function () {
if (prevent_timeout) return
if (LiteGraphInternal.dialog_close_on_mouse_leave) {
if (
!dialog.is_modified &&
LiteGraphInternal.dialog_close_on_mouse_leave
) {
if (LiteGraph.dialog_close_on_mouse_leave) {
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) {
// @ts-expect-error - setTimeout type
dialogCloseTimer = setTimeout(
dialog.close,
LiteGraphInternal.dialog_close_on_mouse_leave_delay
LiteGraph.dialog_close_on_mouse_leave_delay
)
}
}
})
LiteGraphInternal.pointerListenerAdd(dialog, 'enter', function () {
if (LiteGraphInternal.dialog_close_on_mouse_leave && dialogCloseTimer)
LiteGraph.pointerListenerAdd(dialog, 'enter', function () {
if (LiteGraph.dialog_close_on_mouse_leave && dialogCloseTimer)
clearTimeout(dialogCloseTimer)
})
const selInDia = dialog.querySelectorAll('select')
@@ -6805,7 +6766,7 @@ export class LGraphCanvas
node_to: null,
// TODO check for registered_slot_[in/out]_types not empty
// this will be checked for functionality enabled : filter on slot type, in and out
do_type_filter: LiteGraphInternal.search_filter_enabled,
do_type_filter: LiteGraph.search_filter_enabled,
// these are default: pass to set initially set values
// @ts-expect-error Property missing from interface definition
@@ -6814,9 +6775,9 @@ export class LGraphCanvas
type_filter_out: false,
show_general_if_none_on_typefilter: true,
show_general_after_typefiltered: true,
hide_on_mouse_leave: LiteGraphInternal.search_hide_on_mouse_leave,
hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave,
show_all_if_empty: true,
show_all_on_open: LiteGraphInternal.search_show_all_on_open
show_all_on_open: LiteGraph.search_show_all_on_open
}
Object.assign(options, searchOptions)
@@ -6877,7 +6838,7 @@ export class LGraphCanvas
// FIXME: Remove "any" kludge
let prevent_timeout: any = false
let timeout_close: number | null = null
LiteGraphInternal.pointerListenerAdd(dialog, 'enter', function () {
LiteGraph.pointerListenerAdd(dialog, 'enter', function () {
if (timeout_close) {
clearTimeout(timeout_close)
timeout_close = null
@@ -6972,12 +6933,12 @@ export class LGraphCanvas
// if should filter on type, load and fill selected and choose elements if passed
if (options.do_type_filter) {
if (selIn) {
const aSlots = LiteGraphInternal.slot_types_in
const aSlots = LiteGraph.slot_types_in
const nSlots = aSlots.length
if (
options.type_filter_in == LiteGraphInternal.EVENT ||
options.type_filter_in == LiteGraphInternal.ACTION
options.type_filter_in == LiteGraph.EVENT ||
options.type_filter_in == LiteGraph.ACTION
) {
options.type_filter_in = '_event_'
}
@@ -7000,11 +6961,11 @@ export class LGraphCanvas
})
}
if (selOut) {
const aSlots = LiteGraphInternal.slot_types_out
const aSlots = LiteGraph.slot_types_out
if (
options.type_filter_out == LiteGraphInternal.EVENT ||
options.type_filter_out == LiteGraphInternal.ACTION
options.type_filter_out == LiteGraph.EVENT ||
options.type_filter_out == LiteGraph.ACTION
) {
options.type_filter_out = '_event_'
}
@@ -7063,7 +7024,7 @@ export class LGraphCanvas
if (!graphcanvas.graph) throw new NullGraphError()
graphcanvas.graph.beforeChange()
const node = LiteGraphInternal.createNode(name)
const node = LiteGraph.createNode(name)
if (node) {
node.pos = graphcanvas.convertEventToCanvasOffset(safeEvent)
graphcanvas.graph.add(node, false)
@@ -7216,7 +7177,7 @@ export class LGraphCanvas
sOut = that.search_box.querySelector('.slot_out_type_filter')
}
const keys = Object.keys(LiteGraphInternal.registered_node_types)
const keys = Object.keys(LiteGraph.registered_node_types)
const filtered = keys.filter((x) => inner_test_filter(x))
for (const item of filtered) {
@@ -7236,7 +7197,7 @@ export class LGraphCanvas
// FIXME: Undeclared variable again
// @ts-expect-error Variable declared without type annotation
filtered_extra = []
for (const i in LiteGraphInternal.registered_node_types) {
for (const i in LiteGraph.registered_node_types) {
if (
inner_test_filter(i, {
inTypeOverride: sIn && sIn.value ? '*' : false,
@@ -7266,7 +7227,7 @@ export class LGraphCanvas
) {
// @ts-expect-error Variable declared without type annotation
filtered_extra = []
for (const i in LiteGraphInternal.registered_node_types) {
for (const i in LiteGraph.registered_node_types) {
if (inner_test_filter(i, { skipFilter: true }))
// @ts-expect-error Variable declared without type annotation
filtered_extra.push(i)
@@ -7297,7 +7258,7 @@ export class LGraphCanvas
outTypeOverride: false
}
const opts = Object.assign(optsDef, optsIn)
const ctor = LiteGraphInternal.registered_node_types[type]
const ctor = LiteGraph.registered_node_types[type]
if (filter && ctor.filter != filter) return false
if (
(!options.show_all_if_empty || str) &&
@@ -7314,30 +7275,18 @@ export class LGraphCanvas
let sV =
opts.inTypeOverride !== false ? opts.inTypeOverride : sIn.value
// type is stored
if (
sIn &&
sV &&
LiteGraphInternal.registered_slot_in_types[sV]?.nodes
) {
if (sIn && sV && LiteGraph.registered_slot_in_types[sV]?.nodes) {
const doesInc =
LiteGraphInternal.registered_slot_in_types[sV].nodes.includes(
sType
)
LiteGraph.registered_slot_in_types[sV].nodes.includes(sType)
if (doesInc === false) return false
}
sV = sOut.value
if (opts.outTypeOverride !== false) sV = opts.outTypeOverride
// type is stored
if (
sOut &&
sV &&
LiteGraphInternal.registered_slot_out_types[sV]?.nodes
) {
if (sOut && sV && LiteGraph.registered_slot_out_types[sV]?.nodes) {
const doesInc =
LiteGraphInternal.registered_slot_out_types[sV].nodes.includes(
sType
)
LiteGraph.registered_slot_out_types[sV].nodes.includes(sType)
if (doesInc === false) return false
}
}
@@ -7349,7 +7298,7 @@ export class LGraphCanvas
const help = document.createElement('div')
first ||= type
const nodeType = LiteGraphInternal.registered_node_types[type]
const nodeType = LiteGraph.registered_node_types[type]
if (nodeType?.title) {
help.textContent = nodeType?.title
const typeEl = document.createElement('span')
@@ -7583,22 +7532,16 @@ export class LGraphCanvas
dialog.addEventListener('mouseleave', function () {
if (prevent_timeout) return
if (
!dialog.is_modified &&
LiteGraphInternal.dialog_close_on_mouse_leave
) {
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) {
// @ts-expect-error - setTimeout type
dialogCloseTimer = setTimeout(
dialog.close,
LiteGraphInternal.dialog_close_on_mouse_leave_delay
LiteGraph.dialog_close_on_mouse_leave_delay
)
}
})
dialog.addEventListener('mouseenter', function () {
if (
options.closeOnLeave ||
LiteGraphInternal.dialog_close_on_mouse_leave
) {
if (options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) {
if (dialogCloseTimer) clearTimeout(dialogCloseTimer)
}
})
@@ -7801,7 +7744,7 @@ export class LGraphCanvas
innerChange(propname, v)
return false
}
new LiteGraphInternal.ContextMenu(
new LiteGraph.ContextMenu(
values,
{
event,
@@ -7884,10 +7827,8 @@ export class LGraphCanvas
if (typeof value !== 'string')
throw new TypeError('Attempting to set mode to non-string value.')
const kV = Object.values(LiteGraphInternal.NODE_MODES).indexOf(
value
)
if (kV !== -1 && LiteGraphInternal.NODE_MODES[kV]) {
const kV = Object.values(LiteGraph.NODE_MODES).indexOf(value)
if (kV !== -1 && LiteGraph.NODE_MODES[kV]) {
node.changeMode(kV)
} else {
console.warn(`unexpected mode: ${value}`)
@@ -7918,12 +7859,12 @@ export class LGraphCanvas
panel.addWidget('string', 'Title', node.title, {}, fUpdate)
const mode =
node.mode == null ? undefined : LiteGraphInternal.NODE_MODES[node.mode]
node.mode == null ? undefined : LiteGraph.NODE_MODES[node.mode]
panel.addWidget(
'combo',
'Mode',
mode,
{ values: LiteGraphInternal.NODE_MODES },
{ values: LiteGraph.NODE_MODES },
fUpdate
)
@@ -8071,7 +8012,7 @@ export class LGraphCanvas
| IContextMenuValue<string | null>
| IContextMenuValue<INodeSlotContextItem>
| IContextMenuValue<unknown, LGraphNode>
| IContextMenuValue<(typeof LiteGraphInternal.VALID_SHAPES)[number]>
| IContextMenuValue<(typeof LiteGraph.VALID_SHAPES)[number]>
| null
)[]
if (node.getMenuOptions) {
@@ -8293,10 +8234,10 @@ export class LGraphCanvas
}
// @ts-expect-error Slot type can be number and has number checks
options.title = (slot.input ? slot.input.type : slot.output.type) || '*'
if (slot.input && slot.input.type == LiteGraphInternal.ACTION)
if (slot.input && slot.input.type == LiteGraph.ACTION)
options.title = 'Action'
if (slot.output && slot.output.type == LiteGraphInternal.EVENT)
if (slot.output && slot.output.type == LiteGraph.EVENT)
options.title = 'Event'
} else {
// on node
@@ -8358,7 +8299,7 @@ export class LGraphCanvas
if (!menu_info) return
// @ts-expect-error Remove param ref_window - unused
new LiteGraphInternal.ContextMenu(menu_info, options, ref_window)
new LiteGraph.ContextMenu(menu_info, options, ref_window)
const createDialog = (options: IDialogOptions) =>
this.createDialog(

View File

@@ -3,7 +3,6 @@ import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphErro
import type { LGraph } from './LGraph'
import { LGraphCanvas } from './LGraphCanvas'
import { LGraphNode } from './LGraphNode'
import { LiteGraphInternal } from './LiteGraphInternal'
import { strokeShape } from './draw'
import type {
ColorOption,
@@ -14,6 +13,7 @@ import type {
Positionable,
Size
} from './interfaces'
import { LiteGraph } from './litegraph'
import {
containsCentre,
containsRect,
@@ -39,7 +39,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
color?: string
title: string
font?: string
font_size: number = LiteGraphInternal.DEFAULT_GROUP_FONT || 24
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
_bounding: Float32Array = new Float32Array([
10,
10,
@@ -170,8 +170,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
*/
draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void {
const { padding, resizeLength, defaultColour } = LGraphGroup
const font_size =
this.font_size || LiteGraphInternal.DEFAULT_GROUP_FONT_SIZE
const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
const [x, y] = this._pos
const [width, height] = this._size
@@ -202,7 +201,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
ctx.fill()
// Title
ctx.font = `${font_size}px ${LiteGraphInternal.GROUP_FONT}`
ctx.font = `${font_size}px ${LiteGraph.GROUP_FONT}`
ctx.textAlign = 'left'
ctx.fillText(
this.title + (this.pinned ? '📌' : ''),
@@ -210,7 +209,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
y + font_size
)
if (LiteGraphInternal.highlight_selected_group && this.selected) {
if (LiteGraph.highlight_selected_group && this.selected) {
strokeShape(ctx, this._bounding, {
title_height: this.titleHeight,
padding

View File

@@ -14,7 +14,6 @@ import { BadgePosition, LGraphBadge } from './LGraphBadge'
import { LGraphButton, type LGraphButtonOptions } from './LGraphButton'
import { LGraphCanvas } from './LGraphCanvas'
import { LLink } from './LLink'
import { LiteGraphInternal } from './LiteGraphInternal'
import type { Reroute, RerouteId } from './Reroute'
import { getNodeInputOnPos, getNodeOutputOnPos } from './canvas/measureSlots'
import type { IDrawBoundingOptions } from './draw'
@@ -43,7 +42,12 @@ import type {
Rect,
Size
} from './interfaces'
import type { LGraphNodeConstructor, Subgraph, SubgraphNode } from './litegraph'
import {
type LGraphNodeConstructor,
LiteGraph,
type Subgraph,
type SubgraphNode
} from './litegraph'
import {
createBounds,
isInRect,
@@ -240,11 +244,11 @@ export class LGraphNode
* The font style used to render the node's title text.
*/
get titleFontStyle(): string {
return `${LiteGraphInternal.NODE_TEXT_SIZE}px ${LiteGraphInternal.NODE_FONT}`
return `${LiteGraph.NODE_TEXT_SIZE}px ${LiteGraph.NODE_FONT}`
}
get innerFontStyle(): string {
return `normal ${LiteGraphInternal.NODE_SUBTEXT_SIZE}px ${LiteGraphInternal.NODE_FONT}`
return `normal ${LiteGraph.NODE_SUBTEXT_SIZE}px ${LiteGraph.NODE_FONT}`
}
get displayType(): string {
@@ -299,19 +303,13 @@ export class LGraphNode
/** The fg color used to render the node. */
get renderingColor(): string {
return (
this.color ||
this.constructor.color ||
LiteGraphInternal.NODE_DEFAULT_COLOR
)
return this.color || this.constructor.color || LiteGraph.NODE_DEFAULT_COLOR
}
/** The bg color used to render the node. */
get renderingBgColor(): string {
return (
this.bgcolor ||
this.constructor.bgcolor ||
LiteGraphInternal.NODE_DEFAULT_BGCOLOR
this.bgcolor || this.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR
)
}
@@ -319,17 +317,17 @@ export class LGraphNode
get renderingBoxColor(): string {
if (this.boxcolor) return this.boxcolor
if (LiteGraphInternal.node_box_coloured_when_on) {
if (LiteGraph.node_box_coloured_when_on) {
if (this.action_triggered) return '#FFF'
if (this.execute_triggered) return '#AAA'
}
if (LiteGraphInternal.node_box_coloured_by_mode) {
if (LiteGraph.node_box_coloured_by_mode) {
const modeColour =
LiteGraphInternal.NODE_MODES_COLORS[this.mode ?? LGraphEventMode.ALWAYS]
LiteGraph.NODE_MODES_COLORS[this.mode ?? LGraphEventMode.ALWAYS]
if (modeColour) return modeColour
}
return LiteGraphInternal.NODE_DEFAULT_BOXCOLOR
return LiteGraph.NODE_DEFAULT_BOXCOLOR
}
/** @inheritdoc {@link IColorable.setColorOption} */
@@ -509,11 +507,7 @@ export class LGraphNode
* The shape of the node used for rendering. @see {@link RenderShape}
*/
get renderingShape(): RenderShape {
return (
this._shape ||
this.constructor.shape ||
LiteGraphInternal.NODE_DEFAULT_SHAPE
)
return this._shape || this.constructor.shape || LiteGraph.NODE_DEFAULT_SHAPE
}
public get is_selected(): boolean | undefined {
@@ -726,7 +720,7 @@ export class LGraphNode
return {
padding: 12,
lineWidth: 10,
color: LiteGraphInternal.NODE_ERROR_COLOUR
color: LiteGraph.NODE_ERROR_COLOUR
}
}
}
@@ -740,10 +734,10 @@ export class LGraphNode
}
constructor(title: string, type?: string) {
this.id = LiteGraphInternal.use_uuids ? LiteGraphInternal.uuidv4() : -1
this.id = LiteGraph.use_uuids ? LiteGraph.uuidv4() : -1
this.title = title || 'Unnamed'
this.type = type ?? ''
this.size = [LiteGraphInternal.NODE_WIDTH, 60]
this.size = [LiteGraph.NODE_WIDTH, 60]
this.pos = [10, 10]
this.strokeStyles = {
error: this.#getErrorStrokeStyle,
@@ -784,7 +778,7 @@ export class LGraphNode
this[j]?.configure(info[j])
} else {
// @ts-expect-error #594
this[j] = LiteGraphInternal.cloneObject(info[j], this[j])
this[j] = LiteGraph.cloneObject(info[j], this[j])
}
} else {
// value
@@ -869,7 +863,7 @@ export class LGraphNode
type: this.type,
pos: [this.pos[0], this.pos[1]],
size: [this.size[0], this.size[1]],
flags: LiteGraphInternal.cloneObject(this.flags),
flags: LiteGraph.cloneObject(this.flags),
order: this.order,
mode: this.mode,
showAdvanced: this.showAdvanced
@@ -887,8 +881,7 @@ export class LGraphNode
if (this.title && this.title != this.constructor.title) o.title = this.title
if (this.properties)
o.properties = LiteGraphInternal.cloneObject(this.properties)
if (this.properties) o.properties = LiteGraph.cloneObject(this.properties)
const { widgets } = this
if (widgets && this.serialize_widgets) {
@@ -918,11 +911,11 @@ export class LGraphNode
/* Creates a clone of this node */
clone(): LGraphNode | null {
if (this.type == null) return null
const node = LiteGraphInternal.createNode(this.type)
const node = LiteGraph.createNode(this.type)
if (!node) return null
// we clone it because serialize returns shared containers
const data = LiteGraphInternal.cloneObject(this.serialize())
const data = LiteGraph.cloneObject(this.serialize())
const { inputs, outputs } = data
// remove links
@@ -941,7 +934,7 @@ export class LGraphNode
// @ts-expect-error Exceptional case: id is removed so that the graph can assign a new one on add.
delete data.id
if (LiteGraphInternal.use_uuids) data.id = LiteGraphInternal.uuidv4()
if (LiteGraph.use_uuids) data.id = LiteGraph.uuidv4()
node.configure(data)
@@ -1262,7 +1255,7 @@ export class LGraphNode
addOnTriggerInput(): number {
const trigS = this.findInputSlot('onTrigger')
if (trigS == -1) {
this.addInput('onTrigger', LiteGraphInternal.EVENT, {
this.addInput('onTrigger', LiteGraph.EVENT, {
nameLocked: true
})
return this.findInputSlot('onTrigger')
@@ -1273,7 +1266,7 @@ export class LGraphNode
addOnExecutedOutput(): number {
const trigS = this.findOutputSlot('onExecuted')
if (trigS == -1) {
this.addOutput('onExecuted', LiteGraphInternal.ACTION, {
this.addOutput('onExecuted', LiteGraph.ACTION, {
nameLocked: true
})
return this.findOutputSlot('onExecuted')
@@ -1305,7 +1298,7 @@ export class LGraphNode
break
// @ts-expect-error Not impl.
case LiteGraphInternal.ON_REQUEST:
case LiteGraph.ON_REQUEST:
break
default:
@@ -1392,12 +1385,12 @@ export class LGraphNode
return
}
if (this.graph) this.graph._last_trigger_time = LiteGraphInternal.getTime()
if (this.graph) this.graph._last_trigger_time = LiteGraph.getTime()
for (const [i, output] of outputs.entries()) {
if (
!output ||
output.type !== LiteGraphInternal.EVENT ||
output.type !== LiteGraph.EVENT ||
(action && output.name != action)
) {
continue
@@ -1437,7 +1430,7 @@ export class LGraphNode
if (!links || !links.length) return
if (!this.graph) throw new NullGraphError()
this.graph._last_trigger_time = LiteGraphInternal.getTime()
this.graph._last_trigger_time = LiteGraph.getTime()
// for every link attached here
for (const id of links) {
@@ -1448,7 +1441,7 @@ export class LGraphNode
// not connected
if (!link_info) continue
link_info._last_time = LiteGraphInternal.getTime()
link_info._last_time = LiteGraph.getTime()
const node = this.graph.getNodeById(link_info.target_id)
// node not found?
if (!node) continue
@@ -1558,8 +1551,8 @@ export class LGraphNode
this.outputs.push(output)
this.onOutputAdded?.(output)
if (LiteGraphInternal.auto_load_slot_types)
LiteGraphInternal.registerNodeAndSlotType(this, type, true)
if (LiteGraph.auto_load_slot_types)
LiteGraph.registerNodeAndSlotType(this, type, true)
this.expandToFitContent()
this.setDirtyCanvas(true, true)
@@ -1616,7 +1609,7 @@ export class LGraphNode
this.expandToFitContent()
this.onInputAdded?.(input)
LiteGraphInternal.registerNodeAndSlotType(this, type)
LiteGraph.registerNodeAndSlotType(this, type)
this.setDirtyCanvas(true, true)
return input
@@ -1663,9 +1656,9 @@ export class LGraphNode
const size = out || new Float32Array([0, 0])
rows = Math.max(rows, 1)
// although it should be graphcanvas.inner_text_font size
const font_size = LiteGraphInternal.NODE_TEXT_SIZE
const font_size = LiteGraph.NODE_TEXT_SIZE
const padLeft = LiteGraphInternal.NODE_TITLE_HEIGHT
const padLeft = LiteGraph.NODE_TITLE_HEIGHT
const padRight = padLeft * 0.33
const title_width =
padLeft + compute_text_size(this.title, this.titleFontStyle) + padRight
@@ -1696,13 +1689,13 @@ export class LGraphNode
}
}
const minWidth = LiteGraphInternal.NODE_WIDTH * (widgets?.length ? 1.5 : 1)
const minWidth = LiteGraph.NODE_WIDTH * (widgets?.length ? 1.5 : 1)
// Text + slot width + centre padding
const centrePadding = input_width && output_width ? 5 : 0
const slotsWidth =
input_width +
output_width +
2 * LiteGraphInternal.NODE_SLOT_HEIGHT +
2 * LiteGraph.NODE_SLOT_HEIGHT +
centrePadding
// Total distance from edge of node to the inner edge of the widget 'previous' arrow button
@@ -1713,8 +1706,7 @@ export class LGraphNode
size[0] = Math.max(slotsWidth, widgetWidth, title_width, minWidth)
size[1] =
(this.constructor.slot_start_y || 0) +
rows * LiteGraphInternal.NODE_SLOT_HEIGHT
(this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT
// Get widget height & expand size if necessary
let widgets_height = 0
@@ -1733,7 +1725,7 @@ export class LGraphNode
widget_height += minHeight
} else {
widget_height += LiteGraphInternal.NODE_WIDGET_HEIGHT
widget_height += LiteGraph.NODE_WIDGET_HEIGHT
}
widgets_height += widget_height + 4
}
@@ -1766,8 +1758,7 @@ export class LGraphNode
inResizeCorner(canvasX: number, canvasY: number): boolean {
const rows = this.outputs ? this.outputs.length : 1
const outputs_offset =
(this.constructor.slot_start_y || 0) +
rows * LiteGraphInternal.NODE_SLOT_HEIGHT
(this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT
return isInRectangle(
canvasX,
canvasY,
@@ -1968,7 +1959,7 @@ export class LGraphNode
// If Vue nodes mode is enabled, skip LiteGraph's direct position update
// The layout store will handle the movement and sync back to LiteGraph
if (LiteGraphInternal.vueNodesMode) {
if (LiteGraph.vueNodesMode) {
// Vue nodes handle their own dragging through the layout store
// This prevents the snap-back issue from conflicting position updates
return
@@ -1992,7 +1983,7 @@ export class LGraphNode
const renderTitle =
titleMode != TitleMode.TRANSPARENT_TITLE &&
titleMode != TitleMode.NO_TITLE
const titleHeight = renderTitle ? LiteGraphInternal.NODE_TITLE_HEIGHT : 0
const titleHeight = renderTitle ? LiteGraph.NODE_TITLE_HEIGHT : 0
out[0] = this.pos[0]
out[1] = this.pos[1] + -titleHeight
@@ -2004,10 +1995,10 @@ export class LGraphNode
this._collapsed_width = Math.min(
this.size[0],
ctx.measureText(this.getTitle() ?? '').width +
LiteGraphInternal.NODE_TITLE_HEIGHT * 2
LiteGraph.NODE_TITLE_HEIGHT * 2
)
out[2] = this._collapsed_width || LiteGraphInternal.NODE_COLLAPSED_WIDTH
out[3] = LiteGraphInternal.NODE_TITLE_HEIGHT
out[2] = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH
out[3] = LiteGraph.NODE_TITLE_HEIGHT
}
}
@@ -2064,7 +2055,7 @@ export class LGraphNode
* @returns true if the x,y point is in the collapse button area, otherwise false
*/
isPointInCollapse(x: number, y: number): boolean {
const squareLength = LiteGraphInternal.NODE_TITLE_HEIGHT
const squareLength = LiteGraph.NODE_TITLE_HEIGHT
return isInRectangle(
x,
y,
@@ -2167,7 +2158,7 @@ export class LGraphNode
const h =
widget.computedHeight ??
widget.computeSize?.(nodeWidth)[1] ??
LiteGraphInternal.NODE_WIDGET_HEIGHT
LiteGraph.NODE_WIDGET_HEIGHT
const maybeDOMWidget = widget as { margin?: number }
const mtop = maybeDOMWidget.margin ?? -2
@@ -2448,12 +2439,10 @@ export class LGraphNode
for (const sourceType of sourceTypes) {
// TODO: Remove _event_ entirely.
const source =
sourceType == '_event_' ? LiteGraphInternal.EVENT : sourceType
const source = sourceType == '_event_' ? LiteGraph.EVENT : sourceType
for (const destType of destTypes) {
const dest =
destType == '_event_' ? LiteGraphInternal.EVENT : destType
const dest = destType == '_event_' ? LiteGraph.EVENT : destType
if (source == dest || source === '*' || dest === '*') {
if (preferFreeSlot && (slot.links?.length || slot.link != null)) {
@@ -2513,10 +2502,9 @@ export class LGraphNode
if (slot >= 0 && slot !== null) return slot
// TODO: Remove or reimpl. events. WILL CREATE THE onTrigger IN SLOT
if (opts.createEventInCase && slotType == LiteGraphInternal.EVENT) {
if (opts.createEventInCase && slotType == LiteGraph.EVENT) {
if (findInputs) return -1
if (LiteGraphInternal.do_add_triggers_slots)
return node.addOnExecutedOutput()
if (LiteGraph.do_add_triggers_slots) return node.addOnExecutedOutput()
}
// connect to the first general output slot if not found a specific type and
@@ -2529,7 +2517,7 @@ export class LGraphNode
opts.wildcardToTyped &&
(slotType == 0 || slotType == '*' || slotType == '')
) {
const opt = { typesNotAccepted: [LiteGraphInternal.EVENT] }
const opt = { typesNotAccepted: [LiteGraph.EVENT] }
const nonEventSlot = findInputs
? node.findInputSlotFree(opt)
: node.findOutputSlotFree(opt)
@@ -2649,7 +2637,7 @@ export class LGraphNode
) {
return (
this.id !== node.id &&
LiteGraphInternal.isValidConnection(fromSlot.type, toSlot.type)
LiteGraph.isValidConnection(fromSlot.type, toSlot.type)
)
}
@@ -2683,13 +2671,12 @@ export class LGraphNode
if (typeof slot === 'string') {
slot = this.findOutputSlot(slot)
if (slot == -1) {
if (LiteGraphInternal.debug)
if (LiteGraph.debug)
console.log(`Connect: Error, no slot of name ${slot}`)
return null
}
} else if (!outputs || slot >= outputs.length) {
if (LiteGraphInternal.debug)
console.log('Connect: Error, slot number not found')
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
return null
}
@@ -2708,13 +2695,13 @@ export class LGraphNode
if (typeof target_slot === 'string') {
targetIndex = target_node.findInputSlot(target_slot)
if (targetIndex == -1) {
if (LiteGraphInternal.debug)
if (LiteGraph.debug)
console.log(`Connect: Error, no slot of name ${targetIndex}`)
return null
}
} else if (target_slot === LiteGraphInternal.EVENT) {
} else if (target_slot === LiteGraph.EVENT) {
// TODO: Events
if (LiteGraphInternal.do_add_triggers_slots) {
if (LiteGraph.do_add_triggers_slots) {
target_node.changeMode(LGraphEventMode.ON_TRIGGER)
targetIndex = target_node.findInputSlot('onTrigger')
} else {
@@ -2741,8 +2728,7 @@ export class LGraphNode
!target_node.inputs ||
targetIndex >= target_node.inputs.length
) {
if (LiteGraphInternal.debug)
console.log('Connect: Error, slot number not found')
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
return null
}
@@ -2753,8 +2739,8 @@ export class LGraphNode
if (output.links?.length) {
if (
output.type === LiteGraphInternal.EVENT &&
!LiteGraphInternal.allow_multi_output_for_events
output.type === LiteGraph.EVENT &&
!LiteGraph.allow_multi_output_for_events
) {
graph.beforeChange()
// @ts-expect-error Unused param
@@ -2797,7 +2783,7 @@ export class LGraphNode
}
// check targetSlot and check connection types
if (!LiteGraphInternal.isValidConnection(output.type, input.type)) {
if (!LiteGraph.isValidConnection(output.type, input.type)) {
this.setDirtyCanvas(false, true)
return null
}
@@ -2968,13 +2954,12 @@ export class LGraphNode
if (typeof slot === 'string') {
slot = this.findOutputSlot(slot)
if (slot == -1) {
if (LiteGraphInternal.debug)
if (LiteGraph.debug)
console.log(`Connect: Error, no slot of name ${slot}`)
return false
}
} else if (!this.outputs || slot >= this.outputs.length) {
if (LiteGraphInternal.debug)
console.log('Connect: Error, slot number not found')
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
return false
}
@@ -3089,12 +3074,12 @@ export class LGraphNode
if (typeof slot === 'string') {
slot = this.findInputSlot(slot)
if (slot == -1) {
if (LiteGraphInternal.debug)
if (LiteGraph.debug)
console.log(`Connect: Error, no slot of name ${slot}`)
return false
}
} else if (!this.inputs || slot >= this.inputs.length) {
if (LiteGraphInternal.debug) {
if (LiteGraph.debug) {
console.log('Connect: Error, slot number not found')
}
return false
@@ -3198,16 +3183,16 @@ export class LGraphNode
} = this
if (this.flags.collapsed) {
const w = this._collapsed_width || LiteGraphInternal.NODE_COLLAPSED_WIDTH
const w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH
out[0] = is_input ? nodeX : nodeX + w
out[1] = nodeY - LiteGraphInternal.NODE_TITLE_HEIGHT * 0.5
out[1] = nodeY - LiteGraph.NODE_TITLE_HEIGHT * 0.5
return out
}
// weird feature that never got finished
if (is_input && slot_number == -1) {
out[0] = nodeX + LiteGraphInternal.NODE_TITLE_HEIGHT * 0.5
out[1] = nodeY + LiteGraphInternal.NODE_TITLE_HEIGHT * 0.5
out[0] = nodeX + LiteGraph.NODE_TITLE_HEIGHT * 0.5
out[1] = nodeY + LiteGraph.NODE_TITLE_HEIGHT * 0.5
return out
}
@@ -3226,7 +3211,7 @@ export class LGraphNode
}
// default vertical slots
const offset = LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
const slotIndex = is_input
? this.#defaultVerticalInputs.indexOf(this.inputs[slot_number])
: this.#defaultVerticalOutputs.indexOf(this.outputs[slot_number])
@@ -3234,7 +3219,7 @@ export class LGraphNode
out[0] = is_input ? nodeX + offset : nodeX + this.size[0] + 1 - offset
out[1] =
nodeY +
(slotIndex + 0.7) * LiteGraphInternal.NODE_SLOT_HEIGHT +
(slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
(this.constructor.slot_start_y || 0)
return out
}
@@ -3312,7 +3297,7 @@ export class LGraphNode
/** @see {@link snapToGrid} */
alignToGrid(): void {
this.snapToGrid(LiteGraphInternal.CANVAS_GRID_SIZE)
this.snapToGrid(LiteGraph.CANVAS_GRID_SIZE)
}
/* Console output */
@@ -3336,7 +3321,7 @@ export class LGraphNode
}
const img: AsyncImageElement = new Image()
img.src = LiteGraphInternal.node_images_path + url
img.src = LiteGraph.node_images_path + url
img.ready = false
const dirty = () => this.setDirtyCanvas(true)
@@ -3431,7 +3416,7 @@ export class LGraphNode
get width() {
return this.collapsed
? this._collapsed_width || LiteGraphInternal.NODE_COLLAPSED_WIDTH
? this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH
: this.size[0]
}
@@ -3439,7 +3424,7 @@ export class LGraphNode
* Returns the height of the node, including the title bar.
*/
get height() {
return LiteGraphInternal.NODE_TITLE_HEIGHT + this.bodyHeight
return LiteGraph.NODE_TITLE_HEIGHT + this.bodyHeight
}
/**
@@ -3462,7 +3447,7 @@ export class LGraphNode
(acc, badge) => acc + badge.getWidth(ctx) + gap,
0
)
const y = -(LiteGraphInternal.NODE_TITLE_HEIGHT + gap)
const y = -(LiteGraph.NODE_TITLE_HEIGHT + gap)
for (const badge of badgeInstances) {
badge.draw(ctx, currentX, y - badge.height)
@@ -3477,7 +3462,7 @@ export class LGraphNode
ctx: CanvasRenderingContext2D,
{
scale,
title_height = LiteGraphInternal.NODE_TITLE_HEIGHT,
title_height = LiteGraph.NODE_TITLE_HEIGHT,
low_quality = false
}: DrawTitleOptions
): void {
@@ -3495,7 +3480,7 @@ export class LGraphNode
}
if (this.collapsed) {
ctx.shadowColor = LiteGraphInternal.DEFAULT_SHADOW_COLOR
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR
}
ctx.fillStyle = this.constructor.title_color || fgcolor
@@ -3510,13 +3495,8 @@ export class LGraphNode
size[0],
title_height,
this.collapsed
? [LiteGraphInternal.ROUND_RADIUS]
: [
LiteGraphInternal.ROUND_RADIUS,
LiteGraphInternal.ROUND_RADIUS,
0,
0
]
? [LiteGraph.ROUND_RADIUS]
: [LiteGraph.ROUND_RADIUS, LiteGraph.ROUND_RADIUS, 0, 0]
)
}
ctx.fill()
@@ -3533,7 +3513,7 @@ export class LGraphNode
{
scale,
low_quality = false,
title_height = LiteGraphInternal.NODE_TITLE_HEIGHT,
title_height = LiteGraph.NODE_TITLE_HEIGHT,
box_size = 10
}: DrawTitleBoxOptions
): void {
@@ -3609,7 +3589,7 @@ export class LGraphNode
scale,
default_title_color,
low_quality = false,
title_height = LiteGraphInternal.NODE_TITLE_HEIGHT
title_height = LiteGraph.NODE_TITLE_HEIGHT
}: DrawTitleTextOptions
): void {
const size = this.renderingSize
@@ -3637,7 +3617,7 @@ export class LGraphNode
const title = String(rawTitle) + (this.pinned ? '📌' : '')
if (title) {
if (selected) {
ctx.fillStyle = LiteGraphInternal.NODE_SELECTED_TITLE_COLOR
ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR
} else {
ctx.fillStyle = this.constructor.title_text_color || default_title_color
}
@@ -3676,7 +3656,7 @@ export class LGraphNode
ctx.fillText(
displayTitle,
title_height,
LiteGraphInternal.NODE_TITLE_TEXT_Y - title_height
LiteGraph.NODE_TITLE_TEXT_Y - title_height
)
}
}
@@ -3709,10 +3689,7 @@ export class LGraphNode
if (input.link == null) continue
const output = outputs[index]
if (
!output ||
!LiteGraphInternal.isValidConnection(input.type, output.type)
)
if (!output || !LiteGraph.isValidConnection(input.type, output.type))
continue
const inLink = _links.get(input.link)
@@ -3736,8 +3713,7 @@ export class LGraphNode
if (!inNode) continue
for (const output of outputs) {
if (!LiteGraphInternal.isValidConnection(input.type, output.type))
continue
if (!LiteGraph.isValidConnection(input.type, output.type)) continue
bypassAllLinks(output, inNode, inLink, graph)
break
@@ -3788,7 +3764,7 @@ export class LGraphNode
const nodeWidth = this.size[0]
const { widgets } = this
const H = LiteGraphInternal.NODE_WIDGET_HEIGHT
const H = LiteGraph.NODE_WIDGET_HEIGHT
const showText = !lowQuality
ctx.save()
ctx.globalAlpha = editorAlpha
@@ -3798,8 +3774,8 @@ export class LGraphNode
const { y } = widget
const outlineColour = widget.advanced
? LiteGraphInternal.WIDGET_ADVANCED_OUTLINE_COLOR
: LiteGraphInternal.WIDGET_OUTLINE_COLOR
? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR
: LiteGraph.WIDGET_OUTLINE_COLOR
widget.last_y = y
// Disable widget if it is disabled or if the value is passed from socket connection.
@@ -3857,12 +3833,12 @@ export class LGraphNode
? this.getInputPos(slotIndex)
: this.getOutputPos(slotIndex)
slot.boundingRect[0] = pos[0] - LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
slot.boundingRect[1] = pos[1] - LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
slot.boundingRect[0] = pos[0] - LiteGraph.NODE_SLOT_HEIGHT * 0.5
slot.boundingRect[1] = pos[1] - LiteGraph.NODE_SLOT_HEIGHT * 0.5
slot.boundingRect[2] = slot.isWidgetInputSlot
? BaseWidget.margin
: LiteGraphInternal.NODE_SLOT_HEIGHT
slot.boundingRect[3] = LiteGraphInternal.NODE_SLOT_HEIGHT
: LiteGraph.NODE_SLOT_HEIGHT
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
}
#measureSlots(): ReadOnlyRect | null {
@@ -3997,7 +3973,7 @@ export class LGraphNode
w
})
} else {
const height = LiteGraphInternal.NODE_WIDGET_HEIGHT + 4
const height = LiteGraph.NODE_WIDGET_HEIGHT + 4
w.computedHeight = height
fixedWidgetHeight += height
}
@@ -4060,13 +4036,13 @@ export class LGraphNode
// Only set custom pos if not using Vue positioning
// Vue positioning calculates widget slot positions dynamically
if (!LiteGraphInternal.vueNodesMode) {
if (!LiteGraph.vueNodesMode) {
for (const widget of this.widgets) {
const slot = slotByWidgetName.get(widget.name)
if (!slot) continue
const actualSlot = this.#concreteInputs[slot.index]
const offset = LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
actualSlot.pos = [offset, widget.y + offset]
this.#measureSlot(actualSlot, slot.index, true)
}

View File

@@ -205,7 +205,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
linkSegment: LinkSegment
): Reroute[] {
if (linkSegment.parentId === undefined) return []
if (!linkSegment.parentId) return []
return network.reroutes.get(linkSegment.parentId)?.getReroutes() ?? []
}
@@ -229,7 +229,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
linkSegment: LinkSegment,
rerouteId: RerouteId
): Reroute | null | undefined {
if (linkSegment.parentId === undefined) return
if (!linkSegment.parentId) return
return network.reroutes
.get(linkSegment.parentId)
?.findNextReroute(rerouteId)
@@ -498,7 +498,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
target_slot: this.target_slot,
type: this.type
}
if (this.parentId !== undefined) copy.parentId = this.parentId
if (this.parentId) copy.parentId = this.parentId
return copy
}
}

View File

@@ -1,7 +0,0 @@
import { LiteGraphGlobal } from './LiteGraphGlobal'
// To prevent circular dependency in litegraph/itself.ts -> litegraph.ts -> litegraph/itself.ts
// For all src/lib/litegraph/**/*.ts files, We should import litegraph instance from here
// And for imports outside from litegraph, We can import it directly from 'src/lib/litegraph/src/litegraph' for simplicity
export const LiteGraphInternal = new LiteGraphGlobal()

View File

@@ -1,6 +1,6 @@
import { LiteGraphInternal } from './LiteGraphInternal'
import type { Rectangle } from './infrastructure/Rectangle'
import type { CanvasColour, Rect } from './interfaces'
import { LiteGraph } from './litegraph'
import { RenderShape, TitleMode } from './types/globalEnums'
const ELLIPSIS = '\u2026'
@@ -80,12 +80,12 @@ export function strokeShape(
}: IDrawBoundingOptions = {}
): void {
// These param defaults are not compile-time static, and must be re-evaluated at runtime
round_radius ??= LiteGraphInternal.ROUND_RADIUS
color ??= LiteGraphInternal.NODE_BOX_OUTLINE_COLOR
round_radius ??= LiteGraph.ROUND_RADIUS
color ??= LiteGraph.NODE_BOX_OUTLINE_COLOR
// Adjust area if title is transparent
if (title_mode === TitleMode.TRANSPARENT_TITLE) {
const height = title_height ?? LiteGraphInternal.NODE_TITLE_HEIGHT
const height = title_height ?? LiteGraph.NODE_TITLE_HEIGHT
area[1] -= height
area[3] += height
}

View File

@@ -82,7 +82,6 @@ export interface Positionable extends Parent<Positionable>, HasBoundingRect {
* @default 0,0
*/
readonly pos: Point
readonly size?: Size
/** true if this object is part of the selection, otherwise false. */
selected?: boolean

View File

@@ -1,6 +1,6 @@
import type { ContextMenu } from './ContextMenu'
import type { LGraphNode } from './LGraphNode'
import { LiteGraphInternal } from './LiteGraphInternal'
import { LiteGraphGlobal } from './LiteGraphGlobal'
import type { ConnectingLink, Point } from './interfaces'
import type { IContextMenuOptions, Size } from './interfaces'
import { loadPolyfills } from './polyfills'
@@ -10,10 +10,7 @@ import type { RenderShape, TitleMode } from './types/globalEnums'
// Must remain above LiteGraphGlobal (circular dependency due to abstract factory behaviour in `configure`)
export { Subgraph } from './subgraph/Subgraph'
// Note: To prevent circular dependency in litegraph/itself.ts -> litegraph.ts -> litegraph/itself.ts
// For all src/lib/litegraph/**/*.ts files, We should import litegraph instance from LiteGraphInternal
// And for imports outside from litegraph, We can import it directly from here for simplicity
export const LiteGraph = LiteGraphInternal
export const LiteGraph = new LiteGraphGlobal()
// Load legacy polyfills
loadPolyfills()
@@ -51,6 +48,7 @@ export interface LinkReleaseContextExtended {
links: ConnectingLink[]
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface LiteGraphCanvasEvent extends CustomEvent<CanvasEventDetail> {}
export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
@@ -142,7 +140,7 @@ export { BaseWidget } from './widgets/BaseWidget'
export { LegacyWidget } from './widgets/LegacyWidget'
export { isComboWidget, isAssetWidget } from './widgets/widgetMap'
export { isComboWidget } from './widgets/widgetMap'
// Additional test-specific exports
export { LGraphButton } from './LGraphButton'
export { MovingOutputLink } from './canvas/MovingOutputLink'

View File

@@ -7,14 +7,13 @@ import type {
OptionalProps,
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
import { isSubgraphInput } from '@/lib/litegraph/src/subgraph/subgraphUtils'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { LiteGraphInternal } from '../LiteGraphInternal'
export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
link: LinkId | null
@@ -34,7 +33,7 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
}
get collapsedPos(): ReadOnlyPoint {
return [0, LiteGraphInternal.NODE_TITLE_HEIGHT * -0.5]
return [0, LiteGraph.NODE_TITLE_HEIGHT * -0.5]
}
constructor(
@@ -53,11 +52,11 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput
): boolean {
if ('links' in fromSlot) {
return LiteGraphInternal.isValidConnection(fromSlot.type, this.type)
return LiteGraph.isValidConnection(fromSlot.type, this.type)
}
if (isSubgraphInput(fromSlot)) {
return LiteGraphInternal.isValidConnection(fromSlot.type, this.type)
return LiteGraph.isValidConnection(fromSlot.type, this.type)
}
return false

View File

@@ -7,13 +7,12 @@ import type {
OptionalProps,
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
import { isSubgraphOutput } from '@/lib/litegraph/src/subgraph/subgraphUtils'
import { LiteGraphInternal } from '../LiteGraphInternal'
export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
#node: LGraphNode
@@ -27,8 +26,8 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
get collapsedPos(): ReadOnlyPoint {
return [
this.#node._collapsed_width ?? LiteGraphInternal.NODE_COLLAPSED_WIDTH,
LiteGraphInternal.NODE_TITLE_HEIGHT * -0.5
this.#node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH,
LiteGraph.NODE_TITLE_HEIGHT * -0.5
]
}
@@ -47,11 +46,11 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput
): boolean {
if ('link' in fromSlot) {
return LiteGraphInternal.isValidConnection(this.type, fromSlot.type)
return LiteGraph.isValidConnection(this.type, fromSlot.type)
}
if (isSubgraphOutput(fromSlot)) {
return LiteGraphInternal.isValidConnection(this.type, fromSlot.type)
return LiteGraph.isValidConnection(this.type, fromSlot.type)
}
return false

View File

@@ -11,7 +11,7 @@ import type {
Point,
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { Rectangle } from '@/lib/litegraph/src/litegraph'
import { LiteGraph, Rectangle } from '@/lib/litegraph/src/litegraph'
import { getCentre } from '@/lib/litegraph/src/measure'
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
@@ -20,7 +20,6 @@ import {
RenderShape
} from '@/lib/litegraph/src/types/globalEnums'
import { LiteGraphInternal } from '../LiteGraphInternal'
import { NodeInputSlot } from './NodeInputSlot'
import { SlotBase } from './SlotBase'
@@ -62,9 +61,9 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
get highlightColor(): CanvasColour {
return (
LiteGraphInternal.NODE_TEXT_HIGHLIGHT_COLOR ??
LiteGraphInternal.NODE_SELECTED_TITLE_COLOR ??
LiteGraphInternal.NODE_TEXT_COLOR
LiteGraph.NODE_TEXT_HIGHLIGHT_COLOR ??
LiteGraph.NODE_SELECTED_TITLE_COLOR ??
LiteGraph.NODE_TEXT_COLOR
)
}
@@ -124,7 +123,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
const labelColor = highlight
? this.highlightColor
: LiteGraphInternal.NODE_TEXT_COLOR
: LiteGraph.NODE_TEXT_COLOR
const pos = this.#centreOffset
const slot_type = this.type

View File

@@ -9,9 +9,8 @@ import type {
CallbackReturn,
ISlotType
} from '@/lib/litegraph/src/interfaces'
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
import { LGraphEventMode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LiteGraphInternal } from '../LiteGraphInternal'
import type { Subgraph } from './Subgraph'
import type { SubgraphNode } from './SubgraphNode'
@@ -347,8 +346,8 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
// Prefer input with the same slot ID
if (
oppositeInput &&
LiteGraphInternal.isValidConnection(oppositeInput.type, outputType) &&
LiteGraphInternal.isValidConnection(oppositeInput.type, type)
LiteGraph.isValidConnection(oppositeInput.type, outputType) &&
LiteGraph.isValidConnection(oppositeInput.type, type)
) {
return slot
}
@@ -360,8 +359,8 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
// Find first matching slot - prefer exact type
return inputs.findIndex(
(input) =>
LiteGraphInternal.isValidConnection(input.type, outputType) &&
LiteGraphInternal.isValidConnection(input.type, type)
LiteGraph.isValidConnection(input.type, outputType) &&
LiteGraph.isValidConnection(input.type, type)
)
}

View File

@@ -9,11 +9,12 @@ import type {
Point,
Positionable
} from '@/lib/litegraph/src/interfaces'
import type {
CanvasColour,
CanvasPointer,
CanvasPointerEvent,
IContextMenuValue
import {
type CanvasColour,
type CanvasPointer,
type CanvasPointerEvent,
type IContextMenuValue,
LiteGraph
} from '@/lib/litegraph/src/litegraph'
import { snapPoint } from '@/lib/litegraph/src/measure'
import { CanvasItem } from '@/lib/litegraph/src/types/globalEnums'
@@ -22,7 +23,6 @@ import type {
Serialisable
} from '@/lib/litegraph/src/types/serialisation'
import { LiteGraphInternal } from '../LiteGraphInternal'
import type { EmptySubgraphInput } from './EmptySubgraphInput'
import type { EmptySubgraphOutput } from './EmptySubgraphOutput'
import type { Subgraph } from './Subgraph'
@@ -194,7 +194,7 @@ export abstract class SubgraphIONodeBase<
const options: (IContextMenuValue | null)[] = this.#getSlotMenuOptions(slot)
if (!(options.length > 0)) return
new LiteGraphInternal.ContextMenu(options, {
new LiteGraph.ContextMenu(options, {
event: event as any,
title: slot.name || 'Subgraph Output',
callback: (item: IContextMenuValue) => {

View File

@@ -9,10 +9,10 @@ import type {
Point,
ReadOnlyRect
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { LiteGraphInternal } from '../LiteGraphInternal'
import type { SubgraphInputNode } from './SubgraphInputNode'
import type { SubgraphOutput } from './SubgraphOutput'
import { SubgraphSlot } from './SubgraphSlotBase'
@@ -237,12 +237,12 @@ export class SubgraphInput extends SubgraphSlot {
if (isNodeSlot(fromSlot)) {
return (
'link' in fromSlot &&
LiteGraphInternal.isValidConnection(this.type, fromSlot.type)
LiteGraph.isValidConnection(this.type, fromSlot.type)
)
}
if (isSubgraphOutput(fromSlot)) {
return LiteGraphInternal.isValidConnection(this.type, fromSlot.type)
return LiteGraph.isValidConnection(this.type, fromSlot.type)
}
return false

View File

@@ -9,9 +9,9 @@ import type {
Point,
ReadOnlyRect
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
import { LiteGraphInternal } from '../LiteGraphInternal'
import type { SubgraphInput } from './SubgraphInput'
import type { SubgraphOutputNode } from './SubgraphOutputNode'
import { SubgraphSlot } from './SubgraphSlotBase'
@@ -39,7 +39,7 @@ export class SubgraphOutput extends SubgraphSlot {
const { subgraph } = this.parent
// Validate type compatibility
if (!LiteGraphInternal.isValidConnection(slot.type, this.type)) return
if (!LiteGraph.isValidConnection(slot.type, this.type)) return
// Allow nodes to block connection
const outputIndex = node.outputs.indexOf(slot)
@@ -143,12 +143,12 @@ export class SubgraphOutput extends SubgraphSlot {
if (isNodeSlot(fromSlot)) {
return (
'links' in fromSlot &&
LiteGraphInternal.isValidConnection(fromSlot.type, this.type)
LiteGraph.isValidConnection(fromSlot.type, this.type)
)
}
if (isSubgraphInput(fromSlot)) {
return LiteGraphInternal.isValidConnection(fromSlot.type, this.type)
return LiteGraph.isValidConnection(fromSlot.type, this.type)
}
return false

View File

@@ -14,6 +14,7 @@ import type {
ReadOnlyRect,
ReadOnlySize
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { SlotBase } from '@/lib/litegraph/src/node/SlotBase'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type {
@@ -22,7 +23,6 @@ import type {
} from '@/lib/litegraph/src/types/serialisation'
import { type UUID, createUuidv4 } from '@/lib/litegraph/src/utils/uuid'
import { LiteGraphInternal } from '../LiteGraphInternal'
import type { SubgraphInput } from './SubgraphInput'
import type { SubgraphInputNode } from './SubgraphInputNode'
import type { SubgraphOutput } from './SubgraphOutput'
@@ -42,7 +42,7 @@ export abstract class SubgraphSlot
implements SubgraphIO, Hoverable, Serialisable<SubgraphIO>
{
static get defaultHeight() {
return LiteGraphInternal.NODE_SLOT_HEIGHT
return LiteGraph.NODE_SLOT_HEIGHT
}
readonly #pos: Point = new Float32Array(2)
@@ -228,9 +228,7 @@ export abstract class SubgraphSlot
if (this.displayName) {
const [labelX, labelY] = this.labelPos
// Also apply highlight logic to text color
ctx.fillStyle = highlight
? 'white'
: LiteGraphInternal.NODE_TEXT_COLOR || '#AAA'
ctx.fillStyle = highlight ? 'white' : LiteGraph.NODE_TEXT_COLOR || '#AAA'
ctx.fillText(this.displayName, labelX, labelY)
}

View File

@@ -12,7 +12,7 @@ import type {
INodeOutputSlot,
Positionable
} from '@/lib/litegraph/src/interfaces'
import { createUuidv4 } from '@/lib/litegraph/src/litegraph'
import { LiteGraph, createUuidv4 } from '@/lib/litegraph/src/litegraph'
import { nextUniqueName } from '@/lib/litegraph/src/strings'
import type {
ISerialisedNode,
@@ -21,7 +21,6 @@ import type {
} from '@/lib/litegraph/src/types/serialisation'
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
import { LiteGraphInternal } from '../LiteGraphInternal'
import type { GraphOrSubgraph } from './Subgraph'
import type { SubgraphInput } from './SubgraphInput'
import { SubgraphInputNode } from './SubgraphInputNode'
@@ -218,14 +217,14 @@ export function multiClone(nodes: Iterable<LGraphNode>): ISerialisedNode[] {
// Selectively clone - keep IDs & links
for (const node of nodes) {
const newNode = LiteGraphInternal.createNode(node.type)
const newNode = LiteGraph.createNode(node.type)
if (!newNode) {
console.warn('Failed to create node', node.type)
continue
}
// Must be cloned; litegraph "serialize" is mostly shallow clone
const data = LiteGraphInternal.cloneObject(node.serialize())
const data = LiteGraph.cloneObject(node.serialize())
newNode.configure(data)
clonedNodes.push(newNode.serialize())

View File

@@ -4,7 +4,6 @@ import type { LGraphConfig, LGraphExtra, LGraphState } from '../LGraph'
import type { IGraphGroupFlags } from '../LGraphGroup'
import type { NodeId, NodeProperty } from '../LGraphNode'
import type { LinkId, SerialisedLLinkArray } from '../LLink'
import type { LiteGraphInternal } from '../LiteGraphInternal'
import type { FloatingRerouteSlot, RerouteId } from '../Reroute'
import type {
Dictionary,
@@ -16,6 +15,7 @@ import type {
Point,
Size
} from '../interfaces'
import type { LiteGraph } from '../litegraph'
import type { TWidgetValue } from '../types/widgets'
import type { RenderShape } from './globalEnums'
@@ -124,7 +124,7 @@ export interface ISerialisedGraph extends BaseExportedGraph {
links: SerialisedLLinkArray[]
floatingLinks?: SerialisableLLink[]
groups: ISerialisedGroup[]
version: typeof LiteGraphInternal.VERSION
version: typeof LiteGraph.VERSION
extra?: LGraphExtra
}

View File

@@ -1,4 +1,4 @@
import { LiteGraphInternal } from '../LiteGraphInternal'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
/** Guard against unbound allocation. */
const UNIQUE_MESSAGE_LIMIT = 10_000
@@ -11,7 +11,7 @@ const sentWarnings: Set<string> = new Set()
* @param source A reference object to include alongside the message, e.g. `this`.
*/
export function warnDeprecated(message: string, source?: object): void {
if (!LiteGraphInternal.alwaysRepeatWarnings) {
if (!LiteGraph.alwaysRepeatWarnings) {
// Do not repeat
if (sentWarnings.has(message)) return
@@ -21,7 +21,7 @@ export function warnDeprecated(message: string, source?: object): void {
sentWarnings.add(message)
}
for (const callback of LiteGraphInternal.onDeprecationWarning) {
for (const callback of LiteGraph.onDeprecationWarning) {
callback(message, source)
}
}

View File

@@ -13,22 +13,6 @@ export class AssetWidget
this.value = widget.value?.toString() ?? ''
}
override set value(value: IAssetWidget['value']) {
const oldValue = this.value
super.value = value
// Force canvas redraw when value changes to show update immediately
if (oldValue !== value && this.node.graph?.list_of_graphcanvas) {
for (const canvas of this.node.graph.list_of_graphcanvas) {
canvas.setDirty(true)
}
}
}
override get value(): IAssetWidget['value'] {
return super.value
}
override get _displayValue(): string {
return String(this.value) //FIXME: Resolve asset name
}

View File

@@ -7,11 +7,10 @@ import type {
LGraphNode,
Size
} from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { LiteGraphInternal } from '../LiteGraphInternal'
export interface DrawWidgetOptions {
/** The width of the node where this widget will be displayed. */
width: number
@@ -148,28 +147,28 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
get outline_color() {
return this.advanced
? LiteGraphInternal.WIDGET_ADVANCED_OUTLINE_COLOR
: LiteGraphInternal.WIDGET_OUTLINE_COLOR
? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR
: LiteGraph.WIDGET_OUTLINE_COLOR
}
get background_color() {
return LiteGraphInternal.WIDGET_BGCOLOR
return LiteGraph.WIDGET_BGCOLOR
}
get height() {
return LiteGraphInternal.NODE_WIDGET_HEIGHT
return LiteGraph.NODE_WIDGET_HEIGHT
}
get text_color() {
return LiteGraphInternal.WIDGET_TEXT_COLOR
return LiteGraph.WIDGET_TEXT_COLOR
}
get secondary_text_color() {
return LiteGraphInternal.WIDGET_SECONDARY_TEXT_COLOR
return LiteGraph.WIDGET_SECONDARY_TEXT_COLOR
}
get disabledTextColor() {
return LiteGraphInternal.WIDGET_DISABLED_TEXT_COLOR
return LiteGraph.WIDGET_DISABLED_TEXT_COLOR
}
get displayName() {
@@ -256,7 +255,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
if (requiredWidth <= totalWidth) {
// Draw label & value normally
drawTextInArea({ ctx, text: displayName, area, align: 'left' })
} else if (LiteGraphInternal.truncateWidgetTextEvenly) {
} else if (LiteGraph.truncateWidgetTextEvenly) {
// Label + value will not fit - scale evenly to fit
const scale = (totalWidth - gap) / (requiredWidth - gap)
area.width = labelWidth * scale
@@ -266,7 +265,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
// Move the area to the right to render the value
area.right = x + totalWidth
area.setWidthRightAnchored(valueWidth * scale)
} else if (LiteGraphInternal.truncateWidgetValuesFirst) {
} else if (LiteGraph.truncateWidgetValuesFirst) {
// Label + value will not fit - use legacy scaling of value first
const cappedLabelWidth = Math.min(labelWidth, totalWidth)

View File

@@ -1,13 +1,13 @@
import { clamp } from 'es-toolkit/compat'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type {
IComboWidget,
IStringComboWidget
} from '@/lib/litegraph/src/types/widgets'
import { warnDeprecated } from '@/lib/litegraph/src/utils/feedback'
import { LiteGraphInternal } from '../LiteGraphInternal'
import { BaseSteppedWidget } from './BaseSteppedWidget'
import type { WidgetEventOptions } from './BaseWidget'
@@ -133,7 +133,7 @@ export class ComboWidget
// Handle center click - show dropdown menu
const text_values = values != values_list ? Object.values(values) : values
new LiteGraphInternal.ContextMenu(text_values, {
new LiteGraph.ContextMenu(text_values, {
scale: Math.max(1, canvas.ds.scale),
event: e,
className: 'dark',

View File

@@ -1,7 +1,7 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { LiteGraphInternal } from '../LiteGraphInternal'
import { BaseWidget, type DrawWidgetOptions } from './BaseWidget'
/**
@@ -27,7 +27,7 @@ export class LegacyWidget<TWidget extends IBaseWidget = IBaseWidget>
ctx: CanvasRenderingContext2D,
options: DrawWidgetOptions
) {
const H = LiteGraphInternal.NODE_WIDGET_HEIGHT
const H = LiteGraph.NODE_WIDGET_HEIGHT
this.draw?.(ctx, this.node, options.width, this.y, H, !!options.showText)
}

View File

@@ -1,6 +1,5 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type {
IAssetWidget,
IBaseWidget,
IComboWidget,
IWidget,
@@ -133,9 +132,4 @@ export function isComboWidget(widget: IBaseWidget): widget is IComboWidget {
return widget.type === 'combo'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IAssetWidget}. */
export function isAssetWidget(widget: IBaseWidget): widget is IAssetWidget {
return widget.type === 'asset'
}
// #endregion Type Guards

View File

@@ -1,8 +1,7 @@
import { describe } from 'vitest'
import { LGraph } from '@/lib/litegraph/src/litegraph'
import { LGraph, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LiteGraphInternal } from '../src/LiteGraphInternal'
import { test } from './testExtensions'
describe('LGraph', () => {
@@ -18,8 +17,8 @@ describe('LGraph', () => {
const directImport = await import('@/lib/litegraph/src/LGraph')
const entryPointImport = await import('@/lib/litegraph/src/litegraph')
expect(LiteGraphInternal.LGraph).toBe(directImport.LGraph)
expect(LiteGraphInternal.LGraph).toBe(entryPointImport.LGraph)
expect(LiteGraph.LGraph).toBe(directImport.LGraph)
expect(LiteGraph.LGraph).toBe(entryPointImport.LGraph)
})
test('populates optional values', ({ expect, minimalSerialisableGraph }) => {
@@ -140,6 +139,6 @@ describe('Legacy LGraph Compatibility Layer', () => {
})
test('is correctly assigned to LiteGraph', ({ expect }) => {
expect(LiteGraphInternal.LGraph).toBe(LGraph)
expect(LiteGraph.LGraph).toBe(LGraph)
})
})

View File

@@ -1,9 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LiteGraphInternal } from '../src/LiteGraphInternal'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
describe('LGraphCanvas Title Button Rendering', () => {
let canvas: LGraphCanvas
@@ -117,7 +115,7 @@ describe('LGraphCanvas Title Button Rendering', () => {
// Check draw positions (right-aligned from node width)
// First button (rightmost): 200 - 5 = 195, then subtract width
// Second button: first button position - 5 - button width
const titleHeight = LiteGraphInternal.NODE_TITLE_HEIGHT
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
const buttonY = -titleHeight + (titleHeight - 20) / 2 // Centered
expect(draw1).toHaveBeenCalledWith(ctx, 180, buttonY) // 200 - 20
expect(draw2).toHaveBeenCalledWith(ctx, 153, buttonY) // 180 - 2 - 25
@@ -182,7 +180,7 @@ describe('LGraphCanvas Title Button Rendering', () => {
canvas.drawNode(node, ctx)
const titleHeight = LiteGraphInternal.NODE_TITLE_HEIGHT
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
// Check positions are correctly spaced (right to left)
// Starting position: 200
@@ -211,8 +209,7 @@ describe('LGraphCanvas Title Button Rendering', () => {
// Buttons should still be rendered in low quality mode
const buttonY =
-LiteGraphInternal.NODE_TITLE_HEIGHT +
(LiteGraphInternal.NODE_TITLE_HEIGHT - 20) / 2
-LiteGraph.NODE_TITLE_HEIGHT + (LiteGraph.NODE_TITLE_HEIGHT - 20) / 2
expect(drawSpy).toHaveBeenCalledWith(ctx, 180, buttonY)
})
@@ -239,7 +236,7 @@ describe('LGraphCanvas Title Button Rendering', () => {
canvas.drawNode(node, ctx)
const titleHeight = LiteGraphInternal.NODE_TITLE_HEIGHT
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
// Small button (rightmost): 200 - 15 = 185
const buttonY = -titleHeight + (titleHeight - 20) / 2
@@ -266,7 +263,7 @@ describe('LGraphCanvas Title Button Rendering', () => {
canvas.drawNode(node, ctx)
const titleHeight = LiteGraphInternal.NODE_TITLE_HEIGHT
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
// Should use new width: 300 - 20 = 280
const buttonY = -titleHeight + (titleHeight - 20) / 2
expect(drawSpy).toHaveBeenCalledWith(ctx, 280, buttonY)

View File

@@ -1,8 +1,7 @@
import { beforeEach, describe, expect } from 'vitest'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LiteGraphInternal } from '../src/LiteGraphInternal'
import { test } from './testExtensions'
describe('LGraphNode resize functionality', () => {
@@ -10,7 +9,7 @@ describe('LGraphNode resize functionality', () => {
beforeEach(() => {
// Set up LiteGraph constants needed for measure
LiteGraphInternal.NODE_TITLE_HEIGHT = 20
LiteGraph.NODE_TITLE_HEIGHT = 20
node = new LGraphNode('Test Node')
node.pos = [100, 100]

View File

@@ -1,13 +1,12 @@
import { afterEach, beforeEach, describe, expect, vi } from 'vitest'
import type { INodeInputSlot, Point } from '@/lib/litegraph/src/interfaces'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LGraph } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot'
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
import { LiteGraphInternal } from '../src/LiteGraphInternal'
import { test } from './testExtensions'
function getMockISerialisedNode(
@@ -29,14 +28,14 @@ function getMockISerialisedNode(
describe('LGraphNode', () => {
let node: LGraphNode
let origLiteGraph: typeof LiteGraphInternal
let origLiteGraph: typeof LiteGraph
beforeEach(() => {
origLiteGraph = Object.assign({}, LiteGraphInternal)
origLiteGraph = Object.assign({}, LiteGraph)
// @ts-expect-error TODO: Fix after merge - Classes property not in type
delete origLiteGraph.Classes
Object.assign(LiteGraphInternal, {
Object.assign(LiteGraph, {
NODE_TITLE_HEIGHT: 20,
NODE_SLOT_HEIGHT: 15,
NODE_TEXT_SIZE: 14,
@@ -53,7 +52,7 @@ describe('LGraphNode', () => {
})
afterEach(() => {
Object.assign(LiteGraphInternal, origLiteGraph)
Object.assign(LiteGraph, origLiteGraph)
})
test('should serialize position/size correctly', () => {
@@ -461,8 +460,8 @@ describe('LGraphNode', () => {
node.addOutput('output2', 'number')
// Calculate expected positions
const slotOffset = LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
const slotSpacing = LiteGraphInternal.NODE_SLOT_HEIGHT
const slotOffset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
const slotSpacing = LiteGraph.NODE_SLOT_HEIGHT
const nodeWidth = node.size[0]
// Test input positions
@@ -497,8 +496,8 @@ describe('LGraphNode', () => {
node.addInput('default-input1', 'number')
node.addInput('default-input2', 'number')
const slotOffset = LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
const slotSpacing = LiteGraphInternal.NODE_SLOT_HEIGHT
const slotOffset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
const slotSpacing = LiteGraph.NODE_SLOT_HEIGHT
// Test: default positioned slots should be consecutive, ignoring absolute positioned ones
expect(node.getInputPos(1)).toEqual([
@@ -577,10 +576,7 @@ describe('LGraphNode', () => {
})
test('should return position based on title height when collapsed', () => {
node.flags.collapsed = true
const expectedPos: Point = [
100,
200 - LiteGraphInternal.NODE_TITLE_HEIGHT * 0.5
]
const expectedPos: Point = [100, 200 - LiteGraph.NODE_TITLE_HEIGHT * 0.5]
expect(node.getInputSlotPos(inputSlot)).toEqual(expectedPos)
})
@@ -604,16 +600,12 @@ describe('LGraphNode', () => {
const slotIndex = 0
const nodeOffsetY = (node.constructor as any).slot_start_y || 0
const expectedY =
200 +
(slotIndex + 0.7) * LiteGraphInternal.NODE_SLOT_HEIGHT +
nodeOffsetY
const expectedX = 100 + LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY
const expectedX = 100 + LiteGraph.NODE_SLOT_HEIGHT * 0.5
expect(node.getInputSlotPos(inputSlot)).toEqual([expectedX, expectedY])
const slotIndex2 = 1
const expectedY2 =
200 +
(slotIndex2 + 0.7) * LiteGraphInternal.NODE_SLOT_HEIGHT +
nodeOffsetY
200 + (slotIndex2 + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY
expect(node.getInputSlotPos(inputSlot2)).toEqual([expectedX, expectedY2])
})
@@ -624,10 +616,8 @@ describe('LGraphNode', () => {
const slotIndex = 0
const nodeOffsetY = 25
const expectedY =
200 +
(slotIndex + 0.7) * LiteGraphInternal.NODE_SLOT_HEIGHT +
nodeOffsetY
const expectedX = 100 + LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY
const expectedX = 100 + LiteGraph.NODE_SLOT_HEIGHT * 0.5
expect(node.getInputSlotPos(inputSlot)).toEqual([expectedX, expectedY])
delete (node.constructor as any).slot_start_y
})
@@ -660,10 +650,8 @@ describe('LGraphNode', () => {
const slotIndex = 0
const nodeOffsetY = (node.constructor as any).slot_start_y || 0
const expectedDefaultY =
200 +
(slotIndex + 0.7) * LiteGraphInternal.NODE_SLOT_HEIGHT +
nodeOffsetY
const expectedDefaultX = 100 + LiteGraphInternal.NODE_SLOT_HEIGHT * 0.5
200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY
const expectedDefaultX = 100 + LiteGraph.NODE_SLOT_HEIGHT * 0.5
expect(node.getInputPos(0)).toEqual([expectedDefaultX, expectedDefaultY])
spy.mockRestore()
})
@@ -672,7 +660,7 @@ describe('LGraphNode', () => {
describe('removeInput/removeOutput on copied nodes', () => {
beforeEach(() => {
// Register a test node type so clone() can work
LiteGraphInternal.registerNodeType('TestNode', LGraphNode)
LiteGraph.registerNodeType('TestNode', LGraphNode)
})
test('should NOT throw error when calling removeInput on a copied node without graph', () => {

View File

@@ -2,15 +2,14 @@ import { clamp } from 'es-toolkit/compat'
import { beforeEach, describe, expect, vi } from 'vitest'
import { LiteGraphGlobal } from '@/lib/litegraph/src/LiteGraphGlobal'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LiteGraphInternal } from '../src/LiteGraphInternal'
import { test } from './testExtensions'
describe('Litegraph module', () => {
test('contains a global export', ({ expect }) => {
expect(LiteGraphInternal).toBeInstanceOf(LiteGraphGlobal)
expect(LiteGraphInternal.LGraphCanvas).toBe(LGraphCanvas)
expect(LiteGraph).toBeInstanceOf(LiteGraphGlobal)
expect(LiteGraph.LGraphCanvas).toBe(LGraphCanvas)
})
test('has the same structure', ({ expect }) => {
@@ -37,12 +36,8 @@ describe('Import order dependency', () => {
const directImport = await import('@/lib/litegraph/src/LGraph')
// Sanity check that imports were cleared.
expect(Object.is(LiteGraphInternal, entryPointImport.LiteGraph)).toBe(
false
)
expect(Object.is(LiteGraphInternal.LGraph, directImport.LGraph)).toBe(
false
)
expect(Object.is(LiteGraph, entryPointImport.LiteGraph)).toBe(false)
expect(Object.is(LiteGraph.LGraph, directImport.LGraph)).toBe(false)
}
await expect(importNormally()).resolves.toBeUndefined()

View File

@@ -1,9 +1,12 @@
import { assert, describe, expect, it } from 'vitest'
import type { ISlotType, LGraph } from '@/lib/litegraph/src/litegraph'
import { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
import {
LGraphGroup,
LGraphNode,
LiteGraph
} from '@/lib/litegraph/src/litegraph'
import { LiteGraphInternal } from '../../src/LiteGraphInternal'
import {
createTestSubgraph,
createTestSubgraphNode
@@ -16,7 +19,7 @@ function createNode(
title?: string
) {
const type = JSON.stringify({ inputs, outputs })
if (!LiteGraphInternal.registered_node_types[type]) {
if (!LiteGraph.registered_node_types[type]) {
class testnode extends LGraphNode {
constructor(title: string) {
super(title)
@@ -27,9 +30,9 @@ function createNode(
this.addOutput('output_' + o_count++, output)
}
}
LiteGraphInternal.registered_node_types[type] = testnode
LiteGraph.registered_node_types[type] = testnode
}
const node = LiteGraphInternal.createNode(type, title)
const node = LiteGraph.createNode(type, title)
if (!node) {
throw new Error('Failed to create node')
}

View File

@@ -1,8 +1,8 @@
import { test as baseTest } from 'vitest'
import { LGraph } from '@/lib/litegraph/src/LGraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LiteGraphInternal } from '../src/LiteGraphInternal'
import type {
ISerialisedGraph,
SerialisableGraph
@@ -74,10 +74,7 @@ export const dirtyTest = test.extend<DirtyFixtures>({
// Register node types
for (const node of basicSerialisableGraph.nodes) {
LiteGraphInternal.registerNodeType(
node.type!,
LiteGraphInternal.LGraphNode
)
LiteGraph.registerNodeType(node.type!, LiteGraph.LGraphNode)
}
await use(structuredClone(basicSerialisableGraph))

View File

@@ -1873,13 +1873,6 @@
"noModelsInFolder": "No {type} available in this folder",
"searchAssetsPlaceholder": "Search assets...",
"allModels": "All Models",
"unknown": "Unknown",
"fileFormats": "File formats",
"baseModels": "Base models",
"sortBy": "Sort by",
"sortAZ": "A-Z",
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular"
"unknown": "Unknown"
}
}

View File

@@ -2,6 +2,11 @@ import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
import * as Sentry from '@sentry/vue'
import { initializeApp } from 'firebase/app'
import {
browserLocalPersistence,
browserSessionPersistence,
indexedDBLocalPersistence
} from 'firebase/auth'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
@@ -9,7 +14,7 @@ import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import { createApp } from 'vue'
import { VueFire, VueFireAuth } from 'vuefire'
import { VueFire, VueFireAuthWithDependencies } from 'vuefire'
import { FIREBASE_CONFIG } from '@/config/firebase'
import '@/lib/litegraph/public/css/litegraph.css'
@@ -66,6 +71,18 @@ app
.use(i18n)
.use(VueFire, {
firebaseApp,
modules: [VueFireAuth()]
modules: [
// Configure Firebase Auth persistence: localStorage first, IndexedDB last.
// Localstorage is preferred to IndexedDB for mobile Safari compatibility.
VueFireAuthWithDependencies({
dependencies: {
persistence: [
browserLocalPersistence,
browserSessionPersistence,
indexedDBLocalPersistence
]
}
})
]
})
.mount('#vue-app')

View File

@@ -1,7 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import {
createMockAssets,
mockAssets
@@ -57,7 +56,7 @@ export const Default: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: AssetDisplayItem) => {
const onAssetSelect = (asset: any) => {
console.log('Selected asset:', asset)
}
const onClose = () => {
@@ -97,7 +96,7 @@ export const SingleAssetType: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: AssetDisplayItem) => {
const onAssetSelect = (asset: any) => {
console.log('Selected asset:', asset)
}
const onClose = () => {
@@ -146,7 +145,7 @@ export const NoLeftPanel: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: AssetDisplayItem) => {
const onAssetSelect = (asset: any) => {
console.log('Selected asset:', asset)
}
const onClose = () => {

View File

@@ -12,7 +12,7 @@
:nav-items="availableCategories"
>
<template #header-icon>
<div class="icon-[lucide--folder] size-4" />
<i-lucide:folder class="size-4" />
</template>
<template #header-title>{{ $t('assetBrowser.browseAssets') }}</template>
</LeftSidePanel>
@@ -37,7 +37,7 @@
</template>
<script setup lang="ts">
import { computed, provide } from 'vue'
import { computed } from 'vue'
import SearchBox from '@/components/input/SearchBox.vue'
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
@@ -46,7 +46,6 @@ import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { OnCloseKey } from '@/types/widgetTypes'
const props = defineProps<{
nodeType?: string
@@ -62,28 +61,35 @@ const emit = defineEmits<{
close: []
}>()
provide(OnCloseKey, props.onClose ?? (() => {}))
// Use AssetBrowser composable for all business logic
const {
searchQuery,
selectedCategory,
availableCategories,
contentTitle,
filteredAssets,
selectAssetWithCallback
selectAsset
} = useAssetBrowser(props.assets)
// Dialog controls panel visibility via prop
const shouldShowLeftPanel = computed(() => {
return props.showLeftPanel ?? true
})
// Handle close button - call both the prop callback and emit the event
const handleClose = () => {
props.onClose?.()
emit('close')
}
const handleAssetSelectAndEmit = async (asset: AssetDisplayItem) => {
emit('asset-select', asset)
await selectAssetWithCallback(asset.id, props.onSelect)
// Handle asset selection and emit to parent
const handleAssetSelectAndEmit = (asset: AssetDisplayItem) => {
selectAsset(asset) // This logs the selection for dev mode
emit('asset-select', asset) // Emit the full asset object
// Call prop callback if provided
if (props.onSelect) {
props.onSelect(asset.name) // Use asset name as the asset path
}
}
</script>

View File

@@ -14,7 +14,7 @@
'bg-ivory-100 border border-gray-300 dark-theme:bg-charcoal-400 dark-theme:border-charcoal-600',
'hover:transform hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/10 hover:border-gray-400',
'dark-theme:hover:shadow-lg dark-theme:hover:shadow-black/30 dark-theme:hover:border-charcoal-700',
'focus:outline-none focus:transform focus:-translate-y-0.5 focus:shadow-lg focus:shadow-black/10 dark-theme:focus:shadow-black/30'
'focus:outline-none focus:ring-2 focus:ring-blue-500 dark-theme:focus:ring-blue-400'
],
// Div-specific styles
!interactive && [

View File

@@ -3,7 +3,7 @@
<div :class="leftSideClasses" data-component-id="asset-filter-bar-left">
<MultiSelect
v-model="fileFormats"
:label="$t('assetBrowser.fileFormats')"
label="File formats"
:options="fileFormatOptions"
:class="selectClasses"
data-component-id="asset-filter-file-formats"
@@ -12,7 +12,7 @@
<MultiSelect
v-model="baseModels"
:label="$t('assetBrowser.baseModels')"
label="Base models"
:options="baseModelOptions"
:class="selectClasses"
data-component-id="asset-filter-base-models"
@@ -23,7 +23,7 @@
<div :class="rightSideClasses" data-component-id="asset-filter-bar-right">
<SingleSelect
v-model="sortBy"
:label="$t('assetBrowser.sortBy')"
label="Sort by"
:options="sortOptions"
:class="selectClasses"
data-component-id="asset-filter-sort"
@@ -43,7 +43,6 @@ import { ref } from 'vue'
import MultiSelect from '@/components/input/MultiSelect.vue'
import SingleSelect from '@/components/input/SingleSelect.vue'
import type { SelectOption } from '@/components/input/types'
import { t } from '@/i18n'
import { cn } from '@/utils/tailwindUtil'
export interface FilterState {
@@ -75,10 +74,10 @@ const baseModelOptions = [
// TODO: Make sortOptions configurable via props
// Different asset types might need different sorting options
const sortOptions = [
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' },
{ name: t('assetBrowser.sortZA'), value: 'name-desc' },
{ name: t('assetBrowser.sortRecent'), value: 'recent' },
{ name: t('assetBrowser.sortPopular'), value: 'popular' }
{ name: 'A-Z', value: 'name-asc' },
{ name: 'Z-A', value: 'name-desc' },
{ name: 'Recent', value: 'recent' },
{ name: 'Popular', value: 'popular' }
]
const emit = defineEmits<{

View File

@@ -1,9 +1,8 @@
import { computed, ref } from 'vue'
import { d, t } from '@/i18n'
import { t } from '@/i18n'
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetFilenameSchema } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import {
getAssetBaseModel,
getAssetDescription
@@ -70,7 +69,7 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
// Create display stats from API data
const stats = {
formattedDate: d(new Date(asset.created_at), { dateStyle: 'short' }),
formattedDate: new Date(asset.created_at).toLocaleDateString(),
downloadCount: undefined, // Not available in API
stars: undefined // Not available in API
}
@@ -163,41 +162,12 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
return filtered.map(transformAssetForDisplay)
})
/**
* Asset selection that fetches full details and executes callback with filename
* @param assetId - The asset ID to select and fetch details for
* @param onSelect - Optional callback to execute with the asset filename
*/
async function selectAssetWithCallback(
assetId: string,
onSelect?: (filename: string) => void
): Promise<void> {
// Actions
function selectAsset(asset: AssetDisplayItem): UUID {
if (import.meta.env.DEV) {
console.debug('Asset selected:', assetId)
}
if (!onSelect) {
return
}
try {
const detailAsset = await assetService.getAssetDetails(assetId)
const filename = detailAsset.user_metadata?.filename
const validatedFilename = assetFilenameSchema.safeParse(filename)
if (!validatedFilename.success) {
console.error(
'Invalid asset filename:',
validatedFilename.error.errors,
'for asset:',
assetId
)
return
}
onSelect(validatedFilename.data)
} catch (error) {
console.error(`Failed to fetch asset details for ${assetId}:`, error)
console.log('Asset selected:', asset.id, asset.name)
}
return asset.id
}
return {
@@ -212,6 +182,7 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
filteredAssets,
// Actions
selectAssetWithCallback
selectAsset,
transformAssetForDisplay
}
}

View File

@@ -1,7 +1,5 @@
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { type DialogComponentProps, useDialogStore } from '@/stores/dialogStore'
import { useDialogStore } from '@/stores/dialogStore'
interface AssetBrowserDialogProps {
/** ComfyUI node type for context (e.g., 'CheckpointLoaderSimple') */
@@ -10,29 +8,36 @@ interface AssetBrowserDialogProps {
inputName: string
/** Current selected asset value */
currentValue?: string
/**
* Callback for when an asset is selected
* @param {string} filename - The validated filename from user_metadata.filename
*/
onAssetSelected?: (filename: string) => void
/** Callback for when an asset is selected */
onAssetSelected?: (assetPath: string) => void
}
export const useAssetBrowserDialog = () => {
const dialogStore = useDialogStore()
const dialogKey = 'global-asset-browser'
async function show(props: AssetBrowserDialogProps) {
const handleAssetSelected = (filename: string) => {
props.onAssetSelected?.(filename)
dialogStore.closeDialog({ key: dialogKey })
function hide() {
dialogStore.closeDialog({ key: dialogKey })
}
function show(props: AssetBrowserDialogProps) {
const handleAssetSelected = (assetPath: string) => {
props.onAssetSelected?.(assetPath)
hide() // Auto-close on selection
}
const dialogComponentProps: DialogComponentProps = {
const handleClose = () => {
hide()
}
// Default dialog configuration for AssetBrowserModal
const dialogComponentProps = {
headless: true,
modal: true,
closable: true,
closable: false,
pt: {
root: {
class: 'rounded-2xl overflow-hidden asset-browser-dialog'
class: 'rounded-2xl overflow-hidden'
},
header: {
class: 'p-0 hidden'
@@ -43,17 +48,6 @@ export const useAssetBrowserDialog = () => {
}
}
const assets: AssetItem[] = await assetService
.getAssetsForNodeType(props.nodeType)
.catch((error) => {
console.error(
'Failed to fetch assets for node type:',
props.nodeType,
error
)
return []
})
dialogStore.showDialog({
key: dialogKey,
component: AssetBrowserModal,
@@ -61,13 +55,12 @@ export const useAssetBrowserDialog = () => {
nodeType: props.nodeType,
inputName: props.inputName,
currentValue: props.currentValue,
assets,
onSelect: handleAssetSelected,
onClose: () => dialogStore.closeDialog({ key: dialogKey })
onClose: handleClose
},
dialogComponentProps
})
}
return { show }
return { show, hide }
}

View File

@@ -4,13 +4,13 @@ import { z } from 'zod'
const zAsset = z.object({
id: z.string(),
name: z.string(),
asset_hash: z.string().nullable(),
asset_hash: z.string(),
size: z.number(),
mime_type: z.string().nullable(),
mime_type: z.string(),
tags: z.array(z.string()),
preview_url: z.string().optional(),
created_at: z.string(),
updated_at: z.string().optional(),
updated_at: z.string(),
last_access_time: z.string(),
user_metadata: z.record(z.unknown()).optional(), // API allows arbitrary key-value pairs
preview_id: z.string().nullable().optional()
@@ -33,14 +33,6 @@ const zModelFile = z.object({
pathIndex: z.number()
})
// Filename validation schema
export const assetFilenameSchema = z
.string()
.min(1, 'Filename cannot be empty')
.regex(/^[^\\:*?"<>|]+$/, 'Invalid filename characters') // Allow forward slashes, block backslashes and other unsafe chars
.regex(/^(?!\/|.*\.\.)/, 'Path must not start with / or contain ..') // Prevent absolute paths and directory traversal
.trim()
// Export schemas following repository patterns
export const assetResponseSchema = zAssetResponse

View File

@@ -1,7 +1,6 @@
import { fromZodError } from 'zod-validation-error'
import {
type AssetItem,
type AssetResponse,
type ModelFile,
type ModelFolder,
@@ -128,75 +127,10 @@ function createAssetService() {
)
}
/**
* Gets assets for a specific node type by finding the matching category
* and fetching all assets with that category tag
*
* @param nodeType - The ComfyUI node type (e.g., 'CheckpointLoaderSimple')
* @returns Promise<AssetItem[]> - Full asset objects with preserved metadata
*/
async function getAssetsForNodeType(nodeType: string): Promise<AssetItem[]> {
if (!nodeType || typeof nodeType !== 'string') {
return []
}
// Find the category for this node type using efficient O(1) lookup
const modelToNodeStore = useModelToNodeStore()
const category = modelToNodeStore.getCategoryForNodeType(nodeType)
if (!category) {
return []
}
// Fetch assets for this category using same API pattern as getAssetModels
const data = await handleAssetRequest(
`${ASSETS_ENDPOINT}?include_tags=${MODELS_TAG},${category}`,
`assets for ${nodeType}`
)
// Return full AssetItem[] objects (don't strip like getAssetModels does)
return (
data?.assets?.filter(
(asset) =>
!asset.tags.includes(MISSING_TAG) && asset.tags.includes(category)
) ?? []
)
}
/**
* Gets complete details for a specific asset by ID
* Calls the detail endpoint which includes user_metadata and all fields
*
* @param id - The asset ID
* @returns Promise<AssetItem> - Complete asset object with user_metadata
*/
async function getAssetDetails(id: string): Promise<AssetItem> {
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`)
if (!res.ok) {
throw new Error(
`Unable to load asset details for ${id}: Server returned ${res.status}. Please try again.`
)
}
const data = await res.json()
// Validate the single asset response against our schema
const result = assetResponseSchema.safeParse({ assets: [data] })
if (result.success && result.data.assets?.[0]) {
return result.data.assets[0]
}
const error = result.error
? fromZodError(result.error)
: 'Unknown validation error'
throw new Error(`Invalid asset response against zod schema:\n${error}`)
}
return {
getAssetModelFolders,
getAssetModels,
isAssetBrowserEligible,
getAssetsForNodeType,
getAssetDetails
isAssetBrowserEligible
}
}

View File

@@ -595,7 +595,7 @@ export const CORE_SETTINGS: SettingParams[] = [
migrateDeprecatedValue: (value: any[]) => {
return value.map((keybinding) => {
if (keybinding['targetSelector'] === '#graph-canvas') {
keybinding['targetElementId'] = 'graph-canvas-container'
keybinding['targetElementId'] = 'graph-canvas'
}
return keybinding
})

View File

@@ -99,16 +99,6 @@ export const useCanvasStore = defineStore('canvas', () => {
const currentGraph = shallowRef<LGraph | null>(null)
const isInSubgraph = ref(false)
// Provide selection state to all Vue nodes
const selectedNodeIds = computed(
() =>
new Set(
selectedItems.value
.filter((item) => item.id !== undefined)
.map((item) => String(item.id))
)
)
whenever(
() => canvas.value,
(newCanvas) => {
@@ -132,7 +122,6 @@ export const useCanvasStore = defineStore('canvas', () => {
return {
canvas,
selectedItems,
selectedNodeIds,
nodeSelected,
groupSelected,
rerouteSelected,

View File

@@ -0,0 +1,25 @@
import type { InjectionKey, Ref } from 'vue'
import type { NodeProgressState } from '@/schemas/apiSchema'
/**
* Injection key for providing selected node IDs to Vue node components.
* Contains a reactive Set of selected node IDs (as strings).
*/
export const SelectedNodeIdsKey: InjectionKey<Ref<Set<string>>> =
Symbol('selectedNodeIds')
/**
* Injection key for providing executing node IDs to Vue node components.
* Contains a reactive Set of currently executing node IDs (as strings).
*/
export const ExecutingNodeIdsKey: InjectionKey<Ref<Set<string>>> =
Symbol('executingNodeIds')
/**
* Injection key for providing node progress states to Vue node components.
* Contains a reactive Record of node IDs to their current progress state.
*/
export const NodeProgressStatesKey: InjectionKey<
Ref<Record<string, NodeProgressState>>
> = Symbol('nodeProgressStates')

View File

@@ -1,6 +1,6 @@
import type { InjectionKey } from 'vue'
import type { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
import type { Point } from '@/renderer/core/layout/types'
/**
* Lightweight, injectable transform state used by layout-aware components.
@@ -21,11 +21,29 @@ import type { useTransformState } from '@/renderer/core/layout/transform/useTran
* const state = inject(TransformStateKey)!
* const screen = state.canvasToScreen({ x: 100, y: 50 })
*/
interface TransformState
extends Pick<
ReturnType<typeof useTransformState>,
'screenToCanvas' | 'canvasToScreen' | 'camera' | 'isNodeInViewport'
> {}
interface TransformState {
/** Convert a screen-space point (CSS pixels) to canvas space. */
screenToCanvas: (p: Point) => Point
/** Convert a canvas-space point to screen space (CSS pixels). */
canvasToScreen: (p: Point) => Point
/** Current pan/zoom; `x`/`y` are offsets, `z` is scale. */
camera?: { x: number; y: number; z: number }
/**
* Test whether a node's rectangle intersects the (expanded) viewport.
* Handy for viewport culling and lazy work.
*
* @param nodePos Top-left in canvas space `[x, y]`
* @param nodeSize Size in canvas units `[width, height]`
* @param viewport Screen-space viewport `{ width, height }`
* @param margin Optional fractional margin (e.g. `0.2` = 20%)
*/
isNodeInViewport?: (
nodePos: ArrayLike<number>,
nodeSize: ArrayLike<number>,
viewport: { width: number; height: number },
margin?: number
) => boolean
}
export const TransformStateKey: InjectionKey<TransformState> =
Symbol('transformState')

View File

@@ -1379,7 +1379,6 @@ class LayoutStoreImpl implements LayoutStore {
this.spatialIndex.update(nodeId, bounds)
ynode.set('bounds', bounds)
ynode.set('position', { x: bounds.x, y: bounds.y })
ynode.set('size', { width: bounds.width, height: bounds.height })
}
}, this.currentActor)

View File

@@ -134,11 +134,7 @@ export function useSlotLayoutSync() {
restoreHandlers = () => {
graph.onNodeAdded = origNodeAdded || undefined
graph.onNodeRemoved = origNodeRemoved || undefined
// Only restore onTrigger if Vue nodes are not active
// Vue node manager sets its own onTrigger handler
if (!LiteGraph.vueNodesMode) {
graph.onTrigger = origTrigger || undefined
}
graph.onTrigger = origTrigger || undefined
graph.onAfterChange = origAfterChange || undefined
}

View File

@@ -1,12 +1,7 @@
<template>
<div
class="absolute inset-0 w-full h-full pointer-events-none"
:class="
cn(
isInteracting ? 'transform-pane--interacting' : 'will-change-auto',
isLOD ? 'isLOD' : ''
)
"
class="transform-pane"
:class="{ 'transform-pane--interacting': isInteracting }"
:style="transformStyle"
@pointerdown="handlePointerDown"
>
@@ -23,8 +18,6 @@ import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
import { useCanvasTransformSync } from '@/renderer/core/layout/transform/useCanvasTransformSync'
import { useTransformSettling } from '@/renderer/core/layout/transform/useTransformSettling'
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
import { useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
import { cn } from '@/utils/tailwindUtil'
interface TransformPaneProps {
canvas?: LGraphCanvas
@@ -41,8 +34,6 @@ const {
isNodeInViewport
} = useTransformState()
const { isLOD } = useLOD(camera)
const canvasElement = computed(() => props.canvas?.canvas)
const { isTransforming: isInteracting } = useTransformSettling(canvasElement, {
settleDelay: 200,

View File

@@ -35,7 +35,7 @@
v-else
:src="currentImageUrl"
:alt="imageAltText"
class="w-full h-[352px] object-contain block"
class="w-full h-[352px] object-cover block"
@load="handleImageLoad"
@error="handleImageError"
/>
@@ -94,20 +94,17 @@
</div>
</div>
<div class="relative">
<!-- Image Dimensions -->
<div class="text-white text-xs text-center mt-2">
<span v-if="imageError" class="text-red-400">
{{ $t('g.errorLoadingImage') }}
</span>
<span v-else-if="isLoading" class="text-gray-400">
{{ $t('g.loading') }}...
</span>
<span v-else>
{{ actualDimensions || $t('g.calculatingDimensions') }}
</span>
</div>
<LODFallback />
<!-- Image Dimensions -->
<div class="text-white text-xs text-center mt-2">
<span v-if="imageError" class="text-red-400">
{{ $t('g.errorLoadingImage') }}
</span>
<span v-else-if="isLoading" class="text-gray-400">
{{ $t('g.loading') }}...
</span>
<span v-else>
{{ actualDimensions || $t('g.calculatingDimensions') }}
</span>
</div>
</div>
</template>
@@ -122,8 +119,6 @@ import { downloadFile } from '@/base/common/downloadUtil'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import LODFallback from './LODFallback.vue'
interface ImagePreviewProps {
/** Array of image URLs to display */
readonly imageUrls: readonly string[]

View File

@@ -10,15 +10,12 @@
/>
<!-- Slot Name -->
<div class="relative">
<span
v-if="!dotOnly"
class="whitespace-nowrap text-sm font-normal dark-theme:text-slate-200 text-stone-200 lod-toggle"
>
{{ slotData.localized_name || slotData.name || `Input ${index}` }}
</span>
<LODFallback />
</div>
<span
v-if="!dotOnly"
class="whitespace-nowrap text-sm font-normal dark-theme:text-slate-200 text-stone-200"
>
{{ slotData.localized_name || slotData.name || `Input ${index}` }}
</span>
</div>
</template>
@@ -41,7 +38,6 @@ import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composabl
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { cn } from '@/utils/tailwindUtil'
import LODFallback from './LODFallback.vue'
import SlotConnectionDot from './SlotConnectionDot.vue'
interface InputSlotProps {

View File

@@ -23,7 +23,7 @@
bypassed,
'will-change-transform': isDragging
},
lodCssClass,
shouldHandleNodePointerEvents
? 'pointer-events-auto'
: 'pointer-events-none'
@@ -31,7 +31,7 @@
"
:style="[
{
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
transform: `translate(${layoutPosition.x ?? position?.x ?? 0}px, ${(layoutPosition.y ?? position?.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
zIndex: zIndex
},
dragStyle
@@ -48,18 +48,21 @@
</template>
<!-- Header only updates on title/color changes -->
<NodeHeader
v-memo="[nodeData.title, isCollapsed]"
v-memo="[nodeData.title, lodLevel, isCollapsed]"
:node-data="nodeData"
:readonly="readonly"
:lod-level="lodLevel"
:collapsed="isCollapsed"
@collapse="handleCollapse"
@update:title="handleHeaderTitleUpdate"
@update:title="handleTitleUpdate"
@enter-subgraph="handleEnterSubgraph"
/>
</div>
<div
v-if="isCollapsed && executing && progress !== undefined"
v-if="
(isMinimalLOD || isCollapsed) && executing && progress !== undefined
"
:class="
cn(
'absolute inset-x-4 -bottom-[1px] translate-y-1/2 rounded-full',
@@ -69,7 +72,7 @@
:style="{ width: `${Math.min(progress * 100, 100)}%` }"
/>
<template v-if="!isCollapsed">
<template v-if="!isMinimalLOD && !isCollapsed">
<div class="mb-4 relative">
<div :class="separatorClasses" />
<!-- Progress bar for executing state -->
@@ -93,24 +96,29 @@
>
<!-- Slots only rendered at full detail -->
<NodeSlots
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length]"
v-if="shouldRenderSlots"
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length, lodLevel]"
:node-data="nodeData"
:readonly="readonly"
:lod-level="lodLevel"
@slot-click="handleSlotClick"
/>
<!-- Widgets rendered at reduced+ detail -->
<NodeWidgets
v-if="nodeData.widgets?.length"
v-memo="[nodeData.widgets?.length]"
v-if="shouldShowWidgets"
v-memo="[nodeData.widgets?.length, lodLevel]"
:node-data="nodeData"
:readonly="readonly"
:lod-level="lodLevel"
/>
<!-- Custom content at reduced+ detail -->
<NodeContent
v-if="hasCustomContent"
v-if="shouldShowContent"
:node-data="nodeData"
:readonly="readonly"
:lod-level="lodLevel"
:image-urls="nodeImageUrls"
/>
<!-- Live preview image -->
@@ -131,21 +139,29 @@
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, inject, onErrorCaptured, onMounted, provide, ref } from 'vue'
import {
computed,
inject,
onErrorCaptured,
onMounted,
provide,
ref,
toRef,
watch
} from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
import { app } from '@/scripts/app'
import { useExecutionStore } from '@/stores/executionStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
@@ -155,6 +171,7 @@ import {
} from '@/utils/graphTraversalUtil'
import { cn } from '@/utils/tailwindUtil'
import { useVueElementTracking } from '../composables/useVueNodeResizeTracking'
import NodeContent from './NodeContent.vue'
import NodeHeader from './NodeHeader.vue'
import NodeSlots from './NodeSlots.vue'
@@ -164,6 +181,8 @@ import SlotConnectionDot from './SlotConnectionDot.vue'
// Extended props for main node component
interface LGraphNodeProps {
nodeData: VueNodeData
position?: { x: number; y: number }
size?: { width: number; height: number }
readonly?: boolean
error?: string | null
zoomLevel?: number
@@ -171,16 +190,39 @@ interface LGraphNodeProps {
const {
nodeData,
position,
size,
error = null,
readonly = false
readonly = false,
zoomLevel = 1
} = defineProps<LGraphNodeProps>()
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeSelect } =
useNodeEventHandlers()
const emit = defineEmits<{
'node-click': [
event: PointerEvent,
nodeData: VueNodeData,
wasDragging: boolean
]
'slot-click': [
event: PointerEvent,
nodeData: VueNodeData,
slotIndex: number,
isInput: boolean
]
dragStart: [event: DragEvent, nodeData: VueNodeData]
'update:collapsed': [nodeId: string, collapsed: boolean]
'update:title': [nodeId: string, newTitle: string]
}>()
useVueElementTracking(() => nodeData.id, 'node')
useVueElementTracking(nodeData.id, 'node')
const { selectedNodeIds } = storeToRefs(useCanvasStore())
// Inject selection state from parent
const selectedNodeIds = inject(SelectedNodeIdsKey)
if (!selectedNodeIds) {
throw new Error(
'SelectedNodeIds not provided - LGraphNode must be used within a component that provides selection state'
)
}
// Inject transform state for coordinate conversion
const transformState = inject(TransformStateKey)
@@ -191,7 +233,7 @@ const isSelected = computed(() => {
})
// Use execution state composable
const { executing, progress } = useNodeExecutionState(() => nodeData.id)
const { executing, progress } = useNodeExecutionState(nodeData.id)
// Direct access to execution store for error state
const executionStore = useExecutionStore()
@@ -207,7 +249,25 @@ const hasAnyError = computed(
const bypassed = computed((): boolean => nodeData.mode === 4)
// Use canvas interactions for proper wheel event handling and pointer event capture control
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()
const {
handleWheel,
handlePointer,
forwardEventToCanvas,
shouldHandleNodePointerEvents
} = useCanvasInteractions()
// LOD (Level of Detail) system based on zoom level
const zoomRef = toRef(() => zoomLevel)
const {
lodLevel,
shouldRenderWidgets,
shouldRenderSlots,
shouldRenderContent,
lodCssClass
} = useLOD(zoomRef)
// Computed properties for template usage
const isMinimalLOD = computed(() => lodLevel.value === LODLevel.MINIMAL)
// Error boundary implementation
const renderError = ref<string | null>(null)
@@ -220,28 +280,48 @@ onErrorCaptured((error) => {
})
// Use layout system for node position and dragging
const { position, size, zIndex, resize } = useNodeLayout(() => nodeData.id)
const {
handlePointerDown,
handlePointerUp,
handlePointerMove,
isDragging,
dragStyle
} = useNodePointerInteractions(() => nodeData, handleNodeSelect)
position: layoutPosition,
zIndex,
startDrag,
handleDrag: handleLayoutDrag,
endDrag,
resize
} = useNodeLayout(nodeData.id)
onMounted(() => {
if (size.value && transformState?.camera) {
if (size && transformState?.camera) {
const scale = transformState.camera.z
const screenSize = {
width: size.value.width * scale,
height: size.value.height * scale
width: size.width * scale,
height: size.height * scale
}
resize(screenSize)
}
})
// Drag state for styling
const isDragging = ref(false)
const dragStyle = computed(() => ({
cursor: isDragging.value ? 'grabbing' : 'grab'
}))
const lastY = ref(0)
const lastX = ref(0)
// Treat tiny pointer jitter as a click, not a drag
const DRAG_THRESHOLD_PX = 4
// Track collapsed state
const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)
const isCollapsed = ref(nodeData.flags?.collapsed ?? false)
// Watch for external changes to the collapsed state
watch(
() => nodeData.flags?.collapsed,
(newCollapsed: boolean | undefined) => {
if (newCollapsed !== undefined && newCollapsed !== isCollapsed.value) {
isCollapsed.value = newCollapsed
}
}
)
// Check if node has custom content (like image outputs)
const hasCustomContent = computed(() => {
@@ -251,16 +331,26 @@ const hasCustomContent = computed(() => {
// Computed classes and conditions for better reusability
const separatorClasses =
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full lod-toggle'
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full'
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
() => nodeData.id,
nodeData.id,
{
isMinimalLOD,
isCollapsed
}
)
// Common condition computations to avoid repetition
const shouldShowWidgets = computed(
() => shouldRenderWidgets.value && nodeData.widgets?.length
)
const shouldShowContent = computed(
() => shouldRenderContent.value && hasCustomContent.value
)
const borderClass = computed(() => {
if (hasAnyError.value) {
return 'border-error'
@@ -285,12 +375,86 @@ const outlineClass = computed(() => {
})
// Event handlers
const handleCollapse = () => {
handleNodeCollapse(nodeData.id, !isCollapsed.value)
const handlePointerDown = (event: PointerEvent) => {
if (!nodeData) {
console.warn('LGraphNode: nodeData is null/undefined in handlePointerDown')
return
}
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event)
return
}
// Start drag using layout system
isDragging.value = true
// Set Vue node dragging state for selection toolbox
layoutStore.isDraggingVueNodes.value = true
startDrag(event)
lastY.value = event.clientY
lastX.value = event.clientX
}
const handleHeaderTitleUpdate = (newTitle: string) => {
handleNodeTitleUpdate(nodeData.id, newTitle)
const handlePointerMove = (event: PointerEvent) => {
// Check if this should be forwarded to canvas (e.g., space panning, middle mouse)
handlePointer(event)
if (isDragging.value) {
void handleLayoutDrag(event)
}
}
const handlePointerUp = (event: PointerEvent) => {
if (isDragging.value) {
isDragging.value = false
void endDrag(event)
// Clear Vue node dragging state for selection toolbox
layoutStore.isDraggingVueNodes.value = false
}
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event)
return
}
// Emit node-click for selection handling in GraphCanvas
const dx = event.clientX - lastX.value
const dy = event.clientY - lastY.value
const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX
emit('node-click', event, nodeData, wasDragging)
}
const handleCollapse = () => {
isCollapsed.value = !isCollapsed.value
// Emit event so parent can sync with LiteGraph if needed
emit('update:collapsed', nodeData.id, isCollapsed.value)
}
const handleSlotClick = (
event: PointerEvent,
slotIndex: number,
isInput: boolean
) => {
if (!nodeData) {
console.warn('LGraphNode: nodeData is null/undefined in handleSlotClick')
return
}
// Don't handle slot clicks when canvas is in panning mode
if (!shouldHandleNodePointerEvents.value) {
return
}
emit('slot-click', event, nodeData, slotIndex, isInput)
}
const handleTitleUpdate = (newTitle: string) => {
emit('update:title', nodeData.id, newTitle)
}
const handleEnterSubgraph = () => {
@@ -320,17 +484,15 @@ const handleEnterSubgraph = () => {
const nodeOutputs = useNodeOutputStore()
const nodeOutputLocatorId = computed(() =>
nodeData.subgraphId ? `${nodeData.subgraphId}:${nodeData.id}` : nodeData.id
)
const nodeImageUrls = computed(() => {
const newOutputs = nodeOutputs.nodeOutputs[nodeOutputLocatorId.value]
const nodeImageUrls = ref<string[]>([])
const onNodeOutputsUpdate = (newOutputs: ExecutedWsMessage['output']) => {
const locatorId = getLocatorIdFromNodeData(nodeData)
// Use root graph for getNodeByLocatorId since it needs to traverse from root
const rootGraph = app.graph?.rootGraph || app.graph
if (!rootGraph) {
return []
nodeImageUrls.value = []
return
}
const node = getNodeByLocatorId(rootGraph, locatorId)
@@ -338,13 +500,30 @@ const nodeImageUrls = computed(() => {
if (node && newOutputs?.images?.length) {
const urls = nodeOutputs.getNodeImageUrls(node)
if (urls) {
return urls
nodeImageUrls.value = urls
}
} else {
// Clear URLs if no outputs or no images
nodeImageUrls.value = []
}
// Clear URLs if no outputs or no images
return []
})
}
const nodeContainerRef = ref()
const nodeOutputLocatorId = computed(() =>
nodeData.subgraphId ? `${nodeData.subgraphId}:${nodeData.id}` : nodeData.id
)
watch(
() => nodeOutputs.nodeOutputs[nodeOutputLocatorId.value],
(newOutputs) => {
onNodeOutputsUpdate(newOutputs)
},
{ deep: true }
)
// Template ref for tooltip positioning
const nodeContainerRef = ref<HTMLElement>()
// Provide nodeImageUrls and tooltip container to child components
provide('nodeImageUrls', nodeImageUrls)
provide('tooltipContainer', nodeContainerRef)
</script>

View File

@@ -1,3 +0,0 @@
<template>
<div class="lod-fallback absolute inset-0 w-full h-full bg-zinc-800"></div>
</template>

View File

@@ -21,6 +21,7 @@ import { computed, onErrorCaptured, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
import ImagePreview from './ImagePreview.vue'
@@ -28,6 +29,7 @@ interface NodeContentProps {
node?: LGraphNode // For backwards compatibility
nodeData?: VueNodeData // New clean data structure
readonly?: boolean
lodLevel?: LODLevel
imageUrls?: string[]
}

View File

@@ -4,44 +4,41 @@
</div>
<div
v-else
class="lg-node-header p-4 rounded-t-2xl cursor-move"
class="lg-node-header flex items-center justify-between p-4 rounded-t-2xl cursor-move w-full"
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
<div class="flex items-center justify-between relative">
<!-- Collapse/Expand Button -->
<button
v-show="!readonly"
class="bg-transparent border-transparent flex items-center lod-toggle"
data-testid="node-collapse-button"
@click.stop="handleCollapse"
@dblclick.stop
>
<i
:class="collapsed ? 'pi pi-chevron-right' : 'pi pi-chevron-down'"
class="text-xs leading-none relative top-px text-stone-200 dark-theme:text-slate-300"
></i>
</button>
<!-- Collapse/Expand Button -->
<button
v-show="!readonly"
class="bg-transparent border-transparent flex items-center"
data-testid="node-collapse-button"
@click.stop="handleCollapse"
@dblclick.stop
>
<i
:class="collapsed ? 'pi pi-chevron-right' : 'pi pi-chevron-down'"
class="text-xs leading-none relative top-px text-stone-200 dark-theme:text-slate-300"
></i>
</button>
<!-- Node Title -->
<div
v-tooltip.top="tooltipConfig"
class="text-sm font-bold truncate flex-1 lod-toggle"
data-testid="node-title"
>
<EditableText
:model-value="displayTitle"
:is-editing="isEditing"
:input-attrs="{ 'data-testid': 'node-title-input' }"
@edit="handleTitleEdit"
@cancel="handleTitleCancel"
/>
</div>
<LODFallback />
<!-- Node Title -->
<div
v-tooltip.top="tooltipConfig"
class="text-sm font-bold truncate flex-1"
data-testid="node-title"
>
<EditableText
:model-value="displayTitle"
:is-editing="isEditing"
:input-attrs="{ 'data-testid': 'node-title-input' }"
@edit="handleTitleEdit"
@cancel="handleTitleCancel"
/>
</div>
<!-- Title Buttons -->
<div v-if="!readonly" class="flex items-center lod-toggle">
<div v-if="!readonly" class="flex items-center">
<IconButton
v-if="isSubgraphNode"
size="sm"
@@ -66,17 +63,17 @@ import EditableText from '@/components/common/EditableText.vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
import { app } from '@/scripts/app'
import {
getLocatorIdFromNodeData,
getNodeByLocatorId
} from '@/utils/graphTraversalUtil'
import LODFallback from './LODFallback.vue'
interface NodeHeaderProps {
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
collapsed?: boolean
}

View File

@@ -35,6 +35,7 @@ import { computed, onErrorCaptured, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
import { isSlotObject } from '@/utils/typeGuardUtil'
import InputSlot from './InputSlot.vue'
@@ -43,6 +44,7 @@ import OutputSlot from './OutputSlot.vue'
interface NodeSlotsProps {
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
}
const { nodeData = null, readonly } = defineProps<NodeSlotsProps>()

View File

@@ -12,17 +12,16 @@
: 'pointer-events-none'
)
"
@pointerdown.stop="handleWidgetPointerEvent"
@pointermove.stop="handleWidgetPointerEvent"
@pointerup.stop="handleWidgetPointerEvent"
@pointerdown="handleWidgetPointerEvent"
@pointermove="handleWidgetPointerEvent"
@pointerup="handleWidgetPointerEvent"
>
<div
v-for="(widget, index) in processedWidgets"
:key="`widget-${index}-${widget.name}`"
class="lg-widget-container flex items-center group"
class="lg-widget-container relative flex items-center group"
>
<!-- Widget Input Slot Dot -->
<div
class="opacity-0 group-hover:opacity-100 transition-opacity duration-150"
>
@@ -62,10 +61,12 @@ import type {
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
// Import widget components directly
import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue'
import {
getComponent,
isEssential,
shouldRenderAsVue
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
@@ -76,9 +77,10 @@ import InputSlot from './InputSlot.vue'
interface NodeWidgetsProps {
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
}
const { nodeData, readonly } = defineProps<NodeWidgetsProps>()
const { nodeData, readonly, lodLevel } = defineProps<NodeWidgetsProps>()
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
useCanvasInteractions()
@@ -123,12 +125,18 @@ const processedWidgets = computed((): ProcessedWidget[] => {
const widgets = nodeData.widgets as SafeWidgetData[]
const result: ProcessedWidget[] = []
if (lodLevel === LODLevel.MINIMAL) {
return []
}
for (const widget of widgets) {
if (widget.options?.hidden) continue
if (widget.options?.canvasOnly) continue
if (!widget.type) continue
if (!shouldRenderAsVue(widget)) continue
if (lodLevel === LODLevel.REDUCED && !isEssential(widget.type)) continue
const vueComponent = getComponent(widget.type) || WidgetInputText
const simplified: SimplifiedWidget = {

Some files were not shown because too many files have changed in this diff Show More