Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
49
custom_nodes/rgthree-comfy/src_web/common/comfyui_shim.ts
Normal file
49
custom_nodes/rgthree-comfy/src_web/common/comfyui_shim.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* [🤮] At some point the new ComfyUI frontend stopped loading it's source as modules and started
|
||||
* bundling them together. This removed the ability for to import the individual modules (like the
|
||||
* api.js or pnginfo.js) in stand alone pages, like link_fixer.
|
||||
*
|
||||
* So, what do we have to do? Well, we have to fork, hardcode, or port what we would want to load
|
||||
* from ComfyUI as our own, independant files again, which is unforunate for several reasons;
|
||||
* duplicate code, risk of falling behind, etc...
|
||||
*
|
||||
* Anyway, this file is a shim that will either detect we're in the ComfyUI app and pass through the
|
||||
* bundled module from the ComfyUI global or load from our own code when that's not available
|
||||
* (because we're not in the actual ComfyUI UI).
|
||||
*/
|
||||
|
||||
import type {getPngMetadata, getWebpMetadata} from "typings/comfy.js";
|
||||
|
||||
const shimCache = new Map<string, any>();
|
||||
|
||||
async function shimComfyUiModule(moduleName: string, prop?: string) {
|
||||
let module = shimCache.get(moduleName);
|
||||
if (!module) {
|
||||
if (window.comfyAPI?.[moduleName]) {
|
||||
module = window.comfyAPI?.[moduleName];
|
||||
} else {
|
||||
module = await import(`./comfyui_shim_${moduleName}.js`);
|
||||
}
|
||||
if (!module) {
|
||||
throw new Error(`Module ${moduleName} could not be loaded.`);
|
||||
}
|
||||
shimCache.set(moduleName, module);
|
||||
}
|
||||
if (prop) {
|
||||
if (!module[prop]) {
|
||||
throw new Error(`Property ${prop} on module ${moduleName} could not be loaded.`);
|
||||
}
|
||||
return module[prop];
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
export async function getPngMetadata(file: File | Blob) {
|
||||
const fn = (await shimComfyUiModule("pnginfo", "getPngMetadata")) as getPngMetadata;
|
||||
return fn(file);
|
||||
}
|
||||
|
||||
export async function getWebpMetadata(file: File | Blob) {
|
||||
const fn = (await shimComfyUiModule("pnginfo", "getWebpMetadata")) as getWebpMetadata;
|
||||
return fn(file);
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
|
||||
/**
|
||||
* [🤮] See `./comfyui_shim.ts`.
|
||||
*
|
||||
* This code has been forked from https://github.com/Comfy-Org/ComfyUI_frontend/blob/0937c1f2cd5026f390a6efa64f630e01ea414d1d/src/scripts/pnginfo.ts
|
||||
* with some modifications made, such as removing unneeded exported functions, cleaning up trivial
|
||||
* typing, etc.
|
||||
*/
|
||||
|
||||
import { rgthreeApi } from "./rgthree_api.js";
|
||||
|
||||
/**
|
||||
* [🤮] A type to add to untypd portions of the code below where they were not yet typed in Comfy's
|
||||
* code.
|
||||
*/
|
||||
type lazyComfyAny = any;
|
||||
|
||||
|
||||
// [🤮] A shim for ComfyAPI getEmbeddings.
|
||||
const api = {
|
||||
async getEmbeddings(): Promise<any> {
|
||||
const resp = await rgthreeApi.fetchComfyApi('/embeddings', { cache: 'no-store' })
|
||||
return await resp.json();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getFromPngBuffer(buffer: ArrayBuffer) {
|
||||
// Get the PNG data as a Uint8Array
|
||||
const pngData = new Uint8Array(buffer)
|
||||
const dataView = new DataView(pngData.buffer)
|
||||
|
||||
// Check that the PNG signature is present
|
||||
if (dataView.getUint32(0) !== 0x89504e47) {
|
||||
console.error('Not a valid PNG file')
|
||||
return
|
||||
}
|
||||
|
||||
// Start searching for chunks after the PNG signature
|
||||
let offset = 8
|
||||
let txt_chunks: Record<string, string> = {}
|
||||
// Loop through the chunks in the PNG file
|
||||
while (offset < pngData.length) {
|
||||
// Get the length of the chunk
|
||||
const length = dataView.getUint32(offset)
|
||||
// Get the chunk type
|
||||
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8))
|
||||
if (type === 'tEXt' || type == 'comf' || type === 'iTXt') {
|
||||
// Get the keyword
|
||||
let keyword_end = offset + 8
|
||||
while (pngData[keyword_end] !== 0) {
|
||||
keyword_end++
|
||||
}
|
||||
const keyword = String.fromCharCode(
|
||||
...pngData.slice(offset + 8, keyword_end)
|
||||
)
|
||||
// Get the text
|
||||
const contentArraySegment = pngData.slice(
|
||||
keyword_end + 1,
|
||||
offset + 8 + length
|
||||
)
|
||||
const contentJson = new TextDecoder('utf-8').decode(contentArraySegment)
|
||||
txt_chunks[keyword] = contentJson
|
||||
}
|
||||
|
||||
offset += 12 + length
|
||||
}
|
||||
return txt_chunks
|
||||
}
|
||||
|
||||
function getFromPngFile(file: File) {
|
||||
return new Promise<Record<string, string>>((r) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
r(getFromPngBuffer((event.target as lazyComfyAny).result as ArrayBuffer) as lazyComfyAny)
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
function parseExifData(exifData: lazyComfyAny) {
|
||||
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
|
||||
const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === 'II'
|
||||
|
||||
// Function to read 16-bit and 32-bit integers from binary data
|
||||
function readInt(offset: lazyComfyAny, isLittleEndian: lazyComfyAny, length: lazyComfyAny) {
|
||||
let arr = exifData.slice(offset, offset + length)
|
||||
if (length === 2) {
|
||||
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(
|
||||
0,
|
||||
isLittleEndian
|
||||
)
|
||||
} else if (length === 4) {
|
||||
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(
|
||||
0,
|
||||
isLittleEndian
|
||||
)
|
||||
}
|
||||
// lazyComfyAny
|
||||
throw new Error('Shouldn\'t get here.');
|
||||
}
|
||||
|
||||
// Read the offset to the first IFD (Image File Directory)
|
||||
const ifdOffset = readInt(4, isLittleEndian, 4)
|
||||
|
||||
function parseIFD(offset: lazyComfyAny) {
|
||||
const numEntries = readInt(offset, isLittleEndian, 2) as lazyComfyAny;
|
||||
const result = {} as lazyComfyAny
|
||||
|
||||
for (let i = 0; i < numEntries; i++) {
|
||||
const entryOffset = offset + 2 + i * 12
|
||||
const tag = readInt(entryOffset, isLittleEndian, 2) as lazyComfyAny
|
||||
const type = readInt(entryOffset + 2, isLittleEndian, 2)
|
||||
const numValues = readInt(entryOffset + 4, isLittleEndian, 4)
|
||||
const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4) as lazyComfyAny;
|
||||
|
||||
// Read the value(s) based on the data type
|
||||
let value
|
||||
if (type === 2) {
|
||||
// ASCII string
|
||||
value = new TextDecoder('utf-8').decode(
|
||||
exifData.subarray(valueOffset, valueOffset + numValues - 1)
|
||||
)
|
||||
}
|
||||
|
||||
result[tag] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Parse the first IFD
|
||||
const ifdData = parseIFD(ifdOffset)
|
||||
return ifdData
|
||||
}
|
||||
|
||||
function splitValues(input: lazyComfyAny) {
|
||||
var output = {} as lazyComfyAny
|
||||
for (var key in input) {
|
||||
var value = input[key]
|
||||
var splitValues = value.split(':', 2)
|
||||
output[splitValues[0]] = splitValues[1]
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export function getPngMetadata(file: File): Promise<Record<string, string>> {
|
||||
return getFromPngFile(file)
|
||||
}
|
||||
|
||||
export function getWebpMetadata(file: lazyComfyAny) {
|
||||
return new Promise<Record<string, string>>((r) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const webp = new Uint8Array((event.target as lazyComfyAny).result as ArrayBuffer)
|
||||
const dataView = new DataView(webp.buffer)
|
||||
|
||||
// Check that the WEBP signature is present
|
||||
if (
|
||||
dataView.getUint32(0) !== 0x52494646 ||
|
||||
dataView.getUint32(8) !== 0x57454250
|
||||
) {
|
||||
console.error('Not a valid WEBP file')
|
||||
r({})
|
||||
return
|
||||
}
|
||||
|
||||
// Start searching for chunks after the WEBP signature
|
||||
let offset = 12
|
||||
let txt_chunks = {} as lazyComfyAny
|
||||
// Loop through the chunks in the WEBP file
|
||||
while (offset < webp.length) {
|
||||
const chunk_length = dataView.getUint32(offset + 4, true)
|
||||
const chunk_type = String.fromCharCode(
|
||||
...webp.slice(offset, offset + 4)
|
||||
)
|
||||
if (chunk_type === 'EXIF') {
|
||||
if (
|
||||
String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) ==
|
||||
'Exif\0\0'
|
||||
) {
|
||||
offset += 6
|
||||
}
|
||||
let data = parseExifData(
|
||||
webp.slice(offset + 8, offset + 8 + chunk_length)
|
||||
)
|
||||
for (var key in data) {
|
||||
const value = data[key] as string
|
||||
if (typeof value === 'string') {
|
||||
const index = value.indexOf(':')
|
||||
txt_chunks[value.slice(0, index)] = value.slice(index + 1)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
offset += 8 + chunk_length
|
||||
}
|
||||
|
||||
r(txt_chunks)
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
export function getLatentMetadata(file: lazyComfyAny) {
|
||||
return new Promise((r) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const safetensorsData = new Uint8Array((event.target as lazyComfyAny).result as ArrayBuffer)
|
||||
const dataView = new DataView(safetensorsData.buffer)
|
||||
let header_size = dataView.getUint32(0, true)
|
||||
let offset = 8
|
||||
let header = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
safetensorsData.slice(offset, offset + header_size)
|
||||
)
|
||||
)
|
||||
r(header.__metadata__)
|
||||
}
|
||||
|
||||
var slice = file.slice(0, 1024 * 1024 * 4)
|
||||
reader.readAsArrayBuffer(slice)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export async function importA1111(graph: lazyComfyAny, parameters: lazyComfyAny) {
|
||||
const p = parameters.lastIndexOf('\nSteps:')
|
||||
if (p > -1) {
|
||||
const embeddings = await api.getEmbeddings()
|
||||
const opts = parameters
|
||||
.substr(p)
|
||||
.split('\n')[1]
|
||||
.match(
|
||||
new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', 'g')
|
||||
)
|
||||
.reduce((p: lazyComfyAny, n: lazyComfyAny) => {
|
||||
const s = n.split(':')
|
||||
if (s[1].endsWith(',')) {
|
||||
s[1] = s[1].substr(0, s[1].length - 1)
|
||||
}
|
||||
p[s[0].trim().toLowerCase()] = s[1].trim()
|
||||
return p
|
||||
}, {})
|
||||
const p2 = parameters.lastIndexOf('\nNegative prompt:', p)
|
||||
if (p2 > -1) {
|
||||
let positive = parameters.substr(0, p2).trim()
|
||||
let negative = parameters.substring(p2 + 18, p).trim()
|
||||
|
||||
const ckptNode = LiteGraph.createNode('CheckpointLoaderSimple')!
|
||||
const clipSkipNode = LiteGraph.createNode('CLIPSetLastLayer')!
|
||||
const positiveNode = LiteGraph.createNode('CLIPTextEncode')!
|
||||
const negativeNode = LiteGraph.createNode('CLIPTextEncode')!
|
||||
const samplerNode = LiteGraph.createNode('KSampler')!
|
||||
const imageNode = LiteGraph.createNode('EmptyLatentImage')!
|
||||
const vaeNode = LiteGraph.createNode('VAEDecode')!
|
||||
const vaeLoaderNode = LiteGraph.createNode('VAELoader')!
|
||||
const saveNode = LiteGraph.createNode('SaveImage')!
|
||||
let hrSamplerNode = null as lazyComfyAny
|
||||
let hrSteps = null
|
||||
|
||||
const ceil64 = (v: lazyComfyAny) => Math.ceil(v / 64) * 64
|
||||
|
||||
const getWidget = (node: lazyComfyAny, name: lazyComfyAny) => {
|
||||
return node.widgets.find((w: lazyComfyAny) => w.name === name)
|
||||
}
|
||||
|
||||
const setWidgetValue = (node: lazyComfyAny, name: lazyComfyAny, value: lazyComfyAny, isOptionPrefix?: lazyComfyAny) => {
|
||||
const w = getWidget(node, name)
|
||||
if (isOptionPrefix) {
|
||||
const o = w.options.values.find((w: lazyComfyAny) => w.startsWith(value))
|
||||
if (o) {
|
||||
w.value = o
|
||||
} else {
|
||||
console.warn(`Unknown value '${value}' for widget '${name}'`, node)
|
||||
w.value = value
|
||||
}
|
||||
} else {
|
||||
w.value = value
|
||||
}
|
||||
}
|
||||
|
||||
const createLoraNodes = (clipNode:lazyComfyAny, text: lazyComfyAny, prevClip: lazyComfyAny, prevModel: lazyComfyAny) => {
|
||||
const loras = [] as lazyComfyAny
|
||||
text = text.replace(/<lora:([^:]+:[^>]+)>/g, function (m: lazyComfyAny, c: lazyComfyAny) {
|
||||
const s = c.split(':')
|
||||
const weight = parseFloat(s[1])
|
||||
if (isNaN(weight)) {
|
||||
console.warn('Invalid LORA', m)
|
||||
} else {
|
||||
loras.push({ name: s[0], weight })
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
for (const l of loras) {
|
||||
const loraNode = LiteGraph.createNode('LoraLoader')
|
||||
graph.add(loraNode)
|
||||
setWidgetValue(loraNode, 'lora_name', l.name, true)
|
||||
setWidgetValue(loraNode, 'strength_model', l.weight)
|
||||
setWidgetValue(loraNode, 'strength_clip', l.weight)
|
||||
prevModel.node.connect(prevModel.index, loraNode, 0)
|
||||
prevClip.node.connect(prevClip.index, loraNode, 1)
|
||||
prevModel = { node: loraNode, index: 0 }
|
||||
prevClip = { node: loraNode, index: 1 }
|
||||
}
|
||||
|
||||
prevClip.node.connect(1, clipNode, 0)
|
||||
prevModel.node.connect(0, samplerNode, 0)
|
||||
if (hrSamplerNode) {
|
||||
prevModel.node.connect(0, hrSamplerNode, 0)
|
||||
}
|
||||
|
||||
return { text, prevModel, prevClip }
|
||||
}
|
||||
|
||||
const replaceEmbeddings = (text: lazyComfyAny) => {
|
||||
if (!embeddings.length) return text
|
||||
return text.replaceAll(
|
||||
new RegExp(
|
||||
'\\b(' +
|
||||
embeddings
|
||||
.map((e: lazyComfyAny) => e.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
.join('\\b|\\b') +
|
||||
')\\b',
|
||||
'ig'
|
||||
),
|
||||
'embedding:$1'
|
||||
)
|
||||
}
|
||||
|
||||
const popOpt = (name: lazyComfyAny) => {
|
||||
const v = opts[name]
|
||||
delete opts[name]
|
||||
return v
|
||||
}
|
||||
|
||||
graph.clear()
|
||||
graph.add(ckptNode)
|
||||
graph.add(clipSkipNode)
|
||||
graph.add(positiveNode)
|
||||
graph.add(negativeNode)
|
||||
graph.add(samplerNode)
|
||||
graph.add(imageNode)
|
||||
graph.add(vaeNode)
|
||||
graph.add(vaeLoaderNode)
|
||||
graph.add(saveNode)
|
||||
|
||||
ckptNode.connect(1, clipSkipNode, 0)
|
||||
clipSkipNode.connect(0, positiveNode, 0)
|
||||
clipSkipNode.connect(0, negativeNode, 0)
|
||||
ckptNode.connect(0, samplerNode, 0)
|
||||
positiveNode.connect(0, samplerNode, 1)
|
||||
negativeNode.connect(0, samplerNode, 2)
|
||||
imageNode.connect(0, samplerNode, 3)
|
||||
vaeNode.connect(0, saveNode, 0)
|
||||
samplerNode.connect(0, vaeNode, 0)
|
||||
vaeLoaderNode.connect(0, vaeNode, 1)
|
||||
|
||||
const handlers = {
|
||||
model(v: lazyComfyAny) {
|
||||
setWidgetValue(ckptNode, 'ckpt_name', v, true)
|
||||
},
|
||||
vae(v: lazyComfyAny) {
|
||||
setWidgetValue(vaeLoaderNode, 'vae_name', v, true)
|
||||
},
|
||||
'cfg scale'(v: lazyComfyAny) {
|
||||
setWidgetValue(samplerNode, 'cfg', +v)
|
||||
},
|
||||
'clip skip'(v: lazyComfyAny) {
|
||||
setWidgetValue(clipSkipNode, 'stop_at_clip_layer', -v)
|
||||
},
|
||||
sampler(v: lazyComfyAny) {
|
||||
let name = v.toLowerCase().replace('++', 'pp').replaceAll(' ', '_')
|
||||
if (name.includes('karras')) {
|
||||
name = name.replace('karras', '').replace(/_+$/, '')
|
||||
setWidgetValue(samplerNode, 'scheduler', 'karras')
|
||||
} else {
|
||||
setWidgetValue(samplerNode, 'scheduler', 'normal')
|
||||
}
|
||||
const w = getWidget(samplerNode, 'sampler_name')
|
||||
const o = w.options.values.find(
|
||||
(w: lazyComfyAny) => w === name || w === 'sample_' + name
|
||||
)
|
||||
if (o) {
|
||||
setWidgetValue(samplerNode, 'sampler_name', o)
|
||||
}
|
||||
},
|
||||
size(v: lazyComfyAny) {
|
||||
const wxh = v.split('x')
|
||||
const w = ceil64(+wxh[0])
|
||||
const h = ceil64(+wxh[1])
|
||||
const hrUp = popOpt('hires upscale')
|
||||
const hrSz = popOpt('hires resize')
|
||||
hrSteps = popOpt('hires steps')
|
||||
let hrMethod = popOpt('hires upscaler')
|
||||
|
||||
setWidgetValue(imageNode, 'width', w)
|
||||
setWidgetValue(imageNode, 'height', h)
|
||||
|
||||
if (hrUp || hrSz) {
|
||||
let uw, uh
|
||||
if (hrUp) {
|
||||
uw = w * hrUp
|
||||
uh = h * hrUp
|
||||
} else {
|
||||
const s = hrSz.split('x')
|
||||
uw = +s[0]
|
||||
uh = +s[1]
|
||||
}
|
||||
|
||||
let upscaleNode
|
||||
let latentNode
|
||||
|
||||
if (hrMethod.startsWith('Latent')) {
|
||||
latentNode = upscaleNode = LiteGraph.createNode('LatentUpscale')!
|
||||
graph.add(upscaleNode)
|
||||
samplerNode.connect(0, upscaleNode, 0)
|
||||
|
||||
switch (hrMethod) {
|
||||
case 'Latent (nearest-exact)':
|
||||
hrMethod = 'nearest-exact'
|
||||
break
|
||||
}
|
||||
setWidgetValue(upscaleNode, 'upscale_method', hrMethod, true)
|
||||
} else {
|
||||
const decode = LiteGraph.createNode('VAEDecodeTiled')!
|
||||
graph.add(decode)
|
||||
samplerNode.connect(0, decode, 0)
|
||||
vaeLoaderNode.connect(0, decode, 1)
|
||||
|
||||
const upscaleLoaderNode = LiteGraph.createNode('UpscaleModelLoader')!
|
||||
graph.add(upscaleLoaderNode)
|
||||
setWidgetValue(upscaleLoaderNode, 'model_name', hrMethod, true)
|
||||
|
||||
const modelUpscaleNode = LiteGraph.createNode(
|
||||
'ImageUpscaleWithModel'
|
||||
)!
|
||||
graph.add(modelUpscaleNode)
|
||||
decode.connect(0, modelUpscaleNode, 1)
|
||||
upscaleLoaderNode.connect(0, modelUpscaleNode, 0)
|
||||
|
||||
upscaleNode = LiteGraph.createNode('ImageScale')!
|
||||
graph.add(upscaleNode)
|
||||
modelUpscaleNode.connect(0, upscaleNode, 0)
|
||||
|
||||
const vaeEncodeNode = (latentNode =
|
||||
LiteGraph.createNode('VAEEncodeTiled')!)!
|
||||
graph.add(vaeEncodeNode)
|
||||
upscaleNode.connect(0, vaeEncodeNode, 0)
|
||||
vaeLoaderNode.connect(0, vaeEncodeNode, 1)
|
||||
}
|
||||
|
||||
setWidgetValue(upscaleNode, 'width', ceil64(uw))
|
||||
setWidgetValue(upscaleNode, 'height', ceil64(uh))
|
||||
|
||||
hrSamplerNode = LiteGraph.createNode('KSampler')!
|
||||
graph.add(hrSamplerNode)
|
||||
ckptNode.connect(0, hrSamplerNode, 0)
|
||||
positiveNode.connect(0, hrSamplerNode, 1)
|
||||
negativeNode.connect(0, hrSamplerNode, 2)
|
||||
latentNode.connect(0, hrSamplerNode, 3)
|
||||
hrSamplerNode.connect(0, vaeNode, 0)
|
||||
}
|
||||
},
|
||||
steps(v: lazyComfyAny) {
|
||||
setWidgetValue(samplerNode, 'steps', +v)
|
||||
},
|
||||
seed(v: lazyComfyAny) {
|
||||
setWidgetValue(samplerNode, 'seed', +v)
|
||||
}
|
||||
}
|
||||
|
||||
for (const opt in opts) {
|
||||
if (opt in handlers) {
|
||||
((handlers as lazyComfyAny)[opt] as lazyComfyAny)(popOpt(opt))
|
||||
}
|
||||
}
|
||||
|
||||
if (hrSamplerNode) {
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
'steps',
|
||||
hrSteps ? +hrSteps : getWidget(samplerNode, 'steps').value
|
||||
)
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
'cfg',
|
||||
getWidget(samplerNode, 'cfg').value
|
||||
)
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
'scheduler',
|
||||
getWidget(samplerNode, 'scheduler').value
|
||||
)
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
'sampler_name',
|
||||
getWidget(samplerNode, 'sampler_name').value
|
||||
)
|
||||
setWidgetValue(
|
||||
hrSamplerNode,
|
||||
'denoise',
|
||||
+(popOpt('denoising strength') || '1')
|
||||
)
|
||||
}
|
||||
|
||||
let n = createLoraNodes(
|
||||
positiveNode,
|
||||
positive,
|
||||
{ node: clipSkipNode, index: 0 },
|
||||
{ node: ckptNode, index: 0 }
|
||||
)
|
||||
positive = n.text
|
||||
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel)
|
||||
negative = n.text
|
||||
|
||||
setWidgetValue(positiveNode, 'text', replaceEmbeddings(positive))
|
||||
setWidgetValue(negativeNode, 'text', replaceEmbeddings(negative))
|
||||
|
||||
graph.arrange()
|
||||
|
||||
for (const opt of [
|
||||
'model hash',
|
||||
'ensd',
|
||||
'version',
|
||||
'vae hash',
|
||||
'ti hashes',
|
||||
'lora hashes',
|
||||
'hashes'
|
||||
]) {
|
||||
delete opts[opt]
|
||||
}
|
||||
|
||||
console.warn('Unhandled parameters:', opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import {$el, getActionEls} from "rgthree/common/utils_dom.js";
|
||||
import {bind, register} from "../utils_templates";
|
||||
|
||||
const CSS_STYLE_SHEETS = new Map<string, string>();
|
||||
const CSS_STYLE_SHEETS_ADDED = new Map<string, HTMLLinkElement>();
|
||||
const HTML_TEMPLATE_FILES = new Map<string, string>();
|
||||
|
||||
function getCommonPath(name: string, extension: string) {
|
||||
return `rgthree/common/components/${name.replace("rgthree-", "").replace(/\-/g, "_")}.${extension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the stylesheet for the component, matched by the element name (minus the "rgthree-"
|
||||
* prefix).
|
||||
*/
|
||||
async function getStyleSheet(name: string, markupOrPath: string) {
|
||||
if (markupOrPath.includes("{")) {
|
||||
return markupOrPath;
|
||||
}
|
||||
if (!CSS_STYLE_SHEETS.has(name)) {
|
||||
try {
|
||||
const path = markupOrPath || getCommonPath(name, "css");
|
||||
const text = await (await fetch(path)).text();
|
||||
CSS_STYLE_SHEETS.set(name, text);
|
||||
} catch (e) {
|
||||
// alert("Error loading rgthree custom component css.");
|
||||
}
|
||||
}
|
||||
return CSS_STYLE_SHEETS.get(name)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the stylesheet to the page, once.
|
||||
*/
|
||||
async function addStyleSheet(name: string, markupOrPath: string) {
|
||||
if (markupOrPath.includes("{")) {
|
||||
throw new Error("Page-level stylesheets should be passed a path.");
|
||||
}
|
||||
if (!CSS_STYLE_SHEETS_ADDED.has(name)) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = markupOrPath;
|
||||
document.head.appendChild(link);
|
||||
CSS_STYLE_SHEETS_ADDED.set(name, link);
|
||||
}
|
||||
return CSS_STYLE_SHEETS_ADDED.get(name)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the stylesheet for the component, matched by the element name (minus the "rgthree-"
|
||||
* prefix).
|
||||
*/
|
||||
async function getTemplateMarkup(name: string, markupOrPath: string) {
|
||||
if (markupOrPath.includes("<template")) {
|
||||
return markupOrPath;
|
||||
}
|
||||
if (!HTML_TEMPLATE_FILES.has(name)) {
|
||||
try {
|
||||
const path = markupOrPath || getCommonPath(name, "html");
|
||||
const text = await (await fetch(path)).text();
|
||||
HTML_TEMPLATE_FILES.set(name, text);
|
||||
} catch (e) {
|
||||
// alert("Error loading rgthree custom component markup.");
|
||||
}
|
||||
}
|
||||
return HTML_TEMPLATE_FILES.get(name)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* A base custom element.
|
||||
*/
|
||||
export abstract class RgthreeCustomElement extends HTMLElement {
|
||||
static readonly NAME: `rgthree-${string}` = "rgthree-override";
|
||||
static readonly USE_SHADOW: boolean = true;
|
||||
static readonly TEMPLATES: string = "";
|
||||
static readonly CSS: string = "";
|
||||
|
||||
static create<T extends RgthreeCustomElement>(): T {
|
||||
if (this.NAME === "rgthree-override") {
|
||||
throw new Error("Must override component NAME");
|
||||
}
|
||||
if (!window.customElements.get(this.NAME)) {
|
||||
window.customElements.define(this.NAME, this as unknown as CustomElementConstructor);
|
||||
}
|
||||
return document.createElement(this.NAME) as T;
|
||||
}
|
||||
|
||||
protected ctor = this.constructor as typeof RgthreeCustomElement;
|
||||
protected hasBeenConnected: boolean = false;
|
||||
protected connected: boolean = false;
|
||||
protected root!: ShadowRoot | HTMLElement;
|
||||
protected readonly templates = new Map<string, HTMLTemplateElement>();
|
||||
protected firstConnectedPromiseResolver!: Function;
|
||||
protected firstConnectedPromise = new Promise(
|
||||
(resolve) => (this.firstConnectedPromiseResolver = resolve),
|
||||
);
|
||||
|
||||
onFirstConnected(): void {
|
||||
// Optionally overridden.
|
||||
}
|
||||
onReconnected(): void {
|
||||
// Optionally overridden.
|
||||
}
|
||||
onConnected(): void {
|
||||
// Optionally overridden.
|
||||
}
|
||||
onDisconnected(): void {
|
||||
// Optionally overridden.
|
||||
}
|
||||
onAction(action: string, e?: Event): void {
|
||||
console.log("onAction", action, e);
|
||||
// Optionally overridden.
|
||||
}
|
||||
|
||||
getElement<E extends HTMLElement>(query: string) {
|
||||
const el = this.querySelector(query);
|
||||
if (!el) {
|
||||
throw new Error("No element found for query: " + query);
|
||||
}
|
||||
return el as E;
|
||||
}
|
||||
|
||||
private onActionInternal(action: string, e?: Event): void {
|
||||
if (typeof (this as any)[action] === "function") {
|
||||
(this as any)[action](e);
|
||||
} else {
|
||||
this.onAction(action, e);
|
||||
}
|
||||
}
|
||||
|
||||
private onConnectedInternal(): void {
|
||||
this.connectActionElements();
|
||||
this.onConnected();
|
||||
}
|
||||
|
||||
private onDisconnectedInternal(): void {
|
||||
this.disconnectActionElements();
|
||||
this.onDisconnected();
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const elementName = this.ctor.NAME;
|
||||
const wasConnected = this.connected;
|
||||
if (!wasConnected) {
|
||||
this.connected = true;
|
||||
}
|
||||
if (!this.hasBeenConnected) {
|
||||
const [stylesheet, markup] = await Promise.all([
|
||||
this.ctor.USE_SHADOW
|
||||
? getStyleSheet(elementName, this.ctor.CSS)
|
||||
: addStyleSheet(elementName, this.ctor.CSS),
|
||||
getTemplateMarkup(elementName, this.ctor.TEMPLATES),
|
||||
]);
|
||||
|
||||
if (markup) {
|
||||
const temp = $el("div");
|
||||
const templatesMarkup = markup.match(/<template[^]*?<\/template>/gm) || [];
|
||||
for (const markup of templatesMarkup) {
|
||||
temp.innerHTML = markup;
|
||||
const template = temp.children[0];
|
||||
if (!(template instanceof HTMLTemplateElement)) {
|
||||
throw new Error("Not a template element.");
|
||||
}
|
||||
let id = template.getAttribute("id");
|
||||
if (!id) {
|
||||
id = this.ctor.NAME;
|
||||
// throw new Error("Not template id.");
|
||||
}
|
||||
this.templates.set(id, template);
|
||||
}
|
||||
}
|
||||
|
||||
// If we're using a shadow, then it's our root as a ShadowRoot. If we're not, then the root is
|
||||
// the custom element itself. This allows easy binding on "this.root" but it also means if we
|
||||
// want to set an atrtibute or otherwise access the actual custom element, we should use
|
||||
// "this" to be compatible with both.
|
||||
if (this.ctor.USE_SHADOW) {
|
||||
this.root = this.attachShadow({mode: "open"});
|
||||
if (typeof stylesheet === "string") {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(stylesheet);
|
||||
this.root.adoptedStyleSheets = [sheet];
|
||||
}
|
||||
} else {
|
||||
this.root = this;
|
||||
}
|
||||
|
||||
let template: HTMLTemplateElement | undefined;
|
||||
if (this.templates.has(elementName)) {
|
||||
template = this.templates.get(elementName);
|
||||
} else if (this.templates.has(elementName.replace("rgthree-", ""))) {
|
||||
template = this.templates.get(elementName.replace("rgthree-", ""));
|
||||
}
|
||||
if (template) {
|
||||
this.root.appendChild(template.content.cloneNode(true));
|
||||
for (const name of template.getAttributeNames()) {
|
||||
if (name != "id" && template.getAttribute(name)) {
|
||||
this.setAttribute(name, template.getAttribute(name)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.onFirstConnected();
|
||||
this.hasBeenConnected = true;
|
||||
this.firstConnectedPromiseResolver();
|
||||
} else {
|
||||
this.onReconnected();
|
||||
}
|
||||
this.onConnectedInternal();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.connected = false;
|
||||
this.onDisconnected();
|
||||
}
|
||||
|
||||
private readonly eventElements = new Map<Element, {[event: string]: EventListener}>();
|
||||
|
||||
private connectActionElements() {
|
||||
const data = getActionEls(this);
|
||||
for (const dataItem of Object.values(data)) {
|
||||
const mapItem = this.eventElements.get(dataItem.el) || {};
|
||||
for (const [event, action] of Object.entries(dataItem.actions)) {
|
||||
if (mapItem[event]) {
|
||||
console.warn(`Element already has an event for ${event}`);
|
||||
continue;
|
||||
}
|
||||
mapItem[event] = (e: Event) => {
|
||||
this.onActionInternal(action, e);
|
||||
};
|
||||
dataItem.el.addEventListener(event as keyof ElementEventMap, mapItem[event]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private disconnectActionElements() {
|
||||
for (const [el, eventData] of this.eventElements.entries()) {
|
||||
for (const [event, fn] of Object.entries(eventData)) {
|
||||
el.removeEventListener(event, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async bindWhenConnected(data: any, el?: HTMLElement | ShadowRoot) {
|
||||
await this.firstConnectedPromise;
|
||||
this.bind(data, el);
|
||||
}
|
||||
|
||||
bind(data: any, el?: HTMLElement | ShadowRoot) {
|
||||
bind(el || this.root, data);
|
||||
}
|
||||
}
|
||||
156
custom_nodes/rgthree-comfy/src_web/common/css/buttons.scss
Normal file
156
custom_nodes/rgthree-comfy/src_web/common/css/buttons.scss
Normal file
@@ -0,0 +1,156 @@
|
||||
:not(#fakeid) .rgthree-button-reset {
|
||||
position: relative;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
$borderRadius: 0.33rem;
|
||||
|
||||
:not(#fakeid) .rgthree-button {
|
||||
--padding-top: 7px;
|
||||
--padding-bottom: 9px;
|
||||
--padding-x: 16px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: $borderRadius;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: calc(16rem / 16);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
margin: 0.25rem;
|
||||
box-shadow: 0px 0px 2px rgb(0, 0, 0);
|
||||
background: #212121;
|
||||
transition: all 0.1s ease-in-out;
|
||||
padding: var(--padding-top) var(--padding-x) var(--padding-bottom);
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: $borderRadius;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow:
|
||||
inset 1px 1px 0px rgba(255, 255, 255, 0.12),
|
||||
inset -1px -1px 0px rgba(0, 0, 0, 0.75);
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(0, 0, 0, 0.15));
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
&::after {
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #303030;
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0);
|
||||
background: #121212;
|
||||
padding: calc(var(--padding-top) + 1px) calc(var(--padding-x) - 1px)
|
||||
calc(var(--padding-bottom) - 1px) calc(var(--padding-x) + 1px);
|
||||
}
|
||||
|
||||
&:active::before,
|
||||
&:active::after {
|
||||
box-shadow:
|
||||
1px 1px 0px rgba(255, 255, 255, 0.15),
|
||||
inset 1px 1px 0px rgba(0, 0, 0, 0.5),
|
||||
inset 1px 3px 5px rgba(0, 0, 0, 0.33);
|
||||
}
|
||||
|
||||
&.-blue {
|
||||
background: #346599 !important;
|
||||
}
|
||||
&.-blue:hover {
|
||||
background: #3b77b8 !important;
|
||||
}
|
||||
&.-blue:active {
|
||||
background: #1d5086 !important;
|
||||
}
|
||||
|
||||
&.-green {
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(0, 0, 0, 0.15)), #14580b;
|
||||
}
|
||||
&.-green:hover {
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(0, 0, 0, 0.15)), #1a6d0f;
|
||||
}
|
||||
&.-green:active {
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.06)), #0f3f09;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
box-shadow: none;
|
||||
background: #666 !important;
|
||||
color: #aaa;
|
||||
pointer-events: none;
|
||||
}
|
||||
&[disabled]::before,
|
||||
&[disabled]::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:not(#fakeid) .rgthree-comfybar-top-button-group {
|
||||
font-size: 0;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
.rgthree-comfybar-top-button {
|
||||
margin: 0;
|
||||
flex: 1 1;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 0;
|
||||
background: var(--p-button-secondary-background);
|
||||
color: var(--p-button-secondary-color);
|
||||
|
||||
&.-primary {
|
||||
background: var(--p-button-primary-background);
|
||||
color: var(--p-button-primary-color);
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.rgthree-comfybar-top-button:first-of-type,
|
||||
.rgthree-comfybar-top-button:first-of-type::before,
|
||||
.rgthree-comfybar-top-button:first-of-type::after {
|
||||
border-top-left-radius: 0.33rem;
|
||||
border-bottom-left-radius: 0.33rem;
|
||||
|
||||
}
|
||||
.rgthree-comfybar-top-button:last-of-type,
|
||||
.rgthree-comfybar-top-button:last-of-type::before,
|
||||
.rgthree-comfybar-top-button:last-of-type::after {
|
||||
border-top-right-radius: 0.33rem;
|
||||
border-bottom-right-radius: 0.33rem;
|
||||
}
|
||||
}
|
||||
129
custom_nodes/rgthree-comfy/src_web/common/css/dialog.scss
Normal file
129
custom_nodes/rgthree-comfy/src_web/common/css/dialog.scss
Normal file
@@ -0,0 +1,129 @@
|
||||
|
||||
.rgthree-dialog {
|
||||
outline: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: #414141;
|
||||
color: #fff;
|
||||
box-shadow:
|
||||
inset 1px 1px 0px rgba(255, 255, 255, 0.05),
|
||||
inset -1px -1px 0px rgba(0, 0, 0, 0.5),
|
||||
2px 2px 20px rgb(0, 0, 0);
|
||||
max-width: 800px;
|
||||
box-sizing: border-box;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
max-height: calc(100% - 32px);
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.rgthree-dialog-container {
|
||||
// padding: 16px;
|
||||
> * {
|
||||
padding: 8px 16px;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 16px;
|
||||
}
|
||||
&:last-child {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rgthree-dialog.-iconed::after {
|
||||
content: "";
|
||||
font-size: 276px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
opacity: 0.15;
|
||||
display: block;
|
||||
width: 237px;
|
||||
overflow: hidden;
|
||||
height: 186px;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
.rgthree-dialog.-iconed.-help::after {
|
||||
content: "🛟";
|
||||
}
|
||||
.rgthree-dialog.-iconed.-settings::after {
|
||||
content: "⚙️";
|
||||
}
|
||||
|
||||
@media (max-width: 832px) {
|
||||
.rgthree-dialog {
|
||||
max-width: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
.rgthree-dialog-container-title > svg:first-child {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
.rgthree-dialog-container-title h2 {
|
||||
font-size: calc(22rem / 16);
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-title h2 small {
|
||||
font-size: calc(13rem / 16);
|
||||
font-weight: normal;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-content {
|
||||
overflow: auto;
|
||||
max-height: calc(100vh - 200px); /* Arbitrary height to copensate for margin, title, and footer.*/
|
||||
}
|
||||
.rgthree-dialog-container-content p {
|
||||
font-size: calc(13rem / 16);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-content ul li p {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-content ul li p + p {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-content ul li ul {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-content p code {
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
margin: 0px 2px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body.rgthree-dialog-open > *:not(.rgthree-dialog):not(.rgthree-top-messages-container) {
|
||||
filter: blur(5px);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
|
||||
.rgthree-lora-chooser-dialog {
|
||||
max-width: 100%;
|
||||
|
||||
|
||||
.rgthree-dialog-container-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.rgthree-dialog-container-title h2 {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.rgthree-lora-chooser-search {
|
||||
margin-left: auto;
|
||||
border-radius: 50px;
|
||||
width: 50%;
|
||||
max-width: 170px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.rgthree-lora-chooser-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.rgthree-lora-filters-container {
|
||||
svg {width: 16px; height: 16px;}
|
||||
}
|
||||
|
||||
.rgthree-dialog-container-content {
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.rgthree-button-reset {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
> svg {width: 100%; height: 100%;}
|
||||
|
||||
}
|
||||
|
||||
ul.rgthree-lora-chooser-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: start;
|
||||
justify-content: space-around;
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 170px;
|
||||
max-width: 100%;
|
||||
margin: 8px 8px 16px;
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
margin: 0;
|
||||
z-index: 2;
|
||||
appearance: none;
|
||||
background-color: #fff;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(120,120,120,1);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
&:checked {
|
||||
opacity: 1;
|
||||
background: #0060df;
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: inset 100px 100px #fff;
|
||||
clip-path: polygon(40.13% 68.39%, 23.05% 51.31%, 17.83% 48.26%, 12.61% 49.57%, 9.57% 53.04%, 8% 60%, 34.13% 85.87%, 39.82% 89.57%, 45.88% 86.73%, 90.66% 32.39%, 88.92% 26.1%, 83.03% 22.17%, 76.94% 22.62%)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
figure {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(120, 120, 120, .8);
|
||||
background: rgba(120, 120, 120, .5);
|
||||
width: 100%;
|
||||
padding-top: 120%;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
opacity: 0.75;
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
&::before {
|
||||
content: 'No image.';
|
||||
color: rgba(200, 200, 200, .8);
|
||||
position: absolute;
|
||||
display: block;
|
||||
inset: 0;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
> img, > video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
div {
|
||||
word-wrap: break-word;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&:hover figure::after{
|
||||
box-shadow: 0px 2px 6px rgba(0,0,0,0.75);
|
||||
}
|
||||
:checked ~ figure::after {
|
||||
box-shadow: 0 0 5px #fff, 0px 0px 15px rgba(49, 131, 255, 0.88), inset 0 0 3px #fff, inset 0px 0px 5px rgba(49, 131, 255, 0.88)
|
||||
}
|
||||
|
||||
&:hover *,
|
||||
&:hover input[type="checkbox"],
|
||||
:checked ~ * {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
|
||||
.rgthree-info-dialog {
|
||||
|
||||
width: 90vw;
|
||||
max-width: 960px;
|
||||
|
||||
.rgthree-info-area {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
|
||||
> li {
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
|
||||
+ li {
|
||||
margin-left: 6px;
|
||||
}
|
||||
&:not(.-link) + li.-link {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&.rgthree-info-tag > * {
|
||||
min-height: 24px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
color: rgba(255,255,255,0.85);
|
||||
background: rgb(69, 92, 85);;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
height: 1.6em;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
padding-bottom: .1em;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5);
|
||||
|
||||
> svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&[href] {
|
||||
box-shadow: inset 0px 1px 0px rgba(255,255,255,0.25), inset 0px -1px 0px rgba(0,0,0,0.66);
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// &.-civitai > * {
|
||||
// color: #ddd;
|
||||
// background: #1b65aa;
|
||||
// transition: all 0.15s ease-in-out;
|
||||
// &:hover {
|
||||
// color: #fff;
|
||||
// border-color: #1971c2;
|
||||
// background: #1971c2;
|
||||
// }
|
||||
// }
|
||||
&.-type > * {
|
||||
background: rgb(73, 54, 94);
|
||||
color: rgb(228, 209, 248);
|
||||
}
|
||||
|
||||
&.rgthree-info-menu {
|
||||
margin-left: auto;
|
||||
|
||||
:not(#fakeid) & .rgthree-button {
|
||||
margin: 0;
|
||||
min-height: 24px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rgthree-info-table {
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0px;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
tr.editable button {
|
||||
display: flex;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg + svg {display: none;}
|
||||
}
|
||||
tr.editable.-rgthree-editing button {
|
||||
svg {display: none;}
|
||||
svg + svg {display: inline-block;}
|
||||
}
|
||||
|
||||
td {
|
||||
position: relative;
|
||||
border: 1px solid rgba(255,255,255,0.25);
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
|
||||
&:first-child {
|
||||
background: rgba(255,255,255,0.075);
|
||||
width: 10px; // Small, so it doesn't adjust.
|
||||
> *:first-child {
|
||||
white-space: nowrap;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
opacity: 0.75;
|
||||
|
||||
> [data-action] {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a, a:hover, a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1.3333em;
|
||||
height: 1.3333em;
|
||||
vertical-align: -0.285em;
|
||||
|
||||
&.logo-civitai {
|
||||
margin-right: 0.3333em;
|
||||
}
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
display: block;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
> input, > textarea{
|
||||
padding: 5px 10px;
|
||||
border: 0;
|
||||
box-shadow: inset 1px 1px 5px 0px rgba(0,0,0,0.5);
|
||||
font: inherit;
|
||||
appearance: none;
|
||||
background: #fff;
|
||||
color: #121212;
|
||||
resize: vertical;
|
||||
|
||||
&:only-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:not(#fakeid) & .rgthree-button[data-action="fetch-civitai"] {
|
||||
font-size: inherit;
|
||||
padding: 6px 16px;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
tr[data-field-name="userNote"] td > span:first-child {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
tr.rgthree-info-table-break-row td {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 12px 4px 4px;
|
||||
font-size: 1.2em;
|
||||
|
||||
> small {
|
||||
font-style: italic;
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
td .-help {
|
||||
border: 1px solid currentColor;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 6px;
|
||||
line-height: 1;
|
||||
font-size: 11px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
cursor: help;
|
||||
&::before {
|
||||
content: '?';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
td > ul.rgthree-info-trained-words-list {
|
||||
list-style: none;
|
||||
padding: 2px 8px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
max-height: 15vh;
|
||||
overflow: auto;
|
||||
|
||||
> li {
|
||||
display: inline-flex;
|
||||
margin: 2px;
|
||||
vertical-align: top;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
color: rgba(255,255,255,0.85);
|
||||
background: rgb(73, 91, 106);
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
height: 1.6em;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: inset 0px 0px 0 1px rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
max-width: 183px;
|
||||
|
||||
&:hover {
|
||||
background: rgb(68, 109, 142);
|
||||
}
|
||||
|
||||
> svg {
|
||||
width: auto;
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
> span {
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
padding-bottom: .1em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> small {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 0.5em;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
&.-rgthree-is-selected {
|
||||
background: rgb(42, 126, 193);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rgthree-info-images {
|
||||
list-style:none;
|
||||
padding:0;
|
||||
margin:0;
|
||||
scroll-snap-type: x mandatory;
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
overflow: auto;
|
||||
|
||||
> li {
|
||||
scroll-snap-align: start;
|
||||
max-width: 90%;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 6px;
|
||||
font-size: 0;
|
||||
position: relative;
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
position: static;
|
||||
|
||||
video, img {
|
||||
max-height: 45vh;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
background: rgba(0,0,0,0.85);
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
transition: all 0.25s ease-in-out;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
margin: 2px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
word-break: break-word;
|
||||
|
||||
label {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 10px;
|
||||
margin-left: 4px;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:empty {
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
content: 'No data.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover figure figcaption {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
.rgthree-info-table {
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rgthree-info-civitai-link {
|
||||
margin: 8px;
|
||||
color: #eee;
|
||||
|
||||
a, a:hover, a:visited {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
> svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
122
custom_nodes/rgthree-comfy/src_web/common/css/menu.scss
Normal file
122
custom_nodes/rgthree-comfy/src_web/common/css/menu.scss
Normal file
@@ -0,0 +1,122 @@
|
||||
|
||||
|
||||
.rgthree-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.08s ease-in-out;
|
||||
|
||||
color: #dde;
|
||||
background-color: #111;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 0 10px black !important;
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
padding: 4px 6px;
|
||||
z-index: 9999;
|
||||
white-space: nowrap;
|
||||
|
||||
&[role="button"] {
|
||||
background-color: var(--comfy-menu-bg) !important;
|
||||
color: var(--input-text);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
filter: brightness(155%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[state^="measuring"] {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
&[state="open"] {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.rgthree-top-menu {
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
background: var(--content-bg);
|
||||
color: var(--content-fg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
* {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
|
||||
> li:not(#fakeid) {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
> button {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px 8px 8px;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
width: auto;
|
||||
margin-inline-end: 0.6em;
|
||||
|
||||
&.github-star {
|
||||
fill: rgb(227, 179, 65);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.rgthree-message {
|
||||
// ComfyUI's code has strange behavior that that always puts the popupat to if its less than
|
||||
// 30px... we'll force our message to be at least 32px tall so it won't do that unless it's
|
||||
// actually on the bottom.
|
||||
min-height: 32px;
|
||||
> span {
|
||||
padding: 8px 12px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.-modal::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
background: #0001;
|
||||
}
|
||||
}
|
||||
|
||||
body.rgthree-modal-menu-open > *:not(.rgthree-menu):not(.rgthree-top-messages-container) {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
html, body {
|
||||
|
||||
}
|
||||
html {
|
||||
font-size: 100%;
|
||||
overflow-y: scroll;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit
|
||||
}
|
||||
|
||||
:root {
|
||||
--header-height: 56px;
|
||||
--progress-height: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.-bevel {
|
||||
position: relative;
|
||||
}
|
||||
.-bevel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid red;
|
||||
border-color: rgba(255,255,255,0.15) rgba(255,255,255,0.15) rgba(0,0,0,0.5) rgba(0,0,0,0.5);
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background: #202020;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: calc(16 * 0.0625rem);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding-top: calc(var(--header-height) + var(--progress-height));
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
height: var( --header-height);
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: #353535;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
165
custom_nodes/rgthree-comfy/src_web/common/dialog.ts
Normal file
165
custom_nodes/rgthree-comfy/src_web/common/dialog.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { LGraphNode, LGraphNodeConstructor } from "@comfyorg/frontend";
|
||||
import { createElement as $el, getClosestOrSelf, setAttributes } from "./utils_dom.js";
|
||||
|
||||
type RgthreeDialogButton = {
|
||||
label: string;
|
||||
className?: string;
|
||||
closes?: boolean;
|
||||
disabled?: boolean;
|
||||
callback?: (e: PointerEvent | MouseEvent) => void;
|
||||
};
|
||||
|
||||
export type RgthreeDialogOptions = {
|
||||
content: string | HTMLElement | HTMLElement[];
|
||||
class?: string | string[];
|
||||
title?: string | HTMLElement | HTMLElement[];
|
||||
closeX?: boolean;
|
||||
closeOnEsc?: boolean;
|
||||
closeOnModalClick?: boolean;
|
||||
closeButtonLabel?: string | boolean;
|
||||
buttons?: RgthreeDialogButton[];
|
||||
onBeforeClose?: () => Promise<boolean> | boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Dialog that shows content, and closes.
|
||||
*/
|
||||
export class RgthreeDialog extends EventTarget {
|
||||
element: HTMLDialogElement;
|
||||
contentElement: HTMLDivElement;
|
||||
titleElement: HTMLDivElement;
|
||||
options: RgthreeDialogOptions;
|
||||
|
||||
constructor(options: RgthreeDialogOptions) {
|
||||
super();
|
||||
this.options = options;
|
||||
let container = $el("div.rgthree-dialog-container");
|
||||
this.element = $el("dialog", {
|
||||
classes: ["rgthree-dialog", options.class || ""],
|
||||
child: container,
|
||||
parent: document.body,
|
||||
events: {
|
||||
click: (event: MouseEvent) => {
|
||||
// Close the dialog if we've clicked outside of our container. The dialog modal will
|
||||
// report itself as the dialog itself, so we use the inner container div (and CSS to
|
||||
// remove default padding from the dialog element).
|
||||
if (
|
||||
!this.element.open ||
|
||||
event.target === container ||
|
||||
getClosestOrSelf(event.target, `.rgthree-dialog-container`) === container
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return this.close();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.element.addEventListener("close", (event) => {
|
||||
this.onDialogElementClose();
|
||||
});
|
||||
|
||||
this.titleElement = $el("div.rgthree-dialog-container-title", {
|
||||
parent: container,
|
||||
children: !options.title
|
||||
? null
|
||||
: options.title instanceof Element || Array.isArray(options.title)
|
||||
? options.title
|
||||
: typeof options.title === "string"
|
||||
? !options.title.includes("<h2")
|
||||
? $el("h2", { html: options.title })
|
||||
: options.title
|
||||
: options.title,
|
||||
});
|
||||
|
||||
this.contentElement = $el("div.rgthree-dialog-container-content", {
|
||||
parent: container,
|
||||
child: options.content,
|
||||
});
|
||||
|
||||
const footerEl = $el("footer.rgthree-dialog-container-footer", { parent: container });
|
||||
for (const button of options.buttons || []) {
|
||||
$el("button", {
|
||||
text: button.label,
|
||||
className: button.className,
|
||||
disabled: !!button.disabled,
|
||||
parent: footerEl,
|
||||
events: {
|
||||
click: (e: MouseEvent) => {
|
||||
button.callback?.(e);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (options.closeButtonLabel !== false) {
|
||||
$el("button", {
|
||||
text: options.closeButtonLabel || "Close",
|
||||
className: "rgthree-button",
|
||||
parent: footerEl,
|
||||
events: {
|
||||
click: (e: MouseEvent) => {
|
||||
this.close(e);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTitle(content: string | HTMLElement | HTMLElement[]) {
|
||||
const title =
|
||||
typeof content !== "string" || content.includes("<h2")
|
||||
? content
|
||||
: $el("h2", { html: content });
|
||||
setAttributes(this.titleElement, { children: title });
|
||||
}
|
||||
|
||||
setContent(content: string | HTMLElement | HTMLElement[]) {
|
||||
setAttributes(this.contentElement, { children: content });
|
||||
}
|
||||
|
||||
show() {
|
||||
document.body.classList.add("rgthree-dialog-open");
|
||||
this.element.showModal();
|
||||
this.dispatchEvent(new CustomEvent("show"));
|
||||
return this;
|
||||
}
|
||||
|
||||
async close(e?: MouseEvent | PointerEvent) {
|
||||
if (this.options.onBeforeClose && !(await this.options.onBeforeClose())) {
|
||||
return;
|
||||
}
|
||||
this.element.close();
|
||||
}
|
||||
|
||||
onDialogElementClose() {
|
||||
document.body.classList.remove("rgthree-dialog-open");
|
||||
this.element.remove();
|
||||
this.dispatchEvent(new CustomEvent("close", this.getCloseEventDetail()));
|
||||
}
|
||||
|
||||
protected getCloseEventDetail(): { detail: any } {
|
||||
return { detail: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A help extension for the dialog class that standardizes help content.
|
||||
*/
|
||||
export class RgthreeHelpDialog extends RgthreeDialog {
|
||||
constructor(
|
||||
node: LGraphNode | LGraphNodeConstructor,
|
||||
content: string,
|
||||
opts: Partial<RgthreeDialogOptions> = {},
|
||||
) {
|
||||
const title = (node.type || node.title || "").replace(
|
||||
/\s*\(rgthree\).*/,
|
||||
" <small>by rgthree</small>",
|
||||
);
|
||||
const options = Object.assign({}, opts, {
|
||||
class: "-iconed -help",
|
||||
title,
|
||||
content,
|
||||
});
|
||||
super(options);
|
||||
}
|
||||
}
|
||||
529
custom_nodes/rgthree-comfy/src_web/common/link_fixer.ts
Normal file
529
custom_nodes/rgthree-comfy/src_web/common/link_fixer.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LLink,
|
||||
ISlotType,
|
||||
INodeOutputSlot,
|
||||
INodeInputSlot,
|
||||
SerialisedLLinkArray,
|
||||
LinkId,
|
||||
ISerialisedNode,
|
||||
ISerialisedGraph,
|
||||
NodeId,
|
||||
} from "@comfyorg/frontend";
|
||||
|
||||
/**
|
||||
* The bad links data returned from either a fixer `check()`, or the results of a `fix()` call.
|
||||
*/
|
||||
export interface BadLinksData<T = ISerialisedGraph | LGraph> {
|
||||
hasBadLinks: boolean;
|
||||
graph: T;
|
||||
patches: number;
|
||||
deletes: number;
|
||||
}
|
||||
|
||||
enum IoDirection {
|
||||
INPUT,
|
||||
OUTPUT,
|
||||
}
|
||||
|
||||
/**
|
||||
* Data interface that mimics a nodes `inputs` and `outputs` holding the _to be_ mutated node data
|
||||
* during a check.
|
||||
*/
|
||||
interface PatchedNodeSlots {
|
||||
[nodeId: string]: {
|
||||
inputs?: {[slot: number]: number | null};
|
||||
outputs?: {
|
||||
[slots: number]: {
|
||||
links: number[];
|
||||
changes: {[linkId: number]: "ADD" | "REMOVE"};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Link data derived from either a ISerialisedGraph or LGraph `links` property.
|
||||
*/
|
||||
interface LinkData {
|
||||
id: LinkId;
|
||||
origin_id: NodeId;
|
||||
origin_slot: number;
|
||||
target_id: NodeId;
|
||||
target_slot: number;
|
||||
type: ISlotType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of links data for the given links type; either from an LGraph or SerializedGraph.
|
||||
*/
|
||||
function getLinksData(
|
||||
links: ISerialisedGraph["links"] | LGraph["links"] | {[key: string]: LLink},
|
||||
): LinkData[] {
|
||||
if (links instanceof Map) {
|
||||
const data: LinkData[] = [];
|
||||
for (const [key, llink] of links.entries()) {
|
||||
if (!llink) continue;
|
||||
data.push(llink);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
// This is apparently marked deprecated in ComfyUI but who knows if we would get stale data in
|
||||
// here that's not a map (handled above). Go ahead and handle it anyway.
|
||||
if (!Array.isArray(links)) {
|
||||
const data: LinkData[] = [];
|
||||
for (const key in links) {
|
||||
const llink = (links.hasOwnProperty(key) && links[key]) || null;
|
||||
if (!llink) continue;
|
||||
data.push(llink);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
return links.map((link: SerialisedLLinkArray) => ({
|
||||
id: link[0],
|
||||
origin_id: link[1],
|
||||
origin_slot: link[2],
|
||||
target_id: link[3],
|
||||
target_slot: link[4],
|
||||
type: link[5],
|
||||
}));
|
||||
}
|
||||
|
||||
/** The instruction data for fixing a node's inputs or outputs. */
|
||||
interface WorkflowLinkFixerNodeInstruction {
|
||||
node: ISerialisedNode | LGraphNode;
|
||||
op: "REMOVE" | "ADD";
|
||||
dir: IoDirection;
|
||||
slot: number;
|
||||
linkId: number;
|
||||
linkIdToUse: number | null;
|
||||
}
|
||||
|
||||
/** The instruction data for fixing a link from a workflow links. */
|
||||
interface WorkflowLinkFixerLinksInstruction {
|
||||
op: "DELETE";
|
||||
linkId: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
type WorkflowLinkFixerInstruction =
|
||||
| WorkflowLinkFixerNodeInstruction
|
||||
| WorkflowLinkFixerLinksInstruction;
|
||||
|
||||
/**
|
||||
* The WorkflowLinkFixer for either ISerialisedGraph or a live LGraph.
|
||||
*
|
||||
* Use `WorkflowLinkFixer.create(graph: ISerialisedGraph | LGraph)` to create a new instance.
|
||||
*/
|
||||
export abstract class WorkflowLinkFixer<
|
||||
G extends ISerialisedGraph | LGraph,
|
||||
N extends ISerialisedNode | LGraphNode,
|
||||
> {
|
||||
silent: boolean = false;
|
||||
checkedData: BadLinksData<G> | null = null;
|
||||
|
||||
protected logger: {log: (...args: any[]) => void} = console;
|
||||
protected graph: G;
|
||||
protected patchedNodeSlots: PatchedNodeSlots = {};
|
||||
protected instructions: WorkflowLinkFixerInstruction[] = [];
|
||||
|
||||
/**
|
||||
* Creates the WorkflowLinkFixer for the given graph type.
|
||||
*/
|
||||
static create(graph: ISerialisedGraph): WorkflowLinkFixerSerialized;
|
||||
static create(graph: LGraph): WorkflowLinkFixerGraph;
|
||||
static create(
|
||||
graph: ISerialisedGraph | LGraph,
|
||||
): WorkflowLinkFixerSerialized | WorkflowLinkFixerGraph {
|
||||
if (typeof (graph as LGraph).getNodeById === "function") {
|
||||
return new WorkflowLinkFixerGraph(graph as LGraph);
|
||||
}
|
||||
return new WorkflowLinkFixerSerialized(graph as ISerialisedGraph);
|
||||
}
|
||||
|
||||
protected constructor(graph: G) {
|
||||
this.graph = graph;
|
||||
}
|
||||
|
||||
abstract getNodeById(id: NodeId): N | null;
|
||||
abstract deleteGraphLink(id: LinkId): true | string;
|
||||
|
||||
/**
|
||||
* Checks the current graph data for any bad links.
|
||||
*/
|
||||
check(force: boolean = false): BadLinksData<G> {
|
||||
if (this.checkedData && !force) {
|
||||
return {...this.checkedData};
|
||||
}
|
||||
this.instructions = [];
|
||||
this.patchedNodeSlots = {};
|
||||
|
||||
const instructions: (WorkflowLinkFixerInstruction | null)[] = [];
|
||||
|
||||
const links: LinkData[] = getLinksData(this.graph.links);
|
||||
links.reverse();
|
||||
for (const link of links) {
|
||||
if (!link) continue;
|
||||
|
||||
const originNode = this.getNodeById(link.origin_id);
|
||||
const originHasLink = () =>
|
||||
this.nodeHasLinkId(originNode!, IoDirection.OUTPUT, link.origin_slot, link.id);
|
||||
const patchOrigin = (op: "ADD" | "REMOVE", id = link.id) =>
|
||||
this.getNodePatchInstruction(originNode!, IoDirection.OUTPUT, link.origin_slot, id, op);
|
||||
|
||||
const targetNode = this.getNodeById(link.target_id);
|
||||
const targetHasLink = () =>
|
||||
this.nodeHasLinkId(targetNode!, IoDirection.INPUT, link.target_slot, link.id);
|
||||
const targetHasAnyLink = () =>
|
||||
this.nodeHasAnyLink(targetNode!, IoDirection.INPUT, link.target_slot);
|
||||
const patchTarget = (op: "ADD" | "REMOVE", id = link.id) =>
|
||||
this.getNodePatchInstruction(targetNode!, IoDirection.INPUT, link.target_slot, id, op);
|
||||
|
||||
const originLog = `origin(${link.origin_id}).outputs[${link.origin_slot}].links`;
|
||||
const targetLog = `target(${link.target_id}).inputs[${link.target_slot}].link`;
|
||||
|
||||
if (!originNode || !targetNode) {
|
||||
if (!originNode && !targetNode) {
|
||||
// This can fall through and continue; we remove it after this loop.
|
||||
} else if (!originNode && targetNode) {
|
||||
this.log(
|
||||
`Link ${link.id} is funky... ` +
|
||||
`origin ${link.origin_id} does not exist, but target ${link.target_id} does.`,
|
||||
);
|
||||
if (targetHasLink()) {
|
||||
this.log(` > [PATCH] ${targetLog} does have link, will remove the inputs' link first.`);
|
||||
instructions.push(patchTarget("REMOVE", -1));
|
||||
}
|
||||
} else if (!targetNode && originNode) {
|
||||
this.log(
|
||||
`Link ${link.id} is funky... ` +
|
||||
`target ${link.target_id} does not exist, but origin ${link.origin_id} does.`,
|
||||
);
|
||||
if (originHasLink()) {
|
||||
this.log(` > [PATCH] Origin's links' has ${link.id}; will remove the link first.`);
|
||||
instructions.push(patchOrigin("REMOVE"));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetHasLink() || originHasLink()) {
|
||||
if (!originHasLink()) {
|
||||
this.log(
|
||||
`${link.id} is funky... ${originLog} does NOT contain it, but ${targetLog} does.`,
|
||||
);
|
||||
this.log(` > [PATCH] Attempt a fix by adding this ${link.id} to ${originLog}.`);
|
||||
instructions.push(patchOrigin("ADD"));
|
||||
} else if (!targetHasLink()) {
|
||||
this.log(
|
||||
`${link.id} is funky... ${targetLog} is NOT correct (is ${
|
||||
targetNode.inputs?.[link.target_slot]?.link
|
||||
}), but ${originLog} contains it`,
|
||||
);
|
||||
if (!targetHasAnyLink()) {
|
||||
this.log(` > [PATCH] ${targetLog} is not defined, will set to ${link.id}.`);
|
||||
let instruction = patchTarget("ADD");
|
||||
if (!instruction) {
|
||||
this.log(
|
||||
` > [PATCH] Nvm, ${targetLog} already patched. Removing ${link.id} from ${originLog}.`,
|
||||
);
|
||||
instruction = patchOrigin("REMOVE");
|
||||
}
|
||||
instructions.push(instruction);
|
||||
} else {
|
||||
this.log(` > [PATCH] ${targetLog} is defined, removing ${link.id} from ${originLog}.`);
|
||||
instructions.push(patchOrigin("REMOVE"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we've cleaned up the inputs, outputs, run through it looking for dangling links.,
|
||||
for (let link of links) {
|
||||
if (!link) continue;
|
||||
const originNode = this.getNodeById(link.origin_id);
|
||||
const targetNode = this.getNodeById(link.target_id);
|
||||
if (!originNode && !targetNode) {
|
||||
instructions.push({
|
||||
op: "DELETE",
|
||||
linkId: link.id,
|
||||
reason: `Both nodes #${link.origin_id} & #${link.target_id} are removed`,
|
||||
});
|
||||
}
|
||||
// Now that we've manipulated the linking, check again if they both exist.
|
||||
if (
|
||||
(!originNode ||
|
||||
!this.nodeHasLinkId(originNode, IoDirection.OUTPUT, link.origin_slot, link.id)) &&
|
||||
(!targetNode ||
|
||||
!this.nodeHasLinkId(targetNode, IoDirection.INPUT, link.target_slot, link.id))
|
||||
) {
|
||||
instructions.push({
|
||||
op: "DELETE",
|
||||
linkId: link.id,
|
||||
reason:
|
||||
`both origin node #${link.origin_id} ` +
|
||||
`${!originNode ? "is removed" : `is missing link id output slot ${link.origin_slot}`}` +
|
||||
`and target node #${link.target_id} ` +
|
||||
`${!targetNode ? "is removed" : `is missing link id input slot ${link.target_slot}`}.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
this.instructions = instructions.filter((i) => !!i);
|
||||
this.checkedData = {
|
||||
hasBadLinks: !!this.instructions.length,
|
||||
graph: this.graph,
|
||||
patches: this.instructions.filter((i) => !!(i as WorkflowLinkFixerNodeInstruction).node)
|
||||
.length,
|
||||
deletes: this.instructions.filter((i) => i.op === "DELETE").length,
|
||||
};
|
||||
return {...this.checkedData};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes a checked graph by running through the instructions generated during the check run. Also
|
||||
* double-checks for inconsistencies after the fix, recursively calling itself up to five times
|
||||
* before giving up.
|
||||
*/
|
||||
fix(force: boolean = false, times?: number): BadLinksData<G> {
|
||||
if (!this.checkedData || force) {
|
||||
this.check(force);
|
||||
}
|
||||
let patches = 0;
|
||||
let deletes = 0;
|
||||
for (const instruction of this.instructions) {
|
||||
if ((instruction as WorkflowLinkFixerNodeInstruction).node) {
|
||||
let {node, slot, linkIdToUse, dir, op} = instruction as WorkflowLinkFixerNodeInstruction;
|
||||
if (dir == IoDirection.INPUT) {
|
||||
node.inputs = node.inputs || [];
|
||||
const old = node.inputs[slot]?.link;
|
||||
node.inputs[slot] = node.inputs[slot] || ({} as INodeInputSlot);
|
||||
node.inputs[slot].link = linkIdToUse;
|
||||
this.log(`Node #${node.id}: Set link ${linkIdToUse} to input slot ${slot} (was ${old})`);
|
||||
} else if (op === "ADD" && linkIdToUse != null) {
|
||||
node.outputs = node.outputs || [];
|
||||
node.outputs[slot] = node.outputs[slot] || ({} as INodeOutputSlot);
|
||||
node.outputs[slot].links = node.outputs[slot].links || [];
|
||||
node.outputs[slot].links.push(linkIdToUse);
|
||||
this.log(`Node #${node.id}: Add link ${linkIdToUse} to output slot #${slot}`);
|
||||
} else if (op === "REMOVE" && linkIdToUse != null) {
|
||||
// We should never not have this data since the check call would have found it to be
|
||||
// removed, but we can be safe and appease TS compiler at the same time.
|
||||
if (node.outputs?.[slot]?.links?.length === undefined) {
|
||||
this.log(
|
||||
`Node #${node.id}: Couldn't remove link ${linkIdToUse} from output slot #${slot}` +
|
||||
` because it didn't exist.`,
|
||||
);
|
||||
} else {
|
||||
let linkIdIndex = node.outputs![slot].links.indexOf(linkIdToUse);
|
||||
node.outputs[slot].links.splice(linkIdIndex, 1);
|
||||
this.log(`Node #${node.id}: Remove link ${linkIdToUse} from output slot #${slot}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unhandled Node Instruction");
|
||||
}
|
||||
patches++;
|
||||
} else if (instruction.op === "DELETE") {
|
||||
const wasDeleted = this.deleteGraphLink(instruction.linkId);
|
||||
if (wasDeleted === true) {
|
||||
this.log(`Link #${instruction.linkId}: Removed workflow link b/c ${instruction.reason}`);
|
||||
} else {
|
||||
this.log(`Error Link #${instruction.linkId} was not removed!`);
|
||||
}
|
||||
deletes += wasDeleted ? 1 : 0;
|
||||
} else {
|
||||
throw new Error("Unhandled Instruction");
|
||||
}
|
||||
}
|
||||
|
||||
const newCheck = this.check(force);
|
||||
times = times == null ? 5 : times;
|
||||
let newFix = null;
|
||||
// If we still have bad links, then recurse (up to five times).
|
||||
if (newCheck.hasBadLinks && times > 0) {
|
||||
newFix = this.fix(true, times - 1);
|
||||
}
|
||||
|
||||
return {
|
||||
hasBadLinks: newFix?.hasBadLinks ?? newCheck.hasBadLinks,
|
||||
graph: this.graph,
|
||||
patches: patches + (newFix?.patches ?? 0),
|
||||
deletes: deletes + (newFix?.deletes ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
/** Logs if not silent. */
|
||||
protected log(...args: any[]) {
|
||||
if (this.silent) return;
|
||||
this.logger.log(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches a node for a check run, returning the instruction that would be made.
|
||||
*/
|
||||
private getNodePatchInstruction(
|
||||
node: N,
|
||||
ioDir: IoDirection,
|
||||
slot: number,
|
||||
linkId: number,
|
||||
op: "ADD" | "REMOVE",
|
||||
): WorkflowLinkFixerNodeInstruction | null {
|
||||
const nodeId = node.id;
|
||||
this.patchedNodeSlots[nodeId] = this.patchedNodeSlots[nodeId] || {};
|
||||
const patchedNode = this.patchedNodeSlots[nodeId];
|
||||
if (ioDir == IoDirection.INPUT) {
|
||||
patchedNode["inputs"] = patchedNode["inputs"] || {};
|
||||
// We can set to null (delete), so undefined means we haven't set it at all.
|
||||
if (patchedNode["inputs"][slot] !== undefined) {
|
||||
this.log(
|
||||
` > Already set ${nodeId}.inputs[${slot}] to ${patchedNode["inputs"][slot]} Skipping.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
let linkIdToUse = op === "REMOVE" ? null : linkId;
|
||||
patchedNode["inputs"][slot] = linkIdToUse;
|
||||
return {node, dir: ioDir, op, slot, linkId, linkIdToUse};
|
||||
}
|
||||
|
||||
patchedNode["outputs"] = patchedNode["outputs"] || {};
|
||||
patchedNode["outputs"][slot] = patchedNode["outputs"][slot] || {
|
||||
links: [...(node.outputs?.[slot]?.links || [])],
|
||||
changes: {},
|
||||
};
|
||||
if (patchedNode["outputs"][slot]["changes"][linkId] !== undefined) {
|
||||
this.log(
|
||||
` > Already set ${nodeId}.outputs[${slot}] to ${patchedNode["outputs"][slot]}! Skipping.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
patchedNode["outputs"][slot]["changes"][linkId] = op;
|
||||
if (op === "ADD") {
|
||||
let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId);
|
||||
if (linkIdIndex !== -1) {
|
||||
this.log(` > Hmmm.. asked to add ${linkId} but it is already in list...`);
|
||||
return null;
|
||||
}
|
||||
patchedNode["outputs"][slot]["links"].push(linkId);
|
||||
return {node, dir: ioDir, op, slot, linkId, linkIdToUse: linkId};
|
||||
}
|
||||
|
||||
let linkIdIndex = patchedNode["outputs"][slot]["links"].indexOf(linkId);
|
||||
if (linkIdIndex === -1) {
|
||||
this.log(` > Hmmm.. asked to remove ${linkId} but it doesn't exist...`);
|
||||
return null;
|
||||
}
|
||||
patchedNode["outputs"][slot]["links"].splice(linkIdIndex, 1);
|
||||
return {node, dir: ioDir, op, slot, linkId, linkIdToUse: linkId};
|
||||
}
|
||||
|
||||
/** Checks if a node (or patched data) has a linkId. */
|
||||
private nodeHasLinkId(node: N, ioDir: IoDirection, slot: number, linkId: number) {
|
||||
const nodeId = node.id;
|
||||
let has = false;
|
||||
if (ioDir === IoDirection.INPUT) {
|
||||
let nodeHasIt = node.inputs?.[slot]?.link === linkId;
|
||||
if (this.patchedNodeSlots[nodeId]?.["inputs"]) {
|
||||
let patchedHasIt = this.patchedNodeSlots[nodeId]["inputs"][slot] === linkId;
|
||||
has = patchedHasIt;
|
||||
} else {
|
||||
has = nodeHasIt;
|
||||
}
|
||||
} else {
|
||||
let nodeHasIt = node.outputs?.[slot]?.links?.includes(linkId);
|
||||
if (this.patchedNodeSlots[nodeId]?.["outputs"]?.[slot]?.["changes"][linkId]) {
|
||||
let patchedHasIt = this.patchedNodeSlots[nodeId]["outputs"][slot].links.includes(linkId);
|
||||
has = !!patchedHasIt;
|
||||
} else {
|
||||
has = !!nodeHasIt;
|
||||
}
|
||||
}
|
||||
return has;
|
||||
}
|
||||
|
||||
/** Checks if a node (or patched data) has a linkId. */
|
||||
private nodeHasAnyLink(node: N, ioDir: IoDirection, slot: number) {
|
||||
// Patched data should be canonical. We can double check if fixing too.
|
||||
const nodeId = node.id;
|
||||
let hasAny = false;
|
||||
if (ioDir === IoDirection.INPUT) {
|
||||
let nodeHasAny = node.inputs?.[slot]?.link != null;
|
||||
if (this.patchedNodeSlots[nodeId]?.["inputs"]) {
|
||||
let patchedHasAny = this.patchedNodeSlots[nodeId]["inputs"][slot] != null;
|
||||
hasAny = patchedHasAny;
|
||||
} else {
|
||||
hasAny = !!nodeHasAny;
|
||||
}
|
||||
} else {
|
||||
let nodeHasAny = node.outputs?.[slot]?.links?.length;
|
||||
if (this.patchedNodeSlots[nodeId]?.["outputs"]?.[slot]?.["changes"]) {
|
||||
let patchedHasAny = this.patchedNodeSlots[nodeId]["outputs"][slot].links?.length;
|
||||
hasAny = !!patchedHasAny;
|
||||
} else {
|
||||
hasAny = !!nodeHasAny;
|
||||
}
|
||||
}
|
||||
return hasAny;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A WorkflowLinkFixer for serialized data.
|
||||
*/
|
||||
class WorkflowLinkFixerSerialized extends WorkflowLinkFixer<ISerialisedGraph, ISerialisedNode> {
|
||||
constructor(graph: ISerialisedGraph) {
|
||||
super(graph);
|
||||
}
|
||||
|
||||
getNodeById(id: NodeId) {
|
||||
return this.graph.nodes.find((node) => Number(node.id) === id) ?? null;
|
||||
}
|
||||
|
||||
override fix(force: boolean = false, times?: number) {
|
||||
const ret = super.fix(force, times);
|
||||
// If we're a serialized graph, we can filter out the links because it's just an array.
|
||||
this.graph.links = this.graph.links.filter((l) => !!l);
|
||||
return ret;
|
||||
}
|
||||
|
||||
deleteGraphLink(id: LinkId) {
|
||||
// Sometimes we got objects instead of serializzed array for links if passed after ComfyUI's
|
||||
// loadGraphData modifies the data. Let's find the id handling the bastardized objects just in
|
||||
// case.
|
||||
const idx = this.graph.links.findIndex((l) => l && (l[0] === id || (l as any).id === id));
|
||||
if (idx === -1) {
|
||||
return `Link #${id} not found in workflow links.`;
|
||||
}
|
||||
this.graph.links.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A WorkflowLinkFixer for live LGraph data.
|
||||
*/
|
||||
class WorkflowLinkFixerGraph extends WorkflowLinkFixer<LGraph, LGraphNode> {
|
||||
constructor(graph: LGraph) {
|
||||
super(graph);
|
||||
}
|
||||
|
||||
getNodeById(id: NodeId) {
|
||||
return this.graph.getNodeById(id) ?? null;
|
||||
}
|
||||
|
||||
deleteGraphLink(id: LinkId) {
|
||||
if (this.graph.links instanceof Map) {
|
||||
if (!this.graph.links.has(id)) {
|
||||
return `Link #${id} not found in workflow links.`;
|
||||
}
|
||||
this.graph.links.delete(id);
|
||||
return true;
|
||||
}
|
||||
if (this.graph.links[id] == null) {
|
||||
return `Link #${id} not found in workflow links.`;
|
||||
}
|
||||
delete this.graph.links[id];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor">
|
||||
<path d="M88.503,158.997 L152.731,196.103 L152.738,196.092 L152.762,196.103 L152.769,196.106 L152.771,196.103 L183.922,142.084 L174.153,136.437 L148.611,180.676 L101.512,153.484 L132.193,30.415 L156.124,71.869 L165.896,66.225 L128.002,0.59 z"></path>
|
||||
<path d="M55.586,148.581l13.44,47.521l0.014,0.051l0.168-0.051l10.689-3.022l-6.589-23.313l45.609,26.335l0.087,0.051l0.027-0.051 l5.617-9.718l-42.648-24.622l35.771-143.45L33.232,164.729l9.77,5.645L55.586,148.581z M87.394,93.484l-16.708,67.018l-5.018-17.747 l-8.028,2.27L87.394,93.484z"></path>
|
||||
<path d="M189.85,107.717 L137.892,137.718 L143.532,147.49 L185.723,123.133 L231.109,201.746 L24.895,201.746 L37.363,180.146 L27.592,174.505 L5.347,213.03 L250.653,213.03 z"></path>
|
||||
<path d="M5.347,247.299v8.111h245.307v-8.111l-41.94-0.003c-1.336,0-2.404-1.065-2.441-2.396v-12.14 c0.037-1.315,1.089-2.368,2.41-2.385h41.972v-8.11H5.347v8.11h41.951c1.338,0.017,2.427,1.104,2.427,2.449v12.01 c0,1.365-1.105,2.462-2.457,2.462L5.347,247.299z M139.438,247.296c-1.334,0-2.406-1.065-2.439-2.396v-12.14 c0.033-1.315,1.085-2.368,2.41-2.385h46.415c1.335,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.459,2.462H139.438z M70.193,247.296c-1.339,0-2.408-1.065-2.441-2.396v-12.14c0.033-1.315,1.086-2.368,2.407-2.385h46.418 c1.336,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.458,2.462H70.193z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
210
custom_nodes/rgthree-comfy/src_web/common/media/svgs.ts
Normal file
210
custom_nodes/rgthree-comfy/src_web/common/media/svgs.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import {createElement as $el} from "../utils_dom.js";
|
||||
|
||||
// Some svg repo : https://www.svgrepo.com/svg/326731/open-outline
|
||||
|
||||
export let logoRgthree: string = "";
|
||||
|
||||
export async function logoRgthreeAsync(): Promise<string> {
|
||||
if (logoRgthree) return logoRgthree;
|
||||
let baseUrl = null;
|
||||
if (window.location.pathname.includes("/rgthree/")) {
|
||||
// Try to find how many relatives paths we need to go back to hit ./rgthree/api
|
||||
const parts = window.location.pathname.split("/rgthree/")[1]?.split("/");
|
||||
if (parts && parts.length) {
|
||||
baseUrl = parts.map(() => "../").join("") + "rgthree";
|
||||
}
|
||||
}
|
||||
baseUrl = baseUrl || "./rgthree";
|
||||
return fetch(`${baseUrl}/logo_markup.svg?fg=currentColor&cssClass=rgthree-logo&w=auto&h=auto`)
|
||||
.then((r) => r.text())
|
||||
.then((t) => {
|
||||
if (t.length < 100) {
|
||||
t = `<svg viewBox="0 0 256 256" fill="currentColor" class="rgthree-logo">
|
||||
<path d="M88.503,158.997 L152.731,196.103 L152.738,196.092 L152.762,196.103 L152.769,196.106 L152.771,196.103 L183.922,142.084 L174.153,136.437 L148.611,180.676 L101.512,153.484 L132.193,30.415 L156.124,71.869 L165.896,66.225 L128.002,0.59 "></path>
|
||||
<path d="M55.586,148.581l13.44,47.521l0.014,0.051l0.168-0.051l10.689-3.022l-6.589-23.313l45.609,26.335l0.087,0.051l0.027-0.051 l5.617-9.718l-42.648-24.622l35.771-143.45L33.232,164.729l9.77,5.645L55.586,148.581z M87.394,93.484l-16.708,67.018l-5.018-17.747 l-8.028,2.27L87.394,93.484z"></path>
|
||||
<path d="M189.85,107.717 L137.892,137.718 L143.532,147.49 L185.723,123.133 L231.109,201.746 L24.895,201.746 L37.363,180.146 L27.592,174.505 L5.347,213.03 L250.653,213.03 "></path>
|
||||
<path d="M5.347,247.299v8.111h245.307v-8.111l-41.94-0.003c-1.336,0-2.404-1.065-2.441-2.396v-12.14 c0.037-1.315,1.089-2.368,2.41-2.385h41.972v-8.11H5.347v8.11h41.951c1.338,0.017,2.427,1.104,2.427,2.449v12.01 c0,1.365-1.105,2.462-2.457,2.462L5.347,247.299z M139.438,247.296c-1.334,0-2.406-1.065-2.439-2.396v-12.14 c0.033-1.315,1.085-2.368,2.41-2.385h46.415c1.335,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.459,2.462H139.438z M70.193,247.296c-1.339,0-2.408-1.065-2.441-2.396v-12.14c0.033-1.315,1.086-2.368,2.407-2.385h46.418 c1.336,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.458,2.462H70.193z"></path>
|
||||
</svg>`;
|
||||
}
|
||||
logoRgthree = t;
|
||||
return t;
|
||||
});
|
||||
}
|
||||
// Kick it off to cache upfront.
|
||||
logoRgthreeAsync();
|
||||
|
||||
export const github = `<svg viewBox="0 0 16 16" fill="currentColor" class="github-logo">
|
||||
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>`;
|
||||
|
||||
export const iconStarFilled = `<svg viewBox="0 0 16 16" fill="currentColor" class="github-star">
|
||||
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"></path>
|
||||
</svg>`;
|
||||
|
||||
export const iconReplace = `<svg viewBox="0 0 52 52" fill="currentColor">
|
||||
<path d="M20,37.5c0-0.8-0.7-1.5-1.5-1.5h-15C2.7,36,2,36.7,2,37.5v11C2,49.3,2.7,50,3.5,50h15c0.8,0,1.5-0.7,1.5-1.5 V37.5z"/>
|
||||
<path d="M8.1,22H3.2c-1,0-1.5,0.9-0.9,1.4l8,8.3c0.4,0.3,1,0.3,1.4,0l8-8.3c0.6-0.6,0.1-1.4-0.9-1.4h-4.7 c0-5,4.9-10,9.9-10V6C15,6,8.1,13,8.1,22z"/>
|
||||
<path d="M41.8,20.3c-0.4-0.3-1-0.3-1.4,0l-8,8.3c-0.6,0.6-0.1,1.4,0.9,1.4h4.8c0,6-4.1,10-10.1,10v6 c9,0,16.1-7,16.1-16H49c1,0,1.5-0.9,0.9-1.4L41.8,20.3z"/>
|
||||
<path d="M50,3.5C50,2.7,49.3,2,48.5,2h-15C32.7,2,32,2.7,32,3.5v11c0,0.8,0.7,1.5,1.5,1.5h15c0.8,0,1.5-0.7,1.5-1.5 V3.5z"/>
|
||||
</svg>`;
|
||||
|
||||
export const iconNode = `<svg viewBox="0 -0.5 25 25" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5 19H9.5C7.29086 19 5.5 17.2091 5.5 15V9C5.5 6.79086 7.29086 5 9.5 5H15.5C17.7091 5 19.5 6.79086 19.5 9V15C19.5 17.2091 17.7091 19 15.5 19Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.5 9.75C19.9142 9.75 20.25 9.41421 20.25 9C20.25 8.58579 19.9142 8.25 19.5 8.25V9.75ZM5.5 8.25C5.08579 8.25 4.75 8.58579 4.75 9C4.75 9.41421 5.08579 9.75 5.5 9.75V8.25ZM11.5 14.25C11.0858 14.25 10.75 14.5858 10.75 15C10.75 15.4142 11.0858 15.75 11.5 15.75V14.25ZM13.5 15.75C13.9142 15.75 14.25 15.4142 14.25 15C14.25 14.5858 13.9142 14.25 13.5 14.25V15.75ZM19.5 8.25H5.5V9.75H19.5V8.25ZM11.5 15.75H13.5V14.25H11.5V15.75Z" fill="currentColor" />
|
||||
</svg>`;
|
||||
|
||||
export const iconGear = `<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7848 0.449982C13.8239 0.449982 14.7167 1.16546 14.9122 2.15495L14.9991 2.59495C15.3408 4.32442 17.1859 5.35722 18.9016 4.7794L19.3383 4.63233C20.3199 4.30175 21.4054 4.69358 21.9249 5.56605L22.7097 6.88386C23.2293 7.75636 23.0365 8.86366 22.2504 9.52253L21.9008 9.81555C20.5267 10.9672 20.5267 13.0328 21.9008 14.1844L22.2504 14.4774C23.0365 15.1363 23.2293 16.2436 22.7097 17.1161L21.925 18.4339C21.4054 19.3064 20.3199 19.6982 19.3382 19.3676L18.9017 19.2205C17.1859 18.6426 15.3408 19.6754 14.9991 21.405L14.9122 21.845C14.7167 22.8345 13.8239 23.55 12.7848 23.55H11.2152C10.1761 23.55 9.28331 22.8345 9.08781 21.8451L9.00082 21.4048C8.65909 19.6754 6.81395 18.6426 5.09822 19.2205L4.66179 19.3675C3.68016 19.6982 2.59465 19.3063 2.07505 18.4338L1.2903 17.1161C0.770719 16.2436 0.963446 15.1363 1.74956 14.4774L2.09922 14.1844C3.47324 13.0327 3.47324 10.9672 2.09922 9.8156L1.74956 9.52254C0.963446 8.86366 0.77072 7.75638 1.2903 6.8839L2.07508 5.56608C2.59466 4.69359 3.68014 4.30176 4.66176 4.63236L5.09831 4.77939C6.81401 5.35722 8.65909 4.32449 9.00082 2.59506L9.0878 2.15487C9.28331 1.16542 10.176 0.449982 11.2152 0.449982H12.7848ZM12 15.3C13.8225 15.3 15.3 13.8225 15.3 12C15.3 10.1774 13.8225 8.69998 12 8.69998C10.1774 8.69998 8.69997 10.1774 8.69997 12C8.69997 13.8225 10.1774 15.3 12 15.3Z" />
|
||||
</svg>`;
|
||||
|
||||
export const checkmark = `<svg viewBox="0 0 32 32" fill="currentColor" class="icon-checkmark">
|
||||
<g transform="translate(-518.000000, -1039.000000)">
|
||||
<path d="M548.783,1040.2 C547.188,1038.57 544.603,1038.57 543.008,1040.2 L528.569,1054.92 L524.96,1051.24 C523.365,1049.62 520.779,1049.62 519.185,1051.24 C517.59,1052.87 517.59,1055.51 519.185,1057.13 L525.682,1063.76 C527.277,1065.39 529.862,1065.39 531.457,1063.76 L548.783,1046.09 C550.378,1044.46 550.378,1041.82 548.783,1040.2"></path>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
export const logoCivitai = `<svg viewBox="0 0 178 178" class="logo-civitai">
|
||||
<defs>
|
||||
<linearGradient id="bgblue" gradientUnits="userSpaceOnUse" x1="89.3" y1="-665.5" x2="89.3" y2="-841.1" gradientTransform="matrix(1 0 0 -1 0 -664)">
|
||||
<stop offset="0" style="stop-color:#1284F7"/>
|
||||
<stop offset="1" style="stop-color:#0A20C9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="#000" d="M13.3,45.4v87.7l76,43.9l76-43.9V45.4l-76-43.9L13.3,45.4z"/>
|
||||
<path style="fill:url(#bgblue);" d="M89.3,29.2l52,30v60l-52,30l-52-30v-60 L89.3,29.2 M89.3,1.5l-76,43.9v87.8l76,43.9l76-43.9V45.4L89.3,1.5z" />
|
||||
<path fill="#FFF" d="M104.1,97.2l-14.9,8.5l-14.9-8.5v-17l14.9-8.5l14.9,8.5h18.2V69.7l-33-19l-33,19v38.1l33,19l33-19V97.2H104.1z" />
|
||||
</svg>`;
|
||||
|
||||
export const iconOutLink = `<svg viewBox="0 0 32 32">
|
||||
<path d="M 18 5 L 18 7 L 23.5625 7 L 11.28125 19.28125 L 12.71875 20.71875 L 25 8.4375 L 25 14 L 27 14 L 27 5 Z M 5 9 L 5 27 L 23 27 L 23 14 L 21 16 L 21 25 L 7 25 L 7 11 L 16 11 L 18 9 Z"></path>
|
||||
</svg>`;
|
||||
|
||||
export const link = `<svg viewBox="0 0 640 512">
|
||||
<path d="M598.6 41.41C570.1 13.8 534.8 0 498.6 0s-72.36 13.8-99.96 41.41l-43.36 43.36c15.11 8.012 29.47 17.58 41.91 30.02c3.146 3.146 5.898 6.518 8.742 9.838l37.96-37.96C458.5 72.05 477.1 64 498.6 64c20.67 0 40.1 8.047 54.71 22.66c14.61 14.61 22.66 34.04 22.66 54.71s-8.049 40.1-22.66 54.71l-133.3 133.3C405.5 343.1 386 352 365.4 352s-40.1-8.048-54.71-22.66C296 314.7 287.1 295.3 287.1 274.6s8.047-40.1 22.66-54.71L314.2 216.4C312.1 212.5 309.9 208.5 306.7 205.3C298.1 196.7 286.8 192 274.6 192c-11.93 0-23.1 4.664-31.61 12.97c-30.71 53.96-23.63 123.6 22.39 169.6C293 402.2 329.2 416 365.4 416c36.18 0 72.36-13.8 99.96-41.41L598.6 241.3c28.45-28.45 42.24-66.01 41.37-103.3C639.1 102.1 625.4 68.16 598.6 41.41zM234 387.4L196.1 425.3C181.5 439.1 162 448 141.4 448c-20.67 0-40.1-8.047-54.71-22.66c-14.61-14.61-22.66-34.04-22.66-54.71s8.049-40.1 22.66-54.71l133.3-133.3C234.5 168 253.1 160 274.6 160s40.1 8.048 54.71 22.66c14.62 14.61 22.66 34.04 22.66 54.71s-8.047 40.1-22.66 54.71L325.8 295.6c2.094 3.939 4.219 7.895 7.465 11.15C341.9 315.3 353.3 320 365.4 320c11.93 0 23.1-4.664 31.61-12.97c30.71-53.96 23.63-123.6-22.39-169.6C346.1 109.8 310.8 96 274.6 96C238.4 96 202.3 109.8 174.7 137.4L41.41 270.7c-27.6 27.6-41.41 63.78-41.41 99.96c-.0001 36.18 13.8 72.36 41.41 99.97C69.01 498.2 105.2 512 141.4 512c36.18 0 72.36-13.8 99.96-41.41l43.36-43.36c-15.11-8.012-29.47-17.58-41.91-30.02C239.6 394.1 236.9 390.7 234 387.4z"/>
|
||||
</svg>`;
|
||||
|
||||
export const pencil = `<svg viewBox="0 0 24 24">
|
||||
<path d="M 16.9375 1.0625 L 3.875 14.125 L 1.0742188 22.925781 L 9.875 20.125 L 22.9375 7.0625 C 22.9375 7.0625 22.8375 4.9615 20.9375 3.0625 C 19.0375 1.1625 16.9375 1.0625 16.9375 1.0625 z M 17.3125 2.6875 C 18.3845 2.8915 19.237984 3.3456094 19.896484 4.0214844 C 20.554984 4.6973594 21.0185 5.595 21.3125 6.6875 L 19.5 8.5 L 15.5 4.5 L 16.9375 3.0625 L 17.3125 2.6875 z M 4.9785156 15.126953 C 4.990338 15.129931 6.1809555 15.430955 7.375 16.625 C 8.675 17.825 8.875 18.925781 8.875 18.925781 L 8.9179688 18.976562 L 5.3691406 20.119141 L 3.8730469 18.623047 L 4.9785156 15.126953 z"/>
|
||||
</svg>`;
|
||||
|
||||
export const dotdotdot = `<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cy="12" r="3" cx="3"></circle>
|
||||
<circle cy="12" r="3" cx="12"></circle>
|
||||
<circle cx="21" cy="12" r="3"></circle>
|
||||
</svg>`;
|
||||
|
||||
export const models = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 4h6v6h-6z"></path>
|
||||
<path d="M14 4h6v6h-6z"></path>
|
||||
<path d="M4 14h6v6h-6z"></path>
|
||||
<path d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||
</svg>`;
|
||||
|
||||
/** https://www.svgrepo.com/svg/402308/pencil */
|
||||
export const pencilColored = `<svg viewBox="0 0 64 64">
|
||||
<path fill="#ffce31" d="M7.934 41.132L39.828 9.246l14.918 14.922l-31.895 31.886z"></path>
|
||||
<path d="M61.3 4.6l-1.9-1.9C55.8-.9 50-.9 46.3 2.7l-6.5 6.5l15 15l6.5-6.5c3.6-3.6 3.6-9.5 0-13.1" fill="#ed4c5c"></path>
|
||||
<path fill="#93a2aa" d="M35.782 13.31l4.1-4.102l14.92 14.92l-4.1 4.101z"></path>
|
||||
<path fill="#c7d3d8" d="M37.338 14.865l4.1-4.101l11.739 11.738l-4.102 4.1z"></path>
|
||||
<path fill="#fed0ac" d="M7.9 41.1l-6.5 17l4.5 4.5l17-6.5z"/>
|
||||
<path d="M.3 61.1c-.9 2.4.3 3.5 2.7 2.6l8.2-3.1l-7.7-7.7l-3.2 8.2" fill="#333"></path>
|
||||
<path fill="#ffdf85" d="M7.89 41.175l27.86-27.86l4.95 4.95l-27.86 27.86z"/>
|
||||
<path fill="#ff8736" d="M17.904 51.142l27.86-27.86l4.95 4.95l-27.86 27.86z"></path>
|
||||
</svg>`;
|
||||
|
||||
/** https://www.svgrepo.com/svg/395640/save */
|
||||
export const diskColored = `<svg viewBox="-0.01 -0.008 100.016 100.016">
|
||||
<path fill="#26f" fill_="#23475F" d="M88.555-.008H83v.016a2 2 0 0 1-2 2H19a2 2 0 0 1-2-2v-.016H4a4 4 0 0 0-4 4v92.016a4 4 0 0 0 4 4h92a4 4 0 0 0 4-4V11.517c.049-.089-11.436-11.454-11.445-11.525z"/>
|
||||
<path fill="#04d" fill_="#1C3C50" d="M81.04 53.008H18.96a2 2 0 0 0-2 2v45h66.08v-45c0-1.106-.895-2-2-2zm-61.957-10h61.834a2 2 0 0 0 2-2V.547A1.993 1.993 0 0 1 81 2.007H19c-.916 0-1.681-.62-1.917-1.46v40.46a2 2 0 0 0 2 2.001z"/>
|
||||
<path fill="#EBF0F1" d="M22 55.977h56a2 2 0 0 1 2 2v37.031a2 2 0 0 1-2 2H22c-1.104 0-2-.396-2-1.5V57.977a2 2 0 0 1 2-2z"/>
|
||||
<path fill="#BCC4C8" d="M25 77.008h50v1H25v-1zm0 10h50v1H25v-1z"/>
|
||||
<path fill="#1C3C50" d="M7 84.008h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm83 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z"/>
|
||||
<path fill="#BCC4C8" d="M37 1.981v36.026a2 2 0 0 0 2 2h39a2 2 0 0 0 2-2V1.981c0 .007-42.982.007-43 0zm37 29.027a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2V10.981a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v20.027z"/>
|
||||
<path fill="#FF9D00" d="M78 55.977H22a2 2 0 0 0-2 2v10.031h60V57.977a2 2 0 0 0-2-2z"/>
|
||||
</svg>`;
|
||||
|
||||
/** https://www.svgrepo.com/svg/229838/folder */
|
||||
export const folderColored = `<svg viewBox="0 0 501.379 501.379">
|
||||
<path style="fill:#EF9F2C;" d="M406.423,93.889H205.889c-17.067,0-30.933-13.867-30.933-30.933s-13.867-30.933-30.933-30.933H30.956
|
||||
c-17.067,0-30.933,13.867-30.933,30.933v375.467c0,17.067,13.867,30.933,30.933,30.933h375.467
|
||||
c17.067,0,30.933-13.867,30.933-30.933v-313.6C436.289,107.756,422.423,93.889,406.423,93.889z"/>
|
||||
<path style="fill:#FEC656;" d="M470.423,157.889H97.089c-13.867,0-26.667,9.6-29.867,22.4l-66.133,249.6
|
||||
c-5.333,19.2,9.6,38.4,29.867,38.4h373.333c13.867,0,26.667-9.6,29.867-22.4l66.133-248.533
|
||||
C505.623,177.089,490.689,157.889,470.423,157.889z"/>
|
||||
</svg>`;
|
||||
|
||||
export const modelsColored = `<svg viewBox="0 0 24 24">
|
||||
<path fill="#aa3366" d="M0 0h10v10h-10z"></path>
|
||||
<path d="M14 0h10v10h-10z" fill="#3366aa"></path>
|
||||
<path d="M0 14h10v10h-10z" fill="#66aa33"></path>
|
||||
<path fill="#dd9922" d="M19 19m-5 0 a5 5 0 1 0 10 0 a5 5 0 1 0 -10 0"></path>
|
||||
</svg>`;
|
||||
|
||||
export const legoBlocksColored = `<svg viewBox="0 0 512 512">
|
||||
<g>
|
||||
<rect x="57.67" style="fill:#00BAB9;" width="101.275" height="78.769"/>
|
||||
<rect x="205.363" style="fill:#00BAB9;" width="101.275" height="78.769"/>
|
||||
<rect x="353.055" style="fill:#00BAB9;" width="101.275" height="78.769"/>
|
||||
</g>
|
||||
<polygon style="fill:#B8DE6F;" points="478.242,289.758 478.242,512 33.758,512 33.758,289.758 256,267.253 "/>
|
||||
<polygon style="fill:#41D4D3;" points="478.242,67.516 478.242,289.758 33.758,289.758 33.758,67.516 57.67,67.516 158.945,67.516
|
||||
205.363,67.516 306.637,67.516 353.055,67.516 454.33,67.516 "/>
|
||||
<g>
|
||||
<circle style="fill:#00BAB9;" cx="402.286" cy="143.473" r="8.44"/>
|
||||
<circle style="fill:#00BAB9;" cx="368.527" cy="177.231" r="8.44"/>
|
||||
</g>
|
||||
<circle style="fill:#7BD288;" cx="109.714" cy="436.044" r="8.44"/>
|
||||
</svg>`;
|
||||
|
||||
export const legoBlockColored = `<svg viewBox="0 0 256 256">
|
||||
<style>
|
||||
.s0 { fill: #ff0000 }
|
||||
.s1 { fill: #c30000 }
|
||||
.s2 { fill: #800000 }
|
||||
.s3 { fill: #cc0000 }
|
||||
.s4 { fill: #e00000 }
|
||||
</style>
|
||||
<g id="Folder 2">
|
||||
<path id="Shape 1 copy 2" class="s0" d="m128 61l116 45-116 139-116-139z"/>
|
||||
<path id="Shape 1" class="s1" d="m12 106l116 45v95l-116-45z"/>
|
||||
<path id="Shape 1 copy" class="s2" d="m244 106l-116 45v95l116-45z"/>
|
||||
<g id="Folder 1">
|
||||
<path id="Shape 2" class="s3" d="m102 111.2c0-6.1 11.4-9.9 25.5-9.9 14.1 0 25.5 3.8 25.5 9.9 0 3.3 0 13.3 0 16.6 0 6.1-11.4 10.9-25.5 10.9-14.1 0-25.5-4.8-25.5-10.9 0-3.3 0-13.3 0-16.6z"/>
|
||||
<path id="Shape 2 copy 4" class="s1" d="m102 111.2c0-6.1 11.4-9.9 25.5-9.9 14.1 0 25.5 3.8 25.5 9.9 0 3.3 0 13.3 0 16.6 0 6.1-11.4 10.9-25.5 10.9-14.1 0-25.5-4.8-25.5-10.9 0-3.3 0-13.3 0-16.6z"/>
|
||||
<path id="Shape 2 copy 2" class="s2" d="m127.5 101.3c14.1 0 25.5 3.8 25.5 9.9 0 3.3 0 13.3 0 16.6 0 6.1-11.4 10.9-25.5 10.9 0-13.1 0-25.7 0-37.4z"/>
|
||||
<path id="Shape 2 copy" class="s0" d="m127.5 118.8c-12.2 0-22-3.4-22-7.6 0-4.2 9.8-7.7 22-7.7 12.2 0 22 3.5 22 7.7 0 4.2-9.8 7.6-22 7.6zm0 0c-12.2 0-22-3.4-22-7.6 0-4.2 9.8-7.7 22-7.7 12.2 0 22 3.5 22 7.7 0 4.2-9.8 7.6-22 7.6z"/>
|
||||
</g>
|
||||
<g id="Folder 1 copy">
|
||||
<path id="Shape 2" class="s4" d="m103 67.5c0-5.8 11-9.5 24.5-9.5 13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5-13.5 0-24.5-4.7-24.5-10.5 0-3.2 0-12.8 0-16z"/>
|
||||
<path id="Shape 2 copy 4" class="s1" d="m103 67.5c0-5.8 11-9.5 24.5-9.5 13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5-13.5 0-24.5-4.7-24.5-10.5 0-3.2 0-12.8 0-16z"/>
|
||||
<path id="Shape 2 copy 2" class="s2" d="m127.5 58c13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5 0-12.6 0-24.8 0-36z"/>
|
||||
<path id="Shape 2 copy" class="s0" d="m127.5 74.9c-11.7 0-21.2-3.3-21.2-7.4 0-4.1 9.5-7.4 21.2-7.4 11.7 0 21.2 3.3 21.2 7.4 0 4.1-9.5 7.4-21.2 7.4zm0 0c-11.7 0-21.2-3.3-21.2-7.4 0-4.1 9.5-7.4 21.2-7.4 11.7 0 21.2 3.3 21.2 7.4 0 4.1-9.5 7.4-21.2 7.4z"/>
|
||||
</g>
|
||||
<g id="Folder 1 copy 2">
|
||||
<path id="Shape 2" class="s4" d="m161 89.5c0-5.8 11-9.5 24.5-9.5 13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5-13.5 0-24.5-4.7-24.5-10.5 0-3.2 0-12.8 0-16z"/>
|
||||
<path id="Shape 2 copy 4" class="s1" d="m161 89.5c0-5.8 11-9.5 24.5-9.5 13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5-13.5 0-24.5-4.7-24.5-10.5 0-3.2 0-12.8 0-16z"/>
|
||||
<path id="Shape 2 copy 2" class="s2" d="m185.5 80c13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5 0-12.6 0-24.8 0-36z"/>
|
||||
<path id="Shape 2 copy" class="s0" d="m185.5 96.9c-11.7 0-21.2-3.3-21.2-7.4 0-4.1 9.5-7.4 21.2-7.4 11.7 0 21.2 3.3 21.2 7.4 0 4.1-9.5 7.4-21.2 7.4zm0 0c-11.7 0-21.2-3.3-21.2-7.4 0-4.1 9.5-7.4 21.2-7.4 11.7 0 21.2 3.3 21.2 7.4 0 4.1-9.5 7.4-21.2 7.4z"/>
|
||||
</g>
|
||||
<g id="Folder 1 copy 3">
|
||||
<path id="Shape 2" class="s4" d="m45 89.5c0-5.8 11-9.5 24.5-9.5 13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5-13.5 0-24.5-4.7-24.5-10.5 0-3.2 0-12.8 0-16z"/>
|
||||
<path id="Shape 2 copy 4" class="s1" d="m45 89.5c0-5.8 11-9.5 24.5-9.5 13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5-13.5 0-24.5-4.7-24.5-10.5 0-3.2 0-12.8 0-16z"/>
|
||||
<path id="Shape 2 copy 2" class="s2" d="m69.5 80c13.5 0 24.5 3.7 24.5 9.5 0 3.2 0 12.8 0 16 0 5.8-11 10.5-24.5 10.5 0-12.6 0-24.8 0-36z"/>
|
||||
<path id="Shape 2 copy" class="s0" d="m69.5 96.9c-11.7 0-21.2-3.3-21.2-7.4 0-4.1 9.5-7.4 21.2-7.4 11.7 0 21.2 3.3 21.2 7.4 0 4.1-9.5 7.4-21.2 7.4zm0 0c-11.7 0-21.2-3.3-21.2-7.4 0-4.1 9.5-7.4 21.2-7.4 11.7 0 21.2 3.3 21.2 7.4 0 4.1-9.5 7.4-21.2 7.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
export const gearColored = `<svg viewBox="0 0 128 128" preserveAspectRatio="xMidYMid meet">
|
||||
<path d="M124 71.85v-15.7c0-.59-.45-1.09-1.03-1.15l-17.83-1.89c-.47-.05-.85-.38-.98-.83c-.86-2.95-2.03-5.76-3.48-8.39c-.23-.41-.19-.92.11-1.28l11.28-13.94c.37-.46.34-1.13-.08-1.54l-11.1-11.1a1.15 1.15 0 0 0-1.54-.08L85.39 27.22c-.37.3-.87.33-1.28.11a41.796 41.796 0 0 0-8.39-3.48c-.45-.13-.78-.51-.83-.98L73 5.03C72.94 4.45 72.44 4 71.85 4h-15.7c-.59 0-1.09.45-1.15 1.03l-1.89 17.83c-.05.47-.38.85-.83.98c-2.95.86-5.76 2.03-8.39 3.48c-.41.23-.92.19-1.28-.11L28.67 15.94a1.15 1.15 0 0 0-1.54.08l-11.1 11.1a1.15 1.15 0 0 0-.08 1.54L27.23 42.6c.3.37.33.87.11 1.28a41.796 41.796 0 0 0-3.48 8.39c-.13.45-.51.78-.98.83L5.03 55c-.58.06-1.03.56-1.03 1.15v15.7c0 .59.45 1.09 1.03 1.15l17.83 1.89c.47.05.85.38.98.83c.86 2.95 2.03 5.76 3.48 8.39c.23.41.19.92-.11 1.28L15.94 99.33c-.37.46-.34 1.13.08 1.54l11.1 11.1c.42.42 1.08.45 1.54.08l13.94-11.28c.37-.3.87-.33 1.28-.11c2.64 1.45 5.45 2.62 8.39 3.48c.45.13.78.51.83.98l1.9 17.85c.06.59.56 1.03 1.15 1.03h15.7c.59 0 1.09-.45 1.15-1.03l1.89-17.83c.05-.47.38-.85.83-.98c2.95-.86 5.76-2.03 8.39-3.48c.41-.23.92-.19 1.28.11l13.94 11.28c.46.37 1.13.34 1.54-.08l11.1-11.1c.42-.42.45-1.08.08-1.54l-11.28-13.94c-.3-.37-.33-.87-.11-1.28c1.45-2.64 2.62-5.45 3.48-8.39c.13-.45.51-.78.98-.83L122.97 73c.58-.06 1.03-.56 1.03-1.15zm-60 3.43c-6.23 0-11.28-5.05-11.28-11.28S57.77 52.72 64 52.72S75.28 57.77 75.28 64S70.23 75.28 64 75.28z" fill="#82aec0"></path>
|
||||
<path d="M80.56 49.48c3.67 4.18 5.78 9.77 5.43 15.85c-.65 11.16-9.83 20.19-21 20.68c-4.75.21-9.18-1.09-12.86-3.45c-.28-.18-.58.2-.34.44a22.412 22.412 0 0 0 17.85 6.67c10.78-.85 19.56-9.5 20.55-20.27c.77-8.36-3.06-15.87-9.23-20.33c-.29-.2-.62.15-.4.41z" fill="#2f7889"></path>
|
||||
<path d="M43.87 65.32c-.67-13.15 7.83-22.79 20.01-22.79c.65 0 1.68 0 2.48.92c1.01 1.18 1.1 2.6 0 3.77c-.81.86-1.95.92-2.53 1c-12.3 1.59-15.18 9.35-15.83 16.77c-.03.33.06 2.35-1.71 2.56c-2.15.25-2.41-1.91-2.42-2.23z" fill="#b9e4ea"></path>
|
||||
<path d="M25.24 65.87c-.01-22.03 15.9-40.19 38.13-41.05c.68-.03 2.45 0 3.55.99c1.01.91 1.38 2.51.79 3.82c-.95 2.11-2.85 2.07-3.36 2.09c-18.51.66-34.18 15.73-34.19 33.95c0 .29-.05.58-.15.84l-.1.25c-.76 1.98-3.52 2.09-4.43.18c-.15-.34-.24-.7-.24-1.07z" fill="#94d1e0"></path>
|
||||
</svg>`;
|
||||
|
||||
export function $svg(markup: string, attrs: {[key: string]: string}) {
|
||||
if (!markup.match(/^\s*<svg/)) {
|
||||
throw new Error("Cannot call $svg with non-svg markup.");
|
||||
}
|
||||
return $el(markup, attrs || {});
|
||||
}
|
||||
130
custom_nodes/rgthree-comfy/src_web/common/menu.ts
Normal file
130
custom_nodes/rgthree-comfy/src_web/common/menu.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { generateId, wait } from "./shared_utils.js";
|
||||
import { createElement as $el, getClosestOrSelf, setAttributes} from "./utils_dom.js";
|
||||
|
||||
/**
|
||||
* A menu. Mimics the comfy menu.
|
||||
*/
|
||||
class Menu {
|
||||
|
||||
private element: HTMLMenuElement = $el('menu.rgthree-menu');
|
||||
private callbacks: Map<string, (e: PointerEvent) => Promise<boolean|void>> = new Map();
|
||||
|
||||
private handleWindowPointerDownBound = this.handleWindowPointerDown.bind(this);
|
||||
|
||||
constructor(options: MenuOption[]) {
|
||||
this.setOptions(options);
|
||||
this.element.addEventListener('pointerup', async (e) => {
|
||||
const target = getClosestOrSelf(e.target as HTMLElement, "[data-callback],menu");
|
||||
if (e.which !== 1) {
|
||||
return;
|
||||
}
|
||||
const callback = target?.dataset?.['callback'];
|
||||
if (callback) {
|
||||
const halt = await this.callbacks.get(callback)?.(e);
|
||||
if (halt !== false) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
});
|
||||
}
|
||||
|
||||
setOptions(options: MenuOption[]) {
|
||||
for (const option of options) {
|
||||
if (option.type === 'title') {
|
||||
this.element.appendChild($el(`li`, {
|
||||
html: option.label
|
||||
}));
|
||||
} else {
|
||||
const id = generateId(8);
|
||||
this.callbacks.set(id, async (e: PointerEvent) => { return option?.callback?.(e); });
|
||||
this.element.appendChild($el(`li[role="button"][data-callback="${id}"]`, {
|
||||
html: option.label
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toElement() {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
async open(e: PointerEvent) {
|
||||
const parent = (e.target as HTMLElement).closest('div,dialog,body') as HTMLElement
|
||||
parent.appendChild(this.element);
|
||||
setAttributes(this.element, {
|
||||
style: {
|
||||
left: `${e.clientX + 16}px`,
|
||||
top: `${e.clientY - 16}px`,
|
||||
}
|
||||
});
|
||||
this.element.setAttribute('state', 'measuring-open');
|
||||
await wait(16);
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth) {
|
||||
this.element.style.left = `${e.clientX - rect.width - 16}px`;
|
||||
await wait(16);
|
||||
}
|
||||
this.element.setAttribute('state', 'open');
|
||||
setTimeout(() => {
|
||||
window.addEventListener('pointerdown', this.handleWindowPointerDownBound);
|
||||
});
|
||||
}
|
||||
|
||||
handleWindowPointerDown(e:PointerEvent) {
|
||||
if (!this.element.contains(e.target as HTMLElement)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
window.removeEventListener('pointerdown', this.handleWindowPointerDownBound);
|
||||
this.element.setAttribute('state', 'measuring-closed');
|
||||
await wait(16);
|
||||
this.element.setAttribute('state', 'closed');
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
return (this.element.getAttribute('state') || '').includes('open');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type MenuOption = {
|
||||
label: string;
|
||||
type?: 'title'|'item'|'separator';
|
||||
callback?: (e: PointerEvent) => void;
|
||||
}
|
||||
|
||||
type MenuButtonOptions = {
|
||||
icon: string;
|
||||
options: MenuOption[];
|
||||
}
|
||||
|
||||
export class MenuButton {
|
||||
|
||||
private options: MenuButtonOptions;
|
||||
private menu: Menu;
|
||||
|
||||
private element: HTMLButtonElement = $el('button.rgthree-button[data-action="open-menu"]')
|
||||
|
||||
constructor(options: MenuButtonOptions) {
|
||||
this.options = options;
|
||||
this.element.innerHTML = options.icon;
|
||||
this.menu = new Menu(options.options);
|
||||
|
||||
this.element.addEventListener('pointerdown', (e) => {
|
||||
if (!this.menu.isOpen()) {
|
||||
this.menu.open(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toElement() {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type {RgthreeModelInfo} from "typings/rgthree.js";
|
||||
import {ModelInfoType, rgthreeApi} from "./rgthree_api.js";
|
||||
import {api} from "scripts/api.js";
|
||||
|
||||
/**
|
||||
* Abstract class defining information syncing for different types.
|
||||
*/
|
||||
abstract class BaseModelInfoService extends EventTarget {
|
||||
private readonly fileToInfo = new Map<string, RgthreeModelInfo | null>();
|
||||
protected abstract readonly modelInfoType: ModelInfoType;
|
||||
|
||||
protected abstract readonly apiRefreshEventString: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
api.addEventListener(
|
||||
this.apiRefreshEventString,
|
||||
this.handleAsyncUpdate.bind(this) as EventListener,
|
||||
);
|
||||
}
|
||||
|
||||
async getInfo(file: string, refresh: boolean, light: boolean) {
|
||||
if (this.fileToInfo.has(file) && !refresh) {
|
||||
return this.fileToInfo.get(file)!;
|
||||
}
|
||||
return this.fetchInfo(file, refresh, light);
|
||||
}
|
||||
|
||||
async refreshInfo(file: string) {
|
||||
return this.fetchInfo(file, true);
|
||||
}
|
||||
|
||||
async clearFetchedInfo(file: string) {
|
||||
await rgthreeApi.clearModelsInfo({type: this.modelInfoType, files: [file]});
|
||||
this.fileToInfo.delete(file);
|
||||
return null;
|
||||
}
|
||||
|
||||
async savePartialInfo(file: string, data: Partial<RgthreeModelInfo>) {
|
||||
let info = await rgthreeApi.saveModelInfo(this.modelInfoType, file, data);
|
||||
this.fileToInfo.set(file, info);
|
||||
return info;
|
||||
}
|
||||
|
||||
handleAsyncUpdate(event: CustomEvent<{data: RgthreeModelInfo}>) {
|
||||
const info = event.detail?.data as RgthreeModelInfo;
|
||||
if (info?.file) {
|
||||
this.setFreshInfo(info.file, info);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchInfo(file: string, refresh = false, light = false) {
|
||||
let info = null;
|
||||
if (!refresh) {
|
||||
info = await rgthreeApi.getModelsInfo({type: this.modelInfoType, files: [file], light});
|
||||
} else {
|
||||
info = await rgthreeApi.refreshModelsInfo({type: this.modelInfoType, files: [file]});
|
||||
}
|
||||
info = info?.[0] ?? null;
|
||||
if (!light) {
|
||||
this.fileToInfo.set(file, info);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single point to set data into the info cache, and fire an event. Note, this doesn't determine
|
||||
* if the data is actually different.
|
||||
*/
|
||||
private setFreshInfo(file: string, info: RgthreeModelInfo) {
|
||||
this.fileToInfo.set(file, info);
|
||||
// this.dispatchEvent(
|
||||
// new CustomEvent("rgthree-model-service-lora-details", { detail: { lora: info } }),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lora type implementation of ModelInfoTypeService.
|
||||
*/
|
||||
class LoraInfoService extends BaseModelInfoService {
|
||||
protected override readonly apiRefreshEventString = "rgthree-refreshed-loras-info";
|
||||
protected override readonly modelInfoType = 'loras';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkpoint type implementation of ModelInfoTypeService.
|
||||
*/
|
||||
class CheckpointInfoService extends BaseModelInfoService {
|
||||
protected override readonly apiRefreshEventString = "rgthree-refreshed-checkpoints-info";
|
||||
protected override readonly modelInfoType = 'checkpoints';
|
||||
}
|
||||
|
||||
export const LORA_INFO_SERVICE = new LoraInfoService();
|
||||
export const CHECKPOINT_INFO_SERVICE = new CheckpointInfoService();
|
||||
218
custom_nodes/rgthree-comfy/src_web/common/progress_bar.ts
Normal file
218
custom_nodes/rgthree-comfy/src_web/common/progress_bar.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Progress bar web component.
|
||||
*/
|
||||
|
||||
import { SERVICE as PROMPT_SERVICE, type PromptExecution } from "rgthree/common/prompt_service.js";
|
||||
import { createElement } from "./utils_dom.js";
|
||||
|
||||
/**
|
||||
* The progress bar web component.
|
||||
*/
|
||||
export class RgthreeProgressBar extends HTMLElement {
|
||||
static NAME = "rgthree-progress-bar";
|
||||
|
||||
static create(): RgthreeProgressBar {
|
||||
return document.createElement(RgthreeProgressBar.NAME) as RgthreeProgressBar;
|
||||
}
|
||||
|
||||
private shadow: ShadowRoot | null = null;
|
||||
private progressNodesEl!: HTMLDivElement;
|
||||
private progressStepsEl!: HTMLDivElement;
|
||||
private progressTextEl!: HTMLSpanElement;
|
||||
|
||||
private currentPromptExecution: PromptExecution | null = null;
|
||||
|
||||
private readonly onProgressUpdateBound = this.onProgressUpdate.bind(this);
|
||||
|
||||
private connected: boolean = false;
|
||||
|
||||
/** The currentNodeId so outside callers can see what we're currently executing against. */
|
||||
get currentNodeId() {
|
||||
const prompt = this.currentPromptExecution;
|
||||
const nodeId = prompt?.errorDetails?.node_id || prompt?.currentlyExecuting?.nodeId;
|
||||
return nodeId || null;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private onProgressUpdate(e: CustomEvent<{ queue: number; prompt: PromptExecution }>) {
|
||||
if (!this.connected) return;
|
||||
|
||||
const prompt = e.detail.prompt;
|
||||
this.currentPromptExecution = prompt;
|
||||
|
||||
if (prompt?.errorDetails) {
|
||||
let progressText = `${prompt.errorDetails?.exception_type} ${
|
||||
prompt.errorDetails?.node_id || ""
|
||||
} ${prompt.errorDetails?.node_type || ""}`;
|
||||
this.progressTextEl.innerText = progressText;
|
||||
this.progressNodesEl.classList.add("-error");
|
||||
this.progressStepsEl.classList.add("-error");
|
||||
return;
|
||||
}
|
||||
if (prompt?.currentlyExecuting) {
|
||||
this.progressNodesEl.classList.remove("-error");
|
||||
this.progressStepsEl.classList.remove("-error");
|
||||
|
||||
const current = prompt?.currentlyExecuting;
|
||||
|
||||
let progressText = `(${e.detail.queue}) `;
|
||||
|
||||
// Sometimes we may get status updates for a workflow that was already running. In that case
|
||||
// we don't know totalNodes.
|
||||
if (!prompt.totalNodes) {
|
||||
progressText += `??%`;
|
||||
this.progressNodesEl.style.width = `0%`;
|
||||
} else {
|
||||
const percent = (prompt.executedNodeIds.length / prompt.totalNodes) * 100;
|
||||
this.progressNodesEl.style.width = `${Math.max(2, percent)}%`;
|
||||
// progressText += `Node ${prompt.executedNodeIds.length + 1} of ${prompt.totalNodes || "?"}`;
|
||||
progressText += `${Math.round(percent)}%`;
|
||||
}
|
||||
|
||||
let nodeLabel = current.nodeLabel?.trim();
|
||||
let stepsLabel = "";
|
||||
if (current.step != null && current.maxSteps) {
|
||||
const percent = (current.step / current.maxSteps) * 100;
|
||||
this.progressStepsEl.style.width = `${percent}%`;
|
||||
// stepsLabel += `Step ${current.step} of ${current.maxSteps}`;
|
||||
if (current.pass > 1 || current.maxPasses != null) {
|
||||
stepsLabel += `#${current.pass}`;
|
||||
if (current.maxPasses && current.maxPasses > 0) {
|
||||
stepsLabel += `/${current.maxPasses}`;
|
||||
}
|
||||
stepsLabel += ` - `;
|
||||
}
|
||||
stepsLabel += `${Math.round(percent)}%`;
|
||||
}
|
||||
|
||||
if (nodeLabel || stepsLabel) {
|
||||
progressText += ` - ${nodeLabel || "???"}${stepsLabel ? ` (${stepsLabel})` : ""}`;
|
||||
}
|
||||
if (!stepsLabel) {
|
||||
this.progressStepsEl.style.width = `0%`;
|
||||
}
|
||||
this.progressTextEl.innerText = progressText;
|
||||
} else {
|
||||
if (e?.detail.queue) {
|
||||
this.progressTextEl.innerText = `(${e.detail.queue}) Running... in another tab`;
|
||||
} else {
|
||||
this.progressTextEl.innerText = "Idle";
|
||||
}
|
||||
this.progressNodesEl.style.width = `0%`;
|
||||
this.progressStepsEl.style.width = `0%`;
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (!this.connected) {
|
||||
PROMPT_SERVICE.addEventListener(
|
||||
"progress-update",
|
||||
this.onProgressUpdateBound as EventListener,
|
||||
);
|
||||
this.connected = true;
|
||||
}
|
||||
// We were already connected, so we just need to reset.
|
||||
if (this.shadow) {
|
||||
this.progressTextEl.innerText = "Idle";
|
||||
this.progressNodesEl.style.width = `0%`;
|
||||
this.progressStepsEl.style.width = `0%`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.shadow = this.attachShadow({ mode: "open" });
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(`
|
||||
|
||||
:host {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
background: var(--rgthree-progress-bg-color);
|
||||
--rgthree-progress-bg-color: rgba(23, 23, 23, 0.9);
|
||||
--rgthree-progress-nodes-bg-color: rgb(0, 128, 0);
|
||||
--rgthree-progress-steps-bg-color: rgb(0, 128, 0);
|
||||
--rgthree-progress-error-bg-color: rgb(128, 0, 0);
|
||||
--rgthree-progress-text-color: #fff;
|
||||
}
|
||||
:host * {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
:host > div.bar {
|
||||
background: var(--rgthree-progress-nodes-bg-color);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0%;
|
||||
height: 50%;
|
||||
z-index: 1;
|
||||
transition: width 50ms ease-in-out;
|
||||
}
|
||||
:host > div.bar + div.bar {
|
||||
background: var(--rgthree-progress-steps-bg-color);
|
||||
top: 50%;
|
||||
height: 50%;
|
||||
z-index: 2;
|
||||
}
|
||||
:host > div.bar.-error {
|
||||
background: var(--rgthree-progress-error-bg-color);
|
||||
}
|
||||
|
||||
:host > .overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 5;
|
||||
background: linear-gradient(to bottom, rgba(255,255,255,0.25), rgba(0,0,0,0.25));
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
:host > span {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
text-shadow: 1px 1px 0px #000;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 6px;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
color: var(--rgthree-progress-text-color);
|
||||
text-shadow: black 0px 0px 2px;
|
||||
}
|
||||
|
||||
:host > div.bar[style*="width: 0%"]:first-child,
|
||||
:host > div.bar[style*="width:0%"]:first-child {
|
||||
height: 0%;
|
||||
}
|
||||
:host > div.bar[style*="width: 0%"]:first-child + div,
|
||||
:host > div.bar[style*="width:0%"]:first-child + div {
|
||||
bottom: 0%;
|
||||
}
|
||||
`);
|
||||
this.shadow.adoptedStyleSheets = [sheet];
|
||||
|
||||
const overlayEl = createElement(`div.overlay[part="overlay"]`, { parent: this.shadow });
|
||||
this.progressNodesEl = createElement(`div.bar[part="progress-nodes"]`, { parent: this.shadow });
|
||||
this.progressStepsEl = createElement(`div.bar[part="progress-steps"]`, { parent: this.shadow });
|
||||
this.progressTextEl = createElement(`span[part="text"]`, { text: "Idle", parent: this.shadow });
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.connected = false;
|
||||
PROMPT_SERVICE.removeEventListener(
|
||||
"progress-update",
|
||||
this.onProgressUpdateBound as EventListener,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(RgthreeProgressBar.NAME, RgthreeProgressBar);
|
||||
274
custom_nodes/rgthree-comfy/src_web/common/prompt_service.ts
Normal file
274
custom_nodes/rgthree-comfy/src_web/common/prompt_service.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import type {
|
||||
ComfyApiEventDetailCached,
|
||||
ComfyApiEventDetailError,
|
||||
ComfyApiEventDetailExecuted,
|
||||
ComfyApiEventDetailExecuting,
|
||||
ComfyApiEventDetailExecutionStart,
|
||||
ComfyApiEventDetailProgress,
|
||||
ComfyApiEventDetailStatus,
|
||||
ComfyApiFormat,
|
||||
ComfyApiPrompt,
|
||||
} from "typings/comfy.js";
|
||||
import { api } from "scripts/api.js";
|
||||
import type { LGraph as TLGraph, LGraphCanvas as TLGraphCanvas } from "@comfyorg/frontend";
|
||||
import { Resolver, getResolver } from "./shared_utils.js";
|
||||
|
||||
/**
|
||||
* Wraps general data of a prompt's execution.
|
||||
*/
|
||||
export class PromptExecution {
|
||||
id: string;
|
||||
promptApi: ComfyApiFormat | null = null;
|
||||
executedNodeIds: string[] = [];
|
||||
totalNodes: number = 0;
|
||||
currentlyExecuting: {
|
||||
nodeId: string;
|
||||
nodeLabel?: string;
|
||||
step?: number;
|
||||
maxSteps?: number;
|
||||
/** The current pass, for nodes with multiple progress passes. */
|
||||
pass: number;
|
||||
/**
|
||||
* The max num of passes. Can be calculated for some nodes, or set to -1 when known there will
|
||||
* be multiple passes, but the number cannot be calculated.
|
||||
*/
|
||||
maxPasses?: number;
|
||||
} | null = null;
|
||||
errorDetails: any | null = null;
|
||||
|
||||
apiPrompt: Resolver<null> = getResolver();
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the prompt and prompt-related data. This can technically come in lazily, like if the web
|
||||
* socket fires the 'execution-start' event before we actually get a response back from the
|
||||
* initial prompt call.
|
||||
*/
|
||||
setPrompt(prompt: ComfyApiPrompt) {
|
||||
this.promptApi = prompt.output;
|
||||
this.totalNodes = Object.keys(this.promptApi).length;
|
||||
this.apiPrompt.resolve(null);
|
||||
}
|
||||
|
||||
getApiNode(nodeId: string | number) {
|
||||
return this.promptApi?.[String(nodeId)] || null;
|
||||
}
|
||||
|
||||
private getNodeLabel(nodeId: string | number) {
|
||||
const apiNode = this.getApiNode(nodeId);
|
||||
let label = apiNode?._meta?.title || apiNode?.class_type || undefined;
|
||||
if (!label) {
|
||||
const graphNode = this.maybeGetComfyGraph()?.getNodeById(Number(nodeId));
|
||||
label = graphNode?.title || graphNode?.type || undefined;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the execution data depending on the passed data, fed from api events.
|
||||
*/
|
||||
executing(nodeId: string | null, step?: number, maxSteps?: number) {
|
||||
if (nodeId == null) {
|
||||
// We're done, any left over nodes must be skipped...
|
||||
this.currentlyExecuting = null;
|
||||
return;
|
||||
}
|
||||
if (this.currentlyExecuting?.nodeId !== nodeId) {
|
||||
if (this.currentlyExecuting != null) {
|
||||
this.executedNodeIds.push(nodeId);
|
||||
}
|
||||
this.currentlyExecuting = { nodeId, nodeLabel: this.getNodeLabel(nodeId), pass: 0 };
|
||||
// We'll see if we're known node for multiple passes, that will come in as generic 'progress'
|
||||
// updates from the api. If we're known to have multiple passes, then we'll pre-set data to
|
||||
// allow the progress bar to handle intial rendering. If we're not, that's OK, the data will
|
||||
// be shown with the second pass.
|
||||
this.apiPrompt.promise.then(() => {
|
||||
// If we execute with a null node id and clear the currently executing, then we can just
|
||||
// move on. This seems to only happen with a super-fast execution (like, just seed node
|
||||
// and display any for testing).
|
||||
if (this.currentlyExecuting == null) {
|
||||
return;
|
||||
}
|
||||
const apiNode = this.getApiNode(nodeId);
|
||||
if (!this.currentlyExecuting.nodeLabel) {
|
||||
this.currentlyExecuting.nodeLabel = this.getNodeLabel(nodeId);
|
||||
}
|
||||
if (apiNode?.class_type === "UltimateSDUpscale") {
|
||||
// From what I can tell, UltimateSDUpscale, does an initial pass that isn't actually a
|
||||
// tile. It seems to always be 4 steps... We'll start our pass at -1, so this prepass is
|
||||
// "0" and "1" will start with the first tile. This way, a user knows they have 4 tiles,
|
||||
// know this pass counter will go to 4 (and not 5). Also, we cannot calculate maxPasses
|
||||
// for 'UltimateSDUpscale' :(
|
||||
this.currentlyExecuting.pass--;
|
||||
this.currentlyExecuting.maxPasses = -1;
|
||||
} else if (apiNode?.class_type === "IterativeImageUpscale") {
|
||||
this.currentlyExecuting.maxPasses = (apiNode?.inputs["steps"] as number) ?? -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (step != null) {
|
||||
// If we haven't had any stpes before, or the passes step is lower than the previous, then
|
||||
// increase the passes.
|
||||
if (!this.currentlyExecuting!.step || step < this.currentlyExecuting!.step) {
|
||||
this.currentlyExecuting!.pass!++;
|
||||
}
|
||||
this.currentlyExecuting!.step = step;
|
||||
this.currentlyExecuting!.maxSteps = maxSteps;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there's an error, we add the details.
|
||||
*/
|
||||
error(details: any) {
|
||||
this.errorDetails = details;
|
||||
}
|
||||
|
||||
private maybeGetComfyGraph(): TLGraph | null {
|
||||
return ((window as any)?.app?.graph as TLGraph) || null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A singleton service that wraps the Comfy API and simplifies the event data being fired.
|
||||
*/
|
||||
class PromptService extends EventTarget {
|
||||
promptsMap: Map<string, PromptExecution> = new Map();
|
||||
currentExecution: PromptExecution | null = null;
|
||||
lastQueueRemaining = 0;
|
||||
|
||||
constructor(api: any) {
|
||||
super();
|
||||
const that = this;
|
||||
|
||||
// Patch the queuePrompt method so we can capture new data going through.
|
||||
const queuePrompt = api.queuePrompt;
|
||||
api.queuePrompt = async function (num: number, prompt: ComfyApiPrompt, ...args: any[]) {
|
||||
let response;
|
||||
try {
|
||||
response = await queuePrompt.apply(api, [...arguments]);
|
||||
} catch (e) {
|
||||
const promptExecution = that.getOrMakePrompt("error");
|
||||
promptExecution.error({ exception_type: "Unknown." });
|
||||
// console.log("ERROR QUEUE PROMPT", response, arguments);
|
||||
throw e;
|
||||
}
|
||||
// console.log("QUEUE PROMPT", response, arguments);
|
||||
const promptExecution = that.getOrMakePrompt(response.prompt_id);
|
||||
promptExecution.setPrompt(prompt);
|
||||
if (!that.currentExecution) {
|
||||
that.currentExecution = promptExecution;
|
||||
}
|
||||
that.promptsMap.set(response.prompt_id, promptExecution);
|
||||
that.dispatchEvent(
|
||||
new CustomEvent("queue-prompt", {
|
||||
detail: {
|
||||
prompt: promptExecution,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
api.addEventListener("status", (e: CustomEvent<ComfyApiEventDetailStatus>) => {
|
||||
// console.log("status", JSON.stringify(e.detail));
|
||||
// Sometimes a status message is fired when the app loades w/o any details.
|
||||
if (!e.detail?.exec_info) return;
|
||||
this.lastQueueRemaining = e.detail.exec_info.queue_remaining;
|
||||
this.dispatchProgressUpdate();
|
||||
});
|
||||
|
||||
api.addEventListener("execution_start", (e: CustomEvent<ComfyApiEventDetailExecutionStart>) => {
|
||||
// console.log("execution_start", JSON.stringify(e.detail));
|
||||
if (!this.promptsMap.has(e.detail.prompt_id)) {
|
||||
console.warn("'execution_start' fired before prompt was made.");
|
||||
}
|
||||
const prompt = this.getOrMakePrompt(e.detail.prompt_id);
|
||||
this.currentExecution = prompt;
|
||||
this.dispatchProgressUpdate();
|
||||
});
|
||||
|
||||
api.addEventListener("executing", (e: CustomEvent<ComfyApiEventDetailExecuting>) => {
|
||||
// console.log("executing", JSON.stringify(e.detail));
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt("unknown");
|
||||
console.warn("'executing' fired before prompt was made.");
|
||||
}
|
||||
this.currentExecution.executing(e.detail);
|
||||
this.dispatchProgressUpdate();
|
||||
if (e.detail == null) {
|
||||
this.currentExecution = null;
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("progress", (e: CustomEvent<ComfyApiEventDetailProgress>) => {
|
||||
// console.log("progress", JSON.stringify(e.detail));
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
|
||||
console.warn("'progress' fired before prompt was made.");
|
||||
}
|
||||
this.currentExecution.executing(e.detail.node, e.detail.value, e.detail.max);
|
||||
this.dispatchProgressUpdate();
|
||||
});
|
||||
|
||||
api.addEventListener("execution_cached", (e: CustomEvent<ComfyApiEventDetailCached>) => {
|
||||
// console.log("execution_cached", JSON.stringify(e.detail));
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
|
||||
console.warn("'execution_cached' fired before prompt was made.");
|
||||
}
|
||||
for (const cached of e.detail.nodes) {
|
||||
this.currentExecution.executing(cached);
|
||||
}
|
||||
this.dispatchProgressUpdate();
|
||||
});
|
||||
|
||||
api.addEventListener("executed", (e: CustomEvent<ComfyApiEventDetailExecuted>) => {
|
||||
// console.log("executed", JSON.stringify(e.detail));
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
|
||||
console.warn("'executed' fired before prompt was made.");
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("execution_error", (e: CustomEvent<ComfyApiEventDetailError>) => {
|
||||
// console.log("execution_error", e.detail);
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
|
||||
console.warn("'execution_error' fired before prompt was made.");
|
||||
}
|
||||
this.currentExecution?.error(e.detail);
|
||||
this.dispatchProgressUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/** A helper method, since we extend/override api.queuePrompt above anyway. */
|
||||
async queuePrompt(prompt: ComfyApiPrompt) {
|
||||
return await api.queuePrompt(-1, prompt);
|
||||
}
|
||||
|
||||
dispatchProgressUpdate() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("progress-update", {
|
||||
detail: {
|
||||
queue: this.lastQueueRemaining,
|
||||
prompt: this.currentExecution,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getOrMakePrompt(id: string) {
|
||||
let prompt = this.promptsMap.get(id);
|
||||
if (!prompt) {
|
||||
prompt = new PromptExecution(id);
|
||||
this.promptsMap.set(id, prompt);
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
}
|
||||
|
||||
export const SERVICE = new PromptService(api);
|
||||
983
custom_nodes/rgthree-comfy/src_web/common/py_parser.ts
Normal file
983
custom_nodes/rgthree-comfy/src_web/common/py_parser.ts
Normal file
@@ -0,0 +1,983 @@
|
||||
/**
|
||||
* @fileoverview An AST executor using the TreeSitter parser to parse python-like code and execute
|
||||
* in JS. This parser is self-contained and isolated from other parts of the app (like Comfy-UI
|
||||
* specific types, etc). Instead, additional handlers, builtins, and types can be passed into the
|
||||
* pure functions below.
|
||||
*/
|
||||
import type {Parser, Node as TreeSitterNode, Tree} from "web-tree-sitter";
|
||||
|
||||
import {check, deepFreeze} from "./shared_utils.js";
|
||||
|
||||
// Hacky memoization because I don't feel like writing a decorator.
|
||||
const MEMOIZED = {parser: null as unknown as Parser};
|
||||
|
||||
interface Dict extends Object {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
interface ExecutionContextData extends Object {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
class ExecuteContext implements ExecutionContextData {
|
||||
[k: string]: unknown;
|
||||
|
||||
constructor(existing: Object = {}) {
|
||||
Object.assign(this, !!window.structuredClone ? structuredClone(existing) : {...existing});
|
||||
}
|
||||
}
|
||||
class InitialExecuteContext extends ExecuteContext {}
|
||||
|
||||
type NodeHandlerArgs = [ExecutionContextData, BuiltInFns];
|
||||
type NodeHandler = (node: Node, ...args: NodeHandlerArgs) => Promise<any>;
|
||||
|
||||
const TYPE_TO_HANDLER = new Map<string, NodeHandler>([
|
||||
["module", handleChildren],
|
||||
["expression_statement", handleChildren],
|
||||
["interpolation", handleInterpolation],
|
||||
["block", handleChildren], // Block of code, like in a for loop
|
||||
|
||||
["comment", handleSwallow],
|
||||
["return_statement", handleReturn],
|
||||
|
||||
["assignment", handleAssignment],
|
||||
["named_expression", handleNamedExpression],
|
||||
|
||||
["identifier", handleIdentifier],
|
||||
["attribute", handleAttribute],
|
||||
["subscript", handleSubscript],
|
||||
|
||||
["call", handleCall],
|
||||
["argument_list", handleArgumentsList],
|
||||
|
||||
["for_statement", handleForStatement],
|
||||
["list_comprehension", handleListComprehension],
|
||||
|
||||
["comparison_operator", handleComparisonOperator],
|
||||
["boolean_operator", handleBooleanOperator],
|
||||
["binary_operator", handleBinaryOperator],
|
||||
["not_operator", handleNotOperator],
|
||||
["unary_operator", handleUnaryOperator],
|
||||
|
||||
// Types
|
||||
["integer", handleNumber],
|
||||
["float", handleNumber],
|
||||
["string", handleString],
|
||||
["tuple", handleList],
|
||||
["list", handleList],
|
||||
["dictionary", handleDictionary],
|
||||
["pair", handleDictionaryPair],
|
||||
["true", async (...args: any[]) => true],
|
||||
["false", async (...args: any[]) => false],
|
||||
]);
|
||||
|
||||
type BuiltInFn = {fn: Function};
|
||||
type BuiltInFns = {[key: string]: BuiltInFn};
|
||||
|
||||
const DEFAULT_BUILT_INS: BuiltInFns = {
|
||||
round: {fn: (n: any) => Math.round(Number(n))},
|
||||
ceil: {fn: (n: any) => Math.ceil(Number(n))},
|
||||
floor: {fn: (n: any) => Math.floor(Number(n))},
|
||||
// Function(name="sqrt", call=math.sqrt, args=(1, 1)),
|
||||
// Function(name="min", call=min, args=(2, None)),
|
||||
// Function(name="max", call=max, args=(2, None)),
|
||||
// Function(name=".random_int", call=random.randint, args=(2, 2)),
|
||||
// Function(name=".random_choice", call=random.choice, args=(1, 1)),
|
||||
// Function(name=".random_seed", call=random.seed, args=(1, 1)),
|
||||
// Function(name="re", call=re.compile, args=(1, 1)),
|
||||
len: {fn: (n: any) => n?.__len__?.() ?? n?.length},
|
||||
// Function(name="enumerate", call=enumerate, args=(1, 1)),
|
||||
// Function(name="range", call=range, args=(1, 3)),
|
||||
|
||||
// Types
|
||||
int: {fn: (n: any) => Math.floor(Number(n))},
|
||||
float: {fn: (n: any) => Number(n)},
|
||||
str: {fn: (n: any) => String(n)},
|
||||
bool: {fn: (n: any) => !!n},
|
||||
list: {fn: (tupl: any[] = []) => new PyList(tupl)},
|
||||
tuple: {fn: (list: any[] = []) => new PyTuple(list)},
|
||||
dict: {fn: (dict: Dict = {}) => new PyDict(dict)},
|
||||
|
||||
// Special
|
||||
dir: {fn: (...args: any[]) => console.dir(...__unwrap__(...args))},
|
||||
print: {fn: (...args: any[]) => console.log(...__unwrap__(...args))},
|
||||
log: {fn: (...args: any[]) => console.log(...__unwrap__(...args))},
|
||||
};
|
||||
|
||||
/**
|
||||
* The main entry point to parse code.
|
||||
*/
|
||||
export async function execute(
|
||||
code: string,
|
||||
ctx: ExecutionContextData,
|
||||
additionalBuiltins?: BuiltInFns,
|
||||
) {
|
||||
const builtIns = deepFreeze({...DEFAULT_BUILT_INS, ...(additionalBuiltins ?? {})});
|
||||
// When we start the execution, we create an InitialExecuteContext as an instance so we can check
|
||||
// if we're the initial, global context during execution (as we may pass in a new context in the
|
||||
// like if evaluating a list comprehension, or setting on an object).
|
||||
ctx = new InitialExecuteContext(ctx);
|
||||
|
||||
const root = (await parse(code)).rootNode;
|
||||
const value = await handleNode(new Node(root), ctx, builtIns);
|
||||
|
||||
console.log("=====");
|
||||
console.log(`value`, value?.__unwrap__?.() ?? value);
|
||||
console.log("context", ctx);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a code string to a `Tree`.
|
||||
*/
|
||||
async function parse(code: string): Promise<Tree> {
|
||||
if (!MEMOIZED.parser) {
|
||||
// @ts-ignore - Path is rewritten.
|
||||
const TreeSitter = (await import("rgthree/lib/tree-sitter.js")) as TreeSitter;
|
||||
await TreeSitter.Parser.init();
|
||||
const lang = await TreeSitter.Language.load("rgthree/lib/tree-sitter-python.wasm");
|
||||
MEMOIZED.parser = new TreeSitter.Parser() as Parser;
|
||||
MEMOIZED.parser.setLanguage(lang);
|
||||
}
|
||||
return MEMOIZED.parser.parse(code)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* The generic node handler, calls out to specific handlers based on the node type. This is
|
||||
* recursively called from other handlers.
|
||||
*/
|
||||
async function handleNode(
|
||||
node: Node,
|
||||
ctx: ExecutionContextData,
|
||||
builtIns: BuiltInFns,
|
||||
): Promise<any> {
|
||||
const type = node.type as string;
|
||||
|
||||
// If we have a returned value, then just return it, which should recursively settle.
|
||||
if (ctx.hasOwnProperty("__returned__")) return ctx["__returned__"];
|
||||
|
||||
// console.log(`-----`);
|
||||
// console.log(`eval_node`);
|
||||
// console.log(`type: ${type}`);
|
||||
// console.log(`text: ${node.text}`);
|
||||
// console.log(`children: ${node.children?.length ?? 0}`);
|
||||
// console.log(ctx);
|
||||
// console.log(node);
|
||||
|
||||
const handler = TYPE_TO_HANDLER.get(type);
|
||||
check(handler, "Unhandled type: " + type, node);
|
||||
return handler(node, ctx, builtIns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic handler to loop over children of a node, and evaluate each.
|
||||
*/
|
||||
async function handleChildren(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
let lastValue = null;
|
||||
for (const child of node.children) {
|
||||
if (!child) continue;
|
||||
lastValue = await handleNode(child, ctx, builtIns);
|
||||
}
|
||||
return lastValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swallows the execution. Likely just to allow development.
|
||||
*/
|
||||
async function handleSwallow(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
// No op
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a return statement.
|
||||
*/
|
||||
async function handleReturn(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const value = node.children.length > 1 ? handleNode(node.child(1), ctx, builtIns) : undefined;
|
||||
// Mark that we have a return value, as we may be deeper in evaluation, like going through an
|
||||
// if condition's body.
|
||||
ctx["__returned__"] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the retrieval of a variable identifier, already be set in the context.
|
||||
*/
|
||||
async function handleIdentifier(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
let value = ctx[node.text];
|
||||
if (value === undefined) {
|
||||
value = builtIns[node.text]?.fn ?? undefined;
|
||||
}
|
||||
return maybeWrapValue(value);
|
||||
}
|
||||
|
||||
async function handleAttribute(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const children = node.children;
|
||||
check(children.length === 3, "Expected 3 children for attribute.");
|
||||
check(children[1]!.type === ".", "Expected middle child to be '.' for attribute.");
|
||||
const inst = await handleNode(children[0]!, ctx, builtIns);
|
||||
// const attr = await handleNode(node.child(2), inst);
|
||||
// console.log('handleAttribute', inst, attr);
|
||||
const attr = children[2]!.text;
|
||||
checkAttributeAccessibility(inst, attr);
|
||||
let attribute = maybeWrapValue(inst[attr]);
|
||||
// check(attribute !== undefined, `"${attr}" not found on instance of type ${typeof inst}.`);
|
||||
// If the attribute is a function, then bind it to the instance.
|
||||
return typeof attribute === "function" ? attribute.bind(inst) : attribute;
|
||||
}
|
||||
|
||||
async function handleSubscript(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const children = node.children;
|
||||
check(children.length === 4, "Expected 4 children for subscript.");
|
||||
check(children[1]!.type === "[", "Expected 2nd child to be '[' for subscript.");
|
||||
check(children[3]!.type === "]", "Expected 4thd child to be ']' for subscript.");
|
||||
const inst = await handleNode(children[0]!, ctx, builtIns);
|
||||
const attr = await handleNode(children[2]!, ctx, builtIns);
|
||||
if (inst instanceof PyTuple && isInt(attr)) {
|
||||
return maybeWrapValue(inst.__at__(attr));
|
||||
}
|
||||
if (inst instanceof PyDict && typeof attr === "string") {
|
||||
return maybeWrapValue(inst.get(attr));
|
||||
}
|
||||
checkAttributeAccessibility(inst, attr);
|
||||
let attribute = maybeWrapValue(inst[attr]);
|
||||
return typeof attribute === "function" ? attribute.bind(inst) : attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the assignment.
|
||||
*/
|
||||
async function handleAssignment(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
check(
|
||||
node.children.length === 3,
|
||||
"Expected 3 children for assignment: identifier/attr, =, and value.",
|
||||
);
|
||||
check(node.children[1]!.type === "=", "Expected middle child to be an '='.");
|
||||
|
||||
let right = await handleNode(node.children[2]!, ctx, builtIns);
|
||||
const leftNode = node.children[0]!;
|
||||
let leftObj: any = ctx;
|
||||
let leftProp: string | number = "";
|
||||
if (leftNode.type === "identifier") {
|
||||
leftProp = leftNode.text;
|
||||
} else if (leftNode.type === "attribute") {
|
||||
leftObj = await handleNode(leftNode.children[0]!, ctx, builtIns);
|
||||
check(
|
||||
leftNode.children[2]!.type === "identifier",
|
||||
"Expected left hand assignment attribute to be an identifier.",
|
||||
leftNode,
|
||||
);
|
||||
leftProp = leftNode.children[2]!.text;
|
||||
} else if (leftNode.type === "subscript") {
|
||||
leftObj = await handleNode(leftNode.children[0]!, ctx, builtIns);
|
||||
check(leftNode.children[1]!.type === "[");
|
||||
check(leftNode.children[3]!.type === "]");
|
||||
leftProp = await handleNode(leftNode.children[2]!, ctx, builtIns);
|
||||
} else {
|
||||
throw new Error(`Unhandled left-hand assignement type: ${leftNode.type}`);
|
||||
}
|
||||
|
||||
if (leftProp == null) {
|
||||
throw new Error(`No property to assign value`);
|
||||
}
|
||||
// If we're a PyTuple or extended from, then try add like a list (PyTuple will fail, PyList will
|
||||
// allow).
|
||||
if (leftObj instanceof PyTuple) {
|
||||
check(isInt(leftProp), "Expected an int for list assignment");
|
||||
leftObj.__put__(leftProp, right);
|
||||
} else if (leftObj instanceof PyDict) {
|
||||
check(typeof leftProp === "string", "Expected a string for dict assignment");
|
||||
leftObj.__put__(leftProp, right);
|
||||
} else {
|
||||
check(typeof leftProp === "string", "Expected a string for object assignment");
|
||||
// InitialExecutionContext can have anything added, otherwise we're a specific context and
|
||||
// should check for attribute accessibility.
|
||||
if (!(leftObj instanceof InitialExecuteContext)) {
|
||||
checkAttributeAccessibility(leftObj, leftProp);
|
||||
}
|
||||
leftObj[leftProp] = right;
|
||||
}
|
||||
return right;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a named expression, like assigning a var in a list comprehension with:
|
||||
* `[name for node in node_list if (name := node.name)]`
|
||||
*/
|
||||
async function handleNamedExpression(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
check(node.children.length === 3, "Expected three children for named expression.");
|
||||
check(node.child(0).type === "identifier", "Expected identifier first in named expression.");
|
||||
const varName = node.child(0).text;
|
||||
ctx[varName] = await handleNode(node.child(2), ctx, builtIns);
|
||||
return maybeWrapValue(ctx[varName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a function call.
|
||||
*/
|
||||
async function handleCall(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
check(node.children.length === 2, "Expected 2 children for call, identifier and arguments.");
|
||||
const fn = await handleNode(node.children[0]!, ctx, builtIns);
|
||||
const args = await handleNode(node.children[1]!, ctx, builtIns);
|
||||
console.log("handleCall", fn, args);
|
||||
return fn(...args);
|
||||
}
|
||||
|
||||
async function handleArgumentsList(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const args = (await handleList(node, ctx, builtIns)).__unwrap__(false);
|
||||
return [...args];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a simple for...in loop.
|
||||
*/
|
||||
async function handleForStatement(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const childs = node.children;
|
||||
check(childs.length === 6);
|
||||
check(childs[4]!.type === ":");
|
||||
check(childs[5]!.type === "block");
|
||||
await helperGetLoopForIn(node, ctx, builtIns, async (forCtx) => {
|
||||
await handleNode(childs[5]!, forCtx, builtIns);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleListComprehension(
|
||||
node: Node,
|
||||
ctx: ExecutionContextData,
|
||||
builtIns: BuiltInFns,
|
||||
) {
|
||||
// Create a new context that we don't want to pollute our outer one.
|
||||
const finalList = new PyList();
|
||||
const newCtx = {...ctx};
|
||||
|
||||
let finalEntryNode;
|
||||
const loopNodes: {forIn: Node; if?: Node}[] = [];
|
||||
|
||||
for (const child of node.children) {
|
||||
if (!child || ["[", "]"].includes(child.type)) continue;
|
||||
if (child.type === "identifier" || child.type === "attribute") {
|
||||
if (finalEntryNode) {
|
||||
throw Error("Already have a list comprehension finalEntryNode.");
|
||||
}
|
||||
finalEntryNode = child;
|
||||
} else if (child.type === "for_in_clause") {
|
||||
loopNodes.push({forIn: child});
|
||||
} else if (child.type === "if_clause") {
|
||||
loopNodes[loopNodes.length - 1]!["if"] = child;
|
||||
}
|
||||
}
|
||||
if (!finalEntryNode) {
|
||||
throw Error("No list comprehension finalEntryNode.");
|
||||
}
|
||||
|
||||
console.log(`handleListComprehension.loopNodes`, loopNodes);
|
||||
|
||||
const handleLoop = async (loopNodes: {forIn: Node; if?: Node}[]) => {
|
||||
const loopNode = loopNodes.shift()!;
|
||||
await helperGetLoopForIn(
|
||||
loopNode.forIn,
|
||||
newCtx,
|
||||
builtIns,
|
||||
async (forCtx) => {
|
||||
if (loopNode.if) {
|
||||
const ifNode = loopNode.if;
|
||||
check(ifNode.children.length === 2, "Expected 2 children for if_clause.");
|
||||
check(ifNode.child(0).text === "if", "Expected first child to be 'if'.");
|
||||
const good = await handleNode(ifNode.child(1), forCtx, builtIns);
|
||||
if (!good) return;
|
||||
}
|
||||
Object.assign(newCtx, forCtx);
|
||||
if (loopNodes.length) {
|
||||
await handleLoop(loopNodes);
|
||||
} else {
|
||||
finalList.append(await handleNode(finalEntryNode, newCtx, builtIns));
|
||||
}
|
||||
},
|
||||
() => ({...newCtx}),
|
||||
);
|
||||
loopNodes.unshift(loopNode);
|
||||
};
|
||||
|
||||
await handleLoop(loopNodes);
|
||||
return finalList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the identifiers, iterable, and initial looping with context setting. Handles both simple
|
||||
* identifiers (like `for item in items`) or a pattern list (like `for key, val in mydict.items()`).
|
||||
*
|
||||
* @param eachFn The function to call for each iteration. Will be passed the current context with
|
||||
* the identifiers assigned.
|
||||
* @param provideForCtx An optional function that can provide an `ctx`. If not supplied the passed
|
||||
* `ctx` param will be used. This is useful for providing a new ctx to use for cases like an
|
||||
* if condition in a list comprhension where we don't want to add to the current context unless
|
||||
* the condition is met.
|
||||
*/
|
||||
async function helperGetLoopForIn(
|
||||
node: Node,
|
||||
ctx: ExecutionContextData,
|
||||
builtIns: BuiltInFns,
|
||||
eachFn: (forCtx: ExecutionContextData) => Promise<void>,
|
||||
provideForCtx?: () => ExecutionContextData,
|
||||
) {
|
||||
const childs = node.children;
|
||||
check(childs.length >= 3);
|
||||
check(childs[0]!.type === "for");
|
||||
check(
|
||||
["identifier", "pattern_list"].includes(childs[1]!.type),
|
||||
"Expected identifier for for loop.",
|
||||
);
|
||||
check(childs[2]!.type === "in");
|
||||
|
||||
let identifiers: string[];
|
||||
if (childs[1]!.type === "identifier") {
|
||||
// identifier: for k in my_list
|
||||
identifiers = [childs[1]!.text];
|
||||
} else {
|
||||
// pattern_list: for k,v in my_dict.items()
|
||||
identifiers = childs[1]!.children
|
||||
.map((n) => {
|
||||
if (n.type === ",") return null;
|
||||
check(n.type === "identifier");
|
||||
return node.text;
|
||||
})
|
||||
.filter((n) => n != null);
|
||||
}
|
||||
const iterable = await handleNode(childs[3]!, ctx, builtIns);
|
||||
check(iterable instanceof PyTuple, "Expected for loop instance to be a list/tuple.");
|
||||
|
||||
for (const item of iterable.__unwrap__(false)) {
|
||||
const forCtx = provideForCtx?.() ?? ctx;
|
||||
if (identifiers.length === 1) {
|
||||
forCtx[identifiers[0]!] = item;
|
||||
} else {
|
||||
check(
|
||||
Array.isArray(item) && identifiers.length === item.length,
|
||||
"Expected iterable to be a list, like using dict.items()",
|
||||
);
|
||||
for (let i = 0; i < identifiers.length; i++) {
|
||||
forCtx[identifiers[i]!] = item[i];
|
||||
}
|
||||
}
|
||||
await eachFn(forCtx);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNumber(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
return Number(node.text);
|
||||
}
|
||||
|
||||
async function handleString(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
// check(node.children.length === 3, "Expected 3 children for str (quotes and value).");
|
||||
let str = "";
|
||||
for (const child of node.children) {
|
||||
if (!child || ["string_start", "string_end"].includes(child.type)) continue;
|
||||
if (child.type === "string_content") {
|
||||
str += child.text;
|
||||
} else if (child.type === "interpolation") {
|
||||
check(child.children.length === 3, "Expected interpolation");
|
||||
str += await handleNode(child, ctx, builtIns);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
async function handleInterpolation(node: Node, ...args: NodeHandlerArgs) {
|
||||
check(node.children.length === 3, "Expected interpolation to be three nodes length.");
|
||||
check(
|
||||
node.children[0]!.type === "{" && node.children[2]!.type === "}",
|
||||
'Expected interpolation to be wrapped in "{" and "}".',
|
||||
);
|
||||
return await handleNode(node.children[1]!, ...args);
|
||||
}
|
||||
|
||||
async function handleList(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const list = [];
|
||||
for (const child of node.children) {
|
||||
if (!child || ["(", "[", ",", "]", ")"].includes(child.type)) continue;
|
||||
list.push(await handleNode(child, ctx, builtIns));
|
||||
}
|
||||
if (node.type === "tuple") {
|
||||
return new PyTuple(list);
|
||||
}
|
||||
return new PyList(list);
|
||||
}
|
||||
|
||||
async function handleComparisonOperator(
|
||||
node: Node,
|
||||
ctx: ExecutionContextData,
|
||||
builtIns: BuiltInFns,
|
||||
) {
|
||||
const op = node.child(1).text;
|
||||
const left = await handleNode(node.child(0), ctx, builtIns);
|
||||
const right = await handleNode(node.child(2), ctx, builtIns);
|
||||
if (op === "==") return left === right; // Python '==' is equiv to '===' in JS.
|
||||
if (op === "!=") return left !== right;
|
||||
if (op === ">") return left > right;
|
||||
if (op === ">=") return left >= right;
|
||||
if (op === "<") return left < right;
|
||||
if (op === "<=") return left <= right;
|
||||
if (op === "in") return (right.__unwrap__ ? right.__unwrap__(false) : right).includes(left);
|
||||
throw new Error(`Comparison not handled: "${op}"`);
|
||||
}
|
||||
async function handleBooleanOperator(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const op = node.child(1).text;
|
||||
const left = await handleNode(node.child(0), ctx, builtIns);
|
||||
// If we're an AND and already false, then don't even evaluate the right.
|
||||
if (!left && op === "and") return left;
|
||||
const right = await handleNode(node.child(2), ctx, builtIns);
|
||||
if (op === "and") return left && right;
|
||||
if (op === "or") return left || right;
|
||||
}
|
||||
|
||||
async function handleBinaryOperator(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const op = node.child(1).text;
|
||||
const left = await handleNode(node.child(0), ctx, builtIns);
|
||||
const right = await handleNode(node.child(2), ctx, builtIns);
|
||||
if (left.constructor !== right.constructor) {
|
||||
throw new Error(`Can only run ${op} operator on same type.`);
|
||||
}
|
||||
if (op === "+") return left.__add__ ? left.__add__(right) : left + right;
|
||||
if (op === "-") return left - right;
|
||||
if (op === "/") return left / right;
|
||||
if (op === "//") return Math.floor(left / right);
|
||||
if (op === "*") return left * right;
|
||||
if (op === "%") return left % right;
|
||||
if (op === "&") return left & right;
|
||||
if (op === "|") return left | right;
|
||||
if (op === "^") return left ^ right;
|
||||
if (op === "<<") return left << right;
|
||||
if (op === ">>") return left >> right;
|
||||
throw new Error(`Comparison not handled: "${op}"`);
|
||||
}
|
||||
|
||||
async function handleNotOperator(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
check(node.children.length === 2, "Expected 2 children for not operator.");
|
||||
check(node.child(0).text === "not", "Expected first child to be 'not'.");
|
||||
const value = await handleNode(node.child(1), ctx, builtIns);
|
||||
return !value;
|
||||
}
|
||||
|
||||
async function handleUnaryOperator(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
check(node.children.length === 2, "Expected 2 children for not operator.");
|
||||
const value = await handleNode(node.child(1), ctx, builtIns);
|
||||
const op = node.child(0).text;
|
||||
if (op === "-") return value * -1;
|
||||
console.warn(`Unhandled unary operator: ${op}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
async function handleDictionary(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
const dict = new PyDict();
|
||||
for (const child of node.children) {
|
||||
if (!child || ["{", ",", "}"].includes(child.type)) continue;
|
||||
check(child.type === "pair", "Expected a pair type for dict.");
|
||||
const pair = await handleNode(child, ctx, builtIns);
|
||||
dict.__put__(pair[0], pair[1]);
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
async function handleDictionaryPair(node: Node, ctx: ExecutionContextData, builtIns: BuiltInFns) {
|
||||
check(node.children.length === 3, "Expected 3 children for dict pair.");
|
||||
let varName = await handleNode(node.child(0)!, ctx, builtIns);
|
||||
let varValue = await handleNode(node.child(2)!, ctx, builtIns);
|
||||
check(typeof varName === "string", "Expected varname to be string.");
|
||||
return [varName, varValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps some common functionality of a TreeSitterNode.
|
||||
*/
|
||||
class Node {
|
||||
type: string;
|
||||
text: string;
|
||||
children: Node[];
|
||||
private node: TreeSitterNode;
|
||||
|
||||
constructor(node: TreeSitterNode) {
|
||||
this.type = node.type;
|
||||
this.text = node.text;
|
||||
if (this.type === "ERROR") {
|
||||
throw new Error(`Error found in parsing near "${this.text}"`);
|
||||
}
|
||||
this.children = [];
|
||||
for (const child of node.children) {
|
||||
this.children.push(new Node(child!));
|
||||
}
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
child(index: number): Node {
|
||||
const child = this.children[index];
|
||||
if (!child) throw Error(`No child at index ${index}.`);
|
||||
return child;
|
||||
}
|
||||
|
||||
log(tab = "", showNode = false) {
|
||||
console.log(`${tab}--- Node`);
|
||||
console.log(`${tab} type: ${this.type}`);
|
||||
console.log(`${tab} text: ${this.text}`);
|
||||
console.log(`${tab} children:`, this.children);
|
||||
if (showNode) {
|
||||
console.log(`${tab} node:`, this.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A type that mimics a Python Tuple.
|
||||
*/
|
||||
export class PyTuple {
|
||||
protected list: any[];
|
||||
constructor(...args: any[]) {
|
||||
if (args.length === 1 && args[0] instanceof PyTuple) {
|
||||
args = args[0].__unwrap__(false);
|
||||
}
|
||||
if (args.length === 1 && Array.isArray(args[0])) {
|
||||
args = [...args[0]];
|
||||
}
|
||||
this.list = [...args];
|
||||
}
|
||||
|
||||
@Exposed count(v: any) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Exposed index() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
__at__(index: number) {
|
||||
index = this.__get_relative_index__(index);
|
||||
return this.list[index];
|
||||
}
|
||||
|
||||
__len__() {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
__add__(v: any) {
|
||||
if (!(v instanceof PyTuple)) {
|
||||
throw new Error("Can only concatenate tuple to tuple.");
|
||||
}
|
||||
return new PyTuple(this.__unwrap__(false).concat(v.__unwrap__(false)));
|
||||
}
|
||||
|
||||
/** Puts the value to the current, existing index. Not available for Tuple. */
|
||||
__put__(index: number, v: any) {
|
||||
throw new Error("Tuple does not support item assignment");
|
||||
}
|
||||
|
||||
/** Gets the index for the current list, with negative index support. Throws if out of range. */
|
||||
protected __get_relative_index__(index: number) {
|
||||
if (index >= 0) {
|
||||
check(this.list.length > index, `Index ${index} out of range.`);
|
||||
return index;
|
||||
}
|
||||
const relIndex = this.list.length + index;
|
||||
check(relIndex >= 0, `Index ${index} out of range.`);
|
||||
return relIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively unwraps the PyTuple returning an Array.
|
||||
*/
|
||||
__unwrap__(deep = true) {
|
||||
const l = [...this.list];
|
||||
if (deep) {
|
||||
for (let i = 0; i < l.length; i++) {
|
||||
l[i] = l[i]?.__unwrap__ ? l[i].__unwrap__(deep) : l[i];
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
// a = [
|
||||
// "__add__",
|
||||
// "__class__",
|
||||
// "__class_getitem__",
|
||||
// "__contains__",
|
||||
// "__delattr__",
|
||||
// "__dir__",
|
||||
// "__doc__",
|
||||
// "__eq__",
|
||||
// "__format__",
|
||||
// "__ge__",
|
||||
// "__getattribute__",
|
||||
// "__getitem__",
|
||||
// "__getnewargs__",
|
||||
// "__gt__",
|
||||
// "__hash__",
|
||||
// "__init__",
|
||||
// "__init_subclass__",
|
||||
// "__iter__",
|
||||
// "__le__",
|
||||
// "__len__",
|
||||
// "__lt__",
|
||||
// "__mul__",
|
||||
// "__ne__",
|
||||
// "__new__",
|
||||
// "__reduce__",
|
||||
// "__reduce_ex__",
|
||||
// "__repr__",
|
||||
// "__rmul__",
|
||||
// "__setattr__",
|
||||
// "__sizeof__",
|
||||
// "__str__",
|
||||
// "__subclasshook__",
|
||||
// "count",
|
||||
// "index",
|
||||
// ];
|
||||
}
|
||||
|
||||
/**
|
||||
* A type that mimics a Python List.
|
||||
*/
|
||||
export class PyList extends PyTuple {
|
||||
@Exposed append(...args: any[]) {
|
||||
this.list.push(...args);
|
||||
}
|
||||
|
||||
@Exposed clear() {
|
||||
this.list.length = 0;
|
||||
}
|
||||
|
||||
@Exposed copy() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Exposed override count() {
|
||||
// TODO
|
||||
}
|
||||
@Exposed extend() {
|
||||
// TODO
|
||||
}
|
||||
@Exposed override index() {
|
||||
// TODO
|
||||
}
|
||||
@Exposed insert() {
|
||||
// TODO
|
||||
}
|
||||
@Exposed pop() {
|
||||
// TODO
|
||||
}
|
||||
@Exposed remove() {
|
||||
// TODO
|
||||
}
|
||||
@Exposed reverse() {
|
||||
// TODO
|
||||
}
|
||||
@Exposed sort() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override __add__(v: any) {
|
||||
if (!(v instanceof PyList)) {
|
||||
throw new Error("Can only concatenate list to list.");
|
||||
}
|
||||
return new PyList(this.__unwrap__(false).concat(v.__unwrap__(false)));
|
||||
}
|
||||
|
||||
/** Assigns an element to the current, existing index. Overriden for support on lists. */
|
||||
override __put__(index: number, v: any) {
|
||||
index = this.__get_relative_index__(index);
|
||||
this.list[index] = v;
|
||||
}
|
||||
|
||||
// aa = [
|
||||
// "__add__",
|
||||
// "__class__",
|
||||
// "__class_getitem__",
|
||||
// "__contains__",
|
||||
// "__delattr__",
|
||||
// "__delitem__",
|
||||
// "__dir__",
|
||||
// "__doc__",
|
||||
// "__eq__",
|
||||
// "__format__",
|
||||
// "__ge__",
|
||||
// "__getattribute__",
|
||||
// "__getitem__",
|
||||
// "__gt__",
|
||||
// "__hash__",
|
||||
// "__iadd__",
|
||||
// "__imul__",
|
||||
// "__init__",
|
||||
// "__init_subclass__",
|
||||
// "__iter__",
|
||||
// "__le__",
|
||||
// "__len__",
|
||||
// "__lt__",
|
||||
// "__mul__",
|
||||
// "__ne__",
|
||||
// "__new__",
|
||||
// "__reduce__",
|
||||
// "__reduce_ex__",
|
||||
// "__repr__",
|
||||
// "__reversed__",
|
||||
// "__rmul__",
|
||||
// "__setattr__",
|
||||
// "__setitem__",
|
||||
// "__sizeof__",
|
||||
// "__str__",
|
||||
// "__subclasshook__",
|
||||
// ];
|
||||
}
|
||||
|
||||
class PyInt {}
|
||||
|
||||
class PyDict {
|
||||
#dict: {[key: string]: any};
|
||||
constructor(dict?: {[key: string]: any}) {
|
||||
this.#dict = {...(dict ?? {})};
|
||||
}
|
||||
|
||||
@Exposed clear() {} // Removes all the elements from the dictionary
|
||||
@Exposed copy() {} // Returns a copy of the dictionary
|
||||
@Exposed fromkeys() {} // Returns a dictionary with the specified keys and value
|
||||
/** Returns the value of the specified key. */
|
||||
@Exposed get(key: string) {
|
||||
return this.#dict[key];
|
||||
}
|
||||
/** Returns a list containing a tuple for each key value pair. */
|
||||
@Exposed items() {
|
||||
return new PyTuple(Object.entries(this.#dict).map((e) => new PyTuple(e)));
|
||||
}
|
||||
@Exposed keys() {} // Returns a list containing the dictionary's keys
|
||||
@Exposed pop() {} // Removes the element with the specified key
|
||||
@Exposed popitem() {} // Removes the last inserted key-value pair
|
||||
@Exposed setdefault() {} // Returns the value of the specified key. If the key does not exist: insert the key, with the specified value
|
||||
@Exposed update() {} // Updates the dictionary with the specified key-value pairs
|
||||
@Exposed values() {} // Returns a list of all the values in the dictionary
|
||||
|
||||
__put__(key: string, v: any) {
|
||||
this.#dict[key] = v;
|
||||
}
|
||||
|
||||
__len__() {
|
||||
return Object.keys(this.#dict).length;
|
||||
}
|
||||
|
||||
// a = [
|
||||
// "__class__",
|
||||
// "__class_getitem__",
|
||||
// "__contains__",
|
||||
// "__delattr__",
|
||||
// "__delitem__",
|
||||
// "__dir__",
|
||||
// "__doc__",
|
||||
// "__eq__",
|
||||
// "__format__",
|
||||
// "__ge__",
|
||||
// "__getattribute__",
|
||||
// "__getitem__",
|
||||
// "__gt__",
|
||||
// "__hash__",
|
||||
// "__init__",
|
||||
// "__init_subclass__",
|
||||
// "__ior__",
|
||||
// "__iter__",
|
||||
// "__le__",
|
||||
// "__lt__",
|
||||
// "__ne__",
|
||||
// "__new__",
|
||||
// "__or__",
|
||||
// "__reduce__",
|
||||
// "__reduce_ex__",
|
||||
// "__repr__",
|
||||
// "__reversed__",
|
||||
// "__ror__",
|
||||
// "__setattr__",
|
||||
// "__setitem__",
|
||||
// "__sizeof__",
|
||||
// "__str__",
|
||||
// "__subclasshook__",
|
||||
// ];
|
||||
|
||||
/**
|
||||
* Recursively unwraps the PyDict returning an Object.
|
||||
*/
|
||||
__unwrap__(deep = true) {
|
||||
const d = {...this.#dict};
|
||||
if (deep) {
|
||||
for (let k of Object.keys(d)) {
|
||||
d[k] = d[k]?.__unwrap__ ? d[k].__unwrap__(deep) : d[k];
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply unwraps a list of values.
|
||||
*/
|
||||
function __unwrap__(...args: any[]) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
args[i] = args[i]?.__unwrap__ ? args[i].__unwrap__(true) : args[i];
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if access to the attribute/method is allowed.
|
||||
*/
|
||||
function checkAttributeAccessibility(inst: any, attr: string) {
|
||||
const instType = typeof inst;
|
||||
check(
|
||||
instType === "object" || instType === "function",
|
||||
`Instance of type ${instType} does not have attributes.`,
|
||||
);
|
||||
|
||||
// If the attr starts and ends with a "__" then consider it unaccessible.
|
||||
check(!attr.startsWith("__") && !attr.endsWith("__"), `"${attr}" is not accessible.`);
|
||||
|
||||
const attrType = typeof inst[attr];
|
||||
if (attrType === "function") {
|
||||
const allowedMethods = inst.constructor?.__ALLOWED_METHODS__ ?? inst.__ALLOWED_METHODS__ ?? [];
|
||||
check(allowedMethods.includes(attr), `Method ${attr} is not accessible.`);
|
||||
} else {
|
||||
const allowedProps =
|
||||
inst.constructor?.__ALLOWED_PROPERTIES__ ?? inst.__ALLOWED_PROPERTIES__ ?? [];
|
||||
check(allowedProps.includes(attr), `Property ${attr} is not accessible.`);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeWrapValue(value: any) {
|
||||
if (Array.isArray(value)) {
|
||||
return new PyList(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isInt(value: any): value is number {
|
||||
return typeof value === "number" && Math.round(value) === value;
|
||||
}
|
||||
|
||||
function isIntLike(value: any): boolean {
|
||||
let is = isInt(value);
|
||||
if (!is) {
|
||||
is = typeof value === "string" && !!/^\d+$/.exec(value);
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
/**
|
||||
* An experimental decorator to add allowed properties and methods to an instance. Decorated
|
||||
* properties and methods on a class, and they'll be added to a static __ALLOWED_PROPERTIES__ and
|
||||
* __ALLOWED_METHODS__ lists, which can then be checked while parsing to ensure entered code
|
||||
* cannot end up calling something more.
|
||||
*
|
||||
* Note: The decorator does no work on static members; only on instance properties, methods, and
|
||||
* getters (or setters). If you wish to allow access to only a getter and not setter, then you'll
|
||||
* need not define the setter (or vice-versa), as adding `@Exposed` to a getter/setter decorates
|
||||
* the property entirely, not just that individual getter/setter.
|
||||
*/
|
||||
export function Exposed(target: any, key: string) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
||||
if (typeof descriptor?.value === "function") {
|
||||
target.constructor.__ALLOWED_METHODS__ = target.constructor.__ALLOWED_METHODS__ || [];
|
||||
target.constructor.__ALLOWED_METHODS__.push(key);
|
||||
} else {
|
||||
target.constructor.__ALLOWED_PROPERTIES__ = target.constructor.__ALLOWED_PROPERTIES__ || [];
|
||||
target.constructor.__ALLOWED_PROPERTIES__.push(key);
|
||||
}
|
||||
}
|
||||
209
custom_nodes/rgthree-comfy/src_web/common/rgthree_api.ts
Normal file
209
custom_nodes/rgthree-comfy/src_web/common/rgthree_api.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import type {RgthreeModelInfo} from "typings/rgthree.js";
|
||||
|
||||
export type ModelInfoType = "loras" | "checkpoints";
|
||||
|
||||
type ModelsOptions = {
|
||||
type: ModelInfoType;
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
type GetModelsOptions = ModelsOptions & {
|
||||
type: ModelInfoType;
|
||||
files?: string[];
|
||||
format?: null | "details";
|
||||
};
|
||||
|
||||
type GetModelsInfoOptions = GetModelsOptions & {
|
||||
light?: boolean;
|
||||
};
|
||||
|
||||
type GetModelsResponseDetails = {
|
||||
file: string;
|
||||
modified: number;
|
||||
has_info: boolean;
|
||||
image?: string;
|
||||
};
|
||||
|
||||
class RgthreeApi {
|
||||
private baseUrl!: string;
|
||||
private comfyBaseUrl!: string;
|
||||
getCheckpointsPromise: Promise<string[]> | null = null;
|
||||
getSamplersPromise: Promise<string[]> | null = null;
|
||||
getSchedulersPromise: Promise<string[]> | null = null;
|
||||
getLorasPromise: Promise<GetModelsResponseDetails[]> | null = null;
|
||||
getWorkflowsPromise: Promise<string[]> | null = null;
|
||||
|
||||
constructor(baseUrl?: string) {
|
||||
this.setBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
setBaseUrl(baseUrlArg?: string) {
|
||||
let baseUrl = null;
|
||||
if (baseUrlArg) {
|
||||
baseUrl = baseUrlArg;
|
||||
} else if (window.location.pathname.includes("/rgthree/")) {
|
||||
// Try to find how many relatives paths we need to go back to hit ./rgthree/api
|
||||
const parts = window.location.pathname.split("/rgthree/")[1]?.split("/");
|
||||
if (parts && parts.length) {
|
||||
baseUrl = parts.map(() => "../").join("") + "rgthree/api";
|
||||
}
|
||||
}
|
||||
this.baseUrl = baseUrl || "./rgthree/api";
|
||||
|
||||
// Calculate the comfyUI api base path by checkin gif we're on an rgthree independant page (as
|
||||
// we'll always use '/rgthree/' prefix) and, if so, assume the path before `/rgthree/` is the
|
||||
// base path. If we're not, then just use the same pathname logic as the ComfyUI api.js uses.
|
||||
const comfyBasePathname = location.pathname.includes("/rgthree/")
|
||||
? location.pathname.split("rgthree/")[0]!
|
||||
: location.pathname;
|
||||
this.comfyBaseUrl = comfyBasePathname.split("/").slice(0, -1).join("/");
|
||||
}
|
||||
|
||||
apiURL(route: string) {
|
||||
return `${this.baseUrl}${route}`;
|
||||
}
|
||||
|
||||
fetchApi(route: string, options?: RequestInit) {
|
||||
return fetch(this.apiURL(route), options);
|
||||
}
|
||||
|
||||
async fetchJson(route: string, options?: RequestInit) {
|
||||
const r = await this.fetchApi(route, options);
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
async postJson(route: string, json: any) {
|
||||
const body = new FormData();
|
||||
body.append("json", JSON.stringify(json));
|
||||
return await rgthreeApi.fetchJson(route, {method: "POST", body});
|
||||
}
|
||||
|
||||
getLoras(force = false) {
|
||||
if (!this.getLorasPromise || force) {
|
||||
this.getLorasPromise = this.fetchJson("/loras?format=details", {cache: "no-store"});
|
||||
}
|
||||
return this.getLorasPromise;
|
||||
}
|
||||
|
||||
async fetchApiJsonOrNull<T>(route: string, options?: RequestInit) {
|
||||
const response = await this.fetchJson(route, options);
|
||||
if (response.status === 200 && response.data) {
|
||||
return (response.data as T) || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the lora information.
|
||||
*
|
||||
* @param light Whether or not to generate a json file if there isn't one. This isn't necessary if
|
||||
* we're just checking for values, but is more necessary when opening an info dialog.
|
||||
*/
|
||||
|
||||
async getModelsInfo(options: GetModelsInfoOptions): Promise<RgthreeModelInfo[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (options.files?.length) {
|
||||
params.set("files", options.files.join(","));
|
||||
}
|
||||
if (options.light) {
|
||||
params.set("light", "1");
|
||||
}
|
||||
if (options.format) {
|
||||
params.set("format", options.format);
|
||||
}
|
||||
const path = `/${options.type}/info?` + params.toString();
|
||||
return (await this.fetchApiJsonOrNull<RgthreeModelInfo[]>(path)) || [];
|
||||
}
|
||||
async getLorasInfo(options: Omit<GetModelsInfoOptions, "type"> = {}) {
|
||||
return this.getModelsInfo({type: "loras", ...options});
|
||||
}
|
||||
async getCheckpointsInfo(options: Omit<GetModelsInfoOptions, "type"> = {}) {
|
||||
return this.getModelsInfo({type: "checkpoints", ...options});
|
||||
}
|
||||
|
||||
async refreshModelsInfo(options: ModelsOptions) {
|
||||
const params = new URLSearchParams();
|
||||
if (options.files?.length) {
|
||||
params.set("files", options.files.join(","));
|
||||
}
|
||||
const path = `/${options.type}/info/refresh?` + params.toString();
|
||||
const infos = await this.fetchApiJsonOrNull<RgthreeModelInfo[]>(path);
|
||||
return infos;
|
||||
}
|
||||
async refreshLorasInfo(options: Omit<ModelsOptions, "type"> = {}) {
|
||||
return this.refreshModelsInfo({type: "loras", ...options});
|
||||
}
|
||||
async refreshCheckpointsInfo(options: Omit<ModelsOptions, "type"> = {}) {
|
||||
return this.refreshModelsInfo({type: "checkpoints", ...options});
|
||||
}
|
||||
|
||||
async clearModelsInfo(options: ModelsOptions) {
|
||||
const params = new URLSearchParams();
|
||||
if (options.files?.length) {
|
||||
// encodeURIComponent ?
|
||||
params.set("files", options.files.join(","));
|
||||
}
|
||||
const path = `/${options.type}/info/clear?` + params.toString();
|
||||
await this.fetchApiJsonOrNull<RgthreeModelInfo[]>(path);
|
||||
return;
|
||||
}
|
||||
async clearLorasInfo(options: Omit<ModelsOptions, "type"> = {}) {
|
||||
return this.clearModelsInfo({type: "loras", ...options});
|
||||
}
|
||||
async clearCheckpointsInfo(options: Omit<ModelsOptions, "type"> = {}) {
|
||||
return this.clearModelsInfo({type: "checkpoints", ...options});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves partial data sending it to the backend..
|
||||
*/
|
||||
async saveModelInfo(
|
||||
type: ModelInfoType,
|
||||
file: string,
|
||||
data: Partial<RgthreeModelInfo>,
|
||||
): Promise<RgthreeModelInfo | null> {
|
||||
const body = new FormData();
|
||||
body.append("json", JSON.stringify(data));
|
||||
return await this.fetchApiJsonOrNull<RgthreeModelInfo>(
|
||||
`/${type}/info?file=${encodeURIComponent(file)}`,
|
||||
{cache: "no-store", method: "POST", body},
|
||||
);
|
||||
}
|
||||
|
||||
async saveLoraInfo(
|
||||
file: string,
|
||||
data: Partial<RgthreeModelInfo>,
|
||||
): Promise<RgthreeModelInfo | null> {
|
||||
return this.saveModelInfo("loras", file, data);
|
||||
}
|
||||
|
||||
async saveCheckpointsInfo(
|
||||
file: string,
|
||||
data: Partial<RgthreeModelInfo>,
|
||||
): Promise<RgthreeModelInfo | null> {
|
||||
return this.saveModelInfo("checkpoints", file, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* [🤮] Fetches from the ComfyUI given a similar functionality to the real ComfyUI API
|
||||
* implementation, but can be available on independant pages outside of the ComfyUI UI. This is
|
||||
* because ComfyUI frontend stopped serving its modules independantly and opted for a giant bundle
|
||||
* instead which no longer allows us to load its `api.js` file separately.
|
||||
*/
|
||||
fetchComfyApi(route: string, options?: any): Promise<any> {
|
||||
const url = this.comfyBaseUrl + "/api" + route;
|
||||
options = options || {};
|
||||
options.headers = options.headers || {};
|
||||
options.cache = options.cache || "no-cache";
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* A way to log to the terminal from JS.
|
||||
*/
|
||||
print(messageType: string) {
|
||||
this.fetchApi(`/print?type=${messageType}`, {})
|
||||
}
|
||||
}
|
||||
|
||||
export const rgthreeApi = new RgthreeApi();
|
||||
581
custom_nodes/rgthree-comfy/src_web/common/shared_utils.ts
Normal file
581
custom_nodes/rgthree-comfy/src_web/common/shared_utils.ts
Normal file
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* A bunch of shared utils that can be used in ComfyUI, as well as in any single-HTML pages.
|
||||
*/
|
||||
|
||||
export type Resolver<T> = {
|
||||
id: string;
|
||||
completed: boolean;
|
||||
resolved: boolean;
|
||||
rejected: boolean;
|
||||
promise: Promise<T>;
|
||||
resolve: (data: T) => void;
|
||||
reject: (e?: Error) => void;
|
||||
timeout: number | null;
|
||||
deferment?: {data?: any; timeout?: number | null; signal?: string};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a new `Resolver` type that allows creating a "disconnected" `Promise` that can be
|
||||
* returned and resolved separately.
|
||||
*/
|
||||
export function getResolver<T>(timeout: number = 5000): Resolver<T> {
|
||||
const resolver: Partial<Resolver<T>> = {};
|
||||
resolver.id = generateId(8);
|
||||
resolver.completed = false;
|
||||
resolver.resolved = false;
|
||||
resolver.rejected = false;
|
||||
resolver.promise = new Promise((resolve, reject) => {
|
||||
resolver.reject = (e?: Error) => {
|
||||
resolver.completed = true;
|
||||
resolver.rejected = true;
|
||||
reject(e);
|
||||
};
|
||||
resolver.resolve = (data: T) => {
|
||||
resolver.completed = true;
|
||||
resolver.resolved = true;
|
||||
resolve(data);
|
||||
};
|
||||
});
|
||||
resolver.timeout = setTimeout(() => {
|
||||
if (!resolver.completed) {
|
||||
resolver.reject!();
|
||||
}
|
||||
}, timeout);
|
||||
return resolver as Resolver<T>;
|
||||
}
|
||||
|
||||
/** The WeakMap for debounced functions. */
|
||||
const DEBOUNCE_FN_TO_PROMISE: WeakMap<Function, Promise<void>> = new WeakMap();
|
||||
|
||||
/**
|
||||
* Debounces a function call so it is only called once in the initially provided ms even if asked
|
||||
* to be called multiple times within that period.
|
||||
*/
|
||||
export function debounce(fn: Function, ms = 64) {
|
||||
if (!DEBOUNCE_FN_TO_PROMISE.get(fn)) {
|
||||
DEBOUNCE_FN_TO_PROMISE.set(
|
||||
fn,
|
||||
wait(ms).then(() => {
|
||||
DEBOUNCE_FN_TO_PROMISE.delete(fn);
|
||||
fn();
|
||||
}),
|
||||
);
|
||||
}
|
||||
return DEBOUNCE_FN_TO_PROMISE.get(fn);
|
||||
}
|
||||
|
||||
/** Checks that a value is not falsy. */
|
||||
export function check(value: any, msg = "", ...args: any[]): asserts value {
|
||||
if (!value) {
|
||||
console.error(msg, ...(args || []));
|
||||
throw new Error(msg || "Error");
|
||||
}
|
||||
}
|
||||
|
||||
/** Waits a certain number of ms, as a `Promise.` */
|
||||
export function wait(ms = 16): Promise<void> {
|
||||
// Special logic, if we're waiting 16ms, then trigger on next frame.
|
||||
if (ms === 16) {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
/** Deeply freezes the passed in object. */
|
||||
export function deepFreeze<T extends Object>(obj: T): T {
|
||||
// Retrieve the property names defined on object
|
||||
const propNames = Reflect.ownKeys(obj);
|
||||
|
||||
// Freeze properties before freezing self
|
||||
for (const name of propNames) {
|
||||
const value = (obj as any)[name];
|
||||
if ((value && typeof value === "object") || typeof value === "function") {
|
||||
deepFreeze(value);
|
||||
}
|
||||
}
|
||||
return Object.freeze(obj);
|
||||
}
|
||||
|
||||
function dec2hex(dec: number) {
|
||||
return dec.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
/** Generates an unique id of a specific length. */
|
||||
export function generateId(length: number) {
|
||||
const arr = new Uint8Array(length / 2);
|
||||
crypto.getRandomValues(arr);
|
||||
return Array.from(arr, dec2hex).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deep value of an object given a dot-delimited key.
|
||||
*/
|
||||
export function getObjectValue(obj: {[key: string]: any}, objKey: string, def?: any) {
|
||||
if (!obj || !objKey) return def;
|
||||
|
||||
const keys = objKey.split(".");
|
||||
const key = keys.shift()!;
|
||||
const found = obj[key];
|
||||
if (keys.length) {
|
||||
return getObjectValue(found, keys.join("."), def);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the deep value of an object given a dot-delimited key.
|
||||
*
|
||||
* By default, missing objects will be created while settng the path. If `createMissingObjects` is
|
||||
* set to false, then the setting will be abandoned if the key path is missing an intermediate
|
||||
* value. For example:
|
||||
*
|
||||
* setObjectValue({a: {z: false}}, 'a.b.c', true); // {a: {z: false, b: {c: true } } }
|
||||
* setObjectValue({a: {z: false}}, 'a.b.c', true, false); // {a: {z: false}}
|
||||
*
|
||||
*/
|
||||
export function setObjectValue(obj: any, objKey: string, value: any, createMissingObjects = true) {
|
||||
if (!obj || !objKey) return obj;
|
||||
|
||||
const keys = objKey.split(".");
|
||||
const key = keys.shift()!;
|
||||
if (obj[key] === undefined) {
|
||||
if (!createMissingObjects) {
|
||||
return;
|
||||
}
|
||||
obj[key] = {};
|
||||
}
|
||||
if (!keys.length) {
|
||||
obj[key] = value;
|
||||
} else {
|
||||
if (typeof obj[key] != "object") {
|
||||
obj[key] = {};
|
||||
}
|
||||
setObjectValue(obj[key], keys.join("."), value, createMissingObjects);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an item in an array (by item or its index) to another index.
|
||||
*/
|
||||
export function moveArrayItem(arr: any[], itemOrFrom: any, to: number) {
|
||||
const from = typeof itemOrFrom === "number" ? itemOrFrom : arr.indexOf(itemOrFrom);
|
||||
arr.splice(to, 0, arr.splice(from, 1)[0]!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an item in an array (by item or its index) to another index.
|
||||
*/
|
||||
export function removeArrayItem<T>(arr: T[], itemOrIndex: T | number) {
|
||||
const index = typeof itemOrIndex === "number" ? itemOrIndex : arr.indexOf(itemOrIndex);
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects CSS into the page with a promise when complete.
|
||||
*/
|
||||
export function injectCss(href: string): Promise<void> {
|
||||
if (document.querySelector(`link[href^="${href}"]`)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const link = document.createElement("link");
|
||||
link.setAttribute("rel", "stylesheet");
|
||||
link.setAttribute("type", "text/css");
|
||||
const timeout = setTimeout(resolve, 1000);
|
||||
link.addEventListener("load", (e) => {
|
||||
clearInterval(timeout);
|
||||
resolve();
|
||||
});
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `Object.defineProperty` with special care around getters and setters to call out to a
|
||||
* parent getter or setter (like a super.set call) to ensure any side effects up the chain
|
||||
* are still invoked.
|
||||
*/
|
||||
export function defineProperty(instance: any, property: string, desc: PropertyDescriptor) {
|
||||
const existingDesc = Object.getOwnPropertyDescriptor(instance, property);
|
||||
if (existingDesc?.configurable === false) {
|
||||
throw new Error(`Error: rgthree-comfy cannot define un-configurable property "${property}"`);
|
||||
}
|
||||
|
||||
if (existingDesc?.get && desc.get) {
|
||||
const descGet = desc.get;
|
||||
desc.get = () => {
|
||||
existingDesc.get!.apply(instance, []);
|
||||
return descGet!.apply(instance, []);
|
||||
};
|
||||
}
|
||||
if (existingDesc?.set && desc.set) {
|
||||
const descSet = desc.set;
|
||||
desc.set = (v: any) => {
|
||||
existingDesc.set!.apply(instance, [v]);
|
||||
return descSet!.apply(instance, [v]);
|
||||
};
|
||||
}
|
||||
|
||||
desc.enumerable = desc.enumerable ?? existingDesc?.enumerable ?? true;
|
||||
desc.configurable = desc.configurable ?? existingDesc?.configurable ?? true;
|
||||
if (!desc.get && !desc.set) {
|
||||
desc.writable = desc.writable ?? existingDesc?.writable ?? true;
|
||||
}
|
||||
return Object.defineProperty(instance, property, desc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if two DataViews are equal.
|
||||
*/
|
||||
export function areDataViewsEqual(a: DataView, b: DataView) {
|
||||
if (a.byteLength !== b.byteLength) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.byteLength; i++) {
|
||||
if (a.getUint8(i) !== b.getUint8(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A cheap check if the source looks like base64.
|
||||
*/
|
||||
function looksLikeBase64(source: string) {
|
||||
return source.length > 500 || source.startsWith("data:") || source.includes(";base64,");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if two ArrayBuffers are equal.
|
||||
*/
|
||||
export function areArrayBuffersEqual(a?: ArrayBuffer | null, b?: ArrayBuffer | null) {
|
||||
if (a == b || !a || !b) {
|
||||
return a == b;
|
||||
}
|
||||
return areDataViewsEqual(new DataView(a), new DataView(b));
|
||||
}
|
||||
|
||||
export function newCanvas(
|
||||
widthOrPtOrImage: number | {width: number; height: number} | HTMLImageElement,
|
||||
height?: number,
|
||||
) {
|
||||
let width: number;
|
||||
if (typeof widthOrPtOrImage !== "number") {
|
||||
width = widthOrPtOrImage.width;
|
||||
height = widthOrPtOrImage.height;
|
||||
} else {
|
||||
width = widthOrPtOrImage;
|
||||
height = height;
|
||||
}
|
||||
if (height == null) {
|
||||
throw new Error("Invalid height supplied when creating new canvas object.");
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
if (widthOrPtOrImage instanceof HTMLImageElement) {
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.drawImage(widthOrPtOrImage, 0, 0, width, height);
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns canvas image data for an HTML Image.
|
||||
*/
|
||||
export function getCanvasImageData(
|
||||
image: HTMLImageElement,
|
||||
): [HTMLCanvasElement, CanvasRenderingContext2D, ImageData] {
|
||||
const canvas = newCanvas(image);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
return [canvas, ctx, imageData];
|
||||
}
|
||||
|
||||
/** Union of types for image conversion. */
|
||||
type ImageConverstionTypes = string | Blob | ArrayBuffer | HTMLImageElement | HTMLCanvasElement;
|
||||
|
||||
/**
|
||||
* Converts an ImageConverstionTypes to a base64 string.
|
||||
*/
|
||||
export async function convertToBase64(
|
||||
source: ImageConverstionTypes | Promise<ImageConverstionTypes>,
|
||||
): Promise<string> {
|
||||
if (source instanceof Promise) {
|
||||
source = await source;
|
||||
}
|
||||
if (typeof source === "string" && looksLikeBase64(source)) {
|
||||
return source;
|
||||
}
|
||||
if (typeof source === "string" || source instanceof Blob || source instanceof ArrayBuffer) {
|
||||
return convertToBase64(await loadImage(source));
|
||||
}
|
||||
if (source instanceof HTMLImageElement) {
|
||||
if (looksLikeBase64(source.src)) {
|
||||
return source.src;
|
||||
}
|
||||
const [canvas, ctx, imageData] = getCanvasImageData(source);
|
||||
return convertToBase64(canvas);
|
||||
}
|
||||
if (source instanceof HTMLCanvasElement) {
|
||||
return source.toDataURL("image/png");
|
||||
}
|
||||
throw Error("Unknown source to convert to base64.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an ImageConverstionTypes to an image array buffer.
|
||||
*/
|
||||
export async function convertToArrayBuffer(
|
||||
source: ImageConverstionTypes | Promise<ImageConverstionTypes>,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (source instanceof Promise) {
|
||||
source = await source;
|
||||
}
|
||||
if (source instanceof ArrayBuffer) {
|
||||
return source;
|
||||
}
|
||||
if (typeof source === "string") {
|
||||
if (looksLikeBase64(source)) {
|
||||
var binaryString = atob(source.replace(/^.*?;base64,/, ""));
|
||||
var bytes = new Uint8Array(binaryString.length);
|
||||
for (var i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
return convertToArrayBuffer(await loadImage(source));
|
||||
}
|
||||
if (source instanceof HTMLImageElement) {
|
||||
const [canvas, ctx, imageData] = getCanvasImageData(source);
|
||||
return convertToArrayBuffer(canvas);
|
||||
}
|
||||
if (source instanceof HTMLCanvasElement) {
|
||||
return convertToArrayBuffer(source.toDataURL());
|
||||
}
|
||||
if (source instanceof Blob) {
|
||||
return source.arrayBuffer();
|
||||
}
|
||||
throw Error("Unknown source to convert to arraybuffer.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an image into an HTMLImageElement.
|
||||
*/
|
||||
export async function loadImage(
|
||||
source: ImageConverstionTypes | Promise<ImageConverstionTypes>,
|
||||
): Promise<HTMLImageElement> {
|
||||
if (source instanceof Promise) {
|
||||
source = await source;
|
||||
}
|
||||
if (source instanceof HTMLImageElement) {
|
||||
return loadImage(source.src);
|
||||
}
|
||||
if (source instanceof Blob) {
|
||||
return loadImage(source.arrayBuffer());
|
||||
}
|
||||
if (source instanceof HTMLCanvasElement) {
|
||||
return loadImage(source.toDataURL());
|
||||
}
|
||||
if (source instanceof ArrayBuffer) {
|
||||
var binary = "";
|
||||
var bytes = new Uint8Array(source);
|
||||
var len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]!);
|
||||
}
|
||||
return loadImage(`data:${getMimeTypeFromArrayBuffer(bytes)};base64,${btoa(binary)}`);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.addEventListener("load", () => {
|
||||
resolve(img);
|
||||
});
|
||||
img.addEventListener("error", () => {
|
||||
reject(img);
|
||||
});
|
||||
img.src = source;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the mime type from an array buffer.
|
||||
*/
|
||||
function getMimeTypeFromArrayBuffer(buffer: Uint8Array) {
|
||||
const len = 4;
|
||||
if (buffer.length >= len) {
|
||||
let signatureArr = new Array(len);
|
||||
for (let i = 0; i < len; i++) signatureArr[i] = buffer[i]!.toString(16);
|
||||
const signature = signatureArr.join("").toUpperCase();
|
||||
switch (signature) {
|
||||
case "89504E47":
|
||||
return "image/png";
|
||||
case "47494638":
|
||||
return "image/gif";
|
||||
case "25504446":
|
||||
return "application/pdf";
|
||||
case "FFD8FFDB":
|
||||
case "FFD8FFE0":
|
||||
return "image/jpeg";
|
||||
case "504B0304":
|
||||
return "application/zip";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type BroadcasterMessage<T extends {}> = {
|
||||
id: string;
|
||||
replyId?: string;
|
||||
action: string;
|
||||
window: Window;
|
||||
port: MessagePort;
|
||||
payload?: T;
|
||||
};
|
||||
|
||||
type BroadcasterMessageOptions = {
|
||||
timeout?: number;
|
||||
listenForReply?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Broadcaster is a wrapper around a BroadcastChannel for communication with other windows.
|
||||
*/
|
||||
export class Broadcaster<OutPayload extends {}, InPayload extends {}> extends EventTarget {
|
||||
private channel: BroadcastChannel;
|
||||
private queue: {[key: string]: Resolver<InPayload[]>} = {};
|
||||
|
||||
constructor(channelName: string) {
|
||||
super();
|
||||
this.queue = {};
|
||||
this.channel = new BroadcastChannel(channelName);
|
||||
this.channel.addEventListener("message", (e) => {
|
||||
this.onMessage(e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique id within the queue.
|
||||
*/
|
||||
private getId() {
|
||||
let id: string;
|
||||
do {
|
||||
id = generateId(6);
|
||||
} while (this.queue[id]);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an action, and waits for a response, with a timeout before cancelling.
|
||||
*/
|
||||
async broadcastAndWait(
|
||||
action: string,
|
||||
payload?: OutPayload,
|
||||
options?: BroadcasterMessageOptions,
|
||||
): Promise<InPayload[]> {
|
||||
const id = this.getId();
|
||||
this.queue[id] = getResolver<InPayload[]>(options?.timeout);
|
||||
this.channel.postMessage({
|
||||
id,
|
||||
action,
|
||||
payload,
|
||||
});
|
||||
let response: InPayload[];
|
||||
try {
|
||||
response = await this.queue[id]!.promise;
|
||||
} catch (e) {
|
||||
console.log("CAUGHT", e);
|
||||
response = [];
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
broadcast(action: string, payload?: OutPayload) {
|
||||
this.channel.postMessage({
|
||||
id: this.getId(),
|
||||
action,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
reply(replyId: string, action: string, payload?: OutPayload) {
|
||||
this.channel.postMessage({
|
||||
id: this.getId(),
|
||||
replyId,
|
||||
action,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
openWindowAndWaitForMessage(rgthreePath: string, windowName?: string) {
|
||||
const id = this.getId();
|
||||
this.queue[id] = getResolver();
|
||||
const win = window.open(`/rgthree/${rgthreePath}#broadcastLoadMsgId=${id}`, windowName);
|
||||
return {window: win, promise: this.queue[id]!.promise};
|
||||
}
|
||||
|
||||
onMessage(e: MessageEvent<BroadcasterMessage<InPayload>>) {
|
||||
const msgId = e.data?.replyId || "";
|
||||
const queueItem = this.queue[msgId];
|
||||
if (queueItem) {
|
||||
if (queueItem.completed) {
|
||||
console.error(`${msgId} already completed..`);
|
||||
}
|
||||
queueItem.deferment = queueItem.deferment || {data: []};
|
||||
queueItem.deferment.data.push(e.data.payload);
|
||||
queueItem.deferment.timeout && clearTimeout(queueItem.deferment.timeout);
|
||||
queueItem.deferment.timeout = setTimeout(() => {
|
||||
queueItem.resolve(queueItem.deferment!.data);
|
||||
}, 250);
|
||||
} else {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("rgthree-broadcast-message", {
|
||||
detail: Object.assign({replyTo: e.data?.id}, e.data),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
addMessageListener(callback: EventListener, options?: any) {
|
||||
return super.addEventListener("rgthree-broadcast-message", callback, options);
|
||||
}
|
||||
}
|
||||
|
||||
const broadcastChannelMap: Map<BroadcastChannel, {[key: string]: Resolver<any>}> = new Map();
|
||||
|
||||
export function broadcastOnChannel<T extends {}>(
|
||||
channel: BroadcastChannel,
|
||||
action: string,
|
||||
payload?: T,
|
||||
) {
|
||||
let queue = broadcastChannelMap.get(channel);
|
||||
if (!queue) {
|
||||
broadcastChannelMap.set(channel, {});
|
||||
queue = broadcastChannelMap.get(channel)!;
|
||||
}
|
||||
let id: string;
|
||||
do {
|
||||
id = generateId(6);
|
||||
} while (queue[id]);
|
||||
queue[id] = getResolver();
|
||||
channel.postMessage({
|
||||
id,
|
||||
action,
|
||||
payload,
|
||||
});
|
||||
return queue[id]!.promise;
|
||||
}
|
||||
429
custom_nodes/rgthree-comfy/src_web/common/utils_dom.ts
Normal file
429
custom_nodes/rgthree-comfy/src_web/common/utils_dom.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* Various dom manipulation utils that have followed me around.
|
||||
*/
|
||||
const DIRECT_ATTRIBUTE_MAP: {[name: string]: string} = {
|
||||
cellpadding: "cellPadding",
|
||||
cellspacing: "cellSpacing",
|
||||
colspan: "colSpan",
|
||||
frameborder: "frameBorder",
|
||||
height: "height",
|
||||
maxlength: "maxLength",
|
||||
nonce: "nonce",
|
||||
role: "role",
|
||||
rowspan: "rowSpan",
|
||||
type: "type",
|
||||
usemap: "useMap",
|
||||
valign: "vAlign",
|
||||
width: "width",
|
||||
};
|
||||
|
||||
const RGX_NUMERIC_STYLE_UNIT = "px";
|
||||
const RGX_NUMERIC_STYLE =
|
||||
/^((max|min)?(width|height)|margin|padding|(margin|padding)?(left|top|bottom|right)|fontsize|borderwidth)$/i;
|
||||
const RGX_DEFAULT_VALUE_PROP = /input|textarea|select/i;
|
||||
|
||||
function localAssertNotFalsy<T>(input?: T | null, errorMsg = `Input is not of type.`): T {
|
||||
if (input == null) {
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
const RGX_STRING_VALID = "[a-z0-9_-]";
|
||||
const RGX_TAG = new RegExp(`^([a-z]${RGX_STRING_VALID}*)(\\.|\\[|\\#|$)`, "i");
|
||||
const RGX_ATTR_ID = new RegExp(`#(${RGX_STRING_VALID}+)`, "gi");
|
||||
const RGX_ATTR_CLASS = new RegExp(`(^|\\S)\\.([a-z0-9_\\-\\.]+)`, "gi");
|
||||
const RGX_STRING_CONTENT_TO_SQUARES = "(.*?)(\\[|\\])";
|
||||
const RGX_ATTRS_MAYBE_OPEN = new RegExp(`\\[${RGX_STRING_CONTENT_TO_SQUARES}`, "gi");
|
||||
const RGX_ATTRS_FOLLOW_OPEN = new RegExp(`^${RGX_STRING_CONTENT_TO_SQUARES}`, "gi");
|
||||
|
||||
type QueryParent = HTMLElement | Document | DocumentFragment;
|
||||
|
||||
export function queryAll<K extends keyof HTMLElementTagNameMap>(
|
||||
selectors: K,
|
||||
parent?: QueryParent,
|
||||
): Array<HTMLElementTagNameMap[K]>;
|
||||
export function queryAll<K extends keyof SVGElementTagNameMap>(
|
||||
selectors: K,
|
||||
parent?: QueryParent,
|
||||
): Array<SVGElementTagNameMap[K]>;
|
||||
export function queryAll<K extends keyof MathMLElementTagNameMap>(
|
||||
selectors: K,
|
||||
parent?: QueryParent,
|
||||
): Array<MathMLElementTagNameMap[K]>;
|
||||
export function queryAll<T extends HTMLElement>(selectors: string, parent?: QueryParent): Array<T>;
|
||||
export function queryAll(selectors: string, parent: QueryParent = document) {
|
||||
return Array.from(parent.querySelectorAll(selectors)).filter((n) => !!n);
|
||||
}
|
||||
|
||||
export function query<K extends keyof HTMLElementTagNameMap>(
|
||||
selectors: K,
|
||||
parent?: QueryParent,
|
||||
): HTMLElementTagNameMap[K] | null;
|
||||
export function query<K extends keyof SVGElementTagNameMap>(
|
||||
selectors: K,
|
||||
parent?: QueryParent,
|
||||
): SVGElementTagNameMap[K] | null;
|
||||
export function query<K extends keyof MathMLElementTagNameMap>(
|
||||
selectors: K,
|
||||
parent?: QueryParent,
|
||||
): MathMLElementTagNameMap[K] | null;
|
||||
export function query<T extends HTMLElement>(selectors: string, parent?: QueryParent): T | null;
|
||||
export function query(selectors: string, parent: QueryParent = document) {
|
||||
return parent.querySelector(selectors) ?? null;
|
||||
}
|
||||
|
||||
export function createText(text: string) {
|
||||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
export function getClosestOrSelf(
|
||||
element: EventTarget | HTMLElement | null,
|
||||
query: string,
|
||||
): HTMLElement | null {
|
||||
const el = element as HTMLElement;
|
||||
return (el?.closest && (((el.matches(query) && el) || el.closest(query)) as HTMLElement)) || null;
|
||||
}
|
||||
|
||||
export function containsOrSelf(
|
||||
parent: EventTarget | HTMLElement | null,
|
||||
contained: EventTarget | HTMLElement | null,
|
||||
): boolean {
|
||||
return (
|
||||
parent === contained || (parent as HTMLElement)?.contains?.(contained as HTMLElement) || false
|
||||
);
|
||||
}
|
||||
|
||||
type Attrs = {
|
||||
[name: string]: any;
|
||||
};
|
||||
|
||||
export function createElement<T extends HTMLElement>(selectorOrMarkup: string, attrs?: Attrs) {
|
||||
const frag = getHtmlFragment(selectorOrMarkup);
|
||||
let element = frag?.firstElementChild as HTMLElement;
|
||||
let selector = "";
|
||||
if (!element) {
|
||||
selector = selectorOrMarkup.replace(/[\r\n]\s*/g, "");
|
||||
const tag = getSelectorTag(selector) || "div";
|
||||
element = document.createElement(tag);
|
||||
selector = selector.replace(RGX_TAG, "$2");
|
||||
const brackets = selector.match(/(\[[^\]]+\])/g) || [];
|
||||
for (const bracket of brackets) {
|
||||
selector = selector.replace(bracket, "");
|
||||
}
|
||||
// Turn id and classname into [attr]s that can be nested
|
||||
selector = selector.replace(RGX_ATTR_ID, '[id="$1"]');
|
||||
selector = selector.replace(
|
||||
RGX_ATTR_CLASS,
|
||||
(match, p1, p2) => `${p1}[class="${p2.replace(/\./g, " ")}"]`,
|
||||
);
|
||||
selector += brackets.join("");
|
||||
}
|
||||
|
||||
const selectorAttrs = getSelectorAttributes(selector);
|
||||
if (selectorAttrs) {
|
||||
for (const attr of selectorAttrs) {
|
||||
let matches = attr.substring(1, attr.length - 1).split("=");
|
||||
let key = localAssertNotFalsy(matches.shift());
|
||||
let value: string = matches.join("=");
|
||||
if (value === undefined) {
|
||||
setAttribute(element, key, true);
|
||||
} else {
|
||||
value = value.replace(/^['"](.*)['"]$/, "$1");
|
||||
setAttribute(element, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attrs) {
|
||||
setAttributes(element, attrs);
|
||||
}
|
||||
return element as T;
|
||||
}
|
||||
export const $el = createElement;
|
||||
|
||||
function getSelectorTag(str: string) {
|
||||
return tryMatch(str, RGX_TAG);
|
||||
}
|
||||
|
||||
function getSelectorAttributes(selector: string) {
|
||||
RGX_ATTRS_MAYBE_OPEN.lastIndex = 0;
|
||||
let attrs: string[] = [];
|
||||
let result;
|
||||
while ((result = RGX_ATTRS_MAYBE_OPEN.exec(selector))) {
|
||||
let attr = result[0];
|
||||
if (attr.endsWith("]")) {
|
||||
attrs.push(attr);
|
||||
} else {
|
||||
attr =
|
||||
result[0] + getOpenAttributesRecursive(selector.substr(RGX_ATTRS_MAYBE_OPEN.lastIndex), 2);
|
||||
RGX_ATTRS_MAYBE_OPEN.lastIndex += attr.length - result[0].length;
|
||||
attrs.push(attr);
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function getOpenAttributesRecursive(selectorSubstring: string, openCount: number) {
|
||||
let matches = selectorSubstring.match(RGX_ATTRS_FOLLOW_OPEN);
|
||||
let result = "";
|
||||
if (matches && matches.length) {
|
||||
result = matches[0];
|
||||
openCount += result.endsWith("]") ? -1 : 1;
|
||||
if (openCount > 0) {
|
||||
result += getOpenAttributesRecursive(selectorSubstring.substr(result.length), openCount);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function tryMatch(str: string, rgx: RegExp, index = 1) {
|
||||
let found = "";
|
||||
try {
|
||||
found = str.match(rgx)?.[index] || "";
|
||||
} catch (e) {
|
||||
found = "";
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export function setAttributes(element: HTMLElement, data: {[name: string]: any}) {
|
||||
let attr;
|
||||
for (attr in data) {
|
||||
if (data.hasOwnProperty(attr)) {
|
||||
setAttribute(element, attr, data[attr]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getHtmlFragment(value: string) {
|
||||
if (value.match(/^\s*<.*?>[\s\S]*<\/[a-z0-9]+>\s*$/)) {
|
||||
return document.createRange().createContextualFragment(value.trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getChild(value: any): HTMLElement | DocumentFragment | Text | null {
|
||||
if (value instanceof Node) {
|
||||
return value as HTMLElement;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
let child = getHtmlFragment(value);
|
||||
if (child) {
|
||||
return child;
|
||||
}
|
||||
if (getSelectorTag(value)) {
|
||||
return createElement(value);
|
||||
}
|
||||
return createText(value);
|
||||
}
|
||||
if (value && typeof value.toElement === "function") {
|
||||
return value.toElement() as HTMLElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function setAttribute(element: HTMLElement, attribute: string, value: any) {
|
||||
let isRemoving = value == null;
|
||||
|
||||
if (attribute === "default") {
|
||||
attribute = RGX_DEFAULT_VALUE_PROP.test(element.nodeName) ? "value" : "text";
|
||||
}
|
||||
|
||||
if (attribute === "text") {
|
||||
empty(element).appendChild(createText(value != null ? String(value) : ""));
|
||||
} else if (attribute === "html") {
|
||||
empty(element).innerHTML += value != null ? String(value) : "";
|
||||
} else if (attribute == "style") {
|
||||
if (typeof value === "string") {
|
||||
element.style.cssText = isRemoving ? "" : value != null ? String(value) : "";
|
||||
} else {
|
||||
for (const [styleKey, styleValue] of Object.entries(value as {[key: string]: any})) {
|
||||
element.style[styleKey as "display"] = styleValue;
|
||||
}
|
||||
}
|
||||
} else if (attribute == "events") {
|
||||
for (const [key, fn] of Object.entries(value as {[key: string]: (e: Event) => void})) {
|
||||
addEvent(element, key, fn);
|
||||
}
|
||||
} else if (attribute === "parent") {
|
||||
value.appendChild(element);
|
||||
} else if (attribute === "child" || attribute === "children") {
|
||||
// Try to handle an array, like [li,li,li]. Not nested brackets, though
|
||||
if (typeof value === "string" && /^\[[^\[\]]+\]$/.test(value)) {
|
||||
const parseable = value.replace(/^\[([^\[\]]+)\]$/, '["$1"]').replace(/,/g, '","');
|
||||
try {
|
||||
const parsed = JSON.parse(parseable);
|
||||
value = parsed;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// "children" is a replace of the children, while "child" appends a new child if others exist.
|
||||
if (attribute === "children") {
|
||||
empty(element);
|
||||
}
|
||||
|
||||
let children = value instanceof Array ? value : [value];
|
||||
for (let child of children) {
|
||||
child = getChild(child);
|
||||
if (child instanceof Node) {
|
||||
if (element instanceof HTMLTemplateElement) {
|
||||
element.content.appendChild(child);
|
||||
} else {
|
||||
element.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (attribute == "for") {
|
||||
(element as HTMLLabelElement).htmlFor = value != null ? String(value) : "";
|
||||
if (isRemoving) {
|
||||
// delete (element as HTMLLabelElement).htmlFor;
|
||||
element.removeAttribute("for");
|
||||
}
|
||||
} else if (attribute === "class" || attribute === "className" || attribute === "classes") {
|
||||
element.className = isRemoving ? "" : Array.isArray(value) ? value.join(" ") : String(value);
|
||||
} else if (attribute === "dataset") {
|
||||
if (typeof value !== "object") {
|
||||
console.error("Expecting an object for dataset");
|
||||
return;
|
||||
}
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
element.dataset[key] = String(val);
|
||||
}
|
||||
} else if (attribute.startsWith("on") && typeof value === "function") {
|
||||
element.addEventListener(attribute.substring(2), value);
|
||||
} else if (["checked", "disabled", "readonly", "required", "selected"].includes(attribute)) {
|
||||
// Could be input, button, etc. We are not discriminate.
|
||||
(element as HTMLInputElement)[attribute as "checked"] = !!value;
|
||||
if (!value) {
|
||||
(element as HTMLInputElement).removeAttribute(attribute);
|
||||
} else {
|
||||
(element as HTMLInputElement).setAttribute(attribute, attribute);
|
||||
}
|
||||
} else if (DIRECT_ATTRIBUTE_MAP.hasOwnProperty(attribute)) {
|
||||
if (isRemoving) {
|
||||
element.removeAttribute(DIRECT_ATTRIBUTE_MAP[attribute]!);
|
||||
} else {
|
||||
element.setAttribute(DIRECT_ATTRIBUTE_MAP[attribute]!, String(value));
|
||||
}
|
||||
} else if (isRemoving) {
|
||||
element.removeAttribute(attribute);
|
||||
} else {
|
||||
let oldVal = element.getAttribute(attribute);
|
||||
if (oldVal !== value) {
|
||||
element.setAttribute(attribute, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addEvent(element: HTMLElement, key: string, fn: (e: Event) => void) {
|
||||
element.addEventListener(key, fn);
|
||||
}
|
||||
|
||||
function setStyles(element: HTMLElement, styles: {[name: string]: string | number} | null = null) {
|
||||
if (styles) {
|
||||
for (let name in styles) {
|
||||
setStyle(element, name, styles[name]!);
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
export function setStyle(element: HTMLElement, name: string, value: string | number | null) {
|
||||
// Note: Old IE uses 'styleFloat'
|
||||
name = name.indexOf("float") > -1 ? "cssFloat" : name;
|
||||
// Camelcase
|
||||
if (name.indexOf("-") != -1) {
|
||||
name = name.replace(/-\D/g, (match) => {
|
||||
return match.charAt(1).toUpperCase();
|
||||
});
|
||||
}
|
||||
if (value == String(Number(value)) && RGX_NUMERIC_STYLE.test(name)) {
|
||||
value = value + RGX_NUMERIC_STYLE_UNIT;
|
||||
}
|
||||
if (name === "display" && typeof value !== "string") {
|
||||
value = !!value ? null : "none";
|
||||
}
|
||||
(element.style as any)[name] = value === null ? null : String(value);
|
||||
return element;
|
||||
}
|
||||
|
||||
export function empty(element: HTMLElement) {
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
export function remove(element: HTMLElement) {
|
||||
while (element.parentElement) {
|
||||
element.parentElement.removeChild(element);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
export function replaceChild(oldChildNode: Node, newNode: Node) {
|
||||
oldChildNode.parentNode!.replaceChild(newNode, oldChildNode);
|
||||
return newNode;
|
||||
}
|
||||
|
||||
type ChildType = HTMLElement | DocumentFragment | Text | string | null;
|
||||
export function appendChildren(el: HTMLElement, children: ChildType | ChildType[]) {
|
||||
children = !Array.isArray(children) ? [children] : children;
|
||||
for (let child of children) {
|
||||
child = getChild(child);
|
||||
if (child instanceof Node) {
|
||||
if (el instanceof HTMLTemplateElement) {
|
||||
el.content.appendChild(child);
|
||||
} else {
|
||||
el.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns elements and their actions.
|
||||
*
|
||||
* data-action="click:action-signal"
|
||||
*/
|
||||
export function getActionEls(parent: Element | Document = document) {
|
||||
const els = Array.from(parent.querySelectorAll("[data-action],[on-action],[on]"));
|
||||
if (parent instanceof Element) {
|
||||
els.unshift(parent);
|
||||
}
|
||||
return els
|
||||
.map((actionEl) => {
|
||||
const actions: {[action: string]: string} = {};
|
||||
const actionSegments = (
|
||||
actionEl.getAttribute("data-action") ||
|
||||
actionEl.getAttribute("on-action") ||
|
||||
actionEl.getAttribute("on") ||
|
||||
""
|
||||
).split(";");
|
||||
for (let segment of actionSegments) {
|
||||
let actionsData = segment
|
||||
.trim()
|
||||
.split(/\s*:\s*/g)
|
||||
.filter((i) => !!i.trim()) as [string, string?];
|
||||
if (!actionsData.length) continue;
|
||||
if (actionsData.length === 1) {
|
||||
if (actionEl instanceof HTMLInputElement) {
|
||||
actionsData.unshift("input");
|
||||
} else {
|
||||
actionsData.unshift("click");
|
||||
}
|
||||
}
|
||||
if (actionsData[0] && actionsData[1]) {
|
||||
actions[actionsData[0]] = actionsData[1];
|
||||
// actionEl.addEventListener(actionsData[0], (e) => {this.handleAction(actionsData[1]!, actionEl, e);});
|
||||
}
|
||||
}
|
||||
return {
|
||||
el: actionEl,
|
||||
actions,
|
||||
};
|
||||
})
|
||||
.filter((el) => !!el);
|
||||
}
|
||||
879
custom_nodes/rgthree-comfy/src_web/common/utils_templates.ts
Normal file
879
custom_nodes/rgthree-comfy/src_web/common/utils_templates.ts
Normal file
@@ -0,0 +1,879 @@
|
||||
import * as dom from "./utils_dom";
|
||||
import {getObjectValue} from "./shared_utils";
|
||||
|
||||
const CONFIG_DEFAULT = {
|
||||
attrBind: "data-bind",
|
||||
attrIf: "data-if",
|
||||
attrIfIs: "data-if-is",
|
||||
};
|
||||
|
||||
const CONFIG = Object.assign({}, CONFIG_DEFAULT, {
|
||||
attrBind: "bind",
|
||||
attrIf: "if",
|
||||
attrIfIs: "if-is",
|
||||
});
|
||||
|
||||
export interface BindOptions {
|
||||
/**
|
||||
* If true then only those data-bind keys in the data map will be bound,
|
||||
* and no `data-bind` fields will be unbound.
|
||||
*/
|
||||
onlyDefined?: boolean;
|
||||
|
||||
/** If true, then binding/init will not be called on nested templates. */
|
||||
singleScoped?: boolean;
|
||||
|
||||
/** Context elemnt. */
|
||||
contextElement?: HTMLElement;
|
||||
}
|
||||
|
||||
export interface InflateOptions {
|
||||
skipInit?: boolean;
|
||||
bindOptions?: BindOptions;
|
||||
}
|
||||
|
||||
export interface TemplateData {
|
||||
fragment: DocumentFragment;
|
||||
preProcessScript: (data: any) => void;
|
||||
}
|
||||
|
||||
export interface BindingContext {
|
||||
data: any;
|
||||
contextElement: HTMLElement;
|
||||
currentElement: HTMLElement;
|
||||
}
|
||||
|
||||
// const RGX_COMPARISON = /^\(?([a-z0-9\.\-\[\]'"]+)((?:<|>|==)=?)([a-z0-9\.\-\[\]'"]+)\)?$/i;
|
||||
const RGX_COMPARISON = (() => {
|
||||
// /^\(?([a-z0-9\.\-\[\]'"]+)((?:<|>|==)=?)([a-z0-9\.\-\[\]'"]+)\)?$/i;
|
||||
let value = "((?:\\!*)[_a-z0-9\\.\\-\\[\\]'\"]+)";
|
||||
let comparison = "((?:<|>|==|\\!=)=?)";
|
||||
return new RegExp(`^(?:\\!*)\\(?${value}\\s*${comparison}\\s*${value}\\)?$`, "i");
|
||||
})();
|
||||
|
||||
const RGXPART_BIND_FN_TEMPLATE_STRING = "template|tpl";
|
||||
const RGXPART_BIND_FN_ELEMENT_STRING = "element|el";
|
||||
const RGX_BIND_FN_TEMPLATE = new RegExp(
|
||||
`^(?:${RGXPART_BIND_FN_TEMPLATE_STRING})\\(([^\\)]+)\\)`,
|
||||
"i",
|
||||
);
|
||||
const RGX_BIND_FN_ELEMENT = new RegExp(
|
||||
`^(?:${RGXPART_BIND_FN_ELEMENT_STRING})\\(([^\\)]+)\\)`,
|
||||
"i",
|
||||
);
|
||||
const RGX_BIND_FN_TEMPLATE_OR_ELEMENT = new RegExp(
|
||||
`^(?:${RGXPART_BIND_FN_TEMPLATE_STRING}|${RGXPART_BIND_FN_ELEMENT_STRING})\\(([^\\)]+)\\)`,
|
||||
"i",
|
||||
);
|
||||
const RGX_BIND_FN_LENGTH = /^(?:length|len|size)\(([^\)]+)\)/i;
|
||||
const RGX_BIND_FN_FORMAT = /^(?:format|fmt)\(([^\,]+),([^\)]+)\)/i;
|
||||
const RGX_BIND_FN_CALL = /^([^\(]+)\(([^\)]*)\)/i;
|
||||
|
||||
const EMPTY_PREPROCESS_FN = (data: any) => data;
|
||||
|
||||
// This is used within exec, so we don't need to check the first part since it's
|
||||
// always the lastIndex start position
|
||||
// const RGX_BIND_DECLARATIONS = /\s*((?:[\$_a-z0-9-\.]|\?\?|\|\|)+(?:\([^\)]+\))?)(?::(.*?))?(\s|$)/ig;
|
||||
const RGX_BIND_DECLARATIONS =
|
||||
/\s*(\!*(?:[\$_a-z0-9-\.\'\"]|\?\?|\|\||\&\&|(?:(?:<|>|==|\!=)=?))+(?:\`[^\`]+\`)?(?:\([^\)]*\))?)(?::(.*?))?(\s|$)/gi;
|
||||
|
||||
/**
|
||||
* Asserts that something is not null of undefined.
|
||||
*/
|
||||
function localAssertNotFalsy<T>(input?: T | null, errorMsg = `Input is not of type.`): T {
|
||||
if (input == null) {
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans a key.
|
||||
*/
|
||||
function cleanKey(key: string) {
|
||||
return key.toLowerCase().trim().replace(/\s/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the value is an array, converting array-like items to an array.
|
||||
*/
|
||||
function toArray(value: any | any[]): any[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
return Array.from(value);
|
||||
}
|
||||
// Array-like.
|
||||
if (typeof value === "object" && typeof value.length === "number") {
|
||||
return [].slice.call(value);
|
||||
}
|
||||
return [value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens an array.
|
||||
*/
|
||||
function flattenArray(arr: any | any[]): any[] {
|
||||
return toArray(arr).reduce((acc, val) => {
|
||||
return acc.concat(Array.isArray(val) ? flattenArray(val) : val);
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an object value by a string lookup.
|
||||
*/
|
||||
function getObjValue(lookup: string, obj: any): any {
|
||||
// If we want to cast as a boolean via a /!+/ prefix.
|
||||
let booleanMatch: string[] = lookup.match(/^(\!+)(.+?)$/i) || [];
|
||||
let booleanNots: string[] = [];
|
||||
if (booleanMatch[1] && booleanMatch[2]) {
|
||||
booleanNots = booleanMatch[1].split("");
|
||||
lookup = booleanMatch[2];
|
||||
}
|
||||
|
||||
let value = getObjectValue(obj, lookup);
|
||||
while (booleanNots.length) {
|
||||
value = !value;
|
||||
booleanNots.shift();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a primotove or object value.
|
||||
*/
|
||||
function getPrimitiveOrObjValue(stringValue: string | null | undefined, data: any) {
|
||||
let value;
|
||||
if (stringValue == null) {
|
||||
return stringValue;
|
||||
}
|
||||
let negate = getNegates(stringValue);
|
||||
if (negate != null) {
|
||||
stringValue = stringValue.replace(/^\!+/, "");
|
||||
}
|
||||
try {
|
||||
const cleanedStringValue = stringValue.replace(/^'(.*)'$/, '"$1"');
|
||||
value = JSON.parse(cleanedStringValue);
|
||||
} catch (e) {
|
||||
value = getObjValue(stringValue, data);
|
||||
}
|
||||
value = negate !== null ? (negate === 1 ? !value : !!value) : value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the negates for a string. A `null` value means there are no negates,
|
||||
* otherwise a `1` means it should be negated, and a `0` means double-negate
|
||||
* (e.g. cast as boolean).
|
||||
*
|
||||
* 'boolVar' => null
|
||||
* '!boolVar' => 1
|
||||
* '!!boolVar' => 0
|
||||
* '!!!boolVar' => 1
|
||||
* '!!!!boolVar' => 0
|
||||
*/
|
||||
function getNegates(stringValue: string): number | null {
|
||||
let negate = null;
|
||||
let negateMatches = stringValue.match(/^(\!+)(.*)/);
|
||||
if (negateMatches && negateMatches.length >= 3) {
|
||||
negate = negateMatches[1]!.length % 2;
|
||||
}
|
||||
return negate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boolean for a comparison of object values. A `null` value would
|
||||
* be if the experssion does not match a comparison, and undefined if there was
|
||||
* but it's not a known comparison (===, ==, <=, >=, !==, !=, <, >).
|
||||
*/
|
||||
function getStringComparisonExpression(
|
||||
bindingPropName: string,
|
||||
data: any,
|
||||
): boolean | null | undefined {
|
||||
let comparisonMatches = bindingPropName.match(RGX_COMPARISON);
|
||||
if (!comparisonMatches?.length) {
|
||||
return null;
|
||||
}
|
||||
let a = getPrimitiveOrObjValue(comparisonMatches[1]!, data);
|
||||
let b = getPrimitiveOrObjValue(comparisonMatches[3]!, data);
|
||||
let c = comparisonMatches[2];
|
||||
let value = (() => {
|
||||
switch (c) {
|
||||
case "===":
|
||||
return a === b;
|
||||
case "==":
|
||||
return a == b;
|
||||
case "<=":
|
||||
return a <= b;
|
||||
case ">=":
|
||||
return a >= b;
|
||||
case "!==":
|
||||
return a !== b;
|
||||
case "!=":
|
||||
return a != b;
|
||||
case "<":
|
||||
return a < b;
|
||||
case ">":
|
||||
return a > b;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a <tpl> element with children with special attribute copies for
|
||||
* single children.
|
||||
*/
|
||||
function replaceTplElementWithChildren(
|
||||
tplEl: HTMLElement,
|
||||
fragOrElOrEls: DocumentFragment | HTMLElement | Array<DocumentFragment | HTMLElement>,
|
||||
) {
|
||||
const els = Array.isArray(fragOrElOrEls) ? fragOrElOrEls : [fragOrElOrEls];
|
||||
tplEl.replaceWith(...els);
|
||||
// dom.replaceChild(tplEl, fragOrElOrEls);
|
||||
const numOfChildren = Array.isArray(fragOrElOrEls)
|
||||
? fragOrElOrEls.length
|
||||
: fragOrElOrEls.childElementCount;
|
||||
if (numOfChildren === 1) {
|
||||
const firstChild = Array.isArray(fragOrElOrEls)
|
||||
? fragOrElOrEls[0]
|
||||
: fragOrElOrEls.firstElementChild;
|
||||
if (firstChild instanceof Element) {
|
||||
if (tplEl.className.length) {
|
||||
firstChild.className += ` ${tplEl.className}`;
|
||||
}
|
||||
let attr = tplEl.getAttribute("data");
|
||||
if (attr) {
|
||||
firstChild.setAttribute("data", attr);
|
||||
}
|
||||
attr = tplEl.getAttribute(CONFIG.attrBind);
|
||||
if (attr) {
|
||||
firstChild.setAttribute(CONFIG.attrBind, attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point to get data from a binding name. Checks for null coelescing and
|
||||
* logical-or operators.
|
||||
*
|
||||
* Handles templates as well:
|
||||
*
|
||||
* my.value`some string ${value} is cool.`
|
||||
*/
|
||||
function getValueForBinding(bindingPropName: string, context: BindingContext) {
|
||||
console.log("getValueForBinding", bindingPropName, context);
|
||||
const data = context.data;
|
||||
let stringTemplate = null;
|
||||
let stringTemplates = /^(.*?)\`([^\`]*)\`$/.exec(bindingPropName.trim());
|
||||
if (stringTemplates?.length === 3) {
|
||||
bindingPropName = stringTemplates[1]!;
|
||||
stringTemplate = stringTemplates[2];
|
||||
}
|
||||
let value = null;
|
||||
|
||||
let hadALogicalOp = false;
|
||||
const opsToValidation = new Map([
|
||||
[/\s*\?\?\s*/, (v: any) => v != null],
|
||||
[/\s*\|\|\s*/, (v: any) => !!v],
|
||||
[/\s*\&\&\s*/, (v: any) => !v],
|
||||
]);
|
||||
for (const [op, fn] of opsToValidation.entries()) {
|
||||
if (bindingPropName.match(op)) {
|
||||
hadALogicalOp = true;
|
||||
const bindingPropNames = bindingPropName.split(op);
|
||||
for (const propName of bindingPropNames) {
|
||||
value = getValueForBindingPropName(propName, context);
|
||||
if (fn(value)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hadALogicalOp) {
|
||||
value = getValueForBindingPropName(bindingPropName, context);
|
||||
}
|
||||
|
||||
return stringTemplate && value != null
|
||||
? stringTemplate.replace(/\$\{value\}/g, String(value))
|
||||
: value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value for a binding prop name.
|
||||
*/
|
||||
function getValueForBindingPropName(bindingPropName: string, context: BindingContext) {
|
||||
const data = context.data;
|
||||
let negate = getNegates(bindingPropName);
|
||||
if (negate != null) {
|
||||
bindingPropName = bindingPropName.replace(/^\!+/, "");
|
||||
}
|
||||
let value;
|
||||
RGX_COMPARISON.lastIndex = 0;
|
||||
if (RGX_COMPARISON.test(bindingPropName)) {
|
||||
value = getStringComparisonExpression(bindingPropName, data);
|
||||
} else if (RGX_BIND_FN_LENGTH.test(bindingPropName)) {
|
||||
bindingPropName = RGX_BIND_FN_LENGTH.exec(bindingPropName)![1]!;
|
||||
value = getPrimitiveOrObjValue(bindingPropName, data);
|
||||
value = (value && value.length) || 0;
|
||||
} else if (RGX_BIND_FN_FORMAT.test(bindingPropName)) {
|
||||
let matches = RGX_BIND_FN_FORMAT.exec(bindingPropName);
|
||||
bindingPropName = matches![1]!;
|
||||
value = getPrimitiveOrObjValue(bindingPropName, data);
|
||||
value = matches![2]!.replace(/^['"]/, "").replace(/['"]$/, "").replace(/\$1/g, value);
|
||||
} else if (RGX_BIND_FN_CALL.test(bindingPropName)) {
|
||||
console.log("-----");
|
||||
console.log(bindingPropName);
|
||||
let matches = RGX_BIND_FN_CALL.exec(bindingPropName);
|
||||
const functionName = matches![1]!;
|
||||
const maybeDataName = matches![2] ?? null;
|
||||
value = getPrimitiveOrObjValue(maybeDataName, data);
|
||||
console.log(functionName, maybeDataName, value);
|
||||
// First, see if the instance has this call
|
||||
if (typeof value?.[functionName] === "function") {
|
||||
value = value[functionName](value, data, context.currentElement, context.contextElement);
|
||||
} else if (typeof data?.[functionName] === "function") {
|
||||
value = data[functionName](value, data, context.currentElement, context.contextElement);
|
||||
} else if (typeof (context.currentElement as any)?.[functionName] === "function") {
|
||||
value = (context.currentElement as any)[functionName](
|
||||
value,
|
||||
data,
|
||||
context.currentElement,
|
||||
context.contextElement,
|
||||
);
|
||||
} else if (typeof (context.contextElement as any)?.[functionName] === "function") {
|
||||
value = (context.contextElement as any)[functionName](
|
||||
value,
|
||||
data,
|
||||
context.currentElement,
|
||||
context.contextElement,
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`No method named ${functionName} on data or element instance. Just calling regular value.`,
|
||||
);
|
||||
value = getPrimitiveOrObjValue(bindingPropName, data);
|
||||
}
|
||||
} else {
|
||||
value = getPrimitiveOrObjValue(bindingPropName, data);
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
value = negate !== null ? (negate === 1 ? !value : !!value) : value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes data-bind attributes, ostensibly "freezing" the current element.
|
||||
*
|
||||
* @param deep Will remove all data-bind attributes when true. default behavior
|
||||
* is only up to the next data-tpl.
|
||||
*/
|
||||
function removeBindingAttributes(
|
||||
elOrEls: DocumentFragment | HTMLElement | HTMLElement[],
|
||||
deep = false,
|
||||
) {
|
||||
flattenArray(elOrEls || []).forEach((el) => {
|
||||
el.removeAttribute(CONFIG.attrBind);
|
||||
const innerBinds = dom.queryAll(`:scope [${CONFIG.attrBind}]`, el);
|
||||
// If we're deep, then pretend there are no data-tpl.
|
||||
const innerTplBinds = deep ? [] : dom.queryAll(`:scope [data-tpl] [${CONFIG.attrBind}]`);
|
||||
|
||||
innerBinds.forEach((el) => {
|
||||
if (deep || !innerTplBinds.includes(el)) {
|
||||
el.removeAttribute(CONFIG.attrBind);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const templateCache: {[key: string]: TemplateData} = {};
|
||||
|
||||
/**
|
||||
* Checks if a template exists.
|
||||
*/
|
||||
export function checkKey(key: string) {
|
||||
return !!templateCache[cleanKey(key)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a template to it's key and a DocumentFragment to store the markup.
|
||||
* Uses `<template>` shadow DOM if possible. Overloaded to accept a register
|
||||
* a script as second param.
|
||||
*/
|
||||
export function register(
|
||||
key: string,
|
||||
htmlOrElement: string | Element | null = null,
|
||||
preProcessScript?: (data: any) => void,
|
||||
) {
|
||||
key = cleanKey(key);
|
||||
if (templateCache[key]) {
|
||||
return templateCache[key];
|
||||
}
|
||||
|
||||
let fragment: DocumentFragment | null = null;
|
||||
if (typeof htmlOrElement === "string") {
|
||||
const frag = document.createDocumentFragment();
|
||||
if (htmlOrElement.includes("<")) {
|
||||
const html = htmlOrElement.trim();
|
||||
const htmlParentTag =
|
||||
(html.startsWith("<tr") && "tbody") ||
|
||||
(/^<t(body|head|foot)/i.test(html) && "table") ||
|
||||
(/^<t(d|h)/i.test(html) && "tr") ||
|
||||
"div";
|
||||
const temp = document.createElement(htmlParentTag);
|
||||
temp.innerHTML = html;
|
||||
for (const child of temp.children) {
|
||||
frag.appendChild(child);
|
||||
}
|
||||
} else {
|
||||
frag.appendChild(dom.createElement(htmlOrElement));
|
||||
}
|
||||
fragment = frag;
|
||||
} else if (htmlOrElement instanceof Element) {
|
||||
const element = htmlOrElement as HTMLElement;
|
||||
const tag = element.nodeName.toLowerCase();
|
||||
if (tag === "template" && (element as HTMLTemplateElement).content) {
|
||||
fragment = (element as HTMLTemplateElement).content;
|
||||
} else {
|
||||
throw Error("Non-template element not handled");
|
||||
}
|
||||
} else if (!htmlOrElement) {
|
||||
let element = dom.query(`template[id="${key}"],template[data-id="${key}"]`);
|
||||
if (element && (element as HTMLTemplateElement).content) {
|
||||
fragment = (element as HTMLTemplateElement).content;
|
||||
} else {
|
||||
throw Error("Non-template element not handled");
|
||||
}
|
||||
}
|
||||
|
||||
if (fragment) {
|
||||
templateCache[key] = {
|
||||
fragment,
|
||||
preProcessScript: preProcessScript || EMPTY_PREPROCESS_FN,
|
||||
};
|
||||
}
|
||||
return templateCache[key] || null;
|
||||
}
|
||||
|
||||
export function getPreProcessScript(keyOrEl: string | Element) {
|
||||
if (typeof keyOrEl === "string") {
|
||||
if (!templateCache[keyOrEl]) {
|
||||
throw Error(`Template key does not exist ${keyOrEl}`);
|
||||
}
|
||||
return templateCache[keyOrEl].preProcessScript;
|
||||
}
|
||||
if (keyOrEl instanceof Element) {
|
||||
const tpl = keyOrEl.getAttribute("data-tpl") || "";
|
||||
return templateCache[tpl]?.preProcessScript || EMPTY_PREPROCESS_FN;
|
||||
}
|
||||
return EMPTY_PREPROCESS_FN;
|
||||
}
|
||||
|
||||
/** Gets a template Node. */
|
||||
export function getTemplateFragment(key: string): DocumentFragment {
|
||||
key = cleanKey(key);
|
||||
if (!checkKey(key)) {
|
||||
register(key);
|
||||
}
|
||||
let templateData = templateCache[key];
|
||||
if (templateData && templateData.fragment) {
|
||||
let imported: DocumentFragment;
|
||||
if (document.importNode) {
|
||||
imported = document.importNode(templateData.fragment, true);
|
||||
} else {
|
||||
imported = templateData.fragment.cloneNode(true) as DocumentFragment;
|
||||
}
|
||||
(imported as any).__templateid__ = key;
|
||||
return imported;
|
||||
} else {
|
||||
throw new Error("Ain't no template called " + key + " (" + typeof templateCache[key] + ")");
|
||||
}
|
||||
}
|
||||
|
||||
// ### templates::inflate
|
||||
//
|
||||
// Inflate a template.
|
||||
//
|
||||
export function inflate(
|
||||
nodeOrKey: string | HTMLElement | DocumentFragment,
|
||||
templateData: any = null,
|
||||
inflateOptions: InflateOptions = {},
|
||||
) {
|
||||
let node = nodeOrKey as HTMLElement | DocumentFragment;
|
||||
if (typeof node === "string") {
|
||||
node = getTemplateFragment(node);
|
||||
}
|
||||
if (node) {
|
||||
// Check for nested templates by way of a [data-template] attribute
|
||||
// Commented out line below, as :scope doesn't seem to work when node is a document fragment..
|
||||
// dom.queryAll([':scope [data-template]', ':scope [data-templateid]',':scope [template]'], node).forEach((child: HTMLElement) => {
|
||||
// const els = dom.queryAll(['[data-template]', '[data-templateid]','[template]'], node);
|
||||
const els = dom.queryAll("[data-template], [data-templateid], [template]", node);
|
||||
for (const child of els) {
|
||||
// If there's a class name specified on the template call, then we want to add it
|
||||
let className = child.className || null;
|
||||
let childTemplateId = localAssertNotFalsy(
|
||||
child.getAttribute("data-template") ||
|
||||
child.getAttribute("data-templateid") ||
|
||||
child.getAttribute("template"),
|
||||
"No child template id provided.",
|
||||
);
|
||||
|
||||
const dataAttribute = child.getAttribute("data") || "";
|
||||
|
||||
const childData = (dataAttribute && getObjValue(dataAttribute, templateData)) || templateData;
|
||||
const tplsInflateOptions = Object.assign({}, inflateOptions);
|
||||
// If we passed in skipInit we'll use it, otherwise set to true assuming
|
||||
// this pass is initializing final markup
|
||||
if (tplsInflateOptions.skipInit != null) {
|
||||
tplsInflateOptions.skipInit = true;
|
||||
}
|
||||
let tpls = localAssertNotFalsy(
|
||||
inflate(childTemplateId, childData, tplsInflateOptions),
|
||||
`No template inflated from ${childTemplateId}.`,
|
||||
);
|
||||
tpls = !Array.isArray(tpls) ? [tpls] : tpls;
|
||||
if (className) {
|
||||
for (const tpl of tpls) {
|
||||
tpl.classList.add(className);
|
||||
}
|
||||
}
|
||||
if (child.nodeName.toUpperCase() === "TPL") {
|
||||
replaceTplElementWithChildren(child, tpls);
|
||||
} else {
|
||||
child.append(...tpls);
|
||||
}
|
||||
// Old.
|
||||
// tpls.reverse().forEach((tplChild) => {
|
||||
// dom.insertAfter(tplChild, child);
|
||||
// if (className) {
|
||||
// tplChild.classList.add(className);
|
||||
// }
|
||||
// });
|
||||
child.remove();
|
||||
}
|
||||
|
||||
let children: HTMLElement[] = [];
|
||||
for (const child of node.children) {
|
||||
let tplAttributes = (child.getAttribute("data-tpl") || "").split(" ");
|
||||
if (!tplAttributes.includes((node as any).__templateid__)) {
|
||||
tplAttributes.push((node as any).__templateid__);
|
||||
}
|
||||
child.setAttribute("data-tpl", tplAttributes.join(" ").trim());
|
||||
children.push(child as HTMLElement);
|
||||
}
|
||||
let childOrChildren = children.length === 1 ? children[0]! : children;
|
||||
if (!inflateOptions.skipInit) {
|
||||
init(childOrChildren, templateData, inflateOptions.bindOptions);
|
||||
}
|
||||
return childOrChildren;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function inflateSingle(
|
||||
nodeOrKey: string | HTMLElement,
|
||||
scriptData: any = null,
|
||||
bindOptions: InflateOptions = {},
|
||||
): HTMLElement {
|
||||
const inflated = localAssertNotFalsy(inflate(nodeOrKey, scriptData, bindOptions));
|
||||
return Array.isArray(inflated) ? inflated[0]! : inflated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as inflate, but removes bindings after inflating.
|
||||
* Useful when an element only needs to be inflated once without a desire to rebind
|
||||
* (or accidentally unbind elements)
|
||||
*/
|
||||
export function inflateOnce(
|
||||
nodeOrKey: string | HTMLElement | DocumentFragment,
|
||||
templateData: any = null,
|
||||
inflateOptions: InflateOptions = {},
|
||||
) {
|
||||
let children = inflate(nodeOrKey, templateData, inflateOptions);
|
||||
children && removeBindingAttributes(children, false);
|
||||
return children;
|
||||
}
|
||||
|
||||
export function inflateSingleOnce(
|
||||
nodeOrKey: string | HTMLElement,
|
||||
scriptData: any = null,
|
||||
bindOptions: InflateOptions = {},
|
||||
): HTMLElement {
|
||||
const inflated = inflate(nodeOrKey, scriptData, bindOptions) || [];
|
||||
removeBindingAttributes(inflated, false);
|
||||
return Array.isArray(inflated) ? inflated[0]! : inflated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a template and bind to it's data.
|
||||
* Different than bind in that it will check for a registered script
|
||||
* and call that (bind simply binds the data to data-bind fields)
|
||||
*/
|
||||
export function init(els: HTMLElement | HTMLElement[], data: any, bindOptions: BindOptions = {}) {
|
||||
(!els ? [] : els instanceof Element ? [els] : els).forEach((el) => {
|
||||
const dataTplAttr = el.getAttribute("data-tpl");
|
||||
if (dataTplAttr) {
|
||||
const tpls = dataTplAttr.split(" ");
|
||||
tpls.forEach((tpl) => {
|
||||
// if (templateCache[tpl].script)
|
||||
// templateCache[tpl].script(el, (data && (data[tpl] || data[tpl.replace('tpl:','')])) || data, options);
|
||||
// else
|
||||
const dataAttribute = el.getAttribute("data") || "";
|
||||
const childData = (dataAttribute && getObjValue(dataAttribute, data)) || data;
|
||||
bind(el, childData, bindOptions);
|
||||
});
|
||||
} else {
|
||||
bind(el, data, bindOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ### templates::bind
|
||||
//
|
||||
// Binds all elements under a template to the passed `data` JSON object. This *does not* call a registered script.
|
||||
// It will stop binding elements once it reaches an element in the DOM with a `data-autobind` attribute set to false
|
||||
// (which **MooVeeStar.View**s do automatically -- so each view is in control of it's binding)
|
||||
//
|
||||
// - If a single empty element is passed, and data is a string value, then it will be used
|
||||
// as the value for that element
|
||||
// - If `options.onlyDefined === true` then no `data-bind` fields will be unbound, only those
|
||||
// `data-bind` keys in the data map will be bound
|
||||
//
|
||||
export function bind(
|
||||
elOrEls: HTMLElement | ShadowRoot | HTMLElement[],
|
||||
data: any = {},
|
||||
bindOptions: BindOptions = {},
|
||||
) {
|
||||
if (elOrEls instanceof HTMLElement) {
|
||||
data = getPreProcessScript(elOrEls)({...data});
|
||||
}
|
||||
|
||||
// If `els` is a single empty element w/ no `[data-bind]` set _and_
|
||||
// `data` is a string, set it to be the value of the el
|
||||
if (typeof data !== "object") {
|
||||
data = {value: data};
|
||||
if (
|
||||
elOrEls instanceof HTMLElement &&
|
||||
elOrEls.children.length === 0 &&
|
||||
!elOrEls.getAttribute(CONFIG.attrBind)
|
||||
) {
|
||||
dom.setAttributes(elOrEls, {[CONFIG.attrBind]: "value"});
|
||||
}
|
||||
}
|
||||
|
||||
// Get all children to be bind that are not inner binds
|
||||
let passedEls = !Array.isArray(elOrEls) ? [elOrEls] : elOrEls;
|
||||
for (const el of passedEls) {
|
||||
// First, get any condition els, evaluate them, then we'll skip them and children from binding
|
||||
// if they are false.
|
||||
const conditionEls = toArray(dom.queryAll(`[${CONFIG.attrIf}]`, el));
|
||||
const contextElement =
|
||||
bindOptions.contextElement ?? (el instanceof ShadowRoot ? (el.host as HTMLElement) : el);
|
||||
|
||||
for (const conditionEl of conditionEls) {
|
||||
getValueForBindingPropName;
|
||||
// const isTrue = getStringComparisonExpression(conditionEl.getAttribute(CONFIG.attrIf), data);
|
||||
let isTrue = getValueForBinding(conditionEl.getAttribute(CONFIG.attrIf), {
|
||||
data,
|
||||
contextElement: contextElement,
|
||||
currentElement: conditionEl,
|
||||
});
|
||||
conditionEl.setAttribute(CONFIG.attrIfIs, String(!!isTrue));
|
||||
}
|
||||
|
||||
let toBindEls = toArray(
|
||||
dom.queryAll(
|
||||
`:not([${CONFIG.attrIfIs}="false"]) [${CONFIG.attrBind}]:not([data-tpl]):not([${CONFIG.attrIfIs}="false"])`,
|
||||
el,
|
||||
),
|
||||
);
|
||||
if (el instanceof HTMLElement && el.getAttribute(CONFIG.attrBind)) {
|
||||
toBindEls.unshift(el);
|
||||
}
|
||||
|
||||
if (toBindEls.length) {
|
||||
// Exclude any els that are in their own data-tpl (which will follow)
|
||||
// let innerBindsElements = dom.queryAll([':scope [data-tpl] [data-bind]', ':scope [data-autobind="false"] [data-bind]'], el);
|
||||
let innerBindsElements = dom.queryAll(
|
||||
`:scope [data-tpl] [${CONFIG.attrBind}], :scope [data-autobind="false"] [${CONFIG.attrBind}]`,
|
||||
el,
|
||||
);
|
||||
toBindEls = toBindEls.filter((maybeBind) => !innerBindsElements.includes(maybeBind));
|
||||
toBindEls.forEach((child) => {
|
||||
// Get the bindings this elements wants
|
||||
// let bindings = child.getAttribute('data-bind').replace(/\s+/,' ').trim().split(' ') || [];
|
||||
RGX_BIND_DECLARATIONS.lastIndex = 0;
|
||||
let bindings = [];
|
||||
let bindingMatch;
|
||||
while (
|
||||
(bindingMatch = RGX_BIND_DECLARATIONS.exec(
|
||||
child.getAttribute(CONFIG.attrBind).replace(/\s+/, " ").trim(),
|
||||
)) !== null
|
||||
) {
|
||||
bindings.push([bindingMatch[1], bindingMatch[2]]);
|
||||
}
|
||||
|
||||
// let bindingStrings: string[] = child.getAttribute(CONFIG.attrBind).split(' ') || [];
|
||||
// bindingStrings.forEach((bindingString) => {
|
||||
bindings.forEach((bindings) => {
|
||||
// let bindingStringsSplit = bindings.split(':');
|
||||
let bindingDataProperty = localAssertNotFalsy(bindings.shift());
|
||||
let bindingFields = ((bindings.length && bindings[0]) || "default")
|
||||
.trim()
|
||||
.replace(/^\[(.*?)\]$/i, "$1")
|
||||
.split(",");
|
||||
|
||||
let value = getValueForBinding(bindingDataProperty, {
|
||||
data,
|
||||
contextElement: contextElement,
|
||||
currentElement: child,
|
||||
});
|
||||
if (value === undefined) {
|
||||
if (bindOptions.onlyDefined === true) {
|
||||
return;
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
bindingFields.forEach((field) => {
|
||||
if (field.startsWith("style.")) {
|
||||
let stringVal = String(value);
|
||||
if (
|
||||
value &&
|
||||
!stringVal.includes("url(") &&
|
||||
stringVal !== "none" &&
|
||||
(field.includes("background-image") || stringVal.startsWith("http"))
|
||||
) {
|
||||
value = `url(${value})`;
|
||||
}
|
||||
dom.setStyle(child, field.replace("style.", ""), value);
|
||||
|
||||
// special element methods.
|
||||
} else if (field.startsWith("el.")) {
|
||||
if (field === "el.remove") {
|
||||
if (value === true) {
|
||||
child.remove();
|
||||
}
|
||||
} else if (field === "el.toggle") {
|
||||
dom.setStyle(child, "display", value === true ? "" : "none");
|
||||
} else if (field.startsWith("el.classList.toggle")) {
|
||||
const cssClass = field.replace(/el.classList.toggle\(['"]?(.*?)['"]?\)/, "$1");
|
||||
child.classList.toggle(cssClass, !!value);
|
||||
}
|
||||
|
||||
// [array]:tpl(<templatename>) will inflate the specified template for each item
|
||||
} else if (RGX_BIND_FN_TEMPLATE_OR_ELEMENT.test(field)) {
|
||||
dom.empty(child);
|
||||
let elementOrTemplateName = RGX_BIND_FN_TEMPLATE_OR_ELEMENT.exec(field)![1]!;
|
||||
if (Array.isArray(value) || value instanceof Set) {
|
||||
const arrayVals = toArray(value);
|
||||
let isElement = RGX_BIND_FN_ELEMENT.test(field);
|
||||
let frag = document.createDocumentFragment();
|
||||
arrayVals.forEach((item, index) => {
|
||||
let itemData: {};
|
||||
if (typeof item === "object") {
|
||||
itemData = Object.assign({$index: index}, item);
|
||||
} else {
|
||||
itemData = {$index: index, value: item};
|
||||
}
|
||||
|
||||
const els = bindToElOrTemplate(elementOrTemplateName, itemData);
|
||||
frag.append(...els);
|
||||
});
|
||||
// If we're a <tpl>
|
||||
if (child.nodeName.toUpperCase() === "TPL") {
|
||||
replaceTplElementWithChildren(child, frag);
|
||||
} else {
|
||||
dom.empty(child).appendChild(frag);
|
||||
}
|
||||
} else if (value) {
|
||||
const els = bindToElOrTemplate(elementOrTemplateName, value);
|
||||
// If we're a <tpl>
|
||||
if (child.nodeName.toUpperCase() === "TPL") {
|
||||
replaceTplElementWithChildren(child, els);
|
||||
} else {
|
||||
child.append(...els);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dom.setAttributes(child, {[field]: value});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Now loop over children w/ "data-tpl" and init them, unless they have an "data-autobind" set to "false"
|
||||
// (as in, they have a separate View Controller rendering their data)
|
||||
if (bindOptions.singleScoped !== true) {
|
||||
let toInitEls = toArray(el.querySelectorAll(":scope *[data-tpl]"));
|
||||
if (toInitEls.length) {
|
||||
// let innerInits = dom.queryAll([':scope *[data-tpl] *[data-tpl]', ':scope [data-autobind="false"] [data-tpl]'], el);
|
||||
let innerInits = dom.queryAll(
|
||||
':scope *[data-tpl] *[data-tpl], :scope [data-autobind="false"] [data-tpl]',
|
||||
el,
|
||||
);
|
||||
toInitEls = toInitEls.filter((maybeInitEl) => {
|
||||
// If the el is inside another [data-tpl] don't init now (it will recursively next time)
|
||||
if (innerInits.includes(maybeInitEl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we passed in a specific map in data for this, then init
|
||||
let tplKey = maybeInitEl.getAttribute("data-tpl");
|
||||
if (data && (data[tplKey] || data[tplKey.replace("tpl:", "")])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only init cascadingly if autobind is not "false"
|
||||
// (as in, a separate controller handles it's own rendering)
|
||||
return maybeInitEl.getAttribute("data-autobind") !== "false";
|
||||
});
|
||||
toInitEls.forEach((toInitEl) => {
|
||||
var tplKey = toInitEl.getAttribute("data-tpl");
|
||||
init(toInitEl, (data && (data[tplKey] || data[tplKey.replace("tpl:", "")])) || data);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bindToElOrTemplate(elementOrTemplateName: string, data: any) {
|
||||
let el: DocumentFragment | HTMLElement | HTMLElement[] | null =
|
||||
getTemplateFragment(elementOrTemplateName);
|
||||
if (!el) {
|
||||
el = dom.createElement(elementOrTemplateName, data);
|
||||
} else {
|
||||
// Inflate each template passing in item and have them init (force false skipInit)
|
||||
el = inflateOnce(el, data, {skipInit: false});
|
||||
}
|
||||
// Then, remove data-tpl b/c we just inflated it (and, presumably, it's data is
|
||||
// already set so we don't want to set it again below).
|
||||
// const els = (Array.isArray(el) ? el : [el]).filter(el => !!el) as HTMLElement[];
|
||||
// els.forEach(el => {
|
||||
// el.removeAttribute('data-tpl');
|
||||
// dom.queryAll('[data-tpl]', el).forEach(c => c.removeAttribute('data-tpl'));
|
||||
// });
|
||||
const els = (Array.isArray(el) ? el : [el]).filter((el) => !!el) as HTMLElement[];
|
||||
els.forEach((el) => {
|
||||
el.removeAttribute("data-tpl");
|
||||
let toBindEls = dom.queryAll("[data-tpl]", el);
|
||||
// let innerBindsElements = dom.queryAll([':scope [data-tpl] [data-bind]', ':scope [data-autobind="false"] [data-bind]'], el);
|
||||
let innerBindsElements = dom.queryAll(
|
||||
`:scope [data-tpl] [${CONFIG.attrBind}], :scope [data-autobind="false"] [${CONFIG.attrBind}]`,
|
||||
el,
|
||||
);
|
||||
toBindEls = toBindEls.filter((maybeBind) => !innerBindsElements.includes(maybeBind));
|
||||
toBindEls.forEach((c) => c.removeAttribute("data-tpl"));
|
||||
});
|
||||
return els;
|
||||
}
|
||||
72
custom_nodes/rgthree-comfy/src_web/common/utils_workflow.ts
Normal file
72
custom_nodes/rgthree-comfy/src_web/common/utils_workflow.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type {ISerialisedGraph} from "@comfyorg/frontend";
|
||||
import type {ComfyApiFormat} from "typings/comfy.js";
|
||||
|
||||
import {getResolver} from "./shared_utils.js";
|
||||
import {getPngMetadata, getWebpMetadata} from "./comfyui_shim.js";
|
||||
|
||||
/**
|
||||
* Parses the workflow JSON and do any necessary cleanup.
|
||||
*/
|
||||
function parseWorkflowJson(stringJson?: string) {
|
||||
stringJson = stringJson || "null";
|
||||
// Starting around August 2024 the serialized JSON started to get messy and contained `NaN` (for
|
||||
// an is_changed property, specifically). NaN is not parseable, so we'll get those on out of there
|
||||
// and cleanup anything else we need.
|
||||
stringJson = stringJson.replace(/:\s*NaN/g, ": null");
|
||||
return JSON.parse(stringJson);
|
||||
}
|
||||
|
||||
export async function tryToGetWorkflowDataFromEvent(
|
||||
e: DragEvent,
|
||||
): Promise<{workflow: ISerialisedGraph | null; prompt: ComfyApiFormat | null}> {
|
||||
let work;
|
||||
for (const file of e.dataTransfer?.files || []) {
|
||||
const data = await tryToGetWorkflowDataFromFile(file);
|
||||
if (data.workflow || data.prompt) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
const validTypes = ["text/uri-list", "text/x-moz-url"];
|
||||
const match = (e.dataTransfer?.types || []).find((t) => validTypes.find((v) => t === v));
|
||||
if (match) {
|
||||
const uri = e.dataTransfer!.getData(match)?.split("\n")?.[0];
|
||||
if (uri) {
|
||||
return tryToGetWorkflowDataFromFile(await (await fetch(uri)).blob());
|
||||
}
|
||||
}
|
||||
return {workflow: null, prompt: null};
|
||||
}
|
||||
|
||||
export async function tryToGetWorkflowDataFromFile(
|
||||
file: File | Blob,
|
||||
): Promise<{workflow: ISerialisedGraph | null; prompt: ComfyApiFormat | null}> {
|
||||
if (file.type === "image/png") {
|
||||
const pngInfo = await getPngMetadata(file);
|
||||
return {
|
||||
workflow: parseWorkflowJson(pngInfo?.workflow),
|
||||
prompt: parseWorkflowJson(pngInfo?.prompt),
|
||||
};
|
||||
}
|
||||
|
||||
if (file.type === "image/webp") {
|
||||
const pngInfo = await getWebpMetadata(file);
|
||||
// Support loading workflows from that webp custom node.
|
||||
const workflow = parseWorkflowJson(pngInfo?.workflow || pngInfo?.Workflow || "null");
|
||||
const prompt = parseWorkflowJson(pngInfo?.prompt || pngInfo?.Prompt || "null");
|
||||
return {workflow, prompt};
|
||||
}
|
||||
|
||||
if (file.type === "application/json" || (file as File).name?.endsWith(".json")) {
|
||||
const resolver = getResolver<{workflow: any; prompt: any}>();
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const json = parseWorkflowJson(reader.result as string);
|
||||
const isApiJson = Object.values(json).every((v: any) => v.class_type);
|
||||
const prompt = isApiJson ? json : null;
|
||||
const workflow = !isApiJson && !json?.templates ? json : null;
|
||||
return {workflow, prompt};
|
||||
};
|
||||
return resolver.promise;
|
||||
}
|
||||
return {workflow: null, prompt: null};
|
||||
}
|
||||
Reference in New Issue
Block a user