File size: 5,627 Bytes
7312c42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | import { createRoot, type Root } from 'react-dom/client'
import type { ComfyApp } from '@comfyorg/comfyui-frontend-types'
import { SamplingPreviewWidget } from '@/components/widgets/SamplingPreviewWidget'
import { CUSTOM_NODE_CLASS } from '@/lib/constants'
const NODE_NAME = 'easy multitrackProject'
const WIDGET_NAME = 'easy-media-sampling-preview'
const COLLAPSED_HEIGHT = 8
const PREVIEW_HEIGHT = 280
const PREVIEW_HEIGHT_DELTA = PREVIEW_HEIGHT - COLLAPSED_HEIGHT
interface PreviewWidget {
computedHeight?: number
hidden?: boolean
}
interface PreviewNode {
id: string | number
size?: [number, number]
graph?: { setDirtyCanvas?: (foreground: boolean, background: boolean) => void }
addDOMWidget: (
name: string,
type: string,
container: HTMLDivElement,
options: Record<string, unknown>,
) => PreviewWidget
setSize?: (size: [number, number]) => void
onResize?: (size: [number, number] | undefined) => void
onNodeCreated?: (...args: unknown[]) => void
onExecutionStart?: (...args: unknown[]) => void
onRemoved?: (...args: unknown[]) => void
easyMediaSamplingPreviewCleanup?: () => void
easyMediaSamplingPreviewReset?: () => void
}
interface PreviewNodeType {
prototype: object
}
function installWidget(node: PreviewNode, app: ComfyApp) {
let visible = false
let previewHeightApplied = false
let executionRevision = 0
let root: Root | null = null
let hostObserver: MutationObserver | null = null
let hostStyleObserver: MutationObserver | null = null
let trackedHost: HTMLElement | null = null
const container = document.createElement('div')
container.classList.add('comfyui-react-widget', CUSTOM_NODE_CLASS)
container.hidden = true
Object.assign(container.style, {
width: '100%',
height: `${PREVIEW_HEIGHT}px`,
maxHeight: `${PREVIEW_HEIGHT}px`,
overflow: 'hidden',
position: 'relative',
pointerEvents: 'none',
})
const widget = node.addDOMWidget(WIDGET_NAME, WIDGET_NAME, container, {
getMinHeight: () => visible ? PREVIEW_HEIGHT : COLLAPSED_HEIGHT,
getMaxHeight: () => visible ? PREVIEW_HEIGHT : COLLAPSED_HEIGHT,
hideOnZoom: false,
margin: 0,
serialize: false,
})
widget.hidden = false
const syncHostBounds = () => {
const host = container.parentElement
if (!host) return
if (host !== trackedHost) {
hostStyleObserver?.disconnect()
trackedHost = host
hostStyleObserver = new MutationObserver(syncHostBounds)
hostStyleObserver.observe(host, { attributes: true, attributeFilter: ['style'] })
hostObserver?.disconnect()
hostObserver = null
}
const height = visible ? PREVIEW_HEIGHT : COLLAPSED_HEIGHT
const pointerEvents = visible ? 'auto' : 'none'
if (host.style.maxHeight !== `${height}px`) host.style.maxHeight = `${height}px`
if (host.style.overflow !== 'hidden') host.style.overflow = 'hidden'
if (host.style.pointerEvents !== pointerEvents) host.style.pointerEvents = pointerEvents
}
hostObserver = new MutationObserver(syncHostBounds)
if (document.body) hostObserver.observe(document.body, { childList: true, subtree: true })
globalThis.requestAnimationFrame?.(syncHostBounds)
const setVisible = (nextVisible: boolean) => {
if (visible === nextVisible) return
visible = nextVisible
container.hidden = !visible
container.style.pointerEvents = visible ? 'auto' : 'none'
delete widget.computedHeight
if (node.size && node.setSize) {
if (visible && !previewHeightApplied) {
node.setSize([node.size[0], node.size[1] + PREVIEW_HEIGHT_DELTA])
previewHeightApplied = true
} else if (!visible && previewHeightApplied) {
node.setSize([node.size[0], Math.max(COLLAPSED_HEIGHT, node.size[1] - PREVIEW_HEIGHT_DELTA)])
previewHeightApplied = false
}
}
node.onResize?.(node.size)
node.graph?.setDirtyCanvas?.(true, true)
globalThis.requestAnimationFrame?.(syncHostBounds)
}
const render = () => root?.render(
<SamplingPreviewWidget
app={app}
nodeId={() => node.id}
executionRevision={executionRevision}
onVisibilityChange={setVisible}
/>,
)
root = createRoot(container)
render()
return {
reset: () => {
executionRevision += 1
render()
},
cleanup: () => {
hostObserver?.disconnect()
hostStyleObserver?.disconnect()
root?.unmount()
container.remove()
root = null
},
}
}
export function installProjectSamplingPreview(
nodeType: PreviewNodeType,
nodeData: { name?: string },
app: ComfyApp,
) {
if (nodeData.name !== NODE_NAME) return
const prototype = nodeType.prototype as PreviewNode
const originalCreated = prototype.onNodeCreated
const originalExecutionStart = prototype.onExecutionStart
const originalRemoved = prototype.onRemoved
prototype.onNodeCreated = function onProjectNodeCreated(this: PreviewNode, ...args: unknown[]) {
originalCreated?.apply(this, args)
const preview = installWidget(this, app)
this.easyMediaSamplingPreviewCleanup = preview.cleanup
this.easyMediaSamplingPreviewReset = preview.reset
}
prototype.onExecutionStart = function onProjectExecutionStart(this: PreviewNode, ...args: unknown[]) {
this.easyMediaSamplingPreviewReset?.()
originalExecutionStart?.apply(this, args)
}
prototype.onRemoved = function onProjectNodeRemoved(this: PreviewNode, ...args: unknown[]) {
this.easyMediaSamplingPreviewCleanup?.()
delete this.easyMediaSamplingPreviewCleanup
delete this.easyMediaSamplingPreviewReset
originalRemoved?.apply(this, args)
}
}
|