diff --git a/nodes/visualization/preview_mesh_dual.py b/nodes/visualization/preview_mesh_dual.py index c4deefb9..a6e4537b 100644 --- a/nodes/visualization/preview_mesh_dual.py +++ b/nodes/visualization/preview_mesh_dual.py @@ -46,6 +46,22 @@ def has_fields(mesh): return has_vertex_attrs or has_face_attrs +def get_texture_info(mesh): + """Extract texture/visual information from a mesh.""" + has_visual = hasattr(mesh, 'visual') and mesh.visual is not None + visual_kind = mesh.visual.kind if has_visual else None + has_texture = visual_kind == 'texture' and hasattr(mesh.visual, 'material') if has_visual else False + has_vertex_colors = visual_kind == 'vertex' if has_visual else False + has_material = has_texture + return { + 'has_visual': has_visual, + 'visual_kind': visual_kind, + 'has_texture': has_texture, + 'has_vertex_colors': has_vertex_colors, + 'has_material': has_material + } + + class PreviewMeshDualNode: """ Unified dual mesh preview with VTK.js - supports both side-by-side and overlay layouts. @@ -63,6 +79,7 @@ class PreviewMeshDualNode: }, "optional": { "layout": (["side_by_side", "overlay"], {"default": "side_by_side"}), + "mode": (["fields", "texture"], {"default": "fields"}), "opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}), } } @@ -72,20 +89,21 @@ class PreviewMeshDualNode: FUNCTION = "preview_dual" CATEGORY = "geompack/visualization" - def preview_dual(self, mesh_1, mesh_2, layout="side_by_side", opacity=1.0): + def preview_dual(self, mesh_1, mesh_2, layout="side_by_side", mode="fields", opacity=1.0): """ - Preview two meshes with chosen layout and field visualization. + Preview two meshes with chosen layout and visualization mode. Args: mesh_1: First trimesh object mesh_2: Second trimesh object layout: "side_by_side" or "overlay" + mode: "fields" (scientific visualization) or "texture" (textured rendering) opacity: Opacity for both meshes (0.0-1.0) Returns: dict: UI data for frontend widget """ - print(f"[PreviewMeshDual] Layout: {layout}") + print(f"[PreviewMeshDual] Layout: {layout}, Mode: {mode}") print(f"[PreviewMeshDual] Mesh 1: {get_geometry_type(mesh_1)} - {len(mesh_1.vertices)} vertices, {get_face_count(mesh_1)} faces") print(f"[PreviewMeshDual] Mesh 2: {get_geometry_type(mesh_2)} - {len(mesh_2.vertices)} vertices, {get_face_count(mesh_2)} faces") @@ -100,6 +118,13 @@ class PreviewMeshDualNode: print(f"[PreviewMeshDual] Mesh 2 fields: {field_names_2}") print(f"[PreviewMeshDual] Common fields: {common_fields}") + # Check for texture/visual data + texture_info_1 = get_texture_info(mesh_1) + texture_info_2 = get_texture_info(mesh_2) + + print(f"[PreviewMeshDual] Mesh 1 visual: kind={texture_info_1['visual_kind']}, texture={texture_info_1['has_texture']}, vertex_colors={texture_info_1['has_vertex_colors']}") + print(f"[PreviewMeshDual] Mesh 2 visual: kind={texture_info_2['visual_kind']}, texture={texture_info_2['has_texture']}, vertex_colors={texture_info_2['has_vertex_colors']}") + # Check if meshes are point clouds (need VTP, STL doesn't support point clouds) mesh_1_is_pc = is_point_cloud(mesh_1) mesh_2_is_pc = is_point_cloud(mesh_2) @@ -108,13 +133,20 @@ class PreviewMeshDualNode: preview_id = uuid.uuid4().hex[:8] if layout == "side_by_side": - # Export meshes separately - use VTP for fields OR point clouds - filename_1, filepath_1 = self._export_mesh(mesh_1, f"preview_dual_1_{preview_id}", mesh_1_has_fields or mesh_1_is_pc) - filename_2, filepath_2 = self._export_mesh(mesh_2, f"preview_dual_2_{preview_id}", mesh_2_has_fields or mesh_2_is_pc) + # Export meshes separately based on mode + if mode == "texture": + # Texture mode: export as GLB + filename_1, filepath_1 = self._export_mesh(mesh_1, f"preview_dual_1_{preview_id}", use_vtp=False, use_glb=True) + filename_2, filepath_2 = self._export_mesh(mesh_2, f"preview_dual_2_{preview_id}", use_vtp=False, use_glb=True) + else: + # Fields mode: use VTP for fields OR point clouds + filename_1, filepath_1 = self._export_mesh(mesh_1, f"preview_dual_1_{preview_id}", use_vtp=(mesh_1_has_fields or mesh_1_is_pc), use_glb=False) + filename_2, filepath_2 = self._export_mesh(mesh_2, f"preview_dual_2_{preview_id}", use_vtp=(mesh_2_has_fields or mesh_2_is_pc), use_glb=False) # Build UI data for side-by-side mode ui_data = { "layout": [layout], + "mode": [mode], "mesh_1_file": [filename_1], "mesh_2_file": [filename_2], "vertex_count_1": [len(mesh_1.vertices)], @@ -129,17 +161,44 @@ class PreviewMeshDualNode: "extents_2": [mesh_2.extents.tolist()], "is_watertight_1": [bool(mesh_1.is_watertight) if not is_point_cloud(mesh_1) else False], "is_watertight_2": [bool(mesh_2.is_watertight) if not is_point_cloud(mesh_2) else False], - "field_names_1": [field_names_1], - "field_names_2": [field_names_2], - "common_fields": [common_fields], } + # Add mode-specific metadata + if mode == "texture": + # Texture mode metadata + ui_data.update({ + "has_texture_1": [texture_info_1['has_texture']], + "has_texture_2": [texture_info_2['has_texture']], + "visual_kind_1": [texture_info_1['visual_kind'] if texture_info_1['visual_kind'] else "none"], + "visual_kind_2": [texture_info_2['visual_kind'] if texture_info_2['visual_kind'] else "none"], + "has_vertex_colors_1": [texture_info_1['has_vertex_colors']], + "has_vertex_colors_2": [texture_info_2['has_vertex_colors']], + "has_material_1": [texture_info_1['has_material']], + "has_material_2": [texture_info_2['has_material']], + }) + else: + # Fields mode metadata + ui_data.update({ + "field_names_1": [field_names_1], + "field_names_2": [field_names_2], + "common_fields": [common_fields], + }) + + else: # overlay # Combine meshes with color coding - filename, filepath = self._export_combined_mesh( - mesh_1, mesh_2, preview_id, opacity, - mesh_1_has_fields, mesh_2_has_fields - ) + if mode == "texture": + # Texture mode: export combined mesh as GLB + filename, filepath = self._export_combined_mesh( + mesh_1, mesh_2, preview_id, opacity, + mesh_1_has_fields, mesh_2_has_fields, use_glb=True + ) + else: + # Fields mode: export combined mesh as VTP + filename, filepath = self._export_combined_mesh( + mesh_1, mesh_2, preview_id, opacity, + mesh_1_has_fields, mesh_2_has_fields, use_glb=False + ) # Calculate combined bounds combined_bounds_min = np.minimum(mesh_1.bounds[0], mesh_2.bounds[0]) @@ -149,6 +208,7 @@ class PreviewMeshDualNode: # Build UI data for overlay mode ui_data = { "layout": [layout], + "mode": [mode], "mesh_file": [filename], "vertex_count_1": [len(mesh_1.vertices)], "vertex_count_2": [len(mesh_2.vertices)], @@ -160,17 +220,37 @@ class PreviewMeshDualNode: "opacity": [float(opacity)], "is_watertight_1": [bool(mesh_1.is_watertight) if not is_point_cloud(mesh_1) else False], "is_watertight_2": [bool(mesh_2.is_watertight) if not is_point_cloud(mesh_2) else False], - "field_names_1": [field_names_1], - "field_names_2": [field_names_2], - "common_fields": [common_fields], } + # Add mode-specific metadata + if mode == "texture": + # Texture mode metadata + ui_data.update({ + "has_texture_1": [texture_info_1['has_texture']], + "has_texture_2": [texture_info_2['has_texture']], + "visual_kind_1": [texture_info_1['visual_kind'] if texture_info_1['visual_kind'] else "none"], + "visual_kind_2": [texture_info_2['visual_kind'] if texture_info_2['visual_kind'] else "none"], + "has_vertex_colors_1": [texture_info_1['has_vertex_colors']], + "has_vertex_colors_2": [texture_info_2['has_vertex_colors']], + "has_material_1": [texture_info_1['has_material']], + "has_material_2": [texture_info_2['has_material']], + }) + else: + # Fields mode metadata + ui_data.update({ + "field_names_1": [field_names_1], + "field_names_2": [field_names_2], + "common_fields": [common_fields], + }) + print(f"[PreviewMeshDual] Preview ready") return {"ui": ui_data} - def _export_mesh(self, mesh, base_filename, use_vtp): + def _export_mesh(self, mesh, base_filename, use_vtp, use_glb): """Export a single mesh to appropriate format.""" - if use_vtp: + if use_glb: + filename = f"{base_filename}.glb" + elif use_vtp: filename = f"{base_filename}.vtp" else: filename = f"{base_filename}.stl" @@ -181,7 +261,10 @@ class PreviewMeshDualNode: filepath = os.path.join(tempfile.gettempdir(), filename) try: - if use_vtp: + if use_glb: + mesh.export(filepath, file_type='glb', include_normals=True) + print(f"[PreviewMeshDual] Exported GLB: {filepath}") + elif use_vtp: export_mesh_with_scalars_vtp(mesh, filepath) print(f"[PreviewMeshDual] Exported VTP with fields: {filepath}") else: @@ -190,29 +273,38 @@ class PreviewMeshDualNode: except Exception as e: print(f"[PreviewMeshDual] Export failed: {e}, trying fallback") # Fallback to OBJ - filename = filename.replace('.vtp', '.obj').replace('.stl', '.obj') - filepath = filepath.replace('.vtp', '.obj').replace('.stl', '.obj') + filename = filename.replace('.vtp', '.obj').replace('.stl', '.obj').replace('.glb', '.obj') + filepath = filepath.replace('.vtp', '.obj').replace('.stl', '.obj').replace('.glb', '.obj') mesh.export(filepath, file_type='obj') print(f"[PreviewMeshDual] Exported OBJ fallback: {filepath}") return filename, filepath def _export_combined_mesh(self, mesh_1, mesh_2, preview_id, opacity, - mesh_1_has_fields, mesh_2_has_fields): - """Export combined mesh for overlay mode as VTP.""" + mesh_1_has_fields, mesh_2_has_fields, use_glb): + """Export combined mesh for overlay mode as VTP or GLB.""" # Combine meshes (with or without fields) try: combined = trimesh_module.util.concatenate([mesh_1, mesh_2]) - filename = f"preview_dual_overlay_{preview_id}.vtp" + + if use_glb: + filename = f"preview_dual_overlay_{preview_id}.glb" + else: + filename = f"preview_dual_overlay_{preview_id}.vtp" if COMFYUI_OUTPUT_FOLDER: filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename) else: filepath = os.path.join(tempfile.gettempdir(), filename) - export_mesh_with_scalars_vtp(combined, filepath) - print(f"[PreviewMeshDual] Exported combined VTP: {filepath}") + if use_glb: + combined.export(filepath, file_type='glb', include_normals=True) + print(f"[PreviewMeshDual] Exported combined GLB: {filepath}") + else: + export_mesh_with_scalars_vtp(combined, filepath) + print(f"[PreviewMeshDual] Exported combined VTP: {filepath}") + print(f"[PreviewMeshDual] Combined {get_geometry_type(combined)}: {len(combined.vertices)} vertices, {get_face_count(combined)} faces") return filename, filepath except Exception as e: diff --git a/nodes/visualization/preview_mesh_vtk.py b/nodes/visualization/preview_mesh_vtk.py index f63e921c..13405a2c 100644 --- a/nodes/visualization/preview_mesh_vtk.py +++ b/nodes/visualization/preview_mesh_vtk.py @@ -39,6 +39,7 @@ class PreviewMeshVTKNode: return { "required": { "trimesh": ("TRIMESH",), + "mode": (["fields", "texture"], {"default": "fields"}), }, } @@ -47,15 +48,17 @@ class PreviewMeshVTKNode: FUNCTION = "preview_mesh_vtk" CATEGORY = "geompack/visualization" - def preview_mesh_vtk(self, trimesh): + def preview_mesh_vtk(self, trimesh, mode="fields"): """ Export mesh and prepare for VTK.js preview. - Automatically detects scalar fields (vertex/face attributes) and exports - to VTP format if present, otherwise exports to STL. + Supports two visualization modes: + - fields: Scientific visualization with scalar fields and colormaps (VTP/STL export) + - texture: Textured mesh visualization with materials (GLB export) Args: trimesh: Input trimesh_module.Trimesh object + mode: Visualization mode - "fields" or "texture" Returns: dict: UI data for frontend widget @@ -79,17 +82,36 @@ class PreviewMeshVTKNode: print(f"[PreviewMeshVTK] DEBUG - has_face_attrs: {has_face_attrs}") print(f"[PreviewMeshVTK] DEBUG - has_fields: {has_fields}") + # Check for visual data (textures/vertex colors) + has_visual = hasattr(trimesh, 'visual') and trimesh.visual is not None + visual_kind = trimesh.visual.kind if has_visual else None + has_texture = visual_kind == 'texture' and hasattr(trimesh.visual, 'material') if has_visual else False + has_vertex_colors = visual_kind == 'vertex' if has_visual else False + has_material = has_texture + + print(f"[PreviewMeshVTK] Mode: {mode}") + print(f"[PreviewMeshVTK] Visual data - has_visual: {has_visual}, kind: {visual_kind}, texture: {has_texture}, vertex_colors: {has_vertex_colors}") + # Check if this is a point cloud is_pc = is_point_cloud(trimesh) - # Choose export format based on whether fields exist or if it's a point cloud - if has_fields or is_pc: - # Export to VTP for: scalar fields OR point clouds (STL doesn't support point clouds) - filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.vtp" - print(f"[PreviewMeshVTK] Using VTP format (fields={has_fields}, point_cloud={is_pc})") + # Choose export format based on visualization mode + if mode == "texture": + # Texture mode: Export GLB to preserve textures/materials/UVs + filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.glb" + viewer_type = "texture" + print(f"[PreviewMeshVTK] Using texture mode - GLB export") else: - # Export to STL (compact format for simple surface meshes) - filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.stl" + # Fields mode: Export VTP/STL for scalar field visualization + if has_fields or is_pc: + # Export to VTP for: scalar fields OR point clouds (STL doesn't support point clouds) + filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.vtp" + print(f"[PreviewMeshVTK] Using VTP format (fields={has_fields}, point_cloud={is_pc})") + else: + # Export to STL (compact format for simple surface meshes) + filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.stl" + viewer_type = "fields" + print(f"[PreviewMeshVTK] Using fields mode - VTP/STL export") # Use ComfyUI's output directory if COMFYUI_OUTPUT_FOLDER: @@ -99,7 +121,11 @@ class PreviewMeshVTKNode: # Export mesh try: - if has_fields or is_pc: + if mode == "texture": + # Export GLB for texture rendering + trimesh.export(filepath, file_type='glb', include_normals=True) + print(f"[PreviewMeshVTK] Exported GLB to: {filepath}") + elif has_fields or is_pc: # Use VTP exporter for fields or point clouds export_mesh_with_scalars_vtp(trimesh, filepath) print(f"[PreviewMeshVTK] Exported VTP to: {filepath}") @@ -146,6 +172,8 @@ class PreviewMeshVTKNode: # Return metadata for frontend widget ui_data = { "mesh_file": [filename], + "viewer_type": [viewer_type], # "fields" or "texture" - tells frontend which viewer to load + "mode": [mode], # User-selected mode "vertex_count": [len(trimesh.vertices)], "face_count": [get_face_count(trimesh)], "bounds_min": [bounds[0].tolist()], @@ -153,19 +181,33 @@ class PreviewMeshVTKNode: "extents": [extents.tolist()], "max_extent": [float(max_extent)], "is_watertight": [bool(is_watertight)], - "field_names": [field_names], # Always include (empty array if no fields) } + # Add mode-specific metadata + if viewer_type == "texture": + # Texture mode metadata + ui_data.update({ + "has_texture": [has_texture], + "has_vertex_colors": [has_vertex_colors], + "has_material": [has_material], + "visual_kind": [visual_kind if visual_kind else "none"], + }) + else: + # Fields mode metadata + ui_data["field_names"] = [field_names] # Field visualization data + # Add optional fields if available if volume is not None: ui_data["volume"] = [volume] if area is not None: ui_data["area"] = [area] - if field_names: - print(f"[PreviewMeshVTK] Mesh info: watertight={is_watertight}, volume={volume}, area={area}, fields={field_names}") + if viewer_type == "texture": + print(f"[PreviewMeshVTK] Texture mode info: watertight={is_watertight}, volume={volume}, area={area}, texture={has_texture}, vertex_colors={has_vertex_colors}") + elif field_names: + print(f"[PreviewMeshVTK] Fields mode info: watertight={is_watertight}, volume={volume}, area={area}, fields={field_names}") else: - print(f"[PreviewMeshVTK] Mesh info: watertight={is_watertight}, volume={volume}, area={area}, no fields") + print(f"[PreviewMeshVTK] Fields mode info: watertight={is_watertight}, volume={volume}, area={area}, no fields") return {"ui": ui_data} diff --git a/web/js/mesh_preview_dual.js b/web/js/mesh_preview_dual.js index 0df6e0f8..5b7e8870 100644 --- a/web/js/mesh_preview_dual.js +++ b/web/js/mesh_preview_dual.js @@ -39,8 +39,12 @@ app.registerExtension({ console.log('[GeomPack Dual] Created iframe with minHeight:', iframe.style.minHeight); // Point to unified dual VTK.js HTML viewer (with cache buster) + // Note: viewer will be dynamically switched based on mode in onExecuted iframe.src = "/extensions/ComfyUI-GeometryPack/viewer_dual.html?v=" + Date.now(); + // Track current viewer type to avoid unnecessary reloads + let currentViewerType = "fields"; + // Create mesh info panel const infoPanel = document.createElement("div"); infoPanel.style.backgroundColor = "#1a1a1a"; @@ -126,6 +130,14 @@ app.registerExtension({ } const layout = message.layout[0]; + const mode = message.mode?.[0] || "fields"; + console.log(`[GeomPack Dual] onExecuted: layout=${layout}, mode=${mode}`); + + // Determine which viewer to use based on mode + const viewerType = mode === "texture" ? "texture" : "fields"; + const viewerUrl = viewerType === "texture" + ? "/extensions/ComfyUI-GeometryPack/viewer_dual_textured.html" + : "/extensions/ComfyUI-GeometryPack/viewer_dual.html"; let infoHTML = ''; let postMessageData = { @@ -157,8 +169,15 @@ app.registerExtension({ const extentsStr2 = extents2.length === 3 ? `${extents2.map(v => v.toFixed(2)).join(' × ')}` : 'N/A'; + // Build info panel with mode info + const modeLabel = mode.charAt(0).toUpperCase() + mode.slice(1); + const modeColor = mode === "texture" ? '#c8c' : '#6cc'; + infoHTML = `
+ Mode: + ${modeLabel} + Mesh 1 Mesh 2 @@ -189,13 +208,27 @@ app.registerExtension({ `; } - // Add field info if available - if (message.common_fields && message.common_fields[0].length > 0) { - const commonFields = message.common_fields[0]; - infoHTML += ` - Fields: - ${commonFields.length} shared field(s) - `; + // Add mode-specific metadata + if (mode === "texture") { + // Texture mode info + if (message.has_texture_1 !== undefined) { + const tex1 = message.has_texture_1[0] ? 'Yes' : 'No'; + const tex2 = message.has_texture_2[0] ? 'Yes' : 'No'; + infoHTML += ` + Textures: + ${tex1} + ${tex2} + `; + } + } else { + // Fields mode info + if (message.common_fields && message.common_fields[0].length > 0) { + const commonFields = message.common_fields[0]; + infoHTML += ` + Fields: + ${commonFields.length} shared field(s) + `; + } } infoHTML += '
'; @@ -221,8 +254,15 @@ app.registerExtension({ const faces1 = message.face_count_1?.[0] || 'N/A'; const faces2 = message.face_count_2?.[0] || 'N/A'; + // Build info panel with mode info + const modeLabel = mode.charAt(0).toUpperCase() + mode.slice(1); + const modeColor = mode === "texture" ? '#c8c' : '#6cc'; + infoHTML = `
+ Mode: + ${modeLabel} (Overlay) + Mesh 1 Mesh 2 @@ -236,13 +276,27 @@ app.registerExtension({ ${faces2.toLocaleString()} `; - // Add color info if using vertex coloring - if (message.mesh_1_color && message.mesh_2_color) { - infoHTML += ` - Colors: - ${message.mesh_1_color[0]} - ${message.mesh_2_color[0]} - `; + // Add mode-specific metadata + if (mode === "texture") { + // Texture mode info + if (message.has_texture_1 !== undefined) { + const tex1 = message.has_texture_1[0] ? 'Yes' : 'No'; + const tex2 = message.has_texture_2[0] ? 'Yes' : 'No'; + infoHTML += ` + Textures: + ${tex1} + ${tex2} + `; + } + } else { + // Fields mode info + if (message.common_fields && message.common_fields[0].length > 0) { + const commonFields = message.common_fields[0]; + infoHTML += ` + Fields: + ${commonFields.length} shared field(s) + `; + } } infoHTML += '
'; @@ -265,11 +319,29 @@ app.registerExtension({ } }; - // Send message after iframe is loaded - if (iframeLoaded) { - sendMessage(); + // Reload iframe if viewer type changed + if (viewerType !== currentViewerType) { + console.log(`[GeomPack Dual] Switching viewer from ${currentViewerType} to ${viewerType}`); + currentViewerType = viewerType; + iframeLoaded = false; + + // Set up one-time load listener before changing src + const onViewerLoaded = () => { + console.log("[GeomPack Dual] New viewer loaded, sending mesh"); + iframeLoaded = true; + sendMessage(); + }; + iframe.addEventListener('load', onViewerLoaded, { once: true }); + + // Change iframe src to trigger reload + iframe.src = viewerUrl + "?v=" + Date.now(); } else { - setTimeout(sendMessage, 500); + // No viewer change needed, send message immediately or after short delay + if (iframeLoaded) { + sendMessage(); + } else { + setTimeout(sendMessage, 500); + } } }; diff --git a/web/js/mesh_preview_vtk.js b/web/js/mesh_preview_vtk.js index 0f514bf4..9e8a5882 100644 --- a/web/js/mesh_preview_vtk.js +++ b/web/js/mesh_preview_vtk.js @@ -40,8 +40,12 @@ app.registerExtension({ iframe.style.backgroundColor = "#2a2a2a"; // Point to VTK.js HTML viewer (with cache buster) + // Note: viewer will be dynamically switched based on mode in onExecuted iframe.src = "/extensions/ComfyUI-GeometryPack/viewer_vtk.html?v=" + Date.now(); + // Track current viewer type to avoid unnecessary reloads + let currentViewerType = "fields"; + // Create mesh info panel const infoPanel = document.createElement("div"); infoPanel.style.backgroundColor = "#1a1a1a"; @@ -155,7 +159,14 @@ app.registerExtension({ // 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}`); + const viewerType = message.viewer_type?.[0] || "fields"; + const mode = message.mode?.[0] || "fields"; + console.log(`[GeomPack VTK] Loading mesh: ${filename}, viewer type: ${viewerType}, mode: ${mode}`); + + // Determine which viewer HTML to use + const viewerUrl = viewerType === "texture" + ? "/extensions/ComfyUI-GeometryPack/viewer_vtk_textured.html" + : "/extensions/ComfyUI-GeometryPack/viewer_vtk.html"; // Update mesh info panel with metadata const vertices = message.vertex_count?.[0] || 'N/A'; @@ -177,8 +188,14 @@ app.registerExtension({ } // Build info HTML + const modeLabel = mode.charAt(0).toUpperCase() + mode.slice(1); + const modeColor = mode === "texture" ? '#c8c' : '#6cc'; + let infoHTML = `
+ Mode: + ${modeLabel} + Vertices: ${vertices.toLocaleString()} @@ -192,7 +209,7 @@ app.registerExtension({ ${extentsStr} `; - // Add optional fields if available + // Add watertight status (always shown) if (message.is_watertight !== undefined) { const watertight = message.is_watertight[0] ? 'Yes' : 'No'; const color = message.is_watertight[0] ? '#6c6' : '#c66'; @@ -202,12 +219,45 @@ app.registerExtension({ `; } - if (message.field_names && message.field_names[0]?.length > 0) { - const fields = message.field_names[0].join(', '); - infoHTML += ` - Fields: - ${fields} - `; + // Add mode-specific info + if (viewerType === "texture") { + // Texture mode info + if (message.visual_kind !== undefined) { + const visualKind = message.visual_kind[0] || 'none'; + infoHTML += ` + Visual Kind: + ${visualKind} + `; + } + if (message.has_texture !== undefined) { + const hasTexture = message.has_texture[0] ? 'Yes' : 'No'; + const texColor = message.has_texture[0] ? '#c8c' : '#888'; + infoHTML += ` + Textures: + ${hasTexture} + `; + } + if (message.has_vertex_colors !== undefined) { + const hasColors = message.has_vertex_colors[0] ? 'Yes' : 'No'; + infoHTML += ` + Vertex Colors: + ${hasColors} + `; + } + } else { + // Fields mode info + if (message.field_names && message.field_names[0]?.length > 0) { + const fields = message.field_names[0].join(', '); + infoHTML += ` + Fields: + ${fields} + `; + } else { + infoHTML += ` + Fields: + None + `; + } } infoHTML += '
'; @@ -231,13 +281,31 @@ app.registerExtension({ } }; - // Send message after iframe is loaded - if (iframeLoaded) { - console.log("[GeomPack VTK DEBUG] Iframe already loaded, sending immediately"); - sendMessage(); + // Reload iframe if viewer type changed + if (viewerType !== currentViewerType) { + console.log(`[GeomPack VTK] Switching viewer from ${currentViewerType} to ${viewerType}`); + currentViewerType = viewerType; + iframeLoaded = false; + + // Set up one-time load listener before changing src + const onViewerLoaded = () => { + console.log("[GeomPack VTK] New viewer loaded, sending mesh"); + iframeLoaded = true; + sendMessage(); + }; + iframe.addEventListener('load', onViewerLoaded, { once: true }); + + // Change iframe src to trigger reload + iframe.src = viewerUrl + "?v=" + Date.now(); } else { - console.log("[GeomPack VTK DEBUG] Waiting for iframe to load..."); - setTimeout(sendMessage, 500); + // No viewer change needed, send message immediately or after short delay + 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 || {})); diff --git a/web/viewer_dual.html b/web/viewer_dual.html index 1e75298a..8f76bda1 100644 --- a/web/viewer_dual.html +++ b/web/viewer_dual.html @@ -73,6 +73,9 @@ flex-wrap: wrap; font-size: 12px; color: #ccc; + min-height: 48px; + overflow-x: auto; + overflow-y: visible; } #controls button, #controls select { background: #333; @@ -108,6 +111,106 @@ display: flex; gap: 12px; } + + /* Settings panel */ + #settingsPanel { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #1a1a1a; + border: 1px solid #555; + border-radius: 6px; + padding: 16px; + z-index: 2000; + display: none; + min-width: 280px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); + } + #settingsPanel.visible { + display: block; + } + #settingsPanel h3 { + margin: 0 0 12px 0; + color: #ccc; + font-size: 14px; + font-weight: normal; + border-bottom: 1px solid #333; + padding-bottom: 8px; + } + #settingsPanel .setting-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + } + #settingsPanel .setting-row label { + flex: 0 0 100px; + color: #ccc; + font-size: 12px; + } + #settingsPanel .setting-row input[type="color"] { + flex: 1; + height: 32px; + border: 1px solid #555; + background: #333; + cursor: pointer; + } + #settingsPanel .setting-row input[type="number"] { + flex: 1; + background: #333; + color: #ccc; + border: 1px solid #555; + padding: 6px 8px; + border-radius: 3px; + font-size: 12px; + font-family: monospace; + } + #settingsPanel .setting-row select { + flex: 1; + background: #333; + color: #ccc; + border: 1px solid #555; + padding: 6px 8px; + border-radius: 3px; + font-size: 12px; + font-family: monospace; + } + #settingsPanel .button-row { + display: flex; + gap: 8px; + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid #333; + } + #settingsPanel .button-row button { + flex: 1; + padding: 6px 12px; + font-size: 12px; + } + #settingsOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1999; + display: none; + } + #settingsOverlay.visible { + display: block; + } + #settingsButton { + background: #333; + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + } @@ -130,9 +233,49 @@
Ready to load meshes
+ + +
+
+

Appearance Settings

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + + + + @@ -149,9 +292,6 @@ -
@@ -881,6 +1021,114 @@ } }); + // Settings panel handlers + const settingsButton = document.getElementById('settingsButton'); + const settingsPanel = document.getElementById('settingsPanel'); + const settingsOverlay = document.getElementById('settingsOverlay'); + const pointSizeInput = document.getElementById('pointSize'); + const meshColorInput = document.getElementById('meshColor'); + const edgeWidthInput = document.getElementById('edgeWidth'); + const representationSettings = document.getElementById('representationSettings'); + const parallelProjectionInput = document.getElementById('parallelProjectionSetting'); + const applySettingsButton = document.getElementById('applySettings'); + const cancelSettingsButton = document.getElementById('cancelSettings'); + + // Store current settings + let currentPointSize = 3; + let currentMeshColor = '#ffffff'; + let currentEdgeWidth = 1; + let currentRepresentation = 2; // 2 = Surface + let currentParallelProjection = false; + + // Open settings panel + settingsButton.addEventListener('click', () => { + // Update inputs with current values + pointSizeInput.value = currentPointSize; + meshColorInput.value = currentMeshColor; + edgeWidthInput.value = currentEdgeWidth; + representationSettings.value = currentRepresentation.toString(); + parallelProjectionInput.checked = currentParallelProjection; + + settingsPanel.classList.add('visible'); + settingsOverlay.classList.add('visible'); + }); + + // Close settings panel + function closeSettingsPanel() { + settingsPanel.classList.remove('visible'); + settingsOverlay.classList.remove('visible'); + } + + // Close on overlay click + settingsOverlay.addEventListener('click', closeSettingsPanel); + + // Cancel button + cancelSettingsButton.addEventListener('click', closeSettingsPanel); + + // Apply settings + applySettingsButton.addEventListener('click', () => { + const newPointSize = parseFloat(pointSizeInput.value); + const newColor = meshColorInput.value; + const newEdgeWidth = parseFloat(edgeWidthInput.value); + const newRepresentation = parseInt(representationSettings.value); + const newParallelProjection = parallelProjectionInput.checked; + + console.log('[GeomPack VTK Dual Viewer] Applying settings - Point Size:', newPointSize, 'Color:', newColor, 'Edge Width:', newEdgeWidth, 'Representation:', newRepresentation, 'Parallel:', newParallelProjection); + + // Convert hex color to RGB (0-1 range) + const r = parseInt(newColor.substr(1, 2), 16) / 255; + const g = parseInt(newColor.substr(3, 2), 16) / 255; + const b = parseInt(newColor.substr(5, 2), 16) / 255; + + // Apply settings to actors + if (viewerLayout === 'side_by_side') { + // Apply to both actors in side-by-side mode + const leftProperty = leftActor.getProperty(); + const rightProperty = rightActor.getProperty(); + + leftProperty.setPointSize(newPointSize); + leftProperty.setColor(r, g, b); + leftProperty.setLineWidth(newEdgeWidth); + leftProperty.setRepresentation(newRepresentation); + + rightProperty.setPointSize(newPointSize); + rightProperty.setColor(r, g, b); + rightProperty.setLineWidth(newEdgeWidth); + rightProperty.setRepresentation(newRepresentation); + + // Apply parallel projection to both cameras + leftRenderer.getActiveCamera().setParallelProjection(newParallelProjection); + rightRenderer.getActiveCamera().setParallelProjection(newParallelProjection); + + leftRenderWindow.render(); + rightRenderWindow.render(); + } else { + // Apply to single actor in overlay mode + const property = singleActor.getProperty(); + property.setPointSize(newPointSize); + property.setColor(r, g, b); + property.setLineWidth(newEdgeWidth); + property.setRepresentation(newRepresentation); + + // Apply parallel projection to camera + singleRenderer.getActiveCamera().setParallelProjection(newParallelProjection); + + singleRenderWindow.render(); + } + + // Update stored settings + currentPointSize = newPointSize; + currentMeshColor = newColor; + currentEdgeWidth = newEdgeWidth; + currentRepresentation = newRepresentation; + currentParallelProjection = newParallelProjection; + + console.log('[GeomPack VTK Dual Viewer] Settings applied - RGB:', [r, g, b], 'Point Size:', newPointSize, 'Line Width:', newEdgeWidth, 'Representation:', newRepresentation, 'Parallel:', newParallelProjection); + + // Close panel + closeSettingsPanel(); + }); + // Event handlers document.getElementById('showEdges').addEventListener('change', (e) => { const edgeViz = e.target.checked; @@ -895,19 +1143,6 @@ } }); - document.getElementById('parallelProjection').addEventListener('change', (e) => { - const parallel = e.target.checked; - if (viewerLayout === 'side_by_side') { - leftRenderer.getActiveCamera().setParallelProjection(parallel); - rightRenderer.getActiveCamera().setParallelProjection(parallel); - leftRenderWindow.render(); - rightRenderWindow.render(); - } else { - singleRenderer.getActiveCamera().setParallelProjection(parallel); - singleRenderWindow.render(); - } - }); - document.getElementById('resetCamera').addEventListener('click', () => { if (!combinedBounds) return; diff --git a/web/viewer_dual_textured.html b/web/viewer_dual_textured.html new file mode 100644 index 00000000..71bb7ff1 --- /dev/null +++ b/web/viewer_dual_textured.html @@ -0,0 +1,1406 @@ + + + + + GeomPack VTK.js Dual Textured Mesh Viewer + + + +
+ +
+
+
Mesh 1
+
+
+
+
Mesh 2
+
+
+
+ + +
+
+
+ +
Ready to load meshes
+ + +
+
+

Appearance Settings

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
+ + + + + +