"""comfyui_mesh_cleanup_oneshot — limpia una malla 3D en una sola llamada. Promocion de la secuencia repetida (issue 0087) del post-proceso de mallas Hunyuan3D: decimar a un presupuesto de caras razonable y, opcionalmente, hacerla estanca. Capitaliza el "Golden 3" del report 0088 (80k caras + estanca a la vez). Compone funciones del registry del grupo `comfyui`: comfyui_simplify_mesh_py_ml (decima conservando apariencia) comfyui_make_watertight_py_ml (cierra la malla; solo si watertight=True) Las mallas nativas de ComfyUI/Hunyuan3D salen densas (decenas de MB, >1M caras) y NO estancas (VoxelToMeshBasic produce "cube-soup"). Este pipeline las deja listas para web/impresion: ligeras y, si se pide, cerradas. Pipeline impuro: lee y escribe archivos en disco. Requiere trimesh + pymeshlab + scipy (simplify) y, para method="voxel", scikit-image (make_watertight). """ from __future__ import annotations import os import sys # Importa las funciones del registry (mismo arbol python/functions). _FUNCTIONS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if _FUNCTIONS_ROOT not in sys.path: sys.path.insert(0, _FUNCTIONS_ROOT) from ml.comfyui_make_watertight import comfyui_make_watertight from ml.comfyui_simplify_mesh import comfyui_simplify_mesh def comfyui_mesh_cleanup_oneshot( in_path: str, *, target_faces: int = 80000, watertight: bool = True, method: str = "repair", out_path: str | None = None, ) -> dict: """Decima una malla y, opcionalmente, la hace estanca, en una sola llamada. Args: in_path: ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off). target_faces: caras objetivo del paso de decimacion (comfyui_simplify_mesh). keyword-only. watertight: si True (default) aplica comfyui_make_watertight tras decimar; si False solo decima. keyword-only. method: metodo de make_watertight cuando watertight=True. "repair" (default) conserva las caras decimadas (fill_holes + fix_normals) pero NO garantiza estanqueidad en mallas muy rotas; "voxel" GARANTIZA is_watertight=True via voxeliza+fill+marching cubes, a costa de re-mallar (cambia el conteo de caras y descarta apariencia). keyword-only. out_path: ruta de salida final. Si None, se deriva de in_path ("_cleaned.glb"). keyword-only. Returns: dict {ok, in_path, out_path, in_faces, simplified_faces, final_faces, watertight_requested, is_watertight, method, steps, error}. `steps` es la lista de resultados crudos de cada funcion compuesta (para auditar). Si algun paso falla, ok=False y error explica en cual. """ base = { "ok": False, "in_path": in_path, "out_path": "", "in_faces": 0, "simplified_faces": 0, "final_faces": 0, "watertight_requested": watertight, "is_watertight": None, "method": method, "steps": [], "error": "", } if watertight and method not in ("repair", "voxel"): return {**base, "error": f"method '{method}' invalido (usa 'repair' o 'voxel')"} if out_path is None: out_path = os.path.splitext(in_path)[0] + "_cleaned.glb" # Paso 1: decimar. Si no se pide watertight, este es el output final directo. simplify_out = out_path if not watertight else ( os.path.splitext(out_path)[0] + "_simplified.glb" ) s = comfyui_simplify_mesh(in_path, target_faces=target_faces, out_path=simplify_out) steps = [{"step": "simplify_mesh", **s}] if not s.get("ok"): return {**base, "steps": steps, "error": f"simplify_mesh fallo: {s.get('error')}"} in_faces = s["in_faces"] simplified_faces = s["out_faces"] if not watertight: return { **base, "ok": True, "out_path": s["out_path"], "in_faces": in_faces, "simplified_faces": simplified_faces, "final_faces": simplified_faces, "is_watertight": None, "steps": steps, } # Paso 2: hacer estanca la malla decimada. w = comfyui_make_watertight(s["out_path"], method=method, out_path=out_path) steps.append({"step": "make_watertight", **w}) if not w.get("ok"): return { **base, "out_path": s["out_path"], "in_faces": in_faces, "simplified_faces": simplified_faces, "final_faces": simplified_faces, "steps": steps, "error": f"make_watertight fallo: {w.get('error')}", } return { **base, "ok": True, "out_path": w["out_path"], "in_faces": in_faces, "simplified_faces": simplified_faces, "final_faces": w["out_faces"], "is_watertight": w["is_watertight"], "steps": steps, } if __name__ == "__main__": import json src = sys.argv[1] if len(sys.argv) > 1 else ( os.path.expanduser("~/ComfyUI/output/3d_mesh_00002_.glb")) res = comfyui_mesh_cleanup_oneshot(src) print(json.dumps({k: v for k, v in res.items() if k != "steps"}, indent=2))