mirror of
https://git.datalinker.icu/kijai/ComfyUI-KJNodes.git
synced 2025-12-14 23:34:35 +08:00
Spline editor updates
This commit is contained in:
parent
1bb4b9bd26
commit
c48cd8b152
@ -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"
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user