Revert "Move CGAL to isolated comfy-env environment"

This reverts commit 0d75216d1f2d3a550f1b4fddb1a2327e2d67be2d.
This commit is contained in:
Andrea Pozzetti
2026-02-12 23:54:40 +01:00
parent c057268134
commit df7be6ca40
6 changed files with 7 additions and 339 deletions
-1
View File
@@ -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
]
-193
View File
@@ -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
}
-111
View File
@@ -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
+5 -32
View File
@@ -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):,}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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