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)
  }
}