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_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"

View File

@ -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;