diff --git a/comfy-env.toml b/comfy-env.toml index 875466f0..4eb75b65 100644 --- a/comfy-env.toml +++ b/comfy-env.toml @@ -15,5 +15,4 @@ pytorch_version = "auto" "numpy<2", # bpy requires numpy 1.x "trimesh>=4.0.0", "tqdm", # Required by cumesh - "cgal>=0.9.0", # CGAL Python bindings for isotropic remeshing ] diff --git a/nodes/_utils/cgal_bridge.py b/nodes/_utils/cgal_bridge.py deleted file mode 100644 index 78221f0e..00000000 --- a/nodes/_utils/cgal_bridge.py +++ /dev/null @@ -1,193 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# Copyright (C) 2025 ComfyUI-GeometryPack Contributors - -""" -CGAL bridge for isolated environment operations. - -This module provides CGAL functions that run in an isolated Python 3.11 -environment with CGAL installed. Functions are called via comfy-env's -VenvWorker.call_module() mechanism. - -Data is passed via IPC (serialized as lists), so functions accept lists and -return lists/dicts with list values. - -Usage from host environment: - from .cgal_worker import call_cgal - result = call_cgal('cgal_isotropic_remesh', vertices=v, faces=f, ...) -""" - -import numpy as np - - -def _to_list(arr): - """Convert numpy array or list to nested list for IPC serialization.""" - if hasattr(arr, 'tolist'): - return arr.tolist() - return arr - - -def _to_numpy(data, dtype=np.float32): - """Convert list to numpy array.""" - return np.array(data, dtype=dtype) - - -def cgal_isotropic_remesh(vertices, faces, target_edge_length, iterations=3, protect_boundaries=True): - """ - Apply CGAL isotropic remeshing using official Python bindings. - - Creates a uniform triangle mesh with specified edge length using CGAL's - high-quality remeshing algorithm. - - Args: - vertices: list of [x,y,z] vertex positions - faces: list of [i,j,k] face indices - target_edge_length: Target edge length for output triangles - iterations: Number of remeshing iterations (1-20) - protect_boundaries: Preserve boundary edges - - Returns: - dict with 'vertices', 'faces', 'info', 'error' keys - """ - from CGAL import CGAL_Polygon_mesh_processing - from CGAL.CGAL_Polyhedron_3 import Polyhedron_3 - from CGAL.CGAL_Kernel import Point_3 - - print(f"[cgal_bridge] ===== Starting CGAL Isotropic Remeshing =====") - - # Convert inputs - vertices = _to_numpy(vertices, np.float64) - faces = _to_numpy(faces, np.int32) - - print(f"[cgal_bridge] Input: {len(vertices)} vertices, {len(faces)} faces") - print(f"[cgal_bridge] Parameters: target_edge_length={target_edge_length}, iterations={iterations}, protect_boundaries={protect_boundaries}") - - if len(vertices) == 0 or len(faces) == 0: - return {'vertices': [], 'faces': [], 'info': '', 'error': 'Mesh is empty'} - - if target_edge_length <= 0: - return {'vertices': [], 'faces': [], 'info': '', 'error': f'Target edge length must be positive, got {target_edge_length}'} - - if iterations < 1 or iterations > 20: - return {'vertices': [], 'faces': [], 'info': '', 'error': f'Iterations must be between 1 and 20, got {iterations}'} - - try: - # Step 1: Convert to CGAL Polyhedron_3 - print(f"[cgal_bridge] Converting to CGAL format...") - - # Create Point_3_Vector for vertices - points = CGAL_Polygon_mesh_processing.Point_3_Vector() - points.reserve(len(vertices)) - for v in vertices: - points.append(Point_3(float(v[0]), float(v[1]), float(v[2]))) - - # Create plain Python list of lists for faces - polygons = [[int(idx) for idx in face] for face in faces] - - # Create polyhedron from polygon soup - P = Polyhedron_3() - CGAL_Polygon_mesh_processing.polygon_soup_to_polygon_mesh(points, polygons, P) - - print(f"[cgal_bridge] CGAL mesh created: {P.size_of_vertices()} vertices, {P.size_of_facets()} facets") - - # Step 2: Collect all facets for remeshing - flist = [] - for fh in P.facets(): - flist.append(fh) - - # Step 3: Handle boundary protection if requested - if protect_boundaries: - print(f"[cgal_bridge] Collecting boundary halfedges for protection...") - hlist = [] - for hh in P.halfedges(): - if hh.is_border() or hh.opposite().is_border(): - hlist.append(hh) - - print(f"[cgal_bridge] Found {len(hlist)} boundary halfedges") - - # Perform remeshing with boundary protection - print(f"[cgal_bridge] Running CGAL isotropic_remeshing (with boundary protection)...") - CGAL_Polygon_mesh_processing.isotropic_remeshing( - flist, - target_edge_length, - P, - iterations, - hlist, - True # protect_constraints - ) - else: - # Perform remeshing without boundary protection - print(f"[cgal_bridge] Running CGAL isotropic_remeshing...") - CGAL_Polygon_mesh_processing.isotropic_remeshing( - flist, - target_edge_length, - P, - iterations - ) - - print(f"[cgal_bridge] Remeshing complete: {P.size_of_vertices()} vertices, {P.size_of_facets()} facets") - - # Step 4: Extract vertices back to numpy arrays - print(f"[cgal_bridge] Extracting results...") - new_vertices = [] - vertex_map = {} - - for i, vertex in enumerate(P.vertices()): - point = vertex.point() - new_vertices.append([point.x(), point.y(), point.z()]) - vertex_map[vertex] = i - - # Step 5: Extract faces back to numpy arrays - new_faces = [] - for facet in P.facets(): - halfedge = facet.halfedge() - face_vertices = [] - - start = halfedge - current = start - while True: - vertex_handle = current.vertex() - face_vertices.append(vertex_map[vertex_handle]) - current = current.next() - if current == start: - break - - if len(face_vertices) == 3: - new_faces.append(face_vertices) - - # Build info string - vertex_change = len(new_vertices) - len(vertices) - face_change = len(new_faces) - len(faces) - vertex_pct = (vertex_change / len(vertices)) * 100 if len(vertices) > 0 else 0 - face_pct = (face_change / len(faces)) * 100 if len(faces) > 0 else 0 - - info = ( - f"CGAL Isotropic Remeshing:\n" - f" Vertices: {len(vertices)} -> {len(new_vertices)} ({vertex_change:+d}, {vertex_pct:+.1f}%)\n" - f" Faces: {len(faces)} -> {len(new_faces)} ({face_change:+d}, {face_pct:+.1f}%)\n" - f" Target edge length: {target_edge_length}\n" - f" Iterations: {iterations}\n" - f" Protect boundaries: {protect_boundaries}" - ) - - print(f"[cgal_bridge] ===== Remeshing Complete =====") - print(f"[cgal_bridge] Vertices: {len(vertices)} -> {len(new_vertices)}") - print(f"[cgal_bridge] Faces: {len(faces)} -> {len(new_faces)}") - - return { - 'vertices': new_vertices, - 'faces': new_faces, - 'info': info, - 'error': '' - } - - except Exception as e: - import traceback - traceback.print_exc() - error_msg = f"Error during CGAL remesh: {str(e)}" - print(f"[cgal_bridge] ERROR: {error_msg}") - return { - 'vertices': [], - 'faces': [], - 'info': '', - 'error': error_msg - } diff --git a/nodes/_utils/cgal_worker.py b/nodes/_utils/cgal_worker.py deleted file mode 100644 index c7712864..00000000 --- a/nodes/_utils/cgal_worker.py +++ /dev/null @@ -1,111 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# Copyright (C) 2025 ComfyUI-GeometryPack Contributors - -""" -CGAL Worker Manager - Manages the isolated environment worker for CGAL operations. - -Provides a simple interface for calling cgal_bridge functions in the -isolated Python 3.11 environment with CGAL installed. - -Usage: - from .._utils.cgal_worker import call_cgal - result = call_cgal('cgal_isotropic_remesh', vertices=v, faces=f, ...) -""" - -from pathlib import Path -from typing import Any, Dict -import numpy as np - -# Lazy-loaded worker -_worker = None - - -def _get_worker(): - """Get or create the VenvWorker for the geometrypack environment.""" - global _worker - if _worker is not None: - return _worker - - from comfy_env import VenvWorker - - # Find the isolated environment - node_dir = Path(__file__).parent.parent.parent # ComfyUI-GeometryPack/ - env_path = node_dir / "_env_geometrypack" - - if not env_path.exists(): - raise RuntimeError( - f"Isolated environment not found at {env_path}\n" - "Run 'comfy-env install' to create it." - ) - - python_path = env_path / "bin" / "python" - if not python_path.exists(): - python_path = env_path / "Scripts" / "python.exe" # Windows - - if not python_path.exists(): - raise RuntimeError(f"Python not found in isolated environment: {env_path}") - - # Create worker with sys.path including our modules - utils_dir = Path(__file__).parent - _worker = VenvWorker( - python=str(python_path), - sys_path=[str(utils_dir)], - name="geometrypack_cgal" - ) - - return _worker - - -def call_cgal(func_name: str, **kwargs) -> Dict[str, Any]: - """ - Call a function in cgal_bridge via the isolated worker. - - Args: - func_name: Name of function in cgal_bridge module - **kwargs: Arguments to pass to the function - - Returns: - Result dict from the function - - Example: - result = call_cgal('cgal_isotropic_remesh', - vertices=[[0,0,0], [1,0,0], [0,1,0]], - faces=[[0,1,2]], - target_edge_length=0.1, - iterations=3, - protect_boundaries=True - ) - """ - worker = _get_worker() - - # Convert numpy arrays to lists for IPC - converted_kwargs = {} - for key, value in kwargs.items(): - if isinstance(value, np.ndarray): - converted_kwargs[key] = value.tolist() - else: - converted_kwargs[key] = value - - return worker.call_module( - module='cgal_bridge', - func=func_name, - **converted_kwargs - ) - - -def is_cgal_available() -> bool: - """Check if CGAL is available in the isolated environment.""" - try: - worker = _get_worker() - # Try a simple call to verify CGAL works - return True - except Exception: - return False - - -def shutdown(): - """Shutdown the worker process.""" - global _worker - if _worker is not None: - _worker.shutdown() - _worker = None diff --git a/nodes/remeshing/remesh.py b/nodes/remeshing/remesh.py index a31a908d..3ebd3249 100644 --- a/nodes/remeshing/remesh.py +++ b/nodes/remeshing/remesh.py @@ -237,46 +237,19 @@ After: return remeshed_mesh, info def _cgal_isotropic(self, trimesh, target_edge_length, iterations, protect_boundaries): - """CGAL isotropic remeshing using isolated environment.""" - from .._utils.cgal_worker import call_cgal - + """CGAL isotropic remeshing.""" protect = (protect_boundaries == "true") - - print(f"[Remesh] Running CGAL isotropic remesh (isolated environment)...") - result = call_cgal('cgal_isotropic_remesh', - vertices=np.asarray(trimesh.vertices, dtype=np.float64), - faces=np.asarray(trimesh.faces, dtype=np.int32), - target_edge_length=target_edge_length, - iterations=iterations, - protect_boundaries=protect + remeshed_mesh, error = mesh_ops.cgal_isotropic_remesh( + trimesh, target_edge_length, iterations, protect ) - - if result.get('error'): - raise ValueError(f"CGAL remeshing failed: {result['error']}") - - remeshed_mesh = trimesh_module.Trimesh( - vertices=np.array(result['vertices'], dtype=np.float64), - faces=np.array(result['faces'], dtype=np.int32), - process=False - ) - - # Preserve metadata - remeshed_mesh.metadata = trimesh.metadata.copy() - remeshed_mesh.metadata['remeshing'] = { - 'algorithm': 'cgal_isotropic', - 'target_edge_length': target_edge_length, - 'iterations': iterations, - 'protect_boundaries': protect, - 'original_vertices': len(trimesh.vertices), - 'original_faces': len(trimesh.faces) - } + if remeshed_mesh is None: + raise ValueError(f"CGAL remeshing failed: {error}") info = f"""Remesh Results (CGAL Isotropic): Target Edge Length: {target_edge_length} Iterations: {iterations} Protect Boundaries: {protect_boundaries} -Method: Isolated environment Before: Vertices: {len(trimesh.vertices):,} diff --git a/pyproject.toml b/pyproject.toml index c9a8cf57..2889f984 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "matplotlib>=3.5.0", "Pillow>=9.0.0", "pymeshlab>=2022.2", - # cgal - now in comfy-env isolated environment (see comfy-env.toml) + "cgal>=0.9.0", "point-cloud-utils>=0.30.0", "mesh-to-sdf>=0.0.14", "xatlas>=0.0.11", diff --git a/requirements.txt b/requirements.txt index bc1e207e..0ee49834 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ numpy>=1.21.0 scipy>=1.7.0 trimesh>=3.15.0 pymeshlab>=2022.2 -# cgal - now in comfy-env isolated environment (see comfy-env.toml) +cgal>=0.9.0 # Official CGAL Python bindings for isotropic remeshing rtree embreex