Port documentation functionality to ACN
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
from .adv_control.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .adv_control import documentation
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', "WEB_DIRECTORY"]
|
||||
documentation.format_descriptions(NODE_CLASS_MAPPINGS)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
from .logger import logger
|
||||
|
||||
def image(src):
|
||||
return f'<img src={src} style="width: 0px; min-width: 100%">'
|
||||
def video(src):
|
||||
return f'<video src={src} autoplay muted loop controls controlslist="nodownload noremoteplayback noplaybackrate" style="width: 0px; min-width: 100%" class="VHS_loopedvideo">'
|
||||
def short_desc(desc):
|
||||
return f'<div id=VHS_shortdesc style="font-size: .8em">{desc}</div>'
|
||||
|
||||
descriptions = {
|
||||
}
|
||||
|
||||
sizes = ['1.4','1.2','1']
|
||||
def as_html(entry, depth=0):
|
||||
if isinstance(entry, dict):
|
||||
size = 0.8 if depth < 2 else 1
|
||||
html = ''
|
||||
for k in entry:
|
||||
if k == "collapsed":
|
||||
continue
|
||||
collapse_single = k.endswith("_collapsed")
|
||||
if collapse_single:
|
||||
name = k[:-len("_collapsed")]
|
||||
else:
|
||||
name = k
|
||||
collapse_flag = ' VHS_precollapse' if entry.get("collapsed", False) or collapse_single else ''
|
||||
html += f'<div vhs_title=\"{name}\" style=\"display: flex; font-size: {size}em\" class=\"VHS_collapse{collapse_flag}\"><div style=\"color: #AAA; height: 1.5em;\">[<span style=\"font-family: monospace\">-</span>]</div><div style=\"width: 100%\">{name}: {as_html(entry[k], depth=depth+1)}</div></div>'
|
||||
return html
|
||||
if isinstance(entry, list):
|
||||
html = ''
|
||||
for i in entry:
|
||||
html += f'<div>{as_html(i, depth=depth)}</div>'
|
||||
return html
|
||||
return str(entry)
|
||||
|
||||
def format_descriptions(nodes):
|
||||
for k in descriptions:
|
||||
if k.endswith("_collapsed"):
|
||||
k = k[:-len("_collapsed")]
|
||||
nodes[k].DESCRIPTION = as_html(descriptions[k])
|
||||
undocumented_nodes = []
|
||||
for k in nodes:
|
||||
if not hasattr(nodes[k], "DESCRIPTION"):
|
||||
undocumented_nodes.append(k)
|
||||
if len(undocumented_nodes) > 0:
|
||||
logger.info(f"Undocumented nodes: {undocumented_nodes}")
|
||||
|
||||
+8
-8
@@ -1,13 +1,6 @@
|
||||
import { app } from '../../../scripts/app.js'
|
||||
app.registerExtension({
|
||||
name: "AdvancedControlNet.autosize",
|
||||
async nodeCreated(node) {
|
||||
if(node.acnAutosize) {
|
||||
let size = node.computeSize(0);
|
||||
size[0] += node.acnAutosize?.padding || 0;
|
||||
node.setSize(size);
|
||||
}
|
||||
},
|
||||
async getCustomWidgets() {
|
||||
return {
|
||||
ACNAUTOSIZE(node, inputName, inputData) {
|
||||
@@ -20,11 +13,18 @@ app.registerExtension({
|
||||
return [0, -4];
|
||||
}
|
||||
}
|
||||
node.acnAutosize = inputData[1];
|
||||
if (!node.widgets) {
|
||||
node.widgets = []
|
||||
}
|
||||
node.widgets.push(w)
|
||||
let origOnCreated = node.onNodeCreated
|
||||
node.onNodeCreated = function() {
|
||||
let r = origOnCreated?.apply(this, arguments)
|
||||
let size = this.computeSize();
|
||||
size[0] += inputData[1].padding || 0;
|
||||
this.setSize(size);
|
||||
return r
|
||||
}
|
||||
return w;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
import { app } from '../../../scripts/app.js'
|
||||
|
||||
function chainCallback(object, property, callback) {
|
||||
if (object == undefined) {
|
||||
//This should not happen.
|
||||
console.error("Tried to add callback to non-existant object")
|
||||
return;
|
||||
}
|
||||
if (property in object && object[property]) {
|
||||
const callback_orig = object[property]
|
||||
object[property] = function () {
|
||||
const r = callback_orig.apply(this, arguments);
|
||||
callback.apply(this, arguments);
|
||||
return r
|
||||
};
|
||||
} else {
|
||||
object[property] = callback;
|
||||
}
|
||||
}
|
||||
var helpDOM;
|
||||
function initHelpDOM() {
|
||||
let parentDOM = document.createElement("div");
|
||||
document.body.appendChild(parentDOM)
|
||||
parentDOM.appendChild(helpDOM)
|
||||
helpDOM.className = "litegraph";
|
||||
let scrollbarStyle = document.createElement('style');
|
||||
scrollbarStyle.innerHTML = `
|
||||
<style id="scroll-properties">
|
||||
* {
|
||||
scrollbar-width: 6px;
|
||||
scrollbar-color: #0003 #0000;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #0005;
|
||||
border-radius: 20px
|
||||
}
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
.VHS_loopedvideo::-webkit-media-controls-mute-button {
|
||||
display:none;
|
||||
}
|
||||
.VHS_loopedvideo::-webkit-media-controls-fullscreen-button {
|
||||
display:none;
|
||||
}
|
||||
</style>
|
||||
`
|
||||
parentDOM.appendChild(scrollbarStyle)
|
||||
chainCallback(app.canvas, "onDrawForeground", function (ctx, visible_rect){
|
||||
let n = helpDOM.node
|
||||
if (!n || !n?.graph) {
|
||||
parentDOM.style['left'] = '-5000px'
|
||||
return
|
||||
}
|
||||
//draw : function(ctx, node, widgetWidth, widgetY, height) {
|
||||
//update widget position, even if off screen
|
||||
const transform = ctx.getTransform();
|
||||
const scale = app.canvas.ds.scale;//gets the litegraph zoom
|
||||
//calculate coordinates with account for browser zoom
|
||||
const bcr = app.canvas.canvas.getBoundingClientRect()
|
||||
const x = transform.e*scale/transform.a + bcr.x;
|
||||
const y = transform.f*scale/transform.a + bcr.y;
|
||||
//TODO: text reflows at low zoom. investigate alternatives
|
||||
Object.assign(parentDOM.style, {
|
||||
left: (x+(n.pos[0] + n.size[0]+15)*scale) + "px",
|
||||
top: (y+(n.pos[1]-LiteGraph.NODE_TITLE_HEIGHT)*scale) + "px",
|
||||
width: "400px",
|
||||
minHeight: "100px",
|
||||
maxHeight: "600px",
|
||||
overflowY: 'scroll',
|
||||
transformOrigin: '0 0',
|
||||
transform: 'scale(' + scale + ',' + scale +')',
|
||||
fontSize: '18px',
|
||||
backgroundColor: LiteGraph.NODE_DEFAULT_BGCOLOR,
|
||||
boxShadow: '0 0 10px black',
|
||||
borderRadius: '4px',
|
||||
padding: '3px',
|
||||
zIndex: 3,
|
||||
position: "absolute",
|
||||
display: 'inline',
|
||||
});
|
||||
});
|
||||
function setCollapse(el, doCollapse) {
|
||||
if (doCollapse) {
|
||||
el.children[0].children[0].innerHTML = '+'
|
||||
Object.assign(el.children[1].style, {
|
||||
color: '#CCC',
|
||||
overflowX: 'hidden',
|
||||
width: '0px',
|
||||
minWidth: 'calc(100% - 20px)',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
})
|
||||
for (let child of el.children[1].children) {
|
||||
if (child.style.display != 'none'){
|
||||
child.origDisplay = child.style.display
|
||||
}
|
||||
child.style.display = 'none'
|
||||
}
|
||||
} else {
|
||||
el.children[0].children[0].innerHTML = '-'
|
||||
Object.assign(el.children[1].style, {
|
||||
color: '',
|
||||
overflowX: '',
|
||||
width: '100%',
|
||||
minWidth: '',
|
||||
textOverflow: '',
|
||||
whiteSpace: '',
|
||||
})
|
||||
for (let child of el.children[1].children) {
|
||||
child.style.display = child.origDisplay
|
||||
}
|
||||
}
|
||||
}
|
||||
helpDOM.collapseOnClick = function() {
|
||||
let doCollapse = this.children[0].innerHTML == '-'
|
||||
setCollapse(this.parentElement, doCollapse)
|
||||
}
|
||||
helpDOM.selectHelp = function(name, value) {
|
||||
//attempt to navigate to name in help
|
||||
function collapseUnlessMatch(items,t) {
|
||||
var match = items.querySelector('[vhs_title="' + t + '"]')
|
||||
if (!match) {
|
||||
for (let i of items.children) {
|
||||
if (i.innerHTML.slice(0,t.length+5).includes(t)) {
|
||||
match = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
//For longer documentation items with fewer collapsable elements,
|
||||
//scroll to make sure the entirety of the selected item is visible
|
||||
//This has the unfortunate side effect of trying to scroll the main
|
||||
//window if the documentation windows is forcibly offscreen,
|
||||
//but it's easy to simply scroll the main window back and seems to
|
||||
//have no visual side effects
|
||||
match.scrollIntoView(false)
|
||||
window.scrollTo(0,0)
|
||||
for (let i of items.querySelectorAll('.VHS_collapse')) {
|
||||
if (i.contains(match)) {
|
||||
setCollapse(i, false)
|
||||
} else {
|
||||
setCollapse(i, true)
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
let target = collapseUnlessMatch(helpDOM, name)
|
||||
if (target && value) {
|
||||
collapseUnlessMatch(target, value)
|
||||
}
|
||||
}
|
||||
|
||||
helpDOM.addHelp = function(node, nodeType, description) {
|
||||
if (!description) {
|
||||
return
|
||||
}
|
||||
//Pad computed size for the clickable question mark
|
||||
let originalComputeSize = node.computeSize
|
||||
node.computeSize = function() {
|
||||
let size = originalComputeSize.apply(this, arguments)
|
||||
if (!this.title) {
|
||||
return size
|
||||
}
|
||||
let title_width = this.title.length * 0.6 * LiteGraph.NODE_TEXT_SIZE
|
||||
size[0] = Math.max(size[0], title_width + LiteGraph.NODE_TITLE_HEIGHT)
|
||||
return size
|
||||
}
|
||||
|
||||
node.description = description
|
||||
chainCallback(node, "onDrawForeground", function (ctx) {
|
||||
//draw question mark
|
||||
ctx.save()
|
||||
ctx.font = 'bold 20px Arial'
|
||||
ctx.fillText("?", this.size[0]-17, -8)
|
||||
ctx.restore()
|
||||
})
|
||||
chainCallback(node, "onMouseDown", function (e, pos, canvas) {
|
||||
//On click would be preferred, but this'll be good enough
|
||||
if (pos[1] < 0 && pos[0] + LiteGraph.NODE_TITLE_HEIGHT > this.size[0]) {
|
||||
//corner question mark clicked
|
||||
if (helpDOM.node == this) {
|
||||
helpDOM.node = undefined
|
||||
} else {
|
||||
helpDOM.node = this;
|
||||
helpDOM.innerHTML = this.description || "no help provided ".repeat(20)
|
||||
for (let e of helpDOM.querySelectorAll('.VHS_collapse')) {
|
||||
e.children[0].onclick = helpDOM.collapseOnClick
|
||||
e.children[0].style.cursor = 'pointer'
|
||||
}
|
||||
for (let e of helpDOM.querySelectorAll('.VHS_precollapse')) {
|
||||
setCollapse(e, true)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
let timeout = null
|
||||
chainCallback(node, "onMouseMove", function (e, pos, canvas) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
if (helpDOM.node != this) {
|
||||
return
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
let n = this
|
||||
if (pos[0] > 0 && pos[0] < n.size[0]
|
||||
&& pos[1] > 0 && pos[1] < n.size[1]) {
|
||||
//TODO: provide help specific to element clicked
|
||||
let inputRows = Math.max(n.inputs.length, n.outputs.length)
|
||||
if (pos[1] < LiteGraph.NODE_SLOT_HEIGHT * inputRows) {
|
||||
let row = Math.floor((pos[1] - 7) / LiteGraph.NODE_SLOT_HEIGHT)
|
||||
if (pos[0] < n.size[0]/2) {
|
||||
if (row < n.inputs.length) {
|
||||
helpDOM.selectHelp(n.inputs[row].name)
|
||||
}
|
||||
} else {
|
||||
if (row < n.outputs.length) {
|
||||
helpDOM.selectHelp(n.outputs[row].name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//probably widget, but widgets have variable height.
|
||||
let basey = LiteGraph.NODE_SLOT_HEIGHT * inputRows + 6
|
||||
for (let w of n.widgets) {
|
||||
if (w.y) {
|
||||
basey = w.y
|
||||
}
|
||||
let wheight = LiteGraph.NODE_WIDGET_HEIGHT+4
|
||||
if (w.computeSize) {
|
||||
wheight = w.computeSize(n.size[0])[1]
|
||||
}
|
||||
if (pos[1] < basey + wheight) {
|
||||
helpDOM.selectHelp(w.name, w.value)
|
||||
break
|
||||
}
|
||||
basey += wheight
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
chainCallback(node, "onMouseLeave", function (e, pos, canvas) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
name: "AdvancedControlNet.documentation",
|
||||
async init() {
|
||||
if (app.VHSHelp) {
|
||||
helpDOM = app.VHSHelp
|
||||
} else {
|
||||
helpDOM = document.createElement("div");
|
||||
initHelpDOM()
|
||||
app.VHSHelp = helpDOM
|
||||
}
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// NOTE: May need manual adjusting for the few non-namespaced nodes
|
||||
if(nodeData?.name?.startsWith("ACN_") && nodeData.description) {
|
||||
let description = nodeData.description
|
||||
let el = document.createElement("div")
|
||||
el.innerHTML = description
|
||||
if (!el.children.length) {
|
||||
//Is plaintext. Do minor convenience formatting
|
||||
let chunks = description.split('\n')
|
||||
nodeData.description = chunks[0]
|
||||
description = chunks.join('<br>')
|
||||
} else {
|
||||
nodeData.description = el.querySelector('#VHS_shortdesc')?.innerHTML || el.children[1]?.firstChild?.innerHTML
|
||||
}
|
||||
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
||||
helpDOM.addHelp(this, nodeType, description)
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user