mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
fix: promote DOM widget textareas to subgraph node via adapter
Replace ProxyWidget with PromotedWidgetSlot and add PromotedDomWidgetAdapter to wrap interior DOM widgets for display on the SubgraphNode. The adapter overrides `node` and `y` so DomWidgets.vue positions the textarea on the parent graph. Fix litegraph:set-graph handler in app.ts that was setting adapter widget states to active=false because adapters are not in any node's widgets array. Now checks if the widget's host node is in the new graph before deactivating. Export NodeId from layout/types.ts and fix NodeId-to-string conversions at Yjs and Map boundaries across the layout system. Amp-Thread-ID: https://ampcode.com/threads/T-019c547f-15bf-716a-8abc-278dc9106c16 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
195
src/core/graph/subgraph/PromotedDomWidgetAdapter.ts
Normal file
195
src/core/graph/subgraph/PromotedDomWidgetAdapter.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type {
|
||||
BaseDOMWidget,
|
||||
ComponentWidget,
|
||||
DOMWidgetOptions
|
||||
} from '@/scripts/domWidget'
|
||||
import { isDOMWidget, isComponentWidget } from '@/scripts/domWidget'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
import type { PromotedWidgetSlot } from './PromotedWidgetSlot'
|
||||
|
||||
/**
|
||||
* Adapts an interior DOM widget for display on a SubgraphNode.
|
||||
*
|
||||
* When a DOM widget is promoted to a subgraph node, `DomWidgets.vue` positions
|
||||
* it using `widget.node.pos` and `widget.y`. This adapter overrides those to
|
||||
* reference the SubgraphNode (host) and the PromotedWidgetSlot's y position,
|
||||
* so the DOM element renders at the correct location on the parent graph.
|
||||
*
|
||||
* Only ONE of {adapter, interior widget} should be registered in
|
||||
* `domWidgetStore` at a time. The adapter is registered on creation and the
|
||||
* interior widget is deactivated. On dispose the interior is reactivated.
|
||||
* `DomWidgets.vue` therefore only ever sees a single `DomWidget.vue` instance
|
||||
* per shared `HTMLElement`, avoiding the "element‑theft" race condition that
|
||||
* occurs when two instances try to `appendChild` the same element.
|
||||
*/
|
||||
export class PromotedDomWidgetAdapter<
|
||||
V extends object | string
|
||||
> implements BaseDOMWidget<V> {
|
||||
// IBaseWidget requires a symbol index signature for Vue reactivity tracking.
|
||||
[symbol: symbol]: boolean
|
||||
readonly id = generateUUID()
|
||||
private readonly inner: BaseDOMWidget<V>
|
||||
private readonly hostNode: LGraphNode
|
||||
private readonly slot: PromotedWidgetSlot
|
||||
|
||||
constructor(
|
||||
inner: BaseDOMWidget<V>,
|
||||
hostNode: LGraphNode,
|
||||
slot: PromotedWidgetSlot
|
||||
) {
|
||||
this.inner = inner
|
||||
this.hostNode = hostNode
|
||||
this.slot = slot
|
||||
}
|
||||
|
||||
get node(): LGraphNode {
|
||||
return this.hostNode
|
||||
}
|
||||
|
||||
get y(): number {
|
||||
return this.slot.y
|
||||
}
|
||||
|
||||
set y(_v: number) {
|
||||
// Position is managed by the slot; ignore external writes.
|
||||
}
|
||||
|
||||
get last_y(): number | undefined {
|
||||
return this.slot.last_y
|
||||
}
|
||||
|
||||
set last_y(_v: number | undefined) {
|
||||
// Managed by the slot.
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.inner.name
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return this.inner.type
|
||||
}
|
||||
|
||||
get options(): DOMWidgetOptions<V> {
|
||||
return this.inner.options
|
||||
}
|
||||
|
||||
get value(): V {
|
||||
return this.inner.value
|
||||
}
|
||||
|
||||
set value(v: V) {
|
||||
this.inner.value = v
|
||||
}
|
||||
|
||||
get promoted(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
get margin(): number {
|
||||
return this.inner.margin
|
||||
}
|
||||
|
||||
get width(): number | undefined {
|
||||
return (this.inner as IBaseWidget).width
|
||||
}
|
||||
|
||||
get computedHeight(): number | undefined {
|
||||
return this.slot.computedHeight
|
||||
}
|
||||
|
||||
set computedHeight(v: number | undefined) {
|
||||
this.slot.computedHeight = v
|
||||
}
|
||||
|
||||
get computedDisabled(): boolean | undefined {
|
||||
return (this.inner as IBaseWidget).computedDisabled
|
||||
}
|
||||
|
||||
get hidden(): boolean | undefined {
|
||||
return (this.inner as IBaseWidget).hidden
|
||||
}
|
||||
|
||||
get serialize(): boolean | undefined {
|
||||
return false
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return (
|
||||
!this.hidden &&
|
||||
this.hostNode.isWidgetVisible(this as unknown as IBaseWidget)
|
||||
)
|
||||
}
|
||||
|
||||
get callback(): BaseDOMWidget<V>['callback'] {
|
||||
return this.inner.callback
|
||||
}
|
||||
|
||||
set callback(v: BaseDOMWidget<V>['callback']) {
|
||||
this.inner.callback = v
|
||||
}
|
||||
|
||||
/** The interior DOM widget this adapter wraps. */
|
||||
get innerWidget(): BaseDOMWidget<V> {
|
||||
return this.inner
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the `element` property so `DomWidget.vue` can access it via
|
||||
* the `isDOMWidget` type guard.
|
||||
*/
|
||||
Object.defineProperty(PromotedDomWidgetAdapter.prototype, 'element', {
|
||||
get(this: PromotedDomWidgetAdapter<object | string>) {
|
||||
const inner = this.innerWidget
|
||||
if (isDOMWidget(inner)) return inner.element
|
||||
return undefined
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
})
|
||||
|
||||
/**
|
||||
* Expose the `component` property so `DomWidget.vue` can access it via
|
||||
* the `isComponentWidget` type guard.
|
||||
*/
|
||||
Object.defineProperty(PromotedDomWidgetAdapter.prototype, 'component', {
|
||||
get(this: PromotedDomWidgetAdapter<object | string>) {
|
||||
const inner = this.innerWidget
|
||||
if (isComponentWidget(inner)) return inner.component
|
||||
return undefined
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
})
|
||||
|
||||
/**
|
||||
* Expose the `inputSpec` property for component widgets.
|
||||
*/
|
||||
Object.defineProperty(PromotedDomWidgetAdapter.prototype, 'inputSpec', {
|
||||
get(this: PromotedDomWidgetAdapter<object | string>) {
|
||||
const inner = this.innerWidget
|
||||
if (isComponentWidget(inner))
|
||||
return (inner as ComponentWidget<object | string>).inputSpec
|
||||
return undefined
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
})
|
||||
|
||||
/**
|
||||
* Expose the `props` property for component widgets.
|
||||
*/
|
||||
Object.defineProperty(PromotedDomWidgetAdapter.prototype, 'props', {
|
||||
get(this: PromotedDomWidgetAdapter<object | string>) {
|
||||
const inner = this.innerWidget
|
||||
if (isComponentWidget(inner))
|
||||
return (inner as ComponentWidget<object | string>).props
|
||||
return undefined
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
})
|
||||
@@ -8,11 +8,16 @@ import type {
|
||||
} from '@/lib/litegraph/src/widgets/BaseWidget'
|
||||
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
|
||||
import { toConcreteWidget } from '@/lib/litegraph/src/widgets/widgetMap'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
import { isDOMWidget, isComponentWidget } from '@/scripts/domWidget'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import {
|
||||
stripGraphPrefix,
|
||||
useWidgetValueStore
|
||||
} from '@/stores/widgetValueStore'
|
||||
|
||||
import { PromotedDomWidgetAdapter } from './PromotedDomWidgetAdapter'
|
||||
|
||||
type WidgetValue = IBaseWidget['value']
|
||||
|
||||
/**
|
||||
@@ -35,6 +40,13 @@ export class PromotedWidgetSlot
|
||||
readonly sourceWidgetName: string
|
||||
private readonly subgraphNode: SubgraphNode
|
||||
|
||||
/**
|
||||
* When the interior widget is a DOM widget, this adapter is registered in
|
||||
* `domWidgetStore` so that `DomWidgets.vue` positions the DOM element on the
|
||||
* SubgraphNode rather than the interior node.
|
||||
*/
|
||||
private domAdapter?: PromotedDomWidgetAdapter<object | string>
|
||||
|
||||
constructor(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNodeId: NodeId,
|
||||
@@ -75,6 +87,46 @@ export class PromotedWidgetSlot
|
||||
if (!resolved) return
|
||||
resolved.widget.callback?.(value, canvas, resolved.node, pos, e)
|
||||
}
|
||||
|
||||
this.syncDomAdapter()
|
||||
this.syncLayoutSize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to the interior widget's `computeLayoutSize` so that
|
||||
* `_arrangeWidgets` treats this slot as a growable widget (e.g. textarea)
|
||||
* and allocates the correct height on the SubgraphNode.
|
||||
*
|
||||
* Assigned dynamically in the constructor via `syncLayoutSize` because
|
||||
* `computeLayoutSize` is an optional method on the base class — it must
|
||||
* either exist or not exist, not return `undefined`.
|
||||
*/
|
||||
declare computeLayoutSize?: (node: LGraphNode) => {
|
||||
minHeight: number
|
||||
maxHeight?: number
|
||||
minWidth: number
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies `computeLayoutSize` from the interior widget when it has one
|
||||
* (e.g. textarea / DOM widgets), so `_arrangeWidgets` allocates the
|
||||
* correct growable height on the SubgraphNode.
|
||||
*/
|
||||
private syncLayoutSize(): void {
|
||||
let resolved: ReturnType<PromotedWidgetSlot['resolve']>
|
||||
try {
|
||||
resolved = this.resolve()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const interiorWidget = resolved?.widget
|
||||
if (interiorWidget?.computeLayoutSize) {
|
||||
this.computeLayoutSize = (node) => interiorWidget.computeLayoutSize!(node)
|
||||
} else {
|
||||
this.computeLayoutSize = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private resolve(): {
|
||||
@@ -164,7 +216,77 @@ export class PromotedWidgetSlot
|
||||
return v != null ? String(v) : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or removes the DOM adapter based on whether the resolved interior
|
||||
* widget is a DOM widget. Call after construction and whenever the interior
|
||||
* widget might change (e.g. reconnection).
|
||||
*
|
||||
* Only one of {adapter, interior widget} is active in `domWidgetStore` at a
|
||||
* time. The adapter is registered and the interior is deactivated, so
|
||||
* `DomWidgets.vue` never mounts two `DomWidget.vue` instances for the same
|
||||
* `HTMLElement`.
|
||||
*/
|
||||
syncDomAdapter(): void {
|
||||
// resolve() may fail during construction if the subgraph is not yet
|
||||
// fully wired (e.g. in tests or during deserialization).
|
||||
let resolved: ReturnType<PromotedWidgetSlot['resolve']>
|
||||
try {
|
||||
resolved = this.resolve()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!resolved) return
|
||||
|
||||
const interiorWidget = resolved.widget
|
||||
|
||||
const isDom =
|
||||
isDOMWidget(interiorWidget) || isComponentWidget(interiorWidget)
|
||||
|
||||
if (isDom && !this.domAdapter) {
|
||||
const domWidget = interiorWidget as BaseDOMWidget<object | string>
|
||||
const adapter = new PromotedDomWidgetAdapter(
|
||||
domWidget,
|
||||
this.subgraphNode,
|
||||
this
|
||||
)
|
||||
this.domAdapter = adapter
|
||||
|
||||
const store = useDomWidgetStore()
|
||||
// The adapter satisfies BaseDOMWidget but TypeScript cannot verify
|
||||
// the IBaseWidget symbol index signature on plain classes.
|
||||
// Start invisible — `updateWidgets()` will set `visible: true` on the
|
||||
// first canvas draw when the SubgraphNode is in the current graph.
|
||||
// This prevents a race where both adapter and interior DomWidget.vue
|
||||
// instances try to mount the same HTMLElement during `onMounted`.
|
||||
store.registerWidget(
|
||||
adapter as unknown as BaseDOMWidget<object | string>,
|
||||
{ visible: false }
|
||||
)
|
||||
} else if (!isDom && this.domAdapter) {
|
||||
this.disposeDomAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the DOM adapter from the store.
|
||||
*/
|
||||
disposeDomAdapter(): void {
|
||||
if (!this.domAdapter) return
|
||||
|
||||
useDomWidgetStore().unregisterWidget(this.domAdapter.id)
|
||||
this.domAdapter = undefined
|
||||
}
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
// Lazily create the DOM adapter if it wasn't ready at construction time.
|
||||
// During deserialization the interior widget may not exist yet when the
|
||||
// PromotedWidgetSlot constructor runs, so syncDomAdapter() is retried here
|
||||
// on every draw until it succeeds.
|
||||
if (!this.domAdapter) {
|
||||
this.syncDomAdapter()
|
||||
this.syncLayoutSize()
|
||||
}
|
||||
|
||||
const resolved = this.resolve()
|
||||
if (!resolved) {
|
||||
this.drawDisconnectedPlaceholder(ctx, options)
|
||||
|
||||
@@ -64,7 +64,11 @@ function syncPromotedWidgets(
|
||||
)
|
||||
|
||||
// Remove existing PromotedWidgetSlot instances and native widgets
|
||||
// that will be re-ordered by the parsed list
|
||||
// that will be re-ordered by the parsed list.
|
||||
// Dispose DOM adapters on slots being removed.
|
||||
for (const w of widgets) {
|
||||
if (w instanceof PromotedWidgetSlot) w.disposeDomAdapter()
|
||||
}
|
||||
node.widgets = widgets.filter((w) => {
|
||||
if (w instanceof PromotedWidgetSlot) return false
|
||||
return !parsed.some(([, name]) => w.name === name)
|
||||
|
||||
@@ -234,14 +234,14 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
return {
|
||||
get: () => {
|
||||
track()
|
||||
const ynode = this.ynodes.get(nodeId)
|
||||
const ynode = this.ynodes.get(String(nodeId))
|
||||
const layout = ynode ? yNodeToLayout(ynode) : null
|
||||
return layout
|
||||
},
|
||||
set: (newLayout: NodeLayout | null) => {
|
||||
if (newLayout === null) {
|
||||
// Delete operation
|
||||
const existing = this.ynodes.get(nodeId)
|
||||
const existing = this.ynodes.get(String(nodeId))
|
||||
if (existing) {
|
||||
this.applyOperation({
|
||||
type: 'deleteNode',
|
||||
@@ -255,7 +255,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
} else {
|
||||
// Update operation - detect what changed
|
||||
const existing = this.ynodes.get(nodeId)
|
||||
const existing = this.ynodes.get(String(nodeId))
|
||||
if (!existing) {
|
||||
// Create operation
|
||||
this.applyOperation({
|
||||
@@ -685,7 +685,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
|
||||
// Precise hit test only on candidates
|
||||
for (const key of candidateKeys) {
|
||||
const segmentLayout = this.linkSegmentLayouts.get(key)
|
||||
const segmentLayout = this.linkSegmentLayouts.get(String(key))
|
||||
if (!segmentLayout) continue
|
||||
|
||||
if (ctx && segmentLayout.path) {
|
||||
@@ -748,7 +748,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
|
||||
// Check precise bounds for candidates
|
||||
for (const key of candidateSlotKeys) {
|
||||
const slotLayout = this.slotLayouts.get(key)
|
||||
const slotLayout = this.slotLayouts.get(String(key))
|
||||
if (slotLayout && pointInBounds(point, slotLayout.bounds)) {
|
||||
return slotLayout
|
||||
}
|
||||
@@ -812,7 +812,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
const segmentKeys = this.linkSegmentSpatialIndex.query(bounds)
|
||||
const linkIds = new Set<LinkId>()
|
||||
for (const key of segmentKeys) {
|
||||
const segment = this.linkSegmentLayouts.get(key)
|
||||
const segment = this.linkSegmentLayouts.get(String(key))
|
||||
if (segment) {
|
||||
linkIds.add(segment.linkId)
|
||||
}
|
||||
@@ -821,7 +821,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
return {
|
||||
nodes: this.queryNodesInBounds(bounds),
|
||||
links: Array.from(linkIds),
|
||||
slots: this.slotSpatialIndex.query(bounds),
|
||||
slots: this.slotSpatialIndex.query(bounds).map(String),
|
||||
reroutes: this.rerouteSpatialIndex
|
||||
.query(bounds)
|
||||
.map((key) => asRerouteId(key))
|
||||
@@ -1002,7 +1002,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
}
|
||||
|
||||
this.ynodes.set(layout.id, layoutToYNode(layout))
|
||||
this.ynodes.set(String(layout.id), layoutToYNode(layout))
|
||||
|
||||
// Add to spatial index
|
||||
this.spatialIndex.insert(layout.id, layout.bounds)
|
||||
@@ -1018,7 +1018,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
operation: MoveNodeOperation,
|
||||
change: LayoutChange
|
||||
): void {
|
||||
const ynode = this.ynodes.get(operation.nodeId)
|
||||
const ynode = this.ynodes.get(String(operation.nodeId))
|
||||
if (!ynode) {
|
||||
return
|
||||
}
|
||||
@@ -1046,7 +1046,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
operation: ResizeNodeOperation,
|
||||
change: LayoutChange
|
||||
): void {
|
||||
const ynode = this.ynodes.get(operation.nodeId)
|
||||
const ynode = this.ynodes.get(String(operation.nodeId))
|
||||
if (!ynode) return
|
||||
|
||||
const position = yNodeToLayout(ynode).position
|
||||
@@ -1072,7 +1072,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
operation: SetNodeZIndexOperation,
|
||||
change: LayoutChange
|
||||
): void {
|
||||
const ynode = this.ynodes.get(operation.nodeId)
|
||||
const ynode = this.ynodes.get(String(operation.nodeId))
|
||||
if (!ynode) return
|
||||
|
||||
ynode.set('zIndex', operation.zIndex)
|
||||
@@ -1084,7 +1084,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
change: LayoutChange
|
||||
): void {
|
||||
const ynode = layoutToYNode(operation.layout)
|
||||
this.ynodes.set(operation.nodeId, ynode)
|
||||
this.ynodes.set(String(operation.nodeId), ynode)
|
||||
|
||||
// Add to spatial index
|
||||
this.spatialIndex.insert(operation.nodeId, operation.layout.bounds)
|
||||
@@ -1097,9 +1097,9 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
operation: DeleteNodeOperation,
|
||||
change: LayoutChange
|
||||
): void {
|
||||
if (!this.ynodes.has(operation.nodeId)) return
|
||||
if (!this.ynodes.has(String(operation.nodeId))) return
|
||||
|
||||
this.ynodes.delete(operation.nodeId)
|
||||
this.ynodes.delete(String(operation.nodeId))
|
||||
// Note: We intentionally do NOT delete nodeRefs and nodeTriggers here.
|
||||
// During undo/redo, Vue components may still hold references to the old ref.
|
||||
// If we delete the trigger, Vue won't be notified when the node is re-created.
|
||||
@@ -1132,7 +1132,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
|
||||
for (const nodeId of operation.nodeIds) {
|
||||
const data = operation.bounds[nodeId]
|
||||
const ynode = this.ynodes.get(nodeId)
|
||||
const ynode = this.ynodes.get(String(nodeId))
|
||||
if (!ynode || !data) continue
|
||||
|
||||
ynode.set('position', { x: data.bounds.x, y: data.bounds.y })
|
||||
@@ -1440,7 +1440,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
const boundsRecord: BatchUpdateBoundsOperation['bounds'] = {}
|
||||
|
||||
for (const { nodeId, bounds } of updates) {
|
||||
const ynode = this.ynodes.get(nodeId)
|
||||
const ynode = this.ynodes.get(String(nodeId))
|
||||
if (!ynode) continue
|
||||
const currentLayout = yNodeToLayout(ynode)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useLayoutSync() {
|
||||
const layout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||
if (!layout) continue
|
||||
|
||||
const liteNode = canvas.graph?.getNodeById(parseInt(nodeId))
|
||||
const liteNode = canvas.graph?.getNodeById(nodeId)
|
||||
if (!liteNode) continue
|
||||
|
||||
if (
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
|
||||
export type { NodeId }
|
||||
|
||||
// Enum for layout source types
|
||||
export enum LayoutSource {
|
||||
Canvas = 'canvas',
|
||||
|
||||
@@ -43,7 +43,7 @@ export class SpatialIndexManager {
|
||||
* Insert a node into the spatial index
|
||||
*/
|
||||
insert(nodeId: NodeId, bounds: Bounds): void {
|
||||
this.quadTree.insert(nodeId, bounds, nodeId)
|
||||
this.quadTree.insert(String(nodeId), bounds, nodeId)
|
||||
this.invalidateCache()
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export class SpatialIndexManager {
|
||||
* Update a node's bounds in the spatial index
|
||||
*/
|
||||
update(nodeId: NodeId, bounds: Bounds): void {
|
||||
this.quadTree.update(nodeId, bounds)
|
||||
this.quadTree.update(String(nodeId), bounds)
|
||||
this.invalidateCache()
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class SpatialIndexManager {
|
||||
*/
|
||||
batchUpdate(updates: Array<{ nodeId: NodeId; bounds: Bounds }>): void {
|
||||
for (const { nodeId, bounds } of updates) {
|
||||
this.quadTree.update(nodeId, bounds)
|
||||
this.quadTree.update(String(nodeId), bounds)
|
||||
}
|
||||
this.invalidateCache()
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export class SpatialIndexManager {
|
||||
* Remove a node from the spatial index
|
||||
*/
|
||||
remove(nodeId: NodeId): void {
|
||||
this.quadTree.remove(nodeId)
|
||||
this.quadTree.remove(String(nodeId))
|
||||
this.invalidateCache()
|
||||
}
|
||||
|
||||
|
||||
@@ -283,11 +283,11 @@ const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
|
||||
useNodeEventHandlers()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
|
||||
useVueElementTracking(() => nodeData.id, 'node')
|
||||
useVueElementTracking(() => String(nodeData.id), 'node')
|
||||
|
||||
const { selectedNodeIds } = storeToRefs(useCanvasStore())
|
||||
const isSelected = computed(() => {
|
||||
return selectedNodeIds.value.has(nodeData.id)
|
||||
return selectedNodeIds.value.has(String(nodeData.id))
|
||||
})
|
||||
|
||||
const nodeLocatorId = computed(() => getLocatorIdFromNodeData(nodeData))
|
||||
@@ -343,8 +343,10 @@ onErrorCaptured((error) => {
|
||||
return false // Prevent error propagation
|
||||
})
|
||||
|
||||
const { position, size, zIndex } = useNodeLayout(() => nodeData.id)
|
||||
const { pointerHandlers } = useNodePointerInteractions(() => nodeData.id)
|
||||
const { position, size, zIndex } = useNodeLayout(() => String(nodeData.id))
|
||||
const { pointerHandlers } = useNodePointerInteractions(() =>
|
||||
String(nodeData.id)
|
||||
)
|
||||
const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers
|
||||
const { startDrag } = useNodeDrag()
|
||||
const badges = usePartitionedBadges(nodeData)
|
||||
@@ -487,7 +489,7 @@ const hasCustomContent = computed(() => {
|
||||
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
|
||||
|
||||
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
() => nodeData.id,
|
||||
() => String(nodeData.id),
|
||||
{
|
||||
isCollapsed
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ function useNodeEventHandlersIndividual() {
|
||||
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
const node = nodeManager.value.getNode(String(nodeId))
|
||||
if (!node) return
|
||||
|
||||
const multiSelect = isMultiSelectKey(event)
|
||||
@@ -69,7 +69,7 @@ function useNodeEventHandlersIndividual() {
|
||||
|
||||
if (!nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
const node = nodeManager.value.getNode(String(nodeId))
|
||||
if (!node) return
|
||||
|
||||
// Use LiteGraph's collapse method if the state needs to change
|
||||
@@ -88,7 +88,7 @@ function useNodeEventHandlersIndividual() {
|
||||
|
||||
if (!nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
const node = nodeManager.value.getNode(String(nodeId))
|
||||
if (!node) return
|
||||
|
||||
// Update the node title in LiteGraph for persistence
|
||||
@@ -104,7 +104,7 @@ function useNodeEventHandlersIndividual() {
|
||||
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
const node = nodeManager.value.getNode(String(nodeId))
|
||||
if (!node) return
|
||||
|
||||
// Prevent default context menu
|
||||
@@ -127,7 +127,7 @@ function useNodeEventHandlersIndividual() {
|
||||
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
const node = nodeManager.value.getNode(String(nodeId))
|
||||
if (!node) return
|
||||
|
||||
if (!multiSelect) {
|
||||
|
||||
@@ -355,7 +355,7 @@ export function useSlotLinkInteraction({
|
||||
|
||||
if (slotCandidate) {
|
||||
const key = getSlotKey(
|
||||
slotCandidate.layout.nodeId,
|
||||
String(slotCandidate.layout.nodeId),
|
||||
slotCandidate.layout.index,
|
||||
slotCandidate.layout.type === 'input'
|
||||
)
|
||||
@@ -363,7 +363,7 @@ export function useSlotLinkInteraction({
|
||||
}
|
||||
if (nodeCandidate && !slotCandidate?.compatible) {
|
||||
const key = getSlotKey(
|
||||
nodeCandidate.layout.nodeId,
|
||||
String(nodeCandidate.layout.nodeId),
|
||||
nodeCandidate.layout.index,
|
||||
nodeCandidate.layout.type === 'input'
|
||||
)
|
||||
@@ -374,7 +374,7 @@ export function useSlotLinkInteraction({
|
||||
const newCandidate = candidate?.compatible ? candidate : null
|
||||
const newCandidateKey = newCandidate
|
||||
? getSlotKey(
|
||||
newCandidate.layout.nodeId,
|
||||
String(newCandidate.layout.nodeId),
|
||||
newCandidate.layout.index,
|
||||
newCandidate.layout.type === 'input'
|
||||
)
|
||||
|
||||
@@ -58,7 +58,7 @@ function useNodeDragIndividual() {
|
||||
|
||||
// capture the starting positions of all other selected nodes
|
||||
// Only move other selected items if the dragged node is part of the selection
|
||||
const isDraggedNodeInSelection = selectedNodes?.has(nodeId)
|
||||
const isDraggedNodeInSelection = selectedNodes?.has(String(nodeId))
|
||||
|
||||
if (isDraggedNodeInSelection && selectedNodes.size > 1) {
|
||||
otherSelectedNodesStartPositions = new Map()
|
||||
|
||||
@@ -849,6 +849,8 @@ export class ComfyApp {
|
||||
.map((w) => [w.id, w])
|
||||
)
|
||||
|
||||
const newGraphNodeSet = new Set(newGraph.nodes)
|
||||
|
||||
for (const [
|
||||
widgetId,
|
||||
widgetState
|
||||
@@ -856,6 +858,10 @@ export class ComfyApp {
|
||||
if (widgetId in activeWidgets) {
|
||||
widgetState.active = true
|
||||
widgetState.widget = activeWidgets[widgetId]
|
||||
} else if (newGraphNodeSet.has(widgetState.widget.node)) {
|
||||
// Adapter widgets (e.g. PromotedDomWidgetAdapter) are not in any
|
||||
// node's widgets array but their host node IS in the graph.
|
||||
widgetState.active = true
|
||||
} else {
|
||||
widgetState.active = false
|
||||
}
|
||||
|
||||
@@ -30,11 +30,12 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
||||
|
||||
// Register a widget with the store
|
||||
const registerWidget = <V extends object | string>(
|
||||
widget: BaseDOMWidget<V>
|
||||
widget: BaseDOMWidget<V>,
|
||||
options?: { visible?: boolean }
|
||||
) => {
|
||||
widgetStates.value.set(widget.id, {
|
||||
widget: markRaw(widget) as unknown as Raw<BaseDOMWidget<object | string>>,
|
||||
visible: true,
|
||||
visible: options?.visible ?? true,
|
||||
readonly: false,
|
||||
zIndex: 0,
|
||||
pos: [0, 0],
|
||||
|
||||
Reference in New Issue
Block a user