textured unified

This commit is contained in:
Andrea Pozzetti
2026-02-12 21:10:40 +01:00
parent b772370f5c
commit f941d616ba
6 changed files with 2004 additions and 89 deletions
+118 -26
View File
@@ -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:
+57 -15
View File
@@ -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}
+90 -18
View File
@@ -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 = `
<div style="display: grid; grid-template-columns: auto 1fr 1fr; gap: 2px 12px;">
<span style="color: #888;">Mode:</span>
<span colspan="2" style="grid-column: 2 / 4; color: ${modeColor}; font-weight: bold;">${modeLabel}</span>
<span style="color: #888;"></span>
<span style="color: #999; font-weight: bold; border-bottom: 1px solid #333;">Mesh 1</span>
<span style="color: #999; font-weight: bold; border-bottom: 1px solid #333;">Mesh 2</span>
@@ -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 += `
<span style="color: #888;">Fields:</span>
<span colspan="2" style="grid-column: 2 / 4; color: #9c9;">${commonFields.length} shared field(s)</span>
`;
// 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 += `
<span style="color: #888;">Textures:</span>
<span style="color: ${message.has_texture_1[0] ? '#c8c' : '#888'};">${tex1}</span>
<span style="color: ${message.has_texture_2[0] ? '#c8c' : '#888'};">${tex2}</span>
`;
}
} else {
// Fields mode info
if (message.common_fields && message.common_fields[0].length > 0) {
const commonFields = message.common_fields[0];
infoHTML += `
<span style="color: #888;">Fields:</span>
<span colspan="2" style="grid-column: 2 / 4; color: #9c9;">${commonFields.length} shared field(s)</span>
`;
}
}
infoHTML += '</div>';
@@ -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 = `
<div style="display: grid; grid-template-columns: auto 1fr 1fr; gap: 2px 12px;">
<span style="color: #888;">Mode:</span>
<span colspan="2" style="grid-column: 2 / 4; color: ${modeColor}; font-weight: bold;">${modeLabel} (Overlay)</span>
<span style="color: #888;"></span>
<span style="color: #999; font-weight: bold; border-bottom: 1px solid #333;">Mesh 1</span>
<span style="color: #999; font-weight: bold; border-bottom: 1px solid #333;">Mesh 2</span>
@@ -236,13 +276,27 @@ app.registerExtension({
<span>${faces2.toLocaleString()}</span>
`;
// Add color info if using vertex coloring
if (message.mesh_1_color && message.mesh_2_color) {
infoHTML += `
<span style="color: #888;">Colors:</span>
<span>${message.mesh_1_color[0]}</span>
<span>${message.mesh_2_color[0]}</span>
`;
// 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 += `
<span style="color: #888;">Textures:</span>
<span style="color: ${message.has_texture_1[0] ? '#c8c' : '#888'};">${tex1}</span>
<span style="color: ${message.has_texture_2[0] ? '#c8c' : '#888'};">${tex2}</span>
`;
}
} else {
// Fields mode info
if (message.common_fields && message.common_fields[0].length > 0) {
const commonFields = message.common_fields[0];
infoHTML += `
<span style="color: #888;">Fields:</span>
<span colspan="2" style="grid-column: 2 / 4; color: #9c9;">${commonFields.length} shared field(s)</span>
`;
}
}
infoHTML += '</div>';
@@ -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);
}
}
};
+82 -14
View File
@@ -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 = `
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 8px;">
<span style="color: #888;">Mode:</span>
<span style="color: ${modeColor}; font-weight: bold;">${modeLabel}</span>
<span style="color: #888;">Vertices:</span>
<span>${vertices.toLocaleString()}</span>
@@ -192,7 +209,7 @@ app.registerExtension({
<span>${extentsStr}</span>
`;
// 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 += `
<span style="color: #888;">Fields:</span>
<span style="font-size: 9px;">${fields}</span>
`;
// Add mode-specific info
if (viewerType === "texture") {
// Texture mode info
if (message.visual_kind !== undefined) {
const visualKind = message.visual_kind[0] || 'none';
infoHTML += `
<span style="color: #888;">Visual Kind:</span>
<span>${visualKind}</span>
`;
}
if (message.has_texture !== undefined) {
const hasTexture = message.has_texture[0] ? 'Yes' : 'No';
const texColor = message.has_texture[0] ? '#c8c' : '#888';
infoHTML += `
<span style="color: #888;">Textures:</span>
<span style="color: ${texColor};">${hasTexture}</span>
`;
}
if (message.has_vertex_colors !== undefined) {
const hasColors = message.has_vertex_colors[0] ? 'Yes' : 'No';
infoHTML += `
<span style="color: #888;">Vertex Colors:</span>
<span>${hasColors}</span>
`;
}
} else {
// Fields mode info
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: 9px; color: #6cc;">${fields}</span>
`;
} else {
infoHTML += `
<span style="color: #888;">Fields:</span>
<span style="color: #888;">None</span>
`;
}
}
infoHTML += '</div>';
@@ -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 || {}));
+251 -16
View File
@@ -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;
}
</style>
</head>
<body>
@@ -130,9 +233,49 @@
</div>
<div id="loading">Ready to load meshes</div>
<!-- Settings overlay and panel -->
<div id="settingsOverlay"></div>
<div id="settingsPanel">
<h3>Appearance Settings</h3>
<div class="setting-row">
<label>Point Size:</label>
<input type="number" id="pointSize" min="1" max="20" step="1" value="3">
</div>
<div class="setting-row">
<label>Mesh Color:</label>
<input type="color" id="meshColor" value="#ffffff">
</div>
<div class="setting-row">
<label>Edge Width:</label>
<input type="number" id="edgeWidth" min="1" max="10" step="1" value="1">
</div>
<div class="setting-row">
<label>Render Mode:</label>
<select id="representationSettings">
<option value="2">Surface</option>
<option value="1">Wireframe</option>
<option value="0">Points</option>
</select>
</div>
<div class="setting-row">
<label>
<input type="checkbox" id="parallelProjectionSetting"> Parallel Projection
</label>
</div>
<div class="button-row">
<button id="applySettings">Apply</button>
<button id="cancelSettings">Cancel</button>
</div>
</div>
</div>
<div id="controls">
<!-- Settings button -->
<button id="settingsButton" title="Appearance Settings">⚙</button>
<label class="divider"></label>
<label>
<input type="checkbox" id="showEdges"> Edges
</label>
@@ -149,9 +292,6 @@
<label class="divider"></label>
<button id="resetCamera">Reset Camera</button>
<label>
<input type="checkbox" id="parallelProjection"> Parallel
</label>
<!-- Field visualization controls -->
<div class="field-controls" id="fieldControls">
@@ -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;
File diff suppressed because it is too large Load Diff