first draft
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
# Blender Integration Guide
|
||||
|
||||
ComfyUI-GeomPack integrates Blender's powerful mesh processing capabilities via subprocess calls. This allows you to leverage Blender's UV unwrapping and remeshing tools directly in your ComfyUI workflows.
|
||||
|
||||
## Overview
|
||||
|
||||
Three Blender nodes are available:
|
||||
1. **Blender UV Unwrap** - Smart UV unwrapping
|
||||
2. **Blender Voxel Remesh** - Voxel-based remeshing
|
||||
3. **Blender Quadriflow Remesh** - Quad-dominant remeshing
|
||||
|
||||
## Installation
|
||||
|
||||
### Install Blender
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install --cask blender
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo apt install blender
|
||||
# or
|
||||
sudo snap install blender
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
Download from https://www.blender.org/download/
|
||||
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
blender --version
|
||||
```
|
||||
|
||||
You should see output like:
|
||||
```
|
||||
Blender 4.5.4 LTS
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The integration uses a subprocess pattern:
|
||||
|
||||
1. **Export**: Mesh is exported to a temporary OBJ file
|
||||
2. **Launch**: Blender is launched in background mode (`--background`)
|
||||
3. **Execute**: Python script is executed via `--python-expr`
|
||||
4. **Process**: Blender performs the operation
|
||||
5. **Export**: Result is exported to a temporary OBJ file
|
||||
6. **Import**: Result is loaded back into trimesh
|
||||
7. **Cleanup**: Temporary files are deleted
|
||||
|
||||
This approach ensures:
|
||||
- No Blender GUI is shown
|
||||
- No manual intervention required
|
||||
- Fast and automated processing
|
||||
- Full integration with ComfyUI workflows
|
||||
|
||||
## Node Documentation
|
||||
|
||||
### 1. Blender UV Unwrap
|
||||
|
||||
**Category**: `geompack/blender`
|
||||
|
||||
**Description**: Generates UV coordinates for texturing using Blender's Smart UV Project algorithm.
|
||||
|
||||
**Parameters**:
|
||||
- `mesh` (MESH): Input mesh
|
||||
- `angle_limit` (FLOAT): Angle threshold for creating seams in degrees (default: 66.0, range: 1.0-89.0)
|
||||
- `island_margin` (FLOAT): Spacing between UV islands (default: 0.02, range: 0.0-1.0)
|
||||
|
||||
**Returns**:
|
||||
- `unwrapped_mesh` (MESH): Mesh with UV coordinates
|
||||
|
||||
**Use Cases**:
|
||||
- Preparing meshes for texture mapping
|
||||
- Creating UV layouts for baking
|
||||
- Automatic seam detection
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Load Mesh → Blender UV Unwrap (angle_limit=66.0, island_margin=0.02) → Save Mesh
|
||||
```
|
||||
|
||||
**Technical Details**:
|
||||
- Uses `bpy.ops.uv.smart_project()`
|
||||
- Automatically creates seams based on angle threshold
|
||||
- Islands are separated by configurable margin
|
||||
- UV coordinates are exported in the OBJ file
|
||||
- Preserves original mesh geometry
|
||||
|
||||
**Test Results**:
|
||||
```
|
||||
Input: 8 vertices, 12 faces (cube)
|
||||
Output: 24 vertices, 12 faces (with UV coordinates)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Blender Voxel Remesh
|
||||
|
||||
**Category**: `geompack/blender`
|
||||
|
||||
**Description**: Creates uniform, watertight meshes by voxelizing the input and reconstructing the surface.
|
||||
|
||||
**Parameters**:
|
||||
- `mesh` (MESH): Input mesh
|
||||
- `voxel_size` (FLOAT): Voxel size for remeshing (default: 0.05, range: 0.001-1.0)
|
||||
- Smaller values = higher resolution = more faces
|
||||
- Larger values = lower resolution = fewer faces
|
||||
|
||||
**Returns**:
|
||||
- `remeshed_mesh` (MESH): Voxel-remeshed mesh
|
||||
|
||||
**Use Cases**:
|
||||
- Mesh repair and cleanup
|
||||
- Creating watertight meshes for 3D printing
|
||||
- Simplifying complex meshes
|
||||
- Uniform mesh density
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Load Mesh → Blender Voxel Remesh (voxel_size=0.1) → Save Mesh
|
||||
```
|
||||
|
||||
**Technical Details**:
|
||||
- Uses `bpy.ops.object.voxel_remesh()`
|
||||
- Converts mesh to voxel grid at specified resolution
|
||||
- Reconstructs surface using marching cubes-like algorithm
|
||||
- Always produces watertight meshes
|
||||
- Good for fixing non-manifold geometry
|
||||
|
||||
**Test Results**:
|
||||
```
|
||||
Input: 42 vertices, 80 faces (sphere, subdivisions=1)
|
||||
Output: 2005 vertices, 1020 faces (voxel_size=0.1)
|
||||
```
|
||||
|
||||
**Performance Notes**:
|
||||
- Processing time depends on voxel size
|
||||
- Smaller voxel sizes = longer processing time
|
||||
- Typical range: 0.01 to 0.2 for most meshes
|
||||
|
||||
---
|
||||
|
||||
### 3. Blender Quadriflow Remesh
|
||||
|
||||
**Category**: `geompack/blender`
|
||||
|
||||
**Description**: Creates quad-dominant meshes with field-aligned topology, better for animation and subdivision.
|
||||
|
||||
**Parameters**:
|
||||
- `mesh` (MESH): Input mesh
|
||||
- `target_face_count` (INT): Target number of faces in output (default: 5000, range: 100-100000)
|
||||
|
||||
**Returns**:
|
||||
- `remeshed_mesh` (MESH): Quad-remeshed mesh (triangulated by trimesh)
|
||||
|
||||
**Use Cases**:
|
||||
- Creating animation-ready meshes
|
||||
- Preparing for subdivision surface modeling
|
||||
- Retopology of high-resolution scans
|
||||
- Better edge flow for character models
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Load Mesh → Blender Quadriflow Remesh (target_face_count=5000) → Save Mesh
|
||||
```
|
||||
|
||||
**Technical Details**:
|
||||
- Uses `bpy.ops.object.quadriflow_remesh()`
|
||||
- Field-aligned quad mesh generation
|
||||
- Optimized for subdivision surfaces
|
||||
- Output is quad-dominant (may contain some triangles)
|
||||
- Trimesh automatically triangulates quads for compatibility
|
||||
|
||||
**Test Results**:
|
||||
```
|
||||
Input: 42 vertices, 80 faces (sphere, subdivisions=1)
|
||||
Output: 2036 vertices, 1022 faces (target_face_count=500)
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
- Actual face count may differ from target
|
||||
- Quadriflow algorithm optimizes for good topology
|
||||
- Better edge flow than isotropic remeshing
|
||||
|
||||
---
|
||||
|
||||
## Comparison: Remeshing Algorithms
|
||||
|
||||
| Algorithm | Output | Use Case | Speed |
|
||||
|-----------|--------|----------|-------|
|
||||
| **CGAL Isotropic** (PyMeshLab) | Uniform triangles | Simulation, analysis | Fast |
|
||||
| **Blender Voxel** | Watertight, uniform | Repair, 3D printing | Medium |
|
||||
| **Blender Quadriflow** | Quad-dominant | Animation, subdivision | Slow |
|
||||
|
||||
**When to use each:**
|
||||
|
||||
- **CGAL Isotropic**: When you need uniform triangle sizes for FEM simulation or mesh analysis
|
||||
- **Blender Voxel**: When you need to repair broken meshes or create watertight geometry
|
||||
- **Blender Quadriflow**: When you need good topology for animation, rigging, or subdivision surfaces
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Blender not found" Error
|
||||
|
||||
**Problem**: Node fails with `RuntimeError: Blender not found`
|
||||
|
||||
**Solution**:
|
||||
1. Verify Blender is installed: `blender --version`
|
||||
2. Add Blender to PATH:
|
||||
- macOS: Already in PATH if installed via Homebrew
|
||||
- Linux: Should be in `/usr/bin/blender` or `/usr/local/bin/blender`
|
||||
- Windows: Add Blender installation folder to PATH
|
||||
|
||||
### Empty Output Mesh
|
||||
|
||||
**Problem**: Remeshing produces 0 vertices, 0 faces
|
||||
|
||||
**Solution**:
|
||||
- Check Blender version compatibility (tested with Blender 4.5.4 LTS)
|
||||
- Try different parameter values (e.g., larger voxel size, different face count)
|
||||
- Check input mesh is valid (use Mesh Info node)
|
||||
|
||||
### Slow Performance
|
||||
|
||||
**Problem**: Remeshing takes a long time
|
||||
|
||||
**Solution**:
|
||||
- **Voxel Remesh**: Increase `voxel_size` (e.g., 0.1 instead of 0.01)
|
||||
- **Quadriflow**: Reduce `target_face_count` (e.g., 1000 instead of 10000)
|
||||
- For large meshes, consider decimating first
|
||||
|
||||
### Subprocess Timeout
|
||||
|
||||
**Problem**: Operation times out after 5 minutes
|
||||
|
||||
**Solution**:
|
||||
- Edit the `timeout=300` parameter in the node code to increase timeout
|
||||
- Simplify the input mesh before processing
|
||||
- Use coarser parameters (larger voxel size, lower face count)
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Blender Scripts
|
||||
|
||||
You can modify the Blender scripts in `nodes.py` to add more Blender operations:
|
||||
|
||||
```python
|
||||
script = f"""
|
||||
import bpy
|
||||
|
||||
# Your custom Blender operations here
|
||||
# Example: Apply a modifier
|
||||
bpy.ops.object.modifier_add(type='SUBSURF')
|
||||
bpy.ops.object.modifier_apply()
|
||||
"""
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
Combine multiple Blender operations in a single workflow:
|
||||
|
||||
```
|
||||
Load Mesh
|
||||
→ Blender Voxel Remesh (repair mesh)
|
||||
→ Blender Quadriflow Remesh (retopology)
|
||||
→ Blender UV Unwrap (add UVs)
|
||||
→ Save Mesh
|
||||
```
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Code Structure
|
||||
|
||||
```python
|
||||
def _find_blender():
|
||||
"""Find Blender executable on system"""
|
||||
# Checks common paths: /Applications/Blender.app, /usr/bin/blender, etc.
|
||||
|
||||
class BlenderUVUnwrapNode:
|
||||
def uv_unwrap(self, mesh, angle_limit, island_margin):
|
||||
# 1. Find Blender
|
||||
blender_path = _find_blender()
|
||||
|
||||
# 2. Export mesh to temp OBJ
|
||||
input_path = tempfile.mktemp(suffix='.obj')
|
||||
mesh.export(input_path)
|
||||
|
||||
# 3. Create Blender script
|
||||
script = f"""
|
||||
import bpy
|
||||
bpy.ops.wm.obj_import(filepath='{input_path}')
|
||||
bpy.ops.uv.smart_project(...)
|
||||
bpy.ops.wm.obj_export(filepath='{output_path}')
|
||||
"""
|
||||
|
||||
# 4. Run Blender
|
||||
subprocess.run([blender_path, '--background', '--python-expr', script])
|
||||
|
||||
# 5. Load result
|
||||
unwrapped = trimesh.load(output_path)
|
||||
|
||||
# 6. Cleanup
|
||||
os.unlink(input_path)
|
||||
os.unlink(output_path)
|
||||
|
||||
return (unwrapped,)
|
||||
```
|
||||
|
||||
### Tested Blender Versions
|
||||
|
||||
- **Blender 4.5.4 LTS** ✓ Fully tested
|
||||
- Older versions should work but may have different API parameters
|
||||
|
||||
### Compatibility Notes
|
||||
|
||||
- **UV Unwrap**: Compatible with all Blender 3.x and 4.x versions
|
||||
- **Voxel Remesh**: Available in Blender 2.81+
|
||||
- **Quadriflow**: Available in Blender 2.81+
|
||||
- Note: Parameter compatibility varies by version
|
||||
- This implementation uses minimal parameter set for maximum compatibility
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: UV Unwrap for Texturing
|
||||
|
||||
**Goal**: Prepare a mesh for texture painting
|
||||
|
||||
**Workflow**:
|
||||
```
|
||||
Load Mesh (character.obj)
|
||||
→ Mesh Info (check stats)
|
||||
→ Blender UV Unwrap (angle_limit=66.0, island_margin=0.02)
|
||||
→ Save Mesh (character_unwrapped.obj)
|
||||
```
|
||||
|
||||
**Result**: Mesh with UV coordinates ready for texture painting in Blender, Substance Painter, etc.
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Mesh Repair for 3D Printing
|
||||
|
||||
**Goal**: Fix broken mesh and make it watertight for 3D printing
|
||||
|
||||
**Workflow**:
|
||||
```
|
||||
Load Mesh (broken_scan.obj)
|
||||
→ Mesh Info (check watertight status)
|
||||
→ Blender Voxel Remesh (voxel_size=0.05)
|
||||
→ Mesh Info (verify watertight=True)
|
||||
→ Save Mesh (repaired_for_printing.stl)
|
||||
```
|
||||
|
||||
**Result**: Watertight mesh suitable for 3D printing
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Retopology for Animation
|
||||
|
||||
**Goal**: Create animation-ready topology from high-resolution scan
|
||||
|
||||
**Workflow**:
|
||||
```
|
||||
Load Mesh (scan_highres.obj)
|
||||
→ Blender Quadriflow Remesh (target_face_count=10000)
|
||||
→ Blender UV Unwrap (angle_limit=66.0)
|
||||
→ Save Mesh (character_lowpoly.obj)
|
||||
```
|
||||
|
||||
**Result**: Clean quad-dominant mesh with good edge flow and UV coordinates
|
||||
|
||||
---
|
||||
|
||||
### Example 4: Multi-Stage Remeshing
|
||||
|
||||
**Goal**: Combine multiple remeshing techniques for best results
|
||||
|
||||
**Workflow**:
|
||||
```
|
||||
Load Mesh (raw_scan.obj)
|
||||
→ Blender Voxel Remesh (voxel_size=0.1, cleanup)
|
||||
→ CGAL Remesh (target_edge_length=0.05, uniform triangles)
|
||||
→ Mesh Info (check quality)
|
||||
→ Save Mesh (processed.obj)
|
||||
```
|
||||
|
||||
**Result**: Clean, uniform mesh with good quality
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
Tested on MacBook Pro (M1, 16GB RAM):
|
||||
|
||||
| Operation | Input | Output | Time |
|
||||
|-----------|-------|--------|------|
|
||||
| UV Unwrap | 8v, 12f | 24v, 12f | ~1.5s |
|
||||
| Voxel Remesh (0.1) | 42v, 80f | 2005v, 1020f | ~2.0s |
|
||||
| Quadriflow (500 faces) | 42v, 80f | 2036v, 1022f | ~3.5s |
|
||||
|
||||
**Notes**:
|
||||
- Times include Blender startup, processing, and shutdown
|
||||
- Larger meshes take proportionally longer
|
||||
- Voxel size and face count significantly affect performance
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- [ ] Add Blender Decimate modifier
|
||||
- [ ] Add Blender Smooth modifier
|
||||
- [ ] Add Blender Shrinkwrap for retopology
|
||||
- [ ] Support for custom Blender scripts via file input
|
||||
- [ ] Progress reporting for long operations
|
||||
- [ ] Parallel Blender processing for batch operations
|
||||
|
||||
## Credits
|
||||
|
||||
- Blender integration pattern inspired by [ComfyUI-MeshCraft](https://github.com/PozzettiAndrea/ComfyUI-MeshCraft)
|
||||
- Blender Python API documentation: https://docs.blender.org/api/current/
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 PozzettiAndrea
|
||||
Copyright (c) 2025 ComfyUI-GeometryPack Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
# ComfyUI libigl Wrapper - MVP Implementation Plan
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Goal**: Create a comprehensive ComfyUI custom node package that wraps libigl geometry processing library for mesh manipulation workflows.
|
||||
|
||||
**Target Users**:
|
||||
- General-purpose mesh processing
|
||||
- High-poly mesh handling (>100K faces)
|
||||
- Visualization essential in ComfyUI
|
||||
- Standalone tool (no dependency on other 3D extensions)
|
||||
|
||||
**Core Technology Stack**:
|
||||
- **libigl**: Python bindings for C++ geometry processing library
|
||||
- **NumPy**: Array operations and mesh data storage
|
||||
- **ComfyUI**: Node-based workflow system
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
ComfyUI-libigl/
|
||||
├── __init__.py # Node registration and mappings
|
||||
├── nodes.py # Node class implementations
|
||||
├── mesh_utils.py # Mesh data structures and utilities
|
||||
├── requirements.txt # Python dependencies
|
||||
├── pyproject.toml # ComfyUI registry metadata
|
||||
├── README.md # Documentation
|
||||
├── LICENSE # MIT License
|
||||
├── .gitignore # Git ignore rules
|
||||
├── examples/ # Test meshes
|
||||
│ ├── cube.obj
|
||||
│ └── bunny.obj
|
||||
└── workflows/ # Example ComfyUI workflows
|
||||
└── basic_workflow.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Mesh Data Type Definition
|
||||
|
||||
**Custom Type**: `"MESH"`
|
||||
|
||||
**Data Structure** (Python dict):
|
||||
```python
|
||||
mesh_data = {
|
||||
"vertices": np.ndarray, # Shape: (N, 3) - XYZ coordinates
|
||||
"faces": np.ndarray, # Shape: (F, 3) - Triangle vertex indices
|
||||
"normals": np.ndarray, # Optional: (N, 3) - Per-vertex normals
|
||||
"vertex_colors": np.ndarray, # Optional: (N, 3) or (N, 4) - RGB/RGBA colors
|
||||
"metadata": {
|
||||
"file_path": str,
|
||||
"is_manifold": bool,
|
||||
"has_boundary": bool,
|
||||
# ... other computed properties
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
1. **NumPy-based**: Use NumPy arrays (not PyTorch tensors) for simplicity and libigl compatibility
|
||||
2. **File Formats**: Support OBJ, PLY, STL (most common)
|
||||
3. **Error Handling**: Validate mesh integrity, provide clear error messages
|
||||
4. **Performance**: For high-poly meshes, implement progress indicators where possible
|
||||
5. **Visualization**: Export mesh previews as images or point clouds for ComfyUI display
|
||||
|
||||
---
|
||||
|
||||
## MVP Scope: Phase 1 (Essential Nodes)
|
||||
|
||||
### Total: 14 Core Nodes
|
||||
|
||||
#### 1. I/O Nodes (3 nodes)
|
||||
**Priority: CRITICAL**
|
||||
|
||||
1. **Load_Mesh**
|
||||
- Load mesh from file (OBJ/PLY/STL)
|
||||
- Inputs: file_path (STRING)
|
||||
- Outputs: MESH
|
||||
- Features: Auto-detect format, validate mesh
|
||||
|
||||
2. **Save_Mesh**
|
||||
- Save mesh to file
|
||||
- Inputs: MESH, file_path (STRING), format (COMBO)
|
||||
- Outputs: STRING (success message)
|
||||
- Features: Support OBJ, PLY, STL formats
|
||||
|
||||
3. **Mesh_Info**
|
||||
- Display mesh statistics
|
||||
- Inputs: MESH
|
||||
- Outputs: STRING (formatted stats)
|
||||
- Features: Vertex/face count, bounds, manifold status, boundary info
|
||||
|
||||
#### 2. Visualization Nodes (2 nodes)
|
||||
**Priority: CRITICAL** (user requirement)
|
||||
|
||||
4. **Preview_Mesh**
|
||||
- Visualize mesh in ComfyUI
|
||||
- Inputs: MESH, camera_angle (COMBO), resolution (INT)
|
||||
- Outputs: IMAGE
|
||||
- Features: Render mesh to image using matplotlib or trimesh
|
||||
|
||||
5. **Mesh_To_PointCloud**
|
||||
- Convert mesh to point cloud for preview
|
||||
- Inputs: MESH, sample_count (INT)
|
||||
- Outputs: IMAGE
|
||||
- Features: Sample points on surface, render as scatter plot
|
||||
|
||||
#### 3. Primitive Generation (1 node)
|
||||
**Priority: HIGH** (useful for testing)
|
||||
|
||||
6. **Create_Primitive**
|
||||
- Generate basic shapes
|
||||
- Inputs: shape (COMBO: cube/sphere/cylinder), size (FLOAT)
|
||||
- Outputs: MESH
|
||||
- Features: Generate test geometry
|
||||
|
||||
#### 4. Essential Cleanup (3 nodes)
|
||||
**Priority: HIGH** (common operations)
|
||||
|
||||
7. **Remove_Duplicates**
|
||||
- Merge duplicate vertices/faces
|
||||
- Inputs: MESH, tolerance (FLOAT)
|
||||
- Outputs: MESH
|
||||
- Features: Configurable epsilon threshold
|
||||
|
||||
8. **Remove_Degenerate**
|
||||
- Delete zero-area faces
|
||||
- Inputs: MESH
|
||||
- Outputs: MESH
|
||||
- Features: Clean up bad geometry
|
||||
|
||||
9. **Compute_Normals**
|
||||
- Calculate vertex/face normals
|
||||
- Inputs: MESH, mode (COMBO: vertex/face)
|
||||
- Outputs: MESH (with normals)
|
||||
- Features: Essential for rendering and visualization
|
||||
|
||||
#### 5. Core Processing (3 nodes)
|
||||
**Priority: HIGH** (key operations for high-poly)
|
||||
|
||||
10. **Decimate**
|
||||
- Reduce mesh complexity
|
||||
- Inputs: MESH, target_faces (INT or %), method (COMBO)
|
||||
- Outputs: MESH
|
||||
- Features: Critical for high-poly optimization
|
||||
|
||||
11. **Subdivide**
|
||||
- Increase mesh resolution
|
||||
- Inputs: MESH, iterations (INT), method (COMBO: loop/linear)
|
||||
- Outputs: MESH
|
||||
- Features: Loop subdivision, linear subdivision
|
||||
|
||||
12. **Smooth_Laplacian**
|
||||
- Smooth mesh surface
|
||||
- Inputs: MESH, iterations (INT), lambda (FLOAT)
|
||||
- Outputs: MESH
|
||||
- Features: Laplacian smoothing with iteration control
|
||||
|
||||
#### 6. Basic Utilities (2 nodes)
|
||||
**Priority: MEDIUM**
|
||||
|
||||
13. **Transform_Mesh**
|
||||
- Apply transformations
|
||||
- Inputs: MESH, translate (VECTOR3), rotate (VECTOR3), scale (FLOAT/VECTOR3)
|
||||
- Outputs: MESH
|
||||
- Features: Basic transformations
|
||||
|
||||
14. **Bounding_Box**
|
||||
- Compute AABB
|
||||
- Inputs: MESH
|
||||
- Outputs: MESH (box mesh), STRING (bounds info)
|
||||
- Features: Get min/max bounds
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Extended Functionality (13 nodes)
|
||||
|
||||
**Priority: MEDIUM** (Implement after MVP is stable)
|
||||
|
||||
### Cleanup & Repair (3 nodes)
|
||||
- **Fix_Normals**: Consistent face winding
|
||||
- **Fill_Holes**: Close boundary loops
|
||||
- **Split_Components**: Separate disconnected pieces
|
||||
|
||||
### Mesh Operations (4 nodes)
|
||||
- **Merge_Meshes**: Combine multiple meshes
|
||||
- **Slice_Plane**: Cut mesh with plane
|
||||
- **Crop_Mesh**: Bounding box crop
|
||||
- **Boolean_Operations**: Union/difference/intersection (if supported)
|
||||
|
||||
### Analysis (6 nodes)
|
||||
- **Boundary_Loops**: Extract boundaries
|
||||
- **Mesh_Volume**: Volume computation
|
||||
- **Face_Areas**: Per-face area array
|
||||
- **Field_To_Colors**: Scalar → vertex colors (for heatmap viz)
|
||||
- **Store_Vertex_Field**: Custom per-vertex data
|
||||
- **Get_Vertex_Field**: Extract field data
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Advanced Features (8 nodes)
|
||||
|
||||
**Priority: LOW** (Nice-to-have, implement if time permits)
|
||||
|
||||
### Deformation (2 nodes)
|
||||
- **ARAP_Deform**: As-rigid-as-possible deformation
|
||||
- **Harmonic_Deform**: Smooth deformation with constraints
|
||||
|
||||
### Distance & Queries (3 nodes)
|
||||
- **Signed_Distance**: SDF at query points
|
||||
- **Heat_Geodesic**: Fast geodesic distances
|
||||
- **Closest_Point**: Nearest point on surface
|
||||
|
||||
### Curvature (1 node)
|
||||
- **Principal_Curvature**: k1, k2 curvature + directions
|
||||
|
||||
### Sampling (2 nodes)
|
||||
- **Sample_Points**: Uniform surface sampling
|
||||
- **Poisson_Disk_Sample**: Blue noise sampling
|
||||
|
||||
---
|
||||
|
||||
## Never Implement (23 nodes)
|
||||
|
||||
**Reason: Redundant, too specialized, or better handled elsewhere**
|
||||
|
||||
### UV/Parameterization (3 nodes)
|
||||
- **Harmonic_UV**: Basic unwrapping (Blender better)
|
||||
- **LSCM_UV**: Conformal mapping (niche)
|
||||
- **Transfer_UV**: UV copying (rare use case)
|
||||
|
||||
### Too Slow/Specialized (5 nodes)
|
||||
- **Exact_Geodesic**: Very slow for high-poly
|
||||
- **Intrinsic_Delaunay**: Complex remeshing
|
||||
- **Ambient_Occlusion**: Better in rendering engines
|
||||
- **Geodesic_Path**: Limited workflow use
|
||||
- **Ray_Mesh_Intersect**: Low-level, better tools exist
|
||||
|
||||
### Redundant/Over-engineered (15 nodes)
|
||||
- **Marching_Cubes**: Other tools handle SDF→mesh
|
||||
- **Store_Face_Field**, **Get_Face_Field**: Over-engineered
|
||||
- **Delete_Field**, **List_Fields**: Unnecessary complexity
|
||||
- **Visualize_Field**: Covered by Field_To_Colors
|
||||
- **Boolean_Preview**: Redundant with main preview
|
||||
- **Compare_Meshes**: Niche analysis
|
||||
- **Mesh_Statistics**: Covered by Mesh_Info
|
||||
- **Vertex_Valence**: Too low-level
|
||||
- **Face_Adjacency**: Too low-level
|
||||
- **Edge_Topology**: Too low-level
|
||||
- **Winding_Number**: Rarely needed in workflows
|
||||
- **Point_Cloud_Distance**: Niche comparison
|
||||
- **Mean_Curvature**, **Gaussian_Curvature**: Covered by Principal_Curvature
|
||||
- **Transfer_Field**: Complex interpolation (niche)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Step 1: Project Setup (Day 1)
|
||||
- [ ] Create directory structure
|
||||
- [ ] Write `requirements.txt`
|
||||
- [ ] Write `pyproject.toml`
|
||||
- [ ] Write `.gitignore`
|
||||
- [ ] Write `README.md` (basic)
|
||||
- [ ] Add `LICENSE` (MIT)
|
||||
|
||||
### Step 2: Core Infrastructure (Day 1-2)
|
||||
- [ ] Create `mesh_utils.py`:
|
||||
- Mesh data structure definition
|
||||
- Validation functions
|
||||
- File I/O wrappers (libigl)
|
||||
- Conversion utilities
|
||||
- [ ] Test basic mesh loading/saving
|
||||
|
||||
### Step 3: Registration Boilerplate (Day 2)
|
||||
- [ ] Create `__init__.py`:
|
||||
- NODE_CLASS_MAPPINGS
|
||||
- NODE_DISPLAY_NAME_MAPPINGS
|
||||
- [ ] Create `nodes.py` skeleton
|
||||
|
||||
### Step 4: I/O Nodes (Day 2-3)
|
||||
- [ ] Implement Load_Mesh
|
||||
- [ ] Implement Save_Mesh
|
||||
- [ ] Implement Mesh_Info
|
||||
- [ ] Test with sample meshes
|
||||
|
||||
### Step 5: Visualization (Day 3-4)
|
||||
- [ ] Implement Preview_Mesh (matplotlib/trimesh)
|
||||
- [ ] Implement Mesh_To_PointCloud
|
||||
- [ ] Test rendering pipeline
|
||||
|
||||
### Step 6: Core Processing (Day 4-6)
|
||||
- [ ] Implement Create_Primitive
|
||||
- [ ] Implement Decimate
|
||||
- [ ] Implement Subdivide
|
||||
- [ ] Implement Smooth_Laplacian
|
||||
- [ ] Implement Compute_Normals
|
||||
- [ ] Test with high-poly meshes
|
||||
|
||||
### Step 7: Cleanup & Utilities (Day 6-7)
|
||||
- [ ] Implement Remove_Duplicates
|
||||
- [ ] Implement Remove_Degenerate
|
||||
- [ ] Implement Transform_Mesh
|
||||
- [ ] Implement Bounding_Box
|
||||
|
||||
### Step 8: Testing & Polish (Day 7-8)
|
||||
- [ ] Test all nodes with various meshes
|
||||
- [ ] Error handling improvements
|
||||
- [ ] Performance optimization
|
||||
- [ ] Create example workflows
|
||||
- [ ] Update README with examples
|
||||
|
||||
### Step 9: Documentation (Day 8)
|
||||
- [ ] Write node documentation
|
||||
- [ ] Create tutorial workflow
|
||||
- [ ] Add screenshots to README
|
||||
|
||||
### Step 10: Release MVP (Day 8)
|
||||
- [ ] Final testing
|
||||
- [ ] Tag v0.1.0
|
||||
- [ ] Publish to GitHub
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### `requirements.txt`
|
||||
```
|
||||
libigl>=2.6.1
|
||||
numpy>=1.21.0
|
||||
scipy>=1.7.0
|
||||
matplotlib>=3.5.0
|
||||
Pillow>=9.0.0
|
||||
```
|
||||
|
||||
### Optional (for enhanced visualization)
|
||||
```
|
||||
trimesh>=3.15.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Meshes
|
||||
1. **Simple**: cube.obj (8 vertices, 12 faces)
|
||||
2. **Medium**: bunny.obj (~5K faces)
|
||||
3. **High-poly**: dragon.obj (~100K faces)
|
||||
4. **Edge cases**:
|
||||
- Non-manifold mesh
|
||||
- Mesh with boundaries
|
||||
- Degenerate triangles
|
||||
|
||||
### Test Workflows
|
||||
1. **Basic Pipeline**: Load → Info → Preview
|
||||
2. **Cleanup Pipeline**: Load → Remove Duplicates → Remove Degenerate → Preview
|
||||
3. **Simplification**: Load → Decimate → Save
|
||||
4. **Smoothing**: Load → Smooth → Preview
|
||||
5. **Transformation**: Create Primitive → Transform → Preview
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria for MVP
|
||||
|
||||
1. ✅ All 14 Phase 1 nodes implemented and working
|
||||
2. ✅ Can load common mesh formats (OBJ, PLY, STL)
|
||||
3. ✅ Can visualize meshes in ComfyUI
|
||||
4. ✅ Can process high-poly meshes (>100K faces) without crashes
|
||||
5. ✅ Decimation reduces mesh complexity effectively
|
||||
6. ✅ All nodes have proper error handling
|
||||
7. ✅ Example workflows demonstrate key features
|
||||
8. ✅ README documentation is complete
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements (Post-MVP)
|
||||
|
||||
### Phase 2 Priority
|
||||
1. Boolean operations (if libigl supports)
|
||||
2. Field-based visualization (heatmaps)
|
||||
3. Advanced cleanup (hole filling, component separation)
|
||||
|
||||
### Phase 3 Priority
|
||||
1. Deformation tools (ARAP)
|
||||
2. Geodesic computations
|
||||
3. Advanced sampling
|
||||
|
||||
### Long-term
|
||||
1. GPU acceleration for large meshes
|
||||
2. Real-time preview updates
|
||||
3. Custom shader support
|
||||
4. Integration with ComfyUI-3D-Pack (if requested)
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Potential Issues
|
||||
|
||||
1. **High-poly performance**:
|
||||
- Solution: Progress indicators, chunked processing
|
||||
|
||||
2. **Visualization memory**:
|
||||
- Solution: Downsampling for preview, configurable resolution
|
||||
|
||||
3. **libigl limitations**:
|
||||
- Solution: Research libigl Python API capabilities early
|
||||
|
||||
4. **ComfyUI integration issues**:
|
||||
- Solution: Study existing 3D node packages, follow patterns
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **libigl Python Tutorial**: https://libigl.github.io/libigl-python-bindings/
|
||||
- **ComfyUI Custom Node Guide**: https://docs.comfy.org/essentials/custom_node_walkthrough
|
||||
- **Template Repository**: https://github.com/jhj0517/ComfyUI-CustomNodes-Template
|
||||
- **Example 3D Pack**: https://github.com/MrForExample/ComfyUI-3D-Pack
|
||||
|
||||
---
|
||||
|
||||
## Contact & Contribution
|
||||
|
||||
This is an MVP implementation plan. Feedback and contributions welcome!
|
||||
|
||||
**Estimated MVP Completion**: 8-10 days
|
||||
**Total Nodes in MVP**: 14 nodes
|
||||
**Total Nodes (all phases)**: ~35 nodes
|
||||
@@ -0,0 +1,441 @@
|
||||
# ComfyUI-GeomPack
|
||||
|
||||
Geometry processing custom nodes for ComfyUI, powered by [trimesh](https://trimesh.org/), [libigl](https://libigl.github.io/), and [CGAL](https://www.cgal.org/).
|
||||
|
||||
## Overview
|
||||
|
||||
ComfyUI-GeomPack provides a comprehensive set of mesh processing nodes for ComfyUI, combining the power of multiple geometry processing libraries. Perfect for mesh cleanup, optimization, remeshing, analysis, and transformation workflows.
|
||||
|
||||
## Features
|
||||
|
||||
- **Mesh I/O**: Load and save OBJ, PLY, STL, OFF, and more formats via trimesh
|
||||
- **Dual 3D Preview**: Two interactive viewers - Three.js for general use, VTK.js for scientific visualization
|
||||
- **Mesh Analysis**: Display statistics, compute normals, analyze geometry properties
|
||||
- **Mesh Transforms**: Center meshes at origin with one click
|
||||
- **CGAL Remeshing**: Isotropic remeshing for uniform triangle quality (via PyMeshLab)
|
||||
- **Blender Integration**: UV unwrapping and remeshing via Blender subprocess
|
||||
- **Primitive Creation**: Generate cubes, spheres, and planes
|
||||
- **Trimesh Integration**: Enhanced mesh handling with automatic validation
|
||||
- **High-poly Support**: Optimized for meshes with 100K+ faces
|
||||
|
||||
## Installation
|
||||
|
||||
### Method 1: Manual Installation
|
||||
|
||||
1. Clone this repository into your ComfyUI custom nodes directory:
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes/
|
||||
git clone https://github.com/yourusername/ComfyUI-GeomPack.git
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
cd ComfyUI-GeomPack
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Install Blender (optional, required for Blender nodes):
|
||||
- macOS: `brew install --cask blender` or download from https://www.blender.org/download/
|
||||
- Linux: `sudo apt install blender` or `sudo snap install blender`
|
||||
- Windows: Download from https://www.blender.org/download/
|
||||
|
||||
4. Restart ComfyUI
|
||||
|
||||
### Method 2: ComfyUI Manager (Coming Soon)
|
||||
|
||||
Install via ComfyUI Manager once published to the registry.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Workflow
|
||||
1. **Create a mesh**: Use the `Create Primitive` node to generate a cube, sphere, or plane
|
||||
2. **View info**: Connect to `Mesh Info` node to see detailed statistics
|
||||
3. **Apply remeshing**: Use `CGAL Remesh` to create uniform triangle meshes
|
||||
4. **Save**: Export with `Save Mesh` node
|
||||
|
||||
### CGAL Remeshing Workflow
|
||||
```
|
||||
Create Primitive (cube) → Mesh Info → CGAL Remesh → Mesh Info → Save Mesh
|
||||
```
|
||||
|
||||
**Result**: Converts a simple 8-vertex cube into a uniformly remeshed 386-vertex mesh with consistent triangle sizes!
|
||||
|
||||
## Available Nodes
|
||||
|
||||
### geompack/io
|
||||
- **Load Mesh**: Load meshes from file (OBJ, PLY, STL, OFF, etc.)
|
||||
- **Save Mesh**: Save meshes to file with trimesh export
|
||||
|
||||
### geompack/analysis
|
||||
- **Mesh Info**: Display detailed statistics including:
|
||||
- Vertex/face/edge counts
|
||||
- Volume and surface area
|
||||
- Watertight status
|
||||
- Bounding box and extents
|
||||
- Metadata
|
||||
|
||||
### geompack/primitives
|
||||
- **Create Primitive**: Generate geometric primitives
|
||||
- Cube (8 vertices, 12 faces)
|
||||
- Sphere (icosphere with subdivision levels)
|
||||
- Plane (subdivided grid)
|
||||
|
||||
### geompack/cgal
|
||||
- **CGAL Remesh (Isotropic)**: Advanced remeshing algorithm
|
||||
- Creates uniform triangle sizes
|
||||
- Target edge length control
|
||||
- Iteration count parameter
|
||||
- Preserves volume
|
||||
- Based on Botsch & Kobbelt (2004) algorithm
|
||||
|
||||
### geompack/transforms
|
||||
- **Center Mesh**: Center mesh at origin (0, 0, 0)
|
||||
- Uses bounding box center
|
||||
- Simple one-click centering
|
||||
- Preserves mesh metadata
|
||||
|
||||
### geompack/visualization
|
||||
- **Preview Mesh (3D)**: Interactive 3D mesh viewer
|
||||
- Powered by Three.js
|
||||
- Orbit controls (rotate, pan, zoom)
|
||||
- Real-time preview directly in ComfyUI
|
||||
- No need to save files first!
|
||||
- **Preview Mesh (VTK)**: Scientific visualization viewer
|
||||
- Powered by VTK.js
|
||||
- Trackball camera controls
|
||||
- Better for scientific visualization and analysis
|
||||
- Excellent for large meshes and datasets
|
||||
|
||||
### geompack/blender
|
||||
- **Blender UV Unwrap**: Smart UV unwrapping using Blender
|
||||
- Angle-based seam detection
|
||||
- Configurable island margin
|
||||
- Exports mesh with UV coordinates
|
||||
- **Blender Voxel Remesh**: Voxel-based remeshing
|
||||
- Creates uniform, watertight meshes
|
||||
- Configurable voxel size
|
||||
- Good for mesh repair
|
||||
- **Blender Quadriflow Remesh**: Quad-dominant remeshing
|
||||
- Field-aligned quad mesh generation
|
||||
- Target face count control
|
||||
- Better for animation and subdivision
|
||||
|
||||
### geompack/examples
|
||||
- **Example Node**: Template for creating custom nodes
|
||||
|
||||
## Mesh Data Format
|
||||
|
||||
Meshes are now represented as **`trimesh.Trimesh`** objects, providing:
|
||||
|
||||
```python
|
||||
mesh = trimesh.Trimesh(vertices=np.ndarray, faces=np.ndarray)
|
||||
|
||||
# Built-in properties:
|
||||
mesh.vertices # (N, 3) numpy array of XYZ coordinates
|
||||
mesh.faces # (F, 3) numpy array of triangle indices
|
||||
mesh.vertex_normals # (N, 3) per-vertex normals (auto-computed)
|
||||
mesh.bounds # [[min_x, min_y, min_z], [max_x, max_y, max_z]]
|
||||
mesh.centroid # [x, y, z] center of mass
|
||||
mesh.volume # Volume of the mesh (if watertight)
|
||||
mesh.area # Total surface area
|
||||
mesh.is_watertight # Boolean: mesh is closed?
|
||||
mesh.metadata # Dictionary for custom data
|
||||
|
||||
# Methods:
|
||||
mesh.export('output.obj') # Save to file
|
||||
mesh.apply_transform(matrix) # Transform mesh
|
||||
# ... and many more!
|
||||
```
|
||||
|
||||
## Example Workflows
|
||||
|
||||
### CGAL Isotropic Remeshing
|
||||
```
|
||||
Create Primitive (cube, size=1.0)
|
||||
→ CGAL Remesh (target_edge_length=0.15, iterations=3)
|
||||
→ Mesh Info
|
||||
→ Save Mesh (output.obj)
|
||||
```
|
||||
|
||||
**Input**: 8 vertices, 12 faces
|
||||
**Output**: 386 vertices, 768 faces with uniform triangle sizes
|
||||
**Volume Preserved**: 1.000000 (0.00% error)
|
||||
|
||||
### Mesh Loading and Analysis
|
||||
```
|
||||
Load Mesh (input.obj)
|
||||
→ Mesh Info (view statistics)
|
||||
→ CGAL Remesh (clean up mesh)
|
||||
→ Save Mesh (output.obj)
|
||||
```
|
||||
|
||||
### Primitive Creation
|
||||
```
|
||||
Create Primitive (sphere, subdivisions=3)
|
||||
→ Mesh Info (check watertight status)
|
||||
→ Save Mesh (sphere.stl)
|
||||
```
|
||||
|
||||
## Development Status
|
||||
|
||||
**Current Version**: 0.2.0
|
||||
|
||||
### Recently Completed
|
||||
- ✅ Migrated from dict-based meshes to trimesh.Trimesh objects
|
||||
- ✅ Integrated CGAL for advanced remeshing (currently using PyMeshLab)
|
||||
- ✅ Load Mesh (with fallback to libigl)
|
||||
- ✅ Save Mesh (all trimesh-supported formats)
|
||||
- ✅ Create Primitive (cube, sphere, plane)
|
||||
- ✅ Mesh Info (enhanced with volume, watertight detection)
|
||||
- ✅ CGAL Isotropic Remeshing (via PyMeshLab, will return to CGAL eventually)
|
||||
- ✅ Blender UV Unwrapping (Smart UV Project)
|
||||
- ✅ Blender Voxel Remeshing
|
||||
- ✅ Blender Quadriflow Remeshing
|
||||
- ✅ Center Mesh (transform to origin)
|
||||
- ✅ Preview Mesh (3D) - Interactive Three.js viewer widget!
|
||||
- ✅ Preview Mesh (VTK) - Scientific visualization with VTK.js!
|
||||
|
||||
### Coming Soon (Future Phases)
|
||||
- [ ] Mesh Decimation
|
||||
- [ ] Mesh Subdivision
|
||||
- [ ] Mesh Scaling/Rotation nodes
|
||||
- [ ] Laplacian Smoothing
|
||||
- [ ] Boolean Operations (CGAL)
|
||||
- [ ] Mesh Repair
|
||||
- [ ] PyNanoInstantMeshes integration
|
||||
- [ ] Native CGAL remeshing (when Python binding issues resolved)
|
||||
|
||||
See [MVP_PLAN.md](MVP_PLAN.md) for the complete roadmap.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- ComfyUI
|
||||
- **trimesh** 3.15.0+
|
||||
- **pymeshlab** 2022.2+ (currently used for remeshing)
|
||||
- **Blender** (optional, for UV unwrapping and Blender remeshing nodes)
|
||||
- libigl 2.6.1+ (fallback for some I/O operations)
|
||||
- numpy 1.21.0+
|
||||
- scipy 1.7.0+
|
||||
- matplotlib 3.5.0+
|
||||
- Pillow 9.0.0+
|
||||
|
||||
## Technical Details
|
||||
|
||||
### CGAL Isotropic Remeshing
|
||||
|
||||
The `CGAL Remesh` node implements the incremental triangle-based isotropic remeshing algorithm from:
|
||||
|
||||
> Botsch, M., & Kobbelt, L. (2004). "A remeshing approach to multiresolution modeling."
|
||||
> Eurographics Symposium on Geometry Processing.
|
||||
|
||||
**How it works:**
|
||||
1. Converts trimesh to CGAL Polyhedron_3
|
||||
2. Applies isotropic remeshing with:
|
||||
- **Edge splits**: Edges longer than target length
|
||||
- **Edge collapses**: Edges shorter than target length
|
||||
- **Edge flips**: Improve triangle quality
|
||||
- **Tangential smoothing**: Relax vertex positions
|
||||
3. Converts back to trimesh with metadata preservation
|
||||
|
||||
**Parameters:**
|
||||
- `target_edge_length`: Desired edge length for triangles (smaller = denser mesh)
|
||||
- `iterations`: Number of refinement passes (typically 3-10)
|
||||
|
||||
**Benefits:**
|
||||
- More uniform triangle distribution
|
||||
- Better numerical stability for simulations
|
||||
- Improved mesh quality for rendering
|
||||
- Volume preservation
|
||||
|
||||
### Blender Integration
|
||||
|
||||
The Blender nodes integrate Blender's powerful mesh processing capabilities via subprocess:
|
||||
|
||||
**How it works:**
|
||||
1. Mesh is exported to temporary OBJ file
|
||||
2. Blender is launched in background mode (`--background`)
|
||||
3. Python script is executed via `--python-expr`
|
||||
4. Blender performs the operation (UV unwrap, remesh, etc.)
|
||||
5. Result is exported to temporary OBJ file
|
||||
6. Result is loaded back into trimesh
|
||||
|
||||
**Available Blender Operations:**
|
||||
|
||||
**UV Unwrapping (Smart UV Project):**
|
||||
- Automatically creates UV seams based on angle threshold
|
||||
- Configurable island margin for texture bleeding prevention
|
||||
- Preserves mesh geometry, adds UV coordinates
|
||||
|
||||
**Voxel Remeshing:**
|
||||
- Voxelizes the input mesh at specified resolution
|
||||
- Reconstructs surface from voxel grid
|
||||
- Creates watertight, uniform meshes
|
||||
- Good for mesh repair and cleanup
|
||||
|
||||
**Quadriflow Remeshing:**
|
||||
- Field-aligned quad mesh generation
|
||||
- Better topology for animation and subdivision
|
||||
- Target face count control
|
||||
- Outputs quad-dominant mesh (triangulated by trimesh)
|
||||
|
||||
**Note:** Blender must be installed and accessible in your PATH for these nodes to work.
|
||||
|
||||
### 3D Preview Widgets
|
||||
|
||||
ComfyUI-GeomPack provides two interactive 3D viewers directly in ComfyUI:
|
||||
|
||||
#### Preview Mesh (3D) - Three.js Viewer
|
||||
|
||||
The Three.js viewer provides a general-purpose 3D visualization with excellent rendering quality:
|
||||
|
||||
**Features:**
|
||||
- **Interactive Camera:** Orbit, pan, and zoom with mouse/trackpad controls
|
||||
- **Real-time Rendering:** Smooth 60fps rendering with WebGL
|
||||
- **Automatic Scaling:** Camera automatically adjusts to fit mesh
|
||||
- **Professional Lighting:** Three-point lighting setup for clear visualization
|
||||
- **Grid and Axes:** Visual reference for orientation
|
||||
|
||||
**How it works:**
|
||||
1. Mesh is exported to GLB format (optimized for web)
|
||||
2. File is saved to ComfyUI's output directory
|
||||
3. Three.js viewer loads and displays the mesh
|
||||
4. OrbitControls enable interactive exploration
|
||||
|
||||
**Controls:**
|
||||
- **Left Mouse:** Rotate camera around mesh
|
||||
- **Right Mouse / Two-finger drag:** Pan camera
|
||||
- **Scroll / Pinch:** Zoom in/out
|
||||
- **Automatic:** Camera resets to fit mesh on load
|
||||
|
||||
**Technical Details:**
|
||||
- Uses Three.js v0.160.0 from CDN (no local dependencies)
|
||||
- Supports GLB and OBJ formats
|
||||
- 512x512px viewport (customizable in code)
|
||||
- Renders in ComfyUI's output directory (accessible via /view API)
|
||||
|
||||
#### Preview Mesh (VTK) - VTK.js Viewer
|
||||
|
||||
The VTK.js viewer is optimized for scientific visualization and large datasets:
|
||||
|
||||
**Features:**
|
||||
- **Trackball Controls:** Professional scientific visualization camera controls
|
||||
- **Scientific Rendering:** Optimized for mesh analysis and inspection
|
||||
- **High Performance:** Excellent for large meshes (100K+ vertices)
|
||||
- **Clean Interface:** Focused on geometry visualization without distractions
|
||||
|
||||
**How it works:**
|
||||
1. Mesh is exported to STL format (native VTK.js format)
|
||||
2. File is saved to ComfyUI's output directory
|
||||
3. VTK.js viewer loads and displays the mesh
|
||||
4. TrackballCamera enables precise exploration
|
||||
|
||||
**Controls:**
|
||||
- **Left Mouse:** Rotate camera (trackball mode)
|
||||
- **Middle Mouse / Shift + Left:** Pan camera
|
||||
- **Right Mouse / Scroll:** Zoom in/out
|
||||
- **Automatic:** Camera resets to fit mesh on load
|
||||
|
||||
**Technical Details:**
|
||||
- Uses VTK.js v29.5.0 from Skypack CDN (no local dependencies)
|
||||
- Supports STL and OBJ formats
|
||||
- 512x512px viewport (customizable in code)
|
||||
- Renders in ComfyUI's output directory (accessible via /view API)
|
||||
|
||||
**When to use which viewer:**
|
||||
- **Three.js**: General 3D visualization, artistic rendering, better lighting
|
||||
- **VTK.js**: Scientific analysis, large datasets, mesh inspection, precise measurements
|
||||
|
||||
**No External Tools Required:** Unlike other 3D preview solutions, both widgets render directly in the ComfyUI interface - no need to save files or use external viewers!
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite to verify installation:
|
||||
|
||||
```bash
|
||||
python test_nodes.py
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
============================================================
|
||||
🎉 ALL TESTS PASSED!
|
||||
============================================================
|
||||
|
||||
The following nodes are working correctly:
|
||||
✓ CreatePrimitive (cube, sphere, plane)
|
||||
✓ SaveMesh (OBJ format)
|
||||
✓ LoadMesh (OBJ format)
|
||||
✓ MeshInfoNode (with trimesh enhancements)
|
||||
✓ CGALRemeshNode (isotropic remeshing)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please feel free to submit issues or pull requests.
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test with `python test_nodes.py`
|
||||
5. Submit a pull request
|
||||
|
||||
### Adding New Nodes
|
||||
|
||||
Follow the pattern in `nodes.py`:
|
||||
|
||||
```python
|
||||
class MyCustomNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mesh": ("MESH",), # trimesh.Trimesh object
|
||||
"parameter": ("FLOAT", {"default": 1.0}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MESH",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "geompack/custom"
|
||||
|
||||
def process(self, mesh, parameter):
|
||||
# Your processing logic using trimesh API
|
||||
# ...
|
||||
return (processed_mesh,)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details
|
||||
|
||||
## Credits
|
||||
|
||||
- Built on [trimesh](https://trimesh.org/) by Michael Dawson-Haggerty
|
||||
- CGAL integration via [CGAL Python bindings](https://github.com/CGAL/cgal-swig-bindings)
|
||||
- [libigl](https://libigl.github.io/) by Alec Jacobson and others
|
||||
- Inspired by existing ComfyUI 3D tools and workflows
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: Report bugs or request features via GitHub Issues
|
||||
- **Discussions**: Join the discussion in GitHub Discussions
|
||||
- **Documentation**: See the [wiki](https://github.com/yourusername/ComfyUI-GeomPack/wiki) for detailed guides
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 0.2.0 (Current)
|
||||
- **Major Refactor**: Migrated from dictionary-based meshes to trimesh.Trimesh objects
|
||||
- **Added**: CGAL isotropic remeshing node
|
||||
- **Added**: Primitive creation (cube, sphere, plane)
|
||||
- **Enhanced**: Mesh Info now shows volume, watertight status, and more
|
||||
- **Improved**: File I/O with trimesh (supports more formats)
|
||||
- **Updated**: All nodes now use trimesh API
|
||||
- **Added**: Comprehensive test suite
|
||||
|
||||
### Version 0.1.0
|
||||
- Initial MVP release
|
||||
- Basic node structure and boilerplate
|
||||
- Example nodes demonstrating functionality
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ComfyUI GeomPack - Geometry Processing Custom Nodes
|
||||
|
||||
This package provides mesh processing nodes for ComfyUI using trimesh, CGAL, and Blender.
|
||||
Includes custom 3D preview widget powered by Three.js.
|
||||
"""
|
||||
|
||||
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
# Set web directory for JavaScript extensions (3D mesh preview widget)
|
||||
# This tells ComfyUI where to find our JavaScript files and HTML viewer
|
||||
# Files will be served at /extensions/ComfyUI-GeomPack/*
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
# Export the mappings so ComfyUI can discover the nodes
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
Mesh utilities for ComfyUI GeomPack nodes
|
||||
Handles mesh data structures, I/O, and processing using trimesh and seagullmesh (CGAL)
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import trimesh
|
||||
import igl
|
||||
import os
|
||||
from typing import Tuple, Optional
|
||||
|
||||
# PyMeshLab for remeshing (TODO: will switch back to CGAL eventually)
|
||||
try:
|
||||
import pymeshlab
|
||||
PYMESHLAB_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYMESHLAB_AVAILABLE = False
|
||||
print("[mesh_utils] Warning: pymeshlab not available. Install with: pip install pymeshlab")
|
||||
|
||||
|
||||
def load_mesh_file(file_path: str) -> Tuple[Optional[trimesh.Trimesh], str]:
|
||||
"""
|
||||
Load a mesh from file.
|
||||
|
||||
Ensures the returned mesh has only triangular faces and is properly processed.
|
||||
|
||||
Args:
|
||||
file_path: Path to mesh file (OBJ, PLY, STL, OFF, etc.)
|
||||
|
||||
Returns:
|
||||
Tuple of (mesh, error_message)
|
||||
"""
|
||||
if not os.path.exists(file_path):
|
||||
return None, f"File not found: {file_path}"
|
||||
|
||||
try:
|
||||
print(f"[load_mesh_file] Loading: {file_path}")
|
||||
|
||||
# Try to load with trimesh first (supports many formats)
|
||||
loaded = trimesh.load(file_path, force='mesh')
|
||||
|
||||
print(f"[load_mesh_file] Loaded type: {type(loaded).__name__}")
|
||||
|
||||
# Handle case where trimesh.load returns a Scene instead of a mesh
|
||||
if isinstance(loaded, trimesh.Scene):
|
||||
print(f"[load_mesh_file] Converting Scene to single mesh (scene has {len(loaded.geometry)} geometries)")
|
||||
# If it's a scene, dump it to a single mesh
|
||||
mesh = loaded.dump(concatenate=True)
|
||||
else:
|
||||
mesh = loaded
|
||||
|
||||
if mesh is None or len(mesh.vertices) == 0 or len(mesh.faces) == 0:
|
||||
return None, f"Failed to read mesh or mesh is empty: {file_path}"
|
||||
|
||||
print(f"[load_mesh_file] Initial mesh: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
|
||||
# Ensure mesh is properly triangulated
|
||||
# Trimesh should handle this, but some file formats might have issues
|
||||
if hasattr(mesh, 'faces') and len(mesh.faces) > 0:
|
||||
# Check if faces are triangular
|
||||
if mesh.faces.shape[1] != 3:
|
||||
# Need to triangulate - this shouldn't normally happen but handle it
|
||||
print(f"[load_mesh_file] Warning: Mesh has non-triangular faces, triangulating...")
|
||||
# trimesh.Trimesh constructor should triangulate automatically with process=True
|
||||
mesh = trimesh.Trimesh(vertices=mesh.vertices, faces=mesh.faces, process=True)
|
||||
print(f"[load_mesh_file] After triangulation: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
|
||||
# Count before cleanup
|
||||
verts_before = len(mesh.vertices)
|
||||
faces_before = len(mesh.faces)
|
||||
|
||||
# Merge duplicate vertices and clean up
|
||||
mesh.merge_vertices()
|
||||
mesh.remove_duplicate_faces()
|
||||
mesh.remove_degenerate_faces()
|
||||
|
||||
verts_after = len(mesh.vertices)
|
||||
faces_after = len(mesh.faces)
|
||||
|
||||
if verts_before != verts_after or faces_before != faces_after:
|
||||
print(f"[load_mesh_file] Cleanup: {verts_before}->{verts_after} vertices, {faces_before}->{faces_after} faces")
|
||||
print(f"[load_mesh_file] Removed: {verts_before - verts_after} duplicate vertices, {faces_before - faces_after} bad faces")
|
||||
|
||||
# Store file metadata
|
||||
mesh.metadata['file_path'] = file_path
|
||||
mesh.metadata['file_name'] = os.path.basename(file_path)
|
||||
mesh.metadata['file_format'] = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
print(f"[load_mesh_file] ✓ Successfully loaded: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
return mesh, ""
|
||||
|
||||
except Exception as e:
|
||||
print(f"[load_mesh_file] Trimesh failed: {str(e)}, trying libigl fallback...")
|
||||
# Fallback to libigl
|
||||
try:
|
||||
v, f = igl.read_triangle_mesh(file_path)
|
||||
if v is None or f is None or len(v) == 0 or len(f) == 0:
|
||||
return None, f"Failed to read mesh: {file_path}"
|
||||
|
||||
print(f"[load_mesh_file] libigl loaded: {len(v)} vertices, {len(f)} faces")
|
||||
|
||||
mesh = trimesh.Trimesh(vertices=v, faces=f, process=True)
|
||||
|
||||
# Count before cleanup
|
||||
verts_before = len(mesh.vertices)
|
||||
faces_before = len(mesh.faces)
|
||||
|
||||
# Clean up the mesh
|
||||
mesh.merge_vertices()
|
||||
mesh.remove_duplicate_faces()
|
||||
mesh.remove_degenerate_faces()
|
||||
|
||||
verts_after = len(mesh.vertices)
|
||||
faces_after = len(mesh.faces)
|
||||
|
||||
if verts_before != verts_after or faces_before != faces_after:
|
||||
print(f"[load_mesh_file] Cleanup: {verts_before}->{verts_after} vertices, {faces_before}->{faces_after} faces")
|
||||
|
||||
# Store metadata
|
||||
mesh.metadata['file_path'] = file_path
|
||||
mesh.metadata['file_name'] = os.path.basename(file_path)
|
||||
mesh.metadata['file_format'] = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
print(f"[load_mesh_file] ✓ Successfully loaded via libigl: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
return mesh, ""
|
||||
except Exception as e2:
|
||||
print(f"[load_mesh_file] ✗ Both loaders failed!")
|
||||
return None, f"Error loading mesh: {str(e)}; Fallback error: {str(e2)}"
|
||||
|
||||
|
||||
def save_mesh_file(mesh: trimesh.Trimesh, file_path: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Save a mesh to file.
|
||||
|
||||
Args:
|
||||
mesh: Trimesh object
|
||||
file_path: Output file path
|
||||
|
||||
Returns:
|
||||
Tuple of (success, error_message)
|
||||
"""
|
||||
if not isinstance(mesh, trimesh.Trimesh):
|
||||
return False, "Input must be a trimesh.Trimesh object"
|
||||
|
||||
if len(mesh.vertices) == 0 or len(mesh.faces) == 0:
|
||||
return False, "Mesh is empty"
|
||||
|
||||
try:
|
||||
# Ensure output directory exists
|
||||
output_dir = os.path.dirname(file_path)
|
||||
if output_dir and not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Export the mesh
|
||||
mesh.export(file_path)
|
||||
|
||||
return True, ""
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Error saving mesh: {str(e)}"
|
||||
|
||||
|
||||
def create_cube(size: float = 1.0) -> trimesh.Trimesh:
|
||||
"""
|
||||
Create a cube mesh.
|
||||
|
||||
Args:
|
||||
size: Side length of the cube
|
||||
|
||||
Returns:
|
||||
Trimesh object
|
||||
"""
|
||||
mesh = trimesh.creation.box(extents=[size, size, size])
|
||||
mesh.metadata['primitive_type'] = 'cube'
|
||||
mesh.metadata['size'] = size
|
||||
return mesh
|
||||
|
||||
|
||||
def create_sphere(radius: float = 1.0, subdivisions: int = 2) -> trimesh.Trimesh:
|
||||
"""
|
||||
Create a sphere mesh using icosphere subdivision.
|
||||
|
||||
Args:
|
||||
radius: Radius of the sphere
|
||||
subdivisions: Number of subdivision levels (0-4 recommended)
|
||||
|
||||
Returns:
|
||||
Trimesh object
|
||||
"""
|
||||
mesh = trimesh.creation.icosphere(subdivisions=subdivisions, radius=radius)
|
||||
mesh.metadata['primitive_type'] = 'sphere'
|
||||
mesh.metadata['radius'] = radius
|
||||
mesh.metadata['subdivisions'] = subdivisions
|
||||
return mesh
|
||||
|
||||
|
||||
def create_plane(size: float = 1.0, subdivisions: int = 1) -> trimesh.Trimesh:
|
||||
"""
|
||||
Create a plane mesh.
|
||||
|
||||
Args:
|
||||
size: Side length of the plane
|
||||
subdivisions: Number of subdivisions per side
|
||||
|
||||
Returns:
|
||||
Trimesh object
|
||||
"""
|
||||
n = subdivisions + 1
|
||||
s = size / 2.0
|
||||
|
||||
# Create grid of vertices
|
||||
x = np.linspace(-s, s, n)
|
||||
y = np.linspace(-s, s, n)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
|
||||
vertices = np.stack([
|
||||
xx.flatten(),
|
||||
yy.flatten(),
|
||||
np.zeros(n * n)
|
||||
], axis=1).astype(np.float64)
|
||||
|
||||
# Create faces
|
||||
faces = []
|
||||
for i in range(n - 1):
|
||||
for j in range(n - 1):
|
||||
idx = i * n + j
|
||||
# Two triangles per quad
|
||||
faces.append([idx, idx + n, idx + n + 1])
|
||||
faces.append([idx, idx + n + 1, idx + 1])
|
||||
|
||||
faces = np.array(faces, dtype=np.int32)
|
||||
|
||||
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
||||
mesh.metadata['primitive_type'] = 'plane'
|
||||
mesh.metadata['size'] = size
|
||||
mesh.metadata['subdivisions'] = subdivisions
|
||||
return mesh
|
||||
|
||||
|
||||
def compute_mesh_info(mesh: trimesh.Trimesh) -> str:
|
||||
"""
|
||||
Generate a formatted string with mesh information.
|
||||
|
||||
Args:
|
||||
mesh: Trimesh object
|
||||
|
||||
Returns:
|
||||
Formatted info string
|
||||
"""
|
||||
if not isinstance(mesh, trimesh.Trimesh):
|
||||
return "Error: Input must be a trimesh.Trimesh object"
|
||||
|
||||
info = "=== Mesh Information ===\n\n"
|
||||
info += f"Vertices: {len(mesh.vertices):,}\n"
|
||||
info += f"Faces: {len(mesh.faces):,}\n"
|
||||
info += f"Edges: {len(mesh.edges):,}\n\n"
|
||||
|
||||
# Geometric properties
|
||||
info += f"Volume: {mesh.volume:.6f}\n"
|
||||
info += f"Surface Area: {mesh.area:.6f}\n"
|
||||
info += f"Is Watertight: {mesh.is_watertight}\n"
|
||||
info += f"Is Winding Consistent: {mesh.is_winding_consistent}\n\n"
|
||||
|
||||
# Bounding box
|
||||
bounds = mesh.bounds
|
||||
center = mesh.centroid
|
||||
extents = mesh.extents
|
||||
|
||||
info += "Bounding Box:\n"
|
||||
info += f" Min: [{bounds[0][0]:.3f}, {bounds[0][1]:.3f}, {bounds[0][2]:.3f}]\n"
|
||||
info += f" Max: [{bounds[1][0]:.3f}, {bounds[1][1]:.3f}, {bounds[1][2]:.3f}]\n"
|
||||
info += f" Center: [{center[0]:.3f}, {center[1]:.3f}, {center[2]:.3f}]\n"
|
||||
info += f" Extents: [{extents[0]:.3f}, {extents[1]:.3f}, {extents[2]:.3f}]\n\n"
|
||||
|
||||
# Additional data
|
||||
if hasattr(mesh.visual, 'vertex_colors') and mesh.visual.vertex_colors is not None:
|
||||
info += f"Vertex Colors: Yes\n"
|
||||
|
||||
if mesh.vertex_normals is not None and len(mesh.vertex_normals) > 0:
|
||||
info += f"Vertex Normals: Yes\n"
|
||||
|
||||
# Metadata
|
||||
if mesh.metadata:
|
||||
info += "\nMetadata:\n"
|
||||
for key, value in mesh.metadata.items():
|
||||
info += f" {key}: {value}\n"
|
||||
|
||||
return info
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Remeshing via PyMeshLab (TODO: will switch back to CGAL eventually)
|
||||
# ============================================================================
|
||||
|
||||
def cgal_isotropic_remesh(
|
||||
mesh: trimesh.Trimesh,
|
||||
target_edge_length: float,
|
||||
iterations: int = 3
|
||||
) -> Tuple[Optional[trimesh.Trimesh], str]:
|
||||
"""
|
||||
Apply isotropic remeshing to create uniform triangle sizes.
|
||||
|
||||
Currently uses PyMeshLab. Will switch back to CGAL once CGAL Python bindings
|
||||
issues are resolved.
|
||||
|
||||
Args:
|
||||
mesh: Input trimesh object
|
||||
target_edge_length: Target edge length for remeshed triangles
|
||||
iterations: Number of remeshing iterations (default: 3)
|
||||
|
||||
Returns:
|
||||
Tuple of (remeshed_mesh, error_message)
|
||||
"""
|
||||
print(f"[cgal_isotropic_remesh] ===== Starting Isotropic Remeshing (via PyMeshLab) =====")
|
||||
print(f"[cgal_isotropic_remesh] Input mesh: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
print(f"[cgal_isotropic_remesh] Parameters:")
|
||||
print(f"[cgal_isotropic_remesh] target_edge_length: {target_edge_length}")
|
||||
print(f"[cgal_isotropic_remesh] iterations: {iterations}")
|
||||
|
||||
if not PYMESHLAB_AVAILABLE:
|
||||
return None, "pymeshlab is not installed. Install with: pip install pymeshlab"
|
||||
|
||||
if not isinstance(mesh, trimesh.Trimesh):
|
||||
return None, "Input must be a trimesh.Trimesh object"
|
||||
|
||||
if len(mesh.vertices) == 0 or len(mesh.faces) == 0:
|
||||
return None, "Mesh is empty"
|
||||
|
||||
if target_edge_length <= 0:
|
||||
return None, f"Target edge length must be positive, got {target_edge_length}"
|
||||
|
||||
if iterations < 1:
|
||||
return None, f"Iterations must be at least 1, got {iterations}"
|
||||
|
||||
try:
|
||||
# Convert trimesh to PyMeshLab
|
||||
print(f"[cgal_isotropic_remesh] Converting to PyMeshLab format...")
|
||||
ms = pymeshlab.MeshSet()
|
||||
|
||||
# Create PyMeshLab mesh from numpy arrays
|
||||
pml_mesh = pymeshlab.Mesh(
|
||||
vertex_matrix=mesh.vertices,
|
||||
face_matrix=mesh.faces
|
||||
)
|
||||
ms.add_mesh(pml_mesh)
|
||||
|
||||
print(f"[cgal_isotropic_remesh] Applying isotropic remeshing...")
|
||||
# PyMeshLab's isotropic remeshing
|
||||
# targetlen is specified as PercentageValue (percentage of bounding box diagonal)
|
||||
# We need to convert our absolute target_edge_length to a percentage
|
||||
bbox_diag = np.linalg.norm(mesh.bounds[1] - mesh.bounds[0])
|
||||
target_pct = (target_edge_length / bbox_diag) * 100.0
|
||||
|
||||
ms.meshing_isotropic_explicit_remeshing(
|
||||
targetlen=pymeshlab.PercentageValue(target_pct),
|
||||
iterations=iterations,
|
||||
adaptive=False
|
||||
)
|
||||
|
||||
# Convert back to trimesh
|
||||
print(f"[cgal_isotropic_remesh] Converting back to trimesh...")
|
||||
remeshed_pml = ms.current_mesh()
|
||||
remeshed_mesh = trimesh.Trimesh(
|
||||
vertices=remeshed_pml.vertex_matrix(),
|
||||
faces=remeshed_pml.face_matrix()
|
||||
)
|
||||
|
||||
# Preserve metadata
|
||||
remeshed_mesh.metadata = mesh.metadata.copy()
|
||||
remeshed_mesh.metadata['remeshing'] = {
|
||||
'algorithm': 'pymeshlab_isotropic',
|
||||
'target_edge_length': target_edge_length,
|
||||
'target_percentage': target_pct,
|
||||
'iterations': iterations,
|
||||
'original_vertices': len(mesh.vertices),
|
||||
'original_faces': len(mesh.faces),
|
||||
'remeshed_vertices': len(remeshed_mesh.vertices),
|
||||
'remeshed_faces': len(remeshed_mesh.faces)
|
||||
}
|
||||
|
||||
# Calculate statistics
|
||||
vertex_change = len(remeshed_mesh.vertices) - len(mesh.vertices)
|
||||
face_change = len(remeshed_mesh.faces) - len(mesh.faces)
|
||||
vertex_pct = (vertex_change / len(mesh.vertices)) * 100 if len(mesh.vertices) > 0 else 0
|
||||
face_pct = (face_change / len(mesh.faces)) * 100 if len(mesh.faces) > 0 else 0
|
||||
|
||||
print(f"[cgal_isotropic_remesh] ===== Remeshing Complete =====")
|
||||
print(f"[cgal_isotropic_remesh] Results:")
|
||||
print(f"[cgal_isotropic_remesh] Vertices: {len(mesh.vertices)} -> {len(remeshed_mesh.vertices)} ({vertex_change:+d}, {vertex_pct:+.1f}%)")
|
||||
print(f"[cgal_isotropic_remesh] Faces: {len(mesh.faces)} -> {len(remeshed_mesh.faces)} ({face_change:+d}, {face_pct:+.1f}%)")
|
||||
|
||||
return remeshed_mesh, ""
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None, f"Error during remeshing: {str(e)}"
|
||||
@@ -0,0 +1,25 @@
|
||||
[project]
|
||||
name = "comfyui-libigl"
|
||||
description = "libigl geometry processing wrapper for ComfyUI - mesh manipulation and analysis nodes"
|
||||
version = "0.1.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"libigl>=2.6.1",
|
||||
"numpy>=1.21.0",
|
||||
"scipy>=1.7.0",
|
||||
"matplotlib>=3.5.0",
|
||||
"Pillow>=9.0.0"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/yourusername/ComfyUI-libigl"
|
||||
Documentation = "https://github.com/yourusername/ComfyUI-libigl/blob/main/README.md"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "your-publisher-id"
|
||||
DisplayName = "ComfyUI libigl"
|
||||
Icon = ""
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
@@ -0,0 +1,10 @@
|
||||
# Core dependencies
|
||||
libigl>=2.6.1
|
||||
numpy>=1.21.0
|
||||
scipy>=1.7.0
|
||||
trimesh>=3.15.0
|
||||
pymeshlab>=2022.2
|
||||
|
||||
# Visualization
|
||||
matplotlib>=3.5.0
|
||||
Pillow>=9.0.0
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Test script to verify the GeomPack nodes work correctly
|
||||
Now testing with trimesh and CGAL functionality
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add current directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import numpy as np
|
||||
import trimesh
|
||||
from nodes import CreatePrimitive, SaveMesh, LoadMesh, MeshInfoNode, CGALRemeshNode
|
||||
|
||||
|
||||
def test_create_primitive():
|
||||
"""Test creating primitive shapes"""
|
||||
print("=" * 60)
|
||||
print("TEST 1: Create Primitive")
|
||||
print("=" * 60)
|
||||
|
||||
node = CreatePrimitive()
|
||||
|
||||
# Test cube
|
||||
print("\n[1/3] Testing cube creation...")
|
||||
mesh, = node.create_primitive("cube", 2.0, subdivisions=0)
|
||||
assert isinstance(mesh, trimesh.Trimesh), "Mesh should be a trimesh.Trimesh object"
|
||||
print(f"✓ Cube created: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
assert len(mesh.vertices) == 8, "Cube should have 8 vertices"
|
||||
assert len(mesh.faces) == 12, "Cube should have 12 faces"
|
||||
|
||||
# Test sphere
|
||||
print("\n[2/3] Testing sphere creation...")
|
||||
mesh, = node.create_primitive("sphere", 1.0, subdivisions=2)
|
||||
assert isinstance(mesh, trimesh.Trimesh), "Mesh should be a trimesh.Trimesh object"
|
||||
print(f"✓ Sphere created: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
assert len(mesh.vertices) > 0, "Sphere should have vertices"
|
||||
assert len(mesh.faces) > 0, "Sphere should have faces"
|
||||
assert mesh.is_watertight, "Sphere should be watertight"
|
||||
|
||||
# Test plane
|
||||
print("\n[3/3] Testing plane creation...")
|
||||
mesh, = node.create_primitive("plane", 1.0, subdivisions=3)
|
||||
assert isinstance(mesh, trimesh.Trimesh), "Mesh should be a trimesh.Trimesh object"
|
||||
print(f"✓ Plane created: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
assert len(mesh.vertices) == 16, "Plane with 3 subdivisions should have 16 vertices"
|
||||
|
||||
print("\n✅ All primitive creation tests passed!")
|
||||
return mesh
|
||||
|
||||
|
||||
def test_save_and_load(mesh):
|
||||
"""Test saving and loading mesh"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 2: Save and Load Mesh")
|
||||
print("=" * 60)
|
||||
|
||||
# Save mesh
|
||||
print("\n[1/2] Testing mesh save...")
|
||||
save_node = SaveMesh()
|
||||
test_file = "/tmp/test_mesh.obj"
|
||||
status, = save_node.save_mesh(mesh, test_file)
|
||||
print(f"✓ Mesh saved to {test_file}")
|
||||
print(f" Status: {status}")
|
||||
|
||||
# Load mesh
|
||||
print("\n[2/2] Testing mesh load...")
|
||||
load_node = LoadMesh()
|
||||
loaded_mesh, = load_node.load_mesh(test_file)
|
||||
assert isinstance(loaded_mesh, trimesh.Trimesh), "Loaded mesh should be a trimesh.Trimesh object"
|
||||
print(f"✓ Mesh loaded from {test_file}")
|
||||
print(f" Vertices: {len(loaded_mesh.vertices)}")
|
||||
print(f" Faces: {len(loaded_mesh.faces)}")
|
||||
|
||||
# Verify
|
||||
assert len(loaded_mesh.vertices) == len(mesh.vertices), "Vertex count should match"
|
||||
assert len(loaded_mesh.faces) == len(mesh.faces), "Face count should match"
|
||||
|
||||
print("\n✅ Save/Load test passed!")
|
||||
return loaded_mesh
|
||||
|
||||
|
||||
def test_mesh_info(mesh):
|
||||
"""Test mesh info node"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 3: Mesh Info")
|
||||
print("=" * 60)
|
||||
|
||||
info_node = MeshInfoNode()
|
||||
info, = info_node.get_mesh_info(mesh)
|
||||
|
||||
print("\n" + info)
|
||||
assert "Vertices:" in info, "Info should contain vertex count"
|
||||
assert "Faces:" in info, "Info should contain face count"
|
||||
assert "Volume:" in info, "Info should contain volume"
|
||||
assert "Is Watertight:" in info, "Info should contain watertight status"
|
||||
|
||||
print("\n✅ Mesh info test passed!")
|
||||
|
||||
|
||||
def test_cgal_remeshing():
|
||||
"""Test CGAL remeshing node"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 4: CGAL Isotropic Remeshing")
|
||||
print("=" * 60)
|
||||
|
||||
# Create a test mesh (cube)
|
||||
print("\n[1/3] Creating test cube...")
|
||||
create_node = CreatePrimitive()
|
||||
mesh, = create_node.create_primitive("cube", 1.0, subdivisions=0)
|
||||
original_vertices = len(mesh.vertices)
|
||||
original_faces = len(mesh.faces)
|
||||
print(f"✓ Original mesh: {original_vertices} vertices, {original_faces} faces")
|
||||
|
||||
# Apply remeshing
|
||||
print("\n[2/3] Applying CGAL remeshing...")
|
||||
remesh_node = CGALRemeshNode()
|
||||
remeshed_mesh, = remesh_node.remesh(mesh, target_edge_length=0.15, iterations=3)
|
||||
assert isinstance(remeshed_mesh, trimesh.Trimesh), "Remeshed mesh should be a trimesh.Trimesh object"
|
||||
print(f"✓ Remeshed mesh: {len(remeshed_mesh.vertices)} vertices, {len(remeshed_mesh.faces)} faces")
|
||||
|
||||
# Verify remeshing worked
|
||||
print("\n[3/3] Verifying remeshing results...")
|
||||
assert len(remeshed_mesh.vertices) > original_vertices, "Remeshing should increase vertex count"
|
||||
assert len(remeshed_mesh.faces) > original_faces, "Remeshing should increase face count"
|
||||
assert remeshed_mesh.is_watertight, "Remeshed mesh should be watertight"
|
||||
|
||||
# Check that volume is approximately preserved
|
||||
volume_diff = abs(remeshed_mesh.volume - mesh.volume)
|
||||
volume_error = volume_diff / mesh.volume
|
||||
print(f" Original volume: {mesh.volume:.6f}")
|
||||
print(f" Remeshed volume: {remeshed_mesh.volume:.6f}")
|
||||
print(f" Volume error: {volume_error*100:.2f}%")
|
||||
assert volume_error < 0.05, f"Volume should be approximately preserved (error: {volume_error*100:.2f}%)"
|
||||
|
||||
print("\n✅ CGAL remeshing test passed!")
|
||||
return remeshed_mesh
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("\n" + "=" * 60)
|
||||
print("ComfyUI GeomPack Node Tests")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Test 1: Create primitives
|
||||
mesh = test_create_primitive()
|
||||
|
||||
# Test 2: Save and load
|
||||
loaded_mesh = test_save_and_load(mesh)
|
||||
|
||||
# Test 3: Mesh info
|
||||
test_mesh_info(loaded_mesh)
|
||||
|
||||
# Test 4: CGAL remeshing
|
||||
remeshed_mesh = test_cgal_remeshing()
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("🎉 ALL TESTS PASSED!")
|
||||
print("=" * 60)
|
||||
print("\nThe following nodes are working correctly:")
|
||||
print(" ✓ CreatePrimitive (cube, sphere, plane)")
|
||||
print(" ✓ SaveMesh (OBJ format)")
|
||||
print(" ✓ LoadMesh (OBJ format)")
|
||||
print(" ✓ MeshInfoNode (with trimesh enhancements)")
|
||||
print(" ✓ CGALRemeshNode (isotropic remeshing)")
|
||||
print("\nYou can now use these nodes in ComfyUI!")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print("\n" + "=" * 60)
|
||||
print("❌ TEST FAILED!")
|
||||
print("=" * 60)
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ComfyUI GeomPack - 3D Mesh Preview Widget
|
||||
* Minimal implementation based on ComfyUI-3D-Pack pattern
|
||||
*/
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
console.log("[GeomPack] Loading mesh preview extension...");
|
||||
|
||||
app.registerExtension({
|
||||
name: "geompack.meshpreview",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "GeomPackPreviewMesh") {
|
||||
console.log("[GeomPack] Registering Preview Mesh node");
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
console.log("[GeomPack] Node created, adding widget");
|
||||
|
||||
// Create iframe for 3D viewer
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.style.width = "100%";
|
||||
iframe.style.height = "500px";
|
||||
iframe.style.border = "none";
|
||||
iframe.style.backgroundColor = "#2a2a2a";
|
||||
|
||||
// Point to our HTML viewer (with cache buster)
|
||||
iframe.src = "/extensions/ComfyUI-GeomPack/viewer.html?v=" + Date.now();
|
||||
|
||||
// Add widget
|
||||
const widget = this.addDOMWidget("preview", "MESH_PREVIEW", iframe);
|
||||
widget.computeSize = () => [512, 520];
|
||||
|
||||
// Store iframe reference
|
||||
this.meshViewerIframe = iframe;
|
||||
|
||||
// Handle execution
|
||||
const onExecuted = this.onExecuted;
|
||||
this.onExecuted = function(message) {
|
||||
console.log("[GeomPack] onExecuted called with message:", message);
|
||||
onExecuted?.apply(this, arguments);
|
||||
|
||||
// The message IS the UI data (not message.ui)
|
||||
if (message?.mesh_file && message.mesh_file[0]) {
|
||||
const filename = message.mesh_file[0];
|
||||
console.log(`[GeomPack] Loading mesh: ${filename}`);
|
||||
|
||||
// ComfyUI serves output files via /view API endpoint
|
||||
const filepath = `/view?filename=${encodeURIComponent(filename)}&type=output&subfolder=`;
|
||||
|
||||
// Send message to iframe (with delay to ensure iframe is loaded)
|
||||
setTimeout(() => {
|
||||
if (iframe.contentWindow) {
|
||||
console.log(`[GeomPack] Sending postMessage to iframe: ${filepath}`);
|
||||
iframe.contentWindow.postMessage({
|
||||
type: "LOAD_MESH",
|
||||
filepath: filepath,
|
||||
timestamp: Date.now()
|
||||
}, "*");
|
||||
} else {
|
||||
console.error("[GeomPack] Iframe contentWindow not available");
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
console.log("[GeomPack] No mesh_file in message data. Keys:", Object.keys(message || {}));
|
||||
}
|
||||
};
|
||||
|
||||
return r;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[GeomPack] Mesh preview extension registered");
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ComfyUI GeomPack - VTK.js Mesh Preview Widget
|
||||
* Scientific visualization with VTK.js
|
||||
*/
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
console.log("[GeomPack] Loading VTK.js mesh preview extension...");
|
||||
|
||||
app.registerExtension({
|
||||
name: "geompack.meshpreview.vtk",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "GeomPackPreviewMeshVTK") {
|
||||
console.log("[GeomPack] Registering Preview Mesh (VTK) node");
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
console.log("[GeomPack] VTK node created, adding widget");
|
||||
|
||||
// Create iframe for VTK.js viewer
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.style.width = "100%";
|
||||
iframe.style.height = "500px";
|
||||
iframe.style.border = "none";
|
||||
iframe.style.backgroundColor = "#2a2a2a";
|
||||
|
||||
// Point to VTK.js HTML viewer (with cache buster)
|
||||
iframe.src = "/extensions/ComfyUI-GeomPack/viewer_vtk.html?v=" + Date.now();
|
||||
|
||||
// Add widget
|
||||
const widget = this.addDOMWidget("preview_vtk", "MESH_PREVIEW_VTK", iframe);
|
||||
widget.computeSize = () => [512, 520];
|
||||
|
||||
// Store iframe reference
|
||||
this.meshViewerIframeVTK = iframe;
|
||||
|
||||
// Handle execution
|
||||
const onExecuted = this.onExecuted;
|
||||
this.onExecuted = function(message) {
|
||||
console.log("[GeomPack VTK] onExecuted called with message:", message);
|
||||
onExecuted?.apply(this, arguments);
|
||||
|
||||
// The message IS the UI data (not message.ui)
|
||||
if (message?.mesh_file && message.mesh_file[0]) {
|
||||
const filename = message.mesh_file[0];
|
||||
console.log(`[GeomPack VTK] Loading mesh: ${filename}`);
|
||||
|
||||
// ComfyUI serves output files via /view API endpoint
|
||||
const filepath = `/view?filename=${encodeURIComponent(filename)}&type=output&subfolder=`;
|
||||
|
||||
// Send message to iframe (with delay to ensure iframe is loaded)
|
||||
setTimeout(() => {
|
||||
if (iframe.contentWindow) {
|
||||
console.log(`[GeomPack VTK] Sending postMessage to iframe: ${filepath}`);
|
||||
iframe.contentWindow.postMessage({
|
||||
type: "LOAD_MESH",
|
||||
filepath: filepath,
|
||||
timestamp: Date.now()
|
||||
}, "*");
|
||||
} else {
|
||||
console.error("[GeomPack VTK] Iframe contentWindow not available");
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
console.log("[GeomPack VTK] No mesh_file in message data. Keys:", Object.keys(message || {}));
|
||||
}
|
||||
};
|
||||
|
||||
return r;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[GeomPack] VTK.js mesh preview extension registered");
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>GeomPack 3D Viewer</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #2a2a2a;
|
||||
font-family: monospace;
|
||||
}
|
||||
#canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
#loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="loading">Ready to load mesh</div>
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
|
||||
|
||||
console.log('[GeomPack Viewer] Initializing...');
|
||||
|
||||
const canvas = document.getElementById('canvas');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
// Scene
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x2a2a2a);
|
||||
|
||||
// Camera
|
||||
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.set(2, 2, 2);
|
||||
|
||||
// Renderer
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
// Controls
|
||||
const controls = new OrbitControls(camera, canvas);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
|
||||
// Lighting
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const dirLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
dirLight1.position.set(5, 10, 7);
|
||||
scene.add(dirLight1);
|
||||
|
||||
const dirLight2 = new THREE.DirectionalLight(0xffffff, 0.4);
|
||||
dirLight2.position.set(-5, -5, -7);
|
||||
scene.add(dirLight2);
|
||||
|
||||
// Grid
|
||||
const grid = new THREE.GridHelper(10, 10, 0x444444, 0x222222);
|
||||
scene.add(grid);
|
||||
|
||||
// Axes
|
||||
const axes = new THREE.AxesHelper(1);
|
||||
scene.add(axes);
|
||||
|
||||
// Current mesh
|
||||
let currentMesh = null;
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
// Load mesh function
|
||||
function loadMesh(filepath) {
|
||||
console.log('[GeomPack Viewer] Loading:', filepath);
|
||||
loading.textContent = 'Loading mesh...';
|
||||
loading.style.display = 'block';
|
||||
|
||||
// Remove old mesh
|
||||
if (currentMesh) {
|
||||
scene.remove(currentMesh);
|
||||
currentMesh = null;
|
||||
}
|
||||
|
||||
// Check if filepath contains .glb or .gltf (even with query params)
|
||||
const isGLB = filepath.includes('.glb') || filepath.includes('.gltf');
|
||||
const loader = isGLB ? new GLTFLoader() : new OBJLoader();
|
||||
|
||||
console.log('[GeomPack Viewer] File type detected:', isGLB ? 'GLB/GLTF' : 'OBJ');
|
||||
|
||||
loader.load(
|
||||
filepath,
|
||||
(result) => {
|
||||
currentMesh = isGLB ? result.scene : result;
|
||||
|
||||
// Center mesh
|
||||
const box = new THREE.Box3().setFromObject(currentMesh);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
currentMesh.position.sub(center);
|
||||
|
||||
// Add material if needed
|
||||
currentMesh.traverse((child) => {
|
||||
if (child.isMesh && !child.material) {
|
||||
child.material = new THREE.MeshStandardMaterial({
|
||||
color: 0x88aacc,
|
||||
metalness: 0.3,
|
||||
roughness: 0.7
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add to scene
|
||||
scene.add(currentMesh);
|
||||
|
||||
// Adjust camera
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const distance = maxDim * 2.5;
|
||||
camera.position.set(distance, distance, distance);
|
||||
camera.lookAt(0, 0, 0);
|
||||
controls.target.set(0, 0, 0);
|
||||
controls.update();
|
||||
|
||||
loading.style.display = 'none';
|
||||
console.log('[GeomPack Viewer] Mesh loaded successfully');
|
||||
},
|
||||
(progress) => {
|
||||
if (progress.lengthComputable) {
|
||||
const percent = (progress.loaded / progress.total * 100).toFixed(0);
|
||||
loading.textContent = `Loading mesh... ${percent}%`;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('[GeomPack Viewer] Error loading mesh:', error);
|
||||
loading.textContent = 'Error loading mesh';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'LOAD_MESH') {
|
||||
console.log('[GeomPack Viewer] Received LOAD_MESH message:', event.data.filepath);
|
||||
loadMesh(event.data.filepath);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[GeomPack Viewer] Ready');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>GeomPack VTK.js Viewer</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #2a2a2a;
|
||||
font-family: monospace;
|
||||
}
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
#loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<div id="loading">Ready to load mesh</div>
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@kitware/vtk.js/": "https://cdn.skypack.dev/@kitware/vtk.js@29.5.0/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
|
||||
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
|
||||
import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
|
||||
import vtkSTLReader from '@kitware/vtk.js/IO/Geometry/STLReader';
|
||||
import vtkOBJReader from '@kitware/vtk.js/IO/Misc/OBJReader';
|
||||
import vtkInteractorStyleTrackballCamera from '@kitware/vtk.js/Interaction/Style/InteractorStyleTrackballCamera';
|
||||
|
||||
console.log('[GeomPack VTK Viewer] Initializing...');
|
||||
|
||||
const loading = document.getElementById('loading');
|
||||
const container = document.getElementById('container');
|
||||
|
||||
// Create full screen renderer
|
||||
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
|
||||
background: [0.16, 0.16, 0.16], // #2a2a2a
|
||||
container: container,
|
||||
});
|
||||
|
||||
const renderer = fullScreenRenderer.getRenderer();
|
||||
const renderWindow = fullScreenRenderer.getRenderWindow();
|
||||
const interactor = renderWindow.getInteractor();
|
||||
|
||||
// Set up trackball camera controls
|
||||
const interactorStyle = vtkInteractorStyleTrackballCamera.newInstance();
|
||||
interactor.setInteractorStyle(interactorStyle);
|
||||
|
||||
// Create actor and mapper for mesh
|
||||
const actor = vtkActor.newInstance();
|
||||
const mapper = vtkMapper.newInstance();
|
||||
actor.setMapper(mapper);
|
||||
|
||||
// Configure appearance
|
||||
actor.getProperty().setEdgeVisibility(false);
|
||||
actor.getProperty().setColor(0.53, 0.67, 0.8); // Light blue-gray
|
||||
actor.getProperty().setSpecular(0.3);
|
||||
actor.getProperty().setSpecularPower(30);
|
||||
|
||||
// Add actor to scene
|
||||
renderer.addActor(actor);
|
||||
|
||||
// Current reader
|
||||
let currentReader = null;
|
||||
|
||||
// Load mesh function
|
||||
async function loadMesh(filepath) {
|
||||
console.log('[GeomPack VTK Viewer] Loading:', filepath);
|
||||
loading.textContent = 'Loading mesh...';
|
||||
loading.style.display = 'block';
|
||||
|
||||
try {
|
||||
// Determine file type
|
||||
const isSTL = filepath.includes('.stl');
|
||||
const isOBJ = filepath.includes('.obj');
|
||||
|
||||
// Create appropriate reader
|
||||
if (isSTL) {
|
||||
console.log('[GeomPack VTK Viewer] File type: STL');
|
||||
currentReader = vtkSTLReader.newInstance();
|
||||
} else if (isOBJ) {
|
||||
console.log('[GeomPack VTK Viewer] File type: OBJ');
|
||||
currentReader = vtkOBJReader.newInstance();
|
||||
} else {
|
||||
throw new Error(`Unsupported file format: ${filepath}`);
|
||||
}
|
||||
|
||||
// Fetch the file
|
||||
const response = await fetch(filepath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Get file data
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
||||
// Parse with appropriate reader
|
||||
if (isSTL) {
|
||||
currentReader.parseAsArrayBuffer(arrayBuffer);
|
||||
} else if (isOBJ) {
|
||||
const text = new TextDecoder().decode(arrayBuffer);
|
||||
currentReader.parseAsText(text);
|
||||
}
|
||||
|
||||
// Get output data
|
||||
const polydata = currentReader.getOutputData();
|
||||
|
||||
if (!polydata) {
|
||||
throw new Error('Failed to parse mesh data');
|
||||
}
|
||||
|
||||
console.log('[GeomPack VTK Viewer] Loaded mesh:',
|
||||
polydata.getNumberOfPoints(), 'points,',
|
||||
polydata.getNumberOfPolys(), 'polygons');
|
||||
|
||||
// Set mapper input
|
||||
mapper.setInputData(polydata);
|
||||
|
||||
// Reset camera to fit mesh
|
||||
renderer.resetCamera();
|
||||
renderWindow.render();
|
||||
|
||||
loading.style.display = 'none';
|
||||
console.log('[GeomPack VTK Viewer] Mesh loaded successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GeomPack VTK Viewer] Error loading mesh:', error);
|
||||
loading.textContent = `Error loading mesh: ${error.message}`;
|
||||
loading.style.color = '#ff6b6b';
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'LOAD_MESH') {
|
||||
console.log('[GeomPack VTK Viewer] Received LOAD_MESH message:', event.data.filepath);
|
||||
loadMesh(event.data.filepath);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
renderWindow.resize();
|
||||
});
|
||||
|
||||
console.log('[GeomPack VTK Viewer] Ready');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"3": {
|
||||
"inputs": {
|
||||
"file_path": "bunny.stl"
|
||||
},
|
||||
"class_type": "GeomPackLoadMesh",
|
||||
"_meta": {
|
||||
"title": "Load Mesh"
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"inputs": {
|
||||
"model_file": [
|
||||
"5",
|
||||
0
|
||||
],
|
||||
"image": ""
|
||||
},
|
||||
"class_type": "Preview3D",
|
||||
"_meta": {
|
||||
"title": "Preview 3D"
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"inputs": {
|
||||
"file_path": "output.obj",
|
||||
"mesh": [
|
||||
"6",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "GeomPackSaveMesh",
|
||||
"_meta": {
|
||||
"title": "Save Mesh"
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"target_edge_length": 0.1,
|
||||
"iterations": 3,
|
||||
"mesh": [
|
||||
"3",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "GeomPackCGALRemesh",
|
||||
"_meta": {
|
||||
"title": "CGAL Remesh (Isotropic)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user