diff --git a/nodes/main/reconstruction_cgal/alpha_wrap.py b/nodes/main/reconstruction_cgal/alpha_wrap.py index 5c824949..8d054b2f 100644 --- a/nodes/main/reconstruction_cgal/alpha_wrap.py +++ b/nodes/main/reconstruction_cgal/alpha_wrap.py @@ -5,26 +5,76 @@ Alpha Wrap Node - Shrink wrap mesh generation using CGAL's Alpha Wrap algorithm. Creates a watertight mesh that tightly wraps around input geometry. -Useful for point clouds, non-manifold meshes, or polygon soups. +Useful for non-manifold meshes or polygon soups. """ import logging +import os +import threading +import time + import numpy as np import trimesh +import comfy.utils from comfy_api.latest import io +from CGAL import CGAL_Alpha_wrap_3 +from CGAL.CGAL_Kernel import Point_3 +from CGAL.CGAL_Polyhedron_3 import Polyhedron_3 + log = logging.getLogger("geometrypack") +# Progress bar layout (percentage of total) +_PHASE_BUILD = 15 # 0-15%: building CGAL input +_PHASE_WRAP_START = 15 # 15%: wrap starts +_PHASE_WRAP_END = 85 # ..85%: wrap ends (time-based fill) +_PHASE_EXTRACT = 100 # 85-100%: extracting result + + +def _get_rss_mb(): + """Get current process RSS in MB (Linux).""" + try: + with open(f"/proc/{os.getpid()}/statm", "r") as f: + pages = int(f.read().split()[1]) + return pages * os.sysconf("SC_PAGE_SIZE") / (1024 * 1024) + except Exception: + return 0.0 + + +def _polyhedron_to_arrays(poly): + """Extract vertices and faces from a CGAL Polyhedron_3.""" + verts = [] + vertex_map = {} + for i, vertex in enumerate(poly.vertices()): + point = vertex.point() + verts.append([float(point.x()), float(point.y()), float(point.z())]) + vertex_map[vertex] = i + + faces = [] + for facet in poly.facets(): + halfedge = facet.halfedge() + face_verts = [] + start_he = halfedge + current = start_he + while True: + face_verts.append(vertex_map[current.vertex()]) + current = current.next() + if current == start_he: + break + if len(face_verts) == 3: + faces.append(face_verts) + + return np.array(verts, dtype=np.float64), np.array(faces, dtype=np.int32) + class AlphaWrapNode(io.ComfyNode): """ Alpha Wrap - Generate a watertight shrink-wrapped mesh. - Uses CGAL's Alpha Wrap algorithm (via PyMeshLab) to create a - tight-fitting watertight mesh around input geometry. + Uses CGAL's Alpha Wrap algorithm to create a tight-fitting + watertight mesh around input geometry. Works with: - - Point clouds - Non-manifold meshes - Meshes with holes - Polygon soups (overlapping/intersecting geometry) @@ -46,8 +96,8 @@ class AlphaWrapNode(io.ComfyNode): is_output_node=True, inputs=[ io.Custom("TRIMESH").Input("input_mesh"), - io.Float.Input("alpha_percent", default=0.04, min=0.001, max=50.0, step=0.01, tooltip="Wrap tightness as % of bounding box diagonal. Smaller = tighter wrap with more detail, but slower.", optional=True), - io.Float.Input("offset_percent", default=1.1, min=0.01, max=10.0, step=0.01, tooltip="Surface offset as % of bounding box diagonal. Smaller = closer to original surface.", optional=True), + io.Float.Input("alpha_percent", default=2.0, min=0.001, max=50.0, step=0.1, tooltip="Wrap tightness as % of bounding box diagonal. Smaller = tighter wrap with more detail, but MUCH slower (runtime ~ 1/alpha\u00b3). Start at 2-5% and tighten if needed.", optional=True), + io.Float.Input("offset_percent", default=2.0, min=0.01, max=10.0, step=0.1, tooltip="Surface offset as % of bounding box diagonal. Smaller = closer to original surface.", optional=True), ], outputs=[ io.Custom("TRIMESH").Output(display_name="wrapped_mesh"), @@ -56,31 +106,10 @@ class AlphaWrapNode(io.ComfyNode): ) @classmethod - def execute(cls, input_mesh, alpha_percent=0.04, offset_percent=1.1): - """ - Generate alpha-wrapped mesh. - - Args: - input_mesh: Input trimesh.Trimesh object (mesh or point cloud) - alpha_percent: Alpha value as percentage of bbox diagonal - offset_percent: Offset value as percentage of bbox diagonal - - Returns: - tuple: (wrapped_trimesh, report_string) - """ - try: - import pymeshlab - except ImportError: - raise ImportError( - "Alpha Wrap requires PyMeshLab with CGAL support.\n" - "Install with: pip install pymeshlab" - ) - - # Get input stats + def execute(cls, input_mesh, alpha_percent=2.0, offset_percent=2.0): vertices = np.asarray(input_mesh.vertices, dtype=np.float64) input_vertex_count = len(vertices) - # Check if input is point cloud or mesh is_point_cloud = ( not hasattr(input_mesh, 'faces') or input_mesh.faces is None or @@ -111,30 +140,69 @@ class AlphaWrapNode(io.ComfyNode): log.info("Alpha: %.6f (%s%% of bbox)", alpha, alpha_percent) log.info("Offset: %.6f (%s%% of bbox)", offset, offset_percent) - # Create PyMeshLab MeshSet - ms = pymeshlab.MeshSet() + pbar = comfy.utils.ProgressBar(100) - # Add mesh to MeshSet - faces = np.asarray(input_mesh.faces, dtype=np.int32) - pml_mesh = pymeshlab.Mesh( - vertex_matrix=vertices, - face_matrix=faces - ) - ms.add_mesh(pml_mesh) + # --- Phase 1: Build CGAL input data (0-15%) --- + t0 = time.monotonic() + cgal_points = CGAL_Alpha_wrap_3.Point_3_Vector() + cgal_points.reserve(input_vertex_count) + report_every = max(1, input_vertex_count // 10) + for i, v in enumerate(vertices): + cgal_points.append(Point_3(float(v[0]), float(v[1]), float(v[2]))) + if i % report_every == 0: + pbar.update_absolute(int(_PHASE_BUILD * i / input_vertex_count)) - # Run Alpha Wrap - log.info("Running CGAL Alpha Wrap... (this may take a while)") - ms.generate_alpha_wrap( - alpha=pymeshlab.PureValue(alpha), - offset=pymeshlab.PureValue(offset) - ) + cgal_faces = [[int(idx) for idx in face] for face in input_mesh.faces] + pbar.update_absolute(_PHASE_BUILD) + log.info("Built CGAL input data in %.1fs", time.monotonic() - t0) - # Get result - result_pml = ms.current_mesh() - result_vertices = result_pml.vertex_matrix() - result_faces = result_pml.face_matrix() + # --- Phase 2: Run alpha wrap (15-85%) --- + output_poly = Polyhedron_3() + exc_holder = [None] + rss_before = _get_rss_mb() + + def _run(): + try: + CGAL_Alpha_wrap_3.alpha_wrap_3(cgal_points, cgal_faces, alpha, offset, output_poly) + except Exception as e: + exc_holder[0] = e + + log.info("Running CGAL Alpha Wrap...") + thread = threading.Thread(target=_run, daemon=True) + thread.start() + + wrap_start = time.monotonic() + interval = 5 + wrap_range = _PHASE_WRAP_END - _PHASE_WRAP_START + while thread.is_alive(): + thread.join(timeout=interval) + elapsed = time.monotonic() - wrap_start + rss = _get_rss_mb() + delta = rss - rss_before + if thread.is_alive(): + # Asymptotic progress: approaches _PHASE_WRAP_END but never reaches it + # Uses 1 - 1/(1 + t/30) so it fills ~50% at 30s, ~75% at 90s, etc. + frac = 1.0 - 1.0 / (1.0 + elapsed / 30.0) + progress = int(_PHASE_WRAP_START + wrap_range * frac) + pbar.update_absolute(progress) + log.info("Alpha Wrap running... %.0fs elapsed, RSS %.0f MB (+%.0f MB)", + elapsed, rss, delta) + + elapsed_total = time.monotonic() - wrap_start + rss_final = _get_rss_mb() + pbar.update_absolute(_PHASE_WRAP_END) + log.info("Alpha Wrap finished in %.1fs, RSS %.0f MB (+%.0f MB)", + elapsed_total, rss_final, rss_final - rss_before) + + if exc_holder[0] is not None: + raise exc_holder[0] + + # --- Phase 3: Extract result mesh (85-100%) --- + t0 = time.monotonic() + result_vertices, result_faces = _polyhedron_to_arrays(output_poly) + pbar.update_absolute(_PHASE_EXTRACT) + log.info("Extracted result mesh in %.1fs", time.monotonic() - t0) - # Create trimesh result result_mesh = trimesh.Trimesh( vertices=result_vertices, faces=result_faces, @@ -147,7 +215,6 @@ class AlphaWrapNode(io.ComfyNode): else: result_mesh.metadata = {} - # Add alpha wrap info to metadata result_mesh.metadata['alpha_wrap'] = { 'alpha': alpha, 'alpha_percent': alpha_percent, @@ -157,7 +224,6 @@ class AlphaWrapNode(io.ComfyNode): 'input_type': input_type } - # Compute stats output_vertex_count = len(result_mesh.vertices) output_face_count = len(result_mesh.faces) is_watertight = result_mesh.is_watertight @@ -165,7 +231,6 @@ class AlphaWrapNode(io.ComfyNode): log.info("Result: %s vertices, %s faces", f"{output_vertex_count:,}", f"{output_face_count:,}") log.info("Watertight: %s", is_watertight) - # Build report report = f"""Alpha Wrap Report {'='*40}