mirror of
https://git.datalinker.icu/kijai/ComfyUI-KJNodes.git
synced 2026-05-27 16:19:09 +08:00
Initial mostly working multiple splines for SplineEditor
Probably buggy and clumsy to use, but workable and way better than having to use multiple SplineEditors...
This commit is contained in:
parent
42c7641776
commit
ea5482a6ee
@ -200,52 +200,77 @@ 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, sampling_method, points_store, tension, repeat_output,
|
points_to_sample, sampling_method, points_store, tension, repeat_output,
|
||||||
min_value=0.0, max_value=1.0, bg_image=None):
|
min_value=0.0, max_value=1.0, bg_image=None):
|
||||||
|
|
||||||
coordinates = json.loads(coordinates)
|
coordinates = json.loads(coordinates)
|
||||||
normalized = []
|
print("Coordinates: ", coordinates)
|
||||||
normalized_y_values = []
|
|
||||||
for coord in coordinates:
|
# Handle nested list structure if present
|
||||||
coord['x'] = int(round(coord['x']))
|
all_normalized = []
|
||||||
coord['y'] = int(round(coord['y']))
|
all_normalized_y_values = []
|
||||||
norm_x = (1.0 - (coord['x'] / mask_height) - 0.0) * (max_value - min_value) + min_value
|
|
||||||
norm_y = (1.0 - (coord['y'] / mask_height) - 0.0) * (max_value - min_value) + min_value
|
# Check if we have a nested list structure
|
||||||
normalized_y_values.append(norm_y)
|
if isinstance(coordinates, list) and len(coordinates) > 0 and isinstance(coordinates[0], list):
|
||||||
normalized.append({'x':norm_x, 'y':norm_y})
|
# Process each list of coordinates in the nested structure
|
||||||
|
coordinate_sets = coordinates
|
||||||
|
else:
|
||||||
|
# If not nested, treat as a single list of coordinates
|
||||||
|
coordinate_sets = [coordinates]
|
||||||
|
|
||||||
|
# Process each set of coordinates
|
||||||
|
for coord_set in coordinate_sets:
|
||||||
|
normalized = []
|
||||||
|
normalized_y_values = []
|
||||||
|
|
||||||
|
for coord in coord_set:
|
||||||
|
coord['x'] = int(round(coord['x']))
|
||||||
|
coord['y'] = int(round(coord['y']))
|
||||||
|
norm_x = (1.0 - (coord['x'] / mask_height) - 0.0) * (max_value - min_value) + min_value
|
||||||
|
norm_y = (1.0 - (coord['y'] / mask_height) - 0.0) * (max_value - min_value) + min_value
|
||||||
|
normalized_y_values.append(norm_y)
|
||||||
|
normalized.append({'x':norm_x, 'y':norm_y})
|
||||||
|
|
||||||
|
all_normalized.extend(normalized)
|
||||||
|
all_normalized_y_values.extend(normalized_y_values)
|
||||||
|
|
||||||
|
# Use the combined normalized values for output
|
||||||
if float_output_type == 'list':
|
if float_output_type == 'list':
|
||||||
out_floats = normalized_y_values * repeat_output
|
out_floats = all_normalized_y_values * repeat_output
|
||||||
elif float_output_type == 'pandas series':
|
elif float_output_type == 'pandas series':
|
||||||
try:
|
try:
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
except:
|
except:
|
||||||
raise Exception("MaskOrImageToWeight: pandas is not installed. Please install pandas to use this output_type")
|
raise Exception("MaskOrImageToWeight: pandas is not installed. Please install pandas to use this output_type")
|
||||||
out_floats = pd.Series(normalized_y_values * repeat_output),
|
out_floats = pd.Series(all_normalized_y_values * repeat_output),
|
||||||
elif float_output_type == 'tensor':
|
elif float_output_type == 'tensor':
|
||||||
out_floats = torch.tensor(normalized_y_values * repeat_output, dtype=torch.float32)
|
out_floats = torch.tensor(all_normalized_y_values * repeat_output, dtype=torch.float32)
|
||||||
|
|
||||||
# Create a color map for grayscale intensities
|
# Create a color map for grayscale intensities
|
||||||
color_map = lambda y: torch.full((mask_height, mask_width, 3), y, dtype=torch.float32)
|
color_map = lambda y: torch.full((mask_height, mask_width, 3), y, dtype=torch.float32)
|
||||||
|
|
||||||
# Create image tensors for each normalized y value
|
# Create image tensors for each normalized y value
|
||||||
mask_tensors = [color_map(y) for y in normalized_y_values]
|
mask_tensors = [color_map(y) for y in all_normalized_y_values]
|
||||||
masks_out = torch.stack(mask_tensors)
|
masks_out = torch.stack(mask_tensors)
|
||||||
masks_out = masks_out.repeat(repeat_output, 1, 1, 1)
|
masks_out = masks_out.repeat(repeat_output, 1, 1, 1)
|
||||||
masks_out = masks_out.mean(dim=-1)
|
masks_out = masks_out.mean(dim=-1)
|
||||||
|
|
||||||
if bg_image is None:
|
if bg_image is None:
|
||||||
return (masks_out, json.dumps(coordinates), out_floats, len(out_floats) , json.dumps(normalized))
|
return (masks_out, json.dumps(coordinates), out_floats, len(out_floats), json.dumps(all_normalized))
|
||||||
else:
|
else:
|
||||||
transform = transforms.ToPILImage()
|
transform = transforms.ToPILImage()
|
||||||
image = transform(bg_image[0].permute(2, 0, 1))
|
image = transform(bg_image[0].permute(2, 0, 1))
|
||||||
buffered = io.BytesIO()
|
buffered = io.BytesIO()
|
||||||
image.save(buffered, format="JPEG", quality=75)
|
image.save(buffered, format="JPEG", quality=75)
|
||||||
|
|
||||||
# Step 3: Encode the image bytes to a Base64 string
|
# Encode the image bytes to a Base64 string
|
||||||
img_bytes = buffered.getvalue()
|
img_bytes = buffered.getvalue()
|
||||||
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
|
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
|
||||||
return {
|
|
||||||
|
return {
|
||||||
"ui": {"bg_image": [img_base64]},
|
"ui": {"bg_image": [img_base64]},
|
||||||
"result":(masks_out, json.dumps(coordinates), out_floats, len(out_floats) , json.dumps(normalized))
|
"result": (masks_out, json.dumps(coordinates), out_floats, len(out_floats), json.dumps(all_normalized))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreateShapeMaskOnPath:
|
class CreateShapeMaskOnPath:
|
||||||
|
|||||||
@ -155,6 +155,9 @@ app.registerExtension({
|
|||||||
createMenuItem(3, "Background image"),
|
createMenuItem(3, "Background image"),
|
||||||
createMenuItem(4, "Invert point order"),
|
createMenuItem(4, "Invert point order"),
|
||||||
createMenuItem(5, "Clear Image"),
|
createMenuItem(5, "Clear Image"),
|
||||||
|
createMenuItem(6, "Add new spline"),
|
||||||
|
createMenuItem(7, "Delete current spline"),
|
||||||
|
createMenuItem(8, "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
|
||||||
@ -175,7 +178,7 @@ app.registerExtension({
|
|||||||
|
|
||||||
document.body.appendChild(this.contextMenu);
|
document.body.appendChild(this.contextMenu);
|
||||||
|
|
||||||
this.addWidget("button", "New spline", null, () => {
|
this.addWidget("button", "New canvas", null, () => {
|
||||||
if (!this.properties || !("points" in this.properties)) {
|
if (!this.properties || !("points" in this.properties)) {
|
||||||
this.editor = new SplineEditor(this);
|
this.editor = new SplineEditor(this);
|
||||||
this.addProperty("points", this.constructor.type, "string");
|
this.addProperty("points", this.constructor.type, "string");
|
||||||
@ -341,29 +344,32 @@ this.heightWidget.callback = () => {
|
|||||||
this.width = this.widthWidget.value;
|
this.width = this.widthWidget.value;
|
||||||
this.height = this.heightWidget.value;
|
this.height = this.heightWidget.value;
|
||||||
var i = 3;
|
var i = 3;
|
||||||
this.points = [];
|
this.splines = [];
|
||||||
|
this.activeSplineIndex = 0; // Track which spline is being edited
|
||||||
|
|
||||||
if (!reset && this.pointsStoreWidget.value != "") {
|
if (!reset && this.pointsStoreWidget.value != "") {
|
||||||
this.points = JSON.parse(this.pointsStoreWidget.value);
|
try {
|
||||||
} else {
|
const parsedData = JSON.parse(this.pointsStoreWidget.value);
|
||||||
this.points = pv.range(1, 4).map((i, index) => {
|
// Check if it's already in the new format (array of splines)
|
||||||
if (index === 0) {
|
if (Array.isArray(parsedData) && parsedData.length > 0 && parsedData[0].hasOwnProperty('points')) {
|
||||||
// First point at the bottom-left corner
|
this.splines = parsedData;
|
||||||
return { x: 0, y: this.height };
|
|
||||||
} else if (index === 2) {
|
|
||||||
// Last point at the top-right corner
|
|
||||||
return { x: this.width, y: 0 };
|
|
||||||
} else {
|
} else {
|
||||||
// Other points remain as they were
|
// Convert old format (single array of points) to new format
|
||||||
return {
|
this.splines = [{
|
||||||
x: i * this.width / 5,
|
points: parsedData,
|
||||||
y: 50 + Math.random() * (this.height - 100)
|
color: "#1f77b4",
|
||||||
};
|
name: "Spline 1"
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
});
|
} catch (e) {
|
||||||
this.pointsStoreWidget.value = JSON.stringify(this.points);
|
console.error("Error parsing spline data:", e);
|
||||||
}
|
this.initializeDefaultSplines();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.initializeDefaultSplines();
|
||||||
|
this.pointsStoreWidget.value = JSON.stringify(this.splines);
|
||||||
|
}
|
||||||
|
|
||||||
this.vis = new pv.Panel()
|
this.vis = new pv.Panel()
|
||||||
.width(this.width)
|
.width(this.width)
|
||||||
.height(this.height)
|
.height(this.height)
|
||||||
@ -378,7 +384,7 @@ this.heightWidget.callback = () => {
|
|||||||
x: this.mouse().x / app.canvas.ds.scale,
|
x: this.mouse().x / app.canvas.ds.scale,
|
||||||
y: this.mouse().y / app.canvas.ds.scale
|
y: this.mouse().y / app.canvas.ds.scale
|
||||||
};
|
};
|
||||||
i = self.points.push(scaledMouse) - 1;
|
i = self.splines[self.activeSplineIndex].points.push(scaledMouse) - 1;
|
||||||
self.updatePath();
|
self.updatePath();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -390,16 +396,17 @@ this.heightWidget.callback = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Find the two closest points to the clicked location
|
// Find the two closest points to the clicked location
|
||||||
let { point1Index, point2Index } = self.findClosestPoints(self.points, clickedPoint);
|
const activePoints = self.splines[self.activeSplineIndex].points;
|
||||||
|
let { point1Index, point2Index } = self.findClosestPoints(self.splines[self.activeSplineIndex].points, clickedPoint);
|
||||||
|
|
||||||
// Calculate the midpoint between the two closest points
|
// Calculate the midpoint between the two closest points
|
||||||
let midpoint = {
|
let midpoint = {
|
||||||
x: (self.points[point1Index].x + self.points[point2Index].x) / 2,
|
x: (activePoints[point1Index].x + activePoints[point2Index].x) / 2,
|
||||||
y: (self.points[point1Index].y + self.points[point2Index].y) / 2
|
y: (activePoints[point1Index].y + activePoints[point2Index].y) / 2
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert the midpoint into the array
|
// Insert the midpoint into the array
|
||||||
self.points.splice(point2Index, 0, midpoint);
|
activePoints.splice(point2Index, 0, midpoint);
|
||||||
i = point2Index;
|
i = point2Index;
|
||||||
self.updatePath();
|
self.updatePath();
|
||||||
}
|
}
|
||||||
@ -418,55 +425,73 @@ this.heightWidget.callback = () => {
|
|||||||
.lineWidth(3)
|
.lineWidth(3)
|
||||||
.visible(() => self.drawRuler)
|
.visible(() => self.drawRuler)
|
||||||
|
|
||||||
// vis.add(pv.Rule)
|
this.hoverSplineIndex = -1;
|
||||||
// .data(pv.range(0, points_to_sample, 1))
|
|
||||||
// .left(d => d * 512 / (points_to_sample - 1))
|
|
||||||
// .strokeStyle("gray")
|
|
||||||
// .lineWidth(2)
|
|
||||||
|
|
||||||
this.vis.add(pv.Line)
|
this.splines.forEach((spline, splineIndex) => {
|
||||||
.data(() => this.points)
|
this.vis.add(pv.Line)
|
||||||
.left(d => d.x)
|
.data(() => spline.points)
|
||||||
.top(d => d.y)
|
.left(d => d.x)
|
||||||
.interpolate(() => this.interpolation)
|
.top(d => d.y)
|
||||||
.tension(() => this.tension)
|
.interpolate(() => this.interpolation)
|
||||||
.segmented(() => false)
|
.tension(() => this.tension)
|
||||||
.strokeStyle(pv.Colors.category10().by(pv.index))
|
.segmented(() => false)
|
||||||
.lineWidth(3)
|
.strokeStyle(spline.color)
|
||||||
|
.lineWidth(() => {
|
||||||
|
// Change line width based on active or hover state
|
||||||
|
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.vis.add(pv.Dot)
|
this.vis.add(pv.Dot)
|
||||||
.data(() => this.points)
|
.data(() => this.splines[this.activeSplineIndex].points)
|
||||||
.left(d => d.x)
|
.left(d => d.x)
|
||||||
.top(d => d.y)
|
.top(d => d.y)
|
||||||
.radius(10)
|
.radius(10)
|
||||||
.shape(function() {
|
.shape(function() {
|
||||||
return self.dotShape;
|
return self.dotShape;
|
||||||
})
|
})
|
||||||
.angle(function() {
|
.angle(function() {
|
||||||
const index = this.index;
|
const index = this.index;
|
||||||
let angle = 0;
|
let angle = 0;
|
||||||
|
|
||||||
if (self.dotShape === "triangle") {
|
if (self.dotShape === "triangle") {
|
||||||
let dxNext = 0, dyNext = 0;
|
const activePoints = self.splines[self.activeSplineIndex].points;
|
||||||
if (index < self.points.length - 1) {
|
let dxNext = 0, dyNext = 0;
|
||||||
dxNext = self.points[index + 1].x - self.points[index].x;
|
if (index < activePoints.length - 1) {
|
||||||
dyNext = self.points[index + 1].y - self.points[index].y;
|
dxNext = activePoints[index + 1].x - activePoints[index].x;
|
||||||
|
dyNext = activePoints[index + 1].y - activePoints[index].y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dxPrev = 0, dyPrev = 0;
|
||||||
|
if (index > 0) {
|
||||||
|
dxPrev = activePoints[index].x - activePoints[index - 1].x;
|
||||||
|
dyPrev = activePoints[index].y - activePoints[index - 1].y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = (dxNext + dxPrev) / 2;
|
||||||
|
const dy = (dyNext + dyPrev) / 2;
|
||||||
|
|
||||||
|
angle = Math.atan2(dy, dx);
|
||||||
|
angle -= Math.PI / 2;
|
||||||
|
angle = (angle + 2 * Math.PI) % (2 * Math.PI);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dxPrev = 0, dyPrev = 0;
|
|
||||||
if (index > 0) {
|
|
||||||
dxPrev = self.points[index].x - self.points[index - 1].x;
|
|
||||||
dyPrev = self.points[index].y - self.points[index - 1].y;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dx = (dxNext + dxPrev) / 2;
|
|
||||||
const dy = (dyNext + dyPrev) / 2;
|
|
||||||
|
|
||||||
angle = Math.atan2(dy, dx);
|
|
||||||
angle -= Math.PI / 2;
|
|
||||||
angle = (angle + 2 * Math.PI) % (2 * Math.PI);
|
|
||||||
}
|
|
||||||
|
|
||||||
return angle;
|
return angle;
|
||||||
})
|
})
|
||||||
.cursor("move")
|
.cursor("move")
|
||||||
@ -477,10 +502,11 @@ this.heightWidget.callback = () => {
|
|||||||
i = this.index;
|
i = this.index;
|
||||||
hoverIndex = this.index;
|
hoverIndex = this.index;
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
if (pv.event.button === 2 && i !== 0 && i !== self.points.length - 1) {
|
const activePoints = self.splines[self.activeSplineIndex].points;
|
||||||
self.points.splice(i--, 1);
|
if (pv.event.button === 2 && i !== 0 && i !== activePoints.length - 1) {
|
||||||
self.vis.render();
|
activePoints.splice(i--, 1);
|
||||||
}
|
self.vis.render();
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
})
|
})
|
||||||
.event("dragend", function() {
|
.event("dragend", function() {
|
||||||
@ -499,7 +525,7 @@ this.heightWidget.callback = () => {
|
|||||||
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
|
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
|
||||||
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
|
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
|
||||||
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
|
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
|
||||||
self.points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
|
self.splines[self.activeSplineIndex].points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
|
||||||
self.vis.render(); // Re-render the visualization to reflect the new position
|
self.vis.render(); // Re-render the visualization to reflect the new position
|
||||||
})
|
})
|
||||||
.event("mouseover", function() {
|
.event("mouseover", function() {
|
||||||
@ -530,64 +556,92 @@ this.heightWidget.callback = () => {
|
|||||||
})
|
})
|
||||||
.textStyle("orange")
|
.textStyle("orange")
|
||||||
|
|
||||||
if (this.points.length != 0) {
|
if (this.splines.length != 0) {
|
||||||
this.vis.render();
|
this.vis.render();
|
||||||
}
|
|
||||||
var svgElement = this.vis.canvas();
|
|
||||||
svgElement.style['zIndex'] = "2"
|
|
||||||
svgElement.style['position'] = "relative"
|
|
||||||
this.node.splineEditor.element.appendChild(svgElement);
|
|
||||||
this.pathElements = svgElement.getElementsByTagName('path'); // Get all path elements
|
|
||||||
|
|
||||||
if (this.width > 256) {
|
|
||||||
this.node.setSize([this.width + 45, this.node.size[1]]);
|
|
||||||
}
|
|
||||||
this.node.setSize([this.node.size[0], this.height + 430]);
|
|
||||||
this.updatePath();
|
|
||||||
this.refreshBackgroundImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePath = () => {
|
|
||||||
if (!this.points || this.points.length === 0) {
|
|
||||||
console.log("no points");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.samplingMethod != "controlpoints") {
|
|
||||||
var coords = this.samplePoints(this.pathElements[0], this.points_to_sample, this.samplingMethod, this.width);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var coords = this.points
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.drawSamplePoints) {
|
|
||||||
if (this.pointsLayer) {
|
|
||||||
// Update the data of the existing points layer
|
|
||||||
this.pointsLayer.data(coords);
|
|
||||||
} else {
|
|
||||||
// Create the points layer if it doesn't exist
|
|
||||||
this.pointsLayer = this.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 {
|
|
||||||
if (this.pointsLayer) {
|
|
||||||
// Remove the points layer
|
|
||||||
this.pointsLayer.data([]);
|
|
||||||
this.vis.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let coordsString = JSON.stringify(coords);
|
|
||||||
this.pointsStoreWidget.value = JSON.stringify(this.points);
|
|
||||||
if (this.coordWidget) {
|
|
||||||
this.coordWidget.value = coordsString;
|
|
||||||
}
|
}
|
||||||
this.vis.render();
|
var svgElement = this.vis.canvas();
|
||||||
};
|
svgElement.style['zIndex'] = "2"
|
||||||
|
svgElement.style['position'] = "relative"
|
||||||
|
this.node.splineEditor.element.appendChild(svgElement);
|
||||||
|
this.pathElements = svgElement.getElementsByTagName('path'); // Get all path elements
|
||||||
|
|
||||||
|
if (this.width > 256) {
|
||||||
|
this.node.setSize([this.width + 45, this.node.size[1]]);
|
||||||
|
}
|
||||||
|
this.node.setSize([this.node.size[0], this.height + 430]);
|
||||||
|
this.updatePath();
|
||||||
|
this.refreshBackgroundImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePath = () => {
|
||||||
|
if (!this.splines || this.splines.length === 0) {
|
||||||
|
console.log("no splines");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Get active spline points
|
||||||
|
console.log("this.activeSplineIndex", this.activeSplineIndex);
|
||||||
|
const activeSpline = this.splines[this.activeSplineIndex];
|
||||||
|
const activePoints = activeSpline.points;
|
||||||
|
|
||||||
|
if (!activePoints || activePoints.length === 0) {
|
||||||
|
console.log("no points in active spline");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("this.pathElements", this.pathElements);
|
||||||
|
|
||||||
|
let coords;
|
||||||
|
if (this.samplingMethod != "controlpoints") {
|
||||||
|
coords = this.samplePoints(this.pathElements[this.activeSplineIndex], this.points_to_sample, this.samplingMethod, this.width, this.activeSplineIndex);
|
||||||
|
} else {
|
||||||
|
coords = activePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allSplineCoords = [];
|
||||||
|
for (let i = 0; i < this.splines.length; i++) {
|
||||||
|
// Use the same sampling method for all splines
|
||||||
|
let splineCoords;
|
||||||
|
const pathElement = this.pathElements[i];
|
||||||
|
|
||||||
|
if (this.samplingMethod != "controlpoints" && pathElement) {
|
||||||
|
splineCoords = this.samplePoints(pathElement, this.points_to_sample, this.samplingMethod, this.width, i);
|
||||||
|
} else {
|
||||||
|
// Fall back to control points if no path element or sampling method is "controlpoints"
|
||||||
|
splineCoords = this.splines[i].points;
|
||||||
|
}
|
||||||
|
|
||||||
|
allSplineCoords.push(splineCoords);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.drawSamplePoints) {
|
||||||
|
if (this.pointsLayer) {
|
||||||
|
// Update the data of the existing points layer
|
||||||
|
this.pointsLayer.data(coords);
|
||||||
|
} else {
|
||||||
|
// Create the points layer if it doesn't exist
|
||||||
|
this.pointsLayer = this.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 {
|
||||||
|
if (this.pointsLayer) {
|
||||||
|
// Remove the points layer
|
||||||
|
this.pointsLayer.data([]);
|
||||||
|
this.vis.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pointsStoreWidget.value = JSON.stringify(this.splines);
|
||||||
|
if (this.coordWidget) {
|
||||||
|
this.coordWidget.value = JSON.stringify(allSplineCoords);
|
||||||
|
}
|
||||||
|
this.vis.render();
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
@ -673,11 +727,143 @@ this.heightWidget.callback = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
refreshSplineElements = () => {
|
||||||
|
// Clear existing line elements and recreate them
|
||||||
|
const svgElement = this.vis.canvas();
|
||||||
|
|
||||||
|
// Remove all existing line elements
|
||||||
|
const oldLines = svgElement.querySelectorAll('path');
|
||||||
|
oldLines.forEach(line => line.remove());
|
||||||
|
|
||||||
|
this.pathElements = [];
|
||||||
|
this.lineObjects = [];
|
||||||
|
|
||||||
|
const originalChildren = [...this.vis.children];
|
||||||
|
|
||||||
|
// Find line objects to remove (those that represent splines)
|
||||||
|
const linesToRemove = originalChildren.filter(child =>
|
||||||
|
child instanceof pv.Line
|
||||||
|
);
|
||||||
|
linesToRemove.forEach(line => line.visible(false));
|
||||||
|
|
||||||
|
// Re-add all spline lines and store references to them
|
||||||
|
this.splines.forEach((spline, splineIndex) => {
|
||||||
|
const lineObj = 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(spline.color)
|
||||||
|
.lineWidth(() => {
|
||||||
|
// Change line width based on active or hover state
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.vis.render();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const allPaths = Array.from(svgElement.querySelectorAll('path'));
|
||||||
|
this.pathElements = [];
|
||||||
|
|
||||||
|
// First try: look at paths with specific childIndex values
|
||||||
|
this.lineObjects.forEach((lineObj, i) => {
|
||||||
|
// Find paths that correspond to our line objects
|
||||||
|
const childIndex = lineObj.childIndex;
|
||||||
|
const matchingPath = allPaths.find(path =>
|
||||||
|
path.$scene && path.$scene.scenes &&
|
||||||
|
path.$scene.scenes.childIndex === childIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingPath) {
|
||||||
|
console.log("matchingPath:", matchingPath);
|
||||||
|
this.pathElements[i] = matchingPath;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if we found all paths
|
||||||
|
if (this.pathElements.filter(p => p).length !== this.splines.length) {
|
||||||
|
// Fallback to color matching
|
||||||
|
this.pathElements = [];
|
||||||
|
for (let i = 0; i < this.splines.length; i++) {
|
||||||
|
const color = this.splines[i].color;
|
||||||
|
const matchingPath = allPaths.find(p =>
|
||||||
|
p.getAttribute('style')?.includes(color) &&
|
||||||
|
!this.pathElements.includes(p)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingPath) {
|
||||||
|
this.pathElements[i] = matchingPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still don't have the right number of paths, use the first N paths
|
||||||
|
if (this.pathElements.filter(p => p).length !== this.splines.length) {
|
||||||
|
this.pathElements = allPaths.slice(0, this.splines.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePath();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
initializeDefaultSplines() {
|
||||||
|
this.splines = [{
|
||||||
|
points: pv.range(1, 4).map((i, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return { x: 0, y: this.height };
|
||||||
|
} else if (index === 2) {
|
||||||
|
return { x: this.width, y: 0 };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
x: i * this.width / 5,
|
||||||
|
y: 50 + Math.random() * (this.height - 100)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
color: this.getSplineColor(0),
|
||||||
|
name: "Spline 1"
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSplineColor(index) {
|
||||||
|
const colors = [
|
||||||
|
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728",
|
||||||
|
"#9467bd", "#8c564b", "#e377c2", "#7f7f7f",
|
||||||
|
"#bcbd22", "#17becf"
|
||||||
|
];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
createContextMenu = () => {
|
createContextMenu = () => {
|
||||||
self = this;
|
const self = this;
|
||||||
|
const oldMenu = this.node.contextMenu;
|
||||||
|
const newMenu = oldMenu.cloneNode(true);
|
||||||
|
oldMenu.parentNode.replaceChild(newMenu, oldMenu);
|
||||||
|
this.node.contextMenu = newMenu;
|
||||||
|
|
||||||
document.addEventListener('contextmenu', function (e) {
|
document.addEventListener('contextmenu', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
@ -686,17 +872,18 @@ this.heightWidget.callback = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.node.menuItems.forEach((menuItem, index) => {
|
this.node.contextMenu.addEventListener('click', function(e) {
|
||||||
self = this;
|
e.preventDefault();
|
||||||
menuItem.addEventListener('click', function (e) {
|
if (e.target.tagName === 'A') {
|
||||||
e.preventDefault();
|
const id = parseInt(e.target.id.split('-')[2]);
|
||||||
switch (index) {
|
|
||||||
|
switch(id) {
|
||||||
case 0:
|
case 0:
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!self.drawHandles) {
|
if (!self.drawHandles) {
|
||||||
self.drawHandles = true
|
self.drawHandles = true
|
||||||
self.vis.add(pv.Line)
|
self.vis.add(pv.Line)
|
||||||
.data(() => self.points.map((point, index) => ({
|
.data(() => self.splines[self.activeSplineIndex].points.map((point, index) => ({
|
||||||
start: point,
|
start: point,
|
||||||
end: [index]
|
end: [index]
|
||||||
})))
|
})))
|
||||||
@ -715,12 +902,11 @@ this.heightWidget.callback = () => {
|
|||||||
self.node.contextMenu.style.display = 'none';
|
self.node.contextMenu.style.display = 'none';
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
e.preventDefault();
|
|
||||||
self.drawSamplePoints = !self.drawSamplePoints;
|
self.drawSamplePoints = !self.drawSamplePoints;
|
||||||
self.updatePath();
|
self.updatePath();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
e.preventDefault();
|
|
||||||
if (self.dotShape == "circle"){
|
if (self.dotShape == "circle"){
|
||||||
self.dotShape = "triangle"
|
self.dotShape = "triangle"
|
||||||
}
|
}
|
||||||
@ -753,8 +939,7 @@ this.heightWidget.callback = () => {
|
|||||||
self.node.contextMenu.style.display = 'none';
|
self.node.contextMenu.style.display = 'none';
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
e.preventDefault();
|
self.splines[self.activeSplineIndex].points.reverse();
|
||||||
self.points.reverse();
|
|
||||||
self.updatePath();
|
self.updatePath();
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
@ -762,34 +947,93 @@ this.heightWidget.callback = () => {
|
|||||||
self.node.properties.imgData = null;
|
self.node.properties.imgData = null;
|
||||||
self.node.contextMenu.style.display = 'none';
|
self.node.contextMenu.style.display = 'none';
|
||||||
break;
|
break;
|
||||||
|
case 6: // Add new spline
|
||||||
|
const newSplineIndex = self.splines.length;
|
||||||
|
self.splines.push({
|
||||||
|
points: [
|
||||||
|
// Create default points for the new spline
|
||||||
|
{ x: 0, y: self.height },
|
||||||
|
{ x: self.width/2, y: self.height/2 },
|
||||||
|
{ x: self.width, y: 0 }
|
||||||
|
],
|
||||||
|
color: self.getSplineColor(newSplineIndex),
|
||||||
|
name: `Spline ${newSplineIndex + 1}`
|
||||||
|
});
|
||||||
|
self.activeSplineIndex = newSplineIndex;
|
||||||
|
self.refreshSplineElements();
|
||||||
|
self.node.contextMenu.style.display = 'none';
|
||||||
|
break;
|
||||||
|
case 7: // Delete current spline
|
||||||
|
if (self.splines.length > 1) {
|
||||||
|
self.splines.splice(self.activeSplineIndex, 1);
|
||||||
|
self.activeSplineIndex = Math.min(self.activeSplineIndex, self.splines.length - 1);
|
||||||
|
self.refreshSplineElements();
|
||||||
|
}
|
||||||
|
self.node.contextMenu.style.display = 'none';
|
||||||
|
break;
|
||||||
|
case 8: // Next spline
|
||||||
|
self.activeSplineIndex = (self.activeSplineIndex + 1) % self.splines.length;
|
||||||
|
self.refreshSplineElements();
|
||||||
|
self.node.contextMenu.style.display = 'none';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
samplePoints(svgPathElement, numSamples, samplingMethod, width) {
|
samplePoints(svgPathElement, numSamples, samplingMethod, width, splineIndex) {
|
||||||
var svgWidth = width; // Fixed width of the SVG element
|
if (!svgPathElement) {
|
||||||
var pathLength = svgPathElement.getTotalLength();
|
console.warn(`Path element not found for spline index: ${splineIndex}. Available paths: ${this.pathElements.length}`);
|
||||||
var points = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < numSamples; i++) {
|
|
||||||
if (samplingMethod === "time") {
|
const splinePoints = this.splines[splineIndex].points;
|
||||||
// Calculate the x-coordinate for the current sample based on the SVG's width
|
|
||||||
var x = (svgWidth / (numSamples - 1)) * i;
|
// If we have no points, return an empty array
|
||||||
// Find the point on the path that intersects the vertical line at the calculated x-coordinate
|
if (!splinePoints || splinePoints.length === 0) {
|
||||||
var point = this.findPointAtX(svgPathElement, x, pathLength);
|
return [];
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a simple interpolation between control points
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < numSamples; i++) {
|
||||||
|
const t = i / (numSamples - 1);
|
||||||
|
const idx = Math.min(
|
||||||
|
Math.floor(t * (splinePoints.length - 1)),
|
||||||
|
splinePoints.length - 2
|
||||||
|
);
|
||||||
|
const fraction = (t * (splinePoints.length - 1)) - idx;
|
||||||
|
|
||||||
|
const x = splinePoints[idx].x + fraction * (splinePoints[idx + 1].x - splinePoints[idx].x);
|
||||||
|
const y = splinePoints[idx].y + fraction * (splinePoints[idx + 1].y - splinePoints[idx].y);
|
||||||
|
|
||||||
|
result.push({ x, y });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var svgWidth = width; // Fixed width of the SVG element
|
||||||
|
var pathLength = svgPathElement.getTotalLength();
|
||||||
|
console.log(" pathLength:", pathLength);
|
||||||
|
var points = [];
|
||||||
|
|
||||||
// Add the point to the array of points
|
for (var i = 0; i < numSamples; i++) {
|
||||||
points.push({ x: point.x, y: point.y });
|
if (samplingMethod === "time") {
|
||||||
}
|
// Calculate the x-coordinate for the current sample based on the SVG's width
|
||||||
return points;
|
var x = (svgWidth / (numSamples - 1)) * i;
|
||||||
|
// Find the point on the path that intersects the vertical line at the calculated x-coordinate
|
||||||
|
var point = this.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 });
|
||||||
|
}
|
||||||
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
findClosestPoints(points, clickedPoint) {
|
findClosestPoints(points, clickedPoint) {
|
||||||
@ -846,16 +1090,7 @@ export function hideWidgetForGood(node, widget, suffix = '') {
|
|||||||
widget.origSerializeValue = widget.serializeValue
|
widget.origSerializeValue = widget.serializeValue
|
||||||
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
|
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
|
||||||
widget.type = "converted-widget" + suffix
|
widget.type = "converted-widget" + suffix
|
||||||
// widget.serializeValue = () => {
|
|
||||||
// // Prevent serializing the widget if we have no input linked
|
|
||||||
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
|
|
||||||
// if (w?.link == null) {
|
|
||||||
// return undefined;
|
|
||||||
// }
|
|
||||||
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Hide any linked widgets, e.g. seed+seedControl
|
|
||||||
if (widget.linkedWidgets) {
|
if (widget.linkedWidgets) {
|
||||||
for (const w of widget.linkedWidgets) {
|
for (const w of widget.linkedWidgets) {
|
||||||
hideWidgetForGood(node, w, ':' + widget.name)
|
hideWidgetForGood(node, w, ':' + widget.name)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user