Revert "Move CGAL to isolated comfy-env environment"
This reverts commit 0d75216d1f2d3a550f1b4fddb1a2327e2d67be2d.
This commit is contained in:
@@ -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
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user