-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge PR #154 from AustinMroz/main - documentation functionality
Port documentation functionality to ACN
- Loading branch information
Showing
4 changed files
with
350 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}") | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
} | ||
}, | ||
}); |