diff --git a/FEATURE_FLAGS_EXPLANATION.md b/FEATURE_FLAGS_EXPLANATION.md new file mode 100644 index 0000000000..9e6f3599a4 --- /dev/null +++ b/FEATURE_FLAGS_EXPLANATION.md @@ -0,0 +1,229 @@ +# Feature Flags System Explanation + +## Overview + +The `useFeatureFlag` hook (actually named `useFeatureFlags`) is a Vue 3 composable that provides **reactive access to server-side feature flags** received via WebSocket from the backend. It enables capability negotiation between frontend and backend, allowing the UI to adapt based on what features the server supports. + +## Architecture Flow + +``` +1. Frontend connects via WebSocket +2. Frontend sends client feature flags (first message) +3. Backend responds with server feature flags +4. Frontend stores flags in api.serverFeatureFlags +5. Components use useFeatureFlags() to access flags reactively +``` + +## Core Implementation + +### 1. The `useFeatureFlags` Composable + +**Location:** `src/composables/useFeatureFlags.ts` + +The composable returns two things: + +#### A. Predefined `flags` Object +A reactive object with getter properties for commonly-used feature flags: + +```typescript +const { flags } = useFeatureFlags() + +// Access predefined flags +flags.supportsPreviewMetadata // boolean | undefined +flags.maxUploadSize // number | undefined +flags.supportsManagerV4 // boolean | undefined +flags.modelUploadButtonEnabled // boolean (checks remoteConfig first) +flags.assetUpdateOptionsEnabled // boolean (checks remoteConfig first) +``` + +**Key Points:** +- Uses Vue's `reactive()` to make the object reactive +- Each getter calls `api.getServerFeature()` which reads from `api.serverFeatureFlags` +- Some flags (like `modelUploadButtonEnabled`) check `remoteConfig` first (from `/api/features` endpoint) before falling back to WebSocket flags +- Returns a `readonly()` wrapper to prevent external mutation + +#### B. Generic `featureFlag` Function +A function that creates a computed ref for any feature flag path: + +```typescript +const { featureFlag } = useFeatureFlags() + +// Create a reactive computed ref for any flag +const myFlag = featureFlag('custom.feature.path', false) // defaultValue is optional +// myFlag is a ComputedRef that updates when serverFeatureFlags changes +``` + +**Key Points:** +- Accepts any string path (supports dot notation for nested values) +- Returns a `computed()` ref that automatically updates when flags change +- Generic type parameter allows type safety: `featureFlag('flag', false)` + +### 2. The Underlying API Layer + +**Location:** `src/scripts/api.ts` + +The `ComfyApi` class manages feature flags: + +```typescript +class ComfyApi { + // Stores flags received from backend + serverFeatureFlags: Record = {} + + // Retrieves a flag value using dot notation + getServerFeature(featureName: string, defaultValue?: T): T { + return get(this.serverFeatureFlags, featureName, defaultValue) as T + } +} +``` + +**How Flags Are Received:** +1. WebSocket connection is established +2. Frontend sends client feature flags as first message +3. Backend responds with a `feature_flags` message type +4. The message handler stores it: `this.serverFeatureFlags = msg.data` + +**The `get` Function:** +- Uses `es-toolkit/compat`'s `get` function (lodash-style) +- Supports dot notation: `'extension.manager.supports_v4'` accesses nested objects +- Returns `defaultValue` if the path doesn't exist + +### 3. Remote Config Integration + +**Location:** `src/platform/remoteConfig/remoteConfig.ts` + +Some flags check `remoteConfig` first (loaded from `/api/features` endpoint): + +```typescript +// Example from modelUploadButtonEnabled +return ( + remoteConfig.value.model_upload_button_enabled ?? // Check remote config first + api.getServerFeature(ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED, false) // Fallback +) +``` + +**Why Two Sources?** +- `remoteConfig`: Fetched via HTTP at app startup, can be updated without WebSocket +- WebSocket flags: Real-time capability negotiation, updated on reconnection + +## Usage Patterns + +### Pattern 1: Using Predefined Flags + +```typescript +import { useFeatureFlags } from '@/composables/useFeatureFlags' + +const { flags } = useFeatureFlags() + +// In template +if (flags.supportsPreviewMetadata) { + // Use enhanced preview feature +} + +// In script +const maxSize = flags.maxUploadSize ?? 100 * 1024 * 1024 // Default 100MB +``` + +### Pattern 2: Using Generic featureFlag Function + +```typescript +import { useFeatureFlags } from '@/composables/useFeatureFlags' + +const { featureFlag } = useFeatureFlags() + +// Create a reactive computed ref +const customFeature = featureFlag('extension.custom.feature', false) + +// Use in template (automatically reactive) +//
New Feature UI
+ +// Use in script +watch(customFeature, (enabled) => { + if (enabled) { + // Feature was enabled + } +}) +``` + +### Pattern 3: Direct API Access (Non-Reactive) + +```typescript +import { api } from '@/scripts/api' + +// Direct access (not reactive, use sparingly) +if (api.serverSupportsFeature('supports_preview_metadata')) { + // Feature is supported +} + +const maxSize = api.getServerFeature('max_upload_size', 100 * 1024 * 1024) +``` + +## Reactivity Explained + +The composable is **reactive** because: + +1. **Predefined flags**: Use `reactive()` with getters, so when `api.serverFeatureFlags` changes, Vue's reactivity system detects it +2. **Generic featureFlag**: Returns `computed()`, which automatically tracks `api.getServerFeature()` calls and re-evaluates when flags change +3. **WebSocket updates**: When flags are updated via WebSocket, `api.serverFeatureFlags` is reassigned, triggering reactivity + +## Adding New Feature Flags + +### Step 1: Add to Enum (if it's a core flag) + +```typescript +// In useFeatureFlags.ts +export enum ServerFeatureFlag { + // ... existing flags + MY_NEW_FEATURE = 'my_new_feature' +} +``` + +### Step 2: Add to flags Object (if commonly used) + +```typescript +// In useFeatureFlags.ts flags object +get myNewFeature() { + return api.getServerFeature(ServerFeatureFlag.MY_NEW_FEATURE, false) +} +``` + +### Step 3: Use in Components + +```typescript +const { flags } = useFeatureFlags() +if (flags.myNewFeature) { + // Use the feature +} +``` + +**OR** use the generic function without modifying the composable: + +```typescript +const { featureFlag } = useFeatureFlags() +const myFeature = featureFlag('my_new_feature', false) +``` + +## Important Notes + +1. **Flags are server-driven**: The backend controls which flags are available +2. **Default values**: Always provide sensible defaults when using `getServerFeature()` +3. **Reactivity**: The composable ensures UI updates automatically when flags change (e.g., on WebSocket reconnection) +4. **Type safety**: Use TypeScript generics with `featureFlag()` for type safety +5. **Dot notation**: Feature flags can be nested, use dot notation: `'extension.manager.supports_v4'` +6. **Remote config priority**: Some flags check `remoteConfig` first, then fall back to WebSocket flags + +## Testing + +See `tests-ui/tests/composables/useFeatureFlags.test.ts` for examples of: +- Mocking `api.getServerFeature()` +- Testing reactive behavior +- Testing default values +- Testing nested paths + +## Related Files + +- `src/composables/useFeatureFlags.ts` - The main composable +- `src/scripts/api.ts` - API layer with `getServerFeature()` method +- `src/platform/remoteConfig/remoteConfig.ts` - Remote config integration +- `docs/FEATURE_FLAGS.md` - Full system documentation +- `tests-ui/tests/composables/useFeatureFlags.test.ts` - Unit tests + diff --git a/FEATURE_FLAG_PAYLOAD.md b/FEATURE_FLAG_PAYLOAD.md new file mode 100644 index 0000000000..421e14321d --- /dev/null +++ b/FEATURE_FLAG_PAYLOAD.md @@ -0,0 +1,196 @@ +# Feature Flag Payload Shape + +## Feature Flag Key +`demo-run-button-experiment` + +## Expected Structure + +The feature flag value should be either: + +### Control (Original Button) +- `false` +- `null` +- `undefined` +- Not set + +When any of these values are present, the original SplitButton will be displayed. + +### Experiment (Experimental Button) +An object with the following structure: + +```typescript +{ + variant: string, // Required: variant name (e.g., "bold-gradient", "animated", "playful", "minimal") + payload?: { // Optional: styling and content overrides + label?: string, // Button text (default: "Run") + icon?: string, // Icon class (default: variant-specific) + backgroundColor?: string, // Background color/class (default: variant-specific) + textColor?: string, // Text color/class (default: variant-specific) + borderRadius?: string, // Border radius class (default: variant-specific) + padding?: string // Padding class (default: "px-4 py-2") + } +} +``` + +## Example Payloads + +### Bold Gradient Variant +```json +{ + "variant": "bold-gradient", + "payload": { + "label": "Run", + "icon": "icon-[lucide--zap]", + "backgroundColor": "transparent", + "textColor": "white", + "borderRadius": "rounded-xl" + } +} +``` + +### Animated Variant +```json +{ + "variant": "animated", + "payload": { + "label": "Launch", + "icon": "icon-[lucide--rocket]", + "backgroundColor": "bg-primary-background", + "textColor": "white", + "borderRadius": "rounded-full" + } +} +``` + +### Playful Variant +```json +{ + "variant": "playful", + "payload": { + "label": "Go!", + "icon": "icon-[lucide--sparkles]", + "backgroundColor": "bg-gradient-to-br from-yellow-400 to-orange-500", + "textColor": "white", + "borderRadius": "rounded-2xl" + } +} +``` + +### Minimal Variant +```json +{ + "variant": "minimal", + "payload": { + "label": "Run", + "icon": "icon-[lucide--play]", + "backgroundColor": "bg-white", + "textColor": "text-gray-800", + "borderRadius": "rounded-md" + } +} +``` + +## Payload Properties + +### `variant` (required) +- Type: `string` +- Description: The variant name that determines the base styling and behavior +- Supported values: + - `"bold-gradient"` - Gradient background with animated effect + - `"animated"` - Pulsing animation effect + - `"playful"` - Sparkle effects with playful styling + - `"minimal"` - Clean, minimal design + - Any custom variant name (will use default styling) + +### `payload.label` (optional) +- Type: `string` +- Default: `"Run"` (or variant-specific default) +- Description: The text displayed on the button + +### `payload.icon` (optional) +- Type: `string` +- Default: Variant-specific icon +- Description: Icon class name (e.g., `"icon-[lucide--play]"`) +- Examples: + - `"icon-[lucide--play]"` - Play icon + - `"icon-[lucide--zap]"` - Lightning bolt + - `"icon-[lucide--rocket]"` - Rocket + - `"icon-[lucide--sparkles]"` - Sparkles + +### `payload.backgroundColor` (optional) +- Type: `string` +- Default: Variant-specific background +- Description: Tailwind CSS class or CSS color value for background +- Examples: + - `"bg-primary-background"` - Primary background color + - `"bg-white"` - White background + - `"transparent"` - Transparent (for gradient overlays) + - `"bg-gradient-to-br from-yellow-400 to-orange-500"` - Gradient + +### `payload.textColor` (optional) +- Type: `string` +- Default: Variant-specific text color +- Description: Tailwind CSS class or CSS color value for text +- Examples: + - `"white"` - White text (CSS color) + - `"text-white"` - White text (Tailwind class) + - `"text-gray-800"` - Dark gray text + +### `payload.borderRadius` (optional) +- Type: `string` +- Default: Variant-specific border radius +- Description: Tailwind CSS border radius class +- Examples: + - `"rounded-md"` - Medium border radius + - `"rounded-xl"` - Extra large border radius + - `"rounded-full"` - Fully rounded (pill shape) + - `"rounded-2xl"` - 2x extra large border radius + +### `payload.padding` (optional) +- Type: `string` +- Default: `"px-4 py-2"` +- Description: Tailwind CSS padding classes +- Examples: + - `"px-4 py-2"` - Standard padding + - `"px-6 py-3"` - Larger padding + - `"px-2 py-1"` - Smaller padding + +## Backend Integration + +The backend should send this feature flag via WebSocket in the `feature_flags` message: + +```json +{ + "type": "feature_flags", + "data": { + "demo-run-button-experiment": { + "variant": "bold-gradient", + "payload": { + "label": "Run", + "icon": "icon-[lucide--zap]", + "textColor": "white", + "borderRadius": "rounded-xl" + } + } + } +} +``` + +Or to show the control (original button): + +```json +{ + "type": "feature_flags", + "data": { + "demo-run-button-experiment": false + } +} +``` + +## Notes + +- All `payload` properties are optional - if omitted, variant-specific defaults will be used +- The `variant` property is required when the flag is truthy +- Color values can be either Tailwind classes (e.g., `"text-white"`) or CSS color values (e.g., `"white"`) +- The component will automatically handle both formats + diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index db48e79237..c642e6bed4 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index 879d866284..2180922d9a 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png index 0bfc41ae25..b3faaced03 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 7971b70119..b66692cdb2 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index 9b6c23c5e1..adf81b55c4 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index fee337f287..9ded7395f6 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index 3cb8614c18..36cdb4ffba 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 7da0164d6b..31a6219b4b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index 55d488c821..4951feb36c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index 75c2b82e8e..fdfd695397 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index bd292f07fb..2df57ff550 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index ebb8299532..3a004a7b86 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts b/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts new file mode 100644 index 0000000000..7ea5bf965b --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts @@ -0,0 +1,54 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' + +test.describe('Vue Node Resizing', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should resize node without position drift after selecting', async ({ + comfyPage + }) => { + // Get a Vue node fixture + const node = await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint') + const initialBox = await node.boundingBox() + if (!initialBox) throw new Error('Node bounding box not found') + + // Select the node first (this was causing the bug) + await node.header.click() + await comfyPage.page.waitForTimeout(100) // Brief pause after selection + + // Get position after selection + const selectedBox = await node.boundingBox() + if (!selectedBox) + throw new Error('Node bounding box not found after select') + + // Verify position unchanged after selection + expect(selectedBox.x).toBeCloseTo(initialBox.x, 1) + expect(selectedBox.y).toBeCloseTo(initialBox.y, 1) + + // Now resize from bottom-right corner + const resizeStartX = selectedBox.x + selectedBox.width - 5 + const resizeStartY = selectedBox.y + selectedBox.height - 5 + + await comfyPage.page.mouse.move(resizeStartX, resizeStartY) + await comfyPage.page.mouse.down() + await comfyPage.page.mouse.move(resizeStartX + 50, resizeStartY + 30) + await comfyPage.page.mouse.up() + + // Get final position and size + const finalBox = await node.boundingBox() + if (!finalBox) throw new Error('Node bounding box not found after resize') + + // Position should NOT have changed (the bug was position drift) + expect(finalBox.x).toBeCloseTo(initialBox.x, 1) + expect(finalBox.y).toBeCloseTo(initialBox.y, 1) + + // Size should have increased + expect(finalBox.width).toBeGreaterThan(initialBox.width) + expect(finalBox.height).toBeGreaterThan(initialBox.height) + }) +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png index 465933484f..3049bf7192 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png index d8bcc4d257..73c8450986 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png index 68fd80359f..5776f8871e 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png index 557837747c..24a30c3f66 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts b/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts deleted file mode 100644 index 96ab721cad..0000000000 --- a/browser_tests/tests/vueNodes/nodeStates/lod.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { expect } from '@playwright/test' - -import { comfyPageFixture as test } from '../../../fixtures/ComfyPage' - -test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') -}) - -test.describe('Vue Nodes - LOD', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setup() - await comfyPage.loadWorkflow('default') - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8) - }) - - 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 comboboxesInNodes = vueNodesContainer.getByRole('combobox') - - await expect(textboxesInNodes.first()).toBeVisible() - await expect(comboboxesInNodes.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(comboboxesInNodes.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(comboboxesInNodes.first()).toBeVisible() - }) -}) diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png index e2ff270455..cf91eb87b2 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png index 4950f265b9..7cc08fbc55 100644 Binary files a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png and b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png differ diff --git a/build/plugins/comfyAPIPlugin.ts b/build/plugins/comfyAPIPlugin.ts index 3e9a9e5b98..1a3b8d93d5 100644 --- a/build/plugins/comfyAPIPlugin.ts +++ b/build/plugins/comfyAPIPlugin.ts @@ -88,12 +88,14 @@ export function comfyAPIPlugin(isDev: boolean): Plugin { if (result.exports.length > 0) { const projectRoot = process.cwd() - const relativePath = path.relative(path.join(projectRoot, 'src'), id) + const relativePath = path + .relative(path.join(projectRoot, 'src'), id) + .replace(/\\/g, '/') const shimFileName = relativePath.replace(/\.ts$/, '.js') let shimContent = `// Shim for ${relativePath}\n` - const fileKey = relativePath.replace(/\.ts$/, '').replace(/\\/g, '/') + const fileKey = relativePath.replace(/\.ts$/, '') const warningMessage = getWarningMessage(fileKey, shimFileName) if (warningMessage) { diff --git a/cloud-loader-dropdown.md b/cloud-loader-dropdown.md new file mode 100644 index 0000000000..84ef263857 --- /dev/null +++ b/cloud-loader-dropdown.md @@ -0,0 +1,24 @@ +Fixes loader dropdown placeholder +=============================== + +Cloud loader dropdowns hydrate via `useAssetWidgetData(nodeType)`, so `dropdownItems` stays empty until the Asset API returns friendly filenames. Meanwhile `modelValue` already holds the saved asset and the watcher at [WidgetSelectDropdown.vue#L215-L227](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue#L215-L227) only tracks `modelValue`. It runs before assets load, fails to find a match, clears `selectedSet`, and the placeholder persists. + +```ts +watch( + modelValue, + (currentValue) => { + if (currentValue === undefined) { + selectedSet.value.clear() + return + } + const item = dropdownItems.value.find((item) => item.name === currentValue) + if (item) { + selectedSet.value.clear() + selectedSet.value.add(item.id) + } + }, + { immediate: true } +) +``` + +Once the API resolves, `dropdownItems` recomputes but nothing resyncs because the watcher never sees that change. Desktop doesn’t hit this because it still reads from `widget.options.values` immediately. diff --git a/frontend/posthog-feature-flag-demo/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.vue b/frontend/posthog-feature-flag-demo/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.vue new file mode 100644 index 0000000000..4f231bb100 --- /dev/null +++ b/frontend/posthog-feature-flag-demo/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.vue @@ -0,0 +1,56 @@ + + + + diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index 9c75987f6d..082d1baf11 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -1329,57 +1329,6 @@ 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; - mask-image: none; - clip-path: none; - background-image: none; - text-rendering: optimizeSpeed; - border-radius: 0; - contain: layout style; - transition: none; -} - -.isLOD .lg-node-header { - border-radius: 0; - pointer-events: none; -} - -.isLOD .lg-node-widgets { - pointer-events: none; -} - -.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 */ - /* ===================== Mask Editor Styles ===================== */ /* To be migrated to Tailwind later */ #maskEditor_brush { diff --git a/public/assets/images/cloud-subscription.webm b/public/assets/images/cloud-subscription.webm index ec81ab6d1e..f26392a66e 100644 Binary files a/public/assets/images/cloud-subscription.webm and b/public/assets/images/cloud-subscription.webm differ diff --git a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue index db28d980c7..3c02303a00 100644 --- a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue +++ b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue @@ -1,35 +1,42 @@ @@ -44,17 +51,23 @@ import { useI18n } from 'vue-i18n' import { isCloud } from '@/platform/distribution/types' import { useTelemetry } from '@/platform/telemetry' +import { app } from '@/scripts/app' import { useCommandStore } from '@/stores/commandStore' +import { useNodeDefStore } from '@/stores/nodeDefStore' import { useQueueSettingsStore } from '@/stores/queueStore' import { useWorkspaceStore } from '@/stores/workspaceStore' -import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' +import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes' import BatchCountEdit from '../BatchCountEdit.vue' +import FeatureFlaggedRunButton from './FeatureFlaggedRunButton.vue' const workspaceStore = useWorkspaceStore() const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore()) -const { hasMissingNodes } = useMissingNodes() +const nodeDefStore = useNodeDefStore() +const hasMissingNodes = computed(() => + graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName) +) const { t } = useI18n() const queueModeMenuItemLookup = computed(() => { diff --git a/src/components/actionbar/ComfyRunButton/ExperimentalRunButton.vue b/src/components/actionbar/ComfyRunButton/ExperimentalRunButton.vue new file mode 100644 index 0000000000..32bdd600d2 --- /dev/null +++ b/src/components/actionbar/ComfyRunButton/ExperimentalRunButton.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/src/components/actionbar/ComfyRunButton/FeatureFlaggedRunButton.vue b/src/components/actionbar/ComfyRunButton/FeatureFlaggedRunButton.vue new file mode 100644 index 0000000000..13caff89ca --- /dev/null +++ b/src/components/actionbar/ComfyRunButton/FeatureFlaggedRunButton.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/components/breadcrumb/SubgraphBreadcrumbItem.vue b/src/components/breadcrumb/SubgraphBreadcrumbItem.vue index 2e86925585..2181cce1b0 100644 --- a/src/components/breadcrumb/SubgraphBreadcrumbItem.vue +++ b/src/components/breadcrumb/SubgraphBreadcrumbItem.vue @@ -64,11 +64,13 @@ import { ComfyWorkflow, useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import { app } from '@/scripts/app' import { useDialogService } from '@/services/dialogService' import { useCommandStore } from '@/stores/commandStore' +import { useNodeDefStore } from '@/stores/nodeDefStore' import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' import { appendJsonExt } from '@/utils/formatUtil' -import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' +import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes' interface Props { item: MenuItem @@ -79,7 +81,10 @@ const props = withDefaults(defineProps(), { isActive: false }) -const { hasMissingNodes } = useMissingNodes() +const nodeDefStore = useNodeDefStore() +const hasMissingNodes = computed(() => + graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName) +) const { t } = useI18n() const menu = ref & MenuState>() diff --git a/src/components/sidebar/ComfyMenuButton.vue b/src/components/sidebar/ComfyMenuButton.vue index b1eb5cc9df..8af51fb694 100644 --- a/src/components/sidebar/ComfyMenuButton.vue +++ b/src/components/sidebar/ComfyMenuButton.vue @@ -73,6 +73,7 @@ @click.stop="handleNodes2ToggleClick" > {{ item.label }} + {{ $t('g.beta') }} import type { MenuItem } from 'primevue/menuitem' +import Tag from 'primevue/tag' import TieredMenu from 'primevue/tieredmenu' import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu' import ToggleSwitch from 'primevue/toggleswitch' diff --git a/src/composables/graph/useVueNodeLifecycle.ts b/src/composables/graph/useVueNodeLifecycle.ts index 7f17440149..dfc2ac06aa 100644 --- a/src/composables/graph/useVueNodeLifecycle.ts +++ b/src/composables/graph/useVueNodeLifecycle.ts @@ -3,7 +3,6 @@ import { shallowRef, watch } from 'vue' import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager' import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager' -import { useRenderModeSetting } from '@/composables/settings/useRenderModeSetting' import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags' import { useVueNodesMigrationDismissed } from '@/composables/useVueNodesMigrationDismissed' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' @@ -11,6 +10,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync' +import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil' import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale' import { app as comfyApp } from '@/scripts/app' import { useToastStore } from '@/platform/updates/common/toastStore' @@ -26,11 +26,6 @@ function useVueNodeLifecycleIndividual() { let hasShownMigrationToast = false - useRenderModeSetting( - { setting: 'LiteGraph.Canvas.MinFontSizeForLOD', vue: 0, litegraph: 8 }, - shouldRenderVueNodes - ) - const initializeNodeManager = () => { // Use canvas graph if available (handles subgraph contexts), fallback to app graph const activeGraph = comfyApp.canvas?.graph @@ -44,7 +39,10 @@ function useVueNodeLifecycleIndividual() { const nodes = activeGraph._nodes.map((node: LGraphNode) => ({ id: node.id.toString(), pos: [node.pos[0], node.pos[1]] as [number, number], - size: [node.size[0], node.size[1]] as [number, number] + size: [node.size[0], removeNodeTitleHeight(node.size[1])] as [ + number, + number + ] })) layoutStore.initializeFromLiteGraph(nodes) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 8e9811e37c..b8c4da046d 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -49,6 +49,21 @@ const calculateRunwayDurationPrice = (node: LGraphNode): string => { return `$${cost}/Run` } +const makeOmniProDurationCalculator = + (pricePerSecond: number): PricingFunction => + (node: LGraphNode): string => { + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + if (!durationWidget) return `$${pricePerSecond.toFixed(3)}/second` + + const seconds = parseFloat(String(durationWidget.value)) + if (!Number.isFinite(seconds)) return `$${pricePerSecond.toFixed(3)}/second` + + const cost = pricePerSecond * seconds + return `$${cost.toFixed(2)}/Run` + } + const pixversePricingCalculator = (node: LGraphNode): string => { const durationWidget = node.widgets?.find( (w) => w.name === 'duration_seconds' @@ -131,6 +146,11 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => { '720p': [0.51, 0.56], '1080p': [1.18, 1.22] }, + 'seedance-1-0-pro-fast': { + '480p': [0.09, 0.1], + '720p': [0.21, 0.23], + '1080p': [0.47, 0.49] + }, 'seedance-1-0-lite': { '480p': [0.17, 0.18], '720p': [0.37, 0.41], @@ -138,11 +158,13 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => { } } - const modelKey = model.includes('seedance-1-0-pro') - ? 'seedance-1-0-pro' - : model.includes('seedance-1-0-lite') - ? 'seedance-1-0-lite' - : '' + const modelKey = model.includes('seedance-1-0-pro-fast') + ? 'seedance-1-0-pro-fast' + : model.includes('seedance-1-0-pro') + ? 'seedance-1-0-pro' + : model.includes('seedance-1-0-lite') + ? 'seedance-1-0-lite' + : '' const resKey = resolution.includes('1080') ? '1080p' @@ -699,6 +721,21 @@ const apiNodeCosts: Record = KlingVirtualTryOnNode: { displayPrice: '$0.07/Run' }, + KlingOmniProTextToVideoNode: { + displayPrice: makeOmniProDurationCalculator(0.112) + }, + KlingOmniProFirstLastFrameNode: { + displayPrice: makeOmniProDurationCalculator(0.112) + }, + KlingOmniProImageToVideoNode: { + displayPrice: makeOmniProDurationCalculator(0.112) + }, + KlingOmniProVideoToVideoNode: { + displayPrice: makeOmniProDurationCalculator(0.168) + }, + KlingOmniProEditVideoNode: { + displayPrice: '$0.168/second' + }, LumaImageToVideoNode: { displayPrice: (node: LGraphNode): string => { // Same pricing as LumaVideoNode per CSV @@ -1873,6 +1910,10 @@ export const useNodePricing = () => { KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'], KlingSingleImageVideoEffectNode: ['effect_scene'], KlingStartEndFrameNode: ['mode', 'model_name', 'duration'], + KlingOmniProTextToVideoNode: ['duration'], + KlingOmniProFirstLastFrameNode: ['duration'], + KlingOmniProImageToVideoNode: ['duration'], + KlingOmniProVideoToVideoNode: ['duration'], MinimaxHailuoVideoNode: ['resolution', 'duration'], OpenAIDalle3: ['size', 'quality'], OpenAIDalle2: ['size', 'n'], diff --git a/src/composables/queue/useJobList.ts b/src/composables/queue/useJobList.ts index 290cabc681..47996636be 100644 --- a/src/composables/queue/useJobList.ts +++ b/src/composables/queue/useJobList.ts @@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n' import { useQueueProgress } from '@/composables/queue/useQueueProgress' import { st } from '@/i18n' +import { isCloud } from '@/platform/distribution/types' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useExecutionStore } from '@/stores/executionStore' import { useQueueStore } from '@/stores/queueStore' @@ -263,7 +264,8 @@ export function useJobList() { totalPercent: isActive ? totalPercent.value : undefined, currentNodePercent: isActive ? currentNodePercent.value : undefined, currentNodeName: isActive ? currentNodeName.value : undefined, - showAddedHint + showAddedHint, + isCloud }) return { diff --git a/src/composables/settings/useRenderModeSetting.ts b/src/composables/settings/useRenderModeSetting.ts deleted file mode 100644 index 6e97471e25..0000000000 --- a/src/composables/settings/useRenderModeSetting.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { ComputedRef } from 'vue' -import { ref, watch } from 'vue' - -import { useSettingStore } from '@/platform/settings/settingStore' -import type { Settings } from '@/schemas/apiSchema' - -interface RenderModeSettingConfig { - setting: TSettingKey - vue: Settings[TSettingKey] - litegraph: Settings[TSettingKey] -} - -export function useRenderModeSetting( - config: RenderModeSettingConfig, - isVueMode: ComputedRef -) { - const settingStore = useSettingStore() - const vueValue = ref(config.vue) - const litegraphValue = ref(config.litegraph) - const lastWasVue = ref(null) - - const load = async (vue: boolean) => { - if (lastWasVue.value === vue) return - - if (lastWasVue.value !== null) { - const currentValue = settingStore.get(config.setting) - if (lastWasVue.value) { - vueValue.value = currentValue - } else { - litegraphValue.value = currentValue - } - } - - await settingStore.set( - config.setting, - vue ? vueValue.value : litegraphValue.value - ) - lastWasVue.value = vue - } - - watch(isVueMode, load, { immediate: true }) -} diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 643bb103ff..2fb7ff9671 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -1,5 +1,6 @@ -import { computed, reactive, readonly } from 'vue' +import { computed, reactive, readonly, ref } from 'vue' +import { remoteConfig } from '@/platform/remoteConfig/remoteConfig' import { api } from '@/scripts/api' /** @@ -9,9 +10,21 @@ export enum ServerFeatureFlag { SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata', MAX_UPLOAD_SIZE = 'max_upload_size', MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4', - MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled' + MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled', + ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled' } +/** + * Feature flag variant structure for experiments + */ +export interface FeatureFlagVariant { + variant: string + payload?: Record +} + +// Demo mode: allows manual override for demonstration +const demoOverrides = ref>({}) + /** * Composable for reactive access to server-side feature flags */ @@ -27,15 +40,57 @@ export function useFeatureFlags() { return api.getServerFeature(ServerFeatureFlag.MANAGER_SUPPORTS_V4) }, get modelUploadButtonEnabled() { - return api.getServerFeature( - ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED, - false + // Check remote config first (from /api/features), fall back to websocket feature flags + return ( + remoteConfig.value.model_upload_button_enabled ?? + api.getServerFeature( + ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED, + false + ) + ) + }, + get assetUpdateOptionsEnabled() { + // Check remote config first (from /api/features), fall back to websocket feature flags + return ( + remoteConfig.value.asset_update_options_enabled ?? + api.getServerFeature( + ServerFeatureFlag.ASSET_UPDATE_OPTIONS_ENABLED, + false + ) ) } }) const featureFlag = (featurePath: string, defaultValue?: T) => - computed(() => api.getServerFeature(featurePath, defaultValue)) + computed(() => { + // Check demo overrides first + if (demoOverrides.value[featurePath] !== undefined) { + return demoOverrides.value[featurePath] as T + } + // Check remote config (from /api/features) - convert hyphens to underscores for lookup + const remoteConfigKey = featurePath.replace(/-/g, '_') + const remoteValue = (remoteConfig.value as Record)[ + remoteConfigKey + ] + if (remoteValue !== undefined) { + return remoteValue as T + } + // Fall back to server feature flags (WebSocket) - try both hyphen and underscore versions + const wsValue = api.getServerFeature(featurePath, undefined) + if (wsValue !== undefined) { + return wsValue as T + } + // Try underscore version for WebSocket flags + const wsValueUnderscore = api.getServerFeature( + featurePath.replace(/-/g, '_'), + undefined + ) + if (wsValueUnderscore !== undefined) { + return wsValueUnderscore as T + } + // Return default if nothing found + return defaultValue as T + }) return { flags: readonly(flags), diff --git a/src/extensions/core/cloudRemoteConfig.ts b/src/extensions/core/cloudRemoteConfig.ts index a40ac401b1..8716269fba 100644 --- a/src/extensions/core/cloudRemoteConfig.ts +++ b/src/extensions/core/cloudRemoteConfig.ts @@ -9,7 +9,7 @@ useExtensionService().registerExtension({ name: 'Comfy.Cloud.RemoteConfig', setup: async () => { - // Poll for config updates every 30 seconds - setInterval(() => void loadRemoteConfig(), 30000) + // Poll for config updates every 10 minutes + setInterval(() => void loadRemoteConfig(), 600_000) } }) diff --git a/src/extensions/core/widgetInputs.ts b/src/extensions/core/widgetInputs.ts index 2c2376b11f..2024a41c7c 100644 --- a/src/extensions/core/widgetInputs.ts +++ b/src/extensions/core/widgetInputs.ts @@ -7,9 +7,9 @@ import type { INodeInputSlot, INodeOutputSlot, ISlotType, - LLink, - Point + LLink } from '@/lib/litegraph/src/litegraph' +import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot' import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import type { InputSpec } from '@/schemas/nodeDefSchema' @@ -37,15 +37,15 @@ export class PrimitiveNode extends LGraphNode { } override applyToGraph(extraLinks: LLink[] = []) { - if (!this.outputs[0].links?.length) return + if (!this.outputs[0].links?.length || !this.graph) return const links = [ - ...this.outputs[0].links.map((l) => app.graph.links[l]), + ...this.outputs[0].links.map((l) => this.graph!.links[l]), ...extraLinks ] let v = this.widgets?.[0].value if (v && this.properties[replacePropertyName]) { - v = applyTextReplacements(app.graph, v as string) + v = applyTextReplacements(this.graph, v as string) } // For each output link copy our value over the original widget value @@ -331,13 +331,13 @@ export class PrimitiveNode extends LGraphNode { const config1 = (output.widget?.[GET_CONFIG] as () => InputSpec)?.() if (!config1) return const isNumber = config1[0] === 'INT' || config1[0] === 'FLOAT' - if (!isNumber) return + if (!isNumber || !this.graph) return for (const linkId of links) { - const link = app.graph.links[linkId] + const link = this.graph.links[linkId] if (!link) continue // Can be null when removing a node - const theirNode = app.graph.getNodeById(link.target_id) + const theirNode = this.graph.getNodeById(link.target_id) if (!theirNode) continue const theirInput = theirNode.inputs[link.target_slot] @@ -441,10 +441,7 @@ function getWidgetType(config: InputSpec) { return { type } } -export function setWidgetConfig( - slot: INodeInputSlot | INodeOutputSlot, - config?: InputSpec -) { +export function setWidgetConfig(slot: INodeInputSlot, config?: InputSpec) { if (!slot.widget) return if (config) { slot.widget[GET_CONFIG] = () => config @@ -452,19 +449,18 @@ export function setWidgetConfig( delete slot.widget } - if ('link' in slot) { - const link = app.graph.links[slot.link ?? -1] - if (link) { - const originNode = app.graph.getNodeById(link.origin_id) - if (originNode && isPrimitiveNode(originNode)) { - if (config) { - originNode.recreateWidget() - } else if (!app.configuringGraph) { - originNode.disconnectOutput(0) - originNode.onLastDisconnect() - } - } - } + if (!(slot instanceof NodeSlot)) return + const graph = slot.node.graph + if (!graph) return + const link = graph.links[slot.link ?? -1] + if (!link) return + const originNode = graph.getNodeById(link.origin_id) + if (!originNode || !isPrimitiveNode(originNode)) return + if (config) { + originNode.recreateWidget() + } else if (!app.configuringGraph) { + originNode.disconnectOutput(0) + originNode.onLastDisconnect() } } @@ -555,15 +551,6 @@ app.registerExtension({ } ) - function isNodeAtPos(pos: Point) { - for (const n of app.graph.nodes) { - if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) { - return true - } - } - return false - } - // Double click a widget input to automatically attach a primitive const origOnInputDblClick = nodeType.prototype.onInputDblClick nodeType.prototype.onInputDblClick = function ( @@ -589,18 +576,18 @@ app.registerExtension({ // Create a primitive node const node = LiteGraph.createNode('PrimitiveNode') - if (!node) return r + const graph = app.canvas.graph + if (!node || !graph) return r - this.graph?.add(node) + graph?.add(node) // Calculate a position that won't directly overlap another node const pos: [number, number] = [ this.pos[0] - node.size[0] - 30, this.pos[1] ] - while (isNodeAtPos(pos)) { + while (graph.getNodeOnPos(pos[0], pos[1], graph.nodes)) pos[1] += LiteGraph.NODE_TITLE_HEIGHT - } node.pos = pos node.connect(0, this, slot) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index fb21e72b53..d1d2062c94 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -7,6 +7,7 @@ import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculatio import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { LayoutSource } from '@/renderer/core/layout/types' +import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil' import { CanvasPointer } from './CanvasPointer' import type { ContextMenu } from './ContextMenu' @@ -4043,16 +4044,25 @@ 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 - } - })) + const newPositions = created + .filter((item): item is LGraphNode => item instanceof LGraphNode) + .map((node) => { + const fullHeight = node.size?.[1] ?? 200 + const layoutHeight = LiteGraph.vueNodesMode + ? removeNodeTitleHeight(fullHeight) + : fullHeight + return { + nodeId: String(node.id), + bounds: { + x: node.pos[0], + y: node.pos[1], + width: node.size?.[0] ?? 100, + height: layoutHeight + } + } + }) + if (newPositions.length) layoutStore.setSource(LayoutSource.Canvas) layoutStore.batchUpdateNodeBounds(newPositions) this.selectItems(created) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index d3a34c8f17..45a04ba3dd 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1,5 +1,6 @@ { "g": { + "beta": "Beta", "user": "User", "currentUser": "Current user", "empty": "Empty", @@ -984,6 +985,7 @@ "initializingAlmostReady": "Initializing - Almost ready", "inQueue": "In queue...", "jobAddedToQueue": "Job added to queue", + "completedIn": "Finished in {duration}", "jobMenu": { "openAsWorkflowNewTab": "Open as workflow in new tab", "openWorkflowNewTab": "Open workflow in new tab", diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts index 43bfbd70fe..8d2c400f37 100644 --- a/src/platform/remoteConfig/types.ts +++ b/src/platform/remoteConfig/types.ts @@ -1,3 +1,5 @@ +import type { TelemetryEventName } from '@/platform/telemetry/types' + /** * Server health alert configuration from the backend */ @@ -31,4 +33,7 @@ export type RemoteConfig = { comfy_api_base_url?: string comfy_platform_base_url?: string firebase_config?: FirebaseRuntimeConfig + telemetry_disabled_events?: TelemetryEventName[] + model_upload_button_enabled?: boolean + asset_update_options_enabled?: boolean } diff --git a/src/platform/settings/composables/useSettingSearch.ts b/src/platform/settings/composables/useSettingSearch.ts index c2bf3cfe87..a2d29f2a64 100644 --- a/src/platform/settings/composables/useSettingSearch.ts +++ b/src/platform/settings/composables/useSettingSearch.ts @@ -8,9 +8,11 @@ import { } from '@/platform/settings/settingStore' import type { ISettingGroup, SettingParams } from '@/platform/settings/types' import { normalizeI18nKey } from '@/utils/formatUtil' +import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags' export function useSettingSearch() { const settingStore = useSettingStore() + const { shouldRenderVueNodes } = useVueFeatureFlags() const searchQuery = ref('') const filteredSettingIds = ref([]) @@ -54,7 +56,11 @@ export function useSettingSearch() { const allSettings = Object.values(settingStore.settingsById) const filteredSettings = allSettings.filter((setting) => { // Filter out hidden and deprecated settings, just like in normal settings tree - if (setting.type === 'hidden' || setting.deprecated) { + if ( + setting.type === 'hidden' || + setting.deprecated || + (shouldRenderVueNodes.value && setting.hideInVueNodes) + ) { return false } diff --git a/src/platform/settings/composables/useSettingUI.ts b/src/platform/settings/composables/useSettingUI.ts index a49cccf191..cfeb571dbd 100644 --- a/src/platform/settings/composables/useSettingUI.ts +++ b/src/platform/settings/composables/useSettingUI.ts @@ -10,6 +10,7 @@ import type { SettingParams } from '@/platform/settings/types' import { isElectron } from '@/utils/envUtil' import { normalizeI18nKey } from '@/utils/formatUtil' import { buildTree } from '@/utils/treeUtil' +import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags' interface SettingPanelItem { node: SettingTreeNode @@ -31,10 +32,14 @@ export function useSettingUI( const settingStore = useSettingStore() const activeCategory = ref(null) + const { shouldRenderVueNodes } = useVueFeatureFlags() + const settingRoot = computed(() => { const root = buildTree( Object.values(settingStore.settingsById).filter( - (setting: SettingParams) => setting.type !== 'hidden' + (setting: SettingParams) => + setting.type !== 'hidden' && + !(shouldRenderVueNodes.value && setting.hideInVueNodes) ), (setting: SettingParams) => setting.category || setting.id.split('.') ) diff --git a/src/platform/settings/constants/coreSettings.ts b/src/platform/settings/constants/coreSettings.ts index 66cb9c310e..0931733ea4 100644 --- a/src/platform/settings/constants/coreSettings.ts +++ b/src/platform/settings/constants/coreSettings.ts @@ -919,7 +919,8 @@ export const CORE_SETTINGS: SettingParams[] = [ step: 1 }, defaultValue: 8, - versionAdded: '1.26.7' + versionAdded: '1.26.7', + hideInVueNodes: true }, { id: 'Comfy.Canvas.SelectionToolbox', diff --git a/src/platform/settings/types.ts b/src/platform/settings/types.ts index b1efbe9a1b..9bd48fc8c9 100644 --- a/src/platform/settings/types.ts +++ b/src/platform/settings/types.ts @@ -47,6 +47,7 @@ export interface SettingParams extends FormItem { // sortOrder for sorting settings within a group. Higher values appear first. // Default is 0 if not specified. sortOrder?: number + hideInVueNodes?: boolean } /** diff --git a/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts b/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts index ea5f97302a..0305acaf7e 100644 --- a/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts +++ b/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts @@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router' import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager' import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces' +import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useTemplateWorkflows } from './useTemplateWorkflows' @@ -13,9 +14,10 @@ import { useTemplateWorkflows } from './useTemplateWorkflows' * Supports URLs like: * - /?template=flux_simple (loads with default source) * - /?template=flux_simple&source=custom (loads from custom source) + * - /?template=flux_simple&mode=linear (loads template in linear mode) * * Input validation: - * - Template and source parameters must match: ^[a-zA-Z0-9_-]+$ + * - Template, source, and mode parameters must match: ^[a-zA-Z0-9_-]+$ * - Invalid formats are rejected with console warnings */ export function useTemplateUrlLoader() { @@ -24,7 +26,10 @@ export function useTemplateUrlLoader() { const { t } = useI18n() const toast = useToast() const templateWorkflows = useTemplateWorkflows() + const canvasStore = useCanvasStore() const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE + const SUPPORTED_MODES = ['linear'] as const + type SupportedMode = (typeof SUPPORTED_MODES)[number] /** * Validates parameter format to prevent path traversal and injection attacks @@ -34,12 +39,20 @@ export function useTemplateUrlLoader() { } /** - * Removes template and source parameters from URL + * Type guard to check if a value is a supported mode + */ + const isSupportedMode = (mode: string): mode is SupportedMode => { + return SUPPORTED_MODES.includes(mode as SupportedMode) + } + + /** + * Removes template, source, and mode parameters from URL */ const cleanupUrlParams = () => { const newQuery = { ...route.query } delete newQuery.template delete newQuery.source + delete newQuery.mode void router.replace({ query: newQuery }) } @@ -70,6 +83,24 @@ export function useTemplateUrlLoader() { return } + const modeParam = route.query.mode as string | undefined + + if ( + modeParam && + (typeof modeParam !== 'string' || !isValidParameter(modeParam)) + ) { + console.warn( + `[useTemplateUrlLoader] Invalid mode parameter format: ${modeParam}` + ) + return + } + + if (modeParam && !isSupportedMode(modeParam)) { + console.warn( + `[useTemplateUrlLoader] Unsupported mode parameter: ${modeParam}. Supported modes: ${SUPPORTED_MODES.join(', ')}` + ) + } + try { await templateWorkflows.loadTemplates() @@ -87,6 +118,9 @@ export function useTemplateUrlLoader() { }), life: 3000 }) + } else if (modeParam === 'linear') { + // Set linear mode after successful template load + canvasStore.linearMode = true } } catch (error) { console.error( diff --git a/src/renderer/core/layout/__tests__/TransformPane.test.ts b/src/renderer/core/layout/__tests__/TransformPane.test.ts index ae18ffbacb..310294c5ce 100644 --- a/src/renderer/core/layout/__tests__/TransformPane.test.ts +++ b/src/renderer/core/layout/__tests__/TransformPane.test.ts @@ -29,12 +29,6 @@ vi.mock('@/renderer/core/layout/transform/useTransformState', () => { } }) -vi.mock('@/renderer/extensions/vueNodes/lod/useLOD', () => ({ - useLOD: vi.fn(() => ({ - isLOD: false - })) -})) - function createMockCanvas(): LGraphCanvas { return { canvas: { diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts index a3449ce823..b6e139e6bc 100644 --- a/src/renderer/core/layout/store/layoutStore.ts +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -9,6 +9,8 @@ import { computed, customRef, ref } from 'vue' import type { ComputedRef, Ref } from 'vue' import * as Y from 'yjs' +import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil' + import { ACTOR_CONFIG } from '@/renderer/core/layout/constants' import { LayoutSource } from '@/renderer/core/layout/types' import type { @@ -136,6 +138,8 @@ class LayoutStoreImpl implements LayoutStore { // Vue dragging state for selection toolbox (public ref for direct mutation) public isDraggingVueNodes = ref(false) + // Vue resizing state to prevent drag from activating during resize + public isResizingVueNodes = ref(false) constructor() { // Initialize Yjs data structures @@ -1414,8 +1418,8 @@ class LayoutStoreImpl implements LayoutStore { batchUpdateNodeBounds(updates: NodeBoundsUpdate[]): void { if (updates.length === 0) return - // Set source to Vue for these DOM-driven updates const originalSource = this.currentSource + const shouldNormalizeHeights = originalSource === LayoutSource.DOM this.currentSource = LayoutSource.Vue const nodeIds: NodeId[] = [] @@ -1426,8 +1430,15 @@ class LayoutStoreImpl implements LayoutStore { if (!ynode) continue const currentLayout = yNodeToLayout(ynode) + const normalizedBounds = shouldNormalizeHeights + ? { + ...bounds, + height: removeNodeTitleHeight(bounds.height) + } + : bounds + boundsRecord[nodeId] = { - bounds, + bounds: normalizedBounds, previousBounds: currentLayout.bounds } nodeIds.push(nodeId) diff --git a/src/renderer/core/layout/sync/useLayoutSync.ts b/src/renderer/core/layout/sync/useLayoutSync.ts index c3cbb9ebd7..b51aeafeee 100644 --- a/src/renderer/core/layout/sync/useLayoutSync.ts +++ b/src/renderer/core/layout/sync/useLayoutSync.ts @@ -8,6 +8,7 @@ import { onUnmounted, ref } from 'vue' import type { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import { addNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil' /** * Composable for syncing LiteGraph with the Layout system @@ -43,12 +44,13 @@ export function useLayoutSync() { liteNode.pos[1] = layout.position.y } + const targetHeight = addNodeTitleHeight(layout.size.height) if ( liteNode.size[0] !== layout.size.width || - liteNode.size[1] !== layout.size.height + liteNode.size[1] !== targetHeight ) { // Use setSize() to trigger onResize callback - liteNode.setSize([layout.size.width, layout.size.height]) + liteNode.setSize([layout.size.width, targetHeight]) } } diff --git a/src/renderer/core/layout/transform/TransformPane.vue b/src/renderer/core/layout/transform/TransformPane.vue index e1287e5996..edc6835729 100644 --- a/src/renderer/core/layout/transform/TransformPane.vue +++ b/src/renderer/core/layout/transform/TransformPane.vue @@ -4,8 +4,7 @@ :class=" cn( 'absolute inset-0 w-full h-full pointer-events-none', - isInteracting ? 'transform-pane--interacting' : 'will-change-auto', - isLOD && 'isLOD' + isInteracting ? 'transform-pane--interacting' : 'will-change-auto' ) " :style="transformStyle" @@ -22,7 +21,6 @@ import { computed } from 'vue' import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph' 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 { @@ -31,9 +29,7 @@ interface TransformPaneProps { const props = defineProps() -const { camera, transformStyle, syncWithCanvas } = useTransformState() - -const { isLOD } = useLOD(camera) +const { transformStyle, syncWithCanvas } = useTransformState() const canvasElement = computed(() => props.canvas?.canvas) const { isTransforming: isInteracting } = useTransformSettling(canvasElement, { diff --git a/src/renderer/core/layout/types.ts b/src/renderer/core/layout/types.ts index 0af4112c71..4332059f5e 100644 --- a/src/renderer/core/layout/types.ts +++ b/src/renderer/core/layout/types.ts @@ -10,6 +10,7 @@ import type { ComputedRef, Ref } from 'vue' export enum LayoutSource { Canvas = 'canvas', Vue = 'vue', + DOM = 'dom', External = 'external' } diff --git a/src/renderer/core/layout/utils/nodeSizeUtil.ts b/src/renderer/core/layout/utils/nodeSizeUtil.ts new file mode 100644 index 0000000000..811240cd09 --- /dev/null +++ b/src/renderer/core/layout/utils/nodeSizeUtil.ts @@ -0,0 +1,7 @@ +import { LiteGraph } from '@/lib/litegraph/src/litegraph' + +export const removeNodeTitleHeight = (height: number) => + Math.max(0, height - (LiteGraph.NODE_TITLE_HEIGHT || 0)) + +export const addNodeTitleHeight = (height: number) => + height + LiteGraph.NODE_TITLE_HEIGHT diff --git a/src/renderer/extensions/vueNodes/VideoPreview.vue b/src/renderer/extensions/vueNodes/VideoPreview.vue index df1609e682..e02d9f15df 100644 --- a/src/renderer/extensions/vueNodes/VideoPreview.vue +++ b/src/renderer/extensions/vueNodes/VideoPreview.vue @@ -83,20 +83,17 @@ -
- -
- - {{ $t('g.errorLoadingVideo') }} - - - {{ $t('g.loading') }}... - - - {{ actualDimensions || $t('g.calculatingDimensions') }} - -
- + +
+ + {{ $t('g.errorLoadingVideo') }} + + + {{ $t('g.loading') }}... + + + {{ actualDimensions || $t('g.calculatingDimensions') }} +
@@ -110,8 +107,6 @@ import { useI18n } from 'vue-i18n' import { downloadFile } from '@/base/common/downloadUtil' import { useNodeOutputStore } from '@/stores/imagePreviewStore' -import LODFallback from './components/LODFallback.vue' - interface VideoPreviewProps { /** Array of video URLs to display */ readonly imageUrls: readonly string[] // Named imageUrls for consistency with parent components diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.vue b/src/renderer/extensions/vueNodes/components/ImagePreview.vue index 9b1020f94c..b7b9bd7b46 100644 --- a/src/renderer/extensions/vueNodes/components/ImagePreview.vue +++ b/src/renderer/extensions/vueNodes/components/ImagePreview.vue @@ -93,20 +93,17 @@ -
- -
- - {{ $t('g.errorLoadingImage') }} - - - {{ $t('g.loading') }}... - - - {{ actualDimensions || $t('g.calculatingDimensions') }} - -
- + +
+ + {{ $t('g.errorLoadingImage') }} + + + {{ $t('g.loading') }}... + + + {{ actualDimensions || $t('g.calculatingDimensions') }} +
@@ -122,8 +119,6 @@ import { app } from '@/scripts/app' 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[] diff --git a/src/renderer/extensions/vueNodes/components/InputSlot.vue b/src/renderer/extensions/vueNodes/components/InputSlot.vue index 238a60027b..9edeacbf8b 100644 --- a/src/renderer/extensions/vueNodes/components/InputSlot.vue +++ b/src/renderer/extensions/vueNodes/components/InputSlot.vue @@ -10,14 +10,13 @@ /> -
+
{{ slotData.localized_name || slotData.name || `Input ${index}` }} -
@@ -37,7 +36,6 @@ import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composabl import { useExecutionStore } from '@/stores/executionStore' import { cn } from '@/utils/tailwindUtil' -import LODFallback from './LODFallback.vue' import SlotConnectionDot from './SlotConnectionDot.vue' interface InputSlotProps { diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 28aa3ac118..109d0c5629 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -99,18 +99,14 @@ /> -
- - -
@@ -121,17 +117,14 @@
- - + +
@@ -175,7 +168,6 @@ import { } from '@/utils/graphTraversalUtil' import { cn } from '@/utils/tailwindUtil' -import type { ResizeHandleDirection } from '../interactions/resize/resizeMath' import { useNodeResize } from '../interactions/resize/useNodeResize' import LivePreview from './LivePreview.vue' import NodeContent from './NodeContent.vue' @@ -267,7 +259,7 @@ onErrorCaptured((error) => { return false // Prevent error propagation }) -const { position, size, zIndex, moveNodeTo } = useNodeLayout(() => nodeData.id) +const { position, size, zIndex } = useNodeLayout(() => nodeData.id) const { pointerHandlers } = useNodePointerInteractions(() => nodeData.id) const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers const { startDrag } = useNodeDrag() @@ -318,41 +310,6 @@ onMounted(() => { const baseResizeHandleClasses = 'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40' -const POSITION_EPSILON = 0.01 - -type CornerResizeHandle = { - id: string - direction: ResizeHandleDirection - classes: string - ariaLabel: string -} - -const cornerResizeHandles: CornerResizeHandle[] = [ - { - id: 'se', - direction: { horizontal: 'right', vertical: 'bottom' }, - classes: 'right-0 bottom-0 cursor-se-resize', - ariaLabel: t('g.resizeFromBottomRight') - }, - { - id: 'ne', - direction: { horizontal: 'right', vertical: 'top' }, - classes: 'right-0 top-0 cursor-ne-resize', - ariaLabel: t('g.resizeFromTopRight') - }, - { - id: 'sw', - direction: { horizontal: 'left', vertical: 'bottom' }, - classes: 'left-0 bottom-0 cursor-sw-resize', - ariaLabel: t('g.resizeFromBottomLeft') - }, - { - id: 'nw', - direction: { horizontal: 'left', vertical: 'top' }, - classes: 'left-0 top-0 cursor-nw-resize', - ariaLabel: t('g.resizeFromTopLeft') - } -] const MIN_NODE_WIDTH = 225 @@ -365,22 +322,11 @@ const { startResize } = useNodeResize((result, element) => { // Apply size directly to DOM element - ResizeObserver will pick this up element.style.setProperty('--node-width', `${clampedWidth}px`) element.style.setProperty('--node-height', `${result.size.height}px`) - - const currentPosition = position.value - const deltaX = Math.abs(result.position.x - currentPosition.x) - const deltaY = Math.abs(result.position.y - currentPosition.y) - - if (deltaX > POSITION_EPSILON || deltaY > POSITION_EPSILON) { - moveNodeTo(result.position) - } }) -const handleResizePointerDown = (direction: ResizeHandleDirection) => { - return (event: PointerEvent) => { - if (nodeData.flags?.pinned) return - - startResize(event, direction, { ...position.value }) - } +const handleResizePointerDown = (event: PointerEvent) => { + if (nodeData.flags?.pinned) return + startResize(event) } watch(isCollapsed, (collapsed) => { diff --git a/src/renderer/extensions/vueNodes/components/LODFallback.vue b/src/renderer/extensions/vueNodes/components/LODFallback.vue deleted file mode 100644 index fcf7ae7a0d..0000000000 --- a/src/renderer/extensions/vueNodes/components/LODFallback.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue index b7f4d9ab78..bb1273e81c 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue @@ -18,7 +18,7 @@
-
+
@@ -57,10 +57,9 @@ />
-
-
+
{{ slotData.localized_name || slotData.name || `Output ${index}` }} -
@@ -91,7 +90,6 @@ import { t } from '@/i18n' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { useToastStore } from '@/platform/updates/common/toastStore' -import LODFallback from '@/renderer/extensions/vueNodes/components/LODFallback.vue' import { app } from '@/scripts/app' import { useAudioService } from '@/services/audioService' import type { SimplifiedWidget } from '@/types/simplifiedWidget' diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue index 1a8c2bdb5b..df3381f534 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue @@ -3,6 +3,7 @@