From 2aac459bfd949f2d4964bd8ef5c2f669dff91531 Mon Sep 17 00:00:00 2001 From: PozzettiAndrea Date: Sat, 11 Jul 2026 11:02:49 +0200 Subject: [PATCH] various improvements to existing nodes, extended coverage --- nodes/__init__.py | 3 - nodes/decimation_cgal/__init__.py | 16 -- nodes/decimation_cgal/cgal_edge_collapse.py | 145 ------------- nodes/repair/backends/__init__.py | 6 + nodes/repair/backends/pymeshfix_repair.py | 100 +++++++++ nodes/repair/backends/trimesh_repair.py | 116 +++++++++++ nodes/repair/compute_normals.py | 4 +- nodes/repair/fix_intersections_removal.py | 1 + nodes/repair/mesh_repair.py | 26 ++- nodes/repair_cgal/__init__.py | 3 + .../fix_intersections_perturbation.py | 1 + nodes/repair_cgal/fix_self_intersections.py | 194 ++++++++++++++++++ nodes/repair_cgal/remesh_intersections.py | 1 + nodes/visualization/preview_mesh.py | 7 +- nodes/visualization/preview_mesh_dual.py | 5 +- nodes/visualization/preview_mesh_multi.py | 5 +- nodes/visualization/preview_mesh_slicer.py | 64 +++++- nodes/visualization/preview_mesh_vtk.py | 5 +- requirements.txt | 2 +- web_src/preview_mesh_slicer.js | 23 +++ workflows/fill_holes_all.json | 2 +- workflows/fix_normals_all.json | 2 +- workflows/fix_self_intersections_all.json | 1 + workflows/normals.json | 194 ------------------ workflows/normals_warp.json | 1 + workflows/slice_save.json | 1 + workflows/split_combine_mesh.json | 2 +- 27 files changed, 555 insertions(+), 375 deletions(-) delete mode 100644 nodes/decimation_cgal/__init__.py delete mode 100644 nodes/decimation_cgal/cgal_edge_collapse.py create mode 100644 nodes/repair/backends/pymeshfix_repair.py create mode 100644 nodes/repair/backends/trimesh_repair.py create mode 100644 nodes/repair_cgal/fix_self_intersections.py create mode 100644 workflows/fix_self_intersections_all.json delete mode 100644 workflows/normals.json create mode 100644 workflows/normals_warp.json create mode 100644 workflows/slice_save.json diff --git a/nodes/__init__.py b/nodes/__init__.py index 0d4ea91f..ddedcdb6 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -56,7 +56,6 @@ from . import reconstruction_cgal from . import boolean from . import remeshing_cgal from . import repair_cgal -from . import decimation_cgal # Collect all node class mappings NODE_CLASS_MAPPINGS = {} @@ -109,7 +108,6 @@ NODE_CLASS_MAPPINGS.update(remeshing_cgal.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(repair_cgal.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(smoothing.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(decimation.NODE_CLASS_MAPPINGS) -NODE_CLASS_MAPPINGS.update(decimation_cgal.NODE_CLASS_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(io.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(primitives.NODE_DISPLAY_NAME_MAPPINGS) @@ -133,6 +131,5 @@ NODE_DISPLAY_NAME_MAPPINGS.update(remeshing_cgal.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(repair_cgal.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(smoothing.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(decimation.NODE_DISPLAY_NAME_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(decimation_cgal.NODE_DISPLAY_NAME_MAPPINGS) __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/decimation_cgal/__init__.py b/nodes/decimation_cgal/__init__.py deleted file mode 100644 index 9ecf8669..00000000 --- a/nodes/decimation_cgal/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# Copyright (C) 2025 ComfyUI-GeometryPack Contributors - -""" -CGAL decimation backend nodes. -""" - -from .cgal_edge_collapse import NODE_CLASS_MAPPINGS as CGAL_MAPS, NODE_DISPLAY_NAME_MAPPINGS as CGAL_DISP - -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} - -NODE_CLASS_MAPPINGS.update(CGAL_MAPS) -NODE_DISPLAY_NAME_MAPPINGS.update(CGAL_DISP) - -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/decimation_cgal/cgal_edge_collapse.py b/nodes/decimation_cgal/cgal_edge_collapse.py deleted file mode 100644 index 5ec018c0..00000000 --- a/nodes/decimation_cgal/cgal_edge_collapse.py +++ /dev/null @@ -1,145 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# Copyright (C) 2025 ComfyUI-GeometryPack Contributors - -"""CGAL edge collapse decimation backend node.""" - -import logging -import numpy as np -import trimesh as trimesh_module -from comfy_api.latest import io - -log = logging.getLogger("geometrypack") - - -def _cgal_edge_collapse(mesh, target_edge_count, cost_strategy): - """CGAL surface mesh simplification via edge collapse.""" - from CGAL import CGAL_Surface_mesh_simplification - from CGAL.CGAL_Kernel import Point_3 - from CGAL.CGAL_Polyhedron_3 import Polyhedron_3 - from CGAL import CGAL_Polygon_mesh_processing - - vertices = np.asarray(mesh.vertices, dtype=np.float64) - faces = np.asarray(mesh.faces, dtype=np.int32) - - if len(vertices) == 0 or len(faces) == 0: - raise ValueError("Mesh is empty") - - # Build CGAL Polyhedron_3 - 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]))) - - polygons = [[int(idx) for idx in face] for face in faces] - - P = Polyhedron_3() - CGAL_Polygon_mesh_processing.polygon_soup_to_polygon_mesh(points, polygons, P) - - # Choose cost strategy - if cost_strategy == "lindstrom_turk": - cost = CGAL_Surface_mesh_simplification.LindstromTurk_cost() - placement = CGAL_Surface_mesh_simplification.LindstromTurk_placement() - else: - cost = CGAL_Surface_mesh_simplification.Edge_length_cost() - placement = CGAL_Surface_mesh_simplification.Midpoint_placement() - - stop = CGAL_Surface_mesh_simplification.Count_stop_predicate(target_edge_count) - - CGAL_Surface_mesh_simplification.edge_collapse(P, stop, cost, placement) - - # Extract result - new_vertices = [] - vertex_map = {} - for i, vertex in enumerate(P.vertices()): - point = vertex.point() - new_vertices.append([float(point.x()), float(point.y()), float(point.z())]) - vertex_map[vertex] = i - - new_faces = [] - for facet in P.facets(): - halfedge = facet.halfedge() - face_vertices = [] - start = halfedge - current = start - while True: - face_vertices.append(vertex_map[current.vertex()]) - current = current.next() - if current == start: - break - if len(face_vertices) == 3: - new_faces.append(face_vertices) - - return trimesh_module.Trimesh( - vertices=np.array(new_vertices, dtype=np.float64), - faces=np.array(new_faces, dtype=np.int32), - process=False, - ) - - -class DecimateCGALEdgeCollapseNode(io.ComfyNode): - """CGAL Lindstrom-Turk edge collapse decimation backend.""" - - @classmethod - def define_schema(cls): - return io.Schema( - node_id="GeomPackDecimate_CGALEdgeCollapse", - display_name="Decimate CGAL Edge Collapse (backend)", - category="geompack/decimation", - is_dev_only=True, - is_output_node=True, - inputs=[ - io.Custom("TRIMESH").Input("trimesh"), - io.Int.Input("target_face_count", default=5000, min=4, max=10000000, step=100, tooltip="Target number of output faces."), - io.Combo.Input("cost_strategy", options=["lindstrom_turk", "edge_length"], default="lindstrom_turk", tooltip="CGAL cost strategy. lindstrom_turk=optimizes geometry+volume+boundary (best), edge_length=collapses shortest edges first (fast)."), - ], - outputs=[ - io.Custom("TRIMESH").Output(display_name="decimated_mesh"), - io.String.Output(display_name="info"), - ], - ) - - @classmethod - def execute(cls, trimesh, target_face_count=5000, cost_strategy="lindstrom_turk"): - log.info("Backend: cgal_edge_collapse") - initial_vertices = len(trimesh.vertices) - initial_faces = len(trimesh.faces) - log.info("Input: %s vertices, %s faces", f"{initial_vertices:,}", f"{initial_faces:,}") - - target_edges = int(target_face_count * 1.5) - log.info("Parameters: target_edges=%d (from target_faces=%d), cost=%s", - target_edges, target_face_count, cost_strategy) - - decimated = _cgal_edge_collapse(trimesh, target_edges, cost_strategy) - - if hasattr(trimesh, "metadata") and trimesh.metadata: - decimated.metadata = trimesh.metadata.copy() - decimated.metadata["decimation"] = { - "algorithm": "cgal_edge_collapse", - "original_vertices": initial_vertices, - "original_faces": initial_faces, - "result_vertices": len(decimated.vertices), - "result_faces": len(decimated.faces), - } - - face_change = len(decimated.faces) - initial_faces - face_pct = (face_change / initial_faces) * 100 if initial_faces > 0 else 0 - - info = f"""Decimate Results (cgal_edge_collapse): - -Target Face Count: {target_face_count:,} -Cost Strategy: {cost_strategy} - -Before: - Vertices: {initial_vertices:,} - Faces: {initial_faces:,} - -After: - Vertices: {len(decimated.vertices):,} - Faces: {len(decimated.faces):,} - Reduction: {abs(face_pct):.1f}% -""" - return io.NodeOutput(decimated, info, ui={"text": [info]}) - - -NODE_CLASS_MAPPINGS = {"GeomPackDecimate_CGALEdgeCollapse": DecimateCGALEdgeCollapseNode} -NODE_DISPLAY_NAME_MAPPINGS = {"GeomPackDecimate_CGALEdgeCollapse": "Decimate CGAL Edge Collapse (backend)"} diff --git a/nodes/repair/backends/__init__.py b/nodes/repair/backends/__init__.py index 155cb899..1002f180 100644 --- a/nodes/repair/backends/__init__.py +++ b/nodes/repair/backends/__init__.py @@ -10,6 +10,8 @@ from .pymeshlab_fill import NODE_CLASS_MAPPINGS as PML_MAPS, NODE_DISPLAY_NAME_M from .pymeshfix_fill import NODE_CLASS_MAPPINGS as PMF_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PMF_DISP from .igl_fan_fill import NODE_CLASS_MAPPINGS as IGL_MAPS, NODE_DISPLAY_NAME_MAPPINGS as IGL_DISP from .pymeshlab_repair import NODE_CLASS_MAPPINGS as MR_PML_MAPS, NODE_DISPLAY_NAME_MAPPINGS as MR_PML_DISP +from .trimesh_repair import NODE_CLASS_MAPPINGS as MR_TM_MAPS, NODE_DISPLAY_NAME_MAPPINGS as MR_TM_DISP +from .pymeshfix_repair import NODE_CLASS_MAPPINGS as MR_PMF_MAPS, NODE_DISPLAY_NAME_MAPPINGS as MR_PMF_DISP NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} @@ -19,11 +21,15 @@ NODE_CLASS_MAPPINGS.update(PML_MAPS) NODE_CLASS_MAPPINGS.update(PMF_MAPS) NODE_CLASS_MAPPINGS.update(IGL_MAPS) NODE_CLASS_MAPPINGS.update(MR_PML_MAPS) +NODE_CLASS_MAPPINGS.update(MR_TM_MAPS) +NODE_CLASS_MAPPINGS.update(MR_PMF_MAPS) NODE_DISPLAY_NAME_MAPPINGS.update(TM_DISP) NODE_DISPLAY_NAME_MAPPINGS.update(PML_DISP) NODE_DISPLAY_NAME_MAPPINGS.update(PMF_DISP) NODE_DISPLAY_NAME_MAPPINGS.update(IGL_DISP) NODE_DISPLAY_NAME_MAPPINGS.update(MR_PML_DISP) +NODE_DISPLAY_NAME_MAPPINGS.update(MR_TM_DISP) +NODE_DISPLAY_NAME_MAPPINGS.update(MR_PMF_DISP) __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/repair/backends/pymeshfix_repair.py b/nodes/repair/backends/pymeshfix_repair.py new file mode 100644 index 00000000..311c5711 --- /dev/null +++ b/nodes/repair/backends/pymeshfix_repair.py @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2025 ComfyUI-GeometryPack Contributors + +""" +PyMeshFix mesh-repair backend node (component cleanup + self-intersection/degenerate cleanup). + +Trimmed from GeomPackMeshFix: hole-filling is intentionally NOT exposed here -- that's +GeomPackFillHoles's "pymeshfix" backend's job. This backend focuses on what's not +already covered by another dispatcher: small-component removal/joining and PyTMesh's +own clean() pass (self-intersection + degenerate-face removal). +""" + +import logging +import numpy as np +import trimesh +from comfy_api.latest import io + +log = logging.getLogger("geompack") + + +class MeshRepairPyMeshFixNode(io.ComfyNode): + """PyMeshFix cleanup backend: small-component removal/joining + clean() pass.""" + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="GeomPackMeshRepair_PyMeshFix", + display_name="Mesh Repair PyMeshFix (backend)", + category="geompack/repair", + is_dev_only=True, + is_output_node=True, + inputs=[ + io.Custom("TRIMESH").Input("mesh"), + io.Combo.Input("remove_small_components", options=["true", "false"], default="true", + tooltip="Remove small isolated mesh fragments before cleaning.", optional=True), + io.Combo.Input("join_components", options=["true", "false"], default="false", + tooltip="Attempt to join nearby disconnected components.", optional=True), + io.Combo.Input("clean_mesh", options=["true", "false"], default="true", + tooltip="Remove self-intersections and degenerate faces (PyTMesh.clean).", optional=True), + io.Int.Input("clean_iterations", default=10, min=1, max=100, step=1, + tooltip="Max iterations for self-intersection removal.", optional=True), + io.Int.Input("inner_loops", default=3, min=1, max=10, step=1, + tooltip="Inner loops per clean iteration.", optional=True), + ], + outputs=[ + io.Custom("TRIMESH").Output(display_name="repaired_mesh"), + io.String.Output(display_name="info"), + ], + ) + + @classmethod + def execute(cls, mesh, remove_small_components="true", join_components="false", + clean_mesh="true", clean_iterations=10, inner_loops=3): + remove_small_components = remove_small_components == "true" + join_components = join_components == "true" + clean_mesh = clean_mesh == "true" + + initial_vertices, initial_faces = len(mesh.vertices), len(mesh.faces) + was_watertight = mesh.is_watertight + + v = np.asarray(mesh.vertices, dtype=np.float64) + f = np.asarray(mesh.faces, dtype=np.int32) + + import pymeshfix + tin = pymeshfix.PyTMesh() + tin.load_array(v, f) + + operations = [] + if remove_small_components: + tin.remove_smallest_components() + operations.append("Removed small components") + if join_components: + tin.join_closest_components() + operations.append("Joined nearby components") + if clean_mesh: + tin.clean(max_iters=clean_iterations, inner_loops=inner_loops) + operations.append(f"Cleaned (iters={clean_iterations})") + + vclean, fclean = tin.return_arrays() + result_mesh = trimesh.Trimesh(vertices=vclean, faces=fclean, process=False) + if hasattr(mesh, "metadata") and mesh.metadata: + result_mesh.metadata = mesh.metadata.copy() + + final_vertices, final_faces = len(result_mesh.vertices), len(result_mesh.faces) + is_watertight = result_mesh.is_watertight + + info = ( + f"Mesh Repair (pymeshfix):\n\n" + f"Applied: {', '.join(operations) if operations else '(none enabled)'}\n" + f"Vertices: {initial_vertices:,} -> {final_vertices:,} ({final_vertices - initial_vertices:+,})\n" + f"Faces: {initial_faces:,} -> {final_faces:,} ({final_faces - initial_faces:+,})\n" + f"Watertight: {was_watertight} -> {is_watertight}" + ) + log.info("Mesh Repair (pymeshfix): applied [%s] | %dv/%df -> %dv/%df", + ", ".join(operations), initial_vertices, initial_faces, final_vertices, final_faces) + return io.NodeOutput(result_mesh, info, ui={"text": [info]}) + + +NODE_CLASS_MAPPINGS = {"GeomPackMeshRepair_PyMeshFix": MeshRepairPyMeshFixNode} +NODE_DISPLAY_NAME_MAPPINGS = {"GeomPackMeshRepair_PyMeshFix": "Mesh Repair PyMeshFix (backend)"} diff --git a/nodes/repair/backends/trimesh_repair.py b/nodes/repair/backends/trimesh_repair.py new file mode 100644 index 00000000..ffb491b5 --- /dev/null +++ b/nodes/repair/backends/trimesh_repair.py @@ -0,0 +1,116 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2025 ComfyUI-GeometryPack Contributors + +"""Trimesh mesh-repair backend node (merge duplicate vertices + drop degenerate/cap faces).""" + +import logging +import numpy as np +import trimesh as trimesh_module +from comfy_api.latest import io + +from ..remove_degenerate_faces import _face_max_angle_apex, _collapse_caps + +log = logging.getLogger("geompack") + + +class MeshRepairTrimeshNode(io.ComfyNode): + """Trimesh cleanup backend: merges duplicate vertices, then drops degenerate/cap faces.""" + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="GeomPackMeshRepair_Trimesh", + display_name="Mesh Repair Trimesh (backend)", + category="geompack/repair", + is_dev_only=True, + is_output_node=True, + inputs=[ + io.Custom("TRIMESH").Input("mesh"), + io.Float.Input("tolerance", default=1e-5, min=1e-8, max=1e-2, step=1e-6, + tooltip="Distance tolerance for merging duplicate vertices (1e-5 " + "recommended for CAD meshes). Runs first -- merging can " + "create degenerate faces, which the next step then cleans up."), + io.Float.Input("min_area", default=1e-10, min=0.0, max=1.0, step=1e-10, + optional=True, + tooltip="Faces with area below this are DELETED (zero/near-zero " + "area slivers). 0 disables this test."), + io.Float.Input("max_angle_deg", default=180.0, min=90.0, max=180.0, step=0.5, + optional=True, + tooltip="CAP-sliver removal by COLLAPSE. A face whose LARGEST " + "interior angle is >= this is a cap -- its apex is " + "collapsed onto the nearer base vertex. 180 = OFF; set " + "~175-179 to clean caps from OCC/image-derived meshes."), + ], + outputs=[ + io.Custom("TRIMESH").Output(display_name="repaired_mesh"), + io.String.Output(display_name="info"), + ], + ) + + @classmethod + def execute(cls, mesh, tolerance=1e-5, min_area=1e-10, max_angle_deg=180.0): + verts_before, faces_before = len(mesh.vertices), len(mesh.faces) + + # Step 1: merge duplicate/near-duplicate vertices. + merged = mesh.copy() + digits = max(0, -int(np.floor(np.log10(tolerance)))) + merged.merge_vertices(digits_vertex=digits) + verts_after_merge, faces_after_merge = len(merged.vertices), len(merged.faces) + + # Step 2: drop degenerate/cap faces (may have been created by the merge above). + cleaned = merged + n_caps = 0 + if 0.0 < float(max_angle_deg) < 180.0: + V = np.asarray(cleaned.vertices, dtype=np.float64) + F = np.asarray(cleaned.faces, dtype=np.int64) + vattrs = {k: np.asarray(v) for k, v in dict(cleaned.vertex_attributes).items()} + fattrs = {k: np.asarray(v) for k, v in dict(cleaned.face_attributes).items()} + V2, F2, va2, fa2, n_caps = _collapse_caps(V, F, vattrs, fattrs, + np.radians(float(max_angle_deg))) + if n_caps > 0: + cleaned = trimesh_module.Trimesh(vertices=V2, faces=F2.astype(np.int32), process=False) + for k, v in va2.items(): + cleaned.vertex_attributes[k] = v + for k, v in fa2.items(): + cleaned.face_attributes[k] = v + + duplicate_mask = np.array([len(set(f)) == 3 for f in cleaned.faces]) + if np.any(~duplicate_mask): + cleaned.update_faces(duplicate_mask) + + if hasattr(cleaned, 'nondegenerate_faces'): + area_mask = cleaned.nondegenerate_faces() + if np.any(~area_mask): + cleaned.update_faces(area_mask) + + if min_area > 0 and len(cleaned.faces): + area_mask = cleaned.area_faces >= min_area + if np.any(~area_mask): + cleaned.update_faces(area_mask) + + cleaned.remove_unreferenced_vertices() + + verts_after, faces_after = len(cleaned.vertices), len(cleaned.faces) + merge_verts_removed = verts_before - verts_after_merge + degen_faces_removed = faces_after_merge - faces_after + + info = f"""Mesh Repair (trimesh): + +Merge Vertices (tolerance={tolerance:.2e}, {digits} decimal places): + Vertices: {verts_before:,} -> {verts_after_merge:,} ({-merge_verts_removed:+,}) + Faces: {faces_before:,} -> {faces_after_merge:,} ({faces_after_merge - faces_before:+,}) + +Remove Degenerate Faces (cap angle >= {max_angle_deg:g} deg, {n_caps:,} collapsed): + Vertices: {verts_after_merge:,} -> {verts_after:,} ({verts_after - verts_after_merge:+,}) + Faces: {faces_after_merge:,} -> {faces_after:,} ({-degen_faces_removed:+,}) + +Overall: + Vertices: {verts_before:,} -> {verts_after:,} ({verts_after - verts_before:+,}) + Faces: {faces_before:,} -> {faces_after:,} ({faces_after - faces_before:+,}) +""" + log.info("Mesh Repair (trimesh): %dv/%df -> %dv/%df", verts_before, faces_before, verts_after, faces_after) + return io.NodeOutput(cleaned, info, ui={"text": [info]}) + + +NODE_CLASS_MAPPINGS = {"GeomPackMeshRepair_Trimesh": MeshRepairTrimeshNode} +NODE_DISPLAY_NAME_MAPPINGS = {"GeomPackMeshRepair_Trimesh": "Mesh Repair Trimesh (backend)"} diff --git a/nodes/repair/compute_normals.py b/nodes/repair/compute_normals.py index c8d27800..4e6e8436 100644 --- a/nodes/repair/compute_normals.py +++ b/nodes/repair/compute_normals.py @@ -83,11 +83,13 @@ class ComputeNormalsNode(io.ComfyNode): result_mesh.metadata['normals_smoothed'] = True log.info("Computed smooth vertex normals") - # Store normals as vertex attributes for visualization + # Store normals as vertex attributes for visualization: separate scalar + # components (existing consumers expect these) plus a combined (n,3) field. result_mesh.vertex_attributes['normal_x'] = vertex_normals[:, 0] result_mesh.vertex_attributes['normal_y'] = vertex_normals[:, 1] result_mesh.vertex_attributes['normal_z'] = vertex_normals[:, 2] result_mesh.vertex_attributes['normal_magnitude'] = np.linalg.norm(vertex_normals, axis=1) + result_mesh.vertex_attributes['normals_xyz'] = vertex_normals return io.NodeOutput(result_mesh) diff --git a/nodes/repair/fix_intersections_removal.py b/nodes/repair/fix_intersections_removal.py index 3b30e8d9..8e42777b 100644 --- a/nodes/repair/fix_intersections_removal.py +++ b/nodes/repair/fix_intersections_removal.py @@ -30,6 +30,7 @@ class FixSelfIntersectionsByRemovalNode(io.ComfyNode): node_id="GeomPackFixSelfIntersectionsByRemoval", display_name="Fix Self Intersections (Removal)", category="geompack/repair", + is_dev_only=True, is_output_node=True, inputs=[ io.Custom("TRIMESH").Input("trimesh"), diff --git a/nodes/repair/mesh_repair.py b/nodes/repair/mesh_repair.py index a5053493..4289c1ff 100644 --- a/nodes/repair/mesh_repair.py +++ b/nodes/repair/mesh_repair.py @@ -6,7 +6,9 @@ Unified Mesh Repair node - single frontend with backend selector. Cleans up degenerate / sliver / duplicate / non-manifold elements. Dispatches to hidden backend nodes via node expansion (GraphBuilder), each in its own isolation -env. Designed to grow more backends (pymeshfix, trimesh, cgal, ...) over time. +env. Self-intersection fixing lives in its own dispatcher (GeomPackFixSelfIntersections, +nodes/repair_cgal/fix_self_intersections.py) -- a distinct topology problem, not +degeneracy/duplication. """ import logging @@ -19,7 +21,9 @@ class MeshRepairNode(io.ComfyNode): """Mesh Repair - unified degenerate/sliver cleanup with backend selection.""" BACKEND_MAP = { + "trimesh": "GeomPackMeshRepair_Trimesh", "pymeshlab": "GeomPackMeshRepair_PyMeshLab", + "pymeshfix": "GeomPackMeshRepair_PyMeshFix", } @classmethod @@ -33,14 +37,25 @@ class MeshRepairNode(io.ComfyNode): "duplicate vertices, T-vertices, non-manifold edges, and small floating " "components. Pick a backend; each exposes its own toggles.\n" "\n" + "trimesh: lightweight, no extra deps -- merges duplicate vertices then drops " + "degenerate/cap-sliver faces.\n" "pymeshlab: per-operation cleanup -- the real sliver/degenerate remover " - "(MeshLab's meshing_remove_* filters). More backends (pymeshfix, ...) to come." + "(MeshLab's meshing_remove_* filters).\n" + "pymeshfix: small-component removal/joining plus PyTMesh's own clean() pass." ), enable_expand=True, is_output_node=True, inputs=[ io.Custom("TRIMESH").Input("mesh"), io.DynamicCombo.Input("backend", tooltip="Repair algorithm and backend", options=[ + io.DynamicCombo.Option("trimesh", [ + io.Float.Input("tolerance", default=1e-5, min=1e-8, max=1e-2, step=1e-6, + tooltip="Distance tolerance for merging duplicate vertices (1e-5 recommended for CAD meshes)."), + io.Float.Input("min_area", default=1e-10, min=0.0, max=1.0, step=1e-10, + tooltip="Faces with area below this are deleted (0 disables this test)."), + io.Float.Input("max_angle_deg", default=180.0, min=90.0, max=180.0, step=0.5, + tooltip="Cap-sliver collapse threshold on largest interior angle. 180 = off."), + ]), io.DynamicCombo.Option("pymeshlab", [ io.Combo.Input("remove_duplicate_vertices", options=["true", "false"], default="true", tooltip="Merge coincident (duplicate) vertices first."), io.Combo.Input("remove_null_faces", options=["true", "false"], default="true", tooltip="Remove null / zero-area (degenerate) faces -- the core sliver remover."), @@ -51,6 +66,13 @@ class MeshRepairNode(io.ComfyNode): io.Float.Input("remove_small_components_pct", default=0.0, min=0.0, max=100.0, step=0.5, display_mode="number", tooltip="Drop floating components below this PERCENT of the mesh's bbox diagonal (0 = off). Removes scan/recon crumbs."), io.Combo.Input("remove_unreferenced_vertices", options=["true", "false"], default="true", tooltip="Drop orphan vertices not used by any face (last)."), ]), + io.DynamicCombo.Option("pymeshfix", [ + io.Combo.Input("remove_small_components", options=["true", "false"], default="true", tooltip="Remove small isolated mesh fragments before cleaning."), + io.Combo.Input("join_components", options=["true", "false"], default="false", tooltip="Attempt to join nearby disconnected components."), + io.Combo.Input("clean_mesh", options=["true", "false"], default="true", tooltip="Remove self-intersections and degenerate faces (PyTMesh.clean)."), + io.Int.Input("clean_iterations", default=10, min=1, max=100, step=1, tooltip="Max iterations for self-intersection removal."), + io.Int.Input("inner_loops", default=3, min=1, max=10, step=1, tooltip="Inner loops per clean iteration."), + ]), ]), ], outputs=[ diff --git a/nodes/repair_cgal/__init__.py b/nodes/repair_cgal/__init__.py index dd2c3e00..48ecd0d8 100644 --- a/nodes/repair_cgal/__init__.py +++ b/nodes/repair_cgal/__init__.py @@ -9,6 +9,7 @@ from .detect_intersections import NODE_CLASS_MAPPINGS as DETECT_INTERSECTIONS_MA from .remesh_intersections import NODE_CLASS_MAPPINGS as REMESH_INTERSECTIONS_MAPS, NODE_DISPLAY_NAME_MAPPINGS as REMESH_INTERSECTIONS_DISP from .fix_intersections_perturbation import NODE_CLASS_MAPPINGS as FIX_INTERSECTIONS_PERTURBATION_MAPS, NODE_DISPLAY_NAME_MAPPINGS as FIX_INTERSECTIONS_PERTURBATION_DISP from .fill_holes_cgal import NODE_CLASS_MAPPINGS as FILL_HOLES_CGAL_MAPS, NODE_DISPLAY_NAME_MAPPINGS as FILL_HOLES_CGAL_DISP +from .fix_self_intersections import NODE_CLASS_MAPPINGS as FIX_SELF_INTERSECTIONS_MAPS, NODE_DISPLAY_NAME_MAPPINGS as FIX_SELF_INTERSECTIONS_DISP NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} @@ -21,5 +22,7 @@ NODE_CLASS_MAPPINGS.update(FIX_INTERSECTIONS_PERTURBATION_MAPS) NODE_DISPLAY_NAME_MAPPINGS.update(FIX_INTERSECTIONS_PERTURBATION_DISP) NODE_CLASS_MAPPINGS.update(FILL_HOLES_CGAL_MAPS) NODE_DISPLAY_NAME_MAPPINGS.update(FILL_HOLES_CGAL_DISP) +NODE_CLASS_MAPPINGS.update(FIX_SELF_INTERSECTIONS_MAPS) +NODE_DISPLAY_NAME_MAPPINGS.update(FIX_SELF_INTERSECTIONS_DISP) __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/nodes/repair_cgal/fix_intersections_perturbation.py b/nodes/repair_cgal/fix_intersections_perturbation.py index 5ae24c0f..48ca9c76 100644 --- a/nodes/repair_cgal/fix_intersections_perturbation.py +++ b/nodes/repair_cgal/fix_intersections_perturbation.py @@ -30,6 +30,7 @@ class FixSelfIntersectionsByPerturbationNode(io.ComfyNode): node_id="GeomPackFixSelfIntersectionsByPerturbation", display_name="Fix Self Intersections (Perturbation)", category="geompack/repair", + is_dev_only=True, is_output_node=True, inputs=[ io.Custom("TRIMESH").Input("trimesh"), diff --git a/nodes/repair_cgal/fix_self_intersections.py b/nodes/repair_cgal/fix_self_intersections.py new file mode 100644 index 00000000..1c969f24 --- /dev/null +++ b/nodes/repair_cgal/fix_self_intersections.py @@ -0,0 +1,194 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2025 ComfyUI-GeometryPack Contributors + +""" +Unified Fix Self Intersections node - single frontend with backend selector. + +Dispatches to the existing standalone self-intersection fixers via node expansion +(GraphBuilder), auto-chaining GeomPackDetectSelfIntersections internally: every backend +gets a fresh field (face_attributes['self_intersecting'] / vertex_attributes +['intersection_flag']/['intersection_count']) recomputed on its OUTPUT, unconditionally +-- not left to each backend's own optional re-detect toggle. Removal/Perturbation +additionally get Detect run BEFORE them too, since they require the field as input +(no need to manually wire a separate Detect node upstream anymore). Remesh works from +raw geometry, so it only needs the after-step. GeomPackDetectSelfIntersections itself +stays a standalone, visible node -- it's inspection, not a fix. +""" + +import logging +from comfy_api.latest import io + +log = logging.getLogger("geometrypack") + + +class FixSelfIntersectionsNode(io.ComfyNode): + """Fix Self Intersections - unified self-intersection repair with backend selection.""" + + BACKEND_MAP = { + "removal": "GeomPackFixSelfIntersectionsByRemoval", + "perturbation": "GeomPackFixSelfIntersectionsByPerturbation", + "remesh": "GeomPackRemeshSelfIntersections", + } + + # Each backend's mesh input parameter has a different name (inherited unchanged + # from the standalone nodes being dispatched to). + MESH_PARAM = { + "removal": "trimesh", + "perturbation": "trimesh", + "remesh": "mesh", + } + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="GeomPackFixSelfIntersections", + display_name="Fix Self Intersections", + category="geompack/repair", + description=( + "Fix self-intersecting geometry. Pick a backend; each exposes its own toggles. " + "Self-intersection detection runs automatically (before and/or after, as needed) " + "-- no need to wire a separate 'Detect Self Intersections' node.\n" + "\n" + "removal: deletes intersecting faces, then optionally fills the resulting holes " + "and fixes normals.\n" + "perturbation: nudges vertices adjacent to intersections along their normals -- " + "non-destructive, preserves topology.\n" + "remesh: subdivides intersecting triangles via libigl/CGAL so intersections lie " + "exactly on edges." + ), + enable_expand=True, + is_output_node=True, + inputs=[ + io.Custom("TRIMESH").Input("mesh"), + io.DynamicCombo.Input("backend", tooltip="Self-intersection fix algorithm and backend", options=[ + io.DynamicCombo.Option("removal", [ + io.Boolean.Input("fill_holes", default=True, tooltip=( + "After deleting the faces flagged as self-intersecting, the removed patch " + "leaves an open hole in the mesh. When enabled, trimesh's fill_holes() " + "re-triangulates that gap so the surface stays closed. Disable if you'd " + "rather inspect the resulting hole, or if you plan to fill it yourself with " + "a more controllable node (the dedicated Fill Holes dispatcher offers " + "CGAL/pymeshfix/igl_fan backends with more quality control than this quick fill).")), + io.Boolean.Input("fix_normals", default=True, tooltip=( + "Recomputes/reorients face normals after the removal+fill step, since " + "deleting faces and adding new triangulated ones can leave inconsistent " + "winding at the seam. Cheap and safe to leave on; only disable if you're " + "already running a dedicated Fix Normals pass afterward anyway.")), + io.Int.Input("max_hole_size", default=100, min=3, max=10000, tooltip=( + "Upper bound, in boundary edges, on how large a hole this node's own " + "fill_holes step will attempt to close. Removing a cluster of intersecting " + "faces can leave a large, irregular hole; big holes are expensive and often " + "triangulate poorly with this simple fill. Holes larger than this are left " + "open -- feed the result into the dedicated Fill Holes node afterward for a " + "higher-quality fill on anything that didn't get closed here.")), + ]), + io.DynamicCombo.Option("perturbation", [ + io.Float.Input("epsilon", default=0.001, min=1e-8, max=1.0, step=0.0001, tooltip=( + "Base displacement distance, in the mesh's own world units (not a " + "percentage), applied along each affected vertex's normal. Too small and " + "the intersection may persist; too large and you visibly deform the " + "surface near the fix site. 0.001 is a reasonable starting point for a mesh " + "roughly normalized to unit scale -- for CAD-derived meshes in mm or with a " + "much larger/smaller bounding box, scale this relative to the mesh's " + "typical edge length rather than using the default blindly.")), + io.Int.Input("max_iterations", default=10, min=1, max=100, tooltip=( + "Number of perturbation passes. Displacement ramps up linearly across " + "iterations (iteration 1 moves epsilon/max_iterations, the final iteration " + "moves the full epsilon) -- so more iterations means a gentler ramp toward " + "the same final displacement, not more total movement. Increase if a single " + "pass still leaves faces marginally intersecting; each extra iteration also " + "recomputes vertex normals, so cost scales roughly linearly with this value.")), + io.Combo.Input("direction", options=["outward", "inward", "adaptive"], default="adaptive", tooltip=( + "Which way to nudge affected vertices along their normals. 'outward' pushes " + "vertices away from the surface (grows the mesh slightly at the intersection " + "site); 'inward' pulls them in (shrinks it slightly); 'adaptive' is intended " + "to choose per-vertex based on local geometry, but this backend's current " + "implementation falls back to the same behavior as 'outward' -- functionally " + "an alias for now. Kept as the default anyway since it's the intended " + "long-term behavior once true per-vertex direction selection lands; switch " + "to 'inward' explicitly if you specifically need the mesh to shrink rather " + "than grow at the fix site.")), + io.Boolean.Input("scale_by_intersection_count", default=True, tooltip=( + "When enabled, vertices touched by more intersecting faces get " + "proportionally larger displacement (scaled 0-1 against the mesh's maximum " + "per-vertex intersection count from the internal Detect pass), so heavily " + "tangled regions get nudged harder than lightly-affected ones. Disable for a " + "uniform epsilon everywhere, regardless of how many intersections each " + "vertex participates in -- useful if you want a predictable, constant " + "displacement rather than one that varies across the mesh.")), + ]), + io.DynamicCombo.Option("remesh", [ + io.Boolean.Input("remove_unreferenced", default=True, tooltip=( + "After CGAL subdivides intersecting triangles along their intersection " + "curves, some original vertices may end up referenced by no face at all. " + "Enabling this strips them out (via igl.remove_unreferenced) so the output " + "mesh's vertex list stays compact. Safe to leave on; only matters if " + "something downstream depends on vertex indices/count staying untouched " + "from the input mesh.")), + io.Boolean.Input("extract_outer_hull", default=False, tooltip=( + "Attempts to extract just the outer, manifold shell of the remeshed result " + "(igl.outer_hull_legacy), discarding any interior geometry the subdivision " + "created at the intersection sites. Produces a cleaner, genuinely manifold " + "mesh suitable for boolean operations or 3D printing, but is significantly " + "slower than the base remesh step and can fail or silently no-op on complex " + "topology. Leave off for a quick fix; enable when you specifically need a " + "watertight, printable result and are willing to pay the extra cost.")), + io.Boolean.Input("stitch_all", default=True, tooltip=( + "Passed straight through to CGAL's remesh_self_intersections call. When " + "enabled, CGAL attempts to stitch together the boundaries created by " + "subdividing intersecting triangles, keeping the result as connected/closed " + "as possible rather than leaving separate disconnected patches at each " + "intersection site. Recommended on by default; disabling it can leave more " + "fragmented geometry that's harder to clean up afterward.")), + ]), + ]), + ], + outputs=[ + io.Custom("TRIMESH").Output(display_name="fixed_mesh"), + io.String.Output(display_name="info"), + ], + ) + + DETECT_NODE_ID = "GeomPackDetectSelfIntersections" + + @classmethod + def execute(cls, mesh, backend): + from comfy_execution.graph_utils import GraphBuilder + + if cls.SCHEMA is None: + cls.GET_SCHEMA() + + selected = backend["backend"] + node_id = cls.BACKEND_MAP[selected] + mesh_param = cls.MESH_PARAM[selected] + + log.info("Fix Self Intersections dispatch: %s -> %s", selected, node_id) + + kwargs = {k: v for k, v in backend.items() if k != "backend"} + + graph = GraphBuilder() + + if selected == "remesh": + # Works from raw geometry -- no pre-detection needed, always a real fix pass. + kwargs["detect_only"] = False + fix_node = graph.node(node_id, mesh=mesh, **kwargs) + else: + # Removal/perturbation read the field to know which faces/vertices to touch, + # so detect first. The dispatcher re-detects after unconditionally (below), + # so the backend's own re-detect is redundant here -- skip it. + detect_before = graph.node(cls.DETECT_NODE_ID, trimesh=mesh) + kwargs["re_detect_after_fix"] = False + fix_node = graph.node(node_id, **{mesh_param: detect_before.out(0)}, **kwargs) + + # Always recompute the field on the final output, regardless of backend, so the + # result never needs a manually-wired Detect node to inspect what (if anything) remains. + detect_after = graph.node(cls.DETECT_NODE_ID, trimesh=fix_node.out(0)) + + return { + "result": (detect_after.out(0), fix_node.out(1)), + "expand": graph.finalize(), + } + + +NODE_CLASS_MAPPINGS = {"GeomPackFixSelfIntersections": FixSelfIntersectionsNode} +NODE_DISPLAY_NAME_MAPPINGS = {"GeomPackFixSelfIntersections": "Fix Self Intersections"} diff --git a/nodes/repair_cgal/remesh_intersections.py b/nodes/repair_cgal/remesh_intersections.py index 305b35c8..e7b3b6bd 100644 --- a/nodes/repair_cgal/remesh_intersections.py +++ b/nodes/repair_cgal/remesh_intersections.py @@ -31,6 +31,7 @@ class RemeshSelfIntersectionsNode(io.ComfyNode): node_id="GeomPackRemeshSelfIntersections", display_name="Remesh Self Intersections", category="geompack/repair", + is_dev_only=True, is_output_node=True, inputs=[ io.Custom("TRIMESH").Input("mesh"), diff --git a/nodes/visualization/preview_mesh.py b/nodes/visualization/preview_mesh.py index 526b47e6..7922fbee 100644 --- a/nodes/visualization/preview_mesh.py +++ b/nodes/visualization/preview_mesh.py @@ -44,6 +44,9 @@ class PreviewMeshNode(io.ComfyNode): inputs=[ io.Custom("TRIMESH").Input("trimesh"), ], + outputs=[ + io.Custom("TRIMESH").Output(display_name="mesh"), + ], ) @classmethod @@ -85,8 +88,8 @@ class PreviewMeshNode(io.ComfyNode): extents = trimesh.extents max_extent = max(extents) - # Return metadata for frontend widget - return io.NodeOutput(ui={ "mesh_file": [filename], "vertex_count": [len(trimesh.vertices)], "face_count": [get_face_count(trimesh)], "bounds_min": [bounds[0].tolist()], "bounds_max": [bounds[1].tolist()], "extents": [extents.tolist()], "max_extent": [float(max_extent)], }) + # Return metadata for frontend widget, and pass the mesh through as an output + return io.NodeOutput(trimesh, ui={ "mesh_file": [filename], "vertex_count": [len(trimesh.vertices)], "face_count": [get_face_count(trimesh)], "bounds_min": [bounds[0].tolist()], "bounds_max": [bounds[1].tolist()], "extents": [extents.tolist()], "max_extent": [float(max_extent)], }) NODE_CLASS_MAPPINGS = { "GeomPackPreviewMesh": PreviewMeshNode, diff --git a/nodes/visualization/preview_mesh_dual.py b/nodes/visualization/preview_mesh_dual.py index 21814b53..379872d9 100644 --- a/nodes/visualization/preview_mesh_dual.py +++ b/nodes/visualization/preview_mesh_dual.py @@ -115,6 +115,9 @@ class PreviewMeshDualNode(io.ComfyNode): io.Float.Input("opacity_1", default=1.0, min=0.0, max=1.0, step=0.1, optional=True), io.Float.Input("opacity_2", default=1.0, min=0.0, max=1.0, step=0.1, optional=True), ], + outputs=[ + io.Custom("TRIMESH").Output(display_name="meshes", is_output_list=True), + ], ) @classmethod @@ -297,7 +300,7 @@ class PreviewMeshDualNode(io.ComfyNode): }) log.info("Preview ready") - return io.NodeOutput(ui=ui_data) + return io.NodeOutput([mesh_1, mesh_2], ui=ui_data) @staticmethod def _export_mesh(mesh, base_filename, use_vtp, use_glb): diff --git a/nodes/visualization/preview_mesh_multi.py b/nodes/visualization/preview_mesh_multi.py index 75352a8b..1b2aeed1 100644 --- a/nodes/visualization/preview_mesh_multi.py +++ b/nodes/visualization/preview_mesh_multi.py @@ -96,6 +96,9 @@ class PreviewMeshMultiNode(io.ComfyNode): io.Custom("TRIMESH").Input("mesh_4", optional=True), io.Combo.Input("mode", options=["fields", "texture"], default="fields", optional=True), ], + outputs=[ + io.Custom("TRIMESH").Output(display_name="meshes", is_output_list=True), + ], ) @classmethod @@ -224,7 +227,7 @@ class PreviewMeshMultiNode(io.ComfyNode): ui_data["field_names_list"] = [field_names_list] log.info("Grid: %dx%d, Preview ready", grid_cols, grid_rows) - return io.NodeOutput(ui=ui_data) + return io.NodeOutput(meshes, ui=ui_data) NODE_CLASS_MAPPINGS = { diff --git a/nodes/visualization/preview_mesh_slicer.py b/nodes/visualization/preview_mesh_slicer.py index 68f92a3c..38dbe422 100644 --- a/nodes/visualization/preview_mesh_slicer.py +++ b/nodes/visualization/preview_mesh_slicer.py @@ -7,15 +7,26 @@ Preview Mesh Slicer - interactive VTK.js clip/slice preview. Top: 3D view of the mesh with a live clipping plane. Bottom: an orthographic pick view (toggle XY / YZ / XZ); click two points to define a cut line, which is swept along the view axis into a clip plane. "Flip clip" slices the other side. +A "3d view:" row of +X/-X/+Y/-Y/+Z/-Z buttons sets an axis-aligned plane through +the mesh center directly, without needing to pick 2 points. -The Python node just exports the mesh to a .vtp; all the clipping is interactive -in the browser (viewer_slicer.html). +The Python node exports the mesh to a .vtp for the interactive viewer; the actual +clip plane (origin + normal) is picked entirely in the browser, then mirrored back +here via a hidden `plane_json` widget (see web_src/preview_mesh_slicer.js) so this +node can reproduce the same cut on the real mesh data and return it as `sliced_mesh` +-- matching whatever is currently shown in the top 3D view. Since the plane only +exists once the browser has posted it back, the very first run (or right after +loading a new mesh) has no plane yet and `sliced_mesh` is the mesh unchanged -- +pick a cut in the viewer, then queue again to get the actual slice. """ +import json import logging import os import uuid +import numpy as np +import trimesh from comfy_api.latest import io log = logging.getLogger("geometrypack") @@ -33,12 +44,18 @@ class PreviewMeshSlicer(io.ComfyNode): is_output_node=True, inputs=[ io.Custom("TRIMESH").Input("mesh", tooltip="Mesh to slice/clip interactively."), + io.String.Input("plane_json", default="", optional=True, + tooltip="Internal: clip plane picked in the viewer, mirrored " + "back by the frontend. Not meant to be edited by hand."), + ], + outputs=[ + io.Custom("TRIMESH").Output(display_name="sliced_mesh"), + io.String.Output(display_name="info"), ], - outputs=[], ) @classmethod - def execute(cls, mesh): + def execute(cls, mesh, plane_json=""): import folder_paths from ._vtp_export import export_mesh_with_scalars_vtp @@ -50,7 +67,44 @@ class PreviewMeshSlicer(io.ComfyNode): nv = len(getattr(mesh, "vertices", [])) nf = len(getattr(mesh, "faces", [])) log.info("[PreviewMeshSlicer] exported %s (%d verts, %d faces)", filename, nv, nf) - return io.NodeOutput(ui={ + + sliced_mesh = mesh + info = (f"{nv:,} verts / {nf:,} faces -- no clip plane set yet. Pick a cut in the " + f"viewer (2 points, or a +X/-X/+Y/-Y/+Z/-Z button), then queue again.") + + plane = None + if plane_json: + try: + plane = json.loads(plane_json) + except (ValueError, TypeError) as e: + log.warning("[PreviewMeshSlicer] bad plane_json: %s", e) + + if plane and plane.get("hasPlane") and "origin" in plane and "normal" in plane: + # Matches the VTK view exactly: no capping (VTK's clipping plane doesn't cap the + # cross-section either), keep the side the normal points TOWARD (trimesh's own + # "positive normal side" convention for slice_faces_plane == VTK's kept side). + from trimesh.intersections import slice_faces_plane + + origin = np.asarray(plane["origin"], dtype=np.float64) + normal = np.asarray(plane["normal"], dtype=np.float64) + V, F, _ = slice_faces_plane( + vertices=np.asarray(mesh.vertices, dtype=np.float64).copy(), + faces=np.asarray(mesh.faces, dtype=np.int64).copy(), + plane_normal=normal, + plane_origin=origin, + ) + sliced_mesh = trimesh.Trimesh(vertices=V, faces=F, process=False) + if hasattr(mesh, "metadata") and mesh.metadata: + sliced_mesh.metadata = mesh.metadata.copy() + + sv, sf = len(sliced_mesh.vertices), len(sliced_mesh.faces) + log.info("[PreviewMeshSlicer] sliced origin=%s normal=%s: %dv/%df -> %dv/%df", + origin.tolist(), normal.tolist(), nv, nf, sv, sf) + info = (f"Sliced at origin={np.round(origin, 4).tolist()}, " + f"normal={np.round(normal, 4).tolist()}\n" + f"{nv:,} -> {sv:,} verts, {nf:,} -> {sf:,} faces") + + return io.NodeOutput(sliced_mesh, info, ui={ "mesh_file": [filename], "summary": [f"{nv:,} verts / {nf:,} faces — slice interactively in the viewer"], }) diff --git a/nodes/visualization/preview_mesh_vtk.py b/nodes/visualization/preview_mesh_vtk.py index 46e81b28..69245844 100644 --- a/nodes/visualization/preview_mesh_vtk.py +++ b/nodes/visualization/preview_mesh_vtk.py @@ -53,6 +53,9 @@ class PreviewMeshVTKNode(io.ComfyNode): io.Custom("TRIMESH").Input("trimesh", optional=True), io.Custom("VOXELGRID").Input("voxelgrid", optional=True), ], + outputs=[ + io.Custom("TRIMESH").Output(display_name="mesh"), + ], ) @classmethod @@ -310,7 +313,7 @@ class PreviewMeshVTKNode(io.ComfyNode): else: log.info("Fields mode info: watertight=%s, volume=%s, area=%s, no fields", is_watertight, volume, area) - return io.NodeOutput(ui=ui_data) + return io.NodeOutput(trimesh, ui=ui_data) NODE_CLASS_MAPPINGS = { diff --git a/requirements.txt b/requirements.txt index 830447cc..fa041802 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ comfy-env==0.4.3 -comfy-3d-viewers==0.2.54 +comfy-3d-viewers==0.2.55 trimesh[easy]>=4.0.0 \ No newline at end of file diff --git a/web_src/preview_mesh_slicer.js b/web_src/preview_mesh_slicer.js index 0a60360f..1285f6b0 100644 --- a/web_src/preview_mesh_slicer.js +++ b/web_src/preview_mesh_slicer.js @@ -42,6 +42,29 @@ app.registerExtension({ widget.computeSize = (w) => [w || 480, Math.round((w || 480) * 1.15)]; node.size = [Math.max(node.size?.[0] || 0, 420), Math.max(node.size?.[1] || 0, 540)]; node._gpSlicerIframe = iframe; + + // plane_json (a real schema input -> auto-created widget) is a machine-written + // mirror of whatever clip plane is picked interactively in the iframe, not + // something meant to be hand-edited -- hide it from the node body. + const planeWidget = node.widgets?.find((w) => w.name === "plane_json"); + if (planeWidget) { + planeWidget.hidden = true; + planeWidget.computeSize = () => [0, -4]; + } + + // The clip plane only ever exists as local JS state inside the iframe + // (viewer_slicer.html) -- execute() has no other way to see it, so mirror it + // into the (hidden) plane_json widget on every change; that's what gets + // serialized into the prompt on the next queue. + window.addEventListener("message", (event) => { + if (event.source !== iframe.contentWindow) return; + if (event.data?.type !== "PLANE_UPDATE") return; + const w = node.widgets?.find((x) => x.name === "plane_json"); + if (!w) return; + w.value = JSON.stringify(event.data); + node.setDirtyCanvas(true, true); + }); + return r; }; diff --git a/workflows/fill_holes_all.json b/workflows/fill_holes_all.json index 68153465..48a1c656 100644 --- a/workflows/fill_holes_all.json +++ b/workflows/fill_holes_all.json @@ -1 +1 @@ -{"id":"a1b2c3d4-e5f6-7890-abcd-fillholes0001","revision":0,"last_node_id":9,"last_link_id":16,"nodes":[{"id":1,"type":"GeomPackLoadMesh","pos":[-877.8583007812498,-118.69721679687503],"size":[270,78],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[2,3,4,5,6,7]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackLoadMesh"},"widgets_values":["3d/camelhead.off"]},{"id":5,"type":"GeomPackFillHoles","pos":[334.9272949218742,-20.695605468749807],"size":[370,110],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":4},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[11]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["igl_fan","false"]},{"id":6,"type":"GeomPackFillHoles","pos":[326.3868652343745,132.61044921874958],"size":[370,134],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":5},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.quality","name":"backend.quality","type":"COMBO","widget":{"name":"backend.quality"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[12]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["cgal","triangulate","false"]},{"id":9,"type":"GeomPackPreviewMeshMulti","pos":[733.4390136718749,-21.472656249999996],"size":[768,702],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":11},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":12},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":13},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":null},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackPreviewMeshMulti"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[0.3450675952860498,-1.1296206877012935,-0.7853370972660157],\\\"focalPoint\\\":[0.000001996755599975586,-0.0636419951915741,-0.03211300075054169],\\\"viewUp\\\":[0.41668520115314117,-0.4307600853225743,0.8005118312884919]}\",\"selected_field\":\"\"}"]},{"id":7,"type":"GeomPackFillHoles","pos":[330.51953125,299.8362304687501],"size":[370,134],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":6},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.perimeter","name":"backend.perimeter","type":"FLOAT","widget":{"name":"backend.perimeter"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[13]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["gpu_cumesh",20,"false"]},{"id":8,"type":"GeomPackPreviewMeshMulti","pos":[-453.58149414062495,-26.819580078124975],"size":[768,702],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":7},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":16},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":15},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":null},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackPreviewMeshMulti"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[0.3450675952860498,-1.1296206877012935,-0.7853370972660157],\\\"focalPoint\\\":[0.000001996755599975586,-0.0636419951915741,-0.03211300075054169],\\\"viewUp\\\":[0.41668520115314117,-0.4307600853225743,0.8005118312884919]}\",\"selected_field\":\"\"}"]},{"id":3,"type":"GeomPackFillHoles","pos":[-872.1312345377477,15.347377293094459],"size":[370,134],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":2},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.maxholesize","name":"backend.maxholesize","type":"INT","widget":{"name":"backend.maxholesize"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[16]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["pymeshlab",0,"false"]},{"id":4,"type":"GeomPackFillHoles","pos":[-880.4343275014749,198.27937524291306],"size":[370,158],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":3},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.max_edges","name":"backend.max_edges","type":"INT","widget":{"name":"backend.max_edges"},"link":null},{"localized_name":"backend.refine","name":"backend.refine","type":"COMBO","widget":{"name":"backend.refine"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[15]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["pymeshfix",0,"true","false"]}],"links":[[2,1,0,3,0,"TRIMESH"],[3,1,0,4,0,"TRIMESH"],[4,1,0,5,0,"TRIMESH"],[5,1,0,6,0,"TRIMESH"],[6,1,0,7,0,"TRIMESH"],[7,1,0,8,0,"TRIMESH"],[11,5,0,9,0,"TRIMESH"],[12,6,0,9,1,"TRIMESH"],[13,7,0,9,2,"TRIMESH"],[15,4,0,8,2,"TRIMESH"],[16,3,0,8,1,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.5557061362999314,"offset":[1336.8524572682982,439.4612648408024]},"frontendVersion":"1.30.6","workflowRendererVersion":"LG"},"version":0.4} \ No newline at end of file +{"id":"a1b2c3d4-e5f6-7890-abcd-fillholes0001","revision":0,"last_node_id":10,"last_link_id":18,"nodes":[{"id":1,"type":"GeomPackLoadMesh","pos":[-877.8583007812498,-118.69721679687503],"size":[270,78],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[2,3,4,5,6,7,17]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackLoadMesh"},"widgets_values":["3d/camelhead.off"]},{"id":5,"type":"GeomPackFillHoles","pos":[334.9272949218742,-20.695605468749807],"size":[370,110],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":4},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[11]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["igl_fan","false"]},{"id":6,"type":"GeomPackFillHoles","pos":[326.3868652343745,132.61044921874958],"size":[370,134],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":5},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.quality","name":"backend.quality","type":"COMBO","widget":{"name":"backend.quality"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[12]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["cgal","triangulate","false"]},{"id":9,"type":"GeomPackPreviewMeshMulti","pos":[733.4390136718749,-21.472656249999996],"size":[768,702],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":11},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":12},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":13},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":18},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackPreviewMeshMulti"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[0.3450675952860498,-1.1296206877012935,-0.7853370972660157],\\\"focalPoint\\\":[0.000001996755599975586,-0.0636419951915741,-0.03211300075054169],\\\"viewUp\\\":[0.41668520115314117,-0.4307600853225743,0.8005118312884919]}\",\"selected_field\":\"\"}"]},{"id":7,"type":"GeomPackFillHoles","pos":[330.51953125,299.8362304687501],"size":[370,134],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":6},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.perimeter","name":"backend.perimeter","type":"FLOAT","widget":{"name":"backend.perimeter"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[13]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["gpu_cumesh",20,"false"]},{"id":8,"type":"GeomPackPreviewMeshMulti","pos":[-453.58149414062495,-26.819580078124975],"size":[768,702],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":7},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":16},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":15},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":null},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackPreviewMeshMulti"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[0.3450675952860498,-1.1296206877012935,-0.7853370972660157],\\\"focalPoint\\\":[0.000001996755599975586,-0.0636419951915741,-0.03211300075054169],\\\"viewUp\\\":[0.41668520115314117,-0.4307600853225743,0.8005118312884919]}\",\"selected_field\":\"\"}"]},{"id":3,"type":"GeomPackFillHoles","pos":[-872.1312345377477,15.347377293094459],"size":[370,134],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":2},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.maxholesize","name":"backend.maxholesize","type":"INT","widget":{"name":"backend.maxholesize"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[16]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["pymeshlab",0,"false"]},{"id":4,"type":"GeomPackFillHoles","pos":[-880.4343275014749,198.27937524291306],"size":[370,158],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":3},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.max_edges","name":"backend.max_edges","type":"INT","widget":{"name":"backend.max_edges"},"link":null},{"localized_name":"backend.refine","name":"backend.refine","type":"COMBO","widget":{"name":"backend.refine"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[15]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["pymeshfix",0,"true","false"]},{"id":10,"type":"GeomPackFillHoles","pos":[330.5,480.0],"size":[370,110],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":17},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"filled_mesh","name":"filled_mesh","type":"TRIMESH","links":[18]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"6e8088fb3283086d421a58c3fdf5b061176519aa","Node name for S&R":"GeomPackFillHoles"},"widgets_values":["trimesh","false"]}],"links":[[2,1,0,3,0,"TRIMESH"],[3,1,0,4,0,"TRIMESH"],[4,1,0,5,0,"TRIMESH"],[5,1,0,6,0,"TRIMESH"],[6,1,0,7,0,"TRIMESH"],[7,1,0,8,0,"TRIMESH"],[11,5,0,9,0,"TRIMESH"],[12,6,0,9,1,"TRIMESH"],[13,7,0,9,2,"TRIMESH"],[15,4,0,8,2,"TRIMESH"],[16,3,0,8,1,"TRIMESH"],[17,1,0,10,0,"TRIMESH"],[18,10,0,9,3,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.5557061362999314,"offset":[1336.8524572682982,439.4612648408024]},"frontendVersion":"1.30.6","workflowRendererVersion":"LG"},"version":0.4} \ No newline at end of file diff --git a/workflows/fix_normals_all.json b/workflows/fix_normals_all.json index ef40d651..28b23c44 100644 --- a/workflows/fix_normals_all.json +++ b/workflows/fix_normals_all.json @@ -1 +1 @@ -{"id":"test-fix-normals-all-0001","revision":0,"last_node_id":11,"last_link_id":14,"nodes":[{"id":6,"type":"GeomPackFixNormals","pos":[-887.3027798647635,491.3849236163281],"size":[370,110],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":5},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[11]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["igl_signed_dist","false"]},{"id":5,"type":"GeomPackFixNormals","pos":[-889.1493998795415,340.07985048214533],"size":[370,110],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":4},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[10]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["igl_raycast","false"]},{"id":4,"type":"GeomPackFixNormals","pos":[-891.2679834338181,191.1117182341826],"size":[370,110],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":3},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[9]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["igl_winding","false"]},{"id":3,"type":"GeomPackFixNormals","pos":[-895.379931408891,43.61392767922324],"size":[370,110],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":2},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[8]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["igl_bfs","false"]},{"id":2,"type":"GeomPackFixNormals","pos":[-895.5148783097702,-102.5023129319287],"size":[370,110],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":1},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[7]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["trimesh","false"]},{"id":9,"type":"GeomPackPreviewMeshMulti","pos":[378.5374405209115,-99.09842223891802],"size":[768,702],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":10},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":11},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":null},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":null},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackPreviewMeshMulti","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[2.4457834419570967,0.9339641357719854,-1.1112698573158992],\\\"focalPoint\\\":[0,0,0],\\\"viewUp\\\":[-0.2837493462372615,0.9439214532462359,0.16881527955561504]}\",\"selected_field\":\"normals\",\"selected_channel\":\"0\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":8,"type":"GeomPackPreviewMeshMulti","pos":[-416.27848735286733,-99.09842223891802],"size":[768,702],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":6},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":7},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":8},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":9},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackPreviewMeshMulti","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[2.4457834419570967,0.9339641357719854,-1.1112698573158992],\\\"focalPoint\\\":[0,0,0],\\\"viewUp\\\":[-0.2837493462372615,0.9439214532462359,0.16881527955561504]}\",\"selected_field\":\"normals\",\"selected_channel\":\"0\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":1,"type":"GeomPackLoadMesh","pos":[-883.8968194340091,-229.55171550212876],"size":[270,78],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[1,2,3,4,5,6]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackLoadMesh","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["3d/truck.obj"]}],"links":[[1,1,0,2,0,"TRIMESH"],[2,1,0,3,0,"TRIMESH"],[3,1,0,4,0,"TRIMESH"],[4,1,0,5,0,"TRIMESH"],[5,1,0,6,0,"TRIMESH"],[6,1,0,8,0,"TRIMESH"],[7,2,0,8,1,"TRIMESH"],[8,3,0,8,2,"TRIMESH"],[9,4,0,8,3,"TRIMESH"],[10,5,0,9,0,"TRIMESH"],[11,6,0,9,1,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.798600000000001,"offset":[1042.4626817735398,377.0953401889155]},"frontendVersion":"1.30.6","workflowRendererVersion":"LG"},"version":0.4} \ No newline at end of file +{"id":"test-fix-normals-all-0001","revision":0,"last_node_id":13,"last_link_id":18,"nodes":[{"id":6,"type":"GeomPackFixNormals","pos":[-887.3027798647635,491.3849236163281],"size":[370,110],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":5},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[11]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["igl_signed_dist","false"]},{"id":5,"type":"GeomPackFixNormals","pos":[-889.1493998795415,340.07985048214533],"size":[370,110],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":4},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[10]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["igl_raycast","false"]},{"id":4,"type":"GeomPackFixNormals","pos":[-891.2679834338181,191.1117182341826],"size":[370,110],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":3},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[9]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["igl_winding","false"]},{"id":3,"type":"GeomPackFixNormals","pos":[-895.379931408891,43.61392767922324],"size":[370,110],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":2},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[8]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["igl_bfs","false"]},{"id":2,"type":"GeomPackFixNormals","pos":[-895.5148783097702,-102.5023129319287],"size":[370,110],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":1},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[7]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["trimesh","false"]},{"id":9,"type":"GeomPackPreviewMeshMulti","pos":[378.5374405209115,-99.09842223891802],"size":[768,702],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":10},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":11},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":16},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":18},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackPreviewMeshMulti","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[2.4457834419570967,0.9339641357719854,-1.1112698573158992],\\\"focalPoint\\\":[0,0,0],\\\"viewUp\\\":[-0.2837493462372615,0.9439214532462359,0.16881527955561504]}\",\"selected_field\":\"normals\",\"selected_channel\":\"0\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":8,"type":"GeomPackPreviewMeshMulti","pos":[-416.27848735286733,-99.09842223891802],"size":[768,702],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":6},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":7},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":8},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":9},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackPreviewMeshMulti","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[2.4457834419570967,0.9339641357719854,-1.1112698573158992],\\\"focalPoint\\\":[0,0,0],\\\"viewUp\\\":[-0.2837493462372615,0.9439214532462359,0.16881527955561504]}\",\"selected_field\":\"normals\",\"selected_channel\":\"0\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":1,"type":"GeomPackLoadMesh","pos":[-883.8968194340091,-229.55171550212876],"size":[270,78],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[1,2,3,4,5,6,15,17]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackLoadMesh","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["3d/truck.obj"]},{"id":12,"type":"GeomPackFixNormals","pos":[-887.3,640.0],"size":[370,110],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":15},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[16]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["cumesh","false"]},{"id":13,"type":"GeomPackFixNormals","pos":[-887.3,790.0],"size":[370,110],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":17},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[18]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackFixNormals","aux_id":"PozzettiAndrea/ComfyUI-GeometryPack"},"widgets_values":["cumesh_raystab",0.0001,"false"]}],"links":[[1,1,0,2,0,"TRIMESH"],[2,1,0,3,0,"TRIMESH"],[3,1,0,4,0,"TRIMESH"],[4,1,0,5,0,"TRIMESH"],[5,1,0,6,0,"TRIMESH"],[6,1,0,8,0,"TRIMESH"],[7,2,0,8,1,"TRIMESH"],[8,3,0,8,2,"TRIMESH"],[9,4,0,8,3,"TRIMESH"],[10,5,0,9,0,"TRIMESH"],[11,6,0,9,1,"TRIMESH"],[15,1,0,12,0,"TRIMESH"],[16,12,0,9,2,"TRIMESH"],[17,1,0,13,0,"TRIMESH"],[18,13,0,9,3,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.798600000000001,"offset":[1042.4626817735398,377.0953401889155]},"frontendVersion":"1.30.6","workflowRendererVersion":"LG"},"version":0.4} \ No newline at end of file diff --git a/workflows/fix_self_intersections_all.json b/workflows/fix_self_intersections_all.json new file mode 100644 index 00000000..6cc7a2ed --- /dev/null +++ b/workflows/fix_self_intersections_all.json @@ -0,0 +1 @@ +{"id":"dd0730d4-ed01-4a53-a9cd-8a3e261ee5e2","revision":0,"last_node_id":18,"last_link_id":32,"nodes":[{"id":14,"type":"GeomPackFixSelfIntersections","pos":[-176.89690513012454,51.3358367313686],"size":[375.130859375,150],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":22},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.fill_holes","name":"backend.fill_holes","type":"BOOLEAN","widget":{"name":"backend.fill_holes"},"link":null},{"localized_name":"backend.fix_normals","name":"backend.fix_normals","type":"BOOLEAN","widget":{"name":"backend.fix_normals"},"link":null},{"localized_name":"backend.max_hole_size","name":"backend.max_hole_size","type":"INT","widget":{"name":"backend.max_hole_size"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[28]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackFixSelfIntersections"},"widgets_values":["removal",true,true,100]},{"id":17,"type":"GeomPackFixSelfIntersections","pos":[-168.76000520558915,503.67004618539994],"size":[375.130859375,150],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":27},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.remove_unreferenced","name":"backend.remove_unreferenced","type":"BOOLEAN","widget":{"name":"backend.remove_unreferenced"},"link":null},{"localized_name":"backend.extract_outer_hull","name":"backend.extract_outer_hull","type":"BOOLEAN","widget":{"name":"backend.extract_outer_hull"},"link":null},{"localized_name":"backend.stitch_all","name":"backend.stitch_all","type":"BOOLEAN","widget":{"name":"backend.stitch_all"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[30]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackFixSelfIntersections"},"widgets_values":["remesh",true,false,true]},{"id":1,"type":"GeomPackLoadMesh","pos":[-502,-58],"size":[270,126],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[22,26,27,31]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"0.1.1","Node name for S&R":"GeomPackLoadMesh"},"widgets_values":["3d/camelhead.off","",null,null]},{"id":18,"type":"GeomPackDetectSelfIntersections","pos":[-174.6450922840335,-89.41062569612437],"size":[210,86],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":31}],"outputs":[{"localized_name":"mesh_with_field","name":"mesh_with_field","type":"TRIMESH","links":[32]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackDetectSelfIntersections"},"widgets_values":["false"]},{"id":15,"type":"GeomPackPreviewMeshMulti","pos":[338.15299276502964,-83.02132397169389],"size":[948.4046279227291,883.7608454776706],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":32},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":28},{"localized_name":"mesh_3","name":"mesh_3","shape":7,"type":"TRIMESH","link":29},{"localized_name":"mesh_4","name":"mesh_4","shape":7,"type":"TRIMESH","link":30},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackPreviewMeshMulti"},"widgets_values":["fields","{\"layout\":\"overlay\",\"show_edges\":false,\"camera_state\":\"{\\\"position\\\":[-0.07810936928734313,0.1213717380765996,0.08783395580966097],\\\"focalPoint\\\":[0.000001996755599975586,-0.0636419951915741,-0.03211300075054169],\\\"viewUp\\\":[-0.9415247743741634,-0.3058521363431022,-0.141370329044114]}\",\"selected_field\":\"face.self_intersecting\",\"selected_channel\":\"magnitude\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":16,"type":"GeomPackFixSelfIntersections","pos":[-169.77923143253508,277.99006214036024],"size":[375.130859375,174],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":26},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null},{"localized_name":"backend.epsilon","name":"backend.epsilon","type":"FLOAT","widget":{"name":"backend.epsilon"},"link":null},{"localized_name":"backend.max_iterations","name":"backend.max_iterations","type":"INT","widget":{"name":"backend.max_iterations"},"link":null},{"localized_name":"backend.direction","name":"backend.direction","type":"COMBO","widget":{"name":"backend.direction"},"link":null},{"localized_name":"backend.scale_by_intersection_count","name":"backend.scale_by_intersection_count","type":"BOOLEAN","widget":{"name":"backend.scale_by_intersection_count"},"link":null}],"outputs":[{"localized_name":"fixed_mesh","name":"fixed_mesh","type":"TRIMESH","links":[29]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackFixSelfIntersections"},"widgets_values":["perturbation",0.0001,20,"adaptive",true]}],"links":[[22,1,0,14,0,"TRIMESH"],[26,1,0,16,0,"TRIMESH"],[27,1,0,17,0,"TRIMESH"],[28,14,0,15,1,"TRIMESH"],[29,16,0,15,2,"TRIMESH"],[30,17,0,15,3,"TRIMESH"],[31,1,0,18,0,"TRIMESH"],[32,18,0,15,0,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.5947007367387036,"offset":[546.5890604670205,315.11283142732157]},"frontendVersion":"1.30.6","workflowRendererVersion":"LG"},"version":0.4} \ No newline at end of file diff --git a/workflows/normals.json b/workflows/normals.json deleted file mode 100644 index 9bd538b4..00000000 --- a/workflows/normals.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "id": "n4e5d6c7-8b9a-0123-def4-234567890123", - "revision": 0, - "last_node_id": 8, - "last_link_id": 9, - "nodes": [ - { - "id": 2, - "type": "GeomPackCheckNormals", - "pos": [ - -380, - -150 - ], - "size": [ - 320, - 78 - ], - "flags": {}, - "order": 1, - "mode": 0, - "inputs": [ - { - "name": "trimesh", - "type": "TRIMESH", - "link": 1 - } - ], - "outputs": [ - { - "name": "report", - "type": "STRING", - "links": [ - 9 - ] - } - ], - "properties": { - "cnr_id": "comfyui-geometrypack", - "ver": "0.1.1", - "Node name for S&R": "GeomPackCheckNormals" - }, - "widgets_values": [] - }, - { - "id": 1, - "type": "GeomPackLoadMesh", - "pos": [ - -700, - 0 - ], - "size": [ - 270, - 78 - ], - "flags": {}, - "order": 0, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "mesh", - "type": "TRIMESH", - "links": [ - 1, - 2 - ] - }, - { - "name": "texture", - "type": "IMAGE", - "links": null - } - ], - "properties": { - "cnr_id": "comfyui-geometrypack", - "ver": "0.1.1", - "Node name for S&R": "GeomPackLoadMesh" - }, - "widgets_values": [ - "3d/Stanford_Bunny.stl" - ] - }, - { - "id": 7, - "type": "GeomPackPreviewMeshVTK", - "pos": [ - 327.31770833333354, - 6.866319444444557 - ], - "size": [ - 512, - 694 - ], - "flags": {}, - "order": 4, - "mode": 0, - "inputs": [ - { - "name": "trimesh", - "type": "TRIMESH", - "link": 8 - } - ], - "outputs": [], - "properties": { - "cnr_id": "comfyui-geometrypack", - "ver": "991c7a75886f50d567e5ed6d36aed06d0729a938", - "Node name for S&R": "GeomPackPreviewMeshVTK" - }, - "widgets_values": [ - "fields", - "{\"show_edges\": false, \"camera_state\": \"\", \"selected_field\": \"\"}" - ] - }, - { - "id": 3, - "type": "GeomPackComputeNormals", - "pos": [ - -291.1762152777778, - 67.71267361111111 - ], - "size": [ - 320, - 78 - ], - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [ - { - "name": "trimesh", - "type": "TRIMESH", - "link": 2 - } - ], - "outputs": [ - { - "name": "mesh_with_normals", - "type": "TRIMESH", - "links": [ - 8 - ] - } - ], - "properties": { - "cnr_id": "comfyui-geometrypack", - "ver": "0.1.1", - "Node name for S&R": "GeomPackComputeNormals" - }, - "widgets_values": [ - "true" - ] - } - ], - "links": [ - [ - 1, - 1, - 0, - 2, - 0, - "TRIMESH" - ], - [ - 2, - 1, - 0, - 3, - 0, - "TRIMESH" - ], - [ - 8, - 3, - 0, - 7, - 0, - "TRIMESH" - ] - ], - "groups": [], - "config": {}, - "extra": { - "ds": { - "scale": 0.9, - "offset": [ - 1002.3090277777774, - 328.9756944444442 - ] - }, - "frontendVersion": "1.30.6" - }, - "version": 0.4 -} diff --git a/workflows/normals_warp.json b/workflows/normals_warp.json new file mode 100644 index 00000000..e4dcb7d2 --- /dev/null +++ b/workflows/normals_warp.json @@ -0,0 +1 @@ +{"id":"n4e5d6c7-8b9a-0123-def4-234567890123","revision":0,"last_node_id":11,"last_link_id":13,"nodes":[{"id":1,"type":"GeomPackLoadMesh","pos":[-901.6642639485106,89.22112817563179],"size":[270,126],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[1,2,13]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"0.1.1","Node name for S&R":"GeomPackLoadMesh"},"widgets_values":["3d/Stanford_Bunny.stl","",null,null]},{"id":2,"type":"GeomPackCheckNormals","pos":[-572.4573001890574,19.96120212905781],"size":[320,66],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":1}],"outputs":[{"localized_name":"info","name":"info","type":"STRING","links":[9]}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"0.1.1","Node name for S&R":"GeomPackCheckNormals"},"widgets_values":["false"]},{"id":3,"type":"GeomPackComputeNormals","pos":[-686.0720966497245,276.43283472543794],"size":[320,78],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":2},{"localized_name":"smooth_vertex_normals","name":"smooth_vertex_normals","type":"COMBO","widget":{"name":"smooth_vertex_normals"},"link":null}],"outputs":[{"localized_name":"mesh_with_normals","name":"mesh_with_normals","type":"TRIMESH","links":[8,10]}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"0.1.1","Node name for S&R":"GeomPackComputeNormals"},"widgets_values":["true"]},{"id":7,"type":"GeomPackPreviewMeshVTK","pos":[-324.622083437174,261.04563720397135],"size":[518.750286694717,540.0924621826683],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","shape":7,"type":"TRIMESH","link":8},{"localized_name":"voxelgrid","name":"voxelgrid","shape":7,"type":"VOXELGRID","link":null},{"localized_name":"mode","name":"mode","type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"991c7a75886f50d567e5ed6d36aed06d0729a938","Node name for S&R":"GeomPackPreviewMeshVTK"},"widgets_values":["fields","{\"show_edges\":false,\"camera_state\":\"{\\\"position\\\":[30.322647094726562,-279.0293731152747,59.22096252441406],\\\"focalPoint\\\":[30.322647094726562,1.8909549713134766,59.22096252441406],\\\"viewUp\\\":[0,0,1]}\",\"selected_field\":\"point:normal_z\",\"selected_channel\":\"magnitude\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":9,"type":"GeomPackWarpMesh","pos":[-101.76826324265102,-39.51744526271689],"size":[266.01857321266675,256],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":10},{"localized_name":"field_name","name":"field_name","shape":7,"type":"STRING","widget":{"name":"field_name"},"link":null},{"localized_name":"scale","name":"scale","shape":7,"type":"FLOAT","widget":{"name":"scale"},"link":null}],"outputs":[{"localized_name":"warped_mesh","name":"warped_mesh","type":"TRIMESH","links":[12]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackWarpMesh"},"widgets_values":["normals_xyz",-5,""]},{"id":11,"type":"GeomPackPreviewMeshDual","pos":[262.2354446192537,-53.01076571081009],"size":[532.1139169856758,834],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":13},{"localized_name":"mesh_2","name":"mesh_2","type":"TRIMESH","link":12},{"localized_name":"layout","name":"layout","shape":7,"type":"COMBO","widget":{"name":"layout"},"link":null},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null},{"localized_name":"opacity_1","name":"opacity_1","shape":7,"type":"FLOAT","widget":{"name":"opacity_1"},"link":null},{"localized_name":"opacity_2","name":"opacity_2","shape":7,"type":"FLOAT","widget":{"name":"opacity_2"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackPreviewMeshDual"},"widgets_values":["overlay","fields",0.5,0.5,"{\"show_edges\":false,\"camera_state\":\"{\\\"position\\\":[98.18463493940439,-46.113355004792425,75.52846265741326],\\\"focalPoint\\\":[19.537668178768655,-10.894511904966933,65.93606114350457],\\\"viewUp\\\":[0.014751951978902087,0.29329868518073204,0.9559070358481866]}\",\"selected_field\":\"\",\"selected_channel\":\"magnitude\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]}],"links":[[1,1,0,2,0,"TRIMESH"],[2,1,0,3,0,"TRIMESH"],[8,3,0,7,0,"TRIMESH"],[10,3,0,9,0,"TRIMESH"],[12,9,0,11,1,"TRIMESH"],[13,1,0,11,0,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.5023324820043956,"offset":[1312.3190918718033,273.85050849773535]},"frontendVersion":"1.30.6"},"version":0.4} \ No newline at end of file diff --git a/workflows/slice_save.json b/workflows/slice_save.json new file mode 100644 index 00000000..e4469ad9 --- /dev/null +++ b/workflows/slice_save.json @@ -0,0 +1 @@ +{"id":"3606b601-de0b-4543-94a3-8fcf692c36bb","revision":0,"last_node_id":22,"last_link_id":23,"nodes":[{"id":1,"type":"GeomPackLoadMesh","pos":[-641.7436980668338,130.9743348300825],"size":[270,126],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[1]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackLoadMesh"},"widgets_values":["3d/Stanford_Bunny.stl","",null,null]},{"id":2,"type":"GeomPackPreviewMeshSlicer","pos":[-353.3955575674956,90.95840600979454],"size":[569.1532433847783,602],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","link":1},{"localized_name":"plane_json","name":"plane_json","shape":7,"type":"STRING","widget":{"name":"plane_json"},"link":null}],"outputs":[{"localized_name":"sliced_mesh","name":"sliced_mesh","type":"TRIMESH","links":[21]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackPreviewMeshSlicer"},"widgets_values":["{\"type\":\"PLANE_UPDATE\",\"hasPlane\":false}",""]},{"id":3,"type":"GeomPackSaveMesh","pos":[243.75112496899632,86.7700884931457],"size":[270,82],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":21},{"localized_name":"file_path","name":"file_path","type":"STRING","widget":{"name":"file_path"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null}],"outputs":[{"localized_name":"file_path","name":"file_path","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackSaveMesh"},"widgets_values":["test","obj"]}],"links":[[1,1,0,2,0,"TRIMESH"],[21,2,0,3,0,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.9229599817706441,"offset":[1071.4329166717343,190.77899387464086]}},"version":0.4} \ No newline at end of file diff --git a/workflows/split_combine_mesh.json b/workflows/split_combine_mesh.json index 9612c502..e7ca4554 100644 --- a/workflows/split_combine_mesh.json +++ b/workflows/split_combine_mesh.json @@ -1 +1 @@ -{"id":"62e5876c-1e68-4bb7-95e6-cbf0d1dea968","revision":0,"last_node_id":11,"last_link_id":5,"nodes":[{"id":1,"type":"GeomPackCombineMeshesBatch","pos":[464.25566183369085,1425.8986203969664],"size":[210,86],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"meshes","name":"meshes","type":"TRIMESH","link":null}],"outputs":[{"localized_name":"combined_mesh","name":"combined_mesh","type":"TRIMESH","links":null},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackCombineMeshesBatch"},"widgets_values":["false"]},{"id":4,"type":"GeomPackLoadMeshGlob","pos":[-463.161407755437,1252.8875728855267],"size":[270,170],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"glob_pattern","name":"glob_pattern","type":"STRING","widget":{"name":"glob_pattern"},"link":null},{"localized_name":"start_index","name":"start_index","type":"INT","widget":{"name":"start_index"},"link":null},{"localized_name":"max_meshes","name":"max_meshes","type":"INT","widget":{"name":"max_meshes"},"link":null},{"localized_name":"sort_by","name":"sort_by","shape":7,"type":"COMBO","widget":{"name":"sort_by"},"link":null}],"outputs":[{"localized_name":"meshes","name":"meshes","shape":6,"type":"TRIMESH","links":null},{"localized_name":"textures","name":"textures","shape":6,"type":"IMAGE","links":null},{"localized_name":"file_paths","name":"file_paths","shape":6,"type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackLoadMeshGlob"},"widgets_values":["",0,-1,"name"]},{"id":5,"type":"GeomPackLoadMesh","pos":[-304.8301622115824,153.54991496156825],"size":[270,130],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[1]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackLoadMesh"},"widgets_values":["3d/truck.obj","",null,null]},{"id":7,"type":"GeomPackConnectedComponents","pos":[37.87113829841811,151.55573112122195],"size":[210,86],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":1}],"outputs":[{"localized_name":"trimesh","name":"trimesh","shape":6,"type":"TRIMESH","links":[3,5]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackConnectedComponents"},"widgets_values":["false"]},{"id":11,"type":"GeomPackPreviewMeshVTK","pos":[543.6922949957188,359.8251845468806],"size":[512,640],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","shape":7,"type":"TRIMESH","link":5},{"localized_name":"voxelgrid","name":"voxelgrid","shape":7,"type":"VOXELGRID","link":null},{"localized_name":"mode","name":"mode","type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackPreviewMeshVTK"},"widgets_values":["fields","{\"show_edges\":false,\"camera_state\":\"{\\\"position\\\":[1.0822377436645814,-0.03384579368716349,-1.3978064229441116],\\\"focalPoint\\\":[0,0,0],\\\"viewUp\\\":[0.16675920028455524,0.9803508071107849,0.10537392522672677]}\",\"selected_field\":\"\",\"selected_channel\":\"magnitude\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":8,"type":"GeomPackPreviewMeshVTKBatch","pos":[-88.49897816299122,668.5981638878701],"size":[512,766],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":4},{"localized_name":"mode","name":"mode","type":"COMBO","widget":{"name":"mode"},"link":null},{"localized_name":"index","name":"index","type":"INT","widget":{"name":"index"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackPreviewMeshVTKBatch"},"widgets_values":["fields",7,"{\"show_edges\":false,\"camera_state\":\"{\\\"position\\\":[1.0822377436645814,-0.03384579368716349,-1.3978064229441116],\\\"focalPoint\\\":[0,0,0],\\\"viewUp\\\":[0.16675920028455524,0.9803508071107849,0.10537392522672677]}\",\"selected_field\":\"\",\"selected_channel\":\"magnitude\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":9,"type":"GeomPackSplitByField","pos":[-307.14347025701375,344.2771933352222],"size":[270,110],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"geometry","name":"geometry","type":"TRIMESH,POINT_CLOUD","link":3},{"localized_name":"field_name","name":"field_name","type":"STRING","widget":{"name":"field_name"},"link":null}],"outputs":[{"localized_name":"geometries","name":"geometries","shape":6,"type":"TRIMESH","links":[4]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackSplitByField"},"widgets_values":["part_id","false"]}],"links":[[1,5,0,7,0,"TRIMESH"],[3,7,0,9,0,"TRIMESH"],[4,9,0,8,0,"TRIMESH"],[5,7,0,11,0,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.5876375746703355,"offset":[1332.7139452842334,-59.120652342936665]}},"version":0.4} \ No newline at end of file +{"id":"62e5876c-1e68-4bb7-95e6-cbf0d1dea968","revision":0,"last_node_id":16,"last_link_id":9,"nodes":[{"id":9,"type":"GeomPackSplitByField","pos":[-304.8744807244649,343.14267260262136],"size":[270,134],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"geometry","name":"geometry","type":"TRIMESH,POINT_CLOUD","link":3},{"localized_name":"field_name","name":"field_name","type":"STRING","widget":{"name":"field_name"},"link":null},{"localized_name":"max_geometries","name":"max_geometries","type":"INT","widget":{"name":"max_geometries"},"link":null}],"outputs":[{"localized_name":"geometries","name":"geometries","shape":6,"type":"TRIMESH","links":[4,6]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackSplitByField"},"widgets_values":["part_id",-1,"false"]},{"id":5,"type":"GeomPackLoadMesh","pos":[-287.0777111283892,158.62207701153503],"size":[270,130],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"COMBO","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[1]},{"localized_name":"texture","name":"texture","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackLoadMesh"},"widgets_values":["3d/truck.obj","",null,null]},{"id":16,"type":"GeomPackPreviewMeshDual","pos":[553.5879526088489,138.355393130082],"size":[615.836183326212,834],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":9},{"localized_name":"mesh_2","name":"mesh_2","type":"TRIMESH","link":8},{"localized_name":"layout","name":"layout","shape":7,"type":"COMBO","widget":{"name":"layout"},"link":null},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null},{"localized_name":"opacity_1","name":"opacity_1","shape":7,"type":"FLOAT","widget":{"name":"opacity_1"},"link":null},{"localized_name":"opacity_2","name":"opacity_2","shape":7,"type":"FLOAT","widget":{"name":"opacity_2"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackPreviewMeshDual"},"widgets_values":["side_by_side","fields",1,1,"{\"show_edges\":false,\"camera_state\":\"{\\\"position\\\":[2.5212539579099578,2.4418813623279996,-3.2300828759586153],\\\"focalPoint\\\":[0,0,0],\\\"viewUp\\\":[-0.09805344886675685,0.8291827228067867,0.5503103972887099]}\",\"selected_field\":\"face.part_id\",\"selected_channel\":\"magnitude\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":8,"type":"GeomPackPreviewMeshVTKBatch","pos":[-11.984025643600006,314.354805739011],"size":[541.2655274479791,766],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":4},{"localized_name":"mode","name":"mode","type":"COMBO","widget":{"name":"mode"},"link":null},{"localized_name":"index","name":"index","type":"INT","widget":{"name":"index"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackPreviewMeshVTKBatch"},"widgets_values":["fields",9,"{\"show_edges\":false,\"camera_state\":\"{\\\"position\\\":[-0.12939065538457956,0.29579903391306567,-0.8705497931022379],\\\"focalPoint\\\":[-0.39261598885059357,-0.19036790542304516,-0.3125719949603081],\\\"viewUp\\\":[-0.29917342015725373,0.7848402800618385,0.5426978896806823]}\",\"selected_field\":\"\",\"selected_channel\":\"magnitude\",\"selected_colormap\":\"erdc_rainbow_bright\"}"]},{"id":7,"type":"GeomPackConnectedComponents","pos":[29.12069861442173,145.30534348772468],"size":[210,86],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"trimesh","name":"trimesh","type":"TRIMESH","link":1}],"outputs":[{"localized_name":"trimesh","name":"trimesh","shape":6,"type":"TRIMESH","links":[3,9]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"25b5f08e0886aeb800481f3061b0bdf2b35ff57d","Node name for S&R":"GeomPackConnectedComponents"},"widgets_values":["false"]},{"id":12,"type":"GeomPackCombineMeshesBatch","pos":[304.96248111489217,180.25285150880757],"size":[210,86],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"meshes","name":"meshes","type":"TRIMESH","link":6}],"outputs":[{"localized_name":"combined_mesh","name":"combined_mesh","type":"TRIMESH","links":[8]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"9636f8cac81c4eb817a1a6e1f55378c5f5a2e528","Node name for S&R":"GeomPackCombineMeshesBatch"},"widgets_values":["false"]}],"links":[[1,5,0,7,0,"TRIMESH"],[3,7,0,9,0,"TRIMESH"],[4,9,0,8,0,"TRIMESH"],[6,9,0,12,0,"TRIMESH"],[8,12,0,16,1,"TRIMESH"],[9,7,0,16,0,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.5333018848295425,"offset":[794.8687820768164,181.13438757233152]}},"version":0.4} \ No newline at end of file