diff --git a/Figure1.png b/Figure1.png new file mode 100644 index 0000000..89da1c6 Binary files /dev/null and b/Figure1.png differ diff --git a/Figure2.png b/Figure2.png new file mode 100644 index 0000000..1f0fe04 Binary files /dev/null and b/Figure2.png differ diff --git a/README.md b/README.md index 30cbc3c..8ad328c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,12 @@ git clone https://github.com/XuanYu-github/comfyui-PlyPreview.git 2) 连接 Preview Gaussian,执行后 iframe 显示控制条(Scale、Reset View、Screenshot)和信息面板。 3) 若启用透明度过滤,会在源旁生成 `_opacity{threshold}.ply`。 +示例: + +![Sharp → PlyPreview 工作流示例](Figure1.png) + +![加载与刷新 PLY 列表示例](Figure2.png) + ## 依赖 - ComfyUI(默认已启用 DOM widgets)。 - Python:`plyfile`、`numpy`(缺失时 `pip install plyfile numpy`)。 diff --git a/__init__.py b/__init__.py index 68a9b76..a974be3 100644 --- a/__init__.py +++ b/__init__.py @@ -7,6 +7,13 @@ from .load_gaussian_ply import LoadGaussianPLY from .load_gaussian_ply_path import LoadGaussianPLYPath from .process_gaussian_ply import ProcessGaussianPLY from .preview_gaussian import PreviewGaussianNode +from aiohttp import web + +try: + from server import PromptServer +except Exception as e: # pragma: no cover - ComfyUI runtime only + PromptServer = None + print(f"[PlyPreview] Warning: server import failed: {e}") WEB_DIRECTORY = "web" @@ -24,6 +31,18 @@ NODE_DISPLAY_NAME_MAPPINGS = { "PlyPreviewPreviewGaussianEnhance": "Preview Gaussian Enhance", } +# Lightweight API to let the frontend refresh PLY dropdowns without restarting ComfyUI +if PromptServer is not None: + async def plypreview_list_ply_files(request): # pragma: no cover - runtime route + files = LoadGaussianPLY._get_ply_files() + return web.json_response({"files": files}) + + try: + PromptServer.instance.routes.get("/plypreview/files")(plypreview_list_ply_files) + print("[PlyPreview] Registered /plypreview/files refresh endpoint") + except Exception as e: # pragma: no cover + print(f"[PlyPreview] Warning: failed to register refresh endpoint: {e}") + __all__ = [ "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", diff --git a/web/js/gaussian_preview.js b/web/js/gaussian_preview.js index 3ddb4b5..d71ffb7 100644 --- a/web/js/gaussian_preview.js +++ b/web/js/gaussian_preview.js @@ -4,6 +4,7 @@ */ import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; // Auto-detect extension folder name (handles comfyui-PlyPreview or other folder names) const EXTENSION_FOLDER = (() => { @@ -18,6 +19,43 @@ app.registerExtension({ name: "plypreview.gaussianpreview", async beforeRegisterNodeDef(nodeType, nodeData, app) { + // Auto-refresh PLY dropdown list for the file selector node + if (nodeData.name === "PlyPreviewLoadGaussianPLYEnhance") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + + const widget = (this.widgets || []).find(w => w.name === "ply_file"); + + const refreshList = async () => { + try { + const resp = await api.fetchApi("/plypreview/files"); + const json = await resp.json(); + const files = Array.isArray(json.files) && json.files.length > 0 ? json.files : ["No PLY files found"]; + if (widget) { + widget.options.values = files; + widget.value = files[0]; + this.setDirtyCanvas(true); + app.graph.setDirtyCanvas(true, true); + } + } catch (e) { + console.warn("[PlyPreview] Failed to refresh PLY list", e); + } + }; + + this.refreshPlyList = refreshList; + refreshList(); + + // Add in-node refresh button instead of relying on context menu + this.addWidget("button", "Refresh PLY list", () => this.refreshPlyList && this.refreshPlyList(), { + serialize: false, + width: 180, + }); + + return r; + }; + } + if (nodeData.name === "PlyPreviewPreviewGaussianEnhance") { console.log("[PlyPreview Gaussian] Registering Preview Gaussian Enhance node"); diff --git a/web/viewer_gaussian.html b/web/viewer_gaussian.html index ba04fce..faca78a 100644 --- a/web/viewer_gaussian.html +++ b/web/viewer_gaussian.html @@ -143,6 +143,12 @@ const infoPanel = document.getElementById('infoPanel'); const infoContent = document.getElementById('infoContent'); + // Make canvas focusable so keyboard controls (WASD/arrows) work after clicking the viewer + canvas.tabIndex = 0; + canvas.style.outline = "none"; + const focusCanvas = () => canvas.focus({ preventScroll: true }); + canvas.addEventListener('pointerdown', focusCanvas); + // gsplat.js components let scene = null; let camera = null;