diff --git a/__init__.py b/__init__.py index df7d180d..1b634ad0 100644 --- a/__init__.py +++ b/__init__.py @@ -3,9 +3,7 @@ import os import sys -import shutil from pathlib import Path -from datetime import datetime print("[geompack] loading...", file=sys.stderr, flush=True) from comfy_env import register_nodes @@ -36,150 +34,5 @@ def _generate_widget_mappings(): _generate_widget_mappings() -# Server routes -try: - from aiohttp import web - from server import PromptServer - import folder_paths - - routes = PromptServer.instance.routes - - @routes.post("/geometrypack/save_preview") - async def save_preview_mesh(request): - try: - json_data = await request.json() - temp_filename = json_data.get("temp_filename") - if not temp_filename: - return web.json_response({"success": False, "error": "No temp_filename"}, status=400) - - output_dir = folder_paths.get_output_directory() - temp_filepath = os.path.join(output_dir, temp_filename) - if not os.path.exists(temp_filepath): - return web.json_response({"success": False, "error": "File not found"}, status=404) - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - file_ext = os.path.splitext(temp_filename)[1] - saved_filename = f"mesh_{timestamp}{file_ext}" - shutil.copy2(temp_filepath, os.path.join(output_dir, saved_filename)) - - return web.json_response({"success": True, "saved_filename": saved_filename}) - except Exception as e: - return web.json_response({"success": False, "error": str(e)}, status=500) - - @routes.post("/geompack/analyze") - async def analyze_mesh(request): - try: - from .nodes.visualization.preview_mesh_analysis import ( - get_cached_mesh, add_field_to_cached_mesh, - compute_boundary_vertices, compute_connected_components, - compute_self_intersections - ) - from .nodes.visualization._vtp_export import export_mesh_with_scalars_vtp - - json_data = await request.json() - mesh_id = json_data.get("mesh_id") - analysis_type = json_data.get("analysis_type") - - if not mesh_id or not analysis_type: - return web.json_response({"success": False, "error": "Missing params"}, status=400) - - cache_entry = get_cached_mesh(mesh_id) - if not cache_entry: - return web.json_response({"success": False, "error": "Mesh not found"}, status=404) - - mesh = cache_entry['mesh'] - filename = cache_entry['filename'] - - if analysis_type == "open_edges": - mesh, count = compute_boundary_vertices(mesh) - field_name = "boundary_vertex" - elif analysis_type == "components": - mesh, count = compute_connected_components(mesh) - field_name = "face.part_id" - elif analysis_type == "self_intersect": - mesh, count = compute_self_intersections(mesh) - field_name = "face.self_intersect" - else: - return web.json_response({"success": False, "error": f"Unknown: {analysis_type}"}, status=400) - - add_field_to_cached_mesh(mesh_id, field_name) - filepath = os.path.join(folder_paths.get_output_directory(), filename) - export_mesh_with_scalars_vtp(mesh, filepath) - - return web.json_response({"success": True, "filename": filename, "field_name": field_name, "count": count}) - except Exception as e: - return web.json_response({"success": False, "error": str(e)}, status=500) - - @routes.post("/geompack/find_location") - async def find_location(request): - try: - from .nodes.visualization.preview_mesh_analysis import get_cached_mesh - import re - - json_data = await request.json() - mesh_id = json_data.get("mesh_id") - query = json_data.get("query", "").strip() - - if not mesh_id or not query: - return web.json_response({"success": False, "error": "Missing params"}, status=400) - - cache_entry = get_cached_mesh(mesh_id) - if not cache_entry: - return web.json_response({"success": False, "error": "Mesh not found"}, status=404) - - mesh = cache_entry['mesh'] - point, query_type, element_id = None, None, None - - # Face ID: f123, face123 - face_match = re.match(r'^[fF](?:ace)?\s*(\d+)$', query) - if face_match: - face_id = int(face_match.group(1)) - if 0 <= face_id < len(mesh.faces): - point = mesh.vertices[mesh.faces[face_id]].mean(axis=0).tolist() - query_type, element_id = "face", face_id - - # Vertex ID: v123, vertex123 - if point is None: - vertex_match = re.match(r'^[vVpP](?:ertex|oint)?\s*(\d+)$', query) - if vertex_match: - vertex_id = int(vertex_match.group(1)) - if 0 <= vertex_id < len(mesh.vertices): - point = mesh.vertices[vertex_id].tolist() - query_type, element_id = "vertex", vertex_id - - # XYZ coordinates - if point is None: - coord_str = query.strip('()[]') - parts = re.split(r'[,\s]+', coord_str) - if len(parts) == 3: - try: - point = [float(parts[0]), float(parts[1]), float(parts[2])] - query_type = "coordinate" - except ValueError: - pass - - # Plain number = face ID - if point is None: - try: - face_id = int(query) - if 0 <= face_id < len(mesh.faces): - point = mesh.vertices[mesh.faces[face_id]].mean(axis=0).tolist() - query_type, element_id = "face", face_id - except ValueError: - pass - - if point is None: - return web.json_response({"success": False, "error": f"Could not parse: {query}"}, status=400) - - response = {"success": True, "point": point, "type": query_type} - if element_id is not None: - response["id"] = element_id - return web.json_response(response) - except Exception as e: - return web.json_response({"success": False, "error": str(e)}, status=500) - -except Exception: - pass - WEB_DIRECTORY = "./web" __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] diff --git a/comfy-env-root.toml b/comfy-env-root.toml index b67efb8e..720445c3 100644 --- a/comfy-env-root.toml +++ b/comfy-env-root.toml @@ -6,12 +6,4 @@ OMP_NUM_THREADS = 1 # Include both old and new package names for compatibility across Ubuntu versions # (libgl1-mesa-glx was renamed to libgl1 in Ubuntu 22.04+) # libopengl0 provides libOpenGL.so.0 (required by pymeshlab) -packages = ["libgl1-mesa-glx", "libgl1", "libopengl0", "libglu1-mesa"] - -# Linux-specific pypi packages -[target.linux-64.pypi-dependencies] -embreex = "*" - -# Windows-specific packages -[target.win-64.pypi-dependencies] -embreex = "*" \ No newline at end of file +packages = ["libgl1-mesa-glx", "libgl1", "libopengl0", "libglu1-mesa"] \ No newline at end of file diff --git a/nodes/cgal/__init__.py b/nodes/cgal/__init__.py deleted file mode 100644 index 8ea16e81..00000000 --- a/nodes/cgal/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# Copyright (C) 2025 ComfyUI-GeometryPack Contributors - -""" -CGAL-dependent nodes - requires igl.copyleft.cgal or CGAL Python bindings. -These nodes will only load if CGAL dependencies are available. -""" - -# Import submodules -from . import repair -from . import boolean -from . import remeshing -from . import reconstruction - -# Collect all node class mappings -NODE_CLASS_MAPPINGS = {} -NODE_CLASS_MAPPINGS.update(repair.NODE_CLASS_MAPPINGS) -NODE_CLASS_MAPPINGS.update(boolean.NODE_CLASS_MAPPINGS) -NODE_CLASS_MAPPINGS.update(remeshing.NODE_CLASS_MAPPINGS) -NODE_CLASS_MAPPINGS.update(reconstruction.NODE_CLASS_MAPPINGS) - -# Collect all display name mappings -NODE_DISPLAY_NAME_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS.update(repair.NODE_DISPLAY_NAME_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(boolean.NODE_DISPLAY_NAME_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(remeshing.NODE_DISPLAY_NAME_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(reconstruction.NODE_DISPLAY_NAME_MAPPINGS) - -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/cgal/comfy-env.toml b/nodes/cgal/comfy-env.toml deleted file mode 100644 index 134f260f..00000000 --- a/nodes/cgal/comfy-env.toml +++ /dev/null @@ -1,19 +0,0 @@ -[apt] -packages = ["libgl1-mesa-glx", "libgl1", "libopengl0", "libglu1-mesa"] - -[dependencies] -cgal = "*" - -[pypi-dependencies] -numpy = "*" -trimesh = { version = "*", extras = ["easy"] } -pymeshlab = ">=2025.7" -libigl = "*" - -# Windows needs Visual C++ runtime for CGAL DLLs -[target.win-64.pypi-dependencies] -msvc-runtime = "*" - -[env_vars] -KMP_DUPLICATE_LIB_OK = "TRUE" -OMP_NUM_THREADS = 1 \ No newline at end of file diff --git a/nodes/gpu/__init__.py b/nodes/gpu/__init__.py deleted file mode 100644 index 13702b31..00000000 --- a/nodes/gpu/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Import submodules -from . import remesh - -# Collect all node class mappings -NODE_CLASS_MAPPINGS = {} -NODE_CLASS_MAPPINGS.update(remesh.NODE_CLASS_MAPPINGS) - -# Collect all display name mappings -NODE_DISPLAY_NAME_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS.update(remesh.NODE_DISPLAY_NAME_MAPPINGS) - -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/gpu/comfy-env.toml b/nodes/gpu/comfy-env.toml deleted file mode 100644 index cf4152ad..00000000 --- a/nodes/gpu/comfy-env.toml +++ /dev/null @@ -1,7 +0,0 @@ -[cuda] -packages = ["cumesh", "torch", "torchvision"] - -[pypi-dependencies] -numpy = "*" -trimesh = { version = "*", extras = ["easy"] } -tqdm = "*" \ No newline at end of file diff --git a/nodes/main/__init__.py b/nodes/main/__init__.py index d35f64f2..e7bc425e 100644 --- a/nodes/main/__init__.py +++ b/nodes/main/__init__.py @@ -2,8 +2,8 @@ # Copyright (C) 2025 ComfyUI-GeometryPack Contributors """ -Main nodes - Basic dependencies (trimesh, numpy, pymeshlab, etc.) -These nodes will always load regardless of CGAL or Blender availability. +Main nodes - All non-Blender geometry processing nodes. +Runs in an isolation env (pixi/conda) to avoid DLL conflicts with ComfyUI's torch. """ from . import io @@ -21,6 +21,15 @@ from . import remeshing from . import reconstruction from . import texture_remeshing +# CGAL nodes (moved from nodes/cgal/) +from . import boolean +from . import reconstruction_cgal +from . import remeshing_cgal +from . import repair_cgal + +# GPU nodes (moved from nodes/gpu/) +from . import remeshing_gpu + # Collect all node class mappings NODE_CLASS_MAPPINGS = {} NODE_CLASS_MAPPINGS.update(io.NODE_CLASS_MAPPINGS) @@ -37,6 +46,11 @@ NODE_CLASS_MAPPINGS.update(repair.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(remeshing.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(reconstruction.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(texture_remeshing.NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(boolean.NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(reconstruction_cgal.NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(remeshing_cgal.NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(repair_cgal.NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(remeshing_gpu.NODE_CLASS_MAPPINGS) # Collect all display name mappings NODE_DISPLAY_NAME_MAPPINGS = {} @@ -54,5 +68,10 @@ NODE_DISPLAY_NAME_MAPPINGS.update(repair.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(remeshing.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(reconstruction.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(texture_remeshing.NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(boolean.NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(reconstruction_cgal.NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(remeshing_cgal.NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(repair_cgal.NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(remeshing_gpu.NODE_DISPLAY_NAME_MAPPINGS) __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/cgal/boolean/__init__.py b/nodes/main/boolean/__init__.py similarity index 100% rename from nodes/cgal/boolean/__init__.py rename to nodes/main/boolean/__init__.py diff --git a/nodes/cgal/boolean/boolean.py b/nodes/main/boolean/boolean.py similarity index 100% rename from nodes/cgal/boolean/boolean.py rename to nodes/main/boolean/boolean.py diff --git a/nodes/main/comfy-env.toml b/nodes/main/comfy-env.toml new file mode 100644 index 00000000..db1b0fed --- /dev/null +++ b/nodes/main/comfy-env.toml @@ -0,0 +1,39 @@ +[apt] +packages = ["libgl1-mesa-glx", "libgl1", "libopengl0", "libglu1-mesa"] + +[dependencies] +cgal = "*" + +[cuda] +packages = ["cumesh", "torch", "torchvision"] + +[pypi-dependencies] +numpy = "*" +scipy = ">=1.7.0" +tqdm = ">=4.60.0" +requests = ">=2.25.0" +trimesh = { version = ">=4.0.0", extras = ["easy"] } +libigl = ">=2.6.1" +pymeshlab = ">=2025.7" +rtree = "*" +point-cloud-utils = ">=0.30.0" +mesh-to-sdf = ">=0.0.14" +fast-simplification = ">=0.1.5" +pymeshfix = ">=0.17.0" +xatlas = ">=0.0.11" +skeletor = ">=1.2.0" +pynanoinstantmeshes = "*" +matplotlib = ">=3.5.0" +pillow = ">=9.0.0" +pyvista = ">=0.43.0" + +[target.win-64.pypi-dependencies] +embreex = "*" +msvc-runtime = "*" + +[target.linux-64.pypi-dependencies] +embreex = "*" + +[env_vars] +KMP_DUPLICATE_LIB_OK = "TRUE" +OMP_NUM_THREADS = 1 diff --git a/nodes/cgal/reconstruction/__init__.py b/nodes/main/reconstruction_cgal/__init__.py similarity index 100% rename from nodes/cgal/reconstruction/__init__.py rename to nodes/main/reconstruction_cgal/__init__.py diff --git a/nodes/cgal/reconstruction/alpha_wrap.py b/nodes/main/reconstruction_cgal/alpha_wrap.py similarity index 100% rename from nodes/cgal/reconstruction/alpha_wrap.py rename to nodes/main/reconstruction_cgal/alpha_wrap.py diff --git a/nodes/cgal/remeshing/__init__.py b/nodes/main/remeshing_cgal/__init__.py similarity index 100% rename from nodes/cgal/remeshing/__init__.py rename to nodes/main/remeshing_cgal/__init__.py diff --git a/nodes/cgal/remeshing/remesh.py b/nodes/main/remeshing_cgal/remesh.py similarity index 100% rename from nodes/cgal/remeshing/remesh.py rename to nodes/main/remeshing_cgal/remesh.py diff --git a/nodes/main/remeshing_gpu/__init__.py b/nodes/main/remeshing_gpu/__init__.py new file mode 100644 index 00000000..34edfb91 --- /dev/null +++ b/nodes/main/remeshing_gpu/__init__.py @@ -0,0 +1,10 @@ +# Import submodules +from .remesh import NODE_CLASS_MAPPINGS as REMESH_MAPS, NODE_DISPLAY_NAME_MAPPINGS as REMESH_DISP + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} + +NODE_CLASS_MAPPINGS.update(REMESH_MAPS) +NODE_DISPLAY_NAME_MAPPINGS.update(REMESH_DISP) + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/gpu/remesh.py b/nodes/main/remeshing_gpu/remesh.py similarity index 100% rename from nodes/gpu/remesh.py rename to nodes/main/remeshing_gpu/remesh.py diff --git a/nodes/cgal/repair/__init__.py b/nodes/main/repair_cgal/__init__.py similarity index 100% rename from nodes/cgal/repair/__init__.py rename to nodes/main/repair_cgal/__init__.py diff --git a/nodes/cgal/repair/detect_intersections.py b/nodes/main/repair_cgal/detect_intersections.py similarity index 100% rename from nodes/cgal/repair/detect_intersections.py rename to nodes/main/repair_cgal/detect_intersections.py diff --git a/nodes/cgal/repair/fix_intersections_perturbation.py b/nodes/main/repair_cgal/fix_intersections_perturbation.py similarity index 100% rename from nodes/cgal/repair/fix_intersections_perturbation.py rename to nodes/main/repair_cgal/fix_intersections_perturbation.py diff --git a/nodes/cgal/repair/remesh_intersections.py b/nodes/main/repair_cgal/remesh_intersections.py similarity index 100% rename from nodes/cgal/repair/remesh_intersections.py rename to nodes/main/repair_cgal/remesh_intersections.py diff --git a/prestartup_script.py b/prestartup_script.py index e99e47f3..016160c3 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -9,42 +9,6 @@ from comfy_3d_viewers import copy_viewer setup_env() -# --------------------------------------------------------------------------- -# Crash diagnostics: install hooks that persist AFTER prestartup completes. -# These help identify which ComfyUI import causes the process to crash. -# Only active when COMFY_ENV_DEBUG=1 (set in CI). -# --------------------------------------------------------------------------- -if os.environ.get("COMFY_ENV_DEBUG", "").lower() in ("1", "true", "yes"): - import atexit - - def _exit_hook(): - print("[geompack] atexit handler called — clean Python shutdown", - file=sys.stderr, flush=True) - atexit.register(_exit_hook) - - _orig_excepthook = sys.excepthook - def _crash_excepthook(exc_type, exc_value, exc_tb): - print(f"[geompack] UNHANDLED: {exc_type.__name__}: {exc_value}", - file=sys.stderr, flush=True) - _orig_excepthook(exc_type, exc_value, exc_tb) - sys.excepthook = _crash_excepthook - - class _ImportTracer: - """Log first-time imports of key modules to identify crash location.""" - _PREFIXES = ('comfy', 'execution', 'server', 'protocol', 'nodes', - 'torch', 'comfyui_version', 'hook_breaker', 'comfy_aimdo', - 'app', 'cuda_malloc') - - def find_spec(self, fullname, path=None, target=None): - if fullname not in sys.modules: - parts = fullname.split('.') - if parts[0] in self._PREFIXES and len(parts) <= 2: - print(f"[import-trace] {fullname}", - file=sys.stderr, flush=True) - return None # never handle — let real finders do the work - - sys.meta_path.insert(0, _ImportTracer()) - SCRIPT_DIR = Path(__file__).resolve().parent COMFYUI_DIR = SCRIPT_DIR.parent.parent diff --git a/requirements.txt b/requirements.txt index b7dcf1ef..2aba538b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,7 @@ -# Core -comfy-env==0.1.63 -scipy>=1.7.0 -tqdm>=4.60.0 -requests>=2.25.0 - -# Mesh processing -trimesh>=4.0.0 -libigl>=2.6.1 -pymeshlab>=2025.7 -rtree -point-cloud-utils>=0.30.0 -mesh-to-sdf>=0.0.14 -fast-simplification>=0.1.5 -pymeshfix>=0.17.0 -xatlas>=0.0.11 -skeletor>=1.2.0 -PyNanoInstantMeshes - -# Visualization -matplotlib>=3.5.0 -Pillow>=9.0.0 -pyvista>=0.43.0 - -# ComfyUI integration +# Main process deps (prestartup + lightweight shared libs) +comfy-env==0.1.64 comfy-dynamic-widgets==0.1.1 comfy-3d-viewers==0.2.2 - -# Platform-specific -embreex; sys_platform != "darwin" \ No newline at end of file +numpy +trimesh>=4.0.0 +scipy>=1.7.0