feat(ml): auto-commit con 7 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Hace estanca (watertight) una malla 3D GLB/OBJ/PLY.
|
||||
|
||||
Post-proceso de las mallas de ComfyUI/Hunyuan3D producidas con el nodo
|
||||
VoxelToMeshBasic (DEPRECATED), que genera mallas NO estancas (is_watertight=False):
|
||||
crea 4 vertices nuevos por cara expuesta sin soldarlos, dejando huecos y bordes
|
||||
non-manifold. Dos metodos:
|
||||
|
||||
- method="voxel" (default, garantiza is_watertight=True): voxeliza el solido,
|
||||
rellena el interior y reconstruye la superficie con marching cubes
|
||||
(trimesh voxelized(pitch).fill().marching_cubes). Produce una malla cerrada por
|
||||
construccion. Coste: mas caras (densidad del marching cubes) y descarta la
|
||||
apariencia (UV/vertex colors). Necesita scikit-image (marching cubes).
|
||||
- method="repair": limpieza ligera con trimesh.repair (fix_winding + fill_holes +
|
||||
fix_normals + merge_vertices). Conserva el detalle y las caras, pero NO garantiza
|
||||
estanqueidad en mallas muy rotas (solo cierra huecos pequenos).
|
||||
|
||||
La via de RAIZ (no este post-proceso) es generar con el nodo VoxelToMesh
|
||||
algorithm='surface net', que da malla manifold cerrada sin reparar (ver report 0088).
|
||||
|
||||
Impura: lee y escribe archivos en disco. Requiere trimesh (+ scikit-image para voxel).
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _load_mesh(path):
|
||||
import trimesh
|
||||
|
||||
obj = trimesh.load(path, process=False)
|
||||
if isinstance(obj, trimesh.Scene):
|
||||
obj = trimesh.util.concatenate(list(obj.geometry.values()))
|
||||
return obj
|
||||
|
||||
|
||||
def comfyui_make_watertight(
|
||||
in_path: str,
|
||||
*,
|
||||
method: str = "voxel",
|
||||
pitch: float | None = None,
|
||||
out_path: str | None = None,
|
||||
) -> dict:
|
||||
"""Hace estanca una malla GLB/OBJ/PLY por voxel-remesh o reparacion.
|
||||
|
||||
Args:
|
||||
in_path: ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off).
|
||||
method: "voxel" (default, garantiza is_watertight=True via voxeliza+fill+
|
||||
marching cubes) o "repair" (fill_holes + fix_normals, conserva detalle
|
||||
pero no siempre estanca). keyword-only.
|
||||
pitch: solo para method="voxel". Tamano de voxel absoluto. Si None, se
|
||||
calcula como diagonal_bbox / 200 (mas fino = mas caras y detalle).
|
||||
keyword-only.
|
||||
out_path: ruta de salida. Si None, escribe "<in>_watertight.glb" junto al
|
||||
original. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, was_watertight, is_watertight, out_path, method, pitch, out_faces,
|
||||
error}. was/is_watertight = estanqueidad antes/despues (trimesh). Si falla,
|
||||
ok=False y error explica.
|
||||
"""
|
||||
base_err = {
|
||||
"ok": False, "was_watertight": None, "is_watertight": None,
|
||||
"out_path": "", "method": method, "pitch": None, "out_faces": 0,
|
||||
}
|
||||
try:
|
||||
import trimesh
|
||||
except ImportError as exc:
|
||||
return {**base_err, "error": f"falta trimesh: {exc}. cd python && uv add trimesh"}
|
||||
|
||||
if method not in ("voxel", "repair"):
|
||||
return {**base_err, "error": f"method '{method}' invalido (usa 'voxel' o 'repair')"}
|
||||
if not os.path.exists(in_path):
|
||||
return {**base_err, "error": f"no existe el archivo de entrada: {in_path!r}"}
|
||||
if out_path is None:
|
||||
out_path = os.path.splitext(in_path)[0] + "_watertight.glb"
|
||||
|
||||
try:
|
||||
mesh = _load_mesh(in_path)
|
||||
except Exception as exc:
|
||||
return {**base_err, "error": f"no se pudo cargar la malla {in_path!r}: {exc}"}
|
||||
was = bool(mesh.is_watertight)
|
||||
|
||||
try:
|
||||
if method == "voxel":
|
||||
m = mesh.copy()
|
||||
m.merge_vertices()
|
||||
if pitch is None:
|
||||
diag = float(np.linalg.norm(m.extents))
|
||||
pitch = diag / 200.0
|
||||
vg = m.voxelized(pitch=float(pitch)).fill()
|
||||
out = vg.marching_cubes
|
||||
out.merge_vertices()
|
||||
trimesh.repair.fix_normals(out)
|
||||
else: # repair
|
||||
out = mesh.copy()
|
||||
out.merge_vertices()
|
||||
trimesh.repair.fix_winding(out)
|
||||
trimesh.repair.fill_holes(out)
|
||||
trimesh.repair.fix_normals(out)
|
||||
|
||||
parent = os.path.dirname(out_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
out.export(out_path)
|
||||
except ImportError as exc:
|
||||
return {**base_err, "was_watertight": was,
|
||||
"error": f"falta dependencia para method='{method}': {exc}. "
|
||||
f"El voxel-remesh necesita scikit-image: cd python && uv add scikit-image"}
|
||||
except Exception as exc:
|
||||
return {**base_err, "was_watertight": was,
|
||||
"error": f"fallo en method='{method}': {type(exc).__name__}: {exc}"}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"was_watertight": was,
|
||||
"is_watertight": bool(out.is_watertight),
|
||||
"out_path": out_path,
|
||||
"method": method,
|
||||
"pitch": round(float(pitch), 6) if pitch is not None else None,
|
||||
"out_faces": int(len(out.faces)),
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import sys
|
||||
|
||||
src = sys.argv[1] if len(sys.argv) > 1 else (
|
||||
os.path.expanduser("~/ComfyUI/output/3d_robot_mesh_00001__dec80k.glb"))
|
||||
method = sys.argv[2] if len(sys.argv) > 2 else "voxel"
|
||||
out = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
print(json.dumps(comfyui_make_watertight(src, method=method, out_path=out), indent=2))
|
||||
Reference in New Issue
Block a user