Two paper cuts spotted while wiring LoadPLY through PreviewGaussians:
## 1. Hardcoded `type=output` in the /view fetch
Repro: LoadPLY hands /home/work/.../ComfyUI/input/apple.ply to
PreviewGaussians. The JS fetches `/view?filename=apple.ply&type=output
&subfolder=` and gets 404 because the file lives in input/, not
output/. PLYs produced by GaussianMerge/GaussianExport/LiToExportPLY
(under output/) worked because the hardcoded `type=output` matched.
Fix is two-sided:
- Python (nodes/preview_gaussian.py): new `_resolve_for_view()`
walks the abs path against folder_paths.get_{output,input}_directory()
and returns (filename, subfolder, folder_kind). The kind is shipped
as a new UI field `ply_type` ("input" | "output"), plus
`ply_subfolder` for nested locations.
- JS (web/js/gaussian_preview.js): reads `ply_type` / `ply_subfolder`
off the onExecuted message and substitutes them into both URLs —
`/view?type=<kind>&subfolder=<sub>` for raw PLY transport, and
`/gaussianpack/spz?type=<kind>&subfolder=<sub>` for the SPZ
transcode route. The Python SPZ route already accepts both
type=input and type=output (spz_route.py:53-56), so no server-side
change needed.
Files truly outside ComfyUI's input/output trees still 404 (the
fallback returns type=output + bare basename), which matches the
prior behavior — the previewer can't serve arbitrary disk paths.
## 2. Spark heartbeat log spam
Every 120 frames (~2 s) the iframe printed
"[Spark] heartbeat: N frames" to the console. Useful while bringing
up the rendering loop, noise now that everything works. Dropped the
line; the "first frame rendered" trace stays as the smoke test.
Match LiTo's project layout. Net file moves:
spz_route.py -> nodes/spz_route.py
install.py -> root (was briefly in nodes/)
server_tools/ -> web/server_tools/ (Node JS subprocess
tool — non-python
asset, sits next to
browser-served web/)
Root and nodes/ now mirror ComfyUI-LiTo:
Root: __init__.py, install.py, pyproject.toml, comfy-env-root.toml,
comfy-test.toml, README.md, LICENSE, web/, scripts/, nodes/
nodes/: __init__.py, comfy-env.toml, common.py, preview_gaussian.py,
merge_gaussians.py, load_ply.py, spz_route.py
## Root __init__.py — LiTo-style
import logging
log = logging.getLogger("comfyui-gaussianpack")
from comfy_env import register_nodes
NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = register_nodes()
WEB_DIRECTORY = "./web"
Delegates node discovery to `comfy_env.register_nodes()`, which scans
`nodes/__init__.py` for the mappings. WEB_DIRECTORY stays at root
(points at top-level web/) so ComfyUI's static handler doesn't have to
walk into nodes/.
## nodes/__init__.py — clean child package
Imports PreviewGaussians, GaussianMerge, LoadPLY from sibling modules,
calls register_routes() for the SPZ HTTP route, exposes the two
mapping dicts. Dropped `WEB_DIRECTORY` (it belongs at root).
## nodes/comfy-env.toml — pixi env declaration
numpy + plyfile + nanogs (git pip). Same content as before — was
previously at root, where comfy-env's discovery wouldn't pick it up
for isolation purposes. Now lives where comfy-env expects.
## spz_route.py — path fix
NODE_SCRIPT was `THIS_DIR / "server_tools" / "ply_to_spz.mjs"` when the
file was at root. Now at nodes/ with server_tools moved to web/, so:
NODE_SCRIPT = THIS_DIR.parent / "web" / "server_tools" / "ply_to_spz.mjs"
Smoke test (worker pixi env):
PYTHONPATH=.../ComfyUI:. python -c "import nodes; print(nodes.NODE_CLASS_MAPPINGS)"
-> {'PreviewGaussians': ..., 'GaussianMerge': ..., 'LoadPLY': ...}
The prior commit (6908dbe) was lost when the local repo was re-cloned
before its push could land. Redoing the full GaussianPack-side work
in one pass, plus two new asks from this session:
1. Display name: "Preview Gaussians Turntable" -> "Preview Gaussians".
2. Camera: OrbitControls -> TrackballControls. ParaView-style free
rotation — no fixed up vector, no gimbal lock at the poles, can
roll the camera. Replaces the "bound at azimuth and bottom" feel.
## Rename (mechanical sed pass)
ComfyUI-GSViewer -> ComfyUI-GaussianPack
GSViewer -> GaussianPack (log tags, extension name)
gsviewer -> gaussianpack (route path, logger name)
GSVIEWER -> GAUSSIANPACK
Hit __init__.py, spz_route.py, pyproject.toml, comfyui_extension.json,
README.md, web/js/gaussian_preview.js.
## LiTo-style project structure
- comfy-test.toml — test matrix; workflow-level testing
happens downstream in ComfyUI-LiTo.
- comfy-env-root.toml — empty [node_reqs]; GaussianPack has no
downstream custom-node deps.
- comfy-env.toml — numpy + plyfile + NanoGS via pypi-git.
- install.py — two-liner mirroring LiTo's.
- requirements.txt — DELETED, superseded by comfy-env.toml.
## GaussianMerge node
merge_gaussians.py: takes a 3DGS .ply + target_count, writes a slimmer
.ply via NanoGS pairwise merging (MPMM — same algorithm
@playcanvas/splat-transform's `--decimate` uses). Pure Python, CPU only;
the upstream paper notes a CUDA port as future work. ~10-30s for 1M ->
200K splats.
Lazy-imports nanogs.simplification + nanogs.utils.params inside the
merge method to keep import cost off ComfyUI startup. Caches via
mtime + (target_count, opacity_threshold, k) sidecar — re-running with
the same inputs is a no-op.
Pass-through if target >= input. ratio is clamped to NanoGS's required
open (0, 1) interval.
Node parameters:
ply_path STRING, forceInput
target_count INT, default 200_000
output_filename STRING, default "merged"
opacity_threshold FLOAT, default 0.1
k INT, default 16
## Camera: TrackballControls
Vendored web/js/TrackballControls.js (three@0.169.0, ~18 KB) +
importmap entry alongside OrbitControls. The viewer now constructs
TrackballControls with rotateSpeed=3.5, zoomSpeed=1.2, panSpeed=0.8,
staticMoving=true (no inertia drift). ResizeObserver now calls
controls.handleResize() so mouse-to-NDC mapping stays correct after
canvas resize. .target / .update() / .enabled API surface is identical
between OrbitControls and TrackballControls — no callsite changes
needed in gaussian_preview.js.
OrbitControls.js stays vendored (still in importmap) for any future
node that wants the constrained "preserve up" behavior.
Workflow chain (LiTo apple):
LoadImage -> LiToImageTo3D -> LiToExportPLY (~225 MB raw)
|
GaussianMerge(target=200_000)
|
PreviewGaussians (~50 MB)
ComfyUI's widgetInputs extension strips the first entry from
info.widgets_values for forceInput-bound STRING inputs (ply_path),
silently shifting every other widget's value left by one. The
in-memory info we receive in onConfigure is already lossy — the
original fov_degrees value is gone before our code can see it.
Solution: persist every named widget into properties["Widget Values"]
on each change (same pattern we already use for "Camera Config"). On
load, prefer properties over info.widgets_values. For old workflows
without Widget Values, fall back to Camera Config.fov so at least
that survives.
* Bulk rename "ComfyUI-TurntableGSViewer" -> "ComfyUI-GSViewer" across
package metadata, route prefix (/turntablegsviewer/spz ->
/gsviewer/spz), logger name, extension id, and all console-log tags.
* Extend onConfigure to defend against stale workflow schemas: rescues
stranded camera-state JSON into properties["Camera Config"] and
resets combo widgets whose value isn't in the option list. Fixes the
validator-reject when an old-schema basic.json (camera state glued
into widgets_values[3]) is loaded against today's renderer combo.
Collapse the 4-node PlyPreview surface to one node:
- PreviewGaussians: takes ply_path (STRING, forceInput) plus camera-
intrinsics widgets (fov_degrees, image_width, image_height) directly
on the node. NODE_CLASS_MAPPINGS = {"PreviewGaussians": ...}.
- Drop load_gaussian_ply.py, load_gaussian_ply_path.py,
process_gaussian_ply.py and the /plypreview/files refresh route.
- common.py stays — get_default_intrinsics + get_default_extrinsics
are reused.
- Frontend JS unchanged except the preview block targets
"PreviewGaussians". The LoadPLY-dropdown refresh block is left in
place as harmless dead code in case the loader node comes back.
- requirements.txt cleared (no Python deps; PLY parsing is in the
browser via gsplat.js).
- README slimmed to a few lines.
- Removed Figure1/2 + image/ + README.en.md + README.zh-CN.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy of XuanYu-github/comfyui-PlyPreview (GPL-3.0) repackaged under
new name and ownership. Metadata changes only:
- pyproject.toml: project name + URLs + tool.comfy.DisplayName
- comfyui_extension.json: name / author / repo
- removed .github/workflows/publish_action.yml (upstream's auto-publish)
Internal class names, NODE_CLASS_MAPPINGS keys, CATEGORY strings,
and source code are intact for further customization.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>