import { app } from '../../../scripts/app.js' import { api } from '../../../scripts/api.js' import { ComfyWidgets } from '../../../scripts/widgets.js' import { $el } from '../../../scripts/ui.js' function downloadJsonFile (jsonData, fileName = 'grid.json') { const dataString = JSON.stringify(jsonData) const blob = new Blob([dataString], { type: 'application/json' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = fileName link.click() // 释放URL对象 setTimeout(() => { URL.revokeObjectURL(url) }, 0) } function createSelectWithOptions (options) { const select = document.createElement('select') options.forEach(option => { const optionElement = document.createElement('option') optionElement.text = option optionElement.value = option select.appendChild(optionElement) }) select.style = `cursor: pointer; font-weight: 300; height: 30px; min-width: 122px; position: absolute; top: 24px; left: 88px; z-index: 999999999999999; ` return select } function drawCanvasWithText (w, h, tag, color = 'rgba(255,255,255,0.4)') { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') // 设置画布大小 canvas.width = w canvas.height = h // 绘制白色背景 ctx.fillStyle = color ctx.fillRect(0, 0, canvas.width, canvas.height) // 绘制文字 ctx.fillStyle = '#000000' ctx.font = '20px Arial' ctx.fillText(tag, 50, 50) // 导出为Base64 const base64 = canvas.toDataURL() return base64 } function get_position_style (ctx, widget_width, y, node_height) { const MARGIN = 4 // the margin around the html element /* Create a transform that deals with all the scrolling and zooming */ const elRect = ctx.canvas.getBoundingClientRect() const transform = new DOMMatrix() .scaleSelf( elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height ) .multiplySelf(ctx.getTransform()) .translateSelf(MARGIN, MARGIN + y) return { transformOrigin: '0 0', transform: transform, left: `0`, top: `0`, cursor: 'pointer', position: 'absolute', maxWidth: `${widget_width - MARGIN * 2}px`, // maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node width: `${widget_width - MARGIN * 2}px`, // height: `${node_height * 0.3 - MARGIN * 2}px`, // background: '#EEEEEE', display: 'flex', // flexDirection: 'column', // alignItems: 'center', justifyContent: 'space-around' } } const getLocalData = key => { let data = {} try { data = JSON.parse(localStorage.getItem(key)) || {} } catch (error) { return {} } return data } function createImage (url) { let im = new Image() return new Promise((res, rej) => { im.onload = () => res(im) im.src = url }) } const parseSvg = async svgContent => { // 创建一个临时的DOM元素来解析SVG const tempContainer = document.createElement('div') tempContainer.innerHTML = svgContent // 提取SVG元素 const svgElement = tempContainer.querySelector('svg') if (!svgElement) return // 获取SVG中 rect元素 var rectElements = svgElement?.querySelectorAll('rect') || [] // console.log(rectElements,svgElement) // 定义一个数组来存储处理后的数据 var data = [] Array.from(rectElements, (rectElement, i) => { // 获取rect元素的属性值 var x = ~~(rectElement.getAttribute('x') || 0) var y = ~~(rectElement.getAttribute('y') || 0) var width = ~~rectElement.getAttribute('width') var height = ~~rectElement.getAttribute('height') // console.log('rectElements',rectElement,x,y,width,height) if (x != undefined && y != undefined && width && height) { // 创建一个新的canvas元素 var canvas = document.createElement('canvas') canvas.width = width canvas.height = height var context = canvas.getContext('2d') // 填充颜色到canvas var fill = rectElement.getAttribute('fill') context.fillStyle = fill context.fillRect(0, 0, width, height) // 将canvas转换为base64格式 var base64 = canvas.toDataURL() // 将数据转化为指定的JSON格式 var rectData = { x: parseInt(x), y: parseInt(y), width: parseInt(width), height: parseInt(height), z_index: i + 1, scale_option: 'width', image: base64, mask: base64, type: 'base64', _t: 'rect' } // 将处理后的数据添加到数组中 data.push(rectData) } }) var svgWidth = svgElement.getAttribute('width') var svgHeight = svgElement.getAttribute('height') if (!(svgWidth && svgHeight)) { // viewBox let viewBox = svgElement.viewBox.baseVal svgWidth = viewBox.width svgHeight = viewBox.height } // 创建一个新的canvas元素 var canvas = document.createElement('canvas') canvas.width = svgWidth canvas.height = svgHeight var context = canvas.getContext('2d') // 绘制SVG到canvas var svgString = new XMLSerializer().serializeToString(svgElement) var DOMURL = window.URL || window.webkitURL || window var svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }) var url = DOMURL.createObjectURL(svgBlob) let img = await createImage(url) context.drawImage(img, 0, 0) let base64 = canvas.toDataURL() var rectData = { x: 0, y: 0, width: parseInt(svgWidth), height: parseInt(svgHeight), z_index: 0, scale_option: 'width', image: base64, mask: base64, type: 'base64', _t: 'canvas' } data.push(rectData) // 打印处理后的数据 console.log('layers', { data, image: base64, svgElement }) return { data, image: base64, svgElement } } function findImages (nodeId) { // 检查当前节点是否有 imgs 字段 const n = app.graph.getNodeById(nodeId) if (n.imgs) { return n.imgs } // 检查当前节点的 inputs 是否有 image 字段 if (n.inputs) { for (let i = 0; i < n.inputs.length; i++) { if (n.inputs[i].name === 'image' || n.inputs[i].name === 'images') { // 获取新的 nodeId,并递归调用 findImages 函数 var linkId = n.inputs[i]?.link var origin_id = app.graph.links[linkId].origin_id return findImages(origin_id) } } } // 如果没有找到 imgs 字段或者 image 字段,则返回 null return null } async function setArea (cw, ch, topBase64, base64, data, fn) { let displayHeight = Math.round(window.screen.availHeight * 0.8) let div = document.createElement('div') div.innerHTML = `
X
` // document.body.querySelector('#ml_overlay') document.body.appendChild(div) // let canvas = document.createElement('canvas') // canvas.width = cw // canvas.height = ch let img = div.querySelector('#ml_video') // let overlay = div.querySelector('#ml_overlay') let selection = div.querySelector('#ml_selection') let close = div.querySelector('.mx_close') let startX, startY, endX, endY let start = false let setDone = false // Set video source img.src = base64 // canvas.toDataURL(); close.style = `cursor: pointer; position: fixed; left: 12px; top: 12px; z-index: 99999999; background: black; width: 44px; height: 44px; text-align: center; line-height: 44px;` // init area // const data = getSetAreaData() let x = 0, y = 0, width = (cw * displayHeight) / ch, height = displayHeight let imgWidth = cw let imgHeight = ch if (data && data.width > 0 && data.height > 0) { // 相同尺寸窗口,恢复选区 x = (width * data.x) / imgWidth y = (height * data.y) / imgHeight width = (width * data.width) / imgWidth height = (height * data.height) / imgHeight } selection.style.left = x + 'px' selection.style.top = y + 'px' selection.style.width = width + 'px' selection.style.height = height + 'px' // Add mouse events img.addEventListener('mousedown', startSelection) img.addEventListener('mousemove', updateSelection) img.addEventListener('mouseup', endSelection) const removeDiv = () => { div.remove() close.removeEventListener('click', removeDiv) img.removeEventListener('mousedown', startSelection) img.removeEventListener('mousemove', updateSelection) img.removeEventListener('mouseup', endSelection) img.removeEventListener('mousedown', setDoneCheck) } close.addEventListener('click', removeDiv) const setDoneCheck = event => { console.log(setDone) if (setDone) { img.addEventListener('mousedown', startSelection) img.addEventListener('mousemove', updateSelection) img.addEventListener('mouseup', endSelection) setDone = false start = false startX = event.clientX startY = event.clientY } } img.addEventListener('mousedown', setDoneCheck) function remove () { img.removeEventListener('mousedown', startSelection) img.removeEventListener('mousemove', updateSelection) img.removeEventListener('mouseup', endSelection) setDone = true // div.remove() } function startSelection (event) { if (start == false) { startX = event.clientX startY = event.clientY updateSelection(event) start = true } else { } } function updateSelection (event) { endX = event.clientX endY = event.clientY // Calculate width, height, and coordinates let width = Math.abs(endX - startX) let height = Math.abs(endY - startY) let left = Math.min(startX, endX) let top = Math.min(startY, endY) // Set selection style selection.style.left = left + 'px' selection.style.top = top + 'px' selection.style.width = width + 'px' selection.style.height = height + 'px' } function endSelection (event) { endX = event.clientX endY = event.clientY // 获取img元素的真实宽度和高度 let imgWidth = img.naturalWidth let imgHeight = img.naturalHeight // 换算起始坐标 let realStartX = (startX / img.offsetWidth) * imgWidth let realStartY = (startY / img.offsetHeight) * imgHeight // 换算起始坐标 let realEndX = (endX / img.offsetWidth) * imgWidth let realEndY = (endY / img.offsetHeight) * imgHeight startX = realStartX startY = realStartY endX = realEndX endY = realEndY // Calculate width, height, and coordinates let width = Math.round(Math.abs(endX - startX)) let height = Math.round(Math.abs(endY - startY)) let left = Math.round(Math.min(startX, endX)) let top = Math.round(Math.min(startY, endY)) if (width <= 0 && height <= 0) return remove() if (fn) fn(left, top, width, height) remove() } } async function setAreaTags (cw, ch, grids, fn) { let base64 = drawCanvasWithText(cw, ch, '', 'white') let displayHeight = Math.round(window.screen.availHeight * 0.8) let div = document.createElement('div') div.innerHTML = `
${Array.from(grids, g => { const { label: tag, grid } = g const [dx, dy, dw, dh] = grid const base64Data = drawCanvasWithText(dw, dh, tag) let x = 0, y = 0, width = (cw * displayHeight) / ch, height = displayHeight let imgWidth = cw let imgHeight = ch if (dw > 0 && dh > 0) { // 相同尺寸窗口,恢复选区 x = (width * dx) / imgWidth y = (height * dy) / imgHeight width = (width * dw) / imgWidth height = (height * dh) / imgHeight } return `
` })}
X
` // document.body.querySelector('#ml_overlay') document.body.appendChild(div) const tags = Array.from(grids, g => g.label) let select = createSelectWithOptions(tags) document.body.appendChild(select) let img = div.querySelector('#ml_video') // let overlay = div.querySelector('#ml_overlay') let selections = [...div.querySelectorAll('.ml_selection')] let selection = selections.filter( s => s.getAttribute('data-tag') === select.value )[0] select.addEventListener('change', e => { selection = selections.filter( s => s.getAttribute('data-tag') === select.value )[0] }) // console.log(select.value,selection) let close = div.querySelector('.mx_close') let startX, startY, endX, endY let start = false let setDone = false // Set video source img.src = base64 // canvas.toDataURL(); close.style = `cursor: pointer; position: fixed; left: 12px; top: 12px; z-index: 99999999; background: black; width: 44px; height: 44px; text-align: center; line-height: 44px;` // Add mouse events img.addEventListener('mousedown', startSelection) img.addEventListener('mousemove', updateSelection) img.addEventListener('mouseup', endSelection) const removeDiv = () => { div.remove() select?.remove() close.removeEventListener('click', removeDiv) img.removeEventListener('mousedown', startSelection) img.removeEventListener('mousemove', updateSelection) img.removeEventListener('mouseup', endSelection) img.removeEventListener('mousedown', setDoneCheck) } close.addEventListener('click', removeDiv) const setDoneCheck = event => { console.log(setDone) if (setDone) { img.addEventListener('mousedown', startSelection) img.addEventListener('mousemove', updateSelection) img.addEventListener('mouseup', endSelection) setDone = false start = false startX = event.clientX startY = event.clientY } } img.addEventListener('mousedown', setDoneCheck) function remove () { img.removeEventListener('mousedown', startSelection) img.removeEventListener('mousemove', updateSelection) img.removeEventListener('mouseup', endSelection) setDone = true // select?.remove() } function startSelection (event) { if (start == false) { startX = event.clientX startY = event.clientY updateSelection(event) start = true } else { } } function updateSelection (event) { endX = event.clientX endY = event.clientY // Calculate width, height, and coordinates let width = Math.abs(endX - startX) let height = Math.abs(endY - startY) let left = Math.min(startX, endX) let top = Math.min(startY, endY) // Set selection style selection.style.left = left + 'px' selection.style.top = top + 'px' selection.style.width = width + 'px' selection.style.height = height + 'px' } function endSelection (event) { endX = event.clientX endY = event.clientY // 获取img元素的真实宽度和高度 let imgWidth = img.naturalWidth let imgHeight = img.naturalHeight // 换算起始坐标 let realStartX = (startX / img.offsetWidth) * imgWidth let realStartY = (startY / img.offsetHeight) * imgHeight // 换算起始坐标 let realEndX = (endX / img.offsetWidth) * imgWidth let realEndY = (endY / img.offsetHeight) * imgHeight startX = realStartX startY = realStartY endX = realEndX endY = realEndY // Calculate width, height, and coordinates let width = Math.round(Math.abs(endX - startX)) let height = Math.round(Math.abs(endY - startY)) let left = Math.round(Math.min(startX, endX)) let top = Math.round(Math.min(startY, endY)) if (width <= 0 && height <= 0) return remove() if (!!fn) fn(select.value, left, top, width, height) remove() } } app.registerExtension({ name: 'Mixlab.layer.ShowLayer', async getCustomWidgets (app) { return { EDIT (node, inputName, inputData, app) { // console.log('EditLayer##node', node,inputName, inputData) const widget = { type: inputData[0], // the type, CHEESE name: inputName, // the name, slice size: [128, 44], // a default size draw (ctx, node, widget_width, y, widget_height) { // console.log('EditLayer', this) if (this.input) Object.assign( this.input.style, get_position_style(ctx, widget_width, 32, node.size[1]) ) }, computeSize (...args) { return [128, 44] // a method to compute the current size of the widget }, async serializeValue (nodeId, widgetIndex) { let d = getLocalData('_mixlab_edit_layer') // console.log('EditLayer',d[node.id]) return d[node.id] } } // widget.something = something; // maybe adds stuff to it node.addCustomWidget(widget) // adds it to the node return widget // and returns it. } } }, async beforeRegisterNodeDef (nodeType, nodeData, app) { if (nodeType.comfyClass == 'ShowLayer') { const orig_nodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = async function () { orig_nodeCreated?.apply(this, arguments) const findNode = nodeId => { let node = app.graph._nodes_by_id[nodeId] if (node?.type == 'Reroute') { let linkId = node.inputs.filter(i => i.type == '*')[0].link nodeId = app.graph.links.filter(link => link.id == linkId)[0] ?.origin_id return findNode(nodeId) } else { return nodeId } } // 获取layers数据 const getLayers = async () => { console.log( 'getLayers1', this.inputs.filter(ip => ip.name === 'layers') ) let linkId = this.inputs.filter(ip => ip.name === 'layers')[0].link let nodeId = app.graph.links?.filter(link => link.id == linkId)[0] ?.origin_id if (nodeId) { nodeId = findNode(nodeId) } // let node = app.graph._nodes_by_id[nodeId] // if (node?.type == 'Reroute') { // linkId = node.inputs[0].link // nodeId = app.graph.links.filter(link => link.id == linkId)[0] // ?.origin_id // } let d = getLocalData('_mixlab_svg_image') console.log('test', d[nodeId]) if (d[nodeId]) { let url = d[nodeId] let dt = await fetch(url) let svgStr = await dt.text() const { data } = (await parseSvg(svgStr)) || {} console.log('fetch', data) return data } else { return [] } } // 修改layers数据 const setLayer = async (editIndex, layers = null) => { // let editIndex = 0 let lys = layers || (await getLayers()) let layer = lys[editIndex] // console.log(layer) const updateValue = name => { const x = this.widgets.filter(w => w.name == name)[0] x.value = layer[name] } if (layer) { Array.from(['x', 'y', 'width', 'height', 'z_index'], n => updateValue(n) ) } } let that = this const save_edit_layer_index = i => { let data = getLocalData('_mixlab_edit_layer') data[that.id] = i localStorage.setItem('_mixlab_edit_layer', JSON.stringify(data)) } await setLayer(0) save_edit_layer_index(0) const edit = this.widgets.filter(w => w.name == 'edit')[0] edit.input = $el('div', {}) edit.input.style = ` display: flex; flex-direction:row; align-items: center; margin-top: 0;` const ip = $el('input', {}) ip.className = 'comfy-multiline-input' ip.type = 'number' ip.min = 0 ip.step = 1 ip.max = Math.max(0, (await getLayers()).length - 1) // ip.className = `${'comfy-multiline-input'} ` ip.value = 0 ip.style = ` background-color: var(--comfy-input-bg); color: var(--input-text); outline: none; border: none; padding: 4px; width: 60%; cursor: pointer; height: 24px;` const label = document.createElement('label') label.style = 'font-size: 10px;min-width:32px' label.innerText = 'Layer Index' edit.input.appendChild(label) edit.input.appendChild(ip) document.body.appendChild(edit.input) ip.addEventListener('click', async event => { console.log(await getLayers()) ip.max = Math.max(0, (await getLayers()).length - 1) }) ip.addEventListener('change', async event => { let index = ~~ip.value let lys = await getLayers() await setLayer(index, lys) app.graph.setDirtyCanvas(true, true) save_edit_layer_index(index) }) // console.log('EditLayer nodeData', edit) const onRemoved = this.onRemoved this.onRemoved = () => { edit.input.remove() return onRemoved?.() } if (this.onResize) { this.onResize(this.size) } this.serialize_widgets = false //需要保存参数 } } }, async loadedGraphNode (node, app) { // Fires every time a node is constructed // You can modify widgets/add handlers/etc here if (node.type === 'SvgImage') { let widget = node.widgets.filter(w => w.div)[0] let data = getLocalData('_mixlab_svg_image') let id = node.id // widget.div.querySelector('.Svg').value = data[id] || '#000000' } } }) app.registerExtension({ name: 'Mixlab.layer.NewLayer', async beforeRegisterNodeDef (nodeType, nodeData, app) { if (nodeData.name === 'NewLayer') { const orig_nodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = async function () { orig_nodeCreated?.apply(this, arguments) let b = this.widgets.filter(w => w.type === 'button')[0] // const [w, h, base64] = canvas if (!b) { const updateValue = (x1, y1, w1, h1) => { if (this.widgets) { for (const widget of this.widgets) { if (widget.name === 'x') { widget.value = x1 } if (widget.name === 'y') { widget.value = y1 } if (widget.name === 'width') { widget.value = w1 } if (widget.name === 'height') { widget.value = h1 } } } } this.addWidget('button', 'Set Area', '', () => { let data = {} for (const widget of this.widgets) { if (widget.name === 'x') { data.x = widget.value } if (widget.name === 'y') { data.y = widget.value } if (widget.name === 'width') { data.width = widget.value } if (widget.name === 'height') { data.height = widget.value } } try { console.log('this.inputs', this.id) let imgs = findImages(this.id) // let topLinkId = this.inputs[0].link // let topNodeId = app.graph.links[topLinkId].origin_id let topIm = imgs[0] let linkId = this.inputs[3].link let nodeId = app.graph.links[linkId].origin_id // console.log(linkId,this.inputs) let imgs2 = findImages(nodeId) let im = imgs2[0] console.log(topIm, im) // let src = im.src setArea( im.naturalWidth, im.naturalHeight, topIm.src, im.src, data, updateValue ) } catch (error) { console.log(error) } }) } } const onRemoved = this.onRemoved this.onRemoved = () => { // let b = this.widgets.filter(w => w.type === 'button')[0]; return onRemoved?.() } if (this.onResize) { this.onResize(this.size) } this.serialize_widgets = true //需要保存参数 } } }) app.registerExtension({ name: 'Mixlab.layer.GridInput', async beforeRegisterNodeDef (nodeType, nodeData, app) { if (nodeType.comfyClass == 'GridInput') { const orig_nodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = async function () { orig_nodeCreated?.apply(this, arguments) const grids_widget = this.widgets.filter(w => w.name == 'grids')[0] const widget = { type: 'div', name: 'upload', draw (ctx, node, widget_width, y, widget_height) { Object.assign( this.div.style, get_position_style(ctx, widget_width, y, node.size[1]), { justifyContent: 'flex-start' } ) } } widget.div = $el('div', {}) const addBtn = document.createElement('button') addBtn.innerText = 'Add Box' addBtn.style = `cursor: pointer; font-weight: 300; margin: 2px; color: var(--descrip-text); background-color: var(--comfy-input-bg); border-radius: 8px; border-color: var(--border-color); border-style: solid;height: 30px;min-width: 122px; ` const vbtn = document.createElement('button') vbtn.innerText = 'Set Box' vbtn.style = `cursor: pointer; font-weight: 300; margin: 2px; color: var(--descrip-text); background-color: var(--comfy-input-bg); border-radius: 8px; border-color: var(--border-color); border-style: solid;height: 30px;min-width: 122px; ` const btn = document.createElement('button') btn.innerText = 'Upload JSON' btn.style = `cursor: pointer; font-weight: 300; margin: 2px; color: var(--descrip-text); background-color: var(--comfy-input-bg); border-radius: 8px; border-color: var(--border-color); border-style: solid;height: 30px;min-width: 122px; ` addBtn.addEventListener('click', () => { const { width, height, grids } = JSON.parse(grids_widget.value) grids.push({ label: 'background', grid: [12, 12, width - 24, height - 24] }) grids_widget.value = JSON.stringify( { width, height, grids }, null, 2 ) }) vbtn.addEventListener('click', () => { const { width, height, grids } = JSON.parse(grids_widget.value) setAreaTags(width, height, grids, (tag, x, y, w, h) => { grids_widget.value = JSON.stringify( { width, height, grids: Array.from(grids, g => { if (g.label === tag) { g.grid = [x, y, w, h] } return g }) }, null, 2 ) }) }) btn.addEventListener('click', () => { let inp = document.createElement('input') inp.type = 'file' inp.accept = '.json' inp.click() inp.addEventListener('change', event => { // 获取选择的文件 const file = event.target.files[0] this.title = file.name.split('.')[0] // console.log(file.name.split('.')[0]) // 创建文件读取器 const reader = new FileReader() // 定义读取完成事件的回调函数 reader.onload = event => { // 读取完成后的文本内容 const fileContent = JSON.parse(event.target.result) const grids = fileContent grids_widget.value = JSON.stringify(grids, null, 2) // widget.value = grids inp.remove() } // 以文本方式读取文件 reader.readAsText(file) }) }) widget.div.appendChild(addBtn) widget.div.appendChild(vbtn) widget.div.appendChild(btn) document.body.appendChild(widget.div) this.addCustomWidget(widget) const onExecuted = nodeType.prototype.onExecuted nodeType.prototype.onExecuted = function (message) { const r = onExecuted?.apply?.(this, arguments) let json = message.json if (json) { json = { width: json[0], height: json[1], grids: json[2] } grids_widget.value = JSON.stringify(json, null, 2) // widget.value = json } return r } const onRemoved = this.onRemoved this.onRemoved = () => { widget.div.remove() return onRemoved?.() } if (this.onResize) { this.onResize(this.size) } this.serialize_widgets = true //需要保存参数 } } }, async loadedGraphNode (node, app) { if (node.type === 'GridInput') { try { const grids_widget = node.widgets.filter(w => w.name == 'grids')[0] const { width, height, grids } = JSON.parse(grids_widget.value) console.log('#GridInput', node, grids) const div = node.widgets.filter(w => w.name == 'upload')[0] div.div.querySelector('select').innerHTML = Array.from( grids, g => `` ).join('') } catch (error) {} } } }) app.registerExtension({ name: 'Mixlab.layer.GridDisplayAndSave', async beforeRegisterNodeDef (nodeType, nodeData, app) { if (nodeType.comfyClass == 'GridDisplayAndSave') { const orig_nodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = async function () { orig_nodeCreated?.apply(this, arguments) const grids_widget = this.widgets.filter(w => w.name == 'grids')[0] console.log('GridDisplayAndSave', grids_widget) const widget = { type: 'div', name: 'save_json', draw (ctx, node, widget_width, y, widget_height) { Object.assign( this.div.style, get_position_style(ctx, widget_width, y, node.size[1]), { justifyContent: 'flex-start', flexDirection: 'column' } ) } } widget.div = $el('div', {}) const btn = document.createElement('button') btn.innerText = 'Save JSON' btn.style = `cursor: pointer; font-weight: 300; margin: 2px; color: var(--descrip-text); background-color: var(--comfy-input-bg); border-radius: 8px; border-color: var(--border-color); border-style: solid;height: 30px;min-width: 122px; max-width: 122px; ` btn.addEventListener('click', () => { if (window._mixlab_grid) downloadJsonFile( window._mixlab_grid, this.widgets.filter(w => w.name == 'filename_prefix')[0]?.value + '_grid.json' ) }) widget.div.appendChild(btn) document.body.appendChild(widget.div) this.addCustomWidget(widget) const onExecuted = nodeType.prototype.onExecuted nodeType.prototype.onExecuted = function (message) { const r = onExecuted?.apply?.(this, arguments) let save_json = this.widgets.filter(d => d.name == 'save_json')[0] let div = save_json?.div // console.log('Test',message) let image = message.image[0] let json = message.json if (image) { const { filename, subfolder, type } = image if (!div.querySelector('img')) { let im = new Image() div.appendChild(im) im.style.width = '100%' } div.querySelector('img').src = api.apiURL( `/view?filename=${encodeURIComponent( filename )}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` ) window._mixlab_grid = { width: json[0], height: json[1], grids: json[2] } // console.log(src) } this.onResize?.(this.size) return r } const onRemoved = this.onRemoved this.onRemoved = () => { widget.div.remove() return onRemoved?.() } if (this.onResize) { this.onResize(this.size) } this.serialize_widgets = true //需要保存参数 } } }, async loadedGraphNode (node, app) { if (node.type === 'GridDisplayAndSave') { try { let grids_widget = node.widgets.filter(w => w.name === 'grids')[0] // let ks = getLocalData(`_mixlab_PromptSlide`) let uploadWidget = node.widgets.filter(w => w.name == 'upload')[0] // console.log('##widget', uploadWidget.value) let grids = JSON.parse(uploadWidget.value) } catch (error) {} } } })