diff --git a/nodes.py b/nodes.py index 3f38913..28479a8 100644 --- a/nodes.py +++ b/nodes.py @@ -4626,7 +4626,8 @@ class SplineEditor: def INPUT_TYPES(cls): return { "required": { - "coordinates": ("STRING", {"multiline": True}), + "points_store": ("STRING", {"multiline": False}), + "coordinates": ("STRING", {"multiline": False}), "mask_width": ("INT", {"default": 512, "min": 8, "max": MAX_RESOLUTION, "step": 8}), "mask_height": ("INT", {"default": 512, "min": 8, "max": MAX_RESOLUTION, "step": 18}), "points_to_sample": ("INT", {"default": 4, "min": 2, "max": 1000, "step": 1}), @@ -4638,10 +4639,14 @@ class SplineEditor: 'linear', 'step-before', 'step-after', + 'polar', + 'polar-reverse', ], { "default": 'cardinal' }), + "tension": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + "segmented": ("BOOLEAN", {"default": False}), }, } @@ -4650,8 +4655,8 @@ class SplineEditor: CATEGORY = "KJNodes/experimental" - def splinedata(self, mask_width, mask_height, coordinates, interpolation, points_to_sample): - + def splinedata(self, mask_width, mask_height, coordinates, interpolation, points_to_sample, points_store, tension, segmented): + print(coordinates) coordinates = json.loads(coordinates) print(coordinates) diff --git a/web/js/jsnodes.js b/web/js/jsnodes.js index 2349636..3ab7d4c 100644 --- a/web/js/jsnodes.js +++ b/web/js/jsnodes.js @@ -5,9 +5,6 @@ app.registerExtension({ async beforeRegisterNodeDef(nodeType, nodeData, app) { switch (nodeData.name) { case "ConditioningMultiCombine": - nodeType.prototype.onNodeMoved = function () { - console.log(this.pos[0]) - } nodeType.prototype.onNodeCreated = function () { //this.inputs_offset = nodeData.name.includes("selective")?1:0 this.cond_type = "CONDITIONING" diff --git a/web/js/spline_editor.js b/web/js/spline_editor.js index 8bea430..531aebe 100644 --- a/web/js/spline_editor.js +++ b/web/js/spline_editor.js @@ -1,5 +1,15 @@ import { app } from '../../../scripts/app.js' +//from melmass +export function makeUUID() { + let dt = new Date().getTime() + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = ((dt + Math.random() * 16) % 16) | 0 + dt = Math.floor(dt / 16) + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) + }) + return uuid +} export const loadScript = ( FILE_URL, @@ -48,20 +58,14 @@ export const loadScript = ( styleTag.id = tag styleTag.innerHTML = ` .spline-editor { - background: var(--comfy-menu-bg); + position: absolute; - color: var(--fg-color); + font: 12px monospace; line-height: 1.5em; padding: 10px; - z-index: 5; + z-index: 0; overflow: hidden; - border-radius: 10px; - border-style: solid; - border-width: medium; - border-color: var(--border-color); - height: 544px; - width: 544px; } ` document.head.appendChild(styleTag) @@ -76,238 +80,187 @@ loadScript('/kjweb_async/protovis.min.js').catch((e) => { }) create_documentation_stylesheet() +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) { + 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; + } +} app.registerExtension({ - name: 'KJNodes.curves', + name: 'KJNodes.SplineEditor', async beforeRegisterNodeDef(nodeType, nodeData) { - if (nodeData.name == 'SplineEditor') { - addElement(nodeData, nodeType); - } - }, -}) + if (nodeData?.name == 'SplineEditor') { + chainCallback(nodeType.prototype, "onNodeCreated", function () { + hideWidgetForGood(this, this.widgets.find(w => w.name === "coordinates")) -export const addElement = (nodeData,nodeType) => { - console.log("Creating spline editor") - const iconSize = 24 - const iconMargin = 4 - - let splineEditor = null - let vis = null - - const drawFg = nodeType.prototype.onDrawForeground - nodeType.prototype.onNodeCreated = function () { - console.log("Node created") - this.coordWidget = this.widgets.find(w => w.name === "coordinates"); - this.interpolationWidget = this.widgets.find(w => w.name === "interpolation"); - this.pointsWidget = this.widgets.find(w => w.name === "points_to_sample"); - } - nodeType.prototype.onRemoved = function () { - console.log("Node removed") - if (splineEditor !== null) { - splineEditor.parentNode.removeChild(splineEditor) - splineEditor = null - } - } - nodeType.prototype.onDrawForeground = function (ctx) { - console.log("Drawing foreground") - const r = drawFg ? drawFg.apply(this, arguments) : undefined - if (this.flags.collapsed) return r - - const x = this.size[0] - iconSize - iconMargin + var element = document.createElement("div"); + this.uuid = makeUUID() + element.id = `spline-editor-${this.uuid}` - if (this.show_doc && splineEditor === null) { - console.log("Drawing spline editor") - splineEditor = document.createElement('div'); - splineEditor.classList.add('spline-editor'); - - // close button - const closeButton = document.createElement('div'); - closeButton.textContent = '❌'; - closeButton.style.position = 'absolute'; - closeButton.style.top = '0'; - closeButton.style.right = '0'; - closeButton.style.cursor = 'pointer'; - closeButton.style.padding = '5px'; - closeButton.style.color = 'red'; - closeButton.style.fontSize = '12px'; - closeButton.addEventListener('mousedown', (e) => { - e.stopPropagation(); - this.show_doc = !this.show_doc - splineEditor.parentNode.removeChild(splineEditor) - splineEditor = null - }); - - splineEditor.appendChild(closeButton) - - var w = 512 - var h = 512 - var i = 3 - - if (points == null) { - var points = pv.range(1, 5).map(i => ({ - x: i * w / 5, - y: 50 + Math.random() * (h - 100) - })); - } - - var segmented = false - vis = new pv.Panel() - .width(w) - .height(h) - .fillStyle("var(--comfy-menu-bg)") - .strokeStyle("orange") - .lineWidth(0) - .antialias(false) - .margin(10) - .event("mousedown", function() { - if (pv.event.shiftKey) { // Use pv.event to access the event object - i = points.push(this.mouse()) - 1; - return this; - } - }); - vis.add(pv.Rule) - .data(pv.range(0, 8, .5)) - .bottom(d => d * 70 + 9.5) - .strokeStyle("gray") - .lineWidth(1) - - vis.add(pv.Line) - .data(() => points) - .left(d => d.x) - .top(d => d.y) - .interpolate(() => this.interpolationWidget.value) - .segmented(() => segmented) - .strokeStyle(pv.Colors.category10().by(pv.index)) - .tension(0.5) - .lineWidth(3) - - vis.add(pv.Dot) - .data(() => points) - .left(d => d.x) - .top(d => d.y) - .radius(7) - .cursor("move") - .strokeStyle(function() { return i == this.index ? "#ff7f0e" : "#1f77b4"; }) - .fillStyle(function() { return "rgba(100, 100, 100, 0.2)"; }) - .event("mousedown", pv.Behavior.drag()) - .event("dragstart", function() { - i = this.index; - return this; - }) - .event("drag", vis) - .anchor("top").add(pv.Label) - .font(d => Math.sqrt(d[2]) * 32 + "px sans-serif") - //.text(d => `(${Math.round(d.x)}, ${Math.round(d.y)})`) - .text(d => { - // Normalize y to range 0.0 to 1.0, considering the inverted y-axis - var normalizedY = 1.0 - (d.y / h); - return `${normalizedY.toFixed(2)}`; - }) - .textStyle("orange") - - //disable context menu on right click - document.addEventListener('contextmenu', function(e) { - if (e.button === 2) { // Right mouse button - e.preventDefault(); - e.stopPropagation(); - } - }) - //right click remove dot - pv.listen(window, "mousedown", () => { - window.focus(); - if (pv.event.button === 2) { - points.splice(i--, 1); - vis.render(); + this.splineEditor = this.addDOMWidget(nodeData.name, "SplineEditorWidget", element, { + serialize: false, + hideOnZoom: false, + }); + this.addWidget("button", "New spline", null, () => { + + if (!this.properties || !("points" in this.properties)) { + createSplineEditor(this) + this.addProperty("points", this.constructor.type, "string"); + } + else { + createSplineEditor(this, true) } }); - //send coordinates to node on mouseup - pv.listen(window, "mouseup", () => { - if (pathElements !== null) { - let coords = samplePoints(pathElements[0], this.pointsWidget.value); - let coordsString = JSON.stringify(coords); - if (this.coordWidget) { - this.coordWidget.value = coordsString; + this.setSize([550, 800]) + this.splineEditor.parentEl = document.createElement("div"); + this.splineEditor.parentEl.className = "spline-editor"; + this.splineEditor.parentEl.id = `spline-editor-${this.uuid}` + element.appendChild(this.splineEditor.parentEl); + + //disable context menu on right click + document.addEventListener('contextmenu', function(e) { + if (e.button === 2) { // Right mouse button + e.preventDefault(); + e.stopPropagation(); } - } - }); - - vis.render(); - var svgElement = vis.canvas(); - splineEditor.appendChild(svgElement); - // this.addDOMWidget("videopreview", "preview", splineEditor, { - // serialize: false, - // hideOnZoom: false, - - // }); - document.body.appendChild(splineEditor) - var pathElements = svgElement.getElementsByTagName('path'); // Get all path elements - } - // close the popup - else if (!this.show_doc && splineEditor !== null) { - splineEditor.parentNode.removeChild(splineEditor) - splineEditor = null - } - - if (this.show_doc && splineEditor !== null && vis !== null) { - const rect = ctx.canvas.getBoundingClientRect() - const scaleX = rect.width / ctx.canvas.width - const scaleY = rect.height / ctx.canvas.height - - const transform = new DOMMatrix() - .scaleSelf(scaleX, scaleY) - .translateSelf(this.size[0] * scaleX, 0) - .multiplySelf(ctx.getTransform()) - .translateSelf(10, -32) - - const scale = new DOMMatrix() - .scaleSelf(transform.a, transform.d); + }) + chainCallback(this, "onGraphConfigured", function() { + createSplineEditor(this) + }); + }); // onAfterGraphConfigured + }//node created + } //before register +})//register - const styleObject = { - transformOrigin: '0 0', - transform: scale, - left: `${transform.a + transform.e}px`, - top: `${transform.d + transform.f}px`, - }; - Object.assign(splineEditor.style, styleObject); - } - ctx.save() - ctx.translate(x - 2, iconSize - 45) - ctx.scale(iconSize / 32, iconSize / 32) - ctx.strokeStyle = 'rgba(255,255,255,0.3)' - ctx.lineCap = 'round' - ctx.lineJoin = 'round' - ctx.lineWidth = 2.4 - ctx.font = 'bold 36px monospace' - ctx.fillStyle = 'orange'; - ctx.fillText('📈', 0, 24) - ctx.restore() - return r + +function createSplineEditor(context, reset=false) { + console.log("creatingSplineEditor") + + if (reset && context.splineEditor.element) { + context.splineEditor.element.innerHTML = ''; // Clear the container + } + const coordWidget = context.widgets.find(w => w.name === "coordinates"); + const interpolationWidget = context.widgets.find(w => w.name === "interpolation"); + const pointsWidget = context.widgets.find(w => w.name === "points_to_sample"); + const pointsStoreWidget = context.widgets.find(w => w.name === "points_store"); + const tensionWidget = context.widgets.find(w => w.name === "tension"); + const segmentedWidget = context.widgets.find(w => w.name === "segmented"); + + // Initialize or reset points array + var w = 512 + var h = 512 + var i = 3 + let points = []; + if (!reset && pointsStoreWidget.value != "") { + points = JSON.parse(pointsStoreWidget.value); + } else { + points = pv.range(1, 4).map((i, index) => { + if (index === 0) { + // First point at the bottom-left corner + return { x: 0, y: h }; + } else if (index === 2) { + // Last point at the top-right corner + return { x: w, y: 0 }; + } else { + // Other points remain as they were + return { + x: i * w / 5, + y: 50 + Math.random() * (h - 100) + }; } - // handle clicking of the icon - const mouseDown = nodeType.prototype.onMouseDown - nodeType.prototype.onMouseDown = function (e, localPos, canvas) { - const r = mouseDown ? mouseDown.apply(this, arguments) : undefined - const iconX = this.size[0] - iconSize - iconMargin - const iconY = iconSize - 45 - if ( - localPos[0] > iconX && - localPos[0] < iconX + iconSize && - localPos[1] > iconY && - localPos[1] < iconY + iconSize - ) { - if (this.show_doc === undefined) { - this.show_doc = true - } else { - this.show_doc = !this.show_doc + }); + pointsStoreWidget.value = JSON.stringify(points); + } + + var vis = new pv.Panel() + .width(w) + .height(h) + .fillStyle("var(--comfy-menu-bg)") + .strokeStyle("gray") + .lineWidth(2) + .antialias(false) + .margin(10) + .event("mousedown", function() { + if (pv.event.shiftKey) { // Use pv.event to access the event object + i = points.push(this.mouse()) - 1; + return this; + } + }) + .event("mouseup", function() { + if (this.pathElements !== null) { + let coords = samplePoints(pathElements[0], pointsWidget.value); + let coordsString = JSON.stringify(coords); + pointsStoreWidget.value = JSON.stringify(points); + if (coordWidget) { + coordWidget.value = coordsString; } - return true; - } - return r; } + }); + + vis.add(pv.Rule) + .data(pv.range(0, 8, .5)) + .bottom(d => d * 64 + 0) + .strokeStyle("gray") + .lineWidth(1) + + vis.add(pv.Line) + .data(() => points) + .left(d => d.x) + .top(d => d.y) + .interpolate(() => interpolationWidget.value) + .tension(() => tensionWidget.value) + .segmented(() => segmentedWidget.value) + .strokeStyle(pv.Colors.category10().by(pv.index)) + .lineWidth(3) + + vis.add(pv.Dot) + .data(() => points) + .left(d => d.x) + .top(d => d.y) + .radius(8) + .cursor("move") + .strokeStyle(function() { return i == this.index ? "#ff7f0e" : "#1f77b4"; }) + .fillStyle(function() { return "rgba(100, 100, 100, 0.2)"; }) + .event("mousedown", pv.Behavior.drag()) + .event("dragstart", function() { + i = this.index; + if (pv.event.button === 2) { + points.splice(i--, 1); + vis.render(); + } + return this; + }) + .event("drag", vis) + .anchor("top").add(pv.Label) + .font(d => Math.sqrt(d[2]) * 32 + "px sans-serif") + //.text(d => `(${Math.round(d.x)}, ${Math.round(d.y)})`) + .text(d => { + // Normalize y to range 0.0 to 1.0, considering the inverted y-axis + var normalizedY = 1.0 - (d.y / h); + return `${normalizedY.toFixed(2)}`; + }) + .textStyle("orange") + + vis.render(); + var svgElement = vis.canvas(); + svgElement.style['zIndex'] = "2" + svgElement.style['position'] = "relative" + context.splineEditor.element.appendChild(svgElement); + var pathElements = svgElement.getElementsByTagName('path'); // Get all path elements + } - - - function samplePoints(svgPathElement, numSamples) { var pathLength = svgPathElement.getTotalLength(); var points = []; @@ -324,4 +277,28 @@ function samplePoints(svgPathElement, numSamples) { } //console.log(points); return points; +} + +//from melmass +export function hideWidgetForGood(node, widget, suffix = '') { + widget.origType = widget.type + widget.origComputeSize = widget.computeSize + widget.origSerializeValue = widget.serializeValue + widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically + widget.type = "converted-widget" + suffix + // widget.serializeValue = () => { + // // Prevent serializing the widget if we have no input linked + // const w = node.inputs?.find((i) => i.widget?.name === widget.name); + // if (w?.link == null) { + // return undefined; + // } + // return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; + // }; + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + hideWidgetForGood(node, w, ':' + widget.name) + } + } } \ No newline at end of file