-
Notifications
You must be signed in to change notification settings - Fork 433
Webcam capture Node #6795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Webcam capture Node #6795
Changes from 22 commits
6f4c335
74aadb3
450b8b9
0ac7687
7fc51c6
a65b063
e1693dc
a455c7b
48c21f6
ca335ba
16b6b5b
59761eb
f9b7e51
ad85956
dc60b54
cfbb4c7
4b62872
68b6159
cd5f6fd
af8ad95
f8ede78
335b72b
f7f0c05
7b109df
8031a83
51ab154
e092986
1e384c6
1c37a36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,16 @@ export interface GraphNodeManager { | |
| // Access to original LiteGraph nodes (non-reactive) | ||
| getNode(id: string): LGraphNode | undefined | ||
|
|
||
| // Update widget options (e.g., hidden, disabled) - triggers Vue reactivity | ||
| updateVueWidgetOptions( | ||
| nodeId: string, | ||
| widgetName: string, | ||
| options: Record<string, unknown> | ||
| ): void | ||
|
|
||
| // Refresh Vue widgets from LiteGraph node - use after modifying node.widgets | ||
| refreshVueWidgets(nodeId: string): void | ||
|
|
||
| // Lifecycle methods | ||
| cleanup(): void | ||
| } | ||
|
|
@@ -298,6 +308,67 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Updates Vue state when widget options change (e.g., hidden, disabled) | ||
| */ | ||
| const updateVueWidgetOptions = ( | ||
| nodeId: string, | ||
| widgetName: string, | ||
| options: Record<string, unknown> | ||
| ): void => { | ||
| try { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need a try catch here? |
||
| const currentData = vueNodeData.get(nodeId) | ||
| if (!currentData?.widgets) return | ||
|
|
||
| const updatedWidgets = currentData.widgets.map((w) => | ||
| w.name === widgetName | ||
| ? { ...w, options: { ...w.options, ...options } } | ||
| : w | ||
| ) | ||
| // Create a completely new object to ensure Vue reactivity triggers | ||
| const updatedData = { | ||
| ...currentData, | ||
| widgets: updatedWidgets | ||
| } | ||
|
|
||
| vueNodeData.set(nodeId, updatedData) | ||
| } catch (error) { | ||
| // Ignore widget update errors to prevent cascade failures | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Refreshes Vue widget state from LiteGraph node widgets. | ||
| * Use this after directly modifying node.widgets to sync Vue state. | ||
| */ | ||
| const refreshVueWidgets = (nodeId: string): void => { | ||
| try { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here |
||
| const node = nodeRefs.get(nodeId) | ||
| const currentData = vueNodeData.get(nodeId) | ||
| if (!node || !currentData) return | ||
|
|
||
| // Re-extract widgets from node | ||
| const slotMetadata = new Map<string, WidgetSlotMetadata>() | ||
| node.inputs?.forEach((input, index) => { | ||
| if (!input?.widget?.name) return | ||
| slotMetadata.set(input.widget.name, { | ||
| index, | ||
| linked: input.link != null | ||
| }) | ||
| }) | ||
|
|
||
| const freshWidgets = | ||
| node.widgets?.map(safeWidgetMapper(node, slotMetadata)) ?? [] | ||
|
|
||
| vueNodeData.set(nodeId, { | ||
| ...currentData, | ||
| widgets: freshWidgets | ||
| }) | ||
| } catch (error) { | ||
| // Ignore refresh errors | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates a wrapped callback for a widget that maintains LiteGraph/Vue sync | ||
| */ | ||
|
|
@@ -624,6 +695,8 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { | |
| return { | ||
| vueNodeData, | ||
| getNode, | ||
| updateVueWidgetOptions, | ||
| refreshVueWidgets, | ||
| cleanup | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| <script setup lang="ts"> | ||
| import { computed } from 'vue' | ||
| import type { SimplifiedWidget } from '@/types/simplifiedWidget' | ||
| import { cn } from '@/utils/tailwindUtil' | ||
| import { | ||
| STANDARD_EXCLUDED_PROPS, | ||
| filterWidgetProps | ||
| } from '@/utils/widgetPropFilter' | ||
| import { WidgetInputBaseClass } from './layout' | ||
| import WidgetLayoutField from './layout/WidgetLayoutField.vue' | ||
| const props = defineProps<{ | ||
| widget: SimplifiedWidget<string | number | boolean> | ||
| }>() | ||
| const modelValue = defineModel<string | number | boolean>({ required: true }) | ||
| const filteredProps = computed(() => | ||
| filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS) | ||
| ) | ||
| interface ToggleOption { | ||
| label: string | ||
| value: string | number | boolean | ||
| } | ||
| const options = computed<ToggleOption[]>(() => { | ||
| // Get options from widget spec or widget options | ||
| const widgetOptions = props.widget.options?.values || props.widget.spec?.[0] | ||
| if (Array.isArray(widgetOptions)) { | ||
| // If options are strings/numbers, convert to {label, value} format | ||
| return widgetOptions.map((opt) => { | ||
| if ( | ||
| typeof opt === 'object' && | ||
| opt !== null && | ||
| 'label' in opt && | ||
| 'value' in opt | ||
| ) { | ||
| return opt as ToggleOption | ||
| } | ||
| return { label: String(opt), value: opt } | ||
| }) | ||
| } | ||
| // Default options for boolean widgets | ||
| if (typeof modelValue.value === 'boolean') { | ||
| return [ | ||
| { label: 'On', value: true }, | ||
| { label: 'Off', value: false } | ||
| ] | ||
| } | ||
| // Fallback default options | ||
| return [ | ||
| { label: 'Yes', value: true }, | ||
| { label: 'No', value: false } | ||
| ] | ||
| }) | ||
Myestery marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| function handleSelect(value: string | number | boolean) { | ||
| modelValue.value = value | ||
| } | ||
| </script> | ||
|
|
||
| <template> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: seems we declare before <script> in most of the codebase... (why I have no idea) maybe switch it to preserve expectations. |
||
| <WidgetLayoutField :widget> | ||
| <div | ||
| v-bind="filteredProps" | ||
| :class="cn(WidgetInputBaseClass, 'flex gap-0.5 p-0.5 w-full')" | ||
| role="group" | ||
| :aria-label="widget.name" | ||
| > | ||
| <button | ||
| v-for="option in options" | ||
| :key="String(option.value)" | ||
| type="button" | ||
| :class=" | ||
| cn( | ||
| 'flex-1 px-2 py-1 text-xs font-medium rounded transition-all duration-150', | ||
| 'bg-transparent border-none', | ||
| 'focus:outline-none', | ||
| modelValue === option.value | ||
| ? 'bg-interface-menu-component-surface-selected text-primary' | ||
| : 'text-secondary hover:bg-interface-menu-component-surface-hovered' | ||
| ) | ||
| " | ||
| :aria-pressed="modelValue === option.value" | ||
| @click="handleSelect(option.value)" | ||
Myestery marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| > | ||
| {{ option.label }} | ||
| </button> | ||
| </div> | ||
| </WidgetLayoutField> | ||
| </template> | ||
Uh oh!
There was an error while loading. Please reload this page.