Skip to main content

nodehammer

CI MIT license C++23 WebAssembly

Turn High-Energy Physics detector geometry into a scene you can actually fly through.


Detector geometries (ROOT/TGeo, DD4hep, GDML, GeoModel, …) are huge, deeply nested, and built for simulation — not for looking at. nodehammer converts them into a clean, semantically-tagged scene graph and renders it in real time with a physically-based pipeline, natively or straight in the browser.

   TGeo / DD4hep ── (GDML/Geant4, GeoModel: planned)
              │
              ▼
    Semantic scene graph (.nhb / .nhb.zst)
    nodes · shapes · materials · deduplicated transforms
              │
       ┌──────┴──────┐
       ▼             ▼
  glTF / OBJ    Render IR (NHR8, FlatBuffers)
                       │
                       ▼
              Interactive viewer
        native (Metal / GLCORE / D3D11) · web (WebGPU / GLES3, wasm)

Every stage is driven by declarative TOML config — keep_if/drop_if selection rules with a small predicate-expression language (tag.sensitive == "true" && any(path ~= "**/Pixels/**")), material remapping, and analytical boolean cuts — so you can go from "the whole detector" to "just the Pixel barrel" without touching code.

Status

nodehammer is under active, pre-1.0 development. Here's what's real today versus what's on the roadmap.

✅ Implemented

  • Importers: ROOT/TGeo and DD4hep, each independently switchable at build time (NODEHAMMER_WITH_TGEO, NODEHAMMER_WITH_DD4HEP).
  • Exporters: glTF/GLB and OBJ (render IR), plus the native .nhb/.nhb.zst FlatBuffers semantic-scene container — a documented, zstd-compressible on-disk format.
  • Analytical geometry surgery: semantic-level azimuthal wedge cuts and boolean operations via manifold, correct on non-manifold swept primitives, cooperative with UI progress.
  • CLI: convert, inspect, config, project, viewer.
  • Viewer rendering pipeline, live today:
    • Full offscreen HDR pipeline with ACES / Reinhard / AgX tonemapping and exposure control
    • GTAO screen-space ambient occlusion, with selectable quality presets
    • Nishita single-scattering atmospheric sky (real Rayleigh + Mie, a real sun disc) baked into the IBL pipeline, plus a visible background dome
    • FXAA 3.11 console-quality antialiasing
    • Dynamic, GPU-load-adaptive render scale with a power-saving idle profile — renders on demand and caps the idle frame rate
    • Live ImPlot performance graphs in the debug panel
    • High-resolution, supersampled PNG screenshot export (nodehammer viewer shot -o out.png ...) — verified on Metal; the WebGL2/GLES3 and WebGPU readback paths compile and link but are not yet verified in-browser
  • Builds on macOS, Linux, and Windows natively (Metal, GLCORE, and D3D11 respectively), plus a wasm build targeting both GLES3 and WebGPU from one configure, with the browser shell auto-picking the right bundle via navigator.gpu. All five configurations build and pass the (headless) test suite in CI. A headless compute-worker wasm module (nodehammer-compute) runs tessellation and boolean cuts off the main thread. macOS is the primary hand-verified target for the interactive viewer itself — see below for what's untested elsewhere.
  • Modern C++: C++23 throughout (<print>, the works); requires GCC ≥ 14, Clang ≥ 18/libc++, or MSVC ≥ 19.37.

🚧 Planned / in progress

  • GDML/Geant4 importer — CMake option and library wiring exist (NODEHAMMER_WITH_GEANT4); the actual importer isn't written yet.
  • GeoModel importer — CMake option declared only; no implementation.
  • Windows viewer verification — the D3D11 backend builds and passes CI's headless tests, but the maintainer has no Windows machine to manually verify interactive rendering; the PNG-export readback for D3D11 is a build-time #error stub for now.
  • Bloom and IBL-quality levels — UI controls exist and are wired through RenderQualitySettings, but both are no-ops for now.
  • Project save/export system — the archive-as-project model below is landing now (working set, IndexedDB persistence, sidecar viewer mode, layered steer, Publish package); in-viewer editor windows and a named multi-document switcher are still to come. See docs/viewer-project-strategy.md.
  • Single-instance enforcement + a real macOS .app bundle — designed, not implemented; see docs/viewer-single-instance.md.

Project model: the archive is the project

