diff --git a/nodes/__init__.py b/nodes/__init__.py index 7bdad58..ba24836 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -11,6 +11,7 @@ from .preview_gaussian import PreviewGaussians from .preview_gaussian_spectate import PreviewGaussianSpectate from .preview_gaussian_dual import PreviewGaussianDual from .preview_gaussian_camera import PreviewGaussianCamera +from .preview_gaussian_dual import PreviewGaussianDual from .merge_gaussians import GaussianMerge from .load_ply import LoadPLY from .load_ply_output import LoadPLYOutput @@ -36,6 +37,7 @@ NODE_CLASS_MAPPINGS = { "GaussianExport": GaussianExport, "TransformGaussian": TransformGaussian, "GaussiansFromPointCloud": GaussiansFromPointCloud, + "PreviewGaussianDual": PreviewGaussianDual, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -50,4 +52,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { "GaussianExport": "Gaussian Export", "TransformGaussian": "Transform Gaussian", "GaussiansFromPointCloud": "Gaussians From Point Cloud", + "PreviewGaussianDual": "Preview Gaussian Dual", } diff --git a/nodes/preview_gaussian.py b/nodes/preview_gaussian.py index 1f6a6a5..76f3e93 100644 --- a/nodes/preview_gaussian.py +++ b/nodes/preview_gaussian.py @@ -67,22 +67,16 @@ class PreviewGaussians: "default": 50.0, "min": 5.0, "max": 170.0, "step": 1.0, "tooltip": "Vertical field of view in degrees", }), - "image_width": ("INT", { - "default": 512, "min": 64, "max": 4096, "step": 8, - }), - "image_height": ("INT", { - "default": 512, "min": 64, "max": 4096, "step": 8, - }), - "renderer": (["spark", "playcanvas"], { - "default": "spark", + "renderer": (["playcanvas", "spark"], { + "default": "playcanvas", "tooltip": ( + "playcanvas — PlayCanvas Engine v2.19 GSplat renderer. " + "WebGPU when available, WebGL2 fallback. Good " + "performance on large scenes (5M+ splats). " + "\n" "spark — Three.js + WebGL2. Best SH3 fidelity, " "auto-detects all formats (PLY, compressed.ply, SPZ, " - "KSPLAT, SOG, SPLAT). " - "\n" - "playcanvas — WebGPU path (currently falls back to " - "spark; real adapter pending). Will win on large " - "scenes (5M+ splats) when implemented." + "KSPLAT, SOG, SPLAT)." ), }), "transport_format": (["ply", "spz"], { @@ -107,7 +101,7 @@ class PreviewGaussians: FUNCTION = "preview" CATEGORY = "viewer" - def preview(self, ply_path, fov_degrees, image_width, image_height, renderer, transport_format="ply"): + def preview(self, ply_path, fov_degrees, renderer, transport_format="ply"): if not ply_path: return {"ui": {"error": ["No PLY path provided"]}} if not os.path.exists(ply_path): @@ -117,18 +111,17 @@ class PreviewGaussians: file_size_mb = round(os.path.getsize(ply_path) / (1024 * 1024), 2) num_gaussians = _count_gaussians(ply_path) - intrinsics = get_default_intrinsics(image_width, image_height, fov_degrees) extrinsics = get_default_extrinsics() return {"ui": { "ply_file": [filename], "filename": [filename], - "ply_type": [folder_kind], # "input" | "output" — JS passes to /view?type= + "ply_type": [folder_kind], "ply_subfolder": [subfolder], "file_size_mb": [file_size_mb], "num_gaussians": [num_gaussians], "extrinsics": [extrinsics], - "intrinsics": [intrinsics], + "intrinsics": [None], "fov_degrees": [fov_degrees], "renderer": [renderer], "transport_format": [transport_format], diff --git a/nodes/preview_gaussian_dual.py b/nodes/preview_gaussian_dual.py index fbee0de..f69634f 100644 --- a/nodes/preview_gaussian_dual.py +++ b/nodes/preview_gaussian_dual.py @@ -1,13 +1,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later -"""Dual Gaussian splat viewer — side-by-side or slider comparison.""" +"""PreviewGaussianDual — side-by-side or slider comparison of two gaussian splats.""" import os -from .common import ( - get_default_extrinsics, - get_default_intrinsics, -) +from .common import get_default_extrinsics from .preview_gaussian import _resolve_for_view, _count_gaussians @@ -29,31 +26,25 @@ class PreviewGaussianDual: "layout": (["side_by_side", "slider"], { "default": "side_by_side", "tooltip": ( - "side_by_side — two synchronized viewports next to each other. " - "\n" - "slider — single viewport with a draggable divider revealing " - "one splat on each side." + "side_by_side — two synchronized viewports. " + "slider — overlaid with a draggable divider." ), }), "fov_degrees": ("FLOAT", { "default": 50.0, "min": 5.0, "max": 170.0, "step": 1.0, - "tooltip": "Vertical field of view in degrees", }), - "image_width": ("INT", { - "default": 512, "min": 64, "max": 4096, "step": 8, - }), - "image_height": ("INT", { - "default": 512, "min": 64, "max": 4096, "step": 8, - }), - "renderer": (["spark", "playcanvas"], { - "default": "spark", + "renderer": (["playcanvas", "spark"], { + "default": "playcanvas", + "tooltip": ( + "playcanvas — PlayCanvas Engine v2.19 GSplat. " + "spark — Three.js + WebGL2, all formats." + ), }), "transport_format": (["ply", "spz"], { "default": "ply", "tooltip": ( - "ply — lossless float32. " - "\n" - "spz — ~9x smaller, server transcodes once and caches." + "ply — lossless, larger. " + "spz — server transcodes to SPZ v2, ~9x smaller." ), }), }, @@ -65,34 +56,29 @@ class PreviewGaussianDual: CATEGORY = "viewer" def preview(self, ply_path_1, ply_path_2, layout, fov_degrees, - image_width, image_height, renderer, transport_format="ply"): - results = {} - for idx, path in enumerate([ply_path_1, ply_path_2], start=1): - suffix = str(idx) - if not path: - return {"ui": {"error": [f"No PLY path provided for input {idx}"]}} - if not os.path.exists(path): - return {"ui": {"error": [f"File not found (input {idx}): {path}"]}} + renderer, transport_format="ply"): + for i, p in enumerate([ply_path_1, ply_path_2], 1): + if not p: + return {"ui": {"error": [f"No PLY path provided for input {i}"]}} + if not os.path.exists(p): + return {"ui": {"error": [f"File not found (input {i}): {p}"]}} - filename, subfolder, folder_kind = _resolve_for_view(path) - file_size_mb = round(os.path.getsize(path) / (1024 * 1024), 2) - num_gaussians = _count_gaussians(path) + fn1, sub1, kind1 = _resolve_for_view(ply_path_1) + fn2, sub2, kind2 = _resolve_for_view(ply_path_2) - results[f"ply_file_{suffix}"] = [filename] - results[f"filename_{suffix}"] = [filename] - results[f"ply_type_{suffix}"] = [folder_kind] - results[f"ply_subfolder_{suffix}"] = [subfolder] - results[f"file_size_mb_{suffix}"] = [file_size_mb] - results[f"num_gaussians_{suffix}"] = [num_gaussians] - - intrinsics = get_default_intrinsics(image_width, image_height, fov_degrees) - extrinsics = get_default_extrinsics() - - results["layout"] = [layout] - results["extrinsics"] = [extrinsics] - results["intrinsics"] = [intrinsics] - results["fov_degrees"] = [fov_degrees] - results["renderer"] = [renderer] - results["transport_format"] = [transport_format] - - return {"ui": results} + return {"ui": { + "ply_file_1": [fn1], + "ply_type_1": [kind1], + "ply_subfolder_1": [sub1], + "file_size_mb_1": [round(os.path.getsize(ply_path_1) / (1024 * 1024), 2)], + "num_gaussians_1": [_count_gaussians(ply_path_1)], + "ply_file_2": [fn2], + "ply_type_2": [kind2], + "ply_subfolder_2": [sub2], + "file_size_mb_2": [round(os.path.getsize(ply_path_2) / (1024 * 1024), 2)], + "num_gaussians_2": [_count_gaussians(ply_path_2)], + "layout": [layout], + "fov_degrees": [fov_degrees], + "renderer": [renderer], + "transport_format": [transport_format], + }} diff --git a/nodes/preview_gaussian_spectate.py b/nodes/preview_gaussian_spectate.py index 1b48651..3a08f34 100644 --- a/nodes/preview_gaussian_spectate.py +++ b/nodes/preview_gaussian_spectate.py @@ -48,12 +48,6 @@ class PreviewGaussianSpectate: "from PreviewGaussians' 50." ), }), - "image_width": ("INT", { - "default": 768, "min": 64, "max": 4096, "step": 8, - }), - "image_height": ("INT", { - "default": 512, "min": 64, "max": 4096, "step": 8, - }), "move_speed": ("FLOAT", { "default": 2.0, "min": 0.05, "max": 50.0, "step": 0.05, "tooltip": ( @@ -62,12 +56,12 @@ class PreviewGaussianSpectate: "scroll the mouse wheel. Shift = 3x boost." ), }), - "renderer": (["spark", "playcanvas"], { - "default": "spark", + "renderer": (["playcanvas", "spark"], { + "default": "playcanvas", "tooltip": ( - "spark — Three.js + WebGL2, all formats. " - "playcanvas — WebGPU adapter (falls back to spark " - "for now)." + "playcanvas — PlayCanvas Engine v2.19 GSplat. " + "WebGPU when available, WebGL2 fallback. " + "spark — Three.js + WebGL2, all formats." ), }), "transport_format": (["ply", "spz"], { @@ -86,7 +80,7 @@ class PreviewGaussianSpectate: FUNCTION = "preview" CATEGORY = "viewer" - def preview(self, ply_path, fov_degrees, image_width, image_height, + def preview(self, ply_path, fov_degrees, move_speed, renderer, transport_format="ply"): if not ply_path: return {"ui": {"error": ["No PLY path provided"]}} @@ -97,7 +91,6 @@ class PreviewGaussianSpectate: file_size_mb = round(os.path.getsize(ply_path) / (1024 * 1024), 2) num_gaussians = _count_gaussians(ply_path) - intrinsics = get_default_intrinsics(image_width, image_height, fov_degrees) extrinsics = get_default_extrinsics() return {"ui": { @@ -108,7 +101,7 @@ class PreviewGaussianSpectate: "file_size_mb": [file_size_mb], "num_gaussians": [num_gaussians], "extrinsics": [extrinsics], - "intrinsics": [intrinsics], + "intrinsics": [None], "fov_degrees": [fov_degrees], "renderer": [renderer], "transport_format": [transport_format], diff --git a/web/js/gaussian_preview.js b/web/js/gaussian_preview.js index 0f9807f..0db1bf4 100644 --- a/web/js/gaussian_preview.js +++ b/web/js/gaussian_preview.js @@ -542,7 +542,7 @@ app.registerExtension({ // immediately + wrap each widget's callback to keep the // mirror up to date. const TRACKED = new Set([ - "fov_degrees", "image_width", "image_height", + "fov_degrees", "renderer", "transport_format", ]); this.properties = this.properties || {}; @@ -829,11 +829,15 @@ app.registerExtension({ const plyType = message.ply_type?.[0] || "output"; const plySubfolder = message.ply_subfolder?.[0] || ""; - // Resize node to match image aspect ratio from intrinsics + // Resize node to match image aspect ratio from intrinsics. + // When width/height are 0 (auto), keep the current node + // size and let the viewer fill whatever space is available. if (intrinsics && intrinsics[0] && intrinsics[1]) { const imageWidth = intrinsics[0][2] * 2; // cx * 2 const imageHeight = intrinsics[1][2] * 2; // cy * 2 - this.resizeToAspectRatio(imageWidth, imageHeight); + if (imageWidth > 0 && imageHeight > 0) { + this.resizeToAspectRatio(imageWidth, imageHeight); + } } // Spectate mode: forward the move_speed slider to the @@ -988,5 +992,169 @@ app.registerExtension({ return r; }; } + + // ============================================================== + // Preview Gaussian Dual — side-by-side / slider comparison + // ============================================================== + if (nodeData.name === "PreviewGaussianDual") { + console.log("[GaussianPack] Registering Preview Gaussian Dual node"); + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + const node = this; + + const container = document.createElement("div"); + container.style.cssText = "width:100%;height:100%;display:flex;flex-direction:column;background:#1a1a1a;overflow:hidden;position:relative;"; + + // Loading bar + const loadBar = document.createElement("div"); + loadBar.style.cssText = "position:absolute;top:0;left:0;right:0;height:4px;background:rgba(0,0,0,0.35);z-index:100;transition:opacity 0.3s;pointer-events:none;opacity:0;"; + const loadFill = document.createElement("div"); + loadFill.style.cssText = "height:100%;width:0%;background:linear-gradient(90deg,#3a8,#6c6);transition:width 0.15s;"; + loadBar.appendChild(loadFill); + + const iframe = document.createElement("iframe"); + iframe.style.cssText = "width:100%;flex:1 1 0;min-height:0;border:none;background:#1a1a1a;"; + + const infoPanel = document.createElement("div"); + infoPanel.style.cssText = "background:#1a1a1a;border-top:1px solid #444;padding:6px 12px;font:10px monospace;color:#ccc;line-height:1.3;flex-shrink:0;overflow:hidden;"; + infoPanel.innerHTML = 'Dual view info will appear after execution'; + + container.appendChild(iframe); + container.appendChild(infoPanel); + container.appendChild(loadBar); + + const widget = this.addDOMWidget("preview_gaussian_dual", "GAUSSIAN_DUAL_PREVIEW", container, { serialize: false }); + let currentNodeSize = [768, 500]; + widget.computeSize = () => currentNodeSize; + + this.gaussianDualIframe = iframe; + this.gaussianDualInfoPanel = infoPanel; + this.setSize([768, 500]); + + let iframeLoaded = false; + iframe.addEventListener('load', () => { iframeLoaded = true; }); + + // Camera state persistence + this.properties = this.properties || {}; + window.addEventListener('message', (event) => { + if (event.source !== iframe.contentWindow) return; + if (event.data?.type === "CAMERA_STATE" && event.data.state) { + node.properties["Camera Config"] = { + cameraType: "perspective", + state: JSON.stringify(event.data.state), + }; + } + if (event.data?.type === "MESH_LOADED") { + loadBar.style.opacity = "0"; + const saved = node.properties["Camera Config"]; + if (saved?.state && iframe.contentWindow) { + try { + iframe.contentWindow.postMessage({ + type: "RESTORE_CAMERA_STATE", + state: JSON.parse(saved.state), + }, "*"); + } catch(_) {} + } + } + if (event.data?.type === "MESH_ERROR") { + loadFill.style.background = "#c44"; + loadFill.style.width = "100%"; + } + }); + + const onExecuted = this.onExecuted; + this.onExecuted = function(message) { + onExecuted?.apply(this, arguments); + if (message?.error?.[0]) { + infoPanel.innerHTML = `