67 Commits
Author SHA1 Message Date
PozzettiAndrea b5a5c241d3 comfy-env-root.toml: [node_reqs] is now [node_packs]
The section never held nodes -- every entry is a whole pack repo. Read
literally, `[node_reqs]` says "the requirements of my node", which is pip
packages, and that section genuinely exists one file over as [dependencies]
in comfy-env.toml. `[node_packs]` names the unit and sits parallel to the
existing [cuda_packages].

Requires comfy-env >= 0.4.27, which renames the key with no shim: the root
file has a closed schema, so an old comfy-env rejects [node_packs] by name
and a new one rejects [node_reqs] by name. Neither fails silently.
2026-08-24 15:48:51 +02:00
PozzettiAndrea a342f20402 Serve frontend JS from javascript/, keep vendor bundles out of the main realm
Move web/ to javascript/ and declare it via [tool.comfy] web.

Rename the seven third-party bundles (playcanvas, spark, three.js, gsplat,
OrbitControls, TrackballControls) to .mjs. Every reference to them is inside the
HTML viewers, but ComfyUI auto-imports **/*.js under the web dir into the shared
browser realm, so ~8MB of vendor code was being evaluated in the main page on
every startup -- and with it 36 collision errors: document.body injection,
document-level listeners, unguarded message listeners and self.onmessage writes.
The importmap values, script srcs and three.module's internal ./three.core ref
are rewritten to match.

DisplayName becomes "GaussianPack": the namespace enforced on
registerExtension names is derived from it, and "ComfyUI-GaussianPack" made
that prefix invalid. The analysis extension is renamed to match.

js/gaussian_preview.js hooked "PlyPreviewLoadGaussianPLYEnhance", a node id
that appears nowhere in this repo, so the PLY dropdown auto-refresh had never
run; it now targets LoadPLY and LoadPLYOutput, the two nodes that actually carry
the ply_file widget. Guard its message listener on event.source.

Asset seeding resolves the input directory through
folder_paths.get_input_directory(), so it follows ComfyUI Desktop and
--input-directory instead of writing into the code tree.

comfy-test.toml moves to the current schema: [test.workflows] cuda (there is
no "gpu" key -- the accelerator is named by backend, and the old spelling parsed
to an empty list, so CUDA runners silently ran the full unfiltered set), an
explicit [test.platforms] platforms allowlist (per-platform booleans are no
longer supported), and the javascript level.
2026-08-22 20:56:09 +02:00
PozzettiAndrea 39e4b5ebcd add av 2026-06-07 19:27:34 +02:00
PozzettiAndrea 6753db6f21 added viewer node for geometrypack 2026-06-07 19:16:51 +02:00
PozzettiAndreaandClaude Opus 4.8 f927d3d638 README + icon + gallery preview (dev)
- README: add SAM3-style 3-option install (Manager / Git URL / manual) and a
  "report problems" note; keep the live gallery link + demo video.
- assets/icon.png (400x400) cropped from the apple-splat preview screenshot.
- assets/gallery-preview.png: screenshot of the dev/linux-cpu test gallery
  (4/4 passing). README image points at the dev raw URL; the gallery link still
  points at the live gh-pages results.
- pyproject.toml: set Icon (dev raw URL) and add the comfy-env install warning
  to the description.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 09:00:00 +02:00
Andrea Pozzetti f16cac0b3e Update README.md 2026-06-07 09:00:00 +02:00
PozzettiAndrea 9d284836b6 add cenv 2026-06-07 09:00:00 +02:00
PozzettiAndrea 56364f2aad no more non cp1252 chars 2026-06-07 09:00:00 +02:00
PozzettiAndrea d62a86fbe7 better controls 2026-06-07 09:00:00 +02:00
PozzettiAndrea dabd3a5527 4 workflows 2026-06-07 09:00:00 +02:00
PozzettiAndrea 77e923a585 improved viewer 2026-06-07 09:00:00 +02:00
PozzettiAndreaandClaude Opus 4.6 714706fa7c Merge main (fullscreen spectate + playcanvas update) into dev
Take main's versions of conflicting dual viewer files as base.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 09:00:00 +02:00
PozzettiAndrea 6a0212fea4 preview gaussian dual 2026-06-07 09:00:00 +02:00
PozzettiAndrea fd37ba899d fullscreen mode for spectate node 2026-06-04 20:36:33 +02:00
PozzettiAndrea 73b37ef2f8 update 2026-06-02 02:52:08 +02:00
PozzettiAndreaandClaude Opus 4.7 a79e2a53f2 PreviewGaussianCamera: fix bg shape for packed gsplat + debug prints
`gsplat.rasterization` defaults to packed=True. In packed mode the
underlying rasterize_to_pixels flattens valid splats to (nnz, 2), so
image_dims = means2d.shape[:-2] = () and the bg-shape assertion
expects exactly (channels,) = (3,) — NOT (C, 3). The previous
`background.unsqueeze(0)` gave (1, 3) and tripped:
  AssertionError: torch.Size([1, 3])  (_wrapper.py:598)

Drop the unsqueeze; pass bg as a 1D (3,) tensor and let gsplat
broadcast it. (If we ever flip to packed=False the bg shape would
need to be (C, 3) instead — the comment in the source flags both.)

Also add debug prints across the pipeline:
- ply load: gaussian count, world-space bounds, scale + opacity ranges
- render(): extrinsics/intrinsics shapes + first translation + first K
- _render_gsplat: input tensor shapes + per-view VRAM probe
  (allocated + reserved) on first and last view
- _render_torch: z-cull diagnostics per view

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 19:32:10 +02:00
PozzettiAndreaandClaude Opus 4.7 da1bd7ffe6 PreviewGaussianCamera: per-view gsplat + VRAM eviction + frustum cull
- _request_vram_eviction helper (vendored from PanoramaDepthMerge): asks
  the parent ComfyUI process via comfy_worker.call_parent IPC to evict
  sibling-worker patchers (HYWM2 DiT, MoGe2, etc.) before rendering.
  Call it from render() with peak_bytes = N_gauss × H × W × 1e-3.
- _render_gsplat now loops per-view instead of batching all viewmats
  through gsplat.rasterization at once. The fused multi-camera path
  allocates a tile-intersection workspace ≈ N_gauss × V × tiles, which
  OOMs at V=12, N=5.5M, 1024² on a 3090. Per-view shrinks workspace
  by 1/V at negligible per-call overhead.
- torch.no_grad() on both _render_gsplat and _render_torch (pure
  inference — no need for autograd buffers).
- X/Y frustum cull in _render_torch after the radius calc: drop
  gaussians whose 3σ ellipse lies entirely outside the image bounds.
  gsplat does this internally; the torch fallback didn't.
- comfy-env.toml: add safetensors for the SPZ route (cosmetic
  worker-startup warning otherwise).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 23:39:59 +02:00
PozzettiAndrea 09a97e1c09 ga 2026-05-29 19:53:20 +02:00
PozzettiAndrea 65db3634b1 comment trim 2026-05-29 19:52:07 +02:00
Andrea Pozzetti e8f0aba145 Add PreviewGaussianCamera node + comfy-env isolation manifest
New node renders a 3DGS PLY from given camera poses to an IMAGE batch.
Tiered renderer: try gsplat (CUDA, reference, ~10-50 ms/frame at 1M
gauss) -> fall back to pure-torch EWA splatter (slower but runs on
any torch backend: CUDA, ROCm, MPS, CPU).

Inputs:
  ply_path     STRING (forceInput)  3DGS PLY path
  extrinsics   EXTRINSICS           [N, 4, 4] w2c
  intrinsics   INTRINSICS           [N, 3, 3] pixel-K or normalized-K
                                    (auto-sniffed via fx<2 rule)
  image_width  INT                  default 1024
  image_height INT                  default 1024
Optional:
  near_plane    FLOAT default 0.01
  background    COMBO black/white
  force_backend COMBO auto/gsplat/torch

Output: IMAGE [N, H, W, 3] in [0, 1] on CPU.

Cross-platform strategy:
  * gsplat declared in nodes/comfy-env.toml [cuda] packages. cuda-wheels
    has a recipe (gsplat.yml) that publishes prebuilt wheels for
    cu12.x/torch2.x/cp31x. On non-NVIDIA installs (AMD ROCm, Apple
    Silicon Metal, CPU-only) cuda-wheels skips the package silently.
  * Node detects gsplat at runtime via try/import. ImportError -> fall
    back to the pure-torch EWA splatter, which works on any torch
    backend without extra deps. The fallback is naive vectorized
    (no tile optimization), so ~1-10 s/frame depending on gaussian
    count + resolution. Correct, just not real-time.

PLY parsing follows the standard 3DGS layout (graphdeco-inria
convention): x/y/z, rot_0..3 (wxyz), scale_0..2 (log space), opacity
(logit space), f_dc_0..2 (sh band 0), optional f_rest_* (higher SH
bands).

comfy-env.toml is new for this pack. Declares plyfile/aiohttp/numpy/
pillow + NanoGS git dep for the existing pack nodes that need them.
Wraps the whole pack in a worker subprocess. Existing iframe-based
PreviewGaussians' route registration becomes a no-op in the worker
(routes can't be registered to the host server from a subprocess),
which is a known trade-off.

Register in nodes/__init__.py alongside the existing entries.
2026-05-29 19:40:47 +02:00
PozzettiAndrea 32ce4c4d4b add spectate + rotation nodes 2026-05-27 11:47:49 +02:00
PozzettiAndrea 6f8632efff bump 2026-05-19 17:51:45 +02:00
Andrea Pozzetti 872a9620eb pyproject: fix PublisherId pozzettiandrea -> pznodes; drop comfy-env warning (not used here) 2026-05-19 12:37:20 +02:00
PozzettiAndrea e0364a9f5f docs: warn about experimental comfy-env install in README + pyproject 2026-05-16 21:39:13 +02:00
PozzettiAndrea dbfe9ebfc3 Trigger CI 2026-05-16 20:46:43 +02:00
PozzettiAndrea 9188ac555a Merge dev into main: workflows, comfy-env removal, gallery link 2026-05-16 20:33:08 +02:00
PozzettiAndrea 5d7672c34f Add .github/workflows/ — pr-gate, publish, run-tests
Three standard comfy-test scaffolding workflows, lifted verbatim from
ComfyUI-TRELLIS2 (identical across TRELLIS2/CADabra/HYWM2/GeometryPack
— no per-repo customization needed; they reference
PozzettiAndrea/comfy-test/.github/workflows/* and GitHub injects the
repo context):

- pr-gate.yml     PR + push-to-main + manual: runs comfy-test gate,
                  publishes results to gh-pages, screenshots the
                  gallery index at 1200x700 to gh-pages/gallery-preview.png
- publish.yml     pyproject.toml changes -> publish to Comfy registry
- run-tests.yml   push-to-dev -> runs the test matrix from comfy-test.toml

Closes the gap that made the README's gallery thumbnail 404 — it
points at https://pozzettiandrea.github.io/ComfyUI-GaussianPack/
gallery-preview.png, which now gets generated on each pr-gate run.
2026-05-16 20:32:17 +02:00
Andrea Pozzetti e439634477 Update README with new asset link
Removed an outdated link and added a new one.
2026-05-16 20:24:21 +02:00
Andrea Pozzetti f50a0f4952 Merge pull request #2 from PozzettiAndrea/dev
Dev
2026-05-16 20:11:48 +02:00
PozzettiAndrea b8de26b68c Drop comfy-env dependency: stdlib + relative import only
User wants GaussianPack fully self-contained — no comfy-env in the
runtime path. Three swaps + one orphan deletion:

1. prestartup_script.py: `comfy_env.copy_files(src, dst)` -> stdlib
   shutil.copy2 loop. Same semantics (top-level files, non-clobbering),
   6 lines. Fixes the NameError that snuck in earlier when the
   `from comfy_env import copy_files` line was removed without
   replacement; in the test workspace `apple.ply` actually lands in
   `input/` now.

2. __init__.py: `comfy_env.register_nodes()` -> `from .nodes import
   NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS`. That's literally
   what register_nodes() boils down to when no nodes/comfy-env.toml is
   present (the no-isolation branch). Hyphenated parent package names
   are fine for relative imports — verified earlier in this session
   with a Foo-Bar fixture via importlib.import_module.

3. nodes/__init__.py: docstring update — drop the "calls
   comfy_env.register_nodes()" mention now that the root does the
   relative import directly.

4. comfy-env-root.toml: deleted. The [node_reqs] section only had
   meaning for comfy-env's isolation discovery; with no comfy-env in
   play it was dead metadata.

`comfy-test.toml` stays (that's a different tool — comfy-test, not
comfy-env). `requirements.txt` stays (numpy + plyfile + NanoGS git).

`grep -rn comfy_env` across the repo returns 0 hits. Import smoke
test in the worker pixi env resolves all 5 nodes
(PreviewGaussians, GaussianMerge, LoadPLY, GaussianAnalysis,
GaussianExport).
2026-05-16 19:55:52 +02:00
PozzettiAndrea a279c930f8 Add gh-pages test-gallery link to README
Same pattern as ComfyUI-TRELLIS2: centered clickable thumbnail +
"View Live Test Gallery →" link, pointing at
https://pozzettiandrea.github.io/ComfyUI-GaussianPack/

Assumes the standard gh-pages publish path that comfy-test writes to
on push (gallery-preview.png lives at the page root). If no run has
been published yet, the image 404s harmlessly and the text link still
works.
2026-05-16 19:51:50 +02:00
PozzettiAndrea dc6909a94e recurse glob 2026-05-16 16:18:40 +02:00
PozzettiAndrea 006259f0b2 ms 2026-05-16 16:12:57 +02:00
PozzettiAndrea 93bc2e3775 LoadPLY: switch to LoadImage idiom — bare filenames + get_annotated_filepath
Previous design scanned input/ AND output/ and stored prefixed combo
entries like 'input/apple.ply'. When comfy-test ran the workflow, the
saved value 'input/apple.ply' didn't match anything the node could
resolve, and VALIDATE_INPUTS rejected the prompt:

  Custom validation failed for node: ply_file -
  LoadPLY: 'input/apple.ply' is not a known PLY in input/ or output/

Refactor to mirror ComfyUI's stock LoadImage node:
- INPUT_TYPES lists ONLY input/, bare filenames (no folder prefix)
- IS_CHANGED uses folder_paths.get_annotated_filepath(name)
- VALIDATE_INPUTS uses folder_paths.exists_annotated_filepath(name)
- load() returns folder_paths.get_annotated_filepath(name)

`get_annotated_filepath` understands `[input]/name`, `[temp]/name`,
and `[output]/name` annotations transparently, so power users can
type those into the combo if they need output/ access — same as
LoadImage. The default UX is "pick from input/".

Output-side PLYs from upstream nodes (GaussianMerge, GaussianExport,
LiToExportPLY) are wired directly into PreviewGaussians via the
STRING output — no LoadPLY needed.

workflows/merge_splats.json: rewrite widgets_values from
"input/apple.ply" -> "apple.ply" to match the new bare-filename
convention.
2026-05-16 16:06:21 +02:00
PozzettiAndrea cdbff35038 Run all 5 levels: syntax, install, registration, instantiation, execution 2026-05-16 16:04:02 +02:00
PozzettiAndrea cda2ef417c Match comfy-test.toml shape to ComfyUI-TRELLIS2
User asked for the test config to mirror TRELLIS2's. Changes:

- levels: ["syntax", "install", "registration", "instantiation"]
       -> ["syntax", "install", "execution"]
  Drops the two intermediate levels (execution implies them) and
  adds `execution` so the actual workflow runs. Without `execution`
  the test was passing vacuously without ever loading the workflow
  JSON.
- publish: true -> 'true' (string, matching TRELLIS2's spelling)
- [test.platforms]: dropped macos_desktop / windows_desktop keys
  to match TRELLIS2's key set. Kept CPU rows enabled and GPU rows
  disabled (inverse of TRELLIS2 since GaussianPack is pure-Python).

[test.workflows] unchanged: cpu = ["merge_splats"], gpu = [].
2026-05-16 16:02:06 +02:00
PozzettiAndrea bbe103e24f Broaden register_routes except clause; unblocks comfy-test instantiation
comfy-test's instantiation level was passing with 0 nodes instead of
the 5 we register, because nodes/__init__.py crashed at module-load:

  nodes/__init__.py:17           _register_spz_route()
  nodes/spz_route.py             from server import PromptServer
  ComfyUI/server.py:8            import nodes
  ComfyUI/nodes.py:26            import comfy.diffusers_load
  comfy/sd.py:7                  from comfy import model_management
  comfy/model_management.py:254  total_vram = get_total_memory(get_torch_device())
  RuntimeError: Cannot access accelerator device when none is available.

ComfyUI's model_management.py evaluates `torch.device(torch.cuda.
current_device())` at module-import time. comfy-test's test
subprocess monkey-patches torch.cuda.{is_available,device_count,
current_device} but `torch.device(0)` still crashes on CPU-only.

Our `except ImportError` clause missed the RuntimeError, so the
exception escaped and nodes/__init__.py never completed →
register_nodes() returned 0 → test "passes" vacuously with
`All 0 node(s) instantiated successfully!`.

Fix: catch `Exception` instead of `ImportError`. Real ComfyUI
startup is unaffected — by the time custom_nodes load, `server` is
already in sys.modules so the import is a no-op lookup with no
transitive imports.

Verified locally:
  [comfy-env] Importing nodes (root)...
  WARNING SPZ route not registered (RuntimeError: Cannot access accelerator device...)
  [comfy-env] Imported nodes root: 5 nodes
  [comfy-env] Registered 5 total nodes
  MAPPINGS: ['PreviewGaussians', 'GaussianMerge', 'LoadPLY', 'GaussianAnalysis', 'GaussianExport']

Next `cds test gaussianpack` should now print
`All 5 node(s) instantiated successfully!`.
2026-05-16 15:59:32 +02:00
PozzettiAndrea d1b617b112 workflow definition 2026-05-16 15:42:15 +02:00
PozzettiAndrea 1bf075ac52 good toggles for comfy test 2026-05-16 15:40:53 +02:00
PozzettiAndrea eee3bee6ba new workflow 2026-05-16 15:40:07 +02:00
PozzettiAndrea 99606e0144 Expand k tooltip on GaussianMerge with actual guidance
The widget's tooltip was just `"kNN candidate count for the merge
graph."` — technically correct but didn't tell the user whether
higher means better or worse, what to set it to, or what it costs.

Three lines now explain:
- What k does (search breadth for candidate merge pairs).
- The trade-off (higher = better selector choice, O(N*k) memory).
- Pragmatic defaults: 16 = sweet spot, 8 = fast draft, 24-32 on
  specular-heavy scenes, 32+ wasted.

No behavior change.
2026-05-16 15:38:50 +02:00
PozzettiAndrea cdb36341ee Resolve PLY → /view params via folder_paths (live), not common.py cache
User feedback: the path → (filename, subfolder, kind) resolution
should read folder_paths directly rather than relying on
COMFYUI_INPUT_FOLDER / COMFYUI_OUTPUT_FOLDER captured in common.py
at module import.

Folded the two-base scan into `folder_paths.get_directory_by_type(t)`
(folder_paths.py:214) over ("output", "input", "temp") in priority
order. Three benefits:

- Picks up runtime config: --input-directory CLI flag, mid-session
  setattr on folder_paths, etc. The previous version froze at first
  import of common.py.
- Adds "temp" coverage for free.
- Single source of truth — common.py no longer needs to mirror
  folder_paths constants for this codepath.

The COMFYUI_INPUT_FOLDER / COMFYUI_OUTPUT_FOLDER definitions in
common.py stay (the other nodes still import them for their own
defaults); only preview_gaussian.py stops using them.
2026-05-16 15:34:14 +02:00
PozzettiAndrea d4be0bda1d Fix /view 404 for PLYs under input/; remove Spark heartbeat log
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.
2026-05-16 15:24:22 +02:00
PozzettiAndrea 4974530084 Add GaussianAnalysis + GaussianExport nodes
## GaussianAnalysis (viewer/Gaussian Analysis)

Header-only PLY inspector — instant even for hundreds-of-MB scenes.
Reports:
  - splat count + file size + bytes/splat
  - PLY encoding (ascii vs binary little/big endian)
  - SH degree (derived from f_rest_* count: 0/9/24/45 -> 0/1/2/3)
  - precision (float32 all / uint8 all / mixed)
  - feature flags: has f_dc_*, has RGB uchar, has opacity, has scale_*,
    has rot_*
  - flavor heuristic: "raw 3DGS PLY (SH degree N)" / "SH-stripped"
    / "RGB-baked" / "compressed / packed"

OUTPUT_NODE: the report is also dumped into the node body via the `ui`
payload, while a STRING is returned so it can be chained into a Note
or printed downstream.

Sanity-tested against the bundled assets/apple.ply (200,000 splats,
47.30 MB, raw 3DGS SH=3, float32 all).

## GaussianExport (viewer/Gaussian Export)

PLY -> PLY (or PLY -> SPZ) reformatter. Three modes:

  ply         passthrough copy (just rename).
  ply_no_sh   strip f_rest_* (view-dependent SH coefficients). Keeps
              the DC term (f_dc_0/1/2 = base color) so the splat still
              looks right; loses specular highlights. ~3-5x smaller.
              Pure Python via plyfile.
  spz         SPZ v2 (gzipped) via the vendored ply_to_spz.mjs (same
              Node subprocess spz_route.py uses for the in-flight
              viewer transcode). ~9x smaller; SH quantizes to 4 bits.
              Requires Node on PATH.

Output is the absolute path to the produced file, so the node chains
into PreviewGaussians / GaussianMerge directly.

Typical use:
  LoadPLY -> GaussianMerge(target=200k) -> GaussianExport(ply_no_sh)
          -> PreviewGaussians

That gets you ~50 MB scenes from ~225 MB raw outputs without losing
view-independent appearance.
2026-05-16 15:19:44 +02:00
PozzettiAndrea 1a638ac662 Add prestartup_script.py to seed ComfyUI/input/ with bundled assets
Mirrors LiTo's prestartup pattern: on each ComfyUI startup, copies
files from this repo's `assets/` directory (currently apple.ply, ~50MB)
into the host ComfyUI's `input/` directory.

Uses `comfy_env.copy_files`, which is non-clobbering — pre-existing
files in input/ are left alone, so user edits and uploads survive
restarts. Skipped if assets/ is missing (e.g. fresh shallow clone).
2026-05-16 15:12:49 +02:00
PozzettiAndrea 11323079d2 added workflow 2026-05-16 15:08:00 +02:00
PozzettiAndrea abd1838e37 add nanogs 2026-05-16 15:01:43 +02:00
PozzettiAndrea e843d5bef0 Replace nodes/comfy-env.toml with a plain root requirements.txt
GaussianPack only needs three pure-Python deps (no CUDA, no conda-side
wheels), so the comfy-env pixi spec was overkill. requirements.txt at
the root is the standard custom-node convention and ComfyUI-Manager
auto-installs it on first launch.

Translation:

  [dependencies]
  numpy = ">=1.24"             ->  numpy>=1.24
  plyfile = "*"                ->  plyfile

  [pypi-dependencies]
  nanogs = { git = "..." }     ->  git+https://github.com/saliteta/NanoGS.git

Without `nodes/comfy-env.toml`, comfy_env.register_nodes() falls back
to its main-process branch (imports nodes/__init__.py normally — no
subprocess isolation), which is exactly what we want for these pure-
Python nodes.
2026-05-16 15:00:43 +02:00
PozzettiAndrea 6bac79935e Reorganize: python in nodes/, non-python in web/, root __init__ uses register_nodes
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': ...}
2026-05-16 14:49:10 +02:00
PozzettiAndrea 0ce11ce8a4 Add LoadPLY file-picker node
User asked to "copy the Load PLY node from HallettVisual/comfyui-
GaussianViewer". After cloning + reading the source, that repo doesn't
ship a LoadPLY node — every PLY input there is forceInput-only
(GaussianViewer / GeomPackRenderGaussian / ExtrinsicsToPose). So
writing one from scratch in the spirit of the request.

`load_ply.py` scans both ComfyUI's input/ AND output/ directories
recursively for .ply files; the combo entries are prefixed with the
folder kind ("input/foo.ply", "output/lito_output.ply") to disambiguate
collisions. LiTo's LiToExportPLY writes to output/, so its files appear
in the dropdown directly — no manual copying.

Implements:
- INPUT_TYPES re-scans on each schema evaluation (no stale lists).
- IS_CHANGED keys on the picked file's mtime so re-runs catch edits.
- VALIDATE_INPUTS rejects entries that disappeared between schema
  fetch and execute.

Output is a STRING `ply_path` (absolute), so it slots directly into
PreviewGaussians / GaussianMerge's `ply_path` forceInput. Lets users
preview an arbitrary PLY without re-running LiTo, or pipe a
hand-edited PLY into GaussianMerge.
2026-05-15 18:32:25 +02:00