vtk viewer
This commit is contained in:
@@ -74,19 +74,43 @@ Create Primitive (cube) → Mesh Info → CGAL Remesh → Mesh Info → Save Mes
|
||||
- Bounding box and extents
|
||||
- Metadata
|
||||
|
||||
### geompack/analysis/distance
|
||||
- **Hausdorff Distance**: Compute Hausdorff distance between two meshes
|
||||
- Measures maximum distance between point sets
|
||||
- Returns symmetric and one-sided distances
|
||||
- Configurable sample count
|
||||
- Useful for measuring worst-case deviation
|
||||
- **Chamfer Distance**: Compute Chamfer distance between two meshes
|
||||
- Average of squared nearest neighbor distances
|
||||
- More sensitive to overall shape similarity
|
||||
- Configurable sample count
|
||||
- Useful for shape matching
|
||||
- **Compute SDF**: Generate Signed Distance Field for a mesh
|
||||
- Voxel-based representation
|
||||
- Negative values = inside, positive = outside
|
||||
- Configurable resolution (16-256³)
|
||||
- Useful for occupancy queries
|
||||
|
||||
### geompack/conversion
|
||||
- **Mesh to Point Cloud**: Sample points from mesh surface
|
||||
- Multiple sampling methods (uniform, even, face_weighted)
|
||||
- Optional normal computation
|
||||
- Configurable sample count
|
||||
- Returns POINT_CLOUD data type
|
||||
|
||||
### 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
|
||||
### geompack/pymeshlab
|
||||
- **PyMeshLab Remesh (Isotropic)**: Isotropic remeshing algorithm
|
||||
- Creates uniform triangle sizes
|
||||
- Target edge length control
|
||||
- Iteration count parameter
|
||||
- Preserves volume
|
||||
- Based on Botsch & Kobbelt (2004) algorithm
|
||||
- Fast, no Blender dependency
|
||||
|
||||
### geompack/transforms
|
||||
- **Center Mesh**: Center mesh at origin (0, 0, 0)
|
||||
@@ -106,11 +130,35 @@ Create Primitive (cube) → Mesh Info → CGAL Remesh → Mesh Info → Save Mes
|
||||
- Better for scientific visualization and analysis
|
||||
- Excellent for large meshes and datasets
|
||||
|
||||
### geompack/blender
|
||||
### geompack/uv
|
||||
- **xAtlas UV Unwrap**: Fast UV unwrapping using xatlas
|
||||
- No Blender dependency
|
||||
- Optimized for lightmaps and texture atlasing
|
||||
- Same algorithm as Blender 3.6+
|
||||
- Production-ready quality
|
||||
- **libigl LSCM Unwrap**: Conformal UV mapping
|
||||
- Least Squares Conformal Maps
|
||||
- Angle-preserving parameterization
|
||||
- Fast, direct solution
|
||||
- No Blender dependency
|
||||
- **Blender Cube Projection**: Cube projection UV mapping
|
||||
- Perfect for box-like geometry
|
||||
- Projects onto 6 cube faces
|
||||
- Configurable cube size
|
||||
- **Blender Cylinder Projection**: Cylinder projection UV mapping
|
||||
- Perfect for cylindrical objects (bottles, pipes)
|
||||
- Configurable radius
|
||||
- Equirectangular projection
|
||||
- **Blender Sphere Projection**: Sphere projection UV mapping
|
||||
- Perfect for spherical objects (planets, eyes)
|
||||
- Equirectangular projection
|
||||
- Single seam
|
||||
- **Blender UV Unwrap**: Smart UV unwrapping using Blender
|
||||
- Angle-based seam detection
|
||||
- Configurable island margin
|
||||
- Exports mesh with UV coordinates
|
||||
|
||||
### geompack/blender
|
||||
- **Blender Voxel Remesh**: Voxel-based remeshing
|
||||
- Creates uniform, watertight meshes
|
||||
- Configurable voxel size
|
||||
@@ -182,18 +230,22 @@ Create Primitive (sphere, subdivisions=3)
|
||||
|
||||
### 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)
|
||||
- ✅ PyMeshLab Isotropic Remeshing
|
||||
- ✅ 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!
|
||||
- ✅ Preview Mesh (VTK) - Scientific visualization with VTK.js with controls and mesh info panel!
|
||||
- ✅ Distance Metrics - Hausdorff, Chamfer, and SDF computation
|
||||
- ✅ Mesh to Point Cloud conversion with multiple sampling methods
|
||||
- ✅ xAtlas UV Unwrap - Fast, production-ready UV unwrapping
|
||||
- ✅ Blender UV Projections - Cube, Cylinder, and Sphere projections
|
||||
- ✅ libigl LSCM - Conformal UV mapping
|
||||
|
||||
### Coming Soon (Future Phases)
|
||||
- [ ] Mesh Decimation
|
||||
@@ -211,14 +263,17 @@ See [MVP_PLAN.md](MVP_PLAN.md) for the complete roadmap.
|
||||
|
||||
- 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+
|
||||
- **trimesh** 3.15.0+ (core mesh handling)
|
||||
- **pymeshlab** 2022.2+ (isotropic remeshing)
|
||||
- **libigl** 2.6.1+ (LSCM UV unwrapping)
|
||||
- **numpy** 1.21.0+
|
||||
- **scipy** 1.7.0+
|
||||
- **matplotlib** 3.5.0+
|
||||
- **Pillow** 9.0.0+
|
||||
- **point-cloud-utils** 0.30.0+ (distance metrics)
|
||||
- **mesh-to-sdf** 0.0.14+ (SDF computation)
|
||||
- **xatlas** 0.0.11+ (fast UV unwrapping)
|
||||
- **Blender** (optional, for Blender-based UV and remeshing nodes)
|
||||
|
||||
## Technical Details
|
||||
|
||||
@@ -407,6 +462,41 @@ class MyCustomNode:
|
||||
return (processed_mesh,)
|
||||
```
|
||||
|
||||
## Planned Features
|
||||
|
||||
The following features are planned for future releases:
|
||||
|
||||
### Point Cloud Visualization
|
||||
- **plas.io Integration**: Advanced point cloud viewer for LAS/LAZ files
|
||||
- Support for massive point cloud datasets
|
||||
- Intensity and RGB visualization
|
||||
- Fast WebGL-based rendering
|
||||
- Interactive classification tools
|
||||
|
||||
### VTK Viewer Enhancements
|
||||
- **Interactive Controls** (✅ Implemented)
|
||||
- Toggle edges on/off
|
||||
- Switch between surface/wireframe/points rendering modes
|
||||
- Screenshot capture and save
|
||||
- Reset camera view
|
||||
- **Mesh Information Panel** (✅ Implemented)
|
||||
- Display vertex and face counts
|
||||
- Show bounding box and extents
|
||||
- Report watertight status
|
||||
- Calculate volume and surface area
|
||||
- List scalar field names
|
||||
- **Scalar Field Visualization** (In Progress)
|
||||
- Dropdown selector for different fields
|
||||
- Color mapping based on field values
|
||||
- Adjustable color transfer functions
|
||||
- Scalar bar legend
|
||||
|
||||
### Additional 3D Viewers
|
||||
- **Gaussian Splatting**: GaussianSplats3D for neural radiance field visualization
|
||||
- **model-viewer**: glTF/GLB viewer with AR support
|
||||
|
||||
See the [Development Status](#development-status) section for the complete roadmap.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details
|
||||
|
||||
Binary file not shown.
+15
-18
@@ -289,19 +289,16 @@ def compute_mesh_info(mesh: trimesh.Trimesh) -> str:
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Remeshing via PyMeshLab (TODO: will switch back to CGAL eventually)
|
||||
# Remeshing via PyMeshLab
|
||||
# ============================================================================
|
||||
|
||||
def cgal_isotropic_remesh(
|
||||
def pymeshlab_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.
|
||||
Apply isotropic remeshing to create uniform triangle sizes using PyMeshLab.
|
||||
|
||||
Args:
|
||||
mesh: Input trimesh object
|
||||
@@ -311,11 +308,11 @@ def cgal_isotropic_remesh(
|
||||
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}")
|
||||
print(f"[pymeshlab_isotropic_remesh] ===== Starting Isotropic Remeshing =====")
|
||||
print(f"[pymeshlab_isotropic_remesh] Input mesh: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
|
||||
print(f"[pymeshlab_isotropic_remesh] Parameters:")
|
||||
print(f"[pymeshlab_isotropic_remesh] target_edge_length: {target_edge_length}")
|
||||
print(f"[pymeshlab_isotropic_remesh] iterations: {iterations}")
|
||||
|
||||
if not PYMESHLAB_AVAILABLE:
|
||||
return None, "pymeshlab is not installed. Install with: pip install pymeshlab"
|
||||
@@ -334,7 +331,7 @@ def cgal_isotropic_remesh(
|
||||
|
||||
try:
|
||||
# Convert trimesh to PyMeshLab
|
||||
print(f"[cgal_isotropic_remesh] Converting to PyMeshLab format...")
|
||||
print(f"[pymeshlab_isotropic_remesh] Converting to PyMeshLab format...")
|
||||
ms = pymeshlab.MeshSet()
|
||||
|
||||
# Create PyMeshLab mesh from numpy arrays
|
||||
@@ -344,7 +341,7 @@ def cgal_isotropic_remesh(
|
||||
)
|
||||
ms.add_mesh(pml_mesh)
|
||||
|
||||
print(f"[cgal_isotropic_remesh] Applying isotropic remeshing...")
|
||||
print(f"[pymeshlab_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
|
||||
@@ -358,7 +355,7 @@ def cgal_isotropic_remesh(
|
||||
)
|
||||
|
||||
# Convert back to trimesh
|
||||
print(f"[cgal_isotropic_remesh] Converting back to trimesh...")
|
||||
print(f"[pymeshlab_isotropic_remesh] Converting back to trimesh...")
|
||||
remeshed_pml = ms.current_mesh()
|
||||
remeshed_mesh = trimesh.Trimesh(
|
||||
vertices=remeshed_pml.vertex_matrix(),
|
||||
@@ -384,10 +381,10 @@ def cgal_isotropic_remesh(
|
||||
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}%)")
|
||||
print(f"[pymeshlab_isotropic_remesh] ===== Remeshing Complete =====")
|
||||
print(f"[pymeshlab_isotropic_remesh] Results:")
|
||||
print(f"[pymeshlab_isotropic_remesh] Vertices: {len(mesh.vertices)} -> {len(remeshed_mesh.vertices)} ({vertex_change:+d}, {vertex_pct:+.1f}%)")
|
||||
print(f"[pymeshlab_isotropic_remesh] Faces: {len(mesh.faces)} -> {len(remeshed_mesh.faces)} ({face_change:+d}, {face_pct:+.1f}%)")
|
||||
|
||||
return remeshed_mesh, ""
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
GeometryPack PreStartup Script
|
||||
Copies the Stanford Bunny example mesh to ComfyUI's input folder on startup.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
|
||||
def setup_example_assets():
|
||||
"""Copy Stanford Bunny STL to ComfyUI input folder if not already present."""
|
||||
try:
|
||||
import folder_paths
|
||||
|
||||
# Get paths
|
||||
input_folder = folder_paths.get_input_directory()
|
||||
custom_node_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
source_file = os.path.join(custom_node_dir, "assets", "Stanford_Bunny.stl")
|
||||
dest_file = os.path.join(input_folder, "Stanford_Bunny.stl")
|
||||
|
||||
# Copy if source exists and destination doesn't
|
||||
if os.path.exists(source_file):
|
||||
if not os.path.exists(dest_file):
|
||||
shutil.copy2(source_file, dest_file)
|
||||
print(f"[GeometryPack] Copied Stanford_Bunny.stl to input folder")
|
||||
else:
|
||||
print(f"[GeometryPack] Stanford_Bunny.stl already exists in input folder")
|
||||
else:
|
||||
print(f"[GeometryPack] Warning: assets/Stanford_Bunny.stl not found")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[GeometryPack] Error setting up example assets: {e}")
|
||||
|
||||
# Run on import
|
||||
setup_example_assets()
|
||||
@@ -8,3 +8,10 @@ pymeshlab>=2022.2
|
||||
# Visualization
|
||||
matplotlib>=3.5.0
|
||||
Pillow>=9.0.0
|
||||
|
||||
# Distance metrics and point cloud processing
|
||||
point-cloud-utils>=0.30.0
|
||||
mesh-to-sdf>=0.0.14
|
||||
|
||||
# UV unwrapping
|
||||
xatlas>=0.0.11
|
||||
|
||||
+36
-5
@@ -18,25 +18,56 @@ app.registerExtension({
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
console.log("[GeomPack DEBUG] this.widgets before:", this.widgets);
|
||||
console.log("[GeomPack DEBUG] nodeData:", nodeData);
|
||||
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.height = "100%";
|
||||
iframe.style.border = "none";
|
||||
iframe.style.backgroundColor = "#2a2a2a";
|
||||
iframe.style.aspectRatio = "1";
|
||||
|
||||
// Point to our HTML viewer (with cache buster)
|
||||
iframe.src = "/extensions/ComfyUI-GeomPack/viewer.html?v=" + Date.now();
|
||||
iframe.src = "/extensions/ComfyUI-GeometryPack/viewer.html?v=" + Date.now();
|
||||
|
||||
// Add widget
|
||||
const widget = this.addDOMWidget("preview", "MESH_PREVIEW", iframe);
|
||||
widget.computeSize = () => [512, 520];
|
||||
// Add widget with required options
|
||||
console.log("[GeomPack DEBUG] About to call addDOMWidget");
|
||||
console.log("[GeomPack DEBUG] typeof this.addDOMWidget:", typeof this.addDOMWidget);
|
||||
|
||||
const widget = this.addDOMWidget("preview", "MESH_PREVIEW", iframe, {
|
||||
getValue() { return ""; },
|
||||
setValue(v) { }
|
||||
});
|
||||
|
||||
console.log("[GeomPack DEBUG] Widget created:", widget);
|
||||
console.log("[GeomPack DEBUG] Widget properties:", Object.keys(widget || {}));
|
||||
console.log("[GeomPack DEBUG] Widget.id:", widget?.id);
|
||||
console.log("[GeomPack DEBUG] this.widgets after addDOMWidget:", this.widgets);
|
||||
|
||||
// Set widget size - computeSize returns [width, height]
|
||||
widget.computeSize = function(width) {
|
||||
console.log("[GeomPack DEBUG] computeSize called with width:", width);
|
||||
const size = [width || 512, width || 512];
|
||||
console.log("[GeomPack DEBUG] computeSize returning:", size);
|
||||
return size;
|
||||
};
|
||||
|
||||
// Also try setting element size directly
|
||||
widget.element = iframe;
|
||||
|
||||
console.log("[GeomPack DEBUG] Widget computeSize set");
|
||||
console.log("[GeomPack DEBUG] Iframe dimensions:", iframe.style.width, iframe.style.height);
|
||||
|
||||
// Store iframe reference
|
||||
this.meshViewerIframe = iframe;
|
||||
|
||||
// Set initial node size to be square
|
||||
this.setSize([512, 512]);
|
||||
console.log("[GeomPack DEBUG] Node size set to [512, 512]");
|
||||
|
||||
// Handle execution
|
||||
const onExecuted = this.onExecuted;
|
||||
this.onExecuted = function(message) {
|
||||
|
||||
+125
-9
@@ -18,24 +18,74 @@ app.registerExtension({
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
console.log("[GeomPack VTK DEBUG] this.widgets before:", this.widgets);
|
||||
console.log("[GeomPack VTK DEBUG] nodeData:", nodeData);
|
||||
console.log("[GeomPack] VTK 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";
|
||||
|
||||
// Create iframe for VTK.js viewer
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.style.width = "100%";
|
||||
iframe.style.height = "500px";
|
||||
iframe.style.flex = "1";
|
||||
iframe.style.border = "none";
|
||||
iframe.style.backgroundColor = "#2a2a2a";
|
||||
iframe.style.aspectRatio = "1";
|
||||
|
||||
// Point to VTK.js HTML viewer (with cache buster)
|
||||
iframe.src = "/extensions/ComfyUI-GeomPack/viewer_vtk.html?v=" + Date.now();
|
||||
iframe.src = "/extensions/ComfyUI-GeometryPack/viewer_vtk.html?v=" + Date.now();
|
||||
|
||||
// Add widget
|
||||
const widget = this.addDOMWidget("preview_vtk", "MESH_PREVIEW_VTK", iframe);
|
||||
widget.computeSize = () => [512, 520];
|
||||
// Create mesh info panel
|
||||
const infoPanel = document.createElement("div");
|
||||
infoPanel.style.backgroundColor = "#1a1a1a";
|
||||
infoPanel.style.borderTop = "1px solid #444";
|
||||
infoPanel.style.padding = "8px 12px";
|
||||
infoPanel.style.fontSize = "11px";
|
||||
infoPanel.style.fontFamily = "monospace";
|
||||
infoPanel.style.color = "#ccc";
|
||||
infoPanel.style.lineHeight = "1.4";
|
||||
infoPanel.innerHTML = '<span style="color: #888;">Mesh info will appear here after execution</span>';
|
||||
|
||||
// Store iframe reference
|
||||
// Add iframe and info panel to container
|
||||
container.appendChild(iframe);
|
||||
container.appendChild(infoPanel);
|
||||
|
||||
// Add widget with required options
|
||||
console.log("[GeomPack VTK DEBUG] About to call addDOMWidget");
|
||||
console.log("[GeomPack VTK DEBUG] typeof this.addDOMWidget:", typeof this.addDOMWidget);
|
||||
|
||||
const widget = this.addDOMWidget("preview_vtk", "MESH_PREVIEW_VTK", container, {
|
||||
getValue() { return ""; },
|
||||
setValue(v) { }
|
||||
});
|
||||
|
||||
console.log("[GeomPack VTK DEBUG] Widget created:", widget);
|
||||
console.log("[GeomPack VTK DEBUG] Widget properties:", Object.keys(widget || {}));
|
||||
console.log("[GeomPack VTK DEBUG] Widget.id:", widget?.id);
|
||||
console.log("[GeomPack VTK DEBUG] this.widgets after addDOMWidget:", this.widgets);
|
||||
|
||||
widget.computeSize = () => [512, 560]; // Increased height for info panel
|
||||
|
||||
// Store iframe and info panel references
|
||||
this.meshViewerIframeVTK = iframe;
|
||||
this.meshInfoPanelVTK = infoPanel;
|
||||
|
||||
// Track iframe load state
|
||||
let iframeLoaded = false;
|
||||
iframe.addEventListener('load', () => {
|
||||
console.log("[GeomPack VTK DEBUG] Iframe loaded");
|
||||
iframeLoaded = true;
|
||||
});
|
||||
|
||||
// Set initial node size (increased for info panel)
|
||||
this.setSize([512, 560]);
|
||||
console.log("[GeomPack VTK DEBUG] Node size set to [512, 560]");
|
||||
|
||||
// Handle execution
|
||||
const onExecuted = this.onExecuted;
|
||||
@@ -48,11 +98,68 @@ app.registerExtension({
|
||||
const filename = message.mesh_file[0];
|
||||
console.log(`[GeomPack VTK] Loading mesh: ${filename}`);
|
||||
|
||||
// Update mesh info panel with metadata
|
||||
const vertices = message.vertex_count?.[0] || 'N/A';
|
||||
const faces = message.face_count?.[0] || 'N/A';
|
||||
const boundsMin = message.bounds_min?.[0] || [];
|
||||
const boundsMax = message.bounds_max?.[0] || [];
|
||||
const extents = message.extents?.[0] || [];
|
||||
|
||||
// Format bounds
|
||||
let boundsStr = 'N/A';
|
||||
if (boundsMin.length === 3 && boundsMax.length === 3) {
|
||||
boundsStr = `[${boundsMin.map(v => v.toFixed(2)).join(', ')}] to [${boundsMax.map(v => v.toFixed(2)).join(', ')}]`;
|
||||
}
|
||||
|
||||
// Format extents
|
||||
let extentsStr = 'N/A';
|
||||
if (extents.length === 3) {
|
||||
extentsStr = `${extents.map(v => v.toFixed(2)).join(' × ')}`;
|
||||
}
|
||||
|
||||
// Build info HTML
|
||||
let infoHTML = `
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 8px;">
|
||||
<span style="color: #888;">Vertices:</span>
|
||||
<span>${vertices.toLocaleString()}</span>
|
||||
|
||||
<span style="color: #888;">Faces:</span>
|
||||
<span>${faces.toLocaleString()}</span>
|
||||
|
||||
<span style="color: #888;">Bounds:</span>
|
||||
<span style="font-size: 10px;">${boundsStr}</span>
|
||||
|
||||
<span style="color: #888;">Extents:</span>
|
||||
<span>${extentsStr}</span>
|
||||
`;
|
||||
|
||||
// Add optional fields if available
|
||||
if (message.is_watertight !== undefined) {
|
||||
const watertight = message.is_watertight[0] ? 'Yes' : 'No';
|
||||
const color = message.is_watertight[0] ? '#6c6' : '#c66';
|
||||
infoHTML += `
|
||||
<span style="color: #888;">Watertight:</span>
|
||||
<span style="color: ${color};">${watertight}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
if (message.field_names && message.field_names[0]?.length > 0) {
|
||||
const fields = message.field_names[0].join(', ');
|
||||
infoHTML += `
|
||||
<span style="color: #888;">Fields:</span>
|
||||
<span style="font-size: 10px;">${fields}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
infoHTML += '</div>';
|
||||
|
||||
infoPanel.innerHTML = infoHTML;
|
||||
|
||||
// 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(() => {
|
||||
// Function to send message
|
||||
const sendMessage = () => {
|
||||
if (iframe.contentWindow) {
|
||||
console.log(`[GeomPack VTK] Sending postMessage to iframe: ${filepath}`);
|
||||
iframe.contentWindow.postMessage({
|
||||
@@ -63,7 +170,16 @@ app.registerExtension({
|
||||
} else {
|
||||
console.error("[GeomPack VTK] Iframe contentWindow not available");
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Send message after iframe is loaded
|
||||
if (iframeLoaded) {
|
||||
console.log("[GeomPack VTK DEBUG] Iframe already loaded, sending immediately");
|
||||
sendMessage();
|
||||
} else {
|
||||
console.log("[GeomPack VTK DEBUG] Waiting for iframe to load...");
|
||||
setTimeout(sendMessage, 500);
|
||||
}
|
||||
} else {
|
||||
console.log("[GeomPack VTK] No mesh_file in message data. Keys:", Object.keys(message || {}));
|
||||
}
|
||||
|
||||
+37
-7
@@ -109,6 +109,7 @@
|
||||
// Load mesh function
|
||||
function loadMesh(filepath) {
|
||||
console.log('[GeomPack Viewer] Loading:', filepath);
|
||||
console.log('[GeomPack Viewer DEBUG] Full filepath URL:', filepath);
|
||||
loading.textContent = 'Loading mesh...';
|
||||
loading.style.display = 'block';
|
||||
|
||||
@@ -123,30 +124,46 @@
|
||||
const loader = isGLB ? new GLTFLoader() : new OBJLoader();
|
||||
|
||||
console.log('[GeomPack Viewer] File type detected:', isGLB ? 'GLB/GLTF' : 'OBJ');
|
||||
console.log('[GeomPack Viewer DEBUG] Using loader:', loader.constructor.name);
|
||||
console.log('[GeomPack Viewer DEBUG] About to call loader.load()...');
|
||||
|
||||
loader.load(
|
||||
filepath,
|
||||
(result) => {
|
||||
console.log('[GeomPack Viewer DEBUG] Loader success callback called');
|
||||
console.log('[GeomPack Viewer DEBUG] Result:', result);
|
||||
|
||||
currentMesh = isGLB ? result.scene : result;
|
||||
console.log('[GeomPack Viewer DEBUG] currentMesh:', currentMesh);
|
||||
|
||||
// Center mesh
|
||||
const box = new THREE.Box3().setFromObject(currentMesh);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
currentMesh.position.sub(center);
|
||||
|
||||
console.log('[GeomPack Viewer DEBUG] Mesh bounds:', box);
|
||||
console.log('[GeomPack Viewer DEBUG] Mesh center:', center);
|
||||
|
||||
// Add material if needed
|
||||
let meshCount = 0;
|
||||
currentMesh.traverse((child) => {
|
||||
if (child.isMesh && !child.material) {
|
||||
child.material = new THREE.MeshStandardMaterial({
|
||||
color: 0x88aacc,
|
||||
metalness: 0.3,
|
||||
roughness: 0.7
|
||||
});
|
||||
if (child.isMesh) {
|
||||
meshCount++;
|
||||
if (!child.material) {
|
||||
child.material = new THREE.MeshStandardMaterial({
|
||||
color: 0x88aacc,
|
||||
metalness: 0.3,
|
||||
roughness: 0.7
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[GeomPack Viewer DEBUG] Mesh count in scene:', meshCount);
|
||||
|
||||
// Add to scene
|
||||
scene.add(currentMesh);
|
||||
console.log('[GeomPack Viewer DEBUG] Mesh added to scene');
|
||||
|
||||
// Adjust camera
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
@@ -157,6 +174,8 @@
|
||||
controls.target.set(0, 0, 0);
|
||||
controls.update();
|
||||
|
||||
console.log('[GeomPack Viewer DEBUG] Camera adjusted to distance:', distance);
|
||||
|
||||
loading.style.display = 'none';
|
||||
console.log('[GeomPack Viewer] Mesh loaded successfully');
|
||||
},
|
||||
@@ -168,20 +187,31 @@
|
||||
},
|
||||
(error) => {
|
||||
console.error('[GeomPack Viewer] Error loading mesh:', error);
|
||||
loading.textContent = 'Error loading mesh';
|
||||
console.error('[GeomPack Viewer DEBUG] Error type:', error?.constructor?.name);
|
||||
console.error('[GeomPack Viewer DEBUG] Error message:', error?.message);
|
||||
console.error('[GeomPack Viewer DEBUG] Error stack:', error?.stack);
|
||||
loading.textContent = 'Error loading mesh - check console';
|
||||
loading.style.color = '#ff6666';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', (event) => {
|
||||
console.log('[GeomPack Viewer DEBUG] Received postMessage:', event.data);
|
||||
console.log('[GeomPack Viewer DEBUG] Message type:', event.data?.type);
|
||||
console.log('[GeomPack Viewer DEBUG] Message filepath:', event.data?.filepath);
|
||||
|
||||
if (event.data.type === 'LOAD_MESH') {
|
||||
console.log('[GeomPack Viewer] Received LOAD_MESH message:', event.data.filepath);
|
||||
loadMesh(event.data.filepath);
|
||||
} else {
|
||||
console.warn('[GeomPack Viewer DEBUG] Unknown message type or missing type');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[GeomPack Viewer] Ready');
|
||||
console.log('[GeomPack Viewer DEBUG] Waiting for LOAD_MESH postMessage...');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+205
-17
@@ -9,10 +9,17 @@
|
||||
overflow: hidden;
|
||||
background: #2a2a2a;
|
||||
font-family: monospace;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#viewerContainer {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
#loading {
|
||||
@@ -26,11 +33,66 @@
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
#controls {
|
||||
background: #1a1a1a;
|
||||
border-top: 1px solid #444;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
#controls label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
#controls select, #controls button {
|
||||
background: #333;
|
||||
color: #ccc;
|
||||
border: 1px solid #555;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
#controls button {
|
||||
cursor: pointer;
|
||||
}
|
||||
#controls button:hover {
|
||||
background: #444;
|
||||
}
|
||||
#controls input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<div id="loading">Ready to load mesh</div>
|
||||
<div id="viewerContainer">
|
||||
<div id="container"></div>
|
||||
<div id="loading">Ready to load mesh</div>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<label>
|
||||
<input type="checkbox" id="showEdges"> Edges
|
||||
</label>
|
||||
<label>
|
||||
Mode:
|
||||
<select id="representation">
|
||||
<option value="2">Surface</option>
|
||||
<option value="1">Wireframe</option>
|
||||
<option value="0">Points</option>
|
||||
</select>
|
||||
</label>
|
||||
<label id="fieldLabel" style="display:none;">
|
||||
Field:
|
||||
<select id="fieldSelector"></select>
|
||||
</label>
|
||||
<button id="resetCamera">Reset Camera</button>
|
||||
<button id="screenshot">Screenshot</button>
|
||||
</div>
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
@@ -41,11 +103,15 @@
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
// CRITICAL: Import rendering profile to load WebGL backend
|
||||
import '@kitware/vtk.js/Rendering/Profiles/Geometry';
|
||||
|
||||
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 vtkPolyDataNormals from '@kitware/vtk.js/Filters/Core/PolyDataNormals';
|
||||
import vtkInteractorStyleTrackballCamera from '@kitware/vtk.js/Interaction/Style/InteractorStyleTrackballCamera';
|
||||
|
||||
console.log('[GeomPack VTK Viewer] Initializing...');
|
||||
@@ -72,21 +138,30 @@
|
||||
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);
|
||||
// Configure appearance with better lighting properties
|
||||
const property = actor.getProperty();
|
||||
property.setEdgeVisibility(false);
|
||||
property.setColor(1.0, 1.0, 1.0); // Bright white for visibility
|
||||
property.setAmbient(0.4); // Increase ambient
|
||||
property.setDiffuse(0.8); // Increase diffuse
|
||||
property.setSpecular(0.3);
|
||||
property.setSpecularPower(20);
|
||||
property.setRepresentation(2); // 2 = surface rendering
|
||||
|
||||
// Add actor to scene
|
||||
renderer.addActor(actor);
|
||||
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Actor configured with lighting properties');
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Actor visible:', actor.getVisibility());
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Renderer background:', renderer.getBackground());
|
||||
|
||||
// Current reader
|
||||
let currentReader = null;
|
||||
|
||||
// Load mesh function
|
||||
async function loadMesh(filepath) {
|
||||
console.log('[GeomPack VTK Viewer] Loading:', filepath);
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Full filepath URL:', filepath);
|
||||
loading.textContent = 'Loading mesh...';
|
||||
loading.style.display = 'block';
|
||||
|
||||
@@ -94,6 +169,9 @@
|
||||
// Determine file type
|
||||
const isSTL = filepath.includes('.stl');
|
||||
const isOBJ = filepath.includes('.obj');
|
||||
const isGLB = filepath.includes('.glb') || filepath.includes('.gltf');
|
||||
|
||||
console.log('[GeomPack VTK Viewer DEBUG] File type detection - STL:', isSTL, 'OBJ:', isOBJ, 'GLB:', isGLB);
|
||||
|
||||
// Create appropriate reader
|
||||
if (isSTL) {
|
||||
@@ -103,49 +181,124 @@
|
||||
console.log('[GeomPack VTK Viewer] File type: OBJ');
|
||||
currentReader = vtkOBJReader.newInstance();
|
||||
} else {
|
||||
throw new Error(`Unsupported file format: ${filepath}`);
|
||||
const errorMsg = `Unsupported file format: ${filepath}. VTK viewer only supports STL and OBJ formats.`;
|
||||
console.error('[GeomPack VTK Viewer DEBUG]', errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
console.log('[GeomPack VTK Viewer DEBUG] About to fetch:', filepath);
|
||||
|
||||
// Fetch the file
|
||||
const response = await fetch(filepath);
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Fetch response:', response);
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Response status:', response.status, response.statusText);
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Response OK:', response.ok);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Get file data
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
console.log('[GeomPack VTK Viewer DEBUG] ArrayBuffer size:', arrayBuffer.byteLength);
|
||||
|
||||
// Parse with appropriate reader
|
||||
if (isSTL) {
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Parsing STL...');
|
||||
currentReader.parseAsArrayBuffer(arrayBuffer);
|
||||
} else if (isOBJ) {
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Parsing OBJ...');
|
||||
const text = new TextDecoder().decode(arrayBuffer);
|
||||
currentReader.parseAsText(text);
|
||||
}
|
||||
|
||||
// Get output data
|
||||
// Get output data for logging
|
||||
const polydata = currentReader.getOutputData();
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Polydata:', polydata);
|
||||
|
||||
if (!polydata) {
|
||||
throw new Error('Failed to parse mesh data');
|
||||
}
|
||||
|
||||
console.log('[GeomPack VTK Viewer] Loaded mesh:',
|
||||
polydata.getNumberOfPoints(), 'points,',
|
||||
polydata.getNumberOfPolys(), 'polygons');
|
||||
const numPoints = polydata.getNumberOfPoints();
|
||||
const numPolys = polydata.getNumberOfPolys();
|
||||
console.log('[GeomPack VTK Viewer] Loaded mesh:', numPoints, 'points,', numPolys, 'polygons');
|
||||
|
||||
// Set mapper input
|
||||
mapper.setInputData(polydata);
|
||||
// Check mesh bounds
|
||||
const bounds = polydata.getBounds();
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Mesh bounds:', bounds);
|
||||
|
||||
// CRITICAL: Use proper VTK pipeline with normals filter
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Setting up VTK pipeline...');
|
||||
|
||||
// Create normals filter (required for STL rendering)
|
||||
const normalsFilter = vtkPolyDataNormals.newInstance();
|
||||
|
||||
// Connect pipeline: reader -> normals -> mapper
|
||||
normalsFilter.setInputConnection(currentReader.getOutputPort());
|
||||
mapper.setInputConnection(normalsFilter.getOutputPort());
|
||||
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Pipeline connected: reader -> normals -> mapper');
|
||||
|
||||
// Force pipeline update
|
||||
mapper.update();
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Mapper updated');
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Mapper has input:', mapper.getInputData());
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Actor mapper:', actor.getMapper());
|
||||
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Resetting camera...');
|
||||
// Reset camera to fit mesh
|
||||
renderer.resetCamera();
|
||||
renderWindow.render();
|
||||
|
||||
// Get camera and adjust view angle
|
||||
const camera = renderer.getActiveCamera();
|
||||
const center = [
|
||||
(bounds[0] + bounds[1]) / 2,
|
||||
(bounds[2] + bounds[3]) / 2,
|
||||
(bounds[4] + bounds[5]) / 2
|
||||
];
|
||||
const maxDim = Math.max(
|
||||
bounds[1] - bounds[0],
|
||||
bounds[3] - bounds[2],
|
||||
bounds[5] - bounds[4]
|
||||
);
|
||||
|
||||
// Position camera at an angle for better 3D view
|
||||
const distance = maxDim * 2.5;
|
||||
camera.setPosition(
|
||||
center[0] + distance,
|
||||
center[1] + distance,
|
||||
center[2] + distance
|
||||
);
|
||||
camera.setFocalPoint(center[0], center[1], center[2]);
|
||||
camera.setViewUp(0, 0, 1);
|
||||
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Camera position:', camera.getPosition());
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Camera focal point:', camera.getFocalPoint());
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Mesh center:', center);
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Max dimension:', maxDim);
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Distance:', distance);
|
||||
|
||||
// Render the scene
|
||||
renderWindow.render();
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Initial render called');
|
||||
|
||||
// Force a resize to ensure proper display in iframe
|
||||
setTimeout(() => {
|
||||
fullScreenRenderer.resize();
|
||||
renderWindow.render();
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Post-load resize and render');
|
||||
}, 100);
|
||||
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Rendering complete');
|
||||
loading.style.display = 'none';
|
||||
console.log('[GeomPack VTK Viewer] Mesh loaded successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[GeomPack VTK Viewer] Error loading mesh:', error);
|
||||
console.error('[GeomPack VTK Viewer DEBUG] Error type:', error?.constructor?.name);
|
||||
console.error('[GeomPack VTK Viewer DEBUG] Error message:', error?.message);
|
||||
console.error('[GeomPack VTK Viewer DEBUG] Error stack:', error?.stack);
|
||||
loading.textContent = `Error loading mesh: ${error.message}`;
|
||||
loading.style.color = '#ff6b6b';
|
||||
}
|
||||
@@ -153,18 +306,53 @@
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', (event) => {
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Received postMessage:', event.data);
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Message type:', event.data?.type);
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Message filepath:', event.data?.filepath);
|
||||
|
||||
if (event.data.type === 'LOAD_MESH') {
|
||||
console.log('[GeomPack VTK Viewer] Received LOAD_MESH message:', event.data.filepath);
|
||||
loadMesh(event.data.filepath);
|
||||
} else {
|
||||
console.warn('[GeomPack VTK Viewer DEBUG] Unknown message type or missing type');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
renderWindow.resize();
|
||||
fullScreenRenderer.resize();
|
||||
renderWindow.render();
|
||||
});
|
||||
|
||||
// Control event handlers
|
||||
document.getElementById('showEdges').addEventListener('change', (e) => {
|
||||
property.setEdgeVisibility(e.target.checked);
|
||||
renderWindow.render();
|
||||
});
|
||||
|
||||
document.getElementById('representation').addEventListener('change', (e) => {
|
||||
property.setRepresentation(parseInt(e.target.value));
|
||||
renderWindow.render();
|
||||
});
|
||||
|
||||
document.getElementById('resetCamera').addEventListener('click', () => {
|
||||
renderer.resetCamera();
|
||||
renderWindow.render();
|
||||
});
|
||||
|
||||
document.getElementById('screenshot').addEventListener('click', () => {
|
||||
const image = renderWindow.captureImages()[0];
|
||||
// Send screenshot back to parent window
|
||||
window.parent.postMessage({
|
||||
type: 'SCREENSHOT',
|
||||
image: image, // base64 PNG data URL
|
||||
timestamp: Date.now()
|
||||
}, '*');
|
||||
console.log('[GeomPack VTK Viewer] Screenshot captured and sent to parent');
|
||||
});
|
||||
|
||||
console.log('[GeomPack VTK Viewer] Ready');
|
||||
console.log('[GeomPack VTK Viewer DEBUG] Waiting for LOAD_MESH postMessage...');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user