One ZipWorkingSet is both the thing you author (edit / curate / drop files into) and the thing you publish (serialize → host → share). Authoring and publishing are the same object flowing in two directions.

A nodehammer project is a .nhproj archive: a ZIP container holding the config, geometry, and materials for a scene, plus an optional root nodehammer.toml that makes it self-describing. The same archive opens natively, in the browser, and behind a published link.

Term Meaning
Working set the live, editable in-memory project (ZipWorkingSet) — what the viewer edits
Archive a serialized .nhproj of a working set — the portable, publishable unit
Project manifest root nodehammer.toml inside the archive: [project] entry keys + [view] initial steer
Sidecar nh_manifest.json next to index.html — points at archive(s), carries deployment presentation (lock, steer overrides)
Steer view-state (camera, angle cut, rotation, toggles) — the ephemeral per-link layer, committed to the URL query
Provenance Empty | Local(name) | Remote(url) — where the working set came from; drives persistence and posture
Package the self-contained static folder emitted by Publish — drop it on any static host, zero server code

The web build has two postures from one wasm binary, branched on whether a sidecar is present:

  • Application mode (no sidecar) — empty start, editable, native-like; the working set auto-persists to IndexedDB and restores on reload. Your document.
  • Viewer mode (sidecar present) — fetches the archive, content locked, re-fetched from source on reload. A publication.

Content-lock is a deployment property of the sidecar; steer is never frozen, so a shared link keeps camera and cuts live. The only viewer→app bridge is explicit: save the .nhproj and open it in application mode.

Full design of record, including backend mapping and mode transitions: docs/viewer-project-strategy.md.

CLI

nodehammer convert   # geometry in, anything out — the pipeline
nodehammer inspect   # summary | tree | tags — ask about a scene
nodehammer config    # validate | flatten — TOML and Lua configs
nodehammer project   # pack | publish | info — .nhproj archives
nodehammer viewer    # open | serve | shot | bench — the app
nodehammer skills    # list | install — the agent skills shipped in the binary

Five nouns and one verb. The design of record, including the vocabulary and why each old name went, is docs/cli-design.md.

convert spans the whole pipeline, and the output format decides how far it runs: -o scene.nhb stops at the semantic scene, -o scene.glb tessellates first, and asking for both in one command is one pass rather than two. Tessellation is lossy by construction — solids become triangles — so .nhb is the faithful stop and .glb is a one-way door.

nodehammer project pack -c scene.toml -i odd.xml -o odd.nhproj
nodehammer viewer odd.nhproj serve          # open it in a browser
nodehammer project publish odd.nhproj -o site/

Agent skills

--help says what the flags are. It does not say which command blocks until a human closes a window, which output is a stable contract and which is prose for a reader, or that a missing importer is a compile-time option rather than a broken file. skills/nodehammer/SKILL.md says those things, and it ships inside the binary — so installing nodehammer, by any route, is enough to have it:

nodehammer skills install                  # ~/.agents/skills, linked from ~/.claude
nodehammer skills install --scope project  # this repository instead
nodehammer skills list                     # what this build carries

The real copy goes to .agents/skills/<name>/, the cross-agent location read by Claude Code, Codex CLI, Gemini CLI, Copilot and Cursor alike. .claude/skills/ gets a symlink to it — Claude Code reads only its own directory but follows a link out of it, so one copy serves both and they cannot drift. That link is created only if .claude/ already exists; inventing a config directory for a tool you may not have is not this command's business.

Because the bytes are compiled in, uvx nodehammer skills install works exactly the same way — the wheel's nodehammer command and the compiled executable are one implementation, so there is no packaging knob to get wrong and no way for a development install to carry a different skill than a released one.

This repository links its own skill into .agents/skills/ and .claude/skills/, so a checkout is already live. That also means skills install --scope project here refuses without --force, which is the point: those links go to the source file, and writing through one would edit the tree.

DD4hep plugins that require the global detector

Some detector plugins, including ALLEGRO's k4geo readout segmentation, look up constants and geometry through dd4hep::Detector::getInstance(). The default XML importer creates a private detector. In a DD4hep-enabled native build, opt into the global detector for these plugins:

nodehammer convert -i ALLEGRO_o1_v03.xml --dd4hep-global -c scene.toml -o allegro.glb

Source the experiment's DD4hep/plugin environment first. The flag works before or after the subcommand, including inspect, and applies only to DD4hep XML loading. It loads one compact into a fresh default detector and leaves it alive until process exit. An already populated detector or a second global import is rejected; nodehammer never resets or replaces an existing detector. Selection and export run normally in the same process, with plugin output on stderr.

If a plugin calls exit() during XML import, the native CLI prints NH0302 before exiting. In private-detector mode the warning suggests retrying with --dd4hep-global. Successful imports and caught exceptions do not trigger it. This uses atexit: it does not catch abort(), _Exit(), or fatal signals, and it preserves the plugin's exit status.

The flag is also available through runCli and the Python console script in DD4hep-enabled builds. Programmatic callers can pass backend options explicitly:

auto result = nodehammer::readSemantic("detector.xml", {
    .importerOptions = {{"dd4hep.useGlobalDetector", true}},
});
import nodehammer as nh

result = nh.read_semantic(
    "detector.xml",
    importer_options={"dd4hep.useGlobalDetector": True},
)

Omitting these options keeps private-detector behavior. Each call or CLI invocation owns its options; they do not affect subsequent calls. Supplying DD4hep options for another format, or without the backend compiled in, raises NH0105. Unknown keys and incorrect value types also raise NH0105; values are not coerced. The option map accepts bool, signed 64-bit integers, double, and strings (Python: bool, int, float, str). Backends declare supported keys and defaults; dd4hep.useGlobalDetector is a boolean defaulting to false. Explicit options, including false, must belong to the selected backend.

Global mode rejects an already populated detector and any second load, including retries after a failed load. It never destroys the singleton. Applications that already own a detector can use fromDD4hep(detector).

The exit-warning handler and stdout redirection remain native executable policy. Library and Python calls do not install a process exit handler; a plugin that exits can terminate their host process. No helper executable or subprocess is required.

Building

Dependencies are managed with Conan; builds with CMake + Ninja.

just recipes    # export vendored recipes (sokol-shdc)
just deps       # conan install (viewer=True)
just configure  # cmake --preset conan-relwithdebinfo
just build
just test

Or drive conan/cmake directly — see the Justfile for the exact flags, including the Emscripten/wasm targets (just wasm-deps, requires emsdk) and the spack-based dev configuration (configure-full) that turns on the TGeo and DD4hep importers together.

Key CMake options:

Option Purpose Status
NODEHAMMER_WITH_VIEWER Build the sokol/Dear ImGui interactive viewer ✅
NODEHAMMER_WITH_TGEO ROOT/TGeo importer ✅
NODEHAMMER_WITH_DD4HEP DD4hep importer ✅
NODEHAMMER_WITH_GEANT4 GDML/Geant4 importer 🚧 links Geant4 only, no importer yet
NODEHAMMER_WITH_GEOMODEL GeoModel importer 🚧 declared only
NODEHAMMER_BUILD_TESTS Build the Catch2 unit-test binary ✅
NODEHAMMER_BUILD_SHARED Also build and install libnodehammer and libnodehammer_ingest + the public headers + a CMake package config 🚧 packaging works; the API it exposes is still being written

With NODEHAMMER_BUILD_SHARED=ON, cmake --install adds both shared libraries, include/nodehammer/, and a package config, so a consumer needs only:

find_package(nodehammer CONFIG REQUIRED COMPONENTS Shared)
target_link_libraries(app PRIVATE nodehammer::shared)

No find_dependency for zstd, flatbuffers, manifold or the rest — the shared library absorbs them. ci/shared-build.sh runs the whole thing end to end and, on ELF, checks that nothing third-party or internal escaped into the export table. CI runs it on every native platform.

A consumer needs only C++20. nodehammer itself is built as C++23, but the installed headers do not require it, and the exported target asks for no more than cxx_std_20. ci/shared_consumer compiles at exactly that floor, so a public header reaching past it fails there rather than in your build.

On Windows the library and its consumer must agree on the C runtime (/MD vs /MT) and on _ITERATOR_DEBUG_LEVEL — so a Debug consumer needs a Debug nodehammer. This is not specific to nodehammer; it is what passing std:: types across a DLL boundary requires, and mismatches usually surface as heap corruption rather than as a link error.

Try it

The repo ships a real detector fixture — the Open Data Detector — ready to convert:

just odd    # full ODD → glb (see the Justfile for single-stave recipes too)
build/RelWithDebInfo/nodehammer viewer open

…in a browser

Every push to main publishes the wasm viewer to https://paulgessinger.github.io/nodehammer/, in both of the postures the web build supports:

/app/ the open application — empty start; drop a .nhproj or loose config/geometry files onto it
/odd/ the full Open Data Detector, preconfigured and content-locked
/odd-simple/ the simplified ODD — same, lighter to build

Each directory is a self-contained static package of the kind the viewer's Publish command emits. Reproduce the whole site locally with just pages-site after just wasm-release.

Project layout

src/           core pipeline: cli, config, ir (intermediate representation),
               scene building, tessellation, selection, the viewer, web glue
shaders/       sokol-shdc GLSL sources for the full render pipeline
schemas/       FlatBuffers schemas (render.fbs, semantic.fbs)
profiles/      Conan profiles (Emscripten cross-compilation)
recipes/       vendored/patched Conan recipes (sokol, sokol-shdc, imgui, implot, nfd)
fixtures/      sample detector geometries and configs
docs/          format specs and design docs (nhb format, predicate expressions,
               orbit navigation, rendering-fidelity strategy, PNG export)
web/           browser viewer entry point

License

MIT © Paul Gessinger — see LICENSE. Third-party license texts for vendored/bundled assets live under LICENSES/.

Embedding geometry ingestion

For experiment integration without the processing/viewer dependency stack, see the C++20 ingestion library. It reuses existing ROOT and DD4hep installations and provides nodehammer::ingest.

Metadata

Release files for nodehammer 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nodehammer 0.3.0
File Size Uploaded Provenance
nodehammer-0.3.0.tar.gz 1.3 MB With provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for nodehammer 0.3.0
File Interpreter ABI Platform
nodehammer-0.3.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl CPython 3.12 abi3 Linux glibc 2.28+ x86-64, Linux glibc 2.27+ x86-64 Details
nodehammer-0.3.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl CPython 3.12 abi3 Linux glibc 2.28+ ARM64, Linux glibc 2.27+ ARM64 Details
nodehammer-0.3.0-cp312-abi3-macosx_13_0_arm64.whl CPython 3.12 abi3 macOS 13.0+ ARM64 Details

Total release size: 9.8 MB

Release files / nodehammer-0.3.0.tar.gz

Download URL nodehammer-0.3.0.tar.gz
Size 1.3 MB
Tags Source
SHA-256 checksum
How to use checksums
a2fc5d758749502f76b7d889f582f57a0432dfbd4bf58509c917e2239fedb0cb
BLAKE2b-256 checksum
How to use checksums
5ea5ce2f72a3dbfb62216e393bd3022b395c657d4f16f43423aec741d7182166
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / nodehammer-0.3.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

Download URL nodehammer-0.3.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Size 3.4 MB
Tags CPython 3.12 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
c1128c9e0e0b07d5f5ed3191ca92f92fe76e17904d5fc00afcedc2a37f2b40cd
BLAKE2b-256 checksum
How to use checksums
42fd89a266abc44ae9e2c9f08b845e8c636f1a20e5324e5176ca3fdad2ce091c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / nodehammer-0.3.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl

Download URL nodehammer-0.3.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl
Size 3.1 MB
Tags CPython 3.12 Linux glibc 2.27+ ARM64 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
fb4fdd54d66e2aef60a0432bbacd417532f288cf34c0040bd02986305d98968b
BLAKE2b-256 checksum
How to use checksums
8d0c0d6123c2469e2f9feb9c5c0a01fa9f1c3b6d703492f94c12e64ef1b7d5fb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / nodehammer-0.3.0-cp312-abi3-macosx_13_0_arm64.whl

Download URL nodehammer-0.3.0-cp312-abi3-macosx_13_0_arm64.whl
Size 2.1 MB
Tags CPython 3.12 abi3 macOS 13.0+ ARM64
SHA-256 checksum
How to use checksums
3522578a6323a9821e18269f73dcaedc92bd5a16b571eaa9e93ba781a9d5b4f9
BLAKE2b-256 checksum
How to use checksums
3afb622ef3b0beb15083e36c4cefead57784b13dc7de004249b03bcae53c1e2d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Security for nodehammer 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 4 files in this release have been uploaded with provenance metadata.

Select from 4 files

Attestation for nodehammer-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page