From ccb67dd80c79f3548cbf622ae9e01e8ff6c90fe3 Mon Sep 17 00:00:00 2001 From: BennyKok Date: Fri, 1 Sep 2023 19:32:49 +0800 Subject: [PATCH] init commit --- .gitignore | 328 ++++++++ README.md | 30 + __init__.py | 55 ++ blender/apply_mesh_transform_as_shape_key.py | 98 +++ blender/clean_face.py | 43 ++ blender/combine_mesh.py | 35 + blender/create_vertex_bounds.py | 63 ++ blender/create_vertex_group.py | 56 ++ blender/disolve_faces.py | 48 ++ blender/export_gltf.py | 74 ++ blender/fill_face.py | 46 ++ blender/group_object.py | 35 + blender/group_vertex_inside.py | 108 +++ blender/knift_project.py | 121 +++ blender/load_pby.py | 41 + blender/mesh_from_texture.py | 101 +++ blender/open_in_blender.py | 80 ++ blender/plane.py | 72 ++ blender/scale_vertex.py | 55 ++ blender/texture.py | 88 +++ blender/uv_unwrap.py | 50 ++ common/point_visualizer.py | 52 ++ common/vector3d.py | 45 ++ global_bpy.py | 29 + js/blendshape_editor.js | 112 +++ js/style-inject.js | 3 + js/style.css | 3 + package.json | 17 + pnpm-lock.yaml | 756 +++++++++++++++++++ requirements.txt | 8 + sam/sam_loader.py | 42 ++ sam/sam_node.py | 81 ++ sam/sam_node_from_emb.py | 99 +++ sam/sam_node_remote.py | 79 ++ style-watcher.js | 24 + tailwind.config.js | 10 + 36 files changed, 2987 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 blender/apply_mesh_transform_as_shape_key.py create mode 100644 blender/clean_face.py create mode 100644 blender/combine_mesh.py create mode 100644 blender/create_vertex_bounds.py create mode 100644 blender/create_vertex_group.py create mode 100644 blender/disolve_faces.py create mode 100644 blender/export_gltf.py create mode 100644 blender/fill_face.py create mode 100644 blender/group_object.py create mode 100644 blender/group_vertex_inside.py create mode 100644 blender/knift_project.py create mode 100644 blender/load_pby.py create mode 100644 blender/mesh_from_texture.py create mode 100644 blender/open_in_blender.py create mode 100644 blender/plane.py create mode 100644 blender/scale_vertex.py create mode 100644 blender/texture.py create mode 100644 blender/uv_unwrap.py create mode 100644 common/point_visualizer.py create mode 100644 common/vector3d.py create mode 100644 global_bpy.py create mode 100644 js/blendshape_editor.js create mode 100644 js/style-inject.js create mode 100644 js/style.css create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 requirements.txt create mode 100644 sam/sam_loader.py create mode 100644 sam/sam_node.py create mode 100644 sam/sam_node_from_emb.py create mode 100644 sam/sam_node_remote.py create mode 100644 style-watcher.js create mode 100644 tailwind.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..078a9b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,328 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node,python,react +# Edit at https://www.toptal.com/developers/gitignore?templates=node,python,react + +js/output.css + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### react ### +.DS_* +**/*.backup.* +**/*.back.* + +node_modules + +*.sublime* + +psd +thumb +sketch + +# End of https://www.toptal.com/developers/gitignore/api/node,python,react diff --git a/README.md b/README.md new file mode 100644 index 0000000..c74e1bd --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# avatar-graph-comfyui + +This introduce a custom workflow on top of comfyui built for avatar-graph. + +# Installation + +1. `cd custom_nodes` + +2. `git clone https://github.com/avatechgg/avatar-graph-comfyui.git` + +3. Install deps `python -m pip install -r requirements.txt` + +4. Navigate to in your comfyui installation folder, open `comfyui/main.py` and add the following lines: +```py +def prompt_worker(q, server): + e = execution.PromptExecutor(server) + + # Add this line here + import sys + sys.path.append(os.path.join(os.path.dirname(__file__), "custom_nodes", "avatar-graph-comfyui")) + import global_bpy + + while True: + item, item_id = q.get() + # Add this line here + global_bpy.reset_bpy() + +``` + +4. Restart comfyui \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..91c9991 --- /dev/null +++ b/__init__.py @@ -0,0 +1,55 @@ +""" +@author: Avatech Limited +@title: Avatar Graph +@nickname: Avatar Graph +@description: Include nodes for sam + bpy operation, that allows workflow creations for generative 2d character rig. +""" + +import os +import sys +import importlib +import subprocess + +ag_path = os.path.join(os.path.dirname(__file__)) + +# pip_install = [sys.executable, '-m', 'pip', 'install'] + +# def ensure_pip_packages(): +# try: +# # import bpy +# import segment_anything +# except Exception: +# my_path = os.path.dirname(__file__) +# requirements_path = os.path.join(my_path, "requirements.txt") +# subprocess.check_call(pip_install + ['-r', requirements_path]) + +# ensure_pip_packages() + +def get_python_files(path): + return [f[:-3] for f in os.listdir(path) if f.endswith('.py')] + +def append_to_sys_path(path): + if path not in sys.path: + sys.path.append(path) + +paths = ['blender', 'sam', 'common'] +files = [] + +for path in paths: + full_path = os.path.join(ag_path, path) + append_to_sys_path(full_path) + files.extend(get_python_files(full_path)) + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} + +# Import all the modules and append their mappings +for file in files: + module = importlib.import_module(file) + if hasattr(module, 'NODE_CLASS_MAPPINGS'): + NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS) + if hasattr(module, 'NODE_DISPLAY_NAME_MAPPINGS'): + NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS) + +WEB_DIRECTORY = "js" +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/blender/apply_mesh_transform_as_shape_key.py b/blender/apply_mesh_transform_as_shape_key.py new file mode 100644 index 0000000..9dfa82e --- /dev/null +++ b/blender/apply_mesh_transform_as_shape_key.py @@ -0,0 +1,98 @@ +class ApplyMeshTransformAsShapeKey: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "bpy_objs": ("BPY_OBJS",), + "shape_key_name": ("STRING", { + # True if you want the field to look like the one on the ClipTextEncode node + "multiline": False, + "default": "EyeBlinkLeft", # default value + }), + "target_vertex_group": ("STRING", { + "multiline": False, + "default": "Group" + }), + "scale_x": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}), + "scale_y": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}), + "offset_x": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}), + "offset_y": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}), + }, + } + + RETURN_TYPES = ("BPY_OBJS", ) + RETURN_NAMES = ("bpy_objs", ) + + FUNCTION = "process" + + # OUTPUT_NODE = False + + CATEGORY = "mesh" + + def process(self, bpy_objs, shape_key_name, target_vertex_group, scale_x, scale_y, offset_x, offset_y): + from mathutils import Vector + import global_bpy + bpy = global_bpy.get_bpy() + + obj = bpy_objs[0] + + # Create new shape key + sk = obj.shape_key_add(name=shape_key_name) + sk.interpolation = 'KEY_LINEAR' + + verts = obj.data.vertices + + # get vertex group + vertex_group = obj.vertex_groups[target_vertex_group] + + # get the verts in the group + verts_in_group = [v for v in verts if vertex_group.index in [vg.group for vg in v.groups]] + + # position each vert + # for i in range(len(verts)): + # Calculate center of mesh + # center = sum((vert.co for vert in verts), Vector()) / len(verts) + center = sum((vert.co for vert in verts_in_group), Vector()) / len(verts_in_group) + # apply scale relative to object center + + # Position each vert + for i, vert in enumerate(verts): + # return if vert is not in group + if vertex_group.index not in [vg.group for vg in vert.groups]: + continue + # Get vector from center to vertex + vec = vert.co - center + + # Scale vector + # if scale_x != 0: + vec.x *= scale_x + # if scale_x != 0: + vec.y *= scale_y + + # # Offset vector + vec.x += offset_x + vec.y += offset_y + + # Calculate new position + new_pos = center + vec + + # Apply new position + # vert.co = new_pos + sk.data[i].co = new_pos + + return (bpy_objs, ) + + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "ApplyMeshTransformAsShapeKey": ApplyMeshTransformAsShapeKey +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "ApplyMeshTransformAsShapeKey": "Transform Shape Key" +} diff --git a/blender/clean_face.py b/blender/clean_face.py new file mode 100644 index 0000000..e223c56 --- /dev/null +++ b/blender/clean_face.py @@ -0,0 +1,43 @@ +class CleanFace(): + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_target": ("BPY_OBJS",), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target): + import global_bpy + bpy = global_bpy.get_bpy() + + target_object = bpy_objs_target[0] + + bpy.context.view_layer.objects.active = target_object + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + # bpy.ops.mesh.delete(type='EDGE_FACE') + bpy.ops.mesh.remove_doubles() + # bpy.ops.mesh.delete(type='FACE') + # bpy.ops.mesh.select_all(action='SELECT') + # bpy.ops.mesh.edge_face_add() + bpy.ops.object.mode_set(mode='OBJECT') + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "CleanFace": CleanFace +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "CleanFace": "Clean Face" +} diff --git a/blender/combine_mesh.py b/blender/combine_mesh.py new file mode 100644 index 0000000..27594fa --- /dev/null +++ b/blender/combine_mesh.py @@ -0,0 +1,35 @@ +class CombineMesh: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "obj_1": ("BPY_OBJS",), + "obj_2": ("BPY_OBJS",), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, obj_1, obj_2): + import global_bpy + bpy = global_bpy.get_bpy() + + override = bpy.context.copy() + override["active_object"] = obj_1[0] + override["selected_editable_objects"] = list([*obj_1, *obj_2]) + bpy.ops.object.join(override) + joined_object = override["active_object"] + return ([joined_object], ) + +NODE_CLASS_MAPPINGS = { + "CombineMesh": CombineMesh +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "CombineMesh": "Combine Mesh" +} diff --git a/blender/create_vertex_bounds.py b/blender/create_vertex_bounds.py new file mode 100644 index 0000000..21bc24f --- /dev/null +++ b/blender/create_vertex_bounds.py @@ -0,0 +1,63 @@ +class CreateVertexBounds(): + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_target": ("BPY_OBJS",), + "name": ("STRING", { + "multiline": False, + "default": "Group" + }), + "extrude": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target, name, extrude): + import global_bpy + bpy = global_bpy.get_bpy() + + if len(bpy_objs_target) == 0: + # throw error + return + + target_object = bpy_objs_target[0] + + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + # select only the target object + bpy.context.view_layer.objects.active = target_object + # enter enter edit mode and select all faces of the object to fill + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={ + "use_normal_flip": False, "use_dissolve_ortho_edges": False, "mirror": False}) + + bpy.ops.transform.resize( + value=(extrude, extrude, 1), constraint_axis=(False, False, False)) + + # remove the extruded vertex from the vertex group "name" + + bpy.ops.object.vertex_group_remove_from() + + # bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "CreateVertexBounds": CreateVertexBounds +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "CreateVertexBounds": "Vertex Bounds" +} diff --git a/blender/create_vertex_group.py b/blender/create_vertex_group.py new file mode 100644 index 0000000..08697e3 --- /dev/null +++ b/blender/create_vertex_group.py @@ -0,0 +1,56 @@ +class CreateVertexGroup(): + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_target": ("BPY_OBJS",), + "name": ("STRING", { + "multiline": False, + "default": "Group" + }), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target, name): + import global_bpy + bpy = global_bpy.get_bpy() + + if len(bpy_objs_target) == 0: + # throw error + return + + target_object = bpy_objs_target[0] + + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + # select only the target object + bpy.context.view_layer.objects.active = target_object + # enter enter edit mode and select all faces of the object to fill + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + # create vertex group and assign all selected faces to it (the name of the vertex group is the same as the name of the object) + bpy.context.object.vertex_groups.new(name=name) + bpy.ops.object.vertex_group_assign() + + # bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "CreateVertexGroup": CreateVertexGroup +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "CreateVertexGroup": "Vertex Group" +} diff --git a/blender/disolve_faces.py b/blender/disolve_faces.py new file mode 100644 index 0000000..857ff65 --- /dev/null +++ b/blender/disolve_faces.py @@ -0,0 +1,48 @@ +class DissolveFaces(): + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_target": ("BPY_OBJS",), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target): + import global_bpy + bpy = global_bpy.get_bpy() + + target_object = bpy_objs_target[0] + + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + # select only the target object + bpy.context.view_layer.objects.active = target_object + # enter enter edit mode and select all faces of the object to fill + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + # dissolve the faces + # bpy.ops.mesh.dissolve_faces() + bpy.ops.mesh.delete(type='ONLY_FACE') + # bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "DissolveFaces": DissolveFaces +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DissolveFaces": "Dissolve Faces" +} diff --git a/blender/export_gltf.py b/blender/export_gltf.py new file mode 100644 index 0000000..2486993 --- /dev/null +++ b/blender/export_gltf.py @@ -0,0 +1,74 @@ +import folder_paths +import os + +class ExportGLTF(): + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "bpy_objects": ("BPY_OBJS",), + "filename": ("STRING", { + "multiline": False, + "default": "out" + }), + "model_type": (["GLB", "GLTF_EMBEDDED"],), + "write_mode": (["Overwrite", "Increment"],), + }, + } + + RETURN_TYPES = () + FUNCTION = "process" + + OUTPUT_NODE = True + + CATEGORY = "mesh" + + def process(self, bpy_objects, filename, model_type, write_mode): + import global_bpy + bpy = global_bpy.get_bpy() + # print(bpy, bpy_objects) + + # deselect all objects + override = bpy.context.copy() + # print(override) + # override["selected_objects"] = list(bpy.context.scene.objects) + print(bpy_objects) + override["selected_objects"] = list(bpy_objects) + # override["active_object"] = list(context.scene.objects)[0] + override["active_object"] = list(bpy_objects)[0] + # override["active_object"] = None + # override["window"] = {} + # set count to the file count + + filepath = self.output_dir + "/" + filename + '.' + ("glb" if model_type == "GLB" else "gltf") + + if write_mode == "Increment": + count = 0 + # while file exists, increment count + while os.path.exists(self.output_dir + "/" + filename + '_' + str(count) + '.' + ("glb" if model_type == "GLB" else "gltf")): + count += 1 + + filepath = self.output_dir + "/" + filename + '_' + str(count) + '.' + ("glb" if model_type == "GLB" else "gltf") + + with bpy.context.temp_override(**override): + bpy.ops.export_scene.gltf(filepath=filepath, export_format=model_type) + print(filepath) + + return { "ui" : { "gltfFilename": { filepath.replace(f"{self.output_dir}/", "") } } } + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "ExportGLTF": ExportGLTF +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "ExportGLTF": "ExportGLTF" +} diff --git a/blender/fill_face.py b/blender/fill_face.py new file mode 100644 index 0000000..635f354 --- /dev/null +++ b/blender/fill_face.py @@ -0,0 +1,46 @@ +class FillFace: + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_target": ("BPY_OBJS",), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target): + import global_bpy + bpy = global_bpy.get_bpy() + + target_object = bpy_objs_target[0] + + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + # select only the target object + bpy.context.view_layer.objects.active = target_object + # enter enter edit mode and select all faces of the object to fill + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + # fill the faces + bpy.ops.mesh.fill() + + bpy.ops.object.mode_set(mode='OBJECT') + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "FillFace": FillFace +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "FillFace": "Fill Face" +} diff --git a/blender/group_object.py b/blender/group_object.py new file mode 100644 index 0000000..5a442bd --- /dev/null +++ b/blender/group_object.py @@ -0,0 +1,35 @@ +class GroupObject(): + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_target": ("BPY_OBJS",), + "bpy_objs_target_2": ("BPY_OBJS",), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target, bpy_objs_target_2): + import global_bpy + bpy = global_bpy.get_bpy() + + target_object = bpy_objs_target[0] + target_object2 = bpy_objs_target_2[0] + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "GroupObject": GroupObject +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "GroupObject": "Group Object" +} diff --git a/blender/group_vertex_inside.py b/blender/group_vertex_inside.py new file mode 100644 index 0000000..a7cca3d --- /dev/null +++ b/blender/group_vertex_inside.py @@ -0,0 +1,108 @@ +class GroupVertexInside(): + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy": ("BPY",), + "bpy_objs_target": ("BPY_OBJS",), + "name": ("STRING", { + "multiline": False, + "default": "Group" + }), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target, name): + import global_bpy + bpy = global_bpy.get_bpy() + + target_object = bpy_objs_target[0] + + bpy.ops.object.mode_set(mode='OBJECT') + + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + # select only the target object + bpy.context.view_layer.objects.active = target_object + # enter enter edit mode and select all faces of the object to fill + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + + # select the vetex group's vertices + bpy.ops.object.vertex_group_select() + + # get vertex group + vertex_group = target_object.vertex_groups[name] + + verts = target_object.data.vertices + + # all_vertices = [(v.co.x, v.co.y, i) for i, v in target_object.data.vertices] + + # get the verts in the group + verts_in_group = [v for v in verts if vertex_group.index in [vg.group for vg in v.groups]] + + # get all selected vertices's xy + # get the vertices of the vertex group + vertex_group_vertices = [(v.co.x, v.co.y) for v in verts_in_group] + + # get all vertices + # all_vertices = [(v.co.x, v.co.y) for v in target_object.data.vertices] + all_vertices_without_vertex_group = [(v.co.x, v.co.y, i) for i, v in enumerate(target_object.data.vertices) if v not in verts_in_group] + + from scipy.spatial import ConvexHull, Delaunay + + # create a convex hull of the vertex group's vertices + hull = ConvexHull(vertex_group_vertices) + + # create a Delaunay triangulation of the convex hull + tri = Delaunay(hull.points[hull.vertices]) + + # get the indices of the vertices that are inside the convex hull + inside_vertices_indices = [i for (x,y,i) in all_vertices_without_vertex_group if tri.find_simplex((x,y))>=0] + + print(inside_vertices_indices) + print(len(vertex_group_vertices)) + + bpy.ops.mesh.select_all(action='DESELECT') + + # select the vertices that are inside the convex hull + # for i in inside_vertices_indices: + # target_object.data.vertices[i].select = True + + # # select vertex from 1 to 10 + # for i in range(1, 100): + # target_object.data.vertices[i].select = True + + # create a new vertex group + # bpy.context.object.vertex_groups.new(name="fuck") + # bpy.ops.object.vertex_group_assign() + bpy.ops.object.mode_set(mode='OBJECT') + # create a new vertex group + # new_group = bpy.context.object.vertex_groups.new(name="new_group") + vertex_group.add(inside_vertices_indices, weight=1.0, type='REPLACE') + + # assign the selected vertices to the new vertex group + # new_group.add([v.index for v in target_object.data.vertices if v.select], weight=1.0, type='REPLACE') + # new_group.add([20, 100], weight=1.0, type='REPLACE') + # assign the selected vertices to the active vertex group + bpy.ops.object.mode_set(mode='OBJECT') + + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "GroupVertexInside": GroupVertexInside +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "GroupVertexInside": "Group Vertex Inside" +} diff --git a/blender/knift_project.py b/blender/knift_project.py new file mode 100644 index 0000000..b778c27 --- /dev/null +++ b/blender/knift_project.py @@ -0,0 +1,121 @@ +class KnifeProjection: + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_knife": ("BPY_OBJS",), + "bpy_objs_target": ("BPY_OBJS",), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_knife, bpy_objs_target): + import mathutils + import math + import global_bpy + bpy = global_bpy.get_bpy() + + knife_object = bpy_objs_knife[0] + target_object = bpy_objs_target[0] + + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = target_object + # knife_object.select_set(True) + # target_object.select_set(True) + + # bpy.ops.object.mode_set(mode='EDIT') + + area = next( + area for area in bpy.context.screen.areas if area.type == 'VIEW_3D') + space = next(space for space in area.spaces if space.type == 'VIEW_3D') + region = next( + region for region in area.regions if region.type == 'WINDOW') + + # space.region_3d.view_perspective = 'ORTHO' + # space.region_3d.view_rotation = mathutils.Euler( + # (0, 0, 0)).to_quaternion() + + # set blender to orthographic view without using numpad + + # bpy.ops.view3d.view_axis(type='TOP') + + override = bpy.context.copy() + + # override["active_object"] = target_object + # override["edit_object"] = target_object + # override["selected_objects"] = [knife_object, target_object] + # override["selected_editable_objects"] = [knife_object, target_object] + + override["region"] = region + override["area"] = area + override["space"] = space + + override["active_object"] = target_object + override["edit_object"] = target_object + override["selected_objects"] = [knife_object, target_object] + override["selected_editable_objects"] = [knife_object, target_object] + + knife_object.select_set(False) + target_object.select_set(True) + # move the kinfe object up a bit + bpy.ops.transform.translate(value=(0, 0, -0.1)) + bpy.ops.object.mode_set(mode='EDIT') + knife_object.select_set(False) + + # Get the viewport rotation + viewport_rotation = space.region_3d.view_rotation + + with bpy.context.temp_override(**override): + # # Create a rotation matrix representing a top-down view + # rotation_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'X') + + # # Create a rotation matrix to make the object face the user + # rotation_matrix_face_user = mathutils.Matrix.Rotation(math.radians(180.0), 4, 'Z') + + # # Create a rotation matrix for the viewport rotation + # rotation_matrix_viewport = viewport_rotation.to_matrix().to_4x4() + + # # Remember the original locations + # original_location_knife = knife_object.location.copy() + # original_location_target = target_object.location.copy() + + # # Move the objects to the origin + # knife_object.location = mathutils.Vector((0, 0, 0)) + # target_object.location = mathutils.Vector((0, 0, 0)) + + # # Rotate the objects + # knife_object.matrix_world = rotation_matrix @ rotation_matrix_face_user @ rotation_matrix_viewport @ knife_object.matrix_world + # target_object.matrix_world = rotation_matrix @ rotation_matrix_face_user @ rotation_matrix_viewport @ target_object.matrix_world + + bpy.ops.view3d.view_axis(type='TOP', align_active=False) + bpy.ops.view3d.view_persportho() + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + bpy.ops.mesh.knife_project() + + # # Rotate the objects back + # knife_object.matrix_world = rotation_matrix.inverted() @ knife_object.matrix_world + # target_object.matrix_world = rotation_matrix.inverted() @ target_object.matrix_world + + # # Move the objects back to their original locations + # knife_object.location = original_location_knife + # target_object.location = original_location_target + + bpy.ops.object.mode_set(mode='OBJECT') + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "KnifeProjection": KnifeProjection +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "KnifeProjection": "Knife Projection" +} diff --git a/blender/load_pby.py b/blender/load_pby.py new file mode 100644 index 0000000..60c9e14 --- /dev/null +++ b/blender/load_pby.py @@ -0,0 +1,41 @@ + +class LoadBPY: + def __init__(self): + import bpy + + self.bpy = bpy + pass + + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), # For disabling cache + }, + } + + RETURN_TYPES = ("BPY",) + RETURN_NAMES = ("bpy",) + + FUNCTION = "process" + + # OUTPUT_NODE = False + + CATEGORY = "mesh" + + def process(self, seed): + self.bpy.ops.wm.read_factory_settings(use_empty=True) + return (self.bpy,) + + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "LoadBPY": LoadBPY +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "LoadBPY": "Load BPY" +} diff --git a/blender/mesh_from_texture.py b/blender/mesh_from_texture.py new file mode 100644 index 0000000..8b07d0d --- /dev/null +++ b/blender/mesh_from_texture.py @@ -0,0 +1,101 @@ +class MeshFromTexture: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + # "min_threshold": ("INT", {"default": 50, "min": 0, "max": 1024}), + # "max_threshold": ("INT", {"default": 150, "min": 0, "max": 1024}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), # For disabling cache + }, + } + + RETURN_TYPES = ("IMAGE","BPY_OBJS") + RETURN_NAMES = ("image","bpy_objs") + + FUNCTION = "process" + + #OUTPUT_NODE = False + + CATEGORY = "mesh" + + def process(self, image, seed): + import torch + import cv2 + import numpy as np + import global_bpy + bpy = global_bpy.get_bpy() + + image = np.copy(image[0].numpy()) + + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + gray = (gray * 255).astype(np.uint8) + # edges = cv2.Canny(gray, min_threshold, max_threshold) + + # Find contours + contours, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + def normalize_vertices(vertices, max_value): + return vertices / float(max_value) * 2 - 1 + + # Get the image width and height + height, width = image.shape[:2] + + # Normalize the vertices + normalized_contours = [] + for contour in contours: + normalized_contour = [] + for vertex in contour: + normalized_vertex = [normalize_vertices(vertex[0][0], width), normalize_vertices(vertex[0][1], height) * -1] + normalized_contour.append(normalized_vertex) + normalized_contours.append(np.array(normalized_contour, dtype=np.float32)) + + meshes = [] + # print(len(normalized_contours)) + for i, contour in enumerate(normalized_contours): + mesh = bpy.data.meshes.new(name=f"NewMesh{i}") # Create a new mesh for each contour + obj = bpy.data.objects.new(f"NewObject{i}", mesh) # Create a new object for each mesh + bpy.context.collection.objects.link(obj) # Link the object to the current collection + + ordered_vertices = [(*vertex, 0) for vertex in contour] # Add a z coordinate to each vertex + face = list(range(len(ordered_vertices))) # Create a face from the vertices + + mesh.from_pydata(ordered_vertices, [], [face]) # Create the mesh from the vertices and face + + + + # Create a default shape key for the mesh + sk_basis = obj.shape_key_add(name='Basis') + + meshes.append(obj) # Add the object to the list of meshes + + # Convert edges back to an image + # image_from_edges = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) + # image_from_edges = image_from_edges.astype(np.float32) + # image_from_edges = [torch.from_numpy(image_from_edges)] + + # Draw contours on the original image + if not image.flags['C_CONTIGUOUS']: + image = np.ascontiguousarray(image) + cv2.drawContours(image, contours, -1, (0, 255, 0), 3) + + # print(image, meshes) + + # Convert image back to tensor + image = [torch.from_numpy(image)] + return (image,meshes) + + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "MeshFromTexture": MeshFromTexture +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "MeshFromTexture": "Mesh from texture" +} diff --git a/blender/open_in_blender.py b/blender/open_in_blender.py new file mode 100644 index 0000000..edf61a0 --- /dev/null +++ b/blender/open_in_blender.py @@ -0,0 +1,80 @@ +import subprocess +import folder_paths +import os + +blender_process_global = [] + +class OpenInBlender: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "bpy_objs": ("BPY_OBJS",), + "blender_path": ("STRING", { + "multiline": False, + "default": "blender" + }), + "shading": (["Material", "Solid", "Rendered", "Wireframe"],), + "camera_location": ("VECTOR3D",), + "camera_rotation": ("VECTOR3D",), + }, + } + + RETURN_TYPES = () + FUNCTION = "process" + + OUTPUT_NODE = True + + CATEGORY = "mesh" + + def process(self, bpy_objs, blender_path, shading, camera_location, camera_rotation): + import mathutils + import global_bpy + bpy = global_bpy.get_bpy() + + # Change shading mode and viewport + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + for space in area.spaces: + if space.type == 'VIEW_3D': + space.shading.type = shading.upper() + rv3d = space.region_3d + rv3d.view_location = camera_location + rv3d.view_rotation = mathutils.Euler(camera_rotation).to_quaternion() + + # Open blender + if hasattr(self, 'blender_process'): + self.blender_process.kill() + # remove from global list so it doesn't get garbage collected + blender_process_global.remove(self.blender_process) + + # Save as .blend + output_file = self.output_dir + '/tmp.blend' + if os.path.exists(output_file): + os.remove(output_file) + bpy.ops.wm.save_as_mainfile(filepath=output_file) + + + self.blender_process = subprocess.Popen([blender_path, output_file]) + # append to global list so it doesn't get garbage collected + blender_process_global.append(self.blender_process) + return { "ui" : { } } + +NODE_CLASS_MAPPINGS = { + "OpenInBlender": OpenInBlender +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "OpenInBlender": "Open in Blender" +} + +# detects when the python process is killed, and kills the blender process +import atexit +@atexit.register +def kill_blender_process(): + print('blender_process_global', blender_process_global) + for process in blender_process_global: + process.kill() \ No newline at end of file diff --git a/blender/plane.py b/blender/plane.py new file mode 100644 index 0000000..34cd60b --- /dev/null +++ b/blender/plane.py @@ -0,0 +1,72 @@ +class Plane: + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "division": ("INT", { + "default": 0, + "min": 0, + "max": 256, + "step": 1, + "display": "number" + }), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), # For disabling cache + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, division, seed): + import global_bpy + bpy = global_bpy.get_bpy() + + # Define coordinates for the plane to object mode + if bpy.context.active_object is not None: + bpy.ops.object.mode_set(mode='OBJECT') + coords = [(-1, -1, 0), (1, -1, 0), (-1, 1, 0), (1, 1, 0)] + + # Create a mesh object + mesh = bpy.data.meshes.new(name="PlaneMesh") + mesh.from_pydata(coords,[],[(0,1,3,2)]) + + # Create a new object with the mesh + object = bpy.data.objects.new(name="PlaneObject", object_data=mesh) + bpy.ops.object.select_all(action='DESELECT') + bpy.context.collection.objects.link(object) + bpy.context.view_layer.objects.active = object + object.select_set(True) + + # Invert the Y axis of the UV to fix the vertical inversion + bpy.ops.transform.resize(value=(-1, -1, 1)) + + # Create a new UV map + uv_map = mesh.uv_layers.new(name="newUV") + mesh.uv_layers.active = uv_map + + # Unwrap the mesh + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.uv.unwrap() + bpy.ops.object.mode_set(mode='OBJECT') + + # Subdivide the plane + if division > 0: + original_mode = bpy.context.object.mode + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.subdivide(number_cuts=division) + bpy.ops.object.mode_set(mode=original_mode) + + return ([object],) + +NODE_CLASS_MAPPINGS = { + "Plane": Plane +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Plane": "Plane" +} diff --git a/blender/scale_vertex.py b/blender/scale_vertex.py new file mode 100644 index 0000000..65ca027 --- /dev/null +++ b/blender/scale_vertex.py @@ -0,0 +1,55 @@ +class ScaleVertex(): + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_target": ("BPY_OBJS",), + "extrude": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display": "number"}), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target, extrude): + import global_bpy + bpy = global_bpy.get_bpy() + + if len(bpy_objs_target) == 0: + # throw error + return + + target_object = bpy_objs_target[0] + + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + # select only the target object + bpy.context.view_layer.objects.active = target_object + # enter enter edit mode and select all faces of the object to fill + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + bpy.ops.transform.resize( + value=(extrude, extrude, 1), constraint_axis=(False, False, False)) + + # remove the extruded vertex from the vertex group "name" + # bpy.ops.object.vertex_group_remove_from() + + # bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "ScaleVertex": ScaleVertex +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ScaleVertex": "Scale Vertex" +} diff --git a/blender/texture.py b/blender/texture.py new file mode 100644 index 0000000..7a71431 --- /dev/null +++ b/blender/texture.py @@ -0,0 +1,88 @@ +class Texture: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "bpy_objs": ("BPY_OBJS",), + "texture": ("IMAGE",) + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + # OUTPUT_NODE = False + + CATEGORY = "mesh" + + def process(self, bpy_objs, texture): + import numpy as np + import global_bpy + bpy = global_bpy.get_bpy() + + # Convert image to numpy + texture = texture[0].numpy() + + # Flip the image vertically + texture = np.flipud(texture) + + # Create an image with the required dimensions + img = bpy.data.images.new('my_image', width=texture.shape[1], height=texture.shape[0]) + + # If there is no alpha channel, append one full of 1's + if texture.shape[2] == 3: + alpha_channel = np.ones((*texture.shape[:2], 1)) + texture = np.concatenate((texture, alpha_channel), axis=2) + + # Flatten image data and rearrange color channels for blender + img.pixels = texture.ravel() + + # Pack image to store it within .blend file + img.pack() + + # Save image to a file + # img.filepath_raw = 'test.png' + # img.file_format = 'PNG' + # img.save() + + # Get the active object + obj = bpy_objs[0] + + # Create a material + mat = bpy.data.materials.new("MaterialName") + mat.use_nodes = True + nodes = mat.node_tree.nodes + for node in nodes: + nodes.remove(node) + + # Add a new texture node + texture_node = nodes.new(type='ShaderNodeTexImage') + texture_node.image = img + + # Add a new BSDF node + bsdf_node = nodes.new(type='ShaderNodeBsdfPrincipled') + + # Add a new output node + output_node = nodes.new(type='ShaderNodeOutputMaterial') + + # Link nodes together + links = mat.node_tree.links + links.new(bsdf_node.inputs['Base Color'], texture_node.outputs['Color']) + links.new(output_node.inputs['Surface'], bsdf_node.outputs['BSDF']) + + # Assign the material to the active object + if obj.data.materials: + obj.data.materials[0] = mat + else: + obj.data.materials.append(mat) + return (bpy_objs,) + +NODE_CLASS_MAPPINGS = { + "Texture": Texture +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Texture": "Texture" +} diff --git a/blender/uv_unwrap.py b/blender/uv_unwrap.py new file mode 100644 index 0000000..e19762d --- /dev/null +++ b/blender/uv_unwrap.py @@ -0,0 +1,50 @@ +class UV_Unwrap(): + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "bpy_objs_target": ("BPY_OBJS",), + }, + } + + RETURN_TYPES = ("BPY_OBJS",) + RETURN_NAMES = ("bpy_objs",) + + FUNCTION = "process" + + CATEGORY = "mesh" + + def process(self, bpy_objs_target): + import global_bpy + bpy = global_bpy.get_bpy() + + target_object = bpy_objs_target[0] + + bpy.context.view_layer.objects.active = target_object + bpy.ops.object.mode_set(mode='OBJECT') + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + # select only the target object + bpy.context.view_layer.objects.active = target_object + # enter enter edit mode and select all faces of the object to fill + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + # perform a cube projection unwrap + bpy.ops.uv.cube_project(cube_size=1.0, correct_aspect=True, clip_to_bounds=False, scale_to_bounds=True) + + # bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + + + return ([target_object],) + + +NODE_CLASS_MAPPINGS = { + "UV_Unwrap": UV_Unwrap +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "UV_Unwrap": "UV Unwrap" +} diff --git a/common/point_visualizer.py b/common/point_visualizer.py new file mode 100644 index 0000000..e46f03a --- /dev/null +++ b/common/point_visualizer.py @@ -0,0 +1,52 @@ +import torch +from einops import rearrange +from nodes import PreviewImage + +class PointVisualizer(PreviewImage): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "point": ("VECTOR3D",), + "point_size": ("INT", { + "default": 1, + "min": 10, + "max": 20, + "step": 1, + "display": "number" + }), + }, + } + + FUNCTION = "process" + + CATEGORY = "image" + + def process(self, images, point, point_size): + x, y, z = point + h, w, c = images[0].shape + point_image = torch.zeros(images[0].shape) + + center_x, center_y, _ = point + top_left_x = center_x - point_size // 2 + top_left_y = center_y - point_size // 2 + + # Make sure the square is within the image boundaries + top_left_x = max(0, min(w - point_size, top_left_x)) + top_left_y = max(0, min(h - point_size, top_left_y)) + + point_image[top_left_y:top_left_y+point_size, top_left_x:top_left_x+point_size] = 1 + point_image = rearrange(point_image, 'h w c -> 1 h w c') + + # Blending + images = images * 0.2 + point_image * 0.8 + return self.save_images(images) + +NODE_CLASS_MAPPINGS = { + "PointVisualizer": PointVisualizer +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "PointVisualizer": "Point Visualizer" +} diff --git a/common/vector3d.py b/common/vector3d.py new file mode 100644 index 0000000..0aa45e1 --- /dev/null +++ b/common/vector3d.py @@ -0,0 +1,45 @@ +class VECTOR3D: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "x": ("INT", { + "default": 0, + "min": -1024, + "max": 1024, + "step": 1, + "display": "number" + }), + "y": ("INT", { + "default": 0, + "min": -1024, + "max": 1024, + "step": 1, + "display": "number" + }), + "z": ("INT", { + "default": 0, + "min": -1024, + "max": 1024, + "step": 1, + "display": "number" + }), + }, + } + + RETURN_TYPES = ("VECTOR3D",) + + FUNCTION = "run" + + CATEGORY = "utils" + + def run(self, x, y, z): + return ([x, y, z],) + +NODE_CLASS_MAPPINGS = { + "VECTOR3D": VECTOR3D +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "VECTOR3D": "Vector 3D" +} diff --git a/global_bpy.py b/global_bpy.py new file mode 100644 index 0000000..bf07caf --- /dev/null +++ b/global_bpy.py @@ -0,0 +1,29 @@ +import bpy +global_bpy = bpy + +def get_bpy(): + global global_bpy + return global_bpy + +def reset_bpy(): + global global_bpy + bpy = global_bpy + # not using this cause this cause seg fault frequently + # global_bpy.ops.wm.read_factory_settings(use_empty=True) + + # Delete all objects + bpy.ops.object.select_all(action='DESELECT') + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete() + + # Delete all meshes + for mesh in bpy.data.meshes: + bpy.data.meshes.remove(mesh) + + # Delete all materials + for material in bpy.data.materials: + bpy.data.materials.remove(material) + + # Delete all textures + for texture in bpy.data.textures: + bpy.data.textures.remove(texture) \ No newline at end of file diff --git a/js/blendshape_editor.js b/js/blendshape_editor.js new file mode 100644 index 0000000..4b771a3 --- /dev/null +++ b/js/blendshape_editor.js @@ -0,0 +1,112 @@ +import { app } from '../../scripts/app.js'; +import { api } from '../../scripts/api.js'; +import van from 'https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.1.3.min.js'; + +function addMenuHandler(nodeType, cb) { + const getOpts = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function () { + const r = getOpts.apply(this, arguments); + cb.apply(this, arguments); + return r; + }; +} + +const iframeSrc = van.state('https://editor.avatech.ai'); +const showEditor = van.state(false); + +function openInAvatechEditor(url, fileName) { + let editor = document.getElementById('avatech-editor-iframe'); + iframeSrc.val = url; + showEditor.val = true; + + editor.contentWindow.postMessage( + { + key: 'key', + value: fileName, + method: 'store', + }, + '*', + ); +} + +app.registerExtension({ + name: 'Avatech.Avatar.BlendshapeEditor', + init(app) {}, + + async setup() { + const { button, iframe, div } = van.tags; + + const AvatarEditor = () => { + return div( + { + class: 'fixed left-0 top-0 w-full h-full z-[200] pointer-events-none', + id: 'avatech-editor', + }, + button( + { + class: () => + 'absolute left-0 top-0 z-[100] pointer-events-auto ' + (showEditor.val ? '' : 'hidden'), + onclick: () => { + console.log('close'); + showEditor.val = false; + }, + }, + 'back', + ), + iframe({ + id: 'avatech-editor-iframe', + title: 'avatech-editor-iframe', + name: 'avatech-editor-iframe', + class: () => + 'w-full h-full pointer-events-auto ' + + (showEditor.val ? '' : 'hidden'), + src: iframeSrc, + }), + ); + }; + + van.add(document.body, AvatarEditor()); + }, + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === 'ExportGLTF') { + addMenuHandler(nodeType, function (_, options) { + const output = app.nodeOutputs[this.id + ""]; + if (!output.gltfFilename) return; + + const gltfFilename = window.location.protocol + '//' + api.api_host + api.api_base + (`/view?filename=${output.gltfFilename[0]}`) + + options.unshift({ + content: 'Save file', + callback: () => { + const a = document.createElement('a'); + let url = new URL(this.gltfFilename); + url.searchParams.delete('preview'); + a.href = url; + a.setAttribute( + 'download', + new URLSearchParams(url.search).get('filename'), + ); + document.body.append(a); + a.click(); + requestAnimationFrame(() => a.remove()); + }, + }); + + options.unshift({ + content: 'Open In Avatech Editor (Local)', + callback: () => { + openInAvatechEditor('http://localhost:3006', gltfFilename); + }, + }); + + options.unshift({ + content: 'Open In Avatech Editor', + callback: () => { + openInAvatechEditor('https://editor.avatech.ai', gltfFilename); + }, + }); + }); + } + }, +}); diff --git a/js/style-inject.js b/js/style-inject.js new file mode 100644 index 0000000..4e3d662 --- /dev/null +++ b/js/style-inject.js @@ -0,0 +1,3 @@ +let style = document.createElement('style'); +style.innerHTML = `.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.left-0{left:0}.top-0{top:0}.z-\\[100\\]{z-index:100}.z-\\[200\\]{z-index:200}.inline{display:inline}.hidden{display:none}.h-full{height:100%}.w-full{width:100%}`; +document.head.appendChild(style); \ No newline at end of file diff --git a/js/style.css b/js/style.css new file mode 100644 index 0000000..48ea014 --- /dev/null +++ b/js/style.css @@ -0,0 +1,3 @@ +/* @tailwind base; */ +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e277d34 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "avatar-graph-comfyui", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "concurrently \"tailwindcss -i ./js/style.css -o ./js/output.css --watch -m\" \"node ./style-watcher.js\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "chokidar": "^3.5.3", + "concurrently": "^8.2.1", + "tailwindcss": "^3.3.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d03979e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,756 @@ +lockfileVersion: '6.1' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + chokidar: + specifier: ^3.5.3 + version: 3.5.3 + concurrently: + specifier: ^8.2.1 + version: 8.2.1 + tailwindcss: + specifier: ^3.3.3 + version: 3.3.3 + +packages: + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + + /@babel/runtime@7.22.11: + resolution: {integrity: sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /concurrently@8.2.1: + resolution: {integrity: sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.1 + shell-quote: 1.8.1 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.22.11 + dev: true + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /jiti@1.19.3: + resolution: {integrity: sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w==} + hasBin: true + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /postcss-import@15.1.0(postcss@8.4.29): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.4 + dev: true + + /postcss-js@4.0.1(postcss@8.4.29): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.29 + dev: true + + /postcss-load-config@4.0.1(postcss@8.4.29): + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.29 + yaml: 2.3.2 + dev: true + + /postcss-nested@6.0.1(postcss@8.4.29): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + dev: true + + /postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.29: + resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve@1.22.4: + resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.2 + dev: true + + /shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /sucrase@3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /tailwindcss@3.3.3: + resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.1 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.19.3 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.29 + postcss-import: 15.1.0(postcss@8.4.29) + postcss-js: 4.0.1(postcss@8.4.29) + postcss-load-config: 4.0.1(postcss@8.4.29) + postcss-nested: 6.0.1(postcss@8.4.29) + postcss-selector-parser: 6.0.13 + resolve: 1.22.4 + sucrase: 3.34.0 + transitivePeerDependencies: + - ts-node + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yaml@2.3.2: + resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==} + engines: {node: '>= 14'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..39acaf4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +torchvision +numpy +opencv-python +opencv-contrib-python +einops +bpy +segment-anything +# -e git+https://github.com/facebookresearch/segment-anything.git#egg=segment_anything \ No newline at end of file diff --git a/sam/sam_loader.py b/sam/sam_loader.py new file mode 100644 index 0000000..bd7801b --- /dev/null +++ b/sam/sam_loader.py @@ -0,0 +1,42 @@ +import folder_paths +import torch +from segment_anything import SamAutomaticMaskGenerator, sam_model_registry +from einops import rearrange, repeat +import requests +from PIL import Image +import io +import numpy as np +from segment_anything import SamAutomaticMaskGenerator, sam_model_registry, SamPredictor +from einops import rearrange, repeat + +class SAM_Remote_Emb: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model_type": (["vit_h", "vit_l", "vit_b"],), + "ckpt": (folder_paths.get_filename_list("sam"),), + }, + } + + RETURN_TYPES = ("SAM", "SAMPREDICTOR") + RETURN_NAMES = ("sam", "predictor") + + FUNCTION = "segment" + + CATEGORY = "image" + + def segment(self, model_type, ckpt): + ckpt = folder_paths.get_full_path("sam", ckpt) + sam = sam_model_registry[model_type](checkpoint=ckpt) + predictor = SamPredictor(sam) + + return (sam, predictor) + +NODE_CLASS_MAPPINGS = { + "SAM_Loader": SAM_Remote_Emb +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SAM_Loader": "SAM Loader" +} diff --git a/sam/sam_node.py b/sam/sam_node.py new file mode 100644 index 0000000..2c76851 --- /dev/null +++ b/sam/sam_node.py @@ -0,0 +1,81 @@ +import folder_paths +import torch +from segment_anything import SamAutomaticMaskGenerator, sam_model_registry +from einops import rearrange, repeat + +class SAM: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "point_1": ("VECTOR3D",), + "model_type": (["vit_h", "vit_l", "vit_b"],), + "ckpt": (folder_paths.get_filename_list("sam"),), + }, + "optional": { + "point_2": ("VECTOR3D",), + "point_3": ("VECTOR3D",), + "point_4": ("VECTOR3D",), + "point_5": ("VECTOR3D",), + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE") + RETURN_NAMES = ("mask_1", "image_1", "mask_2", "image_2", "mask_3", "image_3", "mask_4", "image_4", "mask_5", "image_5",) + + FUNCTION = "segment" + + CATEGORY = "image" + + def is_point_in_bbox(self, point, mask): + x, y, z = point + in_bbox = mask[y, x].item() if y < mask.shape[0] and x < mask.shape[1] else None + return in_bbox + + def find_largest_bbox(self, point, masks): + largest_bbox = None + largest_area = 0 + + for mask in masks: + area = mask['area'] + if self.is_point_in_bbox(point, mask['segmentation']) and area > largest_area: + largest_bbox = mask + largest_area = area + + return largest_bbox + + def segment(self, image, point_1, model_type, ckpt, point_2=None, point_3=None, point_4=None, point_5=None): + image = (image[0] * 255).to(torch.uint8).numpy() + H, W, C = image.shape + ckpt = folder_paths.get_full_path("sam", ckpt) + sam = sam_model_registry[model_type](checkpoint=ckpt) #.to("mps") + mask_generator = SamAutomaticMaskGenerator(sam) + masks = mask_generator.generate(image) + points = [point_1, point_2, point_3, point_4, point_5] + + outputs = [] + for point in points: + if point is not None: + bbox = self.find_largest_bbox(point, masks) + mask = torch.from_numpy(bbox["segmentation"]) if bbox is not None else torch.zeros(H, W) + mask = repeat(mask, 'h w -> h w c', c=3) + out_image = torch.from_numpy(image) * mask + + mask = rearrange(mask, 'h w c -> 1 h w c') + out_image = rearrange(out_image, 'h w c -> 1 h w c') / 255 + + outputs.append(mask) + outputs.append(out_image) + else: + outputs.append(None) + outputs.append(None) + return outputs + +NODE_CLASS_MAPPINGS = { + "Segmentation": SAM +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Segmentation": "Segmentation Node" +} diff --git a/sam/sam_node_from_emb.py b/sam/sam_node_from_emb.py new file mode 100644 index 0000000..aec8035 --- /dev/null +++ b/sam/sam_node_from_emb.py @@ -0,0 +1,99 @@ +import torch +from einops import rearrange, repeat +import numpy as np +import cv2 + +class SAM_Predict: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "embeddings": ("EMBEDDINGS",), + "predictor": ("SAMPREDICTOR",) + }, + "optional": { + "point_1": ("VECTOR3D",), + "mask": ("IMAGE",), + } + } + + RETURN_TYPES = ("IMAGE","IMAGE", "IMAGE") + RETURN_NAMES = ("image","out_image", "mask") + + FUNCTION = "segment" + + CATEGORY = "image" + + def segment(self, image, embeddings, predictor, point_1=None, mask=None): + image_embedding_list = embeddings['image_embedding'] + shape = tuple(embeddings['shape']) + input_size = tuple(embeddings['input_size']) + + # Convert the list back to a numpy array with the original shape + image_embedding_np = np.array(image_embedding_list, dtype=np.single).reshape(shape) + + # Convert the numpy array to a PyTorch tensor + image_embedding_tensor = torch.from_numpy(image_embedding_np) + + # Set the image embeddings for the model + # predictor.set_torch_image(image_embedding_tensor, image.shape[:2]) + # print(image[0].shape[:2]) + predictor.input_size = input_size + predictor.features = image_embedding_tensor + predictor.is_image_set = True + predictor.original_size = image[0].shape[:2] + + if point_1 != None: + x, y, z = point_1 + point_coords = np.array([[x, y]]) + point_labels = np.array([1]) + masks, iou_predictions, low_res_masks = predictor.predict( + point_coords=point_coords, + point_labels=point_labels, + ) + elif mask != None: + # scale the mask to 256x256 + cv2_mask = cv2.resize(np.array(mask[0]), (256, 256)) + cv2_mask = cv2_mask[np.newaxis, :, :] + cv2_mask = (cv2_mask * 255).astype(int) + + true_locations = np.array(np.where(cv2_mask[0] == 255)) + if true_locations.shape[1] > 0: + # Randomly select a point in the mask + rand_index = np.random.randint(true_locations.shape[1]) + y, x = true_locations[:, rand_index] + point_coords = np.array([[x, y]]) + point_labels = np.array([1]) + + masks, iou_predictions, low_res_masks = predictor.predict( + point_coords=point_coords, + point_labels=point_labels, + mask_input=cv2_mask + ) + else: + # No detected mask + h, w, c = image[0].shape + masks = np.zeros((1, h, w)) + + masks = torch.from_numpy(masks) + + masks = rearrange(masks[0], 'h w -> 1 h w') + # masks = rearrange(masks, 'c h w -> 1 c h w') + out_image = repeat(masks, '1 h w -> 1 h w c', c=3) * image + + print(masks.shape, torch.max(masks), torch.min(masks)) + print(image.shape, torch.max(image), torch.min(image)) + + # print(emb) + + # print(masks, out_image) + return (image, out_image, masks) + +NODE_CLASS_MAPPINGS = { + "SAM_Predict": SAM_Predict +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SAM_Predict": "SAM Predict" +} diff --git a/sam/sam_node_remote.py b/sam/sam_node_remote.py new file mode 100644 index 0000000..81eaefc --- /dev/null +++ b/sam/sam_node_remote.py @@ -0,0 +1,79 @@ +import requests +from PIL import Image +import io +import numpy as np + +class SAM_Embedding: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + }, + "optional": { + "predictor": ("SAMPREDICTOR",) + } + } + + RETURN_TYPES = ("EMBEDDINGS", ) + RETURN_NAMES = ("embeddings", ) + + FUNCTION = "segment" + + CATEGORY = "image" + + def segment(self, image, predictor=None): + # Convert PyTorch tensor to numpy array + image_np = (image[0].numpy() * 255).astype(np.uint8) + + if predictor != None: + predictor.set_image(image_np) + emb = predictor.get_image_embedding().cpu().numpy() + output = { + "image_embedding": emb, + "shape": emb.shape, + "input_size": predictor.input_size + } + else: + # Convert numpy array to PIL Image + img = Image.fromarray(image_np) + + # Create an in-memory bytes buffer + img_byte_arr = io.BytesIO() + + # Save the PIL Image to the bytes buffer in PNG format + img.save(img_byte_arr, format='PNG') + + # Get the bytes value of the buffer + img_byte_arr = img_byte_arr.getvalue() + + # Create a dictionary with the image bytes + files = {'image': ('image.png', img_byte_arr)} + + # Send the POST request + response = requests.post('https://avatechgg--segment-anything-entrypoint.modal.run', files=files) + + # Check if the request was successful + if response.status_code == 200: + # Parse the JSON response + output = response.json() + else: + print(f"Request failed with status code {response.status_code}") + output = None + + # sam = sam_model_registry[model_type](checkpoint=ckpt) + # predictor = SamPredictor(sam) + # masks = predictor.set_torch_image + # masks = predictor.predict + + # print(output) + + return (output, ) + +NODE_CLASS_MAPPINGS = { + "SAM_Embedding": SAM_Embedding +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SAM_Embedding": "SAM Embedding" +} diff --git a/style-watcher.js b/style-watcher.js new file mode 100644 index 0000000..4392b84 --- /dev/null +++ b/style-watcher.js @@ -0,0 +1,24 @@ +const fs = require('fs'); +const chokidar = require('chokidar'); +const path = require('path'); + +// Initialize watcher. +const watcher = chokidar.watch(path.resolve(__dirname, './js/output.css'), { + persistent: true +}); + +// Add event listeners. +watcher + .on('change', filePath => { + console.log(`File ${filePath} has been changed`); + let cssContent = fs.readFileSync(filePath, 'utf8'); + let jsContent = `let style = document.createElement('style'); +style.innerHTML = \`${cssContent.replace(/\\/g, '\\\\')}\`; +document.head.appendChild(style);`; + fs.writeFileSync(path.resolve(__dirname, './js/style-inject.js'), jsContent); + }); + +// On script exit, close the watcher +process.on('exit', (code) => { + watcher.close(); +}); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..0ed6c3e --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,10 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + // content: ['./js/**/*.{html,js}'], + content: ['./js/blendshape_editor.js'], + theme: { + extend: {}, + }, + plugins: [], +} +