Updated docs, dual view node, boolean ops
This commit is contained in:
@@ -4,8 +4,9 @@
|
||||
|
||||
Professional geometry processing nodes for ComfyUI. Load, analyze, remesh, unwrap, and visualize 3D meshes directly in your workflows.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 229 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 263 KiB |
+38
-24
@@ -15,8 +15,8 @@ class BooleanOpNode:
|
||||
- difference: Subtract mesh_b from mesh_a
|
||||
- intersection: Keep only overlapping parts
|
||||
|
||||
Uses trimesh's boolean operations (requires manifold3d backend).
|
||||
Fallback to Blender if manifold3d is not available.
|
||||
Uses libigl with CGAL backend for robust boolean operations.
|
||||
Fallback to Blender if CGAL is not available.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -28,7 +28,7 @@ class BooleanOpNode:
|
||||
"operation": (["union", "difference", "intersection"],),
|
||||
},
|
||||
"optional": {
|
||||
"engine": (["auto", "manifold", "blender"], {"default": "auto"}),
|
||||
"engine": (["auto", "libigl_cgal", "blender"], {"default": "auto"}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class BooleanOpNode:
|
||||
mesh_a: First mesh (base mesh for difference)
|
||||
mesh_b: Second mesh (subtracted mesh for difference)
|
||||
operation: Boolean operation type
|
||||
engine: Backend to use
|
||||
engine: Backend to use (auto, libigl_cgal, blender)
|
||||
|
||||
Returns:
|
||||
tuple: (result_mesh, info_string)
|
||||
@@ -54,13 +54,13 @@ class BooleanOpNode:
|
||||
print(f"[Boolean] Mesh B: {len(mesh_b.vertices)} vertices, {len(mesh_b.faces)} faces")
|
||||
print(f"[Boolean] Operation: {operation}, Engine: {engine}")
|
||||
|
||||
# Try manifold backend first
|
||||
if engine in ["auto", "manifold"]:
|
||||
result, info = self._try_manifold(mesh_a, mesh_b, operation)
|
||||
# Try libigl+CGAL backend first
|
||||
if engine in ["auto", "libigl_cgal"]:
|
||||
result, info = self._try_libigl_cgal(mesh_a, mesh_b, operation)
|
||||
if result is not None:
|
||||
return (result, info)
|
||||
if engine == "manifold":
|
||||
raise RuntimeError("Manifold backend failed and was explicitly requested")
|
||||
if engine == "libigl_cgal":
|
||||
raise RuntimeError("libigl+CGAL backend failed and was explicitly requested")
|
||||
|
||||
# Fallback to Blender
|
||||
if engine in ["auto", "blender"]:
|
||||
@@ -70,26 +70,40 @@ class BooleanOpNode:
|
||||
|
||||
raise RuntimeError(f"Boolean operation failed with all available backends")
|
||||
|
||||
def _try_manifold(self, mesh_a, mesh_b, operation):
|
||||
"""Try boolean operation using manifold3d (via trimesh)."""
|
||||
def _try_libigl_cgal(self, mesh_a, mesh_b, operation):
|
||||
"""Try boolean operation using libigl with CGAL backend."""
|
||||
try:
|
||||
print(f"[Boolean] Attempting manifold backend...")
|
||||
import igl.copyleft.cgal as cgal
|
||||
print(f"[Boolean] Attempting libigl+CGAL backend...")
|
||||
|
||||
# trimesh uses manifold3d for boolean operations
|
||||
if operation == "union":
|
||||
result = mesh_a.union(mesh_b, engine="manifold")
|
||||
elif operation == "difference":
|
||||
result = mesh_a.difference(mesh_b, engine="manifold")
|
||||
elif operation == "intersection":
|
||||
result = mesh_a.intersection(mesh_b, engine="manifold")
|
||||
else:
|
||||
# Convert trimesh to numpy arrays
|
||||
VA = np.asarray(mesh_a.vertices, dtype=np.float64)
|
||||
FA = np.asarray(mesh_a.faces, dtype=np.int64)
|
||||
VB = np.asarray(mesh_b.vertices, dtype=np.float64)
|
||||
FB = np.asarray(mesh_b.faces, dtype=np.int64)
|
||||
|
||||
# Map operation to igl type_str
|
||||
# mesh_boolean accepts string: "union", "intersection", "difference"
|
||||
op_map = {
|
||||
"union": "union",
|
||||
"difference": "difference",
|
||||
"intersection": "intersection"
|
||||
}
|
||||
|
||||
if operation not in op_map:
|
||||
raise ValueError(f"Unknown operation: {operation}")
|
||||
|
||||
# Perform boolean operation using CGAL
|
||||
VC, FC, J = cgal.mesh_boolean(VA, FA, VB, FB, op_map[operation])
|
||||
|
||||
# Create result trimesh
|
||||
result = trimesh_module.Trimesh(vertices=VC, faces=FC, process=False)
|
||||
|
||||
# Preserve metadata from mesh_a
|
||||
result.metadata = mesh_a.metadata.copy()
|
||||
result.metadata['boolean'] = {
|
||||
'operation': operation,
|
||||
'engine': 'manifold',
|
||||
'engine': 'libigl_cgal',
|
||||
'mesh_a_vertices': len(mesh_a.vertices),
|
||||
'mesh_a_faces': len(mesh_a.faces),
|
||||
'mesh_b_vertices': len(mesh_b.vertices),
|
||||
@@ -101,7 +115,7 @@ class BooleanOpNode:
|
||||
info = f"""Boolean Operation Results:
|
||||
|
||||
Operation: {operation.upper()}
|
||||
Engine: manifold
|
||||
Engine: libigl + CGAL
|
||||
|
||||
Mesh A:
|
||||
Vertices: {len(mesh_a.vertices):,}
|
||||
@@ -118,11 +132,11 @@ Result:
|
||||
Watertight: {result.is_watertight}
|
||||
"""
|
||||
|
||||
print(f"[Boolean] Manifold success: {len(result.vertices)} vertices, {len(result.faces)} faces")
|
||||
print(f"[Boolean] libigl+CGAL success: {len(result.vertices)} vertices, {len(result.faces)} faces")
|
||||
return result, info
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Boolean] Manifold backend failed: {e}")
|
||||
print(f"[Boolean] libigl+CGAL backend failed: {e}")
|
||||
return None, str(e)
|
||||
|
||||
def _try_blender(self, mesh_a, mesh_b, operation):
|
||||
|
||||
@@ -88,6 +88,7 @@ class BlenderRemeshWithTexture:
|
||||
return {
|
||||
"required": {
|
||||
"trimesh": ("TRIMESH",),
|
||||
"method": (["blender", "xatlas"], {"default": "blender"}),
|
||||
"remesh_method": (["voxel", "quadriflow"], {"default": "quadriflow"}),
|
||||
"voxel_size": ("FLOAT", {
|
||||
"default": 0.05,
|
||||
@@ -116,9 +117,16 @@ class BlenderRemeshWithTexture:
|
||||
FUNCTION = "remesh_with_texture"
|
||||
CATEGORY = "geompack/texture_remeshing"
|
||||
|
||||
def remesh_with_texture(self, trimesh, remesh_method, voxel_size, target_face_count,
|
||||
def remesh_with_texture(self, trimesh, method, remesh_method, voxel_size, target_face_count,
|
||||
texture_size, bake_margin):
|
||||
"""Remesh a textured mesh while preserving texture through Blender baking."""
|
||||
# Check if xatlas method is requested
|
||||
if method == "xatlas":
|
||||
raise NotImplementedError(
|
||||
"XAtlas remeshing method is not yet implemented. "
|
||||
"Please use 'blender' method or help contribute an implementation!"
|
||||
)
|
||||
|
||||
if not PIL_AVAILABLE:
|
||||
raise RuntimeError("PIL required. Install: pip install Pillow")
|
||||
|
||||
@@ -283,9 +291,11 @@ Texture size: {texture_size}x{texture_size}
|
||||
|
||||
# Node mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackRemeshUV": BlenderRemeshWithTexture,
|
||||
"GeomPackBlenderRemeshWithTexture": BlenderRemeshWithTexture,
|
||||
"GeomPackXAtlasRemeshWithTexture": BlenderRemeshWithTexture,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackRemeshUV": "Remesh UV",
|
||||
"GeomPackBlenderRemeshWithTexture": "Remesh with Texture",
|
||||
"GeomPackXAtlasRemeshWithTexture": "Remesh with Texture",
|
||||
}
|
||||
|
||||
+3
-10
@@ -1,20 +1,13 @@
|
||||
"""
|
||||
UV nodes aggregation.
|
||||
|
||||
The unified UV Unwrap node consolidates all UV unwrapping methods into a single node.
|
||||
Old separate nodes are kept in the codebase for reference but are no longer registered.
|
||||
The unified UV Unwrap node consolidates all UV unwrapping methods into a single node
|
||||
with dynamic parameter exposure based on the selected method.
|
||||
"""
|
||||
|
||||
# New unified UV unwrap node
|
||||
# Unified UV unwrap node
|
||||
from .uv_unwrap import UVUnwrapNode, NODE_CLASS_MAPPINGS as UV_UNWRAP_MAPS, NODE_DISPLAY_NAME_MAPPINGS as UV_UNWRAP_DISP
|
||||
|
||||
# Old separate nodes (kept for reference, not registered)
|
||||
# from .xatlas_unwrap import XAtlasUVUnwrapNode, NODE_CLASS_MAPPINGS as XATLAS_UNWRAP_MAPS, NODE_DISPLAY_NAME_MAPPINGS as XATLAS_UNWRAP_DISP
|
||||
# from .libigl_lscm import LibiglLSCMNode, NODE_CLASS_MAPPINGS as LIBIGL_LSCM_MAPS, NODE_DISPLAY_NAME_MAPPINGS as LIBIGL_LSCM_DISP
|
||||
# from .libigl_harmonic import LibiglHarmonicNode, NODE_CLASS_MAPPINGS as LIBIGL_HARMONIC_MAPS, NODE_DISPLAY_NAME_MAPPINGS as LIBIGL_HARMONIC_DISP
|
||||
# from .libigl_arap import LibiglARAPNode, NODE_CLASS_MAPPINGS as LIBIGL_ARAP_MAPS, NODE_DISPLAY_NAME_MAPPINGS as LIBIGL_ARAP_DISP
|
||||
# from .blender_uv import BlenderUVNode, NODE_CLASS_MAPPINGS as BLENDER_UV_MAPS, NODE_DISPLAY_NAME_MAPPINGS as BLENDER_UV_DISP
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
"""
|
||||
Unified Blender UV - Multiple UV projection methods via Blender.
|
||||
|
||||
Supports:
|
||||
- smart_uv_project: Automatic seam-based unwrapping
|
||||
- cube_projection: Project onto 6 cube faces
|
||||
- cylinder_projection: Cylindrical projection
|
||||
- sphere_projection: Spherical/equirectangular projection
|
||||
|
||||
Parameters are method-specific; unused ones are ignored.
|
||||
"""
|
||||
|
||||
import trimesh as trimesh_module
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import math
|
||||
|
||||
from .._utils import blender_bridge
|
||||
|
||||
|
||||
class BlenderUVNode:
|
||||
"""
|
||||
Unified Blender UV - Multiple UV projection methods via Blender.
|
||||
|
||||
Supports:
|
||||
- smart_uv_project: Automatic seam-based unwrapping
|
||||
- cube_projection: Project onto 6 cube faces
|
||||
- cylinder_projection: Cylindrical projection
|
||||
- sphere_projection: Spherical/equirectangular projection
|
||||
|
||||
Parameters are method-specific; unused ones are ignored.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"trimesh": ("TRIMESH",),
|
||||
"method": ([
|
||||
"smart_uv_project",
|
||||
"cube_projection",
|
||||
"cylinder_projection",
|
||||
"sphere_projection"
|
||||
], {"default": "smart_uv_project"}),
|
||||
},
|
||||
"optional": {
|
||||
# Smart UV Project params
|
||||
"angle_limit": ("FLOAT", {
|
||||
"default": 66.0,
|
||||
"min": 1.0,
|
||||
"max": 89.0,
|
||||
"step": 1.0
|
||||
}),
|
||||
"island_margin": ("FLOAT", {
|
||||
"default": 0.02,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
}),
|
||||
# Projection params
|
||||
"scale_to_bounds": (["true", "false"], {"default": "true"}),
|
||||
"cube_size": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.1,
|
||||
"max": 10.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"cylinder_radius": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.1,
|
||||
"max": 10.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("TRIMESH", "STRING")
|
||||
RETURN_NAMES = ("unwrapped_mesh", "info")
|
||||
FUNCTION = "uv_project"
|
||||
CATEGORY = "geompack/uv"
|
||||
|
||||
def uv_project(self, trimesh, method,
|
||||
angle_limit=66.0, island_margin=0.02,
|
||||
scale_to_bounds="true", cube_size=1.0, cylinder_radius=1.0):
|
||||
"""
|
||||
Apply UV projection using Blender.
|
||||
|
||||
Args:
|
||||
trimesh: Input mesh
|
||||
method: UV projection method
|
||||
[other params]: Method-specific parameters
|
||||
|
||||
Returns:
|
||||
tuple: (unwrapped_mesh, info_string)
|
||||
"""
|
||||
print(f"[BlenderUV] Input: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
||||
print(f"[BlenderUV] Method: {method}")
|
||||
|
||||
# Find Blender
|
||||
blender_path = blender_bridge.find_blender()
|
||||
|
||||
# Create temp files
|
||||
with tempfile.NamedTemporaryFile(suffix='.obj', delete=False) as f_in:
|
||||
input_path = f_in.name
|
||||
trimesh.export(input_path)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.obj', delete=False) as f_out:
|
||||
output_path = f_out.name
|
||||
|
||||
try:
|
||||
# Build method-specific script
|
||||
if method == "smart_uv_project":
|
||||
angle_limit_rad = math.radians(angle_limit)
|
||||
uv_script = f"""
|
||||
bpy.ops.uv.smart_project(
|
||||
angle_limit={angle_limit_rad},
|
||||
island_margin={island_margin},
|
||||
area_weight=0.0,
|
||||
correct_aspect=True,
|
||||
scale_to_bounds={'True' if scale_to_bounds == 'true' else 'False'}
|
||||
)
|
||||
"""
|
||||
info_params = f"Angle Limit: {angle_limit}°\nIsland Margin: {island_margin}"
|
||||
|
||||
elif method == "cube_projection":
|
||||
uv_script = f"""
|
||||
bpy.ops.uv.cube_project(
|
||||
cube_size={cube_size},
|
||||
scale_to_bounds={'True' if scale_to_bounds == 'true' else 'False'}
|
||||
)
|
||||
"""
|
||||
info_params = f"Cube Size: {cube_size}"
|
||||
|
||||
elif method == "cylinder_projection":
|
||||
uv_script = f"""
|
||||
bpy.ops.uv.cylinder_project(
|
||||
radius={cylinder_radius},
|
||||
scale_to_bounds={'True' if scale_to_bounds == 'true' else 'False'}
|
||||
)
|
||||
"""
|
||||
info_params = f"Cylinder Radius: {cylinder_radius}"
|
||||
|
||||
elif method == "sphere_projection":
|
||||
uv_script = f"""
|
||||
bpy.ops.uv.sphere_project(
|
||||
scale_to_bounds={'True' if scale_to_bounds == 'true' else 'False'}
|
||||
)
|
||||
"""
|
||||
info_params = "Equirectangular projection"
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
# Complete Blender script
|
||||
script = f"""
|
||||
import bpy
|
||||
|
||||
# Clear scene
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete()
|
||||
|
||||
# Import mesh
|
||||
bpy.ops.wm.obj_import(filepath='{input_path}')
|
||||
|
||||
# Get imported object
|
||||
obj = bpy.context.selected_objects[0]
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
|
||||
# Switch to edit mode and unwrap
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
{uv_script}
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Export with UVs
|
||||
bpy.ops.wm.obj_export(
|
||||
filepath='{output_path}',
|
||||
export_selected_objects=True,
|
||||
export_uv=True,
|
||||
export_materials=False
|
||||
)
|
||||
"""
|
||||
|
||||
print(f"[BlenderUV] Running Blender in background mode...")
|
||||
result = subprocess.run(
|
||||
[blender_path, '--background', '--python-expr', script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Blender failed: {result.stderr}")
|
||||
|
||||
# Load the unwrapped mesh
|
||||
unwrapped = trimesh_module.load(output_path, process=False)
|
||||
|
||||
if isinstance(unwrapped, trimesh_module.Scene):
|
||||
unwrapped = unwrapped.dump(concatenate=True)
|
||||
|
||||
# Preserve metadata
|
||||
unwrapped.metadata = trimesh.metadata.copy()
|
||||
unwrapped.metadata['uv_unwrap'] = {
|
||||
'algorithm': f'blender_{method}',
|
||||
'method': method
|
||||
}
|
||||
|
||||
info = f"""Blender UV Results:
|
||||
|
||||
Method: {method.replace('_', ' ').title()}
|
||||
{info_params}
|
||||
|
||||
Input: {len(trimesh.vertices):,} vertices, {len(trimesh.faces):,} faces
|
||||
Output: {len(unwrapped.vertices):,} vertices, {len(unwrapped.faces):,} faces
|
||||
|
||||
UV coordinates applied successfully.
|
||||
"""
|
||||
|
||||
print(f"[BlenderUV] Complete: {len(unwrapped.vertices)} vertices, {len(unwrapped.faces)} faces")
|
||||
return (unwrapped, info)
|
||||
|
||||
finally:
|
||||
# Cleanup temp files
|
||||
if os.path.exists(input_path):
|
||||
os.unlink(input_path)
|
||||
if os.path.exists(output_path):
|
||||
os.unlink(output_path)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackBlenderUV": BlenderUVNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackBlenderUV": "UV Unwrap (Blender)",
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
"""
|
||||
ARAP (As-Rigid-As-Possible) UV Parameterization using libigl.
|
||||
|
||||
Minimizes distortion by making triangles as rigid as possible.
|
||||
Better preservation of shape and angles compared to simpler methods.
|
||||
Iterative solver - slower but higher quality.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import trimesh as trimesh_module
|
||||
|
||||
|
||||
class LibiglARAPNode:
|
||||
"""
|
||||
ARAP (As-Rigid-As-Possible) UV Parameterization using libigl.
|
||||
|
||||
Minimizes distortion by making triangles as rigid as possible.
|
||||
Better preservation of shape and angles compared to simpler methods.
|
||||
Iterative solver - slower but higher quality.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"trimesh": ("TRIMESH",),
|
||||
"iterations": ("INT", {
|
||||
"default": 10,
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("TRIMESH",)
|
||||
RETURN_NAMES = ("unwrapped_mesh",)
|
||||
FUNCTION = "uv_unwrap"
|
||||
CATEGORY = "geompack/uv"
|
||||
|
||||
def uv_unwrap(self, trimesh, iterations):
|
||||
"""
|
||||
ARAP UV parameterization using libigl.
|
||||
|
||||
Args:
|
||||
trimesh: Input trimesh_module.Trimesh object
|
||||
iterations: Number of ARAP iterations
|
||||
|
||||
Returns:
|
||||
tuple: (unwrapped_trimesh_module.Trimesh,)
|
||||
"""
|
||||
try:
|
||||
import igl
|
||||
import scipy.sparse as sp
|
||||
except ImportError:
|
||||
raise ImportError("libigl and scipy not installed")
|
||||
|
||||
print(f"[LibiglARAP] Input: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
||||
print(f"[LibiglARAP] Iterations: {iterations}")
|
||||
|
||||
# Start with harmonic initialization
|
||||
# Convert TrackedArray to pure numpy array for igl compatibility
|
||||
boundary_loop = igl.boundary_loop(np.asarray(trimesh.faces, dtype=np.int32))
|
||||
|
||||
if len(boundary_loop) == 0:
|
||||
raise ValueError("Mesh has no boundary - ARAP parameterization requires an open mesh")
|
||||
|
||||
# Map boundary to circle
|
||||
bnd_angles = np.linspace(0, 2 * np.pi, len(boundary_loop), endpoint=False)
|
||||
bnd_uv = np.column_stack([
|
||||
0.5 + 0.5 * np.cos(bnd_angles),
|
||||
0.5 + 0.5 * np.sin(bnd_angles)
|
||||
])
|
||||
|
||||
# Initial harmonic solution
|
||||
# Convert TrackedArray to pure numpy for igl compatibility
|
||||
uv_init = igl.harmonic(
|
||||
np.asarray(trimesh.vertices, dtype=np.float64),
|
||||
np.asarray(trimesh.faces, dtype=np.int32),
|
||||
boundary_loop.astype(np.int32),
|
||||
bnd_uv.astype(np.float64),
|
||||
1
|
||||
)
|
||||
|
||||
print(f"[LibiglARAP] Initial harmonic solution computed")
|
||||
|
||||
# Apply ARAP
|
||||
# Note: libigl's ARAP might need different setup depending on version
|
||||
# Using a simplified approach with iterations
|
||||
uv = uv_init.copy()
|
||||
|
||||
try:
|
||||
# Try to use igl.arap if available
|
||||
# ARAP needs energy minimization setup
|
||||
# For simplicity, we'll use multiple iterations of harmonic with adjusted weights
|
||||
# This is a simplified ARAP-like approach
|
||||
for i in range(iterations):
|
||||
# Recompute with current UV as guidance
|
||||
# This is a simplified version - full ARAP is more complex
|
||||
uv = igl.harmonic(
|
||||
np.asarray(trimesh.vertices, dtype=np.float64),
|
||||
np.asarray(trimesh.faces, dtype=np.int32),
|
||||
boundary_loop.astype(np.int32),
|
||||
bnd_uv.astype(np.float64),
|
||||
2 # Use biharmonic (k=2) for smoother result
|
||||
)
|
||||
|
||||
print(f"[LibiglARAP] ARAP optimization complete ({iterations} iterations)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[LibiglARAP] Note: Using biharmonic approximation of ARAP")
|
||||
# Fall back to biharmonic
|
||||
uv = igl.harmonic(
|
||||
np.asarray(trimesh.vertices, dtype=np.float64),
|
||||
np.asarray(trimesh.faces, dtype=np.int32),
|
||||
boundary_loop.astype(np.int32),
|
||||
bnd_uv.astype(np.float64),
|
||||
2 # biharmonic
|
||||
)
|
||||
|
||||
# Normalize to [0, 1]
|
||||
uv_min = uv.min(axis=0)
|
||||
uv_max = uv.max(axis=0)
|
||||
uv_range = uv_max - uv_min
|
||||
uv_range[uv_range < 1e-10] = 1.0
|
||||
uv = (uv - uv_min) / uv_range
|
||||
|
||||
# Create unwrapped mesh (copy original)
|
||||
unwrapped = trimesh.copy()
|
||||
|
||||
# Store UV coordinates in visual
|
||||
from trimesh.visual import TextureVisuals
|
||||
unwrapped.visual = TextureVisuals(uv=uv)
|
||||
|
||||
# Add metadata
|
||||
unwrapped.metadata['uv_unwrap'] = {
|
||||
'algorithm': 'libigl_arap_like',
|
||||
'iterations': iterations,
|
||||
'minimizes_distortion': True
|
||||
}
|
||||
|
||||
print(f"[LibiglARAP] Complete - ARAP-like mapping")
|
||||
|
||||
return (unwrapped,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackLibiglARAP": LibiglARAPNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackLibiglARAP": "UV Unwrap (libigl ARAP)",
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
"""
|
||||
Harmonic UV Parameterization using libigl.
|
||||
|
||||
Simple, fast UV unwrapping using harmonic (Laplacian) mapping.
|
||||
Guarantees valid (non-overlapping) UVs with fixed boundary.
|
||||
Less feature-preserving than LSCM or ABF, but very stable.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import trimesh as trimesh_module
|
||||
|
||||
|
||||
class LibiglHarmonicNode:
|
||||
"""
|
||||
Harmonic UV Parameterization using libigl.
|
||||
|
||||
Simple, fast UV unwrapping using harmonic (Laplacian) mapping.
|
||||
Guarantees valid (non-overlapping) UVs with fixed boundary.
|
||||
Less feature-preserving than LSCM or ABF, but very stable.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"trimesh": ("TRIMESH",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("TRIMESH",)
|
||||
RETURN_NAMES = ("unwrapped_mesh",)
|
||||
FUNCTION = "uv_unwrap"
|
||||
CATEGORY = "geompack/uv"
|
||||
|
||||
def uv_unwrap(self, trimesh):
|
||||
"""
|
||||
Harmonic UV parameterization using libigl.
|
||||
|
||||
Args:
|
||||
trimesh: Input trimesh_module.Trimesh object
|
||||
|
||||
Returns:
|
||||
tuple: (unwrapped_trimesh_module.Trimesh,)
|
||||
"""
|
||||
try:
|
||||
import igl
|
||||
except ImportError:
|
||||
raise ImportError("libigl not installed (should be in requirements.txt)")
|
||||
|
||||
print(f"[LibiglHarmonic] Input: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
||||
|
||||
# Harmonic requires fixing boundary vertices
|
||||
# Find boundary loop
|
||||
# Convert TrackedArray to pure numpy array for igl compatibility
|
||||
boundary_loop = igl.boundary_loop(np.asarray(trimesh.faces, dtype=np.int32))
|
||||
|
||||
if len(boundary_loop) == 0:
|
||||
raise ValueError("Mesh has no boundary - harmonic parameterization requires an open mesh")
|
||||
|
||||
# Map boundary to circle/square
|
||||
# Simple circular boundary
|
||||
bnd_angles = np.linspace(0, 2 * np.pi, len(boundary_loop), endpoint=False)
|
||||
bnd_uv = np.column_stack([
|
||||
0.5 + 0.5 * np.cos(bnd_angles),
|
||||
0.5 + 0.5 * np.sin(bnd_angles)
|
||||
])
|
||||
|
||||
# Compute harmonic parameterization
|
||||
# Convert TrackedArray to pure numpy for igl compatibility
|
||||
uv = igl.harmonic(
|
||||
np.asarray(trimesh.vertices, dtype=np.float64),
|
||||
np.asarray(trimesh.faces, dtype=np.int32),
|
||||
boundary_loop.astype(np.int32),
|
||||
bnd_uv.astype(np.float64),
|
||||
1 # Laplacian type
|
||||
)
|
||||
|
||||
# Create unwrapped mesh (copy original)
|
||||
unwrapped = trimesh.copy()
|
||||
|
||||
# Store UV coordinates in visual
|
||||
from trimesh.visual import TextureVisuals
|
||||
unwrapped.visual = TextureVisuals(uv=uv)
|
||||
|
||||
# Add metadata
|
||||
unwrapped.metadata['uv_unwrap'] = {
|
||||
'algorithm': 'libigl_harmonic',
|
||||
'boundary_vertices': len(boundary_loop),
|
||||
'guarantees_valid_uvs': True
|
||||
}
|
||||
|
||||
print(f"[LibiglHarmonic] Complete - simple harmonic mapping with {len(boundary_loop)} boundary vertices")
|
||||
|
||||
return (unwrapped,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackLibiglHarmonic": LibiglHarmonicNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackLibiglHarmonic": "UV Unwrap (libigl Harmonic)",
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
"""
|
||||
LSCM UV Parameterization using libigl.
|
||||
|
||||
Least Squares Conformal Maps - minimizes angle distortion.
|
||||
Fast, conformal mapping suitable for texturing organic shapes.
|
||||
No Blender dependency required.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import trimesh as trimesh_module
|
||||
|
||||
|
||||
class LibiglLSCMNode:
|
||||
"""
|
||||
LSCM UV Parameterization using libigl.
|
||||
|
||||
Least Squares Conformal Maps - minimizes angle distortion.
|
||||
Fast, conformal mapping suitable for texturing organic shapes.
|
||||
No Blender dependency required.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"trimesh": ("TRIMESH",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("TRIMESH",)
|
||||
RETURN_NAMES = ("unwrapped_mesh",)
|
||||
FUNCTION = "uv_unwrap"
|
||||
CATEGORY = "geompack/uv"
|
||||
|
||||
def uv_unwrap(self, trimesh):
|
||||
"""
|
||||
LSCM UV parameterization using libigl.
|
||||
|
||||
Args:
|
||||
trimesh: Input trimesh_module.Trimesh object
|
||||
|
||||
Returns:
|
||||
tuple: (unwrapped_trimesh_module.Trimesh,)
|
||||
"""
|
||||
try:
|
||||
import igl
|
||||
except ImportError:
|
||||
raise ImportError("libigl not installed (should be in requirements.txt)")
|
||||
|
||||
print(f"[LibiglLSCM] Input: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
||||
|
||||
# LSCM requires fixing 2 vertices for unique solution
|
||||
# Choose first and last vertex
|
||||
v_fixed = np.array([0, len(trimesh.vertices)-1], dtype=np.int32)
|
||||
uv_fixed = np.array([[0.0, 0.0], [1.0, 0.0]], dtype=np.float64)
|
||||
|
||||
# Compute LSCM parameterization
|
||||
# Convert TrackedArray to pure numpy array for igl compatibility
|
||||
uv_result = igl.lscm(
|
||||
np.asarray(trimesh.vertices, dtype=np.float64),
|
||||
np.asarray(trimesh.faces, dtype=np.int32),
|
||||
v_fixed,
|
||||
uv_fixed
|
||||
)
|
||||
# igl.lscm returns a tuple (uv_coords, sparse_matrix)
|
||||
uv = uv_result[0] if isinstance(uv_result, tuple) else uv_result
|
||||
|
||||
# Normalize UVs to [0, 1] range
|
||||
uv_min = uv.min(axis=0)
|
||||
uv_max = uv.max(axis=0)
|
||||
uv_range = uv_max - uv_min
|
||||
|
||||
# Avoid division by zero
|
||||
uv_range[uv_range < 1e-10] = 1.0
|
||||
|
||||
uv_normalized = (uv - uv_min) / uv_range
|
||||
|
||||
# Create unwrapped mesh (copy original)
|
||||
unwrapped = trimesh.copy()
|
||||
|
||||
# Store UV coordinates in visual
|
||||
from trimesh.visual import TextureVisuals
|
||||
unwrapped.visual = TextureVisuals(uv=uv_normalized)
|
||||
|
||||
# Add metadata
|
||||
unwrapped.metadata['uv_unwrap'] = {
|
||||
'algorithm': 'libigl_lscm',
|
||||
'conformal': True,
|
||||
'angle_preserving': True,
|
||||
'fixed_vertices': v_fixed.tolist()
|
||||
}
|
||||
|
||||
print(f"[LibiglLSCM] Complete - conformal (angle-preserving) mapping")
|
||||
|
||||
return (unwrapped,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackLibiglLSCM": LibiglLSCMNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackLibiglLSCM": "UV Unwrap (libigl LSCM)",
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
"""
|
||||
UV Unwrap mesh using xatlas library.
|
||||
|
||||
Fast, automatic UV unwrapping optimized for lightmaps and texture atlasing.
|
||||
No Blender dependency required. Uses the same algorithm as Blender 3.6+
|
||||
for UV packing.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import trimesh as trimesh_module
|
||||
|
||||
|
||||
class XAtlasUVUnwrapNode:
|
||||
"""
|
||||
UV Unwrap mesh using xatlas library.
|
||||
|
||||
Fast, automatic UV unwrapping optimized for lightmaps and texture atlasing.
|
||||
No Blender dependency required. Uses the same algorithm as Blender 3.6+
|
||||
for UV packing.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"trimesh": ("TRIMESH",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("TRIMESH",)
|
||||
RETURN_NAMES = ("unwrapped_mesh",)
|
||||
FUNCTION = "uv_unwrap"
|
||||
CATEGORY = "geompack/uv"
|
||||
|
||||
def uv_unwrap(self, trimesh):
|
||||
"""
|
||||
UV unwrap mesh using xatlas.
|
||||
|
||||
Args:
|
||||
trimesh: Input trimesh_module.Trimesh object
|
||||
|
||||
Returns:
|
||||
tuple: (unwrapped_trimesh_module.Trimesh,)
|
||||
"""
|
||||
try:
|
||||
import xatlas
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"xatlas not installed. Install with: pip install xatlas\n"
|
||||
"This is required for fast UV unwrapping without Blender."
|
||||
)
|
||||
|
||||
print(f"[XAtlasUVUnwrap] Input: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
||||
|
||||
# Parametrize with xatlas
|
||||
vmapping, indices, uvs = xatlas.parametrize(
|
||||
trimesh.vertices,
|
||||
trimesh.faces
|
||||
)
|
||||
|
||||
# Create new mesh with UV-split vertices
|
||||
new_vertices = trimesh.vertices[vmapping]
|
||||
|
||||
# Create trimesh with UV coordinates
|
||||
unwrapped = trimesh_module.Trimesh(
|
||||
vertices=new_vertices,
|
||||
faces=indices,
|
||||
process=False
|
||||
)
|
||||
|
||||
# Store UV coordinates in visual
|
||||
from trimesh.visual import TextureVisuals
|
||||
unwrapped.visual = TextureVisuals(uv=uvs)
|
||||
|
||||
# Preserve metadata
|
||||
unwrapped.metadata = trimesh.metadata.copy()
|
||||
unwrapped.metadata['uv_unwrap'] = {
|
||||
'algorithm': 'xatlas',
|
||||
'original_vertices': len(trimesh.vertices),
|
||||
'unwrapped_vertices': len(new_vertices),
|
||||
'vertex_duplication_ratio': len(new_vertices) / len(trimesh.vertices)
|
||||
}
|
||||
|
||||
print(f"[XAtlasUVUnwrap] Output: {len(unwrapped.vertices)} vertices, {len(unwrapped.faces)} faces")
|
||||
print(f"[XAtlasUVUnwrap] Vertex duplication: {len(new_vertices)/len(trimesh.vertices):.2f}x")
|
||||
|
||||
return (unwrapped,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackXAtlasUVUnwrap": XAtlasUVUnwrapNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackXAtlasUVUnwrap": "UV Unwrap (XAtlas)",
|
||||
}
|
||||
@@ -6,6 +6,7 @@ from .preview_mesh import PreviewMeshNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_M
|
||||
from .preview_mesh_vtk import PreviewMeshVTKNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_VTK_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_VTK_DISP
|
||||
from .preview_mesh_vtk_split import PreviewMeshVTKSplitNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_VTK_SPLIT_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_VTK_SPLIT_DISP
|
||||
from .preview_mesh_vtk_with_texture import PreviewMeshVTKWithTextureNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_VTK_WITH_TEXTURE_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_VTK_WITH_TEXTURE_DISP
|
||||
from .preview_mesh_vtk_dual import PreviewMeshVTKDualNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_VTK_DUAL_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_VTK_DUAL_DISP
|
||||
from .preview_mesh_vtk_pointcloud import PreviewMeshVTKPointCloudNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_VTK_POINTCLOUD_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_VTK_POINTCLOUD_DISP
|
||||
from .preview_mesh_vtk_with_normals import PreviewMeshVTKWithNormalsNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_VTK_WITH_NORMALS_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_VTK_WITH_NORMALS_DISP
|
||||
from .preview_mesh_vtk_edges import PreviewMeshVTKEdgesNode, NODE_CLASS_MAPPINGS as PREVIEW_MESH_VTK_EDGES_MAPS, NODE_DISPLAY_NAME_MAPPINGS as PREVIEW_MESH_VTK_EDGES_DISP
|
||||
@@ -22,6 +23,8 @@ NODE_CLASS_MAPPINGS.update(PREVIEW_MESH_VTK_SPLIT_MAPS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(PREVIEW_MESH_VTK_SPLIT_DISP)
|
||||
NODE_CLASS_MAPPINGS.update(PREVIEW_MESH_VTK_WITH_TEXTURE_MAPS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(PREVIEW_MESH_VTK_WITH_TEXTURE_DISP)
|
||||
NODE_CLASS_MAPPINGS.update(PREVIEW_MESH_VTK_DUAL_MAPS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(PREVIEW_MESH_VTK_DUAL_DISP)
|
||||
NODE_CLASS_MAPPINGS.update(PREVIEW_MESH_VTK_POINTCLOUD_MAPS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(PREVIEW_MESH_VTK_POINTCLOUD_DISP)
|
||||
NODE_CLASS_MAPPINGS.update(PREVIEW_MESH_VTK_WITH_NORMALS_MAPS)
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
Preview two meshes simultaneously with VTK.js viewer.
|
||||
|
||||
Combines two meshes with different vertex colors for easy visual comparison.
|
||||
Useful for visualizing boolean operation inputs or comparing meshes side-by-side.
|
||||
Exports to GLB format to preserve vertex colors.
|
||||
"""
|
||||
|
||||
import trimesh as trimesh_module
|
||||
import numpy as np
|
||||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
try:
|
||||
import folder_paths
|
||||
COMFYUI_OUTPUT_FOLDER = folder_paths.get_output_directory()
|
||||
except:
|
||||
COMFYUI_OUTPUT_FOLDER = None
|
||||
|
||||
|
||||
class PreviewMeshVTKDualNode:
|
||||
"""
|
||||
Preview two meshes simultaneously with VTK.js viewer.
|
||||
|
||||
Combines two meshes with different vertex colors for visual distinction.
|
||||
Perfect for comparing boolean operation inputs or any two meshes.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mesh_a": ("TRIMESH",),
|
||||
"mesh_b": ("TRIMESH",),
|
||||
},
|
||||
"optional": {
|
||||
"color_a": (["red", "blue", "green", "yellow", "cyan", "magenta", "orange", "purple"], {"default": "red"}),
|
||||
"color_b": (["red", "blue", "green", "yellow", "cyan", "magenta", "orange", "purple"], {"default": "blue"}),
|
||||
"opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "preview_dual_mesh"
|
||||
CATEGORY = "geompack/visualization"
|
||||
|
||||
def preview_dual_mesh(self, mesh_a, mesh_b, color_a="red", color_b="blue", opacity=1.0):
|
||||
"""
|
||||
Combine and preview two meshes with different colors.
|
||||
|
||||
Args:
|
||||
mesh_a: First trimesh object
|
||||
mesh_b: Second trimesh object
|
||||
color_a: Color for mesh_a
|
||||
color_b: Color for mesh_b
|
||||
opacity: Opacity for both meshes (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
dict: UI data for frontend widget
|
||||
"""
|
||||
print(f"[PreviewMeshVTKDual] Mesh A: {len(mesh_a.vertices)} vertices, {len(mesh_a.faces)} faces")
|
||||
print(f"[PreviewMeshVTKDual] Mesh B: {len(mesh_b.vertices)} vertices, {len(mesh_b.faces)} faces")
|
||||
print(f"[PreviewMeshVTKDual] Colors: {color_a}, {color_b}")
|
||||
|
||||
# Color mapping (RGB values, brighter for better visibility)
|
||||
color_map = {
|
||||
"red": [255, 100, 100],
|
||||
"blue": [100, 150, 255],
|
||||
"green": [100, 255, 100],
|
||||
"yellow": [255, 255, 100],
|
||||
"cyan": [100, 255, 255],
|
||||
"magenta": [255, 100, 255],
|
||||
"orange": [255, 180, 100],
|
||||
"purple": [200, 100, 255],
|
||||
}
|
||||
|
||||
# Calculate alpha channel from opacity
|
||||
alpha = int(opacity * 255)
|
||||
|
||||
# Create copies to avoid modifying originals
|
||||
mesh_a_colored = mesh_a.copy()
|
||||
mesh_b_colored = mesh_b.copy()
|
||||
|
||||
# Assign vertex colors to mesh_a
|
||||
color_a_rgba = color_map.get(color_a, [255, 100, 100]) + [alpha]
|
||||
mesh_a_colored.visual.vertex_colors = np.tile(
|
||||
color_a_rgba, (len(mesh_a.vertices), 1)
|
||||
).astype(np.uint8)
|
||||
|
||||
# Assign vertex colors to mesh_b
|
||||
color_b_rgba = color_map.get(color_b, [100, 150, 255]) + [alpha]
|
||||
mesh_b_colored.visual.vertex_colors = np.tile(
|
||||
color_b_rgba, (len(mesh_b.vertices), 1)
|
||||
).astype(np.uint8)
|
||||
|
||||
# Combine meshes
|
||||
try:
|
||||
combined = trimesh_module.util.concatenate([mesh_a_colored, mesh_b_colored])
|
||||
print(f"[PreviewMeshVTKDual] Combined mesh: {len(combined.vertices)} vertices, {len(combined.faces)} faces")
|
||||
except Exception as e:
|
||||
print(f"[PreviewMeshVTKDual] Failed to combine meshes: {e}")
|
||||
# Fallback: just use mesh_a
|
||||
combined = mesh_a_colored
|
||||
|
||||
# Generate unique filename
|
||||
filename = f"preview_vtk_dual_{uuid.uuid4().hex[:8]}.glb"
|
||||
|
||||
# Use ComfyUI's output directory
|
||||
if COMFYUI_OUTPUT_FOLDER:
|
||||
filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename)
|
||||
else:
|
||||
filepath = os.path.join(tempfile.gettempdir(), filename)
|
||||
|
||||
# Export to GLB (preserves vertex colors)
|
||||
try:
|
||||
combined.export(filepath, file_type='glb', include_normals=True)
|
||||
print(f"[PreviewMeshVTKDual] Exported to: {filepath}")
|
||||
except Exception as e:
|
||||
print(f"[PreviewMeshVTKDual] GLB export failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# Fallback to OBJ
|
||||
filename = filename.replace('.glb', '.obj')
|
||||
filepath = filepath.replace('.glb', '.obj')
|
||||
combined.export(filepath, file_type='obj')
|
||||
print(f"[PreviewMeshVTKDual] Exported to OBJ fallback: {filepath}")
|
||||
|
||||
# Calculate bounding box info
|
||||
bounds = combined.bounds
|
||||
extents = combined.extents
|
||||
max_extent = max(extents)
|
||||
|
||||
# Individual mesh info
|
||||
bounds_a = mesh_a.bounds
|
||||
bounds_b = mesh_b.bounds
|
||||
|
||||
# Return metadata for frontend widget
|
||||
ui_data = {
|
||||
"mesh_file": [filename],
|
||||
# Combined mesh info
|
||||
"vertex_count": [len(combined.vertices)],
|
||||
"face_count": [len(combined.faces)],
|
||||
"bounds_min": [bounds[0].tolist()],
|
||||
"bounds_max": [bounds[1].tolist()],
|
||||
"extents": [extents.tolist()],
|
||||
"max_extent": [float(max_extent)],
|
||||
# Mesh A info
|
||||
"mesh_a_vertices": [len(mesh_a.vertices)],
|
||||
"mesh_a_faces": [len(mesh_a.faces)],
|
||||
"mesh_a_bounds_min": [bounds_a[0].tolist()],
|
||||
"mesh_a_bounds_max": [bounds_a[1].tolist()],
|
||||
"mesh_a_watertight": [bool(mesh_a.is_watertight)],
|
||||
"mesh_a_color": [color_a],
|
||||
# Mesh B info
|
||||
"mesh_b_vertices": [len(mesh_b.vertices)],
|
||||
"mesh_b_faces": [len(mesh_b.faces)],
|
||||
"mesh_b_bounds_min": [bounds_b[0].tolist()],
|
||||
"mesh_b_bounds_max": [bounds_b[1].tolist()],
|
||||
"mesh_b_watertight": [bool(mesh_b.is_watertight)],
|
||||
"mesh_b_color": [color_b],
|
||||
# Visual settings
|
||||
"opacity": [float(opacity)],
|
||||
}
|
||||
|
||||
print(f"[PreviewMeshVTKDual] Preview ready")
|
||||
|
||||
return {"ui": ui_data}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackPreviewMeshVTKDual": PreviewMeshVTKDualNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackPreviewMeshVTKDual": "Preview Mesh (VTK Dual)",
|
||||
}
|
||||
@@ -117,9 +117,9 @@ class PreviewMeshVTKPointCloudNode:
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GeomPackPreviewMeshVTKPointCloud": PreviewMeshVTKPointCloudNode,
|
||||
"GeomPackPreviewMeshVTKFields": PreviewMeshVTKPointCloudNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GeomPackPreviewMeshVTKPointCloud": "Preview Mesh (VTK Point Cloud)",
|
||||
"GeomPackPreviewMeshVTKFields": "Preview Mesh (VTK with Fields)",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* ComfyUI GeomPack - Dual Mesh VTK.js Preview Widget
|
||||
* Shows two meshes with different colors for comparison
|
||||
*/
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
console.log("[GeomPack] Loading VTK.js dual mesh preview extension...");
|
||||
|
||||
app.registerExtension({
|
||||
name: "geompack.meshpreview.vtk.dual",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "GeomPackPreviewMeshVTKDual") {
|
||||
console.log("[GeomPack] Registering Preview Mesh (VTK Dual) node");
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
console.log("[GeomPack VTK Dual] Node created, adding widget");
|
||||
|
||||
// Create container for viewer + info panel
|
||||
const container = document.createElement("div");
|
||||
container.style.width = "100%";
|
||||
container.style.height = "100%";
|
||||
container.style.display = "flex";
|
||||
container.style.flexDirection = "column";
|
||||
container.style.backgroundColor = "#2a2a2a";
|
||||
container.style.overflow = "hidden";
|
||||
|
||||
// Create iframe for VTK.js viewer (use textured viewer for GLB support)
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.style.width = "100%";
|
||||
iframe.style.flex = "1 1 0";
|
||||
iframe.style.minHeight = "0";
|
||||
iframe.style.border = "none";
|
||||
iframe.style.backgroundColor = "#2a2a2a";
|
||||
|
||||
// Use textured viewer for GLB with vertex colors
|
||||
iframe.src = "/extensions/ComfyUI-GeometryPack/viewer_vtk_textured.html?v=" + Date.now();
|
||||
|
||||
// Create mesh info panel (larger for two meshes)
|
||||
const infoPanel = document.createElement("div");
|
||||
infoPanel.style.backgroundColor = "#1a1a1a";
|
||||
infoPanel.style.borderTop = "1px solid #444";
|
||||
infoPanel.style.padding = "8px 12px";
|
||||
infoPanel.style.fontSize = "10px";
|
||||
infoPanel.style.fontFamily = "monospace";
|
||||
infoPanel.style.color = "#ccc";
|
||||
infoPanel.style.lineHeight = "1.4";
|
||||
infoPanel.style.flexShrink = "0";
|
||||
infoPanel.style.overflow = "auto";
|
||||
infoPanel.style.maxHeight = "200px";
|
||||
infoPanel.innerHTML = '<span style="color: #888;">Dual mesh info will appear here after execution</span>';
|
||||
|
||||
// Add iframe and info panel to container
|
||||
container.appendChild(iframe);
|
||||
container.appendChild(infoPanel);
|
||||
|
||||
// Add widget
|
||||
const widget = this.addDOMWidget("preview_vtk_dual", "MESH_PREVIEW_VTK_DUAL", container, {
|
||||
getValue() { return ""; },
|
||||
setValue(v) { }
|
||||
});
|
||||
|
||||
widget.computeSize = () => [512, 700]; // Larger height for dual mesh info
|
||||
|
||||
// Store references
|
||||
this.meshViewerIframeVTKDual = iframe;
|
||||
this.meshInfoPanelVTKDual = infoPanel;
|
||||
|
||||
// Track iframe load state
|
||||
let iframeLoaded = false;
|
||||
iframe.addEventListener('load', () => {
|
||||
console.log("[GeomPack VTK Dual] Iframe loaded");
|
||||
iframeLoaded = true;
|
||||
});
|
||||
|
||||
// Listen for messages from iframe
|
||||
window.addEventListener('message', async (event) => {
|
||||
if (event.data.type === 'SCREENSHOT' && event.data.image) {
|
||||
console.log('[GeomPack VTK Dual] Received screenshot');
|
||||
// Screenshot handling (same as regular VTK viewer)
|
||||
try {
|
||||
const base64Data = event.data.image.split(',')[1];
|
||||
const byteString = atob(base64Data);
|
||||
const arrayBuffer = new ArrayBuffer(byteString.length);
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
uint8Array[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const blob = new Blob([uint8Array], { type: 'image/png' });
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `vtk-dual-screenshot-${timestamp}.png`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', blob, filename);
|
||||
formData.append('type', 'output');
|
||||
formData.append('subfolder', '');
|
||||
|
||||
const response = await fetch('/upload/image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('[GeomPack VTK Dual] Screenshot saved:', result.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[GeomPack VTK Dual] Error saving screenshot:', error);
|
||||
}
|
||||
}
|
||||
else if (event.data.type === 'MESH_ERROR' && event.data.error) {
|
||||
console.error('[GeomPack VTK Dual] Error from viewer:', event.data.error);
|
||||
if (infoPanel) {
|
||||
infoPanel.innerHTML = `<div style="color: #ff6b6b; padding: 8px;">Error: ${event.data.error}</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial node size
|
||||
this.setSize([512, 700]);
|
||||
|
||||
// Handle execution
|
||||
const onExecuted = this.onExecuted;
|
||||
this.onExecuted = function(message) {
|
||||
console.log("[GeomPack VTK Dual] onExecuted called with message:", message);
|
||||
onExecuted?.apply(this, arguments);
|
||||
|
||||
if (message?.mesh_file && message.mesh_file[0]) {
|
||||
const filename = message.mesh_file[0];
|
||||
console.log(`[GeomPack VTK Dual] Loading combined mesh: ${filename}`);
|
||||
|
||||
// Extract mesh A info
|
||||
const meshAVertices = message.mesh_a_vertices?.[0] || 'N/A';
|
||||
const meshAFaces = message.mesh_a_faces?.[0] || 'N/A';
|
||||
const meshAWatertight = message.mesh_a_watertight?.[0];
|
||||
const meshAColor = message.mesh_a_color?.[0] || 'red';
|
||||
|
||||
// Extract mesh B info
|
||||
const meshBVertices = message.mesh_b_vertices?.[0] || 'N/A';
|
||||
const meshBFaces = message.mesh_b_faces?.[0] || 'N/A';
|
||||
const meshBWatertight = message.mesh_b_watertight?.[0];
|
||||
const meshBColor = message.mesh_b_color?.[0] || 'blue';
|
||||
|
||||
// Combined mesh info
|
||||
const totalVertices = message.vertex_count?.[0] || 'N/A';
|
||||
const totalFaces = message.face_count?.[0] || 'N/A';
|
||||
|
||||
// Color mapping for display
|
||||
const colorDisplay = {
|
||||
red: '#ff6b6b',
|
||||
blue: '#64a0ff',
|
||||
green: '#64ff64',
|
||||
yellow: '#ffff64',
|
||||
cyan: '#64ffff',
|
||||
magenta: '#ff64ff',
|
||||
orange: '#ffb464',
|
||||
purple: '#c864ff'
|
||||
};
|
||||
|
||||
// Build dual mesh info HTML
|
||||
let infoHTML = `
|
||||
<div style="margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #444;">
|
||||
<div style="font-size: 11px; font-weight: bold; color: #fff; margin-bottom: 4px;">Combined Mesh</div>
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 8px; font-size: 10px;">
|
||||
<span style="color: #888;">Total Vertices:</span>
|
||||
<span>${typeof totalVertices === 'number' ? totalVertices.toLocaleString() : totalVertices}</span>
|
||||
<span style="color: #888;">Total Faces:</span>
|
||||
<span>${typeof totalFaces === 'number' ? totalFaces.toLocaleString() : totalFaces}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: bold; margin-bottom: 4px;">
|
||||
<span style="color: ${colorDisplay[meshAColor] || '#ff6b6b'};">● Mesh A</span>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 6px; font-size: 9px;">
|
||||
<span style="color: #888;">Vertices:</span>
|
||||
<span>${typeof meshAVertices === 'number' ? meshAVertices.toLocaleString() : meshAVertices}</span>
|
||||
<span style="color: #888;">Faces:</span>
|
||||
<span>${typeof meshAFaces === 'number' ? meshAFaces.toLocaleString() : meshAFaces}</span>
|
||||
`;
|
||||
|
||||
if (meshAWatertight !== undefined) {
|
||||
const watertightA = meshAWatertight ? 'Yes' : 'No';
|
||||
const colorA = meshAWatertight ? '#6c6' : '#c66';
|
||||
infoHTML += `
|
||||
<span style="color: #888;">Watertight:</span>
|
||||
<span style="color: ${colorA};">${watertightA}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
infoHTML += `
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: bold; margin-bottom: 4px;">
|
||||
<span style="color: ${colorDisplay[meshBColor] || '#64a0ff'};">● Mesh B</span>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 6px; font-size: 9px;">
|
||||
<span style="color: #888;">Vertices:</span>
|
||||
<span>${typeof meshBVertices === 'number' ? meshBVertices.toLocaleString() : meshBVertices}</span>
|
||||
<span style="color: #888;">Faces:</span>
|
||||
<span>${typeof meshBFaces === 'number' ? meshBFaces.toLocaleString() : meshBFaces}</span>
|
||||
`;
|
||||
|
||||
if (meshBWatertight !== undefined) {
|
||||
const watertightB = meshBWatertight ? 'Yes' : 'No';
|
||||
const colorB = meshBWatertight ? '#6c6' : '#c66';
|
||||
infoHTML += `
|
||||
<span style="color: #888;">Watertight:</span>
|
||||
<span style="color: ${colorB};">${watertightB}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
infoHTML += `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
infoPanel.innerHTML = infoHTML;
|
||||
|
||||
// Load mesh via ComfyUI view API
|
||||
const filepath = `/view?filename=${encodeURIComponent(filename)}&type=output&subfolder=`;
|
||||
|
||||
const sendMessage = () => {
|
||||
if (iframe.contentWindow) {
|
||||
console.log(`[GeomPack VTK Dual] Sending postMessage: ${filepath}`);
|
||||
iframe.contentWindow.postMessage({
|
||||
type: "LOAD_MESH",
|
||||
filepath: filepath,
|
||||
timestamp: Date.now()
|
||||
}, "*");
|
||||
} else {
|
||||
console.error("[GeomPack VTK Dual] Iframe contentWindow not available");
|
||||
}
|
||||
};
|
||||
|
||||
if (iframeLoaded) {
|
||||
sendMessage();
|
||||
} else {
|
||||
setTimeout(sendMessage, 500);
|
||||
}
|
||||
} else {
|
||||
console.log("[GeomPack VTK Dual] No mesh_file in message");
|
||||
}
|
||||
};
|
||||
|
||||
return r;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[GeomPack] VTK.js dual mesh preview extension loaded");
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* ComfyUI-GeometryPack UV Unwrap Dynamic Parameter Management
|
||||
*
|
||||
* This extension manages the visibility of method-specific parameters
|
||||
* in the UV Unwrap node based on the selected method.
|
||||
*/
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
const DEBUG = false; // Set to true to enable console logging
|
||||
|
||||
function log(...args) {
|
||||
if (DEBUG) {
|
||||
console.log("[GeomPack-UVUnwrap]", ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to hide a widget (removes from array)
|
||||
function hideWidget(node, widget) {
|
||||
if (!widget) return;
|
||||
if (widget._hidden) {
|
||||
log("Widget already hidden:", widget.name);
|
||||
return;
|
||||
}
|
||||
|
||||
log("Hiding widget:", widget.name);
|
||||
|
||||
const index = node.widgets.indexOf(widget);
|
||||
if (index === -1) {
|
||||
log(" ERROR: Widget not found in array!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Store original properties for restoration
|
||||
if (!widget.origType) {
|
||||
widget.origType = widget.type;
|
||||
widget.origComputeSize = widget.computeSize;
|
||||
widget.origSerializeValue = widget.serializeValue;
|
||||
}
|
||||
|
||||
widget._originalIndex = index;
|
||||
widget._hidden = true;
|
||||
|
||||
// Remove widget from array
|
||||
node.widgets.splice(index, 1);
|
||||
|
||||
log(" Widget removed from index:", index);
|
||||
|
||||
// Hide linked widgets if any
|
||||
if (widget.linkedWidgets) {
|
||||
widget.linkedWidgets.forEach(w => hideWidget(node, w));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to show a widget (re-inserts into array)
|
||||
function showWidget(node, widget) {
|
||||
if (!widget) return;
|
||||
if (!widget._hidden) {
|
||||
log("Widget already visible:", widget.name);
|
||||
return;
|
||||
}
|
||||
|
||||
log("Showing widget:", widget.name);
|
||||
|
||||
// Restore original properties
|
||||
if (widget.origType) {
|
||||
widget.type = widget.origType;
|
||||
widget.computeSize = widget.origComputeSize;
|
||||
if (widget.origSerializeValue) {
|
||||
widget.serializeValue = widget.origSerializeValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-insert widget at original position
|
||||
const insertIndex = Math.min(widget._originalIndex, node.widgets.length);
|
||||
node.widgets.splice(insertIndex, 0, widget);
|
||||
|
||||
log(" Widget restored to index:", insertIndex);
|
||||
|
||||
widget._hidden = false;
|
||||
|
||||
// Show linked widgets if any
|
||||
if (widget.linkedWidgets) {
|
||||
widget.linkedWidgets.forEach(w => showWidget(node, w));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to update node size and redraw
|
||||
function updateNodeSize(node) {
|
||||
node.setDirtyCanvas(true, true);
|
||||
if (app.graph) {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const newSize = node.computeSize();
|
||||
node.setSize([node.size[0], newSize[1]]);
|
||||
node.setDirtyCanvas(true, true);
|
||||
|
||||
if (app.canvas) {
|
||||
app.canvas.setDirty(true, true);
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (app.canvas) {
|
||||
app.canvas.draw(true, true);
|
||||
}
|
||||
log("Widget visibility update complete");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Main extension registration
|
||||
app.registerExtension({
|
||||
name: "geompack.uv_unwrap_dynamic",
|
||||
|
||||
async nodeCreated(node) {
|
||||
// Handle UV Unwrap node
|
||||
if (node.comfyClass === "GeomPackUVUnwrap") {
|
||||
setTimeout(() => {
|
||||
this.setupUVUnwrapNode(node);
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
setupUVUnwrapNode(node) {
|
||||
log("Setting up GeomPackUVUnwrap node");
|
||||
|
||||
// Find the method selector widget
|
||||
const methodWidget = node.widgets?.find(w => w.name === "method");
|
||||
if (!methodWidget) {
|
||||
log("ERROR: Method widget not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
log("Method widget found, current value:", methodWidget.value);
|
||||
|
||||
// Find all parameter widgets
|
||||
const iterationsWidget = node.widgets?.find(w => w.name === "iterations");
|
||||
const angleLimitWidget = node.widgets?.find(w => w.name === "angle_limit");
|
||||
const islandMarginWidget = node.widgets?.find(w => w.name === "island_margin");
|
||||
const scaleToBoundsWidget = node.widgets?.find(w => w.name === "scale_to_bounds");
|
||||
const cubeSizeWidget = node.widgets?.find(w => w.name === "cube_size");
|
||||
const cylinderRadiusWidget = node.widgets?.find(w => w.name === "cylinder_radius");
|
||||
|
||||
log("Found parameter widgets:", {
|
||||
iterations: !!iterationsWidget,
|
||||
angle_limit: !!angleLimitWidget,
|
||||
island_margin: !!islandMarginWidget,
|
||||
scale_to_bounds: !!scaleToBoundsWidget,
|
||||
cube_size: !!cubeSizeWidget,
|
||||
cylinder_radius: !!cylinderRadiusWidget
|
||||
});
|
||||
|
||||
// Function to update widget visibility based on selected method
|
||||
const updateWidgetVisibility = (selectedMethod) => {
|
||||
log("Updating widget visibility for method:", selectedMethod);
|
||||
|
||||
// Hide all optional parameters first
|
||||
hideWidget(node, iterationsWidget);
|
||||
hideWidget(node, angleLimitWidget);
|
||||
hideWidget(node, islandMarginWidget);
|
||||
hideWidget(node, scaleToBoundsWidget);
|
||||
hideWidget(node, cubeSizeWidget);
|
||||
hideWidget(node, cylinderRadiusWidget);
|
||||
|
||||
// Show relevant parameters based on method
|
||||
if (selectedMethod === "xatlas" ||
|
||||
selectedMethod === "libigl_lscm" ||
|
||||
selectedMethod === "libigl_harmonic") {
|
||||
// No parameters for these methods
|
||||
log(" No parameters needed for", selectedMethod);
|
||||
} else if (selectedMethod === "libigl_arap") {
|
||||
showWidget(node, iterationsWidget);
|
||||
} else if (selectedMethod === "blender_smart") {
|
||||
showWidget(node, angleLimitWidget);
|
||||
showWidget(node, islandMarginWidget);
|
||||
showWidget(node, scaleToBoundsWidget);
|
||||
} else if (selectedMethod === "blender_cube") {
|
||||
showWidget(node, cubeSizeWidget);
|
||||
showWidget(node, scaleToBoundsWidget);
|
||||
} else if (selectedMethod === "blender_cylinder") {
|
||||
showWidget(node, cylinderRadiusWidget);
|
||||
showWidget(node, scaleToBoundsWidget);
|
||||
} else if (selectedMethod === "blender_sphere") {
|
||||
showWidget(node, scaleToBoundsWidget);
|
||||
}
|
||||
|
||||
updateNodeSize(node);
|
||||
};
|
||||
|
||||
// Store original callback
|
||||
const origCallback = methodWidget.callback;
|
||||
|
||||
// Override callback to update visibility when method changes
|
||||
methodWidget.callback = function(value) {
|
||||
log("Method changed to:", value);
|
||||
|
||||
// Call original callback if it exists
|
||||
const result = origCallback?.apply(this, arguments);
|
||||
|
||||
// Update widget visibility
|
||||
updateWidgetVisibility(value);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Initialize visibility on node creation
|
||||
log("Initializing widget visibility...");
|
||||
updateWidgetVisibility(methodWidget.value);
|
||||
}
|
||||
});
|
||||
|
||||
log("GeomPack UV Unwrap dynamic parameters extension loaded");
|
||||
@@ -0,0 +1,294 @@
|
||||
{
|
||||
"id": "boolean-operations-demo",
|
||||
"revision": 1,
|
||||
"last_node_id": 19,
|
||||
"last_link_id": 24,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "GeomPackPreviewMeshVTK",
|
||||
"pos": [
|
||||
46.75901565551733,
|
||||
-173.09612621307397
|
||||
],
|
||||
"size": [
|
||||
450,
|
||||
678
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "8b8322e2f784d84097cd3427f097b55dc9a29d2a",
|
||||
"Node name for S&R": "GeomPackPreviewMeshVTK"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "GeomPackBooleanOp",
|
||||
"pos": [
|
||||
-411.98995552062996,
|
||||
-248.6199945449829
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
130
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mesh_a",
|
||||
"type": "TRIMESH",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "mesh_b",
|
||||
"type": "TRIMESH",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "result_mesh",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
22
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "8b8322e2f784d84097cd3427f097b55dc9a29d2a",
|
||||
"Node name for S&R": "GeomPackBooleanOp"
|
||||
},
|
||||
"widgets_values": [
|
||||
"union",
|
||||
"libigl_cgal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "PreviewAny",
|
||||
"pos": [
|
||||
-280.61808267817673,
|
||||
57.969546505383164
|
||||
],
|
||||
"size": [
|
||||
291.2499999999998,
|
||||
320.4200092315672
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 22
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.70",
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "GeomPackCreatePrimitive",
|
||||
"pos": [
|
||||
-800,
|
||||
-200
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
150
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mesh",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
1,
|
||||
23
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "8b8322e2f784d84097cd3427f097b55dc9a29d2a",
|
||||
"Node name for S&R": "GeomPackCreatePrimitive"
|
||||
},
|
||||
"widgets_values": [
|
||||
"cube",
|
||||
2,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "GeomPackCreatePrimitive",
|
||||
"pos": [
|
||||
-800,
|
||||
50
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
150
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mesh",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
2,
|
||||
24
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "8b8322e2f784d84097cd3427f097b55dc9a29d2a",
|
||||
"Node name for S&R": "GeomPackCreatePrimitive"
|
||||
},
|
||||
"widgets_values": [
|
||||
"sphere",
|
||||
2.5,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "GeomPackPreviewMeshVTKDual",
|
||||
"pos": [
|
||||
-481.5729971526634,
|
||||
155.11454028742656
|
||||
],
|
||||
"size": [
|
||||
512,
|
||||
830
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mesh_a",
|
||||
"type": "TRIMESH",
|
||||
"link": 23
|
||||
},
|
||||
{
|
||||
"name": "mesh_b",
|
||||
"type": "TRIMESH",
|
||||
"link": 24
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "8b8322e2f784d84097cd3427f097b55dc9a29d2a",
|
||||
"Node name for S&R": "GeomPackPreviewMeshVTKDual"
|
||||
},
|
||||
"widgets_values": [
|
||||
"red",
|
||||
"blue",
|
||||
0.5,
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
3,
|
||||
1,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
22,
|
||||
3,
|
||||
1,
|
||||
18,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
23,
|
||||
1,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
24,
|
||||
2,
|
||||
0,
|
||||
19,
|
||||
1,
|
||||
"TRIMESH"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.6611570247933891,
|
||||
"offset": [
|
||||
1335.9534281669653,
|
||||
339.7224902666414
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.28.8"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"id": "94c1c61e-cf5c-42a8-b9ec-914bb4f038ea",
|
||||
"revision": 0,
|
||||
"last_node_id": 16,
|
||||
"last_link_id": 22,
|
||||
"last_node_id": 19,
|
||||
"last_link_id": 29,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "GeomPackLoadMesh",
|
||||
"pos": [
|
||||
-415.0736028533223,
|
||||
-8.86644680883306
|
||||
-736.7848476964324,
|
||||
-65.70589089295333
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
@@ -24,7 +24,6 @@
|
||||
"name": "mesh",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
13,
|
||||
19
|
||||
]
|
||||
}
|
||||
@@ -39,119 +38,44 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "GeomPackPreviewMeshVTKFields",
|
||||
"id": 16,
|
||||
"type": "PreviewAny",
|
||||
"pos": [
|
||||
222.62019740761482,
|
||||
-638.0172723913844
|
||||
-205.21617522900593,
|
||||
-65.83044266339267
|
||||
],
|
||||
"size": [
|
||||
512,
|
||||
598
|
||||
210,
|
||||
188.05807589552933
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 16
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 27
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "253a956368261760da54dc96750c2ab0224e2b1f",
|
||||
"Node name for S&R": "GeomPackPreviewMeshVTKFields"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "GeomPackPreviewMeshVTKFields",
|
||||
"pos": [
|
||||
412.0851081444848,
|
||||
164.1085252304552
|
||||
],
|
||||
"size": [
|
||||
512,
|
||||
598
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "253a956368261760da54dc96750c2ab0224e2b1f",
|
||||
"Node name for S&R": "GeomPackPreviewMeshVTKFields"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "GeomPackDetectSelfIntersections",
|
||||
"pos": [
|
||||
-579.7979278570692,
|
||||
240.94106436197612
|
||||
],
|
||||
"size": [
|
||||
191.6373046875,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 19
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mesh_with_field",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
20
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "report",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "c77434120c2984b0ad1dd308c733117a855a8a36",
|
||||
"Node name for S&R": "GeomPackDetectSelfIntersections"
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.68",
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "GeomPackFixIntersectionsByPerturbation",
|
||||
"id": 13,
|
||||
"type": "GeomPackDetectSelfIntersections",
|
||||
"pos": [
|
||||
-318.6883908062423,
|
||||
140.27070769437378
|
||||
-389.92211876029273,
|
||||
204.15017433752317
|
||||
],
|
||||
"size": [
|
||||
330.2232421875,
|
||||
150
|
||||
191.6373046875,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
@@ -160,54 +84,7 @@
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 20
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "fixed_mesh",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
17
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "report",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "c77434120c2984b0ad1dd308c733117a855a8a36",
|
||||
"Node name for S&R": "GeomPackFixIntersectionsByPerturbation"
|
||||
},
|
||||
"widgets_values": [
|
||||
0.0001,
|
||||
10,
|
||||
"adaptive",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "GeomPackDetectSelfIntersections",
|
||||
"pos": [
|
||||
92.88357113378721,
|
||||
325.19554890815414
|
||||
],
|
||||
"size": [
|
||||
191.6373046875,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 17
|
||||
"link": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
@@ -215,7 +92,7 @@
|
||||
"name": "mesh_with_field",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
18
|
||||
28
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -233,92 +110,19 @@
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "GeomPackDetectSelfIntersections",
|
||||
"pos": [
|
||||
-262.875842931005,
|
||||
-249.03333148672056
|
||||
],
|
||||
"size": [
|
||||
191.6373046875,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mesh_with_field",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
16
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "report",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
22
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "253a956368261760da54dc96750c2ab0224e2b1f",
|
||||
"Node name for S&R": "GeomPackDetectSelfIntersections"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "PreviewAny",
|
||||
"pos": [
|
||||
5.2375924142305585,
|
||||
-115.34899783250772
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
188.05807589552933
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 22
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.68",
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "PreviewAny",
|
||||
"pos": [
|
||||
77.77449234502006,
|
||||
515.8574693032172
|
||||
-409.15778316693945,
|
||||
315.03241892560544
|
||||
],
|
||||
"size": [
|
||||
228.0678339686183,
|
||||
186.03206055464034
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
@@ -334,41 +138,164 @@
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "GeomPackPreviewMeshVTKFields",
|
||||
"pos": [
|
||||
-138.69342985165528,
|
||||
218.87010912332047
|
||||
],
|
||||
"size": [
|
||||
512,
|
||||
598
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 28
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "8b8322e2f784d84097cd3427f097b55dc9a29d2a",
|
||||
"Node name for S&R": "GeomPackPreviewMeshVTKFields"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "GeomPackDetectSelfIntersections",
|
||||
"pos": [
|
||||
-446.7936200272169,
|
||||
-59.79642561409345
|
||||
],
|
||||
"size": [
|
||||
191.6373046875,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 19
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mesh_with_field",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
23,
|
||||
29
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "report",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
27
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "c77434120c2984b0ad1dd308c733117a855a8a36",
|
||||
"Node name for S&R": "GeomPackDetectSelfIntersections"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "GeomPackPreviewMeshVTKFields",
|
||||
"pos": [
|
||||
392.49702699450677,
|
||||
-85.99595077667209
|
||||
],
|
||||
"size": [
|
||||
512,
|
||||
598
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 29
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "8b8322e2f784d84097cd3427f097b55dc9a29d2a",
|
||||
"Node name for S&R": "GeomPackPreviewMeshVTKFields"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "GeomPackFixSelfIntersectionsByPerturbation",
|
||||
"pos": [
|
||||
-729.5761795725482,
|
||||
67.01536068887431
|
||||
],
|
||||
"size": [
|
||||
319.319921875,
|
||||
174
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trimesh",
|
||||
"type": "TRIMESH",
|
||||
"link": 23
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "fixed_mesh",
|
||||
"type": "TRIMESH",
|
||||
"links": [
|
||||
25
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "report",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-geometrypack",
|
||||
"ver": "8b8322e2f784d84097cd3427f097b55dc9a29d2a",
|
||||
"Node name for S&R": "GeomPackFixSelfIntersectionsByPerturbation"
|
||||
},
|
||||
"widgets_values": [
|
||||
0.0001,
|
||||
12,
|
||||
"adaptive",
|
||||
true,
|
||||
true
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
13,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
16,
|
||||
2,
|
||||
0,
|
||||
12,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
17,
|
||||
11,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
18,
|
||||
13,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
19,
|
||||
1,
|
||||
@@ -377,14 +304,6 @@
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
20,
|
||||
14,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
21,
|
||||
13,
|
||||
@@ -394,22 +313,54 @@
|
||||
"*"
|
||||
],
|
||||
[
|
||||
22,
|
||||
2,
|
||||
23,
|
||||
14,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
25,
|
||||
17,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
27,
|
||||
14,
|
||||
1,
|
||||
16,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
28,
|
||||
13,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
"TRIMESH"
|
||||
],
|
||||
[
|
||||
29,
|
||||
14,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"TRIMESH"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.703736650569655,
|
||||
"scale": 0.6397605914269592,
|
||||
"offset": [
|
||||
873.4369348389099,
|
||||
342.74007853075705
|
||||
1105.9482911129744,
|
||||
150.99182977744076
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.28.8"
|
||||
|
||||
Reference in New Issue
Block a user