SplineEditor rework

This commit is contained in:
kijai 2024-10-22 00:33:55 +03:00
parent bbb28a0fc9
commit 971bf375e5
2 changed files with 468 additions and 309 deletions

View File

@ -5,6 +5,9 @@ from PIL import Image, ImageDraw, ImageFont, ImageColor, ImageFilter
import numpy as np import numpy as np
from ..utility.utility import pil2tensor from ..utility.utility import pil2tensor
import folder_paths import folder_paths
import io
import base64
from comfy.utils import common_upscale from comfy.utils import common_upscale
def plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, prompt): def plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, prompt):
@ -150,6 +153,7 @@ class SplineEditor:
"optional": { "optional": {
"min_value": ("FLOAT", {"default": 0.0, "min": -10000.0, "max": 10000.0, "step": 0.01}), "min_value": ("FLOAT", {"default": 0.0, "min": -10000.0, "max": 10000.0, "step": 0.01}),
"max_value": ("FLOAT", {"default": 1.0, "min": -10000.0, "max": 10000.0, "step": 0.01}), "max_value": ("FLOAT", {"default": 1.0, "min": -10000.0, "max": 10000.0, "step": 0.01}),
"bg_image": ("IMAGE", ),
} }
} }
@ -196,7 +200,8 @@ 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, 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, bg_image=None):
coordinates = json.loads(coordinates) coordinates = json.loads(coordinates)
normalized = [] normalized = []
@ -226,7 +231,22 @@ output types:
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:
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(normalized))
else:
transform = transforms.ToPILImage()
image = transform(bg_image[0].permute(2, 0, 1))
buffered = io.BytesIO()
image.save(buffered, format="JPEG", quality=75)
# Step 3: Encode the image bytes to a Base64 string
img_bytes = buffered.getvalue()
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
return {
"ui": {"bg_image": [img_base64]},
"result":(masks_out, json.dumps(coordinates), out_floats, len(out_floats) , json.dumps(normalized))
}
class CreateShapeMaskOnPath: class CreateShapeMaskOnPath:
@ -335,8 +355,8 @@ Locations are center locations.
"coordinates": ("STRING", {"forceInput": True}), "coordinates": ("STRING", {"forceInput": True}),
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"shape_width": ("INT", {"default": 128,"min": 8, "max": 4096, "step": 1}), "shape_width": ("INT", {"default": 128,"min": 2, "max": 4096, "step": 1}),
"shape_height": ("INT", {"default": 128,"min": 8, "max": 4096, "step": 1}), "shape_height": ("INT", {"default": 128,"min": 2, "max": 4096, "step": 1}),
"shape_color": ("STRING", {"default": 'white'}), "shape_color": ("STRING", {"default": 'white'}),
"bg_color": ("STRING", {"default": 'black'}), "bg_color": ("STRING", {"default": 'black'}),
"blur_radius": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100, "step": 0.1}), "blur_radius": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100, "step": 0.1}),
@ -1308,9 +1328,6 @@ you can clear the image from the context menu by right clicking on the canvas
""" """
def pointdata(self, points_store, bbox_store, width, height, coordinates, neg_coordinates, normalize, bboxes, bbox_format="xyxy", bg_image=None): def pointdata(self, points_store, bbox_store, width, height, coordinates, neg_coordinates, normalize, bboxes, bbox_format="xyxy", bg_image=None):
import io
import base64
coordinates = json.loads(coordinates) coordinates = json.loads(coordinates)
pos_coordinates = [] pos_coordinates = []
for coord in coordinates: for coord in coordinates:

View File

@ -110,6 +110,10 @@ app.registerExtension({
this.uuid = makeUUID() this.uuid = makeUUID()
element.id = `spline-editor-${this.uuid}` element.id = `spline-editor-${this.uuid}`
// fake image widget to allow copy/paste
const fakeimagewidget = this.addWidget("COMBO", "image", null, () => { }, {});
hideWidgetForGood(this, fakeimagewidget)
this.splineEditor = this.addDOMWidget(nodeData.name, "SplineEditorWidget", element, { this.splineEditor = this.addDOMWidget(nodeData.name, "SplineEditorWidget", element, {
serialize: false, serialize: false,
hideOnZoom: false, hideOnZoom: false,
@ -169,15 +173,15 @@ app.registerExtension({
this.contextMenu.appendChild(menuItem); this.contextMenu.appendChild(menuItem);
}); });
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) this.editor = new SplineEditor(this);
this.addProperty("points", this.constructor.type, "string"); this.addProperty("points", this.constructor.type, "string");
} }
else { else {
createSplineEditor(this, true) this.editor = new SplineEditor(this, true);
} }
}); });
@ -188,8 +192,20 @@ app.registerExtension({
this.splineEditor.parentEl.id = `spline-editor-${this.uuid}` this.splineEditor.parentEl.id = `spline-editor-${this.uuid}`
element.appendChild(this.splineEditor.parentEl); element.appendChild(this.splineEditor.parentEl);
chainCallback(this, "onConfigure", function() { chainCallback(this, "onConfigure", function () {
createSplineEditor(this); try {
this.editor = new SplineEditor(this);
} catch (error) {
console.error("An error occurred while configuring the editor:", error);
}
});
chainCallback(this, "onExecuted", function (message) {
let bg_image = message["bg_image"];
this.properties.imgData = {
name: "bg_image",
base64: bg_image
};
this.editor.refreshBackgroundImage(this);
}); });
}); // onAfterGraphConfigured }); // onAfterGraphConfigured
@ -198,259 +214,159 @@ app.registerExtension({
})//register })//register
function createSplineEditor(context, reset=false) { class SplineEditor{
constructor(context, reset = false) {
this.node = context;
this.reset=reset;
const self = this;
console.log("creatingSplineEditor") console.log("creatingSplineEditor")
this.node.pasteFile = (file) => {
if (file.type.startsWith("image/")) {
this.handleImageFile(file);
return true;
}
return false;
};
this.node.onDragOver = function (e) {
if (e.dataTransfer && e.dataTransfer.items) {
return [...e.dataTransfer.items].some(f => f.kind === "file" && f.type.startsWith("image/"));
}
return false;
};
// On drop upload files
this.node.onDragDrop = (e) => {
console.log("onDragDrop called");
let handled = false;
for (const file of e.dataTransfer.files) {
if (file.type.startsWith("image/")) {
this.handleImageFile(file);
handled = true;
}
}
return handled;
};
// context menu // context menu
function createContextMenu() { this.createContextMenu();
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
});
document.addEventListener('click', function(e) {
if (!context.contextMenu.contains(e.target)) {
context.contextMenu.style.display = 'none';
}
});
context.menuItems.forEach((menuItem, index) => { this.dotShape = "circle";
menuItem.addEventListener('click', function(e) { this.drawSamplePoints = false;
e.preventDefault();
// Logic specific to each menu item based on its index or id
switch (index) {
case 0:
e.preventDefault();
if (!drawHandles) {
drawHandles = true
vis.add(pv.Line)
.data(() => points.map((point, index) => ({
start: point,
end: [index]
})))
.left(d => d.start.x)
.top(d => d.start.y)
.interpolate("linear")
.tension(0) // Straight lines
.strokeStyle("#ff7f0e") // Same color as control points
.lineWidth(1)
.visible(() => drawHandles);
vis.render();
} else {
drawHandles = false
vis.render();
}
context.contextMenu.style.display = 'none';
break;
case 1:
e.preventDefault();
drawSamplePoints = !drawSamplePoints;
updatePath();
break;
case 2:
e.preventDefault();
if (dotShape == "circle"){
dotShape = "triangle"
}
else {
dotShape = "circle"
}
console.log(dotShape)
updatePath();
break;
case 3:
// Create file input element
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*'; // Accept only image files
// Listen for file selection
fileInput.addEventListener('change', function(event) {
const file = event.target.files[0]; // Get the selected file
if (file) {
// Create a URL for the selected file
const imageUrl = URL.createObjectURL(file);
// Set the backgroundImage with the new URL and make it visible
backgroundImage
.url(imageUrl)
.visible(true)
.root.render();
}
});
// If the backgroundImage is already visible, hide it. Otherwise, show file input.
if (backgroundImage.visible()) {
backgroundImage.visible(false)
.root.render();
} else {
// Trigger the file input dialog
fileInput.click();
}
context.contextMenu.style.display = 'none';
break;
case 4:
e.preventDefault();
points.reverse();
updatePath();
}
});
});
}
var dotShape = "circle";
var drawSamplePoints = false;
createContextMenu();
function updatePath() {
if (samplingMethod != "controlpoints") {
var coords = samplePoints(pathElements[0], points_to_sample, samplingMethod, w);
}
else {
var coords = points
}
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
}
} else {
if (pointsLayer) {
// 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();
}
if (reset && context.splineEditor.element) { if (reset && context.splineEditor.element) {
context.splineEditor.element.innerHTML = ''; // Clear the container context.splineEditor.element.innerHTML = ''; // Clear the container
} }
const coordWidget = context.widgets.find(w => w.name === "coordinates"); this.coordWidget = context.widgets.find(w => w.name === "coordinates");
const interpolationWidget = context.widgets.find(w => w.name === "interpolation"); this.interpolationWidget = context.widgets.find(w => w.name === "interpolation");
const pointsWidget = context.widgets.find(w => w.name === "points_to_sample"); this.pointsWidget = context.widgets.find(w => w.name === "points_to_sample");
const pointsStoreWidget = context.widgets.find(w => w.name === "points_store"); this.pointsStoreWidget = context.widgets.find(w => w.name === "points_store");
const tensionWidget = context.widgets.find(w => w.name === "tension"); this.tensionWidget = context.widgets.find(w => w.name === "tension");
const minValueWidget = context.widgets.find(w => w.name === "min_value"); this.minValueWidget = context.widgets.find(w => w.name === "min_value");
const maxValueWidget = context.widgets.find(w => w.name === "max_value"); this.maxValueWidget = context.widgets.find(w => w.name === "max_value");
const samplingMethodWidget = context.widgets.find(w => w.name === "sampling_method"); this.samplingMethodWidget = context.widgets.find(w => w.name === "sampling_method");
const widthWidget = context.widgets.find(w => w.name === "mask_width"); this.widthWidget = context.widgets.find(w => w.name === "mask_width");
const heightWidget = context.widgets.find(w => w.name === "mask_height"); this.heightWidget = context.widgets.find(w => w.name === "mask_height");
//const segmentedWidget = context.widgets.find(w => w.name === "segmented");
var interpolation = interpolationWidget.value var interpolation = this.interpolationWidget.value
var tension = tensionWidget.value var tension = this.tensionWidget.value
var points_to_sample = pointsWidget.value this.points_to_sample = this.pointsWidget.value
var rangeMin = minValueWidget.value var rangeMin = this.minValueWidget.value
var rangeMax = maxValueWidget.value var rangeMax = this.maxValueWidget.value
var pointsLayer = null; this.pointsLayer = null;
var samplingMethod = samplingMethodWidget.value this.samplingMethod = this.samplingMethodWidget.value
if (samplingMethod == "path") { if (this.samplingMethod == "path") {
dotShape = "triangle" this.dotShape = "triangle"
} }
interpolationWidget.callback = () => { this.interpolationWidget.callback = () => {
interpolation = interpolationWidget.value interpolation = this.interpolationWidget.value
updatePath(); this.updatePath();
} }
samplingMethodWidget.callback = () => { this.samplingMethodWidget.callback = () => {
samplingMethod = samplingMethodWidget.value this.samplingMethod = samplingMethodWidget.value
if (samplingMethod == "path") { if (this.samplingMethod == "path") {
dotShape = "triangle" this.dotShape = "triangle"
} }
else if (samplingMethod == "controlpoints") { else if (this.samplingMethod == "controlpoints") {
dotShape = "circle" this.dotShape = "circle"
drawSamplePoints = true; this.drawSamplePoints = true;
} }
updatePath(); this.updatePath();
} }
tensionWidget.callback = () => { this.tensionWidget.callback = () => {
tension = tensionWidget.value tension = this.tensionWidget.value
updatePath(); this.updatePath();
} }
pointsWidget.callback = () => { this.pointsWidget.callback = () => {
points_to_sample = pointsWidget.value this.points_to_sample = this.pointsWidget.value
updatePath(); this.updatePath();
} }
minValueWidget.callback = () => { this.minValueWidget.callback = () => {
rangeMin = minValueWidget.value rangeMin = this.minValueWidget.value
updatePath(); this.updatePath();
} }
maxValueWidget.callback = () => { this.maxValueWidget.callback = () => {
rangeMax = maxValueWidget.value rangeMax = this.maxValueWidget.value
updatePath(); this.updatePath();
} }
widthWidget.callback = () => { this.widthWidget.callback = () => {
w = widthWidget.value; w = this.widthWidget.value;
if (w > 256) { if (w > 256) {
context.setSize([w + 45, context.size[1]]); context.setSize([w + 45, context.size[1]]);
} }
vis.width(w); vis.width(w);
updatePath(); this.updatePath();
} }
heightWidget.callback = () => { this.heightWidget.callback = () => {
h = heightWidget.value this.height = this.heightWidget.value
vis.height(h) vis.height(this.height)
context.setSize([context.size[0], h + 430]); context.setSize([context.size[0], this.height + 430]);
updatePath(); this.updatePath();
} }
pointsStoreWidget.callback = () => { this.pointsStoreWidget.callback = () => {
points = JSON.parse(pointsStoreWidget.value); points = JSON.parse(this.pointsStoreWidget.value);
updatePath(); this.updatePath();
} }
// Initialize or reset points array // Initialize or reset points array
var drawHandles = false; this.drawHandles = false;
this.drawRuler = true;
var hoverIndex = -1; var hoverIndex = -1;
var isDragging = false; var isDragging = false;
var w = widthWidget.value; this.width = this.widthWidget.value;
var h = heightWidget.value; this.height = this.heightWidget.value;
var i = 3; var i = 3;
let points = []; this.points = [];
if (!reset && pointsStoreWidget.value != "") { if (!reset && this.pointsStoreWidget.value != "") {
points = JSON.parse(pointsStoreWidget.value); this.points = JSON.parse(this.pointsStoreWidget.value);
} else { } else {
points = pv.range(1, 4).map((i, index) => { this.points = pv.range(1, 4).map((i, index) => {
if (index === 0) { if (index === 0) {
// First point at the bottom-left corner // First point at the bottom-left corner
return { x: 0, y: h }; return { x: 0, y: this.height };
} else if (index === 2) { } else if (index === 2) {
// Last point at the top-right corner // Last point at the top-right corner
return { x: w, y: 0 }; return { x: this.width, y: 0 };
} else { } else {
// Other points remain as they were // Other points remain as they were
return { return {
x: i * w / 5, x: i * this.width / 5,
y: 50 + Math.random() * (h - 100) y: 50 + Math.random() * (this.height - 100)
}; };
} }
}); });
pointsStoreWidget.value = JSON.stringify(points); this.pointsStoreWidget.value = JSON.stringify(this.points);
} }
var vis = new pv.Panel() this.vis = new pv.Panel()
.width(w) .width(this.width)
.height(h) .height(this.height)
.fillStyle("#222") .fillStyle("#222")
.strokeStyle("gray") .strokeStyle("gray")
.lineWidth(2) .lineWidth(2)
@ -462,8 +378,8 @@ function createSplineEditor(context, reset=false) {
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 = points.push(scaledMouse) - 1; i = self.points.push(scaledMouse) - 1;
updatePath(); self.updatePath();
return this; return this;
} }
else if (pv.event.ctrlKey) { else if (pv.event.ctrlKey) {
@ -474,32 +390,33 @@ function createSplineEditor(context, reset=false) {
}; };
// Find the two closest points to the clicked location // Find the two closest points to the clicked location
let { point1Index, point2Index } = findClosestPoints(points, clickedPoint); let { point1Index, point2Index } = self.findClosestPoints(self.points, clickedPoint);
// Calculate the midpoint between the two closest points // Calculate the midpoint between the two closest points
let midpoint = { let midpoint = {
x: (points[point1Index].x + points[point2Index].x) / 2, x: (self.points[point1Index].x + self.points[point2Index].x) / 2,
y: (points[point1Index].y + points[point2Index].y) / 2 y: (self.points[point1Index].y + self.points[point2Index].y) / 2
}; };
// Insert the midpoint into the array // Insert the midpoint into the array
points.splice(point2Index, 0, midpoint); self.points.splice(point2Index, 0, midpoint);
i = point2Index; i = point2Index;
updatePath(); self.updatePath();
} }
else if (pv.event.button === 2) { else if (pv.event.button === 2) {
context.contextMenu.style.display = 'block'; self.node.contextMenu.style.display = 'block';
context.contextMenu.style.left = `${pv.event.clientX}px`; self.node.contextMenu.style.left = `${pv.event.clientX}px`;
context.contextMenu.style.top = `${pv.event.clientY}px`; self.node.contextMenu.style.top = `${pv.event.clientY}px`;
} }
}) })
var backgroundImage = vis.add(pv.Image) this.backgroundImage = this.vis.add(pv.Image).visible(false)
.visible(false)
vis.add(pv.Rule) this.vis.add(pv.Rule)
.data(pv.range(0, h, 64)) .data(pv.range(0, this.height, 64))
.bottom(d => d) .bottom(d => d)
.strokeStyle("gray") .strokeStyle("gray")
.lineWidth(3) .lineWidth(3)
.visible(() => self.drawRuler)
// vis.add(pv.Rule) // vis.add(pv.Rule)
// .data(pv.range(0, points_to_sample, 1)) // .data(pv.range(0, points_to_sample, 1))
@ -507,8 +424,8 @@ function createSplineEditor(context, reset=false) {
// .strokeStyle("gray") // .strokeStyle("gray")
// .lineWidth(2) // .lineWidth(2)
vis.add(pv.Line) this.vis.add(pv.Line)
.data(() => points) .data(() => this.points)
.left(d => d.x) .left(d => d.x)
.top(d => d.y) .top(d => d.y)
.interpolate(() => interpolation) .interpolate(() => interpolation)
@ -517,19 +434,19 @@ function createSplineEditor(context, reset=false) {
.strokeStyle(pv.Colors.category10().by(pv.index)) .strokeStyle(pv.Colors.category10().by(pv.index))
.lineWidth(3) .lineWidth(3)
vis.add(pv.Dot) this.vis.add(pv.Dot)
.data(() => points) .data(() => this.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 dotShape; return self.dotShape;
}) })
.angle(function() { .angle(function() {
const index = this.index; const index = this.index;
let angle = 0; let angle = 0;
if (dotShape === "triangle") { if (this.dotShape === "triangle") {
let dxNext = 0, dyNext = 0; let dxNext = 0, dyNext = 0;
if (index < points.length - 1) { if (index < points.length - 1) {
dxNext = points[index + 1].x - points[index].x; dxNext = points[index + 1].x - points[index].x;
@ -560,15 +477,15 @@ function createSplineEditor(context, reset=false) {
i = this.index; i = this.index;
hoverIndex = this.index; hoverIndex = this.index;
isDragging = true; isDragging = true;
if (pv.event.button === 2 && i !== 0 && i !== points.length - 1) { if (pv.event.button === 2 && i !== 0 && i !== self.points.length - 1) {
points.splice(i--, 1); self.points.splice(i--, 1);
vis.render(); self.vis.render();
} }
return this; return this;
}) })
.event("dragend", function() { .event("dragend", function() {
if (this.pathElements !== null) { if (this.pathElements !== null) {
updatePath(); self.updatePath();
} }
isDragging = false; isDragging = false;
}) })
@ -576,58 +493,283 @@ function createSplineEditor(context, reset=false) {
let adjustedX = this.mouse().x / app.canvas.ds.scale; // Adjust the new X position by the inverse of the scale factor let adjustedX = this.mouse().x / app.canvas.ds.scale; // Adjust the new X position by the inverse of the scale factor
let adjustedY = this.mouse().y / app.canvas.ds.scale; // Adjust the new Y position by the inverse of the scale factor let adjustedY = this.mouse().y / app.canvas.ds.scale; // Adjust the new Y position by the inverse of the scale factor
// Determine the bounds of the vis.Panel // Determine the bounds of the vis.Panel
const panelWidth = vis.width(); const panelWidth = self.vis.width();
const panelHeight = vis.height(); const panelHeight = self.vis.height();
// 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));
points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position self.points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
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() {
hoverIndex = this.index; // Set the hover index to the index of the hovered dot hoverIndex = this.index; // Set the hover index to the index of the hovered dot
vis.render(); // Re-render the visualization self.vis.render(); // Re-render the visualization
}) })
.event("mouseout", function() { .event("mouseout", function() {
!isDragging && (hoverIndex = -1); // Reset the hover index when the mouse leaves the dot !isDragging && (hoverIndex = -1); // Reset the hover index when the mouse leaves the dot
vis.render(); // Re-render the visualization self.vis.render(); // Re-render the visualization
}) })
.anchor("center") .anchor("center")
.add(pv.Label) .add(pv.Label)
.visible(function() { .visible(function() {
return hoverIndex === this.index; // Only show the label for the hovered dot return hoverIndex === this.index; // Only show the label for the hovered dot
}) })
.left(d => d.x < w / 2 ? d.x + 80 : d.x - 70) // Shift label to right if on left half, otherwise shift to left .left(d => d.x < this.width / 2 ? d.x + 80 : d.x - 70) // Shift label to right if on left half, otherwise shift to left
.top(d => d.y < h / 2 ? d.y + 20 : d.y - 20) // Shift label down if on top half, otherwise shift up .top(d => d.y < this.height / 2 ? d.y + 20 : d.y - 20) // Shift label down if on top half, otherwise shift up
.font(12 + "px sans-serif") .font(12 + "px sans-serif")
.text(d => { .text(d => {
if (samplingMethod == "path") { if (this.samplingMethod == "path") {
return `X: ${Math.round(d.x)}, Y: ${Math.round(d.y)}`; return `X: ${Math.round(d.x)}, Y: ${Math.round(d.y)}`;
} else { } else {
let frame = Math.round((d.x / w) * points_to_sample); let frame = Math.round((d.x / self.width) * this.points_to_sample);
let normalizedY = (1.0 - (d.y / h) - 0.0) * (rangeMax - rangeMin) + rangeMin; let normalizedY = (1.0 - (d.y / self.height) - 0.0) * (rangeMax - rangeMin) + rangeMin;
let normalizedX = (d.x / w); let normalizedX = (d.x / self.width);
return `F: ${frame}, X: ${normalizedX.toFixed(2)}, Y: ${normalizedY.toFixed(2)}`; return `F: ${frame}, X: ${normalizedX.toFixed(2)}, Y: ${normalizedY.toFixed(2)}`;
} }
}) })
.textStyle("orange") .textStyle("orange")
vis.render(); if (this.points.length != 0) {
var svgElement = vis.canvas(); this.vis.render();
}
var svgElement = this.vis.canvas();
svgElement.style['zIndex'] = "2" svgElement.style['zIndex'] = "2"
svgElement.style['position'] = "relative" svgElement.style['position'] = "relative"
context.splineEditor.element.appendChild(svgElement); context.splineEditor.element.appendChild(svgElement);
var pathElements = svgElement.getElementsByTagName('path'); // Get all path elements this.pathElements = svgElement.getElementsByTagName('path'); // Get all path elements
if (w > 256) { if (this.width > 256) {
context.setSize([w + 45, context.size[1]]); this.node.setSize([this.width + 45, context.size[1]]);
} }
context.setSize([context.size[0], h + 430]); this.node.setSize([this.node.size[0], this.height + 430]);
updatePath(); this.updatePath();
} }
function samplePoints(svgPathElement, numSamples, samplingMethod, width) { 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();
};
handleImageLoad = (img, file, base64String) => {
console.log(img.width, img.height); // Access width and height here
this.widthWidget.value = img.width;
this.heightWidget.value = img.height;
this.drawRuler = false;
if (img.width != this.vis.width() || img.height != this.vis.height()) {
if (img.width > 256) {
this.node.setSize([img.width + 45, this.node.size[1]]);
}
this.node.setSize([this.node.size[0], img.height + 300]);
this.vis.width(img.width);
this.vis.height(img.height);
this.height = img.height;
this.width = img.width;
this.updatePath();
}
this.backgroundImage.url(file ? URL.createObjectURL(file) : `data:${this.node.properties.imgData.type};base64,${base64String}`).visible(true).root.render();
};
processImage = (img, file) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const maxWidth = 800; // maximum width
const maxHeight = 600; // maximum height
let width = img.width;
let height = img.height;
// Calculate the new dimensions while preserving the aspect ratio
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// Get the compressed image data as a Base64 string
const base64String = canvas.toDataURL('image/jpeg', 0.5).replace('data:', '').replace(/^.+,/, ''); // 0.5 is the quality from 0 to 1
this.node.properties.imgData = {
name: file.name,
lastModified: file.lastModified,
size: file.size,
type: file.type,
base64: base64String
};
handleImageLoad(img, file, base64String);
};
handleImageFile = (file) => {
const reader = new FileReader();
reader.onloadend = () => {
const img = new Image();
img.src = reader.result;
img.onload = () => processImage(img, file);
};
reader.readAsDataURL(file);
const imageUrl = URL.createObjectURL(file);
const img = new Image();
img.src = imageUrl;
img.onload = () => this.handleImageLoad(img, file, null);
};
refreshBackgroundImage = () => {
if (this.node.properties.imgData && this.node.properties.imgData.base64) {
const base64String = this.node.properties.imgData.base64;
const imageUrl = `data:${this.node.properties.imgData.type};base64,${base64String}`;
const img = new Image();
img.src = imageUrl;
img.onload = () => this.handleImageLoad(img, null, base64String);
}
};
createContextMenu() {
self = this;
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
});
document.addEventListener('click', function(e) {
if (!self.node.contextMenu.contains(e.target)) {
self.node.contextMenu.style.display = 'none';
}
});
this.node.menuItems.forEach((menuItem, index) => {
self = this;
menuItem.addEventListener('click', function(e) {
e.preventDefault();
// Logic specific to each menu item based on its index or id
switch (index) {
case 0:
e.preventDefault();
if (!self.drawHandles) {
self.drawHandles = true
self.vis.add(pv.Line)
.data(() => self.points.map((point, index) => ({
start: point,
end: [index]
})))
.left(d => d.start.x)
.top(d => d.start.y)
.interpolate("linear")
.tension(0) // Straight lines
.strokeStyle("#ff7f0e") // Same color as control points
.lineWidth(1)
.visible(() => self.drawHandles);
self.vis.render();
} else {
self.drawHandles = false
self.vis.render();
}
self.node.contextMenu.style.display = 'none';
break;
case 1:
e.preventDefault();
self.drawSamplePoints = !self.drawSamplePoints;
self.updatePath();
break;
case 2:
e.preventDefault();
if (self.dotShape == "circle"){
self.dotShape = "triangle"
}
else {
self.dotShape = "circle"
}
console.log(self.dotShape)
self.updatePath();
break;
case 3:
// Create file input element
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*'; // Accept only image files
// Listen for file selection
fileInput.addEventListener('change', function(event) {
const file = event.target.files[0]; // Get the selected file
if (file) {
// Create a URL for the selected file
const imageUrl = URL.createObjectURL(file);
// Set the backgroundImage with the new URL and make it visible
self.backgroundImage
.url(imageUrl)
.visible(true)
.root.render();
}
});
// If the backgroundImage is already visible, hide it. Otherwise, show file input.
if (self.backgroundImage.visible()) {
self.backgroundImage.visible(false)
.root.render();
} else {
// Trigger the file input dialog
fileInput.click();
}
self.node.contextMenu.style.display = 'none';
break;
case 4:
e.preventDefault();
self.points.reverse();
self.updatePath();
}
});
});
}
samplePoints(svgPathElement, numSamples, samplingMethod, width) {
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();
var points = []; var points = [];
@ -637,7 +779,7 @@ function samplePoints(svgPathElement, numSamples, samplingMethod, width) {
// Calculate the x-coordinate for the current sample based on the SVG's width // Calculate the x-coordinate for the current sample based on the SVG's width
var x = (svgWidth / (numSamples - 1)) * i; 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 = this.findPointAtX(svgPathElement, x, pathLength);
} }
else if (samplingMethod === "path") { else if (samplingMethod === "path") {
// Calculate the distance along the path for the current sample // Calculate the distance along the path for the current sample
@ -650,9 +792,9 @@ function samplePoints(svgPathElement, numSamples, samplingMethod, width) {
points.push({ x: point.x, y: point.y }); points.push({ x: point.x, y: point.y });
} }
return points; return points;
} }
function findClosestPoints(points, clickedPoint) { findClosestPoints(points, clickedPoint) {
// Calculate distances from clickedPoint to each point in the array // Calculate distances from clickedPoint to each point in the array
let distances = points.map(point => { let distances = points.map(point => {
let dx = clickedPoint.x - point.x; let dx = clickedPoint.x - point.x;
@ -668,9 +810,9 @@ function findClosestPoints(points, clickedPoint) {
[closestPoint1Index, closestPoint2Index] = [closestPoint2Index, closestPoint1Index]; [closestPoint1Index, closestPoint2Index] = [closestPoint2Index, closestPoint1Index];
} }
return { point1Index: closestPoint1Index, point2Index: closestPoint2Index }; return { point1Index: closestPoint1Index, point2Index: closestPoint2Index };
} }
function findPointAtX(svgPathElement, targetX, pathLength) { findPointAtX(svgPathElement, targetX, pathLength) {
let low = 0; let low = 0;
let high = pathLength; let high = pathLength;
let bestPoint = svgPathElement.getPointAtLength(0); let bestPoint = svgPathElement.getPointAtLength(0);
@ -697,8 +839,8 @@ function findPointAtX(svgPathElement, targetX, pathLength) {
// Return the closest point found // Return the closest point found
return bestPoint; return bestPoint;
}
} }
//from melmass //from melmass
export function hideWidgetForGood(node, widget, suffix = '') { export function hideWidgetForGood(node, widget, suffix = '') {
widget.origType = widget.type widget.origType = widget.type