20 Commits
Author SHA1 Message Date
Kijai ac5bb32bb8 editor linking progress 2024-05-07 16:51:07 +03:00
Kijai 43dded2f42 Add PreviewAnimation -node 2024-05-07 14:07:24 +03:00
Kijai a93b8687ac Update spline_editor.js 2024-05-07 12:19:08 +03:00
kijai eb2d3762a5 Merge branch 'main' into develop 2024-05-06 21:43:25 +03:00
Kijai 6afc4c38e7 continue 2024-05-06 17:37:51 +03:00
Kijai b1a061fe47 continue later 2024-05-06 16:44:35 +03:00
Kijai 2c262f722b Update spline_editor.js 2024-05-06 15:55:33 +03:00
kijai 894d9e5a0b Initial work on SplineEditor linking 2024-05-06 01:30:28 +03:00
kijai 48736ca845 Merge branch 'main' into develop 2024-05-06 00:29:03 +03:00
kijai a33961fdab Update spline_editor.js 2024-05-05 23:59:15 +03:00
kijai 62a704fc14 Merge branch 'main' into develop 2024-05-05 23:57:57 +03:00
kijai d5e37ff797 Update spline_editor.js 2024-04-20 21:22:29 +03:00
kijai bdade789ee spline editor fixes 2024-04-20 21:21:34 +03:00
kijai 1aef2b8e43 spline editor updates 2024-04-20 18:20:02 +03:00
kijai cb1e98abf1 Update spline_editor.js 2024-04-20 11:27:50 +03:00
kijai 891daeb438 Merge branch 'main' into develop 2024-04-19 18:40:44 +03:00
kijai d9712b0e04 spline editor work 2024-04-18 01:45:32 +03:00
Kijai 92711dc762 Update spline_editor.js 2024-04-17 19:43:49 +03:00
Kijai 6f256423af Update spline_editor.js 2024-04-17 19:21:32 +03:00
Kijai 47c23d5a19 reworking spline editor (not functional yet) 2024-04-16 19:19:16 +03:00
5 changed files with 230 additions and 24 deletions
+1
View File
@@ -57,6 +57,7 @@ NODE_CONFIG = {
"ImageUpscaleWithModelBatched": {"class": ImageUpscaleWithModelBatched, "name": "Image Upscale With Model Batched"}, "ImageUpscaleWithModelBatched": {"class": ImageUpscaleWithModelBatched, "name": "Image Upscale With Model Batched"},
"InsertImagesToBatchIndexed": {"class": InsertImagesToBatchIndexed, "name": "Insert Images To Batch Indexed"}, "InsertImagesToBatchIndexed": {"class": InsertImagesToBatchIndexed, "name": "Insert Images To Batch Indexed"},
"MergeImageChannels": {"class": MergeImageChannels, "name": "Merge Image Channels"}, "MergeImageChannels": {"class": MergeImageChannels, "name": "Merge Image Channels"},
"PreviewAnimation": {"class": PreviewAnimation, "name": "Preview Animation"},
"RemapImageRange": {"class": RemapImageRange, "name": "Remap Image Range"}, "RemapImageRange": {"class": RemapImageRange, "name": "Remap Image Range"},
"ReverseImageBatch": {"class": ReverseImageBatch, "name": "Reverse Image Batch"}, "ReverseImageBatch": {"class": ReverseImageBatch, "name": "Reverse Image Batch"},
"ReplaceImagesInBatch": {"class": ReplaceImagesInBatch, "name": "Replace Images In Batch"}, "ReplaceImagesInBatch": {"class": ReplaceImagesInBatch, "name": "Replace Images In Batch"},
+5 -4
View File
@@ -146,11 +146,12 @@ 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}),
"editor_link": ("EDITORLINK",),
} }
} }
RETURN_TYPES = ("MASK", "STRING", "FLOAT", "INT") RETURN_TYPES = ("MASK", "STRING", "FLOAT", "INT", "EDITORLINK",)
RETURN_NAMES = ("mask", "coord_str", "float", "count") RETURN_NAMES = ("mask", "coord_str", "float", "count", "editor_link",)
FUNCTION = "splinedata" FUNCTION = "splinedata"
CATEGORY = "KJNodes/weights" CATEGORY = "KJNodes/weights"
DESCRIPTION = """ DESCRIPTION = """
@@ -192,7 +193,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, 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, editor_link=None):
coordinates = json.loads(coordinates) coordinates = json.loads(coordinates)
for coord in coordinates: for coord in coordinates:
@@ -221,7 +222,7 @@ 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)
return (masks_out, str(coordinates), out_floats, len(out_floats)) return (masks_out, str(coordinates), out_floats, len(out_floats), editor_link,)
class CreateShapeMaskOnPath: class CreateShapeMaskOnPath:
+84
View File
@@ -1074,3 +1074,87 @@ with the **inputcount** and clicking update.
new_image = kwargs[f"image_{c + 1}"] new_image = kwargs[f"image_{c + 1}"]
image, = image_batch_node.batch(image, new_image) image, = image_batch_node.batch(image, new_image)
return (image,) return (image,)
class PreviewAnimation:
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
self.compress_level = 1
methods = {"default": 4, "fastest": 0, "slowest": 6}
@classmethod
def INPUT_TYPES(s):
return {"required":
{
"fps": ("FLOAT", {"default": 8.0, "min": 0.01, "max": 1000.0, "step": 0.01}),
},
"optional": {
"images": ("IMAGE", ),
"masks": ("MASK", ),
},
}
RETURN_TYPES = ()
FUNCTION = "preview"
OUTPUT_NODE = True
CATEGORY = "KJNodes/image"
def preview(self, fps, images=None, masks=None):
filename_prefix = "AnimPreview"
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
results = list()
pil_images = []
if images is not None and masks is not None:
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
pil_images.append(img)
for mask in masks:
if pil_images:
mask_np = mask.cpu().numpy()
mask_np = np.clip(mask_np * 255, 0, 255).astype(np.uint8) # Convert to values between 0 and 255
mask_img = Image.fromarray(mask_np, mode='L')
img = pil_images.pop(0) # Remove and get the first image
img = img.convert("RGBA") # Convert base image to RGBA
# Create a new RGBA image based on the grayscale mask
rgba_mask_img = Image.new("RGBA", img.size, (255, 255, 255, 255))
rgba_mask_img.putalpha(mask_img) # Use the mask image as the alpha channel
# Composite the RGBA mask onto the base image
composited_img = Image.alpha_composite(img, rgba_mask_img)
pil_images.append(composited_img) # Add the composited image back
elif images is not None and masks is None:
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
pil_images.append(img)
elif masks is not None and images is None:
for mask in masks:
mask_np = 255. * mask.cpu().numpy()
mask_img = Image.fromarray(np.clip(mask_np, 0, 255).astype(np.uint8))
pil_images.append(mask_img)
else:
print("PreviewAnimation: No images or masks provided")
return { "ui": { "images": results, "animated": (None,), "text": "empty" }}
num_frames = len(pil_images)
c = len(pil_images)
for i in range(0, c, num_frames):
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)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
animated = num_frames != 1
return { "ui": { "images": results, "animated": (animated,), "text": [f"{num_frames}x{pil_images[0].size[0]}x{pil_images[0].size[1]}"] } }
+18
View File
@@ -113,6 +113,24 @@ app.registerExtension({
return r return r
} }
break; break;
case "PreviewAnimation":
const onPreviewAnimationConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
const v = onPreviewAnimationConnectInput?.(this, arguments);
targetSlot.title = "Preview Animation"
return v;
}
const onPreviewAnimationExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
const r = onPreviewAnimationExecuted? onPreviewAnimationExecuted.apply(this,arguments): undefined
let values = message["text"].toString();
console.log(this)
this.title = "Preview Animation " + values
return r
}
break;
case "VRAM_Debug": case "VRAM_Debug":
const onVRAM_DebugConnectInput = nodeType.prototype.onConnectInput; const onVRAM_DebugConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
+115 -13
View File
@@ -176,26 +176,40 @@ app.registerExtension({
}); });
document.body.appendChild( this.contextMenu); document.body.appendChild( this.contextMenu);
this.editor = new SplineEditor(this);
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.createEditor(false);
this.addProperty("points", this.constructor.type, "string"); this.addProperty("points", this.constructor.type, "string");
} }
else { else {
createSplineEditor(this, true) this.editor.createEditor(true);
} }
}); });
this.setSize([550, 920]); this.setSize([550, 950]);
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";
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);
console.log(this)
chainCallback(this, "onConfigure", function() { chainCallback(this, "onGraphConfigured", function() {
createSplineEditor(this); this.editor.createEditor();
});
chainCallback(this, "onConnectInput", function(targetSlot, type, output, originNode, originSlot) {
// console.log("INPUT CONNECTED")
// console.log(targetSlot, type, output, originNode, originSlot)
// console.log("ORIGINNODE: ",originNode)
this.editor.syncEditors(originNode, "input");
});
chainCallback(this, "onConnectOutput", function(slot, type, input, target_node, target_slot) {
// console.log("OUTPUT CONNECTED")
// console.log(slot, type, input, target_node, target_slot)
// console.log("TARGETNODE: ",target_node)
this.editor.syncEditors(target_node, "output");
}); });
}); // onAfterGraphConfigured }); // onAfterGraphConfigured
@@ -203,8 +217,14 @@ app.registerExtension({
} //before register } //before register
})//register })//register
class SplineEditor {
function createSplineEditor(context, reset=false) { constructor(context) {
this.context = context;
this.vis = null;
this.extraLineLayers = {};
}
createEditor(reset=false) {
var context = this.context
console.log("creatingSplineEditor") console.log("creatingSplineEditor")
// context menu // context menu
@@ -332,6 +352,7 @@ function createSplineEditor(context, reset=false) {
if (coordWidget) { if (coordWidget) {
coordWidget.value = coordsString; coordWidget.value = coordsString;
} }
vis.render(); vis.render();
} }
@@ -348,14 +369,17 @@ function createSplineEditor(context, reset=false) {
const samplingMethodWidget = context.widgets.find(w => w.name === "sampling_method"); const samplingMethodWidget = context.widgets.find(w => w.name === "sampling_method");
const widthWidget = context.widgets.find(w => w.name === "mask_width"); const widthWidget = context.widgets.find(w => w.name === "mask_width");
const heightWidget = context.widgets.find(w => w.name === "mask_height"); const heightWidget = context.widgets.find(w => w.name === "mask_height");
//const segmentedWidget = context.widgets.find(w => w.name === "segmented");
let linkedInputEditorPointsToSample = null
let linkedOutputEditorPointsToSample = null
var points_to_sample = pointsWidget.value
var interpolation = interpolationWidget.value var interpolation = interpolationWidget.value
var tension = tensionWidget.value var tension = tensionWidget.value
var points_to_sample = pointsWidget.value
var rangeMin = minValueWidget.value var rangeMin = minValueWidget.value
var rangeMax = maxValueWidget.value var rangeMax = maxValueWidget.value
var pointsLayer = null; var pointsLayer = null;
var extraLineLayer = null;
var samplingMethod = samplingMethodWidget.value var samplingMethod = samplingMethodWidget.value
if (samplingMethod == "path") { if (samplingMethod == "path") {
@@ -378,7 +402,21 @@ function createSplineEditor(context, reset=false) {
updatePath(); updatePath();
} }
pointsWidget.callback = () => { pointsWidget.callback = () => {
let linkedInputEditor = context.getInputNode(0)
let linkedOutputEditor = context.getOutputNodes(4)
if (linkedOutputEditor != null) {
linkedOutputEditorPointsToSample = linkedOutputEditor[0].widgets.find(w => w.name === "points_to_sample")
linkedOutputEditorPointsToSample.value = pointsWidget.value
points_to_sample = pointsWidget.value points_to_sample = pointsWidget.value
}
if (linkedInputEditor != null) {
linkedInputEditorPointsToSample = linkedInputEditor.widgets.find(w => w.name === "points_to_sample")
linkedInputEditorPointsToSample.value = pointsWidget.value
points_to_sample = pointsWidget.value
}
else {
points_to_sample = pointsWidget.value
}
updatePath(); updatePath();
} }
minValueWidget.callback = () => { minValueWidget.callback = () => {
@@ -414,9 +452,12 @@ function createSplineEditor(context, reset=false) {
var h = heightWidget.value; var h = heightWidget.value;
var i = 3; var i = 3;
let points = []; let points = [];
let splineColor = null;
if (!reset && pointsStoreWidget.value != "") { if (!reset && pointsStoreWidget.value != "") {
points = JSON.parse(pointsStoreWidget.value); points = JSON.parse(pointsStoreWidget.value);
splineColor = context.properties.spline_color["color"]
console.log(splineColor["color"])
} else { } else {
points = pv.range(1, 4).map((i, index) => { points = pv.range(1, 4).map((i, index) => {
if (index === 0) { if (index === 0) {
@@ -433,7 +474,20 @@ function createSplineEditor(context, reset=false) {
}; };
} }
}); });
pointsStoreWidget.value = JSON.stringify(points); pointsStoreWidget.value = JSON.stringify(points)
context.properties.points = JSON.stringify(points)
if (!splineColor) {
let colors = pv.Colors.category10().range()
splineColor = colors[Math.floor(Math.random() * pv.Colors.category10().range().length)]
context.addProperty("spline_color", splineColor, pv.Colors)
}
else{
splineColor = context.properties.spline_color
console.log(splineColor["color"])
}
} }
var vis = new pv.Panel() var vis = new pv.Panel()
@@ -502,7 +556,7 @@ function createSplineEditor(context, reset=false) {
.interpolate(() => interpolation) .interpolate(() => interpolation)
.tension(() => tension) .tension(() => tension)
.segmented(() => false) .segmented(() => false)
.strokeStyle(pv.Colors.category10().by(pv.index)) .strokeStyle(splineColor)
.lineWidth(3) .lineWidth(3)
vis.add(pv.Dot) vis.add(pv.Dot)
@@ -556,7 +610,13 @@ function createSplineEditor(context, reset=false) {
}) })
.event("dragend", function() { .event("dragend", function() {
if (this.pathElements !== null) { if (this.pathElements !== null) {
context.editor.syncEditors(context.getInputNode(0))
let outputEditor = context.getOutputNodes(4);
if (outputEditor !== null){
context.editor.syncEditors(outputEditor[0]) }
updatePath(); updatePath();
} }
isDragging = false; isDragging = false;
}) })
@@ -607,9 +667,51 @@ function createSplineEditor(context, reset=false) {
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 var pathElements = svgElement.getElementsByTagName('path'); // Get all path elements
updatePath(); updatePath();
this.vis = vis
} }
syncEditors(node, connectionType) {
if (node == null) return
let linkedEditor = node
if (linkedEditor != null) {
let linkedPointsWidget = linkedEditor.widgets.find(w => w.name === "points_store")
let linkedCoords = JSON.parse(linkedPointsWidget.value)
// Initialize linkedNodes if it doesn't already exist
if (!node.hasOwnProperty("linkedNodes")) {
node["linkedNodes"] = {};
}
node["linkedNodes"][this.context.id] = linkedCoords;
if (Array.isArray(linkedCoords)) {
// Check if a layer for this node ID already exists
if (!this.extraLineLayers.hasOwnProperty(node.id)) {
// Create a new layer for this node ID if it doesn't exist
this.extraLineLayers[node.id] = this.vis.add(pv.Line)
.data(linkedCoords)
.left(d => d.x)
.top(d => d.y)
.interpolate(() => node.widgets.find(w => w.name === "interpolation").value)
.tension(() => node.widgets.find(w => w.name === "tension").value)
.segmented(() => false)
.strokeStyle(node.properties.spline_color["color"])
.lineWidth(3);
} else {
// If the layer already exists, update its data
this.extraLineLayers[node.id].data(linkedCoords);
}
}
this.vis.render()
}
}//syncEditors
}
function samplePoints(svgPathElement, numSamples, samplingMethod, width) { function 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();
@@ -683,7 +785,7 @@ function findPointAtX(svgPathElement, targetX, pathLength) {
} }
//from melmass //from melmass
export function hideWidgetForGood(node, widget, suffix = '') { function hideWidgetForGood(node, widget, suffix = '') {
widget.origType = widget.type widget.origType = widget.type
widget.origComputeSize = widget.computeSize widget.origComputeSize = widget.computeSize
widget.origSerializeValue = widget.serializeValue widget.origSerializeValue = widget.serializeValue