SplineEditor updates

This commit is contained in:
kijai 2025-06-09 17:59:31 +03:00
parent 3f62cf2849
commit 1fbf4805f1
3 changed files with 209 additions and 33 deletions

View File

@ -206,6 +206,16 @@ guaranteed!!
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: Right click on canvas for context menu:
NEW!:
- Add new spline
- Creates a new spline on same canvas, currently these paths are only outputed
as coordinates.
- Add single point
- Creates a single point that only returns it's current position coords
- Delete spline
- Deletes the currently selected spline, you can select a spline by clicking on
it's path, or cycle through them with the 'Next spline' -option.
These are purely visual options, doesn't affect the output: These are purely visual options, doesn't affect the output:
- Toggle handles visibility - Toggle handles visibility
- Display sample points: display the points to be returned. - Display sample points: display the points to be returned.
@ -216,6 +226,7 @@ actual control points, so the interpolation type matters.
sampling_method: sampling_method:
- time: samples along the time axis, used for schedules - time: samples along the time axis, used for schedules
- path: samples along the path itself, useful for coordinates - path: samples along the path itself, useful for coordinates
- controlpoints: samples only the control points themselves
output types: output types:
- mask batch - mask batch

View File

@ -2285,7 +2285,7 @@ class PreviewAnimation:
c = len(pil_images) c = len(pil_images)
for i in range(0, c, num_frames): for i in range(0, c, num_frames):
file = f"{filename}_{counter:05}_.webp" file = f"{filename}_{counter:05}_.webp"
pil_images[i].save(os.path.join(full_output_folder, file), save_all=True, duration=int(1000.0/fps), append_images=pil_images[i + 1:i + num_frames], lossless=False, quality=80, method=4) pil_images[i].save(os.path.join(full_output_folder, file), save_all=True, duration=int(1000.0/fps), append_images=pil_images[i + 1:i + num_frames], lossless=False, quality=50, method=0)
results.append({ results.append({
"filename": file, "filename": file,
"subfolder": subfolder, "subfolder": subfolder,

View File

@ -104,7 +104,6 @@ app.registerExtension({
if (nodeData?.name === 'SplineEditor') { if (nodeData?.name === 'SplineEditor') {
chainCallback(nodeType.prototype, "onNodeCreated", function () { chainCallback(nodeType.prototype, "onNodeCreated", function () {
//hideWidgetForGood(this, this.widgets.find(w => w.name === "coordinates"))
this.widgets.find(w => w.name === "coordinates").hidden = true this.widgets.find(w => w.name === "coordinates").hidden = true
var element = document.createElement("div"); var element = document.createElement("div");
@ -157,8 +156,9 @@ app.registerExtension({
createMenuItem(4, "Invert point order"), createMenuItem(4, "Invert point order"),
createMenuItem(5, "Clear Image"), createMenuItem(5, "Clear Image"),
createMenuItem(6, "Add new spline"), createMenuItem(6, "Add new spline"),
createMenuItem(7, "Delete current spline"), createMenuItem(7, "Add new single point"),
createMenuItem(8, "Next spline"), createMenuItem(8, "Delete current spline"),
createMenuItem(9, "Next spline"),
]; ];
// Add mouseover and mouseout event listeners to each menu item for styling // Add mouseover and mouseout event listeners to each menu item for styling
@ -189,7 +189,7 @@ app.registerExtension({
} }
}); });
this.setSize([550, 950]); this.setSize([550, 1000]);
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";
@ -329,7 +329,7 @@ class SplineEditor{
this.heightWidget.callback = () => { this.heightWidget.callback = () => {
this.height = this.heightWidget.value this.height = this.heightWidget.value
this.vis.height(this.height) this.vis.height(this.height)
context.setSize([context.size[0], this.height + 430]); context.setSize([context.size[0], this.height + 450]);
this.updatePath(); this.updatePath();
} }
this.pointsStoreWidget.callback = () => { this.pointsStoreWidget.callback = () => {
@ -347,6 +347,8 @@ this.heightWidget.callback = () => {
var i = 3; var i = 3;
this.splines = []; this.splines = [];
this.activeSplineIndex = 0; // Track which spline is being edited this.activeSplineIndex = 0; // Track which spline is being edited
// init mouse position
this.lastMousePosition = { x: this.width/2, y: this.height/2 };
if (!reset && this.pointsStoreWidget.value != "") { if (!reset && this.pointsStoreWidget.value != "") {
try { try {
@ -412,6 +414,12 @@ this.heightWidget.callback = () => {
self.updatePath(); self.updatePath();
} }
else if (pv.event.button === 2) { else if (pv.event.button === 2) {
// Store the current mouse position adjusted for scale
self.lastMousePosition = {
x: this.mouse().x / app.canvas.ds.scale,
y: this.mouse().y / app.canvas.ds.scale
};
self.node.contextMenu.style.display = 'block'; self.node.contextMenu.style.display = 'block';
self.node.contextMenu.style.left = `${pv.event.clientX}px`; self.node.contextMenu.style.left = `${pv.event.clientX}px`;
self.node.contextMenu.style.top = `${pv.event.clientY}px`; self.node.contextMenu.style.top = `${pv.event.clientY}px`;
@ -429,6 +437,21 @@ this.heightWidget.callback = () => {
this.hoverSplineIndex = -1; this.hoverSplineIndex = -1;
this.splines.forEach((spline, splineIndex) => { this.splines.forEach((spline, splineIndex) => {
const strokeObj = this.vis.add(pv.Line)
.data(() => spline.points)
.left(d => d.x)
.top(d => d.y)
.interpolate(() => this.interpolation)
.tension(() => this.tension)
.segmented(() => false)
.strokeStyle("black") // Stroke color
.lineWidth(() => {
// Make stroke slightly wider than the main line
if (splineIndex === this.activeSplineIndex) return 5;
if (splineIndex === this.hoverSplineIndex) return 4;
return 3.5;
});
this.vis.add(pv.Line) this.vis.add(pv.Line)
.data(() => spline.points) .data(() => spline.points)
.left(d => d.x) .left(d => d.x)
@ -460,7 +483,14 @@ this.heightWidget.callback = () => {
}); });
this.vis.add(pv.Dot) this.vis.add(pv.Dot)
.data(() => this.splines[this.activeSplineIndex].points) .data(() => {
const activeSpline = this.splines[this.activeSplineIndex];
// If this is a single point, don't show it in the main visualization
if (activeSpline.isSinglePoint || (activeSpline.points && activeSpline.points.length === 1)) {
return []; // Return empty array to hide in main visualization
}
return activeSpline.points;
})
.left(d => d.x) .left(d => d.x)
.top(d => d.y) .top(d => d.y)
.radius(10) .radius(10)
@ -557,6 +587,63 @@ this.heightWidget.callback = () => {
}) })
.textStyle("orange") .textStyle("orange")
// single points
this.vis.add(pv.Dot)
.data(() => {
// Collect all single points from all splines
const singlePoints = [];
this.splines.forEach((spline, splineIndex) => {
if (spline.isSinglePoint || (spline.points && spline.points.length === 1)) {
singlePoints.push({
x: spline.points[0].x,
y: spline.points[0].y,
splineIndex: splineIndex,
color: spline.color
});
}
});
return singlePoints;
})
.left(d => d.x)
.top(d => d.y)
.radius(6)
.shape("square")
.strokeStyle(d => d.splineIndex === this.activeSplineIndex ? "#ff7f0e" : d.color)
.fillStyle(d => "rgba(100, 100, 100, 0.9)")
.lineWidth(d => d.splineIndex === this.activeSplineIndex ? 3 : 1.5)
.cursor("move")
.event("mousedown", pv.Behavior.drag())
.event("dragstart", function(d) {
self.activeSplineIndex = d.splineIndex;
self.refreshSplineElements();
return this;
})
.event("drag", function(d) {
let adjustedX = this.mouse().x / app.canvas.ds.scale;
let adjustedY = this.mouse().y / app.canvas.ds.scale;
// Determine the bounds of the vis.Panel
const panelWidth = self.vis.width();
const panelHeight = self.vis.height();
// Adjust the new position if it would place the dot outside the bounds
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
// Update the point position
const spline = self.splines[d.splineIndex];
spline.points[0] = { x: adjustedX, y: adjustedY };
// For single points, we need to refresh the entire spline element
// to prevent the line-drawing effect
})
.event("dragend", function(d) {
self.refreshSplineElements();
self.updatePath();
})
.visible(d => true); // Make always visible
if (this.splines.length != 0) { if (this.splines.length != 0) {
this.vis.render(); this.vis.render();
} }
@ -569,7 +656,7 @@ this.heightWidget.callback = () => {
if (this.width > 256) { if (this.width > 256) {
this.node.setSize([this.width + 45, this.node.size[1]]); this.node.setSize([this.width + 45, this.node.size[1]]);
} }
this.node.setSize([this.node.size[0], this.height + 430]); this.node.setSize([this.node.size[0], this.height + 450]);
this.updatePath(); this.updatePath();
this.refreshBackgroundImage(); this.refreshBackgroundImage();
} }
@ -589,7 +676,6 @@ this.heightWidget.callback = () => {
return; return;
} }
console.log("this.pathElements", this.pathElements);
let coords; let coords;
if (this.samplingMethod != "controlpoints") { if (this.samplingMethod != "controlpoints") {
@ -644,7 +730,7 @@ this.heightWidget.callback = () => {
}; };
handleImageLoad = (img, file, base64String) => { handleImageLoad = (img, file, base64String) => {
console.log(img.width, img.height); // Access width and height here //console.log(img.width, img.height); // Access width and height here
this.widthWidget.value = img.width; this.widthWidget.value = img.width;
this.heightWidget.value = img.height; this.heightWidget.value = img.height;
this.drawRuler = false; this.drawRuler = false;
@ -653,7 +739,7 @@ this.heightWidget.callback = () => {
if (img.width > 256) { if (img.width > 256) {
this.node.setSize([img.width + 45, this.node.size[1]]); this.node.setSize([img.width + 45, this.node.size[1]]);
} }
this.node.setSize([this.node.size[0], img.height + 500]); this.node.setSize([this.node.size[0], img.height + 520]);
this.vis.width(img.width); this.vis.width(img.width);
this.vis.height(img.height); this.vis.height(img.height);
this.height = img.height; this.height = img.height;
@ -749,7 +835,53 @@ this.heightWidget.callback = () => {
// Re-add all spline lines and store references to them // Re-add all spline lines and store references to them
this.splines.forEach((spline, splineIndex) => { this.splines.forEach((spline, splineIndex) => {
const lineObj = this.vis.add(pv.Line) // For single points, we need a special handling
if (spline.isSinglePoint || (spline.points && spline.points.length === 1)) {
const point = spline.points[0];
// For single points, create a tiny line at the same point
// This ensures we have a path element for the point
const lineObj = this.vis.add(pv.Line)
.data([point, {x: point.x + 0.001, y: point.y + 0.001}])
.left(d => d.x)
.top(d => d.y)
.strokeStyle(spline.color)
.lineWidth(() => {
if (splineIndex === this.activeSplineIndex) return 3;
if (splineIndex === this.hoverSplineIndex) return 2;
return 1.5;
})
.event("mouseover", () => {
this.hoverSplineIndex = splineIndex;
this.vis.render();
})
.event("mouseout", () => {
this.hoverSplineIndex = -1;
this.vis.render();
})
.event("mousedown", () => {
if (this.activeSplineIndex !== splineIndex) {
this.activeSplineIndex = splineIndex;
this.refreshSplineElements();
}
});
this.lineObjects.push(lineObj);
} else {
// For normal multi-point splines
const strokeObj = this.vis.add(pv.Line)
.data(() => spline.points)
.left(d => d.x)
.top(d => d.y)
.interpolate(() => this.interpolation)
.tension(() => this.tension)
.segmented(() => false)
.strokeStyle("black") // Stroke color
.lineWidth(() => {
// Make stroke slightly wider than the main line
if (splineIndex === this.activeSplineIndex) return 5;
if (splineIndex === this.hoverSplineIndex) return 4;
return 3.5;
});
const lineObj = this.vis.add(pv.Line)
.data(() => spline.points) .data(() => spline.points)
.left(d => d.x) .left(d => d.x)
.top(d => d.y) .top(d => d.y)
@ -758,7 +890,6 @@ this.heightWidget.callback = () => {
.segmented(() => false) .segmented(() => false)
.strokeStyle(spline.color) .strokeStyle(spline.color)
.lineWidth(() => { .lineWidth(() => {
// Change line width based on active or hover state
if (splineIndex === this.activeSplineIndex) return 3; if (splineIndex === this.activeSplineIndex) return 3;
if (splineIndex === this.hoverSplineIndex) return 2; if (splineIndex === this.hoverSplineIndex) return 2;
return 1.5; return 1.5;
@ -777,7 +908,34 @@ this.heightWidget.callback = () => {
this.refreshSplineElements(); this.refreshSplineElements();
} }
}); });
this.lineObjects.push(lineObj);
// Add invisible wider hit area for easier selection
this.vis.add(pv.Line)
.data(() => spline.points)
.left(d => d.x)
.top(d => d.y)
.interpolate(() => this.interpolation)
.tension(() => this.tension)
.segmented(() => false)
.strokeStyle("rgba(0,0,0,0.01)") // Nearly invisible
.lineWidth(15) // Much wider hit area
.event("mouseover", () => {
this.hoverSplineIndex = splineIndex;
this.vis.render();
})
.event("mouseout", () => {
this.hoverSplineIndex = -1;
this.vis.render();
})
.event("mousedown", () => {
if (this.activeSplineIndex !== splineIndex) {
this.activeSplineIndex = splineIndex;
this.refreshSplineElements();
}
});
this.lineObjects.push(lineObj);
}
}); });
this.vis.render(); this.vis.render();
@ -796,7 +954,7 @@ this.heightWidget.callback = () => {
); );
if (matchingPath) { if (matchingPath) {
console.log("matchingPath:", matchingPath); //console.log("matchingPath:", matchingPath);
this.pathElements[i] = matchingPath; this.pathElements[i] = matchingPath;
} }
}); });
@ -914,7 +1072,6 @@ this.heightWidget.callback = () => {
else { else {
self.dotShape = "circle" self.dotShape = "circle"
} }
console.log(self.dotShape)
self.updatePath(); self.updatePath();
break; break;
case 3: case 3:
@ -964,7 +1121,21 @@ this.heightWidget.callback = () => {
self.refreshSplineElements(); self.refreshSplineElements();
self.node.contextMenu.style.display = 'none'; self.node.contextMenu.style.display = 'none';
break; break;
case 7: // Delete current spline case 7: // Add new single point
const newSingleSplineIndex = self.splines.length;
self.splines.push({
points: [
{ x: self.lastMousePosition.x, y: self.lastMousePosition.y },
],
color: self.getSplineColor(newSingleSplineIndex),
name: `Spline ${newSingleSplineIndex + 1}`,
isSinglePoint: true
});
self.activeSplineIndex = newSingleSplineIndex;
self.refreshSplineElements();
self.node.contextMenu.style.display = 'none';
break;
case 8: // Delete current spline
if (self.splines.length > 1) { if (self.splines.length > 1) {
self.splines.splice(self.activeSplineIndex, 1); self.splines.splice(self.activeSplineIndex, 1);
self.activeSplineIndex = Math.min(self.activeSplineIndex, self.splines.length - 1); self.activeSplineIndex = Math.min(self.activeSplineIndex, self.splines.length - 1);
@ -972,7 +1143,7 @@ this.heightWidget.callback = () => {
} }
self.node.contextMenu.style.display = 'none'; self.node.contextMenu.style.display = 'none';
break; break;
case 8: // Next spline case 9: // Next spline
self.activeSplineIndex = (self.activeSplineIndex + 1) % self.splines.length; self.activeSplineIndex = (self.activeSplineIndex + 1) % self.splines.length;
self.refreshSplineElements(); self.refreshSplineElements();
self.node.contextMenu.style.display = 'none'; self.node.contextMenu.style.display = 'none';
@ -983,6 +1154,15 @@ this.heightWidget.callback = () => {
} }
samplePoints(svgPathElement, numSamples, samplingMethod, width, splineIndex) { samplePoints(svgPathElement, numSamples, samplingMethod, width, splineIndex) {
const spline = this.splines[splineIndex];
// Check if this is a single point spline
if (spline && (spline.isSinglePoint || (spline.points && spline.points.length === 1))) {
// For a single point, return an array with the same coordinates repeated
const point = spline.points[0];
return Array(numSamples).fill().map(() => ({ x: point.x, y: point.y }));
}
if (!svgPathElement) { if (!svgPathElement) {
console.warn(`Path element not found for spline index: ${splineIndex}. Available paths: ${this.pathElements.length}`); console.warn(`Path element not found for spline index: ${splineIndex}. Available paths: ${this.pathElements.length}`);
@ -1014,7 +1194,6 @@ this.heightWidget.callback = () => {
var svgWidth = width; // Fixed width of the SVG element var svgWidth = width; // Fixed width of the SVG element
var pathLength = svgPathElement.getTotalLength(); var pathLength = svgPathElement.getTotalLength();
console.log(" pathLength:", pathLength);
var points = []; var points = [];
for (var i = 0; i < numSamples; i++) { for (var i = 0; i < numSamples; i++) {
@ -1084,17 +1263,3 @@ this.heightWidget.callback = () => {
return bestPoint; return bestPoint;
} }
} }
//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
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidgetForGood(node, w, ':' + widget.name)
}
}
}