Spline editor updates

This commit is contained in:
kijai 2024-04-29 02:21:03 +03:00
parent 1bb4b9bd26
commit c48cd8b152
2 changed files with 131 additions and 83 deletions

View File

@ -15,6 +15,14 @@ class SplineEditor:
"mask_width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), "mask_width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}),
"mask_height": ("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}), "points_to_sample": ("INT", {"default": 16, "min": 2, "max": 1000, "step": 1}),
"sampling_method": (
[
'path',
'time',
],
{
"default": 'time'
}),
"interpolation": ( "interpolation": (
[ [
'cardinal', 'cardinal',
@ -59,20 +67,26 @@ guaranteed!!
## Graphical editor to create values for various ## Graphical editor to create values for various
## schedules and/or mask batches. ## schedules and/or mask batches.
Shift + click to add control points. **Shift + click** to add control point at end.
Right click to delete control points. **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. 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 **points_to_sample** value sets the number of samples
returned from the **drawn spline itself**, this is independent from the returned from the **drawn spline itself**, this is independent from the
actual control points, so the interpolation type matters. actual control points, so the interpolation type matters.
sampling_method:
Changing interpolation type and tension value takes effect on - time: samples along the time axis, used for schedules
interaction with the graph. - path: samples along the path itself, useful for coordinates
output types: output types:
- mask batch - mask batch
example compatible nodes: anything that takes masks example compatible nodes: anything that takes masks
- list of floats - list of floats
example compatible nodes: IPAdapter weights example compatible nodes: IPAdapter weights
- pandas series - pandas series
@ -83,7 +97,7 @@ output types:
""" """
def splinedata(self, mask_width, mask_height, coordinates, float_output_type, interpolation, 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) coordinates = json.loads(coordinates)
for coord in 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 # Define the number of images in the batch
coordinates = coordinates.replace("'", '"') coordinates = coordinates.replace("'", '"')
coordinates = json.loads(coordinates) coordinates = json.loads(coordinates)
for coord in coordinates:
print(coord)
batch_size = len(coordinates) batch_size = len(coordinates)
print(batch_size)
out = [] out = []
color = "white" color = "white"

View File

@ -116,7 +116,6 @@ app.registerExtension({
}); });
// context menu // context menu
this.contextMenu = document.createElement("div"); this.contextMenu = document.createElement("div");
this.contextMenu.id = "context-menu"; this.contextMenu.id = "context-menu";
this.contextMenu.style.display = "none"; this.contextMenu.style.display = "none";
@ -148,13 +147,13 @@ app.registerExtension({
this.menuItem2.textContent = "Display sample points"; this.menuItem2.textContent = "Display sample points";
styleMenuItem(this.menuItem2); styleMenuItem(this.menuItem2);
this.menuItem3 = document.createElement("a"); // this.menuItem3 = document.createElement("a");
this.menuItem3.href = "#"; // this.menuItem3.href = "#";
this.menuItem3.id = "menu-item-2"; // this.menuItem3.id = "menu-item-2";
this.menuItem3.textContent = "Switch sampling method"; // this.menuItem3.textContent = "Switch sampling method";
styleMenuItem(this.menuItem3); // styleMenuItem(this.menuItem3);
const menuItems = [this.menuItem1, this.menuItem2, this.menuItem3]; const menuItems = [this.menuItem1, this.menuItem2];
menuItems.forEach(menuItem => { menuItems.forEach(menuItem => {
menuItem.addEventListener('mouseover', function() { menuItem.addEventListener('mouseover', function() {
@ -173,7 +172,6 @@ app.registerExtension({
document.body.appendChild( this.contextMenu); document.body.appendChild( this.contextMenu);
this.addWidget("button", "New spline", null, () => { this.addWidget("button", "New spline", null, () => {
if (!this.properties || !("points" in this.properties)) { if (!this.properties || !("points" in this.properties)) {
createSplineEditor(this) createSplineEditor(this)
this.addProperty("points", this.constructor.type, "string"); 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.resizable = false;
this.splineEditor.parentEl = document.createElement("div"); this.splineEditor.parentEl = document.createElement("div");
this.splineEditor.parentEl.className = "spline-editor"; this.splineEditor.parentEl.className = "spline-editor";
@ -191,9 +189,8 @@ app.registerExtension({
element.appendChild(this.splineEditor.parentEl); element.appendChild(this.splineEditor.parentEl);
chainCallback(this, "onGraphConfigured", function() { chainCallback(this, "onGraphConfigured", function() {
console.log('onGraphConfigured');
createSplineEditor(this); createSplineEditor(this);
this.setSize([550, 870]); this.setSize([550, 900]);
}); });
}); // onAfterGraphConfigured }); // onAfterGraphConfigured
@ -248,51 +245,50 @@ function createSplineEditor(context, reset=false) {
updatePath(); updatePath();
}); });
context.menuItem3.addEventListener('click', function(e) { // context.menuItem3.addEventListener('click', function(e) {
e.preventDefault(); // e.preventDefault();
if (pointSamplingMethod == samplePointsTime) { // if (pointSamplingMethod == samplePointsTime) {
pointSamplingMethod = samplePointsPath // pointSamplingMethod = samplePointsPath
} // }
else { // else {
pointSamplingMethod = samplePointsTime // pointSamplingMethod = samplePointsTime
} // }
updatePath(); // updatePath();
}); // });
var drawSamplePoints = false; var drawSamplePoints = false;
var pointSamplingMethod = samplePointsTime //var pointSamplingMethod = samplePointsTime
function updatePath() { function updatePath() {
points_to_sample = pointsWidget.value let coords = samplePoints(pathElements[0], points_to_sample, samplingMethod);
let coords = pointSamplingMethod(pathElements[0], points_to_sample);
if (drawSamplePoints) { if (drawSamplePoints) {
if (pointsLayer) { if (pointsLayer) {
// Update the data of the existing points layer // Update the data of the existing points layer
pointsLayer.data(coords); pointsLayer.data(coords);
} else { } else {
// Create the points layer if it doesn't exist // Create the points layer if it doesn't exist
pointsLayer = vis.add(pv.Dot) pointsLayer = vis.add(pv.Dot)
.data(coords) .data(coords)
.left(function(d) { return d.x; }) .left(function(d) { return d.x; })
.top(function(d) { return d.y; }) .top(function(d) { return d.y; })
.radius(5) // Adjust the radius as needed .radius(5) // Adjust the radius as needed
.fillStyle("red") // Change the color as needed .fillStyle("red") // Change the color as needed
.strokeStyle("black") // Change the stroke color as needed .strokeStyle("black") // Change the stroke color as needed
.lineWidth(1); // Adjust the line width as needed .lineWidth(1); // Adjust the line width as needed
} }
} } else {
else {
if (pointsLayer) { if (pointsLayer) {
// Remove the points layer // Remove the points layer
pointsLayer.data([]); pointsLayer.data([]);
vis.render(); vis.render();
} }
} }
let coordsString = JSON.stringify(coords); let coordsString = JSON.stringify(coords);
pointsStoreWidget.value = JSON.stringify(points); pointsStoreWidget.value = JSON.stringify(points);
if (coordWidget) { if (coordWidget) {
coordWidget.value = coordsString; coordWidget.value = coordsString;
} }
vis.render(); vis.render();
} }
@ -306,6 +302,7 @@ function createSplineEditor(context, reset=false) {
const tensionWidget = context.widgets.find(w => w.name === "tension"); const tensionWidget = context.widgets.find(w => w.name === "tension");
const minValueWidget = context.widgets.find(w => w.name === "min_value"); const minValueWidget = context.widgets.find(w => w.name === "min_value");
const maxValueWidget = context.widgets.find(w => w.name === "max_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"); //const segmentedWidget = context.widgets.find(w => w.name === "segmented");
var interpolation = interpolationWidget.value var interpolation = interpolationWidget.value
@ -314,12 +311,16 @@ function createSplineEditor(context, reset=false) {
var rangeMin = minValueWidget.value var rangeMin = minValueWidget.value
var rangeMax = maxValueWidget.value var rangeMax = maxValueWidget.value
var pointsLayer = null; var pointsLayer = null;
var samplingMethod = samplingMethodWidget.value
interpolationWidget.callback = () => { interpolationWidget.callback = () => {
interpolation = interpolationWidget.value interpolation = interpolationWidget.value
updatePath(); updatePath();
} }
samplingMethodWidget.callback = () => {
samplingMethod = samplingMethodWidget.value
updatePath();
}
tensionWidget.callback = () => { tensionWidget.callback = () => {
tension = tensionWidget.value tension = tensionWidget.value
updatePath(); updatePath();
@ -328,7 +329,6 @@ function createSplineEditor(context, reset=false) {
points_to_sample = pointsWidget.value points_to_sample = pointsWidget.value
updatePath(); updatePath();
} }
minValueWidget.callback = () => { minValueWidget.callback = () => {
rangeMin = minValueWidget.value rangeMin = minValueWidget.value
updatePath(); updatePath();
@ -376,9 +376,34 @@ function createSplineEditor(context, reset=false) {
.margin(10) .margin(10)
.event("mousedown", function() { .event("mousedown", function() {
if (pv.event.shiftKey) { // Use pv.event to access the event object 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; 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) { else if (pv.event.button === 2) {
context.contextMenu.style.display = 'block'; context.contextMenu.style.display = 'block';
context.contextMenu.style.left = `${pv.event.clientX}px`; context.contextMenu.style.left = `${pv.event.clientX}px`;
@ -388,9 +413,15 @@ function createSplineEditor(context, reset=false) {
vis.add(pv.Rule) vis.add(pv.Rule)
.data(pv.range(0, 8, .5)) .data(pv.range(0, 8, .5))
.bottom(d => d * 64 + 0) .bottom(d => d * 64)
.strokeStyle("gray") .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) vis.add(pv.Line)
.data(() => points) .data(() => points)
@ -475,38 +506,26 @@ function createSplineEditor(context, reset=false) {
context.splineEditor.element.appendChild(svgElement); context.splineEditor.element.appendChild(svgElement);
var pathElements = svgElement.getElementsByTagName('path'); // Get all path elements var pathElements = svgElement.getElementsByTagName('path'); // Get all path elements
updatePath(); 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 svgWidth = 512; // Fixed width of the SVG element
var pathLength = svgPathElement.getTotalLength(); var pathLength = svgPathElement.getTotalLength();
var points = []; var points = [];
for (var i = 0; i < numSamples; i++) { for (var i = 0; i < numSamples; i++) {
// Calculate the x-coordinate for the current sample based on the SVG's width if (samplingMethod === "time") {
var x = (svgWidth / (numSamples - 1)) * 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 // Find the point on the path that intersects the vertical line at the calculated x-coordinate
var point = findPointAtX(svgPathElement, x, pathLength); 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 // Add the point to the array of points
points.push({ x: point.x, y: point.y }); points.push({ x: point.x, y: point.y });
@ -514,6 +533,24 @@ function samplePointsTime(svgPathElement, numSamples) {
return points; 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) { function findPointAtX(svgPathElement, targetX, pathLength) {
let low = 0; let low = 0;
let high = pathLength; let high = pathLength;