- Delete 7 dead-squat widgets (registered UI only for other packs' nodes: UniRig/SAM3D/Mocap/FBXPreview/BVH/pointcloud) + their 6 orphaned viewer HTMLs + the entire unused js/viewer tree (21 files, imported by nothing; only ui/styles.css survives) + unused utils barrel. - Rename namespace geompack -> geometrypack (= DisplayName, lowercased); all 17 surviving extensions now geometrypack.*. - Add event.source provenance guards to every message listener (2 inline, 2 factory handlers via new iframe param). - Stop touching document.body: upload file input stays detached (click() works detached in modern browsers); folder autocomplete dropdown becomes a manual popover inside the widget's own DOM (top layer escapes canvas transforms without shared-DOM appends). Passes comfy-test's javascript isolation lint with 0 errors.
265 lines
13 KiB
JavaScript
265 lines
13 KiB
JavaScript
/**
|
|
* ComfyUI GeomPack - VTK.js Textured Mesh Preview Widget
|
|
* Supports GLB/GLTF with textures and PBR materials via VTK.js GLTFImporter
|
|
*/
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
|
|
// Auto-detect extension folder name (handles ComfyUI-GeometryPack or comfyui-geometrypack)
|
|
const EXTENSION_FOLDER = (() => {
|
|
const url = import.meta.url;
|
|
const match = url.match(/\/extensions\/([^/]+)\//);
|
|
return match ? match[1] : "ComfyUI-GeometryPack";
|
|
})();
|
|
|
|
console.log("[GeomPack] Loading VTK.js textured mesh preview extension...");
|
|
|
|
app.registerExtension({
|
|
name: "geometrypack.meshpreview.vtk.textured",
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name === "GeomPackPreviewMeshVTKWithTexture") {
|
|
console.log("[GeomPack] Registering Preview Mesh (VTK with Textures) node");
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
|
|
|
// 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";
|
|
container.style.overflow = "hidden";
|
|
|
|
// Create iframe for VTK.js textured viewer
|
|
const iframe = document.createElement("iframe");
|
|
iframe.style.width = "100%";
|
|
iframe.style.flex = "1 1 0";
|
|
iframe.style.minHeight = "0";
|
|
iframe.style.border = "none";
|
|
iframe.style.backgroundColor = "#2a2a2a";
|
|
|
|
// Point to VTK.js textured HTML viewer (with cache buster)
|
|
// Use unified v2 viewer with modular architecture
|
|
iframe.src = `/extensions/${EXTENSION_FOLDER}/viewer_vtk_textured.html?v=` + Date.now();
|
|
|
|
// Create mesh info panel
|
|
const infoPanel = document.createElement("div");
|
|
infoPanel.style.backgroundColor = "#1a1a1a";
|
|
infoPanel.style.borderTop = "1px solid #444";
|
|
infoPanel.style.padding = "6px 12px";
|
|
infoPanel.style.fontSize = "10px";
|
|
infoPanel.style.fontFamily = "monospace";
|
|
infoPanel.style.color = "#ccc";
|
|
infoPanel.style.lineHeight = "1.3";
|
|
infoPanel.style.flexShrink = "0";
|
|
infoPanel.style.overflow = "hidden";
|
|
infoPanel.innerHTML = '<span style="color: #888;">Textured mesh info will appear here after execution</span>';
|
|
|
|
// Add iframe and info panel to container
|
|
container.appendChild(iframe);
|
|
container.appendChild(infoPanel);
|
|
|
|
// Add widget with required options
|
|
const widget = this.addDOMWidget("preview_vtk_textured", "MESH_PREVIEW_VTK_TEXTURED", container, {
|
|
getValue() { return ""; },
|
|
setValue(v) { }
|
|
});
|
|
|
|
widget.computeSize = () => [512, 640]; // Increased height for viewer + info panel
|
|
|
|
// Store iframe and info panel references
|
|
this.meshViewerIframeVTKTextured = iframe;
|
|
this.meshInfoPanelVTKTextured = infoPanel;
|
|
|
|
// Track iframe load state
|
|
let iframeLoaded = false;
|
|
iframe.addEventListener('load', () => {
|
|
iframeLoaded = true;
|
|
});
|
|
|
|
// Listen for messages from iframe
|
|
window.addEventListener('message', async (event) => {
|
|
// Without this check, every open viewer instance's listener
|
|
// fires for every iframe's messages, not just its own.
|
|
if (event.source !== iframe.contentWindow) return;
|
|
// Handle screenshot messages
|
|
if (event.data.type === 'SCREENSHOT' && event.data.image) {
|
|
console.log('[GeomPack VTK Textured] Received screenshot from iframe');
|
|
|
|
try {
|
|
// Convert base64 data URL to blob
|
|
const base64Data = event.data.image.split(',')[1];
|
|
const byteString = atob(base64Data);
|
|
const arrayBuffer = new ArrayBuffer(byteString.length);
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
|
|
for (let i = 0; i < byteString.length; i++) {
|
|
uint8Array[i] = byteString.charCodeAt(i);
|
|
}
|
|
|
|
const blob = new Blob([uint8Array], { type: 'image/png' });
|
|
|
|
// Generate filename with timestamp
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const filename = `vtk-textured-screenshot-${timestamp}.png`;
|
|
|
|
// Create FormData for upload
|
|
const formData = new FormData();
|
|
formData.append('image', blob, filename);
|
|
formData.append('type', 'output'); // Save to output directory
|
|
formData.append('subfolder', ''); // Root of output folder
|
|
|
|
// Upload to ComfyUI backend
|
|
console.log('[GeomPack VTK Textured] Uploading screenshot to server...');
|
|
const response = await fetch('/upload/image', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
console.log('[GeomPack VTK Textured] Screenshot saved to outputs folder:', result.name);
|
|
} else {
|
|
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[GeomPack VTK Textured] Error saving screenshot:', error);
|
|
}
|
|
}
|
|
// Handle error messages from iframe
|
|
else if (event.data.type === 'MESH_ERROR' && event.data.error) {
|
|
console.error('[GeomPack VTK Textured] Error from viewer:', event.data.error);
|
|
if (infoPanel) {
|
|
infoPanel.innerHTML = `<div style="color: #ff6b6b; padding: 8px;">Error: ${event.data.error}</div>`;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Set initial node size (increased for info panel)
|
|
this.setSize([512, 640]);
|
|
|
|
// Handle execution
|
|
const onExecuted = this.onExecuted;
|
|
this.onExecuted = function(message) {
|
|
onExecuted?.apply(this, arguments);
|
|
|
|
// The message IS the UI data (not message.ui)
|
|
if (message?.mesh_file && message.mesh_file[0]) {
|
|
const filename = message.mesh_file[0];
|
|
|
|
// 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] || [];
|
|
|
|
// Texture/material info
|
|
const hasTexture = message.has_texture?.[0] || false;
|
|
const hasVertexColors = message.has_vertex_colors?.[0] || false;
|
|
const hasMaterial = message.has_material?.[0] || false;
|
|
const visualKind = message.visual_kind?.[0] || 'none';
|
|
|
|
// 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(' x ')}`;
|
|
}
|
|
|
|
// Build info HTML
|
|
let infoHTML = `
|
|
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 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: 9px;">${boundsStr}</span>
|
|
|
|
<span style="color: #888;">Extents:</span>
|
|
<span>${extentsStr}</span>
|
|
|
|
<span style="color: #888;">Visual Type:</span>
|
|
<span style="color: #4fc3f7;">${visualKind}</span>
|
|
|
|
<span style="color: #888;">Texture:</span>
|
|
<span style="color: ${hasTexture ? '#6c6' : '#888'};">${hasTexture ? 'Yes' : 'No'}</span>
|
|
|
|
<span style="color: #888;">Vertex Colors:</span>
|
|
<span style="color: ${hasVertexColors ? '#6c6' : '#888'};">${hasVertexColors ? 'Yes' : 'No'}</span>
|
|
|
|
<span style="color: #888;">Material:</span>
|
|
<span style="color: ${hasMaterial ? '#6c6' : '#888'};">${hasMaterial ? 'Yes' : 'No'}</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>
|
|
`;
|
|
}
|
|
|
|
infoHTML += '</div>';
|
|
|
|
infoPanel.innerHTML = infoHTML;
|
|
|
|
// ComfyUI serves output files via /view API endpoint
|
|
const filepath = `/view?filename=${encodeURIComponent(filename)}&type=output&subfolder=`;
|
|
|
|
// Prepare metadata to pass to viewer
|
|
const metadata = {
|
|
has_texture: hasTexture,
|
|
has_vertex_colors: hasVertexColors,
|
|
has_material: hasMaterial,
|
|
visual_kind: visualKind
|
|
};
|
|
|
|
// Function to send message
|
|
const sendMessage = () => {
|
|
if (iframe.contentWindow) {
|
|
iframe.contentWindow.postMessage({
|
|
type: "LOAD_MESH",
|
|
filepath: filepath,
|
|
metadata: metadata,
|
|
timestamp: Date.now()
|
|
}, "*");
|
|
} else {
|
|
console.error("[GeomPack VTK Textured] Iframe contentWindow not available");
|
|
}
|
|
};
|
|
|
|
// Send message after iframe is loaded
|
|
if (iframeLoaded) {
|
|
sendMessage();
|
|
} else {
|
|
setTimeout(sendMessage, 500);
|
|
}
|
|
} else {
|
|
console.log("[GeomPack VTK Textured] No mesh_file in message data. Keys:", Object.keys(message || {}));
|
|
}
|
|
};
|
|
|
|
return r;
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log("[GeomPack] VTK.js textured mesh preview extension registered");
|