diff --git a/.gitignore b/.gitignore index e67081c..b954089 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ ignore.txt *.bak checkpoints results -backup \ No newline at end of file +backup +node_modules +*-lock.json \ No newline at end of file diff --git a/__init__.py b/__init__.py index f6c7bc0..a68512d 100644 --- a/__init__.py +++ b/__init__.py @@ -300,7 +300,7 @@ class Session(metaclass=Singleton): if f.suffix != ".py" or f.stem.startswith('_'): continue if f.stem in JOV_IGNORE_NODE or f.stem+'.py' in JOV_IGNORE_NODE: - logger.warning(f"💀 Jovimetrix.core.{f.stem}") + logger.warning(f"💀 [IGNORED] Jovimetrix.core.{f.stem}") continue try: module = importlib.import_module(f"Jovimetrix.core.{f.stem}") @@ -313,7 +313,7 @@ class Session(metaclass=Singleton): try: for class_name, class_def in module.import_dynamic(): setattr(module, class_name, class_def) - logger.debug(f"shader: {class_name}") + logger.info(f"shader: {class_name}") except Exception as e: pass diff --git a/core/calc.py b/core/calc.py index a0aecbc..2751267 100644 --- a/core/calc.py +++ b/core/calc.py @@ -898,6 +898,7 @@ Supplies raw or default values for various data types, supporting vector input w params = list(zip_longest_fill(raw, r_x, r_y, r_z, r_w, typ, xyzw, seed, yyzw, x_str)) results = [] pbar = ProgressBar(len(params)) + old_seed = -1 for idx, (raw, r_x, r_y, r_z, r_w, typ, xyzw, seed, yyzw, x_str) in enumerate(params): typ = EnumConvertType[typ] default = [x_str] @@ -924,19 +925,20 @@ Supplies raw or default values for various data types, supporting vector input w self.UPDATE = False if seed != 0 and isinstance(val, (tuple, list,)) and isinstance(val2, (tuple, list,)): self.UPDATE = True - # val = list(val) if isinstance(val, (tuple, list,)) else [val] - # val2 = list(val2) if isinstance(val2, (tuple, list,)) else [val2] + # mutable to update + val = list(val) for i in range(len(val)): mx = max(val[i], val2[i]) mn = min(val[i], val2[i]) if mn == mx: val[i] = mn else: - random.seed(seed) + if old_seed != seed: + random.seed(seed) + old_seed = seed if typ == EnumConvertType.VEC4: val[i] = mn + random.random() * (mx - mn) else: - # logger.debug(f"{i}, {mx}, {mn}") val[i] = random.randint(mn, mx) extra = parse_value(val, typ, val) diff --git a/core/compose.py b/core/compose.py index 4a26681..4ef251e 100644 --- a/core/compose.py +++ b/core/compose.py @@ -114,7 +114,7 @@ Enhance and modify images with various effects such as blurring, sharpening, col pA = tensor2cv(pA) if pA is not None else channel_solid(chan=EnumImageType.BGRA) cc = pA.shape[2] if pA.ndim == 3 else 1 if cc == 4: - alpha = pA[:,:,3] + alpha = pA[..., 3] match EnumAdjustOP[op]: case EnumAdjustOP.INVERT: @@ -201,7 +201,7 @@ Enhance and modify images with various effects such as blurring, sharpening, col mask = 255 - mask pA = image_blend(pA, img_new, mask) if cc == 4: - pA[:,:,3] = alpha + pA[..., 3] = alpha images.append(cv2tensor_full(pA, matte)) pbar.update_absolute(idx) return [torch.cat(i, dim=0) for i in zip(*images)] @@ -245,7 +245,7 @@ Combine two input images using various blending modes, such as normal, screen, m mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name) wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name) - matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC3INT, [(0, 0, 0)], 0, 255) + matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255) invert = parse_param(kw, Lexicon.INVERT, EnumConvertType.BOOLEAN, False) params = list(zip_longest_fill(pA, pB, mask, func, alpha, flip, mode, wihi, sample, matte, invert)) images = [] @@ -281,11 +281,7 @@ Combine two input images using various blending modes, such as normal, screen, m mask = channel_solid(w, h, matte[3], EnumImageType.GRAYSCALE) if tmask is None else image_mask(tmask) else: mask = tensor2cv(mask) - cc = mask.shape[2] if mask.ndim == 3 else 1 - if cc == 4: - mask = mask[:,:,3] - else: - mask = image_grayscale(mask) + mask = image_grayscale(mask) if invert: mask = 255 - mask @@ -516,7 +512,7 @@ Extract a portion of an input image or resize it. It supports various cropping m pA = image_crop_polygonal(pA, points) if alpha is not None: alpha = image_crop_polygonal(alpha, points) - pA[:,:,3] = alpha[:,:,0][:,:] + pA[..., 3] = alpha[..., 0][:,:] elif func == EnumCropMode.XY: pA = image_crop(pA, width, height, xy) @@ -571,7 +567,7 @@ Create masks based on specific color ranges within an image. Specify the color r if img.shape[2] == 3: alpha_channel = np.zeros((img.shape[0], img.shape[1], 1), dtype=img.dtype) img = np.concatenate((img, alpha_channel), axis=2) - img[:,:,3] = mask[:,:] + img[..., 3] = mask[:,:] images.append(cv2tensor_full(img, matte)) pbar.update_absolute(idx) return [torch.cat(i, dim=0) for i in zip(*images)] @@ -852,10 +848,10 @@ Swap pixel values between two input images based on specified channel swizzle op return target # logger.debug(swap_r, swap_g, swap_b, swap_a) - out[:,:,0] = swapper(EnumPixelSwizzle.BLUE_A, swap_b)[:,:,0] + out[..., 0] = swapper(EnumPixelSwizzle.BLUE_A, swap_b)[..., 0] out[:,:,1] = swapper(EnumPixelSwizzle.GREEN_A, swap_g)[:,:,1] out[:,:,2] = swapper(EnumPixelSwizzle.RED_A, swap_r)[:,:,2] - out[:,:,3] = swapper(EnumPixelSwizzle.ALPHA_A, swap_a)[:,:,3] + out[..., 3] = swapper(EnumPixelSwizzle.ALPHA_A, swap_a)[..., 3] images.append(cv2tensor_full(out)) pbar.update_absolute(idx) return [torch.cat(i, dim=0) for i in zip(*images)] diff --git a/core/create.py b/core/create.py index f015cbc..84be1e9 100644 --- a/core/create.py +++ b/core/create.py @@ -19,9 +19,9 @@ from Jovimetrix.sup.lexicon import JOVImageNode, Lexicon from Jovimetrix.sup.util import parse_param, zip_longest_fill, EnumConvertType from Jovimetrix.sup.image import channel_solid, cv2tensor, cv2tensor_full, \ - image_grayscale, image_invert, image_mask_add, pil2cv, \ - image_rotate, image_scalefit, image_stereogram, image_transform, \ - tensor2cv, shape_ellipse, shape_polygon, shape_quad, image_translate, \ + image_grayscale, image_invert, image_mask_add, image_mask_binary, image_matte, \ + image_rotate, image_scalefit, image_stereogram, image_transform, shape_body, \ + tensor2cv, shape_polygon, image_translate, pil2cv, \ EnumScaleMode, EnumInterpolation, EnumEdge, EnumImageType, MIN_IMAGE_SIZE from Jovimetrix.sup.text import font_names, text_autosize, text_draw, \ @@ -50,10 +50,10 @@ Generate a constant image or mask of a specified size and color. It can be used d.update({ "optional": { Lexicon.PIXEL: (JOV_TYPE_IMAGE, {"tooltip":"Optional Image to Matte with Selected Color"}), - Lexicon.RGBA_A: ("VEC4", {"default": (0, 0, 0, 255), "step": 1, + Lexicon.RGBA_A: ("VEC4INT", {"default": (0, 0, 0, 255), "label": [Lexicon.R, Lexicon.G, Lexicon.B, Lexicon.A], "rgb": True, "tooltip": "Constant Color to Output"}), - Lexicon.WH: ("VEC2", {"default": (512, 512), "step": 1, + Lexicon.WH: ("VEC2INT", {"default": (512, 512), "label": [Lexicon.W, Lexicon.H], "tooltip": "Desired Width and Height of the Color Output"}), Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}), @@ -100,15 +100,14 @@ Create n-sided polygons. These shapes can be customized by adjusting parameters "optional": { Lexicon.SHAPE: (EnumShapes._member_names_, {"default": EnumShapes.CIRCLE.name}), Lexicon.SIDES: ("INT", {"default": 3, "min": 3, "max": 100, "step": 1}), - Lexicon.RGBA_A: ("VEC4", {"default": (255, 255, 255, 255), "step": 1, + Lexicon.RGBA_A: ("VEC4INT", {"default": (255, 255, 255, 255), "step": 1, "label": [Lexicon.R, Lexicon.G, Lexicon.B, Lexicon.A], "rgb": True, "tooltip": "Main Shape Color"}), - Lexicon.MATTE: ("VEC4", {"default": (0, 0, 0, 255), "step": 1, + Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "step": 1, "label": [Lexicon.R, Lexicon.G, Lexicon.B, Lexicon.A], "rgb": True, "tooltip": "Background Color"}), - Lexicon.WH: ("VEC2", {"default": (256, 256), - "step": 1, "min":MIN_IMAGE_SIZE, - "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (256, 256), + "min":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), Lexicon.XY: ("VEC2", {"default": (0, 0,), "step": 0.01, "precision": 4, "round": 0.00001, "label": [Lexicon.X, Lexicon.Y]}), Lexicon.ANGLE: ("FLOAT", {"default": 0, "min": -180, "max": 180, @@ -141,41 +140,34 @@ Create n-sided polygons. These shapes can be customized by adjusting parameters sizeX, sizeY = size edge = EnumEdge[edge] shape = EnumShapes[shape] - alpha_m = int(matte[3]) + fill = color[:3][::-1] + back = matte[:3] + match shape: - case EnumShapes.SQUARE: - pA = shape_quad(width, height, sizeX, sizeX, fill=color[:3], back=matte[:3]) - mask = shape_quad(width, height, sizeX, sizeX, fill=alpha_m) + case EnumShapes.SQUARE | EnumShapes.RECTANGLE: + # pA = shape_quad(width, height, sizeX, sizeX, fill=fill, back=back) + pA = shape_body('rectangle', width, height, sizeX=sizeX, sizeY=sizeY, fill=fill, back=back) - case EnumShapes.ELLIPSE: - pA = shape_ellipse(width, height, sizeX, sizeY, fill=color[:3], back=matte[:3]) - mask = shape_ellipse(width, height, sizeX, sizeY, fill=alpha_m) - - case EnumShapes.RECTANGLE: - pA = shape_quad(width, height, sizeX, sizeY, fill=color[:3], back=matte[:3]) - mask = shape_quad(width, height, sizeX, sizeY, fill=alpha_m) + case EnumShapes.CIRCLE | EnumShapes.ELLIPSE: + pA = shape_body('ellipse', width, height, sizeX=sizeX, sizeY=sizeY, fill=fill, back=back) case EnumShapes.POLYGON: - pA = shape_polygon(width, height, sizeX, sides, fill=color[:3], back=matte[:3]) - mask = shape_polygon(width, height, sizeX, sides, fill=alpha_m) - - case EnumShapes.CIRCLE: - pA = shape_ellipse(width, height, sizeX, sizeX, fill=color[:3], back=matte[:3]) - mask = shape_ellipse(width, height, sizeX, sizeX, fill=alpha_m) + pA = shape_polygon(width, height, sizeX, sides, fill=fill, back=back) pA = pil2cv(pA) - mask = pil2cv(mask) - mask = image_grayscale(mask) - pA = image_transform(pA, offset, angle, (1,1), edge=edge) - mask = image_transform(mask, offset, angle, (1,1), edge=edge) - pB = image_mask_add(pA, mask) + pA = image_transform(pA, offset, angle, edge=edge) if blur > 0: # @TODO: Do blur on larger canvas to remove wrap bleed. pA = (gaussian(pA, sigma=blur, channel_axis=2) * 255).astype(np.uint8) - pB = (gaussian(pB, sigma=blur, channel_axis=2) * 255).astype(np.uint8) - mask = (gaussian(mask, sigma=blur, channel_axis=2) * 255).astype(np.uint8) + pA = image_matte(pA, matte) - images.append([cv2tensor(pB), cv2tensor(pA), cv2tensor(mask, True)]) + mask = image_mask_binary(pA) # * float(color[3]) / 255. + pB = image_mask_add(pA, mask) + matte = image_matte(pB, matte) + # matte = np.full((height, width, 4), matte, dtype=np.uint8) + # matte[:, :] = pB + + images.append([cv2tensor(pB), cv2tensor(matte), cv2tensor(mask, True)]) pbar.update_absolute(idx) return [torch.cat(i, dim=0) for i in zip(*images)] @@ -402,7 +394,7 @@ The Wave Graph node visualizes audio waveforms as bars. Adjust parameters like t thick = parse_param(kw, Lexicon.THICK, EnumConvertType.FLOAT, 0.75, 0, 1) wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) rgb_a = parse_param(kw, Lexicon.RGBA_A, EnumConvertType.VEC4INT, [(196, 0, 196)], 0, 255) - matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(42, 12, 42)], 0, 255) + matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(42, 12, 42, 255)], 0, 255) params = list(zip_longest_fill(wave, bars, wihi, thick, rgb_a, matte)) images = [] pbar = ProgressBar(len(params)) diff --git a/core/create_glsl.py b/core/create_glsl.py index 3da476b..b7e5ee5 100644 --- a/core/create_glsl.py +++ b/core/create_glsl.py @@ -7,6 +7,7 @@ import os from pathlib import Path from typing import Any, Tuple +import numpy as np import torch from loguru import logger @@ -129,6 +130,7 @@ class GLSLNodeBase(JOVImageNode): self.__glsl.program(self.VERTEX, self.FRAGMENT) except CompileException as e: comfy_message(ident, "jovi-glsl-error", {"id": ident, "e": str(e)}) + logger.error(self.NAME) logger.error(e) return @@ -199,14 +201,25 @@ class GLSLNodeDynamic(GLSLNodeBase): # parameter list first... data = {} if cls.PARAM is not None: - for glsl_type, name, default, tooltip in cls.PARAM: + for glsl_type, name, default, tooltip, val_min, val_max, val_step in cls.PARAM: typ = PTYPE[glsl_type] d = None if glsl_type != 'sampler2D' and default is not None: d = default.split(',') d = parse_value(d, typ, 0) - data[name] = (typ.name, {"default": d, "min": -2147483647, "nax":2147483647, "step": 0.01, "tooltip": tooltip},) - print(name, (typ.name, {"default": d},)) + + #val_min = -2147483647 + #val_max = 2147483647 + #val_step = 1 + entry = (typ.name, {}) + match typ: + case EnumConvertType.INT | EnumConvertType.VEC2INT | EnumConvertType.VEC3INT | EnumConvertType.VEC4INT: + entry = (typ.name, {"default": d, "min": val_min, "max":val_max, "step": val_step, "tooltip": tooltip},) + case EnumConvertType.FLOAT | EnumConvertType.VEC2 | EnumConvertType.VEC3 | EnumConvertType.VEC4: + entry = (typ.name, {"default": d, "min": val_min, "max":val_max, "step": val_step, "precision": 6, "round": 0.0001, "tooltip": tooltip},) + case EnumConvertType.IMAGE: + entry = (typ.name, {"default": d, "tooltip": tooltip},) + data[name] = entry data.update(opts) original_params['optional'] = data @@ -221,6 +234,7 @@ def import_dynamic() -> Tuple[str,...]: continue meta = shader_meta(shader) + name = meta.get('name', name.split('.')[0]) class_name = f'GLSLNode_{name.title()}' class_def = type(class_name, (GLSLNodeDynamic,), { @@ -229,11 +243,5 @@ def import_dynamic() -> Tuple[str,...]: "FRAGMENT": shader, "PARAM": meta.get('_', []), }) - - #def init_method(self, *arg, **kw) -> None: - # super(class_def, self).__init__(*arg, **kw) - # self.FRAGMENT = shader - - #class_def.__init__ = init_method ret.append((class_name, class_def,)) return ret diff --git a/core/utility.py b/core/utility.py index 971c082..dfe0071 100644 --- a/core/utility.py +++ b/core/utility.py @@ -648,7 +648,8 @@ Manage a queue of items, such as file paths or data. It supports various formats data = torch.cat(ret, dim=0) else: data = process(self.__q[self.__index]) - data = cv2tensor(data) + if isinstance(data[0], (np.ndarray,)): + data = cv2tensor(data) self.__index += 1 self.__previous = data @@ -763,6 +764,29 @@ Save the output image along with its metadata to the specified path. Supports sa pbar.update_absolute(idx) return () +class Terminate(JOVBaseNode): + NAME = "TERMINATE COMFYUI (JOV) " + CATEGORY = f"JOVIMETRIX 🔺🟩🔵/{JOV_CATEGORY}" + OUTPUT_NODE = True + RETURN_TYPES = () + SORT = 115 + DESCRIPTION = """ +Terminate a running ComfyUI server. +""" + + @classmethod + def INPUT_TYPES(cls) -> dict: + d = super().INPUT_TYPES(True, True) + d.update({ + "optional": { + Lexicon.TRIGGER: ("TRIGGER",), + } + }) + return Lexicon._parse(d, cls) + + def run(self, **kw) -> dict[str, Any]: + exit() + ''' class RESTNode: """Make requests and process the responses.""" diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..be3ef94 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,11 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.{js,mjs,cjs,vue}"]}, + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + ...pluginVue.configs["flat/essential"], +]; \ No newline at end of file diff --git a/node_list.json b/node_list.json index 537a998..178e79d 100644 --- a/node_list.json +++ b/node_list.json @@ -14,8 +14,9 @@ "FILTER MASK (JOV) \ud83e\udd3f": "Create masks based on specific color ranges within an image", "FLATTEN (JOV) \u2b07\ufe0f": "Combine multiple input images into a single image by summing their pixel values", "GLSL (JOV) \ud83c\udf69": "Execute custom GLSL (OpenGL Shading Language) fragment shaders to generate images or apply effects", - "GLSL BLEND (JOV) \ud83e\uddd9\ud83c\udffd": "Simple linear blend between two images", + "GLSL BLEND_LINEAR (JOV) \ud83e\uddd9\ud83c\udffd": "Simple linear blend between two images", "GLSL GRAYSCALE (JOV) \ud83e\uddd9\ud83c\udffd": "Convert input to grayscale", + "GLSL NORMAL (JOV) \ud83e\uddd9\ud83c\udffd": "Convert input into a Normal map", "GRADIENT MAP (JOV) \ud83c\uddf2\ud83c\uddfa": "Remaps an input image using a gradient lookup table (LUT)", "GRAPH (JOV) \ud83d\udcc8": "Visualize a series of data points over time", "IMAGE INFO (JOV) \ud83d\udcda": "Exports and Displays immediate information about images", @@ -40,6 +41,7 @@ "STREAM READER (JOV) \ud83d\udcfa": "Capture frames from various sources such as URLs, cameras, monitors, windows, or Spout streams", "STREAM WRITER (JOV) \ud83c\udf9e\ufe0f": "Sends frames to a specified route, typically for live streaming or recording purposes", "SWIZZLE (JOV) \ud83d\ude35": "Swap components between two vectors based on specified swizzle patterns and values", + "TERMINATE COMFYUI (JOV) ": "Terminate a running ComfyUI server", "TEXT GEN (JOV) \ud83d\udcdd": "Generates images containing text based on parameters such as font, size, alignment, color, and position", "THRESHOLD (JOV) \ud83d\udcc9": "Define a range and apply it to an image for segmentation and feature extraction", "TICK (JOV) \u23f1": "A timer and frame counter, emitting pulses or signals based on time intervals", diff --git a/package.json b/package.json new file mode 100644 index 0000000..2f015a7 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "jovimetrix", + "version": "1.0.0", + "description": "

THIS ENTIRE PROJECT IS DONATIONWARE.
PLEASE FEEL FREE TO CONTRIBUTE IN ANYWAY YOU THINK YOU CAN

\r \r \r \r \"ComfyUI\r ", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@eslint/js": "^9.7.0", + "eslint": "^9.7.0", + "eslint-plugin-vue": "^9.27.0", + "globals": "^15.8.0" + } +} diff --git a/res/glsl/color-grayscale.glsl b/res/glsl/color-grayscale.glsl index f54cd3f..6e99892 100644 --- a/res/glsl/color-grayscale.glsl +++ b/res/glsl/color-grayscale.glsl @@ -4,7 +4,7 @@ // default grayscale using NTSC conversion weights uniform sampler2D image; -uniform vec3 conversion; // 0.299, 0.587, 0.114 +uniform vec3 conversion; // 0.299, 0.587, 0.114;0;1;0.01 | Scalar for each channel void mainImage( out vec4 fragColor, vec2 fragCoord ) { vec2 uv = fragCoord.xy / iResolution.xy; diff --git a/res/glsl/comp-blend.glsl b/res/glsl/comp-blend.glsl index f6721b1..2ca2866 100644 --- a/res/glsl/comp-blend.glsl +++ b/res/glsl/comp-blend.glsl @@ -1,14 +1,14 @@ -// name: BLEND +// name: BLEND_LINEAR // desc: Simple linear blend between two images // uniform sampler2D imageA; uniform sampler2D imageB; -uniform float blend_amt; // 0.5 +uniform float blend_amt; // 0.5;0;1;0.01 void mainImage( out vec4 fragColor, vec2 fragCoord ) { vec2 uv = fragCoord.xy / iResolution.xy; vec4 col_a = texture2D(imageA, uv); vec4 col_b = texture2D(imageB, uv); - fragColor = mix(col_a, col_b, blend_amt); + fragColor = mix(col_b, col_a, blend_amt); } \ No newline at end of file diff --git a/res/glsl/util-normal.glsl b/res/glsl/util-normal.glsl new file mode 100644 index 0000000..d71f6c3 --- /dev/null +++ b/res/glsl/util-normal.glsl @@ -0,0 +1,45 @@ +// name: NORMAL +// desc: Convert input into a Normal map +// + +uniform sampler2D image; // | Input image to convert into a normal map +uniform float scalar; // 0.25 | Intensity of depth + +const mat3 scharr_x = mat3( + 3.0, 10.0, 3.0, + 0.0, 0.0, 0.0, + -3.0, -10.0, -3.0 +); + +const mat3 scharr_y = mat3( + 3.0, 0.0, -3.0, + 10.0, 0.0, -10.0, + 3.0, 0.0, -3.0 +); + +vec3 scharr(sampler2D tex, vec2 uv) { + vec3 result = vec3(0.0); + vec2 texelSize = 1.0 / iResolution.xy; + + for (int i = -1; i <= 1; i++) { + for (int j = -1; j <= 1; j++) { + vec2 offset = vec2(float(i), float(j)) * texelSize; + vec3 color = texture(tex, uv + offset).rgb; + float luminance = dot(color, vec3(0.299, 0.587, 0.114)); + result.x += luminance * scharr_x[i+1][j+1]; + result.y += luminance * scharr_y[i+1][j+1]; + } + } + return result; +} + +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + vec2 uv = fragCoord / iResolution.xy; + vec3 normal; + normal.xy = scharr(image, uv).yx * scalar; + normal.x *= -1.0; + normal.z = 1.0; + normal = normalize(normal); + fragColor = vec4(normal * 0.5 + 0.5, 1.0); +} \ No newline at end of file diff --git a/sup/image.py b/sup/image.py index e65642b..c01ae23 100644 --- a/sup/image.py +++ b/sup/image.py @@ -349,7 +349,7 @@ def cv2tensor(image: TYPE_IMAGE, mask:bool=False) -> torch.Tensor: """Convert a CV2 image to a torch tensor.""" if mask or image.ndim < 3 or (image.ndim == 3 and image.shape[2] == 1): mask = True - image = image_grayscale(image)[:,:] + image = image_grayscale(image) ret = torch.from_numpy(image.astype(np.float32) / 255.0).unsqueeze(0) if mask and ret.ndim == 4: ret = ret.squeeze(-1) @@ -357,8 +357,8 @@ def cv2tensor(image: TYPE_IMAGE, mask:bool=False) -> torch.Tensor: def cv2tensor_full(image: TYPE_IMAGE, matte:TYPE_PIXEL=0) -> Tuple[torch.Tensor, ...]: image = image_convert(image, 4) - mask = image_mask(image)[:,:,0][:,:] - image[:,:,3] = mask + mask = image_mask(image) + image[..., 3] = mask rgb = image_matte(image, matte) rgb = image_convert(image, 3) image = torch.from_numpy(image.astype(np.float32) / 255.0).unsqueeze(0) @@ -579,7 +579,7 @@ def channel_merge(channel:List[TYPE_IMAGE]) -> TYPE_IMAGE: if ch.shape[:2] != (max_height, max_width): ch = cv2.resize(ch, (max_width, max_height)) if ch.ndim > 2: - ch = ch[:,:,0] + ch = ch[..., 0] img[:,:,i] = ch if len(channel) == 3: @@ -616,12 +616,6 @@ def shape_body(func: str, width: int, height: int, sizeX:float=1., sizeY:float=1 func(xy, fill=fill) return image -def shape_ellipse(width: int, height: int, sizeX:float=1., sizeY:float=1., fill:TYPE_PIXEL=255, back:TYPE_PIXEL=0) -> Image: - return shape_body('ellipse', width, height, sizeX=sizeX, sizeY=sizeY, fill=fill, back=back) - -def shape_quad(width: int, height: int, sizeX:float=1., sizeY:float=1., fill:TYPE_PIXEL=255, back:TYPE_PIXEL=0) -> Image: - return shape_body('rectangle', width, height, sizeX=sizeX, sizeY=sizeY, fill=fill, back=back) - def shape_polygon(width: int, height: int, size: float=1., sides: int=3, fill:TYPE_PIXEL=255, back:TYPE_PIXEL=0) -> Image: size = max(0.00001, size) r = min(width, height) * size * 0.5 @@ -655,16 +649,16 @@ def image_blend(imageA: TYPE_IMAGE, imageB: TYPE_IMAGE, mask:Optional[TYPE_IMAGE h2 = min(h, h2) imageB = image_crop_center(imageB, w2, h2) imageB = image_matte(imageB, (0,0,0,0), w, h) - old_mask = image_mask(imageB)[:,:,0] + old_mask = image_mask(imageB) if len(old_mask.shape) > 2: - old_mask = old_mask[:,:,0][:,:] + old_mask = old_mask[..., 0][:,:] if mask is not None: mask = image_crop_center(mask, w, h) mask = image_matte(mask, (0,0,0,0), w, h) if len(mask.shape) > 2: - mask = mask[:,:,0][:,:] + mask = mask[..., 0][:,:] old_mask = cv2.bitwise_and(mask, old_mask) - imageB[:,:,3] = old_mask + imageB[..., 3] = old_mask imageB = cv2pil(imageB) image = blendLayers(imageA, imageB, blendOp.value, np.clip(alpha, 0, 1)) image = pil2cv(image) @@ -711,13 +705,15 @@ def image_convert(image: TYPE_IMAGE, channels: int) -> TYPE_IMAGE: """Force image format to number of channels chosen.""" if len(image.shape) < 3: image = np.expand_dims(image, -1).astype(dtype=np.uint8) - ncc = max(1, min(4, channels)) - if ncc < 3: + + channels = max(1, min(4, channels)) + if channels < 3: return image_grayscale(image) + cc = image.shape[2] if image.ndim == 3 else 1 - if ncc == cc: + if channels == cc: return image - if ncc == 3: + if channels == 3: if cc == 1: return cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) elif cc == 4: @@ -733,7 +729,7 @@ def image_crop_polygonal(image: TYPE_IMAGE, points: List[TYPE_COORD]) -> TYPE_IM points = np.array(points, np.int32).reshape((-1, 1, 2)) point_mask = cv2.fillPoly(point_mask, [points], 255) x, y, w, h = cv2.boundingRect(point_mask) - cropped_image = cv2.resize(image[y:y+h, x:x+w], (w, h)) + cropped_image = cv2.resize(image[y:y+h, x:x+w], (w, h)).astype(np.uint8) # Apply the mask to the cropped image point_mask_cropped = cv2.resize(point_mask[y:y+h, x:x+w], (w, h)) if cc == 4: @@ -1046,7 +1042,7 @@ def image_filter(image:TYPE_IMAGE, start:Tuple[int]=(128,128,128), end:Tuple[int image: torch.tensor = cv2tensor(image) cc = image.shape[2] if cc == 4: - old_alpha = image[:,:,3] + old_alpha = image[..., 3] new_image = image[:, :, :3] elif cc == 1: new_image = np.repeat(image, 3, axis=2) @@ -1142,7 +1138,7 @@ def image_gradient_map(image:TYPE_IMAGE, gradient_map:TYPE_IMAGE, reverse:bool=F cmap = cmap[0,:,:].reshape((256, 1, 3)).astype(np.uint8) return cv2.applyColorMap(grey, cmap) -def image_grayscale(image: np.ndarray) -> np.ndarray: +def image_grayscale(image: TYPE_IMAGE) -> TYPE_IMAGE: """ Convert an image to grayscale, preserving alpha if present. @@ -1152,7 +1148,7 @@ def image_grayscale(image: np.ndarray) -> np.ndarray: - Already grayscale images Args: - image (np.ndarray): Input image. Can be 2D (grayscale) or 3D (RGB/RGBA) array. + image (TYPE_IMAGE): Input image. Can be 2D (grayscale) or 3D (RGB/RGBA) array. Returns: np.ndarray: Grayscale image with alpha channel preserved if present in input. @@ -1166,29 +1162,15 @@ def image_grayscale(image: np.ndarray) -> np.ndarray: image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX) image = image.astype(np.uint8) - # Extract alpha channel if present - has_alpha = image.shape[-1] == 4 if image.ndim == 3 else False - alpha = image[..., 3] if has_alpha else None + # already grayscale + if image.ndim < 3 or image.shape[2] == 1: + return np.expand_dims(image, axis=-1) - # Convert to grayscale - if image.ndim == 3: - if image.shape[2] in [3, 4]: - gray = cv2.cvtColor(image[..., :3], cv2.COLOR_BGR2GRAY) - else: - raise ValueError(f"Unexpected number of channels: {image.shape[2]}") - elif image.ndim == 2: - gray = image + if image.shape[2] == 4: + image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY) else: - raise ValueError(f"Unexpected number of dimensions: {image.ndim}") - - # Ensure output is 3D - gray = np.expand_dims(gray, axis=-1) - - # Apply alpha if present - if has_alpha: - gray = np.dstack((gray, alpha[..., np.newaxis])) - - return gray + image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + return np.expand_dims(image, axis=-1) def image_grid(data: List[TYPE_IMAGE], width: int, height: int) -> TYPE_IMAGE: #@TODO: makes poor assumption all images are the same dimensions. @@ -1277,51 +1259,68 @@ def image_lerp(imageA:TYPE_IMAGE, imageB:TYPE_IMAGE, mask:TYPE_IMAGE=None, imageA = (imageA * 255).astype(np.uint8) return np.clip(imageA, 0, 255) -def image_levels(image:torch.Tensor, black_point:int=0, white_point=255, - mid_point=128, gamma=1.0) -> TYPE_IMAGE: +def image_levels(image:np.ndarray, black_point:int=0, white_point=255, + mid_point=128, gamma=1.0) -> np.ndarray: + """ + Adjusts the levels of an image including black, white, midpoints, and gamma correction. + + Args: + image (numpy.ndarray): Input image tensor in RGB(A) format. + black_point (int): The black point to adjust shadows. Default is 0. + white_point (int): The white point to adjust highlights. Default is 255. + mid_point (int): The mid point for mid-tone adjustment. Default is 128. + gamma (float): Gamma correction value. Default is 1.0. + + Returns: + numpy.ndarray: Adjusted image tensor. + """ image, alpha, cc = image2bgr(image) - black = np.array([black_point] * 3, dtype=np.float32) - white = np.array([white_point] * 3, dtype=np.float32) - mid = np.array([mid_point] * 3, dtype=np.float32) - inGamma = np.array([gamma] * 3, dtype=np.float32) + + # Convert points and gamma to float32 for calculations + black = np.array([black_point] * 3, dtype=np.float32) + white = np.array([white_point] * 3, dtype=np.float32) + mid = np.array([mid_point] * 3, dtype=np.float32) + inGamma = np.array([gamma] * 3, dtype=np.float32) outBlack = np.array([0, 0, 0], dtype=np.float32) outWhite = np.array([255, 255, 255], dtype=np.float32) - image = np.clip( (image - black) / (white - black), 0, 255 ) - image = (image ** (1/inGamma) ) * (outWhite - outBlack) + outBlack + + # Apply levels adjustment + image = np.clip((image - black) / (white - black), 0, 1) + image = (image - mid) / (1.0 - mid) + image = (image ** (1 / inGamma)) * (outWhite - outBlack) + outBlack image = np.clip(image, 0, 255).astype(np.uint8) return bgr2image(image, alpha, cc == 1) -def image_load(url: str) -> Tuple[TYPE_IMAGE, TYPE_IMAGE]: - """ - if img.format == 'PSD': - images = [pil2cv(frame.copy()) for frame in ImageSequence.Iterator(img)] - # logger.debug(f"#PSD {len(images)}") - """ +def image_load(url: str) -> Tuple[TYPE_IMAGE, ...]: try: img = cv2.imread(url, cv2.IMREAD_UNCHANGED) if img is None: - raise ValueError() + raise ValueError(f"Image at {url} could not be loaded.") + if img.ndim == 3: if img.shape[2] == 4: img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA) else: img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - if img.ndim < 3: + elif img.ndim < 3: img = np.expand_dims(img, axis=2) - #if img.shape[2] == 1: - # img = image_convert(img, 3) - except Exception as _: + + except Exception: try: img = Image.open(url) img = ImageOps.exif_transpose(img) - img = pil2cv(img) + img = np.array(img) except Exception as e: logger.error(str(e)) + raise Exception(f"Error loading image: {e}") + if img is None: - raise Exception(f"no file {url}") + raise Exception(f"No file found at {url}") + if img.dtype != np.uint8: img = np.clip(np.array(img * 255), 0, 255).astype(dtype=np.uint8) + return img, image_mask(img) def image_load_data(data: str) -> TYPE_IMAGE: @@ -1359,66 +1358,88 @@ def image_load_from_url(url:str) -> TYPE_IMAGE: except Exception as e: logger.error(str(e)) -def image_normalize(image: TYPE_IMAGE) -> TYPE_IMAGE: - image = image.astype(np.float32) - img_min = np.min(image) - img_max = np.max(image) - if img_min == img_max: - return np.zeros_like(image, dtype=np.float32) - image = (image - img_min) / (img_max - img_min) - return (image * 255).astype(np.uint8) - def image_mask(image:TYPE_IMAGE, color:TYPE_PIXEL=255) -> TYPE_IMAGE: - """Returns a mask from an image or a default mask with the color.""" - cc = image.shape[2] if image.ndim == 3 else 1 - height, width = image.shape[:2] - if cc == 4: - return np.expand_dims(image[:,:,3], -1) - return channel_solid(width, height, color, EnumImageType.GRAYSCALE) + """Create a mask from the image, preserving transparency.""" + if image.ndim == 3 and image.shape[2] == 4: + return image[..., 3] + return np.ones_like(image, dtype=np.uint8) * color -def image_mask_add(image:TYPE_IMAGE, mask:TYPE_IMAGE=None) -> TYPE_IMAGE: - """Places a default or custom mask into an image. +def image_mask_add(image:TYPE_IMAGE, mask:TYPE_IMAGE=None, alpha:float=255) -> TYPE_IMAGE: + """Put custom mask into an image. If there is no mask, alpha is applied. Images are expanded to 4 channels. Existing 4 channel images with no mask input just return themselves. """ - h, w = image.shape[:2] image = image_convert(image, 4) if mask is None: - mask = image_mask(image) + mask = np.full_like(image, alpha, np.uint8) else: - mask = image_grayscale(mask) - mask = image_scalefit(mask, w, h, EnumScaleMode.CROP) - image[:,:,3] = mask[:,:,0] + mask = image_convert(image, 1) + image[..., 3] = mask[...,0] return image -def image_matte(image:TYPE_IMAGE, color:TYPE_PIXEL=(0,0,0,255), - width:int=None, height:int=None, imageB:TYPE_IMAGE=None) -> TYPE_IMAGE: - """Puts an image atop a colored matte.""" - cc = image.shape[2] if image.ndim == 3 else 1 - h, w = image.shape[:2] - width = width if width is not None else w - height = height if height is not None else h - width = max(w, width) - height = max(h, height) - y1 = max(0, (height - h) // 2) - y2 = min(height, y1 + h) - x1 = max(0, (width - w) // 2) - x2 = min(width, x1 + w) - if cc != 4: - image = image_convert(image, 4) - # save the old alpha channel - mask_chan = image_mask(image)[:,:,0] - if imageB is not None: - matte = image_scalefit(matte, width, height, EnumScaleMode.FIT) - matte = image_convert(imageB, 4) +def image_mask_binary(image: TYPE_IMAGE) -> TYPE_IMAGE: + """ + Convert an image to a binary mask where non-black pixels are 1 and black pixels are 0. + Supports BGR, single-channel grayscale, and RGBA images. + + Args: + image (TYPE_IMAGE): Input image in BGR, grayscale, or RGBA format. + + Returns: + TYPE_IMAGE: Binary mask with the same width and height as the input image, where + pixels are 1 for non-black and 0 for black. + """ + if image.ndim == 2: + # Grayscale image + gray = image + elif image.shape[2] == 3: + # BGR image + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + elif image.shape[2] == 4: + # RGBA image + alpha_channel = image[..., 3] + # Create a mask from the alpha channel where alpha > 0 + alpha_mask = alpha_channel > 0 + # Convert RGB to grayscale + gray = cv2.cvtColor(image[:, :, :3], cv2.COLOR_BGR2GRAY) + # Apply the alpha mask to the grayscale image + gray = cv2.bitwise_and(gray, gray, mask=alpha_mask.astype(np.uint8)) else: - matte = channel_solid(width, height, color, EnumImageType.BGRA) - matte[y1:y2, x1:x2, 3] = mask_chan - alpha = cv2.bitwise_not(mask_chan) - alpha = cv2.cvtColor(alpha, cv2.COLOR_GRAY2BGRA) / 255.0 - matte[y1:y2, x1:x2] = cv2.convertScaleAbs(image * (1 - alpha) + matte[y1:y2, x1:x2] * alpha) - if cc == 4: - matte[y1:y2, x1:x2, 3] = mask_chan + raise ValueError("Unsupported image format") + + # Create a binary mask where any non-black pixel is set to 1 + _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY) + return mask.astype(np.uint8) + +def image_matte(image:TYPE_IMAGE, color:TYPE_PIXEL=(0, 0, 0, 255), width:int=None, height:int=None) -> TYPE_IMAGE: + """ + Puts an image atop a colored matte with the same dimensions as the image. + + Args: + image (TYPE_IMAGE): The input image. + color (TYPE_PIXEL): The color of the matte as a tuple (R, G, B, A). + + Returns: + TYPE_IMAGE: The composited image on a matte. + """ + + # Determine the dimensions of the matte + image_height, image_width = image.shape[:2] + width = width or image_width + height = height or image_height + + # solid matte + matte = np.full((height, width, 4), color, dtype=np.uint8) + + # Position the image in the center of the matte + x_offset = (width - image_width) // 2 + y_offset = (height - image_height) // 2 + + # everything 4 channel... + image = image_convert(image, 4) + + # Composite the image onto the matte + matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width] = image return matte def image_merge(imageA: TYPE_IMAGE, imageB: TYPE_IMAGE, axis: int=0, flip: bool=False) -> TYPE_IMAGE: @@ -1496,6 +1517,15 @@ def image_mirror_mandela(imageA: np.ndarray, imageB: np.ndarray) -> Tuple[np.nda imageB = np.vstack([top, bottom]) return imageA, imageB +def image_normalize(image: TYPE_IMAGE) -> TYPE_IMAGE: + image = image.astype(np.float32) + img_min = np.min(image) + img_max = np.max(image) + if img_min == img_max: + return np.zeros_like(image, dtype=np.float32) + image = (image - img_min) / (img_max - img_min) + return (image * 255).astype(np.uint8) + def image_pixelate(image: TYPE_IMAGE, amount:float=1.)-> TYPE_IMAGE: h, w = image.shape[:2] @@ -1754,17 +1784,34 @@ def image_threshold(image:TYPE_IMAGE, threshold:float=0.5, _, image = cv2.threshold(image, threshold, 255, mode.value) return bgr2image(image, alpha, cc == 1) -def image_translate(image: TYPE_IMAGE, offset:TYPE_COORD=(0.0, 0.0), edge:EnumEdge=EnumEdge.CLIP) -> TYPE_IMAGE: +def image_translate(image: TYPE_IMAGE, offset: TYPE_COORD = (0.0, 0.0), edge: EnumEdge = EnumEdge.CLIP) -> TYPE_IMAGE: + """ + Translates an image by a given offset. Supports various edge handling methods. + + Args: + image (TYPE_IMAGE): Input image as a numpy array. + offset (TYPE_COORD): Tuple (offset_x, offset_y) representing the translation offset. + edge (EnumEdge): Enum representing edge handling method. Options are 'CLIP', 'WRAP', 'WRAPX', 'WRAPY'. + + Returns: + TYPE_IMAGE: Translated image. + """ def translate(img: TYPE_IMAGE) -> TYPE_IMAGE: height, width = img.shape[:2] - scalarX = 0.333 if edge in [EnumEdge.WRAP, EnumEdge.WRAPX] else 1. - scalarY = 0.333 if edge in [EnumEdge.WRAP, EnumEdge.WRAPY] else 1. + scalarX = 0.333 if edge in [EnumEdge.WRAP, EnumEdge.WRAPX] else 1.0 + scalarY = 0.333 if edge in [EnumEdge.WRAP, EnumEdge.WRAPY] else 1.0 M = np.float32([[1, 0, offset[0] * width * scalarX], [0, 1, offset[1] * height * scalarY]]) - return cv2.warpAffine(img, M, (width, height), flags=cv2.INTER_LINEAR) + if edge == EnumEdge.CLIP: + border_mode = cv2.BORDER_CONSTANT + border_value = 0 # You can change this value to suit your needs + else: + border_mode = cv2.BORDER_WRAP - return image_affine_edge(image, translate, edge) + return cv2.warpAffine(img, M, (width, height), flags=cv2.INTER_LINEAR, borderMode=border_mode, borderValue=border_value) + + return translate(image) def image_transform(image: TYPE_IMAGE, offset:TYPE_COORD=(0.0, 0.0), angle:float=0, scale:TYPE_COORD=(1.0, 1.0), sample:EnumInterpolation=EnumInterpolation.LANCZOS4, edge:EnumEdge=EnumEdge.CLIP) -> TYPE_IMAGE: sX, sY = scale @@ -1889,7 +1936,7 @@ def color_match_histogram(image: TYPE_IMAGE, usermap: TYPE_IMAGE) -> TYPE_IMAGE: image = image_blend(usermap, image, blendOp=BlendType.LUMINOSITY) image = image_convert(image, cc) if cc == 4: - image[:,:,3] = alpha[:,:,0] + image[..., 3] = alpha[..., 0] return image def color_match_reinhard(image: TYPE_IMAGE, target: TYPE_IMAGE) -> TYPE_IMAGE: @@ -1918,7 +1965,7 @@ def color_match_lut(image: TYPE_IMAGE, colormap:int=cv2.COLORMAP_JET, image = cv2.addWeighted(image, 0.5, image, 0.5, 0) image = image_convert(image, cc) if cc == 4: - image[:,:,3] = alpha[:,:,0] + image[..., 3] = alpha[..., 0] return image def color_mean(image: TYPE_IMAGE) -> TYPE_IMAGE: @@ -1930,7 +1977,7 @@ def color_mean(image: TYPE_IMAGE) -> TYPE_IMAGE: else: # each channel.... color = [ - int(np.mean(image[:,:,0])), + int(np.mean(image[..., 0])), int(np.mean(image[:,:,1])), int(np.mean(image[:,:,2])) ] return color @@ -2093,7 +2140,7 @@ def remap_fisheye(image: TYPE_IMAGE, distort: float) -> TYPE_IMAGE: map_x, map_y = coord_fisheye(width, height, distort) image = cv2.remap(image, map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT) #if cc == 1: - # image = image[:,:,0] + # image = image[..., 0] return image def remap_perspective(image: TYPE_IMAGE, pts: list) -> TYPE_IMAGE: @@ -2104,7 +2151,7 @@ def remap_perspective(image: TYPE_IMAGE, pts: list) -> TYPE_IMAGE: pts = coord_perspective(width, height, pts) image = cv2.warpPerspective(image, pts, (width, height)) #if cc == 1: - # image = image[:,:,0] + # image = image[..., 0] return image def remap_polar(image: TYPE_IMAGE) -> TYPE_IMAGE: diff --git a/sup/shader.py b/sup/shader.py index 0b92dab..5762bde 100644 --- a/sup/shader.py +++ b/sup/shader.py @@ -47,8 +47,8 @@ PTYPE = { 'sampler2D': EnumConvertType.IMAGE } -RE_VARIABLE = re.compile(r"uniform\s*(\w*)\s*(\w*);(?:.*\/{2}\s*([A-Za-z0-9\-\.,\s]+)){0,1}(\|[A-Za-z0-9\s]+)?$", re.MULTILINE) -RE_SHADER_META = re.compile(r"\/\/\s(name|desc):\s([A-Za-z\s]+)$", re.MULTILINE) +RE_VARIABLE = re.compile(r"uniform\s+(\w+)\s+(\w+);\s*\/\/\s*([0-9.,\s]*)\s*(?:;\s*([0-9.-]+))?\s*(?:;\s*([0-9.-]+))?\s*(?:;\s*([0-9.-]+))?\s*(?:\|\s*(.*))?$", re.MULTILINE) +RE_SHADER_META = re.compile(r"\/\/\s?([A-Za-z\_]{3,}):\s?([A-Za-z\_\s]+)$", re.MULTILINE) # ============================================================================= @@ -98,13 +98,11 @@ void main() def __init__(self, vertex:str=None, fragment:str=None, width:int=IMAGE_SIZE_DEFAULT, height:int=IMAGE_SIZE_DEFAULT, fps:int=30) -> None: if not glfw.init(): raise RuntimeError("GLFW did not init") - glfw.window_hint(glfw.VISIBLE, glfw.FALSE) # hidden + glfw.window_hint(glfw.VISIBLE, glfw.FALSE) self.__window = glfw.create_window(width, height, "hidden", None, None) if not self.__window: raise RuntimeError("GLFW did not init window") glfw.make_context_current(self.__window) - #gl.glEnable(gl.GL_BLEND) - #gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) self.__size_changed = False self.__size: Tuple[int, int] = (max(width, IMAGE_SIZE_MIN), max(height, IMAGE_SIZE_MIN)) @@ -131,7 +129,7 @@ void main() gl.glCompileShader(shader) if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE: raise CompileException(gl.glGetShaderInfoLog(shader)) - logger.debug(f"{shader_type} compiled") + # logger.debug(f"{shader_type} compiled") return shader def __framebuffer(self) -> None: @@ -305,11 +303,11 @@ void main() self.__userVar = {} # read the fragment and setup the vars.... for match in RE_VARIABLE.finditer(fragment): - typ, name, default, tooltip = match.groups() + typ, name, default, val_min, val_max, val_step, tooltip = match.groups() tex_loc = None if typ in ['sampler2D']: tex_loc = gl.glGenTextures(1) - logger.debug(f"{name}.{typ}: {default}") + logger.debug(f"{name}.{typ}: {default} {val_min} {val_max} {val_step} {tooltip}") self.__userVar[name] = [ # type typ, @@ -321,9 +319,7 @@ void main() tex_loc ] - logger.info("program changed") - self.render() - self.render() + logger.info("program compiled") def render(self, time_delta:float=0., **kw) -> np.ndarray: glfw.make_context_current(self.__window) diff --git a/sup/util.py b/sup/util.py index f91965b..78d7f0a 100644 --- a/sup/util.py +++ b/sup/util.py @@ -201,9 +201,9 @@ def parse_value(val:Any, typ:EnumConvertType, default: Any, d = default[idx] if isinstance(default, (list, tuple, set, dict, torch.Tensor)) and idx < len(default) else 0 v = d if val is None else val[idx] if idx < len(val) else d if isinstance(v, (str, )): + v = v.strip('\n').strip() if v == '': v = 0 - v = v.strip('\n') try: if typ in [EnumConvertType.FLOAT, EnumConvertType.VEC2, EnumConvertType.VEC3, EnumConvertType.VEC4]: v = round(float(v), 16) @@ -308,10 +308,8 @@ def parse_param(data:dict, key:str, typ:EnumConvertType, default: Any, elif isinstance(val, (torch.Tensor,)): if val.ndim > 3: val = [t for t in val] - else: - while (val.ndim < 3): - val = val.unsqueeze(-1) - val = [val] + elif val.ndim == 3: + val = [v.unsqueeze(-1) for v in val] elif isinstance(val, (list, tuple, set)): if len(val) == 0: val = [None] diff --git a/web/core/core_colorize.js b/web/core/core_colorize.js index 38c5bf7..a68c49e 100644 --- a/web/core/core_colorize.js +++ b/web/core/core_colorize.js @@ -72,11 +72,11 @@ app.registerExtension({ multipleInstances: false, appendTo: this.config_dialog.element, noAlpha: false, - init: function(elm, rgb) { + init: function(elm, rgb) { elm.style.backgroundColor = elm.color || LiteGraph.WIDGET_BGCOLOR; elm.style.color = rgb.RGBLuminance > 0.22 ? '#222' : '#ddd' }, - convertCallback: function(data, options) { + convertCallback: function(data) { var AHEX = this.patch.attributes.color if (AHEX === undefined) return var name = this.patch.attributes.name.value @@ -114,7 +114,7 @@ app.registerExtension({ node_color_all(); } }, - async beforeRegisterNodeDef(nodeType, nodeData) { + async beforeRegisterNodeDef(nodeType) { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = async function () { const me = onNodeCreated?.apply(this, arguments); diff --git a/web/core/core_config.js b/web/core/core_config.js index 3aaf03e..04ab5e8 100644 --- a/web/core/core_config.js +++ b/web/core/core_config.js @@ -57,11 +57,13 @@ const templateColorRegex = ({ idx, name, background, title, body }) => ( ]) ); +/* const colorClear = (name) => { api_post("/jovimetrix/config/clear", { name }); delete util_config.CONFIG_THEME[name]; if (util_config.CONFIG_COLOR.overwrite) node_color_all(); }; +*/ export class JovimetrixConfigDialog extends ComfyDialog { constructor() { diff --git a/web/core/core_cozy_data.js b/web/core/core_cozy_data.js index 5a30f8d..f28dd7f 100644 --- a/web/core/core_cozy_data.js +++ b/web/core/core_cozy_data.js @@ -14,10 +14,10 @@ const JDataBucket = (app, name, opts) => { type: "JDATABUCKET", hidden: true, options: options, - draw: function (ctx, node, width, Y, height) { + draw: function () { return; }, - computeSize: function (width) { + computeSize: function () { return [0, 0]; } } @@ -26,7 +26,7 @@ const JDataBucket = (app, name, opts) => { app.registerExtension({ name: "jovimetrix.data.bucket", - async getCustomWidgets(app) { + async getCustomWidgets() { return { JDATABUCKET: (node, inputName, inputData, app) => ({ widget: node.addCustomWidget(JDataBucket(app, inputName, inputData[1])) diff --git a/web/core/core_cozy_fields.js b/web/core/core_cozy_fields.js index 26b28f6..d7db3fa 100644 --- a/web/core/core_cozy_fields.js +++ b/web/core/core_cozy_fields.js @@ -58,7 +58,7 @@ app.registerExtension({ }, }); }, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType) { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = async function () { const me = onNodeCreated.apply(this, arguments); @@ -74,7 +74,7 @@ app.registerExtension({ }, async nodeCreated(node) { const onDrawForeground = node.onDrawForeground; - node.onDrawForeground = async function (ctx, area) { + node.onDrawForeground = async function (ctx) { const me = onDrawForeground?.apply(this, arguments); if (this.widgets) { ctx.save(); @@ -85,7 +85,9 @@ app.registerExtension({ try { color = hex2rgb(g_highlight); color = g_highlight - } catch { } + } catch { + + } } if (g_color_style == "Round Highlight") { const thick = Math.max(1, Math.min(3, g_thickness)); diff --git a/web/core/core_cozy_menu.js b/web/core/core_cozy_menu.js index af28ce2..895ca93 100644 --- a/web/core/core_cozy_menu.js +++ b/web/core/core_cozy_menu.js @@ -8,7 +8,7 @@ import { CONVERTED_TYPE, convertToInput } from '../util/util_widget.js' app.registerExtension({ name: "jovimetrix.cozy.menu", - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (!nodeData.name.includes("(JOV)")) { return; } @@ -40,7 +40,7 @@ app.registerExtension({ (widget.options?.forceInput === undefined || widget.options?.forceInput === false) && widget.options?.menu !== false) { const convertToInputObject = { - content: `Convsert ${widget.name} to input`, + content: `Convert ${widget.name} to input`, callback: () => convertToInput(this, widget, widgetType) }; convertToInputArray.push(convertToInputObject); diff --git a/web/core/core_cozy_tips.js b/web/core/core_cozy_tips.js index 5fdec68..5b5183f 100644 --- a/web/core/core_cozy_tips.js +++ b/web/core/core_cozy_tips.js @@ -16,10 +16,10 @@ const JTooltipWidget = (app, name, opts) => { type: "JTOOLTIP", hidden: true, options: options, - draw: function (ctx, node, width, Y, height) { + draw: function () { return; }, - computeSize: function (width) { + computeSize: function () { return [0, 0]; } } diff --git a/web/core/core_help.js b/web/core/core_help.js index dd3146c..a677574 100644 --- a/web/core/core_help.js +++ b/web/core/core_help.js @@ -344,7 +344,7 @@ app.registerExtension({ // ? clicked const mouseDown = nodeType.prototype.onMouseDown - nodeType.prototype.onMouseDown = function (e, localPos, canvas) { + nodeType.prototype.onMouseDown = function (e, localPos) { const r = mouseDown ? mouseDown.apply(this, arguments) : undefined const iconX = this.size[0] - iconSize - iconMargin const iconY = iconSize - 34 diff --git a/web/nodes/adjust.js b/web/nodes/adjust.js index 1f1ac6c..6b7a670 100644 --- a/web/nodes/adjust.js +++ b/web/nodes/adjust.js @@ -12,7 +12,7 @@ const _id = "ADJUST (JOV) 🕸️" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return } diff --git a/web/nodes/akashic.js b/web/nodes/akashic.js index 3d27bf3..7059077 100644 --- a/web/nodes/akashic.js +++ b/web/nodes/akashic.js @@ -50,13 +50,8 @@ app.registerExtension({ if (message.text != null) { let new_val = message.text.map((txt, index) => `${index}: ${txt}`).join('\n'); this.message.value = new_val; - for (let char of new_val) { - if (char === '\n') { - lineCount++; - } - } } - //fitHeight(this); + // fitHeight(this); return me; } } diff --git a/web/nodes/array.js b/web/nodes/array.js index 265ee90..122dbf3 100644 --- a/web/nodes/array.js +++ b/web/nodes/array.js @@ -13,7 +13,7 @@ const _prefix = '❔' app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/blend.js b/web/nodes/blend.js index aabb668..67b0181 100644 --- a/web/nodes/blend.js +++ b/web/nodes/blend.js @@ -11,7 +11,7 @@ const _id = "BLEND (JOV) ⚗️" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/calc_binary.js b/web/nodes/calc_binary.js index e23824c..06ff7e3 100644 --- a/web/nodes/calc_binary.js +++ b/web/nodes/calc_binary.js @@ -12,7 +12,7 @@ const _id = "OP BINARY (JOV) 🌟" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } @@ -25,7 +25,7 @@ app.registerExtension({ } const onConnectionsChange = nodeType.prototype.onConnectionsChange - nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info, data) { + nodeType.prototype.onConnectionsChange = function (slotType) { if (slotType === TypeSlot.Input) { const widget_combo = this.widgets.find(w => w.name === '❓'); setTimeout(() => { widget_combo.callback(); }, 10); diff --git a/web/nodes/color_match.js b/web/nodes/color_match.js index 40e8018..e51b680 100644 --- a/web/nodes/color_match.js +++ b/web/nodes/color_match.js @@ -12,7 +12,7 @@ const _id = "COLOR MATCH (JOV) 💞" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return } diff --git a/web/nodes/color_theory.js b/web/nodes/color_theory.js index eb81426..61b50da 100644 --- a/web/nodes/color_theory.js +++ b/web/nodes/color_theory.js @@ -11,7 +11,7 @@ const _id = "COLOR THEORY (JOV) 🛞" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return } diff --git a/web/nodes/constant.js b/web/nodes/constant.js index 4931c42..a0bb000 100644 --- a/web/nodes/constant.js +++ b/web/nodes/constant.js @@ -11,7 +11,7 @@ const _id = "CONSTANT (JOV) 🟪" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/crop.js b/web/nodes/crop.js index 941848f..49e4181 100644 --- a/web/nodes/crop.js +++ b/web/nodes/crop.js @@ -12,7 +12,7 @@ const _id = "CROP (JOV) ✂️" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/delay.js b/web/nodes/delay.js index 8f89516..11d4444 100644 --- a/web/nodes/delay.js +++ b/web/nodes/delay.js @@ -67,21 +67,21 @@ app.registerExtension({ // app.canvas.setDirty(true); } - async function python_delay_update(event) { + async function python_delay_update() { } api.addEventListener(EVENT_JOVI_DELAY, python_delay_user); api.addEventListener(EVENT_JOVI_UPDATE, python_delay_update); this.onDestroy = () => { - api.removeEventListener(EVENT_JOVI_DELAY, python_glsl_error); + api.removeEventListener(EVENT_JOVI_DELAY, python_delay_user); api.removeEventListener(EVENT_JOVI_UPDATE, python_delay_update); }; return me; } const onExecutionStart = nodeType.prototype.onExecutionStart - nodeType.prototype.onExecutionStart = function (message) { + nodeType.prototype.onExecutionStart = function() { onExecutionStart?.apply(this, arguments); self.total_timeout = 0; } diff --git a/web/nodes/export.js b/web/nodes/export.js index 8bdc762..78fbdcb 100644 --- a/web/nodes/export.js +++ b/web/nodes/export.js @@ -12,7 +12,7 @@ const _id = "EXPORT (JOV) 📽" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/filter_mask.js b/web/nodes/filter_mask.js index 2e54caa..71f4e1b 100644 --- a/web/nodes/filter_mask.js +++ b/web/nodes/filter_mask.js @@ -12,7 +12,7 @@ const _id = "FILTER MASK (JOV) 🤿" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return } diff --git a/web/nodes/flatten.js b/web/nodes/flatten.js index 97ea935..8da9001 100644 --- a/web/nodes/flatten.js +++ b/web/nodes/flatten.js @@ -13,7 +13,7 @@ const _prefix = '👾' app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/glsl.js b/web/nodes/glsl.js index 93143d6..41c5195 100644 --- a/web/nodes/glsl.js +++ b/web/nodes/glsl.js @@ -8,14 +8,13 @@ import { api } from "../../../scripts/api.js"; import { app } from "../../../scripts/app.js"; import { fitHeight } from '../util/util.js' import { widget_hide, widget_show } from '../util/util_widget.js'; -import { api_post, api_cmd_jovian } from '../util/util_api.js'; +import { api_cmd_jovian } from '../util/util_api.js'; import { flashBackgroundColor } from '../util/util_fun.js'; const _id = "GLSL (JOV) 🍩"; const EVENT_JOVI_GLSL_ERROR = "jovi-glsl-error"; const EVENT_JOVI_GLSL_TIME = "jovi-glsl-time"; -const EVENT_JOVI_GLSL_REGISTER = "jovi-register-glsl"; -const RE_VARIABLE = /uniform\s*(\w*)\s*(\w*);(?:.*\/{2}\s*([A-Za-z0-9\-\.,\s]+)){0,1}\s*$/gm +const RE_VARIABLE = /uniform\s+(\w+)\s+(\w+);\s*\/\/\s*([0-9.,\s]*)\s*(?:;\s*([0-9.-]+))?\s*(?:;\s*([0-9.-]+))?\s*(?:;\s*([0-9.-]+))?\s*(?:\|\s*(.*))?$/gm app.registerExtension({ name: 'jovimetrix.node.' + _id, diff --git a/web/nodes/glsl_dynamic.js b/web/nodes/glsl_dynamic.js index b254b84..f12ea63 100644 --- a/web/nodes/glsl_dynamic.js +++ b/web/nodes/glsl_dynamic.js @@ -12,7 +12,6 @@ import { api_cmd_jovian } from '../util/util_api.js'; const _id = "GLSL DYNAMIC (JOV) 🧙🏽"; const EVENT_JOVI_GLSL_TIME = "jovi-glsl-time"; -const RE_VARIABLE = /uniform\s*(\w*)\s*(\w*);(?:.*\/{2}\s*([A-Za-z0-9\-\.,\s]+)){0,1}\s*$/gm app.registerExtension({ name: 'jovimetrix.node.' + _id, diff --git a/web/nodes/gradient_map.js b/web/nodes/gradient_map.js index bda0302..1e9b728 100644 --- a/web/nodes/gradient_map.js +++ b/web/nodes/gradient_map.js @@ -11,7 +11,7 @@ const _id = "GRADIENT MAP (JOV) 🇲🇺" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/graph.js b/web/nodes/graph.js index d1f41db..0d46dc1 100644 --- a/web/nodes/graph.js +++ b/web/nodes/graph.js @@ -33,7 +33,7 @@ app.registerExtension({ const me = onNodeCreated?.apply(this); const self = this; const widget_reset = this.widgets.find(w => w.name === 'RESET'); - widget_reset.callback = async (e) => { + widget_reset.callback = async() => { widget_reset.value = false; api_cmd_jovian(self.id, "reset"); } @@ -41,7 +41,7 @@ app.registerExtension({ } const onConnectionsChange = nodeType.prototype.onConnectionsChange - nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info, data) { + nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info) { const me = onConnectionsChange?.apply(this, arguments); if (!link_info || slot == this.inputs.length) { return; diff --git a/web/nodes/lerp.js b/web/nodes/lerp.js index 88f44f2..49139ec 100644 --- a/web/nodes/lerp.js +++ b/web/nodes/lerp.js @@ -12,7 +12,7 @@ const _id = "LERP (JOV) 🔰" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/pixel_merge.js b/web/nodes/pixel_merge.js index d57e496..f18fb42 100644 --- a/web/nodes/pixel_merge.js +++ b/web/nodes/pixel_merge.js @@ -11,7 +11,7 @@ const _id = "PIXEL MERGE (JOV) 🫂" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/pixel_swap.js b/web/nodes/pixel_swap.js index a2b2d5d..2f79333 100644 --- a/web/nodes/pixel_swap.js +++ b/web/nodes/pixel_swap.js @@ -12,7 +12,7 @@ const _id = "PIXEL SWAP (JOV) 🔃" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return } diff --git a/web/nodes/queue.js b/web/nodes/queue.js index da9b142..b09079c 100644 --- a/web/nodes/queue.js +++ b/web/nodes/queue.js @@ -49,7 +49,7 @@ app.registerExtension({ const widget_hold = this.widgets.find(w => w.name === '✋🏽'); const widget_reset = this.widgets.find(w => w.name === 'RESET'); const widget_value = this.widgets.find(w => w.name === 'VAL'); - widget_value.callback = async (e) => { + widget_value.callback = async() => { widget_hide(this, widget_hold, '-jov'); widget_hide(this, widget_reset, '-jov'); if (widget_value.value == 0) { @@ -59,12 +59,12 @@ app.registerExtension({ fitHeight(this); } - widget_queue?.inputEl.addEventListener('input', function (event) { + widget_queue?.inputEl.addEventListener('input', function () { const value = widget_queue.value.split('\n'); update_list(self, value); }); - widget_reset.callback = async (e) => { + widget_reset.callback = async() => { widget_reset.value = false; api_cmd_jovian(self.id, "reset"); } @@ -92,9 +92,11 @@ app.registerExtension({ if (event.detail.id != self.id) { return; } + /* let centerX = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; let centerY = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; - // util_fun.bewm(centerX / 2, centerY / 3); + util_fun.bewm(centerX / 2, centerY / 3); + */ await flashBackgroundColor(self.widget_queue.inputEl, 650, 4, "#995242CC"); } @@ -111,52 +113,47 @@ app.registerExtension({ } const onConnectOutput = nodeType.prototype.onConnectOutput; - nodeType.prototype.onConnectOutput = function(outputIndex, inputType, inputSlot, inputNode, inputIndex) { - if (outputIndex == 0) { - if (inputType == "COMBO") { - // can link the "same" list -- user breaks it past that, their problem atm. - const widget = inputNode.widgets.find(w => w.name === inputSlot.name); - if (this.outputs[0].name != _prefix && this.widget_queue.value != widget.options.values.join('\n')) { - return false; - } + nodeType.prototype.onConnectOutput = function(outputIndex, inputType, inputSlot, inputNode) { + if (outputIndex == 0 && inputType == "COMBO") { + // can link the "same" list -- user breaks it past that, their problem atm. + + const widget_queue = this.widgets.find(w => w.name === 'Q'); + const widget = inputNode.widgets.find(w => w.name === inputSlot.name); + const values = widget.options.values.join('\n'); + if (this.outputs[0].name != _prefix && widget_queue.value != values) { + return false; } } return onConnectOutput?.apply(this, arguments); } const onConnectionsChange = nodeType.prototype.onConnectionsChange; - nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info, data) + nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info) //side, slot, connected, link_info { - if (slotType === TypeSlot.Output && slot == 0) { - if (link_info){ - if (event === TypeSlotEvent.Connect) { - const node = app.graph.getNodeById(link_info.target_id); - if (node === undefined || node.inputs === undefined) { - return; - } - const target = node.inputs[link_info.target_slot]; - if (target === undefined) { - return; - } - - const widget = node.widgets?.find(w => w.name === target.name); - if (widget === undefined) { - return; - } - this.outputs[0].name = widget.name; - if (widget?.origType == "combo" || widget.type == "COMBO") { - const values = widget.options.values; - // remove all connections that don't match the list? - this.widget_queue.value = values.join('\n'); - update_list(this, values); - } - } else { - this.outputs[0].name = _prefix; - } - } else { - this.outputs[0].name = _prefix; + if (slotType === TypeSlot.Output && slot == 0 && link_info && event === TypeSlotEvent.Connect) { + const node = app.graph.getNodeById(link_info.target_id); + if (node === undefined || node.inputs === undefined) { + return; } + const target = node.inputs[link_info.target_slot]; + if (target === undefined) { + return; + } + + const widget = node.widgets?.find(w => w.name === target.name); + if (widget === undefined) { + return; + } + this.outputs[0].name = widget.name; + if (widget?.origType == "combo" || widget.type == "COMBO") { + const values = widget.options.values; + const widget_queue = this.widgets.find(w => w.name === 'Q'); + // remove all connections that don't match the list? + widget_queue.value = values.join('\n'); + update_list(this, values); + } + this.outputs[0].name = _prefix; } return onConnectionsChange?.apply(this, arguments); }; diff --git a/web/nodes/route.js b/web/nodes/route.js index 22c0300..114b349 100644 --- a/web/nodes/route.js +++ b/web/nodes/route.js @@ -12,7 +12,7 @@ const _prefix = '🔮' app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } @@ -20,7 +20,7 @@ app.registerExtension({ nodeType = node_add_dynamic_route(nodeType, _prefix); const onConnectionsChange = nodeType.prototype.onConnectionsChange - nodeType.prototype.onConnectionsChange = function (slotType, slot_idx, event, link_info, node_slot) { + nodeType.prototype.onConnectionsChange = function (slotType, slot_idx, event, link_info) { const me = onConnectionsChange?.apply(this, arguments); if (slot_idx == 0) { if (event === TypeSlotEvent.Connect && slotType === TypeSlot.Input && link_info) { diff --git a/web/nodes/select.js b/web/nodes/select.js index c70ed63..b4e6f8d 100644 --- a/web/nodes/select.js +++ b/web/nodes/select.js @@ -13,7 +13,7 @@ const _prefix = '❔' app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } @@ -24,7 +24,7 @@ app.registerExtension({ nodeType.prototype.onNodeCreated = function () { const me = onNodeCreated?.apply(this) const widget_reset = this.widgets.find(w => w.name === 'RESET'); - widget_reset.callback = async (e) => { + widget_reset.callback = async() => { widget_reset.value = false; api_cmd_jovian(self.id, "reset"); } diff --git a/web/nodes/shape_gen.js b/web/nodes/shape_gen.js index 1d8ffe6..fa613c8 100644 --- a/web/nodes/shape_gen.js +++ b/web/nodes/shape_gen.js @@ -12,7 +12,7 @@ const _id = "SHAPE GEN (JOV) ✨" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } @@ -34,7 +34,7 @@ app.registerExtension({ } const onConnectionsChange = nodeType.prototype.onConnectionsChange - nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info, data) { + nodeType.prototype.onConnectionsChange = function (slotType, slot) { if (slotType === TypeSlot.Input && slot.name == 'SHAPE') { const widget_combo = this.widgets.find(w => w.name === 'SHAPE'); setTimeout(() => { widget_combo.callback(); }, 10); @@ -43,7 +43,7 @@ app.registerExtension({ } const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { + nodeType.prototype.onExecuted = function () { const widget_combo = this.widgets.find(w => w.name === 'SHAPE'); if (widget_combo.value == 'SHAPE') { setTimeout(() => { widget_combo.callback(); }, 10); diff --git a/web/nodes/spout_writer.js b/web/nodes/spout_writer.js index 2218b89..9959a7a 100644 --- a/web/nodes/spout_writer.js +++ b/web/nodes/spout_writer.js @@ -11,7 +11,7 @@ const _id = "SPOUT WRITER (JOV) 🎥" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/stack.js b/web/nodes/stack.js index 480be63..08c7e7d 100644 --- a/web/nodes/stack.js +++ b/web/nodes/stack.js @@ -14,7 +14,7 @@ const _prefix = '👾' app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/stream_reader.js b/web/nodes/stream_reader.js index 3b2bd7f..6af2a19 100644 --- a/web/nodes/stream_reader.js +++ b/web/nodes/stream_reader.js @@ -13,7 +13,7 @@ const _id = "STREAM READER (JOV) 📺" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return } diff --git a/web/nodes/stream_writer.js b/web/nodes/stream_writer.js index fae0562..9461218 100644 --- a/web/nodes/stream_writer.js +++ b/web/nodes/stream_writer.js @@ -11,7 +11,7 @@ const _id = "STREAM WRITER (JOV) 🎞️" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/swizzle.js b/web/nodes/swizzle.js index e8a040d..40b6cc4 100644 --- a/web/nodes/swizzle.js +++ b/web/nodes/swizzle.js @@ -13,7 +13,7 @@ const _id = "SWIZZLE (JOV) 😵" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return } diff --git a/web/nodes/text_generator.js b/web/nodes/text_generator.js index f548967..fed01fc 100644 --- a/web/nodes/text_generator.js +++ b/web/nodes/text_generator.js @@ -12,7 +12,7 @@ const _id = "TEXT GEN (JOV) 📝" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return } diff --git a/web/nodes/tick.js b/web/nodes/tick.js index 3e19306..b928444 100644 --- a/web/nodes/tick.js +++ b/web/nodes/tick.js @@ -13,7 +13,7 @@ const EVENT_JOVI_TICK = "jovi-tick"; app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } @@ -22,7 +22,7 @@ app.registerExtension({ const me = onNodeCreated?.apply(this); const self = this; const widget_reset = this.widgets.find(w => w.name === 'RESET'); - widget_reset.callback = async (e) => { + widget_reset.callback = async() => { widget_reset.value = false; api_cmd_jovian(self.id, "reset"); } diff --git a/web/nodes/transform.js b/web/nodes/transform.js index 9ef1958..dd37749 100644 --- a/web/nodes/transform.js +++ b/web/nodes/transform.js @@ -13,7 +13,7 @@ const _id = "TRANSFORM (JOV) 🏝️" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } diff --git a/web/nodes/value.js b/web/nodes/value.js index 9ad5420..033efda 100644 --- a/web/nodes/value.js +++ b/web/nodes/value.js @@ -6,14 +6,14 @@ import { app } from "../../../scripts/app.js" import { hook_widget_AB } from '../util/util_jov.js' -import { fitHeight, TypeSlot } from '../util/util.js' +import { fitHeight } from '../util/util.js' import { widget_hide, process_any, widget_type_name } from '../util/util_widget.js' const _id = "VALUE (JOV) 🧬" app.registerExtension({ name: 'jovimetrix.node.' + _id, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== _id) { return; } @@ -22,7 +22,6 @@ app.registerExtension({ nodeType.prototype.onNodeCreated = function () { const me = onNodeCreated?.apply(this); - const widget_rng = this.widgets.find(w => w.name === 'seed'); const widget_str = this.widgets.find(w => w.name === '📝'); this.outputs[1].type = "*"; diff --git a/web/util/util.js b/web/util/util.js index defa82e..88e887f 100644 --- a/web/util/util.js +++ b/web/util/util.js @@ -89,7 +89,7 @@ export function node_add_dynamic(nodeType, prefix, dynamic_type='*', index_start while (idx < self.inputs.length-1) { const slot = self.inputs[idx]; const parts = slot.name.split('_'); - if (parts.length == 2) { + if (parts.length == 2 && self.graph) { if (slot.link == null) { if (match_output) { self.removeOutput(idx); @@ -290,23 +290,3 @@ export function showModal(innerHTML, eventCallback, timeout=null) { //}, 1000); }); } - -/* -* wraps a single text line into maxWidth chunks -*/ -function wrapText(text, maxWidth = 145) { - const words = text.split(' '); - const lines = []; - let currentLine = ''; - for (const word of words) { - const potentialLine = currentLine ? `${currentLine} ${word}` : word; - if (potentialLine.length <= maxWidth) { - currentLine = potentialLine; - } else { - if (currentLine) lines.push(currentLine); - currentLine = word; - } - } - if (currentLine) lines.push(currentLine); - return lines; -} \ No newline at end of file diff --git a/web/util/util_jov.js b/web/util/util_jov.js index a247022..d3f24e3 100644 --- a/web/util/util_jov.js +++ b/web/util/util_jov.js @@ -29,7 +29,7 @@ export function hook_widget_size_mode(node, wh_hide=true) { export function hook_widget_size_mode2(nodeType, wh_hide=true) { const onNodeCreated = nodeType.prototype.onNodeCreated - nodeType.prototype.onNodeCreated = function () { + nodeType.prototype.onNodeCreated = function (node) { const me = onNodeCreated?.apply(this); const wh = widget_find(node.widgets, '🇼🇭'); const samp = widget_find(node.widgets, '🎞️'); @@ -84,7 +84,7 @@ export function hook_widget_AB(node, control_key, match_output=-1) { widget.options.menu = false; widget.callback = () => { if (widget.type === "toggle") { - trackKey[0] = 1 ? widget.value : 0; + trackKey[0] = widget.value ? 1 : 0; } else { Object.keys(widget.value).forEach((key) => { trackKey[key] = widget.value[key]; diff --git a/web/util/util_widget.js b/web/util/util_widget.js index 6d70166..a98d0e2 100644 --- a/web/util/util_widget.js +++ b/web/util/util_widget.js @@ -77,7 +77,7 @@ export function widget_remove_all(node) { for (const w of node.widgets) { widget_remove(node, w); } - who.widgets.length = 0; + node.widgets.length = 0; } } diff --git a/web/widget/widget_combo.js b/web/widget/widget_combo.js deleted file mode 100644 index 0ae8a57..0000000 --- a/web/widget/widget_combo.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - File: widget_jimage.js - Project: Jovimetrix - - pythongossss to the rescue again - original: https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/betterCombos.js -*/ - -import { app } from "../../../scripts/app.js"; -import { ComfyWidgets } from "../../../scripts/widgets.js"; -import { $el } from "../../../scripts/ui.js"; - -app.registerExtension({ - name: "jovimetrix.widget.combo", - init() { - const splitBy = /\//; - - $el("style", { - textContent: ` - .litemenu-entry:hover .pysssss-combo-image { - display: block; - } - .pysssss-combo-image { - display: none; - position: absolute; - left: 0; - top: 0; - transform: translate(-100%, 0); - width: 384px; - height: 384px; - background-size: contain; - background-position: top right; - background-repeat: no-repeat; - filter: brightness(65%); - } - `, - parent: document.body, - }); - - function buildMenu(widget, values) { - const lookup = { - "": { options: [] }, - }; - - // Split paths into menu structure - for (let value of values) { - value = String(value); - const split = value.split(splitBy); - let path = ""; - for (let i = 0; i < split.length; i++) { - const s = split[i]; - const last = i === split.length - 1; - if (last) { - // Leaf node, manually add handler that sets the lora - lookup[path].options.push({ - ...value, - title: s, - callback: () => { - widget.value = value; - widget.callback(value); - app.graph.setDirtyCanvas(true); - }, - }); - } else { - const prevPath = path; - path += s + splitBy; - if (!lookup[path]) { - const sub = { - title: s, - submenu: { - options: [], - title: s, - }, - }; - - // Add to tree - lookup[path] = sub.submenu; - lookup[prevPath].options.push(sub); - } - } - } - } - return lookup[""].options; - } - - // Override COMBO widgets to patch their values - const combo = ComfyWidgets["COMBO"]; - ComfyWidgets["COMBO"] = function (node) { - const res = combo.apply(this, arguments); - let value = res.widget.value; - return res; - if (value !== 'combo+') { - return res; - } - let values = res.widget.options.values; - res.widget.value = values[0]; - let menu = null; - // Override the option values to check if we should render a menu structure - Object.defineProperty(res.widget.options, "values", { - get() { - let v = values; - if (!menu) { - // Only build the menu once - menu = buildMenu(res.widget, values); - } - v = menu; - - const valuesIncludes = v.includes; - v.includes = function (searchElement) { - const includesFromMenuItem = function (item) { - return includesFromMenuItems(item.submenu.options) - } - const includesFromMenuItems = function (items) { - for (const item of items) { - if (includesFromMenuItem(item)) { - return true; - } - } - return false; - } - const includes = valuesIncludes.apply(this, arguments) || includesFromMenuItems(this); - return includes; - } - - return v; - }, - set(v) { - // Options are changing (refresh) so reset the menu so it can be rebuilt if required - values = v; - menu = null; - }, - }); - - Object.defineProperty(res.widget, "value", { - get() { - return value; - }, - set(v) { - if (v?.submenu) { - // Dont allow selection of submenus - return; - } - value = v; - }, - }); - return res; - }; - }, -}); diff --git a/web/widget/widget_jimage.js b/web/widget/widget_jimage.js deleted file mode 100644 index 25438db..0000000 --- a/web/widget/widget_jimage.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * File: widget_jimage.js - * Project: Jovimetrix - */ - -import { app } from "../../../scripts/app.js" -import { offsetDOMWidget } from '../util/util_dom.js' - -export const JImageWidget = (app, name, value) => { - const w = { - name: name, - type: "JIMAGE", - value: value, - draw: function (ctx, node, widgetWidth, widgetY, height) { - const [cw, ch] = this.computeSize(widgetWidth) - offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) - }, - computeSize: function (width) { - const ratio = this.inputRatio || 1 - if (width) { - return [width, width / ratio + 4] - } - return [128, 128] - }, - onRemoved: function () { - if (this.inputEl) { - this.inputEl.remove() - } - }, - } - - w.inputEl = document.createElement('img') - w.inputEl.src = w.value - w.inputEl.onload = function () { - w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight - } - document.body.appendChild(w.inputEl) - return w -} - -app.registerExtension({ - name: "jovimetrix.widget.jimage", - async getCustomWidgets(app) { - return { - JIMAGE: (node, inputName, inputData, app) => ({ - widget: node.addCustomWidget(JImageWidget(app, inputName, inputData[0])), - }) - } - } -}) diff --git a/web/widget/widget_jlabel.js b/web/widget/widget_jlabel.js deleted file mode 100644 index 8dd6daa..0000000 --- a/web/widget/widget_jlabel.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * File: widget_jlabel.js - * Project: Jovimetrix - */ - -import { app } from "../../../scripts/app.js" -// import * as util from '../util/util.js' -// import { offsetDOMWidget } from '../util/util_dom.js' - -export const JLabelWidget = (label) => { - const widget = { - value: label, - type: "JLABEL", - options: { - serialize: false, - } - }; - - widget.draw = function(ctx, node, widget_width, y, widget_height) { - - } - - widget.mouse = function(event, pos, node) { - - }, - - widget.computeSize = function() { - return [0, 20]; - } - - return widget; -} - -app.registerExtension({ - name: "jovimetrix.widget.jlabel", - async getCustomWidgets(app) { - return { - JLABEL: (node, inputName, inputData, app) => ({ - widget: node.addCustomWidget(JLabelWidget(app, inputName, inputData[0])), - }) - } - } -}) diff --git a/web/widget/widget_jstring.js b/web/widget/widget_jstring.js deleted file mode 100644 index 2e01bb8..0000000 --- a/web/widget/widget_jstring.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * File: widget_jstring.js - * Project: Jovimetrix - */ - -import { app } from "../../../scripts/app.js" -import { offsetDOMWidget } from '../util/util_dom.js' - -const withFont = (ctx, font, cb) => { - const oldFont = ctx.font - ctx.font = font - cb() - ctx.font = oldFont -} - -const calculateTextDimensions = (ctx, value, width, fontSize = 12) => { - const words = value.split(' ') - const lines = [] - let currentLine = '' - for (const word of words) { - const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}` - const testWidth = ctx.measureText(testLine).width - if (testWidth > width) { - lines.push(currentLine) - currentLine = word - } else { - currentLine = testLine - } - } - if (lines.length === 0) lines.push(value) - const textHeight = (lines.length + 1) * fontSize - const maxLineWidth = lines.reduce( - (maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width), - 0 - ) - return { textHeight, maxLineWidth } - } - -export const JStringWidget = (app, name, value) => { - const fontSize = 16 - const w = { - name: name, - type: "JSTRING", - value: value, - draw: function (ctx, node, widgetWidth, widgetY, height) { - offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) - }, - computeSize(width) { - if (!this.value) { - return [32, 32] - } - if (!width) { - console.error(`No width ${this.parent.size}`) - } - let dimensions - withFont(app.ctx, `${fontSize}px`, () => { - dimensions = calculateTextDimensions(app.ctx, this.value, width) - }) - const widgetWidth = Math.max(width || this.width || 32, dimensions.maxLineWidth) - const widgetHeight = dimensions.textHeight * 1.5 - return [widgetWidth, widgetHeight] - }, - onRemoved: function () { - if (this.inputEl) { - this.inputEl.remove() - } - }, - get value() { - return this.inputEl.innerHTML - }, - set value(val) { - this.inputEl.innerHTML = val - this.parent?.setSize?.(this.parent?.computeSize()) - }, - } - - w.inputEl = document.createElement('p') - w.inputEl.style = ` - text-align: center; - font-size: ${fontSize}px; - color: var(--input-text); - line-height: 0; - font-family: monospace; - ` - // w.value = val - document.body.appendChild(w.inputEl) - return w -} - -app.registerExtension({ - name: "jovimetrix.widget.jstring", - async getCustomWidgets(app) { - return { - JSTRING: (node, inputName, inputData, app) => ({ - widget: node.addCustomWidget(JStringWidget(app, inputName, inputData[0])), - }) - } - } -}) diff --git a/web/widget/widget_vector.js b/web/widget/widget_vector.js index 316d462..60a7f16 100644 --- a/web/widget/widget_vector.js +++ b/web/widget/widget_vector.js @@ -4,7 +4,7 @@ */ import { app } from "../../../scripts/app.js" -import { CONVERTED_TYPE, convertToInput } from '../util/util_widget.js' +import { convertToInput } from '../util/util_widget.js' import { inner_value_change } from '../util/util_dom.js' import { hex2rgb, rgb2hex } from '../util/util_color.js' import { $el } from "../../../scripts/ui.js" @@ -167,7 +167,9 @@ export const VectorWidget = (app, inputName, options, initial, desc='') => { if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { try { v = eval(v); - } catch (err) {} + } catch (e) { + + } } if (this.value[idx] != v) { setTimeout( @@ -219,6 +221,15 @@ app.registerExtension({ }), VEC4: (node, inputName, inputData, app) => ({ widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0, 0])), + }), + VEC2INT: (node, inputName, inputData, app) => ({ + widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0])), + }), + VEC3INT: (node, inputName, inputData, app) => ({ + widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0])), + }), + VEC4INT: (node, inputName, inputData, app) => ({ + widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0, 0])), }) } },