Files
yic03685andClaude Opus 4.8 0a2f4041e9 Add Preview Gaussian (Arrow Nav) node
A self-contained ComfyUI custom node: a Gaussian Splatting previewer with
keyboard navigation on top of the standard gsplat.js mouse controls.

  Up/Down            zoom in/out
  Left/Right         orbit horizontally
  PageUp/PageDown    tilt up/down (Shift+Up/Down alias)
  mouse wheel/drag   still work

Navigation reuses gsplat OrbitControls' own wheel/drag handlers via synthetic
events (per animation frame while held), so dampening and clamping match the
mouse exactly. Viewer HTML + gsplat bundle are vendored from comfy-3d-viewers
so the node does not depend on GeometryPack's startup file-copy.

GPL-3.0-or-later; adapts/vendors GPL code from comfy-3d-viewers and
ComfyUI-GeometryPack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:08:57 -07:00

88 lines
2.8 KiB
Python

# SPDX-License-Identifier: GPL-3.0-or-later
"""
ComfyUI-GaussianArrowNav
A drop-in alternative to GeometryPack's "Preview Gaussian" node whose viewer
adds arrow-key navigation (zoom / orbit / tilt) on top of the usual mouse
controls. The Python side is intentionally a thin path-resolver — identical in
behaviour to GeomPackPreviewGaussian — so it accepts the same inputs and can be
wired into the same workflows. All the navigation logic lives in the frontend
(web/viewer_gaussian_arrownav.html).
"""
import logging
import os
log = logging.getLogger("gaussianarrownav")
try:
import folder_paths
COMFYUI_OUTPUT_FOLDER = folder_paths.get_output_directory()
except (ImportError, AttributeError):
COMFYUI_OUTPUT_FOLDER = None
class PreviewGaussianArrowNav:
"""Preview a Gaussian Splatting PLY with an arrow-key navigable viewer."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"ply_path": ("STRING", {"forceInput": True}),
},
"optional": {
# Same custom socket types as GeometryPack's camera nodes, so a
# GeomPack EXTRINSICS/INTRINSICS output can drive the initial view.
"extrinsics": ("EXTRINSICS",),
"intrinsics": ("INTRINSICS",),
},
}
RETURN_TYPES = ()
FUNCTION = "preview"
OUTPUT_NODE = True
CATEGORY = "geompack/visualization"
def preview(self, ply_path: str, extrinsics=None, intrinsics=None):
"""Resolve the PLY path and hand metadata to the frontend widget."""
if not ply_path:
log.info("No PLY path provided")
return {"ui": {"error": ["No PLY path provided"]}}
if not os.path.exists(ply_path):
log.info("PLY file not found: %s", ply_path)
return {"ui": {"error": [f"File not found: {ply_path}"]}}
filename = os.path.basename(ply_path)
# Prefer a path relative to the ComfyUI output dir when possible.
if COMFYUI_OUTPUT_FOLDER and ply_path.startswith(COMFYUI_OUTPUT_FOLDER):
relative_path = os.path.relpath(ply_path, COMFYUI_OUTPUT_FOLDER)
else:
relative_path = filename
file_size_mb = os.path.getsize(ply_path) / (1024 * 1024)
log.info("Loading PLY: %s (%.2f MB)", filename, file_size_mb)
ui_data = {
"ply_file": [relative_path],
"filename": [filename],
"file_size_mb": [round(file_size_mb, 2)],
}
if extrinsics is not None:
ui_data["extrinsics"] = [extrinsics]
if intrinsics is not None:
ui_data["intrinsics"] = [intrinsics]
return {"ui": ui_data}
NODE_CLASS_MAPPINGS = {
"PreviewGaussianArrowNav": PreviewGaussianArrowNav,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PreviewGaussianArrowNav": "Preview Gaussian (Arrow Nav)",
}