updated 'all' workflows
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
"""Individual Blender remeshing backend nodes (hidden, invoked via unified Remesh node)."""
|
||||
|
||||
from .voxel import NODE_CLASS_MAPPINGS as VOXEL_MAPS, NODE_DISPLAY_NAME_MAPPINGS as VOXEL_DISP
|
||||
from .smooth import NODE_CLASS_MAPPINGS as SMOOTH_MAPS, NODE_DISPLAY_NAME_MAPPINGS as SMOOTH_DISP
|
||||
from .sharp import NODE_CLASS_MAPPINGS as SHARP_MAPS, NODE_DISPLAY_NAME_MAPPINGS as SHARP_DISP
|
||||
from .blocks import NODE_CLASS_MAPPINGS as BLOCKS_MAPS, NODE_DISPLAY_NAME_MAPPINGS as BLOCKS_DISP
|
||||
|
||||
@@ -12,12 +11,10 @@ NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(VOXEL_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(SMOOTH_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(SHARP_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(BLOCKS_MAPS)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(VOXEL_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(SMOOTH_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(SHARP_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(BLOCKS_DISP)
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright (C) 2025 ComfyUI-GeometryPack Contributors
|
||||
|
||||
"""Blender smooth remesh modifier backend node."""
|
||||
|
||||
import logging
|
||||
import numpy as np
|
||||
import trimesh as trimesh_module
|
||||
from comfy_api.latest import io
|
||||
from .voxel import _bpy_setup_object, _bpy_extract_and_cleanup
|
||||
|
||||
log = logging.getLogger("geometrypack")
|
||||
|
||||
|
||||
class RemeshBlenderSmoothNode(io.ComfyNode):
|
||||
"""Blender smooth remesh modifier backend."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="GeomPackRemesh_BlenderSmooth",
|
||||
display_name="Remesh Blender Smooth (backend)",
|
||||
category="geompack/remeshing",
|
||||
is_dev_only=True,
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
io.Custom("TRIMESH").Input("trimesh"),
|
||||
io.Int.Input("octree_depth", default=6, min=1, max=10, step=1, tooltip="Resolution. Higher = more detail, more faces."),
|
||||
io.Float.Input("scale", default=0.9, min=0.0, max=1.0, step=0.05, display_mode="number", tooltip="Ratio of output size to input bounding box."),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("TRIMESH").Output(display_name="remeshed_mesh"),
|
||||
io.String.Output(display_name="info"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, trimesh, octree_depth=6, scale=0.9):
|
||||
import bpy
|
||||
|
||||
log.info("Backend: blender_smooth")
|
||||
log.info("Input: %d vertices, %d faces", len(trimesh.vertices), len(trimesh.faces))
|
||||
log.info("Parameters: octree_depth=%d, scale=%s", octree_depth, scale)
|
||||
|
||||
obj, mesh = _bpy_setup_object(
|
||||
np.asarray(trimesh.vertices, dtype=np.float32),
|
||||
np.asarray(trimesh.faces, dtype=np.int32)
|
||||
)
|
||||
mod = obj.modifiers.new(name="Remesh", type='REMESH')
|
||||
mod.mode = 'SMOOTH'
|
||||
mod.octree_depth = octree_depth
|
||||
mod.scale = scale
|
||||
bpy.ops.object.modifier_apply(modifier="Remesh")
|
||||
result = _bpy_extract_and_cleanup(obj)
|
||||
|
||||
remeshed_mesh = trimesh_module.Trimesh(
|
||||
vertices=np.array(result['vertices'], dtype=np.float32),
|
||||
faces=np.array(result['faces'], dtype=np.int32),
|
||||
process=False
|
||||
)
|
||||
remeshed_mesh.metadata = trimesh.metadata.copy()
|
||||
remeshed_mesh.metadata['remeshing'] = {
|
||||
'algorithm': 'blender_smooth', 'octree_depth': octree_depth, 'scale': scale,
|
||||
}
|
||||
|
||||
log.info("Output: %d vertices, %d faces", len(remeshed_mesh.vertices), len(remeshed_mesh.faces))
|
||||
|
||||
info = (f"Remesh (Blender Smooth): "
|
||||
f"{len(trimesh.vertices):,}v/{len(trimesh.faces):,}f -> "
|
||||
f"{len(remeshed_mesh.vertices):,}v/{len(remeshed_mesh.faces):,}f | "
|
||||
f"depth={octree_depth}, scale={scale}")
|
||||
|
||||
return io.NodeOutput(remeshed_mesh, info, ui={"text": [info]})
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"GeomPackRemesh_BlenderSmooth": RemeshBlenderSmoothNode}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"GeomPackRemesh_BlenderSmooth": "Remesh Blender Smooth (backend)"}
|
||||
@@ -31,7 +31,6 @@ from . import paraview
|
||||
|
||||
# CGAL nodes (moved from nodes/cgal/)
|
||||
from . import boolean
|
||||
from . import reconstruction_cgal
|
||||
from . import remeshing_cgal
|
||||
from . import repair_cgal
|
||||
from . import decimation_cgal
|
||||
@@ -54,7 +53,6 @@ NODE_CLASS_MAPPINGS.update(reconstruction.NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(texture_remeshing.NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(paraview.NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(boolean.NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(reconstruction_cgal.NODE_CLASS_MAPPINGS)
|
||||
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)
|
||||
@@ -79,7 +77,6 @@ NODE_DISPLAY_NAME_MAPPINGS.update(reconstruction.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(texture_remeshing.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(paraview.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(boolean.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(reconstruction_cgal.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
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)
|
||||
|
||||
@@ -9,8 +9,6 @@ from .poisson import NODE_CLASS_MAPPINGS as POISSON_MAPS, NODE_DISPLAY_NAME_MAPP
|
||||
from .ball_pivoting import NODE_CLASS_MAPPINGS as BP_MAPS, NODE_DISPLAY_NAME_MAPPINGS as BP_DISP
|
||||
from .alpha_shape import NODE_CLASS_MAPPINGS as AS_MAPS, NODE_DISPLAY_NAME_MAPPINGS as AS_DISP
|
||||
from .convex_hull import NODE_CLASS_MAPPINGS as CH_MAPS, NODE_DISPLAY_NAME_MAPPINGS as CH_DISP
|
||||
from .delaunay_2d import NODE_CLASS_MAPPINGS as D2D_MAPS, NODE_DISPLAY_NAME_MAPPINGS as D2D_DISP
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
@@ -18,12 +16,10 @@ NODE_CLASS_MAPPINGS.update(POISSON_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(BP_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(AS_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(CH_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(D2D_MAPS)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(POISSON_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(BP_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(AS_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(CH_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(D2D_DISP)
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
@@ -24,7 +24,7 @@ class ReconstructBallPivotingNode(io.ComfyNode):
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
io.Custom("TRIMESH").Input("points"),
|
||||
io.Float.Input("ball_radius", default=0.0, min=0.0, max=100.0, step=0.01, tooltip="Radius of the virtual ball rolled over the point cloud to form triangles. Smaller = finer detail but more holes, larger = smoother but loses detail. 0 = auto (PyMeshLab estimates from point spacing)."),
|
||||
io.Float.Input("ball_radius", default=0.0, min=0.0, max=100.0, step=0.01, tooltip="Ball radius in absolute units (same as mesh coordinates). Smaller = finer detail but more holes, larger = smoother but loses detail. 0 = auto (PyMeshLab estimates from point spacing)."),
|
||||
io.Combo.Input("estimate_normals", options=["true", "false"], default="true", tooltip="Re-estimate point normals using k-nearest neighbors. Enable if the input has no normals or unreliable normals."),
|
||||
io.Float.Input("normal_radius", default=0.1, min=0.001, max=10.0, step=0.01, tooltip="Search radius for normal estimation via k-nearest neighbors. Should be 2-3x the average point spacing. Only used when estimate_normals is true."),
|
||||
],
|
||||
@@ -68,7 +68,7 @@ class ReconstructBallPivotingNode(io.ComfyNode):
|
||||
ms.generate_surface_reconstruction_ball_pivoting()
|
||||
else:
|
||||
ms.generate_surface_reconstruction_ball_pivoting(
|
||||
ballradius=pymeshlab.PercentageValue(ball_radius * 100)
|
||||
ballradius=pymeshlab.PureValue(ball_radius)
|
||||
)
|
||||
|
||||
result_mesh = ms.current_mesh()
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright (C) 2025 ComfyUI-GeometryPack Contributors
|
||||
|
||||
"""2D Delaunay triangulation surface reconstruction backend node."""
|
||||
|
||||
import logging
|
||||
import numpy as np
|
||||
import trimesh as trimesh_module
|
||||
from comfy_api.latest import io
|
||||
|
||||
log = logging.getLogger("geometrypack")
|
||||
|
||||
|
||||
class ReconstructDelaunay2DNode(io.ComfyNode):
|
||||
"""2D Delaunay triangulation (for height fields)."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="GeomPackReconstruct_Delaunay2D",
|
||||
display_name="Reconstruct Delaunay 2D (backend)",
|
||||
category="geompack/reconstruction",
|
||||
is_dev_only=True,
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
io.Custom("TRIMESH").Input("points"),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("TRIMESH").Output(display_name="reconstructed_mesh"),
|
||||
io.String.Output(display_name="info"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, points):
|
||||
log.info("Backend: delaunay_2d")
|
||||
vertices = np.asarray(points.vertices, dtype=np.float64)
|
||||
|
||||
log.info("Computing 2D Delaunay triangulation...")
|
||||
|
||||
try:
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
points_2d = vertices[:, :2]
|
||||
tri = Delaunay(points_2d)
|
||||
|
||||
result = trimesh_module.Trimesh(
|
||||
vertices=vertices,
|
||||
faces=tri.simplices,
|
||||
process=False
|
||||
)
|
||||
|
||||
info = f"""Reconstruct Surface Results (2D Delaunay):
|
||||
|
||||
Input Points: {len(vertices):,}
|
||||
Output Vertices: {len(result.vertices):,}
|
||||
Output Faces: {len(result.faces):,}
|
||||
|
||||
2D Delaunay projects points to XY plane for triangulation.
|
||||
Best for height fields and terrain data.
|
||||
"""
|
||||
# Preserve metadata
|
||||
if hasattr(points, 'metadata') and points.metadata:
|
||||
result.metadata = points.metadata.copy()
|
||||
else:
|
||||
result.metadata = {}
|
||||
result.metadata['reconstruction'] = {
|
||||
'method': 'delaunay_2d',
|
||||
'input_points': len(vertices),
|
||||
'output_vertices': len(result.vertices),
|
||||
'output_faces': len(result.faces),
|
||||
}
|
||||
|
||||
return io.NodeOutput(result, info, ui={"text": [info]})
|
||||
|
||||
except ImportError:
|
||||
raise ImportError("2D Delaunay requires scipy. Install with: pip install scipy")
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"GeomPackReconstruct_Delaunay2D": ReconstructDelaunay2DNode}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"GeomPackReconstruct_Delaunay2D": "Reconstruct Delaunay 2D (backend)"}
|
||||
@@ -26,14 +26,10 @@ class ReconstructSurfaceNode(io.ComfyNode):
|
||||
"ball_pivoting": "GeomPackReconstruct_BallPivoting",
|
||||
"alpha_shape": "GeomPackReconstruct_AlphaShape",
|
||||
"convex_hull": "GeomPackReconstruct_ConvexHull",
|
||||
"delaunay_2d": "GeomPackReconstruct_Delaunay2D",
|
||||
"alpha_wrap": "GeomPackAlphaWrap",
|
||||
}
|
||||
|
||||
# Remap frontend param names to backend param names where they differ
|
||||
PARAM_REMAP = {
|
||||
"alpha_wrap": {"points": "input_mesh"},
|
||||
}
|
||||
PARAM_REMAP = {}
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
@@ -50,9 +46,7 @@ class ReconstructSurfaceNode(io.ComfyNode):
|
||||
"poisson=smooth watertight, "
|
||||
"ball_pivoting=preserves detail, "
|
||||
"alpha_shape=controllable detail, "
|
||||
"convex_hull=fast rough, "
|
||||
"delaunay_2d=height fields, "
|
||||
"alpha_wrap=shrink wrap (CGAL)"
|
||||
"convex_hull=fast rough"
|
||||
), options=[
|
||||
io.DynamicCombo.Option("poisson", [
|
||||
io.Int.Input("poisson_depth", default=8, min=1, max=12, step=1, tooltip="Octree depth for the Poisson solver. Higher values capture finer detail but use exponentially more memory and time. 6=coarse, 8=balanced, 10+=high detail."),
|
||||
@@ -61,7 +55,7 @@ class ReconstructSurfaceNode(io.ComfyNode):
|
||||
io.Float.Input("normal_radius", default=0.1, min=0.001, max=10.0, step=0.01, tooltip="Search radius for normal estimation via k-nearest neighbors. Should be 2-3x the average point spacing. Only used when estimate_normals is true."),
|
||||
]),
|
||||
io.DynamicCombo.Option("ball_pivoting", [
|
||||
io.Float.Input("ball_radius", default=0.0, min=0.0, max=100.0, step=0.01, tooltip="Radius of the virtual ball rolled over the point cloud to form triangles. Smaller = finer detail but more holes, larger = smoother but loses detail. 0 = auto (PyMeshLab estimates from point spacing)."),
|
||||
io.Float.Input("ball_radius", default=0.0, min=0.0, max=100.0, step=0.01, tooltip="Ball radius in absolute units (same as mesh coordinates). Smaller = finer detail but more holes, larger = smoother but loses detail. 0 = auto (PyMeshLab estimates from point spacing)."),
|
||||
io.Combo.Input("estimate_normals", options=["true", "false"], default="true", tooltip="Re-estimate point normals using k-nearest neighbors. Enable if the input has no normals or unreliable normals."),
|
||||
io.Float.Input("normal_radius", default=0.1, min=0.001, max=10.0, step=0.01, tooltip="Search radius for normal estimation via k-nearest neighbors. Should be 2-3x the average point spacing. Only used when estimate_normals is true."),
|
||||
]),
|
||||
@@ -69,11 +63,6 @@ class ReconstructSurfaceNode(io.ComfyNode):
|
||||
io.Float.Input("alpha", default=0.0, min=0.0, max=100.0, step=0.01, tooltip="Radius threshold controlling which Delaunay tetrahedra are kept. Only tetrahedra with longest edge < 2*alpha are included. Larger = coarser shape with more fill, smaller = tighter fit with more holes. 0 = auto (10%% of bounding box diagonal)."),
|
||||
]),
|
||||
io.DynamicCombo.Option("convex_hull", []),
|
||||
io.DynamicCombo.Option("delaunay_2d", []),
|
||||
io.DynamicCombo.Option("alpha_wrap", [
|
||||
io.Float.Input("alpha_percent", default=2.0, min=0.001, max=50.0, step=0.1, tooltip="Wrap tightness as %% of bounding box diagonal. Smaller = tighter wrap with more detail, but MUCH slower."),
|
||||
io.Float.Input("offset_percent", default=2.0, min=0.01, max=10.0, step=0.1, tooltip="Surface offset as %% of bounding box diagonal. Smaller = closer to original surface."),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
outputs=[
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright (C) 2025 ComfyUI-GeometryPack Contributors
|
||||
|
||||
"""
|
||||
CGAL reconstruction nodes - requires pymeshlab with CGAL support
|
||||
"""
|
||||
|
||||
from .alpha_wrap import NODE_CLASS_MAPPINGS as ALPHA_WRAP_MAPS, NODE_DISPLAY_NAME_MAPPINGS as ALPHA_WRAP_DISP
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(ALPHA_WRAP_MAPS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(ALPHA_WRAP_DISP)
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
@@ -1,267 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright (C) 2025 ComfyUI-GeometryPack Contributors
|
||||
|
||||
"""
|
||||
Alpha Wrap Node - Shrink wrap mesh generation using CGAL's Alpha Wrap algorithm.
|
||||
|
||||
Creates a watertight mesh that tightly wraps around input geometry.
|
||||
Useful for non-manifold meshes or polygon soups.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import trimesh
|
||||
import comfy.utils
|
||||
from comfy_api.latest import io
|
||||
|
||||
from CGAL import CGAL_Alpha_wrap_3
|
||||
from CGAL.CGAL_Kernel import Point_3
|
||||
from CGAL.CGAL_Polyhedron_3 import Polyhedron_3
|
||||
|
||||
log = logging.getLogger("geometrypack")
|
||||
|
||||
# Progress bar layout (percentage of total)
|
||||
_PHASE_BUILD = 15 # 0-15%: building CGAL input
|
||||
_PHASE_WRAP_START = 15 # 15%: wrap starts
|
||||
_PHASE_WRAP_END = 85 # ..85%: wrap ends (time-based fill)
|
||||
_PHASE_EXTRACT = 100 # 85-100%: extracting result
|
||||
|
||||
|
||||
def _get_rss_mb():
|
||||
"""Get current process RSS in MB (Linux)."""
|
||||
try:
|
||||
with open(f"/proc/{os.getpid()}/statm", "r") as f:
|
||||
pages = int(f.read().split()[1])
|
||||
return pages * os.sysconf("SC_PAGE_SIZE") / (1024 * 1024)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _polyhedron_to_arrays(poly):
|
||||
"""Extract vertices and faces from a CGAL Polyhedron_3."""
|
||||
verts = []
|
||||
vertex_map = {}
|
||||
for i, vertex in enumerate(poly.vertices()):
|
||||
point = vertex.point()
|
||||
verts.append([float(point.x()), float(point.y()), float(point.z())])
|
||||
vertex_map[vertex] = i
|
||||
|
||||
faces = []
|
||||
for facet in poly.facets():
|
||||
halfedge = facet.halfedge()
|
||||
face_verts = []
|
||||
start_he = halfedge
|
||||
current = start_he
|
||||
while True:
|
||||
face_verts.append(vertex_map[current.vertex()])
|
||||
current = current.next()
|
||||
if current == start_he:
|
||||
break
|
||||
if len(face_verts) == 3:
|
||||
faces.append(face_verts)
|
||||
|
||||
return np.array(verts, dtype=np.float64), np.array(faces, dtype=np.int32)
|
||||
|
||||
|
||||
class AlphaWrapNode(io.ComfyNode):
|
||||
"""
|
||||
Alpha Wrap - Generate a watertight shrink-wrapped mesh.
|
||||
|
||||
Uses CGAL's Alpha Wrap algorithm to create a tight-fitting
|
||||
watertight mesh around input geometry.
|
||||
|
||||
Works with:
|
||||
- Non-manifold meshes
|
||||
- Meshes with holes
|
||||
- Polygon soups (overlapping/intersecting geometry)
|
||||
|
||||
Parameters:
|
||||
- alpha: Controls wrap tightness. Smaller = tighter wrap, more detail.
|
||||
Relative to bounding box diagonal (0.01 = 1% of bbox diagonal).
|
||||
- offset: Surface offset distance. Smaller = closer to original surface.
|
||||
Relative to bounding box diagonal.
|
||||
"""
|
||||
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="GeomPackAlphaWrap",
|
||||
display_name="Alpha Wrap (Shrink Wrap)",
|
||||
category="geompack/reconstruction",
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
io.Custom("TRIMESH").Input("input_mesh"),
|
||||
io.Float.Input("alpha_percent", default=2.0, min=0.001, max=50.0, step=0.1, tooltip="Wrap tightness as % of bounding box diagonal. Smaller = tighter wrap with more detail, but MUCH slower (runtime ~ 1/alpha\u00b3). Start at 2-5% and tighten if needed.", optional=True),
|
||||
io.Float.Input("offset_percent", default=2.0, min=0.01, max=10.0, step=0.1, tooltip="Surface offset as % of bounding box diagonal. Smaller = closer to original surface.", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("TRIMESH").Output(display_name="wrapped_mesh"),
|
||||
io.String.Output(display_name="info"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, input_mesh, alpha_percent=2.0, offset_percent=2.0):
|
||||
vertices = np.asarray(input_mesh.vertices, dtype=np.float64)
|
||||
input_vertex_count = len(vertices)
|
||||
|
||||
is_point_cloud = (
|
||||
not hasattr(input_mesh, 'faces') or
|
||||
input_mesh.faces is None or
|
||||
len(input_mesh.faces) == 0
|
||||
)
|
||||
|
||||
if is_point_cloud:
|
||||
raise ValueError(
|
||||
"Alpha Wrap requires a mesh with faces (triangle soup), not a point cloud.\n"
|
||||
"For point clouds, first use 'Reconstruct Surface' node (e.g., Poisson or Ball Pivoting) "
|
||||
"to create a mesh, then apply Alpha Wrap."
|
||||
)
|
||||
|
||||
input_face_count = len(input_mesh.faces)
|
||||
input_type = "mesh"
|
||||
|
||||
# Compute bounding box diagonal for relative parameters
|
||||
bbox_min = vertices.min(axis=0)
|
||||
bbox_max = vertices.max(axis=0)
|
||||
bbox_diagonal = np.linalg.norm(bbox_max - bbox_min)
|
||||
|
||||
# Convert percentages to absolute values
|
||||
alpha = (alpha_percent / 100.0) * bbox_diagonal
|
||||
offset = (offset_percent / 100.0) * bbox_diagonal
|
||||
|
||||
log.info("Input: %s vertices, %s faces (%s)", f"{input_vertex_count:,}", f"{input_face_count:,}", input_type)
|
||||
log.info("Bounding box diagonal: %.4f", bbox_diagonal)
|
||||
log.info("Alpha: %.6f (%s%% of bbox)", alpha, alpha_percent)
|
||||
log.info("Offset: %.6f (%s%% of bbox)", offset, offset_percent)
|
||||
|
||||
pbar = comfy.utils.ProgressBar(100)
|
||||
|
||||
# --- Phase 1: Build CGAL input data (0-15%) ---
|
||||
t0 = time.monotonic()
|
||||
cgal_points = CGAL_Alpha_wrap_3.Point_3_Vector()
|
||||
cgal_points.reserve(input_vertex_count)
|
||||
report_every = max(1, input_vertex_count // 10)
|
||||
for i, v in enumerate(vertices):
|
||||
cgal_points.append(Point_3(float(v[0]), float(v[1]), float(v[2])))
|
||||
if i % report_every == 0:
|
||||
pbar.update_absolute(int(_PHASE_BUILD * i / input_vertex_count))
|
||||
|
||||
cgal_faces = [[int(idx) for idx in face] for face in input_mesh.faces]
|
||||
pbar.update_absolute(_PHASE_BUILD)
|
||||
log.info("Built CGAL input data in %.1fs", time.monotonic() - t0)
|
||||
|
||||
# --- Phase 2: Run alpha wrap (15-85%) ---
|
||||
output_poly = Polyhedron_3()
|
||||
exc_holder = [None]
|
||||
rss_before = _get_rss_mb()
|
||||
|
||||
def _run():
|
||||
try:
|
||||
CGAL_Alpha_wrap_3.alpha_wrap_3(cgal_points, cgal_faces, alpha, offset, output_poly)
|
||||
except Exception as e:
|
||||
exc_holder[0] = e
|
||||
|
||||
log.info("Running CGAL Alpha Wrap...")
|
||||
thread = threading.Thread(target=_run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
wrap_start = time.monotonic()
|
||||
interval = 5
|
||||
wrap_range = _PHASE_WRAP_END - _PHASE_WRAP_START
|
||||
while thread.is_alive():
|
||||
thread.join(timeout=interval)
|
||||
elapsed = time.monotonic() - wrap_start
|
||||
rss = _get_rss_mb()
|
||||
delta = rss - rss_before
|
||||
if thread.is_alive():
|
||||
# Asymptotic progress: approaches _PHASE_WRAP_END but never reaches it
|
||||
# Uses 1 - 1/(1 + t/30) so it fills ~50% at 30s, ~75% at 90s, etc.
|
||||
frac = 1.0 - 1.0 / (1.0 + elapsed / 30.0)
|
||||
progress = int(_PHASE_WRAP_START + wrap_range * frac)
|
||||
pbar.update_absolute(progress)
|
||||
log.info("Alpha Wrap running... %.0fs elapsed, RSS %.0f MB (+%.0f MB)",
|
||||
elapsed, rss, delta)
|
||||
|
||||
elapsed_total = time.monotonic() - wrap_start
|
||||
rss_final = _get_rss_mb()
|
||||
pbar.update_absolute(_PHASE_WRAP_END)
|
||||
log.info("Alpha Wrap finished in %.1fs, RSS %.0f MB (+%.0f MB)",
|
||||
elapsed_total, rss_final, rss_final - rss_before)
|
||||
|
||||
if exc_holder[0] is not None:
|
||||
raise exc_holder[0]
|
||||
|
||||
# --- Phase 3: Extract result mesh (85-100%) ---
|
||||
t0 = time.monotonic()
|
||||
result_vertices, result_faces = _polyhedron_to_arrays(output_poly)
|
||||
pbar.update_absolute(_PHASE_EXTRACT)
|
||||
log.info("Extracted result mesh in %.1fs", time.monotonic() - t0)
|
||||
|
||||
result_mesh = trimesh.Trimesh(
|
||||
vertices=result_vertices,
|
||||
faces=result_faces,
|
||||
process=False
|
||||
)
|
||||
|
||||
# Copy metadata if present
|
||||
if hasattr(input_mesh, 'metadata') and input_mesh.metadata:
|
||||
result_mesh.metadata = input_mesh.metadata.copy()
|
||||
else:
|
||||
result_mesh.metadata = {}
|
||||
|
||||
result_mesh.metadata['alpha_wrap'] = {
|
||||
'alpha': alpha,
|
||||
'alpha_percent': alpha_percent,
|
||||
'offset': offset,
|
||||
'offset_percent': offset_percent,
|
||||
'bbox_diagonal': bbox_diagonal,
|
||||
'input_type': input_type
|
||||
}
|
||||
|
||||
output_vertex_count = len(result_mesh.vertices)
|
||||
output_face_count = len(result_mesh.faces)
|
||||
is_watertight = result_mesh.is_watertight
|
||||
|
||||
log.info("Result: %s vertices, %s faces", f"{output_vertex_count:,}", f"{output_face_count:,}")
|
||||
log.info("Watertight: %s", is_watertight)
|
||||
|
||||
report = f"""Alpha Wrap Report
|
||||
{'='*40}
|
||||
|
||||
Input:
|
||||
Type: {input_type}
|
||||
Vertices: {input_vertex_count:,}
|
||||
Faces: {input_face_count:,}
|
||||
|
||||
Parameters:
|
||||
Alpha: {alpha:.6f} ({alpha_percent}% of bbox diagonal)
|
||||
Offset: {offset:.6f} ({offset_percent}% of bbox diagonal)
|
||||
BBox Diagonal: {bbox_diagonal:.4f}
|
||||
|
||||
Output:
|
||||
Vertices: {output_vertex_count:,}
|
||||
Faces: {output_face_count:,}
|
||||
Watertight: {'Yes' if is_watertight else 'No'}
|
||||
|
||||
Tips:
|
||||
- Decrease alpha_percent for tighter wrap / more detail
|
||||
- Decrease offset_percent to get closer to original surface
|
||||
- Lower values = slower computation
|
||||
"""
|
||||
|
||||
return io.NodeOutput(result_mesh, report, ui={"text": [report]})
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackAlphaWrap": AlphaWrapNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackAlphaWrap": "Alpha Wrap (Shrink Wrap)",
|
||||
}
|
||||
@@ -35,7 +35,6 @@ class RemeshNode(io.ComfyNode):
|
||||
"quadwild": "GeomPackRemesh_QuadWild",
|
||||
"cgal_isotropic": "GeomPackRemesh_CGAL",
|
||||
"blender_voxel": "GeomPackRemesh_BlenderVoxel",
|
||||
"blender_smooth": "GeomPackRemesh_BlenderSmooth",
|
||||
"blender_sharp": "GeomPackRemesh_BlenderSharp",
|
||||
"blender_blocks": "GeomPackRemesh_BlenderBlocks",
|
||||
"gpu_cumesh": "GeomPackRemesh_GPU",
|
||||
@@ -123,10 +122,6 @@ class RemeshNode(io.ComfyNode):
|
||||
io.DynamicCombo.Option("blender_voxel", [
|
||||
io.Float.Input("voxel_size", default=1, min=0.001, max=1.0, step=0.01, display_mode="number", tooltip="Voxel size. Smaller = more detail. Output is always watertight."),
|
||||
]),
|
||||
io.DynamicCombo.Option("blender_smooth", [
|
||||
io.Int.Input("octree_depth", default=6, min=1, max=10, step=1, tooltip="Resolution. Higher = more detail, more faces."),
|
||||
io.Float.Input("scale", default=0.9, min=0.0, max=1.0, step=0.05, display_mode="number", tooltip="Ratio of output size to input bounding box."),
|
||||
]),
|
||||
io.DynamicCombo.Option("blender_sharp", [
|
||||
io.Int.Input("octree_depth", default=6, min=1, max=10, step=1, tooltip="Resolution. Higher = more detail, more faces."),
|
||||
io.Float.Input("scale", default=0.9, min=0.0, max=1.0, step=0.05, display_mode="number", tooltip="Ratio of output size to input bounding box."),
|
||||
|
||||
@@ -24,10 +24,6 @@ Available backends:
|
||||
- guided_normal: Guided mesh normal filtering (Zhang et al. 2015). Uses a
|
||||
min-range-metric guidance signal to drive bilateral normal filtering while
|
||||
preserving sharp edges. Interleaves vertex updates within normal iterations.
|
||||
- vsa_snap: Variational Shape Approximation face clustering with vertex
|
||||
snapping. Clusters faces into proxy plane groups via Lloyd iteration, then
|
||||
snaps vertices to proxy planes (interior), plane intersection lines (edges),
|
||||
or plane intersection points (corners).
|
||||
- fast_effective: Fast and Effective Feature-Preserving Mesh Denoising
|
||||
(Sun et al. TVCG 2007). Uses thresholded cosine-similarity weights for
|
||||
normal filtering: w = max(0, dot(ni,nj) - T)^2. Simple and fast.
|
||||
@@ -56,7 +52,6 @@ class SharpenMeshNode(io.ComfyNode):
|
||||
"libigl_unsharp": "GeomPackSharpen_LibiglUnsharp",
|
||||
"l0_minimize": "GeomPackSharpen_L0Minimize",
|
||||
"guided_normal": "GeomPackSharpen_GuidedNormal",
|
||||
"vsa_snap": "GeomPackSharpen_VSASnap",
|
||||
"fast_effective": "GeomPackSharpen_FastEffective",
|
||||
"non_iterative": "GeomPackSharpen_NonIterative",
|
||||
}
|
||||
@@ -78,7 +73,6 @@ class SharpenMeshNode(io.ComfyNode):
|
||||
"libigl_unsharp=cotangent-weighted unsharp (geometry-aware), "
|
||||
"l0_minimize=piecewise-flat L0 optimization (aggressive CAD prep), "
|
||||
"guided_normal=guided normal filtering with min-range-metric (controllable), "
|
||||
"vsa_snap=face clustering + vertex snapping (explicit patch control), "
|
||||
"fast_effective=thresholded cosine weight normal filtering (fast), "
|
||||
"non_iterative=mollified normal single-pass bilateral (non-iterative)"
|
||||
), options=[
|
||||
@@ -105,15 +99,14 @@ class SharpenMeshNode(io.ComfyNode):
|
||||
)),
|
||||
]),
|
||||
io.DynamicCombo.Option("libigl_unsharp", [
|
||||
io.Float.Input("lambda_", default=0.5, min=0.01, max=5.0, step=0.01, tooltip=(
|
||||
"Unsharp mask strength. Controls how much the cotangent "
|
||||
"Laplacian displacement is amplified. Higher values produce "
|
||||
"stronger sharpening but may cause overshooting. "
|
||||
"Start with 0.3-0.5."
|
||||
io.Float.Input("weight", default=0.5, min=0.01, max=5.0, step=0.01, tooltip=(
|
||||
"How much detail to add back. 0.5 = subtle sharpening, "
|
||||
"1.0 = double the detail, 2.0+ = aggressive."
|
||||
)),
|
||||
io.Int.Input("iterations", default=3, min=1, max=50, step=1, tooltip=(
|
||||
"Number of unsharp mask passes. Multiple light passes "
|
||||
"are more stable than a single heavy pass."
|
||||
"Smoothing iterations for the reference mesh. "
|
||||
"More iterations = smoother reference = sharpens broader features. "
|
||||
"Fewer iterations = sharpens fine detail."
|
||||
)),
|
||||
]),
|
||||
io.DynamicCombo.Option("l0_minimize", [
|
||||
@@ -156,23 +149,6 @@ class SharpenMeshNode(io.ComfyNode):
|
||||
"dihedral angle threshold."
|
||||
)),
|
||||
]),
|
||||
io.DynamicCombo.Option("vsa_snap", [
|
||||
io.Int.Input("num_proxies", default=20, min=2, max=500, step=1, tooltip=(
|
||||
"Number of proxy planes (face clusters). More proxies "
|
||||
"preserve more detail. Fewer proxies create a more "
|
||||
"abstract/simplified appearance. 10-50 for simple shapes, "
|
||||
"50-200 for complex shapes."
|
||||
)),
|
||||
io.Int.Input("lloyd_iterations", default=10, min=1, max=100, step=1, tooltip=(
|
||||
"Number of Lloyd clustering iterations. 10 is usually "
|
||||
"sufficient for convergence."
|
||||
)),
|
||||
io.Float.Input("snap_strength", default=1.0, min=0.0, max=1.0, step=0.01, tooltip=(
|
||||
"How aggressively to snap vertices to proxy planes. "
|
||||
"0.0 = no change, 1.0 = full snap. "
|
||||
"Values like 0.5-0.8 give a subtle sharpening effect."
|
||||
)),
|
||||
]),
|
||||
io.DynamicCombo.Option("fast_effective", [
|
||||
io.Float.Input("threshold_T", default=0.5, min=1e-10, max=1.0, step=0.01, tooltip=(
|
||||
"Cosine similarity threshold (Sun et al. TVCG 2007). "
|
||||
|
||||
@@ -10,7 +10,6 @@ from .unsharp_mask import NODE_CLASS_MAPPINGS as UM_MAPS, NODE_DISPLAY_NAME_MAPP
|
||||
from .libigl_unsharp import NODE_CLASS_MAPPINGS as LU_MAPS, NODE_DISPLAY_NAME_MAPPINGS as LU_DISP
|
||||
from .l0_minimize import NODE_CLASS_MAPPINGS as L0_MAPS, NODE_DISPLAY_NAME_MAPPINGS as L0_DISP
|
||||
from .guided_normal import NODE_CLASS_MAPPINGS as GN_MAPS, NODE_DISPLAY_NAME_MAPPINGS as GN_DISP
|
||||
from .vsa_snap import NODE_CLASS_MAPPINGS as VS_MAPS, NODE_DISPLAY_NAME_MAPPINGS as VS_DISP
|
||||
from .fast_effective import NODE_CLASS_MAPPINGS as FE_MAPS, NODE_DISPLAY_NAME_MAPPINGS as FE_DISP
|
||||
from .non_iterative import NODE_CLASS_MAPPINGS as NI_MAPS, NODE_DISPLAY_NAME_MAPPINGS as NI_DISP
|
||||
|
||||
@@ -22,7 +21,6 @@ NODE_CLASS_MAPPINGS.update(UM_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(LU_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(L0_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(GN_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(VS_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(FE_MAPS)
|
||||
NODE_CLASS_MAPPINGS.update(NI_MAPS)
|
||||
|
||||
@@ -31,7 +29,6 @@ NODE_DISPLAY_NAME_MAPPINGS.update(UM_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(LU_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(L0_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(GN_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(VS_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(FE_DISP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(NI_DISP)
|
||||
|
||||
|
||||
@@ -11,16 +11,19 @@ from comfy_api.latest import io
|
||||
log = logging.getLogger("geometrypack")
|
||||
|
||||
|
||||
def _libigl_unsharp_sharpen(mesh, lambda_, iterations):
|
||||
def _libigl_unsharp_sharpen(mesh, weight, iterations):
|
||||
"""Cotangent-weighted geometric unsharp mask via libigl.
|
||||
|
||||
V_sharp = V + lambda * M^{-1} * L * V
|
||||
where L is the cotangent Laplacian and M is the Voronoi mass matrix.
|
||||
Uses implicit Laplacian smoothing (backward Euler) to compute a stable
|
||||
smooth reference, then amplifies the detail:
|
||||
V_smooth = solve (M - dt*L) V_smooth = M @ V (repeated)
|
||||
V_sharp = V + weight * (V - V_smooth)
|
||||
"""
|
||||
try:
|
||||
import igl
|
||||
except ImportError:
|
||||
return None, "libigl is not installed. Install with: pip install libigl"
|
||||
from scipy.sparse.linalg import spsolve
|
||||
|
||||
V = np.asarray(mesh.vertices, dtype=np.float64)
|
||||
F = np.asarray(mesh.faces, dtype=np.int64)
|
||||
@@ -31,19 +34,21 @@ def _libigl_unsharp_sharpen(mesh, lambda_, iterations):
|
||||
L = igl.cotmatrix(V, F)
|
||||
M = igl.massmatrix(V, F, igl.MASSMATRIX_TYPE_VORONOI)
|
||||
|
||||
# M is diagonal -- invert via element-wise reciprocal
|
||||
M_diag = M.diagonal().copy()
|
||||
M_diag[M_diag == 0] = 1e-12
|
||||
M_inv = 1.0 / M_diag
|
||||
|
||||
# Implicit smoothing: solve (M - dt*L) V_smooth = M @ V
|
||||
# dt=1.0 gives strong smoothing per iteration; always stable.
|
||||
A = M - L
|
||||
V_smooth = V.copy()
|
||||
for _ in range(iterations):
|
||||
# L @ V gives curvature-weighted displacement (n, 3)
|
||||
LV = L @ V
|
||||
delta = M_inv[:, None] * LV
|
||||
V = V + lambda_ * delta
|
||||
for dim in range(3):
|
||||
rhs = M @ V_smooth[:, dim]
|
||||
V_smooth[:, dim] = spsolve(A, rhs)
|
||||
|
||||
# Unsharp mask: amplify the detail signal
|
||||
detail = V - V_smooth
|
||||
V_sharp = V + weight * detail
|
||||
|
||||
result = trimesh_module.Trimesh(
|
||||
vertices=V,
|
||||
vertices=V_sharp,
|
||||
faces=np.asarray(mesh.faces, dtype=np.int32),
|
||||
process=False,
|
||||
)
|
||||
@@ -63,15 +68,14 @@ class SharpenLibiglUnsharpNode(io.ComfyNode):
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
io.Custom("TRIMESH").Input("trimesh"),
|
||||
io.Float.Input("lambda_", default=0.5, min=0.01, max=5.0, step=0.01, tooltip=(
|
||||
"Unsharp mask strength. Controls how much the cotangent "
|
||||
"Laplacian displacement is amplified. Higher values produce "
|
||||
"stronger sharpening but may cause overshooting. "
|
||||
"Start with 0.3-0.5."
|
||||
io.Float.Input("weight", default=0.5, min=0.01, max=5.0, step=0.01, tooltip=(
|
||||
"How much detail to add back. 0.5 = subtle sharpening, "
|
||||
"1.0 = double the detail, 2.0+ = aggressive."
|
||||
)),
|
||||
io.Int.Input("iterations", default=3, min=1, max=50, step=1, tooltip=(
|
||||
"Number of unsharp mask passes. Multiple light passes "
|
||||
"are more stable than a single heavy pass."
|
||||
"Smoothing iterations for the reference mesh. "
|
||||
"More iterations = smoother reference = sharpens broader features. "
|
||||
"Fewer iterations = sharpens fine detail."
|
||||
)),
|
||||
],
|
||||
outputs=[
|
||||
@@ -81,16 +85,16 @@ class SharpenLibiglUnsharpNode(io.ComfyNode):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, trimesh, lambda_=0.5, iterations=3):
|
||||
def execute(cls, trimesh, weight=0.5, iterations=3):
|
||||
log.info("Backend: libigl_unsharp")
|
||||
log.info("Input: %d vertices, %d faces", len(trimesh.vertices), len(trimesh.faces))
|
||||
log.info("Parameters: lambda=%.3f, iterations=%d", lambda_, iterations)
|
||||
log.info("Parameters: weight=%.3f, iterations=%d", weight, iterations)
|
||||
|
||||
initial_vertices = len(trimesh.vertices)
|
||||
initial_faces = len(trimesh.faces)
|
||||
|
||||
sharpened, error = _libigl_unsharp_sharpen(
|
||||
trimesh, lambda_, iterations,
|
||||
trimesh, weight, iterations,
|
||||
)
|
||||
|
||||
if sharpened is None:
|
||||
@@ -117,7 +121,7 @@ class SharpenLibiglUnsharpNode(io.ComfyNode):
|
||||
log.info("Avg vertex displacement: %.6f, max: %.6f", avg_disp, max_disp)
|
||||
|
||||
param_text = (
|
||||
f"Lambda: {lambda_}\n"
|
||||
f"Weight: {weight}\n"
|
||||
f"Iterations: {iterations}"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright (C) 2025 ComfyUI-GeometryPack Contributors
|
||||
|
||||
"""Variational Shape Approximation face clustering + vertex snapping backend node."""
|
||||
|
||||
import logging
|
||||
import numpy as np
|
||||
import trimesh as trimesh_module
|
||||
from comfy_api.latest import io
|
||||
from ._helpers import _compute_face_geometry
|
||||
|
||||
log = logging.getLogger("geometrypack")
|
||||
|
||||
|
||||
def _vsa_snap_sharpen(mesh, num_proxies, lloyd_iterations, snap_strength):
|
||||
"""Variational Shape Approximation clustering with vertex snapping.
|
||||
|
||||
Clusters mesh faces into proxy plane groups via Lloyd iteration (using
|
||||
area-weighted normal distance as the L2,1 metric), then snaps vertices:
|
||||
- Interior vertices (one proxy): project onto proxy plane
|
||||
- Edge vertices (two proxies): snap to plane intersection line
|
||||
- Corner vertices (3+ proxies): snap to plane intersection point
|
||||
"""
|
||||
V = np.asarray(mesh.vertices, dtype=np.float64)
|
||||
F = np.asarray(mesh.faces, dtype=np.int64)
|
||||
|
||||
if len(V) == 0 or len(F) == 0:
|
||||
return None, "Empty mesh (no vertices or faces)."
|
||||
|
||||
m = len(F)
|
||||
k = min(num_proxies, m)
|
||||
|
||||
normals, centroids, areas = _compute_face_geometry(V, F)
|
||||
|
||||
# --- Step 1: farthest-point seeding on area-weighted normal space ---
|
||||
seeds = [0]
|
||||
min_dists = np.full(m, np.inf)
|
||||
|
||||
for _ in range(k - 1):
|
||||
last = seeds[-1]
|
||||
d = areas * np.linalg.norm(normals - normals[last], axis=1)
|
||||
min_dists = np.minimum(min_dists, d)
|
||||
seeds.append(int(np.argmax(min_dists)))
|
||||
|
||||
proxy_normals = normals[seeds].copy()
|
||||
proxy_centroids = centroids[seeds].copy()
|
||||
|
||||
# --- Step 2: Lloyd iteration ---
|
||||
labels = np.zeros(m, dtype=np.int32)
|
||||
|
||||
for _ in range(lloyd_iterations):
|
||||
# Assign each face to nearest proxy (L2,1 metric)
|
||||
# (m, k) distance matrix
|
||||
normal_diffs = np.linalg.norm(
|
||||
normals[:, None, :] - proxy_normals[None, :, :], axis=2
|
||||
)
|
||||
weighted_diffs = areas[:, None] * normal_diffs
|
||||
labels = np.argmin(weighted_diffs, axis=1).astype(np.int32)
|
||||
|
||||
# Refit proxies
|
||||
for p in range(k):
|
||||
mask = labels == p
|
||||
if not np.any(mask):
|
||||
# Empty cluster -- re-seed from farthest face
|
||||
assigned_dists = weighted_diffs[np.arange(m), labels]
|
||||
worst = int(np.argmax(assigned_dists))
|
||||
proxy_normals[p] = normals[worst]
|
||||
proxy_centroids[p] = centroids[worst]
|
||||
continue
|
||||
a = areas[mask]
|
||||
total_a = np.sum(a) + 1e-12
|
||||
avg_n = np.sum(normals[mask] * a[:, None], axis=0) / total_a
|
||||
norm_len = np.linalg.norm(avg_n)
|
||||
if norm_len > 1e-12:
|
||||
avg_n /= norm_len
|
||||
proxy_normals[p] = avg_n
|
||||
proxy_centroids[p] = np.sum(centroids[mask] * a[:, None], axis=0) / total_a
|
||||
|
||||
# --- Step 3: snap vertices ---
|
||||
# Build vertex-to-face adjacency
|
||||
vertex_faces = [[] for _ in range(len(V))]
|
||||
for fi in range(m):
|
||||
for vi in F[fi]:
|
||||
vertex_faces[vi].append(fi)
|
||||
|
||||
V_snapped = V.copy()
|
||||
|
||||
for vi in range(len(V)):
|
||||
adj_fi = vertex_faces[vi]
|
||||
if not adj_fi:
|
||||
continue
|
||||
|
||||
adj_labels = list(set(labels[fi] for fi in adj_fi))
|
||||
|
||||
if len(adj_labels) == 1:
|
||||
# Interior: project onto single proxy plane
|
||||
p = adj_labels[0]
|
||||
n = proxy_normals[p]
|
||||
c = proxy_centroids[p]
|
||||
d = np.dot(V[vi] - c, n)
|
||||
V_snapped[vi] = V[vi] - snap_strength * d * n
|
||||
|
||||
elif len(adj_labels) == 2:
|
||||
# Edge: snap to intersection of two planes
|
||||
p1, p2 = adj_labels
|
||||
n1, c1 = proxy_normals[p1], proxy_centroids[p1]
|
||||
n2, c2 = proxy_normals[p2], proxy_centroids[p2]
|
||||
|
||||
line_dir = np.cross(n1, n2)
|
||||
line_dir_norm = np.linalg.norm(line_dir)
|
||||
|
||||
if line_dir_norm < 1e-8:
|
||||
# Nearly parallel planes -- project onto average
|
||||
avg_n = (n1 + n2)
|
||||
avg_n_norm = np.linalg.norm(avg_n)
|
||||
if avg_n_norm > 1e-12:
|
||||
avg_n /= avg_n_norm
|
||||
avg_c = (c1 + c2) / 2.0
|
||||
d = np.dot(V[vi] - avg_c, avg_n)
|
||||
V_snapped[vi] = V[vi] - snap_strength * d * avg_n
|
||||
else:
|
||||
line_dir /= line_dir_norm
|
||||
# Find a point on the intersection line
|
||||
A = np.array([n1, n2])
|
||||
b = np.array([np.dot(n1, c1), np.dot(n2, c2)])
|
||||
try:
|
||||
AAT = A @ A.T
|
||||
p0 = A.T @ np.linalg.solve(AAT, b)
|
||||
except np.linalg.LinAlgError:
|
||||
continue
|
||||
# Project vertex onto the line
|
||||
v_rel = V[vi] - p0
|
||||
t = np.dot(v_rel, line_dir)
|
||||
closest = p0 + t * line_dir
|
||||
V_snapped[vi] = V[vi] + snap_strength * (closest - V[vi])
|
||||
|
||||
else:
|
||||
# Corner: snap to intersection of 3+ planes
|
||||
A = np.array([proxy_normals[p] for p in adj_labels])
|
||||
b = np.array([np.dot(proxy_normals[p], proxy_centroids[p])
|
||||
for p in adj_labels])
|
||||
try:
|
||||
if len(adj_labels) == 3:
|
||||
corner = np.linalg.solve(A, b)
|
||||
else:
|
||||
corner, _, _, _ = np.linalg.lstsq(A, b, rcond=None)
|
||||
V_snapped[vi] = V[vi] + snap_strength * (corner - V[vi])
|
||||
except np.linalg.LinAlgError:
|
||||
continue
|
||||
|
||||
result = trimesh_module.Trimesh(
|
||||
vertices=V_snapped,
|
||||
faces=np.asarray(mesh.faces, dtype=np.int32),
|
||||
process=False,
|
||||
)
|
||||
return result, ""
|
||||
|
||||
|
||||
class SharpenVSASnapNode(io.ComfyNode):
|
||||
"""Variational Shape Approximation face clustering + vertex snapping backend."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="GeomPackSharpen_VSASnap",
|
||||
display_name="Sharpen VSA Snap (backend)",
|
||||
category="geompack/smoothing",
|
||||
is_dev_only=True,
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
io.Custom("TRIMESH").Input("trimesh"),
|
||||
io.Int.Input("num_proxies", default=20, min=2, max=500, step=1, tooltip=(
|
||||
"Number of proxy planes (face clusters). More proxies "
|
||||
"preserve more detail. Fewer proxies create a more "
|
||||
"abstract/simplified appearance. 10-50 for simple shapes, "
|
||||
"50-200 for complex shapes."
|
||||
)),
|
||||
io.Int.Input("lloyd_iterations", default=10, min=1, max=100, step=1, tooltip=(
|
||||
"Number of Lloyd clustering iterations. 10 is usually "
|
||||
"sufficient for convergence."
|
||||
)),
|
||||
io.Float.Input("snap_strength", default=1.0, min=0.0, max=1.0, step=0.01, tooltip=(
|
||||
"How aggressively to snap vertices to proxy planes. "
|
||||
"0.0 = no change, 1.0 = full snap. "
|
||||
"Values like 0.5-0.8 give a subtle sharpening effect."
|
||||
)),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("TRIMESH").Output(display_name="sharpened_mesh"),
|
||||
io.String.Output(display_name="info"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, trimesh, num_proxies=20, lloyd_iterations=10, snap_strength=1.0):
|
||||
log.info("Backend: vsa_snap")
|
||||
log.info("Input: %d vertices, %d faces", len(trimesh.vertices), len(trimesh.faces))
|
||||
log.info("Parameters: num_proxies=%d, lloyd_iter=%d, snap_strength=%.2f",
|
||||
num_proxies, lloyd_iterations, snap_strength)
|
||||
|
||||
initial_vertices = len(trimesh.vertices)
|
||||
initial_faces = len(trimesh.faces)
|
||||
|
||||
sharpened, error = _vsa_snap_sharpen(
|
||||
trimesh, num_proxies, lloyd_iterations, snap_strength,
|
||||
)
|
||||
|
||||
if sharpened is None:
|
||||
raise ValueError(f"Sharpening failed (vsa_snap): {error}")
|
||||
|
||||
# Copy metadata
|
||||
if hasattr(trimesh, "metadata") and trimesh.metadata:
|
||||
sharpened.metadata = trimesh.metadata.copy()
|
||||
sharpened.metadata["sharpening"] = {
|
||||
"algorithm": "vsa_snap",
|
||||
"original_vertices": initial_vertices,
|
||||
"original_faces": initial_faces,
|
||||
}
|
||||
|
||||
# Compute displacement stats
|
||||
disp = np.linalg.norm(
|
||||
np.asarray(sharpened.vertices) - np.asarray(trimesh.vertices), axis=1
|
||||
)
|
||||
avg_disp = float(np.mean(disp))
|
||||
max_disp = float(np.max(disp))
|
||||
|
||||
log.info("Output: %d vertices, %d faces",
|
||||
len(sharpened.vertices), len(sharpened.faces))
|
||||
log.info("Avg vertex displacement: %.6f, max: %.6f", avg_disp, max_disp)
|
||||
|
||||
param_text = (
|
||||
f"Num Proxies: {num_proxies}\n"
|
||||
f"Lloyd Iterations: {lloyd_iterations}\n"
|
||||
f"Snap Strength: {snap_strength}"
|
||||
)
|
||||
|
||||
info = f"""Sharpen Mesh Results (vsa_snap):
|
||||
|
||||
{param_text}
|
||||
|
||||
Vertices: {initial_vertices:,} (unchanged)
|
||||
Faces: {initial_faces:,} (unchanged)
|
||||
|
||||
Displacement:
|
||||
Average: {avg_disp:.6f}
|
||||
Maximum: {max_disp:.6f}
|
||||
"""
|
||||
return io.NodeOutput(sharpened, info, ui={"text": [info]})
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"GeomPackSharpen_VSASnap": SharpenVSASnapNode}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"GeomPackSharpen_VSASnap": "Sharpen VSA Snap (backend)"}
|
||||
@@ -71,15 +71,33 @@ def export_mesh_with_scalars_vtp(trimesh: trimesh_module.Trimesh, filepath: str)
|
||||
# PointData section (scalar fields)
|
||||
point_data = ET.SubElement(piece, 'PointData')
|
||||
|
||||
# Add vertex normals if available (built-in trimesh property, not in vertex_attributes)
|
||||
if not is_pc and hasattr(trimesh, 'vertex_normals') and len(trimesh.vertex_normals) > 0:
|
||||
try:
|
||||
normals = np.asarray(trimesh.vertex_normals, dtype=np.float32)
|
||||
if normals.shape == (num_verts, 3):
|
||||
log.info("Adding normals field (%d vertices)", num_verts)
|
||||
normals_array = ET.SubElement(point_data, 'DataArray',
|
||||
type='Float32',
|
||||
Name='normals',
|
||||
NumberOfComponents='3',
|
||||
format='ascii')
|
||||
normals_array.text = ' '.join(map(str, normals.flatten()))
|
||||
except Exception as e:
|
||||
log.warning("Could not export normals: %s", e)
|
||||
|
||||
# Add vertex attributes as scalar arrays
|
||||
if hasattr(trimesh, 'vertex_attributes') and trimesh.vertex_attributes:
|
||||
for attr_name, attr_values in trimesh.vertex_attributes.items():
|
||||
log.info("Adding scalar field: %s", attr_name)
|
||||
attr_arr = np.asarray(attr_values)
|
||||
num_components = attr_arr.shape[1] if attr_arr.ndim > 1 else 1
|
||||
log.info("Adding scalar field: %s (components: %d)", attr_name, num_components)
|
||||
scalar_array = ET.SubElement(point_data, 'DataArray',
|
||||
type='Float32',
|
||||
Name=attr_name,
|
||||
NumberOfComponents=str(num_components),
|
||||
format='ascii')
|
||||
scalar_array.text = ' '.join(map(str, attr_values.flatten()))
|
||||
scalar_array.text = ' '.join(map(str, attr_arr.flatten()))
|
||||
|
||||
# CellData section (face attributes) - only for meshes with faces
|
||||
if not is_pc:
|
||||
|
||||
@@ -147,10 +147,8 @@ class PreviewMeshMultiNode(io.ComfyNode):
|
||||
# Export mesh
|
||||
if mode == "texture":
|
||||
filename = f"preview_multi_{i+1}_{preview_id}.glb"
|
||||
elif mesh_has_fields or mesh_is_pc:
|
||||
filename = f"preview_multi_{i+1}_{preview_id}.vtp"
|
||||
else:
|
||||
filename = f"preview_multi_{i+1}_{preview_id}.stl"
|
||||
filename = f"preview_multi_{i+1}_{preview_id}.vtp"
|
||||
|
||||
if COMFYUI_OUTPUT_FOLDER:
|
||||
filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename)
|
||||
@@ -161,12 +159,9 @@ class PreviewMeshMultiNode(io.ComfyNode):
|
||||
if mode == "texture":
|
||||
mesh.export(filepath, file_type='glb', include_normals=True)
|
||||
log.info("Exported GLB: %s", filepath)
|
||||
elif mesh_has_fields or mesh_is_pc:
|
||||
else:
|
||||
export_mesh_with_scalars_vtp(mesh, filepath)
|
||||
log.info("Exported VTP: %s", filepath)
|
||||
else:
|
||||
mesh.export(filepath, file_type='stl')
|
||||
log.info("Exported STL: %s", filepath)
|
||||
except Exception as e:
|
||||
log.error("Export failed: %s, trying OBJ fallback", e)
|
||||
filename = f"preview_multi_{i+1}_{preview_id}.obj"
|
||||
|
||||
@@ -129,16 +129,9 @@ class PreviewMeshVTKNode(io.ComfyNode):
|
||||
viewer_type = "texture"
|
||||
log.info("Using texture mode - GLB export")
|
||||
else:
|
||||
# Fields mode: Export VTP/STL for scalar field visualization
|
||||
if has_fields or is_pc:
|
||||
# Export to VTP for: scalar fields OR point clouds (STL doesn't support point clouds)
|
||||
filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.vtp"
|
||||
log.info("Using VTP format (fields=%s, point_cloud=%s)", has_fields, is_pc)
|
||||
else:
|
||||
# Export to STL (compact format for simple surface meshes)
|
||||
filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.stl"
|
||||
filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.vtp"
|
||||
viewer_type = "fields"
|
||||
log.info("Using fields mode - VTP/STL export")
|
||||
log.info("Using fields mode - VTP export")
|
||||
|
||||
# Use ComfyUI's output directory
|
||||
if COMFYUI_OUTPUT_FOLDER:
|
||||
@@ -166,14 +159,9 @@ class PreviewMeshVTKNode(io.ComfyNode):
|
||||
# Export GLB for texture/PBR rendering
|
||||
trimesh.export(filepath, file_type='glb', include_normals=True)
|
||||
log.info("Exported GLB to: %s", filepath)
|
||||
elif has_fields or is_pc:
|
||||
# Use VTP exporter for fields or point clouds
|
||||
else:
|
||||
export_mesh_with_scalars_vtp(trimesh, filepath)
|
||||
log.info("Exported VTP to: %s", filepath)
|
||||
else:
|
||||
# Use STL for simple surface meshes
|
||||
trimesh.export(filepath, file_type='stl')
|
||||
log.info("Exported STL to: %s", filepath)
|
||||
except Exception as e:
|
||||
log.error("Export failed: %s", e)
|
||||
# Fallback to OBJ
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Main process deps (prestartup + lightweight shared libs)
|
||||
comfy-env==0.2.38
|
||||
comfy-env==0.2.39
|
||||
comfy-dynamic-widgets==0.1.4
|
||||
comfy-3d-viewers==0.2.30
|
||||
numpy
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"id":"test-boolean-all-0001","revision":0,"last_node_id":6,"last_link_id":8,"nodes":[{"id":2,"type":"GeomPackCreatePrimitive","pos":[-600,100],"size":[270,150],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"shape","name":"shape","type":"COMBO","widget":{"name":"shape"},"link":null},{"localized_name":"size","name":"size","type":"FLOAT","widget":{"name":"size"},"link":null},{"localized_name":"subdivisions","name":"subdivisions","shape":7,"type":"INT","widget":{"name":"subdivisions"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[2,4]}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackCreatePrimitive"},"widgets_values":["cube",0.8,1]},{"id":3,"type":"GeomPackBoolean","pos":[-200,-200],"size":[370,102],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"mesh_a","name":"mesh_a","type":"TRIMESH","link":1},{"localized_name":"mesh_b","name":"mesh_b","type":"TRIMESH","link":2},{"localized_name":"operation","name":"operation","type":"COMBO","widget":{"name":"operation"},"link":null},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"result_mesh","name":"result_mesh","type":"TRIMESH","links":[5]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackBoolean"},"widgets_values":["difference","libigl_cgal"]},{"id":4,"type":"GeomPackBoolean","pos":[-200,100],"size":[370,102],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mesh_a","name":"mesh_a","type":"TRIMESH","link":3},{"localized_name":"mesh_b","name":"mesh_b","type":"TRIMESH","link":4},{"localized_name":"operation","name":"operation","type":"COMBO","widget":{"name":"operation"},"link":null},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"result_mesh","name":"result_mesh","type":"TRIMESH","links":[6]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackBoolean"},"widgets_values":["difference","blender_exact"]},{"id":5,"type":"GeomPackPreviewMeshMulti","pos":[213.07101477171835,-194.17306733063847],"size":[768,702],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":5},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":6},{"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"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"\",\"selected_field\":\"\"}"]},{"id":1,"type":"GeomPackCreatePrimitive","pos":[-600,-200],"size":[270,150],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"shape","name":"shape","type":"COMBO","widget":{"name":"shape"},"link":null},{"localized_name":"size","name":"size","type":"FLOAT","widget":{"name":"size"},"link":null},{"localized_name":"subdivisions","name":"subdivisions","shape":7,"type":"INT","widget":{"name":"subdivisions"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[1,3]}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackCreatePrimitive"},"widgets_values":["sphere",1,4]}],"links":[[1,1,0,3,0,"TRIMESH"],[2,2,0,3,1,"TRIMESH"],[3,1,0,4,0,"TRIMESH"],[4,2,0,4,1,"TRIMESH"],[5,3,0,5,0,"TRIMESH"],[6,4,0,5,1,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.9663060000000003,"offset":[877.1812568381522,409.15273095993916]},"frontendVersion":"1.30.6","workflowRendererVersion":"LG"},"version":0.4}
|
||||
{"id":"test-boolean-all-0001","revision":0,"last_node_id":6,"last_link_id":8,"nodes":[{"id":2,"type":"GeomPackCreatePrimitive","pos":[-600,100],"size":[270,150],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"shape","name":"shape","type":"COMBO","widget":{"name":"shape"},"link":null},{"localized_name":"size","name":"size","type":"FLOAT","widget":{"name":"size"},"link":null},{"localized_name":"subdivisions","name":"subdivisions","shape":7,"type":"INT","widget":{"name":"subdivisions"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[2,4]}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackCreatePrimitive"},"widgets_values":["cube",0.8,1]},{"id":3,"type":"GeomPackBoolean","pos":[-200,-200],"size":[370,102],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"mesh_a","name":"mesh_a","type":"TRIMESH","link":1},{"localized_name":"mesh_b","name":"mesh_b","type":"TRIMESH","link":2},{"localized_name":"operation","name":"operation","type":"COMBO","widget":{"name":"operation"},"link":null},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"result_mesh","name":"result_mesh","type":"TRIMESH","links":[5]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackBoolean"},"widgets_values":["difference","libigl_cgal"]},{"id":4,"type":"GeomPackBoolean","pos":[-200,100],"size":[370,102],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mesh_a","name":"mesh_a","type":"TRIMESH","link":3},{"localized_name":"mesh_b","name":"mesh_b","type":"TRIMESH","link":4},{"localized_name":"operation","name":"operation","type":"COMBO","widget":{"name":"operation"},"link":null},{"localized_name":"backend","name":"backend","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"backend"},"link":null}],"outputs":[{"localized_name":"result_mesh","name":"result_mesh","type":"TRIMESH","links":[6]},{"localized_name":"info","name":"info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackBoolean"},"widgets_values":["difference","blender_exact"]},{"id":5,"type":"GeomPackPreviewMeshMulti","pos":[213.07101477171835,-194.17306733063847],"size":[768,702],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mesh_1","name":"mesh_1","type":"TRIMESH","link":5},{"localized_name":"mesh_2","name":"mesh_2","shape":7,"type":"TRIMESH","link":6},{"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"},"widgets_values":["fields","{\"show_edges\":true,\"camera_state\":\"{\\\"position\\\":[1.2245775204403364,1.2245775204403364,1.2245775204403364],\\\"focalPoint\\\":[0,0,0],\\\"viewUp\\\":[0,0,1]}\",\"selected_field\":\"\"}"]},{"id":1,"type":"GeomPackCreatePrimitive","pos":[-600,-200],"size":[270,150],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"shape","name":"shape","type":"COMBO","widget":{"name":"shape"},"link":null},{"localized_name":"size","name":"size","type":"FLOAT","widget":{"name":"size"},"link":null},{"localized_name":"subdivisions","name":"subdivisions","shape":7,"type":"INT","widget":{"name":"subdivisions"},"link":null}],"outputs":[{"localized_name":"mesh","name":"mesh","type":"TRIMESH","links":[1,3]}],"properties":{"cnr_id":"comfyui-geometrypack","ver":"224d843d5a35546212d1fb7babe30efabdf2116c","Node name for S&R":"GeomPackCreatePrimitive"},"widgets_values":["sphere",1,4]}],"links":[[1,1,0,3,0,"TRIMESH"],[2,2,0,3,1,"TRIMESH"],[3,1,0,4,0,"TRIMESH"],[4,2,0,4,1,"TRIMESH"],[5,3,0,5,0,"TRIMESH"],[6,4,0,5,1,"TRIMESH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8719083213796316,"offset":[536.4213213855968,421.65866536973317]},"frontendVersion":"1.30.6","workflowRendererVersion":"LG"},"version":0.4}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user