vtk viewer

This commit is contained in:
Andrea Pozzetti
2025-11-14 21:39:06 +01:00
parent c49d9711e5
commit 7c2cfb46af
10 changed files with 1480 additions and 92 deletions
+105 -15
View File
@@ -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
View File
@@ -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, ""
+917 -21
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -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()
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>