diff --git a/nodes/curve_nodes.py b/nodes/curve_nodes.py index a723f06..f673081 100644 --- a/nodes/curve_nodes.py +++ b/nodes/curve_nodes.py @@ -15,6 +15,14 @@ class SplineEditor: "mask_width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), "mask_height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), "points_to_sample": ("INT", {"default": 16, "min": 2, "max": 1000, "step": 1}), + "sampling_method": ( + [ + 'path', + 'time', + ], + { + "default": 'time' + }), "interpolation": ( [ 'cardinal', @@ -59,20 +67,26 @@ guaranteed!! ## Graphical editor to create values for various ## schedules and/or mask batches. -Shift + click to add control points. -Right click to delete control points. +**Shift + click** to add control point at end. +**Ctrl + click** to add control point (subdivide) between two points. +**Right click on a point** to delete it. Note that you can't delete from start/end. + +Right click on canvas for context menu: +These are purely visual options, doesn't affect the output: + - Toggle handles visibility + - Display sample points: display the points to be returned. **points_to_sample** value sets the number of samples returned from the **drawn spline itself**, this is independent from the actual control points, so the interpolation type matters. - -Changing interpolation type and tension value takes effect on -interaction with the graph. +sampling_method: + - time: samples along the time axis, used for schedules + - path: samples along the path itself, useful for coordinates output types: - mask batch - example compatible nodes: anything that takes masks + example compatible nodes: anything that takes masks - list of floats example compatible nodes: IPAdapter weights - pandas series @@ -83,7 +97,7 @@ output types: """ def splinedata(self, mask_width, mask_height, coordinates, float_output_type, interpolation, - points_to_sample, points_store, tension, repeat_output, min_value=0.0, max_value=1.0): + points_to_sample, sampling_method, points_store, tension, repeat_output, min_value=0.0, max_value=1.0): coordinates = json.loads(coordinates) for coord in coordinates: @@ -151,11 +165,8 @@ Grow value is the amount to grow the shape on each frame, creating animated mask # Define the number of images in the batch coordinates = coordinates.replace("'", '"') coordinates = json.loads(coordinates) - for coord in coordinates: - print(coord) batch_size = len(coordinates) - print(batch_size) out = [] color = "white" diff --git a/web/js/spline_editor.js b/web/js/spline_editor.js index 6b0bf63..27a5e46 100644 --- a/web/js/spline_editor.js +++ b/web/js/spline_editor.js @@ -116,7 +116,6 @@ app.registerExtension({ }); // context menu - this.contextMenu = document.createElement("div"); this.contextMenu.id = "context-menu"; this.contextMenu.style.display = "none"; @@ -148,13 +147,13 @@ app.registerExtension({ this.menuItem2.textContent = "Display sample points"; styleMenuItem(this.menuItem2); - this.menuItem3 = document.createElement("a"); - this.menuItem3.href = "#"; - this.menuItem3.id = "menu-item-2"; - this.menuItem3.textContent = "Switch sampling method"; - styleMenuItem(this.menuItem3); + // this.menuItem3 = document.createElement("a"); + // this.menuItem3.href = "#"; + // this.menuItem3.id = "menu-item-2"; + // this.menuItem3.textContent = "Switch sampling method"; + // styleMenuItem(this.menuItem3); - const menuItems = [this.menuItem1, this.menuItem2, this.menuItem3]; + const menuItems = [this.menuItem1, this.menuItem2]; menuItems.forEach(menuItem => { menuItem.addEventListener('mouseover', function() { @@ -173,7 +172,6 @@ app.registerExtension({ document.body.appendChild( this.contextMenu); this.addWidget("button", "New spline", null, () => { - if (!this.properties || !("points" in this.properties)) { createSplineEditor(this) this.addProperty("points", this.constructor.type, "string"); @@ -183,7 +181,7 @@ app.registerExtension({ } }); - this.setSize([550, 870]); + this.setSize([550, 900]); this.resizable = false; this.splineEditor.parentEl = document.createElement("div"); this.splineEditor.parentEl.className = "spline-editor"; @@ -191,9 +189,8 @@ app.registerExtension({ element.appendChild(this.splineEditor.parentEl); chainCallback(this, "onGraphConfigured", function() { - console.log('onGraphConfigured'); createSplineEditor(this); - this.setSize([550, 870]); + this.setSize([550, 900]); }); }); // onAfterGraphConfigured @@ -248,51 +245,50 @@ function createSplineEditor(context, reset=false) { updatePath(); }); - context.menuItem3.addEventListener('click', function(e) { - e.preventDefault(); - if (pointSamplingMethod == samplePointsTime) { - pointSamplingMethod = samplePointsPath - } - else { - pointSamplingMethod = samplePointsTime - } - updatePath(); -}); +// context.menuItem3.addEventListener('click', function(e) { +// e.preventDefault(); +// if (pointSamplingMethod == samplePointsTime) { +// pointSamplingMethod = samplePointsPath +// } +// else { +// pointSamplingMethod = samplePointsTime +// } +// updatePath(); +// }); var drawSamplePoints = false; - var pointSamplingMethod = samplePointsTime + //var pointSamplingMethod = samplePointsTime function updatePath() { - points_to_sample = pointsWidget.value - let coords = pointSamplingMethod(pathElements[0], points_to_sample); + let coords = samplePoints(pathElements[0], points_to_sample, samplingMethod); + if (drawSamplePoints) { - if (pointsLayer) { - // Update the data of the existing points layer - pointsLayer.data(coords); - } else { - // Create the points layer if it doesn't exist - pointsLayer = vis.add(pv.Dot) - .data(coords) - .left(function(d) { return d.x; }) - .top(function(d) { return d.y; }) - .radius(5) // Adjust the radius as needed - .fillStyle("red") // Change the color as needed - .strokeStyle("black") // Change the stroke color as needed - .lineWidth(1); // Adjust the line width as needed + if (pointsLayer) { + // Update the data of the existing points layer + pointsLayer.data(coords); + } else { + // Create the points layer if it doesn't exist + pointsLayer = vis.add(pv.Dot) + .data(coords) + .left(function(d) { return d.x; }) + .top(function(d) { return d.y; }) + .radius(5) // Adjust the radius as needed + .fillStyle("red") // Change the color as needed + .strokeStyle("black") // Change the stroke color as needed + .lineWidth(1); // Adjust the line width as needed } - } - else { + } else { if (pointsLayer) { - // Remove the points layer - pointsLayer.data([]); - vis.render(); + // Remove the points layer + pointsLayer.data([]); + vis.render(); } } let coordsString = JSON.stringify(coords); pointsStoreWidget.value = JSON.stringify(points); if (coordWidget) { coordWidget.value = coordsString; - } + } vis.render(); } @@ -306,6 +302,7 @@ function createSplineEditor(context, reset=false) { const tensionWidget = context.widgets.find(w => w.name === "tension"); const minValueWidget = context.widgets.find(w => w.name === "min_value"); const maxValueWidget = context.widgets.find(w => w.name === "max_value"); + const samplingMethodWidget = context.widgets.find(w => w.name === "sampling_method"); //const segmentedWidget = context.widgets.find(w => w.name === "segmented"); var interpolation = interpolationWidget.value @@ -314,12 +311,16 @@ function createSplineEditor(context, reset=false) { var rangeMin = minValueWidget.value var rangeMax = maxValueWidget.value var pointsLayer = null; + var samplingMethod = samplingMethodWidget.value interpolationWidget.callback = () => { interpolation = interpolationWidget.value updatePath(); } - + samplingMethodWidget.callback = () => { + samplingMethod = samplingMethodWidget.value + updatePath(); + } tensionWidget.callback = () => { tension = tensionWidget.value updatePath(); @@ -328,7 +329,6 @@ function createSplineEditor(context, reset=false) { points_to_sample = pointsWidget.value updatePath(); } - minValueWidget.callback = () => { rangeMin = minValueWidget.value updatePath(); @@ -376,9 +376,34 @@ function createSplineEditor(context, reset=false) { .margin(10) .event("mousedown", function() { if (pv.event.shiftKey) { // Use pv.event to access the event object - i = points.push(this.mouse()) - 1; + let scaledMouse = { + x: this.mouse().x / app.canvas.ds.scale, + y: this.mouse().y / app.canvas.ds.scale + }; + i = points.push(scaledMouse) - 1; return this; } + else if (pv.event.ctrlKey) { + // Capture the clicked location + let clickedPoint = { + x: this.mouse().x / app.canvas.ds.scale, + y: this.mouse().y / app.canvas.ds.scale + }; + + // Find the two closest points to the clicked location + let { point1Index, point2Index } = findClosestPoints(points, clickedPoint); + + // Calculate the midpoint between the two closest points + let midpoint = { + x: (points[point1Index].x + points[point2Index].x) / 2, + y: (points[point1Index].y + points[point2Index].y) / 2 + }; + + // Insert the midpoint into the array + points.splice(point2Index, 0, midpoint); + i = point2Index; + updatePath(); + } else if (pv.event.button === 2) { context.contextMenu.style.display = 'block'; context.contextMenu.style.left = `${pv.event.clientX}px`; @@ -388,9 +413,15 @@ function createSplineEditor(context, reset=false) { vis.add(pv.Rule) .data(pv.range(0, 8, .5)) - .bottom(d => d * 64 + 0) + .bottom(d => d * 64) .strokeStyle("gray") - .lineWidth(2) + .lineWidth(3) + + // vis.add(pv.Rule) + // .data(pv.range(0, points_to_sample, 1)) + // .left(d => d * 512 / (points_to_sample - 1)) + // .strokeStyle("gray") + // .lineWidth(2) vis.add(pv.Line) .data(() => points) @@ -475,38 +506,26 @@ function createSplineEditor(context, reset=false) { context.splineEditor.element.appendChild(svgElement); var pathElements = svgElement.getElementsByTagName('path'); // Get all path elements updatePath(); - -} -function samplePointsPath(svgPathElement, numSamples) { - var pathLength = svgPathElement.getTotalLength(); - var points = []; - - for (var i = 0; i < numSamples; i++) { - // Calculate the distance along the path for the current sample - var distance = (pathLength / (numSamples - 1)) * i; - console.log(distance) - - // Get the point at the current distance - var point = svgPathElement.getPointAtLength(distance); - - // Add the point to the array of points - points.push({ x: point.x, y: point.y }); - } - console.log(points); - return points; } -function samplePointsTime(svgPathElement, numSamples) { +function samplePoints(svgPathElement, numSamples, samplingMethod) { var svgWidth = 512; // Fixed width of the SVG element var pathLength = svgPathElement.getTotalLength(); var points = []; for (var i = 0; i < numSamples; i++) { - // Calculate the x-coordinate for the current sample based on the SVG's width - var x = (svgWidth / (numSamples - 1)) * i; - - // Find the point on the path that intersects the vertical line at the calculated x-coordinate - var point = findPointAtX(svgPathElement, x, pathLength); + if (samplingMethod === "time") { + // Calculate the x-coordinate for the current sample based on the SVG's width + var x = (svgWidth / (numSamples - 1)) * i; + // Find the point on the path that intersects the vertical line at the calculated x-coordinate + var point = findPointAtX(svgPathElement, x, pathLength); + } + else if (samplingMethod === "path") { + // Calculate the distance along the path for the current sample + var distance = (pathLength / (numSamples - 1)) * i; + // Get the point at the current distance + var point = svgPathElement.getPointAtLength(distance); + } // Add the point to the array of points points.push({ x: point.x, y: point.y }); @@ -514,6 +533,24 @@ function samplePointsTime(svgPathElement, numSamples) { return points; } +function findClosestPoints(points, clickedPoint) { + // Calculate distances from clickedPoint to each point in the array + let distances = points.map(point => { + let dx = clickedPoint.x - point.x; + let dy = clickedPoint.y - point.y; + return { index: points.indexOf(point), distance: Math.sqrt(dx * dx + dy * dy) }; + }); + // Sort distances and get the indices of the two closest points + let sortedDistances = distances.sort((a, b) => a.distance - b.distance); + let closestPoint1Index = sortedDistances[0].index; + let closestPoint2Index = sortedDistances[1].index; + // Ensure point1Index is always the smaller index + if (closestPoint1Index > closestPoint2Index) { + [closestPoint1Index, closestPoint2Index] = [closestPoint2Index, closestPoint1Index]; + } + return { point1Index: closestPoint1Index, point2Index: closestPoint2Index }; +} + function findPointAtX(svgPathElement, targetX, pathLength) { let low = 0; let high = pathLength